c语言初阶 指针
指针
- C语言指针详解
- 1. 指针是什么
- (1)指针的本质
- (2)32位地址的产生
- (3)指针变量的大小
- 2. 指针和指针类型
- (1)指针类型的意义
- (2)指针加减运算
- 3. 野指针
- (1)野指针的成因
- (2)避免野指针的方法
- ① 初始化指针
- ② 避免返回局部变量地址
- ③ 检查指针有效性
- ④ 避免指针越界
- 4. 指针运算
- (1)指针加减整数
- (2)指针相减
- (3)指针的关系运算
- 5. 指针和数组
- (1)数组名与指针的关系
- (2)通过指针遍历数组
- (3)数组作为函数参数
- 6. 二级指针
- (1)二级指针的概念
- (2)二级指针的用途
- (3)多级指针
- 7. 指针数组
- (1)指针数组的概念
- (2)指针数组与二维数组
- (3)指针数组作为命令行参数
- 8. 一句话总结
C语言指针详解
1. 指针是什么
(1)指针的本质
- 从本质来讲,指针是内存中最小存储单元的编号,也就是我们常说的地址。内存被划分成一个个大小为1字节的最小单元,每个单元都有唯一的编号,这个编号就是指针。而平时口语里提到的“指针”,更多时候指的是指针变量,它是专门用来存放内存地址的变量。
示例:
int a = 10; // 在内存中分配4字节存储整数10
int *p = &a; // 定义指针变量p,存储变量a的地址
关键特性:
- 32 位系统中,内存地址由 32 位二进制数表示(范围:0x0000 0000 ~ 0xFFFF FFFF)
- 每个内存地址对应 1 个字节(8 位)的存储空间
- 指针变量本身固定占 4 字节(32 位),无论指向何种数据类型
(2)32位地址的产生
在32位系统中,CPU通过32根地址线生成内存地址:
- 每根地址线可表示高电平(1)或低电平(0)
- 32根地址线组合可表示2³²个不同地址(4GB寻址空间)
地址生成过程:
地址线状态 对应的内存地址
0000…0000 (32 位) → 0x00000000 (最小地址)
0000…0001 → 0x00000001
0000…0010 → 0x00000002
…
1111…1111 → 0xFFFFFFFF (最大地址)
内存单元组织:
- 每个地址对应1字节(8位)存储空间
- 连续的地址对应连续的内存单元
(3)指针变量的大小
当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
注意:
- 指针变量的大小与所指数据类型无关,仅取决于系统位数(32 位 / 64 位)
- 指针类型决定解引用时的访问权限,而非指针本身大小
2. 指针和指针类型
- 在32位操作系统环境下,无论指针的类型是int*、char*、float还是double,指针变量本身占用的内存空间都是4个字节。这是由32位地址的存储需求决定的,所有类型的地址长度相同,因此存储地址的指针变量大小也相同。
- 指针类型的意义主要体现在两个方面:
- 决定指针解引用时访问的字节数:不同类型的指针,在解引用时会根据自身类型访问特定数量的字节。
(1)指针类型的意义
指针类型决定了:
- 解引用时访问的字节数
- 指针加减运算时跳过的字节数
不同类型指针的访问权限:
- char*类型指针:解引用时访问1个字节,刚好能覆盖1个char类型数据的存储空间。
char ch = 'B';char* pch = &ch;*pch = 'C'; // 解引用char*指针,只访问ch所占的1个字节,将其值改为'C'
- short * 类型指针:解引用时访问 2 个字节,对应 short 类型数据的存储空间。
short s = 200;
short* ps = &s;
*ps = 300; // 解引用short*指针,访问s所占的2个字节,修改其值为300
- int*、float * 类型指针:解引用时都访问 4 个字节,分别对应 int 和 float 类型数据的存储空间。
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 * 类型指针:解引用时访问 8 个字节,对应 double 类型数据的存储空间。
double d = 123.456;
double* pd = &d;
*pd = 789.012; // 解引用double*指针,访问d所占的8个字节,修改值为789.012
(2)指针加减运算
指针加减整数时,跳过的字节数由指针类型决定:
示例:
决定指针加减整数时跳过的字节数:指针进行加减整数运算时,跳过的字节数由指针类型决定。
- char * 类型指针 + 1:跳过 1 个字节,指向相邻的下一个 char 类型数据。
char str[5] = "abcde";
char* pstr = str;
pstr++; // 跳过1个字节,从指向str[0]变为指向str[1]
- short * 类型指针 + 1:跳过 2 个字节,指向相邻的下一个 short 类型数据。
short arr_s[3] = {10, 20, 30};
short* ps = arr_s;
ps++; // 跳过2个字节,从指向arr_s[0]变为指向arr_s[1]
- int*、float * 类型指针 + 1:跳过 4 个字节,指向相邻的下一个对应类型数据。
i
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 * 类型指针 + 1:跳过 8 个字节,指向相邻的下一个 double 类型数据。
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。
3. 野指针
- 野指针指的是指针指向的内存位置是不确定的、随机的或者是不被允许访问的,这种指针在解引用时会导致程序出现不可预测的错误。
(1)野指针的成因
野指针指向的位置是随机、不可知的,常见于以下情况:
① 未初始化的指针:
- 指针未初始化:当定义一个指针变量时,如果没有给它赋予明确的地址值,那么它内部会存储一个随机的垃圾值,这个值可能指向内存中的任意位置,此时的指针就是野指针。对未初始化的野指针进行解引用操作,会非法访问未知的内存区域,可能导致程序崩溃。
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 为野指针,解引用操作是非法的。
(2)避免野指针的方法
① 初始化指针
- 对指针进行初始化:在定义指针变量时,如果暂时不知道它要指向哪个变量的地址,可以将其初始化为NULL。NULL是一个宏定义,通常代表地址0(一个不允许访问的内存位置)。需要注意的是,指针被赋值为NULL后不能进行解引用操作,必须在指针有明确的、合法的指向后,才能对其解引用。
int *p = NULL; // 初始化为空指针
int a = 10;
p = &a; // 明确指向后再解引用
② 避免返回局部变量地址
- 避免返回局部变量的地址:在函数中,不要将局部变量的地址作为返回值。因为函数执行结束后,局部变量的内存空间会被释放,其地址变得无效。如果需要通过函数返回指针,可以返回静态变量、全局变量的地址,或者通过动态内存分配(如 malloc)得到的地址,这些地址在函数结束后依然有效。
int* func() {static int a = 10; // 使用静态变量(生命周期为整个程序)return &a; // 安全返回静态变量地址
}
③ 检查指针有效性
- 在使用指针前检查其有效性:在对指针进行解引用操作之前,先判断指针是否为 NULL 或者是否指向合法的内存空间,确保指针有效后再使用。
int *p = get_pointer(); // 假设这是一个可能返回NULL的函数
if (p != NULL) { // 使用前检查*p = 100;
}
④ 避免指针越界
- 小心指针越界:在使用指针访问数组等连续存储的元素时,要严格控制访问范围,确保指针不会超出数组的合法下标范围。在循环访问数组时,可以通过数组的长度来限制循环次数,避免越界。
int arr[5];
for (int i = 0; i < 5; i++) { // 严格控制循环边界arr[i] = i;
}
4. 指针运算
(1)指针加减整数
- 指针进行加减整数运算时,其结果是指针指向的位置发生偏移。需要注意的是,指针偏移后可能会越界(即指向数组以外的位置),指针越界本身不会导致程序立即出错,但如果对越界的指针进行解引用操作,访问了不属于当前数据的内存空间,就会引发错误。
示例:
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)
(2)指针相减
- 指针 - 指针:当两个指针指向同一块连续的内存空间(例如同一个数组)时,两个指针相减的结果是一个整数,代表两个指针之间相隔的元素个数。需要注意的是,两个指针相加是没有意义的,因为地址相加得到的结果无法确定指向哪个有效的内存位置。
示例:
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); // 未定义行为!
(3)指针的关系运算
指针的关系运算:指针的关系运算指的是比较两个指针的大小,通常用于判断指针指向的位置前后关系。
示例:
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) { // 未定义行为!// ...
}
5. 指针和数组
(1)数组名与指针的关系
- 数组名在大多数情况下会被隐式转换为指向数组第一个元素的指针(即数组首地址)。因此,可以通过指针来访问数组元素。例如,对于
int arr[10] = {0}; int* p = arr;
,这里的arr被转换为指向arr[0]的指针,所以p指向arr[0]。 - 由于p和arr都指向数组的首元素,因此
*(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
(2)通过指针遍历数组
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++); // 先解引用,再自增指针
}
(3)数组作为函数参数
数组作为参数传递时会退化为指针:
示例:
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位系统指针大小)
}
6. 二级指针
(1)二级指针的概念
- 二级指针是指向指针的指针,它存储的是一个指针变量的地址。如果把一级指针比作指向数据的“路标”,那么二级指针就是指向“路标”的“路标”。
- 从定义上看,二级指针的第一个
*
表明它是一个指针,第二个*
表明它所指向的类型是一个指针。例如,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(两次解引用二级指针)
(2)二级指针的用途
- 二级指针的主要用途是对一级指针进行间接修改,例如在函数中通过二级指针修改外部一级指针的指向。
示例:
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;
}
(3)多级指针
理论上可以有三级、四级指针,但实际很少超过二级:
示例:
int a = 10;
int *p = &a;
int **pp = &p;
int ***ppp = &pp;printf("%d\n", ***ppp); // 输出:10
7. 指针数组
(1)指针数组的概念
- 指针数组是一种特殊的数组,数组中的每个元素都是一个指针。它的定义形式为:
类型* 数组名[数组长度]
,例如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
(2)指针数组与二维数组
- 指针数组可以用来模拟二维数组,此时指针数组的每个元素分别指向二维数组的每一行(即每一行的首地址)。通过指针数组访问二维数组元素时,先通过数组元素得到行地址,再解引用访问具体元素。
示例:
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"的第三个字符
- 上述代码中,strs 是一个 char * 类型的指针数组,每个元素分别指向字符串 “apple”、“banana”、“cherry” 的首地址,通过指针数组可以方便地访问和操作这些字符串。
特性 | 指针数组 | 二维数组 |
---|---|---|
内存布局 | 非连续 | (指针可能分散) |
访问效率 | 稍低(需两次寻址) | 稍高(一次计算地址) |
初始化方式 | 需分别初始化每行 | 可统一初始化 |
大小灵活性 | 每行长度可不同 | 所有行长度必须相同 |
(3)指针数组作为命令行参数
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
8. 一句话总结
核心概念 | 关键点 | 风险点/注意事项 |
---|---|---|
指针本质 | 32位系统中指针是32位地址,固定占4字节,存储内存单元编号 | 避免解引用未初始化的指针 |
指针类型 | 决定解引用时访问的字节数和加减运算的步长 | 不同类型指针不能混用 |
野指针 | 未初始化、越界或指向已释放内存的指针 | 使用前检查指针有效性,避免返回局部变量地址 |
指针运算 | 加减整数、指针相减(元素个数)、关系运算 | 避免指针越界和无效地址运算 |
指针与数组 | 数组名隐式转换为首元素指针,可通过指针访问数组元素 | 数组作为参数会退化为指针,丢失大小信息 |
二级指针 | 存储一级指针的地址,用于修改实参指针 | 多级指针增加代码复杂度,慎用 |
指针数组 | 存储指针的数组,可模拟二维数组 | 与真正二维数组内存布局不同 |