C语言指针与数组进阶:sizeof、strlen详解及笔试题实战

周叙
周叙
发布时间:2026-10-16| 5人浏览过
原创

C语言学习者在面对指针和数组时,往往容易混淆其底层内存模型与运算符优先级。本文系统梳理了sizeof与strlen的本质区别,并通过整型、字符型数组及二维数组的实例,拆解数组名作为地址与整体大小的边界条件。结合包含结构体对齐、大小端存储及逗号表达式的经典笔试题,提供从理论到实战的完整解析。

核心概念:sizeof 与 strlen

理解指针与数组的关键在于区分两个常用计算长度的工具:

  • sizeof:是C语言的一个操作符,用于判断对象或类型所占用的字节数。它不关心数据类型本身的语义(如是否以\0结尾),只关注内存布局。
  • strlen:是<string.h>库函数,专门计算以空字符'\0'结尾的字符串长度。它从给定地址开始向后查找,直到遇见第一个\0停止,返回值不含\0自身。

数组名的特殊行为

通常情况下,数组名表示指向首元素的地址。但在以下两种特殊情况中,数组名代表整个数组:

  1. 作为 sizeof 的唯一操作数时(例如 sizeof(arr))。
  2. 使用取地址符 & 作用于数组名时(例如 &arr)。

除上述情况外,任何算术运算或解引用操作都会让数组名退化为指向首元素的指针。

第1步:一维整型数组的sizeof分析

针对代码:int a[] = {1,2,3,4};

#include<stdio.h>
int main()
{
    int a[] = {1,2,3,4};
    printf("%d\n", sizeof(a));       // 结果: 16 (4 * 4字节)
    printf("%d\n", sizeof(a + 0));   // 结果: 8 (a退化为指针, 64位为8字节)
    printf("%d\n", sizeof(*a));      // 结果: 4 (首元素int的大小)
    printf("%d\n", sizeof(a[1]));    // 结果: 4 (第二个元素int的大小)
    printf("%d\n", sizeof(&a));      // 结果: 8 (整个数组的地址)
    printf("%d\n", sizeof(*&a));     // 结果: 16 (*&a等价于a, 整个数组)
    printf("%d\n", sizeof(&a + 1)); // 结果: 8 (&a+1跳过整个数组后的地址)
    return 0;
}

解析要点: - sizeof(a) 计算的是整个数组占用空间,即 4个元素 × 4字节 = 16。 - a+0 中 a 被解释为首元素地址(指针),因此结果是指针长度(32位下为4,64位下为8)。 - &a 是整个数组的地址,类型为 int (*)[4]。对其进行 +1 操作会跳过整个数组(16字节),但指针本身的大小依然是机器字长。

第2步:一维字符数组与strlen陷阱

分两种情况讨论:

情况A:无 \0 的初始化

char arr[] = {'a','b','c','d'};
// sizeof(arr) 结果为 4
// strlen(arr) 结果未定义/随机值,因为找不到 '\0' 终止符

情况B:字符串字面量初始化

char arr[] = "abcdef";
// sizeof(arr) 结果为 7 ('a'到'f'共6个字符 + 末尾隐藏的 '\0')
// strlen(arr) 结果为 6 (只统计 '\0' 之前的字符)

注意: 如果传入 arr 给 strlen,它会读取直到内存中偶然出现的 \0。 若尝试执行 strlen(*arr),由于 *arr 返回的是字符 'a'(ASCII码 97),函数会将 97 视为非法内存地址从而引发段错误(Segmentation Fault)。

第3步:指针变量的sizeof与strlen

针对代码:char *p = "abcdef";

printf("%d\n", sizeof(p));        // 结果: 8 (指针变量本身的大小)
printf("%d\n", sizeof(*p));       // 结果: 1 (指向的内容 'a' 的大小)
printf("%d\n", strlen(p));        // 结果: 6 (字符串有效长度)
printf("%d\n", strlen(p + 1));    // 结果: 5 (从 'b' 开始算,长度为5)
printf("%d\n", strlen(&p));       // 结果: 随机值 (&p是指针变量的地址,非字符串开头)

解析要点: - p 只是一个存放地址的变量,无论指向哪里,sizeof(p) 始终等于指针类型的字节大小。 - &p 获取的是这个指针变量在栈上的内存地址,将其传给 strlen 会导致从头遍历该变量周围的内存直到遇到 \0,结果不可预测。

第4步:二维数组的层级推导

针对代码:int a[3][4] = {0};

二维数组可以看作“数组的数组”。这里 a 是由3个一维数组(每个含4个int)组成的。

printf("%d\n", sizeof(a));           // 结果: 48 (3行 * 4列 * 4字节)
printf("%d\n", sizeof(a[0]));        // 结果: 16 (第一行的完整大小, 4*4)
printf("%d\n", sizeof(*(a+0)));      // 结果: 16 (同 a[0])
printf("%d\n", sizeof(a[0]+1));      // 结果: 8 (a[0]退化为&a[0][0]的地址, +1后仍是地址)
printf("%d\n", sizeof(*(a+1)));      // 结果: 16 (第二行&a[1][0]的地址解引用, 即a[1]的大小)
printf("%d\n", sizeof(a[3]));        // 结果: 16 (类型推断: a[3]是第四行的类型, 即使越界不访问)
printf("%d\n", sizeof(*a));          // 结果: 16 (*a等同于a[0])

