把对象作为参数传递给函数以及从函数中返回对象,都会带来潜在的问题。在这两种情况中,产生的问题都是因为使用了默认的按位复制构造函数来创建对象的副本。解决这个问题的办法是创建自己的复制构造函数,这样就可以精确地定义如何构造一个对象的副本。
在我们将一个对象赋给另一个对象时,也会存在问题。在默认情况下,复制运算符左边的对象得到的是右边对象的一个按位复制的副本。这样,如果在一个对象中分配了系统资源,比如内存,那么当资源被创建或者修改,或者被释放时,都将会影响到第 2 个对象,因为它也在使用同样的资源。
下面这个程序是一个有问题的程序:
[C++] 纯文本查看 复制代码
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class sample {
char *s;
public:
sample() { s = 0; }
sample(const sample &ob); //复制构造函数
~sample() { if(s) delete [] s; cout << "Freeing s\n"; }
void show() { cout << s << "\n"; }
void set (char *str);
};
//复制构造函数
sample::sample(const sample &ob)
{
cout << "Copy Constructor called.\n";
s = new char[strlen(ob.s) + 1];
strcpy(s, ob.s);
}
//加载一个字符串
void sample::set(char *str)
{
s = new char[strlen(str) + 1];
strcpy(s, str);
}
//返回sample类型对象
sample input()
{
char instr[80];
sample str;
cout << "Enter a string: ";
cin >> instr; //字符串遇空格或回车终止
str.set(instr);
return str;
}
int main()
{
sample ob;
//将函数返回的对象赋值给 ob
ob = input(); //错误!!
ob.show();
return 0;
}
这段程序在 g++ 和 vc2010 编译下运行会有不同的结果。
g++ 下运行情况:[beyes@beyes cpp]$ ./reterr
Enter a string: hello
Freeing s
*** glibc detected *** ./reterr: double free or corruption (fasttop): 0x09d40008 ***
======= Backtrace: =========
/lib/libc.so.6[0x4bbb52b5]
/usr/lib/libstdc++.so.6(_ZdlPv+0x20)[0x4cf05460]
/usr/lib/libstdc++.so.6(_ZdaPv+0x1c)[0x4cf054bc]
./reterr[0x8048987]
./reterr[0x80488d5]
/lib/libc.so.6(__libc_start_main+0xf3)[0x4bb5d413]
./reterr[0x80486f1]
======= Memory map: ========
008bd000-008be000 r-xp 00000000 00:00 0 [vdso]
08048000-08049000 r-xp 00000000 fd:02 1188114 /home/beyes/cpp/reterr
08049000-0804a000 rw-p 00000000 fd:02 1188114 /home/beyes/cpp/reterr
09d40000-09d61000 rw-p 00000000 00:00 0 [heap]
4bb23000-4bb40000 r-xp 00000000 fd:01 1966840 /lib/ld-2.14.so
4bb40000-4bb41000 r--p 0001d000 fd:01 1966840 /lib/ld-2.14.so
4bb41000-4bb42000 rw-p 0001e000 fd:01 1966840 /lib/ld-2.14.so
4bb44000-4bcc9000 r-xp 00000000 fd:01 1967407 /lib/libc-2.14.so
4bcc9000-4bcca000 ---p 00185000 fd:01 1967407 /lib/libc-2.14.so
4bcca000-4bccc000 r--p 00185000 fd:01 1967407 /lib/libc-2.14.so
4bccc000-4bccd000 rw-p 00187000 fd:01 1967407 /lib/libc-2.14.so
4bccd000-4bcd0000 rw-p 00000000 00:00 0
4bd00000-4bd28000 r-xp 00000000 fd:01 1967504 /lib/libm-2.14.so
4bd28000-4bd29000 r--p 00028000 fd:01 1967504 /lib/libm-2.14.so
4bd29000-4bd2a000 rw-p 00029000 fd:01 1967504 /lib/libm-2.14.so
4bd2c000-4bd48000 r-xp 00000000 fd:01 1971304 /lib/libgcc_s-4.6.0-20110603.so.1
4bd48000-4bd49000 rw-p 0001b000 fd:01 1971304 /lib/libgcc_s-4.6.0-20110603.so.1
4ce54000-4cf36000 r-xp 00000000 fd:01 662924 /usr/lib/libstdc++.so.6.0.16
4cf36000-4cf3a000 r--p 000e1000 fd:01 662924 /usr/lib/libstdc++.so.6.0.16
4cf3a000-4cf3c000 rw-p 000e5000 fd:01 662924 /usr/lib/libstdc++.so.6.0.16
4cf3c000-4cf42000 rw-p 00000000 00:00 0
b774a000-b774d000 rw-p 00000000 00:00 0
b7762000-b7766000 rw-p 00000000 00:00 0
bfe2b000-bfe4c000 rw-p 00000000 00:00 0 [stack]
Aborted (core dumped)
在 vc2010 里编译运行:Enter a string: hello
Copy Constructor called.
Freeing s
Freeing s
葺葺葺葺葺葺葺葺 对比两个编译器所编译的程序的输出结果可以看到,g++ 在 input() 函数返回时并没有调用复制构造函数,而 VC2010 却调用了。但不论如何,程序的输出都是错误的。
下面分析一下该函数所产生问题的原因:
先假设复制构造函数在返回 input() 返回对象时被调用(VC 编译器情况)。那么,当 input() 在最后返回对象时,它通过创建一个临时对象来保存返回值。因为在创建副本时,对象的复制构造函数为临时对象分配了新的内存空间(调用的复制构造函数所为),所以原始对象中的指针变量 s 和副本中的 s 指向内存中不同的区域,它们是互相独立的,这点复制构造函数做的没错,看上去似乎也没有问题。当 input() 结束时,它的局部对象 str 被销毁,并释放它的成员 s 所指向的空间。
但是,当返回的临时对象被赋值给 ob 时,因为默认赋值运算符执行的是按位复制运算。所以在这种情况下,由函数 input() 返回的对象的副本将被按位复制到 ob 中,这也就使得 ob.s 与临时对象中的 s 指向了同样的内存区。当赋值运算完成时,临时对象被销毁,内存也将被释放,就在这个时候,ob.s 指向的是一个已经被释放的内存!并且,在程序结束时,ob.s 被再次释放,从而内存被释放了两次!
而对于 g++ 的情况,即使它不调用复制构造函数,但它最后还是会用 str 的副本对 ob 进行按位复制,所以都免不了造成的二次释放内存的问题,程序最终是要发生错误的!
解决这个问题的办法是“重载赋值运算符”,使用“赋值运算符”左边的对象(即 main() 中的 ob)分配自己的内存!
先用 VC2010 来编译下面的代码:
[C++] 纯文本查看 复制代码 #include "stdafx.h"
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class sample {
char *s;
public:
sample();
sample(const sample &ob);
~sample()
{
if(s) {
delete [] s;
}
cout << "Freeing s\n";
}
void show() { cout << s << "\n"; }
void shows() { cout << &s << endl; }
void set (char *str);
sample operator=(const sample &ob); //重载赋值运算符
};
//普通构造函数
sample::sample()
{
s = new char('\0'); //s指向一个空的字符串
}
//复制构造函数
sample::sample(const sample &ob)
{
cout << "Copy Constructor-1" << endl;
s = new char[strlen(ob.s) + 1];
strcpy(s, ob.s);
}
//装载一个字符串
void sample::set(char *str)
{
s = new char[strlen(str) + 1];
strcpy(s, str);
}
//重载赋值运算符
sample sample::operator=(const sample &ob)
{
cout << "Copy Constructor-2" << endl;
//如果对象中变量s的内存小于ob中s的内存,那么就重新分配内存
if (strlen(ob.s) > strlen(s)) {
delete [] s;
s = new char[strlen(ob.s) + 1];
}
strcpy(s, ob.s);
return *this;
}
//返回sample类型的对象
sample input()
{
char instr[80];
sample str;
cout << "Enter a string: ";
cin >> instr;
str.set(instr);
return str;
}
int _tmain(int argc, _TCHAR* argv[])
{
sample ob;
//将函数返回的对象赋给ob
ob = input();
ob.show();
return 0;
}
运行输出:Enter a string: hello
Copy Constructor-1
Freeing s
Copy Constructor-2
Copy Constructor-1
Freeing s
Freeing s
hello
Freeing s 从输出可以看到,这次没有再输出垃圾数据或者程序崩溃了。下面详细说明一下输出的内容:
1. 在 input() 函数里创建了一个局部的 sample 类对象 str ,然后它会要求输出一个字符串,这里我们输入 "hello",这样在对象 str 的 set() 函数里会开辟一段内存空间以保存该字符串。
2. input() 函数最后将局部对象 str 返回,这时会建立它的一个副本,并调用复制构造函数。在该次调用的复制构造函数里,函数参数是 str 的引用,函数里边的 s 是副本的成员变量。在该次调用里,输出了“输出结果”中的第 2 行的 “Copy Constructor-1” 。
3. 在 input() 函数退出后,局部对象 str 被销毁,这样就会调用析构函数释放它之前所分配的空间,所以在第 3 行里输出 "Freeing s" 。
4. 接着,程序进入对 '=' 号的重载部分。这时调用运算符重载函数,从而输出第 4 行字符串 "Copy Constructor-2“ 。在该重载函数里,函数参数中的 const sample &ob 即是之前建立 str 副本对象的引用。函数体内的 s 即是 main() 中创建的对象 ob 的成员变量,该变量在该重载函数里由 this 指针隐式指出。
5. 在 :operator=() 函数结束时,又返回了对象 *this 。按照复制构造函数的调用规则,此时又会调用复制构造函数。在这次的调用的复制构造函数中,函数中的参数是 main() 中对象 ob 的副本的引用。在函数里输出了“输出结果”中的第 5 行 "Copy Constructor-1" 。在这里,又为副本分配了一段内存空间,其实这段空间在这里实际上已经没啥用了。
6. 在从上面的复制构造函数返回时,因为所建立的副本实际没什么用处,所以很快被销毁掉,因此输出第 6 行的 "Freeing s" 。
7. 程序流程继续返回到 main() 中时,因为 input() 中建立的副本也已经没什么用处,所以也要被销毁,因此输出第 7 行的 "Freeing s" 。
8. ob.show() 输出第 8 行的 "hello" 。
9. 当 main() 退出时,销毁对象 ob 时调用析构函数,输出最后一行的 "Freeing s" 。
下面来看 g++ 的编译结果输出:$ ./reloadeq
Enter a string: hello
Copy Constructor-2
Copy Constructor-1
Freeing s
Freeing s
hello
Freeing s 对比于 VC2010 中的输出,g++ 的编译输出少了第 1 行和第 2 行的:Copy Constructor-1
Freeing s 这说明 g++ 在 input() 返回对象时没调用复制构造函数 sample::sample(const sample &ob); 。因为,事实上赋值运算符重载函数在这里也是一个复制构造函数,而且它直接以 str 的引用作为其参数的作用和 VC 中的使用 str 的副本的引用作为参数的作用一样。所以,g++ 聪明些,直接调用了赋值运算符重载函数。 |