欢迎光临
我们一直在努力

【C++笔记】多态

前言:

        在计算机科学中,多态(Polymorphism)是 C++ 面向对象编程(OOP)中最核心、也是最具魅力的特性之一。

        

一、多态简介

        

1.1 什么是多态

        

在 C++ 中,多态的核心思想是:“同一个接口,不同的行为”。

        

简单来说,你可以向不同的对象发送同一个指令,这些对象会根据自身的类型,以不同的方式去执行这个指令。

        

例如:

        

假设你是一家公司的CEO,向员工发布一条 “开始工作” 的指令。

        

公司里有着不同职位的员工,当他们听到这同一句话时,表现出的行为是完全不一样的:

        

A. 程序员(对象 A)听到后,他的行为是:打开电脑,启动编辑器,开始敲代码。

        

B. 设计师(对象 B)听到后,她的行为是:打开画板,拿起数位笔,开始画设计图。

        

C. 销售员(对象 C)听到后,他的行为是:拿出客户名单,拿起电话,开始推销产品。

        

D. 保洁阿姨(对象 D)听到后,她的行为是:拿起扫把和抹布,开始打扫卫生。

     

           

1.2 多态的分类        

        

我们可以把 C++ 中的多态分为两大类:静态多态(编译时多态)和动态多态(运行时多态),通常我们讨论的“多态”,主要指的是动态多态。

        

二、静态多态        

        

核心概念:静态多态发生在编译阶段,编译器在编译代码时,就已经能够确定具体要调用哪一个函数。

        

静态多态主要有以下两个实现方式:

        

1. 函数重载: 前提在同一个作用域内,对于同一个函数名,但参数列表不同(参数类型、个数或顺序不同),

                       编译器根据你传入的参数,决定调用哪个版本的函数。

        

代码示例:通过不同的参数类型匹配函数,对于同一个函数名,但因为参数类型不同,

                   编译器在听到这句话的瞬间,就已经知道你具体要执行哪一套截然不同的动作了。

#include <iostream>
#include <string>
using namespace std;

// 1. 参数是篮球
void func(string basketball)
{
cout << "去球场挥洒汗水!" << endl;
}

// 2. 参数是车子
void func(int taxi)
{
// 假设用整数代表出租车编号
cout << "站在路边招手叫车!" << endl;
}

int main()
{
func("斯伯丁篮球"); // 编译器看到字符串,自动匹配第一个函数
func(12345); // 编译器看到数字,自动匹配第二个函数
return 0;
}

        

打印结果如下:

        

2. 泛型编程/模板

        

你只需要打造一个通用的模具(Template)

当你告诉编译器你要用什么材料(传入具体类型,如 int、string)时,编译器会在后台自动为你“定制”出对应材料的专属代码。

        

代码示例:

#include <iostream>
#include <string>
using namespace std;

// 打造一个通用的“小熊模具”
// T 代表某种未知的材料(类型)
template <typename T>
void makeBear(T material)
{
cout << "使用传入的材料,制作了一个小熊,材料内容是: " << material << endl;
}

int main()
{
// 倒入整型“材料”
makeBear(100);
// 编译器在后台悄悄生成了一个 void makeBear(int material) 的函数

// 倒入字符串“材料”
makeBear("草莓面糊");
// 编译器又在后台悄悄生成了一个 void makeBear(string material) 的函数

return 0;
}

        

打印结果如下:

        

        

三、动态多态

        

核心概念: 动态多态发生在运行阶段,程序在运行到那一行代码时,才根据对象的真正类型来决定调用哪个函数。

        

如何理解 “动态” 或“运行时” ?

                

        

对于一个程序需要执行如下步骤:

        

第一阶段:预处理 (Preprocessing) —— “整理草稿”

        

第二阶段:编译 (Compilation) —— “核心翻译与语法检查”

        

第三阶段:汇编 (Assembly) —— “翻译成机器语言”

        

第四阶段:链接 (Linking) —— “装订成册,生成 .exe 可执行文件”

        

第五阶段:装载与运行 (Execution) —— “上台表演,执行.exe 文件”        

                

        

静态多态发生在第二阶段编译,动态多态的执行过程在第五阶段,

     

           

3.1 实现动态多态的条件

        

