C++的学习心得和知识总结 第六章(完)

本章内容:

文章目录

  • 本章内容:
  • 第一节:继承的基本意义:
  • 第二节:派生类的构造过程:
  • 第三节:成员方法的重载、隐藏、覆盖:
  • 第四节:虚函数、静态绑定和动态绑定:
  • 第五节:虚析构函数:
  • 第六节:动态绑定:
  • 第七节:多态:
  • 第八节:抽象类:
  • 第九节:继承与多态 笔试题:

1.继承的本质和原理
2.派生类的构造过程
3.成员方法的重载、覆盖、隐藏
4.静态绑定和动态绑定
5.多态 vfptr和vftable
6.抽象类的设计原理
7.多重继承以及问题
8.虚基类 vbptr和vbtable
9.RTTI
10.C++四种类型强转
11.继承多态常见笔试面试题分享
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

第一节:继承的基本意义:

(1)代码的复用:
C++的学习心得和知识总结 第六章(完)_第1张图片
类B 也有类A的成员变量。一种办法:在类B里面再行定义,如上图所示。
另一种办法,由A派生类B。选择继承方式:
C++的学习心得和知识总结 第六章(完)_第2张图片
C++的学习心得和知识总结 第六章(完)_第3张图片
B从基类A派生而来,继承了A的成员变量。所以 B类对象的内存上也是有A的ma mb mc。再加上B的md me mf则对象b的内存为6*4=24字节。
假如 在B类中又定义一个成员变量ma,会发生冲突吗(和从基类继承来的ma)?不会的 也从基类继承来了作用域,他们的作用域不同。所以说 重名的成员变量在作用域不同的情况下也是可以共存的。
在这里插入图片描述
继承的方式和访问限定:
C++的学习心得和知识总结 第六章(完)_第4张图片
对于成员方法的访问权限和成员变量的一样。
C++的学习心得和知识总结 第六章(完)_第5张图片
假如此时的ma在类A中 是public的成员变量,但是B从A继承而来。在B中,ma相同于其中的私有成员mf。因此在C public继承B的时候,在C中对成员变量ma的访问限定是:可以继承过来,但是不可见的。

在C中对成员变量ma的访问限定是看 在C的 直接基类 B中的访问限定是什么?不管继承方式是什么,基类的私有成员在派生类里面都是可以继承而来,但是不可见的。
C++的学习心得和知识总结 第六章(完)_第6张图片
在这里插入图片描述
class定义的类,如果类里面不写访问限定,成员的访问限定默认也是私有的。
struct定义的类,如果类里面不写访问限定,成员的访问限定默认也是公有的。

第二节:派生类的构造过程:

派生类从基类继承来那么多的成员,怎么构造 怎么初始化?

class Base
{
public:
	Base(int data) :ma(data)
	{
		cout << "Base()" << endl;
	}
	~Base()
	{
		cout << "~Base()" << endl;
	}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :ma(data),mb(data)
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
private:
	int mb;
};

int main()
{
	Derive d(0);
	return 0;
}

程序出错:error C2512: “Base”: 没有合适的默认构造函数可用
C++的学习心得和知识总结 第六章(完)_第7张图片
原因分析:不允许以这样的方式 把从基类继承来的成员变量ma 初始化。(在派生类的构造函数初始化列表里面,把从基类继承来的成员变量ma进行直接的初始化,这是不允许的。)
从基类继承来的成员变量ma,只能去找基类自己的构造函数进行初始化。
C++的学习心得和知识总结 第六章(完)_第8张图片
在这里插入图片描述
定义一个派生类对象d,从基类继承来的成员的初始化调用基类的构造方法;然后派生类自己的成员由派生类的构造函数进行初始化。出作用域的时候,派生类部分先析构,调用派生类的析构函数;派生类对象的基类部分的析构则是调用基类的析构函数。
C++的学习心得和知识总结 第六章(完)_第9张图片
总结:
(1)从基类继承来的成员 的初始化是在派生类的构造函数的初始化列表里面:调用基类的构造函数进行初始化的。派生类的构造函数的初始化列表的执行顺序是在 函数体的执行之前。
(2)派生类自己特有的成员 的初始化是调用派生类构造函数完成的。
(派生类对象的使用过程)
(3)派生类对象使用结束 调用派生类的析构函数,释放派生类成员可能占有的外部资源(堆内存 文件之类的)。(这些释放都是写在析构函数之中的)
(4) 调用基类的析构函数,释放派生类对象内存中从基类继承来的成员可能占有的外部资源(堆内存 文件之类的)。(这些释放都是写在析构函数之中的)

第三节:成员方法的重载、隐藏、覆盖:

#include
using namespace std;

class Base
{
public:
	Base(int data) :ma(data)
	{
		cout << "Base()" << endl;
	}
	~Base()
	{
		cout << "~Base()" << endl;
	}
	void show()
	{
		cout << "Base::show()" << endl;
	}
	void show(int)
	{
		cout << "Base::show(int)" << endl;
	}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data)
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
private:
	int mb;
};

int main()
{
	Derive d(0);
	d.show();
	d.show(15);
	return 0;
}

C++的学习心得和知识总结 第六章(完)_第10张图片
Base类的show()、show(int)。派生类对象都可以调用。
C++的学习心得和知识总结 第六章(完)_第11张图片
现在给Derive 类添加一个不带整型参数的show(),在调用的时候。第一个肯定是调用派生类自己的这个不带整型参数的show() 如上图所示。但是第二个 却出错了。
出错原因分析: error C2660: “Derive::show”: 函数不接受 1 个参数
好像这个还是要调用派生类自己的这个不带整型参数的show(),然后参数类型不一样,出错。那么问题来了:它为什么不直接调用 从基类继承来的那个带整型参数的show(int)呢?既然基类和派生类里面的成员可以是相同的(作用域不同)。
C++的学习心得和知识总结 第六章(完)_第12张图片
重载关系只能在基类的一组方法 或者 一组派生类的方法。在基类和派生类之间谈重载是没有意义的(作用域不同)。

