C++知识点之深/浅拷贝

来源:互联网 发布:淘宝收获地址怎么改 编辑:程序博客网 时间:2024/05/21 04:22

c++中深/浅拷贝问题

1 背景

C++中一般创建对象,拷贝或赋值的方式有构造函数拷贝构造函数赋值函数这三种方法。

  • 拷贝构造函数使用已有的对象创建一个新的对象
  • 赋值运算符是将一个对象的值复制给另一个已存在的对象。

区分是调用拷贝构造函数还是赋值运算符,主要看是否有新的对象产生。
下面首先介绍一下上述三种创建对象的方式:

2 构造函数

① 构造函数是一种特殊的类成员函数,是当创建一个类的对象时,它被调用来对类的数据成员进行初始化和分配内存。(构造函数的命名必须和类名完全相同)
② 首先说一下一个C++的空类,编译器会加入哪些默认的成员函数

  • 默认构造函数和拷贝构造函数
  • 析构函数
  • 赋值函数(赋值运算符)
  • 取值函数

*即使类中没定义任何成员,编译器也会插入以上的函数!
注意:构造函数可以被重载,可以多个,可以带参数;析构函数只有一个,不能被重载,不带参数

③ 而默认构造函数没有参数,它什么也不做。当没有重载无参构造函数时,Person man;就是通过默认构造函数来创建一个对象
④下面代码为构造函数重载的实现:

class Person    {      public:      Person()         {            qDebug()<<”无参构造函数”<<endl;        }        Person(int i):m_i(i) {}  //初始化列表    private:      int m_i;  }  

3 拷贝构造函数和赋值函数

① 拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。

② 在默认情况下(用户没有定义,但是也没有显式的删除),编译器会自动的隐式生成一个拷贝构造函数和赋值运算符。但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算。

class Person  {  public:      Person(const Person& p) = delete; //不生成拷贝构造函数      Person& operator=(const Person& p) = delete; //不生成赋值运算符  private:      int age;      string name;  };  

上面的定义的类Person显式的删除了拷贝构造函数和赋值运算符,在需要调用拷贝构造函数或者赋值运算符的地方,会提示无法调用该函数,它是已删除的函数

③ 还有一点需要注意的是,拷贝构造函数必须以引用的方式传递参数。这是因为,在值传递的方式传递给一个函数的时候,会调用拷贝构造函数生成函数的实参。如果拷贝构造函数的参数仍然是以值的方式,就会无限循环的调用下去,直到函数的栈溢出。

4 调用场合

① 拷贝构造函数和赋值运算符的行为比较相似,都是将一个对象的值复制给另一个对象;但是其结果却有些不同,拷贝构造函数使用传入对象的值生成一个新的对象的实例,而赋值运算符是将对象的值复制给一个已经存在的实例。这种区别从两者的名字也可以很轻易的分辨出来,拷贝构造函数也是一种构造函数,那么它的功能就是创建一个新的对象实例;赋值运算符是执行某种运算,将一个对象的值复制给另一个对象(已经存在的)。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。

② 调用拷贝构造函数主要有三个场景:

  • 对象作为函数的参数,以值传递的方式传给函数。 
  • 对象作为函数的返回值,以值的方式从函数返回
  • 使用一个对象给另一个对象初始化

③ 什么时候编译器会生成默认的拷贝构造函数:

  • 如果用户没有自定义拷贝构造函数,并且在代码中使用到了拷贝构造函数,编译器就会生成默认的拷贝构造函数。但如果用户定义了拷贝构造函数,编译器就不再生成。
  • 如果用户定义了一个构造函数,但不是拷贝构造函数,而此时代码中又用到了拷贝构造函数,那编译器也会生成默认的拷贝构造函数。

④ 样例代码如下:

class Person  {  public:      Person(){}      Person(const Person& p)      {          cout << "Copy Constructor" << endl;      }      Person& operator=(const Person& p)      {          cout << "Assign" << endl;          return *this;      }  private:      int age;      string name;  };  void f(Person p)  {      return;  }  Person f1()  {      Person p;      return p;  }  int main()  {      Person p;      Person p1 = p;    // A      Person p2;      p2 = p;           // B      f(p2);            // C      p2 = f1();        // D      Person p3 = f1(); // E      getchar();      return 0;  }  

上面代码中定义了一个类Person,类中显式地定义了拷贝构造函数和赋值运算符。然后定义了两个函数:f(),以值的方式参传入Person对象;f1(),以值的方式返回Person对象。在main中模拟了5中场景,测试调用的是拷贝构造函数还是赋值运算符。执行结果如下:
这里写图片描述
分析如下:

