zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++ 继承与多态详解

C++继承 详解 多态
2023-09-14 09:15:35 时间

PS1:C++的I/O即输入 输出不是很重要,只需要掌握operator>> 和 operator<<即可,这两个函数用在cout cin一个自定义对象的时候,我们必须给这个对象提供operator>> 和 operator<<。
PS2:C++的异常也不是很重要
PS3:类跟类之间的3种关系:
1 )代理:通过容器适配器(stack queue priority_queue)这3个类所有的方法都是调用底层容器的方法。特点: 当前代理类的所有方法调用被代理类的方法。我们使用的是代理类,被代理类我们不会直接使用。即:被代理类的接口的功能子集。被代理以后只公布了被代理类的一部分接口。而这代理类的接口还是被代理类的一部分接口。
2 )组合:两个类满足 —是---的一部分的关系。特点:用一个类定义的对象作为另一个类的成员变量。
3 )继承:满足----是—的一种的关系。

继承与多态:

继承的本质:代码复用

class Base                      // Base是基类
{
public:
	int ma;
protected:
	int mb;
private:
	int mc;
};
                // Derive是派生类,继承到派生类里面的成员访问限定 <= 继承方式
class Derive : public Base
{
public:
	int md;
protected:
	int me;
private:
	int mf;
};
//Base派生了Derive ,Derive 继承了Base。
//访问限定有三种,所以继承方式 3种。
/*
class Derive : Base 没有访问限定符时 默认私有继承。
在没有继承的情况下,Derive 类描述的实体的类型若也想有 属性ma mb mc,
则需要给Derive 类重写属性ma mb mc。当然这是没必要的,因为可以直接从Base类继承而来。
*/

Question1:派生类的内存格局,都继承了基类的哪些东西?
在画一个Base类对象时,只用在内存里放ma mb mc 即可:
在这里插入图片描述
而在Derive 类内存分两部分:起始部分是基类数据 然后才是自己的数据:
在这里插入图片描述
这时如果要求计算sizeof(Derive)=24字节。

注意1
在继承中:还把作用域继承过来。
即使派生类自己也定义了一个 ma,程序依旧OK,并不和基类的ma冲突。原因:继承了基类的作用域。派生类也继承了基类的方法,但是方法不占内存。派生类继承了基类除 构造函数 析构函数以外的所有方法;和全部数据。这两个函数只有在自己类才有用。所以说在派生类里面可以写和基类 同名的成员 成员变量 成员方法。

Question2:继承来的东西,访问限定是什么?
在这里插入图片描述
注意2:public继承下,基类的私有成员在派生类是不可见的,但是继承过来了。除了自己和友元可以访问自己的私有东西,派生类也不可以访问。
(1)Derive是派生类,继承到派生类里面的成员访问限定 <= 继承方式。所以说无论什么继承方式,基类的私有成员都是可继承 不可访问。
(2)在外部main想访问类对象的成员,只能访问公有继承基类的public成员。若对派生类和外部都限定 写成private继承;若是限定外部,而对派生类可访问 写成protected继承。

Question3:派生类怎么初始化从基类继承来的成员呢?
基类部分 数据先构造(即使你在初始化列表里把Base(data)写在派生类数据构造之后),它只能通过调用基类相应的构造函数进行初始化,派生类只会初始化自己的成员。
注意3:如果我没在初始化列表构造基类数据,那么它会调用基类的默认构造函数,然而我在基类里已经写了 构造函数,则编译器会报错。所以必须得给基类部分的构造 必须得有。

class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
protected:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) :Base(ma), mb(data){cout << "Derive()" << endl;}//初始化列表指定基类构造方式
	~Derive(){ cout << "~Derive()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(10);//派生类
	return 0;
}

定义了一个派生类:顺序如下
在这里插入图片描述
注意4