隐藏
在这里插入图片描述
如上上图所示:派生类的show()把基类的show()和show(int)都给隐藏了。派生类对象在调用(派生类与基类同名)的成员方法的时候,派生类的方法把基类的函数名字相同的都给隐藏了。所以不能通过派生类对象来直接调用基类的同名方法show(int)。如果此时仍旧要去调用那个基类的同名方法,则在方法前面加上基类的作用域。如下图所示:
C++的学习心得和知识总结 第六章(完)_第13张图片
所以说:
出错原因为:派生类里面的show()把从基类继承来的show()和show(int)都给隐藏了,此时的d.show(15); 调用的是自己show().而不是基类继承来的show(int)。
在这里插入图片描述
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
重点来了:以下是研究多态的基础。
C++的学习心得和知识总结 第六章(完)_第14张图片
通常也把继承结构叫做 从上(基类)到下(派生类)的结构
(1)把派生类对象 给 基类对象。(类型:从下到上的转化)可以的
C++的学习心得和知识总结 第六章(完)_第15张图片
C++的学习心得和知识总结 第六章(完)_第16张图片
讲一个通俗的例子:Base相当于 人 这个类;Derive类相当于 大学生 类。我现在要一个人,我给你一个大学生 当然美滋滋了。

(2)把基类对象 给 派生类对象。(类型:从上到下的转化)不可以的
C++的学习心得和知识总结 第六章(完)_第17张图片
基类对象到派生类对象 最多把派生类对象的基类部分给初始化了,但是派生类对象的派生类部分呢?

讲一个通俗的例子:Base相当于 人 这个类;Derive类相当于 大学生 类。我现在要一个会写C++语言的大学生,我给你一个 人 (很普通 很随意 ) 你怕是要让我凉凉!!!

(3)基类指针(或引用) 指向 派生类对象。(类型:从下到上的转化)可以的
C++的学习心得和知识总结 第六章(完)_第18张图片
因为派生类对象 由两部分组成:一部分是从基类继承来的成员,一部分是自己独有的成员。 当前指针是 Base*,可以指向 但是只能访问从基类继承来的成员。派生类独有的成员的不能访问到。因为类型Base* 已经限制了指针解引用(解释内存)的能力。
把 Derive* 给 Base* 是可以的。(在默认情况下:)因此Base* 的这个指针访问的成员都是从基类继承来的成员。派生类 的那个show()是绝对访问不了的。
在这里插入图片描述
但是在特殊情况下(类型强转)
C++的学习心得和知识总结 第六章(完)_第19张图片
(4)派生类指针(或引用) 指向 基类对象。(类型:从上到下的转化) NO
现在内存上是没有派生类对象的。
C++的学习心得和知识总结 第六章(完)_第20张图片
基类的对象内存 只有前面一段那么大,但是Derive* 指针类型绝对了 其内存解释能力。但这是在进行内存的非法访问,程序当然要崩溃的。

在特殊情况下(类型强转):但是非常不安全(有内存的非法访问)
C++的学习心得和知识总结 第六章(完)_第21张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++C++的学习心得和知识总结 第六章(完)_第22张图片
如上图所示:派生类指针指向 基类对象。 此时的指针是Derive* 第一个show()调用的是派生类的。此刻打印出来mb的值为 无效值。(因为这个实质上只是一个基类对象) 第二个show()则调用的是基类的show()方法。它这个虚假的派生类对象的基类部分的 成员变量ma的值 打印出来。但是(因为这个实质上只是一个基类对象)所以 打印的ma的值 就是基类对象构造时候 传入的1000 。
总结:
在这里插入图片描述
本节源代码如下:

#include
using namespace std;
class Base
{
public:
	Base(int data) :ma(data)
	{
		cout << "Base()" << endl;
	}
	~Base()
	{
		cout << "~Base()" << endl;
	}
	void show()
	{
		cout << "Base::show()" << " ma" << ma << endl;
	}
	void show(int)
	{
		cout << "Base::show(int)" << endl;
	}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data)
	{
		cout << "Derive()" << endl;
	}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
	void show()
	{
		cout << "Derive::show()" << " mb" << mb << endl;
	}
private:
	int mb;
};
int main()
{
	Derive d(0);
	Base b(1000);
	//这是派生类指针指向基类对象
	Derive* pd = (Derive*)&b;//做了类型强转 这是非常危险的。
	//内存的非法访问了
	pd->show();
	pd->Base::show();
	return 0;
}

第四节:虚函数、静态绑定和动态绑定:

本节非常重要

#include 
#include 
using namespace std;
class Base
{
public:
	Base(int data) :ma(data){cout << "Base()" << endl;}
	~Base(){cout << "~Base()" << endl;}
	void show(){cout << "Base::show()" << " ma:" << ma << endl;}
	void show(int){cout << "Base::show(int)" << endl;}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data){cout << "Derive()" << endl;}
	~Derive(){cout << "~Derive()" << endl;}
	void show(){cout << "Derive::show()" << " mb:" << mb << endl;}
private:
	int mb;
};
int main()
{
	Derive d(1024);
	Base* pb = &d;
	pb->show();
	pb->show(1025);
	
	cout << sizeof(Base) << " " << sizeof(Derive) << endl;
	cout << typeid(pb).name() << "||" << typeid(*pb).name() << endl;
	return 0;
}

C++的学习心得和知识总结 第六章(完)_第23张图片
基类指针指向派生类对象,这里所调用的两个方法都是从基类继承来的。调用不了派生类自己的show()。
静态(编译时期)绑定:函数的调用。
C++的学习心得和知识总结 第六章(完)_第24张图片
在编译时期(产生汇编码上),已经确定函数调用了 调用的是Base作用域下的show()。而且还把函数的地址给定了。(编译器在编译上面两句代码时,发现调用方法的指针是Base* 然后就会去基类里面 然后调用这个方法。)

