C++多态

需要声明的,本片博客中的代码及解释都是在vs2013下的x86程序中,涉及的指针都是4bytes。如果要其他
平台下,部分地方需要改动。比如:如果是x64程序,则需要考虑指针是8bytes问题等等

1.概念:

同一事物,在不同场景下的表现出的不同形态

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

举例子:见人说人话,见鬼说鬼话,
见老婆: 见校长: 见丈母娘:

不同的对象,完成说话这个行为,说话的方式就不一样

举个栗子:比如买票这个行为,当普通人买票时,是全价买
票;学生买票时,是半价买票;军人买票时是优先买票。


再举个栗子: 最近为了争夺支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家
想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。
实这背后也是一个多态行为。


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

#include 
using namespace std;

class person {
     


public:
	virtual void Buyticket(){
     

		cout << "全票" << endl;
	}

};

class student :public person{
     

public:

	virtual void Buyticket(){
     
		cout << "半票" << endl;
	}


};

void testvirtual(person & d)
{
     
	d.Buyticket();
}

int main(){
     

	student s;
	person p;
	testvirtual(s);
	testvirtual(p);
	return 0;
}

结果:
半票
全票

多态的分类:

静态的多态:(静态绑定,早绑定)函数重载–add 模板
在程序编译时就确定了函数的具体行为

动态多态(动态绑定,晚绑定):
在程序运行时,确认函数的具体行为。虚函数

2.多态的实现条件:

1.继承的条件下,基类必须要有虚函数,派生类必须要对基类中的虚函数重写

2.虚函数调用:必须通过基类的指针或者引用调用虚函数

i:虚函数

虚函数就是在类成员数前加上virtual关键字

class person {
     
public:
	virtual void Buyticket(){
     
		cout << "全票" << endl;
	}
};
ii:重写(覆盖)

重写:在基类中该函数必须为虚函数,派生类如果要重写虚函数,必须与基类的虚函数原型完全相同(返回值,参数列表,函数名字),不包括virtule,但是加上也可以

下面就是在不同类中对虚函数进行的重写

#include 
using namespace std;
class person {
     
public:
  virtual void Buyticket(){
     
  	cout << "全票" << endl;
  }
};

class student :public person{
     
public:
  virtual void Buyticket(){
     
  	cout << "半票" << endl;
  }
};

void testvirtual(person & d)
{
     
  d.Buyticket();

}

int main(){
     

  student s;
  person p;

  testvirtual(s);
  testvirtual(p);

  return 0;
}

但是虚函数重写也有例外:

另外虚函数的重写也叫作虚函数的覆盖。

例外

a.返回值类型不同的重写

协变:基类虚函数返回基类对象的指针或引用
派生类的虚函数返回派生类的指针或引用

   斜变举例:

如下:对虚函数的定义,返回类型改为引用类型,结果构成了重写

#include 
using namespace std;
class person {
     
public:
	virtual person& Buyticket(){
     //返回引用类型
		cout << "全票" << endl;
		return *this;
	}
};

class student :public person{
     
public:
	virtual student & Buyticket(){
     //返回引用类型
		cout << "半票" << endl;
		return *this;
	}
};

void testvirtual(person & d)
{
     
	d.Buyticket();
}

int main(){
     
	student s;
	person p;
	testvirtual(s);
	testvirtual(p);
	return 0;
}

b.函数名字不同的重写

析构函数:子类与基类的析构函数也可以构成重写

如下:

#include 
using namespace std;
class  Base{
     
public:

virtual	 ~Base()
	{
     
		cout << "Base virtual" << endl;

	}
};

class C_child :public Base
{
     
public:
virtual ~C_child()
	{
     
		cout << "C_child virtual" << endl;
	}
};

void test(Base & b){
     
	b.~Base();
}
int main(){
     
	Base b;
	C_child d;
	test(b);
	test(d);
	return 0;
}

结果:
Base virtual
C_child virtual

c.不规范的重写行为

在派生类中的重写的虚函数,可以不加virtual关键字,因为派生类继承基类的虚函数保持虚函数的属性


class person {
     


public:
	virtual void Buyticket(){
     

		cout << "全票" << endl;
	}

};

class student :public person{
     

public:
	 void Buyticket(){
     
		cout << "半票" << endl;
	}
};


2.实现

基类指针指向那个类的对象,程序运行时,就调用哪个虚函数

例如:

下面的test(),传递是那个类的对象就调用,那个类的虚函数

#include 
using namespace std;

class  Base{
     

public:
	
	virtual void test1(){
     


		cout << "test1" << endl;
	}
};

class C_child :public Base
{
     
public:
private:

	virtual void test1(){
     
		cout << "c_child test1()" << endl;
	}
};
void test(Base & b){
     
	b.~Base();

}

3.特性:

1.如果条件不满足:优先调用基类的

2.派生类的虚函数可以不用加virtule,保持基类中的虚函数特性,但是基类中必须加上

3.建议在继承体系中,基类的析构函数上virtule,否则容易发生内存泄漏

举例:
Base* pb=new Derived;
delete pb;


