纯虚函数与抽象类:接口该怎么设计
设计一套可扩展的 C 系统时你迟早会写出「只定义契约、不提供实现」的类它规定「凡是我的子类都必须有area()和draw()但怎么实现我不管」。这种类就是抽象类abstract class靠纯虚函数pure virtual function实现。它看起来简单却藏着两个反直觉的坑纯虚函数其实可以有实现、纯虚析构函数反而必须给实现踩中任何一个都可能让你面对诡异的链接错误。这篇把机制、坑和设计规范一次讲透。1. 引子什么是抽象类把虚函数写成 0它就变成了纯虚函数只要类里含至少一个纯虚函数这个类就是抽象类不能直接实例化。// demo1.cpp — 编译: g -stdc17 -Wall -O2 demo1.cpp -o demo1#includeiostream#includestringclassShape{// 抽象类public:virtualdoublearea()const0;// 纯虚函数没有 0 的实现virtual~Shape()default;};classCircle:publicShape{doubler_1.0;public:doublearea()constoverride{return3.14159*r_*r_;}// 必须实现};intmain(){Circle c;std::coutareac.area()\n;}area3.14159如果反过来想new Shape或栈上Shape s;编译器会直接拒绝。这正是抽象类存在的意义它只描述「应该有什么能力」本身不完整。error: cannot declare variable s to be of abstract type Shape because the following virtual functions are pure within Shape: virtual double Shape::area() const2. 反直觉点一纯虚函数可以有定义很多教材说「纯虚函数没有实现」严格说只对了一半。纯虚函数可以有函数体只是定义必须写在类外派生类既可以选择自己重写也可以主动调用基类那份实现。这在「提供一个默认行为、但强制子类至少表态」时很有用。// demo2.cpp — 编译: g -stdc17 -Wall -O2 demo2.cpp -o demo2#includeiostream#includestringclassAnimal{public:virtualstd::stringsound()const0;// 纯虚virtual~Animal()default;protected:std::stringfallback()const{return[unknown];}};// 纯虚函数的定义写在类外这是合法的std::stringAnimal::sound()const{returnfallback();// 提供一个可被调用的默认实现}classDog:publicAnimal{public:std::stringsound()constoverride{returnWoof;}};classMystery:publicAnimal{public:// 不写自己的逻辑主动调用基类的纯虚实现std::stringsound()constoverride{returnAnimal::sound();}};intmain(){Dog d;Mystery m;std::coutd.sound()\n;std::coutm.sound()\n;}Woof [unknown]Mystery::sound()通过Animal::sound()显式调用了基类那份纯虚实现这在普通虚函数里做不到却恰好是纯虚函数能做到的「反直觉」之处。官方文档abstract class (cppreference) —— 明确说明纯虚函数可以提供定义且只能通过Base::f()形式调用。3. 反直觉点二纯虚析构函数必须有定义析构函数也可以是纯虚的但纯虚析构函数必须提供定义否则派生类析构时会链接失败。原因是派生类析构函数执行完后会隐式调用基类的析构函数如果基类析构没有实体链接器就找不到符号。// demo3.cpp — 编译: g -stdc17 -Wall -O2 demo3.cpp -o demo3#includeiostreamclassBase{public:virtual~Base()0;// 纯虚析构};Base::~Base(){std::coutBase destroyed\n;}// 必须提供定义classDerived:publicBase{public:~Derived(){std::coutDerived destroyed\n;}};intmain(){Derived d;// 离开作用域先 ~Derived 再 ~Base}Derived destroyed Base destroyed构造与析构的调用链严格相反构造时基类子对象先建、派生部分后建析构时派生部分先拆、基类子对象后拆Derived d; ┌──────────────┐ │ Base 部分 │ ← ~Base() 最后调用需要 Base::~Base 有定义 │ Derived 部分│ ← ~Derived() 先调用 └──────────────┘如果漏掉Base::~Base()的定义会得到一个扑朔迷离的链接错误undefined reference to Base::~Base()注意报错的不是Base自己而是它的派生类Derived的析构这常让人误以为是派生类写错了。官方文档virtual function (cppreference) —— 析构函数作为虚函数与纯虚函数时的语义说明。4. 普通虚函数 vs 纯虚函数一张表厘清两者的关键差异避免混用项普通虚函数virtual f()纯虚函数virtual f() 0派生类是否必须重写否可继承默认实现是否则派生类仍是抽象类含它的类能否实例化能不能抽象类能否有函数体/定义能类内或类外能但定义必须写在类外典型用途提供可覆写的默认行为定义接口、强制子类实现契约下面这小段代码印证「普通虚函数基类可实例化、带默认行为」// demo4.cpp — 编译: g -stdc17 -Wall -O2 demo4.cpp -o demo4#includeiostream#includestringclassReader{public:virtualstd::stringsource()const{returnunknown;}// 普通虚函数virtual~Reader()default;};classFileReader:publicReader{public:std::stringsource()constoverride{returnfile;}};intmain(){Reader r;// 普通虚函数基类可实例化FileReader f;std::coutr.source()\n;std::coutf.source()\n;}unknown file5. 接口该怎么设计Core Guidelines 的建议抽象基类最典型的用途是当作接口interface。C Core Guidelines 对「接口类」有几个明确主张接口 纯虚函数 虚析构 无数据成员。不放数据成员接口就只表达「能力」不掺状态派生类各管各的状态。基类析构要么publicvirtual要么protected 非virtual。前者允许通过基类指针删除对象多态删除后者表示「这个类只该被继承、不打算被多态删除」。不要塞太多方法ISP 接口隔离原则的体现只放真正需要被多态调用的函数能用更小接口就别搞「胖接口」。推荐接口结构ISP 视角接口只暴露「真正需要被多态调用」的契约 Shape抽象类 / 接口 ┌──────────────────────────┐ │ area() 0 (纯虚) │◄── 只描述必要能力 │ draw() 0 (纯虚) │ │ ~Shape() virtual │ │ 无数据成员 │ └──────────────────────────┘ ▲ ▲ Circle Square area() area() draw() draw() - r_ - s_6. 完整示例形状接口 两个实现 多态调用把上面的规范串起来抽象基类只放纯虚函数与虚析构、无数据成员两个派生类各自持有状态并实现接口用std::unique_ptrShape管理生命周期避免裸new/delete。// demo5.cpp — 编译: g -stdc17 -Wall -O2 demo5.cpp -o demo5#includeiostream#includememory#includevectorclassShape{// 抽象基类 接口public:virtualdoublearea()const0;virtualvoiddraw()const0;virtual~Shape()default;// public virtual 析构};classCircle:publicShape{doubler_;public:explicitCircle(doubler):r_{r}{}doublearea()constoverride{return3.14159*r_*r_;}voiddraw()constoverride{std::coutdraw Circle rr_\n;}};classSquare:publicShape{doubles_;public:explicitSquare(doubles):s_{s}{}doublearea()constoverride{returns_*s_;}voiddraw()constoverride{std::coutdraw Square ss_\n;}};intmain(){std::vectorstd::unique_ptrShapeshapes;shapes.push_back(std::make_uniqueCircle(2.0));shapes.push_back(std::make_uniqueSquare(3.0));for(autoconstp:shapes){p-draw();std::coutareap-area()\n;}}draw Circle r2 area12.5664 draw Square s3 area97. 延伸阅读abstract class (cppreference) —— 纯虚函数与抽象类实例化规则的权威说明。virtual function (cppreference) —— 虚函数、虚析构与覆盖规则的完整参考。C Core Guidelines C.35 —— 基类析构应 publicvirtual 或 protected非 virtual。C Core Guidelines C.129 —— 区分实现继承与接口继承。本知识库内的相关篇目《多重继承与菱形继承虚继承到底解决了什么》 —— 一个类继承两个基类《对象切片object slicing多态失效的隐形杀手》 —— 把派生类对象按值赋给基类对象、按值传给基类参数、塞进 std::vector《override 与 final把多态错误拦在编译期》 —— 派生类想重写基类虚函数却写错签名编译器一声不吭运行时多态静默失效——这是最隐蔽的 bug 之一。8. 一句话总结纯虚函数 0定义抽象类、强制派生类实现接口但它「可以有类外定义、可供派生类主动调用」纯虚析构函数恰恰相反必须自己给定义否则派生类析构链接失败。接口类请遵循「纯虚函数 虚析构 无数据成员」按 Core Guidelines 把析构设为 publicvirtual并接口尽量精简。