A.  存在继承关系: 必须有父类和子类。

        

B.  重写虚函数: 父类中必须有由 virtual 关键字修饰的虚函数,且子类必须重写(Override)这个虚函数。

        

C.  父类指针/引用指向子类对象: 必须通过父类的指针或引用来调用该虚函数。

        

条件一:类A 和 类B 需要有继承关系

        

代码示例: Dog类 继承自 Animal 类

#include <iostream>
using namespace std;

class Animal
{
public:
void eat() { cout << "动物在吃东西" << endl; }

void speak() { cout << "动物在叫" << endl; }
};

class Dog : public Animal
{
public:

};

        

条件二:父类中必须有虚函数,且子类需要对该虚函数进行重写

        

1.虚函数介绍

        

虚函数的语法极其简单,就是在父类的成员函数前面加一个 virtual 关键字。

        

它的核心作用是向编译器声明:"当通过父类指针调用此函数时,不要进行静态绑定,而要在程序运行时根据指针实际指向的子类对象,动态调用相应的重写版本。"

        

代码示例:Animal 类的成员函数speak 函数前添加了virtual关键字

#include <iostream>
using namespace std;

class Animal
{
public:
// 普通函数(非虚函数):编译器会在编译时直接绑死
void eat() { cout << "动物在吃东西" << endl; }

// 虚函数:编译器会留个心眼,推迟到运行时再决定
virtual void speak() { cout << "动物在叫" << endl; }
};

class Dog : public Animal
{
public:

};

        

温馨提示:

        

① 因为Dog类 通过Public继承自 Animal 类 ,所以具有  Animal 类同样的函数,

        

即 Dog类具有 普通函数 void eat() 和 虚函数 virtual void speak() 

        

        

② 子类继承而来的函数代码,实际上存放在内存的代码段中。

        

子类并不会把父类的函数代码“复制”一份存到自己这里,不论是普通函数还是虚函数,在内存中都只有一份实体代码,属于类的所有实例共享。

                

        

2. 虚函数的重写

        

通俗地讲,“重写” 就是子类对父类说:“老爸,你定的这个规矩(虚函数)挺好,但在我这里,我要按照我的方式来执行。”

        

要在子类中成功重写父类的虚函数,子类的函数声明必须和父类完全一模一样,具体来说,要满足以下四个条件 :          

        

1. 函数名必须完全一致。

        

2.参数列表必须完全一致(参数的个数、类型、顺序必须一模一样,形参名可以不同)。

        

3. 常量属性(const)必须完全一致(如果父类函数后面加了 const,子类也必须加)。

        

4. 返回值类型通常也必须一致   (除非是协变返回类型这种高级用法)

        

        

 注:因为子类继承自父类,若父类的成员函数前面加上virtual成为虚函数,继承的子类也会是虚函数,

        

        所有在重写的过程中可以不主动添加virtual。

        

代码示例1:函数名必须完全一致,否则不构成重写。

class Base
{
public:
virtual void attack() { cout << "普通攻击" << endl; }
};

class Derived : public Base
{
public:
// 不满足条件二:手抖把 attack 拼成了 attck。
// 这变成了一个全新的函数,父类的 attack() 被原封不动继承了过来。
void attck() { cout << "Boss 释放大招!" << endl; }

// 满足条件:一字不差
virtual void attack() override { cout << "Boss 释放大招!" << endl; }
};

        

代码示例2:参数列表必须完全一致,包括参数的个数、类型、顺序,必须和父类分毫不差。

                    如果参数不同,哪怕名字一样,会触发隐藏(Hide),导致多态失效。

class Base
{
public:
virtual void processData(int data) { cout << "处理整数" << endl; }
};

class Derived : public Base
{
public:
// 不满足条件二:参数类型不同 (float vs int)。
// 这会导致隐藏!当你用子类对象调用 processData 时,只能传 float。
void processData(float data) { cout << "处理浮点数" << endl; }

// 满足条件:参数严格一致
virtual void processData(int data) override { cout << "子类处理整数" << endl; }
};

        

温馨提示:函数参数列表中的形参名可以不同,只需保证参数类型,个数,顺序相同。

        

代码示例3:常量属性 (const)  必须兼容

