C++ 多态

C++ 多态

文章目录

  • C++ 多态
    • 1.多态的概念
    • 2.多态的定义及实现
      • 2.1 多态的构成条件
      • 2.2 虚函数定义
      • 2.3 虚函数的重写(覆盖)
      • 2.4 重载、覆盖(重写)、隐藏(重定义)的对比
    • 3.抽象类
      • 3.1 抽象类的概念
      • 3.2 接口继承和实现继承理解
    • 4.多态的原理
      • 4.1 虚函数表剖析
      • 4.2 多态的原理
      • 4.3 动态绑定与静态绑定(了解)
    • 5.对于多态条件的思考
    • 6.单继承和多继承关系中的虚函数表
      • 6.1 单继承中的虚函数表
      • 6.2 多继承中的虚函数表


1.多态的概念

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

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

C++ 多态_第1张图片


2.多态的定义及实现

2.1 多态的构成条件

继承中要构成多态的两个条件:

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

满足上述条件后,基类的指针指向基类对象则调用基类的虚函数,指向派生类对象则调用派生类的虚函数

C++ 多态_第2张图片


2.2 虚函数定义

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

class Person 
{
public:
 	virtual void BuyTicket() { cout << "普通人 - 正常排队 - 全价买票" << endl;}
};

2.3 虚函数的重写(覆盖)

虚函数的重写(覆盖)的条件派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的函数名字、参数列表、函数返回值完全相同,即三同+虚函数),称派生类的虚函数重写了基类的虚函数

注:派生类中重写基类的虚函数可以不加 virtual 关键字(因为基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,所以建议加上

虚函数重写的两个例外:协变和析构函数重写—这种情况也是虚函数重写

1.协变(基类与派生类虚函数返回值类型不同)

理解:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即返回值必须为父子类类型的指针或引用时,称为协变

// A和B为父子类
class A {};
class B : public A {};

class Person//父类 
{
public:
	virtual A* BuyTicket()//返回值为父类(A)的指针
	{
		cout << "普通人 - 全价买票" << endl;
		return new A;
	}
};

class Student : public Person //子类
{
public:
	virtual B* BuyTicket()//返回值为子类(B)的指针
	{
		cout << "学生 - 半价买票" << endl;
		return new B;
	}
};

void Func(Person* ptr) 
{
	// 多态 -- ptr指向基类对象则调用基类的虚函数,指向派生类对象则调用派生类的虚函数
	ptr->BuyTicket();
}

int main() 
{
	Person per;
	Func(&per);//打印普通人 - 全价买票

	Student stu;
	Func(&stu);//打印学生 - 半价买票

	return 0;
}

2.析构函数的重写(基类与派生类析构函数的名字不同)

理解:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

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

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

// 基类和派生类的析构函数不是虚函数,它们是隐藏关系
// 基类和派生类的析构函数是虚函数,它们是重写关系

int main()
{
	// 普通场景下,虚函数是否重写都是ok的
	Person p;
	Student s;

	// new对象特殊场景
	// 只有派生类Student的析构函数重写了Person的析构函数,
	// 下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1; // p1->destructor + operator delete(p1)
	delete p2; // p2->destructor + operator delete(p2)
	return 0;
}

C++ 多态_第3张图片

C++ 多态_第4张图片


2.4 重载、覆盖(重写)、隐藏(重定义)的对比

重载:overload,同一作用域,函数名相同,参数列表不同(个数/顺序/类型),与返回值无关
重写(覆盖):override,父子类两个虚函数完全相同,即三同+虚函数

  • 两个函数分别在基类和派生类的作用域

  • 两个函数必须是虚函数

  • 函数名、参数列表、函数返回值都完全相同(协变例外,返回值可以不同)

  • 两个例外:协变、析构函数重写
    隐藏(重定义):不是重写就是重定义

  • 两个函数分别在基类和派生类的作用域

  • 函数名相同

  • 两个基类和派生类的同名函数不构成重写就是隐藏(重定义)

C++ 多态_第5张图片


3.抽象类

3.1 抽象类的概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象

// 基类 - 抽象类 - 不能实例化出对象
class Car
{
public:
	virtual void Drive() = 0; // 纯虚函数,不需要实现它,反正都要重写,实现没有意义
};

// 派生类
class Benz :public Car
{
public:
	virtual void Drive() // 必须重写基类虚函数,派生类才能实例化出对象
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	// 基类是抽象类,不能实例化出对象,但可以定义基类指针,用来实现多态
	Car* pBenz = new Benz;//必须重写基类虚函数才能new
	pBenz->Drive();

	return 0;
}

3.2 接口继承和实现继承理解

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

  • 实现继承—普通函数:继承全部内容
  • 接口继承—虚函数:只继承函数声明属性(函数名、参数列表(含缺省参数)、返回值)

重点:实现继承就是把你全部都继承下来,一点不变;接口继承就是继承你的函数的返回值、函数名、参数列表(包含缺省参数),而函数的实现不继承,进行重写

举例:

C++ 多态_第6张图片


4.多态的原理

C++多态性主要是通过虚函数实现的,要搞懂多态的原理,首先要知道虚函数表

4.1 虚函数表剖析

// 这里常考一道笔试题:请问 sizeof(Base) 是多少(VS2019 x86)?
class Base
{
public:
	virtual void Func1() { cout << "Base::Func1()" << endl; }
private:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;//sizeof(Base)大小为8字节
    //原因:除了int占4字节,里面还有一个虚基表指针占4字节,所以一共8字节
	return 0;
}

通过观察测试我们发现Base类的对象是 8 bytes,除了_b 成员,还多一个 __vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)

