【复习系列】数组和指针

本文聚焦C语言中指针的三大核心应用(数组指针、指针数组、函数指针),附加const与指针的结合、sizeof与strlen的关联高频考点,结合示例与易混点解析,适配面试复习场景。

(一) 数组指针和指针数组

数组指针和指针数组是 C 语言中极易混淆的两个核心知识点,且常被放在一起考察,因此我们将二者整合梳理,帮助大家彻底厘清二者的区别与本质。

首先,我们先明确二者的语法形式和具体示例,建立直观认知。

(1) 直观语法形式与示例

1. 数组指针

语法形式:类型 (* 指针名)[数组长度](注:括号不可省略,直接决定语法优先级的走向)

示例:

复制代码
int (*p)[5];
// 定义了一个数组指针p,此指针专门用于指向一个包含 5 个int类型元素的一维数组

2. 指针数组

语法形式:类型 * 数组名[数组长度]

示例:

复制代码
int* arr[5];
//定义了一个指针数组arr,此数组包含 5 个元素,且每个元素都是一个指向int类型数据的指针

(2) 字面含义拆解

掌握语法形式后,我们可以先从字面含义入手进行简单拆解,这是最直观的区分方法------只需在两个名词中间加入 "的" 字

  1. 数组指针 ------「数组的指针」核心本质是一个指针,这个指针的唯一指向对象是一个完整的一维数组(而非单个基础数据)。
  2. 指针数组 ------「指针的数组」核心本质是一个数组,这个数组的所有元素都统一为同类型的指针变量(而非普通基础数据)。

(3) 深层符号理解(基于运算符优先级)

如果想更透彻地理解二者的语法逻辑,我们可以从 C 语言的运算符优先级入手分析

三者的优先级关系明确为:( ) > [ ] > *。

1. 数组指针 (*p)[n] 分析

由于( )的优先级高于[ ],解析时会先处理括号内的内容:

  • 第一步,先判定*p,这表明p是一个指针变量
  • 第二步,再将*p与后续的[n]结合,[n]代表数组的长度为n,说明该指针的指向对象不是单个int/char等基础数据,而是一个包含n个元素的一维数组;
  • 综上,p是一个「指向包含n个元素的一维数组的指针」,即数组指针。

2. 指针数组 *arr[n] 分析

由于[ ]的优先级高于*,解析时会先处理数组下标相关内容:

  • 第一步,先判定arr[n],这表明arr是一个数组,且数组的长度为n;
  • 第二步,再将arr[n]与前面的*结合,说明该数组的每一个元素(arr[0]到arr[n-1])都不是普通基础数据,而是一个指针变量;
  • 综上,arr是一个「存储了n个同类型指针的数组」,即指针数组。

(4) 指针数组的小细节

1. 定义: 指针数组是一种专门用于存储指针变量的数组,其核心特性是数组中的每一个元素,都是类型相同的指针变量(数组的本质未改变,仅元素类型为指针类型)。

2. 核心用途(博主的认为)

场景1------存放多个字符串

代码:

复制代码
void test2()
{
	//存放多个字符串
	char* str[3] = { "hello","everyone","happy" };
	for (int i = 0; i < 3; i++)
	{
		printf("%s\n", str[i]);
	}
}

//运行结果
hello
everyone
happy

场景2------指向多个同一类型数组

代码:

复制代码
void test3()
{
	//指向多个同一类型数组
	int arr1[3] = { 1,2,3 }, arr2[3] = { 4,5,6 }, arr3[3] = { 7,8,9 };
	int* p_arr[3] = { arr1,arr2,arr3 };
	for (int i = 0; i < 3; i++)
	{
		for (int j = 0; j < 3; j++)
		{
			printf("%d ", p_arr[i][j]);
		}
		printf("\n");
	}
}

//运行结果:
1 2 3
4 5 6
7 8 9

3. 解引用形式

指针数组访问元素有多种等价写法,本质都是 "指针偏移 + 解引用",以下四种形式完全等价,均可以访问到目标元素(以访问pa[1][1]为例,对应值为 5):