class Base
{
public:
virtual void processData(int data) const { cout << "处理整数" << endl; }
};

class Derived : public Base
{
public:

//错误示例:不满足与父类的常属性相同
// virtual void processData(int data) { cout << "子类处理整数" << endl; }

//正确示例:满足与父类函数的常属性相同
virtual void processData(int data) const { cout << "子类处理整数" << endl; }
};

        

代码示例4: 返回值类型,除协变外必须相同

class Base
{
public:
// 返回整数
virtual int getValue() { return 100; }
};

class Derived : public Base
{
public:
// 编译报错! 把返回值改成 float
virtual float getValue() { return 100.5f; }
};

        
        

条件三:必须是基类的指针或者引⽤调⽤虚函数

        

代码示例:必须是基类的指针或者引⽤调⽤虚函数,才能实现多态的效果。

#include <iostream>
using namespace std;

class Animal {
public:
virtual void speak() { cout << "【动物】发出了未知的叫声" << endl; }
};

class Dog : public Animal {
public:
void speak() override { cout << "【狗】汪汪汪!" << endl; }
};

// 错误示范:按值传递(直接传父类对象)
void callByValue(Animal a)
{
cout << "按值传递调用: ";
a.speak();
}

// 正确示范 1:按指针传递
void callByPointer(Animal* a)
{
cout << "按指针传递调用: ";
a->speak();
}

// 正确示范 2:按引用传递
void callByReference(Animal& a)
{
cout << "按引用传递调用: ";
a.speak();
}

int main()
{
Dog myDog; // 实例化一只真实的狗

// 见证奇迹与灾难的时刻:
callByValue(myDog); // 输出: 【动物】发出了未知的叫声 (多态失效!)
callByPointer(&myDog); // 输出: 【狗】汪汪汪! (多态成功!)
callByReference(myDog); // 输出: 【狗】汪汪汪! (多态成功!)

return 0;
}

        

打印结果如下:

     

           

3.2 虚函数重写的特殊情况

        

特殊情况一:协变        

        

协变(仅当了解):当父类虚函数返回的是一个基类的指针(或引用)时,子类重写该函数可以返回派生类的指针(或引用)。

        

协变的条件:        

        

1.  返回类型 必须是指针或引用(不能是直接返回对象/按值返回)。

        

2.  父类返回的是“父类”的指针或引用,子类返回的必须是“子类”的指针和引用。

        

3.  这两个类之间必须是公开继承(public)关系。

        

代码示例:协变可以允许返回类型不同

class Base
{
public:
// 返回整数
virtual Base* getValue() { return this; }
};

class Derived : public Base {
public:

virtual Derived* getValue() { return this; }
};

        

特殊情况二:析构函数

        

析构函数的重写

        

“重写的四个条件”,里面有一条铁律:函数名必须完全一致。

        

但是,析构函数的名字是和类名绑定的(比如:父类叫 ~Animal(),子类叫 ~Dog()),既然名字都不一样,它凭什么能“重写”?

                

事实上,在 C++ 编译器的底层实现中,为了支持析构函数的多态,它会把所有的析构函数统一重命名为一个内部的通用名字,比如叫 destructor()。

        

        

核心问题:为什么需要虚析构函数?

        

主要目的:通过将基类的析构函数声明为 virtual,来确保在使用基类指针删除派生类对象时,能够正确调用派生类的析构函数。

        

代码示例:反面教材(非虚析构函数 -> 内存泄漏)

#include <iostream>
using namespace std;

class Base
{
public:
Base() { cout << "Base 构造函数" << endl; }
~Base() { cout << "Base 析构函数" << endl; } // 注意:这里没有 virtual
};

class Derived : public Base
{
public:
Derived() { cout << "Derived 构造函数" << endl; }
~Derived()
{
delete ptr;
cout << "Derived 析构函数" << endl;
}
private:
int* ptr = new int();
};

int main()
{
Base* ptr = new Derived();
delete ptr;
return 0;
}

        

输出结果: 

        

注意:

        

如果基类 Base 的析构函数不是虚函数(non-virtual),C++ 编译器在执行 delete ptr 时,只能根据指针的静态类型(即 Base*)来决定调用哪个析构函数。

        

