重载

核心机制

操作符重载的本质是静态多态。它不是改变了操作符的优先级或结合性,而是让编译器将表达式映射为函数调用。

底层映射原理

当我们在代码中写表达式时,编译器会进行如下转换:

  • 成员函数形式:

    c = a + b; $\rightarrow$ c = a.operator+(b);

    • 原理:左操作数 a 通过 this 指针隐式传递,右操作数 b 作为参数传递。
  • 全局(友元)函数形式:

    c = a + b; $\rightarrow$ c = operator+(a, b);

    • 原理:两个操作数都作为显式参数传递。

两种实现路径

对于允许重载的操作符,我们只有两条路可选 :

  1. 类成员函数
    • 格式:Obj.operator op (Args)
    • 适用:赋值 =、下标 []、调用 () 等。
  2. 全局(友元)函数
    • 格式:operator op (Obj, Args)
    • 适用:需要满足交换律的二元运算(如 10 + a),或左操作数不可修改(如 cout << a)。

操作符重载时参数至少包含一个用户自定义类型(new delete 除外)

成员函数 全局函数

这是一个常见的设计难点,不仅是语法区别,更关乎对称性

  • 场景:假设有一个复数类 Complex
  • 问题
    • Complex a(1, 2);
    • a + 10; // 正常,编译器看作 a.operator+(10)
    • 10 + a; // 报错! 整数 10 是内置类型,没有成员函数 operator+
  • 解决方案:必须使用全局友元函数来实现 operator+(int, Complex)

代码样例:

class Complex {
    int real, imag;
public:
    Complex(int r, int i) : real(r), imag(i) {}

    // 方案1:成员函数(只能处理 c + int)
    Complex operator+(const Complex& rhs) {
        return Complex(real + rhs.real, imag + rhs.imag);
    }

    // 方案2:友元全局函数(处理 int + c,赋予整数“主动权”)
    friend Complex operator+(int lhs, const Complex& rhs);
};

// 全局函数实现
Complex operator+(int lhs, const Complex& rhs) {
    return Complex(lhs + rhs.real, rhs.imag);
}

禁止重载的操作符

以下四个操作符涉及 C++ 最底层的对象访问和流程控制,编译器严禁程序员修改其行为,否则会导致语言的基本语法崩塌 。

  1. . (成员访问):obj.member。如果重载了它,编译器就无法确定你是要访问成员还是调用函数。
  2. .\* (成员指针访问):这是一个非常底层的操作符,用于通过“类成员指针”来调用函数。
  3. :: (域解析):Class::static_member。这是编译期确定的符号,运行时无法重载。
  4. ?: (三元条件运算符):这是 C++ 唯一的三元操作符,且涉及控制流跳转(短路特性),重载会破坏其逻辑。

深度解析:冷门的 .* 操作符

PPT 右侧的代码 (class A ...) 并不是在演示如何重载,而是在演示 .\* 到底原本是干什么用的。这是一个很少见但在底层框架(如 MFC、Qt)中常用的语法。

代码还原与解读:

class A {
public:
    void f() { ... }
};

// 定义一个“指针”,它不仅仅指向内存地址,还限定了必须是 A 类的成员函数
void (A::*p_f)(); 

// 让指针指向 A 类的函数 f
p_f = &A::f;

A a;
// 核心用法:通过对象 a,调用指针 p_f 指向的函数
(a.*p_f)(); 
  • 含义.* 的作用是“解引用成员指针并绑定到具体对象上”。
  • 结论:这个操作太底层且依赖对象模型,因此禁止重载

可以重载但是不要重载的操作符

逻辑与 (&&) 和 逻辑或 (||)

  • 原生行为:具有短路求值 (Short-circuit Evaluation) 特性。
    • 例如 if (p != nullptr && p->value > 0),如果 p 为空,后面根本不会执行,程序安全。
  • 重载后行为:变成了普通的函数调用
    • operator&&(expression1, expression2)
    • C++ 规定函数调用前必须先计算所有参数。这意味着即使 p 为空,p->value 也会被执行,导致程序崩溃
  • 结论永远不要重载 &&||

重载的“基本法”(三不准原则)