解引用形式 说明
pa[i][j] 最简洁的简化语法,可读性最强,等价于普通二维数组访问
*(pa[i] + j) 先获取一维数组首地址,再指针偏移后解引用
*(*(pa + i) + j) 最底层的指针偏移语法,逐层解引用 + 偏移
(*(pa + i))[j] 先解引用获取一维数组首地址,再用下标访问元素

例子:

复制代码
void test4()
{
    int arr1[] = { 0,1,2,3 }, arr2[] = { 4,5,6,7 }, arr3[] = { 8,9,10,11 };
    int* pa[3] = { arr1, arr2, arr3 };

    printf("指针数组的内容为:\n");
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            printf("%d ", *(*(pa + i) + j));
        }
        printf("\n");
    }

    printf("\n不同解引用操作的结果为:\n");
    printf("%d,%d\n", pa[1][1], *(pa[1] + 1));
    printf("%d,%d\n", pa[1][1], *(*(pa + 1) + 1));
    printf("%d,%d\n", pa[1][1], (*(pa + 1))[1]);
    printf("%d,%d\n", pa[1][1], pa[1][1]);
}

运行结果:

指针数组的内容为:

0 1 2 3

4 5 6 7

8 9 10 11

不同解引用操作的结果为:

5,5

5,5

5,5

5,5

4. 内存特性(关键计算逻辑)

指针数组占用的总字节数遵循固定计算公式:

指针数组总字节数 = 数组长度 × 单个指针变量的字节数

补充说明:

  1. 单个指针变量的字节数由系统架构决定,与指针指向的数据类型、指向的数据长度无关;
  2. 32 位系统中,任意类型的指针均占用 4 字节;64 位系统中,任意类型的指针均占用 8 字节。

示例:int* p_arr[3]在 64 位系统中,总字节数 = 3 × 8 = 24 字节。

5. 易混淆点:指针数组 vs 普通二维数组

指针数组 int *arr[5] ≠ 普通二维数组 int arr[5][3],二者核心差异如下:

  1. 指针数组: 数组元素是独立的指针变量,每个指针可自由指向不同长度、不同位置的内存块(如可以指向长度为 3 的arr1,也可以指向长度为 5 的arr2),灵活性更高;
  2. **普通二维数组:**整个数组在内存中是连续存储的,行地址是派生的常量指针,每行的长度必须统一固定,无法灵活修改,内存利用率相对固定。

总结

指针数组的本质是「数组」,元素是「同类型指针」;

核心优势是「灵活」,无论是存储字符串还是模拟二维数组,都突破了普通数组的长度限制;

多种解引用形式本质等价,优先使用pa[i][j]提升代码可读性;

与普通二维数组的核心区别在于「内存是否连续」和「长度是否可灵活设定」。


(5) 数组指针的小细节

1. 定义: 数组指针是一个专门指向整个数组的指针变量,其核心特性是:指针的指向对象是一个完整的、固定长度的数组,而非数组中的单个元素。

2. 核心用途(博主的认为)

场景------遍历二维数组

因为 C 语言中的二维数组可以看作「"一维数组" 的数组」(二维数组的每一行都是一个独立的一维数组),而数组指针恰好指向一维数组,二者天然匹配,能够高效、规范地遍历二维数组。

复制代码
void test5()
{
    int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
    int (*p)[3] = arr;

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            printf("%d ", p[i][j]);
            // 方式2:
            // printf("%d ", *(*(p + i) + j));
            // 方式3:
            // printf("%d ", (*(p + i))[j]);
        }
        printf("\n"); 
    }
}

运行结果:

1 2 3

4 5 6

7 8 9

3. 关键特性:独特的指针步长

数组指针的核心特性是其指针步长(偏移量)固定为指向数组的总字节数

  1. 普通指针(如int*)的p+1,步长是单个int类型的字节数(通常 4 字节),仅跳过一个元素;
  2. 数组指针(如int (*p)[3])的p+1,步长是整个指向数组的总字节数(此处为3×4=12字节),会直接跳过当前指向的整个一维数组,指向相邻的下一个同长度一维数组。

这一特性也是数组指针能够高效遍历二维数组的核心原因 ------p+i可以直接精准定位到二维数组的第i行,无需额外计算字节偏移。

