C++ 多态篇


在这里插入图片描述

1. 多态的概念和实现


1.1 概念

面向对象语言三大特性之一:多态,意思是多种形态,当不同的对象去做同一件事时会有不同的状态/结果

1.2 实现

多态要在继承的关系中实现,需要满足2个条件:

  1. 子类必须完成父类虚函数的重写
  2. 必须通过父类的指针/引用去调用虚函数

在这里插入图片描述

virtual修饰的成员函数叫做虚函数

子类对父类虚函数的重写需要满足三同,即函数名、返回值、参数类型相同,但有三种情况例外

  1. 协变:返回值可以是其他父子类或自身的指针/引用
  2. 析构函数
  3. 子类重写父类虚函数时可以不加virtual

我们分别对这三点做解释

1.2.1 协变

父子类的返回值可以不相同,但必须是其他父子类后者自身类的指针/引用

在这里插入图片描述

1.2.2 析构函数

在继承的章节,我们说析构函数名被统一处理为destructor(),但没说具体原因,其实是要对析构函数设计多态的原因

在这里插入图片描述

在上面的场景中,ptr1和ptr2分别指向动态开辟出来的A对象和B对象;B对象中的一个成员,指向动态开辟出来的内存;将来我们要手动释放ptr1和ptr2指向的内存,于是使用delete ptr1和delete ptr2,但根据结果,没能释放掉B对象,于是导致了内存泄漏的问题

我们希望的是指针指向哪个对象,就delete哪个对象的析构,这就要求我们对析构函数实现多态调用,而多态调用要满足三同,其中函数名已经不满足了,于是编译器就将父子类的析构函数重命名为destructor(),只要我们将析构函数定义为虚函数,就满足多态了

因此,父子类的析构函数推荐加上virtual

在这里插入图片描述

1.2.3 子类虚函数不加virtual

在这里插入图片描述

可以看到,子类虚函数可以不加virtual修饰,此时BuyTicket函数也满足多态,这是因为重写的本质是对父类虚函数实现的重写;用一道例题来更好的理解这点

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;
}
// A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

在这里插入图片描述

2. C++11 finaloverride


3.1 final

如果希望一个类不能被继承,有两种方式:

  1. 将类的构造函数私有化,这是C++98的做法
  2. 将类进行final修饰,被修饰的类叫做最总类,这是C++11的做法

在这里插入图片描述

3.2 override

override关键字用于检查子类的虚函数是否完成父类虚函数的重写

在这里插入图片描述

3. 函数重载、重写与隐藏


在这里插入图片描述

4. 多态的原理


在这里插入图片描述

上述代码的结果出乎我们的意料,为什么A对象的大小是12字节?经过调试,我们发现A对象中多了一个指针_vfptr

直接给出结论,如果类中有虚函数,那么该类实例化出的对象都要有一个指针,我们把该指针称为虚表指针(在构造函数初始化列表中初始化);该指针指向一个虚表,虚表是一个函数指针数组,存放着虚函数的地址

子类对象会将父类的虚函数拷贝到自己的虚表中,并检查是否完成虚函数的重写,如果完成,将虚函数覆盖为自己的虚函数,当使用父类的引用/指针去调用虚函数时,由于发生切片,如果指向父类,去调用父类的虚函数,如果指向子类,去调用子类中父类部分的虚函数,这就是多态的原理

在这里插入图片描述

多态调用和正常调用区别:当进行编译时,编译器会检查函数是否满足多态,如果满足,那么会在执行代码时去对象的虚表中找虚函数调用,如果不满足,那么在编译时就已经确定要调用函数的地址

在这里要分清楚几个概念:

  1. 虚函数不在虚表中,虚表中存放的是虚函数的地址,虚函数存放在常量区(代码段)上
  2. 虚表存放在常量区上,下面有代码验证
  3. 虚基表是继承中存放虚基类偏移量的,用来解决菱形继承数据冗余和二义性的问题

在这里插入图片描述

5. 抽象类


在这里插入图片描述

在虚函数后加上 =0,表示该虚函数是纯虚函数,有纯虚函数的类叫做抽象类

抽象类在现实生活中没有对应的实体,因此无法实例化出对象;继承抽象类的子类同样也无法实例化出对象,这就强制要求我们完成父类虚函数的重写

在这里插入图片描述

6.单继承和多继承的虚表


6.1 单继承

在这里插入图片描述

