
前言「函数放在哪里、占多少字节」是初学者最容易问、也最容易得到错误答案的问题。常见说法包括「函数在栈上」「函数存在代码区所以sizeof能算出大小」「每个函数占固定 4 KB」这些都不准确。函数体的机器指令确实位于可执行文件的代码段加载后映射到进程的只读可执行页但函数不是对象sizeof(函数名)在 C 里是非法的对函数类型使用sizeof编译期报错你只能得到函数指针的大小。函数的「占用空间」也不是一个可以用语言层面 API 查询的值它受编译器优化、内联、对齐、链接器去重、调试信息等多种因素影响。本文分四层讲可执行文件里的段分布、运行时的栈帧、函数指针与成员函数的表示、以及「函数大小」到底该怎么估。文中的数字只在说明典型布局时出现具体数值随平台与工具链而变不能当作标准保证。一、函数体在可执行文件里代码段与只读数据以常见的 ELF 与 PE 目标格式为例一个目标文件大致包含这些段段/节内容典型属性.text函数的机器指令可读、可执行不可写.rodata字符串字面量、虚表、常量表只读.data已初始化的全局/静态变量可读写.bss零初始化的全局/静态变量可读写文件里不占实际字节.data.rel.ro需要重定位的只读数据部分虚表加载后只读函数的指令进.text。这意味着两件事第一函数体和全局变量不在同一个地方不能拿「变量在数据段」去类比函数第二.text通常是只读的向函数指针指向的地址写入会触发访问违例这是防止代码被篡改的硬件级保护配合 DEP/NX。静态成员函数、自由函数、类的非虚成员函数都只是.text里的一段指令不占用每个对象的空间。虚函数则额外需要虚表virtual table虚表本身放在只读数据段每个多态对象里多出一个指向虚表的指针。所以「加了虚函数对象变大」变大的不是函数体而是对象里的那个指针。内联函数是否出现在.text里取决于编译器被完全内联且没有取地址的inline函数可能根本不生成独立函数体被取地址或未被内联时仍会生成一份。这也解释了为什么不能通过「数符号表里的函数个数」来推断代码体积。二、运行时函数调用与栈帧函数执行时的局部变量、返回地址、保存的寄存器通常放在栈上这块区域叫栈帧stack frame。一个典型的调用过程是调用方把参数放到寄存器或栈上执行call把返回地址压栈被调用方建立栈帧、保存必要的寄存器、为局部变量调整栈指针。// 适用于 C17#include cstddef#include iostreamint add(int a, int b) { // 参数很可能直接走寄存器int sum a b; // sum 可能只在寄存器里根本没有栈槽return sum;}void frame_demo(int n) {int local n * 2;volatile int forced local; // volatile 强制产生一次存储便于观察std::cout forced \n;}注意两点。其一local是否真的占用栈空间由优化级别决定开了优化后它很可能只存在于寄存器甚至被常量折叠掉。其二递归深度受栈大小限制栈溢出stack overflow不是标准行为而是操作系统在 guard page 上触发异常的结果不同平台的默认栈大小不同。栈帧的大小与函数「占用空间」是两回事栈帧是运行时每个调用的开销函数体是指令的静态体积。一个函数可以指令很多但栈帧很小大量计算、少量局部变量也可以指令很少但栈帧很大一个大的局部数组。特别大的局部数组还可能直接导致栈溢出这时应改用std::vector或静态存储。三、函数指针、成员函数指针与sizeof函数类型不是对象类型因此sizeof不能作用于函数// 适用于 C17#include cstddef#include iostreamint free_func(int x) { return x 1; }struct Widget {int value;int get() const { return value; }virtual int kind() const { return 1; }};int main() {// sizeof(free_func); // 错误对函数类型取 sizeofstd::cout sizeof(free_func) \n; // 函数指针的大小std::cout sizeof(int (*)(int)) \n; // 同上类型写法std::cout sizeof(Widget::get) \n; // 成员函数指针std::cout sizeof(Widget) \n; // 对象大小含虚表指针}函数指针的大小是实现定义的常见平台上普通函数指针与void*同为 8 字节64 位但标准并不保证二者可以互相转换。成员函数指针通常比普通函数指针大因为它要处理虚函数分派和this调整具体大小因 ABI 而异不能假设等于 8。指向虚函数的成员函数指针还可能携带虚表偏移信息这也是它更大的原因。另外把函数指针转换成void*再转回来在 POSIX 上被广泛支持但严格按标准属于实现定义行为。跨平台代码不要依赖它。四、「函数占多少字节」与对象布局示例如果确实需要评估代码体积例如嵌入式固件的 flash 预算可行的手段是查看编译产物而不是在语言层面计算用size查看目标文件的.text、.data、.bss大小输出格式随工具链而变。用nm -S或平台对应的符号表工具查看单个符号的大小但内联、ICF相同代码折叠、-ffunction-sections都会影响结果。用链接器生成的 map 文件定位每个目标文件贡献了多少字节。对比开启-Os与-O2的产物理解优化对内联与代码体积的取舍。#仅示意命令形式具体输出随工具链而异size build/appnm -S --size-sort build/app | tail -20需要提醒的是这些数值是某个编译器、某组选项、某个目标平台下的结果换一套工具链就会变。文章里写「某函数占 37 字节」是没有普适意义的。完整示例观察对象与函数的空间关系下面的例子把「对象大小」「函数指针大小」「虚表带来的额外开销」放在一起对比不依赖任何平台头文件。// 适用于 C17#include iostream#include stringstruct Plain {int a;int b;}; // 很可能是 8 字节struct WithVirtual {int a;virtual ~WithVirtual() default;}; // 通常比 Plain 大一个指针struct WithStatic {int a;static int counter; // 静态成员不占对象空间};int WithStatic::counter 0;int main() {std::cout Plain : sizeof(Plain) \n;std::cout WithVirtual : sizeof(WithVirtual) \n;std::cout WithStatic : sizeof(WithStatic) \n;std::cout func ptr : sizeof(void (*)()) \n;std::cout member ptr : sizeof(int (Plain::*)()) \n;}按标准语义WithStatic的大小应当与「只有一个int成员的结构」相同因为静态成员属于类而不属于对象WithVirtual因为含虚函数而成为多态类型常见 ABI 会在对象里放置虚表指针因此通常比Plain大但标准不规定具体布局sizeof的结果以实现为准。Plain的大小受对齐影响两个int一般是 8 字节但这也只是常见结果。常见坑点❌ 写sizeof(some_function)想得到函数体大小。✅ 这在 C 中是编译错误函数不是对象。要评估体积请查看编译产物的段大小或符号表。❌ 认为「函数存在栈上所以调用结束函数就没了」。✅ 函数指令在代码段栈上放的是本次调用的栈帧局部变量、返回地址等二者层次不同。❌ 认为「加了虚函数函数体被复制进每个对象」。✅ 对象里只多一个虚表指针函数体仍在代码段所有同类对象共享同一份指令。❌ 假设成员函数指针与普通函数指针大小相同并互相reinterpret_cast。✅ 两者大小与表示由 ABI 决定标准不保证可以互转需要统一表示时用std::function或额外包装。❌ 用sizeof去「测量」一个函数的栈帧大小。✅sizeof只看类型且局部变量是否落栈由优化决定要了解栈用量请看编译器输出的栈使用报告。❌ 在头文件里定义非inline的普通函数被多个翻译单元包含后链接期重复定义。✅ 头文件里的函数定义加inline或改为在类内定义隐式inline或只放声明。❌ 把大数组作为局部变量认为「函数小所以没事」。✅ 栈帧大小与指令体积无关大缓冲区改用std::vector、静态存储或堆分配。❌ 用(void*)func跨平台传递函数地址并假设一定可行。✅ 函数指针转void*属实现定义需要泛型存储时用std::function或带类型信息的封装。总结问题正确答案函数体在哪可执行文件的代码段如.text加载后为只读可执行页局部变量在哪通常在栈帧优化后可能只在寄存器静态/全局变量在哪.data或.bss与函数体分离虚函数额外开销对象里一个虚表指针虚表本身在只读数据段sizeof(函数)非法函数不是对象函数指针大小实现定义成员函数指针通常更大函数体积怎么量看编译产物的段/符号/map不能靠语言特性计算把「函数」拆成指令、栈帧、指针表示三个层面之后大部分困惑就消失了。记住标准只规定语义不规定布局凡是涉及具体字节数的地方都要以目标平台的 ABI 和实际编译产物为准而不是以某篇文章里的数字为准。