引用与指针
定义与作用
指针和引用是 C++ 中间接访问对象的两种机制:
| 特性 | 指针 T* | 左值引用 T& |
|---|---|---|
| 本质 | 存储地址的对象 | 已有对象的别名 |
| 可否为空 | 可为 nullptr | 不可为空(必须绑定有效对象) |
| 可否重绑定 | 可指向不同对象 | 一旦绑定不可更改 |
| 可否有空值检查 | 有 if (ptr) | 无条件安全 |
| 可否做算术运算 | 有 ptr++ / ptr+1 | 不可 |
| 语法 | *ptr 解引用 | 直接使用 |
核心原则:优先使用引用——它更安全、语法更简洁;需要"可能为空、需要重定向或做地址运算"时才用指针。
核心原理
引用的底层实现
引用在底层通常实现为
T* const(常量指针),但这是编译器实现细节,标准不强制。
指针寻址层级
指向成员的指针
指向成员的指针是一种特殊的指针类型,它不指向具体对象,而是指向类的某个成员:
int MyClass::* p_member; // 指向 MyClass 的 int 成员
void (MyClass::* p_method)(int); // 指向 MyClass 的成员函数
需要通过对象或对象指针来调用:
obj.*p_member; // 通过对象访问
ptr->*p_method(42); // 通过指针访问
完整示例
示例一:引用作为函数参数的效率优势
场景说明:对比传值、传指针、传引用三种参数传递方式的开销和行为差异。
#include <iostream>
#include <string>
#include <chrono>
struct LargeData {
std::string content;
LargeData() : content(10000, 'A') {} // 大对象(约 10KB)
};
// 传值:发生完整拷贝
void process_by_value(LargeData data) {
// data 是副本,修改不影响原对象
data.content[0] = 'B';
}
// 传指针:拷贝 8 字节地址,但需要判空
void process_by_pointer(LargeData* data) {
if (data == nullptr) {
std::cout << "空指针,跳过处理" << std::endl;
return;
}
data->content[0] = 'C';
}
// 传引用:拷贝 8 字节地址,无判空开销,语法简洁
void process_by_reference(LargeData& data) {
data.content[0] = 'D';
}
// 只读引用:承诺不修改,可接收临时对象
void read_only(const LargeData& data) {
std::cout << "数据大小: " << data.content.size() << " 字节" << std::endl;
// data.content[0] = 'X'; // 编译错误:const 引用不可修改
}
int main() {
LargeData obj;
// 传递方式对比
process_by_value(obj); // 完整拷贝,约 10KB 开销
process_by_pointer(&obj); // 拷贝 8 字节,需取地址
process_by_reference(obj); // 拷贝 8 字节,直接用对象名
// const 引用可接受临时对象
read_only(obj); // 可接受左值
read_only(LargeData()); // 可接受右值(临时对象)
// 引用 vs 空安全
LargeData* ptr = nullptr;
process_by_pointer(ptr); // 内部判空,安全
// LargeData& ref = *ptr; // 未定义行为!引用不能绑定到空
std::cout << "obj.content[0] = " << obj.content[0]
<< " (原为 'A',被 reference 版本改为 'D')" << std::endl;
return 0;
}
预期输出:
空指针,跳过处理
数据大小: 10000 字节
数据大小: 10000 字节
obj.content[0] = D (原为 'A',被 reference 版本改为 'D')
逐段分析:
- 传值
process_by_value:对大对象产生完整拷贝,性能最差。函数内修改不影响原对象 - 传指针
process_by_pointer:仅拷贝 8 字节地址,但调用处需取地址&obj,且函数内必须判空 - 传引用
process_by_reference:性能同指针,但调用语法自然(直接传对象名),且无需判空 const T&的独特优势:可接受临时对象(右值),延长其生命周期至函数调用结束- 引用不能重新绑定是安全保证——
int& ref = a; ref = b;是把b的值赋给a,而非让ref绑定到b
示例二:二级指针与引用参数实现链表节点删除
场景说明:用二级指针和指针引用两种方式删除链表中的目标节点,对比可读性。
#include <iostream>
struct Node {
int value;
Node* next;
Node(int v, Node* n = nullptr) : value(v), next(n) {}
};
// 方式 A:二级指针
void remove_v1(Node** head, int target) {
Node** indirect = head;
while (*indirect && (*indirect)->value != target) {
indirect = &((*indirect)->next);
}
if (*indirect) {
Node* to_delete = *indirect;
*indirect = (*indirect)->next;
delete to_delete;
}
}
// 方式 B:指针引用(更清晰的写法)
void remove_v2(Node*& head, int target) {
Node** indirect = &head;
while (*indirect && (*indirect)->value != target) {
indirect = &((*indirect)->next);
}
if (*indirect) {
Node* to_delete = *indirect;
*indirect = (*indirect)->next;
delete to_delete;
}
}
void print_list(const Node* head) {
while (head) {
std::cout << head->value << " -> ";
head = head->next;
}
std::cout << "null" << std::endl;
}
int main() {
Node* list = new Node(1, new Node(2, new Node(3)));
std::cout << "原链表: ";
print_list(list);
remove_v1(&list, 2);
std::cout << "删除 2 后 (v1): ";
print_list(list);
remove_v2(list, 1);
std::cout << "删除 1 后 (v2): ";
print_list(list);
// 清理
while (list) {
Node* tmp = list;
list = list->next;
delete tmp;
}
return 0;
}
预期输出:
原链表: 1 -> 2 -> 3 -> null
删除 2 后 (v1): 1 -> 3 -> null
删除 1 后 (v2): 3 -> null
逐段分析:
remove_v1接收Node**,调用时需&list。语义是"指向链表头指针的指针"remove_v2接收Node*&,调用时直接传list。语义是"链表头指针的引用",意图更清晰- 指针引用在需要修改外部指针值(如删除头节点)时,比二级指针更可读
print_list接收const Node*——只读遍历,不修改链表
示例三:指向成员的指针实现通用排序框架
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
struct Student {
std::string name;
int id;
double gpa;
void print() const {
std::cout << id << " | " << name << " | GPA " << gpa << std::endl;
}
};
// 通用排序:使用指向成员数据指针
void sort_by(std::vector<Student>& students,
double Student::* member,
bool ascending = true) {
if (ascending) {
std::sort(students.begin(), students.end(),
[member](const Student& a, const Student& b) {
return a.*member < b.*member;
});
} else {
std::sort(students.begin(), students.end(),
[member](const Student& a, const Student& b) {
return a.*member > b.*member;
});
}
}
// 使用成员函数指针批量执行操作
void batch_operation(std::vector<Student>& students,
void (Student::* op)() const) {
for (auto& s : students) {
(s.*op)();
}
}
int main() {
std::vector<Student> students = {
{"张三", 1003, 3.8},
{"李四", 1001, 3.5},
{"王五", 1002, 4.0}
};
std::cout << "---- 按 GPA 升序 ----" << std::endl;
sort_by(students, &Student::gpa, true);
batch_operation(students, &Student::print);
std::cout << "\n---- 按 ID 降序 ----" << std::endl;
sort_by(students, &Student::id, false);
batch_operation(students, &Student::print);
return 0;
}
预期输出:
---- 按 GPA 升序 ----
1001 | 李四 | GPA 3.5
1003 | 张三 | GPA 3.8
1002 | 王五 | GPA 4
---- 按 ID 降序 ----
1003 | 张三 | GPA 3.8
1002 | 王五 | GPA 4
1001 | 李四 | GPA 3.5
逐段分析:
double Student::* member是指向 Student 类中 double 类型成员的指针a.*member通过对象访问成员指针指向的成员&Student::gpa获取成员的地址,不绑定到具体对象void (Student::* op)() const是指向 const 成员函数的指针- 成员指针的核心价值:将"访问哪个成员"参数化,实现通用框架
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 返回局部变量的引用 | 悬挂引用,未定义行为 | 返回值类型而非引用 |
| 解引用空指针作引用 | int& r = *nullptr; 是 UB | 引用必须绑定到有效对象 |
| 引用重新绑定误解 | ref = b; 是赋值而非重绑定 | 引用不可重绑定,用指针替代 |
int& const | 语法不合法(引用本身不可变) | 不需要 const |
T* 转为 T& 未判空 | 间接引入 UB | 转换前判空 |
常见面试问题
引用和指针的区别(至少 4 点)?——引用必须初始化、不可为空、不可重绑定、语法更简洁;指针可为空、可重定向、支持算术运算、需要显式解引用。
为什么没有"引用的引用"?C++11 中的引用折叠是什么?——引用本身不是对象,不能有引用。但在模板中
T&&+ 引用叠加会触发引用折叠规则(& + & = &、&& + & = &、& + && = &、&& + && = &&)。何时用指针,何时用引用?——可能为空或需要重定向用指针;必须有效、不需要重新绑定时用引用。函数参数优先
const T&。int* const p和const int* p的区别?——前者是指针本身不可修改(指向固定地址),后者是指针指向的内容不可修改。用从右向左读法区分。什么是悬挂引用?如何避免?——引用绑定的对象已销毁。绝不返回局部对象的引用;注意容器重新分配导致的引用失效。
成员指针有什么实际应用场景?——通用排序框架、ORM 映射、回调机制、序列化/反序列化中按字段操作。
小结
- 引用 = 别名,底层近似
T* const;不可为空、不可重绑定 - 函数参数优先
const T&:零拷贝 + 安全只读 + 可接受临时对象 - 指针在需要可选(nullptr)、可重定向、或地址运算时使用
- 指针引用
T*&是修改外部指针的清晰写法 - 成员指针
T C::*将"访问哪个成员"参数化