在C++的面向对象编程体系中,纯虚函数和抽象类是构建接口层的核心工具,二者配合可以实现模块间的解耦和统一的行为规范定义,是大型项目架构设计里常用的技术手段。
纯虚函数的基础定义
纯虚函数是在基类声明的、没有具体实现的虚函数,它的作用是定义接口的规范,强制要求所有继承该基类的子类必须实现这个函数。声明纯虚函数时需要在虚函数声明的末尾加上=0,语法格式如下:
// 声明一个纯虚函数,没有函数体实现 virtual 返回值类型 函数名(参数列表) = 0;
需要注意,纯虚函数也可以有函数体,但这种情况很少见,通常纯虚函数仅做接口声明,不提供默认实现。下面的示例展示了一个包含纯虚函数的基类定义:
#include <iostream>
using namespace std;
// 图形基类
class Shape {
public:
// 纯虚函数:计算面积
virtual double getArea() = 0;
// 纯虚函数:计算周长
virtual double getPerimeter() = 0;
// 普通虚函数:打印图形信息
virtual void printInfo() {
cout << "这是一个图形" << endl;
}
// 基类析构函数需要声明为虚函数,避免内存泄漏
virtual ~Shape() {}
};
抽象类的特性与规则
包含至少一个纯虚函数的类就是抽象类,抽象类有以下核心特性:
- 抽象类不能被实例化,也就是不能直接创建抽象类的对象,只能作为基类被继承。
- 如果子类继承了抽象类,但是没有实现抽象类中所有的纯虚函数,那么子类也会变成抽象类,同样不能被实例化。
- 抽象类可以包含普通成员变量、普通成员函数、虚函数,不强制所有函数都是纯虚函数。
尝试实例化抽象类会直接触发编译错误,示例代码如下:
int main() {
// 错误:Shape是抽象类,不能实例化
// Shape s;
return 0;
}
用纯虚函数和抽象类实现接口设计
C++没有专门的interface关键字,通常就用抽象类来模拟接口的功能。接口设计的核心是定义统一的行为规范,让不同的实现类遵循同一套调用标准,降低模块间的耦合度。
下面的示例实现了两个继承Shape抽象类的子类,分别实现圆形和矩形的面积、周长计算逻辑:
#include <iostream>
#define PI 3.1415926
using namespace std;
// 抽象类:图形接口
class Shape {
public:
virtual double getArea() = 0;
virtual double getPerimeter() = 0;
virtual void printInfo() = 0;
virtual ~Shape() {}
};
// 圆形类,实现Shape接口
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double getArea() override {
return PI * radius * radius;
}
double getPerimeter() override {
return 2 * PI * radius;
}
void printInfo() override {
cout << "圆形,半径:" << radius << ",面积:" << getArea() << ",周长:" << getPerimeter() << endl;
}
};
// 矩形类,实现Shape接口
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double getArea() override {
return width * height;
}
double getPerimeter() override {
return 2 * (width + height);
}
void printInfo() override {
cout << "矩形,宽:" << width << ",高:" << height << ",面积:" << getArea() << ",周长:" << getPerimeter() << endl;
}
};
在使用时,我们可以用抽象类的指针或引用来统一操作不同的子类对象,这就是多态的体现:
int main() {
// 用抽象类指针指向不同的子类对象
Shape* shape1 = new Circle(5.0);
Shape* shape2 = new Rectangle(3.0, 4.0);
shape1->printInfo();
shape2->printInfo();
// 释放内存
delete shape1;
delete shape2;
return 0;
}
接口设计的最佳实践
在使用纯虚函数和抽象类做接口设计时,有几个需要注意的点:
- 抽象类的析构函数一定要声明为虚函数,否则当用基类指针删除子类对象时,子类的析构函数不会被调用,会导致内存泄漏。
- 接口类(纯抽象类,即所有函数都是纯虚函数)不要包含成员变量,所有属性都通过子类的实现来维护,这样接口的通用性更强。
- 子类实现纯虚函数时,建议加上
override关键字,让编译器帮忙检查是否真的重写了基类的虚函数,避免函数签名写错的问题。 - 如果接口需要升级,尽量不要直接修改已有的纯虚函数,避免影响所有已有的实现类,可以新增纯虚函数,或者新增新的抽象类继承原有接口。
常见问题解答
为什么抽象类不能实例化?
因为抽象类中包含没有实现的纯虚函数,如果允许实例化,调用这些纯虚函数时就没有对应的执行逻辑,会导致程序错误,所以编译器直接禁止抽象类实例化。
子类必须实现所有纯虚函数吗?
是的,如果子类没有实现抽象父类的所有纯虚函数,那么子类仍然属于抽象类,不能被实例化,直到它实现了所有纯虚函数为止。
纯虚函数可以有实现吗?
可以有,但是这种实现不会被默认调用,子类如果要使用父类的纯虚函数实现,需要显式调用,这种场景通常用于给子类提供可选的默认实现,但是仍然强制子类声明该函数。