调用的时候默认调用的是静态的(基类的析构),而对象是
子类的,所以要去调用子类的析构函数
如果子类中涉及资源管理,那么就会产生内存泄漏,申请的空间有释放

如下:

析构函数的重写问题:
基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。
这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编
译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,
这也说明的基类的析构函数最好写成虚函数。


下面是基类和派生类的析构函数,没有加上virtual,那么在释放的时候就会产生冲突,


#include 
using namespace std;
class  Base{
     

public:

 ~Base()
	{
     
		cout << "Base virtual" << endl;

	}
};

class C_child :public Base
{
     
public:

 ~C_child()
	{
     
		cout << "C_child virtual" << endl;
}
private:
	C_child *d;

};

void test(Base & b){
     
	b.~Base();
}
int main(){
     
	Base *a = new C_child;
	test(*a);
	return 0;
}

结果:

Base virtual

本来应该调用C_child的析构函数,确调用了基类的析构函数,所以就产生了错误


4.如果多态条件没有满足,使用的对象的静态类型来调用函数

变量有两种类型:
静态类型:声明类型
动态类型:实际类型
如果多态条件没有满足,使用的对象的静态类型来调用函数

例如:

Base* pb=new Derived;

声明的类型为Base
实际类型为Derived;

5.如果多态条满足,使用动态类型来调用函数 (动态类型程序运行时)

在编译期间检查子类的虚函数是否和基类的虚函数构成重写

做法:

i.在派生类的虚函数列表后加上override;

ii.在基类的虚函数列表后加上final,表示基类不能被重写


final:修饰类,表示类不能被继承
class base final {
     };

如下:针对参数类型和返回类型不构成重写,在编译期间就会报错

class  Base{
     
public:
	virtual void test1(){
     
		cout << "test1" << endl;
	}
	
};
class C_child :public Base
{
     
private:
	virtual void test1(int) override//报错
	{
     
		cout << "c_child test1()" << endl;
	}

};


class  Base{
     
public:

	virtual void test2()final{
     
	cout << "test2" << endl;
	}
};
class C_child :public Base
{
     
private:
	
	virtual int  test2() //报错
	{
     
		cout << "test2" << endl;
		return 0;
	}
};

7.构成重写的两个函数在基类和子类的访问权限可以不一样

但是基类的虚函数必须是可以访问的,要不然继承下来的派生类无法访问

如下:

class  Base{
     

public:
	
	virtual void test1(){
     
		cout << "test1" << endl;
	}
};
class C_child :public Base
{
     

private:
	virtual void test1(){
     
		cout << "c_child test1()" << endl;
	}
};

接口继承和实现继承:

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现

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

抽象类

有纯虚函数的类叫抽象类,在虚函数后面加=0,叫纯虚函数

对于函数使用不名确的,可以指定为虚函数,在子类中实现。

特性:

1.不能实例化对象,因为类型不明确

class person {
     
public:
	virtual void GO_WC() = 0{
     }
};

void test(){
     
    
    person p;
}
报错:

2.可以创建指针,因为类型有了

但是可以new一个空间,类型不明确

class person {
     
public:
	virtual void GO_WC() = 0{
     }
};

void test(){
     
    
    person *p = nullptr;//没有错
    person *p=new person;//编译报错
}

3.抽象类一定要被继承,因为在子类中去实现纯虚函数实现,
如果没有在派生类中重写,则派生类也是抽象类

#include 
using namespace std;
class person {
     
public:
	virtual void GO_WC() = 0{
     }
};


class wenman :public person
{
     
public:
	virtual void GO_WC(){
     

		cout << "go_wenman_wc" << endl;
	}

};
class man :public person
{
     
public:
	virtual void GO_WC(){
     

		cout << "go_man_wc" << endl;
	}
};

void test(person&p){
     

	p.GO_WC();

}

int main(){
     

	man d;
	wenman w;
	test(w);
	test(d);
	return 0;
}

多态调用原理:

// :sizeof(Base)是多少?
class Base
{
     
public:
 virtual void Func1()
 {
     
 cout << "Func1()" << endl;
 }
private:
 int _b = 1;
};

结果为:
8

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会
放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表
function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,
虚函数表也简称虚表。

1.多态中会多四个字节,因为有可能含有多个虚表,存放虚函数表(函数指针数组)的首地址,多出的四个字节为虚表指针

class Base{
     

public:

	virtual void test1(){
     

		cout << "test1()" << endl;

	}

	virtual void test2(){
     

		cout << "test2()" << endl;
	}
private:

	int _d;
};
int main(){
     

	Base b;
	}

在这里插入图片描述
2.基类:基类的虚拟函数按照其在类中的声明次序放在虚表中

在这里插入图片描述

3.派生类的虚表和基类的虚表不同

去观察b和d对象的窗口,就会发现不同

// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
     
public:
 virtual void Func1()
 {
     
 cout << "Base::Func1()" << endl;
 }
 virtual void Func2()
 {
     
 cout << "Base::Func2()" << endl;
 }
 void Func3()
 {
     
 cout << "Base::Func3()" << endl;
 }