运行上述代码,对比监视窗口和内存窗口,父类的func1和func2都继承了下来,重写了func1,于是将父类的func1覆盖;子类自身的虚函数直接往父类部分的虚表中放,监视窗口没显示func3和func4是因为vs监视窗口的设计问题,在内存中是能看到还有两个指针,这两个指针指向的就是func3和func4,用一份代码来证明

在这里插入图片描述

6.2 多继承

在这里插入图片描述

在多继承中,子类中有两个虚表指针,分别在A类的部分和B类的部分中,A类的部分继承A类的虚函数,B类的部分继承B类的虚函数,再看是否完成重写,有就拿自身的覆盖;自身的虚函数默认往先继承类的虚表中放

在这里插入图片描述

为什么要有多继承对象要有两张虚表,不能把所有的虚函数放在一张虚表中吗?有这样的场景,分别有两个父类的指针指向同一个子类对象,通过任何一个父类指针都应该调用子类的虚函数,但如果只有一张虚表,就变成只有其中一个父类指针能调用子类的虚函数

在这里插入图片描述

7. 菱形继承的虚表(了解)


7.1 菱形继承

在这里插入图片描述

在这里插入图片描述

7.2 菱形虚拟继承

在这里插入图片描述

在这里插入图片描述

8. 多态面试题


class A 
{
public:
	A(const char* s)
	{ 
		cout << s << endl; 
	}
};

class B :virtual public A
{
public:
	B(const char* s1,const char* s2) 
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};
class C :virtual public A
{
public:
	C(const char* s1, const char* s2)
		:A(s1) 
	{ 
		cout << s2 << endl; 
	}
};

class D :public B, public C
{
public:
	D(const char* s1, const char* s2, const char* s3, const char* s4)
		:B(s1, s2)
		,C(s1, s3)
		,A(s1)
	{
		cout << s4 << endl;
	}
};

int main()
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

// A:class A class B class C class D 
// B:class D class B class C class A
// C:class D class C class B class A 
// D:class A class C class B class D

// 答案:A

由此可以证明:初始化列表的初始化顺序是按变量在内存中声明的顺序或类在内存中继承的顺序(先继承的类先初始化);同时同一个对象中,同一个类只会初始化一次

  1. inline函数可以是虚函数吗?

    可以,但是编译器会忽略inline属性,因为虚函数要放到虚表中

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

    不可以,静态成员函数属于整个类,无论是通过父类或子类调用,调用的都是同一个静态成员函数;静态成员函数在编译时就已经确定地址,而虚函数要到运行时去虚函数表中找

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

    不可以,虚表指针在构造函数的初始化列表中初始化,如果构造函数时虚函数,虚表指针就无法初始化了

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

    如果是普通对象,一样快;如果是对象的指针/引用且满足多态,则调用普通函数快,调用虚函数需要在虚表中找

  5. 虚函数表在什么阶段生成?存在哪里?

    虚函数表在编译阶段生成,存放在常量区

  • 11
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言实现多态的方式有两种常见的方法: 函数指针和结构体。函数指针是一种将函数作为参数传递或返回函数的地址的方法,可以实现不同函数根据需要动态调用的效果。结构体则是通过定义不同的结构体类型,并将它们作为参数进行传递,从而实现多态的效果。 在C语言中,使用函数指针实现多态可以通过定义一个函数指针类型,并将不同的函数赋值给该函数指针,从而在调用函数时可以根据需要动态选择调用不同的函数。以一种简单的示例来说明,假设我们有一个形状的结构体,其中包含一个面积计算函数指针。我们可以定义多个不同的函数来计算不同形状的面积,然后将这些函数赋值给同一个函数指针,这样就可以在调用计算面积函数时根据传入的形状选择相应的函数进行计算。 另一种实现多态的方式是使用结构体。我们可以定义一个基础的结构体类型,其中包含一个标识符字段和一个函数指针字段。然后,我们可以定义多个继承自基础结构体的派生结构体类型,每个派生结构体类型都有自己的标识符和函数指针。通过将不同的派生结构体作为参数传递给函数,并在函数内部根据传入的标识符选择相应的函数指针进行调用,就可以实现多态的效果。 总结起来,C语言中实现多态的方式可以通过函数指针和结构体来实现。函数指针可以根据需要动态选择调用不同的函数,而结构体则可以通过定义不同的派生结构体类型,并在函数中根据传入的标识符选择相应的函数指针进行调用。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++教程 - How to C++系列专栏第0](https://blog.csdn.net/cat_bayi/article/details/132147427)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值