跳至内容

函数与参数传递

掌握 C++ 中传递参数的不同方式,理解返回值优化、函数重载以及现代属性如 [[nodiscard]]。

函数:核心抽象

函数是 C++ 中代码组织的基本单位。与许多其他语言不同,C++ 让你精细控制参数如何传入函数和从函数返回。这种控制不仅仅是语法糖 ── 它有真实的性能影响。

你传递参数的方式决定了函数是使用参数的副本、对原始值的引用,还是指向原始值的指针。选择正确的机制是设计函数接口时最重要的决策之一。

参数传递机制

C++ 支持四种主要的参数传递方式。每种在性能、安全性和语义方面都有不同的权衡:

parameter_passing.cppc++23
#include <iostream>
#include <string>
#include <vector>

// 1. Pass by VALUE — the function gets a copy
//    Use for: small types (int, double, char), when you need a local copy
void square(int x) {           // x is a copy of the argument
    x = x * x;                 // Modifying x does NOT affect the caller
    std::cout << "Inside: " << x << '\n';
}

// 2. Pass by REFERENCE — the function works on the original
//    Use for: when you need to modify the caller's variable
void double_it(int& x) {       // x IS the caller's variable
    x *= 2;                    // This DOES modify the caller's variable
}

// 3. Pass by CONST REFERENCE — read-only access to the original, no copy
//    Use for: large objects you only need to read
void print_all(const std::vector<std::string>& names) {
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    // names.push_back("Eve");  // ERROR: names is const
}

// 4. Pass by POINTER — explicit nullable indirection
//    Use for: optional parameters (can be nullptr), C API interop
void maybe_increment(int* ptr) {
    if (ptr != nullptr) {       // Must check for null!
        ++(*ptr);
    }
}

int main() {
    int val = 5;

    square(val);
    std::cout << "After square: " << val << '\n';      // Still 5 (copied)

    double_it(val);
    std::cout << "After double_it: " << val << '\n';   // Now 10 (modified)

    std::vector<std::string> people = {"Alice", "Bob", "Charlie"};
    print_all(people);         // No copy of the vector — efficient

    maybe_increment(&val);     // Pass address explicitly
    std::cout << "After maybe_increment: " << val << '\n';  // 11

    maybe_increment(nullptr);  // Safe: the function checks for null
}
输出

点击“运行”即可编译并运行这个示例。

最佳实践

遵循 C++ 核心指南中关于参数传递的建议:

- 复制成本低且不会被修改?按值传递(int、double、char、bool、小型结构体、std::string_view)。
- 复制成本高且只读?按 const& 传递(std::string、std::vector、大型对象)。
- 需要修改调用者的变量?按 & 传递(非 const 引用)。
- 可能为空?按指针传递。但考虑改用 std::optional&。
- 接管所有权(吸收)?按值传递并移动(在移动语义模块中介绍)。

经验法则:大于 2-3 个指针大小(64 位系统上 16-24 字节)的对象应该按引用而不是按值传递。

  • 对小型廉价类型按值传递:int、double、char、bool、string_view
  • 对大型只读对象按 const 引用传递:const std::string&、const std::vector&
  • 仅当函数需要修改调用者的对象时才按非 const 引用传递
  • 当 nullptr 是有效参数时按指针传递 ── 但尽可能优先使用 std::optional
  • 如果不确定,按 const 引用传递 ── 安全且高效

返回值优化(RVO/NRVO)

初学者常担心按值返回大型对象会因复制而产生开销。在现代 C++ 中,由于返回值优化(RVO)和命名返回值优化(NRVO),这种担忧几乎总是多余的。

RVO(C++17 起保证):当函数返回临时对象时(例如 return std::vector{1,2,3};),编译器必须直接在调用者的内存中构造对象。不会发生任何复制或移动。

NRVO(允许但不保证):当函数返回命名局部变量时(例如 return result;),编译器可以直接在调用者的内存中构造该变量。所有主流编译器在实践中都会这样做,即使在调试模式下也是如此。

实用建议:放心地按值返回。不要使用输出参数(void f(std::vector& out))来避免复制 ── 让编译器来优化返回值。

函数重载与默认参数

C++ 允许多个同名但参数列表不同的函数。编译器根据调用点提供的参数选择最佳匹配:

overloading.cppc++23
#include <iostream>
#include <string>
#include <vector>

// Function overloading — same name, different parameter types/counts
void log(int value) {
    std::cout << "[INT] " << value << '\n';
}

void log(double value) {
    std::cout << "[DOUBLE] " << value << '\n';
}

void log(const std::string& value) {
    std::cout << "[STRING] " << value << '\n';
}

// Default arguments — provide fallback values
void greet(const std::string& name, const std::string& greeting = "Hello") {
    std::cout << greeting << ", " << name << "!\n";
}

// [[nodiscard]] (C++17) — warn if return value is ignored
[[nodiscard]] int compute(int a, int b) {
    return a * b + a;
}

// [[nodiscard]] with reason (C++20)
[[nodiscard("Ignoring error codes can hide failures")]]
int open_file(const std::string& path) {
    // Returns 0 on success, negative on error
    return 0;  // Simplified
}

// inline — suggests the compiler inline this function (also allows
// the function to be defined in a header without ODR violations)
inline int fast_max(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    log(42);               // Calls log(int)
    log(3.14);             // Calls log(double)
    log(std::string{"hi"});// Calls log(const string&)

    greet("Alice");                // Uses default: "Hello, Alice!"
    greet("Bob", "Good morning"); // Uses provided: "Good morning, Bob!"

    // int result = compute(3, 4);  // OK
    // compute(3, 4);               // WARNING: ignoring [[nodiscard]] return value

    int status = open_file("data.txt");  // OK: return value used
}
输出

点击“运行”即可编译并运行这个示例。

常见陷阱

函数重载解析遵循复杂的规则,歧义调用是编译错误的常见来源。注意以下陷阱:

1. 隐式转换造成歧义:如果你有 f(int) 和 f(double),调用 f(3.14f)(一个 float)是歧义的,因为 float 同样好地转换为 int 和 double。解决方案:添加 f(float) 重载,或使用 static_cast。

2. 默认参数与重载的不良交互:void f(int x, int y = 0) 和 void f(int x) 在单参数调用时产生歧义。

3. const 引用 vs 值:f(int) 和 f(const int&) 对左值参数是歧义的,因为两者匹配程度相同。

当编译器报告歧义重载时,不要猜测 ── 仔细阅读错误信息。它会告诉你哪些重载是候选的以及为什么它们同样好。

  • 隐式转换可能使重载解析产生歧义 ── 添加显式重载来解决
  • 默认参数和重载可能冲突 ── 避免对同一函数名同时使用两者
  • 如有疑问,编译器错误信息会列出候选重载和歧义原因
  • 当调用参数匹配多个重载时,使用 static_cast 显式选择重载
关键要点
  • 对小型类型按值传递,大型只读类型按 const 引用传递,可变类型按非 const 引用传递
  • 放心地按值返回 ── RVO/NRVO 在实践中消除了复制
  • 函数重载根据参数类型选择最佳匹配 ── 注意隐式转换造成的歧义
  • 对返回值不应忽略的函数使用 [[nodiscard]](错误码、计算结果)
  • 默认参数提供便利但可能与重载冲突 ── 二者择其一使用
  • inline 允许在头文件中定义函数而不违反单一定义规则

测验 — 检验你的知识

(15 经验值)

1. 什么时候应该按 `const&` 传递 `std::vector<int>` 而不是按值传递?

2. 什么是返回值优化(RVO)?

3. `[[nodiscard]]` 属性有什么作用?