背景:
在学习C++编程的时候,都使用过标准库(STL)当中的swap,但更多的是swap(int,int)或 者等等一些基本的类型,发散一下是否也可以用来置换自定义的一个类型,比如说某一class(定义一个class相当于定义一个type了),先不从效率上来考虑,看看可行性如何。ps:欢迎讨论。
正文:
在STL中的swap大概是这样的实现:
template<typename T> void swap(T& a,T& b) { T temp(a); a = b; b = temp; }
在里面很清楚先先调用T类型的拷贝构造函数构造一个临时变量temp,然后利用这个中间变量将a和b进行调换。其中的“=”很阴人。编译器在编译一个空类的时候自动会生成四个缺省的函数,分别是默认构造函数,析构函数,拷贝构造函数,赋值运算符(operator=),取址运算符(operator&)(一对,一个非const的,一个const的)http://topic.csdn.net/t/20051118/12/4402429.html#r_achor在CSDN里面找了个帖子,里面有讨论。有个疑问:都有了赋值运算符函数了,是不是就不需要再重载它了http://topic.csdn.net/u/20111216/10/3890d2aa-6c0c-4472-878f-09132c43ab62.html?
在都是缺省的情况下,拷贝构造函数和赋值运算符的功能是一样的,也就是说它们都是浅拷贝,当然当浅拷贝能够满足我们的需求的时候,就没有必要去重载它们了。就比如:
class myclass { int val; char letter; myclass():val(0),letter('a'){} };
里面的成员变量都是基本的类型,而没有涉及到指针,这个时候无论是拷贝构造函数和赋值运算符都能够满足我们的要求。但是:
class myclass { public: int val; char * letter; myclass():val(0),letter(NULL){} ~myclass() { if(letter) { delete [] letter; } } };
这个时候浅拷贝(这里所谓的浅拷贝包括了拷贝构造函数和运算符重载)显然就不能满足我们的要求,只要有一个对象中途被销毁,另一个就要遭殃了(下图很明确的说明了)。
缺省的赋值运算符重载不能够满足我们的要求,因此我们要重写。还记得上面提过的swap在STL内的实现吗,对,里面就用到了运算符重载,当调用STL swap来置换两个对象的时候,很有必要根据实际情况对swap做出必要的防范,用心设计好运算符重载。下面的讨论都是基于调用STL swap来置换两个对象且类里面定义了指针变量,就比如上面的第三段代码;至于不含有指针变量的很明显,默认的缺省的运算符重载能够满足我们的要求。
试着写这样一个运算符重载:
myclass& operator=(const myclass& mc) { val = mc.val; letter = new char[strlen(mc.letter)+1]; strcpy(letter,mc.letter); return *this; }
一眼看上去是对的,上面的代码缺少安全的检测,因此有缺陷:运算符左值即mc的成员变量letter指针不一定是有效的,即可能它是一个空指针,总不能给他强加个程序崩溃后的罪名,运行起来会有错误。改进后:
myclass& operator=(const myclass& mc) { val = mc.val; if(mc.letter)//先做判断 { letter = new char[strlen(mc.letter)+1]; strcpy(letter,mc.letter); } return *this; }
假设有主程序:
int main() { myclass mc1; mc1.val = 1; mc1.letter = new char[40]; strcpy(mc1.letter,"siyuan"); myclass mc2; mc2.val = 2; mc2.letter = new char[40]; strcpy(mc2.letter,"jialin"); swap(mc1,mc2); cout << mc1.val<< endl; cout << mc1.letter<< endl; cout << mc2.val<< endl; cout << mc2.letter<< endl; return 0; }
当运算符左值的变量letter指针和右值变量letter指针都不为空图解,过程很顺利(下图)
但是左值变量letter指针为空的情况下,结果很意外(下图)
右值变量letter指针为空的情况也一样,很意外(下图)
上面的情况都忽略了对左值变量letter指针为空情况的处理,进一步改进代码:
myclass& operator=(const myclass& mc) { val = mc.val; if(mc.letter) { letter = new char[strlen(mc.letter)+1]; strcpy(letter,mc.letter); } else letter = NULL;//如果左值letter为空指针,也让右值letter指针为空 return *this; }
于是对于运算符左右值其中之一为空的情况有了新的结果(下图),swap成功了
无论是哪种情况,这种代码都不会有失误的时候,甚至当左右值letter都为NULL的时候都不会有错。细心的你一定会有鬼点子(好点子)来鄙视上面那种做法的繁琐(确实够繁琐)。考虑到
int main() { char * m = new char[10]; strcpy(m,"siyuan"); char * n = NULL; m = (m!=NULL?m:" "); n = (n!=NULL?n:" "); cout << m << endl; cout << n << endl; swap(m,n); m = (m!=NULL?m:" "); n = (n!=NULL?n:" "); cout << m << endl; cout << n << endl; return 0; }
这段程序给我很大的启发,确实,它不用再去分配什么空间之类的东西,同时也不用strcpy,毕竟如果数据量较大的话,这些操作花费的时间是不可忽略的。但是“下面的讨论都是基于调用STL swap来置换两个对象且类里面定义了指针变量”,所以这种思路暂时还不能用在这里。
《effective C++》里面有种做法,就是设置一个swap成员函数,在里面再调用STL里面的swap来交换letter指针(更聪明,也容易理解,图解了)和其他的数据。所以标准库(STL)中的swap无疑是可以用来置换两个自定义类型的(比如说class),但是从上面啰嗦那么多就知道需要注意的地方太多了,而且效率也不高,因此虽然标准库(STL)强大,我们应该有选择的利用。
最后总结一下吧:
拷贝构造函数和赋值运算符缺省的情况下功能是一样,当类中有数据涉及指针的时候,我们一定要重载一个不会抛出异常的赋值运算符函数。
- 当你执意要利用STL版swap来置换两个自定义类型时,请细心设计你的赋值运算符(operator=)。
- 在重载赋值运算符(operator=)的时候,一定要小心翼翼,copy每一份数据,并且必须做到一个不漏。
- 当类中涉及太多的指针数据,请选用文章最后说明的方法。
捣乱小子 2011-12-18
ps:欢迎讨论:)