【C++】多态

目录

[一. 概念](#一. 概念)

[1. 概念](#1. 概念)

[2. 析构函数可以是虚函数吗?为什么需要是虚函数?](#2. 析构函数可以是虚函数吗?为什么需要是虚函数?)

[3. C++11 关键字 override、final](#3. C++11 关键字 override、final)

[4. 重载、覆盖(重写)、隐藏(重定义)的对比](#4. 重载、覆盖(重写)、隐藏(重定义)的对比)

[二. 抽象类](#二. 抽象类)

[1. 接口继承和实现继承](#1. 接口继承和实现继承)

[三. 多态的原理](#三. 多态的原理)

[1. 虚函数表](#1. 虚函数表)

[2. 多态条件为什么是那俩](#2. 多态条件为什么是那俩)

[3. 静态多态、动态多态](#3. 静态多态、动态多态)

[四. 多继承的虚函数表](#四. 多继承的虚函数表)


一. 概念

1. 概念

不同对象去完成某个行为时会产生出不同的状态

虚函数:被关键字 virtual 修饰的类成员函数

虚函数的重写(覆盖) :子类中有一个跟父类完全相同的虚函数(即子类虚函数与父类虚函数的++返回值类型、函数名字、参数列表完全相同++),称子类的虚函数重写了父类的虚函数

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
多态调用****看指向的对象,指向父类调父类,指向子类调子类; 普通调用**,看当前调用者的类型**

比如 Student 继承了 Person。Person对象买票全价,Student对象买票半价

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

  1. 调用函数是重写的虚函数
  2. 必须通过父类的指针或者引用调用虚函数**
cpp 复制代码
class Person {
public:
	virtual	void BuyTicket() const { cout << "买票-全价" << endl; }
};

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

void func(const Person& p)
{
	p.BuyTicket();
}

//void func(const Person* p)
//{
//	p->BuyTicket();
//}

int main()
{
	Person ps;
	Student st;

	func(ps);
	func(st);
	//func(&ps);
	//func(&st);
	return 0;
}

虚函数重写的两个例外:
1. 子类的重写虚函数可以不加 virtual(建议都加上)
2. 协变 (用的极少),返回值可以不同,但要求返回值必须同时是父子关系的指针或引用

cpp 复制代码
class Person {
public:
	virtual	int BuyTicket() const { 
		cout << "买票-全价" << endl;
		return 0;
	}
};

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

报错:"Student::BuyTicket": 重写虚函数返回类型有差异,且不是来自"Person::BuyTicket"的协变


cpp 复制代码
class Person {
public:
	virtual	Person* BuyTicket() const { 
		cout << "买票-全价" << endl;
		return 0;
	}
};

class Student : public Person {
public:
	virtual Student* BuyTicket() const {
		cout << "买票-半价" << endl; 
		return 0;
	}
};
cpp 复制代码
class A { };
class B : public A { };

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

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

2. 析构函数可以是虚函数吗?为什么需要是虚函数?

cpp 复制代码
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

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

int main()
{
	Person ps;
	Student st;
	return 0;
}
cpp 复制代码
class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};

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

int main()
{
	Person ps;
	Student st;
	return 0;
}

不加 virtual,结果和上面一样


析构函数加 virtual,是虚函数重写。因为类析构函数都被处理成 destructor 这个统一的名字

处理成统一名字是为了让他们构成重写,为什么让他们构成重写呢?记住下面的场景,不重写过不去

cpp 复制代码
class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};

class Student : public Person {
public:
	~Student() {
		cout << "~Student()" << endl;
		delete[] ptr;
	}

protected:
	int* ptr = new int[10];
};

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

    // 切片行为
	p = new Student;
	delete p; // p->destructor() + operator delete(p)
    // 普通调用,看当前调用者的类型。这里调的是Person的析构函数
    // 这里我们期望p->destructor()是一个多态调用,去调用Student的析构函数,而不是普通调用
	return 0;
}

重点看25、26行

内存泄漏,没有调到子类(Student)的析构函数

多态的2个条件之一:必须通过父类的指针或者引用。已经达成,p 必然是指针

第二个是虚函数的重写

如果不对析构函数名做特殊处理,只加 virtual,因为不符合三同,所以不能构成重写

所以必须把析构函数处理成 destructor,才能构成重写、形成多态
有可能变成父类(要被继承),就把析构函数加 virtual,变成虚函数

cpp 复制代码
class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person {
public:
	virtual ~Student() { // 这里可以不加virtual
		cout << "~Student()" << endl;
		delete[] ptr;
	}

protected:
	int* ptr = new int[10];
};

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

	p = new Student;
	delete p; // p->destructor() + operator delete(p)
	return 0;
}

3. C++11 关键字 override、final

final:修饰虚函数,表示该虚函数不能再被重写

cpp 复制代码
class Car
{
public:
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }
};
// 报错: "Car::Drive": 声明为 "final" 的函数不能由 "Benz::Drive" 重写

int main()
{
	return 0;
}
cpp 复制代码
class Car
{
public:
	virtual void Drive() final {}
};

class Benz :public Car
{
public:
	// void Drive() { cout << "Benz-舒适" << endl; } 报错,还是重写
    void Drive(int i) { cout << "Benz-舒适" << endl; }
    // 不是重写,不符合三同
    // 这俩 Drive 是隐藏关系
};

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

cpp 复制代码
class Car {
public:
	virtual void Drive() {}
};

class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

int main()
{
	return 0; // 不报错
}
cpp 复制代码
class Car {
public:
	void Drive() {}
};

class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};
// 报错: "Benz::Drive": 包含重写说明符"override"的方法没有重写任何基类方法

补充:如何设计不想被继承的类?

C++98:父类构造函数私有

  1. 私有成员在子类不可见

  2. 子类的构造函数必须调父类的构造函数

cpp 复制代码
// 错误代码
class A
{
public:
	A CreateObj()
	{
		return A();
	}
protected:
	A() {}
};

class B : public A
{};

int main()
{
	//B bb; 报错
	CreateObj(); // 报错
	return 0;
}

调 CreateObj 是创建对象,调 CreateObj 得先有对象

cpp 复制代码
// 正确代码
class A
{
public:
	static A CreateObj()
	{
		return A();
	}
protected:
	A() {}
};

class B : public A
{};

int main()
{
	//B bb; 报错
	A::CreateObj();
	return 0;
}

C++11:父类+final(最终类)

cpp 复制代码
class A final
{ };

class B : public A
{ };
// 报错: "B": 无法从 "A" 继承,因为它已声明为 "final"

int main()
{
	return 0;
}

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

重载:

2个函数在同一作用域

函数名、参数相同

覆盖(重写):

2个函数分别在子类、父类作用域

函数名、参数、返回值必须相同(协变除外)

2个函数必须是虚函数

隐藏(重定义):

2个函数分别在子类、父类作用域

函数名相同

父类和子类的2个同名函数不构成重写就是重定义

二. 抽象类

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

子类继承后也不能实例化出对象,只有重写纯虚函数 ,子类才能实例化出对象

纯虚函数间接强制子类重写虚函数

纯虚函数更体现出了接口继承

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car // 继承了纯虚函数
{
public:
};

int main()
{
	Car car;//报错:"Car": 无法实例化抽象类
	Benz b; //报错:"Benz": 无法实例化抽象类
	return 0;
}
cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

int main()
{
	//Car car; 报错:"Car": 无法实例化抽象类
	Benz b;
	return 0;
}

应用场景:

cpp 复制代码
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 Func(Car* p)
{
	p->Drive();
}

int main()
{
	Func(new Benz);
	Func(new BMW);

	return 0;
}

1. 接口继承和实现继承

普通函数的继承都是实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现

虚函数的继承是接口继承,子类继承的是父类虚函数的接口,目的是为了重写 ,达成多态,继承的是接口

所以如果不实现多态,不要把函数定义成虚函数

三. 多态的原理

1. 虚函数表

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{ cout << "Func1()" << endl; }

	virtual void Func2()
	{ cout << "Func2()" << endl; }

	void Func3()
	{ cout << "Func3()" << endl; }
protected:
	int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl; // 8
	Base b1;
	return 0;
}

一个含有虚函数的类中都至少都有一个虚函数表指针
虚函数****在 编译阶段生成**(生成了虚函数的地址就可以生成虚表)** ,在代码段(常量区) ,虚函数表里存的是虚函数的地址


重写:

cpp 复制代码
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
	int _a = 1;
};

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

void Func(Person& p)
{
	// 符合多态,运行时到指向对象的虚函数表中找调用函数的地址
	p.BuyTicket();
}

/*void Func(Person p)
{
	// 不符合多态,即为普通调用,编译时确定地址
	p.BuyTicket();
}*/

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

	Student Johnson;
	Func(Johnson);

	return 0;
}

子类对象由子类部分和父类部分构成

如果没重写,父类部分虚函数表本来就是父类的

如果重写,父类部分虚函数表本来就变成子类的

指针指向父类对象,看到的是父类

指针指向子类对象,切片后看到的还是父类

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的
不满足多态的函数调用时编译时确认好的


cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{ cout << "Base::Func1()" << endl; }

	virtual void Func2()
	{ cout << "Base::Func2()" << endl; }

	void Func3()
	{ cout << "Base::Func3()" << endl; }

protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{ cout << "Derive::Func1()" << endl; }

	virtual void Func4()
	{ cout << "Derive::Func4()" << endl; }

protected:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}
  1. 子类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在这一部分;另一部分是自己的成员。
  2. 父类b对象和子类d对象虚表是不一样的:Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表 本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下子类的虚表生成:
    (理解)a.先将父类中的虚表内容拷贝一份到子类虚表中 b.如果子类重写了父类中某个虚函数,用子类自己的虚函数覆盖虚表中父类的虚函数 c.子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后
    (现实)不会将父类中的虚表内容拷贝一份到子类虚表中
  6. 还有很容易混淆的问题:虚函数存在哪的?虚表存在哪的?
    答:虚函数存在虚表,虚表存在对象中。注意上面的回答的 的。
    注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段(常量区)的,只是虚函数的指针存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在**代码段(常量区)**的,Linux g++下大家自己去验证?

堆是给用户动态申请的,虚表是编译器生成的。所以排除堆
同类型的不同对象共用虚表,栈是跟着栈帧走的。函数结束,栈帧销毁,等会重建虚表吗?所以排除栈

父类、子类,头4个字节是虚表的地址

想办法验证:打印地址,对比

cpp 复制代码
int main()
{
	int a = 0;
	printf("栈:%p\n", &a);

	static int c = 0;
	printf("静态区:%p\n", &c);

	int* p = new int;
	printf("堆:%p\n", p);

	const char* str = "hello world";
	printf("常量区:%p\n", str);
    
    Base b;
	Derive d;
	printf("虚表1:%p\n", *((int*)&b));
	printf("虚表2:%p\n", *((int*)&d));
    //想取前4个字节,32位,前4个字节可以直接取
    //Linux默认是64位要取前8个字节
	return 0;
}

强转成 int* 类型,解引用取4字节


上面那张图,监视窗口没有看到子类的func4;内存窗口有一个地址,怀疑是func4

验证:依次取虚表中的虚函数指针打印并调用

cpp 复制代码
typedef void(*FUNC_PTR) ();

// 打印函数指针数组
//void PrintVFT(FUNC_PTR table[])
void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++) // VS才会以空结束
	{
		printf("[%d]:%p->", i, table[i]);
        
        FUNC_PTR f = table[i]; // 有函数指针就能调用函数
        f();
	}
	printf("\n");
}

