博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【转载】条款29: 避免返回内部数据的句柄
阅读量:6149 次
发布时间:2019-06-21

本文共 4193 字,大约阅读时间需要 13 分钟。

请看面向对象世界里发生的一幕:

对象a:亲爱的,永远别变心!

对象b:别担心,亲爱的,我是const。

然而,和现实生活中一样,a会怀疑,"能相信b吗?" 同样地,和现实生活中一样,答案取决于b的本性:其成员函数的组成结构。

假设b是一个const string对象:

1
2
3
4
5
6
7
8
9
10
11
12
class
string {
public
:
  
string(
const
char
*value);       
// 具体实现参见条款11
  
~string();                       
// 构造函数的注解参见条款m5
 
  
operator
char
*()
const
;         
// 转换string -> char*;
                                    
// 参见条款m5
  
...
 
private
:
  
char
*data;
};
1
const
string b(
"hello world"
);     
// b是一个const对象

既然b为const,最好的情况当然就是无论现在还是以后,b的值总是"hello world"。这就寄希望于别的程序员能以合理的方式使用b了。特别是,千万别有什么人象下面这样残忍地将b强制转换掉const(参见条款21):

1
2
string& alsob =             
// 使得alsob成为b的另一个名字,
  
const_cast
<string&>(b);   
// 但不具有const属性

然而,即使没有人做这种残忍的事,就能保证b永远不会改变吗?看看下面的情形:

1
2
char
*str = b;              
// 调用b.operator char*()
strcpy
(str,
"hi mom"
);      
// 修改str指向的值

b的值现在还是"hello world"吗?或者,它是否已经变成了对母亲的问候语?答案完全取决于string::operator char*的实现。

下面是一个有欠考虑的实现,它导致了错误的结果。但是,它工作起来确实很高效,所以很多程序员才掉进它的错误陷阱之中:

1
2
3
4
5
// 一个执行很快但不正确的实现
inline
string::operator
char
*()
const
{
    
return
data;
}

这个函数的缺陷在于它返回了一个"句柄"(在本例中,是个指针),而这个句柄所指向的信息本来是应该隐藏在被调用函数所在的string对象的内部。这样,这个句柄就给了调用者自由访问data所指的私有数据的机会。换句话说,有了下面的语句:

1
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
8
// 一个执行慢但很安全的实现
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
7
class
string {
public
:
  
operator
const
char
*()
const
;
 
  
...
 
};
1
2
inline
string::operator
const
char
*()
const
{
return
data; }

这个函数既快又安全。虽然它和最初给出的那个函数不一样,但它可以满足大多数程序的需要。这个做法还和c++标准组织处理string/char*难题的方案一致:标准string类型中包含一个成员函数c_str,它的返回值是string的const char*版本。关于标准string类型的更多信息参见条款49。

指针并不是返回内部数据句柄的唯一途径。引用也很容易被滥用。下面是一种常见的用法,还是拿string类做例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,或者重写函数,使之不返回句柄。如果想让string::operator[]既适用于const对象又适用于非const 对象,可以参见条款21。

并不是只有const成员函数需要担心返回句柄的问题,即使是非const成员函数也得承认:句柄的合法性失效的时间和它所对应的对象是完全相同的。这个时间可能比用户期望的要早很多,特别是当涉及的对象是由编译器产生的临时对象时。

例如,看看这个函数,它返回了一个string对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
""
;                       
// 程序不会执行到这儿,
                                    
// 但对于一个有返回值的函数来说,
                                    
// 任何执行途径上都要有返回值
}

希望你的注意力不要集中在随机数是怎样从rand产生的问题上,也不要嘲笑我把自己和这些作家联系在一起。真正要注意的是,somefamousauthor的返回值是一个string对象,一个临时string对象(参见条款m19)。这样的对象是暂时性的,它们的生命周期通常在函数调用表达式结束时终止。例如上面的情况中,包含somefamousauthor函数调用的表达式结束时,返回值对象的生命周期也就随之结束。

具体看看下面这个使用somefamousauthor的例子,假设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其实已经名存实亡。这就是指向临时对象的句柄所带来的危害。

所以,对于const成员函数来说,返回句柄是不明智的,因为它会破坏数据抽象。对于非const成员函数来说,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针一样,可以是悬浮(dangle)的。所以一定要象避免悬浮的指针那样,尽量避免悬浮的句柄。

同样不能对本条款绝对化。在一个大的程序中想消灭所有可能的悬浮指针是不现实的,想消灭所有可能的悬浮句柄也是不现实的。但是,只要不是万不得已,就要避免返回句柄,这样,不但程序会受益,用户也会更信赖你。

原文地址:

转载地址:http://dhqya.baihongyu.com/

你可能感兴趣的文章
linux运维人员的成功面试总结案例分享
查看>>
Windows DHCP Server基于MAC地址过滤客户端请求实现IP地址的分配
查看>>
命令查询每个文件文件数
查看>>
《跟阿铭学Linux》第8章 文档的压缩与打包:课后习题与答案
查看>>
RAC表决磁盘管理和维护
查看>>
Apache通过mod_php5支持PHP
查看>>
发布一个TCP 吞吐性能测试小工具
查看>>
java学习:jdbc连接示例
查看>>
PHP执行批量mysql语句
查看>>
Extjs4.1.x 框架搭建 采用Application动态按需加载MVC各模块
查看>>
Silverlight 如何手动打包xap
查看>>
建筑电气暖通给排水协作流程
查看>>
JavaScript面向对象编程深入分析(2)
查看>>
linux 编码转换
查看>>
POJ-2287 Tian Ji -- The Horse Racing 贪心规则在动态规划中的应用 Or 纯贪心
查看>>
Windows8/Silverlight/WPF/WP7/HTML5周学习导读(1月7日-1月14日)
查看>>
关于C#导出 文本文件
查看>>
使用native 查询时,对特殊字符的处理。
查看>>
maclean liu的oracle学习经历--长篇连载
查看>>
ECSHOP调用指定分类的文章列表
查看>>