Effective C++
来源:互联网 发布:用户行为分析python 编辑:程序博客网 时间:2024/06/02 15:49
前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。
- 了解C默默编写并调用了哪些函数
- 若不想使用编译器自动生成的函数就应该明确拒绝
- 为多态基类声明virtual析构函数
- 别让异常逃离析构函数
- 绝不在构造和析构过程中调用virtual函数
- 令operator返回一个reference to this
- 在operator中处理自我赋值
- 复制对象时勿忘其每一个成分
1 了解C++默默编写并调用了哪些函数
如果是一个空类,那么编译器可能会自动生成:
* copy构造函数
* copy assignment操作符
* 析构函数
* default构造函数
以上这些函数都是public
且inline
的。
class Empty {};// 等价于class Empty {public: Empty() // default构造函数 {} Empty(const Empty& rhs) // copy构造函数 {} ~Empty() // 析构函数(是否是virtual呢?) {} Empty& operator=(const Empty& rhs) // copy assignment操作符 {}};
注意:
1. 惟有当这些函数被调用,它们才会被编译器创建出来。
Empty e1; // default构造函数 // 析构函数Empty e2(e1); // copy构造函数e2 = e1; // copy assignment操作符
- 编译器生成的析构函数是个
non-virtual
,除非这个class的base class自身声明有virtual
析构函数。 copy
构造函数和copy assignment
操作符,编译器创建的版本只是单纯地将来源对象的每一个non-static
成员变量拷贝到目标对象。
#include<iostream>#include<string>template<typename T>class NamedObject {public:#if 1 NamedObject(const char* name, const T& value) : nameValue(name), objectValue(value) { std::cout << "NamedObject(const char* name, const T& value)\n"; }#endif NamedObject(const std::string& name, const T& value) : nameValue(name), objectValue(value) { std::cout << "NamedObject(const std::string& name, const T& value)\n"; }public: std::string nameValue; T objectValue;};int main(){ NamedObject<int> no1("gerry", 1); NamedObject<int> no2(no1); // 调用copy构造函数 NamedObject<int> no3("yang", 2); no3 = no1; std::cout << no3.nameValue << "\n"; return 0;}
NamedObject没有声明copy
构造函数,也没有声明copy assignment
操作符,所以编译器会创建这些函数当它们被调用的时候。编译器生成的copy
构造函数必须以no1.nameValue和no1.objectValue为初值设定no2.nameValue和no2.objectValue。两者之中,nameValue的类型是string
,而标准的string
有个copy
构造函数,所以no2.nameValue的初始化方式是调用string
的copy
构造函数并以no1.nameValue为实参。另一个成员NameObject::objectValue的类型是int(对此template具现体而言T是int),是个内置类型,所以no2.objectValue会以拷贝no1.objectValue内的每一个bits
来完成初始化。编译器为NamedObject所生成的copy assignment
操作符,其行为基本上与copy
构造函数一样。
请记住
编译器可以暗自为class创建
default
构造函数、copy
构造函数、copy assignment
操作符,以及析构函数。
2 若不想使用编译器自动生成的函数,就应该明确拒绝
Explicitly disallow the use of compiler-generated functions you do not want.
通常如果你不希望class支持某一特定机能,只要不声明对应函数就是了。但这个策略对copy构造函数
和copy assignment操作符
却不起作用。因为,如果你不声明它们,而某些人尝试调用它们,编译器会为你声明它们。
如果你不声明copy构造函数
和copy assignment操作符
,编译器可能会为你产出一份,于是你的class支持copying
;如果你声明它们,你的class还是支持copying
。但这里的目标却是要阻止copying!
答案的关键是,所有编译器产出的函数都是public
。为阻止这些函数被创建出来,你得自行声明它们,但这里并没有什么需求使你必须将它们声明为public
。因此你可以将copy构造函数
和copy assignment操作符
声明为private
。这样明确声明一个成员函数,可以阻止编译器暗自创建其专属版本;而令这些函数为private,使得成功阻止人们调用它。
一般而言,这个做法并不绝对安全。因为member函数
和friend函数
还是可以调用你的private函数
。除非你非常聪明不去定义它,那么如果有人不慎调用任何一个,会获得一个连接错误(linkage error
)。
将成员函数声明为private而且故意不实现它们,这一伎俩是如此为大家接受,因而被用在
C++ iostream
程序库中阻止copying行为。
例子:
#include<iostream>#include<string>class HomeForSale;void copy_friend(HomeForSale& lhs, HomeForSale& rhs){ lhs = rhs;}class HomeForSale { friend void copy_friend(HomeForSale& lhs, HomeForSale& rhs);public: HomeForSale() { } HomeForSale(const std::string& lhs) : name(lhs) { } HomeForSale& copy_ctor(HomeForSale& lhs) { // error LNK2019: 无法解析的外部符号 "private: __thiscall HomeForSale::HomeForSale(class HomeForSale const &)" (??0HomeForSale@@AAE@ABV0@@Z),该符号在函数 "public: class HomeForSale & __thiscall HomeForSale::copy_ctor(class HomeForSale &)" (?copy_ctor@HomeForSale@@QAEAAV1@AAV1@@Z) 中被引用 return HomeForSale(lhs); } void copy_assignment(HomeForSale& lhs) { // error LNK2019: 无法解析的外部符号 "private: class HomeForSale & __thiscall HomeForSale::operator=(class HomeForSale const &)" (??4HomeForSale@@AAEAAV0@ABV0@@Z),该符号在函数 "public: void __thiscall HomeForSale::copy(class HomeForSale &)" (?copy@HomeForSale@@QAEXAAV1@@Z) 中被引用 *this = lhs; } std::string name;private: HomeForSale(const HomeForSale&); HomeForSale& operator=(const HomeForSale&);};int main(){ HomeForSale h1("first"); HomeForSale h3(h1); // error HomeForSale h2 = h1; // error HomeForSale h4("fouth"); h4 = h1; // error HomeForSale h5("fifth"); // linkage error h5.copy_ctor(h1); HomeForSale h6("sixth"); h6.copy_assignment(h1); // linkage error std::cout << h6.name << "\n"; HomeForSale h7("seventh"); copy_friend(h7, h1); // error? return 0;}
档用户企图拷贝HomeForSale对象,编译器会阻挠他。如果你不慎在member函数或friend函数之内那么做,会轮到连接器发出抱怨。
另一种方法
将连接器错误移植编译器是可能的,而且那是好事,毕竟越早发现错误越好。方法是:在一个专门为了阻止copying动作而设计的base class
内,将copy构造函数
和copy assignment操作符
声明为private
。
因为,只要任何人,甚至是member函数或friend函数,尝试拷贝HomeForSale对象,编译器便试着生成一个copy构造函数
和一个copy assignment操作符
,这些函数的“编译器生成版”会尝试调用其base class
的对应兄弟,那些调用会被编译器拒绝,因为其base class
的拷贝构造函数是private
。
这种方法也有一个问题,由于它总是扮演base class
,因此使用此项技术可能导致多重继承,因为你往往还可能需要继承其他class,而多重继承有时会阻止empty base class optimization
。
#include<iostream>#include<string>class Uncopyable{protected: Uncopyable() {} // 允许derived对象构造和析构 ~Uncopyable() {}private: Uncopyable(const Uncopyable&); // 阻止copying Uncopyable& operator=(const Uncopyable&);};class HomeForSale : private Uncopyable{public: HomeForSale() { } HomeForSale(const std::string& lhs) : name(lhs) { } HomeForSale& copy_ctor(HomeForSale& lhs) { return HomeForSale(lhs); } void copy_assignment(HomeForSale& lhs) { *this = lhs; } std::string name;private: //HomeForSale(const HomeForSale&); //HomeForSale& operator=(const HomeForSale&);};int main(){ HomeForSale h1("first"); HomeForSale h3(h1); // error HomeForSale h2 = h1; // error HomeForSale h4("fouth"); h4 = h1; // error HomeForSale h5("fifth"); // complie err, not linkage error h5.copy_ctor(h1); HomeForSale h6("sixth"); h6.copy_assignment(h1); // complie err, not linkage error std::cout << h6.name << "\n"; return 0;}
请记住
为驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
3 为多态基类声明virtual析构函数
C++指出,当derived class
对象经由一个base class
指针被删除,而该base class
带着一个non-virtual
析构函数,其结果未定义 —— 实际执行时通常发生的是,对象的derived
成分没被销毁。于是造成一个诡异的“局部销毁”对象,从而导致资源泄露。
base类没有使用virtual析构函数
#include <stdio.h>#include <iostream>using namespace std;class base {public: base() {cout << "base()\n";} ~base() {cout << "~base()\n";} // note, have no virtualprivate: int v1;};class derived : public base {public: derived() {cout << "derived()\n";} ~derived() {cout << "~derived()\n";}private: int v2;};int main() { //derived obj; base *b = new derived; // do something delete b; return 0;}/*output:base()derived()~base() */
base类使用virtual析构函数
#include <stdio.h>#include <iostream>using namespace std;class base {public: base() {cout << "base()\n";} virtual ~base() {cout << "~base()\n";} // have virtualprivate: int v1;};class derived : public base {public: derived() {cout << "derived()\n";} ~derived() {cout << "~derived()\n";}private: int v2;};int main() { //derived obj; base *b = new derived; // do something delete b; return 0;}/*output:base()derived()~derived()~base() */
观点1:任何class只要带有virtual函数,都几乎确定应该也有一个virtual析构函数。
观点2:如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当做base class,令其析构函数为virtual往往是一个馊主意。因为,欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。这份信息通常是由一个所谓
vptr(virtual table pointer)
指针指出,vptr
指向一个由函数指针构成的数组,称为vtbl(virtual table)
。每一个带有virtual函数的class都有一个相应的vtbl
。当对象调用某一virtual函数,实际被调用的函数取决于该对象的vptr
所指的那个vtbl
(编译器在其中寻找合适的函数指针)。这样,如果base class内含virtual函数,那么其对象的体积会增加,在32-bits计算机体系结构中将多占用32bits(vptr大小);而在64-bits计算机体系结构中多占用64bits(指针大小为8字节)。观点3:标准库string不含任何virtual函数,但有时程序员会错误地把它当做base class。那么,当你在程序任意某处无意间将一个
pointer-to-specialstring
转换为一个pointer-to-string
,然后将转换所得的那个string指针
delete掉,则立刻被流放到”不明确行为上”。很不幸C++没有提供类似Java的final classes
禁止派生的机制。
请记住
- 从里向外构造(ctor),从外向里析构(dtor)
- polymorphic (带多态性质) base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- classes的设计目的如果不是作为base classes使用,或不是为了具备多态性使用,此class就不该声明virtual析构函数。
4 别让异常逃离析构函数
C++并不禁止析构函数吐出异常
,但它不鼓励你这样做。
#include <iostream>#include <exception>#include <vector>using namespace std;class Widget {public: Widget() { cout << "Widget()\n"; } ~Widget() { cout << "~Widget()\n"; throw std::runtime_error("~Widget()"); }private: int v;};int main(){ vector<Widget> w_vec; w_vec.resize(3); return 0;}/*output:Widget()Widget()Widget()~Widget()libc++abi.dylib: terminating with unexpected exception of type std::runtime_error: ~Widget()Abort trap: 6 */
当vector对象被销毁,它有责任销毁其内含的所有对象。假设vector内含10个对象,而在析构第一个元素期间,有个异常抛出,其他9个对象还是应该被销毁,否则它们保存的任何资源都会发生泄漏。因此,应该调用它们各个析构函数。
正确的处理方法:在析构函数里捕获每一个异常
#include <iostream>#include <exception>#include <vector>using namespace std;class Widget {public: Widget() { cout << "Widget()\n"; } ~Widget() { // 析构函数里如果抛出异常,需要自己捕获处理,否则会资源泄漏 try { cout << "~Widget()\n"; throw std::runtime_error("~Widget()"); } catch (std::runtime_error &e) { cout << "catch exception at ~Widget()\n"; } }private: int v;};int main(){ try { vector<Widget> w_vec; w_vec.resize(3); } catch (...) { cout << "catch exception at main()\n"; } return 0;}/*Widget()Widget()Widget()~Widget()catch exception at ~Widget()~Widget()catch exception at ~Widget()~Widget()catch exception at ~Widget()*/
请记住
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。
- 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么
class
应该提供一个普通函数(而非在析构函数中)执行该操作。
5 绝不在构造和析构过程中调用virtual函数
你不该在构造函数和析构函数期间调用virtual函数,因为这样的调用不会带来你预期的效果。
例如:假设你有个class继承体系,用来塑模股市交易如买进、卖出的订单等等,这样的交易一定要经过审计,所以每当创建一个交易对象,在审计日志中也需要创建一笔适当记录。
#include<stdio.h>#include<iostream>class Transaction { // base classpublic: Transaction(); // 做出一份因类型不同而不同的日志记录 virtual void logTransaction() const = 0;};Transaction::Transaction(){ std::cout << "Transaction()\n"; // 最后动作是记录日志 logTransaction();}class BuyTransaction: public Transaction { // derived classpublic: virtual void logTransaction() const { std::cout << "BuyTransaction::logTransaction()\n"; }};class SellTransaction: public Transaction { // derived classpublic: virtual void logTransaction() const { std::cout << "SellTransaction::logTransaction()\n"; }};int main(){ BuyTransaction bt;}/*g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cppno_virtual_in_ctor_dtor.cpp:17:2: warning: call to pure virtual member function 'logTransaction'; overrides of 'logTransaction' in subclasses are not available in the constructor of 'Transaction' logTransaction(); ^no_virtual_in_ctor_dtor.cpp:9:2: note: 'logTransaction' declared here virtual void logTransaction() const = 0; ^1 warning generated. */
发现无法调用derived class
的函数,在编译期间就报错了。把pure virtual
去掉:
#include<stdio.h>#include<iostream>class Transaction { // base classpublic: Transaction(); // 做出一份因类型不同而不同的日志记录 virtual void logTransaction() const { std::cout << "Transaction::logTransaction()\n"; }};Transaction::Transaction(){ std::cout << "Transaction()\n"; // 最后动作是记录日志 logTransaction();}class BuyTransaction: public Transaction { // derived classpublic: virtual void logTransaction() const { std::cout << "BuyTransaction::logTransaction()\n"; }};class SellTransaction: public Transaction { // derived classpublic: virtual void logTransaction() const { std::cout << "SellTransaction::logTransaction()\n"; }};int main(){ BuyTransaction bt;}/*g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp./no_virtual_in_ctor_dtor Transaction()Transaction::logTransaction() */
这次可以编译过了,但是发现调用的并不是派生类的virtual函数。
原因分析:
- 在创建派生类对象时,
derived class
对象内的bass class
成分会在derived class
自身成分被构造之前先构造妥当。Transaction构造函数调用virtual函数logTransaction,这时被调用的logTransaction是Transaction内的版本,不是BuyTransaction内的版本。base class构造期间virtual函数绝不会下降到derived classes阶层,在base class构造期间,virtual函数不是virtual函数。- 由于
base class
构造函数的执行更早于derived class
构造函数,当base class
构造函数执行时derived class
的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived classes
阶层,而derived class
的函数使用的local成员变量尚未初始化,将导致不明确行为。- 在
derived class
对象的的base class
构造期间,对象的类型是base class
而不是derived class
。不只virtual函数会被编译器解析至base class
,若使用运行期类型信息(runtime type information),例如dynamic_cast
和typeid
,也会把对象视为base class
类型。
相同的道理也适用于析构函数。一旦derived class
析构函数开始执行,对象内的derived class
成员变量便呈现未定义值,所以C++视它们仿佛不再存在,进入base class
析构函数后对象就成为一个base class
对象。
解决方法:
如何确保每次一有Transaction继承体系上的对象被创建,就会有适当版本的logTransaction被调用呢?
一种做法:
是在class Transaction内将logTransaction函数改为non-virtual
,然后要求derived class
构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全地调用non-virtual
logTransaction。
#include <stdio.h>#include <iostream>#include <string>class Transaction { // base classpublic: explicit Transaction(const std::string& logInfo); // 做出一份因类型不同而不同的日志记录 void logTransaction(const std::string& logInfo) const { std::cout << "Transaction::logTransaction(): " << logInfo << "\n"; }};Transaction::Transaction(const std::string& logInfo){ std::cout << "Transaction()\n"; // 最后动作是记录日志 logTransaction(logInfo);}class BuyTransaction: public Transaction { // derived classpublic: BuyTransaction(const std::string ¶s) : Transaction(createLogString(paras)) // 将log信息传给base class构造函数 {}private: static std::string createLogString(const std::string& paras);};std::string BuyTransaction::createLogString(const std::string& paras){ if (paras == "1") return "1+"; else if (paras == "2") return "2+"; else return "+";}int main(){ BuyTransaction bt1("1"); BuyTransaction bt2("2");}/*g++ -o no_virtual_in_ctor_dtor no_virtual_in_ctor_dtor.cpp./no_virtual_in_ctor_dtorTransaction()Transaction::logTransaction(): 1+Transaction()Transaction::logTransaction(): 2+ */
请记住
在构造和析构期间不要调用virtual函数,因为这类调用从不下降至
derived class
。
6 令operator=返回一个reference to *this
int x, y, z;x = y = z = 10; // 赋值连锁形式x = (y = (z = 10)); // 赋值采用右结合律
为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这是你为classes实现赋值操作符时应该遵循的协议。
#include <iostream>class Widget {public: explicit Widget(int rhs) : a(rhs) { } Widget& operator=(const Widget& rhs) // 返回类型是个reference,指向当前对象 { a = rhs.a; return *this; // 返回左侧对象 } Widget& operator=(int rhs) // 此函数也适用,即使此操作符的参数类型不符协定 { a = rhs; return *this; } Widget& operator+=(const Widget& rhs) { this->a += rhs.a; return *this; } void print() { std::cout << a << "\n"; }private: int a;};int main(){ Widget w(1); w.print(); Widget w2(2); w = w2; w.print(); int i = 100; w = 100; w.print(); w += w2; w.print();}/* ./operator 12100102 */
请记住
令赋值(assignment)操作符返回一个reference to *this。
7 在operator=中处理自我赋值
自我赋值发生在对象被赋值给自己时,这看起来有点愚蠢,但是它合法。所以不要认定客户绝不会那么做。此外自我赋值动作并不总是可以一眼看出来。
// 潜在的自我赋值a[i] = a[j]; *px = *py;
这些并不明显的自我赋值,是名带来的结果。实际上,两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名,因为一个base class的reference或pointer可以指向一个derived class对象。
class Base { ... };class Derived: public Base { ... };// rb和*pb有可能其实是同一对象void doSomething(const Base& rb, Derived* pd);
因此,在处理自我赋值时应该注意保证:
1. 自我赋值安全问题
2. 异常问题
class Bitmap { ... };class Widget {public: Widget& operator=(const Widget& rhs);private: Bitmap* pb;};// 不安全的版本Widget& Widget::operator=(const Widget& rhs){ delete pb; pb = new Bitmap(*rhs.pb); return *this;}// 安全的版本,但不具备异常安全性// 如果new异常,Widget最终会持有一个指针指向一块被删除的BitmapWidget& Widget::operator=(const Widget& rhs){ if (this == &rhs) return *this; // identity test delete pb; pb = new Bitmap(*rhs.pb); return *this;}// 异常安全的版本,同时也是自我赋值安全的// 现在如果new异常,pb保存原状// 即使没有identity test,这段代码还是能够处理自我赋值,虽然不是最高效的方法,但是行得通Widget& Widget::operator=(const Widget& rhs){ Bitmap* pOrig = pb; pb = new Bitmap(*rhs.pb); delete pOrig; return *this;}
对于第三个版本的补充说明:
如果你很关心效率,可以把identity test再次放回函数起始处。然而这样做之前先问问自己,你估计自我赋值的发生概率有多高?因为这项测试也需要成本,它会使代码变得大一些并导入一个新的控制流分支,而两者都会降低执行速度。Prefetching, caching和pipelining等指令的效率都会因此降低。
另一个替代方案是:使用copy and swap技术。此方法,为了伶俐巧妙而牺牲了清晰性。
请记住
- 确保当对象自我赋值时operator=有良好行为,其中技术包括,比较来源对象和目标对象的地址,精心周到的语句顺序,以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
8 复制对象时勿忘其每一个成分
- 设计良好的OO系统会将对象的内部封装起来,只留两个函数负责对象拷贝,copy构造函数和copy assignment操作符,我们称它们为copying函数。
- 编译器会在必要的时候为我们的class创建copying函数,并说明这些“编译器生成版”的行为是,将被拷对象的所有成员变量都做一份拷贝。 如果你声明自己的copying函数,意思就是告诉编译器你不喜欢缺省实现中的某些行为,编译器仿佛被冒犯似的,会以一种奇怪的方式回敬你,当你的实现代码出错时却不告诉你。
- 如果你为class添加一个成员变量,你必须同时修改copying函数,如果你忘记了,编译器也不会告诉你。
- 任何时候,只要你承担起为derived class撰写copying函数的责任,必须很小心地也复制其base class成分,那些成分往往是private,所以你无法直接访问它们,你应该让derived class的copying函数调用相应的base class函数。
- 如果你发现你的copy构造函数和copy assginment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名为init。
// 调用base class的copy构造函数Derived::Derived(const Derived& rhs): Base(rhs), xxx(rhs.xxx){}Derived& Derived::operator=(const Derived& rhs){ Base::operator=(rhs); // 对base class成分进行赋值 xxx = rhs.xxx; return *this;}
当你编写一个copying函数,请确保:
* 复制所有local成员变量
* 调用所有base classes内的适当的copying函数
请记住
- copying函数应该确保复制对象内的所有成员变量,及所有base class成分。
- 不要尝试以某个copying函数实现另一个copying函数,应该将相近的代码放在第三个函数中,并由两个copying函数调用。
- 《Effective C++》和《More Effective C++》汇总
- 《Effective C++》读书笔记
- 《Effective C#》 翻译札记
- 《Effective C++》读感
- 《Effective C++》读后感
- 读《Effective C++》所想
- 《Effective C++》读书笔记
- 《Effective C#》 翻译札记
- 细读《Effective C++》之一
- 《Effective C#》 翻译札记
- effective c sharp 印象
- 细读《Effective C++》之一
- Begin reading 'Effective c++'
- 《Effective C#》 翻译札记
- 《Effective c++》读书笔记
- 《Effective C++》 笔记
- Effective C++(1-4)
- Effective C++(5-12)
- VirtualBox fedora系统安装增强工具
- 求数组的连续子数组之和的最大值(一维二维)
- 人口模型回归
- 生命之树
- ploymer (一) 第一个demo
- Effective C++
- 分组列表,tabs滑动固定框架-stickylistheaders
- python numpy多维数组切片问题
- Mysql数据库引擎
- cogs 727. [网络流24题] 太空飞行计划
- 多租户过程记录四(修改代码中)
- Machine Learning with Scikit-Learn and Tensorflow 6.6 基尼不纯度/熵
- golang 类似set使用
- sublime text 快捷操作