【C++】多态:一篇搞定虚函数、协变、动静态绑定、虚表与内存模型

目录

🌟 核心内容速览:

💡 关键结论:

1. 多态的概念

1.1 概念

多态初体验:动物合唱团

图形世界的多态秀

2. 多态的定义及实现

2.1多态的构成条件

2.2 虚函数

2.3虚函数的重写

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型可以不同)--->返回父子类的指针或者引用

2. 析构函数的重写(基类与派生类析构函数的名字不同)

例题1:考虚函数的重写

例题2:多继承中指针偏移问题?下面说法正确的是( )

2.4 C++11 override 和 final

设计一个类,不能被继承:

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3.多态的原理

3.1虚函数表

3.2多态是如何实现的?

当满足多态条件:动态绑定

不满足多态:静态绑定

3.3 动态绑定与静态绑定

4.单继承和多继承关系的虚函数表

4.1 单继承中的虚函数表

单继承派生类虚函数表对象模型:

4.2 多继承中的虚函数表(了解)

多继承中的虚函数对象模型:

4.3 菱形继承、菱形虚拟继承 

5.抽象类

5.1 概念

5.2 接口继承和实现继承

问题解答:

经典题目:

问答题:


本文深入浅出地剖析了C++中多态这一核心概念,通过生动案例与底层原理结合,带你彻底掌握面向对象编程的“变形”精髓!

🌟 核心内容速览:

  1. 多态的本质

    • 同一操作在不同对象上产生不同行为(如动物合唱团的“喵喵”与“汪汪”)。

    • 核心优势:扩展性高(新增图形类型无需修改渲染系统)与接口统一(通过基类指针调用子类方法)。

  2. 实现多态的关键

    • 虚函数:用virtual声明,允许子类重写。

    • 两大条件:必须通过基类指针/引用调用虚函数,且子类需完成函数重写(协变和析构函数是例外❗)。

  3. 虚函数表(vTable)

    • 每个含虚函数的类拥有一个虚表指针(_vfptr),指向存储虚函数地址的虚表。

    • 动态绑定:运行时根据对象类型查找虚表,决定调用哪个函数(如PersonStudent购票不同价)。

  4. 多继承与虚函数表

    • 多继承对象包含多个虚表指针,未重写的虚函数默认放在第一个基类的虚表中。

    • 菱形继承难题:虚拟继承通过虚基表优化内存布局,避免数据冗余。

  5. 抽象类与实战技巧

    • 纯虚函数(=0)强制子类重写,实现接口标准化(如汽车品牌必须定义驾驶方式)。

    • C++11新特性override检查重写,final禁止继承,让代码更安全🔒。

💡 关键结论:

  • 析构函数必须为虚函数:避免子类对象析构时内存泄漏!

  • 虚函数表位于代码段:虚函数本身存于代码段,虚表存其地址。

  • 多态 vs 重载:多态是运行时动态绑定,重载是编译时静态绑定。

🔗 立即收藏,点赞关注,解锁更多C++黑科技!💻
👇 评论区欢迎提问/讨论,一起攻克编程难关!🚀

1. 多态的概念

1.1 概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

多态是面向对象编程中让代码具备"七十二变"能力的魔法。想象你按下电视遥控器的电源键,不同品牌的电视会执行各自独特的关机动画——这正是多态的精髓:相同指令引发不同对象产生特有行为

多态初体验:动物合唱团

让我们用代码搭建一个动物舞台:

class Animal {
public:
    virtual void speak() {  // 虚函数标记
        cout << "动物发出声音" << endl;
    }
};

class Cat : public Animal {
public:
    void speak() override {  // 重写基类方法
        cout << "喵喵喵!" << endl;
    }
};

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

// 表演时刻
void animalConcert(Animal* performer) {
    performer->speak();  // 同一接口,不同表现
}

int main() {
    Cat whiskers;
    Dog buddy;
    
    animalConcert(&whiskers);  // 输出:喵喵喵!
    animalConcert(&buddy);     // 输出:汪汪汪!
}

