C++運算符重載-下篇

C++運算符重載-下篇

本章內容:
1. 運算符重載的概述
2. 重載算術運算符
3. 重載按位運算符和二元邏輯運算符
4. 重載插入運算符和提取運算符
5. 重載下標運算符
6. 重載函數調用運算符
7. 重載解除引用運算符
8. 編寫轉換運算符
9. 重載內存分配和釋放運算符

5. 重載下標運算符

  • 本節假設你沒有聽說過STL中的vector或array的模板,我們來自己實現一個動態分配的數組類。這個類允許設置和獲取指定索引位置的元素,并自動完成所有的內存分配操作。一個動態分配數組的定義類如下所示:

      template <typename T>
      class Array
      {
      public:
          // 創建一個可以按需要增長的設置了初始化大小的數組
          Array();
          virtual ~Array();
    
          // 不允許分配和按值傳遞
          Array<T>& operator=(const Array<T>& rhs) = delete;      // C++11 禁用賦值函數重載
          Array(const Array<T>& src) = delete;                    // C++11 禁用拷貝構造函數
          
          // 返回下標x對應的值,如果下標x不存在,則拋出超出范圍的異常。
          T getElementAt(size_t x) const;
    
          // 設置下標x的值為val。如果下標x超出范圍,則分配空間使下標在范圍內。
          void setElementAt(size_t x, const T& val);
      private:
          static const size_t kAllocSize = 4;
          void resize(size_t newSize);
          // 初始化所有元素為0
          void initializeElement();
          T *mElems;
          size_t mSize;
      };
    
  • 這個接口支持設置和訪問元素。它提供了隨機訪問的保證:客戶可以創建數組,并設置元素1、100和1000,而不必考慮內存管理的問題。

  • 下面是這些方法的實現:

      template <typename T> Array<T>::Array()
      {
          mSize = kAllocSize;
          mElems = new T[mSize];
          initializeElements();
      }
    
      template <typename T> Array<T>::~Array()
      {
          delete[] mElems;
          mElems = nullptr;
      }
    
      template <typename T> void Array<T>::initializeElements()
      {
          for (size_t i=0; i<mSize; i++)
          {
              mElems[i] = T();
          }
      }
    
      template <typename T> void Array<T>::resize(size_t newSize)
      {
          // 拷貝一份當前數組的指針和大小
          T *oldElems = mElems;
          size_t oldSize = mSize;
          // 創建一個更大的數組
          mSize = newSize;            // 存儲新的大小
          mElems = new T[newSize];    // 給數組分配新的newSize大小空間
          initializeElements();       // 初始化元素為0
          // 新的size肯定大于原來的size大小
          for (size_t i=0; i < oldSize; i++)
          {
              // 從老的數組中拷貝oldSize個元素到新的數組中
              mElems[i] = oldElems[i];
          }
          delete[] oldElems;          // 釋放oldElems的內存空間
          oldElems = nullptr;
      }
    
      template <typename T> T Array<T>::getElementAt(size_t x) const
      {
          if (x >= mSize)
          {
              throw std::out_of_range("");
          }
          return mElems[x];
      }
    
      template <typename T> void Array<T>::setElementAt(size_t x, const T& val)
      {
          if (x >= mSize)
          {
              // 在kAllocSize的基礎上給數組重新分配客戶需要的空間大小
              resize(x + kAllocSize);
          }
          mElems[x] = val;
      }
    
  • 下面是使用這個類的例子:

      Array<int> myArray;
      for (size_t i=0; i<10; i++)
      {
          myArray.setElementAt(i, 100);
      }
      for (size_t j=0; i< 10; j++)
      {
          cout << myArray.getElementAt(j) << " ";
      }
    
  • 從中可以看出,我們不需要告訴數組需要多少空間。數組會分配保存給定元素所需要的足夠空間,但是總是使用setElementAt()getElementAt()方法不是太方便。于是我們想像下面的代碼一樣,使用數組的索引來表示:

      Array<int> myArray;
      for (size_t i=0; i<100; i++)
      {
          myArray[i] = 100;
      }
      for (size_t j=0; j<10; j++)
      {
          cout << myArray[j] << " ";
      }
    
  • 要使用下標方法,則需要使用重載的下標運算符。通過以下方式給類添加operator[]

      template <typename T> T& Array<T>::operator[] (size_t x)
      {
          if (x >= mSize)
          {
              // 在kAllocSize的基礎上給數組重新分配客戶需要的空間大小
              resize(x + kAllocSize);
          }
          return mElems[x];
      }
    
  • 現在,上面使用數組索引表示法的代碼可以正常使用了。operator[]可以設置和獲取元素,因為它返回的是位置x處的元素的索引。可以通過這個引用對這個元素賦值。當operator[]用在賦值語句的左側時,賦值操作實際上修改了mElems數組中位置x處的值。

