目录
条款05
C++的空类中包含哪些函数?
这些函数在调用时生成。
-
默认构造函数
-
拷贝构造函数
-
赋值构造函数
-
析构函数
编译器自己生成的析构函数是
non-static
,除非该类的基类自身有virtual
析构函数 -
C++11新增移动赋值操作符(
move assignment
)和移动构造函数(move constructor
) -
取地址运算符重载,包括普通对象和
const
对象
class Home {
public:
Home(){};
~Home(){};
Home& Home(const Home&) {}
Home& operator=(const Home&) {}
Home(const Home &&) {} // 移动构造函数
Home& operator=(Home &&) {} // 移动赋值运算符
};
这些函数都是 public
和 inline
若类中包含引用成员变量,要想让其支持赋值操作,必须要自定义赋值构造函数,默认赋值构造函数将会拒绝该操作。
而const成员是不能被修改的。
条款06
若不需要自动生成的函数,则应该明确拒绝。
1、若该类不能进行拷贝,每个对象都是唯一的对象,此时可以将 copy constructor
和assignment operator
设置为private
,此时不会被对象调用,就不能被拷贝了。
class Home {
private:
Home&(const Home&); // 此时只是进行了声明,并没有对其进行定义和命名,所以不会有其他函数使用
Home& operator=(const Home&);
};
将其只进行声明不进行定义,则会使成员函数或者友元函数使用他们时会报错,这样能够解决该问题
2、可以让该类继承一个具有私有拷贝构造函数和赋值运算符的基类,因为派生类在默认定义这两个函数时会默认调用基类的函数,而这些调用会被拒绝。
class Uncopyable {
protected:
Uncopyable() {}
~Uncopyable() {}
private:
Uncopyable& (const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
class Home :private Uncopyable {
... // 此时不用声明
};
并且这种方法也能够使 想要调用拷贝操作的函数调用基类的私有函数,而这些函数会被拒绝
3、C++11提供了=delete
操作,这样该类就不会生成默认的生成函数
=delete
,若不像使用编译器默认生成的函数,则可将其定义为private
或=delete
class Home {
public:
Home(){};
~Home(){};
Home& Home(const Home&) = delete;
Home& operator=(const Home&) = delete;
};
条款07
为多态的基类声明virtual
析构函数
若为非虚函数,则可能会发生“局部销毁”现象,会导致资源泄露。
虚函数的实现细节:有虚函数的类中一般会包含一个虚函数表(vptr
),它指向一个由函数指针构成的数组,成为vbtl
,当调用某一virtual
函数时,实际上被调用的函数取决于该对象的vptr
所指的那个vbtl
,编译器在其中寻找合适的函数指针。
在不会被当做基类的类中,将函数设置为虚函数会导致类的大小的增加一个指针的大小,这样会有明显的冗余。但当类要被当做基类时,并需要使用多态性质时,一定要将析构函数设置为virtual
,不然也会导致资源泄露。
一般来说,当class
中至少含有一个virtual
函数时,才将析构函数声明为virtual
析构函数
类的设计目的若不是作为基类,或不是为了具备多态性,就不应该声明virtual析构函数。
条款08
析构函数不要抛出异常,因为包含多个对象的vector
对象需要析构时,若析构前面的对象多次抛出异常,会导致程序的崩溃,这样就会导致后面的对象没有被析构,产生内存泄露。
但有的时候析构函数必须抛出异常,怎么办呢?
-
若析构函数中抛出异常就结束程序,使用
abort()
DBConn::~DBConn() { try { db.close(); } catch(...) { // 记录对close的失败调用 std::abort(); } }
-
吞下因调用
close
而发生的异常DBConn::~DBConn() { try { db.close(); } catch(...) { // 记录对close的失败调用 } }
这两种操作不是最佳的,但是总比程序崩溃会好得多。
更好的办法是重新设计资源分配接口,使客户有机会对可能出现的问题做出反应。 设计一个close()
,使用户自己使用close
函数来进行关闭,当用户没有自己关闭时,才在析构函数中进行关闭,这样就可能会出现用户自己调用close
函数时出现异常,然后用户会有机会解决该异常,而析构函数中不会抛出异常。
class BBConn {
public:
...
void close() {
db.close();
closed = true;
}
~DBConn() {
if (!closed) {
try {
db.close(); // 双保险
}
catch (...) {
// 记录失败
}
}
}
private:
DBConnection db;
bool closed;
};
析构函数绝对不要吐出异常,若析构函数可能抛出异常,则析构函数应该捕捉任何异常,然后吞下或直接结束程序
若客户需要对某个操作函数运行期间抛出的异常做出反应,那么类中应提供一个普通函数执行该操作,而不是在析构函数中,因为析构函数不是我们自己调用的,而是程序自己调用的,我们没有办法来控制这些异常。
条款09
不要在构造和析构过程中调用virtual
函数。
基类与派生类的构造与析构:
-
构造顺序:基类构造函数-->派生类构造函数
-
派生类对象内的基类部分一定会在派生类自身的内容被狗早之前进行构造完成
-
基类构造期间
virtual
绝对不会下降到派生类阶层,换句话说,在基类构造期间,virtual
函数不是virtual
函数。 -
在派生类的基类构造期间,对象的类型是基类而不是派生类,
virtual
函数此时均指向基类本身的函数,运行时类型信息,也会把对象当做基类类型 -
析构时先析构派生类部分,在进入基类析构函数后,对象就成为了一个基类对象
条款10
令operator=
返回一个执行 *this
的引用,为了实现与 =
操作相同的结果,实现连续赋值。 不仅 =
操作符,还有所有的包含 =
操作符的 +=
-=
操作符也应该这样
条款11
operator=
要对“自我赋值”进行处理。
一般实现 operator=
时,要先释放要对要赋值对象的空间,然后将新的内容赋值,就会存在一个问题,若不对“自我赋值”进行处理,会出现“在停止使用之前意外释放了它”,与想象结果不同。
另外,operator=
也要保证异常安全性,此时需要一个简单的设计。
class Name {
...
void swap(Name &rhs); // 交换*this与rhs的数据
...
};
Name& Name::operator=(const Name &rhs) {
Name tmp(rhs); // 为rhs数据制作一份副本
swap(tmp); // 将*this与副本交换
return *this;
}
这样做有两个好处
-
一是能够解决“自我赋值”问题,在赋值之前,我们不会释放原来的空间,我们将双方内容进行交换,若为自我赋值,相当于自己与自己交换,在函数结束时,释放的是临时对象tmp的空间,但由于已经将tmp与
*this
原来的空间交换,所以此时释放的是*this
原来的空间。 -
二是能够解决异常安全问题,若在先析构原来对象后发生了异常,此时就会导致函数的不正常结束,但此时原来对象又被析构了,会导致安全问题。
条款12
复制对象时保证每一个成员都被复制
当成员变量改变时,拷贝构造函数和赋值操作符都要进行改变。
当继承发生时,子类的拷贝构造函数和赋值操作符必须要拷贝和赋值基类部分,一般的实现是先调用基类的拷贝构造函数和赋值运算符操作,然后再加上子类的成员变量部分。
但是注意,不要在赋值运算符中调用拷贝构造函数。最好是将他们之间的相同代码放入第三方函数中,一般这个函数会被定义为 private
的。