int main()
{
	Base b;
	Derive d;

	int vft1 = *((int*)&b);
	PrintVFT((FUNC_PTR*)vft1);

	int vft2 = *((int*)&d);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

上面的成员函数都没有参数、没有访问成员变量

成员函数正常应由对象调用;这里是取到它的地址直接调用,相当于没有传 this 指针,如果成员函数里访问成员变量,可能会报错

2. 多态条件为什么是那俩

必须是虚函数的重写:

只有完成了虚函数的重写,子类虚表里才会是子类的虚函数,才能实现指针指向父类调父类、指向子类调子类

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

  1. 为什么不能通过子类的指针或引用......?

只有父类既可以指向父类,也可以指向子类

到虚表中找,如果是子类的指针,只能指向子类,找到的只有子类的虚函数,不能实现出多种形态

  1. 为什么不能通过父类对象......?

对象的切片 和 指针、引用的切片不同

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{ cout << "Base::Func1()" << endl; }

	virtual void Func2()
	{ cout << "Base::Func2()" << endl; }

	void Func3()
	{ cout << "Base::Func3()" << endl; }

//protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{ cout << "Derive::Func1()" << endl; }

	virtual void Func4()
	{ cout << "Derive::Func4()" << endl; }

protected:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	Base* ptr = &d;
	Base& ref = d;
	d._b = 10;
	b = d;

	return 0;
}