结果就是:只调用了基类 Base 的析构函数,而派生类 Derived 的析构函数被忽略了。

        

如果派生类在堆区申请了内存(比如 new 了一些数据),这部分内存就永远不会被释放,从而导致内存泄漏。

        

如何解决因父类为非虚析构函数而造成的内存泄漏呢?

        

解决方案:将基类的析构函数声明为 virtual

        

① 我们只需要在基类的析构函数前面加上 virtual 关键字。

        

② 当基类的析构函数是虚函数时,执行 delete ptr 会触发动态绑定。

        

③ 程序在运行时会检查 ptr 实际指向的对象类型(即 Derived),然后先调用派生类的析构函数,再调用基类的析构函数。

     

 (基类和派生类满足,继承关系自动调用基类的析构函数)

        

代码示例:正确做法(虚析构函数 -> 安全释放)

#include <iostream>
using namespace std;

class Base
{
public:
Base() { cout << "Base 构造函数" << endl; }
virtual ~Base() { cout << "Base 析构函数" << endl; }
};

class Derived : public Base
{
public:
Derived() { cout << "Derived 构造函数" << endl; }
~Derived()
{
delete ptr;
cout << "Derived 析构函数" << endl;
}
private:
int* ptr = new int();
};

int main()
{
Base* ptr = new Derived();
delete ptr;
return 0;
}

        

输出结果:

        

        

3.3 override 和 final关键字

        

从上述分析可见,C++对虚函数重写的规范要求相当严格。

        

然而在实际编程中,开发者可能因疏忽(例如:拼错函数名或参数错误)导致无法构成有效的函数重写,值得注意的是,这类语法错误在编译阶段通常不会报错。

        

在 C++11 标准中引入的 override 和 final 这两个关键字,它们的主要作用是让编译器帮你检查逻辑错误,并且明确表达你的设计意图。        

        

        

A. override 关键字

       

在 C++11 之前,如果你想重写一个虚函数,函数名、参数列表、const 属性 需要完全一样

        

容易踩的“静默失败”陷阱:

1.不小心拼错了函数名。

        

2. 参数类型少加了个 & 或者 const。

        

3. 基类的函数签被修改了,但你派生类的没改。

        

这种情况下,编译器不会报错,它会以为你在派生类里写了一个全新的普通函数(这叫“隐藏” Hide),导致多态失效,Bug 非常难找。

        

解决方案:

        

1. 在派生类的函数声明末尾加上 override,相当于明确告诉编译器:“我这个函数是要重写基类虚函数,请帮我盯着点!”

        

2. 如果基类中没有与之完全匹配的虚函数,编译器会直接报错(Error),拒绝编译。

        

代码示例:通过对重写的虚函数添加override 关键字,编译器能帮助我们检查出是否完成重写。

class Base
{
public:
virtual void processData(int data) const
{
// 基类的默认处理
}
};

class Derived : public Base
{
public:
// 本意是重写,但不小心漏掉了 const 关键字
// 结果:多态失效,这不是重写,这是一个全新的函数!(编译器不会提醒你)
void processData(int data)
{
// …
}

// 正确做法:加上 override
// 结果:编译器立刻报错!指出基类中没有 `void processData(int)` 这个虚函数
// 强制你发现错误并加上 const,改成 `void processData(int data) const override`
void processData(int data) override
{
// …
}
};

        

        

B. final 关键字

        

final 的字面意思就是 “最终的、不可改变的”,它有两个使用场景:作用于类 和 作用于虚函数。

        

场景一:作用于类

        

如果你设计了一个类,认为它的功能已经非常完整,或者出于安全和架构的考虑,绝对不允许别人再继承它,你就可以在这个类名后加上 final。

        

代码示例:经过final修饰后的类,不再能被继承。

// 声明 CoreSystem 是最终类
class CoreSystem final
{
// 核心底层代码
};

// 【编译报错】无法继承 final 类!
class MySystem : public CoreSystem
{
};

        

场景二:作用于虚函数

        

在多层继承中,你可能允许别人继承你的类,但希望某个特定的虚函数不能再被后续的派生类重写了。

        

代码示例:虚函数经过final修饰,不再能够被继承类重写