虽然我们可以自定义操作符的逻辑(加法变减法),但不能改变编译器解析代码的方式。必须严格遵守以下“宪法”:

  1. 不准改变元数 (Arity)
    • 单目运算符(如 ++)重载后必须还是单目。
    • 双目运算符(如 +)重载后必须还是双目。你不能创造一个 operator+(a, b, c)
  2. 不准改变优先级 (Precedence)
    • 无论怎么重载,*(乘除)永远比 +(加减)先计算。编译器在生成语法树时就已经定好了。
  3. 不准改变结合性 (Associativity)
    • 赋值符 = 永远是从右向左算 (a=b=c),加法 + 永远是从左向右算。这也不能改。

返回方式

原则:不要在 operator+ / operator* 中返回引用或指针

当我们需要返回一个运算结果(如两数之和)时,必须返回对象本身 (Return by Value)

常见错误分析

  1. 在堆上 new
    • 写法:Rational* p = new ...; return *p;
    • 后果:内存泄漏。调用者无法获取隐藏的指针来释放内存。
  2. 使用 static 静态变量
    • 写法:static Rational res; ... return res;
    • 后果:逻辑错误。所有运算共享同一个静态对象,连续运算(a*b == c*d)时,后计算的值会覆盖前面的值,导致比较永远为真。

正确做法return Rational(...);

利用构造函数生成临时对象返回。现代编译器通过 RVO (返回值优化) 技术可以消除拷贝开销。

正确写法:返回临时对象 (Return by Value)

return Rational(n*r.n, d*r.d);

错误写法 A:想省拷贝,结果搞出内存泄漏

Rational *result = new Rational(n*r.n, d*r.d);
return *result;
  • 谁负责 delete
    • 当你写 w = x * y * z; 时,x*y 产生了一个 new 出来的对象,... * z 又产生了一个。这些中间产生的对象不仅没名字,还拿不到指针,根本没法释放。
    • 结论Memory Leak (内存泄漏)。这行代码会导致程序越跑内存占用越高,直到崩溃。

错误写法 B:想省内存,结果逻辑错乱

static Rational result;
result.n = ...; 
return result;
if ((a * b) == (c * d)) { ... }
  1. 计算 a * b,结果存入 static result(比如结果是 10)。
  2. 计算 c * d,结果覆盖static result(比如结果是 20)。
  3. 比较操作符 == 拿到两边的东西。左边是 result(现在是 20),右边也是 result(还是 20)。
  4. 结果永远相等! 哪怕 a*b 不等于 c*d,这个判断也会通过。

单目操作符

前置 ++ vs 后置 ++

为了区分 ++aa++,C++ 规定后置版本多一个 int 哑参数。

  • 前置 ++a

    • 逻辑:先自增,再返回引用。
    • 效率:(不产生临时对象)。
  • 后置 a++

    • 逻辑:先保存旧值(临时对象),自增,返回旧值。
    • 效率:(多了一次对象的构造和析构)。
    • 代码实现:
    // 前置 ++
    Counter& operator++() { 
        value++; 
        return *this; 
    }
      
    // 后置 ++ (int 是标记)
    Counter operator++(int) {
        Counter temp = *this; // 记录旧值(性能损耗点)
        value++;              // 自增
        return temp;          // 返回旧值
    }
    
  • 结论:在迭代器或复杂对象操作中,优先习惯使用前置 ++it

operator=

这是 C++中最容易导致程序崩溃内存泄漏的地方。默认的赋值操作符只做浅拷贝(逐字节复制),一旦类中管理了指针资源,浅拷贝会导致两个对象指向同一块内存,析构时发生 Double Free。

编写健壮的 operator= 必须遵循以下四个步骤,缺一不可:

  1. 证同测试 (Identity Test):检查是否是自己赋值给自己 (a = a)。
    • 底层原因:如果不检查,第2步会把自己的数据删掉。等到第3步想拷贝源数据时,发现源数据(就是自己)已经是垃圾值了。
  2. 释放旧资源:防止内存泄漏。
  3. 分配新资源并拷贝:实现深拷贝
  4. 返回 *this:为了支持链式赋值 a = b = c

标准实现代码:

class String {
    char* data;
public:
    String& operator=(const String& rhs) {
        // 1. 证同测试 (Self-assignment check)
        if (this == &rhs) {
            return *this;
        }

        // 2. 释放旧资源 (防止 Memory Leak)
        delete[] data;

        // 3. 深拷贝 (Deep Copy)
        if (rhs.data) {
            data = new char[strlen(rhs.data) + 1];
            strcpy(data, rhs.data);
        } else {
            data = nullptr;
        }

        // 4. 返回引用 (支持 a = b = c)
        return *this;
    }
};

