指针 (Pointer) 本质上是一个变量,但它比较特殊,存储的不是普通数据,而是另一个变量在内存中的**地址**。
想象一下:你有一本书(变量),书放在图书馆的某个书架上(内存地址)。指针就像一张记录了这本书所在书架号的小纸条。通过这张纸条,你就能找到这本书。
*
(星号): 在声明时,表示这是一个指针变量,例如 int *p;
。在使用时(非声明),称为**解引用 (Dereference)** 运算符,用于访问指针所指向地址处存储的**值**。相当于“通过纸条找到书”。&
(和号): **取地址 (Address-of)** 运算符,用于获取一个变量的内存地址。相当于“查看书在哪个书架上,并记下书架号”。声明指针时,需要指定它将要指向的数据类型。格式为:类型名 *指针名;
一个典型的例子:
int score = 100; // 定义一个整型变量 score
int *ptr_score; // 声明一个指向整型(int)的指针 ptr_score
ptr_score = &score; // 将 score 的地址赋给 ptr_score
// 现在 ptr_score 存储了 score 的内存地址
// *ptr_score 就等于 score 的值,即 100
可以直接在声明时初始化:
double price = 99.8;
double *p_price = &price; // 声明并初始化指针 p_price
int *p;
),它会指向一个随机的、未知的内存地址。对野指针进行解引用 (*p
) 极其危险,可能导致程序崩溃或数据损坏!nullptr
(空指针,稍后介绍)。int *
指向 double
变量。&100
或 &(a+b)
是错误的。空指针是一个特殊的指针,它明确地表示**不指向任何有效的内存地址**。在 C++11 及之后版本中,推荐使用关键字 nullptr
来表示空指针。
为什么需要空指针?
nullptr
是个好习惯。nullptr
可以防止它变成悬挂指针(一种野指针)。int *p1 = nullptr; // 推荐的空指针初始化
int *p2 = NULL; // C++11 之前的常用方式 (NULL 通常是 (void*)0 或 0)
int *p3 = 0; // 也可以用整数 0,但不推荐,可读性差
if (p1 == nullptr) {
std::cout << "p1 是一个空指针" << std::endl;
}
注意: 对空指针进行解引用 (*p1
) 同样是未定义行为,通常会导致程序崩溃。
野指针是指向**无效或未知内存区域**的指针。使用野指针是 C++ 编程中最常见的错误之一,后果严重。
野指针主要有以下几种成因:
delete
) 或回收(例如,函数返回了局部变量的地址),但指针本身没有被置为 nullptr
,它仍然指向那块“废墟”。nullptr
。*p
) 或访问其成员 (p->member
) 之前,务必检查它是否为 nullptr
。 if (p != nullptr) { ... }
或简写 if (p) { ... }
。delete
或 delete[]
释放内存后,立即将对应的指针设置为 nullptr
。 delete p; p = nullptr;
指针可以进行加法和减法运算,但这与普通整数运算不同。对指针加(或减)一个整数 n
,实际上是将指针的地址**移动 n 个它所指向的数据类型的大小**。
ptr + n
: 指针向内存地址**增加**的方向移动 n * sizeof(*ptr)
个字节。ptr - n
: 指针向内存地址**减少**的方向移动 n * sizeof(*ptr)
个字节。ptr1 - ptr2
: 如果 ptr1
和 ptr2
指向**同一个数组**中的元素,它们相减的结果是两个指针之间相隔的**元素个数**(类型为 ptrdiff_t
)。ptr++
(后增量): 使用 ptr
当前指向的值,然后将 ptr
向前移动一个元素的大小。++ptr
(前增量): 先将 ptr
向前移动一个元素的大小,然后使用移动后的 ptr
指向的值。关键点:指针运算的单位是它所指向的“数据块”的大小,而不是字节。
在 C++ 中,数组名在大多数表达式中会被隐式转换成指向数组**第一个元素**的常量指针。
int numbers[5] = {10, 20, 30, 40, 50};
int *p = numbers; // 等价于 int *p = &numbers[0];
// 访问数组元素的方式:
std::cout << numbers[1]; // 输出 20 (数组下标方式)
std::cout << *(numbers + 1); // 输出 20 (指针运算+解引用方式)
std::cout << p[2]; // 输出 30 (指针也可以用下标访问)
std::cout << *(p + 2); // 输出 30 (指针运算+解引用)
这意味着你可以使用指针来遍历数组:
int *ptr = numbers; // 指向数组开头
for (int i = 0; i < 5; ++i) {
std::cout << *ptr << " "; // 输出当前元素
ptr++; // 移动指针到下一个元素
}
// 输出: 10 20 30 40 50
注意:数组名本身是常量指针,不能被赋值(numbers = ...;
是错误的),但指向数组的指针变量 p
是可以改变指向的。
*(numbers + 5)
或 p[-1]
)是未定义行为,非常危险!sizeof(数组名)
得到整个数组的大小;而 sizeof(指针变量)
只得到指针本身的大小(例如 4 或 8 字节)。之前我们看到的变量(如 int score;
)和数组(如 int arr[10];
)通常在**栈 (Stack)** 上分配内存。栈内存分配快速、自动管理,但大小在编译时就固定了,且生命周期受限于其作用域(如函数内部)。
当我们需要在程序**运行时**根据需要决定分配多少内存,或者希望内存的生命周期不局限于某个函数或代码块时,就需要使用**堆 (Heap)** 内存。动态内存分配就是在堆上申请和释放内存。
C++ 使用 new
运算符在堆上分配内存,并返回指向该内存块的指针。使用 delete
运算符释放由 new
分配的内存。
类型 *指针变量 = new 类型(初始值);
delete 指针变量;
类型 *指针变量 = new 类型[数组大小];
delete[] 指针变量;
(注意方括号!)// 分配单个 int
int *p_num = new int(42); // 在堆上创建了一个 int,值为 42
std::cout << *p_num; // 输出 42
// ... 使用 p_num ...
delete p_num; // 释放内存
p_num = nullptr; // 好习惯:置为 nullptr
// 分配包含 5 个 double 的数组
double *d_array = new double[5];
d_array[0] = 1.1;
d_array[1] = 2.2;
// ... 初始化其他元素 ...
// ... 使用 d_array ...
delete[] d_array; // 释放整个数组 (必须用 delete[])
d_array = nullptr; // 好习惯
new
或 new[]
) 但忘记释放 (delete
或 delete[]
)。程序运行时间越长,可用内存越少,最终可能导致程序崩溃或系统变慢。nullptr
,如果后续不小心再次使用该指针,就会访问无效内存。delete
或 delete[]
。这是严重的错误,行为未定义,通常导致程序崩溃。delete
释放由 new[]
分配的数组,或者用 delete[]
释放由 new
分配的单个对象。这两种情况都是未定义行为!必须配对使用。new
分配失败会抛出 std::bad_alloc
异常,但在某些旧代码或特定编译选项下,可能会返回 nullptr
。需要注意处理内存分配失败的情况。建议: 优先使用 C++ 标准库提供的容器(如 std::vector
, std::string
)和智能指针(如 std::unique_ptr
, std::shared_ptr
),它们能自动管理内存,大大减少手动管理内存带来的风险。
既然指针本身也是一个存储地址的变量,那么它自己也占用内存空间,也有自己的地址。我们可以定义一个指针,让它指向另一个指针的地址,这就是**指向指针的指针**,也常被称为**二级指针**。
声明方式:在基本类型后加两个星号 *
。
int value = 42;
int *p_value = &value; // p_value 是 int* 类型, 指向 value (一级指针)
int **pp_value = &p_value; // pp_value 是 int** 类型, 指向 p_value (二级指针)
// 访问原始值 value:
std::cout << value; // 直接访问: 42
std::cout << *p_value; // 通过一级指针解引用: 42
std::cout << **pp_value; // 通过二级指针解引用两次: 42
// 修改原始值 value:
*p_value = 100; // 通过一级指针修改
**pp_value = 200; // 通过二级指针修改
// 修改一级指针 p_value 的指向:
int another_value = 99;
*pp_value = &another_value; // 现在 p_value 指向 another_value 了
// *pp_value 等价于 p_value
二级(或多级)指针常用于:
函数在内存中也有地址。函数指针就是存储函数入口地址的指针变量。通过函数指针,我们可以像调用普通函数一样调用它所指向的函数。
声明方式:返回类型 (*指针名)(参数类型列表);
关键:指针名两边的括号 (*)
非常重要,不能省略!
// 假设有两个函数:
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
// 声明一个函数指针 func_ptr,它可以指向任何接受两个 int 参数并返回 int 的函数
int (*func_ptr)(int, int);
// 将 add 函数的地址赋给 func_ptr (函数名就是地址)
func_ptr = add;
// 调用 add 函数通过 func_ptr
int result1 = func_ptr(5, 3); // result1 = 8
// 将 subtract 函数的地址赋给 func_ptr
func_ptr = subtract;
// 调用 subtract 函数通过 func_ptr
int result2 = func_ptr(5, 3); // result2 = 2
// 也可以直接用函数名初始化
int (*op)(int, int) = add;
int result3 = op(10, 5); // result3 = 15
函数指针常用于:
*pp
得到的是一级指针 p
,而 **pp
才是最终指向的值。int *func(int, int);
声明的是一个名为 `func` 的**函数**,该函数返回 int*
指针。而 int (*func)(int, int);
才是声明一个名为 `func` 的**指针**,指向返回 `int` 的函数。括号必不可少!关键字 const
用于表示常量,当它与指针结合时,情况会变得稍微复杂,因为它既可以修饰指针本身,也可以修饰指针所指向的数据。理解它们的区别对于编写安全、意图明确的代码至关重要。
主要有以下三种形式,关键在于 const
的位置:
const int *p;
或 int const *p;
(两种写法等价)p
所指向的**值**是常量,不能通过指针 p
来修改它。但是,指针 p
本身**可以**改变指向,去指向另一个(可能是 const 或非 const 的)int 变量。const
在 *
左边(或紧挨着类型名),限制的是 *p
(解引用后的值)。
int x = 10;
int y = 20;
const int *p = &x; // p 指向 x
// *p = 15; // 错误!不能通过 p 修改 x 的值
p = &y; // 正确!p 可以指向其他变量
int * const p;
p
本身是常量,它**必须**在声明时初始化,并且之后**不能**再指向其他内存地址。但是,它所指向的**值可以**通过指针 p
来修改(只要指向的不是 const 变量)。const
在 *
右边,限制的是 p
(指针本身)。
int x = 10;
int y = 20;
int * const p = &x; // p 必须初始化,且永远指向 x
*p = 15; // 正确!可以通过 p 修改 x 的值 (现在 x = 15)
// p = &y; // 错误!p 不能改变指向
const int * const p;
或 int const * const p;
p
本身是常量,**不能**改变指向;同时,它所指向的值也是常量,**不能**通过指针 p
修改。*
两边都有 const
,指针和指向的值都被限制。
int x = 10;
const int * const p = &x; // p 必须初始化,永远指向 x,且不能通过 p 修改 x
// *p = 15; // 错误!不能通过 p 修改值
// p = &y; // 错误!p 不能改变指向
声明方式 | 指针指向可变? (p = ...) | 指向的值可变? (*p = ...) |
---|---|---|
int *p |
✅ 可以 | ✅ 可以 |
const int *p |
✅ 可以 | ❌ 不可以 |
int * const p |
❌ 不可以 | ✅ 可以 |
const int * const p |
❌ 不可以 | ❌ 不可以 |
int * const p
读作 "p is a constant pointer to int" (p 是一个常量指针,指向 int)。const int * p
读作 "p is a pointer to const int" (p 是一个指针,指向 const int)。const_cast
),且这样做可能很危险,应谨慎使用。请编写一个函数 void swap_ptr(int *a, int *b)
,使其能够交换两个外部整型变量的值。思考如果不使用指针,直接传递 int a, int b
会有什么问题?
编写一个函数 int** createMatrix(int rows, int cols)
,该函数动态分配一个 rows
行 cols
列的二维整型数组(矩阵),并返回指向该矩阵的指针(即指向指针数组的指针)。同时,思考如何正确地释放这个二维数组的内存?
&
获取地址,使用 *
解引用访问值。nullptr
表示空指针,是安全的标记。野指针(未初始化、悬挂)是危险的根源。new/delete
和 new[]/delete[]
在堆上分配/释放内存。必须配对使用,谨防内存泄漏和重复释放。**p
的解引用层级。掌握函数指针 (*fptr)()
的声明与调用。const T* p
(指向常量)、T* const p
(常量指针) 和 const T* const p
(都常量)。std::vector
, std::string
, std::unique_ptr
, std::shared_ptr
等来避免手动内存管理的陷阱。