拷贝构造函数与赋值运算符重载函数要点

拷贝构造函数

一个小例子

  最近在《剑指Offer》上看到了一道题(程序如下),要求我们分析编译运行的结果,并提供3个选项: A. 编译错误; B. 编译成功,运行时程序崩溃;C. 编译运行正常,输出10。

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

  这个程序是通不过编译的,GCC和VS均通不过。根据《剑指Offer》上的解释,上述程序中的拷贝构造函数A(A other)传入的参数是A的一个实例,所以由于是传值参数,我们把形参复制到实参会调用拷贝构造函数。因此如果允许拷贝构造函数传值,就会在拷贝构造函数内调用复制构造函数,就会形成无休止的递归调用从而导致栈溢出。为了说明这个解释,我们先看下稍微修改过的程序:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class A
 5 {
 6 private:
 7     int value;
 8
 9 public:
10     A(int n)
11     {
12         value = n;
13         cout << "constructor with argument" << endl;
14     }
15
16     A(const A& other)
17     {
18         value = other.value;
19         cout << "copy constructor" << endl;
20     }
21
22     A& operator=(const A& other)
23     {
24         cout << "assignment operator" << endl;
25         value = other.value;
26         return *this;
27     }
28
29     void func(A other)
30     {
31     }
32 };
33
34 int main()
35 {
36     A a = 10;      //    constructor with argument
37     A b = 5;       //    constructor with argument
38     b = a;          //    assignment operator
39     A c = a;       //    copy constructor
40     b.func(a);      //    copy constructor
41
42     return 0;
43 }

  程序的运行结果如下:

  

  对于上述程序的第39行,构造c,实质上是c.A(a)。假如拷贝构造函数参数不是引用类型的话,那么将使得c.A(a)变成a传值给c.A(A other),即A other = a,而other没有被初始化,所以other = a将继续调用拷贝构造函数(这也是为什么调用func函数会调用拷贝构造函数)。接下来是构造other,也就是other.A(a),即A other = a,又会触发拷贝构造函数,这样永远地递归下去。

  

  关于上述程序还有一些值得说明的:

  1)当某对象没被初始化,这时运用赋值运算符调用的是拷贝构造函数;否则调用的是赋值运算符重载函数;

  2)关于public、protected、private几个关键字的重新理解。如果我们直接在main函数用a.value是不行的(因为权限是private),但在拷贝构造函数和重载赋值运算符函数中确是可以的,而且,当我们不将传入参数设定为const的话,我们在函数中还可以修改传入参数的值,这不是自相矛盾了吗?具体解释可以参考博文C++ 类访问控制public/private/protected探讨的说法:

  “   

  类是将数据成员和进行于其上的一系列操作(成员函数)封装在一起。注意:成员函数可以操作数据成员(可以称类中的数据成员为泛数据成员)!  

  对象是类的实例化,怎样理解实例化?其实每一个实例对象都只是对其中的数据成员初始化,内存映像中每个对象仅仅保留属于自己的那份数据成员副本。而成员函数对于整个类而言却是共享的,即一个类只保留一份成员函数。

  那么每个对象怎样和这些可以认为是“分离”的成员函数发生联系,即成员函数如何操作对象的数据成员?记住this指针,无论对象通过(.)操作或者(->)操作调用成员函数。编译时刻,编译器都会将这种调用转换成我们常见的全局函数的形式,并且多出一个参数(一般这个参数放在第一个,有点像python中类中函数声明中的self参数),然后将this指针传入这个参数。于是就完成了对象与成员函数的绑定(或联系)。

  实例化后就得到同一个类的多个不同的对象,既然成员函数共享的,那么成员函数就可以操作对象的数据成员。

  问题是现在有多个对象,成员函数需要知道操作的是哪个对象的数据成员?比如有对象obj1和obj2,都属于A类,A类有public成员函数foo()。如果obj1调用该函数,编译时会给foo函数传入this指针,obj1.foo中操作obj1自身的成员就不用任何修饰,直接访问,因为其中的数据成员自动根据this指针找到。

  如果obj1调用该函数,同样可以访问同类的其他对象的数据成员!那么你需要做的是让foo函数知道是同类对象中哪个对象的数据成员,一个解决办法是传入同类其他对象的指针或引用,那么就可以操作同类其他对象的数据成员。

  foo(A& obj)

  这样定义,然后调用:

  obj1.foo(obj2)

  就可以在obj1访问obj2的数据成员,而无论这些数据成员是private还是protected

  "

