点击蓝字
关注我们
21、构造函数和析构函数可以调用虚函数吗,为什么
在C++中,提倡不在构造函数和析构函数中调用虚函数;
在构造函数和析构函数调用的所有函数(包括虚函数)都是编译时确定的, 虚函数将运行该类中的版本.
因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编;
析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。
22、构造函数的执行顺序?析构函数的执行顺序?构造函数内部干了啥?拷贝构造干了啥?
构造函数顺序:
基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是它们在成员初始化表中的顺序。
成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是对象在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序。
派生类构造函数。
析构函数顺序:
调用派生类的析构函数;
调用成员类对象的析构函数;
调用基类的析构函数。
23、虚析构函数的作用,父类的析构函数是否要设置为虚函数?
C++
中基类采用virtual
虚析构函数是为了防止内存泄漏。如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。
假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。
所以,为了防止这种情况的发生,`C++`中基类的析构函数应采用`virtual` 虚析构函数。
纯虚析构函数一定得有定义,因为每一个派生类析构函数会被编译器加以扩张,以静态调用的方式调用其每一个虚基类以及上一层基类的析构函数。因此缺乏任何一个基类析构函数的定义,就会导致链接失败。==因此,最好不要把虚析构函数定义为纯虚析构函数。==
24、`构造函数``析构函数`可以调用虚函数吗?
在
构造函数
和析构函数
中最好不要调用虚函数;在
构造函数
和析构函数
中调用的成员函数都是属于编译时确定的,并不具有虚函数的动态绑定特性, 有如下原因:在构造时, 父类对象总是先于子类对象构造的, 如果父类的析构函数使用虚函数机制调用子类的函数, 结果将是不可预料的
在析构时, 子类的析构函数总是先于父类执行, 如果父类的析构函数使用虚函数机制调用子类的函数, 结果将是不可预料的
参考: 21、构造函数和析构函数可以调用虚函数吗,为什么
25、`构造函数`, `析构函数`可否抛出异常
构造函数异常
使用智慧指针来管理堆内存. 其不需要在析构函数中手动释放资源. 在发生异常时, 智慧指针会自动释放资源从而避免了内存泄漏.
一般建议不要在构造函数里做过多的资源分配。
(原因):`C++`拒绝为没有完成构造函数的对象调用析构函数,原因是避免开销
构造函数中发生异常,控制权转出构造函数。如果构造函数中申请了堆内存, 则堆内存将无法释放, 从而造成内存泄漏
例如: 在对象`b` 的构造函数中发生异常,对象`b` 的析构函数不会被调用。因此会造成内存泄漏。
后果:
解决方案:
析构函数异常
(后果1): 如果某一个异常发生,某对象的析构函数被调用,而此时析构发生了异常并流出了函数之外,则函数会被立即terminate掉(函数外有catch也不能拯救)
如果异常不可避免,则应在析构函数内捕获,而不应当抛出。
在析构函数中使用`try-catch`块屏蔽所有异常。
在异常传递的堆栈辗转开解的过程中, 如果发生析构异常, `C++`会调用`terminate`终止程序
如果析构函数发生发生异常,则异常后面的代码将不执行,无法确保完成我们想做的清理工作。
后果:
解决方法:
附加说明:
26、类如何实现`只能静态分配`和`只能动态分配`
建立类的对象有两种方式:
动态建立类对象, 使用new操作符将在堆空间分配内存, 然后调用构造函数初始化这片内存空间.
这种方法,间接调用类的构造函数。
静态建立一个类对象,就是由编译器为对象在栈空间中分配内存, 然后调用构造函数初始化这片内存空间.
使用这种方法,直接调用类的构造函数。
静态建立(栈空间)
动态建立(堆空间),
A *p = new A()
;
只能在堆上建立
静态建立:
编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性,
由此引发的问题:
代码如下:
class A { protected : A(){} ~A(){} public : static A* create() { return new A(); } void destory() { delete this ; } };
当对象
建立
在栈上面时,是由编译器分配内存空间的,调用构造函数
来构造
栈对象。当对象使用
完
后,编译器会调用析构函数
来释放
栈对象所占的空间。编译器管理了对象的整个生命周期。
其实不光是析构函数,只要是非静态的函数,编译器都会进行检查。
如果类的析构函数是私有的,则编译器不会在栈空间上为类对象分配内存。
因此,将析构函数设为私有,类对象就无法建立在栈上了。
因为析构函数设置为了私有
需要设置一个
public函数
来调用析构函数分析: 类对象只能建立在堆上,就是不能静态建立类对象,即不能直接调用类的构造函数。
实现方式: 将析构函数设为私有或则受保护
方法分析:
只能在栈上建立
只有使用new运算符,对象才会建立在堆上,因此,只要禁用new运算符就可以实现类对象只能建立在栈上。将operator new()设为私有即可。
注意: 重载了`new`就需要重载`delete`
代码如下:
class A { private : void * operator new ( size_t t){} // 注意函数的第一个参数和返回值都是固定的 void operator delete ( void * ptr){} // 重载了new就需要重载delete public : A(){} ~A(){} };
参考:
在堆/栈上建立对象
如何定义一个只能在堆上(栈上)生成对象的类?
27、如果想将某个类用作基类,为什么该类必须定义而非声明?
因为在继承体系下, 子类会继承父类的成员, 并且编译器会在子类的构造函数和析构函数中插入父类的构造和析构部分, 因而父类必须有定义.
28、什么情况会自动生成默认构造函数?
四种情况:
类成员对象带有默认构造函数.
基类带有默认构造函数
类中存在虚函数
继承体系中存在虚继承
在合成的默认构造函数中,只有基类子对象和类类型对象会被初始化,而其他所有的非静态成员(如整数,指针,数组等),都不会初始化,对他们进行初始化的应该是程序员,而非编译器。
注意:值类型的默认值并不是默认构造的初始化。
29、什么是类的继承?
类与类之间的关系
`(has-A)`包含关系,即一个类的成员属性是另一个已经定义好的类
`(use-A)`使用关系, 一个类使用另一个类,通过类之间的成员函数相互联系,定义友元或者通过传递参数的方式实现;
`(is-A)`继承关系, 继承关系,关系具有传递性;
继承的相关概念
所谓的继承就是一个类继承了另一个类的属性和方法,这个新的类包含了上一个类的属性和方法,
被称为子类或者派生类,被继承的类称为父类或者基类;
继承的特点
子类拥有父类的所有属性和方法,子类对象可以当做父类对象使用;
子类可以拥有父类没有的属性和方法;
继承中的访问控制
`public`、`protected`、`private`
继承中的构造和析构函数
子类中构造函数的调用顺序为: 基类构造函数, 成员对象构造函数, 派生类构造函数
子类中析构函数的调用顺序为: 派生类析构函数, 成员对象析构函数, 基类析构函数
继承中的兼容性原则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
参考: 继承中的类型兼容性原则 - Say舞步 - 博客园
30、什么是组合?
一个类里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员;
如果内嵌类没有零参数构造函数, 则必须使用初始化列表进行初始化
构造函数的执行顺序:
按照内嵌对象成员在组合类中的定义顺序调用内嵌对象的构造函数。
然后执行组合类构造函数的函数体,析构函数调用顺序相反。
*声明:本文于网络整理,版权归原作者所有,如来源信息有误或侵犯权益,请联系我们删除或授权事宜。
戳“阅读原文”我们一起进步