C++类和对象多态

在这里插入图片描述

多态的概念

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

静态的多态 :函数重载,在编译期间就已经确定好了函数的调用关系

namespace mzt 
{
    
    
	//函数名相同参数不同构成重载,
	void add(int left, int right) {
    
     cout << "add(int left, int right)" << endl; }
	void add(float left, float right) {
    
     cout << "add(float left, float right)" << endl; }

	void func() 
	{
    
    
		int a = 10, b = 20;
		float x = 11.1, y = 22.2;
		//C++有函数名修饰规则所以不用担心会调用到同一个函数
		//在这里会根据传递的参数去匹配适用的函数,也是一种多态的行为
		add(a, b);
		add(x, y);
	}
}

动态的多态:父类指针或引用调用重写虚函数
1、父类指针或者引用指向父类,调用的就是父类的虚函数
2、父类指针或者引用指向哪个子类,调用的就是子类的虚函数

多态的定义及实现

多态的构成条件

这里理解就行,先来看看什么是虚函数,和多态的满足条件

要想实现动态的多态那么在继承中要构成多态还有两个条件:

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

虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数,普通函数不能在前面加virtual关键字

class Person 
{
    
    
protected:
	//虚函数只能在类中定义
	virtual void shopping() {
    
     cout << "人会购物" << endl; }
};

注意:
1、只有类的非静态成员函数可以是虚函数
2、虚函数这里virtual和虚继承中用的virtual是同一个关键字,但是他们呢之间没有关系,虚继承解决的是菱形继承的数据冗余和二义性的问题,而虚函数这里是为了实现多态

虚函数的重写

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

class Person 
{
    
    
protected:
	virtual void shopping() {
    
     cout << "人会购物" << endl; }
};

//派生类继承父类,并重写父类的虚函数
class Student : public Person 
{
    
    
protected:
	virtual void shopping() {
    
     cout << "学生买书" << endl; }
};

当基类的虚函数已经被重写了后,我们就已经完成了构成多态的第一个条件了,再来看满足多态的第二个条件

namespace mzt 
{
    
    
	//实现动态多态
	class Person 
	{
    
    
	public:
		virtual void shopping() {
    
     cout << "人会购物" << endl; }
	};

	//派生类继承父类,并重写父类的虚函数
	class Student : public Person 
	{
    
    
	public:
		virtual void shopping() {
    
     cout << "学生买书" << endl; }
	};
	//基类引用会指向实际传递的对象类,而对象的类型不一样所以调用的
	//是不同类作用域的虚函数,实现了调用的多种形态
	void func(Person&p)
	{
    
    
		p.shopping();
	}

}

int main() 
{
    
    
	mzt::Person p;
	mzt::Student s;
		
	mzt::func(p);
	mzt::func(s);
	return 0;
}

在这里插入图片描述

虚函数重写的两个例外

协变(基类与派生类虚函数返回值类型不同)派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

//多态中的协变
namespace mzt01 
{
    
    
	class A 
	{
    
    
	
	};
	//两个类需要构成继承关系
	class B : public A
	{
    
    
	
	};
	class Person 
	{
    
    
		//父类的虚函数
		virtual A* func() 
		{
    
    
			return new A(); //父类虚函数返回父类对象A
		}
	};
	class Student : public Person
	{
    
    
		//子类的虚函数,子类重写父类的虚函数,函数名/参数相同,返回值不同
		virtual B* func()
		{
    
    
			return new B();//子类虚函数返回子类对象B
		}
	};
	//通过协变也可以产生多态的行为
	void func() 
	{
    
    
		Person* ptr;
		Person p;
		Student stu;
		ptr = &p;
		ptr->func(); //调用父类的虚函数,返回父类对象A

		ptr = &stu;
		ptr->func();//调用子类的虚函数,返回子类对象B
	}
}

效果:
在这里插入图片描述

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

//虚函数重写的例外
namespace mzt02 
{
    
    
	class A {
    
    };
	class B : public A {
    
     };
	class Person 
	{
    
    
	public:
		virtual ~Person()
		{
    
    
			cout << "virtual ~Person()" << endl;
		}
	};
	class Student : public Person
	{
    
    
	public:
		//子类完成对父类虚函数的重写,编译器会将析构函数的名字
		//统一处理为destructor所以是构成同名的
		virtual ~Student()
		{
    
    
			cout << "virtual ~Student()" << endl;
		}
	