C++作为静态类型的语言,定义变量一定要指定类型而且变量定义之后类型是不会再改变了。 所以pb的类型 Base* ,*pb就是Base(解引用)了。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
干货来了:虚函数
C++的学习心得和知识总结 第六章(完)_第25张图片
类里面定义虚函数了:
在这里插入图片描述
虚函数表里面放的都是一些整数:
(1)RTTI指针:(RTTI是运行时类型信息)这个指针指向的是一个常量字符串。或者说 类型字符串:这张表是哪个类型产生的,字符串存的就是什么类型。其实说是字符串不太合适,里面是个RTTI对象。
(2)偏移量:一般情况下都是0
(3)类中虚函数的地址
尽管程序中可能有很多类,每个类里面都有可能有虚函数,一个类型产生一个虚函数表。程序运行时,每一个类的虚函数表都会加载到内存的 只读数据区(平时的常量字符串也在这个区)。因此这张表只读不写。
C++的学习心得和知识总结 第六章(完)_第26张图片
vfptr(4字节)虚函数指针:指向这个类型的虚函数地址的 起始部分。同一类型的对象,其虚函数指针指向的都是同一张表。
C++的学习心得和知识总结 第六章(完)_第27张图片
多一个虚函数,对象内存里面还只是一个vfptr 但是在虚函数表里面就会增加一个 虚函数地址。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
在派生类的变化:
C++的学习心得和知识总结 第六章(完)_第28张图片
这两个函数的关系就是 覆盖 的关系。所谓的隐藏是隐藏的是派生类同名的成员把基类成员的作用域给隐藏了。 即使是 派生类的这个show()前面不加virtual,也会自动处理为 虚函数。那么派生类现在 也有虚函数了。

编译阶段,也会给这个Derive类 产生虚函数表,因为它从基类也继承来了虚函数。在其虚函数表里面(因为它从基类里面继承了两个虚函数)本身就放了这两个虚函数的地址,但是编译器发现派生类自己的show()和基类继承的show() 同名 同类型 同参数列表 所以是覆盖关系,重写。所谓覆盖:就是把这个虚函数表里面的虚函数的地址覆盖了。(不要基类的那个show()方法了,派生类把不带参数的show()重写了)
在这里插入图片描述
C++的学习心得和知识总结 第六章(完)_第29张图片
即:在派生类的虚函数表里面放 派生类自己重写的这个 show()方法地址。但是派生类并没有给继承基类的show(int)方法进行重写,所以虚函数表里面 放的还是基类的show(int)方法地址。现在派生类对象 也要多加一个虚函数指针,虚函数指针(指向的就是当前类对应的虚函数表)
C++的学习心得和知识总结 第六章(完)_第30张图片
(因为有虚函数,派生类的虚函数表里面本来放的是从基类继承来的虚函数地址,但是派生类又提供了同名的覆盖重写方法,于是就在派生的虚函数表中把从基类继承来的虚函数地址覆盖掉,是在虚函数表中的覆盖。
此刻在运行一下 刚才一模一样的main 函数。

在编译阶段:pb是Base* ,所以到Base类作用域里面 查看show()方法。
如果是普通函数,直接静态绑定。生成call Base::show(),调用的是Base作用域下的show()方法。
如果是虚函数,发现这个show()是虚函数,就进行动态绑定。
C++的学习心得和知识总结 第六章(完)_第31张图片
首先把pb指向的对象(派生类对象)的前4字节 (也就是vfptr 虚函数表的地址)放到eax寄存器里面。然后到虚函数表里面 取4字节(即 派生类的show()方法地址,此时它是虚函数),相当于得到了 虚函数的地址。最后call ecx调用。

最后的call ecx, 不知道最后要调用的是哪个函数。寄存器里面放的是最终从虚函数表里面取出来的虚函数地址。但是这是哪个虚函数的地址,只有在运行的时候,通过指令在虚函数表里面找到 哪个虚函数的地址就调用哪个虚函数,才知道寄存器里面放的是什么地址。

但是在编译阶段 生成的指令无法确定调用的是哪个函数的,所以我们把这种绑定叫做 动态绑定(运行时期的函数调用)。
C++的学习心得和知识总结 第六章(完)_第32张图片
对于 第二个show(int)一样。虚函数地址在虚函数表里面的先后顺序取决于其定义的顺序。
C++的学习心得和知识总结 第六章(完)_第33张图片
程序运行结果:(动态绑定是实现多态的基础,一定是虚函数的绑定
C++的学习心得和知识总结 第六章(完)_第34张图片
(变化1)现在第一个show(),访问的是派生类的show()。原因分析:通过动态绑定实现的。具体是:调用这个函数的时候,没有直接在指令上调用基类的show()。因为基类的show()是一个虚函数,发生了动态绑定。因为此时的指针指向的是一个派生类对象,最终访问的也是派生类的Vftable。在里面取出来的show()就是派生类自己重写 的show()了。
注:虽然下面的show(int)也是一个虚函数,但是派生类没有去重写,所以最后动态绑定 调用的还是基类的show(int)方法。这个两次运行没有改变。但是这个基类的show(int)也是从原 静态绑定变成了 动态绑定。
C++的学习心得和知识总结 第六章(完)_第35张图片
(变化2)但是最后的*pb 类型变成了Derive,原因分析:
RTTI类型:也是在虚函数表里面存的呢,指向的是派生类对象。因为派生类对象的虚函数指针指向的是 派生类的虚函数表。(通过访问对象的前4个字节,访问的肯定是派生类 类型的虚函数表。)访问其RTTI,因为这个表是派生类产生的,肯定RTTI类型信息就是 class Derive类型。不再是刚才的class Base:刚才的Base里面没有虚函数,所以访问的是 编译时已经确定的类型了。

总结:
两次运行对比:
C++的学习心得和知识总结 第六章(完)_第36张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
最后介绍 在Vftable里面的 偏移量 0的问题:
因为vfptr的优先级非常高,(在没有虚继承的情况下)它永远都在对象的前4字节的。就在内存的起始部分,偏移量就是0。

工具 ———>VS命令提示
Derive类的
C++的学习心得和知识总结 第六章(完)_第37张图片
Base类的
C++的学习心得和知识总结 第六章(完)_第38张图片
我们从上面两个图发现:
在虚函数表里面,虚函数地址的顺序有些问题:(见Derive类的那个图)
首先继承来了,后来重写 所以顺序是这样从上到下排列的。
在这里插入图片描述
以上工具的使用 请见本系列第三章:https://blog.csdn.net/weixin_43949535/article/details/94833943

第五节:虚析构函数:

本节:虚析构函数(析构函数是可以成为虚函数的)
首先想成为虚函数,那么函数地址是要记录在虚函数表中。找到虚函数表才能找到虚函数地址,虚函数表的地址存放在对象内存的vfptr指针里面。
在这里插入图片描述
在这里插入图片描述
构造函数不能成为虚函数,构造函数完成后 才有对象产生。
在这里插入图片描述
构造函数相当于给当前对象初始化的,构造函数若是发生多态则调用的是派生类的方法。但是派生类对象的构造,先调用的是基类的构造函数。可是在基类的构造调用的时候,此时派生类还没有构造呢。所以在基类的构造函数里面发生动态绑定调用的是派生类的方法(派生类还没有初始化呢)。
在这里插入图片描述
静态方法的调用不依赖对象啊。要想成为虚函数,那么函数地址是要记录在虚函数表中。访问的时候则需要对象的内存 获取虚函数表的地址 vfptr,然后去vftable里面取虚函数地址。静态方法不需要对象。所以不能 virtual+static 或者static+virtual

析构函数OK,析构函数调用的时候,对象是存在的

#include 
#include 
using namespace std;
class Base
{
public:
	Base(int data) :ma(data){cout << "Base()" << endl;}
	~Base(){cout << "~Base()" << endl;}
	void show(){cout << "call Base::show()" << endl;}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data){cout << "Derive()" << endl;}
	~Derive(){cout << "~Derive()" << endl;}
private:
	int mb;
};
int main()
{
	Derive d(1024);
	Base* pb = &d;
	pb->show();
	
	return 0;
}

pb是Base* 编译的时候,去Base类里面看show()方法是不是虚函数。
是普通方法,静态绑定。call Base::show()
虚函数则动态绑定,访问指针所指向对象的前四个字节(vfptr)。再间接访问Derive类的虚函数表。
d是栈上的对象,出作用域的时候 先调用析构函数析构自己部分;再调用基类析构函数析构其中基类部分所占用的外部资源。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
现在修改代码:

	Base* pb = new Derive(1024);//指向堆上new的对象
	pb->show();

pb的类型是Base*,此处的show()是一个普通方法,则是静态绑定。
*pb的类型 得看Base类里面有没有虚函数,没有则识别的是编译时期的类型Base。有则 *pb识别的就是RTTI类型,它指向的是派生类对象,最后访问的也是派生类的虚函数表,所以其类型是Derive ,也即动态绑定。
在这里插入图片描述
delete 第一步:调用对象的析构函数(对象使用完成,需要清理外部资源。这些代码都写在函数里面了);第二步:释放内存。
C++的学习心得和知识总结 第六章(完)_第39张图片
如上图所示:
new首先malloc开辟内存,然后调用相应的构造函数。这里定义的是一个派生类对象d,所以先Base() 后Derive()。一切OK。
但是出现问题了:派生类的析构函数没有调,内存泄漏了
C++的学习心得和知识总结 第六章(完)_第40张图片
如上图所示:现在相当于只调用了基类的析构函数,在第二步 free 把对象的一部分内存清理掉了。但是堆上呢?这个是要调用Derive的析构函数取清理的,可是没有调用。

为什么没有调用?
pb的类型是Base* 去Base:: 里面找到析构函数是个普通函数,对于析构函数的调用直接call Base::~Base(),(静态绑定)。对于派生类来说,其析构函数没有机会调用。

解决方法:把析构函数定义成为虚析构函数
C++的学习心得和知识总结 第六章(完)_第41张图片
源代码如下:

#include 
#include 
using namespace std;
class Base
{
public:
	Base(int data) :ma(data){cout << "Base()" << endl;}
	virtual~Base(){cout << "~Base()" << endl;}
	virtual void show(){cout << "call Base::show()" << endl;}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data)
	,ptr(new int(data)){cout << "Derive()" << endl;}
	~Derive()
	{
		delete ptr;
		cout << "~Derive()" << endl;
	}
private:
	int mb;
	int* ptr;
};
int main() 
{
	Base* pb = new Derive(1024);//指向堆上new的对象
	pb->show();
	delete pb;
	return 0;
}