这里animalConcert函数就像指挥家,不需要知道具体是猫还是狗,只需挥动指挥棒(调用speak方法),各动物就会自动展现自己的"声乐技巧"。

图形世界的多态秀

多态的威力在图形处理中更加明显:

class Shape {
public:
    virtual void draw() = 0;  // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "绘制圆形:○" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "绘制矩形:□" << endl;
    }
};

void renderScene(Shape* elements[], int count) {
    for(int i=0; i<count; ++i){
        elements[i]->draw();  // 自动适配具体图形
    }
}

int main() {
    Shape* artboard[] = {new Circle(), new Rectangle()};
    renderScene(artboard, 2); 
    // 输出:
    // 绘制圆形:○
    // 绘制矩形:□
}

这个绘图程序展示了多态的核心优势——扩展性。当需要新增三角形时,只需继承Shape并实现draw方法,渲染系统无需任何修改就能支持新图形。

传的是父类就调用父类,传的是子类就调用的子类

2. 多态的定义及实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表 完全相同),称子类的虚函数重写了基类的虚函数。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
	为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
	这样使用*/
	/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

如果写成这样:

什么是协变:

或者:

如果不是多态:打印出来都是全价,因为是多态,因此打印出来一个全价一个半价

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型可以不同)--->返回父子类的指针或者引用

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指

针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)

class A{};
class B :public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};

2. 析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

 继承有专门讲到过C++继承机制:从基础到避坑详细解说-CSDN博客

一定要记住下面这个场景:

析构函数建不建议定义成虚函数

这里有Person的指针,既可以指向父类,也可以指向子类对象

这里本应该调用子类的析构函数,没有调用到子类的析构函数的原因是:

这里delete的本质是:        析构函数 + operator delete(p)

当不满足多态的时候,就析构这个指针的类型即Person。

这样写的后果:

 会导致内存泄漏,因为ptr只能通过子类析构,释放ptr

期望:指向父类调用父类析构,指向子类调用子类析构 ----> 多态

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
// 数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

 父类析构不能不加virtual() ,重写,在子类重写,强调的是对父类函数的实现

加上virtual就构成重写。析构函数悄悄改成destructor,为了子类析构函数构成重写。

因此:建议析构函数定义为虚函数,防止内存泄漏

例题1:考虚函数的重写

以下程序输出结果是什么()
A: A->0         B: B->1         C: A->1         D: B->0         E: 编译出错         F: 以上都不正确

class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

首先,父类和子类的func构成重写;派生类B的指针调用父类的test()-->继承父类;test中的*thisl类型是A;p传给*this(this 指向B),*this调用func,func构成重写,因此这里是一个多态调用;多态调用:this指向谁调用谁,因此调用子类的func;子类func调用的不是子类的val,使用的是父类的val缺省参数;原因是:当构成多态调用的时候,构成重写,重写的是虚函数的实现,因此使用的是父类的虚函数的整个声明(包括了父类的缺省参数val = 1),实现便使用的是子类的函数内部实现; 因此B -> 1;

再次证明:虚函数的重写,重写的是虚函数的实现。(函数的结构部分(函数名,参数返回值)用的是父类的)     但是不建议写这样的代码(这个例题的代码)

例题2:多继承中指针偏移问题?下面说法正确的是( )