总结:

  1. 总结数组指针的本质是「指针」,指向的是「固定长度的完整数组」;
  2. 核心优势是「精准」,尤其是遍历二维数组时,能直接定位行地址,实现高效规范的访问;
  3. 与指针数组的核心区别在于「本质属性」和「长度灵活性 / 指针步长」。

(二) 函数指针

1. 定义 :一个指向函数的指针,指针存储的是函数的入口地址 (C 语言中,函数名本身就是函数的入口地址)。本质是指针,专门指向函数。

2. 语法格式返回值类型 (*指针名)(参数列表);

注: ()提升优先级,明确指针指向函数,参数列表需与目标函数一致

例:

复制代码
// 目标函数
int add(int x, int y)
{
    return x + y;
}

void test1()
{
    // 函数指针:p指向add函数(可省略&,函数名即地址)
    int (*p)(int, int) = add; 
}

3. 调用方法:

复制代码
// 三种调用方式结果完全一致
printf("%d ", add(2, 3));    // 直接调用原函数
printf("%d ", (*p)(2, 3));   // 解引用函数指针调用(符合指针使用逻辑)
printf("%d ", p(2, 3));      // 简化调用(编译器优化,最常用)

运行结果:

5 5 5

4. 核心用途

实现回调函数(最典型应用:qsort 函数的比较器参数)、函数接口封装、实现程序的动态逻辑选择,大幅提高代码的灵活性和可扩展性。

拓展:qsort函数的比较器参数

1. 定义:

qsort 是 C 语言标准库<stdlib.h>中的通用快速排序函数 ,支持排序任意类型的数组数据,其适配不同类型的核心就是函数指针类型的比较器参数------qsort 本身无法识别数据类型,需要通过自定义比较器函数告诉它具体的比较规则。

2. qsort 函数整体原型:

复制代码
void qsort(void *base, size_t nitems, size_t size, int (*compar)(const void *, const void *));

参数说明

  • void *base:待排序数组的首地址;
  • size_t nitems:数组的元素个数;
  • size_t size:数组中单个元素的字节数;
  • int (*compar)(const void *, const void *):函数指针参数,指向自定义的比较器函数,规定排序规则。

3. 比较器(compar)的强制规范原型(必须严格遵循,否则编译 / 排序出错)

复制代码
int 比较器函数名(const void *a, const void *b);

比较器的核心规则

  1. 参数 :两个const void*类型 ------ 通用指针,能接收任意类型数据的地址,const保证不修改原数据;
  2. 返回值 :int 型,决定排序顺序(升序为默认常用规则 ,降序仅需反转返回值即可):
    • 返回 > 0 :a 指向的数据 排在 b 指向的数据后面
    • 返回 < 0 :a 指向的数据 排在 b 指向的数据前面
    • 返回 = 0:a 和 b 指向的数据相等,顺序不变。
  3. 关键操作void*不能直接解引用,必须强制类型转换为待排序数据的指针类型,再解引用获取数据进行比较。

4. 常用数据类型的比较器代码

以下均为升序比较器 ,实现降序只需将return后的表达式反转 (如 int 类型:return *(const int*)b - *(const int*)a;)。

1)排序 int 类型数组(最基础)

复制代码
// 比较int类型:升序
int cmp_int(const void *a, const void *b) {
    // void*强制转为const int*,解引用后相减
    return *(const int*)a - *(const int*)b;
}

(2)排序 char 类型数组(按 ASCII 码排序)

复制代码
// 比较char类型:升序(数字<大写字母<小写字母,按ASCII码值比较)
int cmp_char(const void *a, const void *b) {
    return *(const char*)a - *(const char*)b;
}

(3)排序 double 类型数组(浮点型禁止直接相减)

复制代码
// 比较double类型:升序(浮点型直接相减会有精度丢失,用条件判断)
int cmp_double(const void *a, const void *b) {
    double val1 = *(const double*)a;
    double val2 = *(const double*)b;
    if (val1 > val2) return 1;
    else if (val1 < val2) return -1;
    else return 0;
}

(4)排序自定义结构体数组(按结构体成员比较)

复制代码
// 先定义自定义结构体
typedef struct {
    char name[20];  // 姓名
    int age;        // 年龄(int型)
    double score;   // 成绩(double型)
} Student;