operator[]

C++ 的 operator[] 有一个硬伤:它只接受一个参数。那么 matrix[3][4] 这种多维数组语法是如何实现的?

核心矛盾与代理模式

  • matrix[3] 必须返回一个对象,这个对象代表“第3行”。
  • 然后对这个“行对象”再次调用 [4],才能拿到具体的元素。
  • 这就是 Proxy Pattern(代理模式)

代码实现逻辑

class Matrix {
    int* data;
    int cols;
public:
    // 内部代理类:代表“一行数据”
    class RowProxy {
        int* rowPtr;
    public:
        RowProxy(int* ptr) : rowPtr(ptr) {}
        
        // 第二次调用 []:在“行”中取“列”
        int& operator[](int c) {
            return rowPtr[c];
        }
    };

    // 第一次调用 []:返回一个“行代理”对象
    RowProxy operator[](int r) {
        return RowProxy(data + r * cols);
    }
};

// 调用链解析:
// m[1][2] 
// 1. m.operator[](1) -> 返回 RowProxy 对象 (指向第1行起始)
// 2. RowProxy.operator[](2) -> 返回 int&

operator()

C++ 通过重载 () 让对象可以像函数一样被调用,这被称为仿函数 (Functor)。这是 C++ 闭包和 Lambda 表达式的基石。

  • 普通函数:只有代码,没有状态(除非用全局变量,但不安全)。
  • 仿函数:是类对象,可以拥有成员变量(携带状态)。

函数对象

特性 函数指针 (Function Pointer) 函数对象 (Function Object)
状态保存 无法保存状态。 函数只是代码段的地址,没有数据成员,除非用全局变量(不安全)。 可以携带状态。 作为类对象,它可以拥有成员变量(如 factor),通过构造函数初始化,根据不同的状态表现出不同的行为。
执行效率 较低。 编译器难以进行内联优化(Inline),因为指针指向哪里通常是运行时才确定的,必须进行间接调用。 极高。 编译器在编译期就知道要调用哪个类的 operator(),可以直接将代码内联展开,消除了函数调用的开销。
class Multiply {
    int factor; // 【状态】成员变量,用来保存倍数
public:
    // 构造函数:初始化状态
    Multiply(int f) : factor(f) {}

    // 【核心】重载 (),让对象像函数一样被调用
    int operator()(int x) const { 
        return x * factor; 
    }
};

// 1. 创建两个不同的函数对象,携带不同的状态
Multiply times2(2); // 这个对象负责 "乘以 2"
Multiply times5(5); // 这个对象负责 "乘以 5"

// 2. 像调用函数一样调用对象
cout << times2(10); // 输出 20 (10 * 2)
cout << times5(10); // 输出 50 (10 * 5)

Lambda 表达式:函数对象的“语法糖”

Lambda 表达式在 C++11 引入,它让我们可以就地定义匿名函数对象,而不需要像上面那样专门写一个 class

  • 映射关系
    • 数学表达:$\lambda x. x + 1$ (定义一个把 $x$ 映射为 $x+1$ 的函数)。
    • C++ 表达[](int x) { return x + 1; }
    • 调用:$(\lambda x. x + 1)(3)$ $\rightarrow$ [](int x){ return x + 1; }(3),结果为5
捕获语法 含义 底层行为
[] 不捕获任何变量。 生成的类没有成员变量。
[&] 引用捕获所有外部变量。 生成的类成员变量全是引用(如 int&)。效率高,但需警惕悬空引用
[=] 值拷贝捕获所有外部变量。 生成的类成员变量全是副本。安全,但拷贝有开销
[=, &foo](foo是任意自定义对象名) 混合模式:默认全值拷贝,唯独 foo 用引用。 针对大对象 foo 单独优化,避免拷贝。
[bar](bar是任意自定义对象名) 显式模式:只值拷贝 bar,不捕获其他任何变量。 推荐写法,明确依赖关系。

Lambda 表达式示例:通用字符串过滤器

这个例子的目标是:编写一个通用的过滤函数 str_filter,它可以根据调用者传入的“任意规则”,筛选出符合条件的字符串。

1.定义通用接口 (std::function)

首先,利用 std::function 定义了一个可以接受“任意判断逻辑”的函数接口:

