4、虚函数相关问题

4.1、面向对象三大特性

​ 面向对象的三大特性:封装、继承、多态。具有相同性质的对象,可以抽象为类。封装就是指将属性(变量)和行为(函数)作为一个整体,表现一个对象。继承,是指类与类之间的特殊关系,下级成员除了拥有上级成员的共性,还有自己的特点,减少重复的代码。多态,就是指多种形态,主要分为静态多态和动态多态,静态多态就是指重载(函数重载和运算符重载)也就是编译多态,动态多态是指继承关系的派生类、子类重写父类中的虚函数实现的运行多态。

1、封装
1)封装的概念

封装就是把数据和代码捆绑在一起,避免外界干扰和不确定性访问,也就是客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法采用private修饰。

2)实现方式
  • 数据封装:保护数据成员,不让类外的程序直接访问或修改,只能通过提供的公共的接口访问;
  • 方法封装:方法的细节是对用户隐藏的,只要接口不变,内容的修改不会影响到外部的调用者;
  • 对象的方法可以接收对象外的消息;
  • 对象外面不能直接访问对象的属性,只能通过和该属性对应的方法访问;
  • 当对象含有完整的属性和与之对应的方法时称之为封装。
2、继承
1)继承的概念

继承就是让某种类型对象获得另一个类型对象的属性和方法。

​ 继承方式一共有三种:公共继承、保护继承、私有继承(公共就是公共访问,保护就是子类访问,私有就是自己才能访问)C++中默认继承方式是private。

​ 私有权限,子类永远不可以访问;保护继承,则子类都是保护属性;公共继承,则子类属性不变,除了不可访问私有。先私有继承之后,再进行公共继承,依然不可以访问,因为第一次私有继承之后所有成员属性都变为私有。

  • public:该数据成员、成员函数是对所有用户开放的,所有用户都可以进行调用;
  • protected:对于子女、朋友来说,就是public的,可以自由使用,没有任何限制,而对于其他的外部class,protected就变成private;
  • private:私有,除了class自己之外,其他都不可以直接使用;
2)常见的继承方式
  • 实现继承:指使用基类的属性和方法而无需额外编码的能力
  • 接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
  • 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力(C++里好像不怎么用)
3、多态

​ 多态:同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为**(重载实现编译时多态,虚函数实现运行时多态)**。 多态一般分为:静态多态、动态多态,还有一个模板template。

  • 静态多态通过重载实现,函数重载(重载参数个数不同,重载参数类型不同或者参数类型顺序不同),运算符重载(operator)。在编译阶段就可以确定函数入口地址。
  • 动态多态通过虚函数实现。地址晚绑定,是在运行阶段确定的。
    • 在基类的函数前面+virtual,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应函数。派生类–派生类,基类–基类。虚函数具有虚函数表和虚函数指针:

      • 虚函数表:类中含有virtual关键字修饰的方法,编译器会自动生成虚函数表。
      • 虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚函数表的指针。
1)多态的底层原理
  • 编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址;
  • 编译器会在每个对象的前四个字节中保存一个虚表指针(vptr),指向对象所属类的虚表。在构造时,根据对象类型去初始化虚表指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数;
  • 在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,先调用父类的构造函数,此时,编译器只看到父类,并为父类对象初始化虚表指针,令其指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令其指向子类的虚表;
  • 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面。
2)静态绑定与动态绑定
  • 静态类型:对象在声明时采用的类型,在编译期既已确定

  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;

  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;

  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;

    非虚函数一般都是静态绑定,而虚函数都是动态绑定

3)区别
  • 静态绑定发生在编译期,动态绑定发生在运行期;
  • 对象的动态类型可以更改,但是静态类型无法更改;
  • 要想实现动态,必须使用动态绑定;
  • 在继承体系中只有虚函数使用的是动态绑定,其他的全部是静态绑定;
4)建议

​ 绝对不要重新定义继承而来的非虚(non-virtual)函数(《Effective C++ 第三版》条款36),因为这样导致函数调用由对象声明时的静态类型确定了,而和对象本身脱离了关系,没有多态,也这将给程序留下不可预知的隐患和莫名其妙的BUG;另外,在动态绑定也即在virtual函数中,要注意默认参数的使用。当缺省参数和virtual函数一起使用的时候一定要谨慎,不然出了问题怕是很难排查。

5)引用能否实现动态绑定?

​ 可以。引用在创建的时候必须初始化,在访问虚函数时,编译器会根据其所绑定的对象类型决定要调用哪个函数。注意只能调用虚函数。

6)A类有B类的对象,B类有A类对象怎么释放

​ 主函数中只实例化A a。会发现先调用B的构造函数,然后构造A的构造函数,之后再调用A的析构函数,最后是B的析构函数。(还是根据实例化对象来的,因为实例化了A,A中有B,所以要先构造B才行,释放了A,那B也没用了,所以B后析构)

**4.2、**虚函数表

1、实现

​ 假设有一个基类ClassA,一个继承了该基类的派生类ClassB,并且基类中有虚函数,派生类实现了基类的虚函数。
我们在代码中运用多态这个特性时,通常以两种方式起手:

  • ClassA *a = new ClassB();
  • ClassB b; ClassA *a = &b;

以上两种方式都是用基类指针去指向一个派生类实例,区别在于第1个用了new关键字而分配在堆上,第2个分配在栈上。

​ 以左图为例,ClassA *a是一个栈上的指针。该指针指向一个在堆上实例化的子类对象。基类如果存在虚函数,那么在子类对象中,除了成员函数与成员变量外,编译器会自动生成一个指向**该类的虚函数表(这里是类ClassB)**的指针,叫作虚函数表指针。通过虚函数表指针,父类指针即可调用该虚函数表中所有的虚函数。

2、类的虚函数表与类实例的虚函数指针

​ 首先不考虑继承的情况。如果一个类中有虚函数,那么该类就有一个虚函数表。
​ 这个虚函数表是属于类的,所有该类的实例化对象中都会有一个虚函数表指针去指向该类的虚函数表。
​ 从第一部分的图中我们也能看到,一个类的实例要么在堆上,要么在栈上。也就是说一个类可以有很多很多个实例。但是!一个类只能有一个虚函数表。在编译时,一个类的虚函数表就确定了,这也是为什么它放在了只读数据段中。

​ 原文链接:https://blog.csdn.net/qq_36359022/article/details/81870219

1)虚函数表指针的创建时机

​ 虚函数指针跟着对象走,所以对象什么时候创建出来,vptr就什么时候创建出来,就是运行的时候才决定。当程序在编译期间,编译器会为构造函数中增加为vptr赋值的代码(这是编译器的行为),当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么这个构造函数里有为这个对象的vptr赋值的语句。

2)虚函数表创建的时机

​ 虚函数表创建时机是在编译期间。编译期间编译器就为每个类确定好了对应的虚函数表里的内容。所以在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针,所以,这个虚函数表指针就有值了。

3、虚继承

​ 由于C++支持多继承,除了public、protected和private三种继承方式外,还支持虚拟(virtual)继承。

class A{}
class B : virtual public A{};
class C : virtual public A{};
class D : public B, public C{};
int main()
{cout << "sizeof(A):" << sizeof A <<endl; // 1,空对象,只有一个占位cout << "sizeof(B):" << sizeof B <<endl; // 4,一个bptr指针,省去占位,不需要对齐cout << "sizeof(C):" << sizeof C <<endl; // 4,一个bptr指针,省去占位,不需要对齐cout << "sizeof(D):" << sizeof D <<endl; // 8,两个bptr,省去占位,不需要对齐
}

​ 上述代码所体现的关系是,B和C虚拟继承A,D又公有继承B和C,这种方式是一种菱形继承或者钻石继承,可以用如下图来表示:

​ **虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。**虚拟继承基类的子类中,子类会增加某种形式的指针,或者指向虚基类子对象,或者指向一个相关的表格;表格中存放的不是虚基类子对象的地址,就是其偏移量,此类指针被称为bptr,如上图所示。如果既存在vptr又存在bptr,某些编译器会将其优化,合并为一个指针。

4、多继承的优缺点
  • C++允许为一个派生类指定多个基类,这样的继承结构被称做多重继承。
  • 多重继承的优点很明显,就是对象可以调用多个基类中的接口;
  • 如果派生类所继承的多个基类有相同的基类,而派生类对象需要调用这个祖先类的接口方法,就会容易出现二义性
  • 加上全局符确定调用哪一份拷贝。比如pa.Author::eat()调用属于Author的拷贝。
  • 使用虚拟继承,使得多重继承类Programmer_Author只拥有Person类的一份拷贝。
5、纯虚函数

​ 包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。

class <类名> { virtual <类型><函数名>(<参数表>)=0; … };

​ 在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。这就是纯虚函数的作用。 纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

纯虚函数引入原因
  • 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
  • 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

​ 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;)。若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重载以实现多态性。同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。例如,绘画程序中,shape作为一个基类可以派生出圆形、矩形、正方形、梯形等, 如果我要求面积总和的话,那么会可以使用一个 shape * 的数组,只要依次调用派生类的area()函数了。如果不用接口就没法定义成数组,因为既可以是circle ,也可以是square ,而且以后还可能加上rectangle,等等.

6、抽象类

​ 抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层。

1)抽象类的定义

​ 称带有纯虚函数的类为抽象类。

2)抽象类的作用

​ 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。

3)抽象类的使用

​ 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。抽象类是不能定义对象的。一个纯虚函数不需要(但是可以)被定义。

7、虚函数的代价
  • 带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类
  • 带有虚函数的类的每一个对象,都会有有一个指向虚表的指针,会增加对象的空间大小
  • 不能再是内敛的函数,因为内敛函数在编译阶段进行替代,而虚函数表示等待,在运行阶段才能确定到低是采用哪种函数,虚函数不能是内敛函数。
8、哪些函数不能为虚函数?
  • 构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
  • 内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
  • 静态函数,静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
  • 友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
  • 普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
9、虚函数与纯虚函数的区别
  • 虚函数是为了实现动态编联产生的,目的是通过基类类型的指针指向不同对象时,自动调用相应的、和基类同名的函数(使用同一种调用形式,既能调用派生类又能调用基类的同名函数)。虚函数需要在基类中加上virtual修饰符修饰,因为virtual会被隐式继承,所以子类中相同函数都是虚函数。当一个成员函数被声明为虚函数之后,其派生类中同名函数自动成为虚函数,在派生类中重新定义此函数时要求函数名、返回值类型、参数个数和类型全部与基类函数相同。

  • 纯虚函数只是相当于一个接口名,但含有纯虚函数的类不能够实例化。纯虚函数首先是虚函数,其次它没有函数体,取而代之的是用“=0”。既然是虚函数,它的函数指针会被存在虚函数表中,由于纯虚函数并没有具体的函数体,因此它在虚函数表中的值就为0,而具有函数体的虚函数则是函数的具体地址。一个类中如果有纯虚函数的话,称其为抽象类。抽象类不能用于实例化对象,否则会报错。抽象类一般用于定义一些公有的方法。子类继承抽象类也必须实现其中的纯虚函数才能实例化对象。

10、虚函数可能带来的安全问题?
  • 父类指针调用子类函数,父类指针在析构的时候,不会调用子类中的析构函数,要是子类中有堆区属性,就会出现内存泄漏;
  • 通过父类指针访问子类自己的虚函数。任何妄图使用父类指针调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,但是,在运行时,我们可以通过指针的方式访问虚函数表来达到违法C++语言的行为;
  • 访问non-public的虚函数。父类的虚函数是private或者是protected,这些非public的虚函数同样也会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方法来访问这些non-public的虚函数。

4.3、相关构造函数

1、四种构造函数
1)无参构造函数

​ 创建一个类,没有写任何构造函数,系统会自动生成无参构造函数,函数为空。如果希望有这样一个无参构造函数,需要自己显式地写出来。

2)一般构造函数

​ 一个类可以有多个一般构造函数,类似于重载函数,创建对象时根据传入参数的不同调用不同的构造函数;

3)拷贝构造函数

​ 参数为对象本身的应用,根据一个已经存在的对象复制出来一个新的该类的对象。若没有写拷贝构造函数,系统会默认一个拷贝构造函数,当类中有指针成员的时候,系统默认创建的构造函数会造成浅拷贝的问题。

4)转换构造函数

​ 转换构造函数的作用是将一个其他类型的数据转化成一个类的对象。转换构造函数只能有一个参数。若是不行让转换构造函数生效,拒绝其他类型通过转换构造转换为本类型,在转换构造函数前面加上explicit。

class Complex
{
public:double m_real;double m_img;Complex(void)  //无参构造{m_real = 0.0;m_img = 0.0;}Complex(double real, double img)//一般构造{m_real = real;m_img = img;}Complex(const Complex& c) //拷贝构造{m_real = c.m_real;m_img = c.m_img;}Complex(int i){m_real = i;m_img = 0.0;}
};int main()
{Complex c1(7, 8);//普通构造函数c1 = 9; //转换构造Complex c2 = 10;  //转换构造cout << c1.m_real << " " << c1.m_img << endl; //输出9 0 cout << c2.m_real << " " << c2.m_img << endl; //输出10 0
}
2、构造函数的执行顺序
  • 在派生类构造函数中,所有的虚基类及上一层基类的构造函数调用;
  • 对象的vptr被初始化;
  • 如果有成员初始化列表,将在构造函数体内扩展开来,这必须在vptr被设定之后才做;
  • 执行程序员所提供的代码;
3、一个类中的全部构造函数的扩展过程
  • 记录在成员初始化列表中的数据成员初始化操作会被放在构造函数的函数体内,并与成员的声明顺序为顺序;
  • 如果一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么默认构造函数必须被调用;
  • 如果class有虚表,那么它必须被设定初值;
  • 所有上一层的基类构造函数必须被调用;
  • 所有虚基类的构造函数必须被调用。
4、为什么拷贝构造函数必须传引用?
1)拷贝构造作用

​ 拷贝构造函数的作用就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例。

2)参数传递过程到底发生了什么?

​ 将地址传递和值传递统一起来,归根结底还是传递的是"值"(地址也是值,只不过通过它可以找到另一个值)!

3)值传递
  • 对于内置数据类型的传递时,直接赋值拷贝给形参(注意形参是函数内局部变量);

  • 对于类类型的传递时,需要首先调用该类的拷贝构造函数来初始化形参(局部对象);

    如void foo(class_type obj_local){}, 如果调用foo(obj); 首先class_type obj_local(obj) ,这样就定义了局部变量obj_local供函数内部使用

4)引用传递

​ 无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).

​ 上述1) 2)回答了为什么拷贝构造函数使用值传递会产生无限递归调用,内存溢出。

​ 拷贝构造函数用来初始化一个非引用类类型对象,如果用传值的方式进行传参数,那么构造实参需要调用拷贝构造函数,而拷贝构造函数需要传递实参,所以会一直递归。

5、什么情况下调用拷贝构造函数?
  • 用类的一个实例化对象去初始化另一个对象的时候

  • 函数的参数是类的对象时(非引用传递)

  • 函数的返回值是函数体内局部对象的类的对象时 ,此时虽然发生(Named return Value优化)NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数

    第三种情况在Linux g++下则不会发生拷贝构造函数,不仅如此即使返回局部对象的引用,依然不会发生拷贝构造函数

总结:即使发生NRV优化的情况下,Linux+ g++的环境是不管值返回方式还是引用方式返回的方式都不会发生拷贝构造函数,而Windows + VS2019在值返回的情况下发生拷贝构造函数,引用返回方式则不发生拷贝构造函数。 在c++编译器发生NRV优化,如果是引用返回的形式则不会调用拷贝构造函数,如果是值传递的方式依然会发生拷贝构造函数。

6、如何禁止程序自动生产拷贝构造函数?
  • 为了阻止编译器默认生成拷贝构造函数和拷贝赋值函数,我们需要手动去重写这两个函数,某些情况下,为了避免调用拷贝构造函数和拷贝赋值函数,我们需要将他们设置成private,防止被调用。
  • 类的成员函数和friend函数还是可以调用private函数,如果这个private函数只声明不定义,则会产生一个连接错误;
  • 针对上述两种情况,我们可以定一个base类,在base类中将拷贝构造函数和拷贝赋值函数设置成private,那么派生类中编译器将不会自动生成这两个函数,且由于base类中该函数是私有的,因此,派生类将阻止编译器执行相关的操作。
7、构造函数、拷贝构造函数、赋值运算符区别
1)构造函数

​ 对象不存在,没用别的对象初始化,在创建一个新的对象时调用构造函数。

2)拷贝构造函数

​ 对象不存在,但是使用别的已经存在的对象来进行初始化。

3)赋值运算符

​ 对象存在,用别的对象给它赋值,这属于重载“=”号运算符的范畴,“=”号两侧的对象都是已存在的。

4)拷贝构造与赋值运算符区别
  • 拷贝构造函数是函数,赋值运算符是运算符重载
  • 拷贝构造函数会生成新的类对象,赋值运算符不能。
  • 拷贝构造函数是直接构造一个新的类对象,所以在初始化对象前不需要检查源对象和新建对象是否相同;赋值运算符需要上述操作并提供两套不同的复制策略,另外赋值运算符中如果原来的对象有内存分配则需要先把内存释放掉。
  • 形参传递是调用拷贝构造函数(调用的被赋值对象的拷贝构造函数),但并不是所有出现"="的地方都是使用赋值运算符
8、什么情况自动调用默认构造函数?
  • 带有默认构造函数的类成员对象,如果一个类没有任何构造函数,但它含有一个成员对象,而后者有默认构造函数,那么编译器就为该类合成出一个默认构造函数。不过这个合成操作只有在构造函数真正被需要的时候才会发生;如果一个类A含有多个成员类对象的话,那么类A的每一个构造函数必须调用每一个成员对象的默认构造函数而且必须按照类对象在类A中的声明顺序进行;
  • 带有默认构造函数的基类,如果一个没有任务构造函数的派生类派生自一个带有默认构造函数基类,那么该派生类会合成一个构造函数调用上一层基类的默认构造函数;
  • 带有一个虚函数的类
  • 带有一个虚基类的类
  • 合成的默认构造函数中,只有基类子对象和成员类对象会被初始化。所有其他的非静态数据成员都不会被初始化。
9、移动构造函数

​ 有时候我们会遇到这样一种情况,我们用对象a初始化对象b后对象a我们就不在使用了,但是对象a的空间还在呀(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a的空间呢?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷;

​ 拷贝构造函数中,对于指针,我们一定要采用深层复制,而移动构造函数中,对于指针,我们采用浅层复制;

​ C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况;

​ 与拷贝类似,移动也使用一个对象的值设置另一个对象的值。但是,又与拷贝不同的是,移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。这里未命名的对象就是那些临时变量,甚至都不会有名称。典型的未命名对象就是函数的返回值或者类型转换的对象。使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:当使用一个临时变量对象进行构造初始化的时候,调用移动构造函数。类似的,使用未命名的变量的值赋给一个对象时,调用移动赋值操作。

4.4、构造函数和析构函数相关问题

1、析构函数的作用
  • 构造函数只是起初始化值的作用,但实例化一个对象的时候,可以通过实例去传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数就是你不写,编译器也自动调用一次。
  • 析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~。析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。 当撤销对象时,编译器也会自动调用析构函数。每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员。
2、什么时候析构函数不会被调用?
1)exit()调用线程

​ 来自显而易见的事物,即exit(),kill signal,power failure等。

#include <stdio.h>class EmbeddedObject {private:char *pBytes;public:EmbeddedObject() {pBytes = new char[1000];}~EmbeddedObject() {printf("EmbeddedObject::~EmbeddedObject()\n");delete [] pBytes;}
};class Base {public:~Base(){printf("Base::~Base()\n");}
};class Derived : public Base {private:EmbeddedObject emb;public:~Derived() {printf("Derived::~Derived()\n");}
};int main (int argc, const char * argv[])
{Derived *pd = new Derived();// later for some good reason, point to it using Base pointerBase* pb = pd;delete pb;
}

~Base()将被调用,但~Derived()不会。这意味着~Derived()中的代码不会执行。它可能需要做一些重要的事情。同样它的EmbeddedObject的析构函数应该被自动调用但不是。因此,EmbeddedObject没有机会释放其动态分配的数据。这会导致内存泄漏。

​ 解决方案:在Base virtual中创建析构函数

class Base {public:virtual ~Base() {}
};
2)未处理的例外并退出

​ 不会为无限循环范围之外的对象调用析构函数。

3)TerminateProcess()

​ 如果使用placement new创建对象,则不会自动调用此对象的析构函数。

4)常见的编程错误
  • 使用创建动态对象数组 object* x = new object[n],但使用delete x而非delete[] x;释放
  • 而不是在对象上调用 delete()而不是调用 free()。虽然通常释放内存,但不会调用析构函数。
  • 假设您有一个应该声明虚拟析构函数的对象层次结构,但由于某种原因不是。如果其中一个子类实例被转换为层次结构中的不同类型然后被删除,则它可能不会调用所有析构函数。
  • 在由于抛出异常而被调用的另一个析构函数中抛出异常。

​ 原文链接:https://www.thinbug.com/q/8733894

3、构造函数、析构函数、虚函数是否可以声明为 inline 函数

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

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

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

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

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

​ 有的人认为虚函数被声明为inline,但是编译器并没有对其内联,他们给出的理由是inline是编译期决定的,而虚函数是运行期决定的,即在不知道将要调用哪个函数的情况下,如何将函数内联呢?上述观点看似正确,其实不然,如果虚函数在编译器就能够决定将要调用哪个函数时,就能够内联,那么什么情况下编译器可以确定要调用哪个函数呢,答案是当用对象调用虚函数(此时不具有多态性)时,就内联展开。

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

4、构造函数为什么不能为虚函数?析构函数呢?
1)从存储空间角度

​ 虚函数相应一个指向vtable虚函数表的指针,这大家都知道,但是这个指向vtable的指针事实上是存储在对象的内存空间的。问题出来了,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

2)从使用角度

​ 虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。虚函数的作用在于通过父类的指针或者引用来调用它的时候可以变成调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。

3)从实现上看

​ vbtl在构造函数调用后才建立,因而构造函数不可能成为虚函数从实际含义上看,在调用构造函数时还不能确定对象的真实类型(由于子类会调父类的构造函数);并且构造函数的作用是提供初始化,在对象生命期仅仅运行一次,不是对象的动态行为,也没有必要成为虚函数。

4)从构造函数角度

构造函数不须要是虚函数,也不同意是虚函数,因为创建一个对象时我们总是要明白指定对象的类型,虽然我们可能通过实验室的基类的指针或引用去訪问它但析构却不一定,我们往往通过基类的指针来销毁对象。这时候假设析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。

当一个构造函数被调用时,它做的首要的事情之中的一个是初始化它的VPTR,因此,它仅仅能知道它是“当前”类的,而全然忽视这个对象后面是否还有继承者。当编译器为这个构造函数产生代码时,它是为这个类的构造函数产生代码——既不是为基类,也不是为它的派生类(由于类不知道谁继承它)。所以它使用的VPTR必须是对于这个类的VTABLE。

​ 并且,仅仅要它是最后的构造函数调用,那么在这个对象的生命期内,VPTR将保持被初始化为指向这个VTABLE, 但假设接着另一个更晚派生的构造函数被调用,这个构造函数又将设置VPTR指向它的VTABLE,直到最后的构造函数结束。

VPTR的状态是由被最后调用的构造函数确定的。这就是为什么构造函数调用是从基类到更加派生类顺序的还有一个理由。可是,当这一系列构造函数调用正发生时,每一个构造函数都已经设置VPTR指向它自己的VTABLE。假设函数调用使用虚机制,它将仅仅产生通过它自己的VTABLE的调用,而不是最后的VTABLE(全部构造函数被调用后才会有最后的VTABLE)。因为构造函数本来就是为了明确初始化对象成员才产生的,然而virtual function主要是为了再不完全了解细节的情况下也能正确处理对象。另外,virtual函数是在不同类型的对象产生不同的动作,现在对象还没有产生,如何使用virtual函数来完成你想完成的动作

5)从析构函数角度

​ C++中基类采用virtual虚析构函数是**为了防止内存泄漏。**具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。

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

5、构造函数、析构函数的执行顺序?
1)构造函数顺序
  • 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
  • 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
  • 派生类构造函数。
2)析构函数顺序
  • 调用派生类的析构函数;
  • 调用成员类对象的析构函数;
  • 调用基类的析构函数。
6、虚析构函数的作用,父类的析构函数是否要设置为虚函数?

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

​ 具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。

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

2)纯虚析构函数一定得定义,因为每一个派生类析构函数会被编译器加以扩张,以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数。因此,缺乏任何一个基类析构函数的定义,就会导致链接失败,最好不要把虚析构函数定义为纯虚析构函数

7、虚析构和纯虚析构

​ 因为虚函数是父类指针指向子类对象。因此,父类指针在析构的时候,不会调用子类中的析构函数,要是子类中有堆区属性,就会出现内存泄漏。因此需要把父类中的析构函数改成虚析构。虚析构、纯虚析构需要声明也需要实现。纯虚析构(=0),该类属于抽象类,无法实例化对象。 纯虚析构需要声明也需要实现,因为父类中也有可能有堆区开辟的属性,所以无论时纯虚析构还是虚析构必须有代码实现。解决父类指针释放子类对象不干净的问题

​ 所以,父类中用了虚析构,子类就不用虚析构。因为C++规定,当一个成员函数被声明为虚函数后,其派生类的同名函数都自动成为虚函数。所以基类的析构函数要使用虚函数。

8、构造函数析构函数可否抛出异常?
  • C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,在对象b的构造函数中发生异常,对象b的析构函数不会被调用。因此会造成内存泄漏。
  • 用auto_ptr对象来取代指针类成员,便对构造函数做了强化,免除了抛出异常时发生资源泄漏的危机,不再需要在析构函数中手动释放资源;
  • 如果控制权基于异常的因素离开析构函数,而此时正有另一个异常处于作用状态,C++会调用terminate函数让程序结束;
  • 如果异常从析构函数抛出,而且没有在当地进行捕捉,那个析构函数便是执行不全的。如果析构函数执行不全,就是没有完成他应该执行的每一件事情。
9、类什么时候析构?
  • 对象生命周期结束,被销毁时;
  • delete指向对象的指针时,或delete指向对象的基类类型指针,而其基类虚构函数是虚函数时;
  • 对象i是对象o的成员,o的析构函数被调用时,对象i的析构函数也被调用。
10、构造函数或析构函数中可以调用虚函数吗?

​ 简要结论:从语法上讲,调用完全没有问题。但是从效果上看,往往不能达到需要的目的。

​ 《Effective C++》的解释是:派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。 同样,进入基类析构函数时,对象也是基类类型。

4.5、成员初始化列表

1、类成员初始化方式
1)赋值初始化

​ 通过在函数体内进行赋值初始化

2)列表初始化

​ 在冒号后使用初始化列表进行初始化。

3)主要区别

​ 对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员赋值,就是说初始化这个数据成员此时函数体还未执行。

2、为什么用成员初始化列表更快?

​ 赋值初始化是在构造函数当中做赋值的操作,而列表初始化是做纯粹的初始化操作。我们都知道,C++的赋值操作是会产生临时对象的。临时对象的出现会降低程序的效率。用初始化列表会快一些的原因是,对于类型,它少了一次调用构造函数的过程,而在函数体中赋值则会多一次调用。而对于内置数据类型则没有差别。

​ 由于对象成员变量的初始化动作发生在进入构造函数之前,对于内置类型没什么影响,但如果有些成员是类,那么在进入构造函数之前,会先调用一次默认构造函数,进入构造函数后所做的事其实是一次赋值操作(对象已存在),所以如果是在构造函数体内进行赋值的话,等于是一次默认构造加一次赋值,而初始化列表只做一次赋值操作。

3、哪些情况必须用成员列表初始化?作用是什么?
1)必须使用成员初始化的四种情况
  • 当初始化一个引用成员时;
  • 当初始化一个常量成员时;
  • 当调用一个基类的构造函数,而它拥有一组参数时;
  • 当调用一个成员类的构造函数,而它拥有一组参数时;
2)作用
  • 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
  • list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;
4、如何阻止一个类被实例化?
  • 将类定义为抽象基类或者将构造函数声明为private;
  • 不允许类外部创建类对象,只能在类内部创建对象。

C++中虚函数相关问题(面经总结)相关推荐

  1. C++中虚函数、虚指针和虚表详解

    关于虚函数的背景知识 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数. 存在虚函数的类都有一个一维的虚函数表叫做虚表.每一个类的对象都有一个指向虚表开始的虚指针.虚表是和类对应的 ...

  2. C++ 在继承中虚函数、纯虚函数、普通函数,三者的区别

    C++ 在继承中虚函数.纯虚函数.普通函数,三者的区别 1.虚函数(impure virtual) C++的虚函数主要作用是"运行时多态",父类中提供虚函数的实现,为子类提供默认的 ...

  3. SDN Overlay网络中虚机到物理机的数据包的转发

    在之前的文章里我们讨论了SDN Overlay 网络中5个不同场景下虚机数据包如何转发,今天我们将继续讨论处于Overlay网络中的虚机如何与物理机进行数据转发.有关于微软网络虚拟化HNV的相关概念, ...

  4. c 语言中虚方法有什么作用是什么,虚函数的作用?

    定义 定义:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数[1] 语法:virtual 函数返回类型 函数名(参数表) {函数体;} 用途:实现多态性,通过指向派生类的 ...

  5. java中虚函数_虚函数

    程序示例 例如,一个基类 Animal 有一个虚函数 eat.子类 Fish 要实做一个函数 eat(),这个子类 Fish 与子类 Wolf 是完全不同的,但是你可以引用类别 Animal 底下的函 ...

  6. C++中虚析构函数的作用及原理

    C++中虚析构函数的作用及原理 先测测你哟,上代码

  7. C++中虚继承产生的虚基类指针和虚基类表,虚函数产生的虚函数指针和虚函数表

    本博客主要通过查看类的内容的变化,深入探讨有关虚指针和虚表的问题. 一.虚继承产生的虚基类表指针和虚基类表 如下代码:写一个棱形继承,父类Base,子类Son1和Son2虚继承Base,又来一个类Gr ...

  8. C#中虚函数,抽象,接口的简单说明

    虚函数:由virtual声明,它允许在派生类中被重写,要重写方法,必须先声名为virtual public class myclass { public virtual int myint() { 函 ...

  9. 多继承中虚基类构造函数的一种调用规则

    规则:如果父类中有虚基类(A),且有一个直接基类(B)是虚基类的子类,那么子类(C或D)若不显式调用虚基类的有参数构造函数,它的直接基类(B)即使在构造列表中调用了非默认构造函数,那么也会直接调用虚基 ...

最新文章

  1. VB6.0使用ADO对象连接数据库
  2. Django startproject的问题
  3. wordpress搭建个人博客
  4. python教程输入_python怎么输入一个集合
  5. 从产品角度谈如何搞定主动用户与被动用户
  6. 将某表某列数据复制到另一张表的某列
  7. 遥感数字图像处理——第三章——空间域处理方法
  8. Restorator 导致win8或win8.1 打开程序提示不支持此接口的解决方法
  9. 【老生谈算法】matlab遗传算法工具箱源码——遗传算法
  10. 无线WIFI短信认证平台(互亿无线)
  11. Kconfig中select与depends on原理
  12. ASP.NET Core Razor 页面入门
  13. 预装WIN8的电脑是GPT分区模式,无法安装WIN7
  14. 前端vue点击切换(黑夜/白天模式)主题最新(源码)
  15. Html中如何自定义Video显示的长宽比
  16. ECCV 2020 best paper: RAFT算法解析
  17. 华为5G手机+鸿蒙系统,还能这么玩儿?
  18. 32 | KafkaAdminClient:Kafka的运维利器
  19. java语言保留结构和联合_Java 语言中取消了联合概念,保留了结构概念。( )_学小易找答案...
  20. MySQL的定时任务详解

热门文章

  1. 伯朗特机器人编程语言_机器人的最佳编程语言是什么?机器人十大流行编程语言汇总...
  2. Xubuntu虚拟机系统终端下载速度太慢解决方法
  3. 拒绝破解,用10大免费软件来代替盗版
  4. MacBook Air 2013年中10.9版本升级到macOS Sierra
  5. css竖向箭头符号_用css打造一个三角形箭头
  6. 计算机毕业设计(附源码)python校园一卡通管理系统
  7. mt7620a上带机量的提高(一)
  8. 生物素标记试剂1869922-24-6,Alkyne-PEG3-Biotin PC,炔烃PEG3生物素PC
  9. 艾永亮:从产品功能需求到打造超级产品过程中,企业经历了什么?
  10. gflags和glog在cartographer中的运用