class Base// Base是基类
{
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
基类的成员对象
};
class Derive : public Base
{
	Derive(int data) :Base(ma), mb(data){cout << "Derive()" << endl;}
	~Derive(){ cout << "~Derive()" << endl; }
派生类的成员对象
};
int main()
{
	Derive d;派生类定义一个派生类对象
}
***************************************************************
这个派生类对象的构造顺序:
1 基类的成员对象的构造函数
2 基类的构造函数
3 派生类的成员对象的构造函数
4 派生类的构造函数
***************************************************************
析构的顺序:
1 派生类自己
2 派生类的成员对象
3 基类自己
4 基类的成员对象的析构函数
***************************************************************

Question4:基类和派生类同名成员函数之间的关系?重载,隐藏,覆盖
重载:
函数名相同 参数列表不同 作用域相同 =》 重载函数

隐藏:
在基类和派生类当中 函数名相同 =》 隐藏
(暂且说:派生类的成员把基类的同名成员给隐藏了)

覆盖/重写:
基类和派生类当中 函数的返回值,函数名,参数列表都相同,而且基类的
函数是virtual虚函数,那么称作覆盖关系

class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
	void show() { cout << "Base::show()" << endl; }//这两个是重载关系。      这两个和下面那个是隐藏关系
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};

class Derive : public Base
{
public:
	Derive(int data) :Base(ma), mb(data){cout << "Derive()" << endl;}
	~Derive(){ cout << "~Derive()" << endl; }
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Derive d(10);//派生类
	d.show();//调用派生类的show()
	return 0;
}

在这里插入图片描述
若是想要调用从基类继承来的show(),只需要
d.Base::show();
在这里插入图片描述
注意5
若是派生类自己没有一个带整型参数的show(),但是可以从基类继承来的show(int),那么d.show(10);是可以的吗?
不可以!!!!!!!!!!
因为:派生类里面同名的show()把基类的两个同名的show()都给隐藏了。派生类的show()会被编译器优先调用,但是这个show()没有参数!!!
在这里插入图片描述
若是我真的想要调用从基类继承来的show(int),只需要
d.Base::show(10);
在这里插入图片描述
在这里插入图片描述
若是派生类里面没有show(),那么d.show(10);则只能在基类继承来的找到,然后就可以调用了。

注意6:成员变量当然 也是同成员方法的。(前提:同名的,要用就得加基类作用域)

重写/覆盖
基类和派生类当中 函数的返回值,函数名,参数列表都相同,而且基类的
函数是virtual虚函数,那么称作覆盖关系

class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }//虚函数
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(ma), mb(data){cout << "Derive()" << endl;}
	~Derive(){ cout << "~Derive()" << endl; }
	void show() { cout << "Derive::show()" << endl; }//自动处理为 虚函数
private:
	int mb;
};
int main()
{
	Derive d(10);
	Base *p = &d;
	p->show();//在编译阶段已经确定了
//	mov         ecx,dword ptr [p]  
//	call        Base::show(0214F1h)
	cout << typeid(p).name() << endl;//class Base*
	cout << typeid(*p).name() << endl;
	return 0;
}

未在基类show()前加virtual
在这里插入图片描述
在基类show()前加virtua
在这里插入图片描述
在这里插入图片描述
只要类里面有虚函数的时候,就会多一个虚函数指针(内存里 多4字节)。虚函数指针永远在对象的前四个字节里,虚函数指针指向的是虚函数表:放的是虚函数地址。这个虚函数表(编译阶段就产生了)在运行阶段加载到内存的只读数据段(全局静态内存区)里面。其生命周期是程序开始到程序结束。
**注意:**一个类型对应一张表,相同类型的对象的虚函数指针都指向同一个虚函数表。对象是不同的,虚函数指针也是不同的。
在这里插入图片描述
函数能成为虚函数有两个前提:
1 能取地址:虚函数表里面记录虚函数的地址
2 怎么找到虚函数:通过虚函数指针vfptr:在对象里面。所以还得先有对象。
构造函数不能作为虚函数:没有对象呢
析构函数可以成为虚函数。
内联函数不能成为虚函数:没地址
stasic成员函数不能成为虚函数:没对象。静态方法调用的时候不需要依赖对象

