深入C++拷贝构造函数

转自: http://longzxr.blog.sohu.com/215857428.html?qq-pf-to=pcqq.c2c

我最近遇到的一个诡异的现象说起,来看一段代码:
#include
using namespace std;
class test
{
public:
    test(){}
    test(test &t)
    {
        data = t.data;
    }
    test func(test t)
    {
        test m;
        return m;
    }
public:
    double data;
};
int main()
{
    test t1,t2;
    test t3 = t1.func(t2);
    return 0;
}
gcc 编译结果如下:




是不是有点不懂是啥意思,其实大体意思是拷贝构造函数提供的函数参数与所需要的不符合,但是拷贝构造函数的形式参数只能为自身类对象的引用而不能为对象(原因待会后面会阐述),而这里的“.....for call to "test::test(test)"”就不太能理解了,,那好吧,,,我们在做一个实验,,将拷贝构造函数改成
   test(test t)
    {
        data = t.data;
    }
重新编译,结果如下:



哈,意料之中,还是编译通不过,因为拷贝构造函数的函数形式参数不可以为自身对象,最多为引用(原因后述),不过现在也不是一无所获,这次编译直接提示你可能需要的类型为"test (const test&)",那就再改吧,按照提示,如下改成const test& t
   test(const test &t)
    {
        data = t.data;
    }
这次编译成功通过。

 
为了准确的定位问题原因,我们再做一个实验,将整体代码改成如下形式
#include
using namespace std;
class test
{
public:
    test(){}
    test(test &t)
    {
        data = t.data;
    }
    test func(test t)
    {
        test m;
        return m;
    }
public:
    double data;
};
int main()
{
    test t1,t2;
/
    test t3;    
    t1.func(t2);
    return 0;
}
编译通过,这次代码,拷贝构造函数依旧是test &形参,只不过,main函数里调用func的时候我没有用一个对象去接受返回值,这样子就通过了,而之前用一个对象变量去接受函数返回就失败。

好了,现在能够定位错误了,最关键的问题就在于 func函数返回值上,根据上述的实验,函数将对象作为返回值并且赋予外部的另一个变量的时候,是调用了类的拷贝构造函数,而且此时的拷贝构造函数的参数是“const myclass &"形式的"myclass &"都不行,为啥呢? 当时到这一步的时候本人就很困惑,为何此时一定要用const呢???

------------------------------------------华丽的分割----------------------------------------

先回味一下c++拷贝构造函数的一些特性:

调用拷贝构造函数的情形

  在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):
  1) 一个对象作为函数参数,以值传递的方式传入函数体;
  2) 一个对象作为函数返回值,以值传递的方式从函数返回;
  3) 一个对象用于给另外一个对象进行初始化(常称为复制初始化);
  如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。
  通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。原因详见后文。
  拷贝构造函数必须以引用的形式传递(参数为引用值)。其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。


那说到根儿上, 加不加const修饰到底有啥区别呢?接着看下面
B b; (1)
A a=b; (2)
A a=B(); (3)
A a(b); (4)

如果(2)能执行成功的话,那么需要满足什么?
       (2)到底与(4)有什么区别?
       (4)能执行成功的条件与(2)有什么区别?

回答完上述问题, 我顺便还能回答一个问题: 
       A(A& a)与A(const A& a)有什么区别?