class Base
{
public:
virtual void step1() { cout << "Base step 1" << endl; }
virtual void step2() { cout << "Base step 2" << endl; }
};

class Intermediate : public Base
{
public:
// 重写 step1,并允许后续类继续重写
void step1() override { cout << "Inter step 1" << endl; }

// 重写 step2,并打上 final,禁止后续类再改动这个步骤!
void step2() override final { cout << "Inter step 2 (Locked!)" << endl; }
};

class Leaf : public Intermediate
{
public:
// 合法:step1 没有被 final 限制
void step1() override { cout << "Leaf step 1" << endl; }

// 【编译报错】step2 在 Intermediate 中被声明为 final,无法重写!
void step2() override { cout << "Leaf step 2" << endl; }
};

        

3.4 纯虚类 和 抽象类

        

在 C++ 的面向对象编程中,如果说普通的虚函数(virtual)是 “ 建议重写 ”,那么纯虚函数就是“ 强制重写 ”。

        

包含了纯虚函数的类,就会自动升级为一个抽象类。

        

A. 纯虚函数

        

语法格式:在虚函数的声明末尾加上 = 0。

class Base
{
public:
virtual void func() = 0;
};

        

核心作用:我们在设计一个基类时,知道所有的派生类都必须要有某个动作,但基类本身根本无法确定这个动作具体该怎么做时可以使用纯虚函数。

        

场景示例:我们要设计一个 基类 Shape(形状),它应该有一个求面积的函数 getArea()。但是,一个抽象的“形状”是没有面积公式,只有具体的“圆形”或“矩形”才有。

          这时候就推荐将 基类Shape(形状) 的求面积函数声明为纯虚函数。

        

代码示例:

class Shape
{
public:
// 这是一个纯虚函数!没有函数体。
virtual double getArea() const = 0;

// 抽象类中也可以有普通函数或虚析构函数(必须要有!)
virtual ~Shape() { cout << "Shape析构" << endl; }
};

        

B. 抽象类

        

抽象类:只谈规矩,不落地的“图纸”。

        

实现方式:只要一个类里面包含了至少一个纯虚函数,这个类就变成了“抽象类”。

        

核心规则:抽象类绝对不能实例化(不能创建对象)

        

核心作用:抽象类的本质是一张图纸或者说是一份合同(契约),它定义了一组接口规范,强制要求所有派生类必须按这个规范办事。

        

        

C. 派生类的宿命

        

当一个子类继承了抽象类时,它面临两个选择:

        

选择 A(乖乖听话):老老实实把基类里所有的纯虚函数都重写(Override)一遍,提供具体的实现。

                                    通过重写基类的虚函数,此时派生类就是一个具体类,可以创建对象了。

        

选择 B(继续摆烂):不重写,或者漏掉了一个纯虚函数没重写。

                                     结果就是:“派生类” 将 “基类的纯虚函数” 原封不动继承过来了,所以子类也会自动变成抽象类,同样不能实例化。

        

代码示例:要么重写,要么成为抽象类

#include <iostream>
using namespace std;

// 1. 抽象类(基类)
class Shape
{
public:
virtual double getArea() const = 0; // 纯虚函数
virtual ~Shape() {}
};

// 2. 具体类:圆形(重写了纯虚函数)
class Circle : public Shape
{

public:
Circle(double r) : radius(r) {}

// 完美履行契约,重写了 getArea
double getArea() const override
{
return 3.14159 * radius * radius;
}

private:
double radius;
};

// 3. 失败的具体类:三角形(忘了重写纯虚函数)
class BadTriangle : public Shape
{
// 程序员忘了写 getArea() 的实现…
};

int main()
{
// Shape* s = new Shape(); // 报错:Shape 是抽象类
// BadTriangle* t = new BadTriangle(); // 报错:BadTriangle 也是抽象类,因为它没重写 getArea

// 多态发挥作用:用基类指针指向派生类对象
Shape* c = new Circle(5.0);
cout << "圆的面积: " << c->getArea() << endl; // 调用 Circle 的 getArea

delete c;
return 0;
}

        

        

3.5 重载、重写和隐藏对比

        

如下图所示:

        

        

简记为:

        

① 重载 (Overload):同处一室(同作用域),同名不同参。

        

