【2个月 C 语言从入门到精通:零基础系统教程】第十二讲:深入了解指针(五)

文章目录

  • 前言
  • 1.sizeof和strlen的对比
    • [1.1 核心概念速览](#1.1 核心概念速览)
      • [1.1.1 sizeof 操作符](#1.1.1 sizeof 操作符)
      • [1.1.2 strlen 函数](#1.1.2 strlen 函数)
      • [1.1.3 数组名的"双重身份"](#1.1.3 数组名的“双重身份”)
    • [1.2 核心代码讲解](#1.2 核心代码讲解)
      • [1.2.1 案例1](#1.2.1 案例1)
      • [1.2.2 案例2](#1.2.2 案例2)
      • [1.2.3 案例3](#1.2.3 案例3)
      • [1.2.4 案例4](#1.2.4 案例4)
      • [1.2.5 案例5](#1.2.5 案例5)
      • [1.2.6 案例6](#1.2.6 案例6)
      • [1.2.7 案例7](#1.2.7 案例7)
      • [1.2.8 案例8](#1.2.8 案例8)
  • [2. 数组和指针笔试题解析](#2. 数组和指针笔试题解析)
    • [2.1 案例1](#2.1 案例1)
  • [3. 指针运算笔试题解析](#3. 指针运算笔试题解析)
    • [3.1 案例1](#3.1 案例1)
    • [3.2 案例2](#3.2 案例2)
    • [3.3 案例3](#3.3 案例3)
    • [3.4 案例4](#3.4 案例4)
    • [3.5 案例5](#3.5 案例5)
    • [3.6 案例6](#3.6 案例6)
    • [3.7 案例7](#3.7 案例7)
  • 总结

前言

在 C 语言的学习旅途中,指针和数组犹如一对形影不离的"双胞胎",既深刻又令人头疼。许多初学者在掌握了基础语法后,往往在指针运算、sizeof 与 strlen 的区别以及二维数组的地址偏移上卡壳,导致笔试和面试频频失利。

本文档正是为了解决这一痛点而编写。我将从最核心的底层逻辑出发,通过 7 个 sizeof/strlen 对比案例 和 7 道经典指针运算笔试题,带你一步步拆解内存模型、厘清数组名的"双重身份"以及多级指针的指向变换。

无论你是正在准备求职笔试,还是想要彻底攻克 C 语言指针的"硬骨头",这份实战解析都能为你提供清晰、深入的指引。让我们从内存视角开始,重新认识指针与数组的真相。


1.sizeof和strlen的对比

1.1 核心概念速览

在分析代码前,先明确两个核心工具和一条数组与指针的"潜规则"。

1.1.1 sizeof 操作符

功能:计算操作数所占内存的字节数。

时期:编译时确定(除变长数组外)。

返回值:size_t(无符号整型),用 %zu 打印。

对象:可以是类型、变量、表达式。它关心的是"类型占多大内存"。

1.1.2 strlen 函数

功能:求字符串长度,即从首地址开始向后计数,直到遇见空字符 \0 停止,且不包含 \0。

时期:运行时扫描内存。

声明:size_t strlen(const char *str);

致命要求:传入的指针必须指向一块包含 \0 的有效字符数组。否则它会一直向后狂读,返回一个随机值,甚至导致程序崩溃。

1.1.3 数组名的"双重身份"

数组名在大多数表达式中会隐式转换为指向首元素的指针。但有两个例外:

sizeof(数组名):此时数组名表示整个数组,计算出数组总大小。

&数组名:取出的是整个数组的地址,类型是 指向数组的指针。

记住这句话:arr 和 &arr0 值相同但意义略有不同;&arr 的值也相同,但类型和步长完全不同。


1.2 核心代码讲解

我将用以下代码向大家展示这两者的不同

1.2.1 案例1

c 复制代码
#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
int main()
{
	char arr1[3] = { 'a', 'b', 'c' };//[a b c]
	char arr2[] = "abc";             //[a b c \0]
	printf("%d\n", strlen(arr1));//未知数
	printf("%d\n", strlen(arr2));//3

	printf("%d\n", sizeof(arr1));//3
	printf("%d\n", sizeof(arr2));//4
	return 0;
}

代码解析

1.2.2 案例2

c 复制代码
int main()
{
	int a[] = { 1,2,3,4 };//4*4 = 16
	printf("%zu\n", sizeof(a));//16
	printf("%zu\n", sizeof(a + 0));//4/8,a+0是首元素的地址
	printf("%zu\n", sizeof(*a));//4, *a == a[0]
	printf("%zu\n", sizeof(a + 1));//4/8, a + 1 --> &a[1]
	printf("%zu\n", sizeof(a[1]));//4
	printf("%zu\n", sizeof(&a));//4/8, &a是整个数组的地址,依然是地址
	printf("%zu\n", sizeof(*&a));//16
	//&a -- int (*)[4]
	//*&a -- a
	//sizeof(*&a) == sizeof(a)
	printf("%zu\n", sizeof(&a + 1));//4/8  &a + 1还是地址
	printf("%zu\n", sizeof(&a[0]));//4/8
	printf("%zu\n", sizeof(&a[0] + 1));//4/8
	return 0;
}

核心讲解

1.2.3 案例3

c 复制代码
int main()
{
	char arr[] = { 'a','b','c','d','e','f' };
	printf("%zu\n", sizeof(arr));//6
	printf("%zu\n", sizeof(arr + 0));//4/8, arr + 0 == &arr[0]
	printf("%zu\n", sizeof(*arr));//1, *arr == arr[0]
	printf("%zu\n", sizeof(arr[1]));//1
	printf("%zu\n", sizeof(&arr));//4/8 -- char(*)[6]
	printf("%zu\n", sizeof(&arr + 1));//4/8-- char(*)[6]
	printf("%zu\n", sizeof(&arr[0] + 1));//4/8
	return 0;
}

1.2.4 案例4

c 复制代码
#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));//*arr == arr[0] == 'a' == 97, 程序会崩溃
	//printf("%zu\n", strlen(arr[1]));//'b'-98,程序会崩溃
	printf("%zu\n", strlen(&arr));//未知数  x
	//char(*)[6]-> const char*
	printf("%zu\n", strlen(&arr + 1));//未知数 x-6
	printf("%zu\n", strlen(&arr[0] + 1));//未知数 x-1
	return 0;
}

1.2.5 案例5

c 复制代码
int main()
{
	char arr[] = "abcdef";

	printf("%zu\n", sizeof(arr));//7
	printf("%zu\n", sizeof(arr + 0));//4/8
	printf("%zu\n", sizeof(*arr));//1, arr[0] == *arr
	printf("%zu\n", sizeof(arr[1]));//1
	printf("%zu\n", sizeof(&arr));//4/8
	printf("%zu\n", sizeof(&arr + 1));//4/8
	printf("%zu\n", sizeof(&arr[0] + 1));//4/8
	return 0;
}

1.2.6 案例6

c 复制代码
int main()
{
	char arr[] = "abcdef";
	printf("%zu\n", strlen(arr));//6
	printf("%zu\n", strlen(arr + 0));//6
	//printf("%zu\n", strlen(*arr));//程序会崩溃
	//printf("%zu\n", strlen(arr[1]));//程序会崩溃
	printf("%zu\n", strlen(&arr));//6
	printf("%zu\n", strlen(&arr + 1));//未知数
	printf("%zu\n", strlen(&arr[0] + 1));//5
	return 0;
}

代码解析

1.2.7 案例7

c 复制代码
#include <stdio.h>

int main()
{
	char* p = "abcdef";

	printf("%zu\n", sizeof(p));//4/8
	printf("%zu\n", sizeof(p + 1));//4/8, p+1是b的地址
	printf("%zu\n", sizeof(*p));//1, *p == 'a'
	printf("%zu\n", sizeof(p[0]));//1
	//p[0]  == *(p+0) == 'a'
	printf("%zu\n", sizeof(&p));//4/8
	printf("%zu\n", sizeof(&p + 1));//4/8
	printf("%zu\n", sizeof(&p[0] + 1));//4/8, 'b'的地址
	return 0;
}

1.2.8 案例8

c 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
	char* p = "abcdef";
	printf("%zu\n", strlen(p));//6
	printf("%zu\n", strlen(p + 1));//5
	printf("%zu\n", strlen(*p));//程序崩溃
	printf("%zu\n", strlen(p[0]));//程序崩溃
	printf("%zu\n", strlen(&p));//未知数
	printf("%zu\n", strlen(&p + 1));//未知数
	printf("%zu\n", strlen(&p[0] + 1));//5
	return 0;
}

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

2.1 案例1

3. 指针运算笔试题解析

3.1 案例1

c 复制代码
int main()
{
    int a[5] = { 1, 2, 3, 4, 5 };

    int* ptr = (int*)(&a + 1);

    printf("%d,%d", *(a + 1), *(ptr - 1));
    
    return 0;

3.2 案例2

c 复制代码
//在X86环境下
//假设结构体的大小是20个字节
//程序输出的结果是啥?
struct Test
{
    int Num;
    char* pcName;
    short sDate;
    char cha[2];
    short sBa[4];
}*p = (struct Test*)0x100000;

int main()
{
    printf("%p\n", p + 0x1);
    printf("%p\n", (unsigned long)p + 0x1);
    printf("%p\n", (unsigned int*)p + 0x1);

    return 0;
}

3.3 案例3

c 复制代码
int main()
{
    int a[3][2] = { (0, 1), (2, 3), (4, 5) };
    int* p;
    p = a[0];
    printf("%d", p[0]);
    return 0;
}

3.4 案例4

c 复制代码
//假设环境是x86环境,程序输出的结果是啥?
#include <stdio.h>

int main()
{
    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]);

    return 0;
}

关键点: p 和 a 的类型是不匹配的!a 的每一行有 5 个元素,但 p 认为它指向的每一行只有 4 个元素。

3.5 案例5

c 复制代码
#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));
    printf("%d,%d", *(ptr1 - 1), *(ptr2 - 1));
    return 0;
}

3.6 案例6

c 复制代码
int main()
{
    char* a[] = { "work","at","alibaba" };
    char** pa = a;
    pa++;
    printf("%s\n", *pa);
    return 0;
}

3.7 案例7

c 复制代码
int main()
{
    char* c[] = { "ENTER","NEW","POINT","FIRST" };
    char** cp[] = { c + 3,c + 2,c + 1,c };
    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;

总结

通过本文的梳理,我们不仅回顾了 sizeof(编译时确定,侧重类型大小)与 strlen(运行时扫描,侧重字符长度)的根本差异,更透过一系列经典的笔试题,揭示了以下几个关键规律:

数组名的"变身":在 sizeof 和 & 操作下,数组名代表整个数组;而在表达式中(如 a+1),它自动退化为指向首元素的指针。

指针加减的"步长":指针算术的偏移量由它指向的数据类型大小决定。例如 struct Test* + 1 跳过整个结构体,而 unsigned int* + 1 只跳过 4 字节。

越界与类型不匹配的陷阱:

二维数组名和指向不同宽度的数组指针(如 int(*p)4 指向 int a55)进行运算时,会产生意料之外的"提前"或"延后"偏移(例如 &p42 - &a42 结果为 -4)。

strlen 要求参数必须是指向 \0 结尾的有效地址。传入字符本身(如 *arr)会导致程序崩溃。

多级指针的"追赶"游戏:在三级指针(如 char*** cpp)的解题中,跟踪 cpp 自身的移动是核心,而 ++ 和 -- 操作符的顺序(前置/后置)会直接影响目标变量值的修改时机。

掌握这些底层规则,你就能在复杂的指针代码面前从容拆解,避免"看到指针就发怵"的困境。建议你将文中案例在编译器中亲手运行并调试,这将帮助你建立起更加坚实的 C 语言内存模型认知

相关推荐
飞天狗1111 小时前
零基础JavaWeb入门——第五课第一小节:九大内置对象 · 第1个:request(请求对象)
java·开发语言·前端·后端·servlet
z落落1 小时前
C#ToolStrip+StatusStrip 状态栏实时显示系统时间+NotifyIcon系统托盘
开发语言·c#
志栋智能1 小时前
从固定周期到动态触发:超自动化巡检的智能调度
运维·网络·自动化
插件开发1 小时前
vs2015 cuda c++ 线程号的计算详解
开发语言·c++·算法
石山代码1 小时前
变量与解构
开发语言·前端·javascript
c++之路2 小时前
Bazel C++ 构建系列文档(五):多目标与多包项目
java·开发语言·c++
Hello:CodeWorld2 小时前
【C++ 避坑指南】告别缓冲区溢出!全面解析 std::snprintf 的安全美学与核心陷阱
开发语言·c++·安全
凡人叶枫2 小时前
Effective C++ 条款38:通过复合塑模出 has-a 或 \“根据某物实现出\
linux·开发语言·c++·windows
枫叶丹42 小时前
【HarmonyOS 6.0】MDM Kit:PC/2in1设备用户行为限制策略详解
开发语言·华为·harmonyos