派生类和基类 两个虚析构函数名字不一样:没办法一样(析构函数和类名一样)
那么现在 delete pb;的时候:
pb的类型是Base* 去Base:: 里面找到析构函数是个虚函数,对于析构函数的调用就是动态绑定。指针指向的是派生类对象,访问的时候也是访问Derive的虚函数表,里面肯定是派生类的虚析构函数地址。派生类的虚析构函数就会负责把派生类对象独有的那块析构了(包括:里面释放堆内存)。然后调用基类的析构函数把派生类对象基类的那块析构了。
现在Base的虚函数表里面放的是:
C++的学习心得和知识总结 第六章(完)_第42张图片
派生类 把这两个继承过来了。但是派生类有自己 的同名覆盖重写析构函数,于是
在这里插入图片描述
把那个覆盖了。结果一切正常。
C++的学习心得和知识总结 第六章(完)_第43张图片
总结:虚析构函数特定场景
在这里插入图片描述
指向的是栈上的派生类对象 或者 全局的对象,也不需要你去delete。而且对象出作用域,会做正确的析构的。
而这里面的问题是:delete pb;调用析构时由于 基类的析构函数是普通函数,直接静态绑定了。没有去再调用派生类的析构函数了。

第六节:动态绑定:

本节:虚函数 和 动态绑定

问题:虚函数的调用是不是一定是动态绑定? NO NO NO
在类的构造函数当中,调用虚函数,也是静态绑定。(构造函数里调用其他函数(虚函数)都不会发生动态绑定)

#include 
#include 
using namespace std;
class Base
{
public:
	Base(int data) :ma(data){cout << "Base()" << endl;}
	~Base(){cout << "~Base()" << endl;}
	virtual void show(){cout << "Base::show()" << endl;}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data){cout << "Derive()" << endl;}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main() 
{
	Base b(1024);
	Derive d(1025);

	b.show();
	d.show();
	return 0;
}

如上代码所示:在编译过程中,Base Derive两个类会产生虚函数表。两类定义的对象的内存中也都会多上一个vfptr,用来指向这个虚函数表。而且Base和Derive类中的show()此时是覆盖重写的关系(函数名 返回值 参数列表都是一样的),则Derive里面的show()也被处理为虚函数。

