Skip to content

参数传递方式:才不是"传个参数而已" ​

数据以什么方式传进函数、结果以什么方式传回来,直接决定了程序的正确性和性能。您可能觉得"传个参数而已,有什么好讲的",但恰恰是这些看似不起眼的细节,在实际项目里制造了大量的 bug 和性能问题——拷贝了一个不该拷贝的大对象导致性能暴跌,或者不小心通过引用改掉了调用者的数据导致难以追踪的逻辑错误。

C++ 的三种核心参数传递方式:值传递、引用传递、const 引用传递。这里,我们就开始我们对他们的认识之旅。

值传递:函数拿到的是副本 ​

值传递是最直观的传参方式:调用函数时,实参被复制一份。

调用函数时,实参被复制一份。调用函数时,实参被复制一份。调用函数时,实参被复制一份

笔者喜欢在这个地方上再三的强调。搞C的朋友会会心一笑。原来C++也有这个问题啊。是的。

我们的函数体操作的是这个副本,咱们传进去的原变量完全不受影响。

C++
#include <iostream>

void add_ten(int x)
{
    x += 10;
    std::cout << "函数内 x = " << x << std::endl;
}

int main()
{
    int value = 5;
    add_ten(value);
    std::cout << "函数外 value = " << value << std::endl;
    return 0;
}

运行结果:

text
函数内 x = 15
函数外 value = 5

value 还是 5,一点没变——add_ten 的参数 x 是 value 的一份拷贝,改的是副本,原始变量毫发无损。这种隔离性在很多时候正是咱们想要的:函数内部的修改不会泄漏到外部。

但值传递的代价也很明显——每次调用都要拷贝。对于 int、double 这种只有几个字节的基本类型,拷贝开销可以忽略不计。可如果咱们要传的参数是一个包含数万个元素的结构体呢?

C++
struct SensorData {
    int readings[10000];
    double timestamps[10000];
    char description[256];
};

void process(SensorData data)  // 整个结构体被拷贝一份
{
    // 处理数据...
}

每次调用 process,编译器都要把 SensorData 的大约 80 KB 数据完整复制一遍。咱们要是在循环里频繁调用,那就是灾难级的无意义拷贝。而且,很有可能根本达到的不是你想要的效果。非原生对象的值传递,很少见。

但是别滥用哦~,有人说我int也搞个引用,double也搞个引用。。。我可以告诉你,你这样做反而必然导致性能退化(在不开优化下),记住,自定义类型,我们才会建议考虑引用。原生类型,放心大胆的值传递,除非你确实希望你的入参被作用后还能带出这个函数。我们把这个叫做入参。

引用传递:直接操作原始数据 ​

引用传递的思路是:不拷贝,直接让函数拿到调用者的原始变量。咱们在参数类型后加 &,就声明了一个引用参数。

C++
void add_ten(int& x)
{
    x += 10;
}

int main()
{
    int value = 5;
    add_ten(value);
    // value 现在是 15
    return 0;
}

这回 value 变成了 15,咱们改的就是原件:x 是 value 的引用,value 本身的另一个名字。

引用传递最经典的应用场景是 swap 函数。在 C 语言里必须传指针,C++ 有了引用,咱们写起来干净多了:

C++
/// @brief 交换两个整数的值
/// @param a 第一个整数
/// @param b 第二个整数
void swap_values(int& a, int& b)
{
    int temp = a;
    a = b;
    b = temp;
}

int main()
{
    int x = 3;
    int y = 7;
    swap_values(x, y);
    std::cout << "x = " << x << ", y = " << y << std::endl;
    return 0;
}

运行结果:

text
x = 7, y = 3

交换成功。调用语法也非常自然,咱们不需要像 C 那样取地址传指针。

但引用传递也带来了新的约束和陷阱。

非 const 引用参数只能绑定到左值——也就是有名字、有地址的变量。字面量、临时值(右值)都不能传给非 const 引用。比如当前的 add_ten(int&) 若被 add_ten(5) 调用会编译报错,因为 5 是个字面量,没有内存地址可供引用绑定。同理,swap_values(x, 3) 也编译不过:数字字面量 3 没法被改写,编译器直接拦下。咱们要是看到类似 cannot bind non-const lvalue reference to an rvalue 的编译错误,多半就是这个问题。

const 引用传递:两全其美 ​

咱们面临的选择是:值传递安全但有拷贝开销,引用传递高效但能修改原始数据。有没有一种方式既不拷贝又不允许修改?有,就是 const 引用:

C++
void print(const std::string& s)
{
    std::cout << s << std::endl;
    // s += "!";  // 编译错误,const 引用不允许修改
}

const std::string& s 拆开看:& 表示引用,不发生拷贝;const 表示只读,函数内部不能通过 s 修改原始字符串。您在调用端看到 const,就知道"这个函数不会动我的数据",意图非常清晰。

