C++中指针的引用*的具体使用

发布时间:2026/11/6 5:26:41
C++中指针的引用*的具体使用

前言


T*&读作「指针的引用」——它首先是一个引用,绑定到的对象是一个指针。这个类型在教科书里出现得不多,但在链表、树、字符串解析、输出参数这些场景里非常实用:函数拿到的不再是「指针的副本」,而是「调用者那个指针变量本身」,于是在函数内部给指针重新赋值,调用者能直接看到。


初学者对*&的困惑通常集中在两点。第一是语法:int*&和int&*看起来差不多,后者却是非法类型,因为「引用的指针」在 C++ 里不存在——引用不是对象,不能有指向它的指针。第二是语义:很多人把*&p这个表达式和int*&这个声明混为一谈。在表达式里,*&p恒等于p(取地址再解引用,互相抵消);在声明里,*&表示「先是指针,再是这个指针的引用」,两者的含义完全不同。


本文先讲清楚声明语法和别名语义,再给三个典型用途的完整代码:链表头指针的修改、输出型指针参数、字符串游标推进。最后单独讲T*&与const组合时的规则——这是最容易踩坑的地方,也是很多编译错误信息看起来莫名其妙的原因。


一、语法:声明符是从右往左读的


C++ 的声明符遵循「从变量名出发,先右后左」的阅读顺序。以int*& r = p;为例:



  1. 找到变量名r;

  2. 往右看没有东西,往左看到&,说明r是一个引用;

  3. 继续往左看到*,说明被引用的类型是指针;

  4. 再往左看到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 = &current->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的位置决定改的是指针还是指向的对象。把这三条弄清楚,链表头指针、输出参数、游标推进这些场景就都不会写错了。