5.1 通過operator[]提供只讀訪問

  • 盡管有時operator[]返回可以作為左值的元素會很方便,但并非總是需要這種行為。最好還能返回const值或const引用,提供對數組中元素的只讀訪問。理想情況下,可以提供兩個operator[]:一個返回引用,另一個返回const引用。示例代碼如下:

      T& operator[] (size_t x);
      const T& operator[] (size_t x);     // 錯誤,不能基于返回類型來重載(overload)該方法。
    
  • 然而,這里存在一個問題:不能僅基于返回類型來重載方法或運算符。因此,上述代碼無法編譯。C++提供了一種繞過這個限制的方法:如果給第二個operator[]標記特性const,編譯器就能區別這兩個版本。如果對const對象調用operator[],編譯器就會使用const operator[];如果對非const對象調用operator[],編譯器會使用非constoperator[]。下面是這兩個運算符的正確原型:

      T& operator[] (size_t x);
      const T& operator[] (size_t x) const;
    
  • 下面是const operator[]的實現:如果索引超出了范圍,這個運算符不會分配新的內存空間,而是拋出異常。如果只是讀取元素值,那么分配新的空間就沒有意義了:

      template <typename T> const T& Array<T>::operator[] (size_t x) const
      {
          if (x >= mSize)
          {
              throw std::out_of_range("");
          }
          return mElems[x];
      }
    
  • 下面的代碼演示了這兩種形式的operator[]

      void printArray(const Array<int>& arr, size_t size);
      int main()
      {
          Array<int> myArray;
          for (size_t i=0; i<10; i++)
          {
              myArray[i] = 100;           // 調用non-const operator[],因為myArray是一個non-const對象
          }
          printArray(myArray, 10);
          return 0;
      }
    
      void printArray(const Array<int>& arr, size_t size)
      {
          for (size_t i=0; i<size; i++)
          {
              cout << arr[i] << "";       //調用const operator[],因為arr是一個const對象
          }
          count << endl;
      }
    
  • 注意,僅僅是因為arr是const,所以printArray()中調用的是const operator[]。如果arr不是const,則調用的是非const operator[],盡管事實上并沒有修改結果值。

5.2 非整數數組索引

  • 這個是通過提供某種類型的鍵,對一個集合進行“索引”的范例的自然延伸;vector(或更廣義的任何線性數組)是一種特例,其中的“鍵”只是數組中的位置。將operator[]的參數看成提供兩個域之間的映射:鍵域到值域的映射。因此,可編寫一個將任意類型作為索引的operator[]。這個類型未必是整數類型。STL的關聯容器就是這么做的,例如:std::map

  • 例如,可以創建一個關聯數組,其中使用string而不是整數作為鍵。下面是關聯數組的定義:

      template <typename T>
      class AssociativeArray
      {
      public:
          AssociativeArray();
          virtual ~AssociativeArray();
          T& operator[] (const std::string& key) const;
          const T& operator[] (const std::string& key) const;
      private:
          // 具體實現部分省略……
      }
    
  • 注意:不能重載下標運算符以便接受多個參數,如果要提供接受多個索引下標的訪問,可以使用函數調用運算符。