// matched 是一个可调用对象,接受 string&,返回 bool
vector<string> str_filter(vector<string> &vec, function<bool(string &)> matched) {
    vector<string> result;
    for (string tmp : vec) {
        // 【多态调用】这里调用传入的逻辑,不管是函数指针还是 Lambda
        if (matched(tmp)) { 
            result.push_back(tmp);
        }
    }
    return result;
}
  • 亮点str_filter 不需要知道具体的过滤规则(是查长度?还是查内容?),它把规则判断权交给了参数 matched

2.Lambda 捕获外部变量

main 函数中,我们需要筛选出包含 ".com" 的网址。

int main(){
    vector<string> vec = {"www.baidu.com", "www.kernel.org", "www.google.com"};
    string pattern = ".com"; // 【外部变量】这是要在 Lambda 内部使用的

    // 调用过滤器,现场定义筛选规则
    vector<string> filterd = str_filter(vec, 
        [&](string &str) {  // 【核心点】捕获列表 [&]
            // 如果不捕获,这里根本访问不到 pattern 变量!
            if (str.find(pattern) != string::npos) 
                return true;
            return false;
        }
    );
}

在这个 Lambda 表达式中:

  • 代码逻辑str.find(pattern)
  • 问题pattern 定义在 Lambda 函数体之外(在 main 函数栈上)。普通的函数无法直接访问外部的局部变量。
  • 解决方案:使用捕获列表 [&]
    • [&] 的含义是:以引用方式捕获作用域内的所有变量
    • 这意味着 Lambda 内部的 pattern 实际上就是 main 函数里那个 pattern 的引用。
    • 注:如果没有 [&](即写成 []),编译器会报错,提示 pattern 未定义。

std::function可存储

  1. 普通函数 (Ordinary Function)
  2. 函数指针 (Function Pointer)
  3. 类成员函数 (Member Function)
  4. 函数对象 / 仿函数 (Functor)
  5. Lambda 表达式 (Lambda Expression)

同一个变量,可以动态改变,存储完全不同类型的逻辑。

#include <functional>
#include <iostream>
using namespace std;

// 1. 普通函数
void hello() { cout << "Hello (Function)" << endl; }

// 2. 仿函数
struct HelloFunctor {
    void operator()() { cout << "Hello (Functor)" << endl; }
};

int main() {
    // 定义一个通用的函数包装器:不接受参数,返回 void
    std::function<void()> func; 

    // A. 存普通函数
    func = hello; 
    func(); // 输出: Hello (Function)

    // B. 存 Lambda 表达式
    func = []() { cout << "Hello (Lambda)" << endl; };
    func(); // 输出: Hello (Lambda)

    // C. 存仿函数对象
    HelloFunctor f;
    func = f;
    func(); // 输出: Hello (Functor)
    
    return 0;
}

std::function 的底层原理:类型擦除

  1. 问题:如何统一存储不同类型的可调用对象?
  2. 方案:利用运行时多态 (Runtime Polymorphism)
    • 内部持有一个抽象基类指针 (_Base*)。
    • 具体的函数对象被包装在一个模板子类 (_Model<F>) 中。
    • _Model<F> 继承自 _Base,从而“擦除”了具体类型 F 的差异,对外只暴露 _Base 接口。
  3. 代价:每次调用 std::function 都会产生一次虚函数调用(Indirect Call),这就是为什么它比直接调用函数指针稍微慢一点点的原因。

image-20251216005013430

operator <基本数据类型>

类型转换运算符

  • 目的:允许自定义的类(Class)像基本数据类型(如 int, double, bool)一样被使用。
  • 语法operator 类型名() { ... }
    • 注意:这种函数没有返回类型(因为类型名本身就暗示了返回类型),并且通常是 public 的。
class Rational {
public:
    // 构造函数:初始化分子 n 和分母 d
    Rational(int n1, int n2) { n = n1; d = n2; }

    // 【关键代码】类型转换操作符重载
    // 这行代码定义了如何将 Rational 对象转换成 double 类型
    operator double() { return (double)n/d; } 

private:
    int n, d; // 分子和分母
};
  • 含义:当你把一个 Rational 对象放到需要 double 的地方时,编译器会自动调用 operator double(),计算 n/d 并返回结果。
