
一、指针int a 1; int *p a; printf(%d,p); printf(\n); printf(%d,*p);指针就是一个自定义的变量例如p利用符号取得了一个变量的地址并且把它存储到了另一个指针变量里面让我们可以使用*对存储了地址的变量进行解引用。二、指针本身在内存中怎么存无论是什么类型的指针int*、char*、double*、结构体指针等指针变量里保存的就是一个内存地址编号本质上是一个无符号整数在虚拟地址空间中。int a 10; int *p a;假设a的地址是0x1000那么变量p里存的值就是0x1000。在内存中p自己也会占一块空间这块空间里放的正是这个地址值。类型的作用不在于“怎么存地址”而在于当你用*p去读取时编译器知道要从0x1000开始读几个字节、怎么解释这些字节。int *→ 读 4 字节按补码整数解释。char *→ 读 1 字节按字符解释。double *→ 读 8 字节按浮点数格式解释。结论不同指针在内存中的存储方式完全一样都是存一个地址。区别只在“解引用时的访问宽度和解释方式”这个信息存在编译器的类型系统里而不是存在指针变量本身。三、指针变量自身的大小指针变量本身也要占空间存放那个地址值。大小只和平台有关与指针的类型无关。32位系统指针大小 4 字节32 位地址64位系统指针大小 8 字节64 位地址简单了解不同位数系统区别跳转被本小节目录尾部在 64 位 Linux 上输出8 8 8在 32 位上是4 4 4。即使是指向非常大的结构体的指针大小也还是 8 字节64位下。函数指针大多数实现下也和数据指针一样大但这不绝对C 标准只保证void*与char*等数据指针能互转不保证函数指针大小相同。32位系统和64位系统的区别64位系统理论上可支持16 EB内存实际常见系统可轻松使用数百 GB 甚至 TB 级内存。软件兼容64位系统通常可以运行64位和32位程序通过兼容层32位系统只能运行32位程序无法直接运行64位软件。运算性能64位处理器一次能处理更长的数据在科学计算、大型数据库、视频处理等需要大量内存和复杂计算的场景下速度更快日常轻量使用时差异不明显。简单说要突破4 GB内存并发挥新硬件的全部性能就需要64位系统。当前主流操作系统和CPU都已全面转向64位。四、指针的算术运算加一到底加多少指针加减整数时实际地址的偏移量 整数 × 指针所指类型的大小。普通指针int arr[5] {10,20,30,40,50}; int *p arr; // 指向 arr[0]假设地址为 0x1000 p 1; // 地址 0x1000 sizeof(int)*1 0x1004若int为4字节p i等价于arr[i]*(p i)等价于arr[i]指针相减int *p1 arr[4]; int *p2 arr[1]; ptrdiff_t diff p1 - p2; // 结果为 3即相差3个int元素前提两个指针必须指向同一数组或最后一个元素的后面一个位置否则行为未定义。void* 的情况标准 C 不允许对void*做算术运算因为不知道步长。GCC 扩展允许void*按字节加减但不建议依赖。指向数组的指针数组指针的算术int arr[5]; int (*pa)[5] arr; // pa 是指向“含有5个int的数组”的指针 pa 1; // 地址增加 sizeof(int[5]) 20 字节若int4此时pa的步长是整个数组的大小。五、数组指针的遍历厘清两种常见情况这里要严格区分两个东西指向数组首元素的指针int *p arr;最常见指向整个数组的指针int (*pa)[N] arr;数组指针情况A用首元素指针遍历一维数组int arr[5] {1,2,3,4,5}; int *p; for(p arr; p arr 5; p) printf(%d , *p);或者for(int i 0; i 5; i) printf(%d , *(arr i)); // arr 退化为首元素指针情况B用数组指针访问一维数组不是遍历首元素指针int arr[5] {1,2,3,4,5}; int (*pa)[5] arr; for(int i 0; i 5; i) printf(%d , (*pa)[i]); // *pa 是数组本身再下标访问 // 或者 printf(%d , *(*pa i));因为*pa得到的是数组arr(*pa)[i]就是arr[i]。注意pa 1会跳过整个数组一般不用于遍历单个元素。情况C用数组指针遍历二维数组的行int a[3][4] {{1,2,3,4},{5,6,7,8},{9,10,11,12}}; int (*p)[4] a; // a退化为指向首行一维数组的指针类型为 int(*)[4] for(int i 0; i 3; i) { for(int j 0; j 4; j) printf(%d , p[i][j]); // 也可写成 *(*(pi)j) printf(\n); }p 1步长 sizeof(int[4]) 16 字节正好跳一行。核心区别int *p arr;步长是一个元素int (*pa)[N] arr;步长是整个数组N个元素。遍历数组元素时绝大多数场景用首元素指针。六、字符串存储进数组1.内存布局C 字符串本质是以空字符\0ASCII 码 0结尾的连续字符序列。char str[6] hello;这个数组在内存中的样子假设起始地址0x2000地址: 0x2000 0x2001 0x2002 0x2003 0x2004 0x2005 内容: h e l l o \0数组大小 6 字节正好放下 5 个可见字符 1 个结束符。如果不指定数组大小而用char str[] hello;编译器自动计算为 6。如果数组给大了如char str[20] hello;则索引 5 处放\0后面的字节不会被printf(%s)或字符串函数关心但依然属于数组。2.遍历字符串 —— 多种写法背后的原理字符串数组名str在表达式中退化为char*指向首字符h。因为char占 1 字节char*的算术运算步长就是1str 1地址真的只加 1。2.1 下标遍历最直观for (int i 0; str[i] ! \0; i) { printf(%c, str[i]); }等价于*(str i) ! \0因为str[i]就是*(str i)。2.2 指针遍历直接操作地址char *p str; while (*p ! \0) { printf(%c, *p); p; // 地址每次加1 }也可以写成for (char *p str; *p; p) // \0的值为0所以 *p 为0时停止 printf(%c, *p);2.3 打印整个字符串printf(%s, str); // 从str开始逐个输出字符直到遇到\03. 地址加减的具体效果我们用代码打印地址验证假设 64 位环境指针大小 8 字节char str[] hello; char *p str; // 设 p 值为 0x7ffe12345600 printf(p %p\n, p); // 0x7ffe12345600 printf(p1 %p\n, p 1); // 0x7ffe12345601 (地址增加 1) printf(p5 %p\n, p 5); // 0x7ffe12345605 (指向 o 后面的 \0)因为char大小为 1所以p 1指向下一个字符。p (strlen(p))恰好指向末尾的\0。对比int*步长int arr[] {1, 2, 3}; int *q arr; // 设 q 为 0x1000 // q 1 0x1004 加了 sizeof(int) 4char*的加减才是真正的“地址直接加1”最贴近内存字节寻址。4. 字符串数组二维—— 存储多行字符串如果存多个字符串通常用二维字符数组char fruits[3][10] {apple, banana, cherry};内存布局每行 10 字节未填满的补\0fruits[0]: a p p l e \0 \0 \0 \0 \0 fruits[1]: b a n a n a \0 \0 \0 \0 fruits[2]: c h e r r y \0 \0 \0 \0fruits退化为char (*)[10]即指向含有 10 个 char 的数组的指针。fruits 1地址加 10 字节跳到第二行。遍历某一行fruits[i]退化为char*可用%s打印该行。for (int i 0; i 3; i) { printf(%s\n, fruits[i]); // fruits[i] 就是第i个字符串的首地址 }也可以直接指针算术char (*pRow)[10] fruits; for (int i 0; i 3; i) { printf(%s\n, *(pRow i)); }补充数组退化C 语言中数组名几乎在任何表达式中使用时都会自动转换成指向它第一个元素的指针。1. 为什么会“退化”—— C 语言的基因语言设计之初考虑的是效率和灵活性。如果每次把数组当作整体来传参或运算需要拷贝整个数组代价太高。因此设计者决定这个转换发生在编译期是类型的转换不改变内存里的任何数据仅仅是“如何看待”这个数组名。那些不退化的例外情况是作为sizeof的操作数sizeof(a)求的是整个数组的字节大小。作为的操作数a得到的是指向整个数组的指针。用于初始化字符数组的字符串字面量这个你目前可暂时忽略。在其他任何表达式里数组名 指向首元素的指针。2. 对于int a[3][4]“首元素”是什么a的类型是int[3][4]即“包含 3 个元素的数组每个元素又是一个包含 4 个 int 的数组”。所以a的“元素”是a[0]、a[1]、a[2]。每个元素的类型是int[4]。首元素就是a[0]它的类型是int[4]。因此a退化后得到的指针是指向a[0]的指针。因为a[0]是int[4]指向它的指针类型就是int (*)[4]指向含有 4 个 int 的数组的指针。int a[3][4]; int (*p)[4] a; // a 退化为 a[0] int (*p)[4] a[0]; // 两者完全一样3. 退化后下标运算如何工作a[i][j] → (*(a i))[j] → *(*(a i) j)一步一步看a退化为指向a[0]的指针类型int(*)[4]。a i指针算术按int[4]的尺寸移动得到指向a[i]的指针。*(a i)解引用得到a[i]这个一维数组本身类型int[4]。但a[i]作为数组名也会立即退化注意为指向a[i][0]的指针类型int*。之后*(a i) j再移动 j 个 int最后外层*取出a[i][j]。所以这里发生了两次退化a退化为int(*)[4]行指针*(ai)即a[i]再退化为int*元素指针图示内存和指针a[0] a[1] a[2] [ 1, 2, 3, 4 ] [ 5, 6, 7, 8 ] [ 9,10,11,12 ] ↑ ↑ ↑ | | | a (退化后) a1 a2 类型 int(*)[4] int(*)[4] int(*)[4]