C++中深拷贝和浅拷贝

来源:互联网 发布:淘宝外围会场有用吗 编辑:程序博客网 时间:2024/06/18 10:28

C++中深拷贝和浅拷贝的问题是很值得我们注意的知识点,如果编程中不注意,可能会出现疏忽,导致bug。本文就详细讲讲C++深浅拷贝的种种。

       对于一般的对象,如:

C++代码
  1. int a = 10;   
  2. int b = 20;  

       它们之间的赋值、复制过程是很简单的。但是对于类对象来说,其内部存在各种类型成员变量,在拷贝过程中会出现问题。如下:

C++代码
  1. #include<iostream>   
  2.  #include<cstring>   
  3.  using namespace std;   
  4.  class String {   
  5.  public:   
  6.      String (const char* psz=NULL) : m_psz(strcpy((new char[strlen(psz?psz:"")+1]),psz?psz:"")){   
  7.          cout << "String构造" << endl;   
  8.      }   
  9.       ~String () {   
  10.          if(m_psz) {   
  11.               delete[] m_psz;   
  12.               m_psz = NULL;   
  13.          }   
  14.          cout << "String析构" << endl;   
  15.      }   
  16.      char* c_str(void) {   
  17.          return m_psz;   
  18.      }   
  19.  private:   
  20.      char* m_psz;   
  21.  };   
  22.  int main(void) {   
  23.      String s1("hello");   
  24.      String s2(s1);   
  25.      cout << "s1    " << s1.c_str() << endl;   
  26.      cout << "s2    " << s2.c_str() << endl;   
  27.      s1.c_str()[0] = 'H';   
  28.      cout << "s1    " << s1.c_str() << endl;   
  29.      cout << "s2    " << s2.c_str() << endl;   
  30.      return 0;   
  31.  }  

       运行结果:

C++深浅拷贝浅析

       编译通过了,运行后出现一堆的错误,为什么?!这就是浅拷贝带来的问题。

       事实是,在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个缺省的拷贝构造函数,缺省的拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型成员变量,调用其相应类型的拷贝构造函数。原型如下:

C++代码
  1. String (const String& that) {}  

       但凡是编译系统提供的缺省函数,总不是十全十美的。

       缺省拷贝构造函数在拷贝过程中是按字节复制的,对于指针型成员变量只复制指针本身,而不复制指针所指向的目标--浅拷贝。

       用下图来解释这个问题:

C++深浅拷贝浅析

       在进行对象复制后,事实上s1、s2里的成员指针m_psz都指向了一块内存空间(即内存空间共享了),在s1析构时,delete了成员指针m_psz所指向的内存空间,而s2析构时同样指向(此时已变成野指针)并且要释放这片已经被s1析构函数释放的内存空间,这就让同样一片内存空间出现了“double free” ,从而出错。而浅拷贝还存在着一个问题,因为一片空间被两个不同的子对象共享了,只要其中的一个子对象改变了其中的值,那另一个对象的值也跟着改变了,正如程序中只改变了s1.c_str()[0] = 'H',然而输出的s1,s2均为hello,所以这并不是真正意义上的复制。

       为了实现深拷贝,往往需要自己定义拷贝构造函数,在源代码里,我们加入自定义的拷贝构造函数如下:

C++代码
  1. String (const String& that) : m_psz(strcpy((new char[strlen(that.m_psz)+1]),that.m_psz)){   
  2.     cout << "String拷贝构造" << endl;   
  3. }  

       这样再运行就没有问题了。

       在程序中,还有哪些情况会用到拷贝构造函数呢?当函数存在对象型的参数或对象型的返回值时都会用到拷贝构造函数。

       而拷贝赋值的情况基本上与拷贝复制是一样的。只是拷贝赋值是属于操作符重载问题。例如在主函数若有:String s3;s3 = s2;这样系统在执行时会调用系统提供的缺省的拷贝赋值函数,原型如下:

C++代码
  1. void operator = (const String& that) {}  

       我们可以自定义拷贝赋值函数如下:

C++代码
  1. void operator=(const String& that) {   
  2.     m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);   
  3. }  

       但是这只是新手级别的写法,考虑的问题太少。我们知道对于普通变量来讲a=b返回的是左值a的引用,所以它可以作为左值继续接收其他值(a=b)=30,这样来讲我们操作符重载后返回的应该是类对象的引用(否则返回值将不能作为左值来进行运算),如下:

C++代码
  1. String& operator=(const String& that){   
  2.     m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);   
  3. }  

       而 m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);这种写法其实也有问题,因为在执行语句时,m_psz已经被构造已经分配了内存空间,但是如此进行指针赋值,m_psz直接转而指向另一片新new出来的内存空间,而丢弃了原来的内存,这样便造成了内存泄露。应更改为:

C++代码
  1. String& operator=(const String& that) {   
  2.     delete[] m_psz;   
  3.     m_psz = strcpy (new char[strlen (that.m_psz)+1],that.m_psz);   
  4. }  

       这样就行了吗?在这个世界上不怕没好事就怕没好人,万一他跟你搞一个自赋值(s3=s3)怎么办?

       操作符左右两边都是同一个对象,这样先delete[] m_psz,后面又有that.m_psz,这就出现了问题。所以为了防止自赋值,我们一般的写法为:

C++代码
  1. String& operator=(cosnt String& that) {   
  2.     if(&that != this) {   
  3.         delete[] m_psz;   
  4.         m_psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);      
  5.     }   
  6.     return *this;   
  7. }  

       可是这样写就完善了吗?是否要再仔细思索一下,还存在问题吗?!其实我可以告诉你,这样的写法也顶多算个初级工程师的写法。前面说过,为了保证内存不泄露,我们前面delete[] m_psz,然后我们在把new出来的空间给了m_psz,但是这样的问题是,你有考虑过万一new失败了呢?!内存分配失败,m_psz没有指向新的内存空间,但是它却已经把旧的空间给扔掉了,所以显然这样的写法依旧存在着问题。一般高级工程师的写法会是这样的:

C++代码
  1. String& operator=(cosnt String& that) {   
  2.      if(&that != this) {   
  3.          char *psz = strcpy (new char[strlen(that.m_psz)+1],that.m_psz);//如果失败会抛出异常,m_psz最后在析构函数里释放   
  4.          delete[] m_psz;   
  5.          m_psz = psz;      
  6.      }   
  7.      return *this;   
  8.  }  

       这样考虑的问题便比较全面了。

       高级工程师高确实高,然而有没有比高级工程师更高的工程师呢?答案是肯定的。对于从事多年C++开发元老级别资深的C++工程师来讲,他们不会这么写,因为有更好更简便的写法,如下:

C++代码
  1. String& operator=(const String& that) {   
  2.      if(&that != this) {   
  3.          String str (that);   
  4.          char *psz = m_psz;   
  5.          m_psz = str.m_psz;   
  6.          str.m_psz = psz;   
  7.      }   
  8.      return *this;   
  9.  }  

       有人看出来这样写的玄机了吗??

       事实上,这是借助了以上自定义的拷贝构造函数。定义了局部对象str,在拷贝构造中已经为str的成员指针分配了一块内存,所以只需要将str.m_psz与this->m_psz交换指针即可,简化了程序的设计,因为str是局部对象,离开作用域会调用析构函数释放交换给str.m_psz的内存,避免了内存泄露。

       大家在读完这篇文章后,对于C++的代码设计,是否有一定的感悟了呢?在我们进行C++的代码设计的过程中,一定要多加思索,考虑问题要全面,精益求精,写出来的代码才经得住推敲!


