【C++】多态详解(虚函数与重写、抽象类、多态原理、虚函数表)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1、多态的概念和定义

1.1 多态的概念

通俗上来说多态就是对于某种事情不同对象去做会有不同的状态。
例如不同身份的人去买火车票会有不一样的价格
面向对象程序上来说多态是在不同继承关系的类对象调用同一个函数产生不同的行为。

多态的构成条件

  1. 被调用的函数必须是虚函数并且子类必须对父类的虚函数进行重写。
  2. 必须通过父类的指针或引用调用虚函数。

不过上面还有两个问题一个是什么是虚函数以及重写是什么


1.2 虚函数与重写

虚函数virtual关键字修饰的类成员函数就是虚函数。
virtual只在声明时加上在类外实现不能加。
虚函数的作用是用来实现多态如果不实现多态就没必要弄成虚函数。
重写也叫覆盖父子类有相同的虚函数函数名返回值参数都相同称子类完成了对父类虚函数的重写。

//BuyTicket就是一个虚函数并且子类也对父类的这个函数进行了重写
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "Person --买票-全价" << endl;
	}
};

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

int main()
{
	Person p;
	Student s;
	
	//BuyTicket虚函数构成重写
	//并且由父类指针调用虚函数这就构成了多态
	Person* pp = &p;
	pp->BuyTicket(); //Person --买票-全价

	pp = &s;
	pp->BuyTicket(); //Student --买票-半价
	return 0;
}

virtual有一个例外的情况
在重写子类虚函数时可以不加virtual关键字因为子类继承父类函数的虚拟属性被保留了下来。
但是这个写法不规范不提倡使用

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

class Student : public Person
{
public:
	void BuyTicket()  //构成重写(覆盖)
	{
		cout << "Student --买票-半价" << endl;
	}
};

1.3 协变和析构函数的重写

协变在这里了解就行用不到。
子类重写父类虚函数时与父类虚函数返回值类型不一样。即父类虚函数返回值是指针或者引用子类虚函数返回子类对象的指针或引用称为协变。

父类虚函数返回类型也可以是别的父类的当然子类返回也可以是别的子类的。

class Person
{
public:
	virtual Person* BuyTicket() //协变 但是没用
	{
		cout << "Person --买票-全价" << endl;
		return this;
	}
};

class Student : public Person
{
public:
	virtual Student* BuyTicket() //协变 但是没用
	{
		cout << "Student --买票-半价" << endl;
		return this;
	}
};


析构函数的重写
先了解两个概念。

普通调用和多态调用
普通调用如果不构成多态对象类型是什么就直接通过类确定调用的位置。
多态调用在构成多态下调用跟本身对象有关不考虑类型通过对象确定调用的位置。

class A
{
public:
	virtual void func() 
	{ 
		cout << "A" << endl; 
	}
	void test()
	{
		cout << "A::test" << endl;
	}
};
class B :public A
{
public:
	virtual void func() 
	{ 
		cout << "B" << endl; 
	}

	virtual void test()
	{
		cout << "B::test" << endl;
	}
};

int main()
{
	A a;
	B b;
	//普通调用 不构成多态下 只和类型A有关
	A* ptr = &a;
	ptr->test(); //A::test
	ptr = &b;
	ptr->test(); //A::test

	//多态调用 构成多态下 只和对象本身有关
	ptr = &a;
	ptr->func(); //A
	ptr = &b;
	ptr->func(); //B
	return 0;
}

首先如果设计一个父类父类的析构函数一定无脑加virtual修饰。
目的是保证子类析构函数加不加virtual都能正确调用子类析构。

下面看原因

class Person
{
public:
	virtual ~Person()
	{
		delete[] _p;
		cout << "~Person()" << endl;
	}
protected:
	int* _p = new int[10];
};

class Student : public Person
{
public:
	virtual ~Student()
	{
		delete[] _s;
		cout << "~Student()" << endl;
	}
protected:
	int* _s = new int[20];
};

int main()
{
	//这种情况下正常析构
	Person p;
	Student s;
	
	//但以下情况如果给各自析构函数去掉virtual,就会造成内存泄漏
	//父类析构函数建议加virtual 为的就是避免这个场景造成内存泄漏
	Person* ptr = new Person;
	Person* str = new Student;
	delete ptr;
	//如果没有构成多态调用 就只凭借Person类调用Person析构函数
	//因为析构函数名都被处理成destructor,虚函数加三同构成重写
	delete str;
	return 0;
}

1.4 C++11的final和override

首先如果想要一个类不能被继承。
一个方法是将构造函数私有这样不能实例化也就没用。
另一个方法就是用final修饰类让这个类不能被继承。

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

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

int main()
{
	B b; //err
	return 0;
}