相关新闻

业务参与者规则不显示构件包?沿工作流运行时加载链路排查

业务参与者规则不显示构件包?沿工作流运行时加载链路排查

看到“业务参与者规则没有显示构件包及构件包下流程事件,work目录下也未生成当前构件包目录”这个描述,我第一反应是:这不是一个单纯的界面显示问题,而是服务在运行态压根没有把目标构件包加载进来。业务参与者规则界面显示的构件…

2026/10/10 6:06:48 阅读更多 →
Linux 端口不通怎么排查?firewalld 不是超时而是 No route to host,三台真机实测 firewalld / ufw / iptables / SELinux

Linux 端口不通怎么排查?firewalld 不是超时而是 No route to host,三台真机实测 firewalld / ufw / iptables / SELinux

这一篇讲什么 「端口不通」是最常见也最容易瞎折腾的问题:服务起了、防火墙也开了,外面还是连不上。本篇在三台机器上把排查链条从头到尾实跑一遍:先分清报错类型 → 本机在不在监听 → 防火墙 → SELinux / AppArmor。 实测环境同(一):CentOS 7.9(VMware)、Rocky 9.8、Ubuntu …

2026/10/10 6:06:48 阅读更多 →
CentOS 7 yum 源失效怎么办?vault 返回 403、EPEL 7 卡死,三台真机实测能用的换源方法(附 Rocky 9 / Ubuntu 24.04)

CentOS 7 yum 源失效怎么办?vault 返回 403、EPEL 7 卡死,三台真机实测能用的换源方法(附 Rocky 9 / Ubuntu 24.04)