const 引用还有一个咱们马上要用到的特性:它能绑定到右值。前面非 const 引用不肯收的字面量,const 引用都能收:

C++
void print(const std::string& s);

print(std::string("hello"));  // OK:const 引用绑定到临时对象
print("world");               // OK:const 引用绑定到隐式构造的临时 string

这让 const T& 成为极其灵活的参数类型:左值右值都能收,又避免拷贝、保证只读。咱们后面会反复用到它。

咱们回头看前面那个拷贝大结构体的例子,用 const 引用改写:

C++
void process(const SensorData& data)  // 零拷贝,只读访问
{
    // 处理数据...
}

拷贝开销消失了,data 在函数内部还是只读的,不会意外改掉咱们传进去的数据——前面说的"两全其美",落在这儿了。

怎么选:传参方式的决策指南 ​

三种传参方式各有适用场景,咱们把决策规则整理清楚。对于基本类型(int、double、float、指针等,通常不超过 8 字节),直接用值传递。这些类型拷贝成本极低,值传递既安全又简单,而且对编译器优化更友好。咱们要是看到有人写 void foo(const int& x),那大概率是过度优化——传一个 int 的引用并不比传 int 本身快,在某些平台上反而更慢(引用本质上是指针实现的,需要一次额外的间接寻址)。

对于比较大或者比较复杂的类型(std::string、std::vector、自定义结构体等),咱们只需要读取数据而不修改,用 const T&;要修改调用者的数据(比如 swap、填充输出结构体),用非 const 引用 T&。

咱们把决策规则总结成一张表:

参数类型不修改需要修改
基本类型T(值传递)T(值传递后返回)
非平凡类型const T&T&

这条规则在绝大多数情况下都适用。等您学到移动语义和完美转发之后,会知道还有更精细的传参策略(比如按值传递 + move),但在当前阶段,上面这张表就足够指导日常编码了。

返回值:怎么把结果交还给调用者 ​

函数的返回值同样涉及传递方式的选择。大多数情况下,咱们直接按值返回就对了:

C++
std::string greet(const std::string& name)
{
    return "Hello, " + name + "!";
}

您可能会担心:返回一个 std::string 不会发生拷贝吗?实际上,现代 C++ 编译器会做两种关键优化:RVO(Return Value Optimization)和 NRVO(Named Return Value Optimization)。简单来说,编译器会直接在调用者预留的内存空间里构造返回值,省去中间的拷贝或移动操作。从 C++17 开始,RVO 在某些情况下甚至是强制保证的。所以 return "Hello, " + name + "!"; 并不会产生额外的字符串拷贝,性能方面完全不用担心。

但您要是试图返回局部变量的引用,事情就危险了:

C++
const int& get_value()
{
    int x = 42;
    return x;  // 返回局部变量的引用——悬垂引用!
}

这段代码能编译通过,但运行时是未定义行为(Undefined Behavior)。x 是函数内的局部变量,函数返回后 x 的内存就被回收了,咱们返回的引用指向一块已经不存在的内存。通过这个引用去读取数据,可能读到垃圾值,可能读到"碰巧还在"的旧值,也可能直接段错误。编译器不会报错(语法上完全合法,虽然可能会警告),所以这个 bug 非常隐蔽。原则很简单:永远不要返回局部变量的引用或指针。按值返回就好了,编译器会帮咱们优化。

输出参数 vs 返回值 ​

当函数需要产生多个结果时,老式 C 代码常用引用参数来"输出",但调用端 divide(a, b, q, r) 咱们不看签名根本分不清输入输出。现代 C++ 更推荐用结构体直接返回:

C++
struct DivResult {
    int quotient;
    int remainder;
};

DivResult divide(int a, int b)
{
    return {a / b, a % b};
}

调用端 auto result = divide(a, b); 一目了然,result.quotient 比 result.first 可读性强太多了。输出参数在往大缓冲区填充数据的场景下仍有意义,但大多数时候咱们优先选择返回值。

实战演练——passing.cpp ​

咱们把这一章的知识点串成一个完整的示例程序:演示 swap 操作、不同传参方式的性能对比,以及 const 引用在字符串处理中的用法。

展开代码收起代码共 95 行
C++
// passing.cpp —— 演示值传递、引用传递和 const 引用传递

#include <iostream>
#include <string>
#include <chrono>

/// @brief 交换两个整数的值
void swap_values(int& a, int& b)
{
    int temp = a;
    a = b;
    b = temp;
}

struct BigData {
    int payload[4096];  // 16 KB
};

/// @brief 值传递版本:每次调用拷贝整个 BigData
long sum_by_value(BigData data)
{
    long total = 0;
    for (int i = 0; i < 4096; ++i) {
        total += data.payload[i];
    }
    return total;
}