Rational r(1, 2); // 创建一个对象 r,代表分数 1/2
double x = r;     // 【隐式转换】r 自动变成了 double 类型 (0.5) 赋值给 x
x = x + r;        // 【混合运算】这里 r 再次自动变成 0.5,与 x 相加
  • 如果没有这个重载,写 double x = r; 编译器会报错,因为它不知道怎么把“对象”变成“数字”。

“减少混合计算中需要定义的操作符重载函数的数量”

  • 如果不写类型转换:如果你想让分数和整数相加、分数和浮点数相加,你可能需要分别重载 operator+(Rational, int)operator+(Rational, double) 等一大堆函数。
  • 写了类型转换:编译器会自动把 Rational 变成 double,然后利用系统自带的加减法进行计算。这样你就不用写那么多数学运算重载函数了。
ostream f("abc.txt");
if (f) ....

这是一个常见的实际应用场景(文件流)。

  • f 是一个文件对象。
  • if(f) 之所以能成立,是因为 ostream 类重载了类似 operator bool() (或 void*) 的转换操作符。它允许文件对象直接像布尔值一样被判断(用来检查文件是否成功打开)。

这张 PPT 紧接上一页,深入讲解了类型转换操作符中的一个重要关键字:explicit(显式)

它的核心目的是:防止编译器在用户毫不知情的情况下,自动乱做类型转换,从而避免歧义和错误。

class A {
public:
    operator int() { return 1; }              // 【隐式转换】允许自动转成 int
    explicit operator double() { return 2.0; } // 【显式转换】必须强制转换才能转成 double
};
  • operator int(): 这是一个普通的转换函数。当代码需要一个 int 时,编译器会自动把对象 A 变成 1
  • explicit operator double(): 这是一个受限制的转换函数。关键字 explicit 告诉编译器:“除非程序员明确写出来要转成 double,否则绝对不要自动转”。
A a;
cout << a + 0; // 该转成 int 还是 double?

答案是:转成 int

  • 原因
    • 在计算 a + 0 时,编译器需要把 a 转换成一个能和 0 (int) 进行加法运算的数字。
    • 虽然类里定义了 double 的转换,但因为它被标记为 explicit,所以编译器在进行这种“自动(隐式)转换”查找时,直接忽略了它。
    • 编译器只看到了 operator int() 是可用的。
    • 因此,a 变成了 1,结果是 1 + 0 = 1
cout << static_cast<double>(a) + 0;
  • 如果你非要用那个 return 2.0 的逻辑,你就必须使用强制类型转换(如 static_cast)。
  • 这相当于你在对编译器说:“我知道我在做什么,请强制把它转成 double。”
  • 这时候 operator double() 就会被调用,a 变成 2.0,结果输出就是 2

指针访问操作符 -> 的重载

让一个普通的对象,表现得像一个指针一样

普通的重载(比如 +)是算出结果就结束了。但 -> 操作符有一种特殊的“钻头”属性:它会一直向下钻,直到找到一个真正的原生指针为止。

  • 你写代码:a->f()
  • 编译器自动把它翻译成:( a.operator->() )->f()

这意味着:

当你对对象 a 使用 -> 时,编译器会调用 a 内部定义的 operator->() 函数。这个函数必须返回一个指针(或者另一个重载了 -> 的对象),然后编译器会继续用 -> 去操作那个返回回来的指针。

代码实例详解

第一步:定义一个普通的类 CPen (笔)

class CPen {
    int m_color;
    // ... 其他属性
public:
    void setColor(int c) { m_color = c; } // 设置颜色的函数
    // ...
};

这是一个很普通的类,里面有 setColor 方法。

第二步:定义一个包装类 CPanel (面板)

class CPanel {
    CPen m_pen; // 【关键】它内部私藏了一支笔
    // ...
public:
    // 【核心重载函数】
    CPen* operator->() { 
        return &m_pen; // 返回内部那支笔的地址(指针)
    }
};

这里是魔法发生的地方。CPanel 重载了 ->,它的逻辑是:“如果你对我用箭头,我就把我的内部成员 m_pen 的地址给你”。

实际调用

CPanel c;          // c 是一个对象,不是指针!
c->setColor(16);   // 竟然可以像指针一样用箭头调用函数?

为什么能这么写?编译器背后的动作是:

  1. 看到 c->,编译器去查 CPanel 类,发现重载了 operator->
  2. 执行 c.operator->(),这个函数返回了 &m_pen(即内部那支笔的指针 CPen*)。
  3. 现在代码变成了:(CPen*)->setColor(16)
  4. 这既然是一个真正的指针,就可以正常调用 setColor 了。

