指针(5)
目录
1. sizeof 与 strlen 核心对比(底层逻辑 + 超详示例)
1.1 sizeof:编译期的 “内存尺子”(不关心内容,只看类型)
1.2 strlen:运行期的 “字符串计数器”(依赖 \0,只认地址)
2. 数组名的 “特殊身份”—— 两个关键例外(解题核心!)
场景 2:无 \0 的字符数组(strlen,崩溃 + 随机值)
5. 二维数组笔试题(行指针 vs 元素指针 + 越界不崩溃原因)
6. 经典指针笔试题(6 道题 + 内存布局图解 + 分步拆解)
题目 4:指针 - 指针的元素个数计算(补码 + 地址打印)
✨引言:
C 语言中sizeof、strlen与数组指针的组合题是新手的 “高频踩坑点”,也是面试必考点。这份笔记将逐行拆解每道题的底层逻辑,结合 “内存布局图解 + 类型分析 + 易错点标注”,把每个知识点讲透,让你不仅知其然,更知其所以然!
1. sizeof 与 strlen 核心对比(底层逻辑 + 超详示例)
1.1 sizeof:编译期的 “内存尺子”(不关心内容,只看类型)
- 本质:单目操作符(绝对不是函数!),在编译阶段就完成计算,无需运行程序。
- 核心规则:
- 计算变量 / 数据类型占用的内存大小(单位:字节),与内存中实际存储的内容无关;
- 括号内若有表达式,表达式不执行(编译期仅判断变量类型,不处理运行期逻辑);
- 语法灵活:
sizeof(变量名)可省略括号(如sizeof a),但sizeof(类型)必须加括号(如sizeof(int))。
示例 1:证明 sizeof 不是函数 + 基本用法
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void test() { printf("test\n"); }
int main() {
int a = 0;
test(a); // 函数调用必须加括号,test a; 语法错误(函数调用需传参格式)
printf("%zd\n", sizeof(a)); // 输出4 → int类型占4字节(32/64位平台一致)
printf("%zd\n", sizeof a); // 输出4 → 变量名可省括号,证明sizeof是操作符(函数必须加括号)
printf("%zd\n", sizeof(int)); // 输出4 → 直接计算类型大小,必须加括号
return 0;
}
示例 2:表达式不执行的底层原因(关键!)
int main() {
int a = 0;
short s = 4;
printf("%d\n", sizeof(s = a + 2)); // 输出2 → 重点:表达式s=a+2未执行!
printf("%d\n", s); // 输出4 → s的原值未被修改
// 底层逻辑拆解:
// 1. C语言执行流程:源文件(.c)→ 编译(语法分析+生成汇编)→ 链接 → 可执行文件(.exe)→ 运行
// 2. sizeof在「编译阶段」工作:仅判断变量s的类型是short(占2字节),直接返回2,不处理表达式;
// 3. 表达式s=a+2在「运行阶段」才会执行,但此时sizeof已完成计算,所以s仍为初始值4。
return 0;
}
1.2 strlen:运行期的 “字符串计数器”(依赖 \0,只认地址)
- 本质:C 标准库函数(需包含头文件
string.h),仅在程序运行时计算。 - 核心规则:
- 功能:统计字符串中
\0(字符串结束符)之前的字符个数,不包含\0; - 依赖条件:必须以
\0结尾,否则会越界访问栈区随机数据,结果为随机值; - 参数要求:必须传入合法的字符串首地址,若传入非地址(如字符
'a'),会触发野指针崩溃。
- 功能:统计字符串中
示例 1:正常情况(有 \0)
#include <string.h>
int main() {
int len1 = strlen("abcdef"); // "abcdef"在内存中存储为:a b c d e f \0(隐含\0)
int len2 = strlen("abc\0def"); // \0直接截断字符串,后续字符不统计
printf("%zd\n", len1); // 输出6 → 统计\0前6个字符
printf("%zd\n", len2); // 输出3 → 统计到\0停止
return 0;
}
示例 2:异常情况 1(无 \0,随机值)
int main() {
char arr[] = {'a','b','c'}; // 数组内存布局:a b c(无\0)
printf("%zd\n", strlen(arr)); // 输出随机值(如42)
// 解析:
// strlen从arr[0]开始向后遍历,直到找到\0才停止,但数组中没有\0,会继续访问栈区后续的随机数据,
// 直到遇到内存中某个位置的\0为止,所以结果不确定。
return 0;
}
示例 3:异常情况 2(传非地址,程序崩溃)
int main() {
char arr[] = {'a','b','c'};
// printf("%zd\n", strlen(*arr)); // 程序崩溃!编译无错,运行时触发访问冲突
// 解析:
// 1. *arr是数组首元素'a',ASCII码值为97;
// 2. strlen要求传入地址,此时相当于把97作为内存地址传给strlen;
// 3. 97对应的十六进制地址是0x00000061,属于系统保护的非法地址,访问时直接崩溃(提示“读取位置0x00000061时发生访问冲突”)。
return 0;
}
1.3 核心差异表(必记!)
| 特性 | sizeof | strlen |
|---|---|---|
| 本质 | 单目操作符(编译期计算) | 库函数(运行期计算) |
| 计算依据 | 变量 / 类型的内存大小(仅看类型) | 字符串中 \0 前的字符数(仅看内容) |
| 依赖条件 | 无(不依赖 \0 或具体内容) | 必须依赖 \0 结束符 |
| 参数类型 | 变量、数据类型(如 int、char []) | 仅接受字符串首地址(const char*) |
| 易错点 | 括号内表达式不执行 | 1. 无 \0 返回随机值;2. 传非地址程序崩溃 |
| 返回值类型 | size_t(无符号整数,用 % zd 格式化) | size_t(无符号整数,用 % zd 格式化) |
2. 数组名的 “特殊身份”—— 两个关键例外(解题核心!)
数组名的默认本质是 “数组首元素的地址”,但有两个特殊场景,数组名会代表 “整个数组”,这是所有数组指针题的 “解题钥匙”:
核心规则:
- 场景 1:
sizeof(数组名)→ 数组名代表整个数组,计算的是整个数组的内存大小(单位:字节); - 场景 2:
&数组名→ 数组名代表整个数组,取出的是整个数组的地址(不是首元素地址,类型为 “数组指针”); - 除此之外,所有数组名的出现(如
a+1、数组名作为函数参数),都等价于 “首元素地址”。
示例:直观理解数组名的不同身份
int a[] = {1,2,3,4}; // 数组总大小16字节(4×4)
// 1. sizeof(a) → 16字节(数组名代表整个数组)
// 2. &a → 类型为int(*)[4](数组指针),指向整个数组,步长为16字节(跳过整个数组)
// 3. a → 等价于&a[0](首元素地址),类型为int*,步长为4字节(跳过一个int)
// 4. a+1 → 等价于&a[1](第二个元素地址),类型为int*,步长4字节
比喻:数组名就像 “快递盒标签”—— 平时只写单个物品的位置(首元素地址),但遇到
sizeof(数组名)时,会显示整个盒子的体积(数组总大小);遇到&数组名时,会显示整个盒子的存放地址(数组地址)。
3. 一维数组笔试题(逐题拆解 + 类型分析)
int main() {
int a[] = {1,2,3,4}; // 数组内存布局:[1][2][3][4](4元素×4字节=16字节)
// 题1:sizeof(a)
printf("%zd\n", sizeof(a)); // 输出16
// 解析:
// 数组名a单独放在sizeof内部,触发“例外场景1”,a代表整个数组,大小=4元素×4字节=16字节。
// 题2:sizeof(a + 0)
printf("%zd\n", sizeof(a + 0)); // 输出4/8(32位平台4字节,64位平台8字节)
// 解析:
// a是首元素地址(类型int*),a+0仍为为首元素地址(未触发任何例外),
// 地址的大小仅与平台有关(32位系统指针占4字节,64位占8字节),与指向的类型无关。
// 题3:sizeof(*a)
printf("%zd\n", sizeof(*a)); // 输出4
// 解析:
// a是首元素地址(类型int*),*a是对首元素地址的解引用,得到首元素(值1,类型int),
// int类型占4字节,所以结果为4。(等价于sizeof(a[0]))
// 题4:sizeof(a + 1)
printf("%zd\n", sizeof(a + 1)); // 输出4/8
// 解析:
// a是首元素地址(int*),a+1跳过一个int(4字节),指向第二个元素(值2),
// 本质还是地址(int*类型),指针大小为4/8字节。
// 题5:sizeof(a[1])
printf("%zd\n", sizeof(a[1])); // 输出4
// 解析:
// a[1]等价于*(a+1),是数组的第二个元素(类型int),int占4字节,所以结果为4。
// 题6:sizeof(&a)
printf("%zd\n", sizeof(&a)); // 输出4/8
// 解析:
// &a触发“例外场景2”,a代表整个数组,取出的是“数组地址”(类型int(*)[4],数组指针),
// 但“数组地址”本质还是地址,指针大小仅与平台有关(4/8字节),与指针类型无关。
// 补充:指针类型仅决定“指针±整数时的步长”,不影响指针本身的内存大小。
// 题7:sizeof(*&a)
printf("%zd\n", sizeof(*&a)); // 输出16
// 解析(两种理解方式,结果一致):
// 方式1:&a是数组地址(int(*)[4]),*&a是对数组地址的解引用,访问的是整个数组,大小=16字节;
// 方式2:&和*是互逆操作,*&a等价于a,所以sizeof(*&a)=sizeof(a)=16字节。
// 题8:sizeof(&a + 1)
printf("%zd\n", sizeof(&a + 1)); // 输出4/8
// 解析:
// &a是数组地址(int(*)[4]),数组指针的步长=数组总大小(16字节),
// &a+1跳过整个数组,指向数组后面的空闲内存,本质还是地址(指针),大小为4/8字节。
// 题9:sizeof(&a[0])
printf("%zd\n", sizeof(&a[0])); // 输出4/8
// 解析:
// &a[0]是数组首元素的地址(类型int*),指针大小为4/8字节,与题2本质一致。
// 题10:sizeof(&a[0] + 1)
printf("%zd\n", sizeof(&a[0] + 1)); // 输出4/8
// 解析:
// &a[0]是首元素地址(int*),+1跳过一个int(4字节),指向第二个元素的地址,
// 本质是地址(指针),大小为4/8字节。
return 0;
}
易错点总结:
- 指针的内存大小仅与平台有关(32 位 4 字节,64 位 8 字节),与指针类型(int*、char*、数组指针)无关;
- 区分 “指针本身的大小” 和 “指针指向内容的大小”:
sizeof(指针)是指针本身大小,sizeof(*指针)是指向内容的大小。
4. 字符数组笔试题(6 种场景 + 崩溃原因深度解析)
字符数组的核心坑点:sizeof不依赖\0,strlen依赖\0;数组名 vs 字符指针的区别。
场景 1:无 \0 的字符数组(sizeof)
int main() {
char arr[] = {'a','b','c','d','e','f'}; // 内存布局:a b c d e f(无\0,6字节)
printf("%zd\n", sizeof(arr)); // 输出6
// 解析:sizeof(数组名),arr代表整个数组,大小=6元素×1字节=6字节(char占1字节)。
printf("%zd\n", sizeof(arr + 0)); // 输出4/8
// 解析:arr是首元素地址(char*),arr+0仍为地址,指针大小4/8字节。
printf("%zd\n", sizeof(*arr)); // 输出1
// 解析:*arr是首元素'a'(char类型),char占1字节。
printf("%zd\n", sizeof(arr[1])); // 输出1
// 解析:arr[1]是第二个元素'b'(char类型),占1字节。
printf("%zd\n", sizeof(&arr)); // 输出4/8
// 解析:&arr是数组地址(类型char(*)[6],数组指针),本质是地址,大小4/8字节。
printf("%zd\n", sizeof(&arr + 1)); // 输出4/8
// 解析:&arr+1跳过整个数组(6字节),指向数组后的空闲内存,仍是地址,大小4/8字节。
printf("%zd\n", sizeof(&arr[0] + 1));// 输出4/8
// 解析:&arr[0]是首元素地址(char*),+1指向第二个元素地址,指针大小4/8字节。
return 0;
}
场景 2:无 \0 的字符数组(strlen,崩溃 + 随机值)
#include <string.h>
int main() {
char arr[] = {'a','b','c','d','e','f'}; // 无\0
printf("%zd\n", strlen(arr)); // 输出随机值x
// 解析:arr是首元素地址,无\0,strlen越界访问栈区随机数据,直到找到\0,结果随机。
printf("%zd\n", strlen(arr + 0)); // 输出随机值x
// 解析:arr+0仍是首元素地址,无\0,结果同上。
// printf("%zd\n", strlen(*arr)); // 程序崩溃!
// 解析:*arr='a'(ASCII97),作为地址传入strlen,访问0x00000061非法地址。
// printf("%zd\n", strlen(arr[1]));// 程序崩溃!
// 解析:arr[1]='b'(ASCII98),作为地址传入strlen,访问0x00000062非法地址。
printf("%zd\n", strlen(&arr)); // 输出随机值x
// 解析:&arr是数组地址(char(*)[6]),传参时隐式转为char*(首元素地址),无\0,结果随机。
printf("%zd\n", strlen(&arr + 1)); // 输出随机值x-6
// 解析:&arr+1跳过整个数组(6字节),从数组后的地址开始找\0,比x少6个字符(数组长度)。
printf("%zd\n", strlen(&arr[0] + 1));// 输出随机值x-1
// 解析:&arr[0]+1是第二个元素地址,从'b'开始找\0,比x少1个字符。
return 0;
}
场景 3:带 \0 的字符数组(sizeof)
int main() {
char arr[] = "abcdef"; // 内存布局:a b c d e f \0(7字节,隐含\0)
printf("%zd\n", sizeof(arr)); // 输出7
// 解析:sizeof(数组名),arr代表整个数组,包含\0,大小=7×1=7字节。
printf("%zd\n", sizeof(arr + 0)); // 输出4/8
// 解析:arr是首元素地址(char*),arr+0仍是地址,指针大小4/8字节。
printf("%zd\n", sizeof(*arr)); // 输出1
// 解析:*arr是首元素'a'(char类型),占1字节。
printf("%zd\n", sizeof(arr[1])); // 输出1
// 解析:arr[1]是第二个元素'b'(char类型),占1字节。
printf("%zd\n", sizeof(&arr)); // 输出4/8
// 解析:&arr是数组地址(char(*)[7]),本质是地址,大小4/8字节。
printf("%zd\n", sizeof(&arr + 1)); // 输出4/8
// 解析:&arr+1跳过整个数组(7字节),仍是地址,大小4/8字节。
printf("%zd\n", sizeof(&arr[0] + 1));// 输出4/8
// 解析:&arr[0]+1是第二个元素地址,指针大小4/8字节。
return 0;
}
场景 4:带 \0 的字符数组(strlen)
#include <string.h>
int main() {
char arr[] = "abcdef"; // 内存布局:a b c d e f \0(带\0)
printf("%zd\n", strlen(arr)); // 输出6
// 解析:arr是首元素地址,strlen统计到\0前的字符数,共6个。
printf("%zd\n", strlen(arr + 0)); // 输出6
// 解析:arr+0仍是首元素地址,统计结果同上。
// printf("%zd\n", strlen(*arr)); // 程序崩溃!*arr='a'=97,非法地址。
// printf("%zd\n", strlen(arr[1]));// 程序崩溃!arr[1]='b'=98,非法地址。
printf("%zd\n", strlen(&arr)); // 输出6
// 解析:&arr是数组地址(char(*)[7]),传参时隐式转为char*(首元素地址),
// 从首元素开始统计到\0,结果6。
printf("%zd\n", strlen(&arr + 1)); // 输出随机值
// 解析:&arr+1跳过整个数组(7字节),指向\0后面的地址,从该地址开始找\0,结果随机。
printf("%zd\n", strlen(&arr[0] + 1));// 输出5
// 解析:&arr[0]+1是第二个元素地址('b'),统计到\0前共5个字符(b c d e f)。
return 0;
}
场景 5:字符指针指向常量字符串(sizeof)
int main() {
const char* p = "abcdef"; // p是指针变量,存储常量字符串首地址(常量区:a b c d e f \0)
printf("%zd\n", sizeof(p)); // 输出4/8
// 解析:p是指针变量(无论指向什么类型),指针大小4/8字节。
printf("%zd\n", sizeof(p + 1)); // 输出4/8
// 解析:p+1指向常量字符串的第二个字符'b'(地址),仍是指针,大小4/8字节。
printf("%zd\n", sizeof(*p)); // 输出1
// 解析:p的类型是const char*,*p是首字符'a'(char类型),占1字节。
printf("%zd\n", sizeof(p[0])); // 输出1
// 解析:p[0]等价于*(p+0),即*p,是首字符'a'(char类型),占1字节。
printf("%zd\n", sizeof(&p)); // 输出4/8
// 解析:&p是指针变量p的地址(二级指针,类型char**),本质是地址,大小4/8字节。
printf("%zd\n", sizeof(&p + 1)); // 输出4/8
// 解析:&p+1跳过指针变量p的内存(4/8字节),指向p后面的空闲地址,仍是指针,大小4/8字节。
printf("%zd\n", sizeof(&p[0] + 1));// 输出4/8
// 解析:&p[0]是首字符地址(char*),+1指向第二个字符'b'的地址,指针大小4/8字节。
return 0;
}
场景 6:字符指针指向常量字符串(strlen)
#include <string.h>
int main() {
char* p = "abcdef"; // 常量字符串:a b c d e f \0
printf("%zd\n", strlen(p)); // 输出6
// 解析:p是首字符地址,统计到\0前共6个字符。
printf("%zd\n", strlen(p + 1)); // 输出5
// 解析:p+1指向'b',统计到\0前共5个字符。
// printf("%zd\n", strlen(*p)); // 程序崩溃!*p='a'=97,非法地址。
// printf("%zd\n", strlen(p[0]));// 程序崩溃!p[0]='a'=97,非法地址。
printf("%zd\n", strlen(&p)); // 输出随机值
// 解析:&p是指针变量p的地址(栈区),与常量字符串无关,
// 从p的地址开始找\0,栈区数据随机,结果不确定。
printf("%zd\n", strlen(&p + 1)); // 输出随机值
// 解析:&p+1跳过p的内存,从p后面的栈区地址开始找\0,结果随机。
printf("%zd\n", strlen(&p[0] + 1));// 输出5
// 解析:&p[0]是首字符地址,+1指向'b',统计到\0前共5个字符。
return 0;
}
字符数组 vs 字符指针核心区别:
- 字符数组(
char arr[] = "abc"):内存在栈区,可修改(如arr[0]='x'),sizeof计算整个数组大小(含 \0); - 字符指针(
char* p = "abc"):内存在常量区(只读),p存储字符串首地址,sizeof计算指针大小(4/8 字节)。
5. 二维数组笔试题(行指针 vs 元素指针 + 越界不崩溃原因)
二维数组的本质是 “数组的数组”(如int a[3][4]是 3 个 “4 元素 int 数组” 的集合),数组名a是 “第一行的地址”(行指针,类型int(*)[4])。
int main() {
int a[3][4] = {0}; // 内存布局:3行4列,总大小=3×4×4=48字节
// 行0:[0][0][0][0],行1:[0][0][0][0],行2:[0][0][0][0]
printf("%d\n", sizeof(a));// 输出48
// 解析:a是二维数组名,单独放在sizeof内部,代表整个二维数组,大小=3×4×4=48字节。
printf("%d\n", sizeof(a[0][0]));// 输出4
// 解析:a[0][0]是第一行第一个元素(int类型),占4字节。
printf("%d\n", sizeof(a[0]));// 输出16
// 解析:a[0]是第一行的数组名(一维数组),单独放在sizeof内部,代表第一行整个数组,
// 大小=4元素×4字节=16字节。
printf("%d\n", sizeof(a[0] + 1));// 输出4/8
// 解析:a[0]是第一行的数组名,未单独放在sizeof内部,等价于第一行首元素地址(&a[0][0],int*),
// a[0]+1跳过一个int,指向a[0][1]的地址,指针大小4/8字节。
printf("%d\n", sizeof(*(a[0] + 1)));// 输出4
// 解析:*(a[0]+1)等价于a[0][1],是第一行第二个元素(int类型),占4字节。
printf("%d\n", sizeof(a + 1));// 输出4/8
// 解析:a是二维数组名,未单独放在sizeof内部,代表第一行的地址(行指针,类型int(*)[4]),
// a+1跳过一行(16字节),指向第二行的地址,本质是地址,大小4/8字节。
printf("%d\n", sizeof(*(a + 1)));// 输出16
// 解析:a+1是第二行的地址(行指针),*(a+1)是对行指针的解引用,得到第二行的数组名(a[1]),
// 等价于sizeof(a[1]),大小=4×4=16字节。
printf("%d\n", sizeof(&a[0] + 1));// 输出4/8
// 解析:&a[0]是第一行的地址(行指针,int(*)[4]),+1跳过一行,指向第二行的地址,
// 本质是地址,大小4/8字节。
printf("%d\n", sizeof(*(&a[0] + 1)));// 输出16
// 解析:&a[0]+1是第二行的地址(行指针),解引用后得到第二行的数组名(a[1]),
// 大小=4×4=16字节。
printf("%d\n", sizeof(*a));// 输出16
// 解析:a是第一行的地址(行指针),*a是解引用行指针,得到第一行的数组名(a[0]),
// 等价于sizeof(a[0]),大小=16字节。
printf("%d\n", sizeof(a[3]));// 输出16
// 解析:关键!a[3]是“第四行的数组名”(仅类型概念,无需真实存在),
// sizeof仅通过类型判断大小(a[3]的类型是int[4]),大小=4×4=16字节,
// 无需访问真实内存,所以不会越界崩溃(sizeof不触发内存访问)。
return 0;
}
核心概念:行指针 vs 元素指针
| 表达式 | 类型 | 步长(字节) | 指向对象 |
|---|---|---|---|
a | int(*)[4](行指针) | 16 | 第一行数组 |
a+1 | int(*)[4](行指针) | 16 | 第二行数组 |
a[0] | int*(元素指针) | 4 | 第一行首元素 |
a[0]+1 | int*(元素指针) | 4 | 第一行第二个元素 |
易错点:sizeof 不触发内存访问,所以不会越界崩溃
sizeof(a[3])中a[3]是 “不存在的第四行”,但sizeof仅通过类型(int[4])计算大小,不访问真实内存,因此不会触发越界错误。
6. 经典指针笔试题(6 道题 + 内存布局图解 + 分步拆解)
题目 1:数组地址 + 1 的偏移计算
int main() {
int a[5] = {1,2,3,4,5}; // 内存布局:[1][2][3][4][5](5×4=20字节)
int* ptr = (int*)(&a + 1); // 关键:&a是数组地址,+1跳过整个数组
printf("%d,%d", *(a + 1), *(ptr - 1));// 输出2,5
// 分步拆解:
// 1. 分析*(a + 1):
// a是首元素地址(int*),a+1指向第二个元素(值2),*(a+1)=2;
// 2. 分析&a + 1:
// &a是数组地址(int(*)[5]),数组指针步长=20字节(数组总大小),
// &a+1跳过整个数组,指向数组后面的空闲内存;
// 3. 分析ptr的类型:
// (int*)(&a + 1)将“数组指针”强制转为“int*”,ptr指向数组后的空闲地址;
// 4. 分析*(ptr - 1):
// ptr是int*,步长4字节,ptr-1向前跳过4字节,指向数组最后一个元素(值5),
// *(ptr-1)=5;
return 0;
}
题目 2:指针类型决定步长(结构体指针 + 强制类型转换)
// 假设结构体大小为20字节(x86环境,结构体对齐后)
struct Test {
int Num;
char* pcName;
short sDate;
char cha[2];
short sBa[4];
}*p = (struct Test*)0x100000; // p指向地址0x100000
int main() {
printf("%p\n", p + 0x1); // 输出0x100014
printf("%p\n", (unsigned long)p + 0x1); // 输出0x100001
printf("%p\n", (unsigned int*)p + 0x1); // 输出0x100004
// 分步拆解:
// 1. p + 0x1:
// p是struct Test*类型,指针步长=结构体大小(20字节),
// 0x100000 + 20(十进制)= 0x100000 + 0x14(十六进制)= 0x100014;
// 2. (unsigned long)p + 0x1:
// 将指针p强制转为unsigned long(无符号长整型),此时p是数值0x100000,
// 数值+1=0x100001,输出该地址;
// 3. (unsigned int*)p + 0x1:
// 将指针p强制转为unsigned int*类型,步长=4字节(int大小),
// 0x100000 + 4 = 0x100004;
return 0;
}
题目 3:逗号表达式的陷阱(初始化赋值)
int main()
{
int a[3][2] = { (0,1),(2,3),(4,5) };
//1 3
//5 0
//0 0
int* p;
p = a[0];//a[0]是第一行的数组名,数组名表示首元素的地址:&a[0][0]
printf("%d", p[0]);//p[0] == *(p+0) == *p == *&a[0][0] == a[0][0] == 1
return 0;
}
题目 4:指针 - 指针的元素个数计算(补码 + 地址打印)
int main()
{
int a[5][5];
int(*p)[4];//p是一个数组指针,p指向的数组是4个整型的元素
p = a;//a的类型是int(*)[5],p的类型是int(*)[4]
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);//FFFFFFFC -4
//%d 是打印有符号的整数
//%p 是打印地址
//-4:
//原码:10000000 00000000 00000000 00000100
//反码:11111111 11111111 11111111 11111011
//补码:11111111 11111111 11111111 11111100
//内存存放的是:FF FF FF FC
return 0;
}
//p p+1 p+2 p+3 p+4 &p[4][2] &a[4][2]
//| | | | | | |
//V V V V V V V
// 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4
//
// a[0] a[1] a[2] a[3] a[4]
//
题目 5:二维数组地址偏移(行指针转元素指针)
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));
//*(aa+1) == aa[1] == &aa[1][0] 是第二行的数组名,数组名表示首元素的地址
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));//10 5
return 0;
}
//&aa ptr2 ptr1
//| | |
//V V V
// 1 2 3 4 5 6 7 8 9 10
题目 6:指针数组 + 二级指针(字符串数组遍历)
int main()
{
char* a[] = { "work","at","alibaba" };
char** pa = a;
pa++;
printf("%s\n", *pa);//at
return 0;
}
//a是指针数组
//
//char* work
//char* at
//char* alibaba
//数组名表示首元素的地址,首元素为"work",也代表着第一个字符'w'的地址,类型为char*,
//所以首元素的地址是一个二级指针,类型为char**,由二级指针变量来储存
//pa的类型为char**,加一跳过一个元素,相当于跳过一个char*的内存,
//此时pa指向的是"at"中'a'的地址,解引用得到'a'的地址,再打印,就为at
题目 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);//POINT
// ++cpp -> &cp[1]
//*++cpp -> *&cp[1] -> cp[1] -> c+2
//**++cpp -> *(c+2) -> c[2] -> 'P'的地址 -> POINT
//此时cpp为cpp+1
printf("%s\n", *-- * ++cpp + 3);//ER
//++cpp -> &cp[2]
//*++cpp -> *&cp[2] -> cp[2] -> c+1
//--*++cpp -> c
//*--*++cpp -> *(c) -> c[0] ->'E'的地址
//*--*++cpp+3 -> 'E'的地址 -> ER
//此时cpp为cpp+2
printf("%s\n", *cpp[-2] + 3);//ST
//cpp[-2] == *(cpp-2) == *cpp == cp[0] == c+3
//*cpp[-2] == *(c+3) == c[3] == 'F'的地址
//*cpp[-2]+3 -> 'S'的地址 -> ST
//此时cpp依然为cpp+2
printf("%s\n", cpp[-1][-1] + 1);//EW
//cpp[-1] == *(cpp-1) == *(cpp+1) == cp[1] == c+2
//cpp[-1][-1] == *(*(cpp+1)-1) == *(c+1) == c[1] == 'N'的地址
//cpp[-1][-1]+1 == 'E'的地址 -> EW
return 0;
}
// char*** char** char*
// cpp cpp-> cp-->
// cp[0] = c+3 c[0] = char* ENTER
// cpp+1-> cp+1->
// cp[1] = c+2 c[1] = char* NEW
// cpp+2-> cp+2->
// cp[2] = c+1 c[2] = char* POINT
// cpp+3-> cp+3->
// cp[3] = c c[3] = char* FIRST
7. 核心知识点总结(必记!)
- sizeof:编译期操作符,量内存大小,不执行表达式,数组名单独放里面算整个数组大小;
- strlen:运行期函数,数
\0前字符数,必须传地址,无\0随机值,传非地址崩溃; - 数组名:默认是首元素地址,
sizeof(数组名)和&数组名代表整个数组; - 指针运算:
- 指针 ± 整数:步长 = 指针指向类型的大小;
- 指针 - 指针:结果 = 元素个数(地址差 ÷ 元素大小),仅同数组内指针相减有意义;
- 指针类型:决定 “指向内容的类型” 和 “± 整数的步长”,不影响指针本身的内存大小(4/8 字节);
- 易错点:
- 字符数组无
\0时 strlen 结果随机; - 给 strlen 传非地址会崩溃;
- sizeof 不触发内存访问,所以不会越界崩溃;
- 逗号表达式取最后一个表达式的值。
- 字符数组无
掌握这些核心规则,再结合 “内存布局图解” 分析每道题,数组指针题就能迎刃而解!
1243

被折叠的 条评论
为什么被折叠?