override的作用是用来检查子类的虚函数是否完成重写如果没有完成就会报错。
在我们对虚函数是否重写不确定下就可以添加。

class Car
{
public:
	virtual void Drive(int) {}
};

class Benz : public Car
{
public:
	virtual void Drive(int) override
	{}
};

int main()
{
	return 0;
}

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

在这里插入图片描述
重写相较于重定义的条件更加苛刻当重写缺少除了函数名相同条件外就构成重定义。

2、抽象类

了解抽象类前首先得了解纯虚函数。
在虚函数的后面写上 =0 则这个函数为纯虚函数。
包含纯虚函数的类才叫抽象类也叫接口类

子类必须对父类纯虚函数进行重写子类才能实例化对象。
抽象类的派生类如果不实现纯虚函数它也是抽象类因为子类不实现父类所有的纯虚函数则子类还属于抽象类仍然不能实例化对象

抽象类的出现强制了子类必须重写虚函数

//实际上抽象的东西 不需要有实例化对象 就可以定义为抽象类
//比如车品牌中车就是一个抽象没有车品牌
class Car
{
public:
	virtual void Drive() = 0; //像这样写就称为纯虚函数
};

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

int main()
{
	//Car c; //err  抽象类不能实例化出对象
	Car* p; //这样写没问题
	Benz z; //虚函数重写后 派生类就可以实例化
	return 0;
}

纯虚函数更体现了接口继承
普通函数是一种实现继承子类继承父类为了使用函数。
虚函数的继承是一种接口继承子类继承父类的接口为了重写形成多态接口继承的参数也会继承会继承缺省参数

一个经典题体现了虚函数的接口继承

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

int main()
{
	B* p = new B;
	p->test(); //结果就是B->1 多态调用调用的是类B中的func()但是接口继承后参数是1.
	return 0;
}

3、多态的原理

3.1 虚函数表

虚函数表的引出
问sizeof Base多大

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

结果是8字节原因是除了成员变量大小4字节外还有一个_vfptr指针它叫虚函数指针
一个含有虚函数的类至少都有一个虚函数指针这个指针指向一个虚函数表简称虚表来自代码段虚函数表中有着类中所有虚函数的地址。本质是一个函数指针数组

接着分析

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

通过调试我们看到确实是这样。
在这里插入图片描述

通过改造并且构成重写后。

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

	virtual void Func2()
	{
		cout << "Func2()" << endl;
	}
private:
	int _b = 1;
};

class Drive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b1;
	Base b2;
	//子类对象相较于父类对象模型多了一个_d并且子类对象中父类那一部分因为虚函数重写对应虚表位置被覆盖
	Drive d;
	return 0;
}

通过调试
在这里插入图片描述

首先可以看到同一类的不同对象用的是同一个虚函数表。
不同类的对象有各自的虚函数表。

由于func1函数构成了重写地址不同子类对象d在_vfptr[0]对原本父类的func1进行了覆盖。
func2函数就是_vfptr[1]

虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。
当然在Linux下不一样这只是在vs环境下
在这里插入图片描述

子类的虚表生成其实就是先把父类的拷贝过来如果重写了虚函数就将原来的覆盖自己的虚函数就再加上。

虚函数和虚表在哪个位置
虚函数不用说肯定在代码段那虚表呢

//检验虚表在哪个位置
int main()
{
	int a = 1;
	cout << "栈:" << &a << endl;

	int* pa = new int;
	cout << "堆:" << pa << endl;

	const char* str = "hello world";
	cout << "代码段/常量区:" << (void*)str << endl;

	static int b = 2;
	cout << "静态区/数据段:" << &b << endl;

	Base bb;
	Base* ptr = &bb;
	cout << (void*)(*((int*)ptr)) << endl;//取4个字节看虚函数指针的内容
	cout << (*((void**)ptr)) << endl;//适应不同平台这样写 

	//栈:0099F978
	//堆:00E19BD8
	//代码段 / 常量区 : 00589D80
	//静态区 / 数据段:0058C008
	//00589B34
	//00589B34
	//距离代码段比较近 所以在代码段
	return 0;
}

3.2 多态的原理

看下面代码

class A
{
public:
	virtual void func() 
	{ 
		cout << "A" << endl; 
	}
};
class B :public A
{
public:
	virtual void func() 
	{ 
		cout << "B" << endl; 
	}
};

void test(A& ra)
{
	ra.func();
}

int main()
{
	A a;
	B b;
	
	//指针调用
	A* ptr;
	ptr = &a;
	ptr->func(); //A
	ptr = &b;
	ptr->func(); //B
	
	//引用调用
	test(a); //A
	test(b); //B
	
	return 0;
}

