代码仓库shanchuann/CPP-Learninng
编译时多态(静态多态) 编译时多态在程序编译阶段完成函数地址绑定,执行效率高,核心实现方式为函数重载、运算符重载,以及模板多态。模板多态也被称为参数化多态,是C++中另一种强大的编译时多态机制,它通过模板参数在编译时生成具体的函数或类实现,从而实现“一份代码,多种类型适配”的效果。
函数重载 同一个类中可以定义多个同名函数,只要参数列表(个数、类型、顺序)不同,编译器会根据实参自动匹配对应版本。这种匹配是在编译阶段完成的,编译器会检查实参与形参的类型兼容性,选择最匹配的函数进行调用,不会产生运行时额外开销。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class Calculator {public : int add (int a, int b) { return a + b; } int add (int a, int b, int c) { return a + b + c; } 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 { 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; cout << "\n正确示例" << endl; BaseRight* r = new DerivedRight (); delete r; 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* 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 () {} }; class ProductA : public Product {public : void show () const override { cout << "这是产品A" << endl; } void specificFuncA () const { cout << "产品A的特有功能" << endl; } }; class Factory {public : virtual Product* createProduct () const { return new Product (); } virtual ~Factory () {} }; class FactoryA : public Factory {public : ProductA* createProduct () const override { return new ProductA (); } }; int main () { Factory* f = new FactoryA (); Product* p = f->createProduct (); p->show (); ProductA* pa = static_cast <ProductA*>(p); pa->specificFuncA (); 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 : virtual void func (int x = 10 ) const { cout << "Base::func,x = " << x << endl; } virtual ~Base () {} }; class Derived : public Base {public : void func (int x = 20 ) const override { cout << "Derived::func,x = " << x << endl; } }; int main () { Base* b = new Derived (); b->func (); 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 (); Derived* d = dynamic_cast <Derived*>(b); if (d != nullptr ) { cout << "dynamic_cast转换成功" << endl; d->derivedFunc (); } else cout << "dynamic_cast转换失败" << endl; 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; cout << "Base 对象大小:" << sizeof (b) << endl; cout << "Derived 对象大小:" << sizeof (d) << endl; using FuncPtr = void (*)(); Base* ptr = &d; long * vptr = (long *)ptr; long * vtable = (long *)*vptr; FuncPtr func = (FuncPtr)vtable[0 ]; func (); func = (FuncPtr)vtable[1 ]; func (); return 0 ; }
只有达到同返回类型,同函数名,同参数列表才能构成运行时的多态(注:C++ 允许协变返回类型 例外,即重写虚函数时,返回类型可以是基类虚函数返回类型的派生类指针或引用,此情况仍满足多态条件)。编译器在数据区创建一个虚表(Base::vftable),在构建时 Base 要额外多出一个虚表指针 __Vfptr 用于指向 Base 虚表的地址。当开始构建 Derived 时,虚表指针将指向 Derived 的虚表,也就是说虚表最终的指向是最后构建的对象的虚表地址。
在这个例子中,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(补充:这涉及静态类型 与动态类型 的区别 ——Base*是静态类型,决定了成员访问的 “权限范围”;Derived是动态类型,决定了虚函数调用的实际目标。因此即使 Derived 虚表中有 func3,也无法通过 Base 类型访问)。
只有用指针和引用调用虚函数时才查询虚表,用对象名调用时则是静态链接。
总的来说:
派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。
只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。
静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。但内联作为建议,对于编译器来说虚的等级要高于内联,因此同时书写也依然可以编译通过。
构造函数和拷贝构造函数不能作为虚函数。因为构造函数和拷贝构造函数是用于设置虚表指针的函数。
析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。
实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。
在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
需要注意的是,静态成员函数、友元函数不能是虚函数,它们不属于对象的成员,无法与具体对象绑定;子类重写虚函数时,函数签名必须与基类完全一致,建议使用override关键字;虚函数会带来轻微的运行时开销(虚函数指针存储、间接寻址),性能敏感场景需权衡。另外,虚函数和虚继承虽然都使用了virtual关键字,但它们的作用完全不同:虚函数是为了实现运行时多态,虚继承是为了解决菱形继承中的数据冗余和二义性问题,不要将两者混淆。
静态联编和动态联编 联编(Binding)是 C++ 中确定函数调用语句与函数执行体对应关系的过程,是函数调用的底层核心环节。根据联编执行的时间节点,分为静态联编(早绑定)和 动态联编(晚绑定) ,二者分别对应了 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 Base {public : void func () { cout << "Base::func 非虚函数调用" << endl; } virtual void vFunc () { cout << "Base::vFunc 虚函数调用" << endl; } virtual ~Base () {} }; class Derived : public Base {public : void func () { cout << "Derived::func 非虚函数调用" << endl; } void vFunc () override { cout << "Derived::vFunc 虚函数调用" << endl; } }; int main () { Base b; Derived d; cout << "对象名直接调用" << endl; b.func (); b.vFunc (); d.func (); d.vFunc (); cout << "\n基类指针调用" << endl; Base* ptr = &d; ptr->func (); ptr->vFunc (); cout << "\n基类引用调用" << endl; Base& ref = d; ref.func (); ref.vFunc (); return 0 ; }
静态联编(早绑定) 静态联编指的是在程序编译阶段 就完成函数调用与函数体的绑定,编译器根据调用方的静态类型 (声明时的类型)直接确定要执行的函数地址,程序运行时不再修改。在上述示例中,所有非虚函数的调用、对象名直接调用的函数,均为静态联编:
非虚函数 func 的调用,编译器仅看指针 / 引用的静态类型 Base*,直接绑定 Base::func,无论指针实际指向哪个子类对象,都只会执行基类的函数实现;
用对象名 b、d 直接调用函数时,编译器明确知道对象的实际类型,直接绑定对应类的函数,即使是虚函数,也不会触发动态联编。
静态联编的核心优势是执行效率高,编译时直接确定函数地址,无任何运行时额外开销,缺点是不支持运行时多态,灵活性不足。
动态联编(晚绑定) 动态联编指的是在程序运行阶段 才完成函数调用与函数体的绑定,编译器在编译时无法确定调用方的实际类型,会在程序运行时,根据调用方的动态类型 (实际指向的对象类型),通过虚函数表找到对应的函数执行体,这也是 C++ 运行时多态的底层实现原理。动态联编的触发必须同时满足两个核心条件:
被调用的函数必须是虚函数 ,且子类完成了对该虚函数的重写;
必须通过基类的指针或引用 调用该虚函数。
在上述示例中,通过 Base* ptr 和 Base& ref 调用 vFunc() 时,触发了动态联编:编译阶段,编译器仅知道 ptr 和 ref 的静态类型是 Base* / Base& ,无法确定实际指向的对象类型;运行阶段,程序通过对象头部的虚函数指针 _vptr 找到对应类的虚函数表,查表获取到重写后的Derived::vFunc地址,执行对应的函数体,最终实现运行时多态。
只有用基类指针和引用调用虚函数时才会触发动态联编、查询虚函数表,用对象名直接调用时,无论是否为虚函数,均为静态联编。
二者差异
性能差异:静态联编在编译时确定函数地址,执行效率更高;动态联编需要运行时查询虚函数表,有轻微的性能开销,但换来的是极高的代码灵活性和可扩展性。
适用场景:静态联编适用于函数逻辑固定、无需重写的场景,如工具类函数、非虚成员函数;动态联编适用于需要基于继承实现多态、接口统一但实现差异化的场景,如之前示例中的图形绘制、工厂模式等。
边界注意:友元函数、全局函数、静态成员函数、构造函数均无法触发动态联编,只有类的非静态虚成员函数,才能通过基类指针 / 引用实现动态联编。