/// @brief const 引用版本:零拷贝
long sum_by_const_ref(const BigData& data)
{
    long total = 0;
    for (int i = 0; i < 4096; ++i) {
        total += data.payload[i];
    }
    return total;
}

/// @brief 拼接问候语,const 引用避免字符串拷贝
std::string build_greeting(const std::string& name)
{
    return "Hello, " + name + "! Welcome to Modern C++.";
}

int main()
{
    // swap 演示
    int a = 10;
    int b = 20;
    std::cout << "交换前: a = " << a << ", b = " << b << std::endl;
    swap_values(a, b);
    std::cout << "交换后: a = " << a << ", b = " << b << std::endl;

    // 性能对比
    BigData data{};
    for (int i = 0; i < 4096; ++i) {
        data.payload[i] = i;
    }

    constexpr int kIterations = 100000;

    auto start = std::chrono::high_resolution_clock::now();
    long result_value = 0;
    for (int i = 0; i < kIterations; ++i) {
        result_value = sum_by_value(data);
    }
    auto end = std::chrono::high_resolution_clock::now();
    auto ms_value = std::chrono::duration_cast<std::chrono::milliseconds>(
                        end - start)
                        .count();

    start = std::chrono::high_resolution_clock::now();
    long result_ref = 0;
    for (int i = 0; i < kIterations; ++i) {
        result_ref = sum_by_const_ref(data);
    }
    end = std::chrono::high_resolution_clock::now();
    auto ms_ref = std::chrono::duration_cast<std::chrono::milliseconds>(
                      end - start)
                      .count();

    std::cout << "\n--- 性能对比 (" << kIterations << " 次调用) ---"
              << std::endl;
    std::cout << "值传递: " << result_value
              << ", 耗时: " << ms_value << " ms" << std::endl;
    std::cout << "const引用: " << result_ref
              << ", 耗时: " << ms_ref << " ms" << std::endl;

    // 字符串处理
    std::string name = "Charlie";
    std::cout << build_greeting(name) << std::endl;
    std::cout << build_greeting(std::string("World")) << std::endl;

    return 0;
}

编译运行:

bash
g++ -std=c++17 -O2 -Wall -Wextra -o passing passing.cpp
./passing

预期输出:

text
交换前: a = 10, b = 20
交换后: a = 20, b = 10

--- 性能对比 (100000 次调用) ---
值传递: 8386560, 耗时: 680 ms
const引用: 8386560, 耗时: 190 ms
Hello, Charlie! Welcome to Modern C++.
Hello, World! Welcome to Modern C++.

性能数据会因机器和编译优化级别不同而有差异,但趋势一致:值传递每次拷贝 16 KB,const 引用版本避开了拷贝,快了数倍。注意咱们用了 -O2,即使如此编译器也必须遵守语言语义:您让它拷贝它就得拷贝。

build_greeting 的两次调用也值得咱们注意:第一次传左值 name,第二次传临时对象 std::string("World"),两者都能通过 const std::string& 接收,这正是 const 引用的灵活性所在。

在线运行 ​

您也可以在线运行参数传递对比示例,观察值传递与 const 引用传递的性能差异:

Compiler Explorer

参数传递方式对比:值传递 vs const 引用

在线运行并对比值传递拷贝 16KB 结构体与 const 引用零拷贝的性能差异。

code/examples/vol1/09_passing.cpp

动手试试 ​

练习一:实现 swap ​

请您写一个 swap_values 函数交换两个 double 的值,再写一个重载版本交换两个 std::string。用 main 函数验证结果。

参考答案
展开代码收起代码共 39 行
C++
#include <iostream>
#include <string>

void swap_values(double &first, double &second)
{
    double temporary = first;
    first = second;
    second = temporary;
}

void swap_values(std::string &first, std::string &second)
{
    std::string temporary = first;
    first = second;
    second = temporary;
}

int main()
{
    double first_number = 3.14;
    double second_number = 2.71;

    std::cout << "交换 double 前:" << first_number << ", "
              << second_number << '\n';
    swap_values(first_number, second_number);
    std::cout << "交换 double 后:" << first_number << ", "
              << second_number << '\n';

    std::string first_text = "hello";
    std::string second_text = "world";

    std::cout << "交换字符串前:" << first_text << ", "
              << second_text << '\n';
    swap_values(first_text, second_text);
    std::cout << "交换字符串后:" << first_text << ", "
              << second_text << '\n';

    return 0;
}

编译运行:

bash
g++ -std=c++17  -Wall -Wextra main.cpp -o main &&./main

运行结果:

text
交换 double 前:3.14, 2.71
交换 double 后:2.71, 3.14
交换字符串前:hello, world
交换字符串后:world, hello

练习二:高效处理大型结构体 ​

请您定义一个包含至少 1000 个 double 元素数组的结构体 Measurement,再写两个函数:一个用值传递计算平均值,一个用 const 引用传递计算平均值。分别计时比较性能。

