示例:
int a = 10; // 在内存中分配4字节存储整数10
int *p = &a; // 定义指针变量p,存储变量a的地址
关键特性:
在32位系统中,CPU通过32根地址线生成内存地址:
地址生成过程:
地址线状态 对应的内存地址
0000…0000 (32 位) → 0x00000000 (最小地址)
0000…0001 → 0x00000001
0000…0010 → 0x00000002
…
1111…1111 → 0xFFFFFFFF (最大地址)
内存单元组织:
当32根地址线同时工作时,会产生由32个0或1组成的二进制序列,这个序列就是32位地址。由于1个字节能存储8个二进制位,所以32位的地址需要4个字节(32÷8=4)的存储空间来存放。所以在32位系统中,所有类型的指针变量均占4字节(32位)。
指针的大小在32位平台是4个字节,在64位平台是8个字节。
示例:
char c = 'A'; // char类型占1字节
int i = 100; // int类型占4字节
double d = 3.14; // double类型占8字节
char *pc = &c; // char*指针占4字节
int *pi = &i; // int*指针占4字节
double *pd = &d; // double*指针占4字节
printf("%lu\n", sizeof(pc)); // 输出:4
printf("%lu\n", sizeof(pi)); // 输出:4
printf("%lu\n", sizeof(pd)); // 输出:4
注意:
指针类型决定了:
不同类型指针的访问权限:
char ch = 'B';
char* pch = &ch;
*pch = 'C'; // 解引用char*指针,只访问ch所占的1个字节,将其值改为'C'
short s = 200;
short* ps = &s;
*ps = 300; // 解引用short*指针,访问s所占的2个字节,修改其值为300
int i = 10;
int* pi = &i;
*pi = 20; // 解引用int*指针,访问i所占的4个字节,修改值为20
float f = 3.14f;
float* pf = &f;
*pf = 2.71f; // 解引用float*指针,访问f所占的4个字节,修改值为2.71f
double d = 123.456;
double* pd = &d;
*pd = 789.012; // 解引用double*指针,访问d所占的8个字节,修改值为789.012
指针加减整数时,跳过的字节数由指针类型决定:
示例:
决定指针加减整数时跳过的字节数:指针进行加减整数运算时,跳过的字节数由指针类型决定。
char str[5] = "abcde";
char* pstr = str;
pstr++; // 跳过1个字节,从指向str[0]变为指向str[1]
short arr_s[3] = {10, 20, 30};
short* ps = arr_s;
ps++; // 跳过2个字节,从指向arr_s[0]变为指向arr_s[1]
nt arr_i[4] = {1, 2, 3, 4};
int* pi = arr_i;
pi++; // 跳过4个字节,从指向arr_i[0]变为指向arr_i[1]
float arr_f[2] = {1.1f, 2.2f};
float* pf = arr_f;
pf++; // 跳过4个字节,从指向arr_f[0]变为指向arr_f[1]
double arr_d[2] = {10.5, 20.5};
double* pd = arr_d;
pd++; // 跳过8个字节,从指向arr_d[0]变为指向arr_d[1]
指针运算公式:
ptr + n → ptr + n × sizeof(指针类型)
注意:
不同类型的指针不能混用,即使大小相同
即使 int和 float类型的指针解引用时都访问 4 个字节,且 + 1 时都跳过 4 个字节,它们也不能混用。这是因为 int 类型和 float 类型在内存中的存储方式完全不同:int 类型直接存储整数的二进制形式,而 float 类型按照 IEEE 754 标准的浮点数格式存储,包括符号位、指数位和尾数位。如果混用,会导致数据解析错误,例如:
int i = 10;
float* pf = (float*)&i; // 强制将int*转换为float*
printf("%f\n", *pf); // 输出结果不是10.000000,因为解析方式不同
上述代码中
,将 int 类型变量 i 的地址强制转换为 float * 类型并解引用,由于存储方式的差异,得到的结果并非预期的 10.0。
野指针指向的位置是随机、不可知的,常见于以下情况:
① 未初始化的指针:
int *p; // 未初始化,p的值是随机的
*p = 10; // 解引用野指针,导致未定义行为
② 指针越界访问:
//实例1
int arr[5];
int *p = arr;
p[10] = 100; // 越界访问,p指向数组外的内存
//实例2
int arr[3] = {10, 20, 30}; // 数组有3个元素,下标0、1、2
int* p = arr;
for (int i = 0; i <= 3; i++) {
*(p + i) = i * 10; // 当i=3时,p+i指向数组第3个元素之后,越界成为野指针
}
上述实例2代码中,数组 arr 只有 3 个元素,当 i=3 时,p+i 指向的位置超出了数组范围,此时解引用会导致越界访问。
③ 访问已销毁的内存:
int* get_addr() {
int temp = 50; // 局部变量,函数结束后销毁
return &temp; // 返回局部变量的地址
}
int main() {
int* p = get_addr(); // p指向已销毁的temp的空间,成为野指针
*p = 100; // 错误,访问已销毁的空间,程序行为不可预测
return 0;
}
在 main 函数中,p 接收的是 get_addr 函数中局部变量 temp 的地址,但 temp 在函数结束后已被销毁,此时 p 为野指针,解引用操作是非法的。
int *p = NULL; // 初始化为空指针
int a = 10;
p = &a; // 明确指向后再解引用
int* func() {
static int a = 10; // 使用静态变量(生命周期为整个程序)
return &a; // 安全返回静态变量地址
}
int *p = get_pointer(); // 假设这是一个可能返回NULL的函数
if (p != NULL) { // 使用前检查
*p = 100;
}
int arr[5];
for (int i = 0; i < 5; i++) { // 严格控制循环边界
arr[i] = i;
}
示例:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;
printf("%d\n", *p); // 输出:1(arr[0])
printf("%d\n", *(p+2)); // 输出:3(arr[2])
p += 3; // p指向arr[3]
printf("%d\n", *p); // 输出:4
注意:
*p++ = 0
可以拆解为两个步骤:首先执行*p = 0
,将p指向的内存位置赋值为0;然后执行p++
,使p指向当前位置的下一个元素(根据指针类型偏移相应字节)。在数组中,指针+1意味着指向数组的下一个元素。p++
和 p + 1
的区别在于是否改变指针 p 本身的值:p++
是后缀自增操作,会先使用 p 当前的值,然后将 p 的值增加 1(即 p 指向的地址发生改变);而p + 1只是计算出 p 指向位置的下一个位置的地址,p 本身的值不会发生变化。
int a = 10, b = 20;
int *p = &a;
int *q = p + 1; // q指向a后面的内存(未定义)
p++; // p现在指向b后面的内存(假设b紧跟a)
示例:
int arr[10];
int *p1 = &arr[2];
int *p2 = &arr[7];
ptrdiff_t diff = p2 - p1; // 结果:5(元素个数)
printf("%td\n", diff); // 输出:5
计算原理:
(地址p2 - 地址p1) / sizeof(指针类型)
注意:
指针相加无意义(地址相加可能超出有效范围)
不同数组的指针相减结果未定义
int arr1[5], arr2[5];
int *p = &arr1[0];
int *q = &arr2[0];
printf("%td\n", q - p); // 未定义行为!
指针的关系运算:指针的关系运算指的是比较两个指针的大小,通常用于判断指针指向的位置前后关系。
示例:
int arr[5] = {1, 3, 5, 7, 9};
int *p = arr;
int *end = arr + 5; // 指向数组最后一个元素的下一个位置
while (p < end) { // 合法比较
printf("%d ", *p);
p++;
}
C 语言标准规定:
int arr[5];
int *p = arr;
int *before = arr - 1; // 指向数组前一个位置
if (p > before) { // 未定义行为!
// ...
}
int arr[10] = {0}; int* p = arr;
,这里的arr被转换为指向arr[0]的指针,所以p指向arr[0]。*(arr + 1)
和*(p +1)
是等价的,都表示访问数组的第二个元素 arr [1]。这是因为 arr + 1 和 p + 1 都指向数组的第二个元素,解引用后就得到该元素的值。示例:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等价于:int *p = &arr[0];
printf("%d\n", arr[2]); // 输出:3
printf("%d\n", *(arr+2)); // 等价写法:3
printf("%d\n", p[2]); // 等价写法:3
printf("%d\n", *(p+2)); // 等价写法:3
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;
// 方法1:下标法
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
// 方法2:指针偏移法
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i));
}
// 方法3:指针自增法
for (int i = 0; i < 5; i++) {
printf("%d ", *p++); // 先解引用,再自增指针
}
数组作为参数传递时会退化为指针:
示例:
void print_array(int *arr, int size) { // 等价于:void print_array(int arr[], int size)
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
print_array(arr, 5); // 数组名隐式转换为指针
return 0;
}
注意:
函数内无法用sizeof(arr)获取数组大小
void func(int arr[]) {
printf("%lu\n", sizeof(arr)); // 输出:4(32位系统指针大小)
}
*
表明它是一个指针,第二个*
表明它所指向的类型是一个指针。例如,int** pp
表示pp是一个二级指针,它指向的是一个int*类型的一级指针。示例:
int a = 10;
int *p = &a; // 一级指针,指向int
int **pp = &p; // 二级指针,指向int*
对二级指针的解引用需要两次解引用操作:第一次解引用*pp得到的是它所指向的一级指针 p;第二次解引用**pp得到的是 p 所指向的变量 a 的值。
printf("%d\n", a); // 输出:10
printf("%d\n", *p); // 输出:10(解引用一级指针)
printf("%d\n", **pp); // 输出:10(两次解引用二级指针)
示例:
void allocate_memory(int **pp, int size) {
*pp = (int*)malloc(size * sizeof(int)); // 修改实参指针
}
int main() {
int *p = NULL;
allocate_memory(&p, 5); // 传递指针的地址
if (p != NULL) {
// 使用分配的内存...
free(p);
}
return 0;
}
理论上可以有三级、四级指针,但实际很少超过二级:
示例:
int a = 10;
int *p = &a;
int **pp = &p;
int ***ppp = &pp;
printf("%d\n", ***ppp); // 输出:10
类型* 数组名[数组长度]
,例如int* parr[5]
表示parr是一个包含5个int*类型指针的数组。示例:
int a = 10, b = 20, c = 30;
int *arr[3] = {&a, &b, &c}; // 指针数组
printf("%d\n", *arr[0]); // 输出:10
printf("%d\n", *arr[1]); // 输出:20
printf("%d\n", *arr[2]); // 输出:30
示例:
int arr2[2][3] = {{10, 20, 30}, {40, 50, 60}};
int* parr[2] = {arr2[0], arr2[1]}; // 指针数组,元素分别指向二维数组的两行
// 访问arr2[0][1]
printf("%d\n", *(parr[0] + 1)); // 输出20,parr[0]指向第一行,+1指向该行第二个元素
// 访问arr2[1][2]
printf("%d\n", *(parr[1] + 2)); // 输出60,parr[1]指向第二行,+2指向该行第三个元素
与真正二维数组的区别:
int arr1[] = {1, 2};
int arr2[] = {3, 4, 5};
int* parr[] = {arr1, arr2}; // 指针数组元素指向不同长度的数组
printf("%d\n", parr[0][1]); // 输出2,访问arr1[1]
printf("%d\n", parr[1][2]); // 输出5,访问arr2[2]
char* strs[] = {"apple", "banana", "cherry"}; // 指针数组,元素指向不同字符串
printf("%s\n", strs[1]); // 输出"banana",直接通过指针数组元素访问字符串
printf("%c\n", *(strs[0] + 2)); // 输出"p",访问"apple"的第三个字符
特性 | 指针数组 | 二维数组 |
---|---|---|
内存布局 | 非连续 | (指针可能分散) |
访问效率 | 稍低(需两次寻址) | 稍高(一次计算地址) |
初始化方式 | 需分别初始化每行 | 可统一初始化 |
大小灵活性 | 每行长度可不同 | 所有行长度必须相同 |
main
函数的参数argv
就是一个指针数组:
示例:
int main(int argc, char *argv[]) {
// argc:参数个数
// argv:参数数组(每个元素是一个字符串指针)
for (int i = 0; i < argc; i++) {
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
调用示例:
输出:
argv[0]: ./program
argv[1]: hello
argv[2]: world
核心概念 | 关键点 | 风险点/注意事项 |
---|---|---|
指针本质 | 32位系统中指针是32位地址,固定占4字节,存储内存单元编号 | 避免解引用未初始化的指针 |
指针类型 | 决定解引用时访问的字节数和加减运算的步长 | 不同类型指针不能混用 |
野指针 | 未初始化、越界或指向已释放内存的指针 | 使用前检查指针有效性,避免返回局部变量地址 |
指针运算 | 加减整数、指针相减(元素个数)、关系运算 | 避免指针越界和无效地址运算 |
指针与数组 | 数组名隐式转换为首元素指针,可通过指针访问数组元素 | 数组作为参数会退化为指针,丢失大小信息 |
二级指针 | 存储一级指针的地址,用于修改实参指针 | 多级指针增加代码复杂度,慎用 |
指针数组 | 存储指针的数组,可模拟二维数组 | 与真正二维数组内存布局不同 |