// 比较结构体:按age成员升序
int cmp_student_age(const void *a, const void *b) {
    // 强制转为const Student*,通过->访问结构体成员
    return ((const Student*)a)->age - ((const Student*)b)->age;
}

5. 核心总结

  • 函数指针的本质是「指针」,指向函数的入口地址,调用方式灵活且多写法等价;
  • qsort 的比较器是函数指针的典型应用,需严格遵循固定原型,核心是「const void * 强制转目标类型指针后解引用比较」;
  • 不同数据类型适配不同比较逻辑,浮点型禁直接相减、字符串依赖 strcmp 函数;
  • 升序 / 降序可通过「反转返回值表达式」快速切换,无需重写比较器核心逻辑。

(三) 附件高频考点

1. const与指针的结合

const修饰指针有两种核心场景,关键看const与*的位置关系,本质是限制"指针指向的值"或"指针本身"的可修改性。

场景1:const修饰指针指向的值(const在*左侧)

语法: const 类型名 *指针名; 或 类型名 const *指针名;

规则: 指针指向的值不可修改,但指针本身的指向可以修改。

例:

复制代码
void test1()
{
    int a = 10, b = 20; 
    const int* p = &a; 
    *p = 30; //错误:指向的值不可改 
    p = &b; // 正确:指针指向可改
}

场景2:const修饰指针本身(const在*右侧)

语法:类型名 *const 指针名;

规则: 指针本身的指向不可修改,但指针指向的值可以修改。

例:

复制代码
void test2()
{
    int a = 10, b = 20; 
    int* const p = &a; 
    *p = 30; // 正确:指向的值可改 
    p = &b; //错误:指针指向不可改
}

记忆口诀左定值,右定址(const在*左,限制值;const在*右,限制地址)。


2. sizeof 与 strlen 面试知识点

二者是 C 语言面试必考易混核心点 ,笔试常考代码计算题,口述常问本质差异,默认64 位系统。

(1) sizeof 核心面试知识点

1. 本质

sizeof 是 C 语言内置单目运算符,并非函数,属于语言语法层面,编译阶段即可完成计算,使用时无需包含任何头文件。

2. 核心计算规则

计算目标:变量 / 数据类型在内存中被编译器分配的总字节数 ,仅关注「内存分配大小」,与内存中实际存储的内容无关;

例1:基础数据类型变量

复制代码
void test2()
{
    int a = 1;    
    int b = 1000; 
    char c = 'x'; 
    char d = 0;  
    printf("sizeof(a)=%d, sizeof(b)=%d\n", sizeof(a), sizeof(b)); 
    printf("sizeof(c)=%d, sizeof(d)=%d\n", sizeof(c), sizeof(d)); 
}

运行结果:

sizeof(a)=4, sizeof(b)=4

sizeof(c)=1, sizeof(d)=1

解释:

发现:int 类型在编译器中固定分配 4 字节,char 固定 1 字节

故不管变量存的数值是大是小、是有意义的值还是 0,分配的内存大小不变,sizeof 结果就不变。

例2:数组(分配固定长度的连续内存)

复制代码
void test3()
{
    char arr[5];        
    char brr[5] = { 'a' };
    printf("sizeof(arr)=%d, sizeof(brr)=%d\n", sizeof(arr), sizeof(brr)); 
}

运行结果:

sizeof(arr)=5, sizeof(brr)=5

解释:

发现:数组[5] 无论里面啥都没存(随机值)还是只存了几个字符,剩下字符为空都是5

故 类型 数组[5] 明确告诉编译器 "分配 5 个该类型 的内存",不管存没存内容、存了几个,如例子类型为 char 总分配字节数都是5*1=5,sizeof 只测这个总大小。

终止符处理: 不识别\0,无论内存中是否存在\0,都会计算分配的全部内存空间;

  • 这是易和 strlen 搞混 的点,先明确:\0是 C 语言字符串的结束标志 ,只有strlen(字符串长度函数)会识别 \0 并 "算到 \0 为止",而sizeof 完全无视 \0,只要是编译器分配的内存,哪怕里面有 \0,也全部计算。