而且如果在Base类在show(int)前加virtual,和加一个virtual没变化。只要有一个虚函数就会有变化,但是这个变化和虚函数的个数没关系。

*p的类型变化和p->show()变化分析:
(1)最开始没有虚函数的时候,编译器在编译 p->show(); 时只能看p的类型,*p是Base类,而且在这个Base类里show只是个普通函数,所以直接 编译成call Base::show()。编译期间就已经确定调用的是show()。因此 这样的方式被称为是 编译时绑定。
(2)但是在现在有virtual时,**p->show();**在这个Base类里show是个虚函数,就不能直接编译,得去虚函数表里取它的地址。

Derive d(10);
Base *p = &d;

现在这个指针p指向一个派生类对象,那么汇编如下:
mov eax ,dword ptr[p] //前四个字节 虚函数表的地址。
mov ecx , dword ptr[eax] //eax是表的地址,那么就是这个虚函数的地址,放到ecx
call ecx // 这是变成这个句子了。
注意: 这样的话,编译期间能知道ecx里面放的是谁的地址吗?所以只有在运行时,才可以知道在虚函数表里面取到的地址是这个虚函数的地址。因此 这样的方式被称为是 运行时绑定。
此时 派生类提供了一个同名覆盖函数,因为p指向的是派生类对象,派生类的虚函数指针指向派生类的虚函数表。这个表里面放的是这个同名覆盖函数的地址,虽然这个指针是基类指针,但是通过p->show(); 调用的是派生类的show函数。因此这个过程称为运行时多态
在这里插入图片描述
*cout << typeid(p).name() << endl; 这时基类里面有虚函数了,指针指向对象类型是RTTI(运行时)类型信息。打印它所指向对象的类型即:访问虚函数表里面的RTTI信息。
重要提示 :

Base &b=d;//引用
cout<<typeid(b).name()<<endl;
/*
因为现在对象d有虚函数,所以有虚函数指针。所以在打印类型的时候,要访问虚函数表的RTTI信息。即:class Derive。
这所有的变化都是由于 虚函数指针 虚函数表引起的。
*/

虚函数表是同类型对象所共享的。对于派生类的表,因为也在基类继承来一个虚函数指针 虚函数表,那么在这个派生类表里面放什么东西 已经在编译期间确定了。若派生类提供同名覆盖函数,则在这个表里面 把基类的虚函数覆盖掉了。如果没有提供,则使用基类继承过来的虚函数。
在这里插入图片描述

################################################################
Question5:C++支持的四种类型强转?(为了提高代码的安全性)
在C语言里
int p;charq;
q=(char*)p;

1 )const_cast : 去掉对象的const属性的(把有const属性的强转为无const的)
用法:
const int a = 10;
int p = (int)&a;
int p = const_cast<int>(&a);

2 )static_cast:编译器认为可以支持的强转, 安全性略高些
用法:
int p;charq;
q=static_cast<char*>§;//类模板 类模板使用的时候必须写类型

3 )reinterpret_cast <=> (Type):类似C的强转 无条件强转
用法:
int p=NULL;charq=NULL;
q=reinterpret_cast<char*>§;

4 )dynamic_cast : 识别RTTI信息的类型转换 run-time type information

Question6: 继承结构中,默认支持下到上的类型转换 (派->基) =》赋值
》》》 基类对象 =》 派生类对象 no
》》》 派生类对象 =》 基类对象 yes(把派生类对象从基类继承来的基类部分赋给基类对象)

》》》 基类指针/引用 =》 派生类对象 yes 指针的时候(基类----》派生类)
》》》 派生类指针/引用 =》 基类对象 no
前两种代码如下:

class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
	void show() { cout << "Base::show()" << endl; }//这两个是重载关系。      这两个和下面那个是隐藏关系
	void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(ma), mb(data){cout << "Derive()" << endl;}
	~Derive(){ cout << "~Derive()" << endl; }
	void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Base b(10);
	Derive d(20);
	//d = b;//基类对象  赋给  派生类对象    no
	//b = d;//派生类对象  =》  基类对象   yes
	return 0;
}

