资讯详情

纯虚函数与抽象类:接口该怎么设计

📅 2026/10/10 6:06:49 | 华诺云谱 👁 阅读
纯虚函数与抽象类:接口该怎么设计
设计一套可扩展的 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并接口尽量精简。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