对象在哪儿出生,凭什么由调用者随手决定?这一章让类设计者拍板——析构函数私有,对象只能在堆上降生;operator new 私有,对象只能在栈上安家。编译器的报错,就是最好的门卫
核心方法论:运筹帷幄
「打仗最怕的不是敌人太强,而是粮草在自己后院失火——内存泄漏就是这样的后院失火。与其等它烧起来再救火,不如在布阵时就把粮仓设在攻不进来的地方。这一章的两招,都在对象还没出生之前就替它定好归宿:用访问控制把错误的路径堵死,让编译器在编译期就把犯规的人拦在门外。未雨绸缪,胜过亡羊补牢。」
上一章我们看到,栈上创建(Foo f;)和堆上创建(Foo* p = new Foo;)是两条完全不同的生命周期契约:栈对象由编译器管生管死,堆对象由你亲手管生管死。那么问题来了——到底在哪条道上出生,由谁决定?默认情况下是调用者随手决定。但对某些类来说,"让调用者随便选"本身就是风险:一个需要自我销毁的对象被建在栈上,一个本该零管理的类型被 new 到堆上,都是定时炸弹。
哪些场景要收回选择权?两种典型动机。第一种是只在堆上:有些对象需要"自我销毁"的能力——在成员函数里执行 delete this,把自己从内存里抹掉。这在堆对象里天经地义,在栈对象里却是灾难:栈对象的生死归编译器管,你一个 delete this 等于提前拆了编译器要收回的地基,程序随后必崩。第二种是只在栈上:有些类型要给你一个保障——"这种对象绝不会发生内存泄漏"。如果在嵌入式系统上工作,就可能遇到这种情况:嵌入式系统的堆空间非常珍贵,发生在它上面的内存泄漏非常严重,所以有些类宁可只许用栈。两类动机的代码雏形:
// 动机一:自我销毁的对象 —— 必须在堆上
class Session {
public:
void close() { delete this; } // 只有堆对象才配拥有这句
// 若调用者随手建了栈对象,这行就是悬垂指针的温床
};
// 动机二:防泄漏的"安全类型" —— 只在栈上
// 嵌入式设备堆空间珍贵:禁止 new,从语法上消灭泄漏可能(15.4 节装锁)
class FixedBuffer { };
| 场景 | 核心诉求 | 该限制到哪 |
|---|---|---|
| 单例 / 全局服务对象 | 生命周期覆盖整个程序 | 只在堆上 |
自我销毁对象(delete this) | 对象能亲手了结自己 | 只在堆上 |
| 引用计数对象 | 计数归零才释放 | 只在堆上 |
| 嵌入式内存受限 | 堆空间珍贵,不许泄漏 | 只在栈上 |
| 短命临时对象 | 用完即走,零管理负担 | 只在栈上 |
两种限制放在一起看,本质是同一个思路:用设计约束管控生命周期,而不是用运行期检查碰运气。约束的价值在于——犯规的人根本编译不过,连拿到一个"能跑但会泄漏"的程序的机会都没有。编译期的报错,是最便宜的教训。
注意:这两种限制都是"设计层约定",不是语言级的绝对封锁——15.4 节你会亲眼看到 ::new 和 placement new 仍能绕过。所以它防的是常规路径上的无心之失(顺手写了个 new、顺手建了个栈对象),不是铁了心要绕过的人。
先试一条直觉方案:把构造函数设为私有。表面上说得通——外部无法调用构造函数,不就只能内部 new 了吗?但这条路走不通。回想第 14 章:new 运算符的执行分两步——先调用 operator new 分配内存,再调用构造函数完成构造。你虽然可以重载 operator new,但它只负责分配内存,无法提供构造功能;构造函数一旦私有,连 new 的第二步也一起被禁——结果是一个对象都造不出来(本机实测报错):
// ex9_ctor_err.cpp 关键行 —— 构造函数私有,连 new 都被禁,此路不通
private:
NoCtor() { } // 构造函数私有
// main 里:
NoCtor* p = new NoCtor; // new 第二步要调构造函数 —— 私有,也不许调!
# 编译报错(真实):ex9_ctor_err.cpp:10:21: error: calling a private constructor of class 'NoCtor'
正解是把目光从"入口"挪到"出口":把析构函数设为私有。原理藏在栈对象的生命周期里:栈对象由编译器分配内存、调用构造函数,使用完毕再由编译器调用析构函数释放空间——整个生命周期都由编译器管理。那么反过来想:如果编译器无法调用类的析构函数呢?析构私有,编译器就无法调用它来释放内存。于是编译器在为类对象分配栈空间时,会先检查析构函数的可访问性——其实不光是析构函数,只要是非静态的成员函数,编译器都会进行检查;析构私有,编译器就不会在栈空间上为类对象分配内存,直接报错。而 new 只调用公有构造函数,堆上创建畅通无阻(本机实测报错):
// ex2_stack_err.cpp —— 析构私有,栈上创建 → 编译报错
#include <cstdio>
class HeapOnly {
public:
HeapOnly() { std::printf("HeapOnly 构造\n"); }
private:
~HeapOnly() { std::printf("HeapOnly 析构\n"); }
};
int main() {
HeapOnly h; // 编译器要在作用域结束时调用析构 —— 私有,不许调!
return 0;
}
# 编译(真实报错,Apple clang 21 本机实测):
g++ -std=c++17 ex2_stack_err.cpp -o ex2
# ex2_stack_err.cpp:12:14: error: variable of type 'HeapOnly' has private destructor
# 12 | HeapOnly h; // 编译器要在作用域结束时调用析构 —— 私有,不许调!
# | ^
# ...(另附 note: declared private here,指向私有析构的声明行)
# 1 error generated.
这一招还有个副作用:析构私有之后,外部的 delete 同样被堵死——因为 delete 也要先调用析构函数再归还内存(本机实测报错):
// ex3_delete_err.cpp 关键行 —— 析构私有,delete 同样被堵(副作用)
HeapOnly* p = new HeapOnly; // 合法:构造函数是公有的
delete p; // 非法:析构函数是私有的
# 编译报错(真实):ex3_delete_err.cpp:13:12: error: calling a private destructor of class 'HeapOnly'
再交代一个继承场景的坑:如果 HeapOnly 作为基类,析构函数通常要设为 virtual 供子类重写以实现多态——可 private 会堵死子类的调用。解决办法是把析构函数设为 protected(构造函数保持 public):外部依旧无法栈建、无法 delete,但子类可以正常调用基类析构。实测:Base 直接建栈对象被拒,派生类 Derived 则堆上、栈上畅通无阻:
// ex4_protected_base.cpp —— 析构受保护:既限制直接栈对象,又不堵死继承(可编译可运行)
class Base {
public:
Base() { std::printf("Base 构造\n"); }
protected:
~Base() { std::printf("Base 析构\n"); } // 外部 delete 不了,子类可以
};
class Derived : public Base {
public:
Derived() { std::printf("Derived 构造\n"); }
~Derived() { std::printf("Derived 析构\n"); } // 公有:把释放通道重新打开
};
int main() {
Derived* d = new Derived; // OK:堆上创建没问题
delete d; // OK:Derived 析构公有,内部调 Base 受保护析构
// Base b; // 编译错误:Base 析构受保护,不能建栈对象
}
# 运行(真实):Base 构造 / Derived 构造 / Derived 析构 / Base 析构
| 析构函数访问级别 | 外部栈上创建 | 外部 new + delete | 派生类 |
|---|---|---|---|
public | 允许 | 允许 | 正常 |
protected | 禁止(编译报错) | 禁止(编译报错) | 正常(子类析构内部可调用) |
private | 禁止(编译报错) | 禁止(编译报错) | 受阻(子类也无法调用基类析构) |
注意报错发生的时机和位置:错误出现在"尝试创建栈对象的那一行"(调用处),而不是类定义处——类本身完全合法,只是"在外面"不允许析构它。报错在编译期、定位在调用处,犯错的人当场被拦下,不用等程序跑起来才爆雷。
注意:素材里有一句原话值得细品——"不光是析构函数,只要是非静态的函数,编译器都会进行检查"。严谨地说,是编译器在离开作用域时必须生成对析构函数的调用,访问检查在这一步失败,于是栈对象的分配被一并否决。另外记住:析构私有 ≠ 不能创建堆对象,只是"外面的人"既不能 delete 也不能建栈对象——堆对象建了之后怎么释放?正是下一节要解决的事。
上一节把 delete 也一并堵死了——但堆对象总不能只生不死。破局思路很简单:门从外面锁死了,就从里面开一扇窗。谁有权访问私有析构?两类人:类的成员函数和友元函数。于是有两个常见姿势:一是类自己提供一个 static 成员函数 destroy(),把 delete this 收进内部——"自毁"成为类的内部事务;二是预约一个友元释放函数,让外部唯一的释放通道走它。两个版本都完整可编译、可运行(本机实测):
// ex1_heaponly_ok.cpp —— 姿势一:静态成员 destroy() 封装 "delete this"(可编译可运行)
#include <cstdio>
class HeapOnly {
public:
HeapOnly() { std::printf("HeapOnly 构造 @%p\n", (void*)this); }
// 唯一的"合法死法":成员函数有权访问私有析构
static void destroy(HeapOnly* p) {
std::printf("destroy() 执行 delete this @%p\n", (void*)p);
delete p;
}
private:
~HeapOnly() { std::printf("HeapOnly 析构 @%p\n", (void*)this); }
};
int main() {
HeapOnly* p = new HeapOnly; // OK:new 只调用公有构造函数
// delete p; // 编译错误:析构函数是私有的
HeapOnly::destroy(p); // OK:唯一的合法释放通道
return 0;
}
# 运行(真实,本机实测;地址每次运行都不同):
HeapOnly 构造 @0x10140dab0
destroy() 执行 delete this @0x10140dab0
HeapOnly 析构 @0x10140dab0
// ex5_friend_release.cpp —— 姿势二:友元释放函数(可编译可运行)
#include <cstdio>
class HeapOnly {
public:
HeapOnly() { std::printf("HeapOnly 构造 @%p\n", (void*)this); }
friend void release(HeapOnly* p); // 提前预约"释放人"
private:
~HeapOnly() { std::printf("HeapOnly 析构 @%p\n", (void*)this); }
};
void release(HeapOnly* p) { delete p; } // 友元享有和成员同等的访问权
int main() {
HeapOnly* p = new HeapOnly;
release(p);
}
# 运行(真实):HeapOnly 构造 @0x1053fd9a0 / HeapOnly 析构 @0x1053fd9a0
两种姿势的差别在于"释放人"的归属。姿势一里 destroy() 是类的静态成员,随类走、命名空间干净,调用约定是 HeapOnly::destroy(p)——类自己掌握生死大权,自洽性最好,引用计数对象(计数归零自动释放)这类场景常用它。姿势二把释放权委托给外部友元,调用形式更像 C 风格的 free(p)——适合你想把"释放策略"外置、或一个释放函数打理多个类型的情形。对比如下:
| 释放姿势 | 调用形式 | 访问权限来源 | 风格 | 典型用途 |
|---|---|---|---|---|
静态成员 destroy() | HeapOnly::destroy(p) | 成员函数天然可访问私有析构 | 类自管生死,内聚 | 引用计数对象、自毁型对象 |
友元函数 release() | release(p) | 友元声明授予同等访问权 | 释放策略外置,类 C 风格 | 多个类型共用一个释放函数 |
无论哪种姿势,对外暴露的合法死法只有一条通道:要么 destroy(),要么 release()。这就是设计约束的完整形态——堵死所有暗门,再光明正大地开一扇正门。调用方只需记住一个约定:"new 出来的对象,一律走 destroy/release 释放",其它写法都在编译期被拒。
注意:delete this 有三条铁律。①释放后指针立即作废:destroy(p) 之后再碰 p 就是悬垂指针,读写即未定义行为。②销毁后别再访问成员:destroy() 里 delete this 之后又读 this->data,等于摸黑走钢丝。③不要重复释放:同一个指针只能 destroy 一次,双释是崩溃和堆损坏的头号元凶。
反过来:只许栈上创建,怎么禁掉堆上创建?你无法影响 new 运算符本身(它是 C++ 语言内建的),但可以利用一个事实:new 运算符总是先调用 operator new,而后者是可以自行声明重写的。于是把 operator new 声明为私有,就禁止了对象被 new 在堆上——new 的第一步(分配内存)就被拦下。而栈上创建根本不经过 operator new,内存由编译器安排,畅通无阻(本机实测):
// ex8_stackonly_ok.cpp —— 栈上创建畅通无阻(可编译可运行)
#include <cstdio>
#include <cstddef>
class StackOnly {
public:
StackOnly() { std::printf("StackOnly 构造 @%p\n", (void*)this); }
~StackOnly() { std::printf("StackOnly 析构\n"); }
private:
static void* operator new(std::size_t); // 私有:拒绝"堆上出生"
static void operator delete(void*) noexcept;
};
int main() {
StackOnly s; // 栈对象:编译器分配内存,不经过 operator new
return 0;
}
# 运行(真实):StackOnly 构造 @0x16bae20db / StackOnly 析构
一旦有人尝试 new,编译器在第一步就翻车。注意一个细节:报错里 operator new 和 operator delete 双双被点名——编译器为 new 生成代码时,还要顺手找一个配套的 operator delete,以备构造中途抛异常时回滚内存(本机实测报错):
// ex6_stackonly_err.cpp —— 类定义同 ex8,仅 main 不同:
int main() {
StackOnly* p = new StackOnly; // new 第一步就要调 operator new —— 私有,不许调!
return 0;
}
# 编译(真实报错,本机实测)—— operator new 与 operator delete 双双被点名:
g++ -std=c++17 ex6_stackonly_err.cpp -o ex6
# ex6_stackonly_err.cpp:15:20: error: 'operator new' is a private member of 'StackOnly'
# 15 | StackOnly* p = new StackOnly;
# | ^~~~~~~~~~~~~
# ex6_stackonly_err.cpp:15:20: error: 'operator delete' is a private member of 'StackOnly'
# ...(各附 note: declared private here,指向私有声明行)
# 2 errors generated.
还有一层"意外之喜"值得实测:类作用域里一旦声明了 operator new,编译器就在类里找它,不再向外看全局版本——这个机制叫名字遮蔽(name hiding,说人话:类里的同名声明把全局的"挡"住了)。于是不只是普通 new,连 new (std::nothrow) 和普通(不带 :: 前缀的)placement new 也一并被挡下。真正的洞只剩下:显式写成 ::new 强制从全局作用域查找,或干脆 malloc + 手动构造。两个都实测给你看:
// ex7_placement_bypass.cpp 关键行 —— 普通 placement new 也翻不过这道墙(真实报错)
alignas(StackOnly) unsigned char buf[sizeof(StackOnly)];
StackOnly* p = new (buf) StackOnly; // 想直接在地基上构造
p->~StackOnly();
# 编译报错(真实):no matching function for call to 'operator new'(20:20)
# note: candidate function not viable: requires 1 argument, but 2 were provided
// ex10_global_bypass.cpp 关键行 —— ::new 显式全局作用域:真正的绕过通道(警示实验)
// :: 强制从全局作用域查找 —— 类的私有版本被跳过,禁令失效
StackOnly* p = ::new (buf) StackOnly;
# 编译 + 运行(真实):地基地址 0x16b0ba0eb / StackOnly 构造 @0x16b0ba0eb / StackOnly 析构
| 堆上通道 | 写法 | 封锁情况 | 实测结果 |
|---|---|---|---|
| 普通 new | new StackOnly | 堵死 | 'operator new' is a private member |
| 不抛异常版 | new (std::nothrow) StackOnly | 堵死 | no matching function(被遮蔽) |
| 普通 placement new | new (buf) StackOnly | 堵死 | no matching function(被遮蔽) |
| 显式全局 new | ::new (buf) StackOnly | 漏(可绕过) | 编译通过,构造成功 |
| malloc + 手动构造 | malloc + placement new | 漏(可绕过) | 完全不经过 operator new |
名字遮蔽说人话就是"一夫当关":类里一旦有叫 operator new 的成员,编译器在类作用域里"看见"它,就不再出门去找全局版本——哪怕这个成员参数对不上,也宁缺毋滥直接报错。所以声明一个私有的 operator new(std::size_t),等于顺手把 nothrow 版和普通 placement 版一起关了,比直觉上防得更严。
注意:设计约束挡得住常规写法,挡不住铁了心的 ::new 或 malloc。所以这条禁令的真正价值是:团队里任何人用正规写法 new 这个类型,都会当场编译失败——违规者必须写出"刻意绕过"的代码,代码审查一眼就能揪出来。用这类技巧的类,记得在头文件注释里写清楚约定。
把两招并排看,会发现它们一个管"出口"、一个管"入口":析构函数私有,堵的是对象"离开"的环节——栈对象要离开作用域时编译器调不动析构,于是栈上创建被拒;operator new 私有,堵的是对象"出生"的环节——new 的第一步分配被拒,于是堆上创建被禁。一个锁死后门,一个锁死前门,殊途同归:让类设计者拍板对象的出生地,收回调用者的选择权。两招的声明骨架放在一起,差异一眼可见:
// 招式一:只在堆上 —— 析构函数私有(出不去,就只能在堆上生)
class HeapOnly {
public:
HeapOnly() {}
static void destroy(HeapOnly*); // 唯一合法死法
private:
~HeapOnly() {}
};
// 招式二:只在栈上 —— operator new 私有(进不去,就只能在栈上生)
class StackOnly {
public:
StackOnly() {}
~StackOnly() {}
private:
static void* operator new(std::size_t);
};
两大招的核心参数对比如下——限制方式、原理、报错时机、典型场景一次看全:
| 对比项 | 只在堆上创建 | 只在栈上创建 |
|---|---|---|
| 限制方式 | 析构函数设为 private(需继承时用 protected) | operator new 声明为 private |
| 底层原理 | 栈对象离开作用域必须由编译器调用析构,私有则编译器拒绝分配栈空间 | new 第一步必调 operator new,私有则 new 第一步就被拦 |
| 报错时机 | 编译期(尝试创建栈对象的那一行) | 编译期(写 new 的那一行) |
| 报错样例 | variable of type 'HeapOnly' has private destructor | 'operator new' is a private member of 'StackOnly' |
| 正确释放姿势 | 静态 destroy() 或友元释放函数 | 无需手动释放,离开作用域编译器自动析构 |
| 典型场景 | 单例、自我销毁对象、引用计数对象 | 嵌入式内存受限、防泄漏"安全类型"、短命临时对象 |
选型时记住一个朴素的判断:对象需要"活得比作用域长"或"自己决定死期",就只许堆上;对象小而短命、只想零管理,就只许栈上。有一点必须说透:两种限制不可兼得——一个对象不可能同时"必须住在堆上"又"必须住在栈上"。如果你真正想要的是"既不让人随便 new、也不让人随手建栈对象"的完全管控型类,那是另一类设计(比如工厂函数 + 私有构造),第 16 章会给出完整方案。
两句口诀收好:析构私有 → 栈上活不下去 → 只许堆上生;new 私有 → 堆上活不下去 → 只许栈上生。口诀背后是同一个底层图景:对象的生死契约由"谁有权调析构、谁有权调 operator new"这两个访问点决定——把访问点管住,生命周期就管住了。
注意:别为"限制"而限制。每多一道约束,就多一分使用摩擦——调用方要背 destroy() 的规矩、要忍受不能 delete 的别扭。判断标准只有一个:这个类有没有明确的"资源归属故事"(谁拥有、谁释放、何时释放)。有,限制就是护身符;没有,限制就是官僚主义。
这一章的核心,是把上一章讲的"栈与堆两条生命周期契约"变成可由设计者强制执行的规则。两招的本质都是"用访问控制做设计约束":析构私有,让编译器拒绝为栈对象分配空间(外加外部 delete 一并失效),再开 destroy()/友元这一扇正门;operator new 私有,让 new 的第一步就翻车(名字遮蔽还顺手挡下 nothrow 与普通 placement new),只留 ::new 这条刻意才能走的暗路。两条路共同点:报错都发生在编译期、都定位在调用处——犯错成本被压到最低,这正是运筹帷幄的含义:在代码跑起来之前,就把规则定好。
两招的完整形态收进一张速查卡(可编译可运行,可直接当样板抄):
// 速查卡:两招合一(可编译可运行)—— 两行报错一并记住
#include <cstdio>
#include <cstddef>
class HeapOnly { // 招式一:只在堆上
public:
HeapOnly() { std::printf("HeapOnly 构造\n"); }
static void destroy(HeapOnly* p) { delete p; }
private:
~HeapOnly() { std::printf("HeapOnly 析构\n"); }
};
class StackOnly { // 招式二:只在栈上
public:
StackOnly() { std::printf("StackOnly 构造\n"); }
~StackOnly() { std::printf("StackOnly 析构\n"); }
private:
static void* operator new(std::size_t);
static void operator delete(void*) noexcept;
};
int main() {
HeapOnly* h = new HeapOnly; // 堆-only:new 合法
HeapOnly::destroy(h); // 堆-only:销毁走 destroy()
StackOnly s; // 栈-only:直接创建
// StackOnly* p = new StackOnly; // 栈-only:new 非法(编译错误)
}
# 两条报错"真身"(本机实测):has private destructor(堆-only 侧)/
# 'operator new' is a private member(栈-only 侧)
| 招式 | 关键字 | 管住什么 | 放行什么 | 记忆锚点 |
|---|---|---|---|---|
| 只在堆上 | 私有析构函数 | 栈对象、外部 delete | new + destroy()/友元 | 锁死后门(出口) |
| 只在栈上 | 私有 operator new | new、nothrow new、普通 placement new | 栈上直接创建 | 锁死前门(入口) |
把这一章放回整条技术线里看位置:第 14 章给了你"栈与堆"的底层地图,本章教你在类定义层面立法,第 16 章的综合实战会把这些立法手段组合起来——工厂函数、禁止拷贝、引用计数、智能指针风格的内存控制,做成一个完整的"内存控制与安全"工具箱。到那时你会发现,这一章的两招只是开始。
注意:本章所有报错与运行输出均为本机(Apple clang 21,arm64 macOS)实测。不同编译器、不同版本的报错措辞可能略有差异——比如 GCC 可能说 is private within this context——但"访问私有成员被拒"的核心语义完全一致。实验时请以你自己的编译器输出为准,别背报错原文。
# 速查卡运行输出(本机实测):两条路都走通,生命周期井井有条
g++ -std=c++17 cards.cpp -o cards && ./cards
# HeapOnly 构造 <- new 正常走公有构造函数
# HeapOnly 析构 <- destroy() 这扇正门释放堆对象
# StackOnly 构造 <- 栈对象正常构造
# StackOnly 析构 <- 离开作用域自动析构
把"控制手段"与"达成的限制"配对:① 析构函数设为 private;② operator new 设为 private;③ 构造函数设为 private。候选效果:A. 只能在栈上创建;B. 只能在堆上创建;C. 哪里都不能创建。
回顾 15.2 与 15.4:栈对象离开作用域要调析构,new 第一步要调 operator new,而构造函数是两种创建方式共同必经的一步。
① → B(析构私有,编译器拒绝为栈对象分配空间);② → A(new 第一步 operator new 被拒);③ → C(new 和栈上创建都要调构造函数,私有则全禁)。口诀:管析构管"出口",管 operator new 管"入口",管构造函数管一切。
已知 HeapOnly 的析构函数是私有的,并提供了 static void destroy(HeapOnly*)。请判断以下四条语句哪些能通过编译:① HeapOnly h;;② HeapOnly* p = new HeapOnly;;③ delete p;;④ HeapOnly::destroy(p);。
逐条对照 15.2 / 15.3 的实测结果:栈上创建、外部 delete、new 与 destroy() 分别是什么命运?
② 和 ④ 合法,① 和 ③ 编译报错。① 建栈对象需在作用域结束时调用私有析构——报 has private destructor;③ 外部 delete 同样要调私有析构——报 calling a private destructor;② new 只调公有构造函数,合法;④ destroy() 是成员函数,有权访问私有析构,合法。这套"只能 new + 只能 destroy"的纪律就是堆-only 类的完整用法。
StackOnly 声明了私有 operator new(std::size_t)。为什么 new (buf) StackOnly 这样的普通 placement new 也会编译失败,而 ::new (buf) StackOnly 却能成功?想进一步堵住 ::new 这条暗路,可以怎么做?
想想名字遮蔽发生在哪一级作用域:类作用域里"看见"成员 operator new 后还出不出去?:: 前缀又是在强制什么?
普通 placement new 在类作用域里查找到私有 operator new 成员,名字遮蔽让它不再去全局找 placement 版本,重载决议失败报 no matching function。::new 强制从全局作用域查找,跳过类成员,于是命中全局 operator new(size_t, void*),绕过成功。想堵暗路,可以在类里再声明一个私有的 placement 版本 static void* operator new(std::size_t, void*)——类作用域查到它,全局版本依旧被遮蔽;但坦白讲,恶意代码还能 malloc + 手动构造,所以设计约束的最终防线永远是代码审查。
写一个 StackOnly 类(私有 operator new),依次尝试编译并记录结果:① new StackOnly;② new (std::nothrow) StackOnly;③ new (buf) StackOnly;④ ::new (buf) StackOnly。再写一个 HeapOnly 类(私有析构 + destroy()),验证栈上创建与外部 delete 都报错、而 destroy() 正常释放。把四种 new 的结果整理成一张表。
参考 15.4 节的 ex6/ex7/ex8/ex10 四个实验:②③的报错都长成 no matching function for call to 'operator new',①的报错是 'operator new' is a private member,④能编译并运行成功。
① 报 'operator new' is a private member of 'StackOnly';②③ 报 no matching function for call to 'operator new'(名字遮蔽,candidate not viable);④ 编译通过、构造成功(全局作用域绕过)。HeapOnly 侧:HeapOnly h; 报 has private destructor,delete p 报 calling a private destructor,HeapOnly::destroy(p) 正常输出"构造 → destroy() → 析构"三行。四表一张,你就把这章的两招彻底焊死在手上了。