C语言学习笔记20260615-指针与数组进阶

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,单位是元素个数,不是字节数