【2个月 C 语言从入门到精通:零基础系统教程】第十二讲:深入了解指针(五)
文章目录
前言
在 C 语言的学习旅途中,指针和数组犹如一对形影不离的“双胞胎”,既深刻又令人头疼。许多初学者在掌握了基础语法后,往往在指针运算、sizeof 与 strlen 的区别以及二维数组的地址偏移上卡壳,导致笔试和面试频频失利。
本文档正是为了解决这一痛点而编写。我将从最核心的底层逻辑出发,通过 7 个 sizeof/strlen 对比案例 和 7 道经典指针运算笔试题,带你一步步拆解内存模型、厘清数组名的“双重身份”以及多级指针的指向变换。
无论你是正在准备求职笔试,还是想要彻底攻克 C 语言指针的“硬骨头”,这份实战解析都能为你提供清晰、深入的指引。让我们从内存视角开始,重新认识指针与数组的真相。
1.sizeof和strlen的对比
1.1 核心概念速览
在分析代码前,先明确两个核心工具和一条数组与指针的“潜规则”。
1.1.1 sizeof 操作符
功能:计算操作数所占内存的字节数。
时期:编译时确定(除变长数组外)。
返回值:size_t(无符号整型),用 %zu 打印。
对象:可以是类型、变量、表达式。它关心的是“类型占多大内存”。
1.1.2 strlen 函数
功能:求字符串长度,即从首地址开始向后计数,直到遇见空字符 \0 停止,且不包含 \0。
时期:运行时扫描内存。
声明:size_t strlen(const char *str);
致命要求:传入的指针必须指向一块包含 \0 的有效字符数组。否则它会一直向后狂读,返回一个随机值,甚至导致程序崩溃。
1.1.3 数组名的“双重身份”
数组名在大多数表达式中会隐式转换为指向首元素的指针。但有两个例外:
sizeof(数组名):此时数组名表示整个数组,计算出数组总大小。
&数组名:取出的是整个数组的地址,类型是 指向数组的指针。
记住这句话:arr 和 &arr[0] 值相同但意义略有不同;&arr 的值也相同,但类型和步长完全不同。
1.2 核心代码讲解
我将用以下代码向大家展示这两者的不同
1.2.1 案例1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main()
{
char arr1[3] = { 'a', 'b', 'c' };//[a b c]
char arr2[] = "abc"; //[a b c \0]
printf("%d\n", strlen(arr1));//未知数
printf("%d\n", strlen(arr2));//3
printf("%d\n", sizeof(arr1));//3
printf("%d\n", sizeof(arr2));//4
return 0;
}

代码解析
1.2.2 案例2
int main()
{
int a[] = { 1,2,3,4 };//4*4 = 16
printf("%zu\n", sizeof(a));//16
printf("%zu\n", sizeof(a + 0));//4/8,a+0是首元素的地址
printf("%zu\n", sizeof(*a));//4, *a == a[0]
printf("%zu\n", sizeof(a + 1));//4/8, a + 1 --> &a[1]
printf("%zu\n", sizeof(a[1]));//4
printf("%zu\n", sizeof(&a));//4/8, &a是整个数组的地址,依然是地址
printf("%zu\n", sizeof(*&a));//16
//&a -- int (*)[4]
//*&a -- a
//sizeof(*&a) == sizeof(a)
printf("%zu\n", sizeof(&a + 1));//4/8 &a + 1还是地址
printf("%zu\n", sizeof(&a[0]));//4/8
printf("%zu\n", sizeof(&a[0] + 1));//4/8
return 0;
}

核心讲解
1.2.3 案例3
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%zu\n", sizeof(arr));//6
printf("%zu\n", sizeof(arr + 0));//4/8, arr + 0 == &arr[0]
printf("%zu\n", sizeof(*arr));//1, *arr == arr[0]
printf("%zu\n", sizeof(arr[1]));//1
printf("%zu\n", sizeof(&arr));//4/8 -- char(*)[6]
printf("%zu\n", sizeof(&arr + 1));//4/8-- char(*)[6]
printf("%zu\n", sizeof(&arr[0] + 1));//4/8
return 0;
}

1.2.4 案例4
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = { 'a','b','c','d','e','f' };
printf("%zu\n", strlen(arr));//未知数
printf("%zu\n", strlen(arr + 0));//未知数
//printf("%zu\n", strlen(*arr));//*arr == arr[0] == 'a' == 97, 程序会崩溃
//printf("%zu\n", strlen(arr[1]));//'b'-98,程序会崩溃
printf("%zu\n", strlen(&arr));//未知数 x
//char(*)[6]-> const char*
printf("%zu\n", strlen(&arr + 1));//未知数 x-6
printf("%zu\n", strlen(&arr[0] + 1));//未知数 x-1
return 0;
}


