曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 3466|回复: 1
打印 上一主题 下一主题

返回对象及其潜在问题

[复制链接]

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
跳转到指定楼层
楼主
发表于 2011-8-15 11:02:06 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
对象可以作为参数传递给函数,函数也可以返回对象。要返回一个对象,首先要将函数的返回类型声明为一个类,其次用 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 !

所以,在从函数中返回对象时,应该避免上述情况。一种解决方法是返回一个对象指针或对象引用,但并不总是可行,最好的方法是使用复制构造函数。

4917

主题

5879

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34382
沙发
 楼主| 发表于 2011-8-16 12:42:40 | 只看该作者
上面说到,在返回对象时,“在函数中自动产生了一个(对我们而言)不可见的临时对象,它保存了要返回的值”。下面以一个简单的例子,从反汇编代码来观察这个不可见的临时对象。

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() 这样的赋值操作。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2024-5-17 23:17 , Processed in 0.068627 second(s), 22 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表