目录

一.多态的概念 

1. 静态多态(编译时多态)

2. 动态多态(运行时多态)

二.定义和实现多态

1.实现多态还有两个必须重要条件:

2.虚函数

3.析构函数重写

4.协变(了解即可)

三.override和final关键字

  1.override

2.final

四.重载/重写/隐藏的对⽐

五.纯虚函数和抽象类

六.底层实现机制

1. 虚函数表(vtable)

2. 虚指针(vptr)

3.底层细节

4.多继承情况下的虚函数表


一.多态的概念 

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。

1. 静态多态(编译时多态)
  • 概念:在编译阶段确定函数调用,通过函数重载或模板实现。
  • 特征
    • 编译器根据参数类型、数量或顺序匹配具体函数。
    • 无运行时开销,性能高。
    • 典型应用:函数重载、运算符重载、模板。
2. 动态多态(运行时多态)
  • 概念:在运行时根据对象实际类型确定函数调用,通过虚函数和继承实现。
  • 特征
    • 需满足三条件:继承关系、虚函数声明、基类指针/引用指向派生类对象。
    • 通过虚函数表(vtable)实现动态绑定,有轻微运行时开销。
    • 典型应用:接口抽象、行为扩展。

二.定义和实现多态

多态是继承关系下的类,去调用同一个函数,产生不同的行为,比如狗(dog)继承了动物(animal)叫声

1.实现多态还有两个必须重要条件:

         •必须是基类的指针或者引⽤调⽤虚函数

        • 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。

注意:如果派生类没有virtual,基类有virtual也构成多态不推荐使用。

        如果俩个都没有就是重定义(不是多态)派生类会隐藏基类。

class animal
{
public:
	virtual void call()
	{
		std::cout << "动物叫声" << std::endl;
	}
};
class dog : public animal
{
public:
	virtual void call()
	{
		std::cout << "汪汪" << std::endl;
	}
};
void func(animal& A)
{
	A.call();
}
int main()
{
	animal a;
	dog d;
	func(a);//动物叫声
	func(d);//汪汪
	return 0;
}
2.虚函数
  • 虚函数:用virtual关键字修饰的类成员函数
  • 核心目的:实现运行时多态(动态绑定),允许基类指针/引用调用派生类的重写函数
3.析构函数重写

基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派⽣类的析构函数就构成重写。

如果析构函数不加virtual的话可能会产生析构。下面代码如果没有加,b会产生内存泄漏,~B不会运行。

class A
{
public:
	virtual ~A()
	{
		std::cout << "~A()" << std::endl;
	}
};
class B : public A
{
public:
	virtual ~B()
	{
		std::cout << "~B()" << std::endl;
	}
};
int main()
{
	A* a = new A;
	A* b = new B;
	delete a;// ~A()
	delete b;// ~B()  +   ~A()
	return 0;
}

4.协变(了解即可)

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引 ⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们 了解⼀下即可。

class Base {};
class Derived : public Base {};

class Creator {
public:
    virtual Base* create() {  // 基类返回 Base*
        return new Base();
    }
};

class DerivedCreator : public Creator {
public:
    virtual Derived* create() override {  // 协变返回:Derived*
        return new Derived();
    }
};

三.override和final关键字

  1.override

  • 编译器检查:确保函数签名与基类虚函数完全匹配
  • 防止意外隐藏:检测拼写错误/参数类型错误
  • 代码可读性:显式表达设计意图
    class animal
    {
    public:
    	virtual void call()
    	{
    		std::cout << "动物叫声" << std::endl;
    	}
    	int a;
    };
    class dog : public animal
    {
    public:
    	virtual void call() override //明确重写关系
    	{
    		std::cout << "汪汪" << std::endl;
    	}
    	int b;
    };

    2.final

  • 安全加固:防止关键功能被意外修改
  • 性能优化:提示编译器可去虚拟化(devirtualization)
  • 接口锁定:在框架设计中明确终止扩展点
    class animal //如果放在这里就是不能被继承
    {
    public:
    	virtual void call() final//声明不能重写
    	{
    		std::cout << "动物叫声" << std::endl;
    	}
    	int a;
    };
    class dog : public animal
    {
    public:
    	virtual void call()//这里报错
    	{
    		std::cout << "汪汪" << std::endl;
    	}
    	int b;
    };

    四.重载/重写/隐藏的对⽐

  • 注意:这个概念对⽐经常考,⼤家得理解记忆⼀下

特性 重载(Overload) 重写/覆盖(Override) 重定义/隐藏(Redefine)
作用域 同一作用域 继承体系 继承体系
函数签名要求 参数列表必须不同 签名完全一致(协变返回除外) 函数名相同,参数可不同
virtual关键字 不需要 基类必须声明virtual 不需要
多态类型 编译时多态(静态) 运行时多态(动态) 无多态(静态绑定)
关键字支持 override(C++11)
基类函数可见性 不影响 不隐藏其他重载 隐藏基类所有同名函数

五.纯虚函数和抽象类

在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了 派⽣类重写虚函数,因为不重写实例化不出对象

class animal
{
public:
	virtual void call() = 0;
	int a;
};
class dog : public animal
{
public:
	virtual void call()
	{
		std::cout << "汪汪" << std::endl;
	}
	int b;
};
class cat : public animal
{
public:
	virtual void call()
	{
		std::cout << "喵喵" << std::endl;
	}
};
void func(animal& A)
{
	A.call();
}
int main()
{
	animal c1;//这里抽象类不能实例化
	cat* c2 = new animal;//这样子也不行
	animal* c3 = new cat;//但是可以这样子
	dog d;
	cat c;
	func(c);
	func(d);
	c3->call();//输出喵喵
	delete c3;
	delete c2;
	return 0;
}

六.底层实现机制

class animal
{
public:
	virtual void call()
	{
		std::cout << "动物叫声" << std::endl;
	}
	int a;
};
int main()
{
	printf("%zu\n", sizeof(animal));//如果是x64环境是 8(因为这里有虚函数指针+int)
    return 0;
}
1. 虚函数表(vtable)
  • 每个包含虚函数的类都有一个隐藏的虚函数表
  • 存储该类所有虚函数的函数指针
  • 编译器自动生成,在编译期确定
2. 虚指针(vptr)
  • 每个对象包含一个指向vtable的隐藏指针
  • 位于对象内存布局首部(32位系统4字节,64位系统8字节)
  • 构造函数中初始化,析构函数中销毁
#include <iostream>

class Base {
public:
    virtual void display() {
        std::cout << "Base::display()" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {
        std::cout << "Derived::display()" << std::endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->display();  // 运行时动态绑定
    delete ptr;
    return 0;
}
3.底层细节
    1. 当创建 Derived 对象时,对象内部会有一个虚指针 vptr,它指向 Derived 类的虚函数表。
    2. 当通过基类指针 ptr 调用 display 函数时,编译器会通过 ptr 找到对象的虚指针 vptr
    3. 然后通过 vptr 找到 Derived 类的虚函数表。
    4. 在虚函数表中找到 display 函数的地址,并调用该函数,从而实现了动态绑定。
4.多继承情况下的虚函数表

在多继承的情况下,每个派生类可能会有多个虚函数表,分别对应不同的基类。派生类对象的虚指针会根据基类的顺序指向不同的虚函数表,具体的实现会更加复杂,但基本原理仍然是通过虚函数表和虚指针来实现动态绑定。

总之,C++ 的静态多态在编译阶段确定函数调用,而动态多态在运行时根据对象的实际类型确定函数调用,通过虚函数表和虚指针实现了灵活的多态性。

更多推荐