C++ 多态

多态的概念

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

以买票为例:普通人 买票时,是全价 买票,学生 买票时,是半价买票

以扫码得红包为例:同样的扫码行为,不同的用户扫得到的不一样的红包,即多态

多态的定义及实现

多态的构成条件

如果一个Student类继承了Person类,两个类对象分别去调用买票这个函数,会得到不同的结果:

Person对象买票全价,Student对象买票半价

构成多态的条件:

1 必须通过基类的指针或者引用调用虚函数(父类的指针或者引用去调用虚函数)

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

虚函数

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

不是类成员函数却被virtual修饰会报错:

cpp 复制代码
class Person 
{
public:
	virtual void BuyTicket() //虚函数
	{
		cout << "买票-全价" << endl; 
	}
};

虚函数的重写

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

简单来说,父子继承关系的两个虚函数,三同(函数名/参数/返回值)

但是三同也有例外:协变 -->返回值可以不同 ,但是必须是父子类关系的指针或者引用

注意:在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,这也可以构成重写

因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性,但是此种写法不建议使用,不规范

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



class Student : public Person
{
public:
	virtual void BuyTicket()//规范写 or  void BuyTicket() 直接去掉virtual也行(但不建议)
	{
		cout << "Student 买票-半价" << endl;
	}
};

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

int main()
{
	Person p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

虚函数重写的两个例外:

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

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

实例1:

cpp 复制代码
class A {};
class B : public A {};

class Person 
{
public:
	virtual A* f() 
	{ 
		return new A; 
	}
};

class Student : public Person 
{
public:
	virtual B* f() 
	{ 
		return new B; 
	}
};

实例2:

cpp 复制代码
class Person 
{
public:
	virtual Person* f()
	{ 
		return new Person;
	}
};

class Student : public Person 
{
public:
	virtual Student* f()
	{ 
		return new Student;
	}
};

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

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,

都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,

看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处

理,编译后析构函数的名称统一处理成destructor

为什么派生类和基类的析构函数需要构成重写?来看下面这个例子:

cpp 复制代码
class Person
{
public:
	 ~Person() 
	{ 
		cout << "~Person()" << endl;
	}
};

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

int main()
{
	Person* p1 = new Person;//父类指针既可以指向父类对象
	Person* p2 = new Student;//也可以指向子类对象中父类的那部分成员
    // 析构是虚函数,才能正确调用析构函数
	// p1->destrutor() + operator delete(p1)
	delete p1;//本意是调用父类析构函数完成父类对象中资源的清理工作+释放父类对象的空间,可以做到正确调用父类析构函数,ok
    // p2->destrutor() + operator delete(p2)
	delete p2;//本意是调用子类析构函数完成子类对象中资源的清理工作+释放子类对象的空间,不能正确调用子类析构函数,而是根据指针类型为父类指针,调用了父类析构,不ok
	return 0;
}

析构函数是不是调用不合理呢?怎么没有调用子类的析构函数?这就是需要子类和父类的析构函数构成重写,从而构成多态的症结所在

只有实现多态调用,才能解决这种问题,多态的两个条件:

1 父类的指针/引用去调用虚函数

2 虚函数重写

以上实例中已经满足了条件1,父类的指针去调用析构函数

那么此时的析构函数需要为虚函数,且要满足虚函数重写,就可以成功解决问题:

cpp 复制代码
class Person
{
public:
	 virtual ~Person() 
	{ 
		cout << "~Person()" << endl;
	}
};

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

C++11 override 和 final

C++11提供了override和final两个关键字,可以帮助用户检测虚函数是否重写

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

联想到之前的知识:final 修饰类,不能被继承

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

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

抽象类

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

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

类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承

简单总结:1 纯虚函数间接强制去派生类去重写

2 抽象类-不能实例化出对象

cpp 复制代码
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;
	}
};

int main()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

接口继承和实现继承

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

现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口 ,目的是为了重写,达成

多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

多态的原理

虚函数表

在介绍虚函数表之前,先看一下以下实例:

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

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

b的大小是8字节(32位平台下),通过监视窗口来看一下:

可以发现,b这个对象除了_b成员,还多了一个_vfptr(虚函数表指针),v代表virtual,f代表function

一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址 要被放到虚函数表 中,虚函数表也简称虚表

再来看一段代码:

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

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

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

监视窗口观察:

子类重写了父类的虚函数func1,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖, 覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法

虚函数表本质是一个存虚函数指针的数组(即虚函数指针数组),一般情况下这个数组的最后放了一个nullptr(如vs下是这样的),不是虚函数就不会被放进虚表中,如父类的func3

子类虚表的生成:1 将父类虚表的内容拷贝一份到子类虚表中

2 若是子类重写了父类中的某个虚函数,则用子类自己的虚函数覆盖虚表中父类的虚函数

3 子类自己新增加的虚函数按其在子类中的声明次序增加到子类虚表的最后

注意:虚表存的是虚函数指针,不是虚函数!对象中存的是虚表指针,不是虚表!

虚函数和普通函数一样,都存在代码段,虚表在vs下也是存在于代码段的

多态的原理

下面是多态调用的实例:

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

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

void Func(Person* p)
{
	p->BuyTicket();
}

int main()
{
	Person p;
	Func(&p);
	Student s;
	Func(&s);
	return 0;
}

在满足多态的条件下,多态调用为什么能够做到:以指针指向的对象/引用的对象为标准,去调用指向/引用对象中的虚函数呢?

p指向父类对象时,在父类对象的虚表中找到的虚函数是:Person::BuyTicket

p指向子类对象时,在子类对象的虚表中找到的虚函数是:Student::BuyTicket

这就是多态的原理,也就实现出了不同对象去完成同一行为时,展现出不同的形态

满足多态后的函数调用,不是在编译时确定的,是运行起来以后到对象的虚表中去找的

即去虚表里找到函数地址,确定地址,再调用

不满足多态的函数调用(普通调用)是编译链接时就确认好的

证明:

再来思考:为什么构成多态的条件之一可以是父类的指针/引用去调用虚函数,但父类的对象去调用虚函数却不行呢?

Person p = s (切割出(形象的说法)子类对象中父类的那一部分成员拷贝给父类,但是不会拷贝虚函数表指针)

假设父类对象=子类对象时会拷贝虚函数表指针,此时便有一个问题:

多态调用,指向父类,要调父类的虚函数,但调用的一定是父类的虚函数吗?

答案是不一定,因为也拷贝了子类对象的虚表指针,所以无法保证指向父类调用父类虚函数,因此也就无法构成多态调用,所以假设失败

也就无法做到根据对象的不同调用不同的虚函数,当Person p = s,想要调用子类虚函数,却无法做到,因为p对象里没有子类虚表的指针

动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为 ,也称为静态多态

如函数重载

动态绑定又称后期绑定(晚绑定),是在程序运行期间 ,根据具体拿到的类型确定程序的具体

行为,调用具体的函数 ,也称为动态多态

**普通调用:**根据类型去调用,如,A类的指针去调用函数,就调用A类里的某个函数

多态调用: 看指针指向的对象/引用的对象是谁,就去调用它里面所对应的函数

如指向父类调用父类的函数,指向子类调用子类的函数

单继承和多继承关系的虚函数表

单继承中的虚函数表

思考一个问题:虚函数的地址一定会被放进类的虚函数表吗?

答案:是的

看下面一段代码:

cpp 复制代码
class Base 
{
public:
	virtual void func1() 
	{ 
		cout << "Base::func1" << endl;
	}

	virtual void func2()
	{ 
		cout << "Base::func2" << endl;
	}
private:
	int a = 1;
};

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

	virtual void func3()
	{ 
		cout << "Derive::func3" << endl; 
	}

	virtual void func4() 
	{
		cout << "Derive::func4" << endl;
	}

private:
	int b = 2;
};

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

在监视窗口中我们发现看不见func3和func4,那是不是说明不是所有的虚函数的指针都会存到虚表中? 答案是否定的,所有虚函数的指针都会存到虚表里

这里看不到子类虚表中的func3和func4的地址是因为编译器的监视窗口故意隐藏了这两个函数,监视窗口有时看到的不一定是真实的,但是内存窗口所看到的一定是真实的

调用内存窗口看一看:

d的虚表指针所指向的虚表有4个虚函数地址,前两个我们可以高度肯定就是虚函数func1和func2

但是后两个虚函数地址我们不能很肯定它们就是func3和func4,高度怀疑中......

那么来打印看看:(32位平台下)

