构造函数和析构函数常见面试题?

  • 构造函数和析构函数常见面试题?
    • 1、永远不要在构造函数或析构函数中调用虚函数
    • 2、为什么构造函数不能定义为虚函数
    • 3、为什么析构函数可以定义为虚函数
    • 4、构造函数的执行顺序?析构函数的执行顺序?
    • 5、构造函数可以是内联函数
    • 6、什么情况下,需要在派生类初始化列表中初始化,不能在构造函数体内初始化(应该叫做赋值)?
  • 参考

构造函数和析构函数常见面试题?

1、永远不要在构造函数或析构函数中调用虚函数

#include<iostream>
using namespace std;class Base
{public:Base(){Function();}virtual void Function(){cout << "Base::Fuction" << endl;}
};class A : public Base
{public:A(){Function();}virtual void Function(){cout << "A::Fuction" << endl;}
};int main()
{A a;// Base * b=&a;a.Function();return 0;
}

首先回答标题的问题,调用当然是没有问题的,但是获得的是你想要的结果吗?或者说你想要什么样的结果?

   有人说会输出:
A::Fuction
A::Fuction

如果是这样,首先我们回顾下C++对象模型里面的构造顺序,在构造一个子类对象的时候,首先会构造它的基类,如果有多层继承关系,实际上会从最顶层的基类逐层往下构造(虚继承、多重继承这里不讨论),如果是按照上面的情形进行输出的话,那就是说在构造Base的时候,也就是在Base的构造函数中调用Fuction的时候,调用了子类A的Fuction,而实际上A还没有开始构造,这样函数的行为就是完全不可预测的,因此显然不是这样,实际的输出结果是:

派生类对象构造期间进入基类的构造函数时,对象类型变成了基类类型,而不是派生类类型。同样,进入基类析构函数时,对象也是基类类型。

所以,虚函数始终仅仅调用基类的虚函数(如果是基类调用虚函数),不能达到多态的效果,所以放在构造函数中是没有意义的,而且往往不能达到本来想要的效果。

《Effective C++ 》条款9:永远不要在构造函数中调用虚函数

在基类的构造过程中,虚函数调用从不会被传递到派生类中。代之的是,派生类对象表现出来的行为好象其本身就是基类型。不规范地说,在基类的构造过程中,虚函数并没有被"构造"。

对上面这种看上去有点违背直觉的行为可以用一个理由来解释-因为基类构造器是在派生类之前执行的,所以在基类构造器运行的时候派生类的数据成员还没有被初始化。

如果在基类的构造过程中对虚函数的调用传递到了派生类,派生类对象当然可以参照引用局部的数据成员,但是这些数据成员其时尚未被初始化。这将会导致无休止的未定义行为和彻夜的代码调试。沿类层次往下调用尚未初始化的对象的某些部分本来就是危险的,所以C++干脆不让你这样做。

事实上还有比这更具基本的要求。在派生类对象的基类对象构造过程中,该类的类型是基类类型。不仅虚函数依赖于基类,而且使用运行时刻信息的语言的相应部分(例如, dynamic _cast(参见Item 27)和typeid)也把该对象当基类类型对待。
  在C++编程中处处都这样处理,这样做很有意义:在基类对象的初始化中,派生类对象相关部分并未被初始化,所以其时把这些部分当作根本不存在是最安全的。 在一个派生类对象的构造器开始执行之前,它不会成为一个派生类对象的。

《Effective C++ 》条款9:永远不要在析构函数中调用虚函数

effective C++ 中有这样的描述:同样的原因也适用于析构过程。一旦派生类析构函数运行,这个对象的派生类数据成员就被视为未定义的值,所以 C++ 就将它们视为不再存在。
  
一旦进入到基类的析构器中,该对象即变为一个基类对象,C++中各个部分(虚函数,dynamic_cast运算符等等)都这样处理。

#include<iostream>
using namespace std;class Base
{public:Base(){}virtual void Function(){cout << "Base::Fuction" << endl;}~Base(){Function();}
};class A : public Base
{public:A(){}virtual void Function(){cout << "A::Fuction" << endl;}~A(){Function();}
};int main()
{A a;// Base * b=&a;a.Function();return 0;
}

2、为什么构造函数不能定义为虚函数

  • 虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。

    • 构造函数必须清晰的构造类型

从C++之父Bjarne的回答我们应该知道C++为什么不支持构造函数是虚函数了,简单讲就是没有意义。虚函数的作用在于通过父类的指针或引用来调用子类的那个成员函数。而构造函数是在创建对象时自己主动调用的,不可能通过子类的指针或者引用去调用。

3、为什么析构函数可以定义为虚函数

基类类型指针或者引用指向派生类实例,析构的时候,如果基类析构函数不是虚函数,则只会析构基类,不会析构派生类对象,从而造成内存泄漏。

为什么会出现这种现象呢,个人认为析构的时候如果没有虚函数的动态绑定功能,就只根据指针的类型来进行的,而不是根据指针绑定的对象来进行,所以只是调用了基类的析构函数;如果基类的析构函数是虚函数,则析构的时候就要根据指针绑定的对象来调用对应的析构函数了。

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

4、构造函数的执行顺序?析构函数的执行顺序?

1) 构造函数顺序

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

2) 析构函数顺序

  • ① 调用派生类的析构函数;
  • ② 调用成员类对象的析构函数;
  • ③ 调用基类的析构函数。
构造函数执行顺序1、调用虚基类构造函数(如有多个则按虚基类声明顺序从左到右)
2、调用基类构造函数(如有多个则按基类声明顺序从左到右)
3、设定虚函数表指针值(virtual table pointer)
4、执行初始化列表、调用成员变量构造函数(按成员变量声明顺序)执行自身构造函数1、析构函数执行顺序(与构造函数相反)
2、执行自身析构函数
3、调用成员变量析构函数(与成员声明顺序相反)
4、调整虚函数表指针值
5、调用基类析构函数(从右到左)
6、调用虚基类析构函数(从右到左)特例 局部对象,在退出程序块时析构静态对象,在定义所在文件结束时析构全局对象,在程序结束时析构 继承对象,先析构派生类,再析构父类 对象成员,先析构类对象,再析构对象成员

应用举例

class  ObjectD
{public:ObjectD(){cout << "ObjectD()......" << endl;}~ObjectD(){cout << "~ObjectD()......" << endl;}
};class Base
{public:Base(int b):b_(b){cout << "Base()......" << endl;}~Base(){cout << "~Base()......" << endl;}
//private:int b_;
};class Derived :public Base
{public:Derived(int d):d_(d),Base(100){cout << "Derived()......" << endl;}~Derived(){cout << "~Derived()......" << endl;}int d_;ObjectD objd_;
};Derived d(10);


注意:
当基类中也有定义的类成员对象,同样遵从上述顺序,先构造成员对象,在构造基类。

class  ObjectB
{public:ObjectB(){cout << "ObjectB()......" << endl;}~ObjectB(){cout << "~ObjectB()......" << endl;}
};
class Base
{public:Base(int b):b_(b){cout << "Base()......" << endl;}~Base(){cout << "~Base()......" << endl;}
//private:int b_;ObjectB objb_;
};


特例 说明

#include <iostream>
using namespace std;class Base1
{public:Base1(void) { cnt++; cout << "Base1::constructor(" << cnt << ")" << endl; }~Base1(void) { cnt--; cout << "Base1::deconstructor(" << cnt + 1 << ")" << endl; }
private:static int cnt;
};
int Base1::cnt = 0;class Base2
{public:Base2(int m) { num = m; cout << "Base2::constructor(" << num << ")" << endl; }~Base2(void) { cout << "Base2::deconstructor(" << num << ")" << endl; }
private:int num;
};class Example
{public:Example(int n) { num = n; cout << "Example::constructor(" << num << ")" << endl; }~Example(void) { cout << "Example::deconstructor(" << num << ")" << endl; }
private:int num;
};class Derived :public Base1, public Base2
{public:Derived(int m, int n) :Base2(m), ex(n) { cnt++; cout << "Derived::constructor(" << cnt << ")" << endl; }~Derived(void) { cnt--; cout << "Derived::deconstructor(" << cnt + 1 << ")" << endl; }
private:Example ex;static Example stex;    //Example::constructor(1) //不能输出static int cnt;
};
int Derived::cnt = 0;Derived ge_a(1, 2); // Base1::constructor(1)// Base2::constructor(1)// Example::constructor(2)// Derived::constructor(1)
static Derived gs_b(3, 4);   // Base1::constructor(2)// Base2::constructor(3)// Example::constructor(4)// Derived::constructor(2)
int main(void)
{cout << "---------start---------" << endl;Derived d(5, 6); // Base1::constructor(3)// Base2::constructor(5)// Example::constructor(6)// Derived::constructor(3)Derived e(7, 8); // Base1::constructor(4)// Base2::constructor(7)// Example::constructor(8)// Derived::constructor(4)cout << "----------end----------" << endl;//Derived e(7,8) 析构// Derived::deconstructor(4)// Example::deconstructor(8)// Base2::deconstructor(7)// Base1::deconstructor(4)//Derived d(5,6) 析构// Derived::deconstructor(3)// Example::deconstructor(6)// Base2::deconstructor(5)// Base1::deconstructor(3)return 0;
}

结果输出说明;

这是定义的全局对象的输出结果:构造顺序——》基类构造函数、对象成员构造函数、派生类本身的构造函数
Derived ge_a(1,2); // Base1::constructor(1)// Base2::constructor(1)// Example::constructor(2)// Derived::constructor(1)
static Derived gs_b(3,4);   // Base1::constructor(2)// Base2::constructor(3)// Example::constructor(4)// Derived::constructor(2)main函数局部变量的输出结果: 构造顺序——》基类构造函数、对象成员构造函数、派生类本身的构造函数Derived d(5, 6); // Base1::constructor(3)// Base2::constructor(5)// Example::constructor(6)// Derived::constructor(3)Derived e(7, 8); // Base1::constructor(4)// Base2::constructor(7)// Example::constructor(8)// Derived::constructor(4)main函数局部变量的输出结果: 析构顺序——》派生类本身的析构函数、对象成员析构函数、基类析构函数
//Derived e(7,8) 析构// Derived::deconstructor(4)// Example::deconstructor(8)// Base2::deconstructor(7)// Base1::deconstructor(4)//Derived d(5,6) 析构// Derived::deconstructor(3)// Example::deconstructor(6)// Base2::deconstructor(5)// Base1::deconstructor(3)全局对象没有结果输出的说明://static Derived gs_b(3,4) 析构// Derived::deconstructor(2)// Example::deconstructor(4)// Base2::deconstructor(3)// Base1::deconstructor(2)
//Derived ge_a(1,2) 析构// Derived::deconstructor(1)// Example::deconstructor(2)// Base2::deconstructor(1)// Base1::deconstructor(1)//static Example stex 析构//Example::deconstructor(1) //不能输出

5、构造函数可以是内联函数

构造函数可以是内联函数
1、只要是类的成员函数,都可以放在类内定义,在类内定义的函数,编译器一般视作内联
2、inline只是一种申请,其目的是为了提高函数的执行效率(速度)。究竟是不是内联还得由编译器决定,自动地取消不值得的内联。一般情况下,构造函数比较小的情况下,不管你是否指定其为内联函数,C++编译器会自动将其置为内联,如果函数太大,你即使将其指定为内联函数系统也会不理的。因为这会使程序过大。

if ((构造函数的函数体写在类里面 || 定义构造函数时加上inline)&& 函数体合适用内联)
{
构造函数是内联函数 = true;
}
else
{
构造函数是内联函数 = false;
}

6、什么情况下,需要在派生类初始化列表中初始化,不能在构造函数体内初始化(应该叫做赋值)?

(1)const成员:因为定义的时候需要初始化
(2)引用成员:因为定义的时候需要初始化
(3)基类没有默认构造函数,在初始化列表中调用基类构造函数完成
(4)派生类里面的对象成员没有默认构造函数

参考

1、https://blog.csdn.net/magictong/article/details/6734241
2、https://blog.csdn.net/sumup/article/details/78174915
3、https://blog.csdn.net/hxz_qlh/article/details/14089895
4、https://blog.csdn.net/shilikun841122/article/details/79012779

C++中构造函数和析构函数常见面试题?相关推荐

  1. java 字符串 面试_JAVA中String介绍及常见面试题小结

    字符串广泛应用 在 Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串. 深刻认识String 1)String为字符串常量:即String对象一旦 ...

  2. Java中String类的常见面试题

    第一题:==与equals()的区别 1.判断定义为String类型的s1和s2是否相等 String s1 = "abc";String s2 = "abc" ...

  3. java中string类相等_Java中String类的常见面试题

    第一题:==与equals()的区别 1.判断定义为String类型的s1和s2是否相等 String s1 = "abc"; String s2 = "abc" ...

  4. 面试系列第1篇:常见面试题和面试套路有哪些?

    作者 | 面哥 来源 | Java面试真题解析(ID:aimianshi666) 转载请联系授权(微信ID:GG_Stone) 面试是人生中为数不多的改变自身命运的途径之一,当然有效的准备面试也是人生 ...

  5. Python常见面试题:TCP 协议中的三次握手与四次挥手相关概念详解

    今天来聊聊Python常见面试题中面试频率特别高的一个题目:TCP 协议中的三次握手与四次挥手. 涉及到的知识点有: 1.TCP.UDP 协议的区别 2.TCP 头部结构 3.三次握手与四次挥手过程详 ...

  6. js怎么在一个div中嵌入另一网站_好程序员web前端学习路线分享HTML5常见面试题集锦一...

    好程序员web前端学习路线分享HTML5常见面试题集锦,接下来将会持续为大家分享几篇HTML5常见面试题. 1.布局 左边20% 中间自适应 右边200px 不能用定位 答案:圣杯布局/双飞翼布局或者 ...

  7. 通信工程中常用算法c语言,通信工程常见面试题.doc

    通信工程常见面试题 模拟电路 1. 基尔霍夫定理的内容是什么?(仕兰微电子) 基尔霍夫电流定律是一个电荷守恒定律,即在一个电路中流入一个节点的电荷与流出同一个节点的电荷相等. 基尔霍夫电压定律是一个能 ...

  8. 合肥Java面试常考题_北大青鸟java 面试--常见面试题(中)

    上一文中,我们总结了java面试的基础,多线程,jvm的常见面试题,本文合肥北大青鸟合工大校区的袁老师继续介绍面试中网络.数据结构和算法.分布式理论和微服务的常见面试题. 一.网络 网络的话,主要集中 ...

  9. 《二叉树中常见面试题--单值二叉树、翻转二叉树、平衡二叉树》

    二叉树常见面试题 1.单值二叉树 2.二叉树最大深度 3.翻转二叉树 4.相同的树 5.平衡二叉树 1.单值二叉树 描述:如果二叉树每个节点都具有相同的值,那么该二叉树就是单值二叉树. 只有给定的树是 ...

最新文章

  1. linux学习笔记十四:安装SAMBA(Server Message Block)
  2. Chrome firefox ie等浏览器空格nbsp;宽度不一样怎么办
  3. python生成词云_今天玩点啥:使用python生成微信好友地域分析、微信昵称、个性签名词云...
  4. Liferay开发学习Part6:Service Builder
  5. 了不起!靠技术脱贫,他们只用了短短两年!
  6. 中断程序_ABB机器人中断程序详解(安川FANUC)
  7. python是哪一年发明的_Python的发明,竟然是因为他不喜欢花括号。
  8. Linux 网络性能测试工具 iperf 的安装和使用
  9. winform ComboBox基本操作
  10. 低版本 android 软件下载,纳米盒旧版本下载-纳米盒旧版下载4.1安卓版-西西软件下载...
  11. 创作短视频怎么去除素材水印?
  12. 解决Windows11能登录QQ微信,但不可以使用浏览器上网
  13. LE-MSFE-DDNet:基于微光增强和多尺度特征提取的缺陷检测网络--论文笔记
  14. 【NVMe2.0b 8】NVMe 队列仲裁机制
  15. Application.platform 平台
  16. 【Windows】服务程序
  17. 【Python】递归实现n的全排列
  18. 《算法笔记》学习日记——6.1 vector的常见用法详解
  19. [嵌入式Linux项目实战开发]基于QT4.8的仓库管理系统实现功能【2019年给力项目】
  20. 解决U盘空间足够,储存是却是文件过大

热门文章

  1. 基于语义分割的矸石充填捣实机构防碰撞系统
  2. 主元素问题(蒙特卡洛法和分治法)
  3. windows下npm安装vue 问题和解决方法
  4. 提高工作效率的宝藏网站和宝藏工具
  5. Rabbit和Es默认端口
  6. Jacoco覆盖率工具使用
  7. clion中使用gdal的辛酸历程
  8. docker基础:使用非root用户操作docker
  9. JAVA创建TXT文件并写入内容
  10. 【尚筹网】IDEA版实现(十二)项目展示