C++多态

目录

1.多态的概念

2.多态的定义和实现

2.1多态的构成条件

2.2虚函数的重写

2.3虚函数重写的两个例外:

2.4 C++11 override 和 final

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

3. 抽象类

3.1 概念

3.2 接口继承和实现继承

4.多态的原理

4.1虚函数表

4.2多态原理

4.3 动态绑定与静态绑定

5.多继承关系的虚函数表


1.多态的概念

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


2.多态的定义和实现

2.1多态的构成条件

多态是在不同继承关系的类对象去调用同一函数,产生了不同的行为

那么在继承中要构成多态还有两个条件

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

虚函数:即被virtual修饰的类成员函数称为虚函数。

2.2虚函数的重写

虚函数的重写或者叫覆盖

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

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.3虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

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

2. 析构函数的重写(基类与派生类析构函数的名字不同)

这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,然后还是满足函数名字相同

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

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

int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
	delete p1;
	delete p2;
	return 0;
}

2.4 C++11 override 和 final

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

1. final:修饰虚函数,表示该虚函数不能再被重写,若重写编译报错(在基类虚函数修饰)

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

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

 报错:"Person::~Person": 声明为 "final" 的函数不能由 "Student::~Student" 重写

2. override(重写覆盖) 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。(在派生类虚函数修饰)

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

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

报错:“Student::~Student”: 包含重写说明符“override”的方法没有重写任何基类方法

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


3. 抽象类

3.1 概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

包含纯虚函数的类叫做抽象类(也叫接口类)抽象类不能实例化出对象派生类继承后也不能实例化出对象只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

纯虚函数可以不是空函数,有函数体。

抽象类的派生类,也可以不重写。

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 Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

3.2 接口继承和实现继承

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

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

例:

class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
};

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main()
{
    B* p = new B;
    p->test();
    return 0;
}


4.多态的原理

4.1虚函数表

在32位平台下,sizeof(Base)等于多少?

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

int main()
{
	Base b;
	cout << sizeof(Base) << endl;
	return 0;
}

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表

虚函数表本质是一个存虚函数指针的指针数组,在vs下这个数组最后面放了一个nullptr。

总结一下派生类的虚表生成(编译阶段):

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

虚函数指针生成在构造函数的初始化列表(编译器自动)

虚函数存在代码段,虚表存在代码段

虚函数表打印:

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

	virtual void Func()
	{
		cout << _id << endl;
		BuyTicket();
	}
protected:
	int _id = 1;
};

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

protected:
	int _id = 2;
};

typedef void(*VFPtr) ();

void PrintVFTable(VFPtr* p)
{
	for (int i = 0; p[i] != nullptr; ++i)
	{
		printf("[%d] -> %p\n", i, p[i]);
	}
	cout << endl;
}

int main()
{
	Person p;
	Student s;

	PrintVFTable(*(VFPtr**)&p);
	PrintVFTable(*(VFPtr**)&s);
	return 0;
}

4.2多态原理

通过汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用是编译时确认好的。

eax存虚函数指针

edx存虚表指针

4.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。


5.多继承关系的虚函数表

多继承派生类的虚表个数与继承基类个数相关

多继承派生类的虚表生成顺序与继承声明顺序有关

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表的最后

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};

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

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

typedef void(*VFPtr) ();

void PrintVFTable(VFPtr* p)
{
	cout << " 虚表地址:" << p << endl;
	for (int i = 0; p[i] != nullptr; ++i)
	{
		printf("[%d] -> %p ", i, p[i]);
		VFPtr f = p[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base1 b1;
	Base2 b2;
	PrintVFTable(*(VFPtr**)&b1);
	PrintVFTable(*(VFPtr**)&b2);

	Derive d;
	VFPtr* vTableb1 = *(VFPtr**)&d;
	PrintVFTable(vTableb1);

	Base2* b = &d;
	VFPtr* vTableb2 = *(VFPtr**)b;
	PrintVFTable(vTableb2);
	return 0;
}


面试题:

1.什么是多态?

多种形态,为了完成某种行为,不同的对象调用会产生不同的结果

2.什么是重载、重写(覆盖)、重定义(隐藏)?

重载是在同一类域中,函数名相同,参数不同,本质上是利用C++函数的修饰规则实现的调用

重定义体现在继承关系中,分别在基类和派生类的两个函数,且函数名相同就构成重定义,基类的函数被隐藏,可以显示调用

重写是在重定义的基础上,两个函数都是虚函数,且函数名,返回值类型和参数类型都相同就构成重写

3.多态的实现原理?

在包含虚函数的类,在编译过程中,会在代码段(常量区)生成一张虚函数表,里面包含虚函数的指针。

通过这个类定义的对象,被实例化的过程中,编译器会通过初始化列表定义一个虚表指针指向这个虚表。

基类对象的指针或引用调用虚函数时,编译器就会通过对象的虚表指针进一步找到虚函数的地址

4. 虚函数可以加inlin吗?

可以,加inline可以,但加了也没用

要变成内联函数,由于内联函数是直接展开代码,并不存在函数调用,即没有函数地址,但如果要是虚函数就必须要有虚表,虚表就是存虚函数地址的

是内联函数就不是虚函数,是虚函数就不是内联函数

5.静态成员函数可以是虚函数吗

不行,静态成员函数不可以是虚函数。静态函数是属于类的,不属于对象本身,静态成员函数没有this指针,就找不到虚函数表指针。

6.构造函数可以是虚函数吗?

不行,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。实例化对象需要构造函数,调用构造函数需要虚表指针,虚表指针还没有被实例化出来

7.析构函数可以是虚函数吗?

可以,并且最好把基类的析构函数定义成虚函数。

8. 对象访问普通函数快还是虚函数更快?

首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为多态调用,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

10.C++菱形继承的问题?虚继承的原理?

数据冗余和二义性

在虚继承的基类成员统一开辟一块区域存储,用虚基表记录偏移量,通过虚基表指针找到偏移量进而找到

11.什么是抽象类?抽象类的作用?

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)

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

纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值