Effective C++ 其次版 29)内部句柄 30)成员函数返回非const指针或引用
类和函数: 实现
C++是高度类型化的语言, 给出合适的类和模板的定义以及合适的函数声明是整个设计中最大的一部分; 这部分做好, 类, 模板以及函数的实现就不容易出问题;
人们常常犯错的原因是不小心违反了抽象的原则: 让实现细节可以提起类和函数内部的数据; 不清楚对象生命周期; 不合理的前期优化, 滥用inline; 有些实现策略会导致源文件间的相互联结问题, 小规模范围内合适, 但在重建大系统时带来巨大成本;
条款29 避免返回内部数据的句柄
请看面向对象世界里的一幕: 对象A:"亲爱的, 永远别变心" 对象B:"别担心, 亲爱的, 我是const"; 和现实生活中一样, A会怀疑, 能相信B么? again, 和现实生活中一样, 答案取决于B的本性: 成员函数的组成结构;
e.g. 假设B是const String对象:
1
2
3
4
5
6
7
8
9
10
11
|
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 对象
|
B是const, 最好的情况就是无论什么时候, B的值总是"Hello World"; 这就要寄希望于其他程序员以合理的方式使用B;
万一有人将B强制转换掉const; [拜拜了, 亲爱的]
1
|
String&
alsoB = const_cast <String&>(B); //
使得alsoB 成为B 的另一个名字 但不具有const 属性
|
即使没有人做这种残暴的事情, 还是不能保证B永远不变:
1
2
|
char *str
= B; //
调用B.operator char*()
strcpy (str, "Hi
Money" ); //
修改str 指向的值
|
>B现在的值到底还是不是Hello World, 答案取决于String::operator char*的实现;
下面的实现会导致错误的结果, 虽然工作起来高效;
1
2
|
//
一个执行很快但不正确的实现
inline String::operator char *() const { return data;
}
|
这个函数的缺陷在于它返回了一个"句柄" (这里是个指针), 句柄所指向的信息本来是应该隐藏在被调用函数所在的String对象的内部; 这个句柄给了调用者自由访问data所指的私有数据的机会;
char *str = B; str-----> "Hello World\0" <---B.data; 任何对str所指向的内存的修改都使得B的有效值发生变化; 即使B声明为const, 而且即使只调用了B的某个const成员函数, B也会在程序运行过程中得到不同的值; 当str修改了所指向的值, B也会改变; [你好 钞票, 亲爱的是谁??]
String::operator char*本身没写错, 麻烦的是他可以用于const对象; 如果将这个函数声明为非const, 就能阻止他作用与像B这样的const对象;
但是, 将String对象转换成相应的char*形式是合理的行为, 无论这个对象是否是const; 所以, 应该使函数保持const, 重写函数, 使它不返回指向对象内部数据的句柄:
1
2
3
4
5
6
7
|
//
一个执行慢但很安全的实现
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的指针:
1
2
3
4
5
6
|
class String
{
public :
operator const char *() const ;
...
};
inline String::operator const char *() const { return data;
}
|
这个函数既快又安全, 可以满足大多数程序的需要; 这个做法和C++标准组织处理string/char*难题的方案一致: 标准string类型中包含一个成员函数c_str, 返回值是string的const char*版本; 标准string类型的信息参见条款49;
指针不是返回内部数据句柄的唯一途径, 引用也很容易被滥用;
e.g. 以String为例, 一种常见的用法;
1
2
3
4
5
6
7
8
9
10
11
12
|
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对象的内部数据; 问题和指针的相同;
这类问题的通用解决方案和指针的一样: 1) 使函数为非const [不允许const对象调用]; 2) 重写函数, 不返回句柄; [返回const 指针];
如果想让String:operator[]既适用const对象又适用非const对象, 参见条款21; [重载]
并不是只有const成员函数需要担心返回句柄的问题, 即使是非const成员函数也会遇到问题, 句柄的合法性失效的时间和它对应的对象是完全相同的; 这个时间可能比用户预期的早得多, 特别是涉及编译器产生的临时对象时;
e.g.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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对象; 这样的对象是暂时性的, 声明周期通常在函数调用表达式结束时终止; 包含someFamousAuthor函数调用的表达式结束时, 返回值对象的生命周期随之结束;
e.g. 假设String声明了一个上面的operator const char*成员函数:
1
2
|
const char *pc
= someFamousAuthor();
cout
<< pc;
|
>谁也不能预测这段代码会做些什么, 至少无法确定; 当你打印pc所指的字符串时, 值是不确定的;
原因在于pc初始化时发生了:
1) 产生一个临时String对象用来保存someFamousAuthor的返回值;
2) 通过String的operator const char*成员函数将临时String对象转换为const char*指针, 并用指针初始化pc;
3) 临时String对象被销毁, 析构被调用; 析构函数中, data指针被删除(代码见条款11), data和pc所指的是同一块内存, 因此pc指向了被删除的内存, 其内容是不可确定的;
指向临时对象的句柄带来的问题: 因为pc是被一个指向类是对象的句柄初始化的, 临时对象在创建后又立即被销毁, 所以在pc被使用前句柄已经是非法的了; 当想要使用pc时, pc的内容已经不存在了;
Note 对于const成员函数来说, 返回句柄是不明智的, 它会破坏数据抽象; 对于非const成员函数来说, 返回句柄会带来麻烦, 特别是涉及到临时对象时;
句柄就像指针一样, 可以是悬浮dangle的; 一定要像避免悬浮的指针那样, 尽量避免悬浮的句柄;
同样, 不能对条款绝对化, 在一个大型程序中想消灭所有可能的悬浮指针/悬浮句柄是不现实的; 但是要尽量避免返回句柄, 增加安全性;
条款30 避免这样的成员函数: 返回值是指向成员的非const指针或引用, 但成员的访问级比这个函数低
一个成员声明为private或protected是想限制对它的访问; 实际编程中很容易违反这条规则:
1
2
3
4
5
6
7
8
|
class Address
{ ... }; //
某人居住在此
class Person
{
public :
Address&
personAddress() { return address;
}
...
private :
Address
address;
};
|
成员函数personAddress为调用者提供Person对象中包含的Address对象, 可能是出于效率考虑, 返回的是引用, 不是值(条款22); 这个成员函数的做法有违背当初将Person::adress声明为private的初衷:
1
2
|
Person
scott(...); //
为简化省略了参数
Address&
addr = scott.personAddress(); //
假设addr 为全局变量
|
>全局对象addr成了scott.adress的别名; 利用它可以随意读写scott.adress; 实际上, scott.adress不再为private, 而是public; 访问等级提升的根源在于成员函数personAddress;
不仅仅是引用, 指针也会产生以上问题:
1
2
3
4
5
6
7
8
|
class Person
{
public :
Address
* personAddress() { return &address;
}
...
private :
Address
address;
};
Address
*addrPtr = scott.personAddress(); //
问题和上面一样
|
>对指针来说, 要担心的不仅是数据成员, 还要考虑成员函数, 返回一个成员函数的指针也是可能的:
1
2
3
4
5
6
7
8
9
10
11
|
class Person; //
提前声明
//
PPMF = "pointer to Person member function" (指向Person 成员函数的指针)
typedef void (Person::*PPMF)();
class Person
{
public :
static PPMF
verificationFunction() { return &Person::verifyAddress;
}
...
private :
Address
address;
void verifyAddress();
};
|
如果你没试过将成员函数指针和typedef结合起来的用法, 可能会觉得Person::verificationFunction的声明有点吓人; [callback]
含义:
- verificationFunction是一个没有输入参数的成员函数;
- 返回值是Person类中一个成员函数的指针;
- 被指向的函数 (verificationFunction的返回值) 没有输入参数且没有返回值(void);
static关键字对成员声明时, 表示整个类只有这个成员的一份拷贝, 并且这个成员可以不通过类的具体对象来访问; [static避免了this指针, 否则必须要由实例来调用函数]
verifyAddress是私有成员函数, 是类的实现细节, 只有类的成员(友元)才应该知道它; 但是由于公有成员函数verificationFunction返回了指向verifyAdress的指针, 用户可以做这样的事情:
1
2
|
PPMF
pmf = scott.verificationFunction();
(scott.*pmf)(); //
等同于调用 scott.verifyAddress
|
>pmf成了Person::verifyAdress的同义词, 区别是, 他可以没有限制地被调用;
Note 虽然要避免, 但有一天可能为了程序的性能还是不得不写出像上面那样的函数--返回值是某个访问级别较低的成员的指针或引用; 如果不想牺牲private/protected提供的访问限制; 可以通过返回指向const对象的指针或引用来达到效果; (条款21)