代码仓库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 ; }
只有达到同返回类型,同函数名,同参数列表才能构成运行时的多态。编译器在数据区创建一个虚表(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。
只有用指针和引用调用虚函数时才查询虚表,用对象名调用时则是静态链接。
需要注意的是,构造函数不能是虚函数,因为对象构造阶段虚函数指针尚未完全初始化,无法正确指向虚函数表;静态成员函数、友元函数不能是虚函数,它们不属于对象的成员,无法与具体对象绑定;子类重写虚函数时,函数签名必须与基类完全一致,建议使用override关键字;虚函数会带来轻微的运行时开销(虚函数指针存储、间接寻址),性能敏感场景需权衡。另外,虚函数和虚继承虽然都使用了virtual关键字,但它们的作用完全不同:虚函数是为了实现运行时多态,虚继承是为了解决菱形继承中的数据冗余和二义性问题,不要将两者混淆。