一个含有虚函数的类中都至少都有一个虚函数表指针虚函数的地址被放在虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析:
C++ 多态_第7张图片

我们再来看一个代码:

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() // 重写基类虚函数Func1()
	{
		cout << "Derive::Func1()" << endl;
	}

private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

通过内存窗口我们又可以看到:

  • Base类对象的模型:
    C++ 多态_第8张图片

  • Base类对象与Derive类对象内存模型:
    C++ 多态_第9张图片

通过观察和测试,我们发现了以下几点问题:

C++ 多态_第10张图片

1.虚函数表创建的时机是在编译期间

  • 编译期间编译器就为「每个有虚函数的类」确定好了「对应的虚函数表」里的内容

2.虚函数表指针 __vfptr 创建的时机:在构造函数中的初始化列表位置,这里才是真正的初始化,这个时候才会生成虚表指针,并把虚函数表的首地址赋给虚表指针

  • vfptr 跟着对象走,所以对象什么时候创建出来,vfptr 就什么时候创建出来,也就是运行的时候。程序在编译期间时,编译器会为构造函数中增加为 vfptr 赋值的代码(这个是编译器的行为)。当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有为这个对象的 vfptr 赋值的语句
  • 所以在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针,这个虚函数表指针就有值了

3.派生类对象 d 中也有一个虚表指针 __vfptr,d 对象由两部分构成,一部分是基类继承下来的成员,虚表指针也就是存在这部分的,另一部分是自己的成员

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

  • 所以:派生类对象 d 的虚表中本来该放的是基类虚函数的地址,但是派生类重写了基类的虚函数,基类虚函数的地址就被覆盖变成了派生类虚函数的地址,本意是调用基类的虚函数,结果却调到了派生类的虚函数。这就实现了多态

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

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

7.总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中。 b.如果派生类重写了基类中某个虚函数,用派生类自己重写的虚函数覆盖虚表中基类的虚函数。 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

8.基类和派生类,无论是否完成了虚函数的重写,都有各自独立的虚表

9.一个类的所有对象共享同一张虚表(就像一个类的所有对象共享成员函数一样)

C++ 多态_第11张图片

思考:这里还有两个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?

虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现 vs 下是存在代码段

虚表存虚函数指针(不存xu’ha),虚表和虚函数存在代码段

验证代码:

class Base
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
private:
	int a;
};

void Test() {}

int main()
{
	Base b;

	int a1 = 0; // 栈帧
	int* p1 = new int; // 堆区
	const char* p2 = "hello"; // 常量区
	auto pf = Test; // 函数地址
	static int a2 = 1; // 静态区

	printf("栈帧        :0x%p\n", &a1);
	printf("堆区        :0x%p\n", p1);
	printf("常量区      :0x%p\n", p2);
	printf("函数地址    :0x%p\n", pf);
	printf("静态区      :0x%p\n", &a2);
	printf("虚函数表地址:0x%p\n", *((int*)&b));

	return 0;
}

C++ 多态_第12张图片


4.2 多态的原理

上面分析了半天虚函数表的原理到底是什么?还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket的例子吧

C++ 多态_第13张图片

// 基类
class Person 
{
public:
	virtual void BuyTicket() { cout << "普通人 - 全价买票" << endl; }
};

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

void Func(Person& p) // 基类引用
{
	p.BuyTicket();
}

int main()
{
	Person per;
	Func(per);

	Student stu;
	Func(stu);

	return 0;
}

C++ 多态_第14张图片

多态实现原理的剖析:

  1. 观察图中的红色箭头我们看到,p 引用 per 对象时,p.BuyTicket() 在 per 的虚表中找到虚函数是 Person::BuyTicket
  2. 观察图中的蓝色箭头我们看到,p 引用 stu 对象时,p.BuyTicket() 在 stu 的虚表中找到虚函数是 Student::BuyTicket
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态

4.3 动态绑定与静态绑定(了解)

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

5.对于多态条件的思考

多态的实现条件:一个是虚函数重写,一个是基类对象的指针(或引用)调用虚函数

思考一:基类对象的指针(或引用)调用虚函数的原理是什么?

原因:不管基类指针(或引用)指向的是基类还是派生类,执行这段代码 p.BuyTicket() 的指令是一模一样的,都是先找到「虚表指针」(对象中的头 4 个字节),通过虚表指针找到虚表,取对应「虚函数的地址」并调用该虚函数