取出b、d对象的头4字节,就是虚表的指针,虚函数表本质是虚函数指针数组,这个数组最后面放了一个nullptr

1 取b/d的地址,强转成一个int*的指针(指针的类型决定该指针可以看多大)

2 解引用取值,取到了b对象头4字节的值,即虚表的指针

3 强转成VFUNC*,虚表是存VFUNC类型的数组,虚表的指针即VFUNC的地址

4 打印虚表,虚表是数组,要传递数组名,数组名是首元素(VFUNC)的地址,即传递VFUNC*

cpp 复制代码
typedef void(*VFUNC)();//虚函数指针类型
void PrintVFT(VFUNC* a)//
{
	for (int i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p-> ", i, a[i]);
		VFUNC f = a[i];
		f();//调用虚函数
	}
	printf("\n");

}

int main()
{
	Base b;
	PrintVFT((VFUNC*)(*((int*)&b)));
	Derive d;
	PrintVFT((VFUNC*)(*((int*)&d)));

	return 0;
}

现在可以证实确实是所有虚函数的地址都会放进类的虚表里

多继承中的虚函数表

cpp 复制代码
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(*VFUNC)();
void PrintVFT(VFUNC* a)
{
	for (int i = 0; a[i] != 0; i++)
	{
		printf("[%d]:%p->", i, a[i]);
		VFUNC f = a[i];
		f();
	}
	printf("\n");
}

int main()
{
	
	Derive d;
	PrintVFT((VFUNC *)(*(int*)&d));//打印第一张虚表
	Base2* ptr = &d;
	PrintVFT((VFUNC*)(*(int*)ptr));//打印第二张虚表
	return 0;
}

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

两张虚表里子类重写的func1是同一个,但为什么地址不同呢?(原理仅了解)

同一个的证明:

cpp 复制代码
    Derive d;
	Base1* ptr = &d;
	ptr->func1();
	Base2* ptr2 = &d;
	ptr2->func1();

原理:

考察问题:

1 什么是多态?

静态多态:函数重载

动态多态:a 父类的指针/引用去调用虚函数 b 虚函数完成重写

指向谁调用谁的虚函数,实现多种形态

2 inline函数可以是虚函数吗?

可以,普通调用,inline起作用,多态调用,inline不起作用,因为编译器会忽略inline属性,这个函数就不再是inline,因为虚函数的地址要放到虚表中去

cpp 复制代码
class Base {
public:
	Base()
		:a(10)
	{

	}

	inline virtual void func1() { cout << "Base::func1" << endl; }
	//virtual static void func2() { cout << "Base::func2" << endl; }
private:
	int a = 0;
};

class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
	void func5() { cout << "Derive::func5" << endl; }
private:
	int b;
};

int main()
{
	Base* ptr = new Base;
	ptr->func1();

	Base b;
	b.func1();

	return 0;
}

多态调用:inline不起作用

普通调用:inline起作用

3 静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,它可以指定类域调用,无法构成多态,没有意义

4 构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的

若是构造函数是虚函数,虚函数多态调用,要到虚表中去找,但是虚表指针都还未初始化

5 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

可以,场景:父类对象=new 子类对象,detele 父类指针(此时只有析构函数构成重写,delete父类指针时,构成多态调用,才能正确调用子类析构函数,而不是像普通调用一样直接调用父类析构函数(这不是我们想要的))

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

若是普通调用,则一样快

若是多态调用,则调用普通函数快些,因为调用虚函数,运行时要到虚表中去找

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

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

相关推荐
霁月风13 分钟前
设计模式——适配器模式
c++·适配器模式
jrrz082835 分钟前
LeetCode 热题100(七)【链表】(1)
数据结构·c++·算法·leetcode·链表
咖啡里的茶i1 小时前
Vehicle友元Date多态Sedan和Truck
c++
海绵波波1071 小时前
Webserver(4.9)本地套接字的通信
c++
@小博的博客1 小时前
C++初阶学习第十弹——深入讲解vector的迭代器失效
数据结构·c++·学习
爱吃喵的鲤鱼2 小时前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++
7年老菜鸡2 小时前
策略模式(C++)三分钟读懂
c++·qt·策略模式
Ni-Guvara3 小时前
函数对象笔记
c++·算法
似霰3 小时前
安卓智能指针sp、wp、RefBase浅析
android·c++·binder
芊寻(嵌入式)3 小时前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习