C++多态详解——都是干货

多态的概念与定义

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

定义:多态是在不同继承关系的类对象,去调用同一个函数,产生了不同的行为

:在买车票时,普通人买票时,是全价票;学生买票时,是半价票;军人买票时,是优先买票。

代码演示

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

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

class Soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};

Student类继承了Person类,但是在调用BuyTicket时,Student是半价票,Person是全价票 

实现多态的条件

实现多态需要满足两个条件

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

虚函数的定义

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

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

对于虚函数的调用在运行时才被解析,当某个虚函数通过指针或者引用调用时,运行时到指向对象虚表中找调用虚函数地址

对于普通函数的调用,编译时就确定了调用函数的地址

虚函数的重写

虚函数重写的条件 

  1. 该函数必须是虚函数
  2. 函数名、参数列表、返回值必须相同
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票-全价" << endl;
	}
};

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

class Soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student stu;
	Soldier sd;

	Func(ps);
	Func(stu);
	Func(sd);

	return 0;
}

重写的特例

特例1:派生类不加virtual依旧满足重写(实际中最好加上),因为继承后父类的虚函数接口被继承下来了在子类依旧保持虚函数属性

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

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

 

特例2:返回值是具有父子关系的指针或者引用(父类返回父类指针,子类返回子类指针),这个也叫做协变

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

class Student : public Person
{
public:
	virtual Student* BuyTicket()
	{
		cout << "买票-半价" << endl;
		return this;
	}
};

或者

class A
{};
class B : public A
{};

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

class Student : public Person
{
public:
	virtual B* BuyTicket()
	{
		cout << "买票-半价" << endl;
		return nullptr;
	}
};

特例3:析构函数的重写,如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类析构函数构成重写。虽然父类析构函数和子类析构函数的名字看起来不同,但是编译器对析构函数的名字做了处理,析构函数的名字统一处理为了destructor()

做这个处理是为了父类指针指向子类时能够调用子类的析构函数。

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

class Student : public Person
{
public:
	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Person* ptr1 = new Person;
	delete ptr1;

	Person* ptr2 = new Student;
	delete ptr2;
}

final和override

final用于修饰虚函数,表示该虚函数不能被重写。

class Person
{
public:
	virtual void BuyTicket() final{}

};

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

	}
};

 override用于检查子类虚函数是否重写了父类的某个虚函数,如果没有则编译报错

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

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

重载、重写、重定义的对比

抽象基类 

概念 

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

class Car
{
public:
	virtual void Drive() = 0;
};
class Byd :public Car
{
public:
	virtual void Drive()
	{
		cout << "Byd-舒适" << endl;
	}
};
int main()
{
	Byd b;
}

接口继承和实现继承 

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

多态的原理 

虚函数表

先来看看下面的代码,sizeof(B)是多少?

class B
{
public:
	virtual void Func()
	{
		cout << "Func()" << endl;
	}
private:
	int _a;
};

我们知道int类型是占4个字节,而普通函数是存在类外空间中的,那么为什么打印出来的会是8呢(32位下)。

我们通过监视窗口看一下b对象中有什么吧

在b对象中除了_a成员,还多了一个_vfptr指针,对象中的这个指针叫做虚函数表指针(v代表virtual,f代表function),这个指针指向的是一个虚函数表(vftable)。一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也称虚表。所以我们打印出来的b的大小是8(在32位下指针的大小是4,在64位下指针的大小是8),b中不仅存了_a成员还有一个虚表指针。

 那么在实现了继承的情况下结果又会是如何?

将上面的代码进行如下的改造

class B
{
public:
	virtual void Func()
	{
		cout << "Func()" << endl;
	}
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
	void Func3()
	{
		cout << "Func3()" << endl;
	}
private:
	int _a;
};

class C : public B
{
public:
	virtual void Func()
	{
		cout << "C::Func()" << endl;
	}
private:
	int _c;
};

int main()
{
	B b;
	C c;

	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;
	return 0;
}

再通过监视窗口看一下b和c对象中都有什么

对上图进行分析,我们可以得出以下结论

  • 在一个类中不管虚函数有多少都会放进同一个虚表中,并且由一个虚表指针指向这个虚表
  • 派生类对象c中也有一个虚表指针,c对象有两部分构成,一部分是父类继承下来的成员,父类的虚表指针也就是存在这部分的,另一部分是自己的成员(所以上面的结果会打印出12,包括了继承下来的b的成员和c中自己的成员,还有一个虚表指针)
  • 基类b对象和派生类c对象的虚表是不一样的,我们在监视窗口中发现Func函数完成了重写,所以c的虚表中存的是重写后的C::Func,所以虚函数的重写也叫做覆盖。覆盖就是指对虚表中的虚函数的覆盖。(重写是语法的叫法,覆盖是原理层的叫法)
  • 只有虚函数才会被放进虚表中,但在vs的监视窗口中对于派生类中没有实现重写的虚函数没有显示
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面会放一个nullptr,在下面的打印虚表的代码正是利用了这一特性。

