文章一览
- 前言
- 一、指针的提出
- 二、指针类型
- 2.1 类型本质:内存访问的`「解码器」`
- 2.2 void指针:万用钥匙的两面性
- 三、指针的「七宗罪」(常见错误)
- 3.1 野指针
- 3.2 内存泄漏
- 3.3 越界访问
- 3.4类型双标
- 四、指针的运算
- 4.1 指针加减整数:内存导航仪
- 4.2 指针相减:元素距离测量
- 4.3 关系运算:内存地址排序
- 总结
前言
指针是C语言的核心机制,本质上是存储内存地址的变量。每个变量在内存中都有唯一的地址,通过&运算符可获取该地址,而指针则像“地址簿”一样记录并管理这些位置信息。例如,int *p = &a
;表示p存储了整型变量a的内存地址。
指针的核心能力体现在两个方面:直接访问内存和高效操作数据。通过运算符解引用(如p = 10;),可直接读写目标内存;通过指针算术(如p++),可遍历数组等连续内存结构。这种特性使指针成为实现动态内存分配(malloc/free)、构建链表/树等数据结构、优化函数参数传递的关键工具。
学习指针需要理解“地址-值”的双重性,掌握指针与数组、函数、结构体的交互规则。虽然可能面临野指针、内存泄漏等风险,但合理使用指针能大幅提升程序效率和灵活性,是突破初级编程迈向系统级开发的重要里程碑。
一、指针的提出
指针是一个重要的计算机科学概念,主要在编程语言中使用。它可以被视为一个变量,其值是指向另一个变量或函数内存地址的引用。
指针在C语言口中扮演着核心角色,它允许程序间接访问
和操作内存中的数据。
int var = 42; // 在内存0x1000位置存储42
int *ptr = &var; // ptr的值是0x1000
指针声明时的符号是*
, 但是这个*
在不同的场景下,所表达的含义是不同的,具体如下:
星号(*)的三种身份:
声明时:int * 表示"指向整型的指针类型"
解引用时:*ptr 表示"取出该地址存储的值"
运算时:* 可作为乘号(但编译器能区分语境)
总结来说,指针就是一个变量,用来存放地址的变量
。
二、指针类型
指针的类型决定了指针可以指向的数据类型和步长
(每次递增或递减的字节数)。例如,int类型的指针用于指向整型数据,每次递增或递减都会影响到相邻的整型数据。
2.1 类型本质:内存访问的「解码器」
int n = 0x12345678;
char *pc = (char*)&n; // 按字节解析
int *pi = &n; // 按4字节整型解析
核心作用:
- 访问宽度:sizeof(*pointer)决定每次访问的字节数
- 解码方式:指导编译器如何解释内存中的二进制数据
- 指针运算:p+1的实际地址增量 = sizeof(指针类型)
2.2 void指针:万用钥匙的两面性
有一类特殊类型的指针(void*
)
void *vp = malloc(100); // 通用存储容器
这个类型是一个无类型指针,他不表示任何类型。
作为一个无类型指针它的优势是:
- 可以作为泛型函数参数:如
qsort(void *base, ...)
- 用于实现内存操作函数:如
memcpy(void* dest, ...)
但是在使用过程中一定要注意,作为无类型指针,无法直接参与运算(编译器不知道你要走多少),也不可以直接解引用(编译器不知道你要把多少内存拿出来)。
要想对无类型指针进行操作,需要进行强制类型转换:
float f = 3.14f;
void *vp = &f;
int *ip = (int*)vp; // 强制重新解释二进制位
结果:
IEEE754浮点:0x4048f5c3
转换为int:1085488611
硬件视角
:CPU不关心类型,只按指令处理比特位
这时候发现不同类型进行指针转换会出现读取错误,这个错误与不同的数据在内存中的存储方式有关。
所以说在类型转换的时候要遵循一定的准则:
安全转换四原则
- 大小匹配检查:
assert(sizeof(Source) == sizeof(Dest));
- 对齐要求验证:
_Static_assert(_Alignof(int) <= _Alignof(float), "Alignment mismatch");
- 类型双关的正确方式:
int64_t num = 0x12345678;
double d;
memcpy(&d, &num, sizeof(d)); // 标准允许的复制方式
- 限定符传播规则:
const void *cvp = &x;
int *ip = (int*)cvp; // 丢失const限定
三、指针的「七宗罪」(常见错误)
指针作为一个底层操作,它使得我们可以操作计算机内存,但是随之而来的便是一系列问题,对于内存的操作必须慎之又慎,否则将会引起严重的错误,接下来介绍指针的几种常见错误:
3.1 野指针
int *p; *p = 5; (未初始化的指针)
未初始化的指针或非法指针使用可能导致程序崩溃。在使用指针前,必须确保它指向一个合法的内存地址。
野指针的成因通常有以下几种:
- 指针未初始化
- 指针越界访问
- 指针指向的空间释放 (尽量不要返回临时变量的地址)
关于如何避免野指针,要注意几个方面:
- 指针初始化 不知道初始化指向谁时,可以赋一个空指针 (NULL)
- 小心指针越界
- 指针指向空间释放即置为空
- 使用指针之前检测指针的有效性
3.2 内存泄漏
malloc()后忘记free()
malloc
是向堆空间申请一段内存,向堆申请的内存在没有手动释放时,不会自动释放,所以不及时释放可能会导致 栈溢出
错误,在实际开发中可能会造成严重的系统奔溃。
3.3 越界访问
int arr[3]; *(arr+5) = 10;
由于指针操作,CPU不关心类型
,只是按照bit位进行操作,这意味这系统不会进行类型安全检查,所以越界错误常有发生,实际编程中一定要注意这个错误。
3.4类型双标
float *p = (float*)&var; (int变量强行按float解释)
除非确实有需要,否则将不同的类型指针进行转换极易操作数据读取的错误,得不到需要的结果。
四、指针的运算
4.1 指针加减整数:内存导航仪
int arr[5] = {10,20,30,40,50};
int *p = &arr[1]; // 指向20// 运算演示
p += 2; // 现在指向40
p -= 3; // 现在指向10(首元素)
指针的加减运算是与指针类型有关的,指针的类型决定了指针的步进距离:
char *cp = (char*)arr;
cp += 5; // 移动5字节(可能跨越多个int元素)double *dp = (double*)arr;
dp += 1; // 移动8字节(假设sizeof(double)=8)
同样的使用指针操作时要注意越界问题
。
4.2 指针相减:元素距离测量
int *start = &arr[0];
int *end = &arr[4];
printf("%d", start-end);
- 得到的是指针之间得元素个数,小地址减大地址会得到负数。指针要指向同一块空间,否则会出现错误。
- 指针运算时会自动关注指针大小,以类型大小为单位
4.3 关系运算:内存地址排序
标准规定:允许指向数组元素的指针与指向数组最后一个元素后面
的那个内存位置的指针比较,但是不允许与指向第一个元素之前
的那个位置的指针比较
也就是说:
关键限制
:
- 允许比较范围:arr[0] 到 arr[N](含结尾后的位置)
- 禁止比较范围:arr[-1] 之前的位置
合法操作
int arr[5];
int *p = arr;// 合法比较(结尾后位置)
while (p < arr+5) { // 等价于 p <= arr+4*p++ = 0;
}// 合法指针运算
int *end = arr + 5; // 指向结尾后位置
for (p=arr; p != end; p++) { ... }
非法操作
int arr[5];
int *p = arr - 1; // 未定义行为// 错误比较(头前位置)
if (p > arr) { ... } // 违反标准规定// 错误运算导致的崩溃
*p = 10; // 可能触发段错误
总结
以上就是指针的开篇,讲了指针的定义、指针的常见错误、指针的类型以及指针的运算,后面会持续讲解指针与数组、多级指针以及指针的各种混合类型。