运行结果如下:
在这里插入图片描述
第三种: 基类指针/引用 =》 派生类对象 yes
Base b(10);
Derive d(20);
Base *p = &d;

p解引用只能访问派生类对象从基类继承来的基类部分,其余无法访问。

int main()
{
	Base b(10);
	Derive d(20);
	Base *p = &d;// 基类指针/引用 =》 派生类对象   yes
	p->show();//Base::show()
	return 0;
}

运行结果:
在这里插入图片描述
因为:指针的类型指示了Base,解引用只能访问派生类对象从基类继承来的成员,派生类自己的成员不能访问。一切都是由指针的类型所决定的。

第四种:》》》 派生类指针/引用 =》 基类对象 no
在这里插入图片描述
Question7:虚函数 多态(见上面)
静态(编译阶段)的多态:函数重载和模板(任意类型实例化,而得到相同功能的代码)
动态(运行阶段才绑定)的多态:虚函数

Question8:什么情况下会产生多态的编译/调用?

#include"headfile.h"
using namespace std;
class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }//虚函数
	virtual void show(int i) { cout << "Base::show(int)" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(ma), mb(data){cout << "Derive()" << endl;}
	~Derive(){ cout << "~Derive()" << endl; }
	virtual void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	Base b(10);
	Derive d(20);

	b.show();//基类对象调      这两个靠对象本身调,在编译期间就确定的函数调用    Base::show()
	d.show();//派生类对象调                                                      Derive::show()

	Base *ptr1 = &b;//基类指针指向 基类对象
	ptr1->show();//                                                              Base::show()
	Base *ptr2=&d;//基类指针指向 派生类对象(是多态)
	ptr2->show();//                                                              Derive::show()

	Base &btr1 = b;//基类引用 引用基类对象
	btr1.show();//                                                               Base::show()
	Base &btr2=d;//基类引用 引用派生类对象
	btr2.show();//                                                               Derive::show()

	Derive *ptr3 = &d;//派生类指针指向 派生类对象
	ptr3->show();//                                                              Derive::show()
	Derive &btr3=d;//派生类引用  指向派生类对象
	btr3.show();//                                                               Derive::show()
	return 0;
}

总结:只要是使用指针 引用都会发生多态调用。使用对象本身调用 不是多态。
反汇编如下:
1 在这里插入图片描述
2 在这里插入图片描述
3 在这里插入图片描述
4 在这里插入图片描述
5 在这里插入图片描述
6 在这里插入图片描述
7 在这里插入图片描述
注意:

Base *p=&d;
p->show(10);//基类指针指向了 派生类对象。调用有参数的show(int)。

这个show(int) 在基类是虚函数
1 如果派生类没有这样的show(int),那么调用的是基类的show(int)。此时也发生了多态。
2 如果派生类有 同名覆盖函数,那么调用的是派生类的show(int) 。因为p指向的是派生类对象,派生类虚函数指针 指向的是派生类的虚函数表。
3 派生类指针不能指向基类对象。
但是我可以对它进行类型强转:Derive *pd = static_cast<Derive*>(&b);
派生类指针指向一个派生类那么大的对象,但是内存里只有基类这么大的内存。此时
pd->show(); //调用的方式是多态的,但是调用的是 Base::show()。因为它所使用的虚函数表是基类的虚函数表,只能访问基类的虚函数地址。

Question 9: 纯虚函数以及 抽象类

基类在设计上,是成派生类共有属性的集合。这样可以代码复用,继承即可。这个时候的基类不代表任何实体,所以无需实现它的方法。
抽象类和普通类最大的区别: 不代表任何实体 不能定义对象,但是可以定义指针和引用。

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 << " bark:喵喵!" << endl; }
};
class Dog : public Animal
{
public:
	Dog(string name) :Animal(name) {}
	void bark() { cout << _name << " bark:旺旺!" << endl; }
};

