C++ 参数传递方式全解析(多种方式)

📅 发布时间:2026/10/21 14:45:56
C++ 参数传递方式全解析(多种方式)
前言函数参数怎么传是 C 里少数每天都在用、却很少系统梳理的知识点。初学者通常记住三条口诀内置类型传值、大对象传const引用、需要修改传引用。这三条在多数情况下够用但它们背后还有几个必须理解的分支值传递什么时候发生拷贝、什么时候发生移动const T绑定临时对象时生命周期怎么延长模板里的T为什么不是右值引用而是转发引用数组作为参数为什么会退化成指针。一个典型误解是传引用一定比传值快。引用在语义上避免拷贝但编译器对小的可平凡复制类型如int、double、小结构体传值时往往能通过寄存器完成而引用本质上常以指针实现反而多了一次间接寻址。另一个误解是const T可以随便存起来。它绑定临时对象时生命周期只延长到该引用本身的生命周期把引用成员或返回的引用保存下来很容易变成悬垂引用。本文按值、左值引用、const引用、右值引用与转发引用、指针与数组五类展开每类说明语义、开销来源和适用场景最后给出一个综合示例。代码以 C17 为基准涉及 C20 的std::span会单独注明。本机没有编译环境示例未经过编译结论按标准语义推导。一、值传递拷贝、移动与对象切片按值传递意味着形参是实参的一个独立对象。对类类型这会调用拷贝构造函数或移动构造函数// 适用于 C17void take_value(std::string s); // 传左值拷贝构造传右值移动构造std::string a hello;take_value(a); // 拷贝a 不受影响take_value(std::string(world)); // 移动或省略移动直接构造形参take_value(std::move(a)); // 移动此后 a 处于有效但未指定状态C17 起用纯右值prvalue初始化同类型对象时保证复制省略guaranteed copy elisionstd::string(world)直接构造在形参的存储位置上不产生临时对象再拷贝。这减少了值传递在右值场景下的成本但不改变形参是独立对象的语义。值传递最大的语义陷阱是对象切片object slicing把派生类对象按值传给基类参数只复制基类子对象派生部分被丢掉。// 适用于 C17struct Base {virtual ~Base() default;virtual void describe() const {}};struct Derived : Base {int extra 42;void describe() const override {}};void by_value(Base b); // 传 Derived 会切片void by_reference(const Base b); // 保留动态类型by_value内部对b调用虚函数调用的是Base::describe因为对象的动态类型已经是Base。需要多态行为时参数必须是引用或指针。值传递适合内置类型、小型可平凡复制结构、需要在函数内拥有一份独立副本的场景以及接收所有权的 sink 参数见第三节。二、左值引用与 const 引用T只能绑定左值函数内对形参的修改直接作用于实参。const T既能绑定左值也能绑定右值并且不复制对象// 适用于 C17void append_bang(std::string s) { s !; } // 可修改实参void print_size(const std::string s); // 只读绑定左值与右值均可std::string text hi;append_bang(text); // text 变为 hi!print_size(text); // 绑定左值print_size(std::string(temp)); // 绑定临时对象生命周期延长到函数返回const T绑定临时对象时临时对象的生命周期被延长到该引用的生命周期结束。注意这只适用于直接绑定如果引用是某个对象的成员或者经过函数返回延长规则不覆盖仍可能悬垂。// 适用于 C17const std::string bad() {return std::string(temporary); // 返回悬垂引用UB}函数返回后临时对象已销毁调用方拿到的是悬垂引用任何读取都是未定义行为undefined behavior, UB。同理类的引用成员若绑定构造时传入的临时对象对象构造完成后引用就可能悬垂。const T并不总是最优。对于int、double、指针、两个int的小结构体按值传递通常更直接也更容易被优化。经验规则是小型可平凡复制类型按值大型或复制昂贵的类型按const引用。具体阈值取决于 ABI 和编译器不应教条化。三、右值引用、转发引用与 sink 参数T出现在非模板函数参数中时是右值引用只能绑定右值// 适用于 C17void consume(std::string s); // 只能传右值consume(std::string(x)); // OK// std::string v; consume(v); // 错误不能把左值绑定到右值引用而在模板参数中T是转发引用forwarding reference旧称 universal reference。它既能绑定左值也能绑定右值具体类型由实参推导// 适用于 C17#include utilitytemplate typename Tvoid relay(T arg) {consume(std::forwardT(arg)); // 左值实参保持左值右值实参转为右值}推导规则是引用折叠传左值std::string时T推导为std::stringT折叠为std::string传右值时T推导为std::stringT就是std::string。std::forwardT根据T是否为左值引用决定是否转换从而把原始值类别传给下一层。转发引用只在类型推导发生时成立。const T、std::vectorT这类带额外限定的形式不是转发引用成员函数的T若T是类模板参数而不是该函数自己的模板参数也不是转发引用。这是看起来像却不能用的常见来源。对于接管一个对象的 sink 参数现代写法是按值接收再移动// 适用于 C17#include string#include utilityclass Widget {public:explicit Widget(std::string name) : name_(std::move(name)) {}const std::string name() const { return name_; }private:std::string name_;};传左值时形参拷贝一次、移入成员一次传右值时形参移动构造一次、移入成员一次。相比写const std::string和std::string两个重载代码更短且对左值/右值都不会多出额外拷贝。代价是左值场景多一次移动通常很便宜并且模板推导时可能不如完美转发精确。四、指针、数组与函数参数退化指针传递本质上也是按值传递——复制的是地址。它可以是空指针需要调用方或函数内检查引用则通常要求绑定有效对象但悬垂引用仍可能产生。数组作为参数时会退化为指向首元素的指针长度信息丢失// 适用于 C17#include cstddefvoid bad(int a[10]); // 实际类型是 int*10 被忽略template std::size_t Nvoid good(const int (a)[N]); // 保留长度但只能接受数组void modern(const int* data, std::size_t n);void bad(int a[10])与void bad(int* a)是同一个函数类型函数体内sizeof(a)得到的是指针大小而不是数组大小。要保留长度可以传指针加长度或用std::array、std::vectorC20 起还可以用std::span需要 C20 及以上且各标准库支持情况不同。字符串参数同理C17 的std::string_view可以避免构造std::string但它不保证以空字符结尾不能直接传给期望 C 字符串的接口。函数指针作为参数时通常优先用模板或std::function。模板接受任意可调用对象且没有类型擦除开销std::function可以存储状态但可能有堆分配和间接调用成本。选择取决于是否需要运行时多态。五、综合示例与选择表// 适用于 C17#include cstddef#include iostream#include string#include utility#include vectorstruct Base {virtual ~Base() default;virtual const char* name() const { return Base; }};struct Derived : Base {const char* name() const override { return Derived; }};void show(const Base b) { // 多态参数用引用std::cout b.name() \n;}void fill(std::vectorint out, std::size_t n) { // 输出参数用引用out.clear();out.reserve(n);for (std::size_t i 0; i n; i) {out.push_back(static_castint(i));}}void log_text(std::string_view text) { // 只读字符串视图按值传递std::cout text.size() \n;}class Session {public:explicit Session(std::string id) : id_(std::move(id)) {} // sink 参数const std::string id() const { return id_; }private:std::string id_;};int main() {Derived d;show(d); // 不发生切片std::vectorint values;fill(values, 4);log_text(hello);Session s(session-1);return 0;}传递方式是否复制能否绑定右值典型用途T拷贝或移动是小型类型、需要独立副本、sink 参数T否否修改实参、输出参数const T否是只读的大对象、避免切片T否是仅右值移动构造、接管资源模板T否是左右值均可完美转发T*否复制地址是可选参数、C 接口、数组std::string_view否是只读字符串不要求所有权std::spanTC20否是连续区间视图选择时先问三个问题函数需不需要修改实参需不需要拥有数据对象是不是多态类型答案决定用引用、值还是指针。至于性能先保证语义正确再用测量工具比较不要凭引用一定更快下结论。常见坑点❌ 返回局部变量的引用或指针。✅ 按值返回或返回静态/堆上对象的引用并明确所有权悬垂引用的读取是 UB。❌ 用const T成员保存构造时传入的临时对象。✅ 需要长期持有时按值保存或使用智能指针引用成员的延长规则不覆盖成员初始化。❌ 在非模板函数里把T当成万能引用。✅ 非模板的T只是右值引用转发引用要求类型推导典型是函数模板参数。❌ 对const对象使用std::move以为能移动。✅const T无法调用接受T的移动构造通常退化为拷贝std::move本身不移动任何东西。❌ 写void f(int a[10])并认为函数知道数组长度。✅ 数组参数退化为int*传长度、用std::array/std::vector或 C20 的std::span。❌ 把派生类对象按值传给基类参数指望虚函数生效。✅ 用const Base或Base*按值传递会发生对象切片。❌ 在同一个表达式里多次std::forward同一个转发引用。✅ 转发引用只能转发一次第二次使用时它可能已被移动处于有效但未指定状态。❌ 认为const T对所有类型都最快。✅ 小型可平凡复制类型按值往往更合适以 ABI、编译器优化和实际测量为准。总结决策推荐写法只读、大对象、多态const T需要修改实参T接管所有权按值 std::move泛型转发模板Tstd::forwardT可选参数T*或std::optionalTC17只读字符串std::string_view注意生命周期连续区间指针加长度或 C20 的std::span参数传递的第一原则是语义谁拥有数据、谁修改数据、对象是否多态。性能只是第二层考量而且高度依赖类型大小、ABI 和优化器。把值类别和生命周期这两件事想清楚绝大多数悬垂引用和意外拷贝都能在写代码时就避免。