代码改变世界

STL swap潜在的危险

2011-12-18 19:48  捣乱小子  阅读(5417)  评论(3编辑  收藏  举报

背景:

在学习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;
}
}
};

 

  这个时候浅拷贝(这里所谓的浅拷贝包括了拷贝构造函数和运算符重载)显然就不能满足我们的要求,只要有一个对象中途被销毁,另一个就要遭殃了(下图很明确的说明了)。

 

image

 

  缺省的赋值运算符重载不能够满足我们的要求,因此我们要重写。还记得上面提过的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指针都不为空图解,过程很顺利(下图)

image

 

但是左值变量letter指针为空的情况下,结果很意外(下图)

image

 

右值变量letter指针为空的情况也一样,很意外(下图)

image

 

上面的情况都忽略了对左值变量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成功了

image 

 

  无论是哪种情况,这种代码都不会有失误的时候,甚至当左右值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来置换两个对象且类里面定义了指针变量”,所以这种思路暂时还不能用在这里。

image

 

  《effective C++》里面有种做法,就是设置一个swap成员函数,在里面再调用STL里面的swap来交换letter指针(更聪明,也容易理解,图解了)和其他的数据。所以标准库(STL)中的swap无疑是可以用来置换两个自定义类型的(比如说class),但是从上面啰嗦那么多就知道需要注意的地方太多了,而且效率也不高,因此虽然标准库(STL)强大,我们应该有选择的利用。

 

最后总结一下吧:

拷贝构造函数和赋值运算符缺省的情况下功能是一样,当类中有数据涉及指针的时候,我们一定要重载一个不会抛出异常的赋值运算符函数。

  • 当你执意要利用STL版swap来置换两个自定义类型时,请细心设计你的赋值运算符(operator=)。
  • 在重载赋值运算符(operator=)的时候,一定要小心翼翼copy每一份数据,并且必须做到一个不漏。
  • 当类中涉及太多的指针数据,请选用文章最后说明的方法。

 

捣乱小子 2011-12-18

ps:欢迎讨论:)