《C 语言 sizeof 与 strlen 深度对比:原理、差异与实战陷阱》

目录

[一. sizeof 和 strlen 的对比](#一. sizeof 和 strlen 的对比)

[1.1 sizeof](#1.1 sizeof)

[1.2 strlen](#1.2 strlen)

[1.3 对比表格](#1.3 对比表格)

[二. 数组和指针笔试题解析](#二. 数组和指针笔试题解析)

[2.1 一维数组](#2.1 一维数组)

[2.2 字符数组](#2.2 字符数组)

[2.2.1 代码练习一](#2.2.1 代码练习一)

[2.2.2 代码练习二](#2.2.2 代码练习二)

[2.2.3 代码练习三](#2.2.3 代码练习三)

[2.2.4 代码练习四](#2.2.4 代码练习四)

[2.2.5 代码练习五](#2.2.5 代码练习五)

[2.2.6 代码练习六](#2.2.6 代码练习六)

[2.3 二维数组](#2.3 二维数组)

[三. 指针运算笔试题解析](#三. 指针运算笔试题解析)

[3.1 试题练习一](#3.1 试题练习一)

[3.2 试题练习二](#3.2 试题练习二)

[3.3 试题练习三](#3.3 试题练习三)

[3.4 试题练习四](#3.4 试题练习四)

[3.5 试题练习五](#3.5 试题练习五)

[3.6 试题练习六](#3.6 试题练习六)

[3.7 试题练习七](#3.7 试题练习七)


一. sizeof 和 strlen 的对比

1.1 sizeof

sizeof计算变量所占内存空间的大小,单位是字节,如果操作数是类型的话,计算的是使用类型创建的变量所占内存空间的大小。

sizeof 只关注占用内存空间的大小,不在乎内存中存放什么数据;并且sizeof中表达式不计算

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
int main()
{
	int a = 10;
	printf("sizeof(a): %d\n", sizeof(a));
	printf("sizeof(a+3.14): %d\n", sizeof(a+3.14));
	printf("sizeof(int): %d\n", sizeof(int));
	return 0;
}

通过运行该代码可以得到

其中a是int类型 占4个字节

a+3.14 由于a算术提升到了double类型 因此占8个字节

1.2 strlen

strlen是C语言库函数,功能是求字符串长度

  • 计算字符串的实际长度(不包含'\0')

  • 必须接收以'\0'结尾的有效字符串指针

    cpp 复制代码
    strlen("hello");  // 返回5
  • 遇到第一个'\0'停止计数

    cpp 复制代码
    strlen("hel\0lo");  // 返回3

练习strlen函数 请自己思考一下 下面函数会打印那些值

cpp 复制代码
#include <stdio.h>
#include<string.h>
int main()
{
	char arr1[3] = { 'a', 'b', 'c' };
	char arr2[] = "abc";
	printf("%d\n", strlen(arr1));
	printf("%d\n", strlen(arr2));
	printf("%d\n", sizeof(arr1));
	printf("%d\n", sizeof(arr2));
	return 0;
}

运行结果如下

其中由于arr1并没有 ' \0 ' 结尾 因此打印随机值 35

1.3 对比表格

特性 sizeof 运算符 strlen 函数
类型 编译时运算符(非函数) 运行时函数(来自 <string.h>
作用对象 变量、类型或表达式 仅适用于以 \0 结尾的字符串
计算时机 编译时确定 运行时计算
返回值 对象/类型占用的内存字节数(含 \0 字符串长度(不含 \0
参数示例 sizeof(int), sizeof(arr) strlen("hello")
对指针的行为 返回指针本身的大小(通常4/8字节) 计算指针指向的字符串长度
对数组的行为 返回整个数组的字节大小 将数组退化为指针后计算长度
对字符串字面量 包含 \0 的总大小(如 "abc" 返回4) 不包含 \0 的长度(如 "abc" 返回3)
时间复杂度 O(1)(编译时完成) O(n)(需遍历字符串到 \0

二. 数组和指针笔试题解析

首先我们先再次明确数组名的意义:

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组所占内存的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
  3. 除此之外所有的数组名都表示首元素的地址。

2.1 一维数组

首先请思考一下 下面这段代码将会打印什么

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	int a[] = { 1,2,3,4 };
	printf("%zu\n", sizeof(a));
	printf("%zu\n", sizeof(a + 0));
	printf("%zu\n", sizeof(*a));
	printf("%zu\n", sizeof(a + 1));
	printf("%zu\n", sizeof(a[1]));
	printf("%zu\n", sizeof(&a));
	printf("%zu\n", sizeof(*&a));
	printf("%zu\n", sizeof(&a + 1));
	printf("%zu\n", sizeof(&a[0]));
	printf("%zu\n", sizeof(&a[0] + 1));
	return 0;
}

思考完了请验证你的答案是否正确

典型输出(64位系统):

cpp 复制代码
16  // 整个数组大小
8   // 指针大小
4   // int大小
8   // 指针大小
4   // int大小
8   // 指针大小
16  // 整个数组大小
8   // 指针大小
8   // 指针大小
8   // 指针大小

注释讲解如下

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
    int a[] = { 1,2,3,4 };  // 定义一个包含4个int元素的数组
    
    printf("%zu\n", sizeof(a));  // 1. 整个数组的大小:4个int × 4字节/int = 16字节
    
    printf("%zu\n", sizeof(a + 0));  // 2. 数组名a在表达式中退化为指针,a+0是指向第一个元素的指针,指针大小通常为4或8字节
    
    printf("%zu\n", sizeof(*a));  // 3. 解引用数组名得到第一个元素,int类型大小为4字节
    
    printf("%zu\n", sizeof(a + 1));  // 4. 数组名a退化为指针,a+1是指向第二个元素的指针,指针大小
    
    printf("%zu\n", sizeof(a[1]));  // 5. 第二个元素的大小,int类型为4字节
    
    printf("%zu\n", sizeof(&a));  // 6. &a是"指向整个数组的指针",但仍然是指针,指针大小
    
    printf("%zu\n", sizeof(*&a));  // 7. 解引用数组指针得到整个数组,大小为16字节
    
    printf("%zu\n", sizeof(&a + 1));  // 8. 指向数组后面位置的指针,指针大小
    
    printf("%zu\n", sizeof(&a[0]));  // 9. 指向第一个元素的指针,指针大小
    
    printf("%zu\n", sizeof(&a[0] + 1));  // 10. 指向第二个元素的指针,指针大小
    
    return 0;
}

关键点说明:

  1. 数组名在大多数情况下会退化为指针 ,但在sizeof(a)&a操作中不会退化。
  2. 指针的大小取决于系统架构(32位系统通常4字节,64位系统通常8字节)。
  3. &a是"指向整个数组的指针",类型是int(*)[4],但它的值仍然是数组的起始地址。
  4. *&a等价于a,所以sizeof(*&a)就是整个数组的大小。
  5. 数组元素的地址运算(如a+1&a[0]+1)得到的是指针,不是数组。

2.2 字符数组

2.2.1 代码练习一

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%zu\n", sizeof(arr));
	printf("%zu\n", sizeof(arr + 0));
	printf("%zu\n", sizeof(*arr));
	printf("%zu\n", sizeof(arr[1]));
	printf("%zu\n", sizeof(&arr));
	printf("%zu\n", sizeof(&arr + 1));
	printf("%zu\n", sizeof(&arr[0] + 1));
}

典型输出(64位系统):

cpp 复制代码
6  // 整个数组大小
8  // 指针大小
1  // char大小
1  // char大小
8  // 指针大小
8  // 指针大小
8  // 指针大小

注释解析如下

cpp 复制代码
#include<stdio.h>
int main()
{
    char arr[] = { 'a','b','c','d','e','f' };  // 定义一个包含6个char元素的字符数组
    
    printf("%zu\n", sizeof(arr));  // 1. 整个数组的大小:6个char × 1字节/char = 6字节
    
    printf("%zu\n", sizeof(arr + 0));  // 2. 数组名arr在表达式中退化为指针,arr+0是指向第一个元素的指针,指针大小通常为4或8字节
    
    printf("%zu\n", sizeof(*arr));  // 3. 解引用数组名得到第一个元素,char类型大小为1字节
    
    printf("%zu\n", sizeof(arr[1]));  // 4. 第二个元素的大小,char类型为1字节
    
    printf("%zu\n", sizeof(&arr));  // 5. &arr是"指向整个数组的指针",但仍然是指针,指针大小
    
    printf("%zu\n", sizeof(&arr + 1));  // 6. 指向数组后面位置的指针,指针大小
    
    printf("%zu\n", sizeof(&arr[0] + 1));  // 7. 指向第二个元素的指针,指针大小
    
    return 0;
}

关键点说明:

  1. 数组名在大多数情况下会退化为指针 ,但在sizeof(arr)&arr操作中不会退化。
  2. 指针的大小取决于系统架构(32位系统通常4字节,64位系统通常8字节)。
  3. &arr是"指向整个数组的指针",类型是char(*)[6],但它的值仍然是数组的起始地址。
  4. 字符数组arr的大小是6字节,因为每个char类型占用1字节。
  5. 数组元素的地址运算(如arr+0&arr[0]+1)得到的是指针,不是数组。

2.2.2 代码练习二

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#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));
	printf("%zu\n", strlen(&arr + 1));
	printf("%zu\n", strlen(&arr[0] + 1));
}

典型输出(64位系统):

cpp 复制代码
36//随机值
36//随机值
36//随机值
30//随机值-6
35//随机值-1

注释解析如下:

cpp 复制代码
int main()
{
    // 定义并初始化一个字符数组,注意没有包含字符串终止符'\0'
    char arr[] = { 'a','b','c','d','e','f' };
    
    // 1. 计算从数组首元素开始的"字符串"长度
    printf("%zu\n", strlen(arr));
    // arr作为数组名,在表达式中退化为指向首元素的指针
    // strlen会从'a'开始向后查找'\0',但数组中没有'\0'
    // 会继续访问数组后面的内存,直到偶然遇到'\0'
    // 结果是不可预测的随机值,且可能导致未定义行为
    
    // 2. 计算从数组首元素开始的"字符串"长度
    printf("%zu\n", strlen(arr + 0));
    // arr + 0 仍然指向数组首元素
    // 情况与第一个printf完全相同
    // 结果也是随机值,且可能与第一个printf相同
    
    // 3. 计算从数组地址开始的"字符串"长度
    printf("%zu\n", strlen(&arr));
    // &arr 是"指向整个数组的指针"(类型为 char(*)[6])
    // 但它的值与arr相同,都是数组的起始地址
    // 情况与前两个printf相同,结果也是随机值
    
    // 4. 计算从数组末尾后一个位置开始的"字符串"长度
    printf("%zu\n", strlen(&arr + 1));
    // &arr + 1 跳过整个数组(6个字节),指向数组之后的内存位置
    // strlen从这个新位置开始查找'\0'
    // 结果也是随机值,但通常比前几个结果小6(因为跳过了6个字符)
    // 注意这仍然是未定义行为
    
    // 5. 计算从数组第二个元素开始的"字符串"长度
    printf("%zu\n", strlen(&arr[0] + 1));
    // &arr[0] + 1 指向数组的第二个元素'b'
    // 从这个位置开始查找'\0'
    // 结果也是随机值,但通常比第一个结果小1
    // 同样属于未定义行为
    
    return 0;
}

关键点总结:

  1. 字符串终止符的重要性

    • strlen依赖\0来确定字符串结束位置
    • 这个数组没有以\0结尾,导致strlen会越界访问
  2. 指针运算

    • arrarr+0都指向首元素
    • &arr是整个数组的指针(类型不同但值相同)
    • &arr + 1跳过整个数组
    • &arr[0] + 1指向第二个元素

2.2.3 代码练习三

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
int main()
{
	char arr[] = "abcdef";
	printf("%zu\n", sizeof(arr));
	printf("%zu\n", sizeof(arr + 0));
	printf("%zu\n", sizeof(*arr));
	printf("%zu\n", sizeof(arr[1]));
	printf("%zu\n", sizeof(&arr));
	printf("%zu\n", sizeof(&arr + 1));
	printf("%zu\n", sizeof(&arr[0] + 1));
}

典型输出(64位系统):

cpp 复制代码
7  // 整个数组大小(包括'\0')
8  // 指针大小
1  // char大小
1  // char大小
8  // 指针大小
8  // 指针大小
8  // 指针大小

注释解析如下:

cpp 复制代码
#include<stdio.h>
int main()
{
    char arr[] = "abcdef";  // 定义一个字符串数组,包含6个字符和1个'\0',共7个元素
    
    printf("%zu\n", sizeof(arr));  // 1. 整个数组的大小:7个char × 1字节/char = 7字节
    // 注意:字符串字面量"abcdef"会自动添加'\0',所以数组大小是7不是6
    
    printf("%zu\n", sizeof(arr + 0));  // 2. arr在表达式中退化为指针,arr+0是指向第一个元素的指针
    // 指针大小通常为4(32位)或8(64位)字节
    
    printf("%zu\n", sizeof(*arr));  // 3. 解引用数组名得到第一个元素'a',char类型大小为1字节
    
    printf("%zu\n", sizeof(arr[1]));  // 4. 第二个元素'b'的大小,char类型为1字节
    
    printf("%zu\n", sizeof(&arr));  // 5. &arr是"指向整个数组的指针",指针大小
    
    printf("%zu\n", sizeof(&arr + 1));  // 6. 指向数组后面位置的指针,指针大小
    
    printf("%zu\n", sizeof(&arr[0] + 1));  // 7. 指向第二个元素的指针,指针大小
    
    return 0;
}

关键点说明:

  1. 字符串数组的特殊性

    • char arr[] = "abcdef" 会创建一个包含7个元素的数组:'a','b','c','d','e','f','\0'
    • 所以sizeof(arr)是7而不是6
  2. 数组名退化为指针

    • 在大多数表达式中,数组名会退化为指向其首元素的指针
    • 但在sizeof(arr)&arr操作中不会退化
  3. 指针大小

    • 32位系统:通常4字节
    • 64位系统:通常8字节
    • 示例输出假设是64位系统
  4. 指针类型差异

    • arr+0&arr[0]+1char*类型
    • &arr&arr+1char(*)[7]类型(指向整个数组的指针)
    • 但所有指针的sizeof结果相同

2.2.4 代码练习四

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<string.h> 
int main()
{
	char arr[] = "abcdef";
	printf("%zu\n", strlen(arr));
	printf("%zu\n", strlen(arr + 0));
	printf("%zu\n", strlen(&arr));
	printf("%zu\n", strlen(&arr + 1));
	printf("%zu\n", strlen(&arr[0] + 1));
}

典型输出(64位系统):

cpp 复制代码
6
6
6
26
5

注释解析如下:

cpp 复制代码
int main()
{
    // 定义并初始化一个字符串数组,包含6个字符和1个'\0'终止符
    char arr[] = "abcdef";  // 等价于 {'a','b','c','d','e','f','\0'}
    
    // 1. 计算字符串长度
    printf("%zu\n", strlen(arr)); // 输出6
    // arr作为数组名,在表达式中退化为指向首元素的指针
    // strlen从'a'开始向后查找'\0',在'f'后找到'\0'
    // 计算结果是6个字符
    
    // 2. 计算从首元素开始的字符串长度
    printf("%zu\n", strlen(arr + 0)); // 输出6
    // arr + 0 仍然指向数组首元素
    // 情况与第一个printf完全相同
    // 结果也是6
    
    // 3. 计算从数组地址开始的字符串长度
    printf("%zu\n", strlen(&arr)); // 输出6
    // &arr 是"指向整个数组的指针"(类型为 char(*)[7])
    // 但它的值与arr相同,都是数组的起始地址
    // 情况与前两个printf相同,结果也是6
    
    // 4. 计算从数组末尾后一个位置开始的字符串长度
    printf("%zu\n", strlen(&arr + 1)); // 未定义行为
    // &arr + 1 跳过整个数组(7个字节),指向数组之后的内存位置
    // strlen从这个新位置开始查找'\0'
    // 这是未定义行为,可能返回随机值或导致程序崩溃
    
    // 5. 计算从数组第二个元素开始的字符串长度
    printf("%zu\n", strlen(&arr[0] + 1)); // 输出5
    // &arr[0] + 1 指向数组的第二个元素'b'
    // 从这个位置开始查找'\0',会找到'c','d','e','f','\0'
    // 结果是5个字符
    
    return 0;
}

关键点总结:

  1. 字符串终止符

    • 字符串"abcdef"实际上包含7个字节:6个字符+1个'\0'
    • strlen计算的是'\0'前的字符数量
  2. 指针运算

    • arrarr+0都指向首元素
    • &arr是整个数组的指针(类型不同但值相同)
    • &arr + 1跳过整个数组(7个字节)
    • &arr[0] + 1指向第二个元素
  3. 安全与未定义行为

    • 前4个printf都是安全的,因为都在数组范围内查找'\0'
    • strlen(&arr + 1)是未定义行为,因为它访问了数组外的内存
  4. 结果分析

    • 前三个输出都是6(完整字符串长度)
    • 第五个输出是5(从第二个字符开始计算)
    • 第四个输出不可预测

2.2.5 代码练习五

cpp 复制代码
#include<stdio.h>
int main()
{
	char* p = "abcdef";
	printf("%zu\n", sizeof(p));
	printf("%zu\n", sizeof(p + 1));
	printf("%zu\n", sizeof(*p));
	printf("%zu\n", sizeof(p[0]));
	printf("%zu\n", sizeof(&p));
	printf("%zu\n", sizeof(&p + 1));
	printf("%zu\n", sizeof(&p[0] + 1));
}

典型输出(64位系统):

cpp 复制代码
8  // 指针大小
8  // 指针大小
1  // char大小
1  // char大小
8  // 指针大小
8  // 指针大小
8  // 指针大小

注释解析如下:

cpp 复制代码
#include<stdio.h>
int main()
{
    char* p = "abcdef";  // 定义一个字符指针p,指向字符串常量"abcdef"
    
    printf("%zu\n", sizeof(p));  // 1. 指针p本身的大小:在32位系统为4字节,64位系统为8字节
    
    printf("%zu\n", sizeof(p + 1));  // 2. 指针运算结果的大小:p+1是指向第二个字符的指针,大小与p相同
    
    printf("%zu\n", sizeof(*p));  // 3. 解引用指针得到字符'a'的大小:char类型为1字节
    
    printf("%zu\n", sizeof(p[0]));  // 4. 等同于*(p+0),即第一个字符'a'的大小:1字节
    
    printf("%zu\n", sizeof(&p));  // 5. 取指针p的地址,得到指向指针的指针:大小仍为指针大小
    
    printf("%zu\n", sizeof(&p + 1));  // 6. 指针p的地址加1,指向下一个指针位置:大小仍为指针大小
    
    printf("%zu\n", sizeof(&p[0] + 1));  // 7. 等同于&(*(p+0))+1,即指向第二个字符的指针:大小仍为指针大小
    
    return 0;
}

关键点说明:

  1. 指针大小:所有指针的大小相同,取决于系统架构(32位系统4字节,64位系统8字节)

  2. 字符串常量"abcdef"是字符串常量,存储在只读内存区域,以'\0'结尾

  3. 指针运算

    • p + 1:指向字符串的第二个字符'b'
    • &p + 1:指向内存中下一个指针位置
    • &p[0] + 1:等同于p + 1,指向第二个字符
  4. 解引用操作

    • *pp[0]都得到第一个字符'a',大小为1字节

2.2.6 代码练习六

cpp 复制代码
#include<stdio.h>
#include<string.h> 
int main()
{
	char* p = "abcdef";
	printf("%zu\n", strlen(p));
	printf("%zu\n", strlen(p + 1));
	printf("%zu\n", strlen(&p));
	printf("%zu\n", strlen(&p + 1));
	printf("%zu\n", strlen(&p[0] + 1));
	return 0;
}

运行结果如下:

cpp 复制代码
6   // strlen(p)
5   // strlen(p+1)
随机值或崩溃  // strlen(&p)
随机值或崩溃  // strlen(&p+1)
5   // strlen(&p[0]+1)

注释解析如下

cpp 复制代码
#include<stdio.h>
#include<string.h> 

int main()
{
    // 定义一个字符指针p,指向字符串常量"abcdef"
    // 字符串常量会自动以'\0'结尾
    char* p = "abcdef";
    
    // 1. 计算字符串p的长度
    printf("%zu\n", strlen(p));
    // p指向字符串"abcdef"的首地址
    // strlen从'a'开始计算,直到遇到'\0'为止
    // 结果是6(a b c d e f 共6个字符)

    // 2. 计算从p+1位置开始的字符串长度
    printf("%zu\n", strlen(p + 1));
    // p+1指向字符串的第二个字符'b'
    // strlen从'b'开始计算,直到'\0'
    // 结果是5(b c d e f 共5个字符)

    // 3. 计算从指针p的地址开始的"字符串"长度
    printf("%zu\n", strlen(&p));
    // &p是指针变量p本身的地址,不是字符串地址
    // strlen会将指针地址当作字符数组起始地址
    // 这是未定义行为,结果不可预测
    // 可能返回随机值或导致程序崩溃

    // 4. 计算从&p+1位置开始的"字符串"长度
    printf("%zu\n", strlen(&p + 1));
    // &p+1是指向p变量之后的内存位置
    // 同样不是有效的字符串地址
    // 未定义行为,结果不可预测

    // 5. 计算从第二个字符地址开始的字符串长度
    printf("%zu\n", strlen(&p[0] + 1));
    // &p[0]是字符串首字符'a'的地址
    // &p[0]+1是第二个字符'b'的地址
    // 等同于p+1,结果是5

    return 0;
}

关键点总结:

  1. 字符串常量

    • "abcdef"是字符串常量,自动以\0结尾
    • strlen可以正确计算其长度
  2. 指针操作

    • p指向字符串首地址
    • p+1指向第二个字符
    • &p指针变量的地址,不是字符串地址
  3. 安全与危险操作

    • 安全操作:pp+1等指向字符串内部的指针
    • 危险操作:&p&p+1不是字符串地址,会导致未定义行为

2.3 二维数组

cpp 复制代码
#include<stdio.h>
int main()
{
	int a[3][4] = { 0 };
	printf("%zu\n", sizeof(a));
	printf("%zu\n", sizeof(a[0][0]));
	printf("%zu\n", sizeof(a[0]));
	printf("%zu\n", sizeof(a[0] + 1));
	printf("%zu\n", sizeof(*(a[0] + 1)));
	printf("%zu\n", sizeof(a + 1));
	printf("%zu\n", sizeof(*(a + 1)));
	printf("%zu\n", sizeof(&a[0] + 1));
	printf("%zu\n", sizeof(*(&a[0] + 1)));
	printf("%zu\n", sizeof(*a));
	printf("%zu\n", sizeof(a[3]));
}

典型输出(64位系统):

cpp 复制代码
48  // 整个数组
4   // 单个元素
16  // 一行
8   // 指针
4   // 元素
8   // 指针
16  // 一行
8   // 指针
16  // 一行
16  // 一行
16  // 一行(编译时计算)

注释解析如下:

cpp 复制代码
#include<stdio.h>
int main()
{
    int a[3][4] = { 0 };  // 定义一个3行4列的二维整型数组
    
    printf("%zu\n", sizeof(a));      // 1. 整个数组的大小:3×4×4字节 = 48字节
    
    printf("%zu\n", sizeof(a[0][0]));// 2. 单个元素的大小:int类型 = 4字节
    
    printf("%zu\n", sizeof(a[0]));   // 3. 第一行的大小:4个int = 16字节
                                     // a[0]是第一行的数组名,不会退化为指针
    
    printf("%zu\n", sizeof(a[0] + 1));// 4. 第一行第二个元素的地址:指针大小 = 8字节
                                     // a[0]退化为指针,+1指向下一个元素
    
    printf("%zu\n", sizeof(*(a[0] + 1)));// 5. 第一行第二个元素的大小:int = 4字节
    
    printf("%zu\n", sizeof(a + 1));  // 6. 第二行的地址:指针大小 = 8字节
                                    // a退化为指向第一行的指针,+1指向第二行
    
    printf("%zu\n", sizeof(*(a + 1)));// 7. 第二行的大小:4个int = 16字节
                                     // *(a+1)等价于a[1]
    
    printf("%zu\n", sizeof(&a[0] + 1));// 8. 第二行的地址:指针大小 = 8字节
                                      // &a[0]是第一行的地址,+1指向第二行
    
    printf("%zu\n", sizeof(*(&a[0] + 1)));// 9. 第二行的大小:4个int = 16字节
                                         // *(&a[0]+1)等价于a[1]
    
    printf("%zu\n", sizeof(*a));     // 10. 第一行的大小:4个int = 16字节
                                    // *a等价于a[0]
    
    printf("%zu\n", sizeof(a[3]));   // 11. 第四行的大小:4个int = 16字节
                                    // 虽然a只有3行,但sizeof是编译时操作
                                    // 不会实际访问a[3]
    
    return 0;
}

关键点说明:

  1. 二维数组的内存布局int a[3][4]在内存中是按行连续存储的12个int。

  2. 数组名退化规则

    • a在大多数情况下退化为指向第一行的指针(类型为int(*)[4]
    • a[0]在大多数情况下退化为指向第一个元素的指针(类型为int*
  3. sizeof的特殊性

    • sizeof(a)sizeof(a[0])中,数组名不会退化为指针
    • sizeof是编译时操作,不会实际计算表达式
  4. 指针运算

    • a + 1&a[0] + 1都指向第二行
    • a[0] + 1指向第一行的第二个元素

三. 指针运算笔试题解析

注: 以下试题请先思考

3.1 试题练习一

cpp 复制代码
#include <stdio.h>
int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };
    int* ptr = (int*)(&a + 1);  // 关键点1
    printf("%d,%d", *(a + 1), *(ptr - 1));  // 关键点2
    return 0;
}

关键点解析:

  1. &a + 1:

    • a是一个int数组,&a的类型是int (*)[5](指向包含5个int的数组的指针)
    • 当对数组指针进行+1运算时,会跳过整个数组的大小(5*sizeof(int))
    • 所以&a + 1指向数组末尾之后的位置
  2. 指针转换:

    • (int*)(&a + 1)将这个数组指针强制转换为普通的int指针
    • 现在ptr是一个指向int的指针,指向数组末尾之后的位置
  3. 输出表达式:

    • *(a + 1):
      • a在表达式中退化为指向首元素的指针(int*)
      • a + 1指向第二个元素(值为2)
      • 所以*(a + 1)输出2
    • *(ptr - 1):
      • ptr指向数组末尾之后的位置
      • ptr - 1回退一个int的大小,指向最后一个元素5
      • 所以*(ptr - 1)输出5

3.2 试题练习二

cpp 复制代码
#include<stdio.h>
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);            // 关键点1
	printf("%p\n", (unsigned long)p + 0x1);  // 关键点2
	printf("%p\n", (unsigned int*)p + 0x1);  // 关键点3
	return 0;
}

关键点解析:

  1. 结构体大小计算

    • struct Test的大小取决于内存对齐(假设在32位系统上):
      • int Num:4字节
      • char* pcName:4字节(指针)
      • short sDate:2字节
      • char cha[2]:2字节
      • short sBa[4]:8字节(4个short)
    • 总大小:4 + 4 + 2 + 2 + 8 = 20字节(假设编译器没有额外填充)
  2. p + 0x1

    • pstruct Test*类型,指针算术以结构体大小为步长
    • p + 1会跳过整个结构体的大小(20字节)
    • 所以p + 0x1 = 0x100000 + 0x14(20的十六进制)= 0x100014
  3. (unsigned long)p + 0x1

    • 将指针p强制转换为unsigned long,变成普通整数
    • 整数加法直接加1:0x100000 + 0x1 = 0x100001
  4. (unsigned int*)p + 0x1

    • p强制转换为unsigned int*(指向4字节int的指针)
    • 指针算术以unsigned int大小(4字节)为步长
    • (unsigned int*)p + 1 = 0x100000 + 0x4 = 0x100004

3.3 试题练习三

cpp 复制代码
#include <stdio.h>
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };  // 关键点1:注意这里用的是逗号表达式
    int* p;
    p = a[0];  // 关键点2:获取第一行的首地址
    printf("%d", p[0]);  // 关键点3:输出p[0]
    return 0;
}

关键点解析:

  1. 数组初始化

    • 表面上看是初始化一个3行2列的二维数组

    • 但实际上使用的是逗号表达式而不是常规的花括号初始化

    • 在C语言中,(x, y)是逗号表达式,其值为最后一个表达式的值

    • 所以实际初始化值为:

      • (0, 1) → 1
      • (2, 3) → 3
      • (4, 5) → 5
    • 由于只提供了3个值,但数组需要6个值(3×2),剩余元素会被初始化为0

    • 实际数组内容为:

      cpp 复制代码
      a[0][0] = 1
      a[0][1] = 3
      a[1][0] = 5
      a[1][1] = 0  (自动补0)
      a[2][0] = 0  (自动补0)
      a[2][1] = 0  (自动补0)
  2. 指针赋值

    • a[0]表示二维数组的第一行(即{1, 0}
    • p = a[0]将p指向第一行的第一个元素
    • 输出p[0]
      • p[0]等价于*(p+0),即第一个元素的值
      • 根据上面的初始化,a[0][0]的值为1

3.4 试题练习四

cpp 复制代码
#include <stdio.h>
int main()
{
    int a[5][5];          // 定义一个5x5的二维数组
    int(*p)[4];           // 定义一个指向包含4个int的数组的指针
    p = a;     // 关键点1:将a强制转换为指向4元素数组的指针
    printf("%p,%d\n", 
           &p[4][2] - &a[4][2],  // 关键点2:指针相减
           &p[4][2] - &a[4][2]);  // 关键点3:结果转换为整数
    return 0;
}

关键点解析:

  1. 指针类型转换

    • aint[5][5]类型,&a[0]int(*)[5](指向5个int的指针)
    • pint(*)[4](指向4个int的指针)
    • p = (int(*)[4])a将5列的数组指针强制转换为4列的指针
  2. 指针运算

    • p[4]相当于*(p + 4),会跳过4 * sizeof(int[4]) = 16个int
    • a[4][2]是第4行第2列元素(从0开始计数)
  3. 地址计算

    • &p[4][2] = a + 4*4 + 2 = a + 18(int单位)
    • &a[4][2] = a + 4*5 + 2 = a + 22(int单位)
    • &p[4][2] - &a[4][2] = (a + 18) - (a + 22) = -4
  4. 输出格式

    • %p会以指针形式输出-4(即0xFFFFFFFC,补码表示)
    • %d直接输出-4

总结:

  • 指针类型转换导致指针算术的步长不同(4 vs 5)
  • 指针相减的结果是元素间隔数(-4表示p[4][2]在a[4][2]前面4个int位置)
  • 这个例子展示了指针类型对算术运算的影响

3.5 试题练习五

cpp 复制代码
#include <stdio.h>
int main()
{
    int aa[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };  // 初始化2行5列的二维数组
    int* ptr1 = (int*)(&aa + 1);  // 关键点1
    int* ptr2 = (int*)(*(aa + 1)); // 关键点2
    printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));  // 关键点3
    return 0;
}

关键点解析:

  1. 数组初始化

    • aa是一个2行5列的二维数组,内存布局如下:

      cpp 复制代码
      aa[0][0] = 1
      aa[0][1] = 2
      aa[0][2] = 3
      aa[0][3] = 4
      aa[0][4] = 5
      aa[1][0] = 6
      aa[1][1] = 7
      aa[1][2] = 8
      aa[1][3] = 9
      aa[1][4] = 10
  2. ptr1的赋值

    • &aa是"指向整个二维数组的指针",类型是int (*)[2][5]
    • &aa + 1会跳过整个数组(2×5=10个int),指向数组末尾之后的位置
    • (int*)强制转换为int指针,所以ptr1指向数组末尾之后的位置
  3. ptr2的赋值

    • aa + 1aa会退化为指向第一行的指针(类型int (*)[5]
    • aa + 1指向第二行(即{6,7,8,9,10}
    • *(aa + 1)解引用得到第二行(类型int [5],会退化为指向第一个元素的指针)
    • (int*)强制转换是多余的,但结果不变
  4. 输出表达式

    • *(ptr1 - 1)
      • ptr1指向数组末尾之后
      • ptr1 - 1回退一个int,指向最后一个元素aa[1][4](值为10)
    • *(ptr2 - 1)
      • ptr2指向第二行第一个元素aa[1][0](值为6)
      • ptr2 - 1回退一个int,指向aa[0][4](值为5)

总结:

  • &aa + 1跳过整个二维数组
  • aa + 1跳过一行(5个int)
  • 指针运算时要注意指针的类型和步长
  • 输出结果是最后一个元素10和第一行最后一个元素5

3.6 试题练习六

cpp 复制代码
#include <stdio.h>
int main()
{
    char* a[] = { "work","at","alibaba" };  // 关键点1:指针数组
    char** pa = a;                          // 关键点2:二级指针指向数组首元素
    pa++;                                   // 关键点3:指针移动
    printf("%s\n", *pa);                    // 关键点4:解引用输出
    return 0;
}

关键点解析:

  1. 指针数组 char* a[]:

    • a是一个数组,包含3个char*类型的指针元素
    • 每个指针指向一个字符串常量:
      • a[0]指向"work"
      • a[1]指向"at"
      • a[2]指向"alibaba"
  2. 二级指针 char** pa = a:

    • a在表达式中退化为指向首元素的指针(即char**类型)
    • pa指向a[0],也就是指向"work"的指针
  3. 指针运算 pa++:

    • pachar**类型,pa++会使指针移动一个char*的大小(通常4或8字节)
    • 移动后pa指向a[1],即指向"at"的指针
  4. 解引用输出 *pa:

    • *pa获取pa当前指向的值,即a[1](指向"at"的指针)
    • printf使用%s格式打印该指针指向的字符串

总结:

  • 指针数组a存储的是指向字符串常量的指针
  • 二级指针pa初始指向数组的第一个元素
  • pa++使指针移动到数组的第二个元素
  • *pa解引用得到指向"at"的指针,%s打印出字符串"at"

这个例子展示了指针数组和二级指针的配合使用,以及指针运算如何遍历指针数组。

3.7 试题练习七

cpp 复制代码
#include <stdio.h>
int main()
{
    // 初始化字符串指针数组
    char* c[] = { "ENTER","NEW","POINT","FIRST" };
    // 初始化指向c数组元素的指针数组
    char** cp[] = { c + 3, c + 2, c + 1, c };
    // 三级指针指向cp数组
    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;
}

初始内存布局:

  1. 字符串数组:

    cpp 复制代码
    c[0] -> "ENTER"
    c[1] -> "NEW"
    c[2] -> "POINT"
    c[3] -> "FIRST"
  2. 指针数组cp:

    cpp 复制代码
    cp[0] = c + 3 -> &c[3] -> "FIRST"
    cp[1] = c + 2 -> &c[2] -> "POINT"
    cp[2] = c + 1 -> &c[1] -> "NEW"
    cp[3] = c     -> &c[0] -> "ENTER"
  3. 三级指针:

    cpp 复制代码
    cpp -> cp

逐步解析:

第一次输出:**++cpp

  1. ++cpp:cpp先自增,现在指向cp[1]

  2. *cpp:解引用得到cp[1] -> c + 2

  3. **cpp:再次解引用得到c[2] -> "POINT"

  4. 输出:POINT

第二次输出:*-- * ++cpp + 3

  1. ++cpp:cpp自增,现在指向cp[2]

  2. *cpp:解引用得到cp[2] -> c + 1

  3. -- *cpp:c + 1减1变为c + 0

  4. *-- *cpp:解引用得到c[0] -> "ENTER"

  5. +3:指针运算,指向"ENTER"的第3个字符'E'之后

  6. 输出:ER(从第3个字符开始)

第三次输出:*cpp[-2] + 3

  1. cpp[-2]:相当于*(cpp - 2),即cp[0] -> c + 3

  2. *cpp[-2]:解引用得到c[3] -> "FIRST"

  3. +3:指针运算,指向"FIRST"的第3个字符'R'之后

  4. 输出:ST(从第3个字符开始)

第四次输出:cpp[-1][-1] + 1

  1. cpp[-1]:相当于*(cpp - 1),即cp[1] -> c + 2

  2. cpp[-1][-1]:相当于*(*(cpp - 1) - 1) -> *(c + 2 - 1) -> c[1] -> "NEW"

  3. +1:指针运算,指向"NEW"的第1个字符'N'之后

  4. 输出:EW(从第1个字符开始)

关键点总结:

  1. 多级指针的解引用需要从外向内逐步分析

  2. 指针运算会改变指针的指向位置

  3. 数组下标访问和指针运算可以互相转换(如cpp[-1]等价于*(cpp - 1)

  4. 字符串指针加上偏移量会从指定位置开始输出

这个例子展示了C语言中复杂指针操作的强大能力,但也显示了这类代码容易造成混淆的特点。在实际开发中,建议使用更清晰的表达方式。


本篇内容到此结束 如果对你有所帮助 希望能一键三连 谢谢

往期回顾:

《初探指针世界:揭开内存管理与编程优化的第一篇章》 -----指针一

《C 语言指针进阶:const 修饰、断言机制与传址调用深度解析》----指针二

《C 语言指针高级指南:字符、数组、函数指针的进阶攻略》 ----指针三

《从回调函数到 qsort:C 语言指针高级应用全攻略》----指针四

相关推荐
liuzhangfeiabc19 分钟前
[luogu12541] [APIO2025] Hack! - 交互 - 构造 - 数论 - BSGS
c++·算法·题解
平和男人杨争争2 小时前
山东大学计算机图形学期末复习15——CG15
人工智能·算法·计算机视觉·图形渲染
爱coding的橙子3 小时前
每日算法刷题Day11 5.20:leetcode不定长滑动窗口求最长/最大6道题,结束不定长滑动窗口求最长/最大,用时1h20min
算法·leetcode·职场和发展
WenGyyyL3 小时前
力扣热题——零数组变换 |
算法·leetcode·职场和发展·蓝桥杯
芯眼3 小时前
AMD Vivado™ 设计套件生成加密比特流和加密密钥
算法·fpga开发·集成测试·软件工程
咪嗷喵挖藕哇3 小时前
leetcode 合并区间 java
java·算法·leetcode
沐风ya3 小时前
leetcode每日一题 -- 3355. 零数组变换 I
算法·leetcode
纪伊路上盛名在3 小时前
leetcode字符串篇【公共前缀】:14-最长公共前缀
python·算法·leetcode
JK0x073 小时前
代码随想录算法训练营 Day52 图论Ⅲ 岛屿问题Ⅱ 面积 孤岛 水流 造岛
算法·深度优先·图论
Hygge-star4 小时前
【算法】定长滑动窗口5.20
java·数据结构·算法·学习方法·代码规范