底层汇编查找原理:

void Func(Person& p)
{
//...
	p.BuyTicket();
// p中存的是per对象的地址,将p移动到eax寄存器中
00CA2571  mov         eax,dword ptr [p]
// [eax]就是取eax寄存器的值s指向的内容,这里相当于把per对象头4个字节(虚表指针)移动到了[edx]寄存器
00CA2574  mov         edx,dword ptr [eax]  
00CA2576  mov         esi,esp  
00CA2578  mov         ecx,dword ptr [p]  
// [edx]就是取edx寄存器的值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了[eax]寄存器
00CA257B  mov         eax,dword ptr [edx]  
// call eax中存的虚函数指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的
00CA257D  call        eax  
00CA257F  cmp         esi,esp  
00CA2581  call        __RTC_CheckEsp (0CA12B2h)
}

int main()
{
	Person per;
	Func(per);
    
    return 0;
}

思考二:为什么多态必须要用基类的指针(或引用)来调用虚函数,而用基类对象调用却不行?

  1. 派生类对象赋值给基类对象,不会拷贝派生类的虚表指针,只会拷贝对象中的数据成员过去,俗称的浅拷贝
  2. 我们不妨这样来理解,一个类的所有对象共享同一张虚表,就像一个类的所有对象共享成员函数一样,只能供这个类自己的对象使用,所以派生类对象是不可能把虚表拷贝过去的,不然就违背同一个类共享的规则了
  3. 既然不会把派生类的虚表指针拷贝过去,那「基类对象」自然就不能调用到「派生类的虚函数」了

C++ 多态_第15张图片

思考三:用基类引用(或指针)引用不同对象去完成同一行为时,如何展现出不同的形态?

C++ 多态_第16张图片


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

6.1 单继承中的虚函数表

单继承举例:

class Base
{
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};

class Derive :public Base
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

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

通过调试,可以观察到,派生类对象 d 中的虚表看不到虚函数 Func3 和 Func4,这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是它的一个小bug

C++ 多态_第17张图片

那么我们如何查看对象 d 的虚表呢?下面我们使用代码打印出虚表中的函数:

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

// 打印虚表,传入虚函数指针数组
// void PrintVFT(vfptr vft[])
void PrintVFT(void* vft[])
{
	printf("虚表地址__vfptr:%p\n", vft);
	for (size_t i = 0; vft[i] != nullptr; i++)
	{
		// 依次打印虚表各元素
		printf("vft[%d]:%p->", i, vft[i]);
		// 把虚表各元素由void*强转为函数指针类型后,赋值给函数指针ptr
		vfptr ptr = (vfptr)vft[i];
		// 调用函数
		ptr();
	}
	printf("\n");
}

int main()
{
	Base b;
	Derive d;
    
// 思路:取出b、d对象的头4字节,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针 --> (int*)&b
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针 --> *((int*)&b)
// 3.再强转成void**,得到存放void*类型(虚函数指针类型)数组首元素的地址,即指针的地址,所以是v** --> (void**)(*((int*)&b))
// 4.将虚表首元素地址(即虚表指针)传递给PrintVFT进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了
    
	PrintVFT((void**)(*((int*)&b))); // 打印对象b的虚表
	PrintVFT((void**)(*((int*)&d))); // 打印对象d的虚表

	return 0;
}

C++ 多态_第18张图片


6.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;
};

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

// 打印虚表,传入虚函数指针数组的地址(即虚表指针)
void PrintVFT(void* vft[])
{
	printf("虚表地址__vfptr:%p\n", vft);
	for (size_t i = 0; vft[i] != nullptr; i++)
	{
		// 依次打印虚表各元素
		printf("vft[%d]:%p->", i, vft[i]);
		// 把虚表各元素赋值给函数指针ptr
		vfptr ptr = (vfptr)vft[i];
		// 调用函数
		ptr();
	}
	printf("\n");
}

int main()
{
	Derive d;
    
    // 打印第一张虚表
    PrintVFT((void**)*((int*)&d));
	// 打印第二张虚表
    // 必须先强转成char*,然后加Base1大小个字节,再强转成int*,解引用,强转成void**
	PrintVFT((void**)*((int*)((char*)&d + sizeof(Base1))));
    
	return 0;
}

对于多继承种虚函数表的思考:

思考一:Base1 和 Base2 中都有虚函数 func1,那么Derive类中的 func1 到底是重写的哪一个基类的呢?

结果:两个基类 Base1 和 Base2 中的虚函数 func1 都会被重写。因为要满足多态条件

Derive d;

Base1* p1 = &d; // 用基类1指针调用
p1->func1();

Base2* p2 = &d; // 用基类2指针调用
p2->func1();

思考二:多继承体系,Derive 继承了两个基类,那么 Derive 对象中有几张虚表呢?-- 有两张

C++ 多态_第19张图片

你可能感兴趣的:(#,C++编程,c++,java,开发语言)