// 全局方法:听动物怎么叫的。可是动物各有不同,怎么写形参呢?
void ListenBark(Animal *p)//如何写一个形参接收 各种各样不同的派生类对象呢?         基类指针或引用
{
	p->bark();//这个调用的是指针p的bark()  指向派生类对象的bark                (发生了多态)
}

int main()
{
	Animal *p1 = new Cat("猫");
	Animal *p2 = new Dog("二哈");
	ListenBark(p1);//这个animal里面的bark是个纯虚函数,没有实现。
	ListenBark(p2);

	int *p11 = (int*)p1;//这一段 交换了虚函数指针 指向     虚函数表换了
	int *p22 = (int*)p2;
	int tmp = p11[0];
	p11[0] = p22[0];
	p22[0] = tmp;
	ListenBark(p1);//这个animal里面的bark是个纯虚函数,没有实现。
	ListenBark(p2);

	cout << typeid(*p1).name() << endl;//class Dog

	Cat c("mimi");
	cout << sizeof(c) << endl;                       //32     28+4
	cout << sizeof(string) << endl;                //28
	return 0;
}
注1:抽象类在被继承过来后,一定要把抽象类的纯虚函数要实现了,不然的话 这个派生类自己就会成为一个 抽象类(因为它继承了 纯虚函数)
注2 :派生类可以访问基类的 保护成员

在这里插入图片描述
Question 10: 继承与多态的常见笔试题分析:
笔试题1:

class Base// Base是基类
{
public:
	Base(int a) :ma(a){}
	void show() { cout << "Base::show()" << endl; }//虚函数
	virtual void show(int i) { cout << "Base::show(int)" << endl; }//虚函数
	void operator delete(void *ptr)
	{
		cout << "operator delete address:" << ptr << endl;
		free(ptr);
	}
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data){}
	virtual void show() { cout << "Derive::show()" << endl; }
	void* operator new(size_t size)//new运算符重载  
	{
		void *p = NULL;
		if ((p = malloc(size)) == NULL)
		{
			throw "";
		}
		cout << "operator new address:" << p << endl;
		return p;
	}
private:
	int mb;
};
int main()
{
	Base*p = new Derive(10);//用基类指针指向派生类对象时,指针里面放的是派生类基类部分数据地址
	cout << "p:" << p << endl;
	p->show();
	delete p;
	return 0;
}
Q1:p->show();是静态还是动态绑定?即是否发生多态?
A:p的类型是Base*, 编译器只能看到Base里的show ,但是他只是普通函数,于是是静态绑定
Q2:派生类对象的内存布局?
A:

在这里插入图片描述
在这里插入图片描述
注: 不可以搞个基类是普通函数,派生类是虚函数然后使用多态。因为这样的话,这样的代码会导致派生类的内存起始部分 放的不是基类的数据。如果用基类指针指向派生类对象,编译器在编译时 一定会给基类指针放派生类基类部分的起始地址。如下代码及结果显示:

class Base// Base是基类
{
public:
	Base(int a) :ma(a){}
	void show() { cout << "Base::show()" << endl; }//虚函数
	//virtual void show(int i) { cout << "Base::show(int)" << endl; }//虚函数
	void operator delete(void *ptr)
	{
		cout << "operator delete address:" << ptr << endl;
		free(ptr);
	}
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data){}
	virtual void show() { cout << "Derive::show()" << endl; }
	void* operator new(size_t size)//new运算符重载  
	{
		void *p = NULL;
		if ((p = malloc(size)) == NULL)
		{
			throw "";
		}
		cout << "operator new address:" << p << endl;
		return p;
	}
private:
	int mb;
};
int main()
{
	Base*p = new Derive(10);//用基类指针指向派生类对象时,指针里面放的是派生类基类部分数据地址
	cout << "p:" << p << endl;
	p->show();
	//p = (Base*)((int)p - 4);
	delete p;
	return 0;
}

