c++命名对象和匿名对象的解析

发布时间:2026/11/4 5:56:19
c++命名对象和匿名对象的解析 前言先澄清术语。中文语境里的「匿名对象」通常指临时对象temporary object也就是没有名字、由编译器在表达式求值过程中创建的类类型对象比如Widget(x)或者a b的结果。它和「命名对象」named object有标识符的变量相对。需要说明的是C 标准里并没有「匿名对象」这个术语标准用的是「临时对象」另一个容易被混进来的概念是匿名命名空间anonymous namespace那是限制链接性的机制和本文讲的不是一回事。围绕临时对象的误解主要有三个。第一是以为Widget w Widget();一定会构造两次、拷贝一次于是在 C11 之前拼命优化返回值实际上 C17 起这种写法保证只有一次构造。第二是以为临时对象的生命周期总是到分号结束忽略了绑定到引用时的延长规则。第三是反过来以为只要用const接住就万事大吉结果在「引用成员」「函数返回值」「成员访问」这几种场景下拿到悬垂引用。本文按「什么时候产生临时对象、它活多久、拷贝是否真的发生、几种初始化写法有什么区别」的顺序展开最后给出可以照着检查的规则表。代码以 C17 为基准。一、临时对象在什么时候产生临时对象主要由以下几类表达式产生第一显式调用构造函数但不给它名字如Widget(x)。这是一个纯右值prvalue表示「一个尚未具现化的对象」。第二函数按值返回。Widget make();的调用表达式本身是 prvalue如果函数返回引用则不产生临时对象但可能悬垂。第三类型转换。把一个int传给接受Widget的参数时编译器会用转换构造函数创建一个临时对象。第四运算符的结果。a b返回Widget时结果是一个 prvalue内置类型的a b不产生类对象但同样属于右值。第五绑定引用时的中间对象。const Widget r Widget(x);中的Widget(x)是临时对象但它的生命周期被延长了。理解这些来源之后一个重要的推论是临时对象是「值」而不是「变量」。不能对纯右值表达式取地址也不能给它赋值它只能被读取、被绑定到引用、被移动或者作为实参传递。二、生命周期全表达式与引用延长默认规则是临时对象在完整表达式full-expression结束时销毁。所谓完整表达式通常就是一条以分号结尾的语句。所以std::cout make().name \n;里的临时对象活到分号安全而const char* p make().name.c_str();里的临时对象也在分号处销毁p立刻悬垂。例外是引用延长把一个临时对象绑定到const左值引用或右值引用时临时对象的生命周期被延长到该引用的生命周期结束。// 适用于 C17const Widget a make(); // 延长a 有效期内临时对象一直存在Widget b make(); // 同样延长延长规则有几个明确的例外必须记住第一绑定到函数参数的引用不延长。void f(const Widget);调用f(make());时临时对象只活到f返回之后的那条完整表达式结束f内部使用它是安全的但f不能把它存起来。第二return语句中的引用不延长。函数返回const Widget而实际返回的是一个临时对象时临时对象在函数返回时就销毁调用方拿到的是悬垂引用编译器通常会给出警告但这属于未定义行为undefined behavior, UB不是「一定崩溃」。第三绑定到类的引用成员不延长。在构造函数的成员初始化列表里写ref_(make())临时对象在构造函数结束时就销毁成员引用立刻悬垂。这是最隐蔽的一种。第四通过成员访问取得的引用不延长。const auto x make().name;中被延长的是name这个成员所绑定的对象吗不是——延长只作用于「直接绑定到临时对象」的引用而这里引用绑定的是临时对象的成员临时对象本身仍然在分号处销毁x悬垂。正确写法是先auto tmp make();再取tmp.name。三、拷贝消除C17 的保证在 C17 之前Widget w Widget(x);在语义上要求「先构造临时对象再拷贝构造w」编译器可以优化掉这次拷贝但这不是强制的而且被优化的拷贝构造仍然必须是可访问的。C17 引入了「保证的拷贝消除」guaranteed copy elision当用同类型的 prvalue 初始化对象时直接在该对象的存储上构造根本不存在临时对象也就不存在拷贝。这条规则的直接后果是即使一个类删除了拷贝构造和移动构造Widget w Widget(x);依然合法。同理函数返回 prvalue 时也不需要移动构造。// 适用于 C17struct NonCopyable {NonCopyable() default;NonCopyable(const NonCopyable) delete;NonCopyable(NonCopyable) delete;};NonCopyable make_nc() {return NonCopyable{}; // C17 起合法无需拷贝或移动}int main() {NonCopyable a NonCopyable{}; // 合法NonCopyable b make_nc(); // 合法// NonCopyable c std::move(a); // 错误移动构造被删除return 0;}注意最后一行的对比拷贝消除只适用于「prvalue 直接初始化同类型对象」一旦写成std::move(a)表达式变成将亡值xvalue必须调用移动构造而移动构造被删除了于是编译失败。这条边界经常被误解为「C17 之后移动构造没用了」实际上移动构造在容器扩容、std::vector::push_back(std::move(x))这些场景里仍然不可替代。四、几种初始化写法的区别写法语义备注Widget w;默认初始化对内置类型成员不初始化类类型调用默认构造Widget w{};列表初始化内置类型成员被零初始化避免未定义值Widget w(x);直接初始化一次构造无临时对象Widget w Widget(x);拷贝初始化C17 起保证消除等价于直接初始化Widget w x;隐式转换构造函数不能是explicit否则编译失败auto w Widget(x);值推导推导出Widget同样消除最容易被忽略的是第一行与第二行的差别。Widget w;对类类型会调用默认构造函数如果类里有int成员且构造函数没有初始化它这个成员的值是不确定的读取它是 UB。Widget w{};会做值初始化把内置类型成员置零。对于聚合类型Widget w;甚至不会初始化任何成员。Widget w x;依赖隐式转换如果构造函数声明为explicit这几乎是所有单参数构造函数的推荐做法这行就会编译失败必须写成Widget w(x);或Widget w{x};。常见坑点坑点一把c_str()的指针存起来。❌const char* p make_string().c_str();临时std::string在分号处销毁p悬垂。✅ 先std::string s make_string();再在s的生命周期内使用s.c_str()。坑点二认为成员访问也能延长临时对象生命周期。❌const auto name make_widget().name;被延长的是成员绑定的对象不是外层临时对象。✅ 先具名保存对象auto w make_widget(); const auto name w.name;。坑点三在构造函数初始化列表里绑定引用成员。❌Holder() : ref_(make()) {}构造结束临时对象销毁ref_立刻悬垂。✅ 成员按值保存或者要求调用方传入生命周期足够长的对象。坑点四返回局部对象的引用。❌const Widget make() { Widget w; return w; }返回后w已销毁。✅ 按值返回依赖 C17 的拷贝消除不产生额外开销。坑点五以为 C17 之后移动构造可以删掉。❌ 删除移动构造后仍在容器里push_back(std::move(x))编译失败。✅ 拷贝消除只覆盖 prvalue 初始化需要转移所有权的地方仍然依赖移动构造。坑点六用Widget w;得到未初始化的内置成员。❌ 类里有int count;且构造函数未初始化随后count;读取未初始化值是 UB。✅ 用Widget w{};或在类内给成员默认初始化int count 0;。坑点七以为临时对象可以作为非 const 引用参数传递。❌void f(Widget); f(Widget(x));编译失败非 const 左值引用不能绑定临时对象。✅ 参数改成const Widget或Widget或者先具名再传。坑点八在 range-for 中遍历临时对象的成员。❌for (auto x : make_holder().items)绑定的是临时对象的成员引用外层临时对象在循环开始前就可能销毁C20 及以前的规则下这是悬垂。✅ 先把对象具名保存auto h make_holder(); for (auto x : h.items)。C23 起范围初始化中的所有临时对象都被延长到循环结束但不要依赖这个较新的规则。总结要点结论术语标准称「临时对象」不叫匿名对象销毁时机默认在完整表达式结束时引用延长绑定到const或时延长不延长的情况函数参数、return、引用成员、成员访问拷贝消除C17 起 prvalue 初始化同类型对象保证无拷贝移动构造仍然必要处理 xvalue 与容器操作初始化写法优先T x{};与直接初始化悬垂高发点c_str()、成员引用、返回引用临时对象的全部难点可以压缩成一句话它活在表达式里不在语句之外。凡是想让它的生命周期超过当前语句就必须用引用直接绑定或者先具名保存凡是间接拿到它的内部指针或成员引用都要先问一句「外层对象还在吗」。把这条规则记牢绝大多数悬垂问题都能在写代码时就避开。