C语言小知识——指针(3)

指针与数组的 sizeof、strlen 计算,以及多级指针运算,一直是 C 语言面试和笔试中的高频考点,也是很多初学者容易混淆的地方。本文将通过经典例题+底层原理+内存布局的方式,带你彻底搞懂这些容易踩坑的知识点,让你在遇到类似题目时不再困惑。


一、核心知识点对比

  1. 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;
}

二、数组名的意义

  1. sizeof(数组名):数组名代表整个数组,计算整个数组的内存大小。

  2. &数组名:数组名代表整个数组,取出的是整个数组的地址。

  3. 其他场景:数组名仅代表首元素的地址(如数组名+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;
}

解析:

  1. p + 0x1:指针 p 的类型是 struct Test*,+1 会跳过整个结构体(20字节 = 0x14),所以 0x100000 + 0x14 = 0x100014。

  2. (unsigned long)p + 0x1:p 被转成无符号长整型,此时是纯数值加法,0x100000 + 0x1 = 0x100001。

  3. (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

指针与数组的核心就是类型决定行为,掌握这一点,再复杂的运算也能迎刃而解。希望本文能帮你彻底吃透这些高频考点,下次刷题或面试都能稳操胜券。

相关推荐
Hcoco_me2 小时前
大模型面试题84:是否了解 OpenAI 提出的Clip,它和SigLip有什么区别?为什么SigLip效果更好?
人工智能·算法·机器学习·chatgpt·机器人
burning_maple2 小时前
mysql数据库笔记
数据库·笔记·mysql
hkNaruto2 小时前
【AI】AI学习笔记:LangGraph 与 LangChain的关系以及系统性学习路线选择
笔记·学习·langchain
jrlong2 小时前
DataWhale大模型基础与量化微调task3学习笔记(第 5章:深入大模型架构_MoE 架构解析)
笔记·学习
BHXDML2 小时前
第九章:EM 算法
人工智能·算法·机器学习
想放学的刺客3 小时前
单片机嵌入式嵌入式试题(第16期):硬件可靠性设计与复杂状态机架构设计
c语言·stm32·单片机·嵌入式硬件·物联网
code_li3 小时前
聊聊支付宝架构
java·开发语言·架构
却道天凉_好个秋3 小时前
目标检测算法与原理(三):PyTorch实现迁移学习
pytorch·算法·目标检测
少控科技3 小时前
QT高阶日记01
开发语言·qt