1 指针的本质:地址与解引用
1.1 什么是指针?
指针是一个变量,其存储的值是另一个变量的内存地址。你可以将内存想象成一个巨大的公寓楼,每个字节是一个房间,每个房间都有唯一的门牌号(地址)。指针就是记录着这些门牌号的便签。
-
声明指针 :
数据类型 *指针变量名;
cint *p; // 指向整型的指针 char *ch; // 指向字符型的指针 float *fp; // 指向浮点型的指针
*
表示这是一个指针变量,数据类型
说明了指针所指向的内存区域中存储的数据类型。 -
初始化指针 :使用
&
(取地址操作符)获取变量的地址。cint a = 10; int *p = &a; // p 指向变量a的地址
未初始化的指针是"野指针",指向随机内存,非常危险。良好的习惯是定义时立即初始化 ,若暂无明确指向,可初始化为
NULL
(或0
)。cint *safe_ptr = NULL; // 安全的初始化
-
解引用指针 :使用
*
(解引用操作符)访问或修改指针所指向地址的值。cprintf("a = %d\n", a); // 输出: a = 10 printf("*p = %d\n", *p); // 输出: *p = 10 (通过p访问a的值) *p = 20; // 通过指针p修改其指向地址(即变量a)的值 printf("a is now %d\n", a); // 输出: a is now 20
解引用本质上是一次内存访问 。对
*p
的操作就是对p
所存地址处数据的操作。
1.2 指针的大小
指针变量的大小是固定的,取决于系统的寻址能力,与它指向的数据类型无关:
- 32位系统:通常为 4字节
- 64位系统:通常为 8字节
你可以用sizeof
操作符验证:
c
printf("Size of int pointer: %zu\n", sizeof(int*)); // 输出: 8 (在64位系统)
printf("Size of char pointer: %zu\n", sizeof(char*)); // 输出: 8
printf("Size of double pointer: %zu\n", sizeof(double*)); // 输出: 8
2 指针的算术运算
指针的算术运算(加减)不是简单的整数加减,而是以所指向数据类型的大小为单位进行移动。
c
int arr[] = {10, 20, 30, 40, 50};
int *p = arr; // p 指向数组首元素(arr[0])的地址
printf("*p = %d\n", *p); // 输出: 10
printf("Address: %p\n", p);
p++; // 向后移动一个int单位(通常是4字节),指向arr[1]
printf("After p++:\n");
printf("*p = %d\n", *p); // 输出: 20
printf("Address: %p\n", p); // 地址值比之前增加了4
p += 2; // 向后移动两个int单位,指向arr[3]
printf("After p += 2:\n");
printf("*p = %d\n", *p); // 输出: 40
减法运算可以计算两个指针之间的距离(元素个数):
c
int *p1 = &arr[0];
int *p2 = &arr[3];
ptrdiff_t diff = p2 - p1; // 计算两个指针之间相差的元素个数
printf("p2 - p1 = %td\n", diff); // 输出: 3 (相差3个元素)
注意 :指针减法的两个指针必须指向同一块连续内存空间(如同一个数组),否则行为未定义。
3 指针与数组
在C语言中,数组名在大多数情况下是一个指向数组首元素的常量指针。
c
int arr[5] = {1, 2, 3, 4, 5};
// 以下访问方式是等价的:
printf("arr[0] = %d\n", arr[0]);
printf("*arr = %d\n", *arr); // 对数组名解引用访问第一个元素
// 通过指针算术访问数组元素
printf("arr[1] = %d\n", *(arr + 1));
printf("arr[2] = %d\n", *(arr + 2));
// 定义一个指针遍历数组
int *ptr = arr;
for (int i = 0; i < 5; i++) {
printf("Element %d: %d\n", i, *(ptr + i));
// 或 printf("Element %d: %d\n", i, ptr[i]); // 指针也可以使用下标!
}
重要区别 :数组名是指针常量,其值(指向的地址)不可改变。而指针变量可以重新赋值。
c
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
// p = p + 1; // 合法,p现在指向arr[1]
// arr = arr + 1; // 非法!编译错误!数组名是常量,不能修改。
4 指针与函数
4.1 指针作为函数参数(模拟"传引用")
C语言函数参数传递默认是传值调用 ,即函数获得的是实参值的副本。修改副本不会影响原始实参。若希望函数内部修改外部变量的值,需要传递变量的指针。
c
// 一个交换两个变量值的函数
void swap(int *x, int *y) { // 接收指针作为参数
int temp = *x; // 解引用x,获取其指向的值
*x = *y; // 将y指向的值赋给x指向的内存
*y = temp; // 将temp的值赋给y指向的内存
}
int main() {
int a = 10, b = 20;
printf("Before swap: a = %d, b = %d\n", a, b);
swap(&a, &b); // 传递变量a和b的地址
printf("After swap: a = %d, b = %d\n", a, b);
return 0;
}
输出:
Before swap: a = 10, b = 20
After swap: a = 20, b = 10
工作原理 :函数 swap
接收的是 a
和 b
的地址(指针),通过解引用操作 *x
和 *y
,直接操作 main
函数栈帧中 a
和 b
所在的内存单元,从而真正交换它们的值。
4.2 数组作为函数参数
当数组作为函数参数传递时,它会退化为指向其首元素的指针 。因此,函数内部无法通过 sizeof
获取原始数组的长度。
c
void printArray(int arr[], int size) { // int arr[] 等价于 int *arr
// 在函数内部,sizeof(arr) 将是指针的大小(8或4),而不是整个数组的大小!
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]); // 虽然arr是指针,但仍可使用下标语法
}
printf("\n");
}
int main() {
int myArray[] = {1, 2, 3, 4, 5};
int length = sizeof(myArray) / sizeof(myArray[0]); // 正确计算数组长度
printArray(myArray, length); // 传递数组名和实际长度
return 0;
}
5 动态内存分配:堆(Heap)
5.1 堆与栈的区别
理解堆 和栈的区别至关重要。
特性 | 栈 (Stack) | 堆 (Heap) |
---|---|---|
管理方式 | 编译器自动分配和释放 | 程序员手动分配 (malloc , calloc ) 和释放 (free ) |
生命周期 | 函数执行期间,函数返回后自动销毁 | 从分配开始直到显式释放为止 |
大小限制 | 较小(例如几MB),操作系统依赖 | 很大,仅受系统可用虚拟内存大小限制 |
分配速度 | 非常快 | 相对较慢,涉及更复杂的管理 |
碎片化 | 无 | 可能产生碎片 |
灵活性 | 大小和生命周期在编译时确定 | 大小和生命周期在运行时动态决定 |
5.2 动态内存分配函数
C语言使用 malloc
, calloc
, realloc
在堆上分配内存,使用 free
释放内存。
-
malloc
:分配指定字节数的未初始化内存。c// 分配可存储10个int的内存空间 int *arr = (int *)malloc(10 * sizeof(int)); if (arr == NULL) { // 分配失败必须检查!NULL可能意味着内存不足 fprintf(stderr, "Memory allocation failed!\n"); exit(1); } // 使用分配的内存...
-
calloc
:分配指定数量和大小的内存,并初始化为0。c// 分配10个int,并全部初始化为0 int *arr_zero = (int *)calloc(10, sizeof(int));
-
realloc
:调整已分配内存块的大小(可能移动位置)。c// 将之前分配的内存扩大到20个int int *new_arr = (int *)realloc(arr, 20 * sizeof(int)); if (new_arr == NULL) { // 处理失败,注意:原来的arr指针依然有效,需要单独释放 free(arr); fprintf(stderr, "Memory reallocation failed!\n"); exit(1); } else { arr = new_arr; // 让arr指向新的内存块 }
-
free
:释放之前动态分配的内存。cfree(arr); // 释放arr指向的内存 arr = NULL; // 良好实践:释放后立即将指针置为NULL,防止悬垂指针
谁分配,谁释放 :确保每个
malloc
,calloc
,realloc
都有对应的free
。
5.3 常见动态内存错误(漏洞根源)
-
内存泄漏 (Memory Leak):分配的内存没有被释放,导致程序持续占用内存直至耗尽。
cvoid leak() { int *ptr = (int *)malloc(100 * sizeof(int)); // ... 使用ptr ... // 忘记 free(ptr); 函数返回后,再也无法访问或释放那100个int的内存! }
-
Use-After-Free :释放内存后,再次使用该指针访问已释放的内存。这是一个严重的安全漏洞,攻击者可能利用此漏洞执行恶意代码。
cint *ptr = (int *)malloc(sizeof(int)); *ptr = 42; free(ptr); // 内存被释放,交还给系统 // ptr现在是一个"悬垂指针"(Dangling Pointer) *ptr = 10; // 危险!未定义行为:可能崩溃,也可能 silently corrupt data。
-
Double Free :对同一块动态内存多次调用
free
。这会导致内存管理数据结构损坏,可能引发程序崩溃。cint *ptr = (int *)malloc(sizeof(int)); free(ptr); // ... free(ptr); // 错误!同一内存释放两次。