② 重写 (Override):父子关系(继承),同名同参,父有 virtual。

        

③ 隐藏 (Hide):父子关系(继承),名字相同,只要不满足重写,就是隐藏。

        

        

四、多态的原理

        

C++ 实现运行时多态的核心秘密,其实可以浓缩为六个字:虚表(vtable)与虚指针(vptr)。        

        

4.1 虚函数表       

        

A. 虚函数表简介     

           

① 核心概念: 只要一个类里面包含至少一个虚函数(virtual),C++ 编译器在编译阶段,就会偷偷为这个类生成一张特殊的数据表,这就是虚函数表。        

        

② 虚函数表中存储的内容:虚函数表本质是一个函数指针数组,其存储的是这个类所有虚函数的实际内存地址。     

        

温馨提示:虚函数本质存储在代码段,只是将其地址拷贝到虚函数表(函数指针数组) 

        

③ 共享性: 同一个类的所有对象,共享同一张虚表。

        

温馨提示:基类对象的虚函数表中存放基类所有虚函数的地址,派生类对象的虚函数表存放派生类中所有虚函数的地址。

        

④ 虚函数表存储位置:在主流编译器的典型实现中,虚函数表(vtable)通常被存储在只读数据段(.rodata 或常量区)。

        

B. 覆盖机制(重写的本质)

        

场景示例:

        

①基类 Base 有自己的 虚函数表,里面记录着 Base::func1 和 Base::func2 的地址。

        
②派生类 Derived 也会有自己的 虚函数表,编译器会先把基类的 虚函数表内容 拷贝过来。

        

重点:如果 Derived 重写了 func1,编译器就会把 Derived vtable 中 func1 的地址 替换(覆盖)成 Derived::func1 的新地址。

        

代码示例:编译器的幕后工作流,三步建表

class Base {
public:
virtual void func1() { cout << "Base func1"; }
virtual void func2() { cout << "Base func2"; }
};

class Derived : public Base {
public:
// 重写了 func1
void func1() override { cout << "Derived func1"; }

// 新增了一个虚函数 func3
virtual void func3() { cout << "Derived func3"; }
};

        

重写机制核心讲解:        

        

① 第 1 步:为基类造表(排版原版菜单)

        

编译器扫描 Base 类,发现两个虚函数,它建立一张表 Base_vtable:

        

[坑位 0] 填入 Base::func1 的内存地址。

                

[坑位 1] 填入 Base::func2 的内存地址。

        

        

② 第 2 步:派生类建表,先全盘照抄     

        

当编译器开始处理 Derived 类时,因为它继承自 Base,编译器做的第一件事是:建一张新表 Derived_vtable,并把老爹的表原封不动地“复印”过来。

        

此时,Derived_vtable 长这样:

        

[坑位 0] 填入 Base::func1 的地址。 (照抄)

        

[坑位 1] 填入 Base::func2 的地址。 (照抄)

        

        

③ 第 3 步 :扫描派生类,实施“覆盖(替换)”      

        

发现重写: 编译器发现 Derived 提供了一个全新的 func1()。

        

精准定位: 编译器知道 func1 在基类中属于 [坑位 0]。

        

无情覆盖: 编译器毫不犹豫地把 Derived_vtable 中 [坑位 0] 里的 Base::func1 地址擦除,

                

                   替换(覆盖)成刚刚编译好的 Derived::func1 的地址。

        

发现新增: 编译器发现 Derived 还有一个全新的虚函数 func3()。

                   

                   因为它是一个新加的虚函数,基类里没有它的坑位,于是编译器在表的最末尾追加一个新坑位:[坑位 2],填入 Derived::func3 的地址。

        

如下图所示:

        

        

4.2 虚函数表指针

                

当你实例化一个包含虚函数的对象时,编译器会在这个对象的内存布局的最起始位置,强行塞进去一个隐藏的指针,这就是虚表指针(vptr)。

        

A、虚函数表指针简介        

        

① 虚函数表指针指向:这个 虚表指针 ( vptr ) 永远指向该对象所属类的 vtable。

        

② 从属关系: vptr 是属于对象的,你创建 100 个对象,就会有 100 个 vptr(但它们都指向同一张属于该类的 vtable)。

        