多态的原理其实就是在类对象各自创建好自己的虚表后通过指针或引用直接访问对应对象的父类那一部分中的虚表从而访问不同的虚函数。

为什么重写的虚函数需要在虚表中覆盖是为了对象访问虚表时能调用不同虚函数。

为什么一定得指针或者引用
如果凭借以下方式调用
void test(A ra) { ra.func(); }
这个过程会产生新的对象而对象的产生依靠的是类A那就只能调用类A的对象的虚表了就没有多态了而指针和引用不产生新的对象。

3.3 动态绑定与静态绑定

再谈谈多态调用和普通调用。

普通调用凭借类型可以直接确定函数位置所以在编译时就确定了是一种静态绑定。如A aa调用函数直接通过类域A确定了

多态调用 运行时才确定 动态/绑定
多态调用在编译时不确定访问哪个类但是父类和子类的对象都有共同的父类那一份只要访问父类那一份调用虚表就行了而差别在于子类父类那部分的虚表是函数地址不一样。

静态绑定和动态绑定

C/C++中的静态都指的是编译时。
静态绑定是一种在编译期间就可以确定程序的行为也称静态多态。比如函数重载在编译期间通过不同的文件名描述方式找到函数。

动态绑定是在程序的运行期间通过具体拿到的类型确定程序具体的行为调用具体函数。

4、多继承中的虚函数表

前面知道虚函数表其实就是个函数指针数组并且子类继承的第一个父类的虚函数表指针在对应对象模型中其实就在开头个指针大小所以可以实现一个打印虚表。

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; }
private:
	int b;
};

typedef void(*VFPTR)(); //函数指针类型特殊定义方式

void Print(VFPTR* VFTable) //VFTable指针指向虚表
{
	for (int i = 0; VFTable[i] != nullptr; ++i) //vs下虚表结尾是空
	{
		printf("VFTable[%d]:[%p]", i, VFTable[i]);
		VFTable[i](); //顺便调用函数
	}
	cout << endl;
}

int main()
{
	Base b;
	Derive d;
	
	//强转为的是限制访问大小
	Print((VFPTR*)(*(int*)&b));
	Print((VFPTR*)(*(void**)&d)); //32和64位下都适应
	return 0;
}

多继承中的虚函数表
看代码

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

typedef void(*VFPTR)(); //函数指针类型特殊定义方式

void Print(VFPTR* VFTable) //VFTable指针指向虚表
{
	for (int i = 0; VFTable[i] != nullptr; ++i) //vs下虚表结尾是空
	{
		printf("VFTable[%d]:[%p]", i, VFTable[i]);
		VFTable[i](); //顺便调用函数
	}
	cout << endl;
}

int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;

	Print((VFPTR*)(*(void**)&b1)); //强转为的是限制访问大小
	Print((VFPTR*)(*(void**)&b2));
	Print((VFPTR*)(*(void**)&d)); //多继承自己的虚函数func3 只放第一个继承类的虚函数表
	//Print((VFPTR*)(*(void**)(((char*)&d)+sizeof(Base1)))); //没有func3 
	//或者
	Base2* ptr = &d;
	Print((VFPTR*)(*(void**)ptr));
	return 0;
}

通过调试可以知道对象b1b2d中对应结构。
在这里插入图片描述
值得注意的是对于子类对象d来说由于继承了两个父类自身会有两张虚表所对应的func3函数会按照继承顺序放在第一个继承类创造的虚表中。vs调试有优化没显示看打印
在这里插入图片描述
一个经典题考察继承顺序
下面会打印什么?

class A {
public:
	A(char* s) { cout << s << endl; }
	~A() {}
};
class B :virtual public A
{
public:
	B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
	C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public C, public B
{
public:
	D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl;
	}
};
int main() {
	D* p = new D((char*)"class A", (char*)"class B", (char*)"class C", (char*)"class D");
	delete p;
	return 0;
}

注意D的初始化顺序看的是类对象声明的顺序所以看继承的顺序D先继承CC先继承A所以先A初始化(初始化只进行一次)再C再B再D所以最后打印
class A
class C
class B
class D

5、虚函数的注意

1、虚函数如果被inline修饰虽然可以编译通过但是编译器会将inline属性默认忽略掉也就是不会有作用。因为虚函数要在虚表里找不然调用会出问题

2、虚函数不能是静态成员因为静态函数没有this指针没有this指针用类::访问静态函数没办法访问虚表。

3、构造函数不能是虚函数因为虚表指针是在构造函数初始化列表阶段才初始化的。

4、虚函数表在编译期间就生成了并且放在代码段。

本章完~

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++

“【C++】多态详解(虚函数与重写、抽象类、多态原理、虚函数表)” 的相关文章