一、引言
C++ 有两种主要的机制来实现间接访问:引用和指针。这篇文章剖析C++引用和指针的内在本质、核心差异,在何种情况优先选择引用,又在何种情况必须用指针。
二、C++引用的本质和特性
C++引用是不同于指针的间接访问机制。
从最核心的层面来看,C++引用是一个别名。
- 引用不是一个独立的对象: 不创建新的内存空间来存储一个值或地址。只是现有对象的一个替代名称。就像一个人有两个名字,但是同一个人。
- 对引用的操作等同直接对它所引用的对象进行操作: 通过引用来读取或修改数据实际上是在直接操作它所绑定的那个原始对象。编译器在底层会用地址来实现引用,但对使用来说,引用就像是原始对象本身。
引用的“别名”本质决定一系列独特的行为和规则:
-
声明时必须初始化: 引用在声明时必须立即绑定到一个有效的对象。因为只是一个别名,所以必须知道自己是谁的别名。
-
一旦绑定,不能重新绑定: 引用一旦初始化,就永久地绑定到那个对象。不能让一个引用在生命周期内改变所引用的对象。
-
不能为
nullptr: 因为引用必须绑定到一个有效的对象,因此不存在“空引用”的概念。 -
没有独立的内存地址: 从使用者的角度看,引用本身不占用独立的内存空间。用
&操作符获取引用的地址得到的是它所引用对象的地址,而不是引用本身的地址。再次强调,引用只是一个别名,而非一个独立的实体。虽然编译器在底层会用指针来实现引用,但这对于C++的语言语义来说是透明的。 -
不用解引用操作符: 用引用时,可以直接用其名字来访问或修改所引用的对象,就像用原始对象一样。不用像指针那样用
*操作符进行解引用。 -
不支持指针算术: 因为引用不是指针,没有地址的概念(从程序员角度),所以不支持
++、--、+、-等指针算术运算。 -
没有“引用的引用”: C++不存在
int&& ref_of_ref;这样的“引用的引用”的概念。int&&在C++11及以后版本是右值引用,是一个完全不同的概念,用来实现移动语义和完美转发。
示例:
#include <iostream>
int main()
{
// 1. 声明时必须初始化
int original_value = 10;
int& ref_to_value = original_value;
std::cout << "--- Initial State ---" << std::endl;
std::cout << "original_value: " << original_value << std::endl; // 10
std::cout << "ref_to_value: " << ref_to_value << std::endl; // 10
std::cout << "Address of original_value: " << &original_value << std::endl;
std::cout << "Address of ref_to_value: " << &ref_to_value << std::endl;
// int& uninitialized_ref; // 编译错误:引用必须初始化
// int& null_ref = nullptr; // 编译错误:引用不能绑定到nullptr
// 2. 不能重新绑定 (赋值操作修改的是被引用对象的值)
int another_value = 20;
std::cout << "\n--- Attempting to 're-bind' ---" << std::endl;
std::cout << "Before 're-bind': original_value = " << original_value << ", another_value = " << another_value << std::endl;
std::cout << "ref_to_value = " << ref_to_value << std::endl;
ref_to_value = another_value; // 这不是重新绑定,而是another_value的值赋给original_value
std::cout << "After ref_to_value = another_value;" << std::endl;
std::cout << "original_value: " << original_value << std::endl; // 变为20
std::cout << "another_value: " << another_value << std::endl; // 仍为20
std::cout << "ref_to_value: " << ref_to_value << std::endl; // 20
std::cout << "Address of original_value: " << &original_value << std::endl;
std::cout << "Address of ref_to_value: " << &ref_to_value << std::endl; // 仍然是original_value的地址
// 3. 无解引用操作符,直接使用
std::cout << "\n--- Modifying via reference ---" << std::endl;
ref_to_value = 30;
std::cout << "original_value after modification: " << original_value << std::endl; // 30
std::cout << "ref_to_value after modification: " << ref_to_value << std::endl; // 30
// 4. 不支持指针算术
// int& invalid_op_ref = ref_to_value + 1; // 编译错误
// ref_to_value++;
// std::cout << "original_value after ref_to_value++: " << original_value << std::endl; // 31
return 0;
}
三、C++指针的本质和特性
C++指针是实现间接访问的另一项基本机制,比引用出现得更早,也更接近硬件层面。
指针的本质是一个变量,但这个变量存储的不是普通的数据值,而是另一个变量的内存地址。
- 指针本身是一个独立的变量: 有自己的内存空间,用来存储一个地址值。这个地址值指向内存的某个位置,是另一个变量的起始地址。
- 指针可以间接访问或修改其指向的内存位置: 有一个变量的内存地址就可以通过这个地址找到并操作那个变量。这种通过地址进行的操作称为“间接访问”或“解引用”。
指针的特性:
-
可以不初始化或初始化为
nullptr: 指针可以在声明时不进行初始化。此时,它将包含一个不确定的“垃圾”内存地址,被称为 野指针。为了表示指针不指向任何有效的内存,C++11引入nullptr关键字(在C++98/03中用NULL宏),可以把指针明确地初始化为或赋值为nullptr。 -
可以重新指向其他对象: 指针变量可以随时被重新赋值,使其指向不同的内存地址。这意味着一个指针在其生命周期内可以指向多个不同的对象。这是指针与引用最根本的区别之一。
-
占用独立的内存空间: 指针本身是一个变量,它需要内存来存储其所指向的地址。在32位系统上,指针通常占用4字节;在64位系统上,指针通常占用8字节。
sizeof操作符可以返回指针变量本身的大小。 -
需要解引用操作符 (
*): 要访问指针所指向的实际数据,必须使用解引用操作符*。*ptr表示“ptr所指向的内存位置上的值”。 -
支持指针算术: 指针可以进行算术运算,如
++、--、+、-。这些运算是基于指针所指向的数据类型的大小进行的。例如,一个int*指针加1,实际上是地址值增加了sizeof(int)个字节。这在遍历数组或处理连续内存块时非常有用。 -
可以有多级指针: 指针本身是一个变量,因此可以有一个指针来存储另一个指针的地址,形成“指向指针的指针”(二级指针)。这可以扩展到任意级别(三级指针等),尽管在实际编程中二级指针已相对复杂。
示例:
#include <iostream>
int main()
{
// 1. 声明与初始化:可以不初始化,可以为nullptr
int* uninitialized_ptr; // 野指针 (不推荐)
// std::cout << *uninitialized_ptr << std::endl; // 危险!可能崩溃
int* null_ptr = nullptr; // 空指针
int value = 10;
int* ptr = &value; // ptr存储value的地址
std::cout << "--- Initial State ---" << std::endl;
std::cout << "value: " << value << std::endl; // 10
std::cout << "Address of value (&value): " << &value << std::endl;
std::cout << "ptr (stored address): " << ptr << std::endl; // 跟&value相同
std::cout << "*ptr (value at address): " << *ptr << std::endl; // 10
std::cout << "Address of ptr itself (&ptr): " << &ptr << std::endl; // 指针变量自身的地址
// 2. 可以重新指向其他对象
int another_value = 20;
std::cout << "\n--- Re-pointing Pointer ---" << std::endl;
std::cout << "Before re-point: *ptr = " << *ptr << std::endl; // 10
ptr = &another_value; // ptr现在指向another_value
std::cout << "After re-point: *ptr = " << *ptr << std::endl; // 20
std::cout << "ptr (new stored address): " << ptr << std::endl; // 跟&another_value相同
std::cout << "Address of ptr itself (&ptr): " << &ptr << std::endl; // 仍然是ptr变量自身的地址
// 3. 指针算术
std::cout << "\n--- Pointer Arithmetic ---" << std::endl;
int arr[] = {100, 200, 300};
int* arr_ptr = arr; // arr_ptr指向arr[0]
std::cout << "arr_ptr points to: " << *arr_ptr << std::endl; // 100
arr_ptr++; // 移动到下一个int元素的地址 (地址值增加sizeof(int))
std::cout << "arr_ptr++ points to: " << *arr_ptr << std::endl; // 200
arr_ptr = arr_ptr + 1; // 再次移动到下一个int元素的地址
std::cout << "arr_ptr + 1 points to: " << *arr_ptr << std::endl; // 300
// 4. 多级指针
std::cout << "\n--- Multi-level Pointer ---" << std::endl;
int data_for_pptr = 500;
int* p = &data_for_pptr; // p指向data_for_pptr
int** pp = &p; // pp指向p (即pp存储了p的地址)
std::cout << "data_for_pptr: " << data_for_pptr << std::endl; // 500
std::cout << "*p: " << *p << std::endl; // 500
std::cout << "**pp: " << **pp << std::endl; // 500 (通过pp访问p,再通过p访问data_for_pptr)
std::cout << "Address of data_for_pptr: " << &data_for_pptr << std::endl;
std::cout << "Address of p: " << &p << std::endl;
std::cout << "Address of pp: " << &pp << std::endl;
return 0;
}
四、引用和指针的区别
虽然引用和指针都能实现对其他对象的间接访问,但有根本性的差异。
| 特性 | 引用 | 指针 |
|---|---|---|
| 本质 | 已存在对象的别名,没有独立内存 | 存储另一个变量内存地址的变量,有独立内存 |
| 初始化 | 必须在声明时初始化,且绑定到有效对象 | 可以在声明时不初始化(野指针),或初始化为nullptr,或指向对象 |
| 空值 | 不能为nullptr,必须引用一个有效对象 | 可以为nullptr,表示不指向任何对象 |
| 重新绑定 | 一旦绑定,不能重新绑定 到另一个对象。赋值操作修改的是被引用对象的值。 | 可以随时重新赋值,指向不同的对象 |
| 内存占用 | 不占用独立内存(是其引用对象的别名),或编译器优化后不占用额外内存。 | 占用独立内存空间来存储地址。 |
| 间接访问 | 不用解引用操作符(*),直接用引用名 | 要解引用操作符(*)来访问其指向的对象 |
| 算术运算 | 不支持算术运算 | 支持算术运算,遍历数组或内存块 |
| 多级 | 没有“引用的引用”(int&&是右值引用,概念不同) | 有“指向指针的指针”(int**) |
| 安全性 | 更安全,避免空值、野指针和指针算术等问题 | 相对不安全,容易出现空指针解引用、野指针等问题 |
sizeof操作 | 返回其所引用对象的大小 | 返回指针本身的大小 |
初始化和空值:引用必须初始化且不为空,指针可以为空或不初始化。
- 引用: 这是引用最严格的规则之一。从根本上杜绝“空引用”和“野引用”的可能。
- 指针: 指针灵活得多。可以在声明时不初始化,但非常危险。要进行空值检查,避免解引用
nullptr。
重新绑定:引用不可重新绑定,指针可以。
- 引用: 一旦引用初始化并绑定到一个对象,就永远是那个对象的别名。
- 指针: 指针是一个变量,其值是内存地址。可以随时修改指针的值,指向不同的内存地址,指向不同的对象。
内存占用和sizeof:引用无独立内存,指针有独立内存。
- 引用: 从C++语言的抽象层面来看,引用本身不占用独立的内存空间。用
sizeof操作符返回的是其所引用对象的大小,而不是引用自身的大小。 - 指针: 指针是一个实实在在的变量,要内存来存储一个地址值。用
sizeof操作符返回的是指针变量自身的大小,而不是它所指向对象的大小。
操作符使用:引用不用解引用,指针要*和&。
- 引用: 因为引用是别名,直接用引用名来访问或修改其所引用的对象。
- 指针: 要访问指针所指向的对象,必须用解引用操作符
*。同时,要获取一个变量的地址赋给指针,也要用地址操作符&。
五、何时用引用,何时用指针?
能用引用的地方优先用引用,因为更安全、更简洁;只有引用的特性不能满足需求,才用指针。
5.1、优先用引用的场景
(1)函数参数传递,修改实参值:
void increment(int& num) { // num是传入变量的别名
num++;
}
int main() {
int x = 5;
increment(x); // x变为6
return 0;
}
(2)避免大对象的拷贝开销(const引用):
struct BigData { /* ... lots of members ... */ };
void printData(const BigData& data) { // 避免拷贝,且不能修改data
// ... 使用data ...
}
BigData myData;
printData(myData);
引用参数的语法和值传递相似,但语义上是地址传递,函数调用看起来更自然,同时避免指针的解引用操作和空值检查。
(3)函数返回值:对函数返回的对象进行修改。
int global_var = 10;
int& getGlobalVar() {
return global_var;
}
int main() {
getGlobalVar() = 20; // 修改global_var为20
std::cout << global_var << std::endl; // 输出20
return 0;
}
(4)避免返回对象的拷贝: 返回引用可以避免昂贵的拷贝构造。
(5)操作符重载: 重载赋值操作符=、下标操作符[]、流插入操作符<<等,引用是标准做法。
class MyArray {
int arr[10];
public:
int& operator[](int index) {
return arr[index];
}
};
MyArray ma;
ma[0] = 100;
(6)提供别名:
std::vector<std::string> long_vector_name;
// ...
std::string& current_element = long_vector_name[i]; // 简化访问
current_element = "new value";
(7)避免空值和野指针: 引用始终保证指向一个有效的对象,不用进行空值检查,代码更简洁、更安全。
5.2、必须用指针的场景
(1)动态内存管理 (new/delete): 堆上分配和释放内存必须用指针。new操作符返回的是一个指向新分配内存的指针。
int* dynamic_int = new int; // 在堆上分配一个int
*dynamic_int = 100;
delete dynamic_int; // 释放内存
dynamic_int = nullptr; // 避免悬空指针
(2)表示“无”或“可选”状态 (nullptr):
Node* findNode(int value, Node* head) {
// ... 查找逻辑 ...
if (found) {
return found_node_ptr;
} else {
return nullptr; // 没有找到,返回空指针
}
}
// 使用时需要检查
Node* result = findNode(10, my_list_head);
if (result != nullptr) {
// ... 处理找到的节点 ...
}
(3)多态: 基类指针或引用操作派生类对象实现多态行为。虽然引用也可以实现多态,但在容器存储异构对象会存储基类指针(std::vector<Base*>)。
class Base {
public:
virtual void print() { std::cout << "Base\n"; }
};
class Derived : public Base {
public:
void print() override { std::cout << "Derived\n"; }
};
void processObject(Base* obj) { // 参数可以是基类指针
obj->print(); // 多态调用
}
int main() {
Derived d;
processObject(&d); // 输出 "Derived"
Base* b_ptr = new Derived();
processObject(b_ptr);
delete b_ptr;
return 0;
}
(4)指针算术:
int arr[] = {1, 2, 3, 4, 5};
int* p = arr; // 指向第一个元素
for (int i = 0; i < 5; ++i) {
std::cout << *(p + i) << " "; // 使用指针算术访问元素
}
std::cout << std::endl;
(5)数据结构: 实现链表、树、图等要显式链接内存块的复杂数据结构。
struct Node {
int data;
Node* next; // 指向下一个节点的指针
};
(6)跟C语言兼容: C语言没有引用。要调用C库函数或跟C风格代码交互就要用指针作为参数或返回值。
5.3、共同使用
引用和指针不完全互斥,有时可以协同工作。
(1)引用一个指针 (Type*&): 表示一个指向Type类型对象的指针的引用。这种用法出现在函数中,函数能修改传入的指针本身 。
void modifyPointer(int*& ptr) { // ptr现在是传入指针变量的别名
ptr = nullptr; // 将外部的指针设置为nullptr
}
int main() {
int value = 10;
int* my_ptr = &value;
std::cout << "Before: " << my_ptr << std::endl; // 输出地址
modifyPointer(my_ptr);
std::cout << "After: " << my_ptr << std::endl; // 输出0x0 (nullptr)
return 0;
}
(2)const引用和const指针:
const引用 (const Type&): 表示不能通过该引用修改其所引用的对象。const指针 (const Type*): 表示不能通过该指针修改其所指向的对象。- 指针常量 (
Type* const): 表示指针本身的值(即它存储的地址)不能被修改,但它所指向的对象可以通过该指针被修改。 - 指向常量对象的常量指针 (
const Type* const): 最严格的形式,既不能修改指针指向的对象,也不能修改指针本身。
六、总结
引用是已存在对象的别名,它不是一个独立的对象,不占用独立的内存空间。
指针是存储另一个变量内存地址的变量,本身占用独立的内存空间。
能用引用就用,非必要不要用指针。
指针的场景:
- 动态内存管理。
- 实现多态。
- 指针算术。
- C语言接口交互。

1389

被折叠的 条评论
为什么被折叠?