最终效果:

表面上你在“戳”面板(CPanel),实际上是面板把这个操作转发给了它内部的笔(CPen)。

应用场景

Smart Pointer (智能指针)

这是 C++ 高级编程极其重要的概念。想象一下,如果 operator->() 里面不仅仅是返回指针,还顺便做了一些其他事情:

  • 比如检查空指针:防止程序崩溃。
  • 比如引用计数:记录有多少人在用这个指针,没人用时自动删除(防止内存泄漏)。

重载 -> 的目的是为了“代理”或“转发”。

当你写 c->func() 时,你实际上是在操作 c 内部持有的另一个对象的指针。这让对象 c 用起来像个指针,但比普通指针更强大(因为你可以在转发过程中夹带私货,做额外的管理工作)。

new delete 重载

通过重载new和delete来实现程序自身管理内存,即申请一块较大的内存,针对该内存自己管理存储分配

重载的new和delete为静态成员,且遵循类的控制访问,可继承

重载 new

  • 函数签名void *operator new(size_t size, ...)
    • 返回类型 void*:因为 new 只负责给出一块“生肉”(原始内存地址),它不管上面存什么。
    • 第一个参数 size_t size:这是必须的。当你写 new MyClass() 时,编译器会自动算出 MyClass 需要多少字节,并传给这个参数。
  • 关键点
    • 这些函数默认是静态成员 (static)(即使你不写 static),因为在创建对象之前,对象还不存在,必须通过类来调用。
    • 一旦你写了这个函数,以后创建这个类的对象时,就会跑你的代码,而不是系统默认的代码。

三种 operator new 的重载版本,主要区别在于参数列表的不同,而这些参数决定了它们各自独特的用途。

  1. 标准版本 (Standard New)

代码: void* operator new(std::size_t size)

  • 参数 size_t size 代表什么?
    • 这是编译器自动计算出的、你的对象所占用的字节数
    • 比如你写 new int,编译器会传 4 进去;如果你写 new MyClass,编译器会算出 MyClass 的大小传进去。
  • 用途:
    • 这是最普通的用法。
    • 它负责去堆(Heap)里找一块大小为 size 的空地,返回地址。
    • 如果找不到(内存满了),它会抛出异常 (std::bad_alloc),导致程序崩溃或跳转到 catch 块。
  1. 定位 New (Placement New) —— 最特殊的一个

代码: void* operator new(std::size_t size, void* ptr)

  • 参数 void* ptr 代表什么?

    • 这是你手动提供的一个内存地址
    • 它的意思是:“不要去堆里申请新内存了,就用我给你的这一块现成的地 (ptr)。”
  • 第一个参数 size 还有用吗?

    • 虽然函数声明里必须有它,但在 Placement New 的逻辑里,它通常没用。因为地皮已经有了,不需要再申请大小了。
  • 怎么调用?

    • 你必须显式地把地址传进去:
    char buffer[100]; // 我自己准备了一块地(在栈上或预先分配的池里)
    MyClass* p = new (buffer) MyClass(); // 把对象建在 buffer 这块地上
    
  1. 不抛异常的 New (Nothrow New)

代码: void* operator new(std::size_t size, const std::nothrow_t&)

  • 参数 const std::nothrow_t& 代表什么?

    • 这是一个占位符(Tag),或者叫“暗号”。
    • 这个参数本身没有实际数据意义,它的存在只是为了触发函数重载
    • 当你传了这个参数,编译器就会知道:“哦,你想要的是那个不报错的版本。”
  • 用途:

    • 主要用于不能容忍程序因为内存不足而崩溃(抛出异常)的场合(比如嵌入式或老旧系统)。
    • 如果内存分配失败,它不抛异常,而是静悄悄地返回一个空指针 (nullptr)
  • 怎么调用?

    // 显式传入 std::nothrow 这个“暗号”
    MyClass* p = new (std::nothrow) MyClass();
      
    if (p == nullptr) {
        // 只能在这里手动检查是否成功
        // 处理内存不足的情况
    }
    

重载 delete

如果你重载了 new,你就必须重载 delete

  • 函数签名void operator delete(void *p, size_t size)

  • 参数

    • void *p:要销毁的对象的地址。
    • size_t size(可选):对象的大小。

    delete的重载只能有一个 new可以有多个