A:p1 == p2 == p3         B:p1 < p2 < p3         C:p1 == p3 != p2         D:p1 != p2 != p3

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main() {
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

在C++多继承中,派生类对象的布局按照继承顺序排列基类子对象(按照声明的顺序排列)Derive类的对象内存布局依次为Base1Base2Derive自身的成员。当将派生类指针转换为不同基类指针时:

  • Base1* p1:直接指向Derive对象起始地址(Base1子对象在头部),无需偏移。

  • Base2* p2:需要偏移Base1子对象的大小(4字节),指向Base2子对象的起始地址。

  • Derive* p3:指向整个对象的起始地址,与p1相同。

因此,p1p3的值相等,而p2的值比它们大4字节。正确选项为:

答案:C:p1 == p3 != p2

A:p1 == p2 == p3         B:p1 < p2 < p3         C:p2 == p3 != p1         D:p1 != p2 != p3

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base2, public Base1 { public: int _d; };
int main() {
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

2.4 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

1. final:修饰虚函数,表示 该虚函数不能再被重写--->  不能被继承

class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
}

设计一个类,不能被继承:

方法一,C++98提供:

class Car 
{
public:
	virtual void Drive() {}
private:
	//将父类的构造方法私有,但不够稳定
	//子类的构造无法生成和实现。导致子类对象无法实例化
	Car(){}
};
class Benz :public Car 
{
public:
};

int main()
{
	Benz b;
	return 0;
}

方法二,c++11提供:

用final修饰的类叫做最终类,不能被继承

//用final修饰的类叫做最终类,不能被继承
class Car final
{
public:
	virtual void Drive() {}


};
class Benz :public Car 
{
public:
};

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

加到子类的重写虚函数,检查是否完成重写,帮助我们快速检查

class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

使用:


2.5 重载、覆盖(重写)、隐藏(重定义)的对比

不构成重写就是隐藏(重定义)

3.多态的原理

3.1虚函数表

这里常考一道笔试题:sizeof(Base)是多少?(X86,32位平台下)

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

输出结果:12 :_b:4 ,_vf:4,_ch:1 ---> 内存对齐 --->12

通过调试:b对象里会多包含一个并没有定义的虚表指针(虚函数表指针)

只要有虚函数,就一定有虚函数表指针

虚表指针:为了来实现虚函数

无论有多少个虚函数,都只有一个虚表指针;

在内存中

b所存的是_vfptr:

_vfptr指向:这就是虚函数表(存的是所有的虚函数地址)这里放的是func1,func2的地址

对象模型:简单来说,一个对象在内存当中怎么存;

3.2多态是如何实现的?

如何实现传父类调用父类,传子类调用子类?

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
	int _i = 0;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	int _j = 1;
};
void Func(Person* p)
{
	p->BuyTicket();
}
int main()
{
	Person Mike;
	Func(&Mike);
	Student Johnson;
	Func(&Johnson);
	return 0;
}

满足多态:

当满足多态条件:动态绑定

那么这里调用生成的指令,就会去指向对象的虚表中找对应的虚函数进行调用。由此实现了指向谁调用什么函数。运行时决定调用谁。

在运行时去指向对象虚函数表中找BuyTicket的地址

同类型对象共用一张虚表,不同类型对象,虚表不一样。

不满足多态:静态绑定

在编译链接时根据调用对象类型,确定调用函数,确定函数地址

3.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

4.单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

4.1 单继承中的虚函数表

class Base {
public :
virtual void func1() { cout<<"Base::func1" <<endl;}
virtual void func2() {cout<<"Base::func2" <<endl;}
private :
int a;
};
class Derive :public Base {
public :
virtual void func1() {cout<<"Derive::func1" <<endl;}
virtual void func3() {cout<<"Derive::func3" <<endl;}
virtual void func4() {cout<<"Derive::func4" <<endl;}
private :
int b;
}

观察下图中的监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。那么我们如何查看d的虚表呢?

下面我们使用代码打印出虚表中的函数:

内存:不确认是否是func3、4的地址

证明是func3、4的地址

首先:虚函数表的本质是一个函数指针数组

先回顾一下C语言的函数指针与函数指针数组:

typedef void(*VFPTR)();

int main()
{

	//函数指针
	void (*p1)();
	VFPTR p2;
	//函数指针数组
	void (*pa2[10])();
	VFPTR pa3[10];

	return 0;
}

虚函数的指针就是对象的地址头四个字节

int 指针传int指针的地址,因此函数指针传函数指针的地址,不支持直接传数组;

怎么取头四个字节??

