C语言笔记归纳14:指针(5)

指针(5)

目录

指针(5)

1. sizeof 与 strlen 核心对比(底层逻辑 + 超详示例)

1.1 sizeof:编译期的 “内存尺子”(不关心内容,只看类型)

示例 1:证明 sizeof 不是函数 + 基本用法

示例 2:表达式不执行的底层原因(关键!)

1.2 strlen:运行期的 “字符串计数器”(依赖 \0,只认地址)

示例 1:正常情况(有 \0)

示例 2:异常情况 1(无 \0,随机值)

示例 3:异常情况 2(传非地址,程序崩溃)

1.3 核心差异表(必记!)

2. 数组名的 “特殊身份”—— 两个关键例外(解题核心!)

核心规则:

示例:直观理解数组名的不同身份

3. 一维数组笔试题(逐题拆解 + 类型分析)

易错点总结:

4. 字符数组笔试题(6 种场景 + 崩溃原因深度解析)

场景 1:无 \0 的字符数组(sizeof)

场景 2:无 \0 的字符数组(strlen,崩溃 + 随机值)

场景 3:带 \0 的字符数组(sizeof)

场景 4:带 \0 的字符数组(strlen)

场景 5:字符指针指向常量字符串(sizeof)

场景 6:字符指针指向常量字符串(strlen)

字符数组 vs 字符指针核心区别:

5. 二维数组笔试题(行指针 vs 元素指针 + 越界不崩溃原因)

核心概念:行指针 vs 元素指针

易错点:sizeof 不触发内存访问,所以不会越界崩溃

6. 经典指针笔试题(6 道题 + 内存布局图解 + 分步拆解)

题目 1:数组地址 + 1 的偏移计算

题目 2:指针类型决定步长(结构体指针 + 强制类型转换)

题目 3:逗号表达式的陷阱(初始化赋值)

题目 4:指针 - 指针的元素个数计算(补码 + 地址打印)

题目 5:二维数组地址偏移(行指针转元素指针)

题目 6:指针数组 + 二级指针(字符串数组遍历)

题目 7:三级指针嵌套(指针数组 + 二级指针数组)

7. 核心知识点总结(必记!)


✨引言:

C 语言中sizeofstrlen与数组指针的组合题是新手的 “高频踩坑点”,也是面试必考点。这份笔记将逐行拆解每道题的底层逻辑,结合 “内存布局图解 + 类型分析 + 易错点标注”,把每个知识点讲透,让你不仅知其然,更知其所以然!

1. sizeof 与 strlen 核心对比(底层逻辑 + 超详示例)

1.1 sizeof:编译期的 “内存尺子”(不关心内容,只看类型)

  • 本质:单目操作符(绝对不是函数!),在编译阶段就完成计算,无需运行程序。
  • 核心规则
    1. 计算变量 / 数据类型占用的内存大小(单位:字节),与内存中实际存储的内容无关;
    2. 括号内若有表达式,表达式不执行(编译期仅判断变量类型,不处理运行期逻辑);
    3. 语法灵活: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),仅在程序运行时计算。
  • 核心规则
    1. 功能:统计字符串中\0(字符串结束符)之前的字符个数,不包含\0
    2. 依赖条件:必须以\0结尾,否则会越界访问栈区随机数据,结果为随机值;
    3. 参数要求:必须传入合法的字符串首地址,若传入非地址(如字符'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 核心差异表(必记!)

特性sizeofstrlen
本质单目操作符(编译期计算)库函数(运行期计算)
计算依据变量 / 类型的内存大小(仅看类型)字符串中 \0 前的字符数(仅看内容)
依赖条件无(不依赖 \0 或具体内容)必须依赖 \0 结束符
参数类型变量、数据类型(如 int、char [])仅接受字符串首地址(const char*)
易错点括号内表达式不执行1. 无 \0 返回随机值;2. 传非地址程序崩溃
返回值类型size_t(无符号整数,用 % zd 格式化)size_t(无符号整数,用 % zd 格式化)

2. 数组名的 “特殊身份”—— 两个关键例外(解题核心!)

数组名的默认本质是 “数组首元素的地址”,但有两个特殊场景,数组名会代表 “整个数组”,这是所有数组指针题的 “解题钥匙”:

核心规则:

  1. 场景 1:sizeof(数组名) → 数组名代表整个数组,计算的是整个数组的内存大小(单位:字节);
  2. 场景 2:&数组名 → 数组名代表整个数组,取出的是整个数组的地址(不是首元素地址,类型为 “数组指针”);
  3. 除此之外,所有数组名的出现(如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不依赖\0strlen依赖\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 元素指针

表达式类型步长(字节)指向对象
aint(*)[4](行指针)16第一行数组
a+1int(*)[4](行指针)16第二行数组
a[0]int*(元素指针)4第一行首元素
a[0]+1int*(元素指针)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. 核心知识点总结(必记!)

  1. sizeof:编译期操作符,量内存大小,不执行表达式,数组名单独放里面算整个数组大小;
  2. strlen:运行期函数,数\0前字符数,必须传地址,无\0随机值,传非地址崩溃;
  3. 数组名:默认是首元素地址,sizeof(数组名)&数组名代表整个数组;
  4. 指针运算
    • 指针 ± 整数:步长 = 指针指向类型的大小;
    • 指针 - 指针:结果 = 元素个数(地址差 ÷ 元素大小),仅同数组内指针相减有意义;
  5. 指针类型:决定 “指向内容的类型” 和 “± 整数的步长”,不影响指针本身的内存大小(4/8 字节);
  6. 易错点
    • 字符数组无\0时 strlen 结果随机;
    • 给 strlen 传非地址会崩溃;
    • sizeof 不触发内存访问,所以不会越界崩溃;
    • 逗号表达式取最后一个表达式的值。

掌握这些核心规则,再结合 “内存布局图解” 分析每道题,数组指针题就能迎刃而解!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值