C_and_CPP 版 (精华区)
发信人: seaboy (浪小), 信区: C_and_CPP
标 题: Solmyr 的小品文系列之八:拷贝
发信站: 哈工大紫丁香 (2003年08月30日12:18:57 星期六), 站内信件
拷贝
“zero 帮帮忙吧 ~~ ”
“灿烂”的笑脸,充满诚意的眼神,再加上点头哈腰的姿势,这三者构成了一尊名为“有
求于人”的塑像。
在 QQ 上聊的正欢的 zero 抬起头,看着塑像的作者和材料 ——— pisces ,方圆五十米
内唯一的女性程序员 ——— 问道:“什么事?”
“我这里有一段 C++ 程序调不通。”
“这类问题你应该去问 Solmyr。”
“哎呀,别开玩笑了,我哪敢去问他呀!总说我笨!上次问他一个小问题,结果又被训的
狗血喷头,哼!”,pisces 显得忿忿不平,“还是你来帮帮我吧,我知道你是部门里有数
的高手,肯定搞的定的。帮帮忙吧 ~~”
zero 明显的被打动了,于是,在 pisces 的努力下,zero 坐到了 pisces 的计算机前。
“好吧,什么问题?”
“是这样的啦,这里有一组 C 风格的 API ,负责管理设备上的字符通信链接。它们是好
些年前设计的”,说着,pisces 调出了一些代码:
// old C style API
typedef int conn_handle;
typedef struct
{
/* ... 打开链接所需的参数和属性 ... */
}conn_attr;
conn_handle open_conn(conn_attr* p_attr, char* buf, unsigned int buf_size);
void close_conn(conn_handle h);
char read_conn(conn_handle h);
void write_conn(conn_handle h, char c);
...
“枝节的东西不算,主干大概就是这样,一对函数负责打开和关闭,一对函数负责读写。
创建链接时候的那个 buf 参数指向一个缓冲区,这个要你自己分配并把长度传进去,和链
接一一对应,read_conn/write_conn 会用它做缓冲。我的任务就是写个类把这些 API 包
装起来。”,说着 pisces 又调出了另外一段代码:
// pisces' connection class
class connection
{
private:
conn_attr m_attr;
bool m_opened;
int m_bufsize;
char* m_buf;
conn_handle m_h;
...
public:
connection(const conn_attr& attr, int bufsize)
{
m_attr = attr;
m_opened = false;
m_bufsize = bufsize;
m_buf = new char[m_bufsize];
}
~connection() { delete m_buf; }
void open()
{
m_h = open_conn(&m_attr, m_buf, m_bufsize);
m_opened = true;
}
void close()
{
close_conn(m_h);
m_opened = false;
}
char read()
{
assert(m_opened);
return read_conn(m_h);
}
void write(char c)
{
assert(m_opened);
write_conn(m_h, c);
}
...
};
“应该是很简单的,可是不知道怎么回事,用了 connection 类的程序总是时不时的崩溃
,说是非法的内存操作。”,pisces 显得很苦恼。
zero 一眼就看出了毛病 ——— 这使他小小的自鸣得意了一下 ——— 但是表面上不动声
色,等到他看过 pisces 提供的“总是引发崩溃”的代码段之后,他才开口说到:
“这是一个常见的错误 pisces”,zero 尽量使自己的口吻和语气听起来象一个权威,“
关于 C++,有一条重要的指导原则:析构函数、拷贝构造函数和赋值运算符三者几乎总是
一起出现。也就是说,如果你为一个类写了析构函数,那么往往你不得不再提供一个拷贝
构造函数和一个赋值运算符,违反它往往意味着错误。你看这里:”
说着,zero 在屏幕上标出了两行代码:
void some_func()
{
conn_attr attr;
...
connection c1(512, attr);
connection tmp = c1;
...
}
“这里对象 tmp 是从 c1 拷贝构造而来的,而你没有定义拷贝构造函数,这使得编译器在
这里自动进行按位拷贝,而这使得 tmp 和 c1 的所有成员都相等,包括 m_buf 成员。这
样在函数返回时,c1 析构的时候 delete 了一遍 m_buf,在 tmp 析构的时候又 delete
了一遍 ……”
“哦!我明白了!” pisces 打断了 zero ,“所以就出现一个非法内存操作,对吧?哎
呀,这一条以前在学校里写 string 类的时候遇到过,我怎么会忘了呢?”
“对,你只要写一个拷贝构造函数和一个赋值运算符处理一下 m_buf 指针就可以解决这个
问题了。这你自己搞的定吧?”
“我可以的,多谢了 zero !”
zero 心满意足的回到了自己的座位上,开始继续和“你不懂我纤细的心”在 QQ 上探讨“
爱情的意义”。可是好景不长,没过多久,本文开头所描述的景象再一次的出现了。
“zero 帮帮忙吧 ~~ ”
zero 在心中叹了口气,抬头问道:“又是什么问题,pisces?”
“呃,还是那个类。我照你说的给 conn 添加了拷贝构造函数,非法内存操作确实少多了
,可还是有,还有好像链接传输数据也有点问题 ———— 你还是过来帮我看看吧 ~~”
zero 心不甘情不愿的再次来到了 pisces 的计算机前,翻出 pisces 写的拷贝构造函数检
查起来:
connection(const connection& other)
{
m_attr = other.m_attr;
m_bufsize = other.m_bufsize;
m_buf = new char[m_bufsize];
memcpy(m_buf, other.m_buf, m_bufsize);
m_opened = other.m_opened;
m_h = other.m_h;
}
zero 的眉头皱了起来,这个拷贝构造函数似乎应该可以解决问题,显然现在两个 m_buf
各自指向合法的内存,不再存在两次释放的问题。那么问题出在哪儿呢?zero 陷入了沉思
。不过仅仅多花了不到 1 分钟,zero 就明白了过来。
“哦!我明白了!见鬼,我怎么会没注意到这个。pisces ,问题还是出在 m_buf 上面,
因为链接和缓冲区指针是一一对应的,所以拷贝构造函数里新分配的缓冲区根本不起作用
。”
pisces 眨了眨眼,表情略显呆滞。
“给你举个例子吧。”zero 飞快的键入一段测试代码:
connection* pc = NULL;
{
conn_attr attr;
connection c1(512, attr);
c1.open();
pc = new connection(c1);
}
pc->write('A');
“c1 的构造函数里调用 new 为它的 m_buf 成员分配内存,紧接着在 open 函数里调用
open_conn 打开了一个链接,注意这里我们传入 open_conn 的参数是 c1.m_buf ,所以这
个链接对应的缓冲区指针是 c1.m_buf 。然后我们执行 pc = new connection(c1),新对
象从 c1 拷贝构造,所以 pc->m_h 和 c1.m_h 相等,也就是说这两个对象保存的 m_h 标
识着同一个链接,对应的缓冲区指针都是 c1.m_buf ———— ”
zero 象 Solmyr 常做的那样停了下来,但却失望的看到 pisces 毫无反应,只好接着往下
说:
“所以接下来的 pc->write 在调用 write_conn 时候,这个 API 并不知道这是通过另外
一个对象在调用它,它仍然试图使用 c1.m_buf 作为缓冲区,但这个时候 c1 已经结束了
它的生命周期,c1.m_buf 已经被释放了,所以,这是一个非法的内存访问。”
pisces 舔了舔嘴唇:“ …… 那 …… 那么现在怎么办?”
zero 翻了个白眼 ——— 很明显 pisces 根本没明白是怎么一回事 ——— 开始考虑怎样
应付眼前这个问题。
“嗯,看样子,这里必须考虑多个对象共享一个指针的问题,嗯,为了保证这块内存被释
放 …… 恐怕 …… 恐怕得用上引用计数技术(请参见“小品文系列之五:垃圾收集”)
才搞得定,要不要用 boost::shared_ptr 呢?”,zero 一边想,一边自言自语。突然间
———
“逻辑的混乱导致实现上的复杂,zero,这个 connection 类千疮百孔啊。”,Solmyr 的
声音毫无预兆的在背后响起。
zero 在 0.01 秒内控制住了拔腿飞奔的冲动,以尽可能放松的姿态缓缓的转过身来。在他
的面前是披着一贯优雅伪装的 Solmyr,一手端着果汁,一手牢牢的拽着仍在拼命挣扎试图
逃走的 pisces 。
“啊 Solmyr ,我正想找你呢,这个问题稍许有点棘手。”
“是吗?那你的腿为什么在抖?”
“嗯?没有,有点冷而已 …… 啊 Solmyr ,你刚刚说什么来着?”
“逻辑的混乱导致实现上的复杂,zero,这个 connection 类千疮百孔。”Solmyr 把
pisces 按在旁边的座位上,接着说到:“你刚才发现的问题只是其中之一而已。看一下这
个:”
void some_func()
{
conn_attr attr;
...
connection c1(512, attr);
c1.open();
...
connection tmp = c1;
c1.close();
tmp.write('a');
...
}
“这会导致什么?”
“ …… 试图写入一个已经关闭了链接。”
“还需要我给出多次打开一个链接,多次关闭一个链接,以及各种链接处于打开状态但读
写却会引发断言错误的例子吗?”
“ …… 不用了。”
“那你打算怎样修复这些问题?要不要在每个对象里保存一个由它拷贝构造而来的对象列
表?或者你打算在文档里写‘以下 371 种方式使用该类会导致无法预知的错误’?”
“ …… ”
Solmyr 重重的叹了口气:“你被 pisces 误导了,zero,因为你只想着怎么帮 pisces 解
决问题,如果一开始就让你来设计这个类,情况一定不会这么糟糕。”说着,Solmyr 狠狠
的瞪了 pisces 一眼。“不要忘了,C++ 类不是简单的把一堆成员变量和成员函数凑在一
起,永远记得这个原则:C++ 中用类来表示概念。”
zero 点了点头。
“我来问你,connection 这个类应该表示什么概念?”
“呃,应该表示‘链接’这个概念。”
“一个 connection 类的对象应该代表 ……”
“应该代表一个实际‘链接’。”
“很好。那么你告诉我,你刚才努力想设计出的那个拷贝构造函数要干什么?”
“ …… 让两个 connection 对象能够表示同一链接。”
“所以 ……”
“ …… 所以 …… 嗯 …… 哦 …… ”zero 露出了恍然大悟的表情:“所以我实际上想
做的是要表达这样一个概念:如果一个 connection 对象没有被拷贝,它就表示一个独立
的链接,如果它被拷贝了,那么它就和拷贝者表示同一个链接,这也包括拷贝者的拷贝者
,拷贝者的拷贝者的拷贝者 …… 天哪,这根本是一团乱麻!”
“对,问题就在这里。一个 connection 对象代表什么?你试图给出一个在逻辑上非常混
乱的答案,这导致了实现的复杂性。实际上,如果理清这个逻辑,问题是很简单的:一个
connection 对象代表一个链接,它构造,代表建立了一个链接;它析构,代表这个链接走
完了它的生命历程 ——— 这里 open 和 close 这两个成员函数根本就是多余的。至于拷
贝构造 ……”
Solmyr 顿了顿,以一种斩钉截铁式的语气说到:
“应该禁止。”
“禁止拷贝?!”
“对,应该禁止。事实上,对于‘链接’这个概念而言,‘拷贝’动作含义模糊:拷贝意
味着什么?拷贝构造的对象所表示的链接和原来的链接是什么关系?当使用 connection
类的程序员看到 connection c2 = c1; 这样的代码时,他没法从代码本身看出这是什么意
思,他会猜测,c1 和 c2 代表的是一个链接?还是两个链接?只能通过查阅文档来解决,
这加重了使用者的负担,而如果禁止拷贝,所有智力正常的程序员都会明白每个
connection 对象唯一的代表一个链接。”
zero 若有所思的点了点头。
“同时,这还能阻止程序员用传值方式向函数传递 connection 对象 ——— 想象一下,
如果一个程序员这样使用 connection ,会发生什么?”,Solmyr 键入了下面的代码:
void send_a_greeting(connection c)
{
c.write("Hello!");
}
zero 没费什么劲就看出了问题:“函数的设计者以为他是在向调用者传入的链接发送消息
,但实际上这个函数在按值传递参数的时候创建了一个新链接。”
Solmyr 点了点头,继续说到:“还有,从扩展性的角度考虑,也应该禁止拷贝。比如,假
设你将来打算控制链接的创建,把创建过程封装起来,那么这个拷贝构造函数就在你的封
装上捅了一个大窟窿 ——— 每个人都可以很方便的利用拷贝构造任意创建链接;又比如
,假设将来你需要支持多个类型的链接,要把 connection 作为一个类层次的接口基类,
那时,connection 的拷贝构造就必须要禁止,而你之前支持拷贝构造带来的代价就是辛苦
的翻遍之前所有的代码去掉所有拷贝构造。”
“那,如果我确实需要在多处访问一个链接,该 ……” zero 没等 Solmyr 回答,自己就
接了上去,“呃,也很简单,只要传递引用就可以了,或者如果需要更好的控制,可以用
智能指针什么的。”
“完全正确。说起来,其实许多类 ——— 比许多人所认为的要多的多 ——— 所表示的
概念对于‘拷贝’这个动作都没有清楚的定义,比如常见的‘窗口’、‘文件’、‘事务
’等等等等,禁止它们拷贝往往可以让代码的逻辑清楚许多。以后你在设计类的时候,完
全可以首先考虑是否禁止它的拷贝构造,如果不能禁止,再去考虑怎么写拷贝构造函数的
问题。好了 zero ,现在你能给出 connection 的实现吗?”
“Sure!只要将拷贝构造函数和重载赋值运算符设为私有,就可以禁止拷贝了。”zero 拖
过键盘,三两下屏幕上就出现了一个新的实现:
class connection
{
private:
conn_attr m_attr;
int m_bufsize;
char* m_buf;
conn_handle m_h;
...
public:
connection(const conn_attr& attr, int bufsize)
: m_attr(attr), m_bufsize(bufsize)
{
m_buf = new char[m_bufsize];
m_h = open_conn(&m_attr, m_buf, m_bufsize);
}
~connection()
{
close_conn(m_h);
delete m_buf;
}
void write(char c){ write_conn(m_h, c); }
char read(){ return read_conn(m_h); }
...
private:
connection(const connection&);
connection& operator=(const connection&);
};
“嗯,很好,这个问题可以告一段落了。”Solmyr 点了点头,准备离开,但又停了下来:
“对了 zero ,pisces 他们这边曾经打报告要求增加人手,从今天的情况来看也确实需要
有个懂点 C++ 的人加强这边。我看你正好有空,这个事就你来负责吧。”
zero 心中暗暗叫苦,赶紧分辨:“没有啊 Solmyr,我现在手边的事情多得做不完啊!”
“是吗?哦 …… 对了,我刚才接到网管的报告,说有个人的电脑最近频繁的访问 QQ 的
服务器,那个人是谁来着?”,Solmyr 又露出了他招牌式的微笑。
“呃 …… 我又想了想,虽然我确实事情比较多,但团队合作精神还是要发扬的。”
“嗯,这样就好。” Solmyr 心满意足的离开了。
“真见鬼!”确认 Solmyr 走远后,zero 才把在心里憋着的抱怨吐了出来:“好不容易有
一段可以休息休息的空档,这下子又泡汤了!真该死。”正在 zero 忿忿不平的时候,一
个幽幽的声音从旁边飘了过来:
“zero,刚才你和 Solmyr 讲的什么‘概念’、‘禁止拷贝’、‘类层次’…… 这些都是
什么呀?还是你给我讲讲吧 ~~”
zero 转过头,看到 pisces 又在以非常“诚恳”的眼神看着他,再想到自己今后的任务,
突然间觉得脑袋隐隐的痛了起来 ——— 他似乎有一点明白了,为什么没事的时候
Solmyr 总在揉自己的太阳穴 ……
--
欢迎到C_and_CPP版讨论相关问题。
※ 来源:·哈工大紫丁香 bbs.hit.edu.cn·[FROM: 202.118.239.104]
Powered by KBS BBS 2.0 (http://dev.kcn.cn)
页面执行时间:2.630毫秒