此时定义了两个对象,并且使用对象来调用方法。此时的两个show()应该都是虚函数,但是使用对象来调用虚函数方法,竟然出现下面问题:
C++的学习心得和知识总结 第六章(完)_第44张图片
使用对象来调用虚函数方法,都是直接 call 类的作用域 方法(静态绑定)。 而不是去访问对象的前4字节(虚函数指针),进而去虚函数表,获得虚函数地址(发生动态绑定)。
在这里插入图片描述
这里没有发生动态绑定的原因分析:没必要
为什么说是没必要呢?
因为即使发生了动态绑定(访问这个类的虚函数表,获得虚函数地址),可是虚函数表也是这个类型产生的,可最终调用的还是作用域下的这个show()方法。结果都一样,哪个对象调用的函数最终调用的还是这个类的函数。
C++的学习心得和知识总结 第六章(完)_第45张图片
用指针指向的时候,发生的都是动态绑定。
C++的学习心得和知识总结 第六章(完)_第46张图片
基类指针指向基类对象 和 基类指针指向派生类对象,发生的都是动态绑定。因为pb1 pb2 都只是指针而已。它指向哪个对象(就要访问哪个对象的虚函数指针,进而访问其虚函数表),就最终调用哪个对象的同名覆盖方法。单单是通过指针本身并不知道它要访问的是哪个方法,必须要动态绑定。才可以知道其所指向的对象的同名覆盖方法。
C++的学习心得和知识总结 第六章(完)_第47张图片
基类引用引用基类对象 和 基类引用引用派生类对象,发生的都是动态绑定。
C++的学习心得和知识总结 第六章(完)_第48张图片
(需要保证虚函数的调用 是由指针或者引用,才可以发生动态绑定。若虚函数是由对象进行调用的 则没必要动态绑定:因为就算是动态绑定,访问的也是这个类的方法)。
C++的学习心得和知识总结 第六章(完)_第49张图片
如上:派生类指针或者引用派生类对象,调用虚函数依旧是发生动态绑定。
C++的学习心得和知识总结 第六章(完)_第50张图片
如下图:派生类指针或者引用基类对象,调用虚函数依旧是发生动态绑定。原因分析:因为调用的类型是Derive* 或者引用,然后就会去Derive 类里面发现show()是一个虚函数(覆盖重写了)。既然是动态绑定,就要去这个指针或者引用的 对象(此时是基类对象b)的前4字节 vfptr,进而访问vftable。因为内存上只是一个基类对象b,所以这个vftable 也是基类的虚函数表。所以从里面取出来的虚函数地址,也只能是 Base::show() 和 Base::show()。
C++的学习心得和知识总结 第六章(完)_第51张图片
总结:
在这里插入图片描述
本节源代码:

#include 
#include 
using namespace std;
class Base
{
public:
	Base(int data) :ma(data){cout << "Base()" << endl;}
	~Base(){cout << "~Base()" << endl;}
	virtual void show(){cout << "Base::show()" << endl;}
private:
	int ma;
};
class Derive :public Base
{
public:
	Derive(int data) :Base(data),mb(data){cout << "Derive()" << endl;}
	~Derive()
	{
		cout << "~Derive()" << endl;
	}
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main() 
{
	Base b(1024);
	Derive d(1025);

	b.show();//静态绑定
	d.show();//静态绑定
	cout << "****************************************" << endl;
	Base* pb1 = &b;//基类指针指向基类对象 动态绑定
	pb1->show();
	Base* pb2 = &d;//基类指针指向派生类对象 动态绑定
	pb2->show();
	cout << "****************************************" << endl;
	Base& rb1 = b;//基类引用引用基类对象 动态绑定
	rb1.show();
	Base& rb2 = d; //基类引用引用派生类对象 动态绑定
	rb2.show();
	cout << "****************************************" << endl;
	Derive& rb3 = d;//派生类引用引用派生类对象 动态绑定
	rb3.show();
	Derive* rb4 = &d; //派生类指针指向派生类对象 动态绑定
	rb4->show();
	cout << "****************************************" << endl;
	//这下面 做的是非常危险的
	Derive& rb5 =(Derive&)b;//派生类引用引用基类对象 动态绑定
	rb5.show();
	Derive* rb6 = (Derive*)&b; //派生类指针指向基类对象 动态绑定
	rb6->show();
	cout << "****************************************" << endl;
	return 0;
}

第七节:多态:

本节重点:多态
C++的学习心得和知识总结 第六章(完)_第52张图片
函数重载:函数名相同,参数列表不同:类型或个数的不同(在同一个作用域下)。
一个函数名展现出来的不同的形态,最终调用的是哪个函数,这些是在编译时期确定好的函数版本。

模板里面可以定义模板参数(尤其是定义模板类型参数),可以使用模板写上一套代码,适用于任何类型。需要用哪个类型,用这个类型从模板实例化一下就行。而这个模板的实例化也是发生在编译阶段,编译阶段实例化产生 处理某一类型的函数代码之后 这个函数才可以进行编译的。所以模板也是编译时期的多态,虽然看起来只有一个函数,但是可以用在各种类型(只要用这个类型从模板实例化一下就行)。就算是没有去实例化,函数模板还可以进行 类型的实参推演。
C++的学习心得和知识总结 第六章(完)_第53张图片
看起来代码上,都是pbase->show(),基类指针调用同名覆盖重写方法。但是为什么可以展现出不同的结果呢?因为基类指针指向了不同的派生类对象,指向了哪个派生类对象就会调用哪个派生类对象的同名覆盖方法。

其原理是: 多态底层是通过动态绑定实现的,看起来代码上,都是pbase->show(),基类指针调用同名覆盖重写方法。(当然在这里,基类的show()它是一个虚函数,派生类里面的同名方法进行了 覆盖重写。随着基类指针指向了不同的派生类对象,指向了哪个派生类对象就会调用相应的派生类对象的同名覆盖方法。)既然是动态绑定,就要去这个基类指针所指向的对象的前4字节 vfptr,进而访问这个派生类对象的vftable,从虚函数表里面取出来的虚函数地址就是哪个派生类对象的同名覆盖重写方法了。
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

class Animal//基类
{
public:
	Animal(string name):_name(name){}