在这里插入图片描述
原因分析:
此时的派生类对象的内存布局如下:
在这里插入图片描述
vfptr的地址是007DA260
用基类指针指向派生类对象,编译器在编译时 一定会给基类指针放派生类基类部分的起始地址 p为007DA264。两者相差 4个字节即vfptr 。但是最后delete的时候,并没有从头(007DA260)开始而是从007DA264 开始。这样的话,vfptr没有释放,故而出错。
这样的话可以进行强转:

Base*p = new Derive(10);//用基类指针指向派生类对象时,指针里面放的是派生类基类部分数据地址
	cout << "p:" << p << endl;
	p->show();
	p = (Base*)((int)p - 4);                   //强转
	delete p;

在这里插入图片描述
<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	~Base(){ cout << "~Base()" << endl; }
	virtual void show() { cout << "Base::show()" << endl; }//虚函数
protected:
	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*p = new Derive(10);//用基类指针指向派生类对象时,指针里面放的是派生类基类部分数据地址
	p->show();//动态绑定
	delete p;
	return 0;
}

结果如下:
在这里插入图片描述
没有析构 Derive类 ,即派生类的析构函数没调用

如果在 delete (Derive)p;一切正常 ok :*
原因分析:因为p的类型就是 Base*。在new派生类对象时,会自动调用基类的构造和派生类的构造;然后在delete指针时,由于p只是个基类指针类型,只会调用基类的析构函数,而派生类自己的那部分无法析构,所以析构函数要写成 虚函数。

delete p;//做两件事情:调用析构函数 释放内存
在调用析构函数时,~Base()是个普通函数,直接静态绑定。
即:call Base::~Base()
而我们的本意是:想让p指向谁就调用谁的析构函数
所以 只能用于动态绑定。
故而 用上虚析构函数
代码如下:

class Base// Base是基类
{
public:
	Base(int a) :ma(a){ cout << "Base()" << endl; }
	virtual ~Base(){ cout << "~Base()" << endl; }            //虚析构函数
	virtual void show() { cout << "Base::show()" << endl; }//虚函数
protected:
	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*p = new Derive(10);//用基类指针指向派生类对象时,指针里面放的是派生类基类部分数据地址
	p->show();//动态绑定
	delete p;//调用析构函数时也发生了多态。
	return 0;
}
**注:基类的析构函数是虚函数,那么派生类的析构函数自动成为 虚函数。
派生类对象从基类继承的虚函数表里是基类的虚析构函数地址。派生类也提供析构函数的同名覆盖方法,虽然不是同名,但是没法选择。
那么在派生类的虚函数表里面 用派生类的析构函数的地址把基类的析构函数的地址覆盖了。**

运行结果:
在这里插入图片描述
重要问题: 什么时候基类的析构函数需要写成虚函数?
答:不仅仅是在 基类指针指向派生类对象时,而是基类指针指向 堆上 内存时,后面需要写 delete时。
<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