使用指针(指针本质就是一个个编号,和整形有关联,支持隐式类型的转换)

(先取到d的整个地址,将Derive*转成int,再解引用取到地址)*((int*) & d)---> 得到的是一个int

因此再强转一次(VFPTR*)(*((int*)&d));

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
//打印虚表

typedef void(*VFPTR)();
//virtual function table --- 虚函数表
// //打印函数指针数组
//void PrintVFT(VFPTR VFT[])
void PrintVFT(VFPTR* vft)  //函数指针传函数指针的地址
{
	for (size_t i = 0; i < 4; i++)
	{
		printf("%p\n", vft[i]);
	}

}
int main()
{
	Base b;
	Derive d;
	//怎么取头四个字节??使用指针(指针本质就是一个个编号,和整形有关联,支持隐式类型的转换)
	//(先取到d的整个地址,将Derive*转成int,再解引用取到地址)*((int*) & d)---> 得到的是一个int
	//因此再强转一次(VFPTR*)(*((int*)&d));
	VFPTR* ptr = (VFPTR*)(*((int*)&d));
	PrintVFT(ptr);
	return 0;
}

运行结果:

是函数指针就可以调用函数,因此这里可以将地址与调用的函数对应起来来证明内存那里的是func3、4的地址:

void PrintVFT(VFPTR* vft)  //函数指针传函数指针的地址
{
	for (size_t i = 0; i < 4; i++)
	{
		printf("%p->", vft[i]);

		VFPTR pf = vft[i];
		(*pf)(); //是函数指针就可以调用函数,因此这里可以将地址与调用的函数对应起来来证明内存那里的是func3、4的地址
	}
}

运行结果:

博主学到这里的时候也觉得,太妙了,妙蛙种子妙妙妙。<---请忽略

单继承派生类虚函数表对象模型:

4.2 多继承中的虚函数表(了解)

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

int main()
{
	cout << sizeof(Derive) << endl;
	return 0;
}

此代码输出结果是:

        20

多继承中的虚函数对象模型:

 查看两张虚表:

typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		(*f)();
	}
	cout << endl;
}
int main()
{
	cout << sizeof(Derive) << endl;
	Derive d;
	VFPTR* vTABLE1 = (VFPTR*)(*((int*)&d));
	PrintVTable(vTABLE1);
	//打印第二张虚表
	Base2* ptr = &d; //直接切片
	VFPTR* vTableb2 = (VFPTR*)(*(int*)(ptr));
	PrintVTable(vTableb2);
	return 0;
}

如果运行时出现了错误,清理一下再重新生成:

运行结果:这里发现实际上func3放在Base1的虚表当中:

规定:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

因此一个对象当中可以有多个虚表指针(多继承里面就有可能有多个虚表指针)

4.3 菱形继承、菱形虚拟继承 

C++ 虚函数表解析 | 酷 壳 - CoolShell

C++ 对象的内存布局 | 酷 壳 - CoolShell

菱形继承:

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,在菱形继承也同样

菱形虚拟继承:

派生类对象自己是没有虚表指针的,他包含两个部分:继承的他的父类们,父类中有虚表指针BC,虚基表指针A。再就是自己的成员变量

class A {
public:
	virtual void func1() { cout << "A::func1" << endl; }
	int _a;
};

class B : virtual public A {
public:

	virtual void func2() { cout << "B::func2" << endl; }
	int _b;
};
class C : virtual public A {
public:

	virtual void func3() { cout << "C::func3" << endl; }
	int _c;
};
class D : public B, public C {
public:

	virtual void func4() { cout << "D::func4" << endl; }
	int _d;
};
int main()
{
	D d;

	d.B::_a = 1;
	d.C::_a = 2;

	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}
菱形虚拟继承,每个类都有一个虚函数
子类有虚函数,继承的父类有虚函数就有虚表,子类对象中就不需要单独建立虚表

5.抽象类

5.1 概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	Car c;
	Benz b1;
	BMW b2;

	return 0;
}

