详细地介绍C++的继承、虚函数、多态,以及虚函数表,并通过代码示例进行说明。
1. 继承 (Inheritance)
-
概念: 继承是面向对象编程中的一个核心概念,它允许一个类(子类/派生类)从另一个类(父类/基类)那里获得属性和方法。子类可以重用父类的代码,并添加或修改自己的特性。
-
语法:
class BaseClass { // ... 父类成员 ... }; class DerivedClass : public BaseClass { // public 继承 // ... 子类成员 ... };
public
继承: 父类的public
成员在子类中仍然是public
,protected
成员在子类中仍然是protected
,private
成员在子类中不可访问。protected
继承: 父类的public
和protected
成员在子类中都变成protected
,private
成员在子类中不可访问。private
继承: 父类的所有成员在子类中都变成private
,private
成员在子类中不可访问。(一般不推荐使用)
-
示例1:继承
#include <iostream> #include <string> class Animal { public: Animal(const std::string& name) : name_(name) {} void eat() { std::cout << name_ << " is eating." << std::endl; } std::string getName() { return name_; } protected: std::string name_; }; class Dog : public Animal { public: Dog(const std::string& name, const std::string& breed) : Animal(name), breed_(breed) {} void bark() { std::cout << name_ << " (a " << breed_ << ") is barking." << std::endl; } private: std::string breed_; }; int main() { Animal animal("Generic Animal"); animal.eat(); // animal.bark() //错误,Animal类没有bark函数 Dog dog("Buddy", "Golden Retriever"); dog.eat(); // 从 Animal 继承 dog.bark(); std::cout << "Dog's name using base class function: " << dog.getName() << std::endl; //访问父类的public函数 // std::cout << dog.name_ << std::endl; //错误,name_是protected,无法在main中访问。 return 0; }
2. 虚函数 (Virtual Functions)
-
概念: 虚函数是C++中实现多态的关键。在基类中声明为
virtual
的成员函数,可以在派生类中被重写(override)。当通过基类指针或引用调用虚函数时,实际调用的函数版本取决于指针或引用所指向的对象的实际类型,而不是指针或引用本身的类型。 -
语法:
class Base { public: virtual void someFunction() { // ... 基类实现 ... } }; class Derived : public Base { public: void someFunction() override { // 使用 override 关键字(C++11 及以后) // ... 派生类实现 ... } };
override
关键字(可选,但强烈推荐):显式地告诉编译器,这个函数是重写基类中的虚函数。如果基类中没有对应的虚函数,编译器会报错,这有助于避免错误。
-
纯虚函数 (Pure Virtual Functions)
-
一个没有实现的虚函数,用
= 0
标记。 -
包含纯虚函数的类称为抽象类 (Abstract Class)。
-
抽象类不能被实例化(不能创建对象)。
-
派生类必须实现(重写)所有纯虚函数,才能被实例化。
-
纯虚函数用于定义接口,强制派生类实现特定的功能。
class Shape { // 抽象类 public: virtual double area() const = 0; // 纯虚函数 virtual void draw() const = 0; // 纯虚函数 }; // 派生类必须实现 area() 和 draw() class Circle : public Shape { // ... double area() const override { /* ... */ } void draw() const override { /* ... */ } };
-
3. 多态 (Polymorphism)
-
概念: 多态是指“多种形态”。在C++中,多态允许我们使用基类指针或引用来操作派生类对象,并在运行时根据对象的实际类型来调用相应的函数。
-
实现: 多态主要通过虚函数来实现。
-
优点:
- 代码灵活性: 可以编写通用的代码来处理不同类型的对象。
- 可扩展性: 添加新的派生类时,无需修改现有的基类代码。
- 代码复用: 可以使用相同的接口来处理不同类型的对象。
-
示例1:多态
#include <iostream> #include <vector> class Shape { public: virtual void draw() const { std::cout << "Drawing a generic shape." << std::endl; } virtual ~Shape() {} // 基类析构函数通常应为虚函数 }; class Circle : public Shape { public: void draw() const override { std::cout << "Drawing a circle." << std::endl; } }; class Square : public Shape { public: void draw() const override { std::cout << "Drawing a square." << std::endl; } }; int main() { std::vector<Shape*> shapes; shapes.push_back(new Circle()); shapes.push_back(new Square()); shapes.push_back(new Shape()); // 可以添加基类对象,但通常我们会使用抽象类 for (const Shape* shape : shapes) { shape->draw(); // 多态:根据对象的实际类型调用 draw() } // 释放内存 (重要!) for (Shape* shape : shapes) { delete shape; } return 0; }
输出结果:
Drawing a circle. Drawing a square. Drawing a generic shape.
-
示例2(结合虚函数和多态):
#include <iostream>
#include <vector>
class Shape { // 抽象基类
public:
virtual double area() const = 0; // 纯虚函数
virtual void draw() const = 0; // 纯虚函数
virtual ~Shape() {} //虚析构函数
};
class Circle : public Shape {
public:
Circle(double radius) : radius_(radius) {}
double area() const override {
return 3.14159 * radius_ * radius_;
}
void draw() const override{
std::cout << "Draw Circle" << std::endl;
}
private:
double radius_;
};
class Rectangle : public Shape {
public:
Rectangle(double width, double height) : width_(width), height_(height) {}
double area() const override {
return width_ * height_;
}
void draw() const override{
std::cout << "Draw Rectangle" << std::endl;
}
private:
double width_;
double height_;
};
int main() {
std::vector<Shape*> shapes; // 存储基类指针的容器
shapes.push_back(new Circle(5.0));
shapes.push_back(new Rectangle(4.0, 6.0));
for (const Shape* shape : shapes) {
std::cout << "Area: " << shape->area() << std::endl; // 多态:调用实际对象的 area()
shape->draw();
}
// 释放内存
for (Shape* shape : shapes) {
delete shape;
}
shapes.clear();
return 0;
}
输出:
Area: 78.5397
Draw Circle
Area: 24
Draw Rectangle
代码解释:
Shape
是一个抽象基类,定义了area()
和draw()
纯虚函数。Circle
和Rectangle
继承自Shape
,并分别实现了area()
和draw()
函数。- 在
main
函数中,创建了一个std::vector<Shape*>
,它可以存储指向Shape
及其派生类对象的指针。 - 通过循环,使用
shape->area()
和shape->draw()
调用虚函数。由于多态性,即使shape
是Shape*
类型的指针,实际调用的也是Circle
或Rectangle
中对应的函数。 - 使用完后记得释放内存。
4. 虚函数表 (Virtual Function Table, vtable)
-
概念: 虚函数表是C++编译器在幕后用来实现虚函数的一种机制。每个包含虚函数的类(以及其派生类)都有一个与之关联的虚函数表。虚函数表是一个函数指针数组,其中每个元素指向该类的一个虚函数的实际地址。
-
工作原理:
- 创建对象: 当创建一个包含虚函数的类的对象时,编译器会在对象内存的开头(或某个固定位置)添加一个指向该类的虚函数表的指针(通常称为
vptr
)。 - 调用虚函数: 当通过基类指针或引用调用虚函数时,编译器会:
- 通过对象的
vptr
找到对应的虚函数表。 - 在虚函数表中查找要调用的虚函数的索引(这个索引在编译时就确定了)。
- 根据索引找到虚函数的实际地址,并调用该函数。
- 通过对象的
- 创建对象: 当创建一个包含虚函数的类的对象时,编译器会在对象内存的开头(或某个固定位置)添加一个指向该类的虚函数表的指针(通常称为
-
示意图:
// 假设有以下类: class Base { public: virtual void f1(); virtual void f2(); }; class Derived : public Base { public: void f1() override; // 重写 f1 virtual void f3(); // 新的虚函数 }; // 虚函数表结构可能如下: // Base 类的虚函数表 (Base::vtable) +-----------------+ | &Base::f1 | // 指向 Base::f1 的地址 +-----------------+ | &Base::f2 | // 指向 Base::f2 的地址 +-----------------+ // Derived 类的虚函数表 (Derived::vtable) +-----------------+ | &Derived::f1 | // 指向 Derived::f1 的地址 (重写) +-----------------+ | &Base::f2 | // 指向 Base::f2 的地址 (继承) +-----------------+ | &Derived::f3 | // 指向 Derived::f3 的地址 (新增) +-----------------+ // 对象内存布局: // Base 对象 +--------+-----------------+ | vptr |--> Base::vtable | +--------+-----------------+ | ... | | // 其他成员数据 +--------+-----------------+ // Derived 对象 +--------+-----------------+ | vptr |--> Derived::vtable| +--------+-----------------+ | ... | | // Base 类的成员数据 +--------+-----------------+ | ... | | // Derived 类的成员数据 +--------+-----------------+
-
虚函数表的存在会有很小的空间开销(每个对象一个vptr,每个类一个vtable),但带来的好处远大于这点开销。
总结
- 继承提供了代码重用和层次化结构。
- 虚函数和虚函数表是实现运行时多态的关键。
- 多态提高了代码的灵活性、可扩展性和可维护性。
- 纯虚函数和抽象类用于定义接口,强制派生类实现特定功能。
- 虚析构函数对于防止内存泄漏至关重要,尤其是在涉及多态和动态内存分配时。 当你通过基类指针删除一个派生类对象时,如果基类的析构函数不是虚函数,那么只有基类的析构函数会被调用,派生类的析构函数不会被调用,这可能导致资源泄漏(例如,派生类中分配的内存没有被释放)。
把基类的析构函数声明为虚函数可以确保在删除对象时,首先调用派生类的析构函数,然后调用基类的析构函数,从而正确地释放所有资源。