前言
这篇文章将记录我阅读《effective C++》时的一些笔记,每天更新一点,一直更新到完结。
今天是更新第12天。
0.导读
- 除非类的构造函数用于隐式类型转换,否则声明为
explicit
(用来避免隐式类型的转换)
class B{
public:
explicit B(int x = 0, bool b = true);
};
void doSomething(B bObject)
B bObj1;
doSomething(bObj1);//√
doSomething(28);//×,没有从28到B的隐式转换
doSomething(B(28));//√,可以用B构造函数对int显式转换
- 类等号用的是copy构造函数和copy赋值函数?
- 如果对象是被新定义的(如
Widget w3 = w2
),使用的是copy构造函数。 - 如果没有新对象被定义(如
w1=w2
),使用的是赋值操作。
class Widget {
public:
Widget();//default构造函数
Widget(const Widget& rhs);//copy构造函数
Widget& operator=(const Widget& rhs);//copy赋值操作符
...
};
Widget w1;//default构造函数
Widget w2(w1);//copy构造函数
w1 = w2;//copy赋值函数
Widget w3 = w2;//调用的是copy构造函数!!
-
尽量不要用值传递的方式(特别对于用户自定义的类型)调用copy构造函数,用传const引用更好。
-
不明确行为:
- 对空指针使用间接访问操作(*)
- 无效的数组索引(数组越界)
- 字符数组末尾还有一个’\0’别忘了:
char name[] = "Darla";//长度为6,最后的'\0'别忘了
- 命名习惯
- lhs和rhs分别表示left hand side和right hand side,对于它们的使用可以有如下准则(不强制,要看情况)
- 在非成员二元操作符里使用lhs和rhs
- 在成员函数使用rhs(左侧实参由this指针表现)
class Rational{
public:
Rational operator* (const Rational& rhs);//成员函数
};
const Rational operator* (const Rational& lhs, const Rational& rhs);//非成员函数
- 指针的命名:指向T型对象的指针命名为pt,意思是pointer to T。
- 引用的命名:指向T型对象的引用命名为rt,意思是reference to T。
class Airplane;
Airplane* pa; //pa="pointer to Airplane"
Airplane& ra = pa; //ra="reference to Airplane"
1. 视C++为一个语言联邦
C和C++有什么不同,C++增加了什么功能?
C++最开始只是C加上面向对象的特性,后来成为了多重泛型编程语言,成为同时支持过程形式,面向对象形式,函数式形式,泛型形式,元编程形式的语言。例如加入了Exception(异常)、template(模板),STL。
- C:C++以C为基础,但用C++的C成分工作,体现其局限:没有模板,没有异常,没有重载。C推荐使用值传递,C++推荐使用引用传递。
- 面向对象的C++:面向对象部分。加入了classes(构造函数和析构函数)、封装、继承、多态、virtual函数(动态绑定)
- Template C++:泛型编程部分。威力很强大,带来了新的编程泛型模板元编程(TMP,template meta programming)。
- STL:STL是一个template的程序库,对容器、迭代器、算法和函数对象进行规约,
2. 尽量用const, enum, inline替代#define
2.1 const替代#define定义常量
使用#define
其实是用编译器替代预处理器。#define
不是语言的一部分:
#define APSECT_RATIO 1.653
编译器并未看到APSECT_RATIO
,因为可能开始处理源码被预处理器全部移走了,而APSECT_RATIO
并未进入记号表。而如果报错信息显示提到1.653而不是APSECT_RATIO
,APSECT_RATIO
定义在非你所写的头文件,很难定位到错误来自何处。
解决方式是使用常量替代:
const double AspectRatio = 1.653; //使用大写名称通常用于宏,因此这里改变名称
这样AspectRatio
就可以被编译器看到,且对浮点常量来说,常量可能比使用#define
导致较小的码,盲目用宏替换APSECT_RATIO
为1.653可能让源码出现多份1.653,使用要非常小心,而使用AspectRatio
不会出现这种情况。
特殊情况:
(1)常量指针,由于常量常被放在头文件里,因此指针声明为const
很有必要。要在头文件定义指向常量的char*-based的指针,写const
两次(在第3小节会讨论const):
const char* const authorName = "Scott Meyers";
string比char*-based更现代化点,所以上面的authorName
往往定义为:
const std::string authorName("Scott Meyers");
(2)class的专属常量。由于用#define
是无视作用域的,所以#define
无法被用来创建class专属常量,一旦宏被定义,则它在之后的编译过程有效(除非某处被#undef
)。这意味着:#define
不能定义class专属常量,也不能提供任何封装性,所以没有所谓的private # define
这样的东西。而const
是可以封装的。class里定义常量成员作用域只在这个类里,为了让此常量至多有一个实体,让它成为一个static
成员:
class GamePlayer{
private:
static cosnt int NumTurns = 5;//常量声明式
int scores[NumTurns];//使用该常量
};
上面的NumTurns
没有提供常量定义式,但也是可以使用的,这是因为对于类的专属static
整数类型常量(如int
, char
, bool
),只要不取它们的地址,只需声明即可使用无需再提供定义式。
但如果取class专属常量的地址,就必须提供定义:
const int GamePlayer::NumTurns;//NumsTurns的定义
声明需放在头文件,定义放在实现的cpp文件。而由于class常量已在声明里获得了初值,在先前声明NumTurns
的时候设置为了5,因此定义时不可以再设置初值。
如果你的编译器不支持static
在其声明式获得初值,可以将初值放在定义式。
class CostEstimate {
private:
static const double FudgeFactor; // static class 常量声明放在头文件里
}
const double
CostEstimate::FudgeFactor = 1.35; // static class 常量定义放在实现文件里
2.2 enum代替#define定义常量
但是上面的写法有一个例外,如果编译期间类内就需要这样一个类内常量值(注意类内只是声明了常量值,定义在类的外面),比如数组声明中数组的大小(前面的scores[NumTurns]
),这时候如果编译器不允许用“static整型class常量”完成类内初值设定,可以改用枚举类型数值充当int来使用:
class GamePlayer{
private:
enum {NumTurns = 5}; //NumTurns是5的一个记号名称
int scores[NumTurns];
...
};
枚举更像#define
而不像const
,如果想取一个const
的地址是可行的,而取enum
的地址是不可行的(取#define
通常也不可行)。如果不想让别人获得一个指针或引用指向某个整型常量,enum
可以帮助实现这个约束。不够优秀的编译器会为“整数型const对象”分配另外的存储空间(优秀的则不会),这可能不是我们想要的,但是enum
和#define
绝对不会导致非必要的内存分配。
2.3 inline替代#define实现宏
另一个常见的#define
误用是用它实现宏(一个看起来像函数的东西,但不会导致函数调用的额外开销),例如下面这个宏带着宏实参调用函数f:
//以a和b的较大值调用f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
这么长的宏有很多缺点,在编写的时候很容易疏忽掉宏实参的小括号,否则在调用的时候会遭遇麻烦,但即使都加上了小括号,也会发生不可思议的事情:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);//a被累加两次
CALL_WITH_MAX(++a, b + 10);//书上说a被累加一次,在VSCode测试了一下也是累加两次?
调用f之前,a的递增次数取决于它和谁比较!这点很让人懊恼。
为了规避上面的问题,我们只要写出template inline函数就可以获得宏带来的效率以及一般的函数的所有可预料行为和类型安全性,它不需要再函数本体为参数加上括号,不需要操心参数被计算多次,此外callWithMax
也是真正的函数,它遵守作用域和访问规则,例如可以写出类内的private inline函数,而宏则无法完成这件事:
template<typename T>
inline void callWithMax(const T& a, const T& b)//这里使用的是const引用
{
f(a > b ? a : b);
}
通过使用const、enum和inline,我们对预处理器(特别是#define)的需求降低了,但并非完全消除,#include还是必需品,而#ifdef、#ifindef也会继续充当编译的重要角色。
- 对于单纯常量,最好用
const
对象或enum
替换#define
。- 对于形似函数的宏,最好用
inline
函数替代#define
。
3. 尽可能使用const
3.1 const 基本用法
const
允许指定一个不该被改动的对象,而编译器会强制实施这项约束。可以用const
在类外部修饰全局或namespace作用域的常量,或修饰文件、函数、或区块作用域被声明为static
的对象,可以用它修饰类内部的static
和non-static
成员对象。对于指针,可以指出指针、指针所指物,或两者都是const
:
char greeting[] = "Hello";
char* p = greeting; //非const指针,非const数据
const char* p = greeting; // 非const指针,const数据
char* const p = greeting; //const指针,非const数据
const char* const p = greeting; // const指针,const数据
const
出现的位置:
- 在
*
的左侧:被指物是常量; - 在
*
的右侧,指针自身是常量; - 在
*
的两侧,被指物和指针都是常量
如果被指物是常量,可以把const
写在类型之前,也可以写在类型之后,这两种写法没有区别,const
都是出现在*
的左侧:
void f1(const Widget* pw); // f1获得一个指针,指向一个常量的Widget对象
void f2(Widget const * pw); // f2也是
两种形式都有人用,遇到要认识。
3.2 const和STL迭代器
STL迭代器作用像个T指针,声明迭代器为const就像声明指针为const一样(声明一个T const指针),表示这个迭代器不能指向不同的东西,但是它的所指物可以改变,如果希望迭代器所指的东西不可改变(STL模拟处一个const T*的指针),需要使用const_iterator
:
std::vector<int> vec;
...
const std::vector<int>::iterator iter = vec.begin();//iter类似T* const
*iter = 10;//√
++iter;//×,iter是const!
std::vector<int>::const_iterator cIter = vec.begin();//cIter类似const T*
*cIter = 10;//×,cIter是const!
++cIter;//√
3.3 const在函数声明中的使用
const
最大的作用是面对函数声明时的应用,在一个函数声明式里,const
可以和函数返回值、各参数、函数自身(如果是成员函数)有关联。
让函数返回一个常量值,往往可以降低因客户错误而造成的以外,又不至于放弃安全性和高效性,比如考虑有理数的operator*
声明:
class Rational {...};
const Rational operator* (const Rational& lhs, const Rational& rhs);
为什么要返回一个const
呢?如果不这样客户可能实现这样的暴行:
Rational a, b, c;
...
(a * b) = c; // 在 a * b 的成果上调用operator=
上面的情况很奇怪对不对,有程序员这样写吗?但是如果在一个if 判断语句判断两个值相等等号写成了赋值号不就有了上面的情况?(这个问题确实很常见):
if (a * b = c)...//想比较但是写成了赋值
如果a
和b
都是内置类型,这样的代码直接就是不合法,但是用户自定义类型如果不设置const
却不行,因为也确实可以对两值乘积赋值(覆盖了乘积的值!),所以将operator*
的返回值声明为const
可以预防“不被期望的幅值”。
const
参数则没有新颖的概念,它和local const对象一样,除非要改动参数是或local对象,否则请将它们声明为const
,同前面避免了想键入==
却键入=
”的错误。
3.4 const成员函数
const成员函数可以作用于const对象上:
- 可以让class接口容易理解,有时候得知哪个函数可以改动对象的内容而哪个函数不行很重要;
- 是它使操作const对象成为可能,C++用传引用提高程序的效率,该技术可行的前提是有const成员函数,需要有const成员函数处理取得的const对象。
在C++中两个成员函数如果只是常量性不同可以被重载:
class TextBlock {
public:
...
//operator[] for const对象
const char& operator[](std::size_t position) const
{ return text[position];}
//operator[] for 非const对象
char& operator[](std::size_t position)
{ return text[position];}
private:
std::string text;
};
void print(const TextBlock& ctb)//此函数ctb是const
{
std::cout << ctb[0];//调用const TextBlock::operator[]
...
}
调用operator[]可以这样使用:
TextBlock tb("Hello");
std::cout << tb[0]; // 调用非const TextBlock::operator[]
tb[0] = 'x';//√,非const改数据
const TextBlock ctb("Hello");
std::cout << ctb[0]; // 调用const TextBlock::operator[]
ctb[0] = 'x';//×,const不能写数据
非const的operator[]返回类型是一个char类型引用,而不是char,如果返回一个char,下面的句子无法通过编译:
tb[0] = 'x';
函数的返回类型是个内置类型,改动函数返回值不合法,纵使合法,C++以值传递返回意味tb.text[0]的一个副本被改变,这也不是我们想要的结果。
成员函数是const
,有两个概念:bitwise constness(不能更改对象内任何非static成员变量)和logical constness(可以在客户端监测不到的情况下更改对象内任何某些bits)。
- bitwise constness
可能会出现下面的情况,设置为常量但是还是改变了值。
class CTextBlock {
public:
...
char& operator[](std::size_t position) const
{ return pText[position];}
private:
char* pText;
};
...
const CTextBlock cctb("Hello");
char* pc = &cctb[0];//取得cctb指针
*pc = 'J';//cctb改变为了“Jello”
- logical constness
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
std::size_t textLength;// 最近一次计算的文本区块的长度
bool lengthIsValid;//目前的长度是否有效
};
std::size_t CTextBlock::length() const
{
//错误,在const成员函数里不能赋值给textLength和lengthIsValid
if (!lengthIsValid){
textLength= std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
下面的length的实现不是bitwise const,因为textLength和lengthIsValid都可能被修改,但是编译器不通过坚持bitwise const,这时候的解决办法是使用mutable
关键字,它可以释放non-static成员变量的bitwise constness约束
class CTextBlock {
public:
...
std::size_t length() const;
private:
char* pText;
//使用mutable关键字都可以改变成员变量,即使在const成员函数里
mutable std::size_t textLength;
mutable bool lengthIsValid;
};
std::size_t CTextBlock::length() const
{
//修改为mutable变量,在const成员函数里可以赋值给textLength和lengthIsValid
if (!lengthIsValid){
textLength= std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
3.5 const和非const函数避免重复
mutable
也不能解决所有const
问题,如下面的代码:
class CTextBlock {
public:
...
const char& operator[](std::size_t position) const
{
...
return text[position];
}
char& operator[](std::size_t position)
{
...//和const版本的operator[]函数一致
return text[position];
}
private:
std::string text;
};
上面的两个operator[]里都是一样的代码,需要做的是使用常量性转移,令non-const对象调用另一个const函数,不建议反向做法,逻辑上const函数不能改变对象:
class CTextBlock {
public:
...
const char& operator[](std::size_t position) const
{
...
return text[position];
}
char& operator[](std::size_t position)//只调用const op[]
{
return const_cast<char&>(
static_cast<const TextBlock&>(*this)
[position]
);//把op[]返回值的const转除,为*this加上const调用const op[]
}
private:
std::string text;
};
这里使用了两次转型,static_cast
把(*this)添加const
,而const_cast
从返回值移除const
。
const
声明可以帮编译器监测错误用法,可被添加再作用域的对象、函数参数、函数返回类型、成员函数本体。- 编译器强制实施
bitwise constness
,但编写程序应该使用概念上的常量性。const
和non-const
成员函数有等价实现时,用non-const
版本调用const
版本避免代码重复。
4. 确定对象被使用前已先被初始化
4.1 类构造函数的初始化
在某些语境下,x被保证为初始化为0
int x;
但是其他语境不保证:
class Point {
int x, y;
};
Point p;
未初始化会造成不明确行为,最佳处理方法是,在使用对象了前先将它初始化,对于无任何成员的内置类型,必须手工完成此事。
int x = 0;
double d;
std::cin >> d;//用cin初始化
内置类型外的东西初始化使用构造函数,确保构造函数对每一个成员初始化。
class Point {
public:
Point(cosnt xP, const yP);
private:
int x, y;
};
Point::Point(const& xP, cons& yP)
{
x = xP;//这里是赋值不是初始化
y = yP;
}
上面的代码的x和y是赋值而不是初始化,先构造后赋值效率也不高,初始化是构造函数被调用前发生的。初始化应该这样写,构造函数本体没有操作直接构造并初始化,效率较高:
class Point {
public:
Point(cosnt xP, const yP);
private:
int x, y;
};
Point::Point(const& xP, cons& yP):x(xP), y(yP)
{}
上面的x和y都是进行copy构造。无参数的构造函数也可以这样实现:
class Point {
public:
Point(cosnt xP, const yP);
private:
int x, y;
};
Point::Point():x(0), y(0)
{}
所以总是使用成员初值列,在初值列列出所有成员变量,以免还得记得哪些成员变量无需初值。这种做法很简单,对于如果成员变量是const或引用一定需要初值不能被赋值的情况也适用。
类有多个构造函数,每个构造函数又有自己的成员初值列,如果成员变量或基类很多,这时候多份初值列可能有所重复,这种情况可以合理在初值列遗漏“赋值像初始化一样好的成员变量”,改用赋值操作,并把赋值移到某个private函数,供所有构造函数调用,这种方法对“成员变量的初值由文件或数据库读入”特别由于。但比起伪初始化,通过成员初值列完成的真正初始化更为推荐。
C++里基类总是先于继承类被初始化,类的成员变量总是以其声明顺序初始化,即使在初值列顺序不一样。为了避免错误,初值列的顺序和类的成员变量的声明顺序最好一致。
4.2 non-local static对象初始化次序
static对象寿命从被构造出来到程序结束为止,包括global、namespace作用域内的对象、class内、函数内、file作用域被声明为static的对象。函数内的static对象称为local static对象(对函数是local的),其他static对象是non-local static对象。析构函数在main函数结束被自动调用自动销毁这些static对象。
问题是两个源码文件,每一个都有non-local static对象,其中某个non-local static对象的初始化动作使用了另一个编译单元内的某个non-local static对象,它使用的对象可能没被初始化,C++对定义在不同编译单元的non-local static对象的初始化次序没有明确的定义。解决的办法是:将每个non-local static对象搬到自己的专属函数,此对象在函数被声明为static,使用这些函数返回一个引用指向它所指的对象,用户调用这些函数而不直接涉及这些对象,non-local static对象被static对象替换(单例模式)。
这是因为C++函数内的local static对象会在该函数被调用时、首次遇到对象定义式被初始化,所以如果以函数对用替换直接访问non-local static对象保证了获得的引用是经过初始化的对象,而且没有使用non-local static对象的仿真函数,不会引发构造和析构的成本。
class FileSystem{ ... };
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
class Directory { ... };
Directory::Directory(params)
{
...
std::size_t disks = tfs().numDisks();
...
}
Directory& tempDir()
{
static Directory td;
return td;
}
这样用户使用下面语句创建对象就保证了FileSystem对象在Directory对象前建立,使用的是它们的引用。
Directory tempDir(params);
使用返回引用函数可以防止次序问题,前提是有一个对对象而言合理的初始化次序。为了避免初始化前过早使用它们,需要:
- 手工初始化内置non-member对象。
- 使用成员初值列对付对象的所有成分。不要再构造函数本体使用赋值,初值列成员变量次序和类里的声明次序相同。
- 初始化次序不确定性氛围下加强设计。用local static对象替换non-local static对象。
5. 了解C++默默编写并调用哪些函数
如果类是一个空类,C++会为它声明一个copy构造函数(无任何构造函数还会声明一个default构造函数)、copy赋值操作符和一个析构函数,所有这些函数都是public和inline的,当它们被调用才会被编译器创建出来。。
class Empey{};
等价于
class Empty{
public:
Empty() {...}//default构造函数
Empty(const Empty& rhs) {...}//copy构造函数
~Empty() {...}//析构函数
Empty& operator=(const Empty& rhs){...}//copy赋值操作符
};
除非类的基类自身声明有virtual 析构函数,否则编译器产出的析构函数是non-virtual的。copy构造函数和copy赋值操作符编译器只将来源对象的每个non-static成员变量拷贝到目标对象。
+ 如果声明了构造函数,编译器不会产出default构造函数。
+ 如果在类里有reference或const成员,必须自己定义赋值操作,否则编译器拒绝生成赋值操作符。
+ 如果基类将copy赋值操作符声明为static,编译器拒绝为其继承类生成copy赋值操作符。因为static的基类成员函数继承类无权调用。
6. 若不想使用编译器自动生成的函数,就该明确拒绝。
编译器会自动声明copy构造函数和copy赋值运算符,而且是public的。
一种解决思路是声明为private的并且不实现它(只有声明),但是不安全(被member函数和友元函数调用,造成连接错误)。
class HomeForSale {
public:
...
private:
...
HomeForSale(const HomeForSale&);//只有声明
HomeForSale& operator=(const HomeForSale&);
};
另一种解决思路是想在编译时发现调用它们的错误(更早出现错误),设计一个基类声明copy构造函数和copy赋值运算符,然后定义一个子类继承它,由于基类的copy构造函数和copy赋值运算符是private的,子类调用会出现编译错误。基类不包含数据(是empty基类),可能导致多重继承,多重继承会阻止empty 基类优化,
class Uncopyable {
protected://允许子类构造和析构
Uncopyable() {}
~Uncopyable() {}
private:
...
Uncopyable(const Uncopyable&);//阻止子类copying
Uncopyable& operator=(const Uncopyable&);
};
7. 为多态基类函数声明虚析构函数
7.1 虚函数
来看一个记录时间的例子:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
...
};
class AtomicClock: public TimeKeeper {...};//原子钟
class WaterClock: public TimeKeeper {...};//水钟
class WristClock: public TimeKeeper {...};//腕表
使用TimeKeeper
函数可以使用工厂函数getTimeKeeper
返回计时对象,用一个基类指针指向继承类对象:
TimeKeeper* getTimeKeeper();//返回一个指针指向TimeKeeper派生类的动态分配对象
被getTimeKeeper
返回的对象要位于堆才不会泄漏内存和其他资源,所以将factory函数返回的对象delete很重要:
Timekeeper* ptk = getTimerKeeper();
...
delete ptk;
下面这样写也是错的:
AtomicClock* ptk = new AtomicClock();
Timekeeper* tk;
tk = ptk;
...
delete tk;
这里由于基类的析构函数非虚函数,子类对象经由基类函数指针删除,其结果未定义:对象的derived成分没被销毁,如果getTimeKeeper
返回的是一个AtomicClock
的对象,其内的AtomicClock
可能没被销毁,而AtomicClock
的析构函数未执行,基类成分(TimeKeeper
的部分)却被销毁了,出现了局部销毁的情况,从而使得资源泄漏。
解决方式是给基类一个虚析构函数,然后删除子类进藏中完整地销毁对象(包括derived的部分)
class TimeKeeper {
public:
TimeKeeper();
virtual ~TimeKeeper();
...
};
Timekeeper* ptk = getTimerKeeper();
...
delete ptk;//√
虚函数的目的是允许子类的实现可以客制化,基类定义一个虚函数,在子类可以有不同的实现,任何有虚函数的类也有一个虚析构函数。
如果类不含虚函数,通常表示它不是一个基类,当类不是基类,不要让其析构函数是一个虚函数。虚函数调用的起始是一个虚表指针,它指向一个函数指针构成的数组称为虚表,当对象调用某个虚函数,实际被调用的函数取决于该对象的虚表指针指向的虚表。使用虚函数会使对象的体积增加,其他语言不具有虚表指针所以不好移植给其他语言。
一个没有虚函数时不要将其析构函数声明为虚函数,否则是错误的。最好也不要继承STL容器和string等类,它们不带虚析构函数,使用时可能会造成内存泄漏。
7.2 纯虚函数
纯虚函数导致一个抽象类,不能被实体化,所以不能为这种类型实体化,这个类统称被用于当基类。抽象类要声明一个纯虚的析构函数:
class AWOV {
public:
virtual ~AWOV() = 0;//声明纯虚析构函数
};
一个窍门是:为纯虚析构函数提供定义:
AWOV::~AWOV() { }//纯虚析构函数的定义
这样就可以通过基类接口处理子类对象的析构(用在带多态的基类)。
- 对于多态基类应声明一个虚析构函数,如果类带有虚函数,它应该有一个虚析构函数。
- 类的设计不是为了基类使用或不具有多态性,不声明虚析构函数。
8. 别让异常逃离析构函数
析构函数如果出现了异常,程序可能过早结束或出现不明确行为。来看一个例子:
class DBConnection {
...
static DBConnection create();//数据库连接
void close();//关机
};
class DBConn()//管理DBConnection
{
public:
...
~DBConn()//确保数据库连接总会被关闭
{
db.close();
}
private:
DBConnection db;
};
客户使用:
{
DBConn dbc(DBConnection:create());
...
}//作用域结束应该销毁DBConn,自动为DBConnection对象调用close函数
如果DBConn的析构函数里的close出现了异常,那就难以驾驭了,两个办法解决:
方法1:close异常使用abort结束程序,将不明确行为终止
DBConn::~DBConn()
{
try {db.close();}
catch (...) {
//输出错误信息
std::abort();
}
}
方法2:吞下close异常,可能压制了一些动作失败的消息,但有时候需要这样:忽略一个错误后程序还能继续执行。
DBConn::~DBConn()
{
try {db.close();}
catch (...) {
//输出错误信息
}
}
但是这两种办法无法对“导致close出现异常的情况”做出反映,一种解决方式是重新设计DBConn接口:
class DBConnection {
...
static DBConnection create();//数据库连接
void close();//关机
};
class DBConn()//管理DBConnection
{
public:
...
void close()
{
db.close();
closed = true;
}
~DBConn()//确保数据库连接总会被关闭
{
if (!closed){
try {
db.close();
}
catch (...) {
//输出错误信息
...
}
}
}
private:
DBConnection db;
bool closed
};
这样如果未关闭数据库closed记录为false析构函数再尝试关闭它,这样就把close调用的责任从DBConn析构函数转移到了DBConn客户手上,这给客户使用增加了处理错误的机会。
- 析构函数不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们或结束程序。
- 客户需要对某个操作函数运行的异常做出反映,嘞应该提供一个普通函数执行操作(不在析构函数)。
9. 绝不在构造和析构过程中调用虚函数
如果在基类的构造函数里使用了虚函数,那么在其子类对象的基类部分构造过程中,这里调用的是基类的虚函数,而不是子类的虚函数(基类构造过程中,虚函数不是虚函数,好像是基类自己的成员函数一样),子类对象在子类构造函数执行前不会成为一个子类对象。这个反直觉的行为有个好处,由于基类构造函数更早,当基类函数执行时子类的成员变量还没有初始化,调用的虚函数会下降到子类,但是子类的函数需要使用自己的成员变量,而这些成员变量还没有初始化,这是非常危险的。同样的道理用用于析构函数,一旦子类析构函数执行,对象的子类成员呈现未定义值,再进入基类析构函数对象变为基类对象。
然而一般来说基类的虚函数是纯虚函数,没有被定义,程序无法连接。而如果虚函数有定义,程序会被调用,这时子类对象可能就调用了错误版本的虚函数。侦测构造和析构过程中是否调用虚函数很难,一个好的避免代码重复的办法是把共同的初始化代码放在一个函数里。
如何保证每次创建对象有适当版本的虚函数版本被调用?
- 方法1:不在构造函数调用构造虚函数
- 方法2:把虚函数改成非虚函数(既然无法使用虚函数从基类向下调用,让子类将必要的信息向上传递至基类构造函数)
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;//是个非虚函数
};
Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo);//如今是个非虚函数
}
class BuyTransaction: public Transaction {
public:
BuyTransaction(parameters)
:Transaction(createLogString(parameters))//将log信息传递给基类构造函数
{...}
...
private:
static std::string createLogString(parameters);
};
注意这里子类BuyTransaction内的private static函数createLogString,比起在成员初值列内给予基类数据,使用辅助函数创建一个值传递给基类构造函数比较方便可读,令其为static,就不会意外指向初期未成熟子类BuyTransaction对象内未初始化的成员变量(如果不这样,子类成员变量处于未定义的状态,所以基类构造和析构期间调用的虚函数不可下降为子类)
- 在构造和析构函数里不要调用虚函数,因为这类调用不会下降到子类。
10. 令operator=返回一个reference to *this
关于赋值可以写作连锁形式,赋值符合右结合律:
int x, y, z;
x = y = z = 15;
上述赋值被解析为:
x = (y = (z = 15));
15先赋值给z,z再赋值给y,y再赋值给x,为了实现这样的连锁赋值,赋值操作符必须返回一个引用指向左侧实参,特别是在class实现赋值操作符,必须这样,并且对于所有赋值相关运算都可以这样操作:
class Widget {
public:
...
Widget& operator=(const Widget& rhs)//返回类型是个引用,指向当前对象
{
...
return *this;//返回左侧实参
}
Widget& operator+=(const Widget& rhs)//对于+=, -=, *=也适用
{
...
return *this;//返回左侧实参
}
};
- 令operator=返回一个reference to *this
11. 在operator=中处理自我赋值
自我赋值发生在对象被赋值给自己:
class Widget {
...
};
Widget w;
...
w = w; //赋值给自己
这虽然奇怪,但是合法,下面的例子也是自我赋值:
a[i] = a[j];//潜在的自我赋值(i=j)
*px = *py;//潜在的自我赋值(两个指针指向同一个东西)
引用和指针都可以指向同一个东西,所以引出了别名(有一个以上的方法指称某对象)。而两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能造成别名,一个基类的引用或指针可以指向一个子类对象:
class Base {...};
class Derived: public Base {...};
void doSomething(const Base& rb, Derived* pd);//rd和*pd可能是同一对象
如果使用对象管理资源,在copy时举措正确,自赋值不会有问题,如果尝试自行管理资源(自己写一个class),可能会出现在停止使用资源前意外释放了它,比如建立一个class保存指针指向一块动态分配的位图:
class Bitmap {...};
class Widget {
...
private:
Bitmap* pb;//指针,指向一个从heap分配而得到的对象
};
Widget&
Widget::operator=(const Widget& rhs)//一份不安全的operator=实现版本
{
delete pb;//停止使用当前的bitmap
pb = new Bitmap(*rhs.pb);//使用rhs's bitmap的副本
return *this;
}
这里自赋值的问题是,operator=函数里的*this可能和rhs是同一个对象,这样delete不仅销毁了当前对象的bitmap,也销毁了rhs的bitmap,在函数末尾,就出现了一个指向已被删除的对象的指针,出现自赋值安全性和异常安全性问题。
要阻止这种错误,传统做法是用operator=最前面的“证同测试”达到自赋值的检测目的:
Widget::operator=(const Widget& rhs)//一份不安全的operator=实现版本
{
if (this == &rhs) return *this;//如果是自赋值,什么事也不做
delete pb;//停止使用当前的bitmap
pb = new Bitmap(*rhs.pb);//使用rhs's bitmap的副本
return *this;
}
上面的改进的代码可以进行自赋值,但依然有异常方面的麻烦,如果new Bitmap导致异常(分配时内存不足或因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一块被删除的Bitmap(return *this),这样的指针无法安全地删除和读取,是十分有害的。
一般而言解决了operator=的异常安全性就能获得自我赋值安全的回报,如下面的代码,在赋值pb所指的东西前别删除pb:
Widget::operator=(const Widget& rhs)//一份不安全的operator=实现版本
{
Bitmap* pOrig = pb; //记住原来的pb
pb = new Bitmap(*rhs.pb);//令pb指向 *pb的一个副本
delete pOrig;//删除原来的pb。
return *this;
}
如果new Bitmap出现了异常,pb保持原状,即使没有证同测试,这段代码也可以处理自赋值。它或许不是最高效的办法,但它行得通。如果关心效率,可以再把“证同测试”放在前面,不过它也有成本,让原来的代码变得大一点。
另外一个在operator=保证异常安全和自我赋值安全的替代方案是:使用copy and swap技术,这个技术和异常安全性关联紧密,但它足够好:
class Widget {
...
void swap(Widget& rhs)//交换*this和rhs的数据,见第29条
...
}.
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);//为rhs数据制作一个副本
swap(temp);//*this和上述副本数据交换
return *this;
}
另一个可能出现的情况是(1)某类的copy赋值操作符可能被声明为“以传值方式接受实参”;(2)以传值方式传递东西会造成一个副本:
Widget& Widget::operator=(const Widget rhs)//这里使用传值,得到的是一个副本
{
swap(rhs);//直接*this和rhs副本数据交换
return *this;
}
尽管这种方法进行了巧妙的修补但是失去了清晰,不过将拷贝动作从函数本体移到函数参数构造阶段可能让代码更高效。
- 确保对象自赋值operator=有良好的行为,其中技术包括来源对象和目标对象的地址,精心周到的语句顺序以及copy and swap。
- 确定函数如果操作一个以上的对象,其中多个对象是同一个对象时,其行为是正确的。
12. 复制对象时勿忘其每一个部分
copy构造函数和copy赋值运算符我们称为copying函数,如果自己声明copying函数,编译器并不喜欢缺省实现中的某些行为。当你的实现代码几乎必然出错式编译器不会告诉你。
void logCall(const std::string& funcName);
class Customer {
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string name;
};
Customer::Customer(const Customer& rhs)//复制rhs的数据
: name(rhs.name)
{
logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)//复制rhs的数据
{
logCall("Customer copy assognment operator");
name = rhs.name;
return *this;
}
class Date {...};
class Customer {
public:
...
private:
std::string name;
Date lastTransaction;
};
如果我们使用copying函数,我们复制了顾客的name,但是没有复制新添加的lastTransaction。编译器也不会报错。如果要为class添加一个成员变量,就必须同时修改copying函数、所有的构造函数以及任何非标准形式的operator=。
而一旦发生集成,可能会有潜在的危机:
class PriorityCustomer: public Customer {
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: priority(rhs.priority)
{
logCall("PriorityCustomer copy construcor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
这里看起来PriorityCustomer复制了PriorityCustomer每一样东西,但是对于它继承的Customer变量,并没有复制。
-
如果PriorityCustomer使用的是copy构造函数,由于没有指定实参传给基类的构造函数(在它的成员初值列没有提到Customer构造函数),因此PriorityCustomer对象的Customer被不带实参的构造函数(default构造函数——必有一个否则无法通过编译)初始化,default构造函数将针对name和lastTransaction执行缺省的初始化动作。
-
如果PriorityCustomer使用的是copy赋值运算符,它不曾修改基类的成员变量,所以那些成员变量保持不变。
所以如果要为继承的子类写copying函数,要小心地复制基类的部分,那些成分往往也是private,所以无法直接访问,应该让子类的copying函数调用响应的基类函数:
class PriorityCustomer: public Customer {
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), priority(rhs.priority)//调用基类的copy构造函数
{
logCall("PriorityCustomer copy construcor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");
Customer::operator=(rhs);//对基类成分赋值
priority = rhs.priority;
return *this;
}
这两个copying函数有相似的内容,我们可能会让某个函数调用另一个函数避免代码重复,但是令一个copying函数调用另一个copying函数无法达到目的:
- 在copy赋值操作符调用copy构造函数不合理,仿佛试图构造一个已经存在的对象,在某些情况下回让对象损坏。
- 在copy构造函数调用copy赋值操作符也不合理,构造函数用来初始化新对象,而赋值运算符只施加在已初始化对象上——别尝试对一个未构造好的对象赋值。
消除两者代码重复的做法是:建立一个新的成员函数给两者调用,这样的函数往往是private而且常被命名为init,这个策略可以消除copy构造函数和copy赋值操作符代码重复。
- 编写copying函数,确保(1)复制所有local成员带变量,(2)调用所有基类内的适当的copying函数
- 不要试图用一个copying函数实现另一个copying函数,应该把共同的部分放进第三个函数,供两个copying函数共同调用。
13. 以对象管理资源
在C++中可能使用动态分配对象(比如使用new):
void f()
{
int* iInv = new int(1);
...
delete iInv;
}
上面的"…"区域内可能过早return
或者出现错误而没有对iInv
进行delete
释放动态内存,从而导致内存泄露的问题,我们不能单纯依赖f总会执行delete
语句。
为确保资源总是被释放,我们需将资源放入对象内,当控制流离开f,对象的析构函数会自动释放资源。我们可以使用智能指针:
auto_ptr
(该智能指针在C++11版本及以后被废弃了)
#include <memory>
...
void f()
{
std::auto_ptr<int> iInv(new int(1));
...//auto_ptr的析构函数自动删除iInv
}
这里使用了两个关键的想法:
- 获得资源后立即放进管理对象(RAII):获取资源时就初始化对象。
- 管理对象运用析构函数确保资源释放。
但由于auto_ptr
被销毁会自动删除它所指之物,如果有多个auto_ptr
指向同一个对象,对象被删除一次以上会导致未定义行为!为了预防这个问题,auto_ptr
有一个不寻常的性质:当通过copy构造函数或copy赋值操作符复制时,原指针会变成NULL
,而复制得到的指针将取得资源的唯一拥有权。
shared_ptr
:相比auto_ptr
它可以进行我们预想的复制。
标准库的auto_ptr
和shared_ptr
在析构函数均使用的是delete
而不是delete []
,动态分配的array使用智能指针是件坏事(尽管也能编译成功)!幸好我们可以用vector和string动态分配得到想要的数组。
手工释放资源容易出错,如果智能指针不能解决问题,就需要精巧设计自己的资源管理类。
14. 在资源管理类小心copying行为
获得资源后立即放进管理对象(RAII)描述了智能指针在堆上的使用,如果我们要自己设计资源管理类(有些情况不适合使用智能指针)。
比如我们想定义一个互斥器:
void lock(Mutex* pm);//互斥器锁定
void unlock(Mutex* pm);//互斥器解锁
class Lock {
public:
explicit Lock(Mutex* pm): mutexptr(pm)
{lock(mutexPtr);} //获得资源
~Lock() {unlock(mutexPtr);} //释放资源
private:
Mutex* mutexPtr;
};
下面的调用是可以的
Mutex m;
...
{
Lock m1(&m);
...
}
但如果Lock对象被复制回发生什么事?
Lock ml1(&m);
Lock ml2(ml1);
我们可以选择两种可能:
- 禁止赋值。复制动作不合适时,我们应该禁止复制,可以把copying操作声明为private,对Lock而言是这样:
class Lock: private Uncopyable {
public:
...
};
- 使用引用记数法,有时我们希望保存资源,直到它的最后一个使用者被销毁,当复制RAII对象时,应该将资源的被引用数递增,
shared_ptr
就是如此。可以把Lock类里的Mutex*改为shared_ptr<Mutex>
,但shared_ptr
在引用次数为0就删除所有物,可能不是我们想要的行为。当我们想用一个Mutex
,我们想要的释放动作是锁定而不是删除。不过我们可以为shared_ptr
指定删除器,那是一个函数或函数对象,而引用记数为0时便被调用(而auto_ptr
总是将指针删除)。删除器对shared_ptr构造函数是可有可无的第二参数,我们可以这样写:
class Lock:
{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm, unlock)
{
lock(mutexPtr.get());
}
private:
std::shared_ptr<Mutex> mutexPtr;
};
在本例里我们不需要为Lock声明析构函数,当类缺省析构函数,编译器会自动生成默认的析构函数,析构函数会调用非static成员变量的析构函数(mutexPtr),而mutexPtr的析构函数会在互斥器的引用次数为0时自动调用shared_ptr
的删除其(这里是unlock)。
- 复制底部资源。有时候,我们可以让一份资源有任意数量的副本,而当不需要某个复件时确保释放。我们在复制资源管理对象时同时也要复制它包含的资源,这就是深度拷贝。某些字符串是由指向堆的内存的指针构成的,这种字符串对象有一个指针指向一块堆内存,当一个字符串对象被复制,指针和所指内存都会复制一个副本,表现深度拷贝行为。
- 转移底层资源的拥有权。有时候我们想确保永远只有一个RAII对象指向一个未加工资源,即使RAII对象被复制也是如此,此时资源的拥有权从被复制物转移到目标物,这也就是
auto_ptr
的意义(C++11换位unique_ptr
了)。
copying函数可能被编译器自动创建,除非编译器的版本做了你想要做的事,否则应该自己编写:
- 复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
- 普遍而常见的RAII class行为是抑制copying,采用引用记数法,也可能有其他行为。
15. 在资源管理类中提供对原始资源的访问
资源管理类有助于排除资源泄漏的问题,理想的情况是我们使用资源管理类来和资源互动,而不是直接处理原始资源,但是有时我们要使用其他的API,我们不得不直接绕过资源管理对象直接访问原始资源。
在条款13里我们有:
std::shared_ptr<int> iInv(new int(1));
如果我们希望使用函数处理int对象:
int f(const int* pi);
调用:
int b = f(iInv);
编译错误,因为无法进行shared_ptr
到const int *
的转换。我们如果要进行这样的转换,有两种做法:
15.1 使用显示转换
显式转换,我们可以使用智能指针的get()
函数,返回它内部的原始指针的复件:
int b = f(iInv.get());
智能指针的操作符->
和*
也重载了,他们允许隐式转换为底层的原始指针:
class Investment {
public:
bool isTaxFree() const;
...
};
Investment* createInvestment()//函数返回一个Investment*的指针
{
...
return new Investment();
}
std::shared_ptr<Investment> g1(createInvestment());
bool taxable1 = !(g1->isTaxFree());//使用operator->访问资源
...
std::auto_ptr<Investment> g2(createInvestment());
bool taxable2 = !((*g2).isTaxFree());//使用operator*访问资源
有时我们想取得RAII对象的原始资源,做法是提供一个隐式转换函数,来看下面的例子:
FontHandle getFont(); //C的API
void releaseFont(FontHandle fh); //C的API
class Font {
public:
explicit Font( FontHandle fh): f(fh) {}
~Font() { releaseFont(f); }
private:
FontHandle f;
};
如果有大量的与字体相关的C API,我们需要需要经常讲Font转换为FontHandle,我们可以为Font类提供一个显式的转换函数,类似get
class Font() {
public:
...
FontHandle get() const { return f; } //显式转换函数
...
};
然而这样用户在使用API的时候必须使用get
void changeFontSize(FontHanndle f, int newSize); //C API
Font f(getFont());
int newFontSize;
...
changeFontSize(f.get(), newFontSize);//明白地将Font转换为FontHandle
用户可能不喜欢这样显式转换不愿使用这个类,于是可能泄漏字体,Font类的设计就是为了防止泄漏字体。
15.2 使用隐式转换
另一个方法是令Font提供隐式转换函数,转型为FontHandle:
class Font{
public:
...
operator FontHandle() const //隐式转换函数
{ return f; }
...
};
这样客户调用C API可以比较轻松:
Font f(getFont());
int newFOntSize;
changeFontSize(f, newFontSize);//将Font隐式转换为FontHandle
但是这样可能出错:
Font f1(getFont());
...
FontHandle f2 = f1;
上面的程序中,想拷贝的是一个Font对象,但是将f1隐式这换为了FontHandle才复制它,当f1被销毁,字体被释放,而f2会成为“虚吊的”。
使用显示转换(如get())或隐式转换,取决RAII具体的工作,通常显式转换get比较好,因为降低了“非故意的类型转换”的可能性。
RAII只是为了确保资源的正确释放,设计良好的类如shared_ptr隐藏了用户不可见部分,也备妥了用户需要的所有东西。
- 每一个RAII类要提供取得其管理资源的办法,因为API可能想访问的是原始的资源
- 原始资源的访问可以使用显式或隐式转换,显式转换更为安全,隐式转换更为方便
16. 成对使用new和delete要采取相同形式
当使用new,发生:
- 内存被分配出来
- 内存会有一个或多个构造函数被调用
当使用delete,发生:
- 内存会有一个或多个析构函数被调用
- 内存被释放
delete最大的问题是内存里究竟有多少对象,也即有多少析构函数必须被调用。因为被删除的指针可能指向单一对象也可能指向对象数组。单一对象的内存布局一般而言不同于数组的内存布局。数组的内存通常还包括数组大小的记录,以便delete知道要调用多少次析构函数,单一对象的内存则没有这笔记录。
使用delete时,如果加上中括号,删除的是一个数组,否则是单一对象。
std::string* str1 = new std::string;//删除一个对象
std::string* str2 = new std::string(100);//删除一个由对象组成的数组
...
delete str1;
delete [] str2;
delete
加不加括号不要搞错了,原则是:使用new
用了[],delete
也用[],而使用new
没有使用[],也不用delete
使用[]。
当类里有一个指针指向动态分配内存,提供多个构造函数必须小心地在构造函数使用相同形式的new
将指针成员初始化,这样才知道析构函数里使用什么形式的delete。对于使用typedef
类型对象,应该怎么delete
也要注意:
typedef std::string AddressLines[4];
std::string* pal = new AddressLine;
...
delete pal;//×
delete [] pal;//√
为了避免上面的错误,我们不要对数组形式使用typedef
动作,我们不使用数组,使用标准库的vector更好:vector<string>
- 使用
new
用了[],delete
也用[],而使用new
没有使用[],也不用delete
使用[]。
17. 以独立语句将newed对象放入智能指针
可以用类似下面的方式将原生指针转换为shared_ptr指针:
process(std::shared_ptr<Widget>(new Widget), priority());
new Widget肯定在shared_ptr的构造函数前完成,但可能在priority前面也可能在它的后面,如果是前者,就有内存泄漏的风险。对象被创建和对象被作为资源管理对象两个时间点可能有干扰。
所以我们最好分离开:
std::shared_ptr<Widget> pw(new Widget);
void process(pw, priority());
- 以独立语句将newed对象放入智能指针,否则可能会内存泄漏。
18. 让接口容易被使用,不易被误用
接口设置得不好,有可能使用函数的时候没有提示错误:
比如
class Date
{
public:
Date(int month, int day, int year);
};
输入的参数以此是月、日、年,但可能输入的时候不按这个顺序或者输入的有问题:
Date d(2, 30, 1995);// 呃。。2月没有30天
Date d(30, 3, 1995);// 呃。。输入成了日月年
struct Day
{
explicit Day(int d): val(d) {}
int val;
};
struct Month
{
explicit Month(int d): val(d) {}
int val;
};
struct Year
{
explicit Year(int d): val(d) {}
int val;
};
class Date
{
public:
Date(const Month& month, const Day& day, const Yera& year);
};
这样封装会好一点:
Date d(Month(3), Day(30), Year(1995));
我们还要检查值是不是对的,比如一年只有12个月:一个方法是使用enum,但是它没有检查类型的安全性,比较好的办法是预先定义有效的month:
class Month
{
public:
static Month Jan() {return Month(1);}
static Month Feb() {return Month(2);}
...
static Month Dec() {return Month(12);}
private:
explicit Month(int m);
};
Date d(Month::Mar(), Day(30), Year(1995));
预防错误的另一个办法是限制类型内某些事可做,某些事不可做,常见的限制是加上const。
另一个准则是让变量类型不容易被误用,比如a和b都是int,a*b赋值就是不合法的。
另外像shared_ptr这样的智能指针可以防范跨DLL问题,new和delete会成对存在,减少了函数的错误。而且我们可以自己定义删除器,而不是自己来手工地delete。
19. 设计class犹如设计type
设计class要考虑的问题:
- 新的对象如何创建和销毁-构造、析构、内存分配和释放
- 对象的初始化和赋值的差别?
- 新的对象被传递值的形参该当如何?
- 新的对象的合法性如何确保?(抛出异常)
- 新的对象需要配合某个继承图系吗?
- 新的对象需要什么样的转换?
- 什么样的操作符和函数对于对象是合理的?
- 什么样的标准函数必须驳回?(声明为private)
- 谁该取用新的对象的成员?
- 什么是对象的“未声明接口”?
- 新的对象有多么一般化,是否使用模板?
- 真的需要一个新的对象吗?
20. 宁以pass-by-reference-to-const替换pass-by-value
使用函数的时候如果使用传值可能导致对象的拷贝而变得耗时,如果传递的是一个类,会拷贝一个临时对象并析构,而且如果类里还有对象,类里的对象也会经历拷贝并析构的操作,效率太低了。使用const引用比较好。没有任何对象被创建,使用const是很重要的,我们保证在函数内不会对传入的对象修改。
而且用传引用也避免对象切割的问题:一个derived对象以传值的方式传递给基类对象,基类的拷贝构造函数会被调用,而derived对象的其他特化性质都没了,如果函数是一个虚函数,传入一个值调用的将是基类的虚函数(这不符合我们的预期!)而传递const引用就不会有这个问题。
一般来说传值不拉低效率的唯一对象是内置类型和STL的迭代器和函数对象,其他尽量还是传const引用吧。