6. 重載函數調用運算符

  • C++允許重載函數調用運算符,寫作operator()。如果自定義類中編寫一個operator(),那么這個類的對象就可以當做函數指針使用。只能將這個運算符重載為類中的非static方法。下面的例子是一個簡單的類,它帶有一個重載的operator()以及一個具有相同行為的方法:

      class FunctionObject
      {
      public:
          int operator() (int inParam);   // 函數調用運算符
          int doSquare(int inParam);      // 普通方法函數
      };
    
      // 實現重載的函數調用運算符
      int FunctionObject::operator() (int inParam);
      {
          return inParam * inParam;
      }
    
  • 下面是使用函數調用運算符的代碼示例,注意和類的普通方法調用進行比較:

      int x = 3, xSquared, xSquaredAgain;
      FunctionObject square;
      xSquared = square(x);                   // 調用函數調用運算符
      xSquaredAgain = square.doSquare(x);     // 調用普通方法函數
    
  • 帶有函數調用運算符的類的對象稱為函數對象,或簡稱為仿函數(functor)。

  • 函數調用運算符看上去有點奇怪,為什么要為類編寫一個特殊方法,使這個類的對象看上去像函數指針?為什么不直接編寫一個函數或標準的類的方法?相比標準的對象方法,函數函數對象的好處如下:這些對象有時可以偽裝為函數指針。只要函數指針類型是模板化的,就可以把這些函數對象當成回調函數傳入需要接受的函數指針的例程。

  • 相比全局函數,函數對象的好處更加復雜,主要有兩個好處:

  • (1)對象可以在函數對象運算符的重復調用之間,在數據數據成員中保存信息。例如,函數對象可以用于記錄每次通過函數調用運算符調用采集到的數字的連續總和。

  • (2)可以通過設置數據成員來自定義函數對象的行為。例如,可以編寫一個函數對象,來比較函數參數和數據成員的值。這個數據成員是可配置的,因此這個對象可以自定義為執行任何比較操作。

  • 當然,通過全局變量或靜態變量都可以實現上述任何好處。然而,函數對象提供了一種更簡潔的方式,而使用全局變量或靜態變量在多線程應用程序中可能會產生問題。

  • 通過遵循一般的方法重載規則,可為類編寫任意數量的operator()。確切的講,不同的operator()必須有不同數目的參數或不同類型的參數。例如,可以向FunctionObject類添加一個帶string引用參數的operator()

      int operator() (int inParam);
      void operator() (string& str);
    
  • 函數調用運算符還可以用于提供數組的多重索引的下標。只要編寫一個行為類似于operator[],但接受多個參數的operator()即可。這項技術的唯一問題是需要使用()而不是[]進行索引,例如myArray(3, 4) = 6

7. 重載解除引用運算符

  • 可以重載3個解除引用運算符:*、->、->*。目前不考慮->(在后面的章節有討論),該節只考慮*和->的原始意義。解除對指針的引用,允許直接訪問這個指針指向的值,->是*解除引用之后再接.成員選擇操作的簡寫。下面的代碼演示了這兩者的一致性:

      SpreadsheetCell* cell = new SpreadsheetCell;
      (*cell).set(5);     // 解除引用加成員函數調用
      cell->set(5);       // 單箭頭解除引用和成員函數調用
    
  • 在類中重載解除引用運算符,可以使這個類的對象行為和指針一致。這種能力的主要用途是實現智能指針,還能用于STL使用的迭代器。本節通過智能指針類模板的例子,講解重載相關運算符的基本機制。

  • 警告:C++有兩個標準的智能指針:std::shared_ptr和std::unique_ptr。強烈使用這些標準的智能指針而不是自己編寫。本節列舉的例子是為了演示如何編寫解除引用運算符。

  • 下面是這個示例智能指針類模板的定義,其中還沒有填入解引用運算符:

      template <typename T> class Pointer
      {
      public:
          Pointer(T* inPtr);
          virtual ~Pointer();
          // 阻止賦值和按值傳值
          Pointer(const Pointer<T>& src) = delete;                // C++11 禁用拷貝構造函數
          Pointer<T>& operator=(const Pointer<T>& rhs) = delete;  // C++11 禁用賦值函數重載
    
          // 解引用運算符將會在這里
      private:
          T* mPtr;
      };
    
  • 這個智能指針只是保存了一個普通指針,在智能指針銷毀時,刪除這個指針指向的存儲空間。這個實現同樣十分簡單:構造函數接受一個真正的指針(普通指針),該指針保存為類中僅有的數據成員。析構函數釋放這個指針引用的存儲空間。

      template <typename T> Pointer<T>::Pointer(T* inPtr) : mPtr(inPtr);
      {
      }
      template <typename T> Pointer<T>::~Pointer()
      {
          delete mPtr;
          mPtr = nullptr;
      }
    
  • 可以采用以下方式使用這個智能指針模板:

      Pointer<int> smartInt(new int);
      *smartInt = 5;                  //智能指針解引用
      cout << *smartInt << endl;
      Pointer<SpreadsheetCell> smartCell(new SpreadsheetCell);
      smartCell->set(5);              //解引用同時調用set方法
      cout << smartCell->getValue() << endl;
    
  • 從這個例子可以看出,這個類必須提供operator*operator->的實現。其實現部分在下兩節中講解。

