431-C++基础语法(31-40)

本文详细探讨了C++中的多态性实现,包括虚函数的使用、虚表和虚表指针的概念。解释了构造函数不能为虚函数的原因,强调析构函数为何必须为虚函数以避免内存泄漏。同时,介绍了构造函数和析构函数的执行顺序以及它们调用虚函数的注意事项。此外,还涵盖了模板的编译过程和使用限制,以及构造函数和析构函数中异常处理的策略。
摘要由CSDN通过智能技术生成

31、C++的多态如何实现

C++的多态性:

  • 基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。
#include <iostream>
using namespace std;

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

class Son1 : public Base{
public:
	virtual void fun() override{
		cout << " Son1::func()" <<endl;
	}
};

class Son2 : public Base{

};

int main()
{
	Base* base = new Son1;
	base->fun();
	base = new Son2;
	base->fun();
	delete base;
	base = NULL;
	return 0;
}
// 运行结果
// Son1::func()
// Base::func()

例子: Base为基类,其中的函数为虚函数。子类1继承并重写了基类的函数,子类2继承基类但没有重写基类的函数,从结果分析子类体现了多态性,那么为什么会出现多态性,其底层的原理是什么?

虚表和虚基表指针的概念:

  • 虚表: 虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表。
  • 虚表指针:含有虚函数的类实例化对象时对象地址的前四个字节存储的指向虚表的指针。
    在这里插入图片描述

上图中展示了虚表和虚表指针在基类对象和派生类对象中的模型,下面阐述实现多态的过程:

  • 1、编译器在发现基类中虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址
  • 2、编译器会在每个对象的前四个字节中保存一个虚函数表指针,即vptr指向对象所属类的虚函数表。(在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数。)
  • 3、在派生类定义对象时,程序运行会自动调用构造函数在构造函数中创建虚表并对虚表初始化
  • 4、在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;
  • 5、当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面。

这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性

32、构造函数能否声明为虚函数或者纯虚函数,析构函数呢?

析构函数:

  • 析构函数可以为虚函数,并且一般情况下基类析构函数要定义为虚函数。
  • 只有在基类析构函数定义为虚函数时,调用操作符delete销毁指向对象的基类指针时,才能准确调用派生类的析构函数(从该级向上按序调用虚函数),才能准确销毁数据。
  • 析构函数可以是纯虚函数,含有纯虚函数的类是抽象类,此时不能被实例化。但派生类中可以根据自身需求重新改写基类中的纯虚函数。

构造函数:

  • 根据《effective C++》的条款09:绝不在构造和析构过程中调用虚函数可知,在构造函数中虽然可以调用虚函数,但是强烈建议不要这样做。
  • 因为基类的构造的过程中,虚函数不能算作是虚函数。若构造函数中调用虚函数,可能会导致不确定行为的发生
  • 虚函数对应一个vtable(虚函数表),类中存储一个vptr指向这个vtable。如果构造函数是虚函数,就需要通过vtable调用,可是对象没有初始化就没有vptr无法找到vtable,所以构造函数不能是虚函数

33、基类的虚函数表存放在内存的什么区,虚表指针vptr的初始化时间

首先整理一下虚函数表的特征:

  • 虚函数表全局共享的元素,即全局仅有一个,在编译时就构造完成
  • 虚函数表类似一个数组类对象中存储vptr指针指向虚函数表,即虚函数表不是函数,不是程序代码,不可能存储在代码段
  • 虚函数表存储虚函数的地址,即虚函数表的元素是指向类成员函数的指针,而类中虚函数的个数在编译时期可以确定,即虚函数表的大小可以确定,即大小是在编译时期确定的,不必动态分配内存空间存储虚函数表,所以不在堆中

根据以上特征,虚函数表类似于类中静态成员变量静态成员变量也是全局共享,大小确定,因此最有可能存在全局数据区,测试结果显示:

  • 虚函数表 vtable在Linux/Unix中存放在可执行文件只读数据段中(rodata);
  • 微软编译器将虚函数表存放在常量段

由于虚表指针vptr跟虚函数密不可分,对于有虚函数或者继承于拥有虚函数的基类,对该类进行实例化时,在构造函数执行时会对虚表指针进行初始化,并且存在对象内存布局的最前面

一般分为五个区域栈区堆区函数区(存放函数体等二进制代码)、全局静态区常量区

C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区

34、构造函数、析构函数、虚函数可否声明为内联函数

首先,将这些函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联

register关键字: 这个关键字请求编译器尽可能的将变量存在CPU内部寄存器中,而不是通过内存寻址访问,以提高效率。

举个例子:

#include <iostream>
using namespace std;
class A
{
public:
    inline A() {
		cout << "inline construct()" <<endl;
	}
    inline ~A() {
		cout << "inline destruct()" <<endl;
	}
    inline virtual void  virtualFun() {
		cout << "inline virtual function" <<endl;
	}
};
 
