===条款29: 避免返回内部数据的句柄===
假设b是一个const string对象:
class string {
public:
string(const char *value); // 具体实现参见条款11
~string(); // 构造函数的注解参见条款m5
operator char *() const; // 转换string -> char*;
...
private:
char *data;
};
const string b("hello world"); // b是一个const对象
char *str = b; // 调用b.operator char*()
strcpy(str, "hi mom"); // 修改str指向的值
通过str修改了const对象的值,问题的原因来自:
// 一个执行很快但不正确的实现
inline string::operator char*() const
{ return data; }
这个函数的缺陷在于它返回了一个"句柄",而这个句柄所指向的信息本来是应该隐藏在被调用函数所在的string对象的内部。
这样,这个句柄就给了调用者自由访问data所指的私有数据的机会
解决办法:
1、将string::operator char*()声明成非const型,这样const对象就不能调用它
但是这种解决方法似乎不合理,因为无论这个对象是否为const,将它转换成char*形式是很合理的事情
2、不返回内部数据句柄,而返回数据拷贝
// 一个执行慢但很安全的实现
inline string::operator char*() const
{
char *copy = new char[strlen(data) + 1];
strcpy(copy, data);
return copy;
}
缺点:速度慢,而且要手工delete掉返回的指针
3、函数返回加const
inline string::operator const char*() const
{ return data; }
这个函数既快又安全,跟stl中string类型中的c_str()返回值一样const char*
===条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低===
class address { ... }; // 某人居住在此
class person {
public:
address& personaddress() { return address; }
private:
address address;
};
person scott(...); // 为简化省略了参数
address& addr = // 假设addr为全局变量
scott.personaddress();
现在,全局对象addr成为了scott.address的另一个名字,利用它可以随意读写scott.address。
实际上,scott.address不再为private,而是public,访问级提升的根源在于成员函数personaddress。
这个成员函数的做法有违当初将person::address声明为private的初衷
===条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用===
class rational { // 一个有理数类
public:
rational(int numerator = 0, int denominator = 1);
~rational();
private:
int n, d; // 分子和分母
// 注意operator* (不正确地)返回了一个引用
friend const rational& operator*(const rational& lhs,
const rational& rhs);
};
// operator*不正确的实现
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}
//调用
rational two = 2;
rational four = two * two; // 同operator*(two, two)
函数调用时将发生如下事件:
1. 局部对象result被创建。
2. 初始化一个引用,使之成为result的另一个名字;这个引用先放在另一边,留做operator*的返回值。
3. 局部对象result被销毁,它在堆栈所占的空间可被本程序其它部分或其他程序使用。
4. 用步骤2中的引用初始化对象four。
// operator*的另一个不正确的实现
inline const rational& operator*(const rational& lhs,
const rational& rhs)
{
// create a new object on the heap
rational *result =
new rational(lhs.n * rhs.n, lhs.d * rhs.d);
// return it
return *result;
}
这个方法的确避免了上面例子中的问题,但却引发了新的难题。
如何delete?必须要知道它返回的名字,然后手工去delete
但是下面的将无法delete
rational one(1), two(2), three(3), four(4);
rational product;
product = one * two * three * four;
===条款32: 尽可能地推迟变量的定义===
如果定义了一个有构造函数和析构函数的类型的变量,当程序运行到变量定义之处时,必然面临构造的开销;
当变量离开它的生命空间时,又要承担析构的开销。这意味着定义无用的变量必然伴随着不必要的开销,
所以只要可能,就要避免这种情况发生。
// 此函数太早定义了变量"encrypted"
string encryptPassword(const string& password)
{
string encrypted;
if (password.length() < MINIMUM_PASSWORD_LENGTH) {
throw logic_error("Password is too short");
}
进行必要的操作,将口令的加密版本
放进encrypted之中;
return encrypted;
}
改进:
// 这个函数推迟了encrypted的定义,
// 直到真正需要时才定义
string encryptPassword(const string& password)
{
if (password.length() < MINIMUM_PASSWORD_LENGTH) {
throw logic_error("Password is too short");
}
string encrypted; // 缺省构造encrypted
encrypted = password; // 给encrypted赋值
encrypt(encrypted);
return encrypted;
}
改进:用password来初始化encrypted,从而绕过了对缺省构造函数不必要的调用
// 定义和初始化encrypted的最好方式
string encryptPassword(const string& password)
{
... // 检查长度
string encrypted(password); // 通过拷贝构造函数定义并初始化
encrypt(encrypted);
return encrypted;
}
你不仅要将变量的定义推迟到必须使用它的时候,还要尽量推迟到可以为它提供一个初始化参数为止。
===条款33: 明智地使用内联===
要牢记在心的一条是,inline指令就象register,它只是对编译器的一种提示,而不是命令。
也就是说,只要编译器愿意,它就可以随意地忽略掉你的指令,事实上编译器常常会这么做。
程序库的设计者必须预先估计到声明内联函数带来的负面影响。
因为想对程序库中的内联函数进行二进制代码升级是不可能的。换句话说,如果f是库中的一个内联函数,用户会将f的函数体编译到自己的程序中。
如果程序库的设计者后来要修改f,所有使用f的用户程序必须重新编译。
相反,如果f是非内联函数,对f的修改仅需要用户重新链接,这就比需要重新编译大大减轻了负担;如果包含这个函数的程序库是被动态链接的,程序库的修改对用户来说完全是透明的。
一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单
===条款34: 将文件间的编译依赖性降至最低===
目的:减少程序重新编译时消耗的时间
将接口从实现分离
class Person {
public:
Person(const string& name, const Date& birthday,
const Address& addr, const Country& country);
virtual ~Person();
... // 简化起见,省略了拷贝构造
// 函数和赋值运算符函数
string name() const;
string birthDate() const;
string address() const;
string nationality() const;
private:
string name_; // 实现细节
Date birthDate_; // 实现细节
Address address_; // 实现细节
Country citizenship_; // 实现细节
};
这里要注意到的重要一点是,Person的实现用到了一些类,即string, Date,Address和Country;
Person要想被编译,就得让编译器能够访问得到这些类的定义。这样的定义一般是通过#include指令来提供的,所以在定义Person类的文件头部,可以看到象下面这样的语句:
#include <string>
#include "date.h"
#include "address.h"
#include "country.h"
*******这样一来,定义Person的文件和这些头文件之间就建立了编译依赖关系!*******
如果任一个辅助类(即string, Date,Address和Country)改变了它的实现(这里指的是修改头文件中的任何地方,哪怕增加一个空行),或任一个辅助类所依赖的类改变了实现,包含Person类的文件以及任何使用了Person类的文件就必须重新编译。
解决办法:
使用类指针("将一个对象的实现隐藏在指针身后")实现Person接口和实现的分离
方法1:句柄类
// 编译器还是要知道这些类型名,
// 因为Person的构造函数要用到它们
class string; // 对标准string来说这样做不对,
// 原因参见条款49
class Date;
class Address;
class Country;
// 类PersonImpl将包含Person对象的实
// 现细节,此处只是类名的提前声明
class PersonImpl;
class Person {
public:
Person(const string& name, const Date& birthday,
const Address& addr, const Country& country);
virtual ~Person();
... // 拷贝构造函数, operator=
string name() const;
string birthDate() const;
string address() const;
string nationality() const;
private:
PersonImpl *impl; // 指向具体的实现类
};
现在Person的用户程序完全和string,date,address,country以及person的实现细节分家了
修改string、date、address、country、PersonImpl都不会让包含Person类的文件以及任何使用了Person类的文件重新编译
分离的关键在于,"对类定义的依赖" 被 "对类声明的依赖" 取代了。
Person类仅仅用一个指针来指向某个不确定的实现,这样的类常常被称为句炳类(Handle class)或信封类(Envelope class)
你一定会好奇句炳类实际上都做了些什么。
答案很简单:它只是把所有的函数调用都转移到了对应的主体类中,主体类真正完成工作。例如,下面是Person的两个成员函数的实现:
#include "Person.h" // 因为是在实现Person类,所以必须包含类的定义
#include "PersonImpl.h" // 也必须包含PersonImpl类的定义,
// 否则不能调用它的成员函数。
// 注意PersonImpl和Person含有一样的
// 成员函数,它们的接口完全相同
Person::Person(const string& name, const Date& birthday,
const Address& addr, const Country& country)
{
impl = new PersonImpl(name, birthday, addr, country);
}
string Person::name() const
{
return impl->name();
}
方法2:协议类
除了句柄类,另一选择是使Person成为一种特殊类型的抽象基类,称为协议类(Protocol class)。
class Person {
public:
virtual ~Person();
virtual string name() const = 0;
virtual string birthDate() const = 0;
virtual string address() const = 0;
virtual string nationality() const = 0;
static Person * makePerson(const string& name,
const Date& birthday,
const Address& addr,
const Country& country);
};
Person * Person::makePerson(const string& name,
const Date& birthday,
const Address& addr,
const Country& country)
{
//RealPerson是Person的派生类
return new RealPerson(name, birthday, addr, country);//这种设计类似与设计模式中的工厂方法模式
}
Person类的用户必须通过Person的指针和引用来使用它,因为实例化一个包含纯虚函数的类是不可能的。
//main.cpp
string name;
Date dateOfBirth;
Address address;
Country nation;
// 创建一个支持Person接口的对象
Person *pp = makePerson(name, dateOfBirth, address, nation);
cout << pp->name() // 通过Person接口使用对象
<< " was born on "
<< pp->birthDate()
<< " and now lives at "
<< pp->address();
delete pp; // 删除对象
代价:时间+内存
句柄类和协议类分离了接口和实现,从而降低了文件间编译的依赖性。"但,所有这些把戏会带来多少代价呢?"
句柄类:
成员函数必须通过(指向实现的)指针来获得对象数据。这样,每次访问的间接性就多一层。
此外,计算每个对象所占用的内存大小时,还应该算上这个指针。
还有,指针本身还要被初始化(在句柄类的构造函数内),以使之指向被动态分配的实现对象,所以,还要承担动态内存分配(以及后续的内存释放)所带来的开销
协议类:
每个函数都是虚函数,所以每次调用函数时必须承担间接跳转的开销
每个从协议类派生而来的对象必然包含一个虚指针,这个指针可能会增加对象存储所需要的内存数量(具体取决于:对于对象的虚函数来说,此协议类是不是它们的唯一来源)。
最后一点,句柄类和协议类都不大会使用内联函数。使用任何内联函数时都要访问实现细节,而设计句柄类和协议类的初衷正是为了避免这种情况。