参数范围: 支持 C 语言所有数据类型,包括 char、int、数组、指针、结构体、空指针等,均可合法计算;

  • 核心要点指针的大小和指向的类型无关 ------ 不管是int*还是char*本质都是 "存储内存地址的变量",64 位系统的地址是 8 字节(二进制 64 位),所以所有指针 sizeof 都是 8;32 位系统地址是 4 字节,所有指针都是 4。

计算时机:编译期计算 ,结果在程序编译阶段就已确定,不会随程序运行时的内存数据变化而改变;

例子:运行时移动指针,指针本身的 sizeof 不变(这个是博主忘记的例子)

复制代码
void test4()
{
    int arr[5] = { 1,2,3,4,5 };
    int* p = arr; // 指针指向数组首元素
    printf("指针初始:sizeof(p)=%d\n", sizeof(p)); // 8

    // 程序运行时:指针向后移动,指向数组第3个元素
    p += 2;
    printf("指针移动后:sizeof(p)=%d\n", sizeof(p)); // 还是8!
}

运行结果:

指针初始:sizeof(p)=8

指针移动后:sizeof(p)=8

解释:

指针 p 的大小是 8 字节(编译期确定),运行时p += 2只是改变了 p 里面存的 "内存地址",但 p 这个变量本身的内存分配大小(8 字节)没变,所以 sizeof (p) 不变。

3. 面试高频关键特性

括号省减规则:变量 计算时可省略括号(如char c; sizeof c合法);对数据类型 计算时必须加括号(如sizeof(int)),语法判断题常考;

数组与指针核心区别:直接传入数组名时,不会将数组名退化为指针 ,直接计算整个数组总字节数 (公式:数组长度 × 单个元素字节数);若数组名退化为指针(赋值给指针变量、函数传参),sizeof 仅计算指针本身字节数

例:

复制代码
void test6()
{
    int arr[5] = { 1,2,3,4,5 };
    int* p = arr;

    // 情况1:直接测数组名,不退化,算总字节数
    printf("sizeof(arr) = %d\n", sizeof(arr)); 

    // 情况2:测退化为指针的数组名(赋值给p),算指针大小
    printf("sizeof(p) = %d\n", sizeof(p));     

    // 情况3:数组名参与算术运算(arr+1),退化为指针,算指针大小
    printf("sizeof(arr+1) = %d\n", sizeof(arr + 1)); 

    // 情况4:测数组单个元素,算元素类型大小(对比用)
    printf("sizeof(arr[0]) = %d\n", sizeof(arr[0])); 

    // 推导数组长度的正确写法:总字节数 / 单个元素字节数(面试常考)
    printf("数组实际长度 = %d\n", sizeof(arr) / sizeof(arr[0])); 
}

运行结果:

sizeof(arr) = 20

sizeof(p) = 8

sizeof(arr+1) = 8

sizeof(arr[0]) = 4

数组实际长度 = 5

**数组传参坑点:**函数内部计算传入的数组参数时,因数组传参必然退化为指针,结果为指针字节数,而非数组实际大小;

  • C 语言中数组不能直接作为函数参数传递 ,当你把数组名传给函数时,编译器会自动将其退化为指向首元素的指针 ,所以函数内部的 "数组参数",本质是一个指针变量 。此时在函数内部用 sizeof 计算这个参数,得到的只是指针的字节数,绝对不是原数组的总大小

例子:

复制代码
// 错误写法:想在函数内部用sizeof算数组大小
void getArrSize(int arr[]) // 形参arr[]本质是int* arr,编译器会自动解析为指针
{ 
    printf("错函数内部:sizeof(arr) = %d\n", sizeof(arr)); 
    printf("错函数内部错误推导长度:%d\n", sizeof(arr) / sizeof(arr[0])); 
}

// 正确写法:主函数计算数组长度,作为参数传给函数
void getRealArrSize(int arr[], int len) 
{
    printf("对函数内部正确长度:%d\n", len); 
}

void test7()
{
    int arr[5] = { 1,2,3,4,5 };
    int realLen = sizeof(arr) / sizeof(arr[0]); 
    printf("主函数:sizeof(arr) = %d\n", sizeof(arr)); 
    printf("主函数推导长度:%d\n", realLen); 

    getArrSize(arr); // 错误函数
    getRealArrSize(arr, realLen); // 正确函数,传递真实长度
}

