代码仓库shanchuann/CPP-Learninng

编译时多态(静态多态)

编译时多态在程序编译阶段完成函数地址绑定,执行效率高,核心实现方式为函数重载、运算符重载,以及模板多态。模板多态也被称为参数化多态,是C++中另一种强大的编译时多态机制,它通过模板参数在编译时生成具体的函数或类实现,从而实现“一份代码,多种类型适配”的效果。

函数重载

同一个类中可以定义多个同名函数,只要参数列表(个数、类型、顺序)不同,编译器会根据实参自动匹配对应版本。这种匹配是在编译阶段完成的,编译器会检查实参与形参的类型兼容性,选择最匹配的函数进行调用,不会产生运行时额外开销。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Calculator {
public:
// 重载1:两个整数相加
int add(int a, int b) { return a + b; }
// 重载2:三个整数相加
int add(int a, int b, int c) { return a + b + c; }
// 重载3:两个浮点数相加
double add(double a, double b) { return a + b; }
};
int main() {
Calculator calc;
cout << "两个整数相加:" << calc.add(10, 20) << endl;
cout << "三个整数相加:" << calc.add(10, 20, 30) << endl;
cout << "两个浮点数相加:" << calc.add(1.5, 2.5) << endl;
return 0;
}

在这个例子中,编译器会根据main函数中传入的实参类型和数量,自动选择对应的add函数版本。比如传入两个整数时,调用 add(int a, int b); 传入两个浮点数时,调用 add(double a, double b);。整个过程在编译时就已经确定,运行时直接跳转到对应函数地址执行,效率很高。

运算符重载

为自定义类型重新定义运算符行为,本质是函数重载,关键字为operator。运算符重载可以让自定义类型的对象像内置类型一样使用运算符,大大提升代码的可读性和直观性。不过运算符重载不能改变运算符的优先级、结合性和操作数个数,也不能创建新的运算符,只能重载已有的运算符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 重载 + 运算符:实现复数相加
Complex operator+(const Complex& other) const { return Complex(real + other.real, imag + other.imag); }
// 重载 << 运算符:实现复数的输出,需要声明为友元函数以访问私有成员
friend ostream& operator<<(ostream& os, const Complex& c);
void show() const { cout << real << " + " << imag << "i" << endl; }
};
// 友元函数实现 << 运算符重载
ostream& operator<<(ostream& os, const Complex& c) {
os << c.real << " + " << c.imag << "i";
return os;
}
int main() {
Complex c1(1.0, 2.0);
Complex c2(3.0, 4.0);
Complex c3 = c1 + c2; // 调用重载的 +
cout << "复数相加结果:" << c3 << endl; // 调用重载的 <<
return 0;
}

这里不仅重载了加法运算符,还重载了输出流运算符<<。输出流运算符通常需要声明为类的友元函数,因为它的第一个操作数是ostream对象,而不是类的对象,无法作为类的成员函数实现。通过友元函数,我们可以直接访问类的私有成员real和imag,完成复数的输出。

模板多态(参数化多态)

模板多态是编译时多态的另一种重要形式,包括函数模板和类模板。模板本身不是具体的函数或类,而是一个“蓝图”,编译器会根据传入的模板参数在编译时生成具体的函数或类实例,这个过程称为模板实例化。模板多态实现了代码的泛化,让一份代码可以适配多种数据类型,同时保持编译时绑定的高效率。

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
// 函数模板:通用加法
template <typename T>
T add(T a, T b) {
return a + b;
}
// 类模板:通用数组
template <typename T, int size>
class Array {
private:
T arr[size];
public:
void set(int index, T value) {
if (index >= 0 && index < size) arr[index] = value;
}
T get(int index) const { return arr[index]; }
};
int main() {
// 函数模板实例化
cout << "整数相加:" << add<int>(10, 20) << endl;
cout << "浮点数相加:" << add<double>(1.5, 2.5) << endl;
// 也可以让编译器自动推导模板参数类型
cout << "自动推导类型相加:" << add(30, 40) << endl;
// 类模板实例化
Array<int, 5> intArr;
intArr.set(0, 100);
cout << "整数数组第一个元素:" << intArr.get(0) << endl;
Array<double, 3> doubleArr;
doubleArr.set(1, 3.14);
cout << "浮点数数组第二个元素:" << doubleArr.get(1) << endl;
return 0;
}