处理静态成员变量

  关于静态成员变量详细的可参考之前博文C/C++中关键字static的用法及作用。静态成员变量主要是为了同个类的不同实例之间数据的共享,所以其处理方法也跟其他成员变量稍微不一样。

  先看下程序:

 1 class Rect
 2 {
 3 public:
 4     Rect()      // 构造函数,计数器加1
 5     {
 6         count++;
 7     }
 8     ~Rect()     // 析构函数,计数器减1
 9     {
10         count--;
11     }
12     static int getCount()       // 返回计数器的值
13     {
14         return count;
15     }
16 private:
17     int width;
18     int height;
19     static int count;       // 一静态成员做为计数器
20 };
21
22 int Rect::count = 0;        // 初始化计数器
23
24 int main()
25 {
26     Rect rect1;
27     cout << "The count of Rect: " << Rect::getCount() << endl;
28
29     Rect rect2(rect1);   // 使用rect1复制rect2,此时应该有两个对象
30     cout << "The count of Rect: " << Rect::getCount() << endl;
31
32     return 0;
33 }

  程序的输出结果都是count = 1,这明显跟我们的期望值(应该是2)不一样。具体原因在于我们没有定制拷贝构造函数,而是由编译器帮我们自动生成一个默认拷贝函数:

1 Rect::Rect(const Rect& orig)
2 {
3     width = orig.width;
4     height = orig.height;
5 }

  显然这里并没有处理静态成员变量count,所以我们需要定制拷贝构造函数:

1 Rect(const Rect& orig)   // 拷贝构造函数
2 {
3     width = orig.width;
4     height = orig.height;
5     count++;             // 计数器加1
6 }

深拷贝与浅拷贝

  深拷贝主要解决的问题是指针成员变量浅拷贝的问题。这方面的博文很多,可以参考博文C++拷贝构造函数详解。这篇博文有提到了几点值得注意的:

  1. 防止默认拷贝(也能够禁止复制)

  有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。如下程序:

 1 #include <iostream>
 2 using namespace std;
 3
 4 class CExample
 5 {
 6 private:
 7     int value;
 8
 9 public:
10     //构造函数
11     CExample(int val)
12     {
13         value = val;
14         cout << "creat: " << value << endl;
15     }
16
17 private:
18     //拷贝构造,只是声明
19     CExample(const CExample& C);
20
21 public:
22     ~CExample()
23     {
24         cout << "delete: " << value << endl;
25     }
26
27     void Show()
28     {
29         cout << value << endl;
30     }
31 };
32
33 //全局函数
34 void g_Fun(CExample C)
35 {
36     cout << "test" << endl;
37 }
38
39 int main()
40 {
41     CExample test(1);
42     // g_Fun(test); // 按值传递将出错
43
44     return 0;
45 }

  而根据《C++ Primer》第四版13.1.3节,要禁止类的复制, 类必须显示声明其复制构造函数为private。

  

  2. 小问题1:以下哪个函数是拷贝构造函数,为什么?

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

  解答:对于一个类X,如果一个构造函数的第一个参数是下列之一:
  a)X&
  b)const X&
  c) volatile X&
  d)const volatile X&
  且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数.

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

  

  3. 小问题2:一个类中可以有多个拷贝构造函数吗?

  解答:类中可以存在超过一个拷贝构造函数。

1 class X {
2 public:
3   X(const X&);      // const 的拷贝构造
4   X(X&);            // 非const的拷贝构造
5 };

赋值运算符重载

   关于运算符重载可参考之前博文C++运算符重载

  在本文的第2个程序中我们就已经对赋值运算符进行了重载:

1 A& operator=(const A& other)
2 {
3     cout << "assignment operator" << endl;
4     value = other.value;
5
6     return *this;
7 }

  为了便于说明,我们用一个新的例子(我们极容易写出这样的代码):

 1 class CMyString
 2 {
 3 public:
 4     CMyString(char *ptr = nullptr);
 5     CMyString(const CMyString &str);
 6     ~CMyString();
 7     CMyString& operator=(const CMyString &str);
 8
 9 private:
10     char *pData;
11 };
12
13 CMyString& CMyString::operator=(const CMyString& str)
14 {
15     pData = str.pData;
16     return *this;
17 }

   这个赋值运算符重载函数存在的问题如下:

  1)浅拷贝;

  2)没有(检查)释放实例自身已有的内存。如果我们忘记在分配新内存之前释放自身已有的空间,程序将出现内存泄漏;

  3)没有判断穿入的参数和当前的实例(*this)是不是同一个实例。如果是同一个,则不进行复制操作,直接返回。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此在也找不到需要赋值的内容了。

  修改之后的赋值运算符重载函数如下:

 1 CMyString& CMyString::operator=(const CMyString& str)
 2 {
 3     if (this == &str)
 4         return *this;
 5
 6     delete []pData;
 7     pData = nullptr;
 8
 9     pData = new char[strlen(str.pData) + 1];
10     strcpy(pData, str.pData);
11
12     return *this;
13 }

  上述代码现在的问题在于4)异常安全性,即new可能会抛出异常,而我们却没有处理!所以我们可以将程序继续修改:

 1 CMyString& CMyString::operator=(const CMyString& str)
 2 {
 3     if (this == &str)
 4         return *this;
 5
 6     char *tmp = new(nothrow) char[strlen(str.pData) + 1];
 7     if (tmp == nullptr)
 8         return *this;
 9
10     strcpy(tmp, str.pData);
11
12     delete []pData;
13     pData = tmp;
14     tmp = nullptr;
15
16     return *this;
17 }

  除了前边提到的4个点,赋值运算符重载还有两点需要注意:

  5)是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(即*this)。只有返回一个引用,才可以允许连续赋值。否则如果函数的返回值是void,应用该赋值将不能做连续赋值。假设有3个CMyString对象:str1、str2和str3,在程序中语句str1=str2=str3将不能通过编译。

  6)是否把传入的参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次拷贝构造函数。把参数声明为引用可以避免这样的无谓消耗,从而提高代码效率。同时,我们在赋值运算符函数内不会修改传入的实例的状态,因此应该为传入的引用参数加上const关键字。

参考资料

  《剑指Offer》

  C++拷贝构造函数详解

  C++ 类访问控制public/private/protected探讨

时间: 2024-10-22 06:19:04

拷贝构造函数与赋值运算符重载函数要点的相关文章

第五篇:明确拒绝不想编译器自动生成的拷贝构造函数和赋值运算符重载函数

前言 如果你不想要编译器帮你自动生成的拷贝机制 (参考前文),那么你应当明确的拒绝. 如何拒绝?这便是本文要解决的主要问题. 问题描述 当你定义了一个类,而这个类中各对象之间也是封装的 - 禁止同类对象之间的相互赋值以及复制,那么你需要屏蔽掉编译器帮你生成的拷贝构造函数以及赋值运算符. 在许多代码中,会看到通过将拷贝构造函数和赋值运算符重载函数声明为私有且不予实现来实现这个功能.然而,这不是最科学的做法. 因为这没有做到真正的屏蔽:你在自己的成员函数中,或者友元函数中仍然可以调用这两个私有函数,

明确拒绝不想编译器自动生成的拷贝构造函数和赋值运算符重载函数

前言 如果你不想要编译器帮你自动生成的拷贝机制 (参考前文),那么你应当明确的拒绝. 如何拒绝?这便是本文要解决的主要问题. 问题描述 当你定义了一个类,而这个类中各对象之间也是封装的 - 禁止同类对象之间的相互赋值以及复制,那么你需要屏蔽掉编译器帮你生成的拷贝构造函数以及赋值运算符. 在许多代码中,会看到通过将拷贝构造函数和赋值运算符重载函数声明为私有且不予实现来实现这个功能.然而,这不是最科学的做法. 因为这没有做到真正的屏蔽:你在自己的成员函数中,或者友元函数中仍然可以调用这两个私有函数,

