代码仓库shanchuann/CPP-Learninng

在C++的面向对象编程中,纯虚函数和抽象类是实现多态性的核心机制,它们允许我们定义一组类的共同接口,并强制派生类实现特定的功能,从而让代码更具灵活性和可扩展性。理解纯虚函数和抽象类的概念与用法,是掌握C++多态编程的关键。

纯虚函数

纯虚函数是一种特殊的虚函数,它在基类中声明,但不提供具体的实现。其语法是在虚函数声明的结尾加上“= 0”,例如 virtual void functionName() = 0;。纯虚函数的存在意味着基类并不打算直接实例化,而是希望派生类来重写并实现这个函数。如果一个类包含纯虚函数,那么它就不能被用来创建对象,必须由派生类继承后,重写所有的纯虚函数,才能实例化派生类的对象。

抽象类

抽象类是指包含至少一个纯虚函数的类。抽象类的主要作用是作为基类,为派生类提供一个统一的接口规范,它描述了派生类应该具备的基本行为,但并不具体实现这些行为。由于抽象类包含纯虚函数,所以它不能被实例化,也就是说,我们不能直接创建抽象类的对象,只能通过继承它的派生类来间接使用。抽象类可以包含普通的成员函数和成员变量,也可以包含构造函数和析构函数,这些普通成员可以被派生类继承和使用。

之间的关系

纯虚函数是抽象类的核心组成部分,抽象类通过纯虚函数来定义接口,而派生类则通过重写纯虚函数来实现这些接口。如果一个派生类继承了抽象类,但没有重写所有的纯虚函数,那么这个派生类仍然是一个抽象类,同样不能被实例化。只有当派生类重写了基类中的所有纯虚函数后,它才成为一个具体的类,可以用来创建对象。抽象类和纯虚函数的这种关系,确保了派生类必须遵循基类定义的接口规范,从而实现了接口与实现的分离。

使用场景

抽象类的主要作用是定义一组相关类的共同接口,隐藏实现细节,让代码更易于维护和扩展。在实际开发中,抽象类常用于以下场景:一是当我们需要定义一组类的共同行为,但这些行为的具体实现因类而异时,可以用抽象类定义接口,派生类各自实现;二是当我们希望通过基类指针或引用来操作不同的派生类对象,实现多态性时,抽象类是理想的选择;三是当我们需要限制某个类不能被实例化,只能作为基类使用时,可以将其定义为抽象类。例如,在图形处理系统中,我们可以定义一个抽象类“Shape”,其中包含纯虚函数“draw()”和“getArea()”,然后派生“Circle”“Rectangle”“Triangle”等类,每个派生类都重写这两个纯虚函数,实现各自的绘制和面积计算功能。

注意事项

使用抽象类时需要注意以下几点:一是抽象类的构造函数通常应该声明为protected,而不是public,因为抽象类不能被实例化,声明为protected可以防止直接创建抽象类的对象,同时允许派生类的构造函数调用基类的构造函数;二是抽象类的析构函数应该声明为虚函数,这样当通过基类指针删除派生类对象时,会正确调用派生类的析构函数,避免内存泄漏;三是纯虚函数可以在抽象类外部提供实现,但这种实现很少使用,通常纯虚函数都是由派生类来实现的;四是抽象类可以继承其他抽象类,并且可以添加新的纯虚函数或普通成员函数,派生类需要重写所有继承链中的纯虚函数才能成为具体类。

首先定义抽象类Shape,包含纯虚函数draw和getArea:

1
2
3
4
5
6
7
8
9
10
11
12
class Shape {
protected:
// 抽象类的构造函数声明为protected,防止直接实例化
Shape() = default;
public:
// 虚析构函数,确保派生类对象被正确释放
virtual ~Shape() = default;
// 纯虚函数:绘制图形
virtual void draw() const = 0;
// 纯虚函数:计算面积
virtual double getArea() const = 0;
};

然后定义派生类Circle,继承Shape并重写所有纯虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
// 重写draw函数
void draw() const override {
std::cout << "绘制圆形,半径为 " << radius << std::endl;
}
// 重写getArea函数
double getArea() const override {
return 3.14159 * radius * radius;
}
};

再定义派生类Rectangle,同样继承Shape并重写纯虚函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 重写draw函数
void draw() const override {
std::cout << "绘制矩形,宽为 " << width << ",高为 " << height << std::endl;
}
// 重写getArea函数
double getArea() const override {
return width * height;
}
};

最后在主函数中使用抽象类指针操作派生类对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
// 创建派生类对象,用基类指针指向
Shape* circle = new Circle(5.0);
Shape* rectangle = new Rectangle(4.0, 6.0);

// 通过基类指针调用派生类的方法,实现多态
circle->draw();
std::cout << "圆形面积:" << circle->getArea() << std::endl;