下面就来回答这个问题:
首先来分析(2)是如何执行的,分为以下步骤:
1. b能转化为一个A的临时对象;(转化的方法可以是B自身包含一个到A的强制类型转换操作符或者A有一个
未指定explicit的参数为B或者B&或者const B&的构造函数
2. 调用A的拷贝构造函数, 而且必须是A(const A& a), 如果没有const是不行的, 至于原因我一会儿再说。

再说说(3)执行的步骤与条件:
1. B的这个临时对象能转化为一个A的临时对象; (转化的方法可以是B自身包含一个到A的强制类型转换操作符或者A有一个未指定explicit的参数必须为B或者const B&的构造函数
2. 调用A的拷贝构造函数, 而且必须是A(const A& a), 如果没有const是不行的, 至于原因我一会儿再说。

最后说说(4)执行的条件:
A有参数为B或者B&或者const B&的构造函数;
或者是: B自身支持强制类型转化为某种类型, 该类型能恰好为A的某一个构造函数的参数.

按照我上面的分析, 对于(2)和(3)如果B自身不提供转化机制的话, 那么岂不是要调用A的构造函数两次了, 其实最后只调用一次就可以了, 也就是步骤2里调用A的拷贝构造函数不会被调用, 但这只是编译器作了优化, 省掉了一次, 并不代表只执行步骤1, 不信的话你把A拷贝构造函数里的const去掉试试看, 要是能编译过去才怪.

其实这才是关键所在, 理解了这个也就知道本质原因了, 下面我就来说说这个const:
首先要让大家知道A(const A&)和A(A&)是两个函数吧?也就是构造函数的重载, 对于重载的函数参数类型或个数至少得有点不同, 这里我告诉你参数类型加不加const是完全不同的两种参数类型.
知道了上面的概念, 再引用一段话:(出自http://www.xmsc.com.cn/InfoView/Article_122425.html)
说说左值和右值以及临时对象的问题。也许你会说左值应该就是能够改变的变量,右值当然就是不能改变的变量喽!对吗?对了一点点,实际上左值是能够被引用的变量,说的通俗点就是有名字的变量,你一定想到了些什么,对了,临时变量就没有名字,即使有你也不会知道,因为它不是由你创建的,编译器会在内部辨别它,但你并不知道,因此临时变量不是左值,而是右值。你也许还会问,那const变量是不是左值呢?根据定义,它有名字,当然就是左值了。因此左值并非一定“ 可被修改”。但是左值和右值与参数有什么关系吗?我要告诉你的是:有,而且相当密切,因为标准c++规定:若传递给类型为引用的形参的实参是右值的话必须保证形参为const引用。
       现在回到最开头的例子,由于func是将对象作为返回值,所以其调用了拷贝构造函数,又由于,最后返回的其实也是一个拷贝,这个拷贝事实上是临时变量---看到变量的一份拷贝,所以其是右值,所以就要求拷贝构造函数形参为const引用。到此所有困惑都解决了。^_^
其实上面这段话最关键的地方在于指出了左值能被引用右值不能被引用, 临时变量以及没有显示名字的变量都是右值, 因此在(2)或(3)中生成的A的临时变量必定是右值, 而A的构造函数必须为引用类型, 因此按照"若传递给类型为引用的形参的实参是右值的话必须保证形参为const引用"这个断言, A的拷贝构造函数里包含const也就是必须的了.

所以大家要消除一个固有的概念: 拷贝构造函数就得A(const A&)这么写, 完全错误, 其实A(A&)有些情况下是必须有的, 没有都不行, 比如auto_ptr, 因为它在调用拷贝构造函数时, 需要把自己当前维护的那根指针销毁掉(即设为NULL)同时把这根指针交给另一个auto_ptr保管.
下面的代码是我自己实验的一个小例子:
using namespace std;

template 
class A
{
public:
    A(){}

    A(A& a)
    {
        cout<<"A(A& a)"<
    }

    template 
    A(const A& a)
    {
        cout<
        cout<
        cout<<"A(const A&a)"<
    }
};

int main(int argc, char* argv[])
{
    A a_int;
    A a=(a_int);
    return 0;
}

输出结果是:
Ss
i
A(A&a)  //第一次调用A(const A<--->&a),T=string,T2=int,通过A产生A临时对象)
Ss
Ss
A(A&a)  //第二次调用A(const A<--->&a)),T=string,T2=sring,即将第一步产生的string临时对象拷贝过去

如果把A的两个拷贝构造函数都加上const,或者把模板拷贝构造函数的那个const移到非模板拷贝构造函数上, 结果都会为:   
Ss
i
A(A&a)
(?????我的理解是根据前文所述,加了const之后第二次就变成调用这个非模板构造函数了,但为何没有输出函数体内部字符串呢?编译器优化了?)
上述代码是在linux的gcc3.4下通过的, 我相信大家应该可以分析出原因来, 不过对于模板类我还是得罗唆几句, 首先大家应该清楚A和A是完全不同的两个类型, 就像int和string一样风马牛不相及, 只不过它们公用了一些模板代码而已, 因此你把A和A看作两个不同的类型来分析就应该能分析透彻了.



在http://www.xmsc.com.cn/InfoView/Article_122425.html一文中还讲解了auto_ptr中auto_ptr_ref的作用, 说到这里,让我真的很恨auto_ptr源码中的注释啊,因为它的注释有错误,一直误导了我, 现在才体会到“错误的注释还不如没有注释”的真谛了。错误出现在:
       *    auto_ptr  func_returning_auto_ptr(.....);
       *    ...
       *    auto_ptr ptr = func_returning_auto_ptr(.....);
其实应该是:
       *    auto_ptr  func_returning_auto_ptr(.....);
       *    ...
       *    auto_ptr ptr(func_returning_auto_ptr(.....));   or   auto_ptr ptr = func_returning_auto_ptr ;
不要小看这点改动,上面的那种方案是编译不过去的。因为上面的方案里需要转换两次再调用一次拷贝构造函数,这违背了C++最多转换一次的限制。
最后, 或许你在VC下实验的结果会和我说的不一样, 这是因为VC在临时对象这一点上对标准C++的支持不够好,用临时对象作参数的时候不加const也可以编译通过.

附:

还有一种特殊种类的虚拟构造函数――虚拟拷贝构造函数――也有着广泛的用途。虚拟拷贝构造函数能返回一个指针,指向调用该函数的对象的新拷贝。因为这种行为特性,虚拟拷贝构造函数的名字一般都是copySelf,cloneSelf或者是象下面这样就叫做clone。很少会有函数能以这么直接的方式实现它。
class NLComponent {

public:

  // declaration of virtual copy constructor

  virtual NLComponent * clone() const = 0;

  ...

};

class TextBlock: public NLComponent {

public:

  virtual TextBlock * clone() const         // virtual copy

  { return new TextBlock(*this); }          // constructor

  ...

};

class Graphic: public NLComponent {

public:

  virtual Graphic * clone() const            // virtual copy

  { return new Graphic(*this); }             // constructor

  ...

}

作者所谓的virtual Graphic * clone() const   就是虚拟拷贝构造函数。其实这个是广义上的虚拟拷贝构造函数,他符合(1)虚拟,只向谁就是谁。(2)拷贝构造函数,在 clone() 成员函数中,代码 new TextBlock(*this); 调用 TextBlock的拷贝构造函数来复制this的状态到新创建的TextBlock对象

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值