C++ 多态

本文围绕C++多态展开,介绍了多态的概念、条件及实现,包括虚函数、重写等内容。阐述了override和final关键字的作用,对比了重载、重写、隐藏概念。分析了多态的原理、虚表创建,探讨了多态分类、多继承和抽象类等问题,还给出实例分析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述

多态的概念

不同对象的同一行为有不同的结果,从程序的角度就是,对相同类下的不同对象调用同一个成员函数时,执行的成员函数不同

多态的条件及实现

虚函数

被virtual修饰的成员函数是虚函数

class Person
{
public:
	virtual void BuyT()
	{
		cout << "全价" << endl;
	}
};

这里BuyT前面被virtual修饰了,所以BuyT就变成了虚函数,当然,必须是成员函数,才有虚函数这个概念,

虚函数的重写

虚函数存在的意义就是支持重写/覆盖
派生类中有一个跟基类完全相同的虚函数(返回类型、函数名、参数列表完全相同),则称子类的虚函数重写了基类的虚函数
重写 = 虚函数 + 三同

class Person
{
public:
	virtual void BuyT()
	{
		cout << "全价" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyT()
	{
		cout << "半价" << endl;
	}
};

这里Person中的BuyT和Student的BuyT就构成了重写(这里其实子类的virtual可以去掉,依旧是重写,但是一般都会写上,含义会更明确、更易阅读)

多态的条件

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

如果不满足多态条件时就是普通调用,看当前调用者的类型即可,调用者是父类则调用父类的成员函数,调用者是子类则调用子类的成员函数

class Person
{
public:
	virtual void BuyT()
	{
		cout << "全价" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyT()
	{
		cout << "半价" << endl;
	}
};
void Func(Person& people)//基类引用,这里也可以是Person* people,后面只需改成people->BuyT()
{
	people.BuyT();
}
void test1()
{
	Person P1;
	Student S1;
	
	// 方案一:
	// Person* p = &P1;
	// p->BuyT();// 全价
	// p = &S1;
	// p->BuyT();// 半价
	
	// 方案二:
	// Person& p1 = P1;
	// Person& p2 = S1;
	// C++不允许引用在创建后改变,所以这里就只能再创建一个变量p2,不过大多数多态不会这么用了,
	// 一般都是方案一或者方案三
	// p1.BuyT();// 全价
	// p2.BuyT();// 半价

	//方案三:
	Func(P1);// 全价
	Func(S1);// 半价
}

上面我们创建了Person类的对象P1,在Func中通过基类的引用调用BuyT()函数,得到了不同的结果,达到了多态的目的
注意到这里我们传递的S1,让Func的Person对象接收时是发生了切片的

虚函数重写的细节

1、父类必须加virtual,子类重写的虚函数可以不加virtual

2、调用虚函数时不能用域作用限定符限定

class Person
{
public:
	virtual void BuyT()
	{
		cout << "全价" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyT()
	{
		cout << "半价" << endl;
	}
};

void Func(Person& people)
{
	people.Person::BuyT();
	// people.Student::BuyT();
}
void test1()
{
	Person P1;
	Student S1;
	Func(P1);
	Func(S1);
}

这个例子中我们用域作用限定符限定people对象调用的是Person类的BuyT()也无法构成多态

3、协变情况下,不要求三同中返回值类型相同,就可以达到多态
协变是指返回值类型是父子类关系的指针或者引用(协变的实用意义不大但是笔试容易考

class Person
{
public:
	virtual Person* BuyT()//Person*
	{
		cout << "全价" << endl;
		return nullptr;
	}
};
class Student : public Person
{
public:
	virtual Student* BuyT()//Student*
	{
		cout << "半价" << endl;
		return nullptr;
	}
};
void Func(Person* people)
{
	people->BuyT();
}
void test1()
{
	Person P1;
	Student S1;
	Func(P1);
	Func(S1);
}

返回值类型用Person* Student*达到了协变的条件,也可以满足多态

并且协变的返回值满足父子类关系,并不强调是自身的父子类关系,看下例:

class A
{};
class B : public A
{};
class Person
{
public:
	virtual A* BuyT()
	{
		cout << "全价" << endl;
		return nullptr;
	}
};
class Student : public Person
{
public:
	virtual B* BuyT()
	{
		cout << "半价" << endl;
		return nullptr;
	}
};
void Func(Person& people)
{
	people.BuyT();
}
void test1()
{
	Person P1;
	Student S1;
	Func(P1);
	Func(S1);
}

A和B是父子类关系,在Person和Student中,也可以使用AB父子类的指针作为返回值

4、析构函数的重写
由于有以下场景:

class Person
{
public:
	virtual void BuyT()
	{
		cout << "全价" << endl;
	}
	~Person() { cout << "Person的析构" << endl; }
};
class Student : public Person
{
public:
	virtual void BuyT()
	{
		cout << "半价" << endl;
	}
	~Student() { cout << "Student的析构" << endl; }
};
void test2()
{
	//一个指针p既可以指向父类,也可以指向子类,调用父类的成员函数、调用子类的成员函数
	Person* p = new Person;
	delete p;// Person的析构
	p = new Student;
	delete p;// Person的析构
}

我们这里new Student 给到p后delete p我们想要的效果是调用子类的析构函数,但是这里却没有调用Student析构函数。这种情况就可能发生内存泄漏,当我们在析构前面加上virtual,发现,就能够调用Student的析构函数了(当然符合继承的特点,子类析构先调用父类析构,再调用子类析构)

class Person
{
public:
	virtual void BuyT()
	{
		cout << "全价" << endl;
	}
	virtual ~Person() { cout << "Person的析构" << endl; }
};
class Student : public Person
{
public:
	virtual void BuyT()
	{
		cout << "半价" << endl;
	}
	virtual ~Student() { cout << "Student的析构" << endl; }
};
void test2()
{
	Person* p = new Person;
	delete p;// Person的析构
	p = new Student;// Person的析构\n Student的析构
	delete p;
}

但有人会问这不满足三同,的确,外显的,析构天然是不能构成三同的,但是又大量有上述场景,所以编译器会对基类加了virtual的析构函数(子类可以不加,但是建议还是加上,方便阅读、理解)特殊处理,把基类派生类的析构都统一处理为destructor(),这样就能够构成三同,而对于delete p的两个步骤:p所在的类的析构 + operator delete§,也就是把前面这一步变成destructor(),即p->destructor的多态调用
,就能够处理上面的场景

override和final关键字

override是帮助派生类检查是否完成重写,如果没有完成重写会报错
在这里插入图片描述
在这里插入图片描述

final 修饰虚函数,表示该虚函数不能被重写(final在继承中也有应用,修饰某个类,表示该类无法被继承,final嘛,到终点了,到头了,在下面案例中会讲解)

class Car
{
public:
	virtual void test()
	{
		cout << "Car class..." << endl;
	}
};

class Car_Child :public Car
{
public:
	virtual void test() final
	{
		cout << "Car_Child class..." << endl;
	}
};

class Car_GrandChild :public Car_Child
{
public:
	void test()//err 不能将final修饰的虚函数重写
	{
		cout << "Car_GrandChild class..." << endl;
	}
};

案例

设计一个类,不能被继承
C++98的方案:把父类的构造函数私有化

class A
{
private:
	A(){}
};
class B : public A
{};

void test3()
{
	B b;// err A()不可访问
}

这种设计方式下需要一个成员函数,用于A对象的创建:内部调用构造函数,返回类型是A类型:

class A
{
public:
	A CreatObj()
	{
		return A();
	}
private:
	A(){}
};
class B : public A
{};

但是,还有个问题,我们调用CreatObj()也是需要依靠对象的,所以我们还要把CreatObj变成static静态的,通过域作用限定符访问到这个CreatObj()即可

class A
{
public:
	static A CreatObj()
	{
		return A();
	}
private:
	A(){}
};
class B : public A
{
	B(){}// err A()不可访问
};
void test()
{
	A::CreatObj();
}

C++11的方案:
对类加final,直接就能够限制继承

class A final
{
private:
	A(){}
};
class B : public A
{};

void test3()
{
	B b;// err A()不可访问
}

总结:final作用
1、修饰类,让这个类不能被继承
2、修饰虚函数,让虚函数不能被重写

重载、重写、隐藏概念对比

重载:
1、两个函数在同一作用域
2、函数名相同,参数不完全相同
重写/覆盖:
1、两个函数分别在派生类和基类
2、两个函数必须是虚函数
3、函数名、参数类型、返回值完全相同(协变除外,协变使得返回值可以是父类基类的指针、引用关系)
用基类指针、引用调用重写的虚函数以达到多态

重定义/隐藏:
1、两个函数分别在派生类和基类
2、函数名相同
重定义也就是条件更弱的重写,只要不满足虚函数 + 三同,且函数名相同,那么就是重定义,另一个角度讲,就是子类中写了一个和父类函数名相同的函数,在创建子类对象的时候调用的是子类中这个函数,调用子类这个函数,而不是调用父类的函数,这里这两个函数就构成了重定义/隐藏

多态的原理

多态的实现原理

1、我们先来用sizeof计算一下有虚函数的类的大小

class base
{
public:
	base(char b = 1)
		:_b(b)
	{}
	virtual void fun()
	{
		cout << "hello base" << endl;
	}
protected:
	char _b;
};
void test()
{
	sizeof(base);// 8
}

只有一个char类型的数据,怎么sizeof大小是8?
2、我们再创建一个base类型的对象,通过监视窗口观察b中存储的内容

void test()
{
	base b;
}

在这里插入图片描述
也就是说在创建base类型的时候还会自动生成一个指针,我们称这个指针__vfptr就是虚函数表指针(virtual funtion table point)
所有的虚函数地址都会存储在虚函数表中(虚函数都还是在代码段的)

不管创建了多少对象,这个虚函数表指针,都是使用的同一个指针
在这里插入图片描述

3、接着我们在构成重写的父子类对象中观察:

class base
{
public:
	base(char b = 1)
		:_b(b)
	{}
	virtual void fun1()
	{
		cout << "hello base fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "hello base fun2" << endl;
	}
protected:
	char _b;
};
class child : public base
{
public:
	child(base b = base(), int c = 2)
		:base(b)
		,_c(c)
	{}
	void fun1()
	{
		cout << "hello child fun1" << endl;
	}
protected:
	int _c;
};
void test3()
{
	base b;
	child c;
}

在这里插入图片描述
fun1构成重写,观察c对象,c对象中虚函数表对于fun1存储的是child::的fun1,fun2是从base中继承来的,所以是base::,观察b对象,b是基类,虚函数表中存储的都是base::

4、接下来我们在切片中进行观察

class base
{
public:
	base(char b = 1)
		:_b(b)
	{}
	virtual void fun1()
	{
		cout << "hello base fun1" << endl;
	}
	virtual void fun2()
	{
		cout << "hello base fun2" << endl;
	}
protected:
	char _b;
};
class child : public base
{
public:
	child(base b = base(), int c = 2)
		:base(b)
		,_c(c)
	{}
	void fun1()
	{
		cout << "hello child fun1" << endl;
	}
protected:
	int _c;
};
void fun(base& b)//引用
{
	b.fun1();
}
void test4()
{
	base b;
	child c;
	fun(b);// hello base fun1
	fun(c);// hello child fun1
}

这里通过调用fun函数,把c进行了切片,在fun函数中,依旧调用的是子类的虚函数fun1,完成了多态
下图是base b进入fun()
在这里插入图片描述
下图是child c进入fun()
在这里插入图片描述
c进行了切片,_c变量被切掉了,但是观察到__vfptr中的fun1()还是child::

也就是说编译器是通过虚函数表中的虚函数地址来进行调用函数的

我们回顾函数的调用原理,是在编译环节,就确定了函数的调用,但是虚函数这里是例外,虚函数的调用是在运行阶段通过虚函数表确定的,上述这就是实现多态的原理。
总结:通过virtual关键字,将函数变成虚函数,在创建对象时创建虚函数表,并在父子继承中两个虚函数满足三同,构成了重载,子类对象通过切片赋给了父类对象时,虚函数表,编译器也就通过这个虚函数表中函数地址去访问到了子类虚函数,由此实现了多态。

所以从原理层面判断符合多态和不符合多态:
符合多态:运行时到对象的虚函数表中找调用函数的地址
不符合多态:编译时就确定类型

多态的条件提问

多态的条件为什么不能是子类的指针或引用?子类无法通过切片拿到父类的虚函数地址,也就无法在不同对象的情况下,调用不同的函数
为什么不能是父类的对象?对象的切片和指针引用的切片不同,指针和引用在切片时不会进行拷贝,就是直接把变量和虚函数表指针给过去,对象的切片只会拷贝父类成员变量数据,但是不会拷贝虚函数表指针,使用的还是父类的虚函数表指针

多态中虚表的创建

在这里插入图片描述
父类的虚函数表是在编译阶段进行的,每有一个虚函数,就会把代码段中虚函数的地址放进虚函数表中(后面会验证虚函数的内存所在位置)

子类的虚函数表是有可能有父类的虚函数的,因为每一个父类虚函数指针(被virtual修饰的成员函数都是虚函数)都会被放进虚表,在子类对象创建时就会把父类的虚表放进子类的虚表中,然后子类与父类构成多态的虚函数就会进行重写,覆盖掉父类的虚函数指针,并且独属于子类自己的虚函数(用于子类与子类的子类构成多态) —— 简单来说子类的虚函数表就是:父类拷贝 + 子类覆盖 + 子类虚函数添加(所以这里可以看出,为什么函数重写也叫覆盖)

vs中虚函数表的问题

1、虚表、虚函数的存储的地址位置在哪?
2、对子类添加虚函数后在监视窗口的__vfptr中无法查看到新增虚函数的处理办法?
对问题一:我们可以用验证的方法来判断虚表虚函数存储的位置,具体方法是通过已知去判断位置,比如我们知道普通变量存储在栈,动态开辟内存存在堆,static静态变量存储在静态区,常量字符串存储在常量区,通过打印已知和位置的存储地址位置,来判断未知的存储地址位置
对问题二:问题二主要是想说明监视窗口不一定完全正确,我们在监视窗口中有违背所学或者无法判断之时,更多的要通过内存去判断,内存中的数据更加准确,或者可以查阅官方文档,查阅源代码等方式进行学习,判断,具体我们这里可以通过在vs中打印虚基表来判断,这里会涉及到一个vs对虚函数表的特性,我们利用vs的特性把虚表打印出来,vs中对虚表的结束,是通过置空nullptr来实现的(vs实现的不是太好,有的时候在修改了代码,增加了虚函数时,会出bug——虚表不以空结尾,这时,只需要清理解决方案,再重新编译即可)
这两个问题的具体代码如下,代码分析在注释中

//Car BigCount父子类多态
class Car
{
public:
	Car(){}
	virtual void fun1() { cout << "Car fun1" << endl; }
	virtual void fun2() { cout << "Car fun2" << endl; }
};
class BigCount : public Car
{
public:
	BigCount(){}
	virtual void fun1() { cout << "BigCount fun1" << endl; }
	virtual void fun3() { cout << "BigCount fun3" << endl; }
};

//前面的fun1 fun2 fun3的返回值、参数都相同,所以我们这里类型重定义一下,方便后序书写
//解读:FUNC_PTR是函数指针,是返回值为void,参数为无参的函数的指针
typedef void (*FUNC_PTR)();

//打印虚基表,这里FUNC_PTR* 是函数指针数组的首地址
//我们这里是传给函数虚函数表的首地址,用table存储,table就跟int array[10]; 中array的意义是相同的
void Print(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)//以vs的特性置空作为停止条件
	{
		printf("[%d]:%p", i, table[i]);
		cout << endl;
	}
	cout << endl;
}
void funC(){}//得到一个代码段的地址
void test10()
{
	int a = 0;
	printf(" 栈区 :%p\n", &a);

	int* pa = new int;
	printf(" 堆区 :%p\n", pa);//new出来的是堆区空间,pa本身的地址还是栈区中,所以是查看pa,而不是&pa
	printf(" 栈区 :%p\n", &pa);//new出来的是堆区空间,pa本身的地址还是栈区中,所以是查看pa,而不是&pa

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

	const char* str = "abc";
	printf("常量区:%p\n", str);

	printf("代码段:%p\n", funC);

	cout << endl;
	
	//上面都是我们已知区域的变量、函数内存位置
	//下面我们通过打印未知的与已知比较,来判断,未知所在的内存位置
	BigCount cd;
	
	//这里我们&cd拿到对象的首地址,然后强制类型转换为int*,然后就拿到了前四个空间地址,对其解引用,拿到虚函数表指针,也就是虚函数表的地址
	//我们将其暂时存储在int类型的tmpptr中,然后后面把这个tmpptr强制类型转换成FUNC_PTR*,让他以函数指针数组首地址的角度解读这个tmpptr,进而就能够放到Print进行打印虚函数表中的虚函数地址
	int tmpptr = *(int*)(&cd);
	Print((FUNC_PTR*)tmpptr);
	Print((FUNC_PTR*)*(int*)(&cd));//一步到位

	printf("  ??? :%p", tmpptr);//tmpptr就算虚函数表的地址
}

下图为执行结果:
在这里插入图片描述
这里虚函数表和静态区的str离的最近,虚函数和代码段的funC()离的最近,数据的存储不会在每个区中距离太远
这里就说明了虚函数表存储在静态区,虚函数存储在代码段

虚函数表是在常量区,和我们之前的const char* 存储的“abc”这种信息是类似的,也就是说,程序一旦运行,虚函数表就唯一确定,那么我们在创建有虚函数表的对象时,不管创建多少,虚函数表都是同一张

void test11()
{
	BigCount cd1;
	BigCount cd2;
	BigCount cd3;
	int tmpptr1 = *(int*)(&cd1);
	int tmpptr2 = *(int*)(&cd2);
	int tmpptr3 = *(int*)(&cd3);
	//这三个地址都相同
	printf("%p\n", tmpptr1);
	printf("%p\n", tmpptr2);
	printf("%p\n", tmpptr3);
}

最后我画了下图来解释说明对象bc中关于虚函数表的整体分布
在这里插入图片描述
图中解释:方框内的内容是存储的内容,不是该位置的地址

by the way,我们通过函数指针调用的函数,就不需要管是public限定,还是protected限定,还是private限定了,因为共有私有是语法层面的限制,而函数指针调用是原理层的调用,所以设想一个场景,父类中有个虚函数是放在private中,子类是无法进行访问的,但是虚函数表中是由这个虚函数地址的,所以我们是可以通过访问虚表中元素(虚函数)的方式去调用该private中的虚函数,下面是测试:
用Car中的fun4进行测试
在这里插入图片描述

在这里插入图片描述

实例分析

class A
{
public:
	virtual void func(int val = 1) 
	{ 
		cout << "A->" << val << endl; 
	}
	virtual void test()
	{
		func();
	}
};
class B : public A
{
public:
	void func(int val = 0)
	{
		cout << "B->" << val << endl;
	}
};
void test5()
{
	B b;
	b.test();//B->1
}

为了解答b.test()为什么结果是B->1 需要解释两层:
1、为什么调用的子类的func?func()构成了重写,b调用的是继承来的test(),这时就发生了切片,this指针就是父类指针,在test()中调用了func(),实际上就是this->func(),所以满足多态条件,父类指针调用 + 重写,所以是调用的子类的func
2、为什么val是1?回答这个问题可以去回答:为什么子类函数不需要加上virtual就可以和父类构成重写?因为编译器在检查构成重写时,只检查三同,如果不相同,那么不构成重写,而构成重定义/覆盖,如果相同,此时编译器还是使用父类的框架(virtual 函数返回值 参数类型以及参数缺省值),实现就使用的是子类的实现,虚函数的重写,重写的是实现,此时用的父类框架,那么就是父类的val以及父类的缺省值
在这里插入图片描述

多态分类问题

多态的分类问题主要是为了清楚多态相关的一些说法,能让自己听懂各种角度来描述多态相关的内容
静态多态和动态多态:
1、静态多态也就是静态绑定,也就是函数重载,是编译时决定程序行为
2、动态多态也就是动态绑定,也就是用函数重写实现多态,运行时决定程序行为

多继承

前面介绍的都是单继承下的多态,后面是多继承下的多态
在这里插入图片描述
回顾:根据多态的特性,即父类类型对于不同对象,调用不同的函数,所以子类的虚函数表就是直接用的是父类传下来,并进行重写后的虚函数表,并不会自己再创建一个虚函数表

多继承这里对于多个父类,就会创建多张虚表(一个父类一个虚表)

class Base1
{
public:
	virtual void fun1() { cout << "Base1::fun1" << endl; }
	virtual void fun2() { cout << "Base1::fun2" << endl; }
};
class Base2
{
public:
	virtual void fun1() { cout << "Base2::fun1" << endl; }
	virtual void fun2() { cout << "Base2::fun2" << endl; }
};
class Child : public Base1, public Base2
{
public:
	virtual void fun1() { cout << "Child::fun1" << endl; }
	virtual void fun3() { cout << "Child::fun3" << endl; }
};

void test2()
{
	Base1 b1;
	Base2 b2;
	Child c;
}

在这里插入图片描述
这里子类c中的来自Base1有一张虚表__vfptr,来自Base2有一张虚表__vfptr,可能会注意到Base2的__vfptr中的[0]中有[thunk]:Child::fun1’adjustor{4}'{void},这里的adjustor是调整器,是一种机制,后面会提到
同时这里c中__vfptr[0]是Child::fun1,__vfptr[0]也是Child::fun1,所以Child的fun1重写了Base1和Base2的fun1

发问及原理

我们先提出一个问题:Derive中会有两张虚表,第一张是Base1生成的,第二张是Base2生成的,func3() 存在哪里?
我们想要直到func3存在哪里,首先需要拿到两张虚表的地址,然后进行打印验证
第一步:
之前我们的虚函数表指针都是放在类对象的开头,但是这里是两张虚表,所以必定有一张虚表放在开头,另一张虚表紧邻其后,我们可以通过上面Print(FUNC_PTR* table)函数的方式打印虚函数表指针在对象开头的虚表中的虚函数,我们会发现就是Base1的虚表
(其实顺序是和我们 : public Base1, Base2这里的继承顺序相同,先继承的Base1,那么Base1的虚表就在开头;先继承Base2,那么Base2的虚表就在开头,其实这很合理,我们先继承Base1,那么会先执行Base1的构造函数,也就会把Base1的虚表拷贝下来,拷贝下来就立刻放到首地址处了,再继承Base2的此时就只能把虚表放到Base1的虚表后面了)
这里Base2的地址可以先拿到Base1的地址,然后将其转换成(char*),并加上sizeof(Base1),这里能这么做是因为Base1中没有其他的变量,只有虚表,此时就能够定位到Base2虚函数表指针的首地址,然后将其强制类型转换成int*,对其解引用,就可以用int的方式拿到Base2的虚函数表

第二步:
后序操作和前文相同

(这里第一步还可以用切片的思想:创建一个Base2*类型的指针,然后把Child对象的地址给到它,此时会发生切片,Base2* 类型的首地址就是第二张虚函数表的地址,后序操作相同)

typedef void (*FUNC_PTR)();
void Print(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p\n", i, table[i]);
		table[i]();
	}
	cout << endl;
}
class Base1
{
public:
	virtual void fun1() { cout << "Base1::fun1" << endl; }
	virtual void fun2() { cout << "Base1::fun2" << endl; }
};
class Base2
{
public:
	virtual void fun1() { cout << "Base2::fun1" << endl; }
	virtual void fun2() { cout << "Base2::fun2" << endl; }
};
class Child : public Base1, public Base2
{
public:
	virtual void fun1() { cout << "Child::fun1" << endl; }
	virtual void fun3() { cout << "Child::fun3" << endl; }
};
void test3()
{
	Child c;
	int ptr1 = *((int*)&c);
	//方法一:
	int ptr2 = *((int*)((char*)&c + sizeof(Base1)));
	//(char*)&c
	//(char*)&c + sizeof(Base1)
	//(int*)((char*)&c + sizeof(Base1))
	//*((int*)((char*)&c + sizeof(Base1)))
	
	//方法二:
	//Base2* tmp = &c;
	//int ptr2 = *((int*)tmp);
	
	Print((FUNC_PTR*)ptr1);
	Print((FUNC_PTR*)ptr2);
}

从结果说明,Child的fun3是放在Base1中的
在这里插入图片描述

注意到第一次调用和第二次调用fun1的时候的函数指针不同
在这里插入图片描述
按道理,虚函数是存储在静态区的,应该调用同一个虚函数指针,访问同一份虚函数,这里需要通过汇编去理解
我们执行下面这段代码,去查看汇编进行理解

void test4()
{
	Child c;
	Base1* ptr1 = &c;
	Base2* ptr2 = &c;

	ptr1->fun1();
	ptr2->fun1();
}

在vs中调试代码时,在调试 - 窗口中有一个反汇编,用于查看汇编指令
根据切片的性质,这段代码中ptr1和ptr2的虚表还是c的虚表,访问的fun1是Child类的fun1
ptr1:对于ptr1调用fun1,在汇编中是通过call jump直接访问到fun1
ptr2:ptr2是通过call jump jump sub然后调用fun1,这两次访问fun1,是使用同一个函数地址,ptr2汇编中有一个关键点,寄存器存储了一个差值,这个差值的大小与我们前面sizeof(Base1)的大小相同,编译器可以用这个差值,通过sub操作,然后去访问到fun1

简单来说,ptr1直接访问fun1,ptr2通过sub然后访问fun1,是通过同一个虚函数指针访问的fun1

关于ptr1能直接call eax 到 009F1195,进而jump到fun1中,ptr2为什么不能直接call eax到009F1195的解释
ptr1调用func1()时this指针和函数指针重合(this需要指向对象起始),所以可以直接call eax 009F1195,但是ptr2的this指针是指向的第二个虚函数表指针的位置,需要修正到c对象的首地址所以这里sub ecx, 4(前面在监视窗口中观察到的adjustor{4}也就是反应的这里)然后就能够去call eax 到 009F1195,进而jump到fun1

所以多继承还是遵守了静态区只会生成一个函数地址,只是ptr2 - 第二个父类包装了一层

像这里多继承情况的多态底层问题的答案在不同的平台都可能不同,解题思路就是各种的测试,调试看汇编

抽象类

抽象类是包含了纯虚函数的类,抽象类无法实例化对象,这是为了处理现实世界中没有对应的实体的情况,比如我们创建了一个animal类,我们在现实生活中并没有animal类这对象,animal是我们把cat、dog、fish这些所有的实体类集合在一起给定的一个抽象的名称:animal,所以比如我们给定动物都有的一个行为(函数)—— 前进,对于cat类、dog类,就是四条腿交替向前…,对于fish类,那么就是摆动…,我们并不会创建一个animal类对象,而是仅仅会使用其子类。

纯虚函数

纯虚函数是在虚函数后加一个 = 0;

//世界上是把奥迪、大众、林肯、牧马人...这些类抽象成了Car类
class Car
{
public:
	virtual void Drive() = 0;	
};

纯虚函数只需要声明,不需要定义,不需要定义也好理解:我们是无法对Car类,对animal类的某一行为(函数),做统一的解释(定义),所以解释(定义)的工作全权交给子类完成
当然不要把virtual给忘了——纯虚函数首先得是虚函数,需要有virtual关键字进行修饰

包含纯虚函数的类是抽象类 - 这句话还需要扩充为通过继承来的纯虚函数所在类也是抽象类,也无法实例化对象

class Animal
{
public:
	virtual void walk() = 0;
};
class Dog : public Animal
{
};
void test5()
{
	Animal anm;//err,父类无论如何都不能实例化
	Dog d;//err
}

在这里插入图片描述

只有当派生类重写了纯虚函数,派生类才能实例化对象

class Animal
{
public:
	virtual void walk() = 0;
};
class Dog : public Animal
{
public:
	virtual void walk()
	{
		cout << "四条腿向前交替..." << endl;
	}
};
class Fish : public Animal
{
public:
	virtual void walk()
	{
		cout << "鱼鳍摆动..." << endl;
	}
};
void test5()
{
	//Animal anm;//err
	Dog d;
	Fish f;
}

虽然我们不能定义抽象类的对象,但是可以定义抽象类的指针、引用

class Animal
{
public:
	virtual void walk() = 0;
};
class Dog : public Animal
{
public:
	virtual void walk()
	{
		cout << "四条腿向前交替..." << endl;
	}
};
class Fish : public Animal
{
public:
	virtual void walk()
	{
		cout << "鱼鳍摆动..." << endl;
	}
};
void Func(Animal& anm)
{
	anm.walk();
}
void func(Animal* anm)
{
	anm->walk();
}
void test6()
{
	Dog d;
	Fish f;
	Func(d);//引用
	Func(f);
	cout << endl;
	func(&d);//指针
	func(&f);
}

抽象类强制了派生类重写行为
override与纯虚函数的区别?
override是帮助派生类检查是否完成重写,如果没有完成重写会报错,是一种检测机制
纯虚函数是一种思想,是一种接口继承的思想

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失去梦想的小草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值