每一个Item都很经典,都需要去思考揣摩,我在这里将要点抽象出来,便于日后快速回忆;我只是在做文章的“搬运工”。
Item 5 了解 C++ 为你偷偷地加上和调用了什么函数
1. 一个空类,如果不声明一个拷贝构造函数,一个拷贝赋值运算符和一个析构函数,编译器就会为这些东西声明一个它自己的版本。此外,如果没有声明构造函数,编译器就会为你声明一个缺省构造函数。所有这些函数都被声明为 public 和 inline。
[参考之前总结的 C++类默认函数 ]
2. 缺省构造函数和析构函数主要是给编译器一个地方放置“幕后”代码的,比如“非静态数据成员”和“基类”的构造函数和析构函数的调用。
注意,生成的析构函数是非虚拟的,除非它所在的 class是从一个基类继承而来,而基类自己声明了一个 virtual析构函数;这种情况下,函数的 virtualness(虚拟性)来自基类。
对于拷贝构造函数和拷贝赋值运算符,编译器生成版本只是简单地从源对象拷贝每一个非静态数据成员到目标对象。
[有时候,编译器生成的默认构造函数、拷贝函数比我们自己实现的完善]
3. 如果一个类包含引用数据成员,编译器无法隐式生成拷贝赋值运算符;因为引用数据成员无法改变;
如果一个类包含const数据成员,编译器无法隐式生成拷贝赋值运算符;因为const数据成员无法赋值;
如果基类将拷贝赋值运算符声明为 private,编译器拒绝为从它继承的派生类生成隐式拷贝赋值运算符。因为,编译器为派生类生成的拷贝赋值运算符也要处理其基类构件,但如果这样做,它们无法调用那些派生类的成员函数(数据)。
[如同Item 6]
Item 6 如果你不想使用编译器生成函数,就明确拒绝
1. 如果不想使类具有拷贝和赋值功能,应该将拷贝构造函数和拷贝赋值运算符声明为private的。通过显式声明一个拷贝函数,可以防止编译器生成它自己的版本,而且将这个函数声明为 private私有的,可以防止别人调用它。
2. 这个方案并不十分保险,因为成员函数和friend函数还是能够调用你的private函数。当有人不小心地调用了它们(只有声明,没有定义的拷贝函数),在 link-time会出现错误。
3. 将 link-time错误提前到编译时间的方法,在一个为防止拷贝而特意设计的 base class(基类)Uncopyable中声明private 的拷贝构造函数和拷贝赋值运算符。
4. 这样做是因为,如果有人——甚至是成员或友元函数——试图拷贝一个对象,编译器将试图生成一个拷贝构造函数和一个拷贝赋值运算符,这些函数的编译器生成版会试图调用基类的相应函数,而这些调用将被拒绝,因为在基类中,拷贝操作是 private私有的。
5. Uncopyable 的实现和使用包含一些微妙之处,比如,从 Uncopyable 继承不必是 public,而且 Uncopyable 的析构函数不必是 virtual的。因为 Uncopyable 不包含数据,所以它符合 Item 39 描述的 empty base class optimization(空基类优化)的条件,但因为它是基类,此项技术的应用不能引入 multiple inheritance(多继承)。
[基类中声明private的拷贝/赋值是一个很巧妙的方法]
Item 7 在多态基类中将析构函数声明为 virtual虚拟
1. 析构函数的工作方式是:层次最低的派生类的析构函数最先被调用,然后调用每一个基类的析构函数。
2. 当一个派生类对象通过使用一个指向带有非虚拟析构函数的基类的指针被删除,运行时比较典型的后果是这个对象的派生部分不会被析构。这会导致泄漏资源,破坏数据结构以及消耗大量调试时间。消除这个问题很简单:给基类一个虚拟析构函数。
3. 如果一个 class不包含virtual函数,这经常预示不打算将它作为基类使用。当一个 class不打算作为基类时,将析构函数虚拟通常是个坏主意。为class加上 vptr 将会使它的大小增长 50-100%!当且仅当一个类中包含至少一个虚拟函数时,则在类中声明一个虚拟析构函数。为基类提供 virtual析构函数的规则仅仅适用于多态基类——基类被设计成允许通过基类接口对派生类类型进行操作。
[只在使用多态时使用virtual函数]
Item 8 防止因为异常而离开析构函数
1. 析构函数应该永不引发异常,如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后抑制它们或者终止程序。
2. 析构函数引发异常是危险的,永远都要冒着程序过早终止或未定义行为的风险。在本小节例子中,让客户自己调用 close 并不是强加给他们的负担,而是给他们一个时机去应付错误,否则他们将没有机会做出回应。
[必须防止析构函数内发生异常,所以一些释放工作可以在“客户端”做,而析构函数只用check一下。]
[想想和构造函数发生异常有什么不同?]
Item 9 绝不要在构造或析构期间调用 virtual
1. 基类构造期间,virtual函数从来不会 go down(向下匹配)到派生类。取而代之的是,那个对象的行为好像它就是基类型。非正式地讲,基类构造期间,virtual函数被禁止。
*基类构造函数派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间虚拟函数的调用 went down(向下匹配)到派生类,派生类的函数差不多总会涉及到局部数据成员,但是那些数据成员至此还没有被初始化,调用涉及到一个对象还没有被初始化的构件自然是危险的。
*在一个派生类对象的基类构造期间,对象的类型是基类的类型。不仅virtual函数会解析到基类,而且用到运行时类型信息的语言构件(例如,dynamic_cast和 typeid),也会将那个对象视为基类类型。
2. 同样的推理也适用于析构函数。一旦派生类析构函数运行,这个对象的派生类数据成员就呈现为未定义的值,所以 C++ 就将它们视为不再存在。在进入基类析构函数时,这个对象就成为一个基类对象,C++ 的所有构件—— virtual函数,dynamic_casts 等——都以此看待它。
3. 换句话说,由于你不能在基类的构造过程中使用 virtual函数向下匹配,你可以改为让派生类将必要的构造信息上传给基类构造函数作为补偿。
使用一个辅助函数创建一个值传递给基类构造函数,通常比通过在成员初始化列表给基类它所需要的东西更加便利(也更加具有可读性)。将那个函数做成 static,就不会有偶然触及到一个新生的对象的使用仍未初始化的数据成员的危险。这很重要,因为实际上那些数据成员处在一个未定义状态,这也为什么在基类构造和析构期间调用 virtual函数不能首先向下匹配到派生类的原因。
[在构造/析构函数中使用virtual时,对象没有成型或销毁,是没有意义的;如果想在基类构造中使用派生类数据,通过派生类构造函数初始化列表上传]
Item 10 让赋值运算符返回一个引向 *this 的引用
1. 让赋值运算符返回一个引向 *this 的引用。
你可以把赋值串成一串,比如:
int x, y, z;
x = y = z = 15;
这仅仅是一个惯例,这个惯例被所有的 built-in types(内建类型)和标准库中所遵守。
Item 11 在 operator= 中处理自赋值
1.在 operator= 中处理自赋值是不安全的。
[不安全体现在:1.当赋值的2个对象(源、目标)相同时,容易删除其中一个而导致另外一个也被删除;2.为目标对象重新分配内存失败(异常)]
2.在编写默认赋值函数(operator=)时的几种方法:
//基础代码
class Bitmap {};
class Widget
{
private:
Bitmap *pb;
};
方法1)
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; //一致性检查,确保自赋值安全
delete pb;
pb = new Bitmap(*rhs.pb); //可能出现异常,new失败;此时pb又被delete,导致this->pb是一个野指针。
return *this;
}
方法2)
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //创建一个第3方指针pOrig指向pb
pb = new Bitmap(*rhs.pb); //为pb开辟新内存,并赋新值;如果这里new失败(异常),后续代码不会进行,即pOrig(this->pb)不会删除,不会影响原状态。
delete pOrig; //new成功,通过删除第3方指针pOrig(即删除原pb内存);这里即使是一致性(this == &rhs),对rhs也没有影响。
return *this;
}
方法3)
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //做一份rhs的拷贝
swap(temp); //copy-and-swap. 对this->pb赋予新值,temp为临时对象,将被销毁。
return *this;
}
方法4)
Widget& Widget::operator=(Widget rhs) //传值,传rhs的拷贝
{
swap(rhs); //copy-and-swap. 对this->pb赋予新值,rhs为临时拷贝对象,将被销毁。
return *this;
[方法1是最常用的方法;方法2借用了第3方变量;方法3/4都是copy-and-swap的巧妙应用,方法4在灵活的基础上牺牲了清晰度,通过将拷贝操作从函数体中转移到参数的构造中]
[方法2/3/4让我开阔了眼界,想象力是无穷的]
Item 12 拷贝一个对象的所有组成部分
1. 如果需要,编译器会生成拷贝函数,而且编译器生成的版本正象你所期望的:它们拷贝被拷贝对象的全部数据。
当你声明了你自己的拷贝函数,你就是在告诉编译器你不喜欢缺省实现中的某些东西。编译器会用一种古怪的方式报复:当你的实现存在一些几乎可以确定错误时,它不会告诉你。
[编译器不会检查默认构造函数,拷贝函数的正确性]
2. 如果你为一个类增加了一个数据成员,你务必要做到更新拷贝函数;你还需要更新类中的全部的构造函数以及任何非标准形式的 operator=;如果你忘记了,编译器未必会提醒你。
3. 你打算自己为一个派生类写拷贝函数时,你必须注意同时拷贝基类部分。当基类数据部分是 private时,你不能直接访问它们,派生类的拷贝函数必须调用和它们对应的基类函数。
4. 用拷贝赋值运算符调用拷贝构造函数是没有意义的,因为你这样做就是试图去构造一个已经存在的对象。
用拷贝构造函数调用拷贝赋值运算符——这同样是荒谬的,一个构造函数初始化新的对象,而一个赋值运算符只能用于已经初始化过的对象。借助构造过程给一个对象赋值将意味着对一个尚未初始化的对象做一些事,而这些事只有用于已初始化对象才有意义。
如果想使用拷贝构造函数和拷贝赋值运算符相互调用,作为代替,将通用功能放入第三方供双方调用的函数。
Item 5 了解 C++ 为你偷偷地加上和调用了什么函数
1. 一个空类,如果不声明一个拷贝构造函数,一个拷贝赋值运算符和一个析构函数,编译器就会为这些东西声明一个它自己的版本。此外,如果没有声明构造函数,编译器就会为你声明一个缺省构造函数。所有这些函数都被声明为 public 和 inline。
[参考之前总结的 C++类默认函数 ]
2. 缺省构造函数和析构函数主要是给编译器一个地方放置“幕后”代码的,比如“非静态数据成员”和“基类”的构造函数和析构函数的调用。
注意,生成的析构函数是非虚拟的,除非它所在的 class是从一个基类继承而来,而基类自己声明了一个 virtual析构函数;这种情况下,函数的 virtualness(虚拟性)来自基类。
对于拷贝构造函数和拷贝赋值运算符,编译器生成版本只是简单地从源对象拷贝每一个非静态数据成员到目标对象。
[有时候,编译器生成的默认构造函数、拷贝函数比我们自己实现的完善]
3. 如果一个类包含引用数据成员,编译器无法隐式生成拷贝赋值运算符;因为引用数据成员无法改变;
如果一个类包含const数据成员,编译器无法隐式生成拷贝赋值运算符;因为const数据成员无法赋值;
如果基类将拷贝赋值运算符声明为 private,编译器拒绝为从它继承的派生类生成隐式拷贝赋值运算符。因为,编译器为派生类生成的拷贝赋值运算符也要处理其基类构件,但如果这样做,它们无法调用那些派生类的成员函数(数据)。
[如同Item 6]
Item 6 如果你不想使用编译器生成函数,就明确拒绝
1. 如果不想使类具有拷贝和赋值功能,应该将拷贝构造函数和拷贝赋值运算符声明为private的。通过显式声明一个拷贝函数,可以防止编译器生成它自己的版本,而且将这个函数声明为 private私有的,可以防止别人调用它。
2. 这个方案并不十分保险,因为成员函数和friend函数还是能够调用你的private函数。当有人不小心地调用了它们(只有声明,没有定义的拷贝函数),在 link-time会出现错误。
3. 将 link-time错误提前到编译时间的方法,在一个为防止拷贝而特意设计的 base class(基类)Uncopyable中声明private 的拷贝构造函数和拷贝赋值运算符。
4. 这样做是因为,如果有人——甚至是成员或友元函数——试图拷贝一个对象,编译器将试图生成一个拷贝构造函数和一个拷贝赋值运算符,这些函数的编译器生成版会试图调用基类的相应函数,而这些调用将被拒绝,因为在基类中,拷贝操作是 private私有的。
5. Uncopyable 的实现和使用包含一些微妙之处,比如,从 Uncopyable 继承不必是 public,而且 Uncopyable 的析构函数不必是 virtual的。因为 Uncopyable 不包含数据,所以它符合 Item 39 描述的 empty base class optimization(空基类优化)的条件,但因为它是基类,此项技术的应用不能引入 multiple inheritance(多继承)。
[基类中声明private的拷贝/赋值是一个很巧妙的方法]
Item 7 在多态基类中将析构函数声明为 virtual虚拟
1. 析构函数的工作方式是:层次最低的派生类的析构函数最先被调用,然后调用每一个基类的析构函数。
2. 当一个派生类对象通过使用一个指向带有非虚拟析构函数的基类的指针被删除,运行时比较典型的后果是这个对象的派生部分不会被析构。这会导致泄漏资源,破坏数据结构以及消耗大量调试时间。消除这个问题很简单:给基类一个虚拟析构函数。
3. 如果一个 class不包含virtual函数,这经常预示不打算将它作为基类使用。当一个 class不打算作为基类时,将析构函数虚拟通常是个坏主意。为class加上 vptr 将会使它的大小增长 50-100%!当且仅当一个类中包含至少一个虚拟函数时,则在类中声明一个虚拟析构函数。为基类提供 virtual析构函数的规则仅仅适用于多态基类——基类被设计成允许通过基类接口对派生类类型进行操作。
[只在使用多态时使用virtual函数]
Item 8 防止因为异常而离开析构函数
1. 析构函数应该永不引发异常,如果析构函数调用了可能抛出异常的函数,析构函数应该捕捉所有异常,然后抑制它们或者终止程序。
2. 析构函数引发异常是危险的,永远都要冒着程序过早终止或未定义行为的风险。在本小节例子中,让客户自己调用 close 并不是强加给他们的负担,而是给他们一个时机去应付错误,否则他们将没有机会做出回应。
[必须防止析构函数内发生异常,所以一些释放工作可以在“客户端”做,而析构函数只用check一下。]
[想想和构造函数发生异常有什么不同?]
Item 9 绝不要在构造或析构期间调用 virtual
1. 基类构造期间,virtual函数从来不会 go down(向下匹配)到派生类。取而代之的是,那个对象的行为好像它就是基类型。非正式地讲,基类构造期间,virtual函数被禁止。
*基类构造函数派生类构造函数之前执行,当基类构造函数运行时,派生类数据成员还没有被初始化。如果基类构造期间虚拟函数的调用 went down(向下匹配)到派生类,派生类的函数差不多总会涉及到局部数据成员,但是那些数据成员至此还没有被初始化,调用涉及到一个对象还没有被初始化的构件自然是危险的。
*在一个派生类对象的基类构造期间,对象的类型是基类的类型。不仅virtual函数会解析到基类,而且用到运行时类型信息的语言构件(例如,dynamic_cast和 typeid),也会将那个对象视为基类类型。
2. 同样的推理也适用于析构函数。一旦派生类析构函数运行,这个对象的派生类数据成员就呈现为未定义的值,所以 C++ 就将它们视为不再存在。在进入基类析构函数时,这个对象就成为一个基类对象,C++ 的所有构件—— virtual函数,dynamic_casts 等——都以此看待它。
3. 换句话说,由于你不能在基类的构造过程中使用 virtual函数向下匹配,你可以改为让派生类将必要的构造信息上传给基类构造函数作为补偿。
使用一个辅助函数创建一个值传递给基类构造函数,通常比通过在成员初始化列表给基类它所需要的东西更加便利(也更加具有可读性)。将那个函数做成 static,就不会有偶然触及到一个新生的对象的使用仍未初始化的数据成员的危险。这很重要,因为实际上那些数据成员处在一个未定义状态,这也为什么在基类构造和析构期间调用 virtual函数不能首先向下匹配到派生类的原因。
[在构造/析构函数中使用virtual时,对象没有成型或销毁,是没有意义的;如果想在基类构造中使用派生类数据,通过派生类构造函数初始化列表上传]
Item 10 让赋值运算符返回一个引向 *this 的引用
1. 让赋值运算符返回一个引向 *this 的引用。
你可以把赋值串成一串,比如:
int x, y, z;
x = y = z = 15;
这仅仅是一个惯例,这个惯例被所有的 built-in types(内建类型)和标准库中所遵守。
Item 11 在 operator= 中处理自赋值
1.在 operator= 中处理自赋值是不安全的。
[不安全体现在:1.当赋值的2个对象(源、目标)相同时,容易删除其中一个而导致另外一个也被删除;2.为目标对象重新分配内存失败(异常)]
2.在编写默认赋值函数(operator=)时的几种方法:
//基础代码
class Bitmap {};
class Widget
{
private:
Bitmap *pb;
};
方法1)
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; //一致性检查,确保自赋值安全
delete pb;
pb = new Bitmap(*rhs.pb); //可能出现异常,new失败;此时pb又被delete,导致this->pb是一个野指针。
return *this;
}
方法2)
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; //创建一个第3方指针pOrig指向pb
pb = new Bitmap(*rhs.pb); //为pb开辟新内存,并赋新值;如果这里new失败(异常),后续代码不会进行,即pOrig(this->pb)不会删除,不会影响原状态。
delete pOrig; //new成功,通过删除第3方指针pOrig(即删除原pb内存);这里即使是一致性(this == &rhs),对rhs也没有影响。
return *this;
}
方法3)
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); //做一份rhs的拷贝
swap(temp); //copy-and-swap. 对this->pb赋予新值,temp为临时对象,将被销毁。
return *this;
}
方法4)
Widget& Widget::operator=(Widget rhs) //传值,传rhs的拷贝
{
swap(rhs); //copy-and-swap. 对this->pb赋予新值,rhs为临时拷贝对象,将被销毁。
return *this;
}
[方法1是最常用的方法;方法2借用了第3方变量;方法3/4都是copy-and-swap的巧妙应用,方法4在灵活的基础上牺牲了清晰度,通过将拷贝操作从函数体中转移到参数的构造中]
[方法2/3/4让我开阔了眼界,想象力是无穷的]
Item 12 拷贝一个对象的所有组成部分
1. 如果需要,编译器会生成拷贝函数,而且编译器生成的版本正象你所期望的:它们拷贝被拷贝对象的全部数据。
当你声明了你自己的拷贝函数,你就是在告诉编译器你不喜欢缺省实现中的某些东西。编译器会用一种古怪的方式报复:当你的实现存在一些几乎可以确定错误时,它不会告诉你。
[编译器不会检查默认构造函数,拷贝函数的正确性]
2. 如果你为一个类增加了一个数据成员,你务必要做到更新拷贝函数;你还需要更新类中的全部的构造函数以及任何非标准形式的 operator=;如果你忘记了,编译器未必会提醒你。
3. 你打算自己为一个派生类写拷贝函数时,你必须注意同时拷贝基类部分。当基类数据部分是 private时,你不能直接访问它们,派生类的拷贝函数必须调用和它们对应的基类函数。
4. 用拷贝赋值运算符调用拷贝构造函数是没有意义的,因为你这样做就是试图去构造一个已经存在的对象。
用拷贝构造函数调用拷贝赋值运算符——这同样是荒谬的,一个构造函数初始化新的对象,而一个赋值运算符只能用于已经初始化过的对象。借助构造过程给一个对象赋值将意味着对一个尚未初始化的对象做一些事,而这些事只有用于已初始化对象才有意义。
如果想使用拷贝构造函数和拷贝赋值运算符相互调用,作为代替,将通用功能放入第三方供双方调用的函数。