条款05:了解c++默默编写并调用哪些函数
(一)c++默认产生
a) 当程序员没有明确编写以下四个函数时。C++编译器会自动添加构造函数、复制构造函数、析构函数、赋值运算操作四个默认函数。这四个函数均是public的inline型的。若自己已经编写了上面四个中的某几个,那么编写的那几个,编译器就不会再自动生成了。
(二)四个函数的调用时机
a) 构造:对象构造时调用
b) 复制构造:形参、值传的返回值、用一个对象初始化另一个对象(深、浅拷贝)
c) 赋值运算符:将一个对象赋值给另一个对象(注意不是初始化)
d) 析构:对象销毁时调用
条款06:若不想使用编译其自动生成的函数
(一)如果不想编译自动生成上述函数
a) 将自己的赋值操作、复制构造声明成private
b) 继承的基类中的赋值操作、复制构造是private。(子类赋值、复制构造会调用到父类们的这些方法)
条款07:为多态基类声明vitrual析构函数(析构)
(一)多态基类的析构函数应该为vitrual
a) 若基类类型指针指向了子类类型对象。如果基类析构函数不为虚函数,那么调用的是基类的析构函数,那么根据对象模型,我们知道,基类析构函数只是释放了基类部分的对象,子类部分的对象未被释放,导致内存泄漏。基类析构函数为虚,那么基类指针指向的子类对象的虚表中的析构函数的地址其实是子类对象的。也就是说在析构时实际上调用的是子类对象的析构函数,这样的话,整个子类对象就会被析构掉,不会出现内存泄漏的情况。(会先调用子类的析构函数会析构掉子类部分,然后调用父类的析构函数将父类的那部分析构掉)(析构函数的调用与构造函数的调用顺序相反,会先调用子类的析构函数,然后在逐层向上调用父类、祖父类的析构函数)
(二)不做基类的类的析构函数不应该为vitrual
a) 不做基类,也就没必要有虚函数。若将析构函数声明成虚函数,则会多一个指向虚表的虚指针。也就是说对象会多一个指针,浪费内存空间
条款08:别让异常逃离析构函数(析构)
(一)异常
a) 设计异常的目的,就是为了将问题检测和问题处理分离
(二)别让异常出析构,出析构会导致剩余资源未被释放,从而出现不明不白的问题。
a) 方法一:直接结束进程
i. 在catch中,记录异常,再直接用abort结束进程(根据日志来查找错误)
b) 方法二:吞下异常
i. 在catch中,记录异常,但不结束进程,吞下异常,继续执行程序
c) 方法三:现在用户函数中试着处理异常,不行,再使用上述两种方法
i. class DBConn{
public:
void close(){ //供客户使用的新函数
db.close();
closed = true;}
~DBConn(){
if(!closed){
try{ //关闭连接(如果客户不那么做的话)
db.close();
}catch(...){ //如果关闭动作失
//制作运转记录,记下对close的调用失败
//记录下来并结束程序或吞下异常}}}
priavte:
DBConnection db;bool closed;};
(1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
(2)如果客户需要对某个操作函数运行期间抛出的异常作出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作
条款09:绝不在构造和析构过程中调用virtual函数(构造+析构)
(一)忠告
a) 前提:在基类构造函数中调用虚函数,子类中对该虚函数进行了重写。
b) 动作:那么在构造子类对象的过程中,
c) 结果: 基类构造函数调用的虚函数还是基类自己的函数,而非子类重写的函数。这样就产生了不想要的结果(本来是想用子类的重写的那个函数的)。
(二)构造函数与对象构成
a) 在构造本类对象的过程中:构造函数从基类调起,逐层往下调用祖父类、父类、本类的构造函数。由于构造函数承担的是构造对象的任务,也即调用了构造函数就构造了一个对象。因此子类对象其实是由基类对象部分、祖父类对象部分、父类对象部分和本类对象部分所组成的。
b) 因此构造子类对象的过程中,应该是构造到哪一步,那么到目前为止构造的对象就是属于那一步的类的对象
c) 所以在基类构造函数中调用虚函数,由于仍未构造出子类对象,对象属于基类对象范畴。所以基类构造函数中调用的虚函数是基类的虚函数
(三)如何达到在基类对象中调用子类信息
a) 通过子类的初始化列表来实现向基类构造函数传递信息的目的
b) Derived():Base(info){};
条款10:令operator=返回一个reference to *this(赋值运算)
赋值运算符返回一个*this的引用,才能实现连续赋值的效果。a=b=c(5);
条款11:在operator=中处理自我赋值(赋值运算)
在赋值运算过程中,需要判定=左右是不是同一个对象。If(this==&rhs) return *this。如果this==&rhs然后又没检测,那么他们指向了同一个对象。这时候,如果rhs销毁释放了对象,那么this指向的那块内存中,存在的是什么就不得而知了,所以需要在赋值运算中防止自我赋值的现象发生
条款12:复制对象时勿忘其每一个成分(复制构造+赋值运算)
(一)问题一【深、浅拷贝】
a) 防止浅拷贝:成员变量中有指针和引用时,要注意防止浅拷贝。
(二)问题二【勿忘拷贝继承来的基类成员变量】
a) 在编写子类自己的复制构造函数和赋值运算符时,需要注意的是:除了需要将子类的各个成员变量都拷贝过去外,还需要调用父类的复制构造或赋值运算,以免忘记拷贝从父类中继承得来的成员变量。
b) 复制构造:
i. derived(const derived & rhs):base(rhs){};
c) 赋值运算:
i. derived & operator=(const derived &rhs){
base::operator=(rhs)
···
Return *this;
}