cpp

C++ 拷贝构造函数

Posted by kevin on October 13, 2019

perface

最近在做题目时知道了 C++ 拷贝构造函数,好像以前上课的时候没有注意过这个东西,可能是个盲区,过来补一下。

回顾构造函数

在上课的时候我们学过构造函数和析构函数,构造函数就是在创建对象时会自动调用的函数,目的是给类的某些变量初始化。默认的构造函数是没有参数的,然而通过 C++ 的重载功能,我们又可以重载出好几个构造函数,包括默认参数的、参数不同的。。

class A{
public:
	A(){}	// 默认构造函数
	A(int a = 0, int b = 0){}	// 带默认参数的构造函数
	A(int a, int b, int c){}	// 带参数构造函数
};

悄咪咪补充个知识点,列表初始化很高效 A(int x, int y):a(x), b(y) {}​

拷贝构造函数

那么拷贝构造函数是怎么回事呢,在一般情况下,两个变量进行复制是很简单的

int a = 0;
int b = a;

但是类和这些普通的类型不同,类里面还包含了很多的成员变量,因此相同类型的对象进行赋值是怎么操作的呢,我们用下面这个例子来讲一下。

class CExample {
public:
    CExample(int b)
    {
        a = b;
    }

    void Show ()
    {
        cout<<a<<endl;
    }
private:
    int a;
};

int main()
{
    CExample A(10);
    CExample B = A; // 对象初始化要调用拷贝构造函数
    B.Show ();
    return 0;
}

最终的输出是 10 , 说明了系统为 B 对象分配了内存并且将与 A 有关的变量全都赋值给了 B ,这里的复制过程就是通过拷贝构造函数来完成的。这下面就是一种构造函数的原型。

CExample(const CExample & B){
    a = B.a;
}

可以看出来,构造函数是一种特殊的构造函数,他的函数名也必须和类名相同,同时也没有函数返回值,他的参数必须包含本类型的一个引用对象

调用拷贝构造函数的时机

其实调用拷贝构造函数也不经常用到吧,至少在我写过看过的代码中没有什么人会用拷贝函数,但是还是跟着博客记录一下,万一哪天面试官问起来。。

0x01 对象以值传递的方式传入函数参数

class CExample {
public:
 //构造函数
CExample(int b)
{ 
    a = b;
    cout << "creat:" << a << endl;
}

//拷贝构造
CExample(const CExample& C)
{
    a = C.a;
    cout << "copy" << endl;
}
 
//析构函数
~CExample()
{
    cout << "delete:" << a << endl;
}

private:
    int a;
};

void g_Fun(CExample C)
{
    cout<<"test"<<endl;
}

int main()
{
    CExample test(1);
    g_Fun(test);
    return 0;
}

main 函数调用 g_Fun() 函数时的步骤如下:

  1. test 对象传入形参,会产生一个临时变量 C
  2. 调用拷贝构造函数把 test 的值给 C,前面两步有点像 CExample C(test)
  3. 执行完 g_Fun() ,调用析构函数析构 C 对象

所以调用后的输出如下:

creat: 1
copy
test
delete: 1
delete: 1

0x02 对象以值传递的方式从函数返回

代码依然如上,只是将全局函数 g_Fun 修改了一下

CExample g_Fun()
{
    CExample temp(0);
    return temp;
}

int main()
{
    g_Fun();
    return 0;
}

按照博客的说法,当 g_Fun() 函数执行到 return 时,会产生以下步骤:

  1. 产生一个临时变量 C
  2. 调用拷贝构造函数将 temp 的值复制给 C ,前面两步像 CExample C(temp)
  3. 在函数执行到最后先调用析构函数析构 temp 局部变量
  4. g_Fun() 执行完后再析构掉 C 对象

但是这个程序的输出是下面的东西,只有一个对象产生和析构,不知道是不是作者在骗我,看上去并没有调用复制构造函数

creat: 0
delete: 0

查阅资料终于知道了,并不是作者在骗我,大大小小的书上都是这么写的,之所以这种情况下没有调用复制构造函数是因为我的编译器进行了优化,导致了函数在返回一个对象时不调用拷贝构造函数,道理是正确的。

0x03 对象需要通过另外一个对象进行初始化

直接上代码,下面两种情况都是会调用拷贝构造函数的

CExample A(10);
CExample B = A;
// CExample B(A);

程序输出如下:

creat: 10
copy
delete: 10
delete: 10

深拷贝和浅拷贝

