天天看點

【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、指派操作符和拷貝構造函數有同等重要的意義

繼續閱讀