C++多态

目录

多态的概念

多态的定义和实现

虚函数

多态的构成条件

虚函数的重写

虚函数重写的两个例外

1.协变(子类与父类的返回值类型不同)

2.析构函数的重写(基类和继承类析构函数的名称不同)

C++11 override 和 final

抽象类

接口继承和实现继承

多态的原理

虚函数表

多态的原理

动态绑定和静态绑定

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

单继承

多继承


多态的概念


多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会
产生出不同的状态。
举个例子:比如 买票这个行为 ,当普通人买票时,是全价买票; 学生买票时,是半价买票; 军人买票时是优先买票。

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

多态的定义和实现

虚函数

virtual修饰的函数是虚函数

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

多态的构成条件

1.必须通过基类的指针或引用调用虚函数(不能是赋值,下面会细讲)

2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

C++多态_第1张图片

 

为什么不能赋值调用?

 赋值没有改变虚表。如果赋值也切片拷贝虚表的话,那么不同类之间调用函数会乱套了,我们不能从传入的类型知道调用结果。

C++多态_第2张图片

C++多态_第3张图片

赋值不切片拷贝vfptr(p1,Johnson),指针和引用切片拷贝(p,Johnson)

不同类的虚表地址是不一样的(指针和引用本质上还是指向不同类,因为有切片),但是虚表中虚函数的地址可能有一样的。

虚函数的重写

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。
 
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;
}

C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来debug会得不偿失,因此:C++11提供了override和fifinal两个关键字,可以帮
助用户检测是否重写。
1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
     virtual void Drive() final {}
};
class Benz :public Car
{
public:
     virtual void Drive() {cout << "Benz-舒适" << endl;}
};
2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
    virtual void Drive(){}
};
class Benz :public Car {
public:
    virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

抽象类

在虚函数的后面写上 =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;
     }
};
void Test()
{
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
}

接口继承和实现继承

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

经典题目:

多态的原理

虚函数表

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
     virtual void Func1()
     {
         cout << "Func1()" << endl;
     }
private:
     int _b = 1;
};
C++多态_第4张图片
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些
平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代
表virtual,f代表function)。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
虚函数表也简称虚表。那么派生类中这个表放了些什么呢?
 
// 针对上面的代码我们做出以下改造
// 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++多态_第5张图片

 通过观察和测试,我们发现了以下几点问题:
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
表指针也就是存在部分的另一部分是自己的成员。


2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。
重写是语法的叫法,覆盖是原理层的叫法。


3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。


4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。


5. 总结一下派生类的虚表生成:

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。


6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的
呢?实际我们去验证一下会发现vs下是存在代码段的。

C++多态_第6张图片

 

多态的原理

C++多态_第7张图片

原理: 通过传入不同的对象,调用不同的虚函数表。

下面是调用时的汇编代码,可以看出,多态以后的函数调用,不是在编译时确定的,而是在运行起来以后到对象中查找的

C++多态_第8张图片

动态绑定和静态绑定

静态绑定:在程序编译期间就确定了程序的行为,在编译中就能确定调用的具体函数,比如函数重载。

动态绑定:在运行过程中才能确定调用的具体的函数,也称为动态多态。

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

单继承

C++多态_第9张图片

多继承

#include
using namespace std;
//基类1
class Base1
{
public:
	virtual void func1() { cout << "Base1::func1()" << endl; }
	virtual void func2() { cout << "Base1::func2()" << endl; }
private:
	int _b1;
};
//基类2
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 PrintVFT(VFPTR* ptr)
{
	printf("虚表地址:%p\n", ptr);
	for (int i = 0; ptr[i] != nullptr; i++)
	{
		printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址
		ptr[i](); //使用虚函数地址调用虚函数
	}
	printf("\n");
}
int main()
{
	Base1 b1;
	Base2 b2;
	PrintVFT((VFPTR*)(*(void**)&b1)); //打印基类对象b1的虚表地址及其内容
	PrintVFT((VFPTR*)(*(void**)&b2)); //打印基类对象b2的虚表地址及其内容
	

	Derive d;
	PrintVFT((VFPTR*)(*(void**)&d)); //打印派生类对象d的第一个虚表地址及其内容
	PrintVFT((VFPTR*)(*(void**)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容
	return 0;
}

C++多态_第10张图片 

你可能感兴趣的:(C++修习之路,前端)