函数模板add可以适配任何支持+运算符的类型,比如int、double,甚至是自定义的Complex类(只要Complex类重载了+运算符)。类模板Array可以创建任意类型、任意大小的数组,避免了为每种类型单独写一个数组类的重复工作。编译器在编译时会根据模板参数生成具体的函数和类,比如add、Array<int,5>等,这些生成的代码和普通的非模板代码效率完全相同。

运行时多态(动态多态)

运行时多态是面向对象的核心,依赖继承和虚函数实现,在程序运行阶段根据对象实际类型动态绑定函数地址。运行时多态必须同时满足三个条件:存在继承关系,基类中至少有一个虚函数(virtual修饰),通过基类指针或引用指向子类对象,调用虚函数。除了这些基础条件,运行时多态还有很多重要的细节和扩展知识点,比如协变返回类型、虚函数默认参数、纯虚函数实现、RTTI等。

基础示例

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
// 基类:图形
class Shape {
public:
// 虚函数:绘制图形
virtual void draw() const {
cout << "绘制一个通用图形" << endl;
}
// 虚析构函数:防止内存泄漏
virtual ~Shape() {}
};
// 派生类:圆形
class Circle : public Shape {
public:
void draw() const override { // C++11 override 显式标记重写
cout << "绘制一个圆形" << endl;
}
};
// 派生类:矩形
class Rectangle : public Shape {
public:
void draw() const override {
cout << "绘制一个矩形" << endl;
}
};
// 通用绘制函数:通过基类指针调用
void drawShape(const Shape* shape) {
shape->draw(); // 动态绑定:根据实际对象类型调用对应版本
}
int main() {
Shape* s1 = new Circle();
Shape* s2 = new Rectangle();
drawShape(s1); // 输出:绘制一个圆形
drawShape(s2); // 输出:绘制一个矩形
delete s1;
delete s2;
return 0;
}

在这个例子中,drawShape函数接收的是Shape*指针,但传入的是Circle和Rectangle对象。当调用 shape->draw() 时,程序不会根据指针的声明类型Shape来调用函数,而是根据指针指向的实际对象类型(Circle或Rectangle)来调用对应的draw函数,这就是动态绑定。C++11引入的override关键字可以显式标记子类重写了基类的虚函数,如果子类的函数签名和基类不一致,编译器会直接报错,避免了拼写错误或参数不匹配导致的意外创建新函数。

虚析构函数的必要性

如果基类析构函数不是虚函数,通过基类指针删除子类对象时,只会调用基类的析构函数,导致子类资源泄漏。这是因为析构函数的调用如果不是虚函数,就会采用静态绑定,只根据指针的声明类型来调用析构函数。而如果基类析构函数是虚函数,删除时会先调用子类的析构函数,再调用基类的析构函数,确保子类和基类的资源都能正确释放。

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
// 错误示例:基类析构函数非虚
class BaseWrong {
public:
~BaseWrong() {
cout << "BaseWrong 析构函数" << endl;
}
};
class DerivedWrong : public BaseWrong {
private:
int* data;
public:
DerivedWrong() {
data = new int[10];
cout << "DerivedWrong 构造函数" << endl;
}
~DerivedWrong() {
delete[] data;
cout << "DerivedWrong 析构函数(释放资源)" << endl;
}
};
// 正确示例:基类析构函数为虚
class BaseRight {
public:
virtual ~BaseRight() { cout << "BaseRight 析构函数" << endl; }
};
class DerivedRight : public BaseRight {
private:
int* data;
public:
DerivedRight() {
data = new int[10];
cout << "DerivedRight 构造函数" << endl;
}
~DerivedRight() {
delete[] data;
cout << "DerivedRight 析构函数(释放资源)" << endl;
}
};
int main() {
cout << "错误示例" << endl;
BaseWrong* w = new DerivedWrong();
delete w; // 只调用 BaseWrong 析构,DerivedWrong 资源泄漏
cout << "\n正确示例" << endl;
BaseRight* r = new DerivedRight();
delete r; // 先调用 DerivedRight 析构,再调用 BaseRight 析构,资源正确释放
return 0;
}

