C语言学习笔记20260615-指针与数组进阶
本笔记基于以下代码片段,分析指针、数组、结构体、多级指针等核心知识点,并总结常见陷阱。
一、32位小端环境
二、逐段分析与知识点讲解
1:数组名与取地址数组名
c
int a[5] = {1,2,3,4,5};
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a+1), *(ptr-1));
输出: 2,5
分析:
-
a 是数组首元素地址,a+1 指向第二个元素 → 2
-
&a 是整个数组的地址,&a+1 跳过整个数组(20字节),指向数组末尾
-
ptr 指向末尾后的 int,ptr-1 回退到最后一个元素 → 5
关键点:
-
&a+1 的步长是 sizeof(a),而 a+1 的步长是 sizeof(int)。
-
数组名在 & 作用下,类型变为"指向整个数组的指针。
2:结构体指针、整数、不同类型指针的加法
c
struct Test { int Num; char* pcName; short sDate; char cha[2]; short sBa[4]; } *p = 0x100000;
// 结构体大小20字节
printf("%p\n", p + 0x1); // 0x100014
printf("%p\n", (unsigned long)p + 0x1); // 0x100001
printf("%p\n", (unsigned int*)p + 0x1); // 0x100004
输出:
- 0x100014
- 0x100001
- 0x100004
分析:
-
p + 0x1:指针加法,移动 1 * sizeof(Test) = 20 字节 → 0x100000 + 20 = 0x100014
-
(unsigned long)p + 0x1:将地址转为整数,直接加1 → 0x100001
-
(unsigned int*)p + 0x1:转为 unsigned int* 类型,步长4字节 → 0x100000 + 4 = 0x100004
关键点:
-
指针加法依赖于指针指向的类型大小。
-
整数加法没有类型缩放。
-
不同类型的指针,步长不同。
3:指针与整数的相互转换 + 字节序
c
int a[4] = {1,2,3,4};
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)((int)a + 1);
printf("%x,%x", ptr1[-1], *ptr2);
输出(小端,int=4字节): 4,2000000
分析:
-
ptr1-1 = *(ptr1-1),指向最后一个元素 → 4
-
(int)a + 1:将数组地址转成整数,加1字节,再转回 int*
-
内存布局(小端):
a0=1 → 01 00 00 00
a1=2 → 02 00 00 00
地址 a+1 处读4字节:00 00 00 02 → 0x02000000 → 2000000
关键点:
-
跨字节边界读取 int 会得到"错位"的值,结果依赖字节序。
-
大端环境下,*ptr2 会是 0x01000000 或其他值,不可移植。
4:逗号表达式与数组初始化
c
int a[3][2] = { (0,1), (2,3), (4,5) };
int* p = a[0];
printf("%d", p[0]);
输出: 1
分析:
-
逗号表达式 (x,y) 的值是 y。
-
初始化列表实际为:{1, 3, 5},但二维数组每行需要2个元素,不足的补0。
-
最终数组内容:
{1, 0}
{3, 0}
{5, 0}
p0 = a00 = 1
关键点:
-
逗号表达式在初始化列表中常被误用,应避免。
-
数组初始化不足时,剩余元素自动初始化为0。
5:不同长度数组指针的减法
c
int a[5][5];
int(*p)[4] = a; // p指向长度为4的int数组
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
输出(32位): 0xFFFFFFFC,-4
分析:
-
p 指向 int4,p+1 移动 4*sizeof(int)=16 字节。
-
a 指向 int5,a+1 移动 5*sizeof(int)=20 字节。
-
&p42 与 &a42 的地址差:(416 + 2 4) - (420 + 24) = 64 - 80 = -16 字节。
-
指针相减得到元素个数差:-16 / sizeof(int) = -4。
-
%p 打印 -4 的补码表示(32位下 0xFFFFFFFC)。
关键点:
-
不同长度的数组指针赋值是合法的,但后续计算容易出错。
-
指针相减结果为 ptrdiff_t,单位是元素个数,不是字节数。
6:二维数组的取地址与数组名
c
int aa[2][5] = {1,2,3,4,5,6,7,8,9,10};
int* ptr1 = (int*)(&aa + 1);
int* ptr2 = (int*)(*(aa + 1));
printf("%d,%d", *(ptr1-1), *(ptr2-1));
输出: 10,5
分析:
-
&aa 是整个二维数组的地址,&aa+1 指向数组末尾(即 aa 后面)。
-
ptr1-1 指向最后一个元素 aa14 = 10。
-
*(aa+1) = aa1(第二行首地址),ptr2 指向 aa10 = 6。
-
ptr2-1 指向 aa04 = 5。
关键点:
-
&aa 类型为 int(*)25,步长是整个数组大小。
-
aa+1 类型为 int(*)5,步长是一行(5个int)。
7:指针数组与二级指针
c
char* a[] = {"work","at","alibaba"};
char** pa = a;
pa++;
printf("%s\n", *pa);
输出: at
分析:
-
a 是指针数组,每个元素指向一个字符串常量。
-
pa 指向 a0,pa++ 指向 a1。
-
*pa 取出 a1 中存储的地址,即字符串 "at" 的首地址。
关键点:
- 指针数组 + 二级指针 是实现字符串数组的常用方式。
8:三级指针的复杂运算
c
char* c[] = {"ENTER","NEW","POINT","FIRST"};
char** cp[] = {c+3, c+2, c+1, c};
char*** cpp = cp;
printf("%s\n", **++cpp); // POINT
printf("%s\n", *--*++cpp + 3);// ER
printf("%s\n", *cpp[-2] + 3); // ST
printf("%s\n", cpp[-1][-1] + 1);// EW
初始状态:
- c0="ENTER", c1="NEW", c2="POINT", c3="FIRST"
- cp0=c+3, cp1=c+2, cp2=c+1, cp3=c
- cpp = cp // 指向 cp0
① **++cpp
-
++cpp → 指向 cp1
-
*cpp → c+2
-
**cpp → *(c+2) = c2 = "POINT"
② --++cpp + 3
-
++cpp → 指向 cp2(此时 cpp 已指向 cp2)
-
*++cpp → c+1
-
--*++cpp → (c+1)-1 = c
-
--++cpp → c0 = "ENTER"
-
+3 → 指向第4个字符 'E' → "ER"
③ *cpp-2 + 3
-
cpp 仍指向 cp2
-
cpp-2 = *(cpp-2) = cp0 = c+3
-
*cpp-2 = c3 = "FIRST"
-
+3 → 指向第4个字符 'S' → "ST"
④ cpp-1-1 + 1
-
cpp-1 = *(cpp-1) = cp1 = c+2
-
cpp-1-1 = *(c+2 -1) = *(c+1) = c1 = "NEW"
-
+1 → 指向第二个字符 'E' → "EW"
关键点:
多级指针的 ++、-- 和 * 组合时,从右向左逐层分析。
\[\] 运算符等价于 * 和 + 的组合:pn = *(p+n)。
三、常见错误与避坑指南
1. &a+1 vs a+1
-
&a+1 跳过整个数组(常用于取数组末尾指针)
-
a+1 仅跳过一个元素
2.(int)a+1
- 依赖字节序,不可移植,应避免直接地址算术后再解引用
3.不同长度数组指针赋值
- 合法但容易引起计算错误,尽量保持类型一致
4.多级指针的 ++ 与 * 结合
- 从右向左理解:--++cpp 先运算 ++cpp,再 *,再 --,再 *
5.指针减法结果
- 类型为 ptrdiff_t,单位是元素个数,不是字节数