int main()
{
	A a;
	a.virtualFun();
    return 0;
}
//输出结果
//inline construct()
//inline virtual function
//inline destruct()

构造函数和析构函数声明为内联函数是没有意义的!

《Effective C++》中所阐述的是:

  • 将构造函数和析构函数声明为inline是没有什么意义的,即编译器并不真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。
  • 其次,class中的函数默认是inline型的,编译器也只是有选择性的inline,将构造函数和析构函数声明为内联函数是没有什么意义的。

将虚函数声明为inline,要分情况讨论:

有的人认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定的,而虚函数是运行期决定的

即在不知道将要调用哪个函数的情况下,如何将函数内联呢?

上述观点看似正确,其实不然!

如果虚函数在编译器就能够决定将要调用哪个函数时,就能够内联,那么什么情况下编译器可以确定要调用哪个函数呢,答案是当用对象调用虚函数(此时不具有多态性)时,就内联展开。

综上,当是指向派生类的指针(多态性)调用声明为inline的虚函数时不会内联展开;当是**对象本身调用虚函数时,会内联展开**,当然前提依然是函数并不复杂的情况下

35、C++模板是什么,你知道底层怎么实现的?

1、编译器并不是把函数模板处理成能够处理任意类的函数;

  • 编译器从函数模板通过具体类型产生不同的函数;编译器会对函数模板进行两次编译:在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译

2、因为函数模板要被实例化后才能成为真正的函数,在使用函数模板的源文件中包含函数模板的头文件,如果该头文件中只有声明,没有定义,那编译器无法实例化该模板,最终导致链接错误(头文件中需要有声明和定义)。

36、构造函数为什么不能为虚函数?析构函数为什么要虚函数?

1、构造函数为什么不能为虚函数?

I、从存储空间角度

  • 虚函数对应一个指向vtable虚函数表的vptr,这大家都知道,但是这个vptr事实上是存储在对象的内存空间的。

  • 问题出来了,假设构造函数是虚的,就须要通过 vptr找到vtable里的虚函数地址来调用,但是对象还没有实例化,也就是内存空间还没有,没有vptr,找不到vtable呢?所以构造函数不能是虚函数

II、从使用角度

  • 虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数
  • 构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数

III、从实现上看

  • vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数
  • 并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数

2、析构函数为什么要虚函数?

C++中基类采用virtual虚析构函数是为了防止内存泄漏

  • 具体地说,如果派生类中申请了内存空间,需要在其析构函数中对这些内存空间进行释放。

  • 如果基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。

  • 那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏

  • 所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

37、析构函数的作用,如何起作用?

析构函数 用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;

特点: 析构函数与构造函数同名,但该函数前面加~。

  • 析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。

  • 当撤销对象时,编译器也会自动调用析构函数。

  • 每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。

  • 一般析构函数定义为类的公有成员。

38、构造函数和析构函数可以调用虚函数吗,为什么?

  • 在C++中,提倡不在构造函数和析构函数中调用虚函数;
  • 构造函数和析构函数调用虚函数时都不使用动态联编,如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本;
  • 因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编;
  • 析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。

39、构造函数、析构函数的执行顺序?构造函数和拷贝构造的内部都干了啥?

构造函数顺序:

  • 1、基类构造函数。如果有多个基类,则构造函数的调用顺序某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
  • 2、成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
  • 3、派生类构造函数。

析构函数顺序:

  • 调用派生类的析构函数;
  • 调用成员类对象的析构函数;
  • 调用基类的析构函数。

40、构造函数、析构函数可否抛出异常?

抛出异常:

  • 抛出异常(也称为抛弃异常)即检测是否产生异常,在C++中,其采用throw语句来实现,如果检测到产生异常,则抛出异常
  • 抛出异常实际是作为另一种返回值来使用的。 抛出异常的好处一是可以不干扰正常的返回值,另一个是调用者必须处理异常,而不像以前c语言返回一个整数型的错误码,调用者往往将它忽略了。

1、构造函数是否抛出异常?

构造函数中尽量不要抛出异常,能避免的就避免,如果必须,要考虑不要内存泄露!

  • 构造函数中抛出异常,会导致析构函数不能被调用,但对象本身已申请到的内存资源会被系统释放(已申请到资源的内部成员变量会被系统依次逆序调用其析构函数)。
  • 因为析构函数不能被调用,所以可能会造成内存泄露或系统资源未被释放。
  • 构造函数中可以抛出异常,但必须保证在构造函数抛出异常之前,把系统资源释放掉,防止内存泄露。(如何保证???使用auto_ptr???)

2、析构函数可否抛出异常?

1)如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题

2)通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liufeng2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值