在错误示例中,delete w时只调用了BaseWrong的析构函数,DerivedWrong中分配的data数组没有被释放,造成内存泄漏。而在正确示例中,delete r时先调用DerivedRight的析构函数释放data数组,再调用BaseRight的析构函数,资源完全正确释放。因此,只要一个类可能被继承,它的析构函数就应该声明为虚函数,这是C++面向对象编程的一个重要规范。

纯虚函数与抽象类

纯虚函数是在基类中声明为virtual 函数名 = 0的虚函数,包含纯虚函数的类称为抽象类,抽象类无法实例化对象,必须由子类重写纯虚函数后才能实例化。纯虚函数的存在强制子类实现特定接口,是面向接口编程的基础。不过需要注意的是,纯虚函数也可以在类外有实现,子类可以通过作用域解析符调用基类的纯虚函数实现。

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
// 抽象类:动物
class Animal {
public:
// 纯虚函数:发出声音
virtual void makeSound() const = 0;
// 普通成员函数
void eat() const { cout << "动物在吃东西" << endl; }
virtual ~Animal() {}
};
// 纯虚函数可以在类外有实现(虽然不常见,但语法允许)
void Animal::makeSound() const {
cout << "动物发出通用的声音" << endl;
}
// 派生类:狗
class Dog : public Animal {
public:
void makeSound() const override { cout << "汪汪汪" << endl; }
// 可以调用基类的纯虚函数实现
void makeBaseSound() const { Animal::makeSound(); }
};
// 派生类:猫
class Cat : public Animal {
public:
void makeSound() const override { cout << "喵喵喵" << endl; }
};
int main() {
// Animal a; // 错误:抽象类无法实例化
Animal* d = new Dog();
Animal* c = new Cat();
d->makeSound(); // 输出:汪汪汪
c->makeSound(); // 输出:喵喵喵
d->eat(); // 输出:动物在吃东西
// 调用基类的纯虚函数实现
static_cast<Dog*>(d)->makeBaseSound(); // 输出:动物发出通用的声音
delete d;
delete c;
return 0;
}

在这个例子中,Animal类是抽象类,无法创建对象,但它的纯虚函数makeSound可以在类外有实现。Dog类不仅重写了makeSound,还提供了makeBaseSound函数来调用基类的纯虚函数实现。这种设计虽然不常见,但在某些场景下很有用,比如基类提供一个默认的实现,子类可以选择直接使用或者在此基础上扩展。

协变返回类型

协变返回类型是C++中虚函数重写的一个特殊规则,它允许子类重写虚函数时,返回值类型是基类虚函数返回值类型的子类(前提是返回值是指针或引用)。协变返回类型的存在让多态更加自然,子类可以返回更具体的类型,而不需要强制类型转换。

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
// 基类:产品
class Product {
public:
virtual void show() const { cout << "这是一个通用产品" << endl; }
virtual ~Product() {}
};
// 派生类:具体产品A
class ProductA : public Product {
public:
void show() const override { cout << "这是产品A" << endl; }
void specificFuncA() const { cout << "产品A的特有功能" << endl; }
};
// 基类:工厂
class Factory {
public:
// 虚函数:创建产品,返回Product*
virtual Product* createProduct() const { return new Product(); }
virtual ~Factory() {}
};
// 派生类:具体工厂A
class FactoryA : public Factory {
public:
// 协变返回类型:返回ProductA*,是Product*的子类
ProductA* createProduct() const override { return new ProductA(); }
};
int main() {
Factory* f = new FactoryA();
// 因为协变返回类型,createProduct返回的实际是ProductA*
Product* p = f->createProduct();
p->show(); // 输出:这是产品A
// 可以直接转换为ProductA*,调用特有功能
ProductA* pa = static_cast<ProductA*>(p);
pa->specificFuncA(); // 输出:产品A的特有功能
delete p;
delete f;
return 0;
}