通过上面的内容,估计大家对拷贝构造函数已经大概了解了,然而 C++ 里面还分浅拷贝和深拷贝,其实也不难,就是类里面有动态分配内存空间的就要执行深拷贝,一般情况下浅拷贝就能很好地完成任务了,C++ 默认的拷贝构造函数也是浅拷贝。

浅拷贝

浅拷贝也就是在对象的复制过程中只对对象中的数据成员进行简单的赋值,结构就类似下面这种形式

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

但是遇到有动态成员的话浅拷贝就会出问题,比如下面:

class Rect
{
public:
    Rect()      // p指向 堆 中分配的空间
    {
        p = new int(100);
    }
    ~Rect()     // 释放动态分配的空间
    {
        if(p != NULL)
        {
            delete p;
        }
    }
private:
    int width;
    int height;
    int *p;    
};

int main()
{
    Rect rect1;
    Rect rect2(rect1);
    return 0;
}

这段代码放到编译器里会报错,原因是使用 rect1 复制 rect2 时,由于执行浅拷贝,所以只是将成员的值进行简单赋值,有 rect1.p = rect2.p ,就是说两个对象的指针变量指向了堆里的同一个空间,肯定不行!因为两个对象的析构函数将同一个内存空间释放了两次。

然而我在 Qt Creator 中运行了,竟然不会报错,后来想想好像 Qt 就是这样的,会自动销毁动态成员。

深拷贝

针对成员变量存在指针的情况,深拷贝就不仅仅是简单的指针赋值,而是重新分配内存空间:

class Rect
{
public:
    Rect()
    {
        p = new int(100);
    }
    Rect(const Rect& r)
    {
        width = r.width;
        height = r.height;
        p = new int;    // 为新对象重新动态分配空间
        *p = *(r.p);
    }
    ~Rect()     
    {
        if(p != NULL)
        {
            delete p;
        }
    }
private:
    int width;
    int height;
    int *p;     
};

这样的话,两个对象赋值后,指针各指向一段内存空间,但是这两段内存里面的内容都是一样的,这就是深拷贝。

FAQ

0x01 以下函数哪个是拷贝构造函数

X::X(const X&);    
X::X(X);    
X::X(X&, int a=1);    
X::X(X&, int a=1, int b=2);

一个函数是拷贝构造函数的话,那它有以下特点:第一个参数是以下几个中的一个,且没有其他参数或者其他参数都有默认值

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

因此,1,3,4 都是拷贝构造函数。

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

那是当然,上题都说了,可以重载那么多的拷贝构造函数,当然可以存在好多个拷贝构造函数了。不过,如果一个类中只存在一个参数为 X& 的拷贝构造函数,那么就不能用 const X 或 volatile X 的对象进行拷贝初始化,像下面这种就是错的

class X {    
public:
  X();    
  X(X&);
};    

const X cx;    
X x = cx;    

如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数,这个默认的参数可能为 X::X(const X&)或 X::X(X&),由编译器根据上下文决定选择哪一个。

实验时间

下面是一段浅拷贝代码,我们通过 debug 方式查看内存分布,以此来更好地了解其机制

class CExample
{
public:
 int num;
 int *x;

public:
    //构造函数
    CExample(int b, int * ss)
    {
    num = b;
    x = ss;
    }
    
    //拷贝构造
    CExample(const CExample& C)
    {
    num = C.num;
    x = C.x;
    cout<<"copy"<<endl;
    }
};

int main()
{
    int t = 10;
    int* p = &t;
    CExample ex1(666, p);
    CExample ex2 = ex1;
    ex2.num = 1;
    *(ex2.x) = 233;
    cout << ex2.num << " " << ex1.num << endl;
    cout << *(ex2.x) << " " << *(ex1.x) << endl;
    return 0;
}

运行截图如下,打个断点,查看内存,可以看到,两个对象的指针指向了同一片内存区,改变非动态成员 num 的值后,另一个对象的 num 值不变,而改变指针指向地址中的值,另一个对象的值也会改变。

shallow copy

我们将上面代码的拷贝函数改一下,变成深拷贝试试:

CExample(const CExample& C)
{
    num = C.num;
	x = new int;
	*x = *(C.x);
    cout<<"copy"<<endl;
}

运行截图如下,打个断点,查看内存,可以看到,两个对象的指针指向了不同的内存区,改变非动态成员 num 的值后,另一个对象的 num 值不变,改变指针指向地址中的值,另一个对象的值并不会跟着改变,这就是深拷贝

deep copy

reference

https://blog.csdn.net/sxhelijian/article/details/50977946

由于编辑器的锅,更新的时候把链接给弄没了,只记得一个了