C/C++编程:拷贝构造函数

1060 篇文章 292 订阅

什么是拷贝构造函数

首先对于普通类型的对象来说,它们之间的复制是很简单的,例如:

int a=100;
int b=a;

而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。
下面看一个类对象拷贝的简单例子。

  #include<iostream>
using namespace std;
class CExample
{
private:
    int a;
public:
    //构造函数
    CExample(int b)
    {
        a=b;
        printf("constructor is called\n");
    }
    //拷贝构造函数
    CExample(const CExample & c)
    {
        a=c.a;
        printf("copy constructor is called\n");
    }
    //析构函数
    ~CExample()
    {
        cout<<"destructor is called\n";
    }
    void Show()
    {
        cout<<a<<endl;
    }
};
int main()
{
    CExample A(100);
    CExample B=A;
    B.Show(); 
    return 0;
}

运行程序,屏幕输出100。从以上代码的运行结果可以看出,系统为对象 B 分配了内存并完成了与对象 A 的复制过程。就类对象而言,相同类型的类对象是通过拷贝构造函数来完成整个复制过程的。

CExample(const CExample& C) 就是我们自定义的拷贝构造函数。可见,拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量

拷贝构造函数的调用时机

  • 当函数的参数为类的对象时
#include<iostream>
using namespace std;
class CExample
{
private:
    int a;
public:
    CExample(int b)
    {
        a=b;
        printf("constructor is called\n");
    }
    CExample(const CExample & c)
    {
        a=c.a;
        printf("copy constructor is called\n");
    }
    ~CExample()
    {
     cout<<"destructor is called\n";
    }
    void Show()
    {
     cout<<a<<endl;
    }
};
void g_fun(CExample c)
{
    cout<<"g_func"<<endl;
}
int main()
{
    CExample A(100); // 调用析构函数
    CExample B=A; // 调用:拷贝构造函数
    g_fun(A);  // 调用拷贝构造函数
    return 0;
}
  • 函数的返回值是类的对象
#include<iostream>
using namespace std;
class CExample
{
private:
    int a;
public:
    //构造函数
    CExample(int b)
    {
        a=b;
        printf("constructor is called\n");
    }
    //拷贝构造函数
    CExample(const CExample & c)
    {
        a=c.a;
        printf("copy constructor is called\n");
    }
    //析构函数
    ~CExample()
    {
        cout<<"destructor is called\n";
    }
    void Show()
    {
        cout<<a<<endl;
    }
};
CExample g_fun()
{
    CExample temp(0);  // constructor is called
    return temp;
}
int main()
{
    g_fun();
    CExample t = g_fun();

    getchar();
    return 0;
}

总结:

凡在对象从同类型的另一对象(以直接初始化或者复制初始化)初始化时,调用复制构造函数(除非重载决议选择了更好的匹配或其调用被消除),情况包括

  • 初始化:T a = b; 或 T a(b);,其中 b 类型为 T;
  • 函数实参传递:f(a);,其中 a 类型为 T 而 f 为 Ret f(T t);
  • 函数返回:在如 T f() 这样的函数内部的 return a;,其中 a 类型为 T,它没有移动构造函数。

浅拷贝与深拷贝

默认拷贝构造函数

很多时候在我们都不知道拷贝构造函数的情况下,传递对象给函数参数或者函数返回对象都能很好的进行,这是因为编译器会给我们自动产生一个拷贝构造函数,这就是“默认拷贝构造函数”,这个构造函数很简单,仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

Rect::Rect(const Rect& r)
{
    width=r.width;
    height=r.height;
}

当然,以上代码不用我们编写,编译器会为我们自动生成。但是如果认为这样就可以解决对象的复制问题,那就错了,让我们来考虑以下一段代码:

#include<iostream>
using namespace std;
class Rect
{
public:
    Rect()
    {
     count++;
    }
    ~Rect()
    {
     count--;
    }
    static int getCount()
    {
     return count;
    }
private:
    int width;
    int height;
    static int count;
};
int Rect::count=0;
int main()
{
    Rect rect1;
    cout<<"The count of Rect:"<<Rect::getCount()<<endl;
    Rect rect2(rect1);
    cout<<"The count of Rect:"<<Rect::getCount()<<endl;
    return 0;
}
  

这段代码对前面的类,加入了一个静态成员,目的是进行计数。在主函数中,首先创建对象rect1,输出此时的对象个数,然后使用rect1复制出对象rect2,再输出此时的对象个数,按照理解,此时应该有两个对象存在,但实际程序运行时,输出的都是1,反应出只有1个对象。此外,在销毁对象时,由于会调用销毁两个对象,类的析构函数会调用两次,此时的计数器将变为负数。

说白了,就是拷贝构造函数没有处理静态数据成员

出现这些问题最根本就在于在复制对象时,计数器没有递增,我们重新编写拷贝构造函数,如下:

#include<iostream>
using namespace std;
class Rect
{
public:
    Rect()
    {
        count++;
    }
    Rect(const Rect& r)
    {
        width=r.width;
        height=r.height;
        count++;
    }
    ~Rect()
    {
        count--;
    }
    static int getCount()
    {
        return count;
    }
private:
    int width;
    int height;
    static int count;
};
int Rect::count=0;
int main()
{
    Rect rect1;
    cout<<"The count of Rect:"<<Rect::getCount()<<endl;
    Rect rect2(rect1);
    cout<<"The count of Rect:"<<Rect::getCount()<<endl;
    return 0;
}

浅拷贝

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单的赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出问题了,让我们考虑如下一段代码

 #include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:
    Rect()
    {
     p=new int(100);
    }
   
    ~Rect()
    {
     assert(p!=NULL);
        delete p;
    }
private:
    int width;
    int height;
    int *p;
};
int main()
{
    Rect rect1;
    Rect rect2(rect1);
    return 0;
}

在这段代码运行结束之前,会出现一个运行错误。原因就在于在进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:

  • 在运行定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况大致如下: 在使用rect1复制rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时 rect1.p = rect2.p,也即这两个指针指向了堆里的同一个空间,如下图所示:

  • 当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

深拷贝

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重新动态分配空间,如上面的例子就应该按照如下的方式进行处理:

#include<iostream>
#include<assert.h>
using namespace std;
class Rect
{
public:
    Rect()
    {
     p=new int(100);
    }
    
    Rect(const Rect& r)
    {
     width=r.width;
        height=r.height;
     p=new int(100);
        *p=*(r.p);
    }
     
    ~Rect()
    {
     assert(p!=NULL);
        delete p;
    }
private:
    int width;
    int height;
    int *p;
};
int main()
{
    Rect rect1;
    Rect rect2(rect1);
    return 0;
}

此时,在完成对象的复制后,内存的一个大致情况如下:

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

防止默认拷贝发生

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

//防止按值传递
class CExample 
{ 
private: 
    int a; 
  
public: 
    //构造函数
    CExample(int b) 
    { 
        a = b; 
        cout<<"creat: "<<a<<endl; 
    } 
  
private: 
    //拷贝构造函数,只是声明
    CExample(const CExample& C); 
  
public: 
    ~CExample() 
    { 
        cout<< "delete: "<<a<<endl; 
    } 
  
    void Show () 
    { 
        cout<<a<<endl; 
    } 
}; 
  
//???? 
void g_Fun(CExample C) 
{ 
    cout<<"test"<<endl; 
} 
  
int main() 
{ 
    CExample test(1); 
    //g_Fun(test);   //按值传递将出错
      
    return 0; 
}

小结:拷贝有两种:深拷贝,浅拷贝。

  • 当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。

  • 当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。

  • 所以,这时,必须采用深拷贝。

    深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。