在这个工厂模式的例子中,Factory类的createProduct返回Product*,而FactoryA类重写createProduct时返回ProductA*,这就是协变返回类型。因为ProductA是Product的子类,所以这种重写是合法的。协变返回类型让我们在使用FactoryA时,可以直接得到ProductA*类型的对象,不需要额外的类型转换,代码更加自然和安全。

虚函数的默认参数陷阱

虚函数的默认参数是静态绑定的,而不是动态绑定的。也就是说,即使调用了子类的虚函数,默认参数的值还是会使用基类中定义的值,而不是子类中定义的值。这是一个非常容易出错的地方,需要特别注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Base {
public:
// 虚函数,默认参数为10
virtual void func(int x = 10) const {
cout << "Base::func,x = " << x << endl;
}
virtual ~Base() {}
};
class Derived : public Base {
public:
// 重写虚函数,默认参数改为20(但这个默认参数不会生效!)
void func(int x = 20) const override {
cout << "Derived::func,x = " << x << endl;
}
};
int main() {
Base* b = new Derived();
b->func(); // 调用的是Derived::func,但x的值是Base的默认参数10!
delete b;
return 0;
}

在这个例子中,b指向的是Derived对象,所以调用的是Derived::func,但默认参数x的值却是Base类中定义的10,而不是Derived类中定义的20。这是因为默认参数是在编译时根据指针的声明类型(Base*)来确定的,而不是在运行时根据对象类型确定的。为了避免这种陷阱,最好不要在虚函数中使用默认参数,或者确保子类和基类的默认参数完全一致。

RTTI(运行时类型识别)

RTTI是Run-Time Type Identification的缩写,即运行时类型识别,它允许程序在运行时获取对象的类型信息。C++中主要通过两个机制实现RTTI:dynamic_cast和typeid。这两个机制都依赖于虚函数,只有包含虚函数的类才能使用RTTI进行动态类型识别。

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
#include <typeinfo>
class Base {
public:
virtual void func() const { cout << "Base::func" << endl; }
virtual ~Base() {}
};
class Derived : public Base {
public:
void func() const override {cout << "Derived::func" << endl;}
void derivedFunc() const { cout << "Derived::derivedFunc(特有功能)" << endl; }
};
int main() {
Base* b = new Derived();
// 1. dynamic_cast:安全的向下类型转换
// 尝试将Base*转换为Derived*,如果转换成功,返回有效的Derived*;如果失败,返回nullptr
Derived* d = dynamic_cast<Derived*>(b);
if (d != nullptr) {
cout << "dynamic_cast转换成功" << endl;
d->derivedFunc(); // 可以调用Derived的特有功能
}
else cout << "dynamic_cast转换失败" << endl;
// 2. typeid:获取对象的类型信息
// typeid返回一个type_info对象的引用,可以用==或!=比较类型
cout << "b的类型:" << typeid(*b).name() << endl;
cout << "Derived的类型:" << typeid(Derived).name() << endl;
if (typeid(*b) == typeid(Derived)) cout << "b指向的是Derived对象" << endl;
delete b;
return 0;
}

