C++中指针的引用*的具体使用
前言
T*&读作「指针的引用」——它首先是一个引用,绑定到的对象是一个指针。这个类型在教科书里出现得不多,但在链表、树、字符串解析、输出参数这些场景里非常实用:函数拿到的不再是「指针的副本」,而是「调用者那个指针变量本身」,于是在函数内部给指针重新赋值,调用者能直接看到。
初学者对*&的困惑通常集中在两点。第一是语法:int*&和int&*看起来差不多,后者却是非法类型,因为「引用的指针」在 C++ 里不存在——引用不是对象,不能有指向它的指针。第二是语义:很多人把*&p这个表达式和int*&这个声明混为一谈。在表达式里,*&p恒等于p(取地址再解引用,互相抵消);在声明里,*&表示「先是指针,再是这个指针的引用」,两者的含义完全不同。
本文先讲清楚声明语法和别名语义,再给三个典型用途的完整代码:链表头指针的修改、输出型指针参数、字符串游标推进。最后单独讲T*&与const组合时的规则——这是最容易踩坑的地方,也是很多编译错误信息看起来莫名其妙的原因。
一、语法:声明符是从右往左读的
C++ 的声明符遵循「从变量名出发,先右后左」的阅读顺序。以int*& r = p;为例:
- 找到变量名
r; - 往右看没有东西,往左看到
&,说明r是一个引用; - 继续往左看到
*,说明被引用的类型是指针; - 再往左看到
int,说明指针指向int。
合起来:r是「指向int的指针」的引用。写成等价的类型别名会更清楚:
// 适用于 C++17
using IntPtr = int*;
IntPtr& r = p; // 与 int*& r = p; 完全等价反过来,int&* p的读法是「p是一个指针,指向int&」,而引用不是对象、没有地址,所以这个类型不成立,编译器会直接拒绝。这是「引用的指针」在 C++ 里不存在的直接体现。
还有一点容易混淆:int*&和int* const&都是合法的,但含义不同。前者是对「指针变量」的非 const 引用,可以修改指针本身;后者是对「指针」的 const 引用,只能读指针的值,不能改。int* const&还能绑定到临时对象(比如函数返回的指针),而int*&不能绑定右值。
| 写法 | 含义 | 能否修改指针本身 | 能否绑定右值 |
|---|
int* p | 指向 int 的指针 | 能 | — |
int*& r | 指针的引用 | 能 | 否 |
int* const& r | 指针的 const 引用 | 否 | 能 |
int** pp | 指向指针的指针 | 需解引用后能 | — |
int& * p | 非法:引用的指针 | — | — |
二、别名而非副本:它到底改变了什么
C++ 的函数参数默认按值传递。传入一个Node* head,函数内部拿到的是指针的副本:副本指向同一块内存,所以能修改节点内容;但给副本重新赋值(让它指向别的节点),调用者的head纹丝不动。
// 适用于 C++17
void wrong_push(Node* head, int value) {
head = new Node{value, head}; // 只改了副本
} // 调用者的 head 没变,还泄漏了一个节点把参数改成Node*& head,函数内部拿到的就是调用者那个变量本身,赋值会直接作用在它身上。这就是「引用是指针变量的别名」的全部含义——没有额外的间接层,没有拷贝,编译后通常就是直接对原变量做操作。
与Node**的对比很能说明问题。用二级指针,函数内部每次访问都要写(*pp),可读性差;用Node*&,写法与普通指针完全一致,但语义上改的是原变量。两者在底层实现上通常等价,选择哪一个主要是表达力的问题:
| 方式 | 调用写法 | 函数内赋值 | 空值 | 可读性 |
|---|
Node* | f(head) | 改副本,无效 | 可以传nullptr | 好,但语义错 |
Node** | f(&head) | *pp = ... | 可以传nullptr | 差,需层层解引用 |
Node*& | f(head) | head = ... | 不能传nullptr | 好,语义清晰 |
最后一行里的「不能传nullptr」很关键:引用必须绑定到一个已存在的对象。如果函数确实需要表达「可能没有指针」这层含义,T*&就不合适,应该用T**或者返回新指针。
三、三个典型用途
3.1 修改链表的头指针
头插法要改变head本身,这是Node*&最经典的用法:
// 适用于 C++17
struct Node {
int value;
Node* next;
};
void pushFront(Node*& head, int value) {
head = new Node{value, head};
}调用方写pushFront(head, 1);,函数返回后head就指向新节点。如果参数写成Node* head,新节点会立刻丢失,同时原链表被截断——这是初学者最常见的错误之一。
3.2 输出型参数
有些函数需要「返回一个指针,同时用返回值表示成功与否」。这时返回值被状态占用,指针只能通过参数带出来:
// 适用于 C++17
bool findNode(Node* head, int value, Node*& out) {
for (Node* p = head; p != nullptr; p = p->next) {
if (p->value == value) {
out = p;
return true;
}
}
out = nullptr;
return false;
}out是引用,函数内对它赋值就是修改调用者的变量。调用方必须先有一个Node* result = nullptr;,再传进去。这种「输出参数」的写法在现代 C++ 里已经不太推荐——更清晰的做法是返回std::optional<Node*>,或者返回std::pair<bool, Node*>。T*&在这里的价值主要是兼容旧接口。
3.3 推进字符串游标
解析数字、跳过空白时,常常需要让指针沿着缓冲区前进并带回新位置:
// 适用于 C++17
bool parseUnsigned(const char*& p, unsigned& out) {
if (p == nullptr || *p < '0' || *p > '9') return false;
unsigned value = 0;
while (*p >= '0' && *p <= '9') {
value = value * 10 + static_cast<unsigned>(*p - '0');
++p;
}
out = value;
return true;
}这里const char*&是对「指向 const char 的指针」的引用:指针本身可以改(++p),但它指向的字符不能改。这个组合在词法分析、命令行解析里非常常见。
四、与 const 的组合
T*&与const的组合有四层,每一层的含义都不同,必须逐层看清楚:
| 写法 | 指针本身 | 指向的对象 | 典型用途 |
|---|
int*& | 可改 | 可改 | 修改调用者的指针 |
const int*& | 可改 | 不可改 | 遍历只读数据并移动游标 |
int* const& | 不可改 | 可改 | 只读地观察一个指针 |
const int* const& | 不可改 | 不可改 | 只读观察只读指针 |
有一个反直觉的规则:const int*&不能绑定到int*类型的变量。原因不是语法,而是类型安全。假设允许这样做:
// 适用于 C++17;这段代码无法通过编译,仅用于说明规则
int* p = &x;
const int*& r = p; // 假如允许
const int y = 0;
r = &y; // 通过 r 把 const int* 写进了 p
*p = 1; // p 现在指向 const 对象,却按 int 修改 —— 类型安全被破坏编译器拒绝这个绑定,正是为了防止上面这条路径。要传int*给只读接口,正确做法是用int* const&(const 加在引用上,而不是指向的对象上),或者干脆按值传const int*。
反过来,int* const&可以绑定到int*变量,也可以绑定到右值(比如函数返回的临时指针),因为它承诺不修改指针本身。这也是为什么标准库的很多接口用T const&而不是T&——前者能接受更多实参。
五、完整示例:用指针的引用删除链表节点
// 适用于 C++17
#include <iostream>
struct Node {
int value;
Node* next;
};
void pushFront(Node*& head, int value) {
head = new Node{value, head};
}
// 删除所有值为 value 的节点
void removeAll(Node*& head, int value) {
Node** link = &head; // 指向「当前应该被检查的指针」
while (*link != nullptr) {
Node* current = *link;
if (current->value == value) {
*link = current->next; // 直接改写前驱的 next,或头指针
delete current;
} else {
link = ¤t->next; // 移动到下一个指针位置
}
}
}
void destroy(Node*& head) {
while (head != nullptr) {
Node* doomed = head;
head = head->next;
delete doomed;
}
}
int main() {
Node* head = nullptr;
for (int i = 1; i <= 5; ++i) pushFront(head, i % 2);
removeAll(head, 1);
for (Node* p = head; p != nullptr; p = p->next) {
std::cout << p->value << ' ';
}
std::cout << '\n';
destroy(head); // 结束后 head 为 nullptr
return 0;
}removeAll里用了二级指针Node**而不是Node*&,因为循环中需要「让 link 指向不同的指针变量」——它一开始指向head,之后指向某个节点的next成员。引用一旦绑定就不能换绑,所以这里只能用指针的指针。这是两种手法各自适用场景的清晰分界:
- 只需要改一个固定的指针变量→
T*&; - 需要在多个指针变量之间移动「当前操作位置」→
T**。
destroy用Node*&是因为它始终操作同一个head,顺带把调用者的指针置空,避免悬垂。
常见坑点
1. 把声明写成int&*
❌void f(int&* p);✅void f(int*& p);
引用不是对象,不能被指针指向。「引用的指针」这个类型在 C++ 里不存在,编译器会报「declaration of pointer to reference」一类的错误。记住声明符从变量名往左读,先遇到&就是引用。
2. 给T*&传nullptr或临时指针
❌f(nullptr);或f(new int(1));✅ 先定义变量:int* p = nullptr; f(p);
非 const 引用只能绑定左值。传字面量nullptr或new表达式的返回值都绑定失败,编译阶段就会报错。如果函数确实要接受临时值,参数应改成T* const&或按值传。
3. 以为改了指针就释放了原内存
❌void reset(int*& p) { p = new int[10]; },调用者以为旧数组被回收 ✅ 先delete[]旧指针再重新分配,并明确所有权归属
给指针重新赋值只是让它指向别处,原来那块内存仍然存在。如果没有别的指针持有它,就是内存泄漏。用T*&做重新分配的函数,必须在文档或命名上把「谁负责释放」写清楚。
4. 返回局部指针的引用
❌int*& get() { int* p = new int(1); return p; }✅ 按值返回int*,或者返回std::unique_ptr<int>
p是局部变量,函数返回后生命周期结束,返回它的引用是悬垂引用,使用是未定义行为。这类错误编译器未必报警,运行时可能「看起来正常」,但随时会崩。
5. 用const T*&接收T*
❌void f(const int*& p); int* q = ...; f(q);✅void f(const int* p);或void f(int* const& p);
这条路会打开「把 const 指针写回非 const 变量」的口子,标准明确禁止。错误信息通常很长,核心是「binding reference of type const int& to intdiscards qualifiers」。
6. 用指针引用表达「可选参数」
❌void f(Node*& node = nullptr);✅ 用Node*按值传(可为空),或std::optional<std::reference_wrapper<Node>>
引用不能为空,默认参数写nullptr根本无法绑定。函数如果确实需要「有或没有」的语义,就不该用引用。
7. 在循环里对引用重新赋值,误以为换绑
❌for (Node*& p = head; ...) { p = p->next; }以为p会依次指向各节点 ✅ 用指针遍历:for (Node* p = head; p != nullptr; p = p->next)
p = p->next修改的是head本身,不是让引用改绑到下一个节点。引用一旦初始化就永远指向同一个对象,这个例子会把整条链表从头部截断。
8. 混用new[]与delete
❌int*& p; ... delete p;而 p 来自new int[n]✅new[]配delete[],或者干脆用std::vector<int>
这是与指针引用无关、但经常一起出现的错误。new[]与delete不配对是未定义行为,可能只析构第一个元素,也可能直接崩溃。
总结
| 要点 | 说明 |
|---|
| 类型读法 | int*&是「指针的引用」,从变量名往左读 |
| 核心语义 | 引用是指针变量的别名,函数内赋值直接作用于调用者 |
与T**的分工 | 固定改一个指针用T*&;需要在多个指针间移动用T** |
| 绑定规则 | 非 const 引用不能绑定右值,不能为「空」 |
| const 组合 | const T*&不能绑定T*,这是类型安全的硬性要求 |
| 生命周期 | 不要返回局部指针的引用 |
| 所有权 | 重新赋值不等于释放,必须明确谁负责delete |
T*&不是炫技,它解决的是一个具体问题:让函数能够修改调用者的指针变量,同时保持和普通指针一样的书写体验。真正需要记住的只有三条——引用必须绑定到已存在的左值、引用不能换绑、const的位置决定改的是指针还是指向的对象。把这三条弄清楚,链表头指针、输出参数、游标推进这些场景就都不会写错了。