运行结果:

主函数:sizeof(arr) = 20

主函数推导长度:5

错函数内部:sizeof(arr) = 8

错函数内部错误推导长度:2

对函数内部正确长度:5

空指针处理:可正常计算空指针的内存大小,不会触发程序错误;

例子:

复制代码
void test8()
{
    int* p1 = NULL;    // 整型空指针
    void* p2 = NULL;   // 无类型空指针(纯空指针)

    // 正常计算空指针大小,无任何报错,结果都是指针大小
    printf("sizeof(p1) = %d\n", sizeof(p1)); 
    printf("sizeof(p2) = %d\n", sizeof(p2)); 

    // 注意:只有**解引用空指针**(访问指向的内存)才会崩溃,和sizeof无关
    // *p1 = 10; // 运行时崩溃:Segmentation fault (段错误),因为访问了0地址
    // sizeof(p1) 不会解引用,只是测p1本身的大小,所以安全
}

运行结果:

sizeof(p1) = 8

sizeof(p2) = 8

结构体进阶点:计算结构体的 sizeof 时,会遵循 C 语言内存对齐规则 ,结果并非各成员字节数简单相加(后面提及)。


(2) strlen 核心面试知识点

1. 核心本质

strlen 是 C 语言标准库中的字符串处理函数 ,属于函数调用层面,使用时必须包含<string.h>头文件,否则编译器无法识别。

2. 核心计算规则

计算目标:以\0为结尾的字符串中,\0之前的有效字符个数 ,仅关注「字符串实际有效内容」,与分配的内存大小无关;

复制代码
void test9()
{
    char str1[10] = "abc";  // 分配10字节,仅存a/b/c+\0,剩下6字节空闲
    char str2[] = "world";  // 编译器自动分配6字节(5个有效字符+\0)

    // strlen:只数\0前有效字符,分配内存再大也没用
    printf("strlen(str1)=%d, strlen(str2)=%d\n", strlen(str1), strlen(str2));
}

运行结果:

strlen(str1)=3, strlen(str2)=5

终止符处理:以\0为唯一终止标志 ,从起始地址开始逐字符遍历,遇到\0立即停止,且不将\0计入结果

复制代码
void test10()
{
    char str1[] = "12345"; 
    printf("strlen(str1)=%d\n", strlen(str1)); 

    char str2[] = "ab\0cd"; 
    printf("strlen(str2)=%d\n", strlen(str2));

    char str3[8] = { 'x', 'y', '\0', 'z' }; 
    printf("strlen(str3)=%d\n", strlen(str3)); 
}

运行结果:

strlen(str1)=5

strlen(str2)=2

strlen(str3)=2

解释:

\0 的位置直接决定 strlen 的结果,不管是系统自动加的 \0,还是手动加的 \0,处理规则完全一致;而 sizeof 会把整个分配的内存算完,哪怕中间全是 \0,结果也不变。

参数范围:仅支持以\0结尾的char*类型字符串 ,传入其他类型(如int*)编译报错,传入非合法字符串地址会程序崩溃;

例子:

复制代码
void test11()
{
    char str1[] = "合法字符串";
    printf("合法:strlen(str1)=%d\n", strlen(str1));

    // 情况2:传入非char*类型(int*)→ 编译直接报错!
    int a = 10;
    int* p = &a;
    // printf("错误:%d\n", strlen(p)); // 报错:期望const char*类型,实际传了int*

    // 情况3:传入char*但无\0(非合法字符串)→ 运行时崩溃!
    char str2[3] = { 'a', 'b', 'c' }; // 仅分配3字节,无任何\0,不是合法字符串
    // printf("崩溃:%d\n", strlen(str2)); // 运行报错

}

运行结果:

合法:strlen(str1)=10

小提醒:

字符串常量(如"abc")会被系统自动在末尾加 \0,永远是合法的,放心用 strlen;

手动用{}初始化 char 数组时,一定要手动加 \0 (如char str[4] = {'a','b','c','\0'}),否则不是合法字符串。

