C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 中的接口、纯虚函数与虚析构函数

C++ 中的接口、纯虚函数与虚析构函数一文全解

作者:daidaidaiyu

很多从Java / C# 转过来写 C++ 的都会有一个疑问:C++ 没有interface关键字,它的"接口"是怎么实现的?为什么反复强调"基类一定要写虚析构函数"?这篇文章主要介绍了浅谈 C++ 中的接口、纯虚函数与虚析构函数,需要的朋友可以参考下

C++ 里“接口”通常指‌只包含纯虚函数、没有数据成员的抽象类‌,它定义了一组“必须做什么”的契约,而具体怎么做交给派生类。 实现接口靠纯虚函数,而纯虚函数靠虚函数机制支撑;想让接口在多态删除时安全,又必须配虚析构函数。‌‌

很多从 Java / C# 转过来写 C++ 的都会有一个疑问:C++ 没有 interface 关键字,它的"接口"是怎么实现的?为什么反复强调"基类一定要写虚析构函数"?

C++ 里的"接口"到底是什么

在C++中,接口是通过纯虚类(Pure Abstract Class)实现的,这是一种只包含纯虚函数而没有数据成员的特殊抽象类。

class CommandAccept {
public:
    CommandAccept() {}  // 基类需要构造函数体
    virtual ~CommandAccept() {}  // 虚析构函数,必须要有函数体!
    virtual QString getCommandCode() = 0;  // 纯虚函数
    virtual Response process(Session* session, Request request) = 0;
};

所谓纯抽象类,指的是:

它的核心是三件事:

什么是纯虚函数?

普通虚函数允许派生类"选择性地"重写,而纯虚函数(pure virtual function)在基类中只有声明、没有强制实现,并在声明末尾加上 = 0。它相当于一份契约:凡继承该类的派生类,都必须给出这个函数的具体实现,否则派生类自己也会变成抽象类、同样无法实例化。

class Shape {
public:
    // = 0 告诉编译器:这是纯虚函数,基类不提供实现,派生类必须重写它。
    virtual double area() const = 0;
};

Java 接口有 default 默认的实现,在C++中也可以
virtual void f(); 是一个有实现的普通虚函数(基类可以给出默认行为);而 virtual void f() = 0; 才是纯虚函数。= 0 是一个特殊的语法标记,含义是"此函数在本类中是抽象的、待实现的",并不是"返回值等于 0"。

只要含纯虚函数的类就不能被实例化

一旦类里出现纯虚函数,它就升级为抽象类(abstract class),编译器会禁止你创建它的对象:

Shape s;                    // 编译错误:Shape 是抽象类,无法实例化
Shape* p = new Shape();     // 同样编译错误
Shape* q = new Circle();    // 正确:Circle 实现了 area(),是具体类

抽象类的唯一用途是被继承,作为多态的"上层抽象"存在。

一个抽象类示例

下面是一份基于 Qt 的典型接口定义,它把"接口三件套"集中体现了出来:

class CommandAccept {
public:
    CommandAccept() {}                  // 基类需要一个构造函数体
    virtual ~CommandAccept() {}         // 虚析构函数,必须要有函数体!
    virtual QString getCommandCode() = 0;                          // 纯虚函数
    virtual Response process(Session* session, Request request) = 0; // 纯虚函数
};

对应的派生类必须把两个纯虚函数都实现出来,否则它自己也是抽象类:

class CommandAcceptRecord : public CommandAccept {
public:
    QString getCommandCode() override {           // override 明确表达"我在重写"
        return "RECORD";
    }
    Response process(Session* session, Request request) override {
        // ……真正的业务逻辑……
        return Response::ok();
    }
};

调用方则永远只面向接口编程,完全不关心背后是哪个具体实现:

// 面向接口,而非面向实现:这正是多态的价值所在
CommandAccept* handler = new CommandAcceptRecord();
Response resp = handler->process(session, request);

接口类的设计规范

必须要有虚析构函数!

CommandAccept* crecord = new CommandAcceptRecord();
delete crecord;   // 如果基类析构函数非虚,将不会调用派生类析构函数!

内存泄漏的根因:析构函数默认不是多态的

