曲径通幽论坛

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

将对象直接传递给函数及存在的潜在问题

[复制链接]

4918

主题

5880

帖子

3万

积分

GROAD

曲径通幽,安觅芳踪。

Rank: 6Rank: 6

积分
34387
跳转到指定楼层
楼主
发表于 2011-8-14 23:50:00 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
和其它类型的数据一样,对象也可以传递到函数中,但这也是值传递,而非传递对象本身,传递的是对象的一个副本。在函数中修改对象的值不会影响作为实际参数传递给函数的对象。下面函数说明了这一点:

[C++] 纯文本查看 复制代码
#include <iostream>
using namespace std;


class OBJ {
    int i;
public:
    void set_i(int x) { i = x; }
    void out_i() { cout << i << "\n"; }
};


void f(OBJ x)
{
    x.out_i();
    x.set_i(100);   //修改的只是对象 o 的副本
    x.out_i();
}


int main()
{
    OBJ o;
    o.set_i(10);
    f(o);    //传递对象 o 到函数 f() 中
    o.out_i();


    return 0;
}

运行输出:
$ ./passobj
10
100
10

上面例子的类中不存在构造函数与析构函数。下面看看当构造函数与析构函数存在时,会发生什么事情:
[C++] 纯文本查看 复制代码
#include <iostream>
using namespace std;

class myclass {
    int val;
public:
    myclass(int i) { val = i; cout << "Constructing\n"; }
    ~myclass() { cout << "Destructing\n"; }
    int getval() { return val; }
};
void display (myclass ob)
{
    cout << ob.getval() << "\n";
}

int main()
{
    myclass a(10);

    display(a);

    return 0;
}

运行输出:
$ ./passproblem
Constructing
10
Destructing
Destructing
从输出可以看出,只调用了 1 次构造函数,却调用了 2 次析构函数。被调用的构造函数显然是在 main() 中创建 a 对象时所调用。那在传递 a 到函数 display() 中有没有调用构造函数呢?

在调用函数 display() 并传递 a 给它时,程序创建了一个 a 对象的副本,此时正常的构造函数并没有被调用,但是调用了一个 “复制构造函数”,默认情况下,这个“复制构造函数” 的调用是隐式的。

复制构造函数” 定义了如何创建一个对象的副本 --- 当类中没有显示的定义 “复制构造函数” 的话,那么 C++ 就为其提供一个默认的 “复制构造函数” 。默认的 “复制构造函数” 将以按位复制的形式创建一个对象的副本。

有了 “复制构造函数” 这个概念,就容易理解为什么在传递对象参数时,没有去调用普通的构造函数了。这是因为:普通的构造函数通常是用来初始化对象的某些成员。所以,在一个对象被创建后,可能已经改动了这些被初始化过的成员。此时,如果我们在传递这个对象到另外的函数中时再调用一下构造函数,那么我们看到的又是被初始化的成员 --- 然而,实际上,我们在传递一个对象到函数中时,我们希望得到的是该对象的当前状态,而不是初始状态

但是,当函数结束时,由于被传递进来的对象副本相当于一个局部变量,它的作用范围仅限于调用函数中,所以在函数结束时,该副本被销毁,从而调用了析构函数。这样就是上面的程序输出中为什么会出现 2 个析构函数中的输出 --- 一个输出是在 display() 函数结束时,另一个是在 main() 函数结束时。

总结:当创建一个对象的副本作为函数的参数时,普通的构造函数没有被调用,所调用的构造函数是按位复制的默认“复制构造函数”。但是,当副本被销毁时,析构函数会被调用。

然而,在使用默认的“复制构造函数”时会存在潜在问题。假设有这么一种情况:
当我们在用作实际参数的对象中分配了动态内存(比如上面的 a 对象中用 malloc() 分配了一段动态内存),并且在销毁时释放内存,那么函数中实际参数的副本在调用析构函数时也将释放同样的内存。这时就会出现问题了,这样造成了原始的实际参数对象仍被用的那块内存被副本给释放了,这会破坏了原始对象而且使得对象变得不可用。下面程序演示这种情况:
[C++] 纯文本查看 复制代码
#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

class myclass {
    char *p;
public:
    myclass(const char *str);
    ~myclass();
    int show() { cout << p << "\n"; }
};

