1. 什么是“多态”?
C++ 作为面向对象的语言,主要有三大特性:继承、封装、多态。关于多态,简单说就是同一个函数调用,在不同对象上有不同的表现。也就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
C++ 的多态主要有两种类型:编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态: 通过函数重载和运算符重载实现。
- 函数重载:在同一个作用域中,多个同名函数根据参数类型或个数不同而共存。
#include <iostream>
using namespace std;
class Print {
public:
void show(int i) {
cout << "整数: " << i << endl;
}
void show(double d) {
cout << "小数: " << d << endl;
}
};
- 运算符重载:对已有运算符进行重定义,使其作用于自定义类型。
#include <iostream>
using namespace std;
class Point {
public:
int x, y;
Point(int a, int b) : x(a), y(b) {}
// 加号重载
Point operator+(const Point& p) {
return Point(x + p.x, y + p.y);
}
void print() {
cout << "(" << x << ", " << y << ")" << endl;
}
};
int main() {
Point p1(1, 2);
Point p2(3, 4);
Point p3 = p1 + p2; // 调用 operator+
p3.print(); // 输出: (4, 6)
return 0;
}
注意 :Point p3 = p1 + p2; 指的是用 p1 调用 加法函数,并把 p2 当作参数传进去。也就是说,它背后实际上是这样运行的:p1.operator+(p2);
return Point(x + p.x, y + p.y); == return Point(p1.x + p2.x, p1.y + p2.y);
运行时多态: 通过虚函数(virtual function)和 纯虚函数(pure virtual function)实现,是多态最核心的部分。
#include <iostream>
using namespace std;
class Animal {
public:
virtual void speak() {
cout << "Animal makes a sound." << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "Dog barks." << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "Cat meows." << endl;
}
};
void makeSound(Animal* a) {
a->speak(); // 动态绑定
}
int main() {
Animal* a1 = new Dog();
Animal* a2 = new Cat();
makeSound(a1); // 输出 Dog barks.
makeSound(a2); // 输出 Cat meows.
delete a1;
delete a2;
return 0;
}
注意:基类中的函数必须用 virtual 关键字。必须通过基类指针或引用调用虚函数。子类重写(override)虚函数。override, 确保函数签名正确,否则编译器会报错。
2. 虚函数
虚函数是希望在子类中进行重写(override)的函数,用 virtual 关键字修饰。
class Base {
public:
virtual void show() {
std::cout << "Base show" << std::endl;
}
};
- 允许通过基类指针或引用调用子类的重写版本(动态绑定)。
- 可以提供默认实现。
- 不强制子类重写,但可以选择重写。
class Derived : public Base {
public:
void show() override {
std::cout << "Derived show" << std::endl;
}
};
Base* obj = new Derived();
obj->show(); // 输出:Derived show
2.1 虚函数表
当使用 virtual 函数时,编译器会为类自动生成一个虚函数表(vtable),本质是一个函数指针数组,每个元素都是一个指向虚函数的指针。每个包含虚函数的类(或继承了虚函数的类)都会生成一个 vtable;而每个对象中会有一个指向该类 vtable 的指针(vptr)。
类A是基类,类B继承类A,其对象模型如下图所示。
class Animal {
public:
virtual void makeSound(); // vtable: [makeSound() 的地址]
};
class Dog : public Animal {
public:
void makeSound() override; // vtable: [Dog::makeSound() 的地址]
};
调用时:
Animal* a = new Dog();
a->makeSound(); // 实际上是通过 vptr 查找 vtable 中的 makeSound 函数指针并调用
当创建一个 Dog 对象时,它会被分配一个 vptr,指向 Dog 类的 vtable。而 a 这个指针虽然是 Animal* 类型,但它实际上指向的是 Dog 类型的对象,所以它的 vptr 指向的是 Dog 类的 vtable。
如果 Dog 没有重写 makeSound(),那么 Dog 的 vtable 会继承 Animal 的 makeSound 的指针。所以 a->vptr 会指向 Dog 的 vtable,而 Dog 的 vtable 中会包含 Animal 的 makeSound 函数指针。
3. 纯虚函数
纯虚函数表示没有实现,只是一个接口声明。使用 = 0 来定义。
class Base {
public:
virtual void show() = 0; // 纯虚函数
};
- 必须由子类重写,否则子类也将是抽象类,无法实例化。抽象类是不能被直接创建对象的,必须通过继承并实现所有抽象方法的子类来实例化。
- 使该类成为一个抽象类。
- 常用于定义接口或基础框架。
class Derived : public Base {
public:
void show() override {
std::cout << "Derived show" << std::endl;
}
};
Base* obj = new Derived();
obj->show(); // 输出:Derived show
4. 如何选用?
如果希望基类提供某些共享行为,允许子类根据需要修改或扩展这些行为,用虚函数。
如果希望强制子类实现某些功能或行为时,或者当你希望定义一个接口(抽象类)时,用纯虚函数。
另外,抽象类不能实例化,但可以作为接口使用。多态性允许你使用 Animal* 或 Animal& 来调用子类实现,提升代码扩展性。