拷贝构造函数和赋值运算符重载的区别

拷贝构造函数是用一个已存在的对象去构造一个不存在的对象(拷贝构造函数毕竟还是构造函数嘛),也就是初始化一个对象.而赋值运算符重载函数是用一个存在的对象去给另一个已存在并初始化过(即已经过构造函数的初始化了)的对象进行赋值. 它们定义上的区别,楼上的已经说过了. 比如:String s1("hello"),s2=s1;//拷贝构造函数Sring s1("hello"),s2;s1=s2;//赋值运算符重载以下情况都会调用拷贝构造函数:1.一个对象以值传递的方式传入函数

c++拷贝构造函数、赋值运算符=重载、深拷贝与浅拷贝

 关键词:构造函数,浅拷贝,深拷贝,堆栈(stack),堆heap,赋值运算符 摘要: 在面向对象程序设计中,对象间的相互拷贝和赋值是经常进行的操作. 如果对象在申明的同时马上进行的初始化操作,则称之为拷贝运算.例如: class1 A("af"); class1 B=A; 此时其实际调用的是B(A)这样的浅拷贝操作. 如果对象在申明之后,在进行的赋值运算,我们称之为赋值运算.例如: class1 A("af"); class1 B; B=A; 此时实际调用的类

[QT入门篇]3 QObject的拷贝构造函数与赋值运算符

本文主要是针对QObject的拷贝构造函数和赋值运算符进行说明.先来看一下拷贝构造函数定义:拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建及初始化.其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制.此函数经常用在函数调用时用户定义类型的值传递及返回.拷贝构造函数要调用基类的拷贝构造函数和成员函数.如果可以的话,它将用常量方式调用,另外,也可以用非常量方式调用. 还记得<[QT入门篇]1 QT中的对象模型>中

不使用编译器自动生成的拷贝构造函数和赋值运算符的方法

方法1:声明私有的拷贝构造函数和赋值运算符,这样不但阻止了编译器生成默认版本,并且使得用户无法调用他们,但是这时成员函数和友元函数还是可以调用他们,为了阻止他们的调用可以不定义这些私有的拷贝构造函数和赋值运算符.(标准库中也是如此阻止拷贝的) 代码段1.1:HomeForSale.h文件 #ifndef HOMEFORSALE_H #define HOMEFORSALE_H class CHomeForSale { public: CHomeForSale(){} private: CHomeF

C++ 拷贝构造函数和赋值运算符

这篇文章主要介绍拷贝构造函数和赋值运算符的区别,以及在什么时候调用拷贝构造函数,什么情况下调用赋值运算符. 拷贝构造函数和赋值运算符 在默认情况下(用户没有定义,但是也没有显示的删除),编译器会自动隐式生成一个拷贝构造函数和赋值运算符,但用户可以使用delete来指定不生成拷贝构造函数和赋值运算符,这样的对象就不能通过值传递,也不能进行赋值运算 1 #include <iostream> 2 3 using namespace std; 4 class Person { 5 public: 6

关注C++细节——含有本类对象指针的类的构造函数、析构函数、拷贝构造函数、赋值运算符的例子

本例只是对含有本类对象指针的类的构造函数.析构函数.拷贝构造函数.复制运算符使用方法的一个简单示例,以加深对构造函数和拷贝控制成员的理解. 读C++ primer 5th 第13章后加上自己的理解,完整的写了下课后习题的代码. 第一版: #include <string> #include <iostream> using namespace std; class TreeNode{ private: string value; TreeNode *left; TreeNode *

C++ String类 ( 构造、拷贝构造、赋值运算符重载和析构函数)

class String { public: //普通构造函数 String(const char *str = NULL) { if(str == NULL) { m_data = new char[1]; *m_data = '\0'; } else { m_data = new char[strlen(str) + 1]; strcpy(m_data, str); } } //拷贝构造函数 String(const String &s) { m_data = new char[strlen