计算时机:运行期计算 ,程序运行时才会从传入的首地址开始遍历,结果依赖运行时的内存实际数据;

  • strlen 是运行期计算 ,编译时编译器只知道要调用这个函数,等程序运行起来,才会从传入的首地址开始逐字符找 \0,结果完全依赖 "运行时内存里的实际内容"------ 内存里的字符或 \0 位置变了,strlen 的结果就跟着变。

    void test12()
    {
    char str[10] = "abcdef";
    printf("初始状态:strlen=%d\n", strlen(str));

    复制代码
      // 程序运行时:修改内存,把str[2]改成\0
      str[2] = '\0';
      printf("修改后:strlen=%d\n", strlen(str));
    
      // 程序运行时:再修改回来,恢复原字符串
      str[2] = 'c';
      printf("修改后:strlen=%d\n", strlen(str));

    }

运行结果:

初始状态:strlen=6

修改后:strlen=2

修改后:strlen=6

解释:

strlen 需要访问内存中的实际数据(逐字符找 \0),编译期编译器根本不知道程序运行时内存里会存什么、\0 会出现在哪个位置,所以只能等程序运行后再计算。

3. 面试高频关键特性

**头文件依赖:**必须包含<string.h>,否则编译报错,笔试编程题易忘此点;

无\0 致命坑:传入未以\0结尾的 char 数组,会向后内存越界遍历 ,直到找到内存中随机的\0,结果为随机值 (笔试计算题最常考);

参数本质:传入的参数是字符串首地址 ,字符数组名、char*指针、字符串常量传入后效果一致,均从该地址开始遍历;

  • strlen 的参数表面上是 char*,本质是字符串的首地址 ------ 不管你传入的是字符数组名char * 指针变量字符串常量 ,最终传递给 strlen 的都是一个内存地址,strlen 只会从这个地址开始逐字符遍历找 \0,三种传入形式的效果完全一致。

例子:

复制代码
void test13()
{
    char str[] = "hello world";    // 原始字符数组
    char* p = str;    // char*指针指向数组首地址
    const char* s = "hello world";    // 字符串常量(本身存储在内存中,有首地址)

    // 形式1:传入字符数组名(本质传数组首地址)
    printf("传入数组名:%d\n", strlen(str)); 
    // 形式2:传入char*指针(本质传指针存储的首地址)
    printf("传入char*指针:%d\n", strlen(p)); 
    // 形式3:传入字符串常量(本质传常量的首地址)
    printf("传入字符串常量:%d\n", strlen("hello world")); 

    // 进阶:传入数组中间地址,从中间开始遍历
    printf("传入数组中间地址str+6:%d\n", strlen(str + 6)); 
}

运行结果:

传入数组名:11

传入char*指针:11

传入字符串常量:11

传入数组中间地址str+6:5

**无符号返回值坑:**返回值是无符号整数,不能直接参与负数比较 ,否则会出现逻辑错误;

例子:

复制代码
void test14()
{
    char str1[] = "a";    
    char str2[] = "abc";  

    if (strlen(str1) - strlen(str2) < 0) {
        printf("str1短\n"); 
    }
    else {
        printf("str2短\n"); 
    }

}

运行结果:

str2短

解释:

发现:这明显是错的,错误逻辑:直接用无符号数相减的结果做负数比较------strlen 返回 size_t(无符号整数),两个无符号数相减的结果还是无符号数 ,永远不会为负数。如果直接用这个结果做负数比较 (如if(strlen(a)-strlen(b) < 0)),这个判断条件永远为假。

正确比较代码:

复制代码
void test14()
{
    char str1[] = "a";    
    char str2[] = "abc";  

    // 正确逻辑:直接用无符号数相减的结果做负数比较
    if ((int)strlen(str1) - (int)strlen(str2) < 0) {
        printf("str1短\n"); 
    }
    else {
        printf("str2短\n"); 
    }

}

运行结果:

str1短

空指针处理:传入 NULL(空指针),会因访问非法内存地址直接导致程序崩溃

**只读特性:**仅对字符串进行逐字符只读遍历,不会修改原字符串的任何内容(包括\0)。

(3) sizeof 与 strlen 之间的对比

