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

本文详细介绍了C++中的多态性概念,包括虚函数、重写、抽象类、虚函数表和动态绑定等原理。文章通过示例代码解释了如何通过虚函数实现多态,以及C++11中的`final`和`override`关键字的用途。此外,还讨论了多继承下的虚函数表和虚函数的注意事项,如虚函数不能是静态成员,构造函数不能是虚函数等。
摘要由CSDN通过智能技术生成

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 a,a调用函数,直接通过类域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;
}

通过调试,可以知道对象b1,b2,d中对应结构。
在这里插入图片描述
值得注意的是,对于子类对象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先继承C,C先继承A,所以先A初始化(初始化只进行一次),再C,再B,再D,所以最后打印
class A
class C
class B
class D

5、虚函数的注意

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

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

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

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

本章完~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值