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

发布于:2025-05-21 ⋅ 阅读:(24) ⋅ 点赞:(0)

目录

一. sizeof 和 strlen 的对比

1.1 sizeof 

1.2 strlen

1.3 对比表格

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

2.1 一维数组

2.2 字符数组

2.2.1  代码练习一

2.2.2  代码练习二

2.2.3  代码练习三

2.2.4  代码练习四

2.2.5  代码练习五

2.2.6  代码练习六

2.3 二维数组

三. 指针运算笔试题解析

3.1 试题练习一

3.2 试题练习二

3.3 试题练习三

3.4 试题练习四

3.5 试题练习五

3.6 试题练习六

3.7 试题练习七


一. sizeof 和 strlen 的对比

1.1 sizeof 

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

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

#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’结尾的有效字符串指针
    strlen("hello");  // 返回5
    
  • 遇到第一个’\0’停止计数
    strlen("hel\0lo");  // 返回3

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

#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 一维数组

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

#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位系统):

16  // 整个数组大小
8   // 指针大小
4   // int大小
8   // 指针大小
4   // int大小
8   // 指针大小
16  // 整个数组大小
8   // 指针大小
8   // 指针大小
8   // 指针大小

注释讲解如下

#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  代码练习一

#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位系统):

6  // 整个数组大小
8  // 指针大小
1  // char大小
1  // char大小
8  // 指针大小
8  // 指针大小
8  // 指针大小

注释解析如下

#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  代码练习二

#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位系统):

36//随机值
36//随机值
36//随机值
30//随机值-6
35//随机值-1

注释解析如下:

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  代码练习三

#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位系统):

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

注释解析如下:

#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  代码练习四

#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位系统):

6
6
6
26
5

注释解析如下:

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  代码练习五

#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位系统):

8  // 指针大小
8  // 指针大小
1  // char大小
1  // char大小
8  // 指针大小
8  // 指针大小
8  // 指针大小

注释解析如下:

#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  代码练习六

#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;
}

    运行结果如下:

    6   // strlen(p)
    5   // strlen(p+1)
    随机值或崩溃  // strlen(&p)
    随机值或崩溃  // strlen(&p+1)
    5   // strlen(&p[0]+1)

    注释解析如下

    #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 二维数组

    #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位系统):

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

    注释解析如下:

    #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 试题练习一

    #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 试题练习二

    #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 试题练习三

    #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
      • 实际数组内容为:
        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 试题练习四

    #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 试题练习五

    #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列的二维数组,内存布局如下:
        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 试题练习六

    #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 试题练习七

    #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. 字符串数组:

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

      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 -> 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 语言指针高级应用全攻略》----指针四


    网站公告

    今日签到

    点亮在社区的每一天
    去签到