条款20:宁以pass_by_reference替换pass_by_value
缺省情况下C++以by value方式传递对象至函数,这可能使得pass_by_value成为费时的操作。而pass_by_reference可以避免构造和析构操作,并且by reference方式传递参数可以避免slicing(对象切割)问题。当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数会被调用,而造成此对象的行为像个derived class对象的那些特化性质全被切割,仅仅留下一个base class对象
class window{
public:
std::string name() const;
virtual void display() const;
};
class windowWithScrollBars:public window{
public:
virtual void display() const;
};
void printNameAndDisplay(window w)
{
std::cout<<w.name;
w.display();
}
当调用上述函数,并且传递一个windowWithScrollBars对象时:
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);
参数w会被构造成一个window对象,它是pass by value,wwsb的所有特化信息都会被切除。在printNameAndDisplay函数内,不论传递过来的对象原本是什么类型,参数w就像一个window对象。因此printNameAndDisplay内调用dispaly调用的总是window::display,绝不会是windowWithScrollBars::display。解决办法就是by reference。
对于内置类型,pass by value往往比pass by reference的效率高一些,这个忠告也适用于STL的迭代器和函数对象,因为习惯上它们都被设计为passed by value。但并不是所有小型的type都是pass_by_value的合格候选者,用户自定义的class也是如此。
请记住:
尽量以pass_by_reference替换pass_by_value。前者通常比较高效,并可以避免切割问题
以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对它们而言pass_by_value往往比较适当
条款21:必须返回对象时,别妄想返回其reference
一旦领悟了pass_by_reference传递的效率,就想一味地使用pass_by_reference,但这往往会犯下致命错误:开始传递一些reference指向其实并不存在的对象。
class rational{
public:
rational(int numerator=0,int denominator=1);
private:
int n,d;
friend const rational operator*(const rational &lhs,const rational &rhs);
};
该函数以pass_by_value返回计算结果,若非必要,没人想要为这样的对象付出太多代价,如果可以改用pass_by_reference就不需要付出代价,所谓reference只是一个名称,代表一个既有对象。任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么。
我们当然不能期望这样一个rational对象在调用operator*之前就存在。如果operator*要返回一个reference指向如此数值,它必须自己创建那个rational对象。
函数创建新对象的途径有二:在stack空间或者在heap空间。如果定义一个local变量,就是在stack空间创立:
const rational& operator*(const rational &lhs,const rational &rhs)
{
rational result(lhs.n*rhs.n);
return result;
}
你的目标是要避免调用构造函数,但result却必须像任何对象一样由构造函数构造起来。更严重的是:这个函数返回一个reference指向result,但result是一个local对象,而local对象在函数退出之前被销毁了。
任何函数如果返回一个reference指向一个local对象,都将一败涂地。
于是我们考虑在heap内构造一个对象,并返回reference指向它,heap_based对象由new创建,所以你得写一个heap-based operator*如下:
const rational& operator*(const rational &lhs,const rational &rhs)
{
rational *result=new rational(lhs.n*rhs.n);
return *result;
}
你还是必须付出一个“构造函数调用”代价,并且,谁应该对这被你new出来的对象实施delete?这就会导致内存泄漏。
上述不论on-the-stack或on-the-heap做法,都对operator*返回的结果调用结构函数而受到惩罚。
请记住:
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。
条款22:将成员变量声明为private
某些东西的封装性与当其内容改变时可能造成的代码破坏量成反比。
假设我们有一个public成员变量,而我们最终取消了它,所有使用它的客户都会被破坏,因此public成员完全没有封装性。如果我们有一个protect成员变量,而我们最终取消了它,所有使用它的derived class都会被破坏,因此在这两种情况下,如果成员变量被改变,都会有不可预知的大量代码受到破坏。从封装的角度来看,只有两种访问权限:private(提供封装)和其他(不提供封装)。
请记住:
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件或的保证,并提供class作者以充分的实现弹性
protect并不比public更具封装性
条款23:宁以non_member、non_friend替换member函数
如果某些东西被封装,它就不再可见。越多东西被封装,越少人可以看见它。而越少人可以看见它,我们就有愈大的弹性可以去改变它,因为我们的改变仅仅直接影响看到改变的那些人事物。因此,越多东西被封装,我们改变那些东西的能力也就越大。这就是我们首先推崇封装的原因:它使我们能够改变事物而只影响有限客户。
越多函数可以访问它,数据封装性就越低。成员变量应该是private,因为如果它们不是,就是无限量的函数可以访问它们,它们就毫无封装性。因此,如果要在一个member函数和一个non_member、non_friend之间作抉择,而且两者提供相同的机能,那么导致较大封装性的时non_friend、non_member函数,因为它并不增加能够访问的class之内的private成分的函数数量。
在这一点上有两件事值得注意。第一,这个论述只适用于non_friend、non_member函数。friend函数对class private成员的冲击力道相同,因此,这里选择的关键不在member和non_member函数之间,而是在member和non_member、non_friend函数之间。
第二,只因在意封装性而让函数成为class的non_member并不意味着它不可以是另一个class的member。
请记住:
宁以non_member、non_friend函数替换member函数。这样做可以增加封装性、包裹弹性、和机能扩充性
条款24:若所有参数皆需类型转换,请为此采用non_member函数
再次参考rational class,将operator*函数编写为member函数:
class rational{
public:
const rational operator*(const rational &rhs) const;
};
当我们尝试混合式运算时,会发现只有一半行得通:
rational result;
result=oneHalf*2; //ok
result=2*oneHalf; //error
整数2并没有相应的class,也就没有operator*成员函数。编译器会尝试寻找可以被一下这般调用的non_member operator*(也就是在命名空间内或者global作用域内),但本例中并不存在一个接受int和rational作为参数的non_member operator*,因此查找失败。
第一次调用成功是由于进行了隐式转换,而第二次并没能够进行隐式转化。只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者,而被调用的成员函数所属的那个对象——this对象——的那个隐喻参数绝不是隐式转换的合格参与者。
因此,我们需要将member函数改写为non_member函数,至于他应不应该成为friend函数,视情况而定,上例中完全可由public接口完成操作,就不需要指定为friend函数。
大多数C++程序员假定,如果一个与某class相关的函数不能成为member,就应该是一个 friend。这是错误的,无论如何能避免friend就避免,不能够只因函数不该成为member就自动让它成为friend。
请记住:
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non_member
条款25:考虑写出一个不抛出异常的swap函数
运用std::swap可能缺乏效率,主要就是以指针指向一个对象,内涵真正数据那种类型。这种设计的常见表现形式是所谓的“pimpl”(point to implementation):
class WidgetImpl{
public:
...
private:
int a,b,c;
std::vector<double> v;
...
};
class Widget{
public:
Widget(const Widget &rhs);
Widget& operator=(const Widget &rhs)
{
...
*pImpl=*(rhs.pImpl);
...
}
private:
WidgetImpl *pImpl;
};
这个例子对于缺省的swap算法来说太缺乏效率,我们应当定义自己的swap算法:声明一个名为swap的public成员函数做真正的置换工作,然后将std::swap特化,令它调用成员函数:
class Widget{
public:
void swap(Widget &other)
{
using std::swap;
swap(pImpl,other.pImpl);
}
};
namespace std{
template<> void swap<Widget>(Widget &a,Widget &b)
{
a.swap(b);
}
}
然而假设Widget和WidgetImpl都是class templates而非class,在Widget内放一个swap成员函数:
namespace std{
template<typename T> void swap<Widget<T>>(Widget<T> &a,Widget<T> &b)
{a.swap(b);}
}
这是错误的,因为我们企图偏特化一个function template,但是C++只允许对class template偏特化,在function template身上偏特化是行不通的。当打算偏特化一个function template是,惯用做法是为它添加一个重载版本:
namespace std{
tempalte<typename T>
void swap(Widget<T> &a,Widget<T> &b)
{a.swap(b);}
}
这也是不合法的,一般而言重载function template没有问题,但std是一个特殊的命名空间,其管理规则也比较特殊:客户可以特化std内的template,但不可以添加新的template到std里面。std的内容完全由C++标准委员会决定,其禁止膨胀那些已经声明好的东西。但其实程序任可以编译和执行,但它们的行为没有明确定义。
解决办法是:不再将那个non_member swap声明为std:swap的特化版本或重载版本。为求简将所有相关机能都置于类的命名空间中(这不是必须的,只是为了避免将class、template、function...之类的塞满global命名空间,从而失去了整洁):
namespace WidgetStuff{
template<typename T>class Widget{...};
template<typename T> void swap(Widget<T> &a,Widget<T> &b)
{
a.swap(b); //这里不属于std命名空间
}
}
这个做法对于class以及template class都行的通,所以似乎我们可以在任何时候使用它,但事实并非如此,有一个情况你应该为class特化std::swap。
我们应该如何使用调用T专属版本,并在该版本不存在的情况下调用std内的一般化版本:
template<typename T> void doSomething(T &a,T &b)
{
using std::swap; //std::swap在此函数内可用
swap(a,b);
}
编译器一看到swap调用便查找适当的swap调用。C++名字查找法则确保找到global作用域或T所在命名空间之内的任何T专属的swap。如果没有T专属的swap存在,编译器就使用std内的swap,这需要using声明让std::swap在函数内曝光。即便如此编译器还是更加喜欢std::swap的T专属特化版本,而非一般的那个template。
总结一下:
首先,如果swap的缺省实现对你的class或template class提供可以接受的效率,无需做任何事。
其次,如果swap缺省版本效率不足(那几乎总是意味着你的class或template使用了某种pimpl手法),试着做下事情:
1. 提供一个public swap函数,让他高效置换你的类型的两个对象值。这将不抛出异常。
2. 在你的class或template所在命名空间内提供一个non_member swap,并令它调用上述swap成员函数。
3. 如果你正编写一个class(而非template class),为你的class特化std::swap。并令它调用你的swap成员函数。
成员版swap不抛出异常,这仅仅基于成员版。
请记住:
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
如果你提供一个member swap,也该提供一个non_member swap用来调用前者。对于class(而非template class),也请特化std::swap
调用swap时应针对std::swap使用using声明,然后调用swap并不带任何命名空间资格修饰
为用户定义类型进行std template全特化是好的,但千万不要尝试在std内加入某些对于std来说全新的东西
条款26:尽可能延后变量定义式的出现时间
我们有可能定义了一个有构造和析构函数的变量类型却没有使用它,这就造成了效率低下
std::string encryptPassword(const std::string &password)
{
using namespace std;
string encrypted;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
return encrypted;
}
对象encrypted在此函数中并没有完全被使用,我们却得付出构造和析构成本。所以最好延后encrypted的定义式:
std::string encryptPassword(const std::string &password)
{
using namespace std;
if(password.length()<MinimumPasswordLength)
{
throw logic_error("Password is too short");
}
string encrypted;
...
return encrypted;
}
这里依然有可以改善的地方,因为encrypted虽获定义却无初值,这意味着会先调用默认构造函数之后再对它赋初值。所以不应只是延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该延后这份定义直到能够给它初值实参为止。如果这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。
对于循环我们因该如何:
//方法A //方法B
Widget w;
for(int i=0;i<=n;i++) for(int i=0;i<=n;i++)
{ {
w=...; widget w(...);
} }
在Widget函数内部,以上两种写法成本如下:
做法A:1个构造函数+1个析构函数+n个赋值操作
做法B:n个构造函数+n个析构函数
,做法A大体而言,比较高效,因此除非:
1. 你知道赋值成本比构造加析构成本低
2. 你正在处理代码中效率高度敏感的部分
否则你应该使用做法B
请记住:
尽可能延后变量定义式的出现。这样做可增加程序的清晰度并改善程序的效率