温馨提示:派生类继承基类时会获得基类的虚表指针,但需注意,派生类继承的基类部分虚表指针与基类对象自身的虚表指针是不同的,它们分别指向各自的虚函数表。

        

        

代码示例:计算包含虚函数的类的大小,验证虚函数指针存在

#include <iostream>
using namespace std;

class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};

int main()
{
Base b;
cout <<"包含虚函数的类Base大小为:" << sizeof(b) << endl;
return 0;
}

        

输出结果为:根据内存对齐规则可知,Base类中包含虚函数表指针

        

对象b的内存分布如下所示:

        

B、虚函数表指针的生命历程        

        

过程一:虚函数表指针的诞生        

vptr 是一个隐藏的指针,在代码里又没有写  vptr = &vtable;  那么它是谁赋值的?什么时候赋值的?        

        

答案是:编译器强行塞到了你的构造函数里。

        

代码示例:当我写下一个包含虚函数的类

class Base
{
public:
Base() { cout << "Base 构造"; }
virtual void show() {}
};

        

编译器在背后偷偷把你的构造函数改写成了类似这样(伪代码):

Base()
{
this->vptr = &Base::vtable; // 编译器偷偷加的代码!将对象的 vptr 指向类的 vtable
cout << "Base 构造";
}

        

过程二:虚函数表指针的生命周期

        

这是 C++ 多态中最反直觉、但也最精妙的设计。

当我们 new 一个派生类对象时,会依次调用:基类构造函数 -> 派生类构造函数,在这个过程中,vptr 并不是一开始就指向派生类的虚表的,它会改变指向。

        

代码示例:演示vptr的指向过程

#include<iostream>
using namespace std;

class Base
{
public:
Base() { /* … */ }
virtual void speak() { cout << "Base speak" << endl; }
};

class Derived : public Base
{
public:
Derived() { /* … */ }
void speak() override { cout << "Derived speak" << endl; }
};

int main()
{
Base* p = new Derived();
return 0;
}

        

详细解释:

        

1.内存分配:

        

在堆区分配一块内存,足够装下 Base 部分、Derived 自己新增的部分,以及那个隐藏的 vptr。

        

        

2.执行 Base 构造函数:

        

编译器先强行让对象的 vptr 指向 Base 类的虚表。

        

        

3. 执行 Derived 构造函数:

        

编译器重新赋值,强行把 vptr 覆盖,让它指向 Derived 类的虚表。

        

温馨提示:多态在构造函数和析构函数期间是失效的

        

如果在 Base 的构造函数 或 析构函数 里调用了虚函数 speak(),会发生什么?

        

①在构造函数中:        

当时 vptr 正指向 Base 的虚表,它只会调用 Base::speak(),而绝不可能调用 Derived::speak(),

                

因为派生类还没构造,vptr 还没指向派生类的虚表。

        

        

②在析构函数中:

        

因为先对Derived类 进行析构,再对Base类 进行析构

        

在进入 Base 析构函数的那一瞬间,派生类 Derived 专属的部分(成员变量等)在逻辑上已经被销毁了,所以无法形成多态。

4.3 多态的实现

        

通过深入对 “虚函数表” 和 “虚函数表指针” 的了解,我们可以深刻体会到多态的实现过程了。

        

代码示例:

       

#include<iostream>
using namespace std;

class Base
{
public:
virtual void func1() { cout << "Base func1"; }
virtual void func2() { cout << "Base func2"; }
};

class Derived : public Base
{
public:
// 重写了 func1
void func1() override { cout << "Derived func1"; }
};

int main()
{
Base* ptr = new Derived();
ptr->func1();
return 0;
}

        

详情解释:

        

① 编译器会把 ptr->func1() 翻译成类似下面这样的底层指令: “找到 ptr 指向的对象,拿出它的 vptr,去查虚函数表表里拿到地址并执行。” 

        

② 编译器根本不去管这个对象到底是 Base 还是 Derived,它只认死理:查寻它自己的表的虚函数表

        

③ 由于重写机制的特性,“派生类中重写的虚函数”会覆盖基类中对应的虚函数在 “派生类虚函数表” 中的位置。

        

如下图所示:

赞(0)
未经允许不得转载:171主机测评 » 【C++笔记】多态
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址