本文聚焦C语言中指针的三大核心应用(数组指针、指针数组、函数指针),附加const与指针的结合、sizeof与strlen的关联高频考点,结合示例与易混点解析,适配面试复习场景。
(一) 数组指针和指针数组
数组指针和指针数组是 C 语言中极易混淆的两个核心知识点,且常被放在一起考察,因此我们将二者整合梳理,帮助大家彻底厘清二者的区别与本质。
首先,我们先明确二者的语法形式和具体示例,建立直观认知。
(1) 直观语法形式与示例
1. 数组指针
语法形式:类型 (* 指针名)[数组长度](注:括号不可省略,直接决定语法优先级的走向)
示例:
int (*p)[5];
// 定义了一个数组指针p,此指针专门用于指向一个包含 5 个int类型元素的一维数组
2. 指针数组
语法形式:类型 * 数组名[数组长度]
示例:
int* arr[5];
//定义了一个指针数组arr,此数组包含 5 个元素,且每个元素都是一个指向int类型数据的指针
(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. 内存特性(关键计算逻辑)
指针数组占用的总字节数遵循固定计算公式:
指针数组总字节数 = 数组长度 × 单个指针变量的字节数
补充说明:
- 单个指针变量的字节数由系统架构决定,与指针指向的数据类型、指向的数据长度无关;
- 32 位系统中,任意类型的指针均占用 4 字节;64 位系统中,任意类型的指针均占用 8 字节。
示例:int* p_arr[3]在 64 位系统中,总字节数 = 3 × 8 = 24 字节。
5. 易混淆点:指针数组 vs 普通二维数组
指针数组 int *arr[5] ≠ 普通二维数组 int arr[5][3],二者核心差异如下:
- 指针数组: 数组元素是独立的指针变量,每个指针可自由指向不同长度、不同位置的内存块(如可以指向长度为 3 的
arr1,也可以指向长度为 5 的arr2),灵活性更高; - **普通二维数组:**整个数组在内存中是连续存储的,行地址是派生的常量指针,每行的长度必须统一固定,无法灵活修改,内存利用率相对固定。
总结
指针数组的本质是「数组」,元素是「同类型指针」;
核心优势是「灵活」,无论是存储字符串还是模拟二维数组,都突破了普通数组的长度限制;
多种解引用形式本质等价,优先使用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. 关键特性:独特的指针步长
数组指针的核心特性是其指针步长(偏移量)固定为指向数组的总字节数:
- 普通指针(如int*)的p+1,步长是单个int类型的字节数(通常 4 字节),仅跳过一个元素;
- 数组指针(如int (*p)[3])的p+1,步长是整个指向数组的总字节数(此处为3×4=12字节),会直接跳过当前指向的整个一维数组,指向相邻的下一个同长度一维数组。
这一特性也是数组指针能够高效遍历二维数组的核心原因 ------p+i可以直接精准定位到二维数组的第i行,无需额外计算字节偏移。
总结:
- 总结数组指针的本质是「指针」,指向的是「固定长度的完整数组」;
- 核心优势是「精准」,尤其是遍历二维数组时,能直接定位行地址,实现高效规范的访问;
- 与指针数组的核心区别在于「本质属性」和「长度灵活性 / 指针步长」。
(二) 函数指针
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);比较器的核心规则
- 参数 :两个
const void*类型 ------ 通用指针,能接收任意类型数据的地址,const保证不修改原数据;- 返回值 :int 型,决定排序顺序(升序为默认常用规则 ,降序仅需反转返回值即可):
- 返回 > 0 :a 指向的数据 排在 b 指向的数据后面;
- 返回 < 0 :a 指向的数据 排在 b 指向的数据前面;
- 返回 = 0:a 和 b 指向的数据相等,顺序不变。
- 关键操作 :
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=5、sizeof=6(含系统自动加的\0)。
问题 4:是否修改原字符串 / 数据
- 问题:二者是否会修改传入的原数据?
- 答案:都不会;strlen 是只读遍历,sizeof 仅计算大小不操作数据。
以上就是我整理的全部知识点啦,大家后续有补充或发现疏漏、错误,欢迎在评论区一起交流探讨~感谢各位的阅读!