C++引用和拷贝构造函数(References & the Copy-Constructor)

本文探讨了C++中引用和拷贝构造函数的区别和使用场景,通过实例展示了不同参数传递方式对对象构造和析构的影响,强调了指针和引用的使用注意事项,以及拷贝构造函数在解决对象复制问题中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

         在Thiking in C++中,专门有一章来讲解References & the Copy-Constructor这个主题,讲解得比较透彻。根据英文的Copy-Constructor,“拷贝构造函数”是一个名词,而不是动宾结构的短语。我用我的方法对这一问题琢磨了一下,在本文的最后有完整的代码。
1. Pass-By-Reference
        在C++里可以使用引用(reference)&符号来传递参数,被称为pass-by-reference,其效果与通过指针引用(pass-by-pointer)是一样的,都能够在函数体内修改外部变量的值。例如:

#include <iostream>
using namespace std;

void test1(int & a) { a += 1; }

void test2(int * a) { *a += 1;}

int main(void) {
    int a = 1;
    cout << a << endl;
    test1(a);
    cout << a << endl;
    test2(&a);
    cout << a << endl;
    return (0);
}
test1和test2都能够起到把a的值增1的作用。我在IA32的CPU和WinXP Sp2上用VC7编译这段代码,并把这段在 VC中反汇编。不熟悉IA32汇编语言的朋友可以忽略汇编代码而直接看我的结论,使用Linux的朋友可以用gcc和gdb来做类似的工作。忽略掉与我们这个主题无关的代码后能够看到:

void test1(int & a) {
0041B1A0  push        ebp 
0041B1A1  mov         ebp,esp
0041B1A3  sub         esp,0C0h
0041B1A9  push        ebx 
0041B1AA  push        esi 
0041B1AB  push        edi 
0041B1AC  lea         edi,[ebp-0C0h]
0041B1B2  mov         ecx,30h
0041B1B7  mov         eax,0CCCCCCCCh
0041B1BC  rep stos    dword ptr [edi]
    a += 1;
0041B1BE  mov         eax,dword ptr [a]
0041B1C1  mov         ecx,dword ptr [eax]
0041B1C3  add         ecx,1
0041B1C6  mov         edx,dword ptr [a]
0041B1C9  mov         dword ptr [edx],ecx
}
0041B1CB  pop         edi 
0041B1CC  pop         esi 
0041B1CD  pop         ebx 
0041B1CE  mov         esp,ebp
0041B1D0  pop         ebp 
0041B1D1  ret

void test2(int * a) {
0041B1E0  push        ebp 
0041B1E1  mov         ebp,esp
0041B1E3  sub         esp,0C0h
0041B1E9  push        ebx 
0041B1EA  push        esi 
0041B1EB  push        edi 
0041B1EC  lea         edi,[ebp-0C0h]
0041B1F2  mov         ecx,30h
0041B1F7  mov         eax,0CCCCCCCCh
0041B1FC  rep stos    dword ptr [edi]
    *a += 1;
0041B1FE  mov         eax,dword ptr [a]
0041B201  mov         ecx,dword ptr [eax]
0041B203  add         ecx,1
0041B206  mov         edx,dword ptr [a]
0041B209  mov         dword ptr [edx],ecx
}
0041B20B  pop         edi 
0041B20C  pop         esi 
0041B20D  pop         ebx 
0041B20E  mov         esp,ebp
0041B210  pop         ebp 
0041B211  ret

int main(void) {
......

    test1(a);
0041B25F  lea         eax,[a]
0041B262  push        eax 
0041B263  call        test1 (41991Ah)
0041B268  add         esp,4

......

    test2(&a);
0041B285  lea         eax,[a]
0041B288  push        eax 
0041B289  call        test2 (41916Dh)
0041B28E  add         esp,4

......
}
即使不熟悉汇编代码(这段代码是很简单的),也可以看出来无论是函数体还是调用的过程,两者是完全一样的。但是在书写上test1(a)比test2(&a)更自然,而test2(&a)则更加明显地表明:传递了一个指针。可以说在写法上两者各有千秋。我不能完全保证这两种方法的一致程度,只是在我这个例子中是这样的。也许根据不同的编译器、或者编译选项的会有不太一样的解释,但至少说明这二者是非常相似的。
 
2. Copy-Constructor
    先看一个简单的例子:

static int g = 0;
static char * stone = (char*)"stone";