易错点: - a+1 跳过一行(16字节),得到第二行的地址,其类型为 int(*)[4]。 - a[3] 虽然索引超出了实际分配的3行(0,1,2),但在编译阶段编译器能推导出它的类型是 int[4],因此 sizeof 依然合法并返回16。

指针进阶笔试题实战解析

笔试题1:数组指针运算

int a[5] = { 1, 2, 3, 4, 5 };
int *ptr = (int *)(&a + 1);
printf( "%d,%d", *(a + 1), *(ptr - 1));

输出结果: 2,5

推导: 1. *(a + 1):标准的首元素后移一位,指向元素2,解引用得 2。 2. (int *)(&a + 1):&a+1 跳过整个数组(20字节),此时 ptr 位于数组末尾之后。强制转换回 int* 后,ptr-1 向前移动一个 int(4字节),刚好指向最后一个元素 5。

笔试题2:类型强制转换与指针步长

struct Test { /* ... */ }*p; // 假设结构体大小为20字节
p = (struct Test*)0x100000;
printf("%p\n", p + 0x1);           
printf("%p\n", (unsigned long)p + 0x1); 
printf("%p\n", (unsigned int*)p + 0x1); 

输出结果: 1. 0x100014(16进制)。结构体指针加1,步长为结构体大小20字节(0x14)。 2. 0x100001。强转为整数,按整数值直接加1。 3. 0x100004。强转为 int*,步长为4字节。

笔试题3:混合运算与大小端存储

int a[4] = { 1, 2, 3, 4 };
int *ptr1 = (int *)(&a + 1);
int *ptr2 = (int *)((int)a + 1);
printf( "%x,%x", ptr1[-1], *ptr2);

输出结果: 4,2000000(基于小端模式)

推导: 1. ptr1[-1]:同理题1,ptr1 指向数组尾,-1 回到末尾元素 4。 2. *ptr2:(int)a 将地址视为整数相加1,再转回指针。这导致 ptr2 的起始地址比原数组起始地址多偏移了1个字节。 - 在小端模式下,整数 1 的十六进制表示为 01 00 00 00。 - 正常 ptr 读出的字节是 01 00 00 00(值为1)。 - ptr2 偏移1字节后,读取到的字节组合变为 00 00 00 02(假设下一个数是2,或者仅看当前数值的字节移位效果,此处需结合具体内存布局)。更准确地说,对于数字1(0x00000001),字节排列是小端序(LSB在前):01 00 00 00。ptr2 指向第二个字节 00。当它作为一个完整的 int 被读取时,它包含的是当前剩余部分 00 00 00 以及下一个元素 2 的低位 02。
根据图示逻辑,通常考查的是移位截断效应。若内存连续,读取内容可能受后续元素影响。本题标准答案通常对应 0x2000000 是基于特定的大/小端判定及数值排列,此处以图中提供的解析为准:ptr2 指向错位字节,组合出该结果。

笔试题4:逗号表达式陷阱

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

输出结果: 1

推导: 圆括号 () 内的 0, 1 是逗号表达式,其结果为最后一个值 1。因此数组初始化为 {1, 3, 5},即第一行是 {1, 3},第二行 {3, 5},第三行... 实际上只分配了三个子数组元素的位置,值为 {1}, {3}, {5} (每行缺省补0?不,初始化列表顺序覆盖)。 修正:数组 a[3][2] 被依次赋值为: - a[0][0]=1, a[0][1]=3 (取自 (0,1)的结果1, 和 (2,3)的结果3) - 等等,让我们仔细检查C初始化规则。 { (0,1), (2,3), (4,5) } 包含了3个逗号表达式。结果分别是1, 3, 5。 数组展开:a[0][0]=1, a[0][1]=3, a[1][0]=5, a[1][1]=0... ? 原文解释指出数组即为 {1, 3, 5} 填充入 3x2 区域,即 a[0][0]=1。所以 p[0] 为 1。

笔试题5:不同类型的数组指针运算

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]);

输出结果: FFFFFFFC,-4(取决于具体实现,通常为差值)

推导: 这是一个经典的“错位”问题。 1. a 是 int[5][5],每一行跨度为 5个int(20字节)。 2. p 是 int (*)[4],每一行跨度为 4个int(16字节)。 3. 当赋值 p=a 时,它们指向同一内存地址(a[0][0])。 4. &p[4][2] 相当于从起点前进 4*p的行宽 + 2 个单位(其中单位是4个int)。偏移量 = 4 * 16字节 + 8字节 = 72字节。 5. &a[4][2] 相当于从起点前进 4*a的行宽 + 2 个单位(其中单位是5个int)。偏移量 = 4 * 20字节 + 8字节 = 88字节。 6. 内存地址差值 = 72 - 88 = -16 字节。 7. 指针减法结果以指针类型的大小为单位。左侧用的是 p 的类型(int[4]),右侧减法的右操作数决定最终结果的显示类型吗?不,通常是两者之一。在打印时,由于 &p[4][2] 参与计算,且题目通常考察的是相对于 p 步长的位移。 -16字节 / 4字节(每步) = -4。十六进制显示为 0xFFFFFFFC(补码)。

以上就是C语言指针与数组进阶详解的详细内容,更多关于C语言指针数组基础教程的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。