4. 确定对象被使用前已先被初始化
1)关于初始化
因为使用C part of C++初始化会招致运行期成本,所以内置类型这些(如int、array等)就不会被初始化;而non-c parts of c++初始化没有成本?所以stl这些容器(如vector)就是会被初始化。
2)默认构造函数的赋值和初始化
在默认构造函数中,有等号就称为赋值,否则为初始化。赋值会先调用参数所属类型对应的默认构造函数,然后调用拷贝函数;而初始化(即用参数列表实现),则只使用拷贝函数赋值。
出于效率考虑以及其他限制(如若成员变量是const或引用,则他们只能初始化而不是被赋值),所以尽量选择参数列表实现的初始化。
3)成员初始化次序
- 基类的成员变量初始化早于继承类。这个可以这么理解,先有爸爸再有儿子,所以肯定爸爸有优先初始化特权~
- class的成员变量总是以其声明次序被初始化,即使它们在成员初值列中以不同的次序出现。先来先得,因为声明时候编译器看到了成员变量的先后顺序,并为此排序~
4)static对象的新理解
寿命是从被构造到程序结束,即main结束时才调用static对象的析构函数。static对象分为local static对象和non-local static对象。
-
local static对象指的是在函数内的static对象,因为其对函数而言是local。应该可以将其理解为作用域只在函数内,因此称为local
-
non-local static对象指的是全局、定义在namespace作用域内、classes内以及file作用内的static对象,因为其作用域是全局,所以称为non-local
(补充文件作用域https://www.cnblogs.com/cygalaxy/p/7103674.html)
-
c++对应定义于不同的编译单元内的non-local static对象的初始化相对次序无明确定义(如eg1),所以要将每个non-local static对象搬到专属函数内,并在该函数内声明为static(如eg2)
eg1:
A.cpp class FileSystem { ... xxx(){} } extern FileSystem tfs; B.cpp class B { B(){tfs.xxx()}; }eg2:
A.cpp class FileSystem { ... xxx(){} } // 该函数目的单纯,并且短小,若其会被频繁使用,可以加个inline /*inline*/FileSystem& tfs() { static FileSystem fs; return fs; } B.cpp class B { // eg1的使用 // B(){tfs.xxx()}; // eg2的使用 B(){tfs().xxx()} }
5)编译单元就是一个cpp以及对应的h文件。定义:产出单一目标文件的那些源码
总结:
-
内置对象由于运行期耗成本,所以C++不初始化他们,要我们手动初始化
-
构造函数中有=就是赋值,要调用默认构造函数以及拷贝函数,而成员初值列就是初始化,只调用拷贝函数
-
使用local static对象替换non-local static对象,免除“跨编译单元的初始化次序”问题
5. 了解C++默认编写并调用哪些函数
1)C++默认编写函数, 并且是public且inline:(如果我们忘了写,编译器也会自动声明)
- 默认构造函数(若自己声明了带参数的构造函数,编译器不会再创建默认构造函数)
- 拷贝构造函数(将源对象的non-static成员变量拷贝到目标对象,因为static是属于大家的)
- 析构函数(编译器产出的是non-virtual)
- 拷贝赋值操作符(将源对象的non-static成员变量拷贝到目标对象,因为static是属于大家的)
eg:
class Eg
{
public:
Eg(){}
Eg(const Eg& rhs){}
~Eg(){}
Eg& operator=(const Empty & rhs){}
}
2)若要在内含reference或const成员的class内支持赋值操作,要自己定义copy assignment操作符。因为reference和const变量不支持赋值,只支持初始化,编译器这么硬怼是不行的,所以会报错。
总结:若我们未声明四大天王(default构造函数,copy构造函数、拷贝赋值操作符和析构函数),编译器可以帮我们创建。
6. 若不想使用编译器自动生成的函数,就该明确拒绝
通常不希望class支持某一特定功能,不声明对应函数即可。但由于“四大天王”即使你不声明,编译器也会自动偷偷声明。
如果想不支持拷贝,那么可以将拷贝构造函数或拷贝赋值操作符声明为private,并故意不实现他们。如果有人使用了,那么就会编译出错~
但如果在该类的成员函数或友元函数中使用了拷贝函数,那么编译过了,但是链接出错,这样不好定位问题,所以可以通过在基类实现将拷贝构造函数或拷贝赋值操作符声明为private,并故意不实现他们,就可以把链接错误转为编译错误,这样只是方便定位bug~
总结:为驳回编译器偷偷提供的的功能,可以将其实现相应功能函数声明为private并不实现,然后通过编译出错,定位bug~可以通过在基类实现,将链接错误变成编译错误。
7. 为多态基类声明virtual析构函数
适用情况:基类指针指向继承类对象,该对象由基类指针释放,所以基类的析构函数要声明virtual。
因为这样才能正确释放继承类自己独有部分的资源。
PS:virtual析构函数可以变成pure virtual析构函数,从而使该类成为抽象类。这种类型是不能创建对象哒~但必须为这个pure virtual析构函数提供一份定义,因为析构函数一定会被调用,所以要对其定义。
总结:当class内至少有一个virtual函数时并且该基类设计的目的是为了多态,析构函数就要被声明为virtual,否则无需virtual析构函数,毕竟是为了正确释放继承类比基类多的那部分资源~
8. 别让异常逃离析构函数
析构函数绝对不要吐出异常,不然很大可能会导致析构里面要做的释放资源动作还没做完就结束程序或导致不明确行为了,风险太大。因为在析构函数中抛出异常但又没有在析构函数里面怼它,那么析构函数就会传播该异常,相当于抛出了难以驾驭的麻烦。(是因为析构函数执行到抛出异常那步后就不往下走了,并且还会导致不明确的行为,因为不知道哪里以及怎么处理该异常)
如果不得不在析构时抛出异常,那应该用一个成员函数抛出该异常并且对其处理,然后在析构函数中对其做二次保证处理,并强迫结束程序,这样可以避免导致不明确行为,但是一样会导致释放资源不成功。或者是选择吞下异常。
总结:尽量不要在析构函数吐出异常,毕竟不知道这异常会被咋样处理,可能会造成意想不到的结果。当然,如果不得不在析构函数吐出异常,可以在析构函数中捕获该异常后强迫结束程序,或吞下异常,假装没事发生过~
9. 绝不在构造和析构过程中调用virtual函数
在构造和析构函数中不要调用或嵌套调用virtual函数,因为构造和析构函数的成员初始化次序是先初始化父类,然后再初始化子类。所以在调用父类构造函数中就遇到virtual,就会以父类视角解读,导致输出结果有误。
10. 令operator=返回一个reference to *this
赋值相关运算(如=、+=、-=、*=等)是可以写成连锁形式的,即传递性。如果要实现这个,赋值操作符就必须返回一个reference只想操作符左侧实参。
这不是强制性要求,只是为了大家统一~

被折叠的 条评论
为什么被折叠?