这个系列是什么 「Linux 实战」按线上真正会撞上的事情排:认清系统和装软件(本篇)、端口不通、服务起不来、磁盘满了、网络和 SSH、日志与性能…… 每一篇都做两件事:①所有命令在真机上跑过、贴原始输出;②同一件事在三个系统上各跑一遍 —— CentOS 7 还大量在线上跑着,新机器…

2026/10/10 6:06:48 阅读更多 →

最新新闻

Spring Boot+Vue云课堂在线教育系统毕设完整方案:从选题到部署

Spring Boot+Vue云课堂在线教育系统毕设完整方案:从选题到部署

毕业设计选题会上,“疫情防控下的云课堂在线教育系统”这个题目几乎每届都会出现。之前我带的学生里,有把它做成一个普通CRUD管理后台的,也有把它做成直播平台的,前者答辩时被老师说“看不出云课堂在哪”,后者开发到一…

2026/10/10 6:42:01 阅读更多 →
基于Python的风光储与废弃矿井抽蓄互补调度系统设计

基于Python的风光储与废弃矿井抽蓄互补调度系统设计

2. 系统架构与调度逻辑设计2.1 为什么是“风光储废弃矿井抽蓄”的组合先把这个组合拆开看。风力和光伏的出力天然具有波动性和间歇性,风电往往夜间出力大、白天出力小,光伏恰好相反,白天中午达到峰值、傍晚归零。这种天然的日内互补特性让“风…

2026/10/10 6:42:01 阅读更多 →
断裂力学实战:从应力强度因子到疲劳裂纹剩余寿命评估

断裂力学实战:从应力强度因子到疲劳裂纹剩余寿命评估

去年年底我接到一个活儿:厂里一台设备的钢结构支撑节点裂了,焊缝热影响区有一条肉眼可见的裂纹,长度大概40毫米,还在继续往母材里钻。业主第一反应是“焊一下不就完了”,我劝他们先别急着补焊——这种含裂纹的结构&…

2026/10/10 6:42:01 阅读更多 →
算电协同中心 27号令横向隔离 + 114/1077号三层合规:PEMS300 纵向加密 + PBox848F 边缘控制 5件套正解

算电协同中心 27号令横向隔离 + 114/1077号三层合规:PEMS300 纵向加密 + PBox848F 边缘控制 5件套正解

034 算电协同中心 27 号令横向隔离 114 号容量电价 1077 号输配电价 三层合规:PEMS300 纵向加密 PBox848F 边缘控制 二次系统正解场景定位:算电协同中心(P0 — MISSION 三 主线场景);27 号令《电力监控系统安全防护规定》(2025-01-01 施行)横向隔离 发改价格〔2026〕114 号…

2026/10/10 6:42:01 阅读更多 →
用Matlab绘制普朗克曲线并精确查找峰值点的完整指南

用Matlab绘制普朗克曲线并精确查找峰值点的完整指南

做热辐射仿真和红外系统设计的朋友,大概率都绕不开一条曲线:普朗克曲线。它的用途很直接——描述黑体在不同温度下辐射能量随波长的分布,从太阳光谱模拟到红外测温仪标定,都要靠它打底。而曲线上的峰值点,更是牵一发动…

2026/10/10 6:42:01 阅读更多 →
VMware虚拟机跑Linux实战:安装、网络、调优与排错

VMware虚拟机跑Linux实战:安装、网络、调优与排错

如果你是在校学生、运维新手,或者跟我一样平时要跟嵌入式开发、机械臂仿真、多系统测试打交道,那么 VMware 搭配 Linux 虚拟机绝对是你绕不开的组合。我这两年几乎把所有乱七八糟的实验环境都塞进了虚拟机里,从 Ubuntu 桌面版到精简的嵌入式 …

2026/10/10 6:41:01 阅读更多 →

日新闻

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

卫星轨道分类全解析:从LEO到GEO的选型逻辑与工程实践

1. 从“卫星轨道分类”这个标题说起:为什么值得花时间搞懂第一次接触“卫星轨道分类”这个概念,很多人会觉得它离自己很远——不就是天上的星星怎么转吗?但如果你正在做航天任务规划、遥感数据接收、星座设计,甚至只是准备一场航天…

2026/10/10 0:00:39 阅读更多 →
Spring AOP 核心原理与实战:从概念到日志切面落地

Spring AOP 核心原理与实战:从概念到日志切面落地

1. 从一个真实痛点说起:为什么你的代码里到处都是重复逻辑刚入行那会儿,我写过一个用户管理模块,注册、登录、改密码、注销四个接口。每个接口里都塞了几乎一样的日志打印、参数校验、事务开启和提交。当时觉得没什么,能跑就行。直…

2026/10/10 0:00:40 阅读更多 →
Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

Python招聘数据采集与分析可视化:从采集清洗到薪资技能城市可视化全链路

简介:这是一套面向计算机相关专业学生与项目实战学习者的Python数据采集与分析可视化完整项目,以Boss直聘岗位数据为对象,适合用作毕业设计、课程设计或期末大作业。资源包共38个文件,约246KB,以13个py源码文件为核心&…

2026/10/10 0:00:40 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 1:36:08 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 10:11:06 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 5:23:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 21:32:20 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/9 6:17:20 阅读更多 →