**指针、引用的切片:**不存在拷贝

指向父类对象,看到父类对象 ==> 父类虚表

指向子类对象,看到子类中父类那一部分(切片,看到的还是父类对象)==> 子类虚表

对象的切片:

结论:子类赋值给父类对象切片,不会拷贝虚表
若拷贝虚表,父类对象的虚表中是父类虚函数还是子类虚函数就不确定了,乱套了

答:如果父类对象也能实现多态,那在切片时必须拷贝过去才能实现,但拷贝过去就乱了,所以父类对象不能实现多态

3. 静态多态、动态多态

静态多态:编译时确定了程序的行为,也称静态绑定、前期绑定、早绑定 eg:函数重载

动态多态:ptr 不知道自己指向父类还是子类,只是运行时到指向的对象的虚表里去取到地址,再 call 调用,也称动态绑定、后期绑定、晚绑定

cpp 复制代码
int main()
{
	int i = 1;
	double d = 1.1;
	cout << i << endl;
	cout << d << endl;

	Base b;
	Base* ptr = &b;

	b.Func1(); // 静态绑定
	ptr->Func1(); // 动态绑定

	return 0;
}

四. 多继承的虚函数表

cpp 复制代码
class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
protected:
	int b1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
protected:
	int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
protected:
	int d1;
};