7.1 實現operator*

  • 當解除對指針的引用時,常常希望能訪問這個指針指向的內存。如果那塊內存包含了一個簡單類型,例如int,應該可以直接修改這個值。如果內存中包含了復雜的類型,例如對象,那么應該能通過.運算符訪問它的數據成員或方法。

  • 為了提供這些語義,operator*應該返回一個變量或對象的引用。在Pointer類中,聲明和定義如下所示:

      template <typename T> class Pointer
      {
      public:
          // 構造部分同上,所以省略
          T& operator*();
          const T& operator*() const;
          // 其它部分暫時省略
      };
      template <typename T> T& Pointer<T>::operator*()
      {
          return *mPtr;
      }
      template <typename T> const T& Pointer<T>::operator*() const
      {
          return *mPtr;
      }
    
  • 從這個例子中可以看出,operator*返回的是底層普通指針指向的對象或變量的引用。與重載下標運算符一樣,同時提供方法的const版本合非const版本也很有用,這兩個版本分別返回const引用和非const引用。

7.2 實現operator->

  • 箭頭運算符稍微復雜一些,應用箭頭運算符的結果應該是對象的一個成員或方法。然而,為了實現這一點,應該要實現operator*operator.;而C++有充足的理由不實現運算符operator.:不可能編寫單個原型,來捕捉任何可能選擇的成員或方法。因此,C++將operator->當成一個特例。例如下面的這行代碼:

      smartCell->set(5);
    
  • C++將這行代碼解釋為:

      (smartCell.operator->())->set(5);
    
  • 從中可以看出,C++給重載的operator->返回的任何結果應用了另一個operator->。因此,必須返回一個指向對象的指針,如下所示:

      template <typename T> class Pointer
      {
      public:
          // 省略構造函數部分
          T* operator->();
          const T* operator->() const;
          // 其它部分省略
      };
      template <typename T> T* Pointer<T>::operator->()
      {
          return mPtr;
      }
      template <typename T> const T* Pointer<T>::operator->() const
      {
          return mPtr;
      }
    

7.3 operator->*的含義

  • 在C++中,獲得類成員和方法的地址,以獲得指向這些成員和方法的指針是完全合法的。然而,不能在沒有對象的情況下訪問非static數據成員或調用非static方法。類數據成員和方法的重點在于它們依附于對象。因此,通過指針調用方法和訪問數據成員時,必須在對象的上下文中解除這個指針的引用。下面的例子說明了.和->運算符:

      SpreadsheetCell myCell;
      double (SpreadsheetCell::*methodPtr)() const = &SpreadsheetCell::getValue;
      cout << (myCell.*methodPtr)() << endl;
    
  • 注意,.*運算符解除對方法指針的引用并調用這個方法。如果有一個指向對象的指針而不是對象本身,還有一個等效的operator->*可以通過指針調用方法。這個運算符如下所示:

      SpreadsheetCell *myCell = new SpreadsheetCell();
      double (SpreadsheetCell::*methodPtr)() const = &SpreadsheetCell::getValue();
      cout << (myCell->*methodPtr)() << endl;
    
  • C++不允許重載operator.*(就像不允許重載operator.一樣),但是可以重載operator->*。然而這個運算符的重載非常復雜,標準庫中的share_ptr模板也沒有重載operator->*

8. 編寫轉換運算符

  • 回到SpreadsheetCell例子,考慮如下兩行代碼:

      SpreadsheetCell cell(1.23);
      string str = cell;          //不能編譯通過
    
  • SpreadsheetCell包含一個字符串表達式,因此將SpreadsheetCell賦值給string變量看上去是符合邏輯的。但不能這么做,編譯器會表示不知道如何將SpreadsheetCell轉換為string。你可能會通過下述方式迫使編譯器進行這種轉換:

      string str = (string)cell;  //仍然不能編譯通過
    
  • 首先,上述代碼依然無法編譯,因為編譯器還是不知道如何將SpreadsheetCell轉換為string。從這行代碼中編譯器已經知道你想讓編譯器做轉換,所以編譯器如果知道如何轉換,就會進行轉換。其次,一般情況下,最好不要在程序中添加這種無理由的類型轉換。如果想允許這類賦值,必須告訴編譯器如何執行它。也就是說,可編寫一個將SpreadsheetCell轉換為string的轉換運算符。其原型如下:

      operator std::string() const;
    
  • 函數名為operator std::string。它沒有返回類型,因為返回類型是通過運算符的名稱確定的:std::string。這個函數時const,因為這個函數不會修改被調用的對象。實現如下:

      SpreadsheetCell::operator string() const
      {
          return mString;
      }
    
  • 這就完成了從SpreadsheetCell到string的轉換運算符的編寫。現在的編譯器可以接受下面這行代碼,并在運行時正確的操作。

      SpreadsheetCell cell(1.23);
      string str = cell;          //按照預期的執行
    
  • 可以同樣的語法編寫任何類型的轉換運算符。例如,下面是從SpreadsheetCell到double的轉換運算符:

      SpreadsheetCell::operator double() const
      {
          return mValue;
      }
    
  • 現在可以編寫以下代碼:

      SpreadsheetCell cell(1.23);
      double d1 = cell;
    