在C++11之后,可以通过将复制构造函数定义为已删除,阻止复制对象:

 Box (const Box& other) = delete;

尝试复制对象会生成错误 C2280:正在尝试引用已删除的函数。

细节

以下函数哪个是拷贝构造函数,为什么?

X::X(const X&);   //拷贝构造函数
X::X(X); 
X::X(X&, int a=1);   //拷贝构造函数
X::X(X&, int a=1, int b=2);  //拷贝构造函数

解答:对于一个类X, 如果一个构造函数的第一个参数是下列之一:

  • X&
  • const X&
  • volatile X&
  • const volatile X&

且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

总结:构造函数的语法如下:

类名 ( const 类名 & )(1)
类名 ( const 类名 & ) = default;(2)
名 ( const 类名 & ) = delete;(3)

(1)、复制构造函数的典型声明
(2)、强制编译器生成复制构造函数
(3)、阻止隐式生产复制构造函数

类 T 的复制构造函数是非模板构造函数,其首个形参为T&、const T&、volatile T& 或 const volatile T&,而且要么没有其他形参,要么剩余形参均有默认值。。也就是说,复制函数可能有以下其中一个签名:

    Box(Box& other); // 如果可能,避免-允许修改其他。
    Box(const Box& other);
    Box(volatile Box& other);
    Box(volatile const Box& other);

    //dditional parameters OK if they have default values
    Box(Box& other, int i = 42, string label = "Box");

定义复制构造函数时,还应将复制赋值运算符定义 (=)

为什么拷贝构造函数必须是引用传递,不能是值传递?

拷贝构造函数的标准写法如下:

class Base
{
public:
  Base(){}
  Base(const Base &b){..}
  //
}

如果写成值传递

class Base
{
public:
  Base(){}
  Base(const Base b){}
  //
}

编译出错:error C2652: ‘Base’ : illegal copy constructor: first parameter must not be a ‘Base’

为什么呢?

  1. 拷贝构造函数的作用是就是用来复制对象的,在使用这个对象的实例来初始化这个对象的一个新的实例
  2. 参数传递过程
  • 值传递:
    • 对于内置数据类型的传递,直接复制拷贝给形参(形参是函数内局部变量)
    • 对于类类型的传递,首先调用的是该类的拷贝构造函数来初始化形参。
    • 因此,如果拷贝构造函数使用值传递会产生无限递归调用
  • 地址传递:
    • 无论对内置类型还是类类型,传递引用或指针最终都是传递的地址值!而地址总是指针类型(属于简单类型), 显然参数传递时,按简单类型的赋值拷贝,而不会有拷贝构造函数的调用(对于类类型).

如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

为什么必须是const引用呢?

  • 拷贝函数的目的是用其他对象的数据来初始化当前对象,并没有期望更改其他对象的数据,添加const限制后,这个含义更明确了
  • 另外一个原因是,添加 const 限制后,可以将 const 对象和非 const 对象传递给形参了,因为非 const 类型可以转换为 const 类型。如果没有 const 限制,就不能将 const 对象传递给形参,因为 const 类型不能转换为非 const 类型,这就意味着,不能使用 const 对象来初始化当前对象了。

在类中有指针数据成员时,拷贝构造函数的使用?

如果不显式声明拷贝构造函数,编译器会默认生成一个默认拷贝构造函数,在一般情况下是没有问题的。但是一旦遇到类有指针数据成员是就会出现问题:

  • 因为默认的拷贝构造函数是按成员拷贝构造,这导致了两个不同的指针(如ptr1=ptr2)指向了相同的内存。
  • 当一个实例销毁时,调用析构函数 free(ptr1)释放了这段内存,那么剩下的一个实例的指针ptr2就无效了,在被销毁的时候free(ptr2)就会出现错误了, 这相当于重复释放一块内存两次。
  • 这种情况必须显式声明并实现自己的拷贝构造函数,来为新的实例的指针分配新的内存。

