前言:如何有效运用C++,包括一般性的设计策略,以及带有具体细节的特定语言特性。知道细节很重要,否则如果疏忽几乎总是导致不可预期的程序行为(undefined behavior)。本文总结对于如何使用C++的一些建议,从而让你成为一个有战斗力的C++程序员。
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 virtual
private:
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 virtual
private:
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 class
public:
Transaction();
// 做出一份因类型不同而不同的日志记录
virtual void logTransaction() const = 0;
};
Transaction::Transaction()
{
std::cout << "Transaction()\n";
// 最后动作是记录日志
logTransaction();
}
class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const {
std::cout << "BuyTransaction::logTransaction()\n";
}
};
class SellTransaction: public Transaction { // derived class
public:
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.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 class
public:
Transaction();
// 做出一份因类型不同而不同的日志记录
virtual void logTransaction() const {
std::cout << "Transaction::logTransaction()\n";
}
};
Transaction::Transaction()
{
std::cout << "Transaction()\n";
// 最后动作是记录日志
logTransaction();
}
class BuyTransaction: public Transaction { // derived class
public:
virtual void logTransaction() const {
std::cout << "BuyTransaction::logTransaction()\n";
}
};
class SellTransaction: public Transaction { // derived class
public:
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 class
public:
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 class
public:
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_dtor
Transaction()
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
1
2
100
102
*/
请记住
令赋值(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最终会持有一个指针指向一块被删除的Bitmap
Widget& 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函数调用。