天天看点

【C++深度解析】24、赋值运算符与深拷贝

文章目录

    • 1 赋值运算符与深拷贝
      • 1.1 注意事项
    • 2 重载赋值操作符优化数组类
    • 3 小结

前面我们学习了操作符重载:

在 【C++深度解析】20、操作符重载中讲了操作符可以重载为成员函数和非成员函数,并重载运算操作符:+,-,*,/ ,比较操作符:==,!= ,和赋值操作符:=,实现了复数类。

在 【C++深度解析】22、数组操作符重载 中讲解了了重载数组访问操作符,并在数组类中重载数组访问操作符,可以对函数对象直接使用下标访问数组元素。

在 【C++深度解析】23、函数对象分析(重载函数调用操作符())中实现了重载函数调用操作符(),使得对象可以像函数调用一样使用。

1 赋值运算符与深拷贝

我们现在有一个问题:什么时候需要重载赋值运算符?

  • 编译器为每个类默认重载了赋值运算符,仅完成浅拷贝
  • 需要深拷贝时必须重载赋值操作符
  • 赋值操作符与拷贝构造函数有相同的存在意义,一起实现

下面我们进行编程实验看一下深拷贝的意义:

#include <iostream>
#include <string>
using namespace std;
class Test
{
    int* m_pointer;
public:
    Test()
    {
        m_pointer = NULL;
    }
    Test(int i)
    {
        m_pointer = new int(i);
    }
    void print()
    {
        cout << "m_pointer = " << hex << m_pointer << endl;
    }
    ~Test()
    {
        delete m_pointer;
    }
};
int main()
{
    Test t1 = 1;
    Test t2;
    t2 = t1;
    t1.print();
    t2.print();
    return 0;
}
           

上面的代码很简单,t2 = t1 时,使用编译器默认的重载赋值运算符,这里是浅拷贝,t1 和 t2 中的 m_pointer 都指向同一块内存空间,t1,t2 生存期结束时,调用析构函数,释放两次空间,会引起程序崩溃。

如何修改呢,就是自己实现拷贝构造函数和赋值运算符,实现深拷贝。

#include <iostream>
#include <string>
using namespace std;
class Test
{
    int* m_pointer;
public:
    Test()
    {
        m_pointer = NULL;
    }
    Test(int i)
    {
        m_pointer = new int(i);
    }
    Test(const Test& obj)
    {
        m_pointer = new int(*obj.m_pointer);
    }
    Test& operator = (const Test& obj)			// 返回值为类型引用,参数为const类型的引用
    {
        if (this != &obj)						// 相等判断,避免自我赋值
        {
            delete m_pointer;
            m_pointer = new int(*obj.m_pointer);
        }
        return *this;							// 返回 *this
    }
    void print()
    {
        cout << "m_pointer = " << hex << m_pointer << endl;
    }
    ~Test()
    {
        delete m_pointer;
    }
};
int main()
{
    Test t1 = 1;
    Test t2;
    t2 = t1;
    t1.print();
    t2.print();
    return 0;
}
           

使用深拷贝,二者的地址空间不同,两次析构释放的是不同的地址空间

$ g++ 24-1.cpp -o 24-1
$ ./24-1
m_pointer = 0x90db008
m_pointer = 0x90db018
           

赋值操作符与拷贝构造函数有相同的存在意义,使用场合不同,t2 = t1 使用赋值操作符,t2(t1) 使用拷贝构造函数。意义是相同的。

1.1 注意事项

重载赋值操作符需要注意四点(非常重要):

  1. 返回类型为类名的引用,这样可以连续赋值
  2. 参数为 const 类名的引用
  3. 判断相等,避免自我赋值
  4. 返回值为 *this

2 重载赋值操作符优化数组类

在博客【C++深度解析】12、构造函数、拷贝构造函数和析构函数中我们实现了数组类

在【C++深度解析】17、二阶构造模式中使用二阶构造模式进行改进,避免构造失败而形成半成品对象。

在【C++深度解析】22、数组操作符重载中重载数组操作符,这样可以直接对对象使用下标访问数组元素。

数组类中有指针会申请内存空间,拷贝时发生的是浅拷贝,容易造成内存错误,这里我们重载赋值操作符,实现深拷贝。另外这里的拷贝构造函数为私有,类外无法调用,不需要重载了。

// IntArray.h
#ifndef _INTARRAY_H_
#define _INRARRAY_H_
class IntArray
{
private:
    IntArray(int len);
    bool construct();
    IntArray(const IntArray& obj);
    int m_length;
    int* m_pointer;
public:
    static IntArray* NewInstance(int length);
    int length();
    bool get(int index, int& value);
    bool set(int index, int value);
    int& operator [] (int index);
    IntArray& operator = (const IntArray& obj);
    IntArray& self();
    ~IntArray();
};
#endif
           
#include"IntArray.h"
#include<stdio.h>
IntArray::IntArray(int len)
{
    m_length = len;
}
bool IntArray::construct()
{
    bool ret = true;
    m_pointer = new int[m_length];
    if (m_pointer)
    {
        for (int i = 0; i < m_length; i++)
        {
            m_pointer[i] = 0;
        }
    }
    else
    {
        ret = false;
    }
    return ret;
}
IntArray* IntArray::NewInstance(int length)
{
    IntArray* ret = new IntArray(length);
    if (!(ret && ret->construct()))
    {
        delete ret;
        ret = NULL;
    }
    return ret;
}
int IntArray::length()
{
    return m_length;
}
bool IntArray::get(int index, int& value)
{
    bool ret = (index >= 0 && index < m_length);
    if (ret)
    {
        value = m_pointer[index];
    }
    return ret;
}
bool IntArray::set(int index, int value)
{
    bool ret = (index >= 0 && index < m_length);
    if (ret)
    {
        m_pointer[index] = value;
    }
    return ret;
}
int& IntArray::operator [] (int index)
{
    return m_pointer[index];
}
IntArray& IntArray::operator = (const IntArray& obj)
{
    if (this != &obj)
    {
        int* pointer = new int[obj.m_length];
        if (pointer)
        {
            for (int i = 0; i < obj.m_length; i++)
            {
                pointer[i] = obj.m_pointer[i];
            }
            m_length = obj.m_length;
            delete[] m_pointer;
            m_pointer = pointer;
        }
    }
    return *this;
}
IntArray& IntArray::self()
{
    return *this;
}
IntArray::~IntArray()
{
    delete[]m_pointer;
}
           
// 24-2.cpp
#include<iostream>
#include"IntArray.h"
using namespace std;
int main()
{
    IntArray* a = IntArray::NewInstance(5);
    IntArray* b = IntArray::NewInstance(10);
    if (a && b)
    {
        IntArray& array = a->self();
        IntArray& brray = b->self();
        cout << "array.length = " << array.length() << endl;
        cout << "brray.length = " << brray.length() << endl;
        array = brray;
        cout << "array.length = " << array.length() << endl;
        cout << "brray.length = " << brray.length() << endl;
    }
    delete a;
    delete b;
    return 0;
}
           

重载赋值运算符实现深拷贝,这样对象使用赋值符号赋值就不会引起内存错误了。因为 不同对象指向的是不同的地址空间。

$ g++ 24-2.cpp IntArray.cpp -o 24-2
$ ./24-2
array.length = 5
brray.length = 10
array.length = 10
brray.length = 10
           

3 小结

1、需要深拷贝时必须重载赋值操作符

2、赋值操作符和拷贝构造函数有同等重要的意义

继续阅读