浅谈C++类的拷贝控制

Posted Coder学习路

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈C++类的拷贝控制相关的知识,希望对你有一定的参考价值。

点击蓝字


在C++语言的学习过程中,类的拷贝控制是一个较为繁杂的知识点。虽然它的难度不是很大,但是细节很多,需要记忆。本文简略地介绍一下C++类的拷贝控制基本内容,即拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数这 5 个函数的写法。


为了便于介绍,我们自己实现一个简单的 string 类,命名为 String 。它只包含一个私有的数据成员:char *data ,函数成员包括:两个构造函数和五大拷贝控制函数。


类的定义如下:


class String{private:    char *data;
public:    String() : data(nullptr) {}
   String(const char *s);
   // 拷贝构造函数    String(const String &s);
   // 拷贝赋值运算符    String &operator=(const String &s);
   // 移动构造函数    String(String &&s);
   // 移动赋值运算符    String &operator=(String &&s);
   // 析构函数释放内存    ~String();};


为了介绍五大拷贝控制函数,还需要先看类的两个构造函数。


第一个为默认构造函数,仅仅把 data 赋值为空指针。


第二个构造函数接收一个 const char * 类型指针,动态申请内存并从参数中拷贝构造字符串。代码实现如下:


    String(const char *s)    {        int length = strlen(s);        data = new char[length + 1];        if (data)        { // 拷贝            strcpy(data, s);            data[length] = '\0'; // 尾 0        }        else        {            cout << "Error!" << endl;            exit(-1);        }    }


接下来正式进入拷贝控制的内容。


先从最简单的析构函数开始:


1、析构函数


析构函数的声明形式为:

~ClassName() { /* 函数体 */ }


析构函数一般进行类的资源释放,对于本String 类,需要释放 data 指针对应的内存,代码如下:


    // 析构函数释放内存    ~String()    {        if (data)            delete[] data;    }


何时调用析构函数:


  1. 变量离开作用域时
  2. 当一个对象被销毁时,其成员被销毁
  3. 容器(标准库容器以及数组)被销毁时,其元素被销毁
  4. 动态分配对象被 delete 时
  5. 对于临时对象,当创建它的完整表达式结束时被销毁



2、拷贝构造函数


拷贝构造函数声明形式为:


ClassName(const ClassName &s) {  /* 函数体 */   }


注意点:


  1. 拷贝构造函数是构造函数, 无返回类型
  2. 拷贝构造函数的参数类型为 const 引用类型
  3. 拷贝构造函数一般需要动态申请内存,进行深拷贝工作,对于本 String 类,需要申请内存,并从参数字符串中拷贝数据


String 类的拷贝构造函数实现如下:


    // 拷贝构造函数    String(const String &s)    {        cout << "拷贝构造函数"             << "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;        if (s.data)        {            int length = strlen(s.data);            data = new char[length + 1]; // 分配内存空间            if (data)            {                strcpy(data, s.data);                data[length] = '\0';            }            else            {                cout << "Error!" << endl;                exit(-1);            }        }        else        {            data = nullptr;        }    }


何时调用拷贝构造函数:


  1. 用 = 定义变量时
  2. 将一个对象作为实参传递给一个非引用类型的形参(函数调用以值传递)
  3. 从一个返回类型为非引用类型的函数返回一个对象(函数返回)
  4. 用花括号列表初始化一个数组中的元素或者一个聚合类中的成员



3、拷贝赋值运算符


拷贝赋值运算符声明形式为:


ClassName & operator= (const ClassName &s) {  /* 函数体 */   }


注意点:


  1. 拷贝赋值运算符是一种运算符重载,可以看作名字为:operator= 的函数,其 参数类型为 const 引用类型, 返回类型为引用类型
  2. 拷贝赋值运算符一般需要执行析构函数和构造函数的功能
  3. 需要考虑自赋值情况,即:a = a ,确保自赋值情况拷贝赋值运算符正确执行。为了保证自赋值正确执行,一般采取:先申明临时变量,然后将参数拷贝到临时变量,最后将临时变量拷贝到本对象 的步骤


String 类的拷贝赋值运算符实现如下:


    // 拷贝赋值运算符    String &operator=(const String &s)    {        cout << "拷贝赋值运算符"             << "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;        int length = strlen(s.data);        char *temp = new char[length + 1]; // 分配内存空间        if (temp)        {            strcpy(temp, s.data);            temp[length] = '\0';            delete[] data; // 释放内存空间            data = temp;        }        else        {            exit(-1);        }        return *this;    }


何时调用拷贝赋值运算符:


  1. 赋值符号右侧为一个左值时



4、移动构造函数


移动构造函数声明形式为:


ClassName(ClassName &&s) {  /* 函数体 */   }


注意点:


  1. 移动构造函数属于构造函数,无返回类型。其 参数类型为右值引用, 不取const 类型
  2. 移动构造函数的特性在于“移动”,它一般执行“浅拷贝”,即不需要动态申请内存,而是将 s 的动态内存指针赋值给本对象的指针,然后将 s 的指针赋值为空


String类的移动构造函数实现:


    // 移动构造函数    String(String &&s)    {        cout << "移动构造函数"             << "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;        if (this != &s) // 排除自赋值情况        {            if (data)                delete[] data; // 释放本对象内存            data = s.data;     // 直接赋值 不申请内存            s.data = nullptr;  // 将赋值运算符左侧对象指针置为空        }    }


何时调用移动构造函数:


  1. 显式地从一个右值进行构造时,例如:利用标准库 move 函数返回一个右值进行构造
  2. 利用移动迭代器构造时,具体内容请自行查阅相关资料



5、移动赋值运算符


移动赋值运算符声明形式为:


ClassName &operator= ( ClassName &&s) {  /* 函数体 */   }


注意点:


  1. 移动赋值运算符同样是一种运算符重载,其返回类型为类的引用类型,参数类型为类的右值引用, 不是 const 类型
  2. 移动赋值运算符特性同样在“移动”二字,它不执行拷贝,执行的动作一般拷贝析构本对象原有的动态内存,然后将参数对象动态内存指针赋值给本对象内指针,最后将参数对象指针赋值为空。正因为该函数需要改变参数,因此不申明为 const 类型


String 类移动赋值运算符实现:


    // 移动赋值运算符    String &operator=(String &&s)    {        cout << "移动赋值运算符"             << "\t" << __LINE__ << ":\t" << __FUNCTION__ << endl;        if (this != &s) // 排除自赋值情况        {            if (data)                delete[] data; // 释放本对象内存            data = s.data;     // 直接赋值 不申请内存            s.data = nullptr;  // 将赋值运算符左侧对象指针置为空        }        return *this;    }



何时调用移动赋值运算符:


  1. 赋值符号右侧为一个右值时


测试函数:


int main(){    String s1;                  // 默认构造函数    String s2("I'm String.\n"); // 由字符串构造
   String s3 = s2;      // 拷贝构造函数    s1 = s2;             // 拷贝赋值运算符    String s4(move(s1)); // 移动构造函数    s2 = String("hhh");  // 移动赋值运算符
   return 0;}



运行结果:



备注:


  • 本文主要参考书籍:《C++  Primer 第五版》 第 13 章拷贝控制。

  • 拷贝控制是学习 C++ 过程中的一个大坑,尤其是移动构造与移动赋值运算符,这二者涉及标准库的实现,若代码编写正确,能很大程度减少拷贝,提高程序运行效率;若代码编写有bug,可能导致runtime error等错误。此外编译器优化、编译器版本支持等也可能使程序运行结果与预期不同。上文仅是最简略的介绍,更多内容请阅读书籍。