前言
「数组引用」和「指针引用」是两种看起来只差一个符号、实际用途完全不同的声明:
int (&arr)[10]是数组的引用,绑定到一个长度为 10 的int数组,保留数组类型与长度。int*& p是指针的引用,绑定到一个int*变量,可以通过它修改调用方的指针本身。
初学者最容易混淆的是括号的位置。int& arr[10]不是「数组的引用」,而是「引用数组」——数组元素是引用,而 C++ 不允许引用数组,直接编译错误。int (*p)[10]又不同,它是指向数组的指针。这三种写法必须分清。
本文讲清数组引用如何避免数组退化、指针引用如何修改调用方指针、以及它们在模板推导、链表操作中的实际用法。代码以 C++17 编写,未在本机编译执行。
一、数组退化:为什么需要数组引用
数组在大多数表达式里会退化为指向首元素的指针,长度信息随之丢失。函数参数写成void f(int a[10])时,10只是注释,编译器把它当成int*。
// 适用于 C++17
#include <cstddef>
#include <iostream>
void by_pointer(int* p, std::size_t n) { // 必须额外传长度
for (std::size_t i = 0; i < n; ++i) {
std::cout << p[i] << ' ';
}
}
void by_array_ref(int (&a)[5]) { // 长度是类型的一部分
for (int x : a) { // 范围 for 可用,因为没退化
std::cout << x << ' ';
}
}
int main() {
int data[5] = {1, 2, 3, 4, 5};
by_pointer(data, 5);
by_array_ref(data); // 只能传长度恰好为 5 的数组
// int other[6]; by_array_ref(other); // 编译错误:长度不匹配
}int (&a)[5]的读法是「a 是一个引用,绑定到int[5]」。括号不能省:int& a[5]会被解析为「5 个int&组成的数组」,非法。
用数组引用的好处是编译期就能检查长度,传错长度直接编译失败;坏处是函数只能接受固定长度的数组,灵活性差。实际工程里更常用模板把长度变成参数:
// 适用于 C++17
#include <cstddef>
template <std::size_t N>
constexpr std::size_t array_size(const int (&)[N]) noexcept {
return N;
}
template <class T, std::size_t N>
constexpr std::size_t size_of(const T (&)[N]) noexcept {
return N;
}
int main() {
int a[7] = {};
static_assert(array_size(a) == 7);
static_assert(size_of(a) == 7);
}模板参数N从数组类型中推导出来,无需调用方传长度。C++17 起标准库提供了std::size(a),对数组和容器都适用,通常不必自己写。注意std::size对指针不适用——一旦数组已经退化成指针,长度就找不回来了。
二、指针引用:修改调用方的指针
函数参数按值传递指针时,函数内让指针指向别处,不影响调用方:
// 适用于 C++17
void reassign_by_value(int* p, int* q) {
p = q; // 只修改副本
}
void reassign_by_ref(int*& p, int* q) {
p = q; // 修改调用方的指针本身
}int*& p的读法是「p 是一个引用,绑定到int*」。它常用于:
- 链表头指针的修改。插入/删除头结点时,需要改变调用方持有的头指针。
- 输出参数。函数分配内存并通过指针引用返回。
- 指针交换。交换两个指针变量,而不是它们指向的值。
// 适用于 C++17
#include <cstddef>
struct Node {
int value;
Node* next;
};
// 在链表头部插入,必须能修改 head 本身
void push_front(Node*& head, int value) {
head = new Node{value, head};
}
// 删除第一个值为 target 的结点
bool remove_first(Node*& head, int target) {
Node** link = &head; // 指向「指针的指针」
while (*link != nullptr) {
if ((*link)->value == target) {
Node* victim = *link;
*link = victim->next;
delete victim;
return true;
}
link = &(*link)->next;
}
return false;
}remove_first用了「指针的指针」而不是「指针的引用」,因为需要在循环中让link依次指向不同结点的next成员。引用一旦绑定就不能改绑,做不到这一点;指针可以重新赋值。这是引用与指针在链表算法里的典型分工。
调用方必须传一个左值:
// 适用于 C++17
Node* head = nullptr;
push_front(head, 1); // OK:head 是左值
// push_front(nullptr, 1); // 错误:不能把引用绑定到临时量int*&不能绑定右值,这是好事:如果允许绑定临时指针,函数内的修改会丢失。需要「可空」语义时,用Node**并允许传nullptr,但要在函数内判空。
三、三种声明的辨析
| 声明 | 含义 | 是否合法 |
|---|
int (&a)[5] | 引用,绑定到int[5] | 合法 |
int*& p | 引用,绑定到int* | 合法 |
int (*p)[5] | 指针,指向int[5] | 合法 |
int* p[5] | 数组,5 个int* | 合法 |
int& a[5] | 数组,5 个int& | 非法:不能有引用数组 |
int (&a)[5]写成int &a[5] | 解析为引用数组 | 非法 |
int (*p)[5]常用于二维数组:int m[3][5]退化为int (*)[5],即「指向长度为 5 的 int 数组的指针」。遍历二维数组时,行指针的类型就是它。
// 适用于 C++17
#include <iostream>
void print_matrix(int (*m)[3], int rows) {
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << m[i][j] << ' ';
}
std::cout << '\n';
}
}
int main() {
int grid[2][3] = {{1, 2, 3}, {4, 5, 6}};
print_matrix(grid, 2); // grid 退化为 int(*)[3]
}四、const、返回引用与现代替代
const int (&a)[5]可以绑定到const数组,也可以绑定到非const数组(加限定是安全的);反过来,int (&a)[5]不能绑定const数组。
// 适用于 C++17
#include <cstddef>
template <std::size_t N>
void read_only(const int (&a)[N]) {
// a[0] = 1; // 错误:a 是 const 引用
}
// 危险:返回局部数组的引用
// int (&bad())[3] {
// int local[3] = {1, 2, 3};
// return local; // 悬垂引用,UB
// }返回数组引用时,被引用的数组必须比引用活得更久。返回局部数组的引用是悬垂引用,使用它是未定义行为(undefined behavior, UB)。安全的做法是返回静态数组、成员数组,或直接返回std::array按值。
指针引用作为返回值同样要小心:返回局部指针变量的引用,函数结束后引用悬垂。
现代替代:std::array与std::span
数组引用解决了「不退化」的问题,但接口仍然绑死长度。C++11 的std::array<T, N>把数组包装成值类型,可以按值传递、可以返回、有size();C++20 的std::span<T>表示「连续内存的一段视图」,长度可以在运行期确定。
// 适用于 C++20
#include <array>
#include <span>
#include <iostream>
void print(std::span<const int> s) { // 接受数组、vector、array
for (int x : s) std::cout << x << ' ';
std::cout << '\n';
}
int main() {
int raw[4] = {1, 2, 3, 4};
std::array<int, 3> arr{5, 6, 7};
print(raw);
print(arr);
}std::span需要 C++20 及以上,各标准库支持时间不同。在 C++17 项目中,数组引用 + 模板长度仍然是最轻量的方案;跨多个容器类型时,可以写一对迭代器或用自己的轻量视图。
常见坑点
- ❌ 写
int& a[5]想表达「数组的引用」。
✅ 括号不能省:int (&a)[5];int& a[5]是引用数组,非法。
- ❌
void f(int a[10])以为长度 10 会被检查。
✅ 形参中的数组退化为指针,长度被忽略;要保留长度用int (&a)[10]或模板。
- ❌ 把
const数组传给int (&)[N]形参。
✅ 形参加const:const int (&)[N]。
- ❌ 用
int*&参数接收字面量nullptr或临时指针。
✅ 引用必须绑定左值;需要可空/临时语义时改用int**或按值返回新指针。
- ❌ 返回局部数组或局部指针变量的引用。
✅ 返回std::array、静态对象或动态分配的对象;引用必须比函数活得久。
- ❌ 混淆
int (*p)[5]与int* p[5]。
✅ 前者是指向数组的指针,后者是存 5 个指针的数组;看括号结合顺序。
- ❌ 在链表删除中用引用想「改绑到下一个结点」。
✅ 引用不能改绑;用Node**或Node*&配合递归。
- ❌ 认为数组引用能延长数组生命周期。
✅ 它只是别名;绑定到临时数组(如某些返回数组的表达式)仍可能悬垂。
总结
| 写法 | 含义 | 典型用途 |
|---|
int (&a)[N] | 数组引用 | 保留长度、范围 for、模板推导 N |
int*& p | 指针引用 | 修改调用方指针、链表头插入 |
int (*p)[N] | 指向数组的指针 | 二维数组行指针 |
int* p[N] | 指针数组 | 存多个指针 |
int& a[N] | 引用数组 | 非法 |
数组引用解决的是「数组退化后长度丢失」,指针引用解决的是「函数要修改调用方的指针」。两者都依赖一个共同点:引用是别名,不产生新对象,因此不能改绑、不能为空、必须绑定左值。把括号的读法练熟(从变量名往外读),再记住「引用数组不存在」,这两种声明就不会再写错。