现代 C++ 跨平台开发-内存篇:函数调用、异常处理、异步等场景的内存管理
本文是整个【现代 C++ 跨平台开发-内存篇】系列的第 5 篇,主要涉及:函数调用、异常处理、异步等场景的内存管理。
现代 C++ 跨平台开发-内存篇:函数调用、异常处理、异步等场景的内存管理
函数调用场景的内存管理
函数调用过程
调用指令(如
call):将控制权转移到函数入口;返回值地址入栈;
参数依次入栈;
保存当前寄存器的值(保存现场);
栈帧指针(即
RSP,用于返回上层调用方)和局部变量入栈(静态变量不入栈);执行函数体;
函数返回过程
恢复寄存器的值(恢复现场);
释放局部变量的栈空间,栈帧指针恢复到上一层函数;
弹出参数和返回值地址;
调用约定
调用约定决定参数传递方式和堆栈清理方式,声明在返回值和函数名之间:
int __stdcall func(int a, int b);
__cdel
默认的约定,可不声明;
参数从右到左入栈;
调用方负责清理堆栈;
__stdcall
参数从右到左入栈;
被调用方清理堆栈;
__fastcall
前两个整型或指针参数存在寄存器,其余参数从右到左入栈;
被调用方清理堆栈;
__thiscall
C++ 特有,用于类成员函数,将
this指针作为隐式参数传递;若参数个数确定,类似
__stdcall:this指针存寄存器;被调用方清理堆栈;
若参数个数不确定,类似
__cdel:this指针在其他参数入栈后再最后入栈;
调用方清理堆栈;
RVO
编译器对于函数调用返回值的优化:直接在调用者的栈帧上构造对象,消除了保存返回值而执行的临时对象构造和拷贝。
返回引用的场景不适用;
如果用
-fno-elide-constructors关闭 RVO,编译器发现局部变量即将离开作用域变成将亡值,会尝试调用移动构造函数;影响 RVO 的场景(不适用或被抑制):
返回
static变量、类成员变量,或直接返回参数;函数内
return std::move(x);函数返回值用于赋值(会触发拷贝赋值运算符),而不是初始化构造;
返回局部引用/指针,这属于 UB(悬垂),跟 RVO 无关;
异常处理场景的内存管理
异常后,程序会从
throw点开始向上回溯调用栈(Unwinding),直到匹配到catch块,这期间栈上构造的所有对象,都会自动析构,顺序与构造顺序相反。
二进制信息
在 ELF/Mach-O 等二进制的只读数据段,包含两块异常处理相关的信息,具体在文件中所处的位置,可参考C++ 跨平台开发-二进制篇:编译器/运行时、二进制格式、静/动态库、符号分析:
.eh_frame
由 DWARF 标准定义,用于支持栈展开(stack unwinding),包含每个函数的帧信息。
下面以 ELF 格式为例,展示 .eh_frame 的主要结构:
├── CIE (Common Information Entry)
│ ├── length // CIE/FDE 总字节数(不包括 length 字段自身)
│ ├── CIE_id = 0 // 标识为 CIE(FDE 中为非零指针)
│ ├── version // 通常为 1
│ ├── augmentation string // 如 "zR"(返回地址寄存器)、"zL"(含 LSDA 指针)等
│ ├── code_alignment_factor
│ ├── data_alignment_factor
│ ├── return_address_register
│ ├── augmentation data // 可选,如 FDE 编码方式、LSDA 地址偏移等
│ └── initial_instructions // 初始 CFI 指令(用于定义寄存器恢复规则)
│
└── FDE (Frame Description Entry) × N(每个函数一个或多个)
├── length
├── CIE_pointer // 相对偏移指向对应 CIE
├── initial_location // 函数起始 PC(虚拟地址)
├── address_range // 覆盖的指令长度
├── augmentation data // 若 augmentation string 含 "zL",此处包含 LSDA 指针(即 .gcc_except_table 中某条目地址)
└── instructions // CFI 指令序列(如 DW_CFA_def_cfa, DW_CFA_offset 等)
.gcc_except_table
包含 LSDA(Language-Specific Data Area),描述:
函数中哪些代码范围有
try/catch;哪些区域需要调用析构函数(cleanup);
对于
noexcept函数,是否应调用std::terminate而非继续传播;
下面仍以 ELF 格式为例,展示 .gcc_except_table 的主要结构:
├── LPStart encoding + value // landing pad 起始地址编码及值(通常为 0,表示与 FDE pc 相同)
├── TType encoding // type table 编码方式(如 DW_EH_PE_uleb128 | DW_EH_PE_pcrel)
├── TType base offset // 指向 type table 的偏移(type_info* 数组)
├── Call Site Table
│ ├── call_site_start // 相对于函数起始的 PC 偏移(try 或可能抛异常区域开始)
│ ├── call_site_len // 区域长度
│ ├── landing_pad_offset // 相对于 LPStart 的 landing pad 偏移(0 表示无 handler,仅 cleanup)
│ └── action_offset // 指向 Action Table 的偏移(0 表示无 action)
│
├── Action Table(可选但关键) // 描述异常处理动作链,每个 entry 为 1 字节或变长
│ └── Action Entry Chain(以 action_offset 为起点的单向链表)
│ ├── ttype_index (signed LEB128)
│ │ ├── > 0:表示 catch 的类型在 Type Table 中的索引(从 1 开始)
│ │ ├── = 0:保留(不应出现)
│ │ └── < 0:表示需执行 cleanup(析构),其绝对值无直接含义,仅标志 cleanup
│ │
│ └── next_action_offset (signed LEB128)
│ ├── = 0:链表结束
│ └── ≠ 0:相对于当前 entry 起始位置的偏移(通常为负,指向前一个 action)
│
└── Type Table(可选) // type_info* 指针数组(按 ttype_index 索引),用于 catch 类型匹配
└── entries[1], entries[2], ... // 索引从 1 开始;entries[i] 是 type_info*(可能经 pcrel 编码)
运行时异常信息
运行时发生异常,会动态生成异常信息:
struct __cxa_exception {
//...
size_t referenceCount; // here on 64-bit platforms
std::type_info *exceptionType;
void (*exceptionDestructor)(void *);
unexpected_handler unexpectedHandler; // by default std::get_unexpected()
terminate_handler terminateHandler; // by default std::get_terminate()
__cxa_exception *nextException; // linked to the next exception on the thread stack
int handlerCount; // incremented in __cxa_begin_catch, decremented in __cxa_end_catch, negated in __cxa_rethrow; last non-dependent performs the clean
//...
const char *actionRecord;
const char *languageSpecificData;
void *catchTemp; // landingPad
void *adjustedPtr; // adjusted pointer of the exception object
//...
_Unwind_Exception unwindHeader;
};
具体的处理流程这里不展开。但从上面可以看出,异常信息不仅包含析构等各种回调函数指针,还包含 type_info。
编译选项
-fno-exceptions -fno-asynchronous-unwind-tables:
栈帧信息和异常表都不会生成;
直接
std::terminate;
-fno-exceptions -fasynchronous-unwind-tables:
异步 unwinding:只生成栈帧信息而无异常表,仅用于跟踪调用栈;
既不会调用析构函数,也不会
std::terminate;
-fexceptions:
全量生成栈信息和异常表(另一个编译参数被忽略);
如果是
noexcept函数,或者未捕获异常,会std::terminate;
性能影响
综合上面的二进制信息和运行时信息看,C++ 异常对性能的影响主要包含:
二进制膨胀;
运行时 RTTI 开销。
最佳实践
尽量多利用 RAII 管理资源,这样即使遇到异常一般也能正常释放;
析构函数不要抛异常:否则会打断 unwinding,直接调用
std::terminate();移动构造/运算符应声明
noexcept,否则不会被调用。
更多相关内容可参考:
异步场景相关内存管理
函数式编程
无捕获:
- 会被优化为静态函数指;
按值捕获:
只读(
const),最安全(避免悬垂引用);若需修改(包括移动),须加上
mutable关键字。
引用捕获:
不能捕获右值引用;
有悬垂引用风险;
指针捕获:
裸指针要按值捕获;
shared_ptr可用于 lambda 等异步场景避免内存提前释放;
static成员会被隐式捕获;避免直接捕获
this,尽量按需捕获成员;std::function底层是类型擦除,涉及虚函数开销,除非用于存储回调等场景,否则应考虑模板函数。
惰性初始化
前面提到,局部静态变量会在第一次调用时初始化,并且编译器保证其线程安全;将它跟 lambda 结合可实现惰性初始化:
std::string write(const Json::Value& value) {
static thread_local const auto builder = [] {
Json::StreamWriterBuilder b;
b.settings_["indentation"] = "";
return b;
}();
return Json::writeString(builder, value);
}
线程初始化
std::thread 构造函数:
参数默认会被
decay()转为值类型,因为异步场景易出现悬垂引用;若确实要传引用,需通过
std::ref包装;
template<typename T>
class reference_wrapper {
T* ptr;
public:
reference_wrapper(T& ref) : ptr(&ref) {}
operator T&() const { return *ptr; } //隐式转换
T& get() const { return *ptr; }
};
template<typename T>
reference_wrapper<T> ref(T& t) {
return reference_wrapper<T>(t);
}
其他场景的内存管理
处理 C 风格字符串
C 风格字符串相关操作一般都要求
\0结尾,否则属于未定义行为;
strlen():通过\0判断结尾;strcpy():逐字符比较直到\0;strcpm():复制直到遇到\0;strdup():strlen()+malloc()+strcpy();
处理 C 风格可变参数
C 风格可变参数实际上是一种运行时参数,编译器无法在编译期检查参数是否匹配,因此必须小心使用。
va_start和va_end要匹配,否则属于 UB;如果要多次遍历或传给其他函数,应该用
va_copy拷贝一份va_list,而不能重复va_start,否则也是不匹配 UB;
va_list args;
va_start(args, msg);
//拷贝一份,用于获取长度
va_list args_copy;
va_copy(args_copy, args);
const auto len = vsnprintf(nullptr, 0, msg, args_copy);
va_end(args_copy);
if (len > 0) {
std::vector<char> buffer(len + 1);
vsnprintf(buffer.data(), buffer.size(), msg, args);
//...
}
va_end(args);
其实对于现代 C++,更安全的的方式是改用编译期可变模板参数:
如果支持 C++20 的
std::format,一行代码即可优雅地处理可变参数格式化;如果不支持,可考虑结合 C++17 的
std::make_tuple和折叠表达式:
#if !defined(USE_STD_FORMAT)
template <typename... Args>
std::string dynxxLogFormatT(std::string_view format, Args&&... args) {
std::ostringstream oss;
auto formatWithArgs = [&oss, &format](auto... xArgs) {
std::string tmpFormat{format};
constexpr auto flag = "{}";
((oss << tmpFormat.substr(0, tmpFormat.find(flag)) << xArgs, tmpFormat.erase(0, tmpFormat.find(flag) + 2)), ...);
oss << tmpFormat;
};
std::apply(formatWithArgs, std::make_tuple(args...));
return oss.str();
}
#endif
template<typename... Args>
void dynxxLogPrintF(DynXXLogLevelX level, std::string_view format, Args&&... args) {
auto fContent =
#if !defined(USE_STD_FORMAT)
dynxxLogFormatT(format, std::forward<Args>(args)...)
#else
std::vformat(std::string{format}, std::make_format_args(args...))
#endif
;
dynxxLogPrint(level, fContent);
}