参考答案
展开代码收起代码共 86 行
C++
#include <chrono>
#include <iostream>

#define kValueCount 1000
#define kIterations 100000

struct Measurement
{
    double values[kValueCount];
};

double average_by_value(Measurement measurement);
double average_by_const_ref(const Measurement &measurement);

int main()
{

    Measurement measurement{};

    for (int i = 0; i < kValueCount; ++i)
    {
        measurement.values[i] = static_cast<double>(i + 1);
    }

    double value_average = 0.0;

    const auto value_start = std::chrono::steady_clock::now();
    for (int i = 0; i < kIterations; ++i)
    {
        value_average = average_by_value(measurement);
    }
    const auto value_end = std::chrono::steady_clock::now();
    const auto value_time =
        std::chrono::duration_cast<std::chrono::microseconds>(
            value_end - value_start)
            .count();
    double const_ref_average = 0.0;
    const auto const_ref_start = std::chrono::steady_clock::now();
    for (int i = 0; i < kIterations; ++i)
    {
        const_ref_average = average_by_const_ref(measurement);
    }
    const auto const_ref_end = std::chrono::steady_clock::now();
    const auto const_ref_time =
        std::chrono::duration_cast<std::chrono::microseconds>(
            const_ref_end - const_ref_start)
            .count();
    const double expected_average =
        (1.0 + static_cast<double>(kValueCount)) / 2.0;
    std::cout << "Measurement 大小: " << sizeof(Measurement) << " 字节\n";
    std::cout << "值传递平均值: " << value_average
              << "(应为 " << expected_average << ")\n";
    std::cout << "const 引用平均值: " << const_ref_average
              << "(应为 " << expected_average << ")\n";
    std::cout << "值传递耗时: " << value_time << " 微秒\n";
    std::cout << "const 引用耗时: " << const_ref_time << " 微秒\n";
    if (const_ref_time > 0)
    {
        std::cout << "耗时比值(值传递 / const 引用): "
                  << static_cast<double>(value_time) /
                         static_cast<double>(const_ref_time)
                  << "x\n";
    }

    return 0;
}

double average_by_value(Measurement measurement)
{
    double sum = 0.0;
    for (double value : measurement.values)
    {
        sum += value;
    }
    return sum / static_cast<double>(kValueCount);
}

double average_by_const_ref(const Measurement &measurement)
{
    double sum = 0.0;
    for (double value : measurement.values)
    {
        sum += value;
    }
    return sum / static_cast<double>(kValueCount);
}

编译运行:

bash
g++ -std=c++17 -O2 -Wall -Wextra main.cpp -o main && ./main

运行结果:

text
Measurement 大小: 8000 字节
值传递平均值: 500.5(应为 500.5)
const 引用平均值: 500.5(应为 500.5)
值传递耗时: 264553 微秒
const 引用耗时: 253918 微秒
耗时比值(值传递 / const 引用): 1.04188x

在这个基准里,值传递形参每次调用都要复制完整的 Measurement(1000 个 double);const Measurement& 只传引用,不复制对象。示例中的耗时只是一次代表性运行,具体数值会受编译器、硬件和系统调度影响;咱们重复运行时应关注整体趋势,而不是某个固定数字。

练习三:修复悬垂引用 ​

下面这段代码有什么问题?请您找到 bug 并修复它。

C++
const std::string& get_prefix()
{
    std::string prefix = "user_";
    return prefix;
}

int main()
{
    std::string name = get_prefix() + "admin";
    std::cout << name << std::endl;
    return 0;
}

提示:您想想函数返回后局部变量 prefix 会发生什么。

参考答案
C++
#include <iostream>
#include <string>

std::string get_prefix()
{
    std::string prefix = "user_";
    return prefix;
}

int main()
{
    std::string name = get_prefix() + "admin";
    std::cout << name << std::endl;
    return 0;
}

编译运行:

bash
g++ -std=c++17  -Wall -Wextra main.cpp -o main &&./main

运行结果:

text
user_admin

原代码中的 prefix 是普通局部变量,只在 get_prefix 函数执行期间存在。函数返回时它的生命周期结束,咱们拿到的 const std::string& 指向已经销毁的对象,后续通过这个引用读取数据就是未定义行为。

修复方式是让 get_prefix 按值返回。prefix 仍是普通局部变量,只在函数执行期间存在;return prefix 会构造并返回一个独立的 std::string 对象,调用者接收的是这个返回值,而不是局部变量的引用,因此函数返回后不会产生悬垂引用。同时把返回类型上的 const 去掉,防止编译器没做 NRVO 时阻碍咱们移动它。移动构造的签名通常是 std::string(std::string&&)。

pdf-latest-4-g85128cc · 85128cc · 2026-10-05