private:
 int _b = 1;
};
class Derive : public Base
{
     
public:
 virtual void Func1()
 {
     
 cout << "Derive::Func1()" << endl;
 }
private:
 int _d = 2;
};
int main()
{
     
 Base b;
 Derive d;
 return 0;
}

C++多态_第1张图片

派生类的虚表生成步骤:

  • 先将基类的虚表中的内容拷贝一份,放在虚表中(有几个基类拷贝几分)

  • 再将派生类重写的某个虚函数,用自己的虚函数替换派生类虚表中相同偏移量的虚函数


基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,
所以d的虚表中存的是重
写的Derive::Func1,所以虚函数的重写也叫作覆盖
  • 对于派生类自己增加的虚函数将其放在派生类的第一个虚表后
  • 如果派生类有多个虚表,则将自己的虚函数放在第一虚表最后

虚表的介绍:

  • 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
  • 虚表的存放位置,虚表是个函数指针数组,数组中存的是虚函数的地址,而对象中的前四个字节存放的是虚表的首地址,
    而虚函数和我们的普通函数一样存在代码段,在vs下虚表存放在数据段和继承里面的虚基表存放位置一样。
因为一个派生类可能继承多个虚表,所以一个派生类可能会有多个虚表

编译器是如何去通过虚表实现多态的

对于单继承来说:
1.首先在基类和派生类中都有一个虚表

2.这个表的首地址存放在类的前四个字节中

3.满足多态的条件的情况下,在程序运行起来的时候,根据传入的基类对象找出对应的虚表

4.再根据对应的虚表去拿到相应的虚函数地址

5.最后在通过传入this指针去调用对应的虚函数

C++多态_第2张图片
C++多态_第3张图片




如何确定虚表指针在类的前四个字节中

#include 
using namespace std;

class Base{
     

public:

	virtual void test1(){
     

		cout << "test1()" << endl;

	}

	virtual void test2(){
     

		cout << "test2()" << endl;
	}
private:

	int _d;
};
class Devir :public Base
{
     
public:
	virtual void test1(){
     

		cout << "Devir test1()" << endl;
	}

	virtual void test2(){
     

		cout << "Devir test2()" << endl;
	}
};


typedef void(*test) (void)  ;//确定出虚表中元素类型函数指针类型的

int main(){
     

	Base b;
	Devir d;
	cout<<"虚函数表地址:"<<(int*)(&b)<<endl;
	
	
	cout<<"虚函数表第一个函数地址:"<<(int*)*(int*)(&b)<<endl;
	
	//最后将函数地址转换成相应的类型 
	
	(*((test)*((int *)*((int*)&b))))();
	(*(((test)*((int *)*((int*)&b) + 1))))();

	return 0;

}

结果:
test1();
test2();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax,dword ptr [p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的
中取找的。
001940EA call eax 
001940EC cmp esi,esp 

注意:不满足多态条件的函数在编译的时候就确定函数地址,和满足多态条件的函数确认时间不同,因为在编译期间函数没有运行起来,对象还没有确定,而多态的调用必须根据基类对象的引用去调用,对象都没确定,当浩调用函数当然也无法确定。

动态绑定与静态绑定:

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数
    重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用
    具体的函数,也称为动态多态。

多继承

1.概念:

一个派生类有多个基类

class  D:public B1,public B2 {};

#include 
using namespace std;

class Base1{


public :

	virtual void Func1(){


		cout << "Func1" << endl;
	}

	virtual void Func2(){


		cout << "Func2" << endl;
	}
};
class Base2{


public:

	virtual void Func3(){


		cout << "Func3" << endl;
	}

	virtual void Func4(){


		cout << "Func4" << endl;
	}
};
class Dervir :public Base1, public Base2
{

public:
	virtual void Func1(){

		cout << "Dervir Func1" << endl;

	}
	virtual void Func3(){

		cout << "Dervir Func3" << endl;

	}

	virtual void Func5(){

		cout << "Dervir Func5" << endl;

	}

};
int main(){
	Dervir d;
	Base1 b1;
	Base2 b2;


	return 0;
}


多继承的虚拟表:

  • 先将基类的虚表中的内容拷贝一份,放在虚表中(有几个基类拷贝几分)

  • 再将派生类重写的某个虚函数,用自己的虚函数替换派生类虚表中相同偏移量的虚函数

  • 对于派生类自己增加的虚函数将其放在派生类的第一个虚表后

对于自己的虚函数在第一个表中是不可见的,但是可以通过内存窗口看到

1.先找到表的对应地址
C++多态_第4张图片
2.再根据相应的地址去看内存的状态
C++多态_第5张图片

3.对比
C++多态_第6张图片

通过对比可以发现第一张表中在调试窗口只有两条数据,而在内存窗口中有三条数据,
而第二张表在监视窗口有两条数据,在内存窗口中也是两条数据。

这也验证了派生类将自己的虚函数放在第一张表中。

你可能感兴趣的:(C++,C++继承)