指针与数组的 sizeof、strlen 计算,以及多级指针运算,一直是 C 语言面试和笔试中的高频考点,也是很多初学者容易混淆的地方。本文将通过经典例题+底层原理+内存布局的方式,带你彻底搞懂这些容易踩坑的知识点,让你在遇到类似题目时不再困惑。
一、核心知识点对比
- sizeof 与 strlen 的区别
|----|----------------|-----------------------|
| 特性 | sizeof | strlen |
| 本质 | 操作符 | 库函数(需包含 <string.h>) |
| 功能 | 计算操作数占用内存的字节大小 | 统计字符串中\0之前的字符个数 |
| 关注 | 只关心内存大小,不关心内容 | 必须找到\0,否则会越界访问 |
cpp
#include <stdio.h>
#include <string.h> // 注意:使用 strlen 必须包含此头文件
int main()
{
char arr1[3] = {'a', 'b', 'c'};
char arr2[] = "abc";
printf("%d\n", strlen(arr1)); // 随机值(arr1 中没有 '\0',越界查找)
printf("%d\n", strlen(arr2)); // 3("abc" 末尾自动带 '\0')
printf("%d\n", sizeof(arr1)); // 3(数组有 3 个 char 元素)
printf("%d\n", sizeof(arr2)); // 4(包含末尾的 '\0',共 4 字节)
return 0;
}
二、数组名的意义
-
sizeof(数组名):数组名代表整个数组,计算整个数组的内存大小。
-
&数组名:数组名代表整个数组,取出的是整个数组的地址。
-
其他场景:数组名仅代表首元素的地址(如数组名+1、函数传参)。
一维数组:
cpp
#include <stdio.h>
int main()
{
int a[] = {1,2,3,4};
// 数组名a作为sizeof的直接参数,代表整个数组,4个int×4字节=16
printf("%d\n",sizeof(a)); // 16
// a+0中,a代表首元素地址,a+0仍然是地址,指针大小为4/8
// 在32位系统中指针占4字节,64位系统中占8字节 → 结果为 4/8。
printf("%d\n",sizeof(a+0)); // 4/8
// *a是首元素,类型为int,占4字节
printf("%d\n",sizeof(*a)); // 4
// a+1是第二个元素的地址,指针大小为4/8
printf("%d\n",sizeof(a+1)); // 4/8
// a[1]是第二个元素,类型为int,占4字节
printf("%d\n",sizeof(a[1])); // 4
// &a是整个数组的地址,指针大小为4/8
printf("%d\n",sizeof(&a)); // 4/8
// *&a等价于a,代表整个数组,大小为16
printf("%d\n",sizeof(*&a)); // 16
// &a+1是跳过整个数组后的地址,指针大小为4/8
printf("%d\n",sizeof(&a+1)); // 4/8
// &a[0]是首元素的地址,指针大小为4/8
printf("%d\n",sizeof(&a[0])); // 4/8
// &a[0]+1是第二个元素的地址,指针大小为4/8
printf("%d\n",sizeof(&a[0]+1)); // 4/8
return 0;
}
三、逐段代码解析与运行结果
1. 基础 sizeof 示例
cpp
#include <stdio.h>
int main()
{
int a = 10;
printf("%d\n", sizeof(a)); // 4(int类型占4字节)
printf("%d\n", sizeof a); // 4(sizeof是操作符,括号可省略)
printf("%d\n", sizeof(int)); // 4(直接计算类型大小)
return 0;
}
2. 字符数组 sizeof vs strlen
代码1:sizeof 作用于无 \0 的数组{'a','b','c','d','e','f'}
cpp
#include <stdio.h>
int main()
{
char arr[] = {'a','b','c','d','e','f'};
// arr作为sizeof的直接参数,代表整个数组,6个char元素×1字节=6
printf("%d\n", sizeof(arr)); // 6
// arr+0中,arr代表首元素地址,arr+0仍然是地址,指针大小为4/8
printf("%d\n", sizeof(arr+0)); // 4/8
// *arr是首元素,类型为char,占1字节
printf("%d\n", sizeof(*arr)); // 1
// arr[1]是第二个元素,类型为char,占1字节
printf("%d\n", sizeof(arr[1])); // 1
// &arr是整个数组的地址,指针大小为4/8
printf("%d\n", sizeof(&arr)); // 4/8
// &arr+1是跳过整个数组后的地址,指针大小为4/8
printf("%d\n", sizeof(&arr+1)); // 4/8
// &arr[0]+1是第二个元素的地址,指针大小为4/8
printf("%d\n", sizeof(&arr[0]+1)); // 4/8
return 0;
}
代码2:strlen 作用于无 \0 的数组{'a','b','c','d','e','f'}
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = {'a','b','c','d','e','f'};
// strlen从arr首地址开始找'\0',但数组里没有'\0',会一直越界读取直到遇到内存中的'\0',结果随机
printf("%d\n", strlen(arr)); // 随机值
// arr+0等价于arr,同样从首地址开始找'\0',无'\0',越界读取,结果随机
printf("%d\n", strlen(arr+0)); // 随机值
// *arr是字符'a'(ASCII值97),strlen会把97当作内存地址去访问,这是非法地址,程序会崩溃
printf("%d\n", strlen(*arr)); // 错误(程序崩溃)
// arr[1]是字符'b'(ASCII值98),同样被当作非法地址访问,程序会崩溃
printf("%d\n", strlen(arr[1])); // 错误(程序崩溃)
// &arr是数组的地址,和首元素地址等价,同样无'\0',越界读取,结果随机
printf("%d\n", strlen(&arr)); // 随机值
// &arr+1指向数组末尾之后的地址,从这里开始找'\0',无'\0',越界读取,结果随机
printf("%d\n", strlen(&arr+1)); // 随机值
// &arr[0]+1指向第二个元素的地址,从这里开始找'\0',无'\0',越界读取,结果随机
printf("%d\n", strlen(&arr[0]+1)); // 随机值
return 0;
}
运行结果:前两行输出随机值,后续行会触发程序崩溃(非法地址访问)。
代码3:sizeof 作用于带 \0 的数组"abcdef"
cpp
#include <stdio.h>
int main()
{
char arr[] = "abcdef"; // 字符串常量末尾自动加'\0',数组共7个元素
printf("%d\n", sizeof(arr)); // 7(sizeof测整个数组,含a-f+末尾'\0',7×1字节)
printf("%d\n", sizeof(arr+0)); // 4/8(arr+0中arr是首元素地址,指针大小看系统位数)
printf("%d\n", sizeof(*arr)); // 1(*arr解引用首元素地址,取char类型首元素,占1字节)
printf("%d\n", sizeof(arr[1])); // 1(arr[1]是数组第2个char元素,char类型固定1字节)
printf("%d\n", sizeof(&arr)); // 4/8(&arr取整个数组地址,本质是指针类型,大小看系统)
printf("%d\n", sizeof(&arr+1)); // 4/8(&arr+1跳整个数组,仍为指针类型,大小不变)
printf("%d\n", sizeof(&arr[0]+1)); // 4/8(&arr[0]+1是第2个元素地址,指针大小看系统)
return 0;
}
代码4:strlen 作用于带 \0 的数组"abcdef"
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "abcdef"; // 字符串末尾自动补'\0',数组实际存a b c d e f \0
printf("%d\n", strlen(arr)); // 6(strlen统计'\0'前字符,a-f共6个,遇'\0'停止)
printf("%d\n", strlen(arr+0)); // 6(arr+0等价arr,首元素地址开始统计,同上得6)
printf("%d\n", strlen(*arr)); // 错误(*arr是'a',ASCII97,被当地址访问,非法崩溃)
printf("%d\n", strlen(arr[1])); // 错误(arr[1]是'b',ASCII98,强作地址,非法崩溃)
printf("%d\n", strlen(&arr)); // 6(&arr是数组地址,和首元素地址同起始位,统计到'\0'得6)
printf("%d\n", strlen(&arr+1)); // 随机值(&arr+1跳过整个数组,指向'\0'后,越界找'\0'结果不定)
printf("%d\n", strlen(&arr[0]+1)); // 5(从b开始统计,到'\0'前共b c d e f 5个字符)
return 0;
}
运行结果:
cpp
6
6
// 后续行触发程序崩溃
3. 字符指针 char *p = "abcdef"
代码5:sizeof 计算
cpp
#include <stdio.h>
int main()
{
char *p = "abcdef"; // p是字符指针,指向字符串常量首地址(a的地址)
printf("%d\n", sizeof(p)); // 4/8(p是指针变量,大小由系统位数决定)
printf("%d\n", sizeof(p+1)); // 4/8(p+1指向b,仍为指针类型,大小不变)
printf("%d\n", sizeof(*p)); // 1(*p解引用取首元素a,char类型占1字节)
printf("%d\n", sizeof(p[0])); // 1(p[0]等价*p,取首元素,char类型1字节)
printf("%d\n", sizeof(&p)); // 4/8(&p取指针p自身的地址,二级指针仍为指针)
printf("%d\n", sizeof(&p+1)); // 4/8(&p+1是指针p地址偏移后,依旧是指针类型)
printf("%d\n", sizeof(&p[0]+1)); // 4/8(&p[0]是a的地址,+1指向b,还是指针)
return 0;
}
代码6:strlen 计算
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char *p = "abcdef"; // p指向字符串常量首地址,末尾自带'\0'
printf("%d\n", strlen(p)); // 6(从p指向的a开始,统计到'\0'前共6个字符)
printf("%d\n", strlen(p+1)); // 5(p+1指向b,从b开始统计到'\0'共5个字符)
printf("%d\n", strlen(*p)); // 错误(*p是'a',ASCII值97,被当作地址访问,非法崩溃)
printf("%d\n", strlen(p[0])); // 错误(p[0]等价*p,是'a',强作地址,非法崩溃)
printf("%d\n", strlen(&p)); // 随机值(&p是指针p自身地址,指向内存无'\0',越界查找)
printf("%d\n", strlen(&p+1)); // 随机值(&p+1偏移指针p地址,指向区域无'\0',越界得随机值)
printf("%d\n", strlen(&p[0]+1)); // 5(&p[0]是a的地址,+1指向b,统计到'\0'共5个字符)
return 0;
}
运行结果:
cpp
6
5
// 后续行触发程序崩溃
4. 二维数组
cpp
#include <stdio.h>
int main()
{
int a[3][4] = {0}; // 3行4列int型二维数组,元素全初始化为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元素,int占4字节)
printf("%d\n",sizeof(a[0])); // 16(a[0]是第一行一维数组名,表整行,4个int×4字节=16)
printf("%d\n",sizeof(a[0]+1)); // 4/8(a[0]非sizeof单独参数,表首行首元素地址,+1是首行第2元素地址,指针大小看系统)
printf("%d\n",sizeof(*(a[0]+1))); // 4(解引用首行第2元素地址,得到int元素,占4字节)
printf("%d\n",sizeof(a+1)); // 4/8(a非sizeof单独参数,表首行地址,+1是第二行地址,指针大小看系统)
printf("%d\n",sizeof(*(a+1))); // 16(解引用第二行地址,得到整行一维数组,4×4字节=16)
printf("%d\n",sizeof(&a[0]+1)); // 4/8(&a[0]取首行地址,+1是第二行地址,指针大小看系统)
printf("%d\n",sizeof(*(&a[0]+1))); // 16(解引用第二行地址,得到整行数组,大小16字节)
printf("%d\n",sizeof(*a)); // 16(a表首行地址,解引用得首行整行数组,大小16字节)
printf("%d\n",sizeof(a[3])); // 16(a[3]编译器按一维数组类型算,不管越界,一行4个int即16字节)
return 0;
}
运行结果(32位系统):
cpp
48
4
16
4
4
4
16
4
16
16
16
5. 指针运算经典题
题目一
cpp
#include <stdio.h>
int main()
{
int a[5] = {1,2,3,4,5};
int *ptr = (int *)(&a + 1); // &a是数组整体地址,+1跳过整个数组,强制转为int*
printf("%d,%d", *(a + 1), *(ptr - 1)); // 2,5
// *(a+1):a是首元素地址,+1指向a[1],解引用得2
// *(ptr-1):ptr指向数组末尾后,-1指向a[4],解引用得5
return 0;
}
解析:
&a + 1:数组地址+1,指向数组末尾之后。
(int *)(&a + 1):强制转为int*类型。
*(a + 1):a是首元素地址,+1后指向第二个元素,值为2。
*(ptr - 1):ptr指向数组末尾之后,-1后指向最后一个元素,值为5。
运行结果:
cpp
2,5
题目二
cpp
// 定义一个名为Test的结构体
struct Test
{
int Num; // 4字节(int类型)
char *pcName; // 4字节(指针类型,X86环境下指针占4字节)
short sDate; // 2字节(short类型)
char cha[2]; // 2字节(2个char,每个1字节)
short sBa[4]; // 8字节(4个short,每个2字节)
}*p = (struct Test*)0x100000; // 定义结构体指针p,初始化为0x100000
int main()
{
// p是struct Test*类型,指针+1会跳过整个结构体(20字节=0x14)
// 0x100000 + 0x14 = 0x100014
printf("%p\n", p + 0x1); // 0x100014
// (unsigned long)p把指针p转成无符号长整型,此时是纯数值加法
// 0x100000 + 0x1 = 0x100001
printf("%p\n", (unsigned long)p + 0x1); // 0x100001
// (unsigned int*)p把指针p转成unsigned int*类型,+1会跳过1个unsigned int(4字节)
// 0x100000 + 0x4 = 0x100004
printf("%p\n", (unsigned int*)p + 0x1); // 0x100004
return 0;
}
解析:
-
p + 0x1:指针 p 的类型是 struct Test*,+1 会跳过整个结构体(20字节 = 0x14),所以 0x100000 + 0x14 = 0x100014。
-
(unsigned long)p + 0x1:p 被转成无符号长整型,此时是纯数值加法,0x100000 + 0x1 = 0x100001。
-
(unsigned int*)p + 0x1:p 被转成 unsigned int*,+1 会跳过一个 unsigned int(4字节),所以 0x100000 + 0x4 = 0x100004。
题目三
cpp
#include <stdio.h>
int main()
{
// 核心坑:()是逗号表达式,结果取最后一个值,不是二维数组初始化的{}
// (0,1)=1 (2,3)=3 (4,5)=5,数组实际初始化:a[3][2]={1,3,5},其余元素默认补0
int a[3][2] = { (0, 1), (2, 3), (4, 5) };
int *p;
p = a[0]; // a[0]是二维数组首行地址,赋值给int*指针p,p指向首行首元素
printf("%d", p[0]); // p[0]等价*p,取首元素值1 // 输出:1
return 0;
}
解析:
• 这里的初始化用了逗号表达式 (0, 1),逗号表达式的结果是最后一个值,所以 (0, 1) = 1,(2, 3) = 3,(4, 5) = 5。
• 数组实际初始化为 a[3][2] = {1, 3, 5}。
• p = a[0] 指向首行首元素,p[0] 就是首元素 1。
题目四
cpp
#include <stdio.h>
int main()
{
int a[5][5];
int(*p)[4]; // p是指向含4个int元素的一维数组的指针
p = a; // a是5×5数组首地址,赋值给p(隐式转换为指向4个int的数组指针)
// &p[4][2] - &a[4][2]
// 1. 指针相减的结果是「元素个数差」,不是字节差
// 2. p是指向4个int的数组指针,p[4]表示第4个数组(每个数组4个int)
// 3. p[4][2] 指向第4个数组的第2个元素,与a[4][2]的地址相差一个int元素
// 4. 以int为单位,地址差为-4 → 所以十进制输出是-4,十六进制输出为0xFFFFFFFC(X86环境)
printf("%p,%d\n", &p[4][2] - &a[4][2], &p[4][2] - &a[4][2]);
return 0;
}
运行结果:
cpp
0xFFFFFFFC,-4
题目五
cpp
#include <stdio.h>
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));
// *(ptr1 - 1):&aa+1跳过整个2×5数组,ptr1指向数组末尾后,-1指向最后一个元素10
// *(ptr2 - 1):*(aa+1)是第二行首地址,ptr2指向6,-1指向5
printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
return 0;
}
运行结果:
cpp
10,5
题目六
cpp
#include <stdio.h>
int main()
{
char *a[] = {"work","at","alibaba"}; // a是指针数组,存放3个字符串指针
char**pa = a; // pa是二级指针,初始指向a[0]
pa++; // pa++后指向a[1]
printf("%s\n", *pa); // *pa取a[1]的内容,即字符串"at"
return 0;
}
解析:
a是指针数组,每个元素是字符串指针。
pa是二级指针,初始指向a[0]。
pa++后指向a[1],*pa即为"at"。
运行结果:
cpp
at
题目七
cpp
#include <stdio.h>
int main()
{
char *c[] = {"ENTER","NEW","POINT","FIRST"};
char**cp[] = {c+3,c+2,c+1,c};
char***cpp = cp;
// 1. ***++cpp:cpp初始指向cp[0],++cpp指向cp[1];**cp[1] = *(c+2) = "POINT"
printf("%s\n", ***++cpp); // POINT
// 2. *--*++cpp+3:++cpp指向cp[2];*cp[2] = c+1;--(c+1) = c;*c = "ENTER";+3指向"ER"
printf("%s\n", *--*++cpp+3); // ER
// 3. *cpp[-2]+3:cpp[-2] = cp[0];*cp[0] = c+3;*(c+3) = "FIRST";+3指向"ST"
printf("%s\n", *cpp[-2]+3); // ST
// 4. cpp[-1][-1]+1:cpp[-1] = cp[1];cp[1][-1] = c+1-1 = c;*c = "ENTER";+1指向"NTER"
printf("%s\n", cpp[-1][-1]+1); // NTER
return 0;
}
运行结果:
cpp
POINT
ER
ST
NTER
指针与数组的核心就是类型决定行为,掌握这一点,再复杂的运算也能迎刃而解。希望本文能帮你彻底吃透这些高频考点,下次刷题或面试都能稳操胜券。