8.1 轉換運算符的多義性問題

  • 注意,為SpreadsheetCell對象編寫double轉換運算符時會引入多義性問題。例如下面這行代碼:

      SpreadsheetCell cell(1.23);
      double d2 = cell + 3.3;     // 不能編譯通過,如果你已經重載了operator double()
    
  • 現在這一行無法成功編譯。在編寫運算符double()之前,這行代碼可以編譯,那么現在出現了什么問題?問題在于,編譯器不知道應該通過operator double()cell轉換為double,再執行double加法,還是通過double構造函數將3.3轉換為SpreadsheetCell,再執行SpreadsheetCell加法。在編寫operator double()之前,編譯器只有一個選擇:通過double構造函數將3.3轉換為SpreadsheetCell,再執行SpreadsheetCell加法。然而,現在編譯器可以執行兩種操作,存在二義性,所以編譯器便報錯。

  • 在C++11之前,通常解決這個難題的方法是將構造函數標記為explicit,以避免使用這個構造函數進行自動轉換。然而,我們不想把這個構造函數標記為explicit,通常希望進行從doubleSpreadsheetCell的自動類型轉換。自C++11以后,可以將double類型轉換運算符標記為explicit,來解決這個問題:

      explicit operator double() const;
    
  • 下面的代碼演示了這種方法的應用:

      SpreadsheetCell cell = 6.6;                     // [1]
      string str = cell;                              // [2]
      double d1 = static_cast<double>(cell);          // [3]
      double d2 = static_cast<double>(cell + 3.3);    // [4]
    
  • 下面解釋了上述代碼中的各行:

  • [1]使用隱式類型轉換從double轉換到SpreadsheetCell。由于這是在聲明中,所以這個是通過調用接受double參數的構造函數進行的。

  • [2]使用了operator string()轉換運算符。

  • [3]使用了operator double()轉換運算符。注意,由于這個轉換運算符現在聲明為explicit,所以要求強制類型轉換。

  • [4]通過隱式類型轉換將3.3轉換為SpreadsheetCell,再進行兩個SpreadsheetCelloperator+操作,之后進行必要的顯式類型轉換來調用operator double()