对比维度 strlen(字符串库函数) sizeof(C 语言单目运算符)
核心定位 运行时字符串有效字符计数器 编译期内存分配字节数测量器
计算目标 仅统计\0前的有效字符个数 仅计算编译器为变量 / 类型分配的总字节数
\0的处理 唯一终止标志,遇\0立即停止,不计入结果 完全无视\0,计算全部分配内存,与\0无关
计算时机 运行期计算,结果依赖运行时内存实际数据 编译期计算,结果直接替换为常量,运行期不变
内存访问行为 必须访问内存(逐字符遍历),可能内存越界 不访问任何内存,仅解析语法定义,绝对安全
头文件依赖 必须包含<string.h>,否则编译报错 无需任何头文件,直接使用
支持参数类型 仅支持以\0结尾的 char*(合法字符串) 支持 C 语言所有数据类型(基础 / 数组 / 指针 / 结构体 / 空指针等)
参数本质 传入的是字符串首地址,数组名 / 指针 / 常量效果一致 直接识别变量 / 类型,数组名不退化时算数组,退化后算指针
返回值 / 打印 size_t(无符号整数),必须用 % zu打印 size_t(无符号整数),推荐用 % zu 打印
传入 NULL(空指针) 访问地址 0(非法内存),运行时直接崩溃 编译期计算,完全安全,结果为指针大小(64 位 8/32 位 4)
传入无\0的 char 数组 向后越界遍历,结果为随机值(笔试必考) 正常计算数组分配大小,结果固定(数组长度 ×1)
对原数据的操作 只读遍历,绝不修改原字符串(含\0 无任何操作,仅计算大小,与原数据无关
数组传参场景 函数内传入数组名(退化为指针),效果与主函数一致(从首地址遍历) 函数内传入数组名(退化为指针),计算指针大小,非数组实际大小(超高频坑)

二者对比问题

问题 1:传入 NULL 时,二者的行为差异

  • 问题:strlen(NULL)sizeof(NULL)的结果 / 行为?
  • 答案:strlen(NULL)运行期崩溃;sizeof(NULL)安全,结果为 8(64 位)/4(32 位)。
  • 本质:strlen 必须访问内存,NULL 是非法地址;sizeof 编译期计算,不访问内存。

问题 2 :传入无\0的 char 数组,二者的结果差异

  • 例子:char arr[3] = {'a','b','c'};,判断strlen(arr)sizeof(arr)结果?
  • 答案:strlen(arr)随机值 (越界找随机\0);sizeof(arr)3(固定分配 3 字节)。

问题 3:字符串数组中,二者的结果计算

  • 例子:char str[10] = "abc";,计算二者结果?
  • 答案:strlen(str)=3(仅\0前有效字符);sizeof(str)=10(分配的总字节数)。
  • 延伸:char arr[] = "hello";,结果strlen=5sizeof=6(含系统自动加的\0)。

问题 4:是否修改原字符串 / 数据

  • 问题:二者是否会修改传入的原数据?
  • 答案:都不会;strlen 是只读遍历,sizeof 仅计算大小不操作数据。

以上就是我整理的全部知识点啦,大家后续有补充或发现疏漏、错误,欢迎在评论区一起交流探讨~感谢各位的阅读!

相关推荐
what丶k2 小时前
高级架构师面试核心题库(高级版)—— 附深度解析
面试·职场和发展
2501_901147832 小时前
四数相加问题的算法优化与工程实现笔记
笔记·算法·面试·职场和发展·哈希算法
努力学算法的蒟蒻2 小时前
day66(1.25)——leetcode面试经典150
面试·职场和发展
程序员_小兵3 小时前
STM32之中断详解
c语言·stm32·单片机·嵌入式硬件·mcu
guslegend12 小时前
HR面试(2)
面试
鑫—萍12 小时前
嵌入式开发学习——STM32单片机入门教程
c语言·驱动开发·stm32·单片机·嵌入式硬件·学习·硬件工程
LYS_061812 小时前
RM赛事C型板九轴IMU解算(4)(卡尔曼滤波)
c语言·开发语言·前端·卡尔曼滤波
2501_9011478314 小时前
题解:有效的正方形
算法·面试·职场和发展·求职招聘
Getgit14 小时前
Linux 下查看 DNS 配置信息的常用命令详解
linux·运维·服务器·面试·maven