	virtual void bark(){}
protected:
	string _name;
};
//下面三个实体类
class Cat :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Cat(string name):Animal(name){}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小猫咪:喵喵喵" << endl;
	}
};
class Dog :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Dog(string name) :Animal(name) {}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小花狗:汪汪汪" << endl;
	}
};
class Person :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Person(string name) :Animal(name) {}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小白白:我爱你" << endl;
	}
};
//下面是API 接口,三种动物 所以需要定义三个接口
//把相应动物的实体类 对象传进去 然后调用
//这三个函数 属于重载关系
void bark(Cat& cat)
{
	cat.bark();
}
void bark(Dog& dog)
{
	dog.bark();
}
void bark(Person& baibai)
{
	baibai.bark();
}

int main()
{
	Cat cat("拉布拉多猫");
	Dog dog("中华田园犬");
	Person baibai("可爱的白白");

	bark(cat);
	bark(dog);
	bark(baibai);

	return 0;
}

运行结果:
C++的学习心得和知识总结 第六章(完)_第54张图片
是在全局定义bark(),具体调用 由对象的引用变量来完成调用的,所以这个是动态绑定。虚函数要发生动态绑定,首先构造函数里面调用虚函数是不会发生动态绑定的,其次用对象本身来调用虚函数也是不会发生动态绑定的。只有用指针或引用调用虚函数才会发生动态绑定的。
C++的学习心得和知识总结 第六章(完)_第55张图片
程序不太好的一点就是:这是一组API接口。随着添加更多的派生类对象,在这里需要再多添加一个API接口,用来接收用户新添加(的动物)的派生类对象的引用。当然在程序的实体类删除的时候,这个API接口也要随着删除。
C++的学习心得和知识总结 第六章(完)_第56张图片
这一组无法达到软件设计要求的“开-闭原则”(高内聚,低耦合)。
在这里插入图片描述
开闭原则:对需求更改的时候,原来的API接口不应该改变,只扩展新的代码即可。比如:现在添加 或者 删除一个动物实体类,原来的API接口不应该有所改变。

于是现在的问题就是:这么多类型不同的派生类对象,就是想写一个bark()方法,那么该用什么类型来接收呢?毕竟这几个API接口,里面的实现都是一样的。无非就是因为接收的不同类型的派生类对象而导致的重载。那么需要用什么类型可以去接收cat dog person这三种对象呢?
答:统一的用基类类型来接收派生类对象,用基类的指针或者引用都是可以的。
用引用的话:引用会自动的取实参地址。而指针则需要我们自己把这个地址写上,都是一样的。

class Animal//基类
{
public:
	Animal(string name):_name(name){}

	virtual void bark(){}
protected:
	string _name;
};
//下面三个实体类
class Cat :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Cat(string name):Animal(name){}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小猫咪:喵喵喵" << endl;
	}
};
class Dog :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Dog(string name) :Animal(name) {}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小花狗:汪汪汪" << endl;
	}
};
class Person :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Person(string name) :Animal(name) {}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小白白:我爱你" << endl;
	}
};
/*
//下面是API 接口,三种动物 所以需要定义三个接口
//把相应动物的实体类 对象传进去 然后调用
//这三个函数 属于重载关系
void bark(Cat& cat)
{
	cat.bark();
}
void bark(Dog& dog)
{
	dog.bark();
}
void bark(Person& baibai)
{
	baibai.bark();
}
*/
void bark(Animal* p)
{
	p->bark();
}

int main()
{
	Cat cat("拉布拉多猫");
	Dog dog("中华田园犬");
	Person baibai("可爱的白白");

	bark(&cat);
	bark(&dog);
	bark(&baibai);

	return 0;
}

此时的API接口 用一个基类的指针接收派生类对象的地址。在这里,无论传入任何类型的派生类对象,都是
在这里插入图片描述

void bark(Animal* p)
{
	p->bark();
}

基类指针调用bark()方法,在编译阶段,Animal类里面的bark()方法是一个虚函数。则进行动态绑定,基类指针调用同名覆盖重写方法。(当然在这里,基类的bark()它是一个虚函数,派生类里面的同名方法进行了 覆盖重写。随着基类指针指向了不同的派生类对象,指向了哪个派生类对象就会调用相应的派生类对象的同名覆盖方法。)既然是动态绑定,就要去这个基类指针所指向的三个不同的对象内存的前4字节 vfptr,进而访问这个派生类对象的vftable,从虚函数表里面取出来的虚函数地址就是哪个派生类的同名覆盖重写方法bark()了。
即:
C++的学习心得和知识总结 第六章(完)_第57张图片
此时 如果我们再去增加新的派生类,只需要从Animal类继承而来,并且重写bark()方法即可,不需要修改我们的API接口
总结:
C++的学习心得和知识总结 第六章(完)_第58张图片

第八节:抽象类:

C++的学习心得和知识总结 第六章(完)_第59张图片
(1)抽象类和普通类的区别:
抽象类不是用来 抽象某一个实体类型的,做的事情就两个(如上图所示:)。它不可以实例化对象,但是定义指针和引用还是可以的。

(2)一般是把什么类设计成抽象类? 基类
C++的学习心得和知识总结 第六章(完)_第60张图片
所以对于基类里面的方法 都实现成为纯虚函数就行了,给所有派生类保留统一的覆盖重写接口。(不用实现,也不知道怎么实现)

/*
给用户设计实现 在当前剩余油量的情况下还可以跑多远
当然不同的车,这个每一 升油跑的距离数是不一样的
*/
//车的抽象类   泛指所有品牌的车  不是抽象某个实体,也不知道怎么去抽象实体类型
class Car  
{
public:
	Car(string name,double oil):_name(name),leftoil(oil){}
	//获取在当前剩余油量的情况下还可以跑多远
	double getLeftMales()
	{
		return leftoil * getOneOilMile();//在这里发生了动态绑定
	}
	string getName()const 
	{
		return _name;
	}
	double getLeftOil()const
	{
		return leftoil;
	}
protected:
	string _name;//汽车品牌名
	double leftoil;//剩余油量
	//每一 升油跑的距离数是不一样的
	virtual double getOneOilMile() = 0;
	//所有就把这个 函数设计成纯虚函数,需要在具体的派生类里面 才可以得到这个数值
};
//三种实体类  车
class Bnze:public Car
{
public:
	Bnze(string name,double oil) :Car(name,oil) {}
	double getOneOilMile(){ return 10.0; }
};
class Audi :public Car
{
public:
	Audi(string name,double oil) :Car(name,oil) {}
	double getOneOilMile() { return 20.0; }
};
class BMW :public Car
{
public:
	BMW(string name,double oil) :Car(name,oil) {}
	double getOneOilMile() { return 30.0; }
};