8.2 用于布爾表達式的轉換

  • 有時,能將對象用在布爾表達式中會非常有用。例如,程序員常常在條件語句中這樣使用指針:

      if (prt != nullptr) { /* 執行一些解除引用的操作 */}
    
  • 有時候程序員會編寫這樣的簡寫條件:

      if (prt) { /* 執行一些解除引用的操作 */}
    
  • 有時還能看到這樣的代碼:

      if (!prt) { /* 執行一些操作 */}
    
  • 目前,上述任何表達式都不能和此前定義的Pointer智能指針類一起編譯。然而,可以給類添加一個轉換運算符,將它轉換為指針類型。然后,這個類型和nullptr的比較,以及單獨一個對象在if語句中的形式都會觸發這個對象向指針類型的轉換。轉換運算符常用的指針類型為void*,因為這個指針類型除了在布爾表達式中測試之外,不能執行其他操作。

      operator void*() const
      {
          return mPtr;
      }
    
  • 現在下面的代碼可以成功編譯,并能完成預期的任務:

      void process(Pointer<SpreadsheetCell>& p)
      {
          if (p != nullptr)
          {
              cout << "not nullptr" << endl;
          }
          if (p != NULL)
          {
              cout << "not NULL" << endl;
          }
          if (p)
          {
              cout << "not nullptr" << endl;
          }
          if (!p)
          {
              cout << "nullptr" << endl;
          }
      }
      int main()
      {
          Pointer<SpreadsheetCell> smartCell(nullptr);
          process(smartCell);
          cout << endl;
          Pointer<SpreadsheetCell> anotherSmartCell(new SpreadsheetCell(5.0));
          process(anotherSmartCell);
      }
    
  • 輸出結果如下所示:

      nullprt
      
      not nullptr
      not NULL
      not nullptr
    
  • 另一種方法是重載operator bool()而不是operator void*()。畢竟是在布爾表達式中使用對象,為什么不能直接轉換為bool呢?

      operator bool() const
      {
          return mPtr != nullptr;
      }
    
  • 下面的比較仍可以運行:

          if (p != NULL)
          {
              cout << "not NULL" << endl;
          }
          if (p)
          {
              cout << "not nullptr" << endl;
          }
          if (!p)
          {
              cout << "nullptr" << endl;
          }
    
  • 然而,使用operator bool()時,下面和nullptr的比較會導致編譯器錯誤:

      if (p != nullptr)   { cout << "not nullptr" << endl; } //Error
    
  • 這是正確的行為,因為nullptr有自己的類型nullptr_t,這個類型沒有自動類型轉換為整數0。編譯器找不到接受Pointer對象和nullptr_t對象的operator!=。可以把這樣的operator!=實現為Pointer類的友元:

      template <typename T>
      bool operator!=(const Pointer<T>& lhs, const std::nullptr_t& rhs)
      {
          return lhs.mPtr != rhs;
      }
    
  • 然而,實現這個operator!=后,下面的比較會無法工作,因為編譯器知道該用哪個operator!=

      if (p != NULL)
      {
          cout << "not NULL" << endl;
      }
    
  • 通過這個例子,你可能得出以下結論:operator bool()技術看上去只適合于不表示指針的對象,以及轉換為指針類型并沒有意義的對象。遺憾的是,添加轉換至bool的轉換運算符會產生其他一些無法預知的后果。當條件允許時,C++會使用“類型提升”規則將bool類型自動轉換為int類型。因此,采用operator bool()時,下面的代碼可以編譯運行:

      Pointer<SpreadsheetCell> smartCell(new SpreadsheetCell);
      int i = smartCell;      //轉換smartCell指針從bool到int
    
  • 這通常并不是期望或需要的行為。因此,很多程序員更偏愛使用operator void*()而不是operator bool()

  • 從中可以看出,重載運算符時需要考慮設計因素。哪些操作符需要重載的決策會直接影響到客戶對類的使用方式。

9. 重載內存分配和釋放運算符

  • C++允許重定義程序中內存分配和釋放的方式。既可以在全局層次也可以在類層次進行這種自定義。這種能力可能產生內存碎片的情況下最有用,當分配和釋放大量小對象時會產生內存碎片。例如,每次需要內存時,不適用默認的C++內存分配,而是編寫一個內存池分配器,以重用固定大小的內存塊。本節詳細講解內存分配和釋放例程,以及如何定制化它們。有了這些工具,就可以根據需求編寫自己的分配器。

9.1 new和delete的工作原理

  • C++最復雜的地方之一就是newdelete的細節。考慮下面幾行代碼:

      SpreadsheetCell* cell = new SpreadsheetCell();
    
  • new SpreadsheetCell()這部分稱為new表達式。它完成了兩件事情。首先,通過調用opetator newSpreadsheetCell對象分配了內存空間。然后,為這個對象調用構造函數。只有這個構造函數完成了,才返回指針。

  • delete的工作方式與此類似。考慮下面這行代碼:

      delete cell;
    
  • 這行稱為delete表達式。它首先調用cell的析構函數,然后調用operator delete來釋放內存。

  • 可以重載operator newoperator delete來控制內存的分配和釋放,但不能重載new表達式和delete表達式。因此,可以自定義實際的內存分配和釋放,但不能自定義構造函數和析構函數的調用。

  • (1). new表達式和operator new

  • 有6種不同形式的new表達式,每種形式都有對應的operator new。前4種new表達式:newnew[]nothrow newnothrow new[]。下面列出了<new>頭文件種對應的4種operator new形式:

      void* operator new(size_t size);                                //For new
      void* operator new[](size_t size);                              //For new[]
      void* operator new(size_t size, const nothrow_t&) noexcept;     //For nothrow new
      void* operator new[](size_t size, const nothrow_t&) noexcept;   //For nothrow new[]
    
  • 有兩種特殊的new表達式,它們不進行內存分配,而在已有的存儲段上調用構造函數。這種操作稱為placement new運算符(包括單對象和數組形式)。它們在已存在的內存上構造對象,如下所示:

      void* ptr = allocateMemorySomehow();
      SpreadsheetCell* cell = new(prt) SpreadsheetCell();
    
  • 這個特性有點偏門,但知道這項特性的存在非常重要。如果需要實現內存池,以便在不釋放內存的情況下重用內存,這項特殊性就非常方便。對應的operator new形式如下,但C++標準禁止重載它們:

    void* operator new(size_t size, void* p) noexcept;
    void* operator new[](size_t size, void* p) noexcept;
  • (2). delete表達式和operator delete

  • 只有兩種不同形式的delete表達式可以調用:deletedelete[];沒有nothrowplacement形式。然而, operator delete有6種形式。為什么有這種不對稱性?兩種nothrowplacement的形式只有在構造函數拋出異常時才會使用。這種情況下,匹配調用構造函數之前分配內存時使用的operator newoperator delete會被調用。然而,如果正常地刪除指針,delete會調用operator deleteoperator delete[](絕不會調用nothrowplacement形式)。在實際中,這并沒有關系:C++標準指出,從delete拋出異常的行為是未定義的,也就是說delete永遠都不應該拋出異常,因此nothrow版本的operator delete是多余的;而placement版本的delete應該是一個空操作,因為在placement operator new中并沒有分配內存,因此也不需要釋放內存。下面是operator delete各種形式的原型:

      void operator delete(void* ptr) noexcept;
      void operator delete[](void* ptr) noexcept;
      void operator delete(void* ptr, const nothrow_t&) noexcept;
      void operator delete[](void* ptr, const nothrow_t&) noexcept;
      void operator delete(void* ptr, void*) noexcept;
      void operator delete[](void* ptr, void*) noexcept;
    