1.2.5 案例5
int main()
{
char arr[] = "abcdef";
printf("%zu\n", sizeof(arr));//7
printf("%zu\n", sizeof(arr + 0));//4/8
printf("%zu\n", sizeof(*arr));//1, arr[0] == *arr
printf("%zu\n", sizeof(arr[1]));//1
printf("%zu\n", sizeof(&arr));//4/8
printf("%zu\n", sizeof(&arr + 1));//4/8
printf("%zu\n", sizeof(&arr[0] + 1));//4/8
return 0;
}

1.2.6 案例6
int main()
{
char arr[] = "abcdef";
printf("%zu\n", strlen(arr));//6
printf("%zu\n", strlen(arr + 0));//6
//printf("%zu\n", strlen(*arr));//程序会崩溃
//printf("%zu\n", strlen(arr[1]));//程序会崩溃
printf("%zu\n", strlen(&arr));//6
printf("%zu\n", strlen(&arr + 1));//未知数
printf("%zu\n", strlen(&arr[0] + 1));//5
return 0;
}

代码解析


1.2.7 案例7
#include <stdio.h>
int main()
{
char* p = "abcdef";
printf("%zu\n", sizeof(p));//4/8
printf("%zu\n", sizeof(p + 1));//4/8, p+1是b的地址
printf("%zu\n", sizeof(*p));//1, *p == 'a'
printf("%zu\n", sizeof(p[0]));//1
//p[0] == *(p+0) == 'a'
printf("%zu\n", sizeof(&p));//4/8
printf("%zu\n", sizeof(&p + 1));//4/8
printf("%zu\n", sizeof(&p[0] + 1));//4/8, 'b'的地址
return 0;
}




1.2.8 案例8
#include <stdio.h>
#include <string.h>
int main()
{
char* p = "abcdef";
printf("%zu\n", strlen(p));//6
printf("%zu\n", strlen(p + 1));//5
printf("%zu\n", strlen(*p));//程序崩溃
printf("%zu\n", strlen(p[0]));//程序崩溃
printf("%zu\n", strlen(&p));//未知数
printf("%zu\n", strlen(&p + 1));//未知数
printf("%zu\n", strlen(&p[0] + 1));//5
return 0;
}


2. 数组和指针笔试题解析
2.1 案例1



3. 指针运算笔试题解析
3.1 案例1
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d", *(a + 1), *(ptr - 1));
return 0;


3.2 案例2
//在X86环境下
//假设结构体的大小是20个字节
//程序输出的结果是啥?
struct Test
{
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000;
int main()
{
printf("%p\n", p + 0x1);
printf("%p\n", (unsigned long)p + 0x1);
printf("%p\n", (unsigned int*)p + 0x1);
return 0;
}



3.3 案例3
int main()
{
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int* p;
p = a[0];
printf("%d", p[0]);
return 0;
}


3.4 案例4
//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4];
p = a;
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}


关键点: p 和 a 的类型是不匹配的!a 的每一行有 5 个元素,但 p 认为它指向的每一行只有 4 个元素。
3.5 案例5
#include <stdio.h>
int main()
{
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));
return 0;
}


3.6 案例6
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);
return 0;
}


3.7 案例7
int main()
{
char* c[] = { "ENTER","NEW","POINT","FIRST" };
char** cp[] = { c + 3,c + 2,c + 1,c };
char*** cpp = cp;
printf("%s\n", **++cpp);
printf("%s\n", *-- * ++cpp + 3);
printf("%s\n", *cpp[-2] + 3);
printf("%s\n", cpp[-1][-1] + 1);
return 0;




总结
通过本文的梳理,我们不仅回顾了 sizeof(编译时确定,侧重类型大小)与 strlen(运行时扫描,侧重字符长度)的根本差异,更透过一系列经典的笔试题,揭示了以下几个关键规律:
数组名的“变身”:在 sizeof 和 & 操作下,数组名代表整个数组;而在表达式中(如 a+1),它自动退化为指向首元素的指针。
指针加减的“步长”:指针算术的偏移量由它指向的数据类型大小决定。例如 struct Test* + 1 跳过整个结构体,而 unsigned int* + 1 只跳过 4 字节。
越界与类型不匹配的陷阱:
二维数组名和指向不同宽度的数组指针(如 int(*p)[4] 指向 int a[5][5])进行运算时,会产生意料之外的“提前”或“延后”偏移(例如 &p[4][2] - &a[4][2] 结果为 -4)。
strlen 要求参数必须是指向 \0 结尾的有效地址。传入字符本身(如 *arr)会导致程序崩溃。
多级指针的“追赶”游戏:在三级指针(如 char*** cpp)的解题中,跟踪 cpp 自身的移动是核心,而 ++ 和 – 操作符的顺序(前置/后置)会直接影响目标变量值的修改时机。
掌握这些底层规则,你就能在复杂的指针代码面前从容拆解,避免“看到指针就发怵”的困境。建议你将文中案例在编译器中亲手运行并调试,这将帮助你建立起更加坚实的 C 语言内存模型认知
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)