//给外部提供一个统一的接口 用来获取汽车剩余路程数
//这里面的getLeftMales处 发生了静态绑定
void showLeftMales(Car& car)
{
	cout << car.getName()<<"在当前剩余"<<car.getLeftOil()
		<<"油的时候的剩余路程数是: " << car.getLeftMales() << endl;
}
int main()
{
	Bnze b1("奔驰", 100.0);
	Audi a("奥迪", 100.0);
	BMW b2("宝马", 100.0);

	showLeftMales(b1);
	showLeftMales(a);
	showLeftMales(b2);
	return 0;
}

运行结果:
C++的学习心得和知识总结 第六章(完)_第61张图片
总结:
C++的学习心得和知识总结 第六章(完)_第62张图片
这里虽然是 基类引用 引用派生类对象,但是这个基类里面的getLeftMales()方法是个普通方法,所以这里发生的是静态绑定(调用的是 call Car::getLeftMales)。
但是
C++的学习心得和知识总结 第六章(完)_第63张图片
这里的getOneOilMile()是一个虚函数,在这里发生了动态绑定。这个基类指针所指向的三个不同的对象内存的前4字节 vfptr,进而访问这个派生类对象的vftable,从虚函数表里面取出来的虚函数地址就是哪个派生类的同名覆盖重写方法getOneOilMile()了。

第九节:继承与多态 笔试题:

第一题:(腾讯的)

class Animal//基类
{
public:
	Animal(string name):_name(name){}

	virtual void bark()=0;
protected:
	string _name;
};
//下面三个实体类
class Cat :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Cat(string name):Animal(name){}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小猫咪:喵喵喵" << endl;
	}
};
class Dog :public Animal
{
public:
	//调用基类的构造函数,初始化从基类继承来的成员变量
	Dog(string name) :Animal(name) {}
	//对于派生类来说,需要对这个基类的虚函数进行重写
	void bark()//通过同名覆盖重写方法
	{
		cout << _name << "  小花狗:汪汪汪" << endl;
	}
};
int main()
{
	Animal* p1 = new Cat("猫");
	Animal* p2 = new Dog("狗");

	int* p11 = (int*)p1;
	int* p22 = (int*)p2;

	int temp = p11[0];//交换两块内存变量值
	p11[0] = p22[0];
	p22[0] = temp;
	 
	p1->bark();
	p2->bark();

	delete p1;
	delete p2;
	return 0;
}

C++的学习心得和知识总结 第六章(完)_第64张图片
C++的学习心得和知识总结 第六章(完)_第65张图片
这3句代码:把Cat 和 Dog的对象内存的前四个字节的值 进行了交换(虚函数指针交换了)。也即Cat对象的vfptr存的是Dog类的vftable的值。
C++的学习心得和知识总结 第六章(完)_第66张图片
第二题:

class Base
{
public:
	virtual void show(int i = 10)
	{
		cout << "call Base::show i:" << i << endl;
	}
};
class Derive : public Base
{
public:
	void show(int i = 20)
	{
		cout << "call Derive::show i:" << i << endl;
	}
};
int main()
{
	Base* p = new Derive(); // 虚析构函数
	/*
	push 0Ah => 函数调用,参数压栈是在编译时期就确定好的
	mov eax, dword ptr[p]
	mov ecx, dword ptr[eax]
	call ecx
	*/
	p->show(); // 动态绑定 p-> Derive vfptr -> Derive vftable
	delete p;

	return 0;
}

两个show()还是覆盖重写的关系,参数默认值的不同不影响。(函数名 返回值 形参列表:类型、个数)一样即可。且基类的show()是虚函数,则子类的show自动处理为虚函数。
覆盖重写:在派生类的对象内存所对应的虚函数表中不在存储 从基类继承来的虚函数show()的地址,而是用的是派生类的虚函数show()的地址。
基类指针指向派生类对象的时候,在delete指针的时候(一定要把派生类的析构函数调用到)。所以需要把基类的析构函数 处理为 虚析构函数,进行动态绑定。但在这里,这两个类产生的对象在析构的时候不需要做什么事情,所以使用默认的析构函数即可。

如果用基类指针指向堆上的派生类对象,而基类的析构函数自定义了,且派生类也自定义了析构函数,此时一定要把基类的析构函数 处理为 虚析构函数,进行动态绑定。

这里的基类指针调用show方法,Base作用域下的show是一个虚函数,所以这是动态绑定。此时需要看p指向的对象Derive,则此时在Derive对象的前4字节取出来的vfptr就是指向Derive类的vftable。在这里面放的是Derive 的show方法虚函数地址。
C++的学习心得和知识总结 第六章(完)_第67张图片
调用函数是正确的。但是 打印的默认值???????
为什么调用的是Derive 的show方法虚函数,打印的默认值却是 基类的默认值?
原因分析:
调用函数的过程中:先压参数,用户传入实参则压实参;没有传入则把形参默认值压栈。参数是一定要压栈的。

此时的动态绑定调用的是Derive 的show方法虚函数,这个过程是在运行时候发生的。但是在编译阶段,只能看到(p只是一个基类指针)Base::show。调用show方法,且这个指针类型为Base*,所以就去Base里面 找到这个方法的参数默认值是10 。所以如下:
C++的学习心得和知识总结 第六章(完)_第68张图片
(当然这里指的是调用show()并没有传入实参)在调用的时候,不管动态绑定(运行时)调用的是哪个函数,只要是show方法,其形参默认值 压栈就是10 ,和派生类的形参默认值20 没有什么关系。函数调用,参数压栈是在编译时期已经确定好的(毕竟已经生成指令了,确定了)。动态绑定(运行时)调用的是哪个对象的虚函数表里面的虚函数,然后进行的是call eax(函数调用)。也就是说 call eax之前,那个默认值10压栈已经做好了

在继承结构中,给基类 派生类的同名覆盖重写方法的形参默认值不同 是没有意义的。因为使用基类指针或者引用 指向派生类对象进行多态调用,派生类的同名覆盖重写方法的形参默认值20是永远用不到的。