拷贝构造函数里能调用参数的private成员变量吗?

可以。

class CExample
{
public:
	CExample(){pBuffer=NULL; nSize=0;}
	~CExample(){delete pBuffer;}
	CExample(const CExample&); 
void Init(int n){ pBuffer=new char[n]; nSize=n;}
private:
	char *pBuffer; 
	int nSize;
};
CExample::CExample(const CExample& RightSides) 
{
	nSize=RightSides.nSize; //!!!!!!请注意这句话!!!!!!
	pBuffer=new char[nSize];
	memcpy(pBuffer,RightSides.pBuffer,nSize*sizeof(char));
}

原因:

  • 访问限制符号是针对类而不是针对一个类的不同对象,只要同属一个类就可以不用区分另一个类的不同对象。因为CExample(const CExample& RightSides) 是类的成员函数,所以有权限访问私有数据成员。如果是在main函数中直接RightSides.nSize,那肯定就会报错了,不能访问,因为这是在类外不能访问私有数据成员。一个类的成员函数可以访问这个类的私有数据成员,我们要理解这个对类的访问限制,而不是针对对象。
  • 因为拷贝构造函数是类的成员函数,当然可以访问类的成员变量 注意私有和公有是对类来说的,不是对对象来说的
  • 啊。 啊 明白了,就是无关对象呗。。只要是同一个类就可访问
  • 允许访问顺手啊[/quote] 那不就结了。 为什么我们限制私有成员的访问,因为我们不希望非类的定义者乱访问我们的对象。既然我已经是类的定义者了,我对这个类的行为了如指掌,为什么还要人为限制不允许访问这个类的其它对象呢? 【还是这个最能说服我,笑】

一个类中可以存在多于一个的拷贝构造函数吗?

可以

class X { 
public: 
  X(const X&); // const 的拷贝构造 
  X(X&); // 非const的拷贝构造 
};

注意,

  • 如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化.
  • 如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。
  • 这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

其他例子

struct A
{
    int n;
    A(int n = 1) : n(n) { }
    A(const A& a) : n(a.n) { } // 用户定义的复制构造函数
};
 
struct B : A
{
    // 隐式默认构造函数 B::B()
    // 隐式复制构造函数 B::B(const B&)
};
 
struct C : B
{
     C() : B() { }
 private:
     C(const C&); // 不可复制,C++98 风格
};
 
int main()
{
    A a1(7);
    A a2(a1); // 调用复制构造函数
    B b;
    B b2 = b;
    A a3 = b; // 转换到 A& 并调用复制构造函数
    volatile A va(10);
    // A a4 = va; // 编译错误
 
    C c;
    // C c2 = c; // 编译错误
}

其他[构造/拷贝函数]知识点

  • 不含参数或者只有一个参数并且参数有默认值的构造函数
class A
{
public:
	A();  //没有参数
};
class B
{
public:
	explicit B(int x = 1, bool b = true);  //每个参数有初始值
	//explicit:阻止执行隐式转换,但是可以显式类型转换
};
class C
{
public:
	explicit C(int c);  //非默认构造函数
};
  • 如果类没有定义任何构造函数,编译器会为其合成一个无参默认构造函数。
class A {
    int val;
};
int main() {
    A a;
    return 0;
}
  • 如果类定义了构造函数,编译器不会为其合成默认构造函数,也就不能执行默认初始化了。比如:
class A {
	int val;
	A(int v):val(v) {}
};

A a;         //编译出错,没有默认构造函数

  • 如果类没有定义拷贝赋值运算,编译器会默认合成的拷贝赋值运算符。
class A {
    int val;
};
int main() {
    A a;
    A b = a;
    return 0;
}
  • 可以指定编译器合成一个默认构造函数,用default关键字。
class A {
public:
    A() = default;
};
int main() {
    A a;
    return 0;
}

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值