函数能不能在干完活之后,把自己从程序里”抹掉”?对象能不能在出生的那一刻自我了断?能。今天这篇讲两个短小精悍的自杀程序——一个 C 的,一个 C++ 的。

📌 更新(2026-08-31):为写这篇重新编译复现了两个程序,并修正了对自杀对象输出的描述——它比想象中死得更干脆,详见运行一节。

自杀函数指针:信使送信的比喻

先看代码:

自杀函数指针.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 一个简单(但可能不那么让人愉快)的比方是,信使P把一封信从A送到B,
// 而信的内容的第一句就是"干掉P,然后再看下面的内容"。

#include <stdio.h>

void init(void);
void (*__init__)(void) = init;

void init(void)
{
printf("init programm\n");
__init__ = NULL;
}

int main()
{
__init__();

// __init__(); //NULL

return 0;
}

这里的 init 函数干了两件事:打印一行字,然后把指向自己的全局函数指针 __init__ 置成 NULL。第一次 __init__() 一切正常,输出:

1
init programm

但如果把第二次调用放开,程序就会对着空指针跳下去——当场去世。这就是”自杀”:函数在执行到一半的时候,亲手解除了自己的可调用状态。

代码开头那段注释是整个程序最好的部分,值得单独抄一遍:

一个简单(但可能不那么让人愉快)的比方是,信使 P 把一封信从 A 送到 B,而信的内容的第一句就是”干掉 P,然后再看下面的内容”。

函数就是信使,函数体就是信。信使在送达消息的过程中,按消息的指示抹掉了自己——消息送达之后,世上再无这个信使。这个比方残忍、精确、还带点文学性,是我个人最喜欢的注释之一。

严格来说这里”自杀”的是函数指针而不是函数本身:函数的字节码还躺在 .text 段里纹丝不动,死掉的是”能找到它的那把钥匙”。这个区别正是它能安全运行的原因——如果它 free 的是自己的代码,那就不是自杀,是他杀了。

自杀对象:出生即了断(C++ 版)

C++ 的对象能不能更狠一点——在构造函数里自我了断?看代码:

自杀对象.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
using namespace std;

class TestClass
{
public:
TestClass();
~TestClass(){cout << "This is destruct function." << endl;}
protected:
private:
};

TestClass::TestClass()
{
cout << "This is construct function." << endl;
delete this;
cout << "And I have suicided " << endl;
}

int main()
{
// struct x struct z<x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(x(y,x(y><y*,x(y*w>v<y*,w,x{}
TestClass obj1;
return 0;
}

为写这篇文章重新编译运行,它的真实输出是:

1
2
This is construct function.
This is destruct function.

退出码 127,进程异常终止。注意:“And I have suicided” 这行永远打不出来,第二次析构也从未发生。要理解为什么,得先搞清楚 delete this 到底干了什么——它不是一句魔法,而是两个动作:先调用析构函数,再释放这块内存

而这里的 obj1 是个栈上对象。于是事件顺序是:

  1. 构造函数打印第一行;
  2. delete this 第一步:调用析构函数——打印第二行 This is destruct function.
  3. delete this 第二步:把这块内存交还给堆——但它是栈地址free 一个不在堆上的指针,进程当场崩溃;
  4. cout << "And I have suicided" 以及作用域结束时本应补上的第二次析构,永远没有机会执行。

也就是说,这场”自杀”的真实剧本是:析构函数在 delete 的第一步就被触发,成了自己的葬礼;随后非法释放直接把整个进程带走。当年编译的 exe 和今天重新编译的 exe,行为一模一样——这个 crash 不是新环境的问题,它从出生那天起就是这么死的,只是没人打开过退出码。

正经话:这些动作在什么情况下是”合法”的

两个程序能跑,不代表这些写法值得提倡——它们全都在未定义行为的边缘蹦迪:

  • delete this 在 C++ 里有一个流传甚广的”合法用法”范式(引用计数归零时对象自毁),条件苛刻:对象必须由 new 创建、之后绝不能再碰任何成员、也不能让析构被触发第二次。范例代码里作用域结束时那第二次析构,就是标准意义上的 UB。
  • 函数指针置 NULL 这边倒是完全无害的安全操作——真正危险的是置空之后再调用它(空指针跳转)。正经的写法是调用前判空,或者干脆用”一次性初始化”的标准工具。

所以这两个程序的正确打开方式是:当语言规则的可视化教具。函数指针是”地址”,所以可以置空;delete 的第一步是调用析构函数、且栈上的东西永远不归 delete 管——看懂了这两行自杀代码,你比背十条规则记得都牢。

系列其他文章:下划线完形填空尊嘟假嘟。系列缘起见抽象代码宣言