myclass::myclass(const char *str)
{
    cout << "Allocating p\n";
    p = (char *)malloc (100);
    strcpy(p, str);
}

myclass::~myclass()
{
    cout << "Freeing p\n";
    free (p);
}

void display(myclass ob)
{
    cout << ob.show() << "\n";
}

int main()
{
    myclass a("hello world");

    display(a);

    return 0;
}

运行输出:
[beyes@beyes   cpp]$ ./passpb
Allocating p
hello world
134520064
Freeing p
Freeing p
*** glibc detected *** ./passpb: double free or corruption (top): 0x09b9f008 ***
======= Backtrace: =========
/lib/libc.so.6(+0x712b5)[0xd3d2b5]
./passpb[0x804875f]
./passpb[0x80487e3]
/lib/libc.so.6(__libc_start_main+0xf3)[0xce5413]
./passpb[0x8048661]
======= 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
001d6000-001f3000 r-xp 00000000 fd:01 1966794    /lib/ld-2.14.so
001f3000-001f4000 r--p 0001d000 fd:01 1966794    /lib/ld-2.14.so
001f4000-001f5000 rw-p 0001e000 fd:01 1966794    /lib/ld-2.14.so
00276000-00277000 r-xp 00000000 00:00 0          [vdso]
0076e000-0084d000 r-xp 00000000 fd:01 660656     /usr/lib/libstdc++.so.6.0.16
0084d000-0084e000 ---p 000df000 fd:01 660656     /usr/lib/libstdc++.so.6.0.16
0084e000-00852000 r--p 000df000 fd:01 660656     /usr/lib/libstdc++.so.6.0.16
00852000-00853000 rw-p 000e3000 fd:01 660656     /usr/lib/libstdc++.so.6.0.16
00853000-0085a000 rw-p 00000000 00:00 0
00b6c000-00b88000 r-xp 00000000 fd:01 1967785    /lib/libgcc_s-4.6.0-20110603.so.1
00b88000-00b89000 rw-p 0001b000 fd:01 1967785    /lib/libgcc_s-4.6.0-20110603.so.1
00ccc000-00e51000 r-xp 00000000 fd:01 1966801    /lib/libc-2.14.so
00e51000-00e52000 ---p 00185000 fd:01 1966801    /lib/libc-2.14.so
00e52000-00e54000 r--p 00185000 fd:01 1966801    /lib/libc-2.14.so
00e54000-00e55000 rw-p 00187000 fd:01 1966801    /lib/libc-2.14.so
00e55000-00e58000 rw-p 00000000 00:00 0
08048000-08049000 r-xp 00000000 fd:02 1189533    /home/beyes/cpp/passpb
08049000-0804a000 rw-p 00000000 fd:02 1189533    /home/beyes/cpp/passpb
09b9f000-09bc0000 rw-p 00000000 00:00 0          [heap]
b77be000-b77c1000 rw-p 00000000 00:00 0
b77d7000-b77da000 rw-p 00000000 00:00 0
bf958000-bf979000 rw-p 00000000 00:00 0          [stack]
Aborted (core dumped)
由输出可以看到,程序发生了致命错误,崩溃了。下面分析一下原因:
在 main() 中的对象 a 被初始化时,程序为指针 a.p 分配了内存。当对象 a 作为参数产地给函数 display() 时,a 的值就被赋给了形式参数 ob。也就是说,在对象 a 和 ob 中,p 的值是一样的,指针 p 指向的是同一块动态分配的内存。当 display() 函数结束时,局部对象 ob 被销毁,它的析构函数被调用,然而析构函数所释放的 ob.p 所指向的内存仍然被 a.p 使用!当程序结束时,a.p 指向的内存再次被释放!对同一块动态内存释放两次在 C++ 中是未定义的操作,这个操作的定义依赖于实现动态内存分配系统的方式,因此这将产生严重的错误!

既然形式参数的析构函数销毁了实际的参数需要的数据,那么避开这个问题的方法之一就是传递一个对象指针或者对象引用,而不是对象的副本,这样在函数返回时将不会调用析构函数。一般情况下,通过引用来传递对象是最好的方法,但也并不能适用于所有情况。一种更为常用的解决方法就是自定义“复制构造函数”。这种方法可以精确地定义如何创建一个对象的副本,从而避免上述问题。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-5-21 10:43 , Processed in 0.067586 second(s), 23 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

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