抽象代码:会自杀的代码
函数能不能在干完活之后,把自己从程序里”抹掉”?对象能不能在出生的那一刻自我了断?能。今天这篇讲两个短小精悍的自杀程序——一个 C 的,一个 C++ 的。
📌 更新(2026-08-31):为写这篇重新编译复现了两个程序,并修正了对自杀对象输出的描述——它比想象中死得更干脆,详见运行一节。
自杀函数指针:信使送信的比喻
先看代码:
1 | // 一个简单(但可能不那么让人愉快)的比方是,信使P把一封信从A送到B, |
这里的 init 函数干了两件事:打印一行字,然后把指向自己的全局函数指针 __init__ 置成 NULL。第一次 __init__() 一切正常,输出:
1 | init programm |
但如果把第二次调用放开,程序就会对着空指针跳下去——当场去世。这就是”自杀”:函数在执行到一半的时候,亲手解除了自己的可调用状态。
代码开头那段注释是整个程序最好的部分,值得单独抄一遍:
一个简单(但可能不那么让人愉快)的比方是,信使 P 把一封信从 A 送到 B,而信的内容的第一句就是”干掉 P,然后再看下面的内容”。
函数就是信使,函数体就是信。信使在送达消息的过程中,按消息的指示抹掉了自己——消息送达之后,世上再无这个信使。这个比方残忍、精确、还带点文学性,是我个人最喜欢的注释之一。
严格来说这里”自杀”的是函数指针而不是函数本身:函数的字节码还躺在 .text 段里纹丝不动,死掉的是”能找到它的那把钥匙”。这个区别正是它能安全运行的原因——如果它 free 的是自己的代码,那就不是自杀,是他杀了。
自杀对象:出生即了断(C++ 版)
C++ 的对象能不能更狠一点——在构造函数里自我了断?看代码:
1 |
|
为写这篇文章重新编译运行,它的真实输出是:
1 | This is construct function. |
退出码 127,进程异常终止。注意:“And I have suicided” 这行永远打不出来,第二次析构也从未发生。要理解为什么,得先搞清楚 delete this 到底干了什么——它不是一句魔法,而是两个动作:先调用析构函数,再释放这块内存。
而这里的 obj1 是个栈上对象。于是事件顺序是:
- 构造函数打印第一行;
delete this第一步:调用析构函数——打印第二行This is destruct function.;delete this第二步:把这块内存交还给堆——但它是栈地址,free一个不在堆上的指针,进程当场崩溃;cout << "And I have suicided"以及作用域结束时本应补上的第二次析构,永远没有机会执行。
也就是说,这场”自杀”的真实剧本是:析构函数在 delete 的第一步就被触发,成了自己的葬礼;随后非法释放直接把整个进程带走。当年编译的 exe 和今天重新编译的 exe,行为一模一样——这个 crash 不是新环境的问题,它从出生那天起就是这么死的,只是没人打开过退出码。
正经话:这些动作在什么情况下是”合法”的
两个程序能跑,不代表这些写法值得提倡——它们全都在未定义行为的边缘蹦迪:
delete this在 C++ 里有一个流传甚广的”合法用法”范式(引用计数归零时对象自毁),条件苛刻:对象必须由new创建、之后绝不能再碰任何成员、也不能让析构被触发第二次。范例代码里作用域结束时那第二次析构,就是标准意义上的 UB。- 函数指针置 NULL 这边倒是完全无害的安全操作——真正危险的是置空之后再调用它(空指针跳转)。正经的写法是调用前判空,或者干脆用”一次性初始化”的标准工具。
所以这两个程序的正确打开方式是:当语言规则的可视化教具。函数指针是”地址”,所以可以置空;delete 的第一步是调用析构函数、且栈上的东西永远不归 delete 管——看懂了这两行自杀代码,你比背十条规则记得都牢。