9.2 重載operator new和operator delete

  • 如有必要,可以替換全局的operator newoperator delete例程。這些函數會被程序中的每個new表達式和delete表達式調用,除非在類中有更特別的版本。然而,引用Bjarne Stroustrup的一句話:“……替換全局的operator newoperator delete是需要膽量的。”。所以我們也不建議替換。

  • 警告:如果決定一定要替換全局的operator new,一定要注意在這個運算符的代碼中不要對new進行任何調用:否則會產生無限循環。

  • 更有用的技術是重載特定類的operator newoperator delete。僅當分配或釋放特定類的對象時,才會調用這些重載的運算符。下面是一個類的例子,它重載了4個非placement形式的operator newoperator delete

      #include <new>
      class MemoryDemo
      {
      public:
          MemoryDemo();
          virtual ~MemoryDemo();
          void* operator new(std::size_t size);
          void operator delete(void* ptr) noexcept;
          void* operator new[](std::size_t size);
          void operator delete[](void* ptr) noexcept;
          void* operator new(std::size_t size, const std::nothrow_t&) noexcept;
          void operator delete(void* ptr, const std::nothrow_t&) noexcept;
          void* operator new[](std::size_t size, const std::nothrow_t&) noexcept;
          void operator delete[](void* ptr, const std::nothrow_t&) noexcept;
      };
    
  • 下面是這些運算符的簡單實現,這些實現將參數傳遞給了這些運算符全局版本的調用。注意nothrow實際上是一個nothrow_t類型的變量:

      void* MemoryDemo::operator new(size_t size)
      {
          cout << "operator new" << endl;
          return ::operator new(size);
      }
      void MemoryDemo::operator delete(void* ptr) noexcept
      {
          cout << "operator delete" << endl;
          ::operator delete(ptr);
      }
      void* MemoryDemo::operator new[](size_t size)
      {
          cout << "operator new[]" << endl;
          return ::operator new[](size);
      }
      void MemoryDemo::operator delete[](void* ptr) noexcept
      {
          cout << "operator delete[]" << endl;
          ::operator delete[](ptr);
      }
      void* MemoryDemo::operator new(size_t size, const nothrow_t&) noexcept
      {
          cout << "operator new nothrow" << endl;
          return ::operator new(size, nothrow);
      }
      void MemoryDemo::operator delete(void* ptr, const nothrow_t&) noexcept
      {
          cout << "operator delete nothrow" << endl;
          ::operator delete(ptr, nothrow);
      }
      void* MemoryDemo::operator new[](size_t size, const nothrow_t&) noexcept
      {
          cout << "operator new[] nothrow" << endl;
          return ::operator new[](size, nothrow);
      }
      void MemoryDemo::operator delete[](void* ptr, const nothrow_t&) noexcept
      {
          cout << "operator delete[] nothrow" << endl;
          ::operator delete[](ptr, nothrow);
      }
    
  • 下面的代碼以不同方式分配和釋放這個類的對象:

      MemoryDemo* mem = new MemoryDemo();
      delete mem;
      mem = new MemoryDemo[10];
      delete[] mem;
      mem = new (nothrow) MemoryDemo();
      delete mem;
      mem = new (nothrow) MemoryDemo[10];
      delete[] mem;
    
  • 下面是運行結果:

      operator new;
      operator delete;
      operator new[];
      operator delete[];
      operator new nothrow;
      operator delete;
      operator new[] nothrow;
      operator delete[];
    
  • 這些operator newoperator delete的實現非常簡單,但作用不大。它們旨在介紹語法形式,以便在實現真正版本時參考。

  • 警告:當重載operator new時,要重載對應形式的operator delete。否則,內存會根據指定的方式分配,但是根據內建的語義釋放,這兩者可能不兼容。

  • 重載所有不同形式的operator new看上去有點過分。但是在一般情況下最好這么做,從而避免內存分配不一致。如果不想提供任何實現,可使用=delete顯示地刪除函數,以避免別人使用。具體內容可參考下一節。

