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[]
,编译器会使用非const
的operator[]
。下面是这两个运算符的正确原型: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
,通常希望进行从double
到SpreadsheetCell
的自动类型转换。自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
,再进行两个SpreadsheetCell
和operator+
操作,之后进行必要的显式类型转换来调用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++最复杂的地方之一就是
new
和delete
的细节。考虑下面几行代码:SpreadsheetCell* cell = new SpreadsheetCell();
new SpreadsheetCell()
这部分称为new
表达式。它完成了两件事情。首先,通过调用opetator new
为SpreadsheetCell
对象分配了内存空间。然后,为这个对象调用构造函数。只有这个构造函数完成了,才返回指针。-
delete
的工作方式与此类似。考虑下面这行代码:delete cell;
这行称为
delete
表达式。它首先调用cell的析构函数,然后调用operator delete
来释放内存。可以重载
operator new
和operator delete
来控制内存的分配和释放,但不能重载new
表达式和delete
表达式。因此,可以自定义实际的内存分配和释放,但不能自定义构造函数和析构函数的调用。(1). new表达式和operator new
-
有6种不同形式的new表达式,每种形式都有对应的
operator new
。前4种new
表达式:new
、new[]
、nothrow new
、nothrow 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
表达式可以调用:delete
和delete[]
;没有nothrow
和placement
形式。然而,operator delete
有6种形式。为什么有这种不对称性?两种nothrow
和placement
的形式只有在构造函数抛出异常时才会使用。这种情况下,匹配调用构造函数之前分配内存时使用的operator new
的operator delete
会被调用。然而,如果正常地删除指针,delete
会调用operator delete
或operator delete[]
(绝不会调用nothrow
或placement
形式)。在实际中,这并没有关系: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 new
和operator delete
例程。这些函数会被程序中的每个new
表达式和delete
表达式调用,除非在类中有更特别的版本。然而,引用Bjarne Stroustrup的一句话:“……替换全局的operator new
和operator delete
是需要胆量的。”。所以我们也不建议替换。警告:如果决定一定要替换全局的
operator new
,一定要注意在这个运算符的代码中不要对new
进行任何调用:否则会产生无限循环。-
更有用的技术是重载特定类的
operator new
和operator delete
。仅当分配或释放特定类的对象时,才会调用这些重载的运算符。下面是一个类的例子,它重载了4个非placement
形式的operator new
和operator 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 new
和operator delete
的实现非常简单,但作用不大。它们旨在介绍语法形式,以便在实现真正版本时参考。警告:当重载
operator new
时,要重载对应形式的operator delete
。否则,内存会根据指定的方式分配,但是根据内建的语义释放,这两者可能不兼容。重载所有不同形式的
operator new
看上去有点过分。但是在一般情况下最好这么做,从而避免内存分配不一致。如果不想提供任何实现,可使用=delete
显示地删除函数,以避免别人使用。具体内容可参考下一节。
9.3 显示地删除/默认化operator new和operator delete
-
显示地删除或默认化不局限用于构造函数和赋值运算符。例如,下面的类删除了
operator new
和new[]
,也就是说这个类不能通过new
或new[]
动态创建: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 new
和operator 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); }
只有需要为自定义类编写复杂的内存分配和释放方案时,才使用这个功能。