typedef void(*FUNC_PTR) ();

// 打印函数指针数组
//void PrintVFT(FUNC_PTR table[])
void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);

		FUNC_PTR f = table[i]; // 有函数指针就能调用函数
		f();
	}
	printf("\n");
}
cpp 复制代码
int main()
{
	Derive d;
	cout << sizeof(d) << endl;

	int vft1 = *((int*)&d);
	//int vft2 = *((int*)((char*)&d + sizeof(Base1)));
    // 如果不强转成char*,+1,+的是一个d对象的大小
	Base2* ptr = &d;
	int vft2 = *((int*)ptr);

	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);

	Base1* ptr1 = &d;
	ptr1->func1(); // 多态调用

	Base2* ptr2 = &d;
	ptr2->func1(); // 多态调用

	Derive* ptr3 = &d;
	ptr3->func1(); // 普通调用

	return 0;
}

都重写了 func1,为什么 Base1 和 Base2 的虚表中 func1 的地址不一样,但都调到了 func1?

反向验证,看汇编

为什么要修正 this 指针?

ptr2 调 func1,func1 是重写的虚函数,调的是 Derive 的成员函数,成员函数里面可能访问子类成员变量,this 指针应该指向 Derive 对象

ptr1 指向 Base1 的开始,恰好指向 Derive 对象的开始,所以不用动 ptr1

还可以在 ptr2 传给 ecx 充当 this 指针后就修正,这样虚函数表的 func1 的地址就相同了。总之一定要修正

本篇的分享就到这里了,感谢观看 ,如果对你有帮助,别忘了点赞+收藏+关注

小编会以自己学习过程中遇到的问题为素材,持续为您推送文章

相关推荐
rechol3 小时前
C++ 继承笔记
java·c++·笔记
朱嘉鼎4 小时前
C语言之可变参函数
c语言·开发语言
SunkingYang4 小时前
详细介绍C++中捕获异常类型的方式有哪些,分别用于哪些情形,哪些异常捕获可用于通过OLE操作excel异常
c++·excel·mfc·异常捕获·comerror
北冥湖畔的燕雀7 小时前
C++泛型编程(函数模板以及类模板)
开发语言·c++
QX_hao8 小时前
【Go】--map和struct数据类型
开发语言·后端·golang
你好,我叫C小白8 小时前
C语言 循环结构(1)
c语言·开发语言·算法·while·do...while
Evand J10 小时前
【MATLAB例程】基于USBL和DVL的线性回归误差补偿,对USBL和DVL导航数据进行相互补偿,提高定位精度,附代码下载链接
开发语言·matlab·线性回归·水下定位·usbl·dvl
Larry_Yanan11 小时前
QML学习笔记(四十二)QML的MessageDialog
c++·笔记·qt·学习·ui
爱喝白开水a11 小时前
LangChain 基础系列之 Prompt 工程详解:从设计原理到实战模板_langchain prompt
开发语言·数据库·人工智能·python·langchain·prompt·知识图谱