class Y {
public:
    int m_i;
    char * s;
    Y() : m_i() {
        m_i = ++g;
        s = stone;
        cout << "create, this pointer is 0x" << this
             << ", s is 0x" << hex << (unsigned)s
             << ", m_i is " << m_i
             << endl;
    }

    ~Y() {
        cout << "destroy, this pointer is 0x" << this
             << ", s is 0x" << hex << (unsigned)s
             << ", m_i is " << m_i
             << endl;
    }
};

Y pass_by_value(Y a) { return a; }
Y pass_by_reference(Y& a) { return a; }
Y * pass_by_pointer(Y * a) { return a; }

int main(void)
{
    Y a;
    Y b = pass_by_value(a);
    Y c = pass_by_reference(a);
    Y * d = pass_by_pointer(&a);
    return (0);
}

我们注意到三个不同的函数传递参数的方法是不同的:
    Y pass_by_value(Y a) { return a; }
    Y pass_by_reference(Y& a) { return a; }
    Y * pass_by_pointer(Y * a) { return a; }

这段代码能够运行的,但是仔细看输出的结果,却非常令人吃惊:

create, this pointer is 0x0012FEC4, s is 0x371360, m_i is 1
destroy, this pointer is 0x0012FDB4, s is 0x371360, m_i is 1
destroy, this pointer is 0x0012FEA4, s is 0x371360, m_i is 1
destroy, this pointer is 0x0012FEB4, s is 0x371360, m_i is 1
destroy, this pointer is 0x0012FEC4, s is 0x371360, m_i is 1


因为构造函数只被调用了一次,而析构函数被调用了四次。经过反汇编后(细节不列出来了),能够找到四次这行代码:
             call        Y::~Y
其中在pass_by_value中,退出该函数之前调用一次,另外三次在退出main函数的时候,按照c, b, a的顺序分别调用了三次,由于d是Y*,不会调用析构函数。

    从反汇编的代码中,我们可以看到,在调用pass_by_value之前,系统先把a的内容复制了一份到新的地址,而不是调用constructor产生一个新的Y的实例,而在退出该函数的时候,按照这个新的地址进行deconstructe。

    在pass_by_reference调用之前,把a的地址&a压栈然后调用函数,但是在退出这个函数的时候,反汇编的代码中显示,先把a的内容复制一份到c的地址,而没有call constructor产生新的实例.

    我们稍微想象一下,在Y的析构函数Y()中给char * s分配内存,也就是增加
             s = new char [100];
而在~Y()中释放内存:
             delete s;
这样,我们就制造了很大的麻烦!

    因为在第一次从pass_by_value返回的时候,我们就已经调用了~Y(),并把s释放了,而所有的s的地址都是相同的,再释放它或者访问它其中的内容都是非法的。

    不执行pass_by_value这个函数会不会解决这个问题呢?不会,pass_bye_reference也存在问题,因为在退出这个函数的时候,同样复制了一份a的内容给c,而在退出main的时候需要析构c和a,在这时c.s和a.s的指针依然是相同的。

    解决问题的方法之一:使用指针传递参数。例如我们在pass_by_pointer中new一个新的Y, 而在main中delete, 如下代码:

Y * pass_by_pointer(Y * a)
{
    //TO DO somthing

    Y * n = new Y;
    return n;
}

int main(void)
{
    Y a;
    Y * d = pass_by_pointer(&a);
    delete d; //Do not forget
    return (0);
}

这显然是解决问题的办法。


    解决办法二:采用copy-constructor
    Copy-Constructor就是形如Y(Y& y)的构造函数,看起来这有点不容易理解,但实际上这个形式是为了解决上面的问题而提出来的。当遇到形如:
     f1(Y a)
     f2(Y& a)
的函数,编译的时候会生成的调用默认调用Y(Y& y)这个构造函数。代码如下:

    Y(const Y& y) {
        m_i = ++g;
        s = new char[100];
        cout << "create , this pointer is 0x" << this
             << ", s is 0x" << hex << (unsigned)s
             << ", m_i is " << m_i
             << endl;
    }

函数体里的代码与Y()函数的代码是完全相同的。输出的结果如下:

create , this pointer is 0x0012FEC4, s is 0x371360, m_i is 1
create , this pointer is 0x0012FDA0, s is 0x371588, m_i is 2
create , this pointer is 0x0012FEB4, s is 0x371618, m_i is 3
destroy, this pointer is 0x0012FDA0, s is 0x371588, m_i is 2
create , this pointer is 0x0012FEA4, s is 0x371588, m_i is 4
destroy, this pointer is 0x0012FEA4, s is 0x371588, m_i is 4
destroy, this pointer is 0x0012FEB4, s is 0x371618, m_i is 3
destroy, this pointer is 0x0012FEC4, s is 0x371360, m_i is 1

构造函数和析构函数分别调用了四次,使用反汇编跟踪,或者在代码中打印更多的信息(这要写一些代码在程序里),我们都可以看到以下的顺序:
    1) 第一次调用Y(),显然是在Y a; 
    2) 然后在调用pass_by_value之前调用了Y(const Y&),生成新实例
    3) 退出pass_by_value之前,调用Y(const Y&)给main函数中的b
    4) 再调用~Y()析构在第2步生成的实例, 然后退出pass_by_value
    5) 调用pass_by_refrence,简单地传递了&a
    6) 退出pass_by_refrence之前,调用Y(const Y&)给main函数中的c
    7) 调用pass_by_pointer,这个比较简单
    8)顺序调用~Y(),析构c, b, a

    其中的效率问题和代码安全问题就不用我再多说了,一琢磨就清楚了。

3. 与此有关系的有趣的话题

1)如果写成这样的函数会如何呢?
Y& pass_by_reference(Y& a) { return a; }

在反汇编之后,清楚地看到,前面的写法是在pass_by_reference函数内部调用Y(const Y&),而现在的这种写法如果仍然采用
         Y c = pass_by_reference(a)
的方式,则编译器在退出pass_by_reference之后调用Y(const Y&)构造函数,对于本例来说更好一点的方法是——而且仅对本例
         Y& c = pass_by_reference(a),
这条语句不会再调用任何的构造函数,只是引用变量,与
         Y * d = pass_by_poiinter(a)
有一样的效果,本质上都采用了指针的方式。

    但是:
     Y& pass_by_reference(Y& a) { Y y ; return y; }
至少编译器会发出警告,“warning C4172: 返回局部变量或临时变量的地址”。
所以,不用认为使用&就一定是安全的,&比*也好不到哪去,使用Y&和Y*这样的返回值要非常地仔细,一定要注意的是变量的生命周期,在函数内部的局部变量不能够在外部访问。还有不要忘记delete也不要乱用delete,这些是通用的编程原则。

2) Y(const Y& y)的写法:
    const不是必须的,但最好写成带const限制的,这样会明确地告诉编译器你不想对y的地址本身做任何事情。
    Y(Y y)是不可以的,编译器规定这是不合法的声明。我们可以想象编译器在调用Y(Y y)之前,先要调用一个Copy-Constructor,那么是不是要引起无穷的递归了?所以编译器干脆就不接受这样的构造函数声明。
    Y(Y * y)是合法的写法,但是编译器不认为这是一个Copy-Constructor,所以根本不会被默认调用,跟没有一样。
    另外我们注意到,Y(const Y& y)函数体中的代码与Y()中的代码是完全相同的,那么干脆就这样写:
    Y(const Y& y) { Y(); }
行不行啊?这个问题就留给聪明的你吧,相信这是一个很有趣的问题。是不是很YY啊?

以下是完整的代码:

 

#include <iostream>
using namespace std;

static int g = 0;
static char * stone = (char*)"stone";

class Y {
public:
    int m_i;
    char * s;
    Y() : m_i() {
        m_i = ++g;
        s = new char[100];
        cout << "create , this pointer is 0x" << this
             << ", s is 0x" << hex << (unsigned)s
             << ", m_i is " << m_i
             << endl;
    }

    Y(Y y) {
        m_i = ++g;
        s = new char[100];
        cout << "create , this pointer is 0x" << this
             << ", s is 0x" << hex << (unsigned)s
             << ", m_i is " << m_i
             << endl;
    }

    ~Y() {
        cout << "destroy, this pointer is 0x" << this
             << ", s is 0x" << hex << (unsigned)s
             << ", m_i is " << m_i
             << endl;
        delete s;
    }
};

Y pass_by_value(Y a) { return a; }
Y pass_by_reference(Y& a) { return a; }
Y * pass_bye_pointer(Y * a) { return a; }

int main(void)
{
    Y a;
    Y b = pass_by_value(a);
    Y c = pass_by_reference(a);
    Y * d = pass_bye_pointer(&a);

    return (0);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值