曲径通幽论坛
标题: 返回对象及其潜在问题 [打印本页]
作者: beyes 时间: 2011-8-15 11:02
标题: 返回对象及其潜在问题
对象可以作为参数传递给函数,函数也可以返回对象。要返回一个对象,首先要将函数的返回类型声明为一个类,其次用 return 语句返回该类的一个对象。
示例程序:
[C++] 纯文本查看 复制代码
#include <iostream>
#include <string>
using namespace std;
class sample {
char buffer[80];
public:
void show() { cout << buffer << "\n"; }
void set (string str) { str.copy(buffer, str.length(), 0); buffer[str.length()] = '\0'; }
};
//该函数返回一个sample类型的对象
sample input()
{
string instr;
sample str;
cout << "Enter a string: ";
getline(cin, instr);
str.set(instr);
return str;
}
int main()
{
sample ob;
//将返回的对象赋给对象 ob
ob = input();
ob.show();
return 0;
}
运行输出:$ ./retobj
Enter a string: hello world
hello world
在函数返回对象时有一点很重要:函数返回对象时,函数会创建一个临时对象来保存要返回的值,而函数所返回的对象实际上是这个临时对象。在对象的值被返回后,临时对象被销毁。这样一来,在某些情况下会带来不可预料的副作用。例如,如果函数所返回的对象的析构函数释放被动态分配的内存,那么即使接受返回对象的对象依然要使用这块内存,内存仍然被释放。看下面的错误程序:
[C++] 纯文本查看 复制代码
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
class sample {
char *s;
public:
sample() { s = 0; }
~sample() { if(s) delete [] s; cout << "Freeing s\n"; }
void show() { cout << s << "\n"; }
void set(char *str);
};
//读入一个字符串
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;
}
该段程序在 linux 下编译运行时会打印出崩溃信息:[beyes@beyes cpp]$ ./passpb2
Enter a string: hello
Freeing s
*** glibc detected *** ./passpb2: double free or corruption (fasttop): 0x09165008 ***
======= Backtrace: =========
/lib/libc.so.6(+0x712b5)[0x7f22b5]
/usr/lib/libstdc++.so.6(_ZdlPv+0x20)[0x5c8b00]
/usr/lib/libstdc++.so.6(_ZdaPv+0x1c)[0x5c8b5c]
./passpb2[0x8048935]
./passpb2[0x8048883]
/lib/libc.so.6(__libc_start_main+0xf3)[0x79a413]
./passpb2[0x80486f1]
======= Memory map: ========
00110000-00138000 r-xp 00000000 fd:01 1966809 /lib/libm-2.14.so
00138000-00139000 r--p 00028000 fd:01 1966809 /lib/libm-2.14.so
00139000-0013a000 rw-p 00029000 fd:01 1966809 /lib/libm-2.14.so
0014d000-0014e000 r-xp 00000000 00:00 0 [vdso]
00475000-00491000 r-xp 00000000 fd:01 1967785 /lib/libgcc_s-4.6.0-20110603.so.1
00491000-00492000 rw-p 0001b000 fd:01 1967785 /lib/libgcc_s-4.6.0-20110603.so.1
004b0000-004cd000 r-xp 00000000 fd:01 1966794 /lib/ld-2.14.so
004cd000-004ce000 r--p 0001d000 fd:01 1966794 /lib/ld-2.14.so
004ce000-004cf000 rw-p 0001e000 fd:01 1966794 /lib/ld-2.14.so
0051a000-005f9000 r-xp 00000000 fd:01 660656 /usr/lib/libstdc++.so.6.0.16
005f9000-005fa000 ---p 000df000 fd:01 660656 /usr/lib/libstdc++.so.6.0.16
005fa000-005fe000 r--p 000df000 fd:01 660656 /usr/lib/libstdc++.so.6.0.16
005fe000-005ff000 rw-p 000e3000 fd:01 660656 /usr/lib/libstdc++.so.6.0.16
005ff000-00606000 rw-p 00000000 00:00 0
00781000-00906000 r-xp 00000000 fd:01 1966801 /lib/libc-2.14.so
00906000-00907000 ---p 00185000 fd:01 1966801 /lib/libc-2.14.so
00907000-00909000 r--p 00185000 fd:01 1966801 /lib/libc-2.14.so
00909000-0090a000 rw-p 00187000 fd:01 1966801 /lib/libc-2.14.so
0090a000-0090d000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 fd:02 1188921 /home/beyes/cpp/passpb2
08049000-0804a000 rw-p 00000000 fd:02 1188921 /home/beyes/cpp/passpb2
09165000-09186000 rw-p 00000000 00:00 0 [heap]
b7849000-b784c000 rw-p 00000000 00:00 0
b7861000-b7865000 rw-p 00000000 00:00 0
bfc33000-bfc54000 rw-p 00000000 00:00 0 [stack]
Aborted (core dumped)
但在 windows7 上使用 vc2010 来编译,运行时命令行窗口会挂掉。
下面分析一下上面的错误:
首先,在局部对象 str 在函数 input() 返回时会调用析构函数,这会释放掉给 s 分配的内存空间。接着,当 input() 返回的临时对象被销毁时(在完成 ob = input() 赋值后)也调用了析构函数,这是因为当对象从函数中返回时,在函数中自动产生了一个(对我们而言)不可见的临时对象,它保存了要返回的值。在这种情况下,临时对象中的值只是对象 str 的一个按位复制的副本。因此,当函数返回后,临时对象的析构函数将被调用。这时候,产生了二次释放,所以在 linux 上运行的程序会提示“./passpb2: double free" 。如果在老的一些系统上,可能不会在这里就产生崩溃,而继续运行到 main() 中的 ob.show() 函数,并打印出一些垃圾数据,最后在 main() 退出时,又再次释放 s ,这样总的就释放了 3 次 s !
所以,在从函数中返回对象时,应该避免上述情况。一种解决方法是返回一个对象指针或对象引用,但并不总是可行,最好的方法是使用复制构造函数。
作者: beyes 时间: 2011-8-16 12:42
上面说到,在返回对象时,“在函数中自动产生了一个(对我们而言)不可见的临时对象,它保存了要返回的值”。下面以一个简单的例子,从反汇编代码来观察这个不可见的临时对象。
C++ 代码如下:
[C++] 纯文本查看 复制代码
#include <iostream>
using namespace std;
class myclass {
int a;
int b;
public:
void setab(int i, int j) { a = i; b = j; }
};
myclass input()
{
myclass temp;
temp.setab(11, 14);
return temp;
}
int main()
{
myclass ob;
ob = input();
return 0;
}
使用 g++ 的 -S 选项生成反汇编代码。由于及时这么一小点程序,反汇编代码也很多,下面只拿出关键部分来说明。
main() 函数部分:
[Plain Text] 纯文本查看 复制代码
.LCFI8:
movl %esp, %ebp
.LCFI9:
pushl %ecx
.LCFI10:
subl $36, %esp #开辟局部变量空间
.LCFI11:
leal -32(%ebp), %eax
movl %eax, (%esp)
call _Z5inputv #调用 input() 函数
subl $4, %esp
movl -32(%ebp), %eax #-32(%ebp) 和 -28(%ebp) 就是副本中a 和 b 两个变量的地址
movl -28(%ebp), %edx
movl %eax, -16(%ebp) #对 main() 里的对象 ob 中的成员 a 和 b 赋值
movl %edx, -12(%ebp)
movl $0, %eax
movl -4(%ebp), %ecx
leave
leal -4(%ecx), %esp
ret
input() 函数代码:
[Plain Text] 纯文本查看 复制代码
_Z5inputv:
.LFB1405:
pushl %ebp
.LCFI2:
movl %esp, %ebp
.LCFI3:
pushl %ebx
.LCFI4:
subl $36, %esp #开辟 input() 函数的栈帧
.LCFI5:
movl 8(%ebp), %ebx #%ebx 里存放的就是“临时对象”的首地址
movl $14, 8(%esp) #setab() 参数入栈(两个整数 11 和 14 )
movl $11, 4(%esp)
movl %ebx, (%esp)
call _ZN7myclass5setabEii #该函数就是 setab()
movl %ebx, %eax # temp 对象的副本地址通过 %eax 来返回,但此时副本已经创建完成,故而在返回到main()中时不会用到这个%eax
addl $36, %esp
popl %ebx
popl %ebp
ret $4
_ZN7myclass5setabEii 对应的是 setab() 函数,代码如下:
[Plain Text] 纯文本查看 复制代码
_ZN7myclass5setabEii:
.LFB1404:
pushl %ebp
.LCFI0:
movl %esp, %ebp
.LCFI1:
movl 8(%ebp), %edx # 8(%ebp) 中存放 temp 副本中 a 的地址,该地址位于 main() 栈帧
movl 12(%ebp), %eax #从 input() 函数的栈帧里取得参数,这里是11
movl %eax, (%edx)
movl 8(%ebp), %edx
movl 16(%ebp), %eax #从 input() 函数的栈帧里取得参数,这里是14
movl %eax, 4(%edx) #4(%edx) 中存放 temp 副本中 b 的地址,该地址位于 main() 栈帧
popl %ebp
ret
在 input() 函数里,由于最后会返回 temp 这个对象,所以在调用 setab() 进行设置 temp 时,temp 对象中的 a 和 b 的值并没有直接保存在 input() 函数的栈帧里,而是放在 main() 的栈帧里,也就是说,temp 对象的 a 和 b 和 副本的 a 和 b 共用了 main() 栈帧中的同一块内存区域里的值。这样,当 input() 函数返回时,在销毁自身时,它的栈帧也会被弹出,但是由于副本是存放在 main() 里的,因此不会被销毁,这样当回到 main() 里时,才能实现 ob = input() 这样的赋值操作。
欢迎光临 曲径通幽论坛 (http://www.groad.net/bbs/) |
Powered by Discuz! X3.2 |