```````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````

对于普通类型的对象来说,它们之间的复制是很简单的,例如:
int a=88;
int b=a; 
而类对象与普通对象不同,类对象内部结构一般较为复杂,存在各种成员变量。下面看一个类对象拷贝的简单例子。 

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;
 
class CExample {
private:
     int a;
public:
     CExample(int b)
     { a=b;}
     void Show ()
     {
        cout<<a<<endl;
    }
};
 
int main()
{
     CExample A(100);
     CExample B=A;
     B.Show ();
     return 0;
}

 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
 
class CExample {
private:
    int a;
public:
    CExample(int b)
    { a=b;}
     
    CExample(const CExample& C)
    {
        a=C.a;
    }
    void Show ()
    {
        cout<<a<<endl;
    }
};
 
int main()
{
    CExample A(100);
    CExample B=A;
    B.Show ();
    return 0;
}

 

 

 

当用一个已初始化过了的自定义类类型对象去初始化另一个新构造的对象的时候,拷贝构造函数就会被自动调用。也就是说,当类的对象需要拷贝时,拷贝构造函数将会被调用以下情况都会调用拷贝构造函数:
(1)一个对象以值传递的方式传入函数体 
(2)一个对象以值传递的方式从函数返回 
(3)一个对象需要通过另外一个对象进行初始化

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,后面将进行说明。

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

浅拷贝和深拷贝

  在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

  深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。下面举个深拷贝的例子。

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
using namespace std;
class CA
{
 public:
  CA(int b,char* cstr)
  {
   a=b;
   str=new char[b];
   strcpy(str,cstr);
  }
  CA(const CA& C)
  {
   a=C.a;
   str=new char[a];//深拷贝
   if(str!=0)
    strcpy(str,C.str);
  }
  void Show()
  {
   cout<<str<<endl;
  }
  ~CA()
  {
   delete str;
  }
 private:
  int a;
  char *str;
};
 
int main()
{
 CA A(10,"Hello!");
 CA B=A;
 B.Show();
 return 0;
}

 

来总结一下关于 深拷贝与浅拷贝需要知道的基本概念和知识:

(1)什么时候用到拷贝函数?

  a.一个对象以值传递的方式传入函数体; 
  b.一个对象以值传递的方式从函数返回;
  c.一个对象需要通过另外一个对象进行初始化。

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝

 

(2)是否应该自定义拷贝函数?

 

 

(3)什么叫深拷贝?什么是浅拷贝?两者异同?

自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

 

 

深如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。

 

(4)深拷贝好还是浅拷贝好?

如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。

``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````

C++中拷贝构造函数

1.什么是拷贝构造函数:

拷贝构造函数嘛,当然就是拷贝和构造了。(其实很多名字,只要静下心来想一想,就真的是顾名思义呀)拷贝又称复制,因此拷贝构造函数又称复制构造函数。百度百科上是这样说的:拷贝构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化。其唯一的参数(对象的引用)是不可变的(const类型)。此函数经常用在函数调用时用户定义类型的值传递及返回

 

2.拷贝构造函数的形式

Class X

{

public:

  X();

  X(const X&);//拷贝构造函数

}

2.1为什么拷贝构造参数是引用类型?

其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动被调用来生成函数中的对象(符合拷贝构造函数调用的情况)。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象,这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。

 

3.拷贝构造函数调用的三种形式

3.1.一个对象作为函数参数,以值传递的方式传入函数体;

3.2.一个对象作为函数返回值,以值传递的方式从函数返回;

3.3.一个对象用于给另外一个对象进行初始化(常称为复制初始化)。

总结:当某对象是按值传递时(无论是作为函数参数,还是作为函数返回值),编译器都会先建立一个此对象的临时拷贝,而在建立该临时拷贝时就会调用类的拷贝构造函数

 

4.深拷贝和浅拷贝

如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。(位拷贝又称浅拷贝,后面将进行说明。)自定义拷贝构造函数是一种良好的编程风格,它可以阻止编译器形成默认的拷贝构造函数,提高源码效率。

在某些状况下,类内成员变量需要动态开辟堆内存,如果实行位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。事实上这就要用到深拷贝了,要自定义拷贝构造函数。

深拷贝和浅拷贝可以简单理解为:如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。下面举个深拷贝的例子。

#include <iostream>

using namespace std;

class CA

{

public:

  CA(int b,char* cstr)

  {

    a=b;

     str=new char[b];

    strcpy(str,cstr);

  }

  CA(const CA& C)

  {

    a=C.a;

    str=new char[a]; //深拷贝

    if(str!=0)

     strcpy(str,C.str);

  }

  void Show()

  {

    cout<<str<<endl;

  }

  ~CA()

  {

    delete str;

  }

 private:

   int a;

   char *str;

};

int main()

{

  CA A(10,"Hello!");

  CA B=A;

  B.Show();

  return 0;

}

浅拷贝资源后在释放资源的时候会产生资源归属不清的情况导致程序运行出错。一定要注意类中是否存在指针成员。

 

 

5.拷贝构造函数与“=“赋值运算符

例如:

class CExample

{};

int main()

{

CExample e1 = new CExample;

CExample e2 = e1;//调用拷贝构造函数

CExample e3(e1);//调用拷贝构造函数

CExample e4;

e4 = e1;//调用=赋值运算符

}

通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载"="赋值操作符号。


0 0
原创粉丝点击