第三题:

class Base
{
public:
	virtual void show()
	{
		cout << "call Base::show "<< endl;
	}
};
class Derive : public Base
{
private:
	void show()
	{
		cout << "call Derive::show "<< endl;
	}
};
int main()
{
	Base* p = new Derive();
	p->show();
	delete p;
	return 0;
}

派生类的show()成了私有的成员方法。还可以调用吗? 可以
C++的学习心得和知识总结 第六章(完)_第69张图片
原因分析:
C++的学习心得和知识总结 第六章(完)_第70张图片
成员方法的调用,不是public的,在外面是调用不了的。我们最终可以调用到这个show(),是因为指令上发生了动态绑定。运行时候访问指针所指对象的虚函数表,得到这个虚函数地址,直接call eax了。(拿了一个函数地址,直接去这个函数执行了)
C/C++代码在程序运行起来的时候,(成员变量、成员方法)内存都是可以访问的。关键就是这些访问限定符控制 在编译阶段用户书写的代码是否符合编译的规范。例如:在类外调用私有成员 是绝对不可以编译通过的。但是在编译阶段,编译器只可以看见 基类指针在调用方法,此时只可以看见Base::show()。所有的在编译阶段需要确定的和show()相关的参数 都是去看Base::show()。 因为Base::show()是一个公有的,所以编译成功通过。至于最终调用的是Base::show() 还是Derive::show()那要看生成的指令是 静态绑定(call Base::show())还是动态绑定(call ecx)。

动态绑定(call ecx)只有在运行时候才可以知道的,运行的时候 找一个虚函数地址放到ecx寄存器,然后从寄存器取函数地址 进行调用。编译阶段无论如何,编译器看这个show()能不能进行调用,能不能进行编译通过 只能 看Base::show()。因为p的类型是Base* 。即使这个Derive::show()是个私有的,编译通过(编译器不看它)。
访问权限 形参默认值的确定都是发生在编译阶段。至于能调用到Derive::show是在运行时期确定的
同理:
C++的学习心得和知识总结 第六章(完)_第71张图片
基类指针指向了派生类对象,因为p的类型是Base* ,在编译阶段,编译器只可以看见 基类指针在调用方法,此时只可以看见Base::show()。关键就是这些访问限定符控制 在编译阶段用户书写的代码是否符合编译的规范。Base::show()是私有的

所以 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋 滚蛋

我写博客写了一整天了,滴水未进 唉 想打人。
好好学习 ,等下吃个好的。good 学习

C++的学习心得和知识总结 第六章(完)_第72张图片
(当然,对于此时的代码 把p类型 改为Derive* (派生类指针指向派生类对象)就可以 当然这些都是废话了)在编译阶段,编译器去看p的类型是Derive* ,在编译阶段,编译器只可以看见 派生类指针在调用方法,此时只可以看见(Derive类里面)Derive::show()。而这个Derive::show()是公有的,所以可以通过。同时这个Derive::show()是个虚函数,所以动态绑定。当然最终调用的时候也是Derive::show()。
什么东西需要在编译时期确定好,什么东西需要延迟到运行时期来完成?
第四题

在构造函数里面,不访问虚函数表 不发生多态。构造函数完成之后,才有对象生成。

class Base
{
public:
	Base()
	{
		cout << "call Base()" << endl;
		clear();
	}
	void clear()
	{// 把这个指针指向的sizeof(*this)这么大一块内存 置为0
		memset(this, 0, sizeof(*this));//就是Base的大小
	}
	virtual void show()
	{
		cout << "call Base::show() "<< endl;
	}
};
class Derive : public Base
{
public:
	Derive()
	{
		cout << "call Derive()" << endl;
	}
	void show()
	{
		cout << "call Derive::show() "<< endl;
	}
};
int main()
{
	Base* pb1 = new Base();//基类指针指向基类对象
	pb1->show();
	delete pb1;

	Base* pb2 = new Derive();//基类指针指向派生类对象
	pb2->show();
	delete pb2;
	return 0;
}

这两个都是动态绑定, 都是通过指针来调用方法。

问题:程序是否可以正确运行?不能的话请分析原因。
C++的学习心得和知识总结 第六章(完)_第73张图片
第一段代码不能执行的原因:如下图 基类的对象内存显示如下(内存里面只有一个vfptr 指向vftable。但是接下来调用clear函数,把Base对象的内存全部置为0,vfptr也变成了00000000. 在进行动态绑定的时候,是要从vfptr里面取出虚函数表的地址。当然是 访问不了。0地址不能读不能写。所以调用出错)
C++的学习心得和知识总结 第六章(完)_第74张图片
C++的学习心得和知识总结 第六章(完)_第75张图片
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
C++的学习心得和知识总结 第六章(完)_第76张图片

第二段代码正确运行的原因:
C++的学习心得和知识总结 第六章(完)_第77张图片
C++的学习心得和知识总结 第六章(完)_第78张图片
把虚函数表地址写到 vfptr里面是在 栈帧开辟并初始化完成之后 首句源代码运行之前 之间进行的(编译时期 构造函数)。类里面只要有虚函数(继承过来的也算),每一个构造函数都会做这件事情,把Derive类的虚函数表的地址放到vfptr里面。

在new一个 Derive对象的时候,首先会调用Base()来构造从基类继承的部分:把Base类的虚函数表的地址放到vfptr里面。 然后接着运行高级源代码:clear()把内存里面的刚才的vfptr的值置为0 ,此时的虚函数指针没有指向任何东西。接下来然后调用Derive()来构造派生类独有的部分:把Derive类的虚函数表的地址放到vfptr里面。 此时的虚函数指针便指向了Derive类的虚函数表。
C++的学习心得和知识总结 第六章(完)_第79张图片
最后在pb2->show(); 的时候,因为基类指针 指向派生类对象,这里的基类指针调用show方法,Base作用域下的show是一个虚函数,所以这是动态绑定。此时需要看pb2指向的对象Derive,则此时在Derive对象的前4字节取出来的vfptr就是指向Derive类的vftable。在这里面放的是Derive 的show方法虚函数地址,所以可以正确调用。

你可能感兴趣的:(C++的学习心得和知识总结)