在 C++ 里,普通的成员函数(包括析构函数)是通过"静态绑定"决定调用哪一个的。对于 delete crecord; 这行代码,编译器只看指针 crecord 的静态类型(这里是基类 CommandAccept*),而不会去看它实际指向的对象类型。

一个直观的对比示例

#include <iostream>
using namespace std;
struct BaseBad {
    BaseBad()  { cout << "BaseBad 构造\n"; }
    ~BaseBad() { cout << "BaseBad 析构\n"; }   // 非虚!
};
struct DerivedBad : BaseBad {
    int* data;
    DerivedBad() : data(new int[1024]) { cout << "DerivedBad 构造\n"; }
    ~DerivedBad() { delete[] data; cout << "DerivedBad 析构\n"; }
};
struct BaseGood {
    BaseGood()  { cout << "BaseGood 构造\n"; }
    virtual ~BaseGood() { cout << "BaseGood 析构\n"; }   // 虚析构
};
struct DerivedGood : BaseGood {
    int* data;
    DerivedGood() : data(new int[1024]) { cout << "DerivedGood 构造\n"; }
    ~DerivedGood() { delete[] data; cout << "DerivedGood 析构\n"; }
};
int main() {
    cout << "--- 非虚析构 ---\n";
    BaseBad*  b = new DerivedBad();
    delete b;            // 只打印 BaseBad 析构,DerivedBad 的资源泄漏
    cout << "--- 虚析构 ---\n";
    BaseGood* g = new DerivedGood();
    delete g;            // 先 DerivedGood 析构,再 BaseGood 析构,资源正确释放
}

运行结果:

--- 非虚析构 ---
BaseBad 构造
DerivedBad 构造
BaseBad 析构              <-- 派生类析构缺失,data 泄漏
--- 虚析构 ---
BaseGood 构造
DerivedGood 构造
DerivedGood 析构          <-- 派生类先析构,资源被释放
BaseGood 析构

可以清楚看到:只要可能用基类指针管理派生类对象,基类析构就必须是 virtual。

默认实现 =default

如果不需要在析构里做任何清理,C++11 起可以用 = default 明确表示"我就是要编译器生成的默认析构",语义比空的 {} 更清晰:

virtual ~CommandAccept() = default;   // C++11 风格,明确使用默认实现

"不是基类"的类要不要写虚析构?

判断准则是 Rule of Three / Rule of Five:

C++11中的 override 与 final

C++11 引入了 override 和 final 能让你在编译期就抓到多态写错的 bug。

override

不用 override 时,如果你把参数类型或 const 写错,函数会被当成"派生类里新加的一个函数",悄悄躲过了多态,非常类似Java的@Override注解:

class Shape {
public:
    virtual double area() const = 0;
};
class Circle : public Shape {
public:
    double r;
    // double area() const override { ... }  // 正确:签名与基类一致,校验通过
    double area() /* 漏写了 const */ override { return 3.14 * r * r; } // 编译报错!
    // 没有 override 时,上面这行会"成功编译",但 area() 永远不会被多态调用,
    // Circle 仍然是抽象类 —— 这种 bug 极难排查。
};

加上 override 后,一旦签名和基类对不上,编译器立刻报错,而不是留下一个静默的逻辑漏洞。

final

final 既可用于虚函数(禁止重写),也可用于类(禁止继承),并且有助于编译器做去虚化(devirtualization)优化。类似Java的final修饰符:

class FinalShape : public Shape {
public:
    double area() const final { return 0.0; } // 该函数不允许再被子类重写
};
// 若把类标记为 final:class Sealed final : public Shape { ... };
// 那么 class XXX : public Sealed { ... };  // 此处会编译报错,禁止继承

最后总结一下

概念关键点语法示例注意事项
​​接口类​​纯虚函数集合class I { virtual void f() = 0; };不能被实例化
​​纯虚函数​​强制派生类实现virtual void f() = 0;可提供默认实现(罕见)
​​虚析构函数​​确保正确析构virtual ~Base() = default;必须提供,即使为空
​​override​​明确重写意图void f() override;C++11引入,提高安全性
​​final​​禁止进一步重写virtual void f() final;用于类或方法

到此这篇关于浅谈 C++ 中的接口、纯虚函数与虚析构函数的文章就介绍到这了,更多相关C++ 中的接口、纯虚函数与虚析构函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文