C++ 传值 vs 传指针 vs 传引用

一句话:传值给函数一个副本(改动出不去)。指针和引用都给它真身(改动出得去)。指针可以为空、可以挪;引用是绑死不变的别名。小东西默认传值,大对象只读传 const 引用。

亲眼看区别 — 两个函数都想把参数改成 0,只有一个成功:

#include <iostream>
using namespace std;

void tryToZero(int x)    { x = 0; }    // 副本 — 调用方看不见
void actuallyZero(int* x) { *x = 0; }  // 真身的地址

int main() {
    int a = 5, b = 5;
    tryToZero(a);
    cout << "after tryToZero:   " << a << endl;
    actuallyZero(&b);
    cout << "after actuallyZero: " << b << endl;
    return 0;
}

本页演示用指针,因为我们浏览器引擎暂不支持引用参数(int&)— 但下面的规则对引用同样成立,语法会用代码块展示。

三种方式并排看

传值传指针传引用
写法f(int x)f(int* x),调用 f(&a)f(int& x),调用 f(a)
到函数手里的是副本对象的地址对象的别名
调用方看得到改动?否是是
能"没有对象"吗?否能 — 空指针不能 — 必须绑定
拷贝开销每次调用整份拷贝一个地址一个别名

经典场景:交换两个变量

swap 必须改调用方的变量,副本毫无用处 — 你需要地址:

#include <iostream>
using namespace std;

void mySwap(int* a, int* b) {
    int t = *a;   // 存下 a 指向的值
    *a = *b;
    *b = t;
}

int main() {
    int x = 1, y = 2;
    mySwap(&x, &y);
    cout << x << " " << y << endl;   // 2 1
    return 0;
}

引用版在调用处更清爽 — 机制相同,没有 &/* 的噪音:

void mySwap(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}
// 调用:mySwap(x, y);   // 长得像传值,行为像指针

冷知识:数组永远按指针传

C 风格数组传进函数时,实际到达的是首元素指针 — 尺寸不会跟过去。所以收数组的函数都要再收一个长度:

#include <iostream>
using namespace std;

int sum(int arr[], int n) {   // arr 实际是 int*
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

int main() {
    int a[3] = {10, 20, 30};
    cout << sum(a, 3) << endl;
    return 0;
}

那我到底用哪个?

值得备好的面试追问

  1. "指针 vs 引用 — 各给一个具体理由。" 指针:可为空、可重指向。引用:必须指东西、永远不用 * 检查。
  2. "大对象为什么传 const 引用?" — 省拷贝是主要矛盾;const 保证只读。
  3. "int& f() 返回局部变量的引用错哪了?" — 局部变量函数一返回就死了,引用比对象活得长。未定义行为,经典陷阱题。
  4. "传值一定发生拷贝吗?" — C++11 起,移动语义可以把临时对象的昂贵拷贝变成便宜的移动。

去练习场练手

  1. 写 addOne(int* p),给调用方的变量加一;调用后打印确认。
  2. 把 mySwap 改成"传指针的值" — 即只交换函数内部的两个指针 — 向自己解释为什么调用方毫无变化。
  3. 用传值写 maxOf(int a, int b, int c),体会小值传值为什么是正确的默认。

相关:15 道初学者面试题(指针 vs 引用是第 1 题)、++i 与 i++、整数除法。