9.3 顯示地刪除/默認化operator new和operator delete

  • 顯示地刪除或默認化不局限用于構造函數和賦值運算符。例如,下面的類刪除了operator newnew[],也就是說這個類不能通過newnew[]動態創建:

      class MyClass
      {
      public:
          void* operator new(std::size_t size) = delete;
          void* operator new[](std::size_t size) = delete;
      };
    
  • 按以下方式使用這個類會產生編譯器錯誤:

      int main()
      {
          MyClass* p1 = new MyClass;      // Error
          MyClass* p2 = new MyClass[2];   // Error
          return 0;
      }
    

9.4 重載帶有額外參數的operator new和operator delete

  • 除了重載標準形式的operator new之外,還可以編寫帶有額外參數的版本。例如下面是MemoryDemo類中有額外整數參數的operator newoperator delete原型:

      void* operator new(std::size_t size, int extra);
      void operator delete(void* ptr, int extra) noexcept;
    
  • 實現如下所示:

      void* MemoryDemo::operator new(size_t size, int extra)
      {
          cout << "operator new with extra int arg: " << extra << endl;
          return ::operator new(size);
      }
      void MemoryDemo::operator delete(void* ptr, int extra) noexcept
      {
          cout << "operator delete with extra in arg: " << extra << endl;
          return ::operator delete(ptr);
      }
    
  • 編寫帶有額外參數的重載operator new時,編譯器會自動允許編寫對應的new表達式。因此可以編寫這樣的代碼:

      MemoryDemo* pmem = new (5) MemoryDemo();
      delete pmem;
    
  • new的額外參數以函數調用的語法傳遞(和nothrow new一樣)。這些額外參數可用于向內存分配例程傳遞各種標志或計數器。例如,一些運行時庫在調試模式中使用這種形式,在分配對象的內存時提供文件名和行號,這樣,在發生內存泄漏時,可以識別出發生問題的分配內存所在的代碼行數。

  • 定義帶有額外參數的operator new時,還應該定義帶有額外參數的對應operator delete。不能自己調用這個帶有額外參數的operator delete,只有在使用了帶額外參數的operator new且對象的構造函數拋出異常時,才會調用這個operator delete

  • 另一種形式的operator delete提供了需釋放的內存大小和指針。只需聲明帶有額外大小參數的operator delete原型。

  • 警告:如果類聲明了兩個一樣版本的operator delete,只不過一個接受大小參數,另一個不接受,那么不接受額外參數的版本總是會調用。如果需要使用帶大小參數的版本,則請只編寫這一個版本。

  • 可獨立地將任何版本的operator delete替換為接受大小參數的operator delete版本。下面是MemoryDemo類的定義,其中的第一個operator delete改為接受要釋放的內存大小作為參數:

      class MemoryDemo
      {
      public:
          // 省略其他內容
          void* operator new(std::size_t size);
          void operator delete(void* ptr, std::size_t size) noexcept;
          // 省略其他內容
      };
    
  • 這個operator delete實現調用沒有大小參數的全局operator delete,因為并不存在接受這個小大參數的全局operator delete

      void MemoryDemo::operator delete(void* ptr, size_t size) noexcept
      {
          cout << "operator delete with size" << endl;
          ::operator delete(ptr);
      }
    
  • 只有需要為自定義類編寫復雜的內存分配和釋放方案時,才使用這個功能。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容