class Base
{
public:
	Base(int a) :ma(a)
	{
		cout << "Base()" << endl;
		clear();
	}
	void clear() { memset(this, 0, sizeof(*this)); }//把基类部分
	virtual void show() { cout << "Base::show()" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data)
	{
		cout << "Derive()" << endl;
	}
	void show() { cout << "Derive::show()" << endl << "ma:" << ma << " " << "mb:" << mb << endl; }
private:
	int mb;
};
int main()
{
	//Base *p = new Base(5);//基类指针指向基类对象
	//p->show();//动态绑定   不能正确调用show(),挂掉了:基类构造调用clear,clear把整个对象的前4个字节清空,又因为是动态绑定 访问要找虚函数指针,间接访问虚函数表
	//delete p;

	Base *p = new Derive(20);//基类指针指向派生类对象  // 问题1:这里为什么没挂掉
	p->show();//基类指针调用派生类与基类的同名覆盖函数 可以正确调用
	delete p; //问题2:这里为什么不调用派生类的析构函数
	/*派生类对象的构造,先构造其基类部分(通过调用基类的构造函数),clear把整个对象的前8个字节(从基类继承的vfptr 和 ma)清空。vfptr确实是从基类继承而来,但是这个vfptr指向的不是基类的虚函数表,而是派生类的虚函数表。虚函数表是在什么时候写入的?如果有虚函数,当前对象有虚函数指针,那么在{和函数第一句代码间的汇编之后 就是把当前类型的vftable 写在 vfptr里面。派生类的构造也是如此。 
	这里没挂掉的原因分析:new 派生类对象的构造,先构造其基类部分(通过调用基类的构造函数),把虚函数表的地址写到虚函数指针里面,但是clear把整个对象的前8个字节(从基类继承的vfptr 和 ma)清空。基类部分结束后,到派生类部分(继承结构中,每一层构造函数都会虚函数表的地址写到vfptr里面。就是为了 指向什么对象,虚函数指针就得存这个对象的虚函数表)这样的话,这个派生类对象的前四个字节(虚函数指针)就不为空(派生类构造重新写了)。
	  什么类型放什么虚函数表。
	结果:ma 是0,mb 20。
	*/

	return 0;
}

<<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
在构造函数里面,对象未生成;在析构函数里面,对象正在死亡。所以在两者里面调用虚函数的时候,都不会发生多态调用,全是静态绑定。因为有了对象才能找到vfptr,也只有vfptr才能找到vftable,有了vftable才能找到虚函数的地址。对象的生存周期在构造 和 析构之间
<<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
多态是怎么编译的? 首先从对象的前4个字节得到虚函数指针的值,即虚函数表的地址。把虚函数表里面的某4个字节放到 eax里面,即:虚函数地址。最后call eax

mov ecx ,dword ptr[this]
mov eax ,dword ptr[ecx]
call ecx
<<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
编译阶段和运行阶段是两个不同的阶段,
1 最终调用哪个派生类对象的方法那是运行时决定的。或者:在虚函数表里取哪个地址,那运行时就调用哪个;
2 编译期 public protected private成员能不能访问;访问权限是否正确?;以及函数的默认值用哪一个?都是在编译期决定的。在编译期 用什么指针调用方法,看这个类型 作用下的方法。在继承层次结构中,虚函数到底能不能调 其访问权限是与基类相关的,因为最终调用的是哪个方法是在编译期就决定了

<<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Question 11:

class Base
{
public:
	Base(int a) :ma(a)
	{
		
	}
	virtual void show(int i = 10) { cout << "Base::show() i:" << i << endl;}//参数有默认值
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data)
	{
		
	}
	void show(int i = 20) { cout << "Derive::show() i:" << i << endl;; }
};
int main()
{
	Base *p = new Derive(30);//基类指针指向派生类对象  
	cout << "******************************************************************" << endl;
	p->show();//动态绑定 派生类的show,但是是基类的show方法 默认值10
	cout << "******************************************************************" << endl;
	delete p; 

	return 0;
}

结果:p->show();《=======》p->show(10);
动态绑定 派生类的show,但是是基类的show方法 默认值10
在这里插入图片描述
原因分析:之所以能够调用到派生类的show,是在运行时知道的。
一个函数调用的时候,要先传参数。写了传你写的,没写 默认值。在编译阶段不能看到derive的值,只能看到base的,这是由p的类型所决定的。所以在编译阶段只能看到Base的show,在这时 编译器要压参数,所以他只能压10。但是在代码的运行阶段,压参数30,运行时调用的show是从寄存器取得地址,是派生类show的地址(调用派生类的show 是在运行时期决定的),但是压什么参数 10 是在编译阶段决定的,但只能看到Base的show。

Question 12: RTTI 运行时类型信息/运行时类型识别
用处:

class Person
{
public:
	Person(string n, int a, string s) : _name(n), _age(a), _sex(s){}
	virtual void showScore() = 0;//作为 接口,具体的派生类去实现
protected:
	string _name;
	int _age;
	string _sex;
};
class Student :public Person
{
public:
	Student(string n, int a, string s,double score) : Person(n,a,s),_score(score){}
	void showScore()//Student 作为一个具体的类对应一个实体,所以定要实现从继承来的纯虚函数 :抽象方法
	{
		cout << "name:" << _name<< endl;
		cout << "age:" << _age << endl;
		cout << "sex:" << _sex << endl;
		cout << "score:" << _score << endl;
	}
private:
	double _score;
};
class Teacher :public Person
{
public:
	Teacher(string n, int a, string s, string l) : Person(n, a, s), _level(l){}
	void showScore()//Teacher 作为一个具体的类对应一个实体,所以定要实现从继承来的纯虚函数 :抽象方法
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "sex:" << _sex << endl;
		cout << "level:" << _level << endl;
	}
private:
	string _level;//职称
};
//用统一的方式去打印这些信息,而不必写上多个函数。即:一个函数能接收不同的对象,只能用基类的指针。 继承
void showSchoolPersonScore(Person *p)//显示学校人员的成绩 学校人员有很多种,学生 老师 领导。
{
	p->showScore();//调用的是派生类传进来的方法
}
int main()
{
	Student stud1("zhang san", 20, "male", 98.5);
	Teacher tea1("wang laoshi", 40, "famale", "senior");

	showSchoolPersonScore(&stud1);
	cout << "*************************************" << endl;
	showSchoolPersonScore(&tea1);

	return 0;
}

运行结果:
在这里插入图片描述
<<<<<<<<<<<<<<<<<<<<<<<<<<<<分割线>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
现在为了保护老师的隐私,改动代码如下:

class Person
{
public:
	Person(string n, int a, string s) : _name(n), _age(a), _sex(s){}
	virtual void showScore() = 0;//作为 接口,具体的派生类去实现
protected:
	string _name;
	int _age;
	string _sex;
};
class Student :public Person
{
public:
	Student(string n, int a, string s,double score) : Person(n,a,s),_score(score){}
	void showScore()//Student 作为一个具体的类对应一个实体,所以定要实现从继承来的纯虚函数 :抽象方法
	{
		cout << "name:" << _name<< endl;
		cout << "age:" << _age << endl;
		cout << "sex:" << _sex << endl;
		cout << "score:" << _score << endl;
	}
private:
	double _score;
};
class Teacher :public Person
{
public:
	Teacher(string n, int a, string s, string l) : Person(n, a, s), _level(l){}
	void showScore()//Teacher 作为一个具体的类对应一个实体,所以定要实现从继承来的纯虚函数 :抽象方法
	{
		cout << "name:" << _name << endl;
		/*cout << "age:" << _age << endl;
		cout << "sex:" << _sex << endl;*/
		cout << "level:" << _level << endl;
	}
	void showTeacherScore()
	{
		cout << "name:" << _name << endl;
		cout << "level:" << _level << endl;
	}
private:
	string _level;//职称
};

void showSchoolPersonScore(Person *p)
{
	if (typeid(*p) == typeid(Teacher))
	{
		static_cast<Teacher*>(p)->showTeacherScore();//类型强转     这样就可以识别类型
	}
	else p->showScore();//调用的是派生类传进来的方法
}
void showSchoolPersonScore2(Person *p)
{
	Teacher*pt = dynamic_cast<Teacher*>(p);//运行时类型识别,如果p真的指向Teacher对象,这个指针就可以强转成功;若不是则强转失败,返回一个空
	if (pt != NULL)
	{
		pt->showTeacherScore();
		return;
	}
	else p->showScore();

}
int main()
{
	Student stud1("zhang san", 20, "male", 98.5);
	Teacher tea1("wang laoshi", 40, "famale", "senior");

	showSchoolPersonScore(&stud1);
	cout << "*************************************" << endl;
	showSchoolPersonScore(&tea1);
	cout << "*************************************" << endl;
	showSchoolPersonScore2(&tea1);
	return 0;
}

运行结果:
在这里插入图片描述
Question 13: