Effective C++学习笔记(二)
构造、析构和赋值运算
条款五 了解C++ 默默编写并调用哪些函数。
-
C++会默认 为类构建default构造、析构(非virtual,除非base class自身有virtual析构函数)、copy assignment构造、copy构造函数。且这些函数都是public和inline。
-
default构造函数和析构函数主要是给编译器一个地方用来防止藏身幕后的代码,像是调用base classes 和no-static成员变量的构造函数和析构函数。
-
至于copy构造函数和copyassignment操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static成员变量拷贝到目标对象。
-
编译器并不是任何情况下都默认生成这些函数,要看具体的情况。
条款六 不想用默认的,就要说啊,不要当渣男。
-
把copy构造函数和copyassignment操作符都声明为private,并不实现。
-
或者使用uncopyable这样的base class也是一种做法。继承一个base class,其copy函数是private。不过有点微妙,需要细心一点研究。
条款七 为多态基类声明virtual析构函数。
-
盲目继承,会造成内存泄漏,比如继承no-virtual的类String。相同的分析适用于任何不带virtual析构函数的class,包括所有STL容器。
class myString: public string{}; myString ms= new myString("hello"); string *ps; ps = ms; delete ps; //未定义!显示中*ps的myString资源会泄漏,因为myString析构函数并没有被调用。
-
class的设计目的如果不是作为base class使用,或者不是为了具备多态性(polymorphically),就不该声明为virtual析构函数。
条款八 别让异常逃离析构函数。析构函数吞下异常,然后记下来。不要轻易终止程序。
-
析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能跑出异常,析构函数应该捕捉任何异常,然后吞下他们(不传播)或结束程序。
class DBConn{ // 这个class用来管理DBConnection对象 public: ~DBConn() // 确保数据库连接总是会被关闭; { try{ db.close(); } catch(){ // 制作运转记录,记下对close的调用失败;或吞下异常 abort(); //close()抛出异常的处理, } } private: DBConnection db; };
-
如果客户需要对某个操作函数运行期间跑出的异常做出反应,那么class应该提供一个普通函数执行该操作,而不是在析构中。
class DBConn{ // 这个class用来管理DBConnection对象 public: void close()//供客户使用的新函数 { db.close(); closed = true; } ~DBConn() // 确保数据库连接总是会被关闭; { if(!closed) // 判断是否关闭 { try{ db.close(); } catch(){ // 制作运转记录,记下对close的调用失败;或吞下异常 abort(); //close()抛出异常的处理, } } } private: DBConnection db; bool closed; };
条款九 不要在构造函数中调用虚函数
-
base class 构造期间virtual函数绝不会下降到derived class阶层。在base class构造期间,virtual函数不是virtual函数。
-
其实b在base class构造(析构也是这样)期间,b的类型是base class,而不是derived class。
class A{ public: A() { //do sth; ······ // final print(); //调用A的方法,而不是B的。 } virtual void print() const; }; class B:public A{ virtual void print() const; }; //先调用A的构造,print,不会调用B的print //因为A构造函数的执行的时候,B的成员变量尚未初始化。要求使用对象内部尚未初始化的成分是危险的。 B b;
-
解决办法:不要使用virtual函数从上向下调用,在构造期间,你可以用参数传递。
class A{ public: explict A(string& loginfo) { //do sth; ······ // final print(loginfo); //调用A的方法,而不是B的。 } void print(string& loginfo) const; }; class B:public A{ public: B(param):A(createlogstring(param)) {···} private: string createlogstring(param); };
条款十 operator= 的时候返回*this
-
这只是一个协议,并无强制性。
class Widget { public: Widget& operator+=(const Widget& rhs) { ··· return *this; } Widget& operator=(int rhs) { ··· return *this; } };
条款十一 operator= 的时候处理自我复制,判断如果等同this ,return
-
确保operator=的安全,包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy and swap
-
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
class Widget { public: //*this和rhs有可能是同一个对象。如果真的是这样,delete m_pb也会把rhs干掉了!!!! Widget& operator=(const Widget& rhs) { delete m_pb; //停止使用当前的bitmnap pb = new Bitmap(*rhs.m_pb); // 使用rhs bitmap的副本 return *this; } private: Bitmap* m_pb; };
-
改进一:先判断this和rhs是否会相等。缺点:new失败后抛异常,Widget会有一个指针指向一块被删除的Bitmap。这样的指针有害,无法安全的删除它们,甚至无法安全地读取。
Widget& operator=(const Widget& rhs) { if(this == &rhs) return *this; // 判断是否相同 delete m_pb; //停止使用当前的bitmnap pb = new Bitmap(*rhs.m_pb); // 使用rhs bitmap的副本 return *this; }
-
改进二:我们需要注意在赋值之前别删除m_pb。或者使用swap的方法
Widget& operator=(const Widget& rhs) { Bitmap* pOrig = m_pb; // 记住原先的pb pb = new Bitmap(*rhs.m_pb); // 使用rhs bitmap的副本 delete pOrig; // 删除原先的pb return *this; }
条款十二 copy对象的时候不要忘记其每一个成分
-
确保复制所有的local成员变量,调用所有base class内的适当copying函数
-
不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放进第三个函数中,并由两个copying函数共同调用。