目录
1 指向指针的指针(多级指针)
1.1 概念
指向指针的指针,作为一种多级间接寻址方式,可以被视为一个指针链。在常规的指针使用中,一个指针存储了一个变量的内存地址。而当我们引入指向指针的指针时,情况就变得更为复杂且灵活:第一个指针存储的是第二个指针的内存地址,而第二个指针则指向实际存储数据的内存位置。这种结构允许我们通过多层引用间接地访问和操作数据,为编程提供了更高的灵活性和控制力。
- 单级指针:直接指向一个特定类型数据的地址。例如,int *p; 定义了一个指向整型数据的指针 p。
- 指向指针的指针:指向另一个指针的地址。例如,int **pp; 定义了一个指向指针 p 的指针 pp,这里的 p 是一个指向整型数据的指针。
1.2 多级指针的定义与使用
1.2.1 声明多级指针
多级指针是指在一个指针的基础上再定义一个或多个指针来指向该指针。
多级指针的声明需要使用多个星号来表示指针的级别。星号的数量决定了指针的层数。
int *ptr; // 一级指针,指向一个整数
int **pptr; // 二级指针,指向一个一级指针
int ***ppptr; // 三级指针,指向一个二级指针
1.2.2 初始化多级指针
初始化多级指针时,需要逐级给指针赋值,确保每个级别的指针都指向正确的目标。这是为了避免未初始化的指针导致的未定义行为。
int var = 42; // 定义一个整数变量
int *ptr = &var; // 一级指针指向 var
int **pptr = &ptr; // 二级指针指向 ptr
int ***ppptr = &pptr; // 三级指针指向 pptr
1.2.3 解引用多级指针
解引用多级指针时,需要根据指针的级别使用适当数量的星号解引用操作。每增加一个星号,就向下一层级访问数据。
printf("Value of var: %d\n", var); // 直接访问 var
printf("Value via ptr: %d\n", *ptr); // 解引用一次,访问 var
printf("Value via pptr: %d\n", **pptr); // 解引用两次,访问 var
printf("Value via ppptr: %d\n", ***ppptr); // 解引用三次,访问 var
1.3 案例演示
#include <stdio.h>
int main()
{
int a = 100; // 定义一个整型变量a并初始化为100
// 定义多级指针
int *ptr = &a; // 定义一个一级指针 ptr,并将其初始化为变量 a 的地址
int **pptr = &ptr; // 定义一个二级指针 pptr,并将其初始化为一级指针 ptr 的地址
int ***ppptr = &pptr; // 定义一个三级指针 ppptr,并将其初始化为二级指针 pptr 的地址
printf("变量a的值为: %d, 本身的地址为: %p\n\n", a, (void *)&a);
printf("一级指针ptr的值为(变量a的地址): %p, \n本身的地址为: %p, \n*ptr(变量a的值): %d\n\n", (void *)ptr, (void *)&ptr, *ptr);
printf("二级指针pptr的值为(一级指针ptr的地址): %p, \n本身的地址为: %p, \n*pptr(一级指针ptr的值,即变量a的地址) = %p, \n**pptr(变量a的值) = %d\n\n", (void *)pptr, (void *)&pptr, (void *)*pptr, **pptr);
printf("三级指针ppptr的值为(二级指针pptr的地址): %p, \n本身的地址为: %p, \n*ppptr(二级指针pptr的值,即一级指针ptr的地址) = %p, \n**ppptr(一级指针ptr的值,即变量a的地址) = %p, \n***ppptr(变量a的值) = %d\n", (void *)ppptr, (void *)&ppptr, (void *)*ppptr, (void **)**ppptr, ***ppptr);
return 0;
}
输出结果如下所示:
内存布局图:
2 空指针
2.1 什么是空指针
赋为 NULL 值的指针被称为空指针。NULL 是一个在标准库头文件(通常是 <stddef.h> 或<stdlib.h>,尽管在某些旧代码中可能会看到它在 <stdio.h> 中被定义,但这并不是其标准的定义位置)中定义的宏常量,其值通常为零(但在 C++ 中,建议使用 nullptr 代替 NULL,因为 nullptr具有类型安全性)。
在声明指针变量时,如果尚未确定要指向的具体地址,将指针变量初始化为 NULL 是一个良好的编程习惯。这样做可以避免野指针(即指向未知内存地址的指针)的出现,从而防止潜在的内存访问错误。
2.2 案例演示
#include <stdio.h>
int main()
{
int *p = NULL; // 将指针 p 初始化为 NULL
int num = 34;
p = # // 将指针 p 指向变量 num 的地址
printf("p=%p\n", p); // 打印指针 p 的值(即 num 的地址)
printf("*p=%d\n", *p); // 打印指针 p 指向的变量 num 的值
return 0;
}
虽然 NULL 可能在某些版本的 <stdio.h> 中被定义,但更标准的做法是直接包含定义 NULL 的头文件,通常是 <stddef.h>。然而,在现代 C 编程中,由于 <stdio.h> 经常间接包含其他必要的头文件,所以通常不需要显式包含 <stddef.h>。但为了代码的清晰性和可移植性,了解 NULL 的标准定义位置是有益的。
3 野指针
3.1 什么是野指针
野指针(Wild Pointer)是指那些指向不确定或无效或者已经被释放的内存地址的指针。野指针的存在可能导致程序的不稳定性和难以调试的错误,因为它们可能指向已经释放的内存、未初始化的内存或其他非法的内存区域。
3.2 野指针的成因
3.2.1 指针使用前未初始化
指针变量在定义时如果未初始化,其值是随机的。此时操作指针就是去访问一个不确定的地址,结果是不可知的,此时 p 就为野指针。
int *p;
printf("%d\n", *p); // 错误:访问未初始化的指针
错误原因:p 未初始化,其值是随机的,因此 *p 访问的是一个不确定的内存地址,结果不可预测。
解决方案:在声明指针时,应立即为其赋值或初始化为 NULL。
int *p = NULL;
3.2.2 指针越界访问
指针超出了数组的有效范围,访问了非法的内存地址。
int arr[4] = {10, 20, 30, 40};
int *p = arr;
p += 4;
printf("%d", *p); // 错误:越界访问
错误原因:p += 4 后,p 指向了数组 arr 之外的内存地址,*p 访问的是非法内存,导致野指针。
解决方案:确保指针始终在数组的有效范围内。
if (p >= arr && p < arr + 4) {
printf("%d", *p);
} else {
printf("Pointer out of bounds\n");
}
3.2.3 指针指向已释放的空间
当一个指针指向的内存被释放后,该指针仍然保留原来的内存地址。如果这个指针在内存释放后继续使用,它就是一个野指针。
#include <stdio.h>
#include <stdlib.h>
int *test() {
int a = 10;
return &a; // 错误:返回局部变量的地址
}
int main() {
int *p = test();
printf("%d", *p); // 错误:访问已释放的内存
return 0;
}
错误原因:test 函数返回的是局部变量 a 的地址,当函数返回后,a 的生命周期结束,其占用的内存被释放。p 指向的是已释放的内存,因此 *p 是野指针。
解决方案:避免返回局部变量的地址,可以考虑使用动态分配的内存或静态变量。
int *test() {
static int a = 10;
return &a; // 返回静态变量的地址
}
3.2.4 指针运算错误
对指针进行错误的运算(如加减操作)也可能导致指针指向一个无效的内存地址。
int *ptr = NULL;
ptr += 1; // 错误的指针运算,导致指向未知内存
3.3 如何避免野指针
3.3.1 初始化为 NULL
如果在声明指针时没有确切的地址赋值,应将其初始化为 NULL。这可以避免未初始化的指针导致的未定义行为。
int *ptr = NULL;
3.3.2 检查边界条件
在使用指针访问数组或其他数据结构时,确保指针始终在有效范围内。
int arr[4] = {10, 20, 30, 40};
int *ptr = arr;
if (ptr >= arr && ptr < arr + 4) {
printf("%d", *ptr);
} else {
printf("Pointer out of bounds\n");
}
3.3.3 使用静态变量或动态分配的内存
避免返回指向局部变量的指针,因为局部变量的生命周期在函数返回后结束。可以考虑使用静态变量或动态分配的内存。
int *test() {
static int a = 10;
return &a; // 返回静态变量的地址
}
int *test2() {
int *a = (int *)malloc(sizeof(int));
*a = 10;
return a; // 返回动态分配的内存地址
}
3.3.4 检查是否为 NULL
在使用指针之前,检查它是否为 NULL,以避免访问无效的内存地址。
int *ptr = NULL;
if (ptr != NULL) {
printf("%d", *ptr);
} else {
printf("Pointer is NULL\n");
}
3.3.5 释放内存后置为 NULL
在释放指针指向的内存后,立即将指针置为 NULL,以避免悬挂指针(即指向已释放内存的指针)。
int *ptr = (int *)malloc(sizeof(int));
if (ptr != NULL) {
*ptr = 10;
free(ptr);
ptr = NULL;
}
3.3.6 使用智能指针(C++)
在 C++ 中,可以使用智能指针(如 std::unique_ptr 和 std::shared_ptr)来自动管理内存,避免野指针和内存泄漏。
#include <memory>
#include <iostream>
void example() {
std::unique_ptr<int> ptr(new int(10));
std::cout << *ptr << std::endl;
// 智能指针会在离开作用域时自动释放内存
}
3.3.7 其余方法
代码审查:定期进行代码审查,确保指针的使用符合最佳实践。
单元测试:编写单元测试,检查指针的使用是否正确,特别是边界条件和异常情况。
4 悬挂指针
4.1 什么是悬挂指针
悬挂指针(Dangling Pointer)是指那些指向已经释放或无效的内存地址的指针。当一个指针指向的内存被释放后,如果该指针没有被置为 NULL 或重新赋值,它仍然会保留原来的地址,但该地址已经不再有效。使用悬挂指针会导致未定义行为,包括程序崩溃、数据损坏或其他不可预测的行为。
4.2 悬挂指针的成因
4.2.1 释放内存后未置为 NULL
当一个指针指向的内存被释放后,如果指针没有被置为 NULL,它就会成为一个悬挂指针。
int *ptr = (int *)malloc(sizeof(int));
*ptr = 10;
free(ptr); // 释放内存
// ptr 仍然是一个悬挂指针,因为它没有被置为 NULL
4.2.2 返回指向局部变量的指针
如果一个指针指向了一个局部变量,而该局部变量的生命周期已经结束(例如,函数返回后),那么这个指针就会成为一个悬挂指针。
int *get_ptr() {
int x = 10;
return &x; // 返回指向局部变量的指针
}
int *ptr = get_ptr(); // ptr 是一个悬挂指针
4.2.3 多线程环境下的竞态条件
在多线程环境中,如果一个线程释放了内存,而另一个线程仍在使用该内存的指针,就会导致悬挂指针。
4.3 如何避免悬挂指针
释放内存后置为 NULL:在释放指针指向的内存后,立即将指针置为 NULL。
避免返回指向局部变量的指针:不要返回指向局部变量的指针,可以考虑使用动态分配的内存或静态变量。
使用智能指针(C++):在C++中,可以使用智能指针(如 std::unique_ptr 和 std::shared_ptr)来自动管理内存,避免悬挂指针和内存泄漏。
同步多线程访问:在多线程环境中,使用互斥锁或其他同步机制来确保内存的释放和使用是同步的。
5 空指针、野指针、悬挂指针对比总结
指针类型 | 定义 | 特性 | 产生原因 | 安全性 | 示例 |
---|---|---|---|---|---|
空指针 | 指向空地址(NULL)的指针 | 1. 值为 NULL,不指向任何实际内存。 | 1. 显式初始化为 NULL。 | 1. 安全,不会访问非法内存。 | int *p = NULL; |
2. 解引用会导致运行时错误(如段错误)。 | 2. 内存分配失败时返回 NULL。 | 2. 需要检查指针是否为NULL。 |
| ||
野指针 | 指向不确定或非法内存位置的指针 | 1. 指向未初始化、已释放或越界的内存。 | 1. 指针变量未初始化。 | 1. 极度危险,可能导致程序崩溃或数据损坏。 |
|
2. 值是不确定的,可能指向任何内存地址。 | 2. 释放内存后未将指针置为 NULL。 | 2. 难以检测和调试。 |
| ||
3. 使用野指针进行解引用是未定义行为。 | 3. 指针运算错误导致越界。 |
| |||
悬挂指针 | 指向已被释放内存区域的指针 | 1. 指向的内存已被 free 或 delete 释放。 | 1. 释放内存后未将指针置为 NULL。 | 1. 危险,可能导致程序崩溃或数据损坏。 |
|
2. 指针值仍然保留原来的内存地址。 | 2. 误用已释放内存的指针。 | 2. 需要确保释放内存后将指针置为 NULL。 | p = NULL; // 释放后重置指针 | ||
3. 访问悬挂指针指向的内存是未定义行为。 |
- 空指针:空指针是一个指向空地址(通常是 NULL)的指针,它不指向任何实际的内存区域。使用空指针进行解引用会导致运行时错误(如段错误),因此在解引用指针之前,通常需要检查指针是否为NULL。
- 野指针:野指针是指向不确定或非法内存位置的指针。它可能指向未初始化的内存、已被释放的内存(但未置为 NULL)或越界的数组元素等。野指针的值是不确定的,可能指向任何内存地址,因此使用野指针进行解引用是未定义行为,极度危险,可能导致程序崩溃或数据损坏。野指针难以检测和调试,因为它们的值是随机的。
- 悬挂指针:悬挂指针是指向已被释放内存区域的指针。在释放内存后,如果未将指针置为 NULL,而后续代码中又误用了这个指针,就会形成悬挂指针。悬挂指针指向的内存已经被释放,但指针值仍然保留原来的内存地址。访问悬挂指针指向的内存是未定义行为,可能导致程序崩溃或数据损坏。因此,在释放内存后,需要确保将指针置为 NULL,以避免悬挂指针的产生。
6 指针使用一览表
变量定义 | 类型表示 | 含义 | 说明 |
---|---|---|---|
int i | int | i 是一个整型变量。 | |
int *p | int * | p 是一个指向整型数据的指针。 | p 存储一个整型变量的地址。 |
int a[5] | int[5] | a 是一个包含 5 个元素的整型数组。 | a 存储 5 个整数。 |
int *p[4] | int *[4] | p 是一个指针数组,每个元素都是指向整型数据的指针。 | p 是一个包含 4 个指针的数组,每个指针都指向一个整型变量。 |
int (*p)[4] | int (*)[4] | p 是一个数组指针,指向一个包含 4 个元素的整型数组。 | p 存储一个指向包含 4 个整数的数组的地址。 |
int f() | int () | f 是一个返回整型数据的函数。 | f 是一个函数,调用后返回一个整数值。 |
int *f() | int *() | f 是一个指针函数,返回指向整型数据的指针。 | f 是一个函数,调用后返回一个指向整型数据的指针。 |
int (*p)() | int (*)() | p 是一个函数指针,指向一个返回整型数据的函数。 | p 存储一个函数的地址,该函数返回一个整数值。 |
int **p | int ** | p 是一个指向指针的指针。 | p 存储一个指向另一个指针的地址,该指针指向一个整型变量。 |
7 测试题
1. 请写出下面程序的运行结果(64 位操作系统)
int num = 250;
int *p1 = #
int **p2 = &p1;
int ***p3 = &p2;
printf("%zu %zu %zu %zu", sizeof p3, sizeof *p3, sizeof **p3, sizeof ***p3);
【答案】8 8 8 4
【解析】
(1)p3 的值是 p2 的地址。
(2)*p3 得到的是 p1 的地址。
(3)**p3 得到的是 num 的地址。
(4)*** p3 得到的是 num 的值 250。