小结

对于派生类的虚表生成:

  1. 先将基类的虚表内容拷贝一份到派生类虚表中
  2. 如果派生类重写了基类的某个虚函数,那么派生类自己的虚函数覆盖虚表中基类的虚函数
  3. 派生类自己增加的虚函数按其在派生类的声明顺序添加到派生类虚表的最后

虚函数中存的是虚函数的地址,虚表指针存在对象中,虚表在VS下是存在代码段的。

虚函数表打印 

class B
{
public:
	virtual void Func()
	{
		cout << "B::Func()" << endl;
	}
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "B::Func2()" << endl;
	}
private:
	int _a;
};

class C : public B
{
public:
	virtual void Func()
	{
		cout << "C::Func()" << endl;
	}
	virtual void Func4()
	{
		cout << "C::Func4()" << endl;
	}
private:
	int _c;
};

typedef void(*VFPTR)();//函数指针

void PrintVFTable(VFPTR table[])
{
	for (size_t i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		table[i]();
	}
	cout << endl;
}


int main()
{
	B b;
	C c;
	PrintVFTable((VFPTR*)*(int*)&c);//因为在32位下指针是4个字节
	PrintVFTable((VFPTR*)*(int*)&b);//所以将指针强转成int*

	return 0;
}

代码说明:取出b、c对象的前4个字节就是虚表的指针,先取c的地址,强转成int*类型,再解引用取值就取到了c对象的前4个字节,这个值就是虚表指针,最后强转成VEPTR*,因为虚表就是一个存VEPTR类型的数组

注:使用这份代码打印虚表时,会经常崩溃,我们只需要清理解决方案再编译就好了。

多态原理 

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

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

class Soldier : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "优先买票" << endl;
	}
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student stu;
	Soldier sd;

	Func(ps);
	Func(stu);
	Func(sd);

	return 0;
}

还是以买票的这个代码为例,当使用基类的指针或者引用去调用虚函数时,例如上面的Func函数,给它传入stu对象,那么在实参stu传给形参p时会发生一个切片的操作,指针p就会指向这个stu,由于stu中完成了对BuyTicket虚函数的重写,所以在继承时,在stu的BuyTicket虚函数表中会覆盖掉继承下来的person中的BuyTicket虚函数。因此就实现出了不同对象去完成同一行为时,展现出不同的形态。

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如:函数重载、操作符重载。
  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;
};

typedef void(*VFPTR)();

void PrintVFTable(VFPTR table[])
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("vft[%d]:%p->", i, table[i]);
		table[i]();
	}
	cout << endl;
}

int main()
{
	Derive d;
	PrintVFTable((VFPTR*)*(int*)&d);
	PrintVFTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
	cout << sizeof(d) << endl;
	return 0;
}

为什么d的大小是20呢 ?

让我们再打印一下d对象的虚函数表以及通过监视窗口看看

通过上面的打印结果以及监视窗口我们可以推出d的内存对象模型,如下图

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

 

d对象中存在两个虚函数表分别来自继承下来的Base1和Base2。

因此我们可以知道为什么d的大小是20了,它包括了继承下来的Base1和Base2的大小以及d自己的成员大小。

在此代码中对func1实现了重写,但是仔细观察一下打印结果我们会发现,为什么两个Derive::func1的地址还不一样呢?

这里我们可以通过汇编的角度来了解一下它的底层到底是怎么回事

int main()
{
	Derive d;
	d.func1();
	Base1* ptr1 = &d;
	ptr1->func1();

	Base2* ptr2 = &d;
	ptr2->func1();
	return 0;
}

将代码进行如上的改造,方便观察

对于d.func1()汇编代码是这样的

对于ptr1->func1()的汇编代码是这样的

从监视窗口看到的eax值是十进制,9376324的十六进制就是008F1244

ptr1->func1()将地址存在了eax寄存器和ecx寄存器中

但是对比一下d.func1()ptr1->func1()找到Derive::func1方式没有什么太大的区别

再看一下ptr2->func1()的汇编代码

可以看到ptr2->func1()有点不一样,它一开始jmp后并没有急着去找到Derive::func1的地址而是是先对ecx减去了8之后,再jmp两次才找到了Derive::func1。减去的这个8也就是Base1的大小。

至于为什么编译器要这样做,那就不得而知,大佬的想法不是我等凡人能猜透的。

但是这样做也是非常适应那些需要切片的场景的

我们在打印虚表时,打印出来的地址其实就是eax寄存器里存的地址,所以看起来两个地址会不一样


今天的分享就到这里了,如果内容有错的话,还望指出

你觉得内容对你有所帮助的话,就给博主一键三连吧,你的支持将是我写作的动力

谢谢!!! 

  • 26
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值