  • A. 这里虽然使用了”=”,但是实际上使用对象p来创建一个新的对象p1,也就是产生了新的对象,所以调用的是拷贝构造函数。
  • B. 首先声明一个对象p2,然后使用赋值运算符”=”,将p的值复制给p2,显然是调用赋值运算符,为一个已经存在的对象赋值 。
  • C. 以值传递的方式将对象p2传入函数f内,调用拷贝构造函数构建一个函数f可用的实参。
  • D. 这条语句拷贝构造函数和赋值运算符都调用了。函数f1以值的方式返回一个Person对象,在返回时会调用拷贝构造函数创建一个临时对象tmp作为返回值;返回后调用赋值运算符将临时对象tmp赋值给p2.
  • E. 按照4的解释,应该是首先调用拷贝构造函数创建临时对象;然后再调用拷贝构造函数使用刚才创建的临时对象创建新的对象p3,也就是会调用两次拷贝构造函数。不过,编译器也没有那么傻,应该是直接调用拷贝构造函数使用返回值创建了对象p3。

5 深拷贝和浅拷贝

① 通常,默认生成的拷贝构造函数和赋值运算符,只是简单的进行值的复制。例如:上面的Person类,字段只有int和string两种类型,这在拷贝或者赋值时进行值复制创建的出来的对象和源对象也是没有任何关联,对源对象的任何操作都不会影响到拷贝出来的对象。反之,假如Person有一个对象类型为int* ,这时在拷贝时若只是进行值复制,那么新创建出来的Person对象的int* 成员就和源对象的int* 成员指向的是同一个地址。任何一个对象对该值的修改都会影响到另一个对象,这种情况就是浅拷贝。
② 系统提供的默认拷贝构造函数工作方式是内存拷贝,也就是浅拷贝。如果对象中用到了需要手动释放的对象,则会出现问题,这时就要手动重载拷贝构造函数,实现深拷贝。
③ 深拷贝与浅拷贝:
浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除其一空间就不存在)
深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。
④ 深拷贝和浅拷贝主要是针对类中的指针和动态分配的空间来说的,因为对于指针只是简单的值复制并不能分割开两个对象的关联,任何一个对象对该指针的操作都会影响到另一个对象。这时候就需要提供自定义的深拷贝的拷贝构造函数,消除这种影响。通常的原则是:

  • 任何含有指针类型的成员或者有动态分配内存的成员的类都应该提供自定义的拷贝构造函数
  • 在提供拷贝构造函数的同时,还应该考虑实现自定义的赋值运算符

⑤ 拷贝构造函数的实现要确保以下几点:

  1. 对于值类型的成员进行值复制
  2. 对于指针和动态分配空间的成员,在拷贝构造函数中应重新分配分配空间
  3. 对于基类,要调用基类合适的拷贝方法,完成基类的拷贝

6 总结

对象不存在,且没用别的对象来初始化,就是调用了构造函数;
对象不存在,且用别的对象来初始化,就是拷贝构造函数;
对象存在,用别的对象来给它赋值,就是赋值函数。

拷贝构造函数和赋值运算符的行为比较相似,却产生不同的结果;拷贝构造函数使用已有的对象创建一个新的对象,赋值运算符是将一个对象的值复制给另一个已存在的对象。区分是调用拷贝构造函数还是赋值运算符,主要是否有新的对象产生。

关于深拷贝和浅拷贝。当类有指针成员或有动态分配空间,都应实现自定义的拷贝构造函数。提供了拷贝构造函数,最后也应考虑提供赋值运算符。

原创粉丝点击