抽象 --->不能有实体,必须由派生类重写纯虚函数(强制),重写后就没有纯虚函数了,就能实例化

重写之后,父类还是不能实例化出对象

但是可以有指针:指向哪个子类对象就调用哪个子类重写的虚函数

Benz b1;
BMW b2;

Car* ptr1 = &b1;
Car* ptr2 = &b2;
ptr1->Drive();
ptr2->Drive();

运行结果:

总结:

想检查派生类有没有完成重写,那就写override,想让基类实例不出对象就写纯虚函数

比如说在这里注重于表达车的品牌,并没有Car这个车的品牌,不需要实例化一个品牌对象。因此必须在派生类做重写,实例化派生类的对象 。

5.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

问题解答:

虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针(虚函数的地址),不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针

进程--->指令

类---> 类的实例化的对象会变成--->指令

函数--->指令

那么虚表存在哪的呢?(栈、堆、常量区(代码段)、静态区)

实际我们去验证一下会发现vs下是存在常量区(代码段)

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
private: 
	int _i = 0;
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	int _j = 1;
};
void Func(Person* p)
{
	p->BuyTicket();
}


int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区(代码段):%p\n", p2);
	Person p;
	Student s;
	Person* p3 = &p;
	Student* p4 = &s;
	//取出头四个字节的地址:
	printf("Person的虚表地址:%p\n", *((int*)p3));
	printf("Student的虚表地址:%p\n", *((int*)p4));

	return 0;
}

运行结果:

可以发现,虚表地址和常量区(代码段的地址是相近的)

经典题目:

下面程序输出结果是什么? ()

A:class A class B class C class D         B:class B  class C  class A  class D

C:class D class C class B class A         D:class A class B class A class C class A  class D

class A {
public:
	A(const char* s) { cout << s << endl; }//A
	~A() {}
};
class B :virtual public A
{
public:
	B(const char* s1, const char* s2) 
		: A(s1) 
	{ cout << s2 << endl; }//B
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2) 
		: A(s1) 
	{ cout << s2 << endl; }//C
};
class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const char* s4) 
		: B(s1, s2) //A、B
		, C(s1, s3) //A、C
		, A(s1) // A
	{
		cout << s4 << endl; //D
	}
};
int main() {
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

首先,一个类的构造都是先走初始化列表再走函数体,结尾打印的不是class D都排除,A到底初始化几次?我们从上面的学习知道,菱形虚拟继承只有一份A,A只会初始化一次!!!这一次直接通过A里面初始化的,为什么?因为成员初始化的顺序按照成员声明的顺序!!(A、B、C)A最先被继承

菱形虚拟继承最好不要在实践中使用

问答题:

  • 1. 什么是多态?答:参考本章的讲解
  • 2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考本节课件内容
  • 3. 多态的实现原理?答:参考本章的讲解
  • 4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
  • inline,因为虚函数要放到虚表中去。
  • 5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  • 6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  • 7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容
  • 8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  • 9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  • 10. C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基表搞混了。
  • 11. 什么是抽象类?抽象类的作用?答:参考(3.抽象类)。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

结语:

       随着这篇博客接近尾声,我衷心希望我所分享的内容能为你带来一些启发和帮助。学习和理解的过程往往充满挑战,但正是这些挑战让我们不断成长和进步。我在准备这篇文章时,也深刻体会到了学习与分享的乐趣。  

         在此,我要特别感谢每一位阅读到这里的你。是你的关注和支持,给予了我持续写作和分享的动力。我深知,无论我在某个领域有多少见解,都离不开大家的鼓励与指正。因此,如果你在阅读过程中有任何疑问、建议或是发现了文章中的不足之处,都欢迎你慷慨赐教。               

        你的每一条反馈都是我前进路上的宝贵财富。同时,我也非常期待能够得到你的点赞、收藏,关注这将是对我莫大的支持和鼓励。当然,我更期待的是能够持续为你带来有价值的内容,让我们在知识的道路上共同前行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值