面试的时候,"引用和指针有什么区别"大概是仅次于 static 和 const 的高频基础题。
很多人的回答是"引用就是指针的语法糖"。这个说法不算错,但太粗糙了。引用和指针在语法层面、语义层面、使用规则上都有明确的区别,面试官想听的是你能不能把这些区别讲清楚。
我们从引用最基本的用法开始,一步步展开。
引用是什么:给变量起别名
引用的核心概念很简单——它是一个已有变量的别名。
int x = 10;
int& ref = x;
ref 就是 x 的另一个名字。从这一刻起,ref 和 x 指的是同一块内存,对 ref 的任何操作都等价于对 x 的操作。
ref = 20;
std::cout << x << "\n"; // 输出 20
你修改了 ref,x 也跟着变了,因为它们就是同一个东西。
反过来也一样:
x = 30;
std::cout << ref << "\n"; // 输出 30
你可以用取地址运算符来验证它们确实是同一块内存:
std::cout << &x << "\n"; // 比如 0x7ffd1234
std::cout << &ref << "\n"; // 也是 0x7ffd1234,完全一样
这就是"别名"的含义——不是一个新变量,不是一份拷贝,就是原来那个变量的另一个名字。
引用必须初始化
这是引用最重要的规则之一。你不能声明一个引用却不告诉它引用谁:
int& ref; // 编译错误!引用必须初始化
为什么?因为引用就是别名,一个"谁的别名都不是"的别名没有任何意义。这和指针不同——指针可以先声明再赋值,甚至可以是空指针:
int* p; // 没问题(虽然是野指针,但语法上合法)
int* q = nullptr; // 也没问题,空指针
但引用不行。它从出生那一刻起就必须绑定到一个对象上。
引用一旦绑定就不能更改
引用的第二个重要规则:一旦绑定,就不能再改为引用另一个变量。
int a = 10;
int b = 20;
int& ref = a; // ref 绑定到 a
ref = b; // 这不是让 ref 引用 b!
最后一行 ref = b 看起来像是"让 ref 改为引用 b",但实际上它做的是"把 b 的值赋给 ref 所引用的对象"——也就是把 20 赋给了 a。
std::cout << a << "\n"; // 输出 20,a 被改了
std::cout << &ref << "\n"; // 还是 a 的地址,ref 没有改变绑定
这一点和指针完全不同。指针可以随时改变指向:
int* p = &a;
p = &b; // p 现在指向 b 了
但引用做不到。它是一辈子的绑定,从初始化到销毁,始终引用同一个对象。
不存在"空引用"
指针可以是 nullptr,表示"我现在不指向任何东西":
int* p = nullptr;
if (p == nullptr) {
std::cout << "p is null\n";
}
但引用没有"空"的概念。你不能写:
int& ref = nullptr; // 编译错误!
引用必须绑定到一个有效的对象上。这意味着当你拿到一个引用时,你可以放心地使用它,不需要像指针那样先检查是不是空的。
这是引用相比指针的一个重要优势:它在语义上保证了"一定有值"。
不存在"引用的引用"
指针可以有多级——指向指针的指针:
int x = 10;
int* p = &x;
int** pp = &p; // 指向指针的指针
但引用不能嵌套。"引用的引用"在 C++ 里不存在:
int x = 10;
int& ref = x;
int& & rref = ref; // 编译错误!不存在引用的引用
当你写 int& ref2 = ref 时,ref2 并不是"引用的引用",而是直接绑定到 ref 所引用的那个对象(也就是 x):
int x = 10;
int& ref = x;
int& ref2 = ref; // ref2 也绑定到 x,不是"引用的引用"
ref2 = 99;
std::cout << x << "\n"; // 输出 99
引用没有独立的大小
对指针做 sizeof,你得到的是指针本身的大小(通常是 4 或 8 字节):
int x = 10;
int* p = &x;
std::cout << sizeof(p) << "\n"; // 8(64 位系统)
std::cout << sizeof(x) << "\n"; // 4
但对引用做 sizeof,你得到的是它所引用的对象的大小:
int x = 10;
int& ref = x;
std::cout << sizeof(ref) << "\n"; // 4,和 sizeof(x) 一样
引用在语义上没有自己的"实体",它就是原对象的别名。sizeof(ref) 等价于 sizeof(x)。
不过在底层实现上,编译器通常会用指针来实现引用。但这是实现细节,C++ 标准并没有规定引用必须占用内存。
引用和指针的区别:放在一起看
到这里,我们已经积累了足够多的细节,可以做一个完整的对比了:
| 特性 | 引用 | 指针 |
|---|---|---|
| 必须初始化 | 是 | 否 |
| 可以为空 | 不可以 | 可以(nullptr) |
| 绑定后可以更改 | 不可以 | 可以 |
| 有多级(引用的引用 / 指针的指针) | 没有 | 有 |
| sizeof | 返回所引用对象的大小 | 返回指针本身的大小 |
| 访问语法 | 直接用名字 | 需要 * 解引用 |
| 自增操作 | 对引用的对象 +1 | 指针地址移动 |
面试的时候能把这张表里的内容讲清楚,这道题基本就过关了。
引用作为函数参数
引用最常见的用途就是函数参数。我们用经典的 swap 函数来感受。
先看值传递的版本:
void swap(int a, int b) {
int temp = a;
a = b;
b = temp;
}
int x = 10, y = 20;
swap(x, y);
std::cout << x << " " << y << "\n"; // 输出 10 20,没有交换!
值传递会复制参数,函数内部操作的是副本,原变量不受影响。
用指针可以解决这个问题:
void swap(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
swap(&x, &y); // 调用时要取地址
能用,但语法比较啰嗦——函数内部到处是 *,调用时还要写 &。
用引用就优雅多了:
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
swap(x, y); // 调用时和值传递一样简洁
std::cout << x << " " << y << "\n"; // 输出 20 10,交换成功!
引用参数的好处是:函数内部直接操作原变量,语法上又和普通变量一样干净,不需要解引用。
const 引用参数:只读访问
如果你只需要读取参数而不修改它,应该用 const 引用:
void print(const std::string& s) {
std::cout << s << "\n";
// s += "!"; // 编译错误!const 引用不能修改
}
const std::string& 同时获得了两个好处:引用避免了复制(std::string 可能很长,复制成本高),const 保证了不会修改原对象。
这里有一个特殊的规则:const 引用可以绑定到临时对象(右值),而普通引用不行。
const std::string& ref = "hello"; // 没问题!const 引用可以绑定临时对象
std::string& ref2 = "hello"; // 编译错误!普通引用不能绑定临时对象
为什么?因为 "hello" 会被隐式转换成一个临时的 std::string 对象,这个临时对象马上就要销毁。如果允许普通引用绑定它,你就能通过引用修改一个即将消失的对象,这毫无意义还容易出 bug。但 const 引用不会修改它,而且 C++ 有一条特殊规则:const 引用会延长临时对象的生命周期,让它活到引用本身销毁为止。
const std::string& ref = std::string("hello");
// 临时对象的生命周期被延长,ref 在整个作用域内都有效
std::cout << ref << "\n"; // 没问题,输出 "hello"
这个特性在函数调用中特别有用:
void print(const std::string& s);
print("hello"); // 没问题!"hello" 被转换成临时 string,绑定到 const 引用
如果 print 的参数是 std::string&(非 const),这行代码就编译不过。这也是为什么"输入参数用 const 引用"是 C++ 的最佳实践之一——它不仅保证安全,还能接受临时对象。
引用作为函数返回值
函数也可以返回引用,这在某些场景下非常有用。
最经典的例子是 operator[]——数组下标运算符:
class IntArray {
int data[100];
public:
int& operator[](int index) {
return data[index];
}
};
返回引用意味着你可以对返回值赋值:
IntArray arr;
arr[0] = 42; // operator[] 返回 data[0] 的引用,然后赋值 42
std::cout << arr[0] << "\n"; // 输出 42
如果 operator[] 返回的是值(int 而不是 int&),那 arr[0] = 42 就只是在修改一个临时副本,原数组不会变。
另一个常见的场景是链式调用。比如 std::cout 的 << 运算符返回的就是 ostream&,所以你可以连续写:
std::cout << "a" << "b" << "c";
// 等价于 ((std::cout << "a") << "b") << "c";
// 每次 << 返回 cout 的引用,下一个 << 继续在上面操作
千万不要返回局部变量的引用
这是一个经典的坑,面试也经常考。
int& getNumber() {
int x = 42;
return x; // 危险!返回局部变量的引用
}
x 是 getNumber() 的局部变量,函数返回后 x 就被销毁了。但你返回了它的引用——这个引用指向的内存已经不属于你了。
int& ref = getNumber();
std::cout << ref << "\n"; // 未定义行为!可能输出 42,也可能输出垃圾值
这就是所谓的"悬空引用"(dangling reference),和"悬空指针"是同一类问题。编译器通常会给出警告(warning: reference to local variable 'x' returned),但不会阻止你编译。
正确的做法是返回静态变量、成员变量、或者堆上分配的对象的引用:
// 返回 static 变量的引用:安全,因为 static 变量生命周期到程序结束
int& getCounter() {
static int count = 0;
count++;
return count;
}
// 返回成员变量的引用:安全,只要对象还活着
class Container {
int value;
public:
Container(int v) : value(v) {}
int& getValue() { return value; }
};
引用和指针在底层的关系
面试里有时会问"引用在底层是怎么实现的"。
答案是:编译器通常用指针来实现引用。当你写 int& ref = x 时,编译器在底层可能生成的代码和 int* const p = &x 差不多——一个不可更改指向的指针。
你可以通过查看汇编来验证。下面两个函数:
void byRef(int& r) {
r = 10;
}
void byPtr(int* p) {
*p = 10;
}
在大多数编译器上,它们生成的汇编代码是完全一样的。
但这不意味着"引用就是指针"。引用是一个语言层面的概念,它有自己的语义规则(必须初始化、不能为空、不能更改绑定)。编译器碰巧用指针来实现它,就像编译器用虚函数表来实现多态一样——实现方式不等于语义。
面试的时候可以这样回答:"引用在底层通常用指针实现,但在语言层面它们有本质区别——引用是别名,指针是变量。引用必须初始化且不能为空,指针可以为空也可以改变指向。"
引用折叠与万能引用(进阶)
这是一个 C++11 引入的进阶知识点,面试中偶尔会被问到。
在模板中,如果模板参数是 T&&,它有一个特殊的行为:
template<typename T>
void wrapper(T&& arg) {
// ...
}
这里的 T&& 不是普通的右值引用,而是所谓的"万能引用"(universal reference,也叫 forwarding reference)。它既能接受左值,也能接受右值。
当你传入一个左值时:
int x = 10;
wrapper(x); // T 被推导为 int&,T&& 变成 int& && → 折叠为 int&
当你传入一个右值时:
wrapper(42); // T 被推导为 int,T&& 就是 int&&
这里涉及到"引用折叠"规则:
T& & → T&
T& && → T&
T&& & → T&
T&& && → T&&
简单来说:只要有一个 &(左值引用),结果就是左值引用。只有两个都是 && 时,结果才是右值引用。
这个机制是 std::forward 和完美转发的基础。如果面试官问到这里,说明他在考察你对现代 C++ 移动语义的理解深度,这已经超出了"引用基础"的范畴,属于加分项。
面试中怎么答"引用和指针的区别"
最后给一个面试回答的思路。当面试官问这个问题时,不要上来就背条目,可以这样组织:
先说本质区别:引用是别名,指针是变量。引用绑定后不可更改,指针可以随时改变指向。
再说安全性:引用必须初始化且不能为空,所以使用引用时不需要做空检查。指针可以为空,使用前需要检查。
然后说使用场景:函数参数传递时,如果需要修改原对象用引用,如果只需要读取用 const 引用。指针更适合需要"可选"语义(可能为空)或者需要改变指向的场景。
如果面试官继续追问,可以补充:引用在底层通常用指针实现,但语义不同。C++11 引入了右值引用(&&),用于支持移动语义和完美转发,这是引用体系的重要扩展。