条款29: 避免返回内部数据的句柄
假设b是一个const string对象:
class string { public: string(const char *value); // 具体实现参见条款11 ~string(); // 构造函数的注解参见条款m5 operator char *() const; // 转换string -> char*; // 参见条款m5 ... private: char *data; }; const string b("hello world"); // b是一个const对象
看看下面的情形:
char *str = b; // 调用b.operator char*()
strcpy(str, "hi mom"); // 修改str指向的值
b的值现在还是"hello world"吗?或者,它是否已经变成了对母亲的问候语?答案完全取决于string::operator char*的实现。
下面是一个有欠考虑的实现,它导致了错误的结果。但是,它工作起来确实很高效,所以很多程序员才掉进它的错误陷阱之中:
// 一个执行很快但不正确的实现 inline string::operator char*() const { return data; }
这个函数的缺陷在于它返回了一个"句柄"(在本例中,是个指针),而这个句柄所指向的信息本来是应该隐藏在被调用函数所在的string对象的内部。这样,这个句柄就给了调用者自由访问data所指的私有数据的机会。换句话说,有了下面的语句:
char *str = b;
情况就会变成这样:
str------------------------->"hello world\0"
显然,任何对str所指向的内存的修改都使得b的有效值发生变化。所以,即使b声明为const,而且即使只是调用了b的某个const成员函数,b也会在程序运行过程中得到不同的值。特别是,如果str修改了它所指的值,b也会改变。
string::operator char*本身写的没有一点错,麻烦的是它可以用于const对象。如果这个函数不声明为const,就不会有问题,因为这样它就不能用于象b这样的const对象了。(const对象不能调用非const成员函数)
但是,将一个string对象转换成它相应的char*形式是很合理的一件事,无论这个对象是否为const。所以,还是应该使函数保持为const。这样的话,就得重写这个函数,使得它不返回指向对象内部数据的句柄:
// 一个执行慢但很安全的实现 inline string::operator char*() const { char *copy = new char[strlen(data) + 1]; strcpy(copy, data); return copy; }
这个实现很安全,因为它返回的指针所指向的数据只是string对象所指向数据的拷贝;通过函数返回的指针无法修改string对象的值。当然,安全是要有代价的:这个版本的string::operator char* 运行起来比前面那个简单版本要慢;此外,函数的调用者还要记得delete掉返回的指针。
如果不能忍受这个版本的速度,或者担心内存泄露,可以来一点小小的改动:使函数返回一个指向const char的指针:
class string { public: operator const char *() const; ... }; inline string::operator const char*() const { return data; }
指针并不是返回内部数据句柄的唯一途径。引用也很容易被滥用。下面是一种常见的用法,还是拿string类做例子:
class string { public: ... char& operator[](int index) const { return data[index]; } private: char *data; }; string s = "i'm not constant"; s[0] = 'x'; // 正确, s不是const const string cs = "i'm constant"; cs[0] = 'x'; // 修改了const string, // 但编译器不会通知
注意string::operator[]是通过引用返回结果的。这意味着函数的调用者得到的是内部数据data[index]的另一个名字,而这个名字可以用来修改const对象的内部数据。这个问题和前面看到的相同,只不过这次的罪魁祸首是引用,而不是指针。
这类问题的通用解决方案和前面关于指针的讨论一样:或者使函数为非const(const对象就不能调用该函数了),或者重写函数,使之不返回句柄(返回const引用即可)。如果想让string::operator[]既适用于const对象又适用于非const 对象,可以参见条款21。
并不是只有const成员函数需要担心返回句柄的问题,即使是非const成员函数也得承认:句柄的合法性失效的时间和它所对应的对象是完全相同的。这个时间可能比用户期望的要早很多,特别是当涉及的对象是由编译器产生的临时对象时。
例如,看看这个函数,它返回了一个string对象:
string somefamousauthor() // 随机选择一个作家名 { // 并返回之 switch (rand() % 3) { // rand()在<stdlib.h>中 // (还有<cstdlib>。参见条款49) case 0: return "margaret mitchell"; // 此作家曾写了 "飘", // 一部绝对经典的作品 case 1: return "stephen king"; // 他的小说使得许多人 // 彻夜不眠 case 2: return "scott meyers"; // 嗯...滥竽充数的一个 } return ""; // 程序不会执行到这儿, // 但对于一个有返回值的函数来说, // 任何执行途径上都要有返回值 }
somefamousauthor的返回值是一个string对象,一个临时string对象(参见条款m19)。这样的对象是暂时性的,它们的生命周期通常在函数调用表达式结束时终止。例如上面的情况中,包含somefamousauthor函数调用的表达式结束时,返回值对象的生命周期也就随之结束。
具体看看下面这个使用somefamousauthor的例子,假设string声明了一个上面的operator const char*成员函数:
const char *pc = somefamousauthor();
cout << pc;
不论你是否相信,谁也不能预测这段代码将会做些什么,至少不能确定它会做些什么。因为当你想打印pc所指的字符串时,字符串的值是不确定的。造成这一结果的原因在于pc初始化时发生了下面这些事件:
1.
产生一个临时string对象用以保存somefamousauthor的返回值(默认拷贝构造函数,浅拷贝指针)。
2. 通过string的operator const
char*成员函数将临时string对象转换为const char*指针,并用这个指针初始化pc。
3.
临时string对象被销毁,其析构函数被调用(我感觉临时对象没有被销毁,但是函数内部string对象被销毁,而临时对象浅拷贝指针,所以临时对象的data指针指向无效的内存空间)。析构函数中,data指针被删除(代码详见条款11)。然而,data和pc所指的是同一块内存,所以现在pc指向的是被删除的内存--------其内容是不可确定的。