dynamic_cast用于将基类的指针或引用安全地转换为子类的指针或引用。如果转换的是指针,失败时返回nullptr;如果转换的是引用,失败时会抛出bad_cast异常。typeid用于获取对象的类型信息,它返回一个type_info对象,通过type_info::name()可以获取类型的名称(不同编译器的名称格式可能不同),也可以直接比较两个type_info对象是否相等来判断两个对象的类型是否相同。需要注意的是,只有当类中有虚函数时,typeid才会返回对象的动态类型(实际类型);如果类中没有虚函数,typeid只会返回指针或引用的声明类型。

虚函数表与虚函数指针

每个包含虚函数的类会生成一张虚函数表(vtable),表中存储该类所有虚函数的地址;每个对象会包含一个虚函数指针(vptr),指向所属类的虚函数表。虚函数表和虚函数指针是运行时多态的底层实现基础,编译器通过虚函数指针找到虚函数表,再根据虚函数在表中的位置调用对应的函数。

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
class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
void nonVirtualFunc() { cout << "Base::nonVirtualFunc" << endl; }
};
class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func3() { cout << "Derived::func3" << endl; }
};
int main() {
Base b;
Derived d;
// 查看对象大小:包含虚函数指针(64位系统占8字节,32位系统占4字节)
cout << "Base 对象大小:" << sizeof(b) << endl;
cout << "Derived 对象大小:" << sizeof(d) << endl;
// 虚函数表验证:通过指针间接调用(仅用于演示,实际开发不建议,因为依赖编译器实现)
using FuncPtr = void (*)();
Base* ptr = &d;
// 虚函数表地址存储在对象的前几个字节(64位是前8字节,32位是前4字节)
long* vptr = (long*)ptr;
long* vtable = (long*)*vptr;
// 调用第一个虚函数:Derived::func1(因为Derived重写了func1)
FuncPtr func = (FuncPtr)vtable[0];
func();
// 调用第二个虚函数:Base::func2(因为Derived没有重写func2)
func = (FuncPtr)vtable[1];
func();
return 0;
}

只有达到同返回类型,同函数名,同参数列表才能构成运行时的多态。编译器在数据区创建一个虚表(Base::vftable),在构建时Base要额外多出一个虚表指针 __Vfptr 用于指向Base虚表的地址。当开始构建Derived时,虚表指针将指向Derived的虚表,也就是说虚表最终的指向是最后构建的对象的虚表地址。

image-20260319000947621

在这个例子中,Base类和Derived类的对象大小都包含了虚函数指针的大小(64位系统是8字节)。Base类的虚函数表中存储了func1和func2的地址,Derived类的虚函数表中,func1的地址被替换为Derived::func1的地址(因为重写了),func2的地址还是Base::func2的地址(因为没有重写),同时Derived类自己的func3也会被添加到虚函数表中(不过不同编译器的添加位置可能不同)。当通过Base* ptr指向Derived对象时,ptr->vptr指向的是Derived类的虚函数表,所以调用func1时会调用Derived::func1,实现了动态绑定。

当我们定义了Base对象并想要其调用func3方法时会编译错误,这是因为我们按照类型识别Base中并没有func3的方法,尽管虚表指针指向的Derived具有func3。

只有用指针和引用调用虚函数时才查询虚表,用对象名调用时则是静态链接。

需要注意的是,构造函数不能是虚函数,因为对象构造阶段虚函数指针尚未完全初始化,无法正确指向虚函数表;静态成员函数、友元函数不能是虚函数,它们不属于对象的成员,无法与具体对象绑定;子类重写虚函数时,函数签名必须与基类完全一致,建议使用override关键字;虚函数会带来轻微的运行时开销(虚函数指针存储、间接寻址),性能敏感场景需权衡。另外,虚函数和虚继承虽然都使用了virtual关键字,但它们的作用完全不同:虚函数是为了实现运行时多态,虚继承是为了解决菱形继承中的数据冗余和二义性问题,不要将两者混淆。