rectangle->draw();
std::cout << "矩形面积:" << rectangle->getArea() << std::endl;

// 释放内存,虚析构函数确保调用正确的析构顺序
delete circle;
delete rectangle;

return 0;
}

在这个例子中,Shape是抽象类,它定义了图形的共同接口draw和getArea,Circle和Rectangle作为派生类,分别实现了这两个接口。通过Shape类型的指针,我们可以统一操作不同的图形对象,调用各自的draw和getArea方法,这就是多态性的直观体现。如果尝试直接创建Shape类的对象,编译器会报错,因为抽象类无法实例化;如果派生类没有重写所有纯虚函数,那么该派生类也会成为抽象类,同样无法实例化。

以下代码是状态设计模式的典型应用,核心思想是让对象在内部状态改变时,其行为也随之动态变化,看起来就像 “换了一个类” 一样。代码中,Character 类是 “上下文角色”,负责对外暴露行为接口并维护状态切换;State 是抽象状态基类,通过纯虚函数 response() 定义了所有状态必须实现的行为接口;ForgPrince 是具体状态类,分别实现了青蛙和王子状态下的响应逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 角色类:封装角色的状态切换与行为响应逻辑
class Character {
// 抽象状态类:定义所有状态的统一接口
class State {
public:
// 纯虚函数:要求派生类必须实现状态的响应行为
virtual void response() = 0;
// 虚析构函数:确保通过基类指针删除派生类对象时能正确析构
virtual ~State() = default;
};
// 具体状态类:青蛙状态,实现State接口
class Forg : public State {
public:
// 重写response函数,输出青蛙状态的专属响应
void response() override {
std::cout << "Forg response" << std::endl;
}
};
// 具体状态类:王子状态,实现State接口
class Prince : public State {
public:
// 重写response函数,输出王子状态的专属响应
void response() override {
std::cout << "Prince response" << std::endl;
}
};
private:
State* state; // 状态指针:指向当前角色所处的状态对象
public:
// 构造函数:初始化角色状态为“青蛙”
Character() : state(new Forg()) {}
// 切换状态函数:删除当前状态,切换为“王子”状态
void changeState() {
delete state; // 释放当前状态对象的内存,避免泄漏
state = new Prince(); // 创建新的王子状态对象,更新状态指针
}
// 角色响应函数:委托给当前状态对象处理具体行为
void response() {
state->response(); // 调用当前状态的response方法,实现行为随状态变化
}
// 析构函数:释放状态对象的内存
~Character() {
delete state; // 释放state指向的状态对象
// 注意:此处“State* state = nullptr;”是局部变量定义,不会修改成员变量state
// 正确写法应为“state = nullptr;”,但析构后对象即将销毁,置空非必须
State* state = nullptr; // 这行代码无实际作用,属于冗余局部变量
}
};
int main() {
Character character; // 创建角色对象,初始状态为“青蛙”
character.response(); // 调用青蛙状态的响应,输出“Forg response”
character.changeState(); // 切换状态为“王子”
character.response(); // 调用王子状态的响应,输出“Prince response”
return 0;
}

以上代码是状态设计模式的典型应用,核心思想是让对象在内部状态改变时,其行为也随之动态变化,看起来就像 “换了一个类” 一样。代码中,Character 类是 “上下文角色”,负责对外暴露行为接口并维护状态切换;State 是抽象状态基类,通过纯虚函数 response() 定义了所有状态必须实现的行为接口;ForgPrince 是具体状态类,分别实现了青蛙和王子状态下的响应逻辑。

Character 类将行为的具体实现 “委托” 给了状态对象。它持有一个 State* 指针指向当前状态,当调用 response() 时,实际上是通过指针调用当前状态对象的 response() 方法,从而实现 “状态不同,行为不同”。changeState() 函数则负责动态切换状态:先删除旧状态对象释放内存,再创建新状态对象更新指针,这种设计避免了用大量 if-elseswitch 语句判断状态,让代码更易扩展。main 函数首先创建 Character 对象,构造函数会用 new Forg() 初始化状态指针,因此第一次调用 character.response() 时,会输出青蛙状态的响应。接着调用 changeState(),先释放青蛙对象,再创建王子对象并赋值给状态指针,第二次调用 response() 时就会输出王子状态的响应。最后析构函数自动执行,释放状态对象的内存,但需注意析构函数中定义了局部变量 State* state,这是一个小瑕疵,它不会修改成员变量,实际开发中应避免这种冗余代码。

状态模式的优势在于将不同状态的行为隔离到独立类中,符合 “单一职责原则”;新增状态时只需添加新的派生类,无需修改原有代码,符合 “开闭原则”。不过这段代码使用了裸指针管理内存,实际项目中更推荐用 std::unique_ptr 等智能指针,能自动管理内存释放,降低泄漏风险。