		// 以下这种写法也是属于例外的,子类的析构函数可以不添加
		//virtual关键字,因为子类继承父类的时候已经将虚属性
		//继承了下来,虚函数名又会被替换成destructor,所以可以构成
		//虚函数的重写,读者了解即可
		~Student()
		{
    
    
			cout << " ~Student()" << endl;
		}
	};
	void func() 
	{
    
    
		Person *p1 = new Person();//调用一次析构函数,因为是父类对象
		Student *p2 = new Student(); 
		//通过继承后子类对象Student会拥有父类对象Person的部分成员,
		//所以为了完成对象的资源清理工作会调用两次析构函数,析构的顺序又是符合栈的特性
		//所以会先调用子类的析构函数,再调用父类的析构函数

		delete p1;  //指向父类对象,调用父类的析构函数
		delete p2;	//指向子类对象,在析构的时候调用子类析构函数,
		//再调用父类的析构函数,防止内存泄漏
	}
}

效果:
在这里插入图片描述

如果子类去继承父类后,实例化子类对象,那么这个对象中会存放父类的成员部分和子类的成员部分,当释放指针所指向的对象,会调用对象的析构函数完成对象的资源清理工作,而为了父类对象析构调用父类的析构函数,子类对象析构调用子类的析构函数,可以将父类的析构函数定义成虚函数,并让子类重写这个析构函数,这样万一对象需要有被清理的资源(比如说动态申请的),就可以让父类析构函数完成父类那一部分成员的资源清理,而子类析构函数完成子类那一部分的资源清理

总结:
在一个继承体系下,建议将父类的析构函数定义成虚函数,并在子类中重写这个析构函数,当释放指针所指向的对象,存在内存泄漏的问题(对象中有需要被清理的资源)

针对虚函数重写,建议尽量不适应协变的方式,应该严格按照重写要求来,这样的代码更容易维护

C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写

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

//如果再刚刚的虚函数后面加上关键字,那么这个虚函数就
//不能在子类中重写了
virtual ~Person ()final
{
    
    
		cout << "virtual ~Person()" << endl;
}
//如果子类去重写父类的虚函数那么就会报编译错误
virtual ~Student()
{
    
    
		cout << " ~Student()" << endl;
}

效果:
在这里插入图片描述

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

//override关键字修饰在虚函数后面是表示这个虚函数一定要被重写
virtual ~Person ()override
{
    
    
	cout << "virtual ~Person()" << endl;
}

//如果屏蔽掉子类虚函数对父类虚函数的重写,编译器在检查的时候会报编译错误
/*virtual ~Student()
{
	cout << "virtual ~Student()" << endl;
}*/

效果:
在这里插入图片描述

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

在这里插入图片描述

多态总结

1、多态的意思是调用一个函数时,函数会呈现不同的状态(函数的名字必须相同,但是函数的意义不同,举例重载和虚函数)

2、静态的多态:函数重载,函数重载是在编译期间通过函数的命名修饰规则来确定调用哪一个函数来完成同名函数的不同行为

3、动态的多态:需要满足多态的条件,条件1,子类必须继承父类,并重写父类的虚函数,条件2,通过父类的引用或者指针去指向重写的虚函数,父类的引用或者指针指向的是哪一个对象,就去调用该对象的虚函数,跟对象有关

4、虚函数重写的条件:函数名/ 参数 /返回值相同,并且这个函数必须是虚函数

5、虚函数重写的两个例外:
1、协变 ,函数名相同,并且是一个虚函数,但是返回值并不相同,返回值类型是一个自定义类型(内置类型不行),两个自定义类型必须构成父子关系

2、析构函数建议定义成虚函数,将父类析构函数定义为虚函数,并在子类中重写,如果父类的指针指向的是new出来的子类对象,在delete时为了完成资源清理,会分别调用父类和子类的析构函数,防止造成内存泄漏

3、子类重写虚函数可以不加virtual,因为子类会继承父类的虚属性,只有函数名和参数相同,那就构成虚函数的重写,但是建议加上可以懂的这是一个虚函数的重写

抽象类

抽象类的概念是内部有一个纯虚函数,包含纯虚函数的类也叫做接口类,而纯虚函数的前提条件必须是一个虚函数,

//抽象类,也叫接口类
class myfunc
{
    
    
public:
	virtual void func() = 0 ; //纯虚函数的语法必须是虚函数,后面加=0
};

注意:
抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

抽象类的价值:
1、可以更好的去模拟现实生活中无法具体确定的类型,适用于去描述没有具体实例对象对应的抽象类型
2、可以体现接口继承,强制子类去重写虚函数

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

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

多态的原理

计算一个类对象的大小

// 计算这个类实例化的对象占用多少字节
namespace mzt03 
{
    
    
	class Base 
	{
    
    
	public:
		virtual void func() {
    
     }
	private:
		int _a;
		char ch = '\0';
	};
	void test() 
	{
    
    
		Base b;
		cout << sizeof(b) << endl; 
		//a的大小是4,ch大小是1,而对象中多了一个指针,结构体对齐规则后是12
	}
}

对象中多出来的这个指针指向的是虚函数表(简称虚表),指针大小跟平台有关

虚函数表

namespace mzt03 
{
    
    
	class Base 
	{
    
    
	public:
		virtual void func1() {
    
     cout << "void Base::func1()" << endl; }
		virtual void func2() {
    
     cout << "void Base::func2()" << endl; }
		virtual void func3() {
    
     cout << "void Base::func3()" << endl; }
	};
	
	void test() 
	{
    
    
		Base b;
	}
}

通过监视窗口可以看到b对象中的_vfptr指针,_vfptr指针指向的是虚函数表,而虚函数表中会存放函数地址,虚函数表其实是一个函数指针数组
在这里插入图片描述
b对象的对象模型
在这里插入图片描述
虚函数表跟虚继承那块的虚基表是不一样的,虽然他们都使用了virtual关键字,但是使用场景是完全不同的,解决的问题也是不同的,他们之间并没有关联,虚基表中存放的是距离虚基类成员的偏移量,而虚函数表存放的是虚函数的指针

子类继承父类后完成虚函数重写和不完成虚函数重写的区别

class A 
{
    
    
public:
	virtual void func() {
    
     cout << "void A::func()" << endl; }
	
};
class B : public A 
{
    
    
public:
	//子类继承父类后没有重写父类的虚函数
};

void test() 
{
    
    
	A a;
	B b;
}

效果:
在这里插入图片描述
得到的对象模型
在这里插入图片描述

观察到的现象是如果子类并不重写父类的虚函数,子类对象还是会生成虚函数表,但是子类对象和父类对象的虚函数表里存放的都是父类的虚函数指针

子类继承父类后,重写父类的虚函数,从监视窗口中可以看出,子类是重写了父类的虚函数,并且子类的虚函数表中会存放重写之后的虚函数的指针
在这里插入图片描述

运行时的多态

namespace mzt03 
{
    
    
	class A 
	{
    
    
	public:
		virtual void func() {
    
     cout << "void A::func()" << endl; }
		
	};
	class B : public A 
	{
    
    
	public:
		//子类继承父类后,重写父类的虚函数,满足多态条件1
		virtual void func() {
    
     cout << "void B::func()" << endl; }
	};

	void functest(A &ref) //父类引用调用虚函数,满足多态条件2
	{
    
    
		ref.func();
	}
	void test() 
	{
    
    
		A a;
		functest(a);//调用父类对象的func();
		B b;
		functest(b);//调用子类对象的func();
	}
}

满足多态条件后,构成多态,指针或者引用调用虚函数时,不是编译器确定,是在运行时通过指向的对象中的虚函数表中找到对应的虚函数并调用,引用的是父类对象就调用父类对象的虚函数,引用的是子类对象就调用子类对象的虚函数,跟实际传递的对象有关

注意:
如果不构成多态,调用的时候就是编译时确定的调用关系,主要看p的类型

总结:
1、如果构成多态,引用的是哪个对象那么就调用这个对象的虚函数,跟对象有关
在这里插入图片描述

2、如果不构成多态,对象类型是什么,调用的是该对象的函数,跟类型有关
在这里插入图片描述
思考构成多态为什么需要使用引用或者指针的方式去调用虚函数时发生多态是,使用父类对象不行吗?

测试代码

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

class Student : public Person {
    
    
public:
	virtual void BuyTicket() {
    
     cout << "买票-半价" << endl; }
	int _s;
};
void Func(Person& p) {
    
    
	p.BuyTicket();
}
int test()
{
    
    
	Person per;
	Func(per);

	Student stu;
	Func(stu);
	return 0;
}

因为子类重写了父类的虚函数,所以子类的虚函数表里存放的是重写后的虚函数的指针
在这里插入图片描述

透过监视窗口我们能得到的对象模型是这样的
在这里插入图片描述
以上构成了多态的第一个条件,再来看第二个条件,必须使用父类的引用或者指针指向重写虚函数的对象

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

在这里需要注意的是如果引用的是父类对象还好,就是对父类对象的取别名,但是如果是指向子类对象的话那会不太一样,考虑切片赋值和父类引用在调用的时候到底会使用谁的虚函数来达到多态的目的

先来看父类引用指向子类对象
在这里插入图片描述
透过监视窗口可以得出这么一个对象模型
在这里插入图片描述
我们会发现父类的引用使用的是子类的虚函数表,从而在虚函数表当中找出对应的虚函数

再看父类引用指向父类对象
在这里插入图片描述
透过监视窗口得到的对象模型就是这样的,父类引用指向父类对象读者理解为给父类对象取别名就行
在这里插入图片描述

通过父类对象中的_vfptr可以找到对应的虚函数,完成虚函数的调用,那么可以得出结论,父类引用指向父类对象的时候调用父类的虚函数,父类引用指向子类对象的时候调用子类对象的虚函数,从而达成多态的目的

总结:
父类指针或者引用切片时指向或者引用父类和子类对象中切片的那一部分,如果引用的是子类对象就调用子类对象的虚函数,如果是父类对象就调用父类对象的虚函数

而如果不使用父类的指针或者引用,直接使用父类对象时切片动作只会将成员变量的值给拷贝过去,_vfptr并不会拷贝过去,因为拷贝过去会发生混乱

同种类型的情况:两个父类对象共享一个虚函数表

在这里插入图片描述
通过监视窗口观察
在这里插入图片描述
透过监视窗口得到的对象模型

在这里插入图片描述
如果是同种类型的话那么就直接调用该父类的虚函数,两个对象共享的都是同一个虚函数表

再来看不是同一种类型的情况
在这里插入图片描述
透过监视窗口得到的对象模型
在这里插入图片描述

stu的_vfptr并没有拷贝到p对象中,只是将stu对象的成员变量的值拷贝进去,并且也会发生切片的动作,p对象中只是存放父类那一部分的成员,并且p对象会自己生成一个虚函数表,并调用父类的构造函数初始化这个虚函数表

虚函数表补充

测试代码:

namespace mzt04 
{
    
    
	class Base 
	{
    
    
	public:
		virtual void func1() {
    
     cout << "void Base::func()" << endl; }
		virtual void func2() {
    
     cout << "void Base::func()" << endl; }
		virtual void func3() {
    
     cout << "void Base::func()" << endl; }
		int b;
	};

	class car : public Base 
	{
    
    
	public:
		//子类继承父类后重写父类的虚函数
		virtual void func1() {
    
     cout << "void car::func1()" << endl; }
	};
	void func() 
	{
    
    
		Base b;
		car c;

	}
}

1、对象中的虚函数表指针是在什么时候初始化的?虚函数表又是在什么阶段生成的

在这里插入图片描述
当调用完构造函数后会使用初始化列表将虚函数表的指针初始化,而虚函数表是在编译时生成的
在这里插入图片描述
2、虚函数准确点来说并不是存放在虚函数表中的,虚函数表里存放的都是函数的地址,而虚函数和普通函数是一样的编译完后都是存放在代码段。

3、一个类的所以虚函数都会被存放在虚函数表中,虚函数表是存放在哪里的呢?

可以通过穷举的方式去推理虚函数表的存放位置,定义多个变量,但是这些个变量的存储区域又不一样,在打印类对象的地址 和变量的地址,通过地址间的比较越接近该对象的地址的存储区域就是虚函数表的存放位置

int aa; // 数据段/全局区
void func() 
{
    
    	//通过指针的视角看待虚函数表的地址
	Base b;
	Base* pb = &b; 

	int a = 10; //栈区
	int *pi = new int(); //堆区
	const char* str = "15454";  //代码段
	printf("栈区:%p \n",&a);
	printf("堆区:%p \n", pi);
	printf("代码段:%p \n", str);
	printf("vfptr:%p \n", *((int*)pb));//对象的前四个字节存放的是虚函数表的地址
	printf("数据段:%p \n", &aa);
}

结果:
在这里插入图片描述
结论:

虚函数表的地址是存放在代码区的

4、虚函数的重写/覆盖

虚函数的重写只是语法层面的意思,而虚函数的覆盖是原理层的意思

测试代码:

class Base 
{
    
    
public:
	Base() {
    
     cout << "Base()" << endl; }
	virtual void func1() {
    
     cout << "void Base::func()" << endl; }
	virtual void func2() {
    
     cout << "void Base::func()" << endl; }
	virtual void func3() {
    
     cout << "void Base::func()" << endl; }
	int b = 10;
};

class car : public Base 
{
    
    
public:
	//如果子类并没有重写父类的虚函数,那么就不会有覆盖的行为
	//virtual void func1() { cout << "void car::func1()" << endl; }
};

void test() 
{
    
    
	Base b; 
	car c;
}

透过监视窗口观察,子类对象在创建的时候,子类对象虚函数表的地址是通过从父类的虚函数表中拷贝一份函数地址再存放的,所以子类和父类中虚函数表里存放的都是一样的值
在这里插入图片描述
注意观察子类重写父类虚函数的现象

class car : public Base 
{
    
    
public:
	//子类重写父类的虚函数,会将原本在表中拷贝的那块函数地址给覆盖掉
	//可以通过监视窗口观察
	virtual void func1() {
    
     cout << "void car::func1()" << endl; }
};

子类重写虚函数后,会覆盖掉原来父类中的虚函数
在这里插入图片描述

虚函数表存放的内存布局,不同编译器处理并不一样,读者了解即可
在这里插入图片描述

动态绑定与静态绑定

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

单继承中的虚函数表

通过监视窗口是无法看到子类未被重写的虚函数的,虽然父类的虚函数可以被看到,下面我们使用代码来观察单继承中的虚函数表

class Base 
{
    
    
public:
	//Base() { cout << "Base()" << endl; }
	virtual void func1() {
    
     cout << "void Base::func1()" << endl; }
	virtual void func2() {
    
     cout << "void Base::func2()" << endl; }
	virtual void func3() {
    
     cout << "void Base::func3()" << endl; }
	int b = 10;
};

class car : public Base 
{
    
    
public:
	virtual void func1() {
    
     cout << "void car::func1()" << endl; }
	virtual void func4() {
    
     cout << "void car::func4()" << endl; }
	virtual void func5() {
    
     cout << "void car::func5()" << endl; }
};
//验证单继承下的虚函数表
typedef void(*Vfunc)(); //虚函数指针
void single(Vfunc *pf)
{
    
    
	int i = 0;
	while (pf[i] != nullptr) //虚函数表的最后一个元素存放的是NULL值,可以通过这个来判定结束位置
	{
    
    
		cout << pf[i] << endl;
		pf[i]();
		i++;
	}
}
void test() 
{
    
    
	Base b;
	Base* pb = &b;
	single( (Vfunc*)(*(int*)pb)); 
	//类对象的前四个字节就是虚函数表地址,这里可以通过指针类型
	//之间的相互转换得到虚函数表的地址

	cout << "--------------------------------------" << endl;

	car c;
	car* pc = &c;
	single((Vfunc*)(*(int*)pc));

}

在这里插入图片描述

多继承中的虚函数表

测试代码:

class A1 
{
    
    
public:
	virtual void func1() {
    
     cout << "void A1::func1()" << endl; }
	virtual void func2() {
    
     cout << "void A1::func2()" << endl; }
};

class A2
{
    
    
public:
	virtual void func1() {
    
     cout << "void A2::func1()" << endl; }
	virtual void func2() {
    
     cout << "void A2::func2()" << endl; }
};

class A3 : public A1, public A2 
{
    
    
public:
	//重写父类的虚函数
	virtual void func1() {
    
     cout << "void A3::func1()" << endl; }
	//子类自己的虚函数
	virtual void func3() {
    
     cout << "void A3::func3()" << endl; }
};
typedef void (*vfunc)();
void test(vfunc *p) 
{
    
    
	int i = 0;
	while (p[i] != nullptr) 
	{
    
    
		printf("vfunc:%p ",p[i]);
		p[i]();
		i++;
	}
	
}
void func() 
{
    
    
	A1 a1;
	A1* pa1 = &a1;
	test( (vfunc*)(*(int*)pa1));
	cout << "------------------------------------------------" << endl;
	A2 a2;
	A2* pa2 = &a2;
	test((vfunc*)(*(int*)pa2));
}

透过监视窗口可以看到A1类对象和A2类对象中都会存在一份虚函数表
在这里插入图片描述
这个继承体系下父类并没有什么影响,但如果是子类的话还是有差异的

测试代码:

void func() 
{
    
    
	A3 a;
	A3* pa = &a;
	test((vfunc*)*(int*)pa); //A1对象的虚函数表
	cout << "------------------------------------------------" << endl;
	test((vfunc*)(*(int*)((char*)&a + sizeof(A1)))); //A2对象的虚函数表
}

在这里插入图片描述
查看子类对象中的两张虚函数表
在这里插入图片描述

总结:
1、多继承下的子类对象会存在两张虚函数表,是存放两个父类的虚函数表的
2、如果子类重写父类的虚函数则会将父类的虚函数覆盖掉

猜你喜欢

转载自blog.csdn.net/m0_53421868/article/details/121770904
今日推荐