字符串函数
目录
1. 🧾 字符分类函数(ctype.h):给字符 “贴身份标签”
2. 🔄 字符转换函数(ctype.h):大小写 “一键切换”
3. 📏 strlen:字符串长度计算器(string.h)
4. 🚚 strcpy:字符串 “搬家工”(string.h)
5. 🧩 strcat:字符串 “拼接器”(string.h)
6. ⚖️ strcmp:字符串 “裁判”(string.h)
7. 🔒 安全版函数:strncpy/strncat/strncmp
7.2 strncat:指定长度追加(比 strncpy 安全)
8. 🔍 strstr:字符串 “找字匠”(string.h)
9. ✂️ strtok:字符串 “分割刀”(string.h)
10. 📖 strerror/perror:错误信息 “翻译官”
11. 🚨 终极避坑:0/NULL/'\0'/'0' 的区别
✨引言:
日常写 C 语言代码时,字符判断、字符串拷贝 / 拼接 / 比较 / 分割简直是 “家常便饭”!但很多人只会 “照搬库函数”,遇到 “字符串越界”“无符号数减法翻车”“空指针崩溃” 这些坑就懵圈😵💫。今天这篇博客,我把所有高频字符 / 字符串函数扒得明明白白 —— 从基础使用到底层模拟实现,再到致命坑点,用 “贴标签”“搬家工” 这种通俗比喻,让新手也能轻松拿捏!
1. 🧾 字符分类函数(ctype.h):给字符 “贴身份标签”
字符分类函数就像给字符 “做体检”,判断它是数字、字母、空格还是标点,头文件是ctype.h。核心特点:传入字符的 ASCII 值(char 会隐式转 int),返回 “非 0(真)” 或 “0(假)”。
1.1 常用函数速查表(必记!)
| 函数 | 功能描述 | 示例 | 返回值 |
|---|---|---|---|
| isdigit | 判断是否是数字(0~9) | isdigit('5') | 非 0(真) |
| islower | 判断是否是小写字母(a~z) | islower('a') | 非 0(真) |
| isupper | 判断是否是大写字母(A~Z) | isupper('A') | 非 0(真) |
| isalpha | 判断是否是字母(A~Z/a~z) | isalpha('b') | 非 0(真) |
| isspace | 判断是否是空白字符(空格 /\n/\t) | isspace(' ') | 非 0(真) |
| ispunct | 判断是否是标点符号(!@#$ 等) | ispunct('@') | 非 0(真) |
1.2 实战:小写字母转大写(两种写法对比)
需求:把字符串中的小写字母转大写,其他字符不变。⚠️ 易错点:别把'\0'写成'0'(前者是字符串结束符,ASCII=0;后者是字符 0,ASCII=48)!
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <ctype.h> // 必须包含头文件
// 写法1:硬判断ASCII值(不推荐,不够通用)
int main()
{
char arr[] = "I Am a Student";
int i = 0;
while (arr[i] != '\0') // 正确:判断字符串结束符
{
if (arr[i] >= 'a' && arr[i] <= 'z')
{
arr[i] -= 32; // 小写转大写:ASCII值差32
}
i++;
}
printf("%s\n", arr); // 输出:I AM A STUDENT
return 0;
}
// 写法2:用islower库函数(推荐,简洁通用)
int main()
{
char arr[] = "I Am a Student";
int i = 0;
while (arr[i] != '\0')
{
if (islower(arr[i])) // 直接判断是否是小写字母
{
arr[i] -= 32;
}
i++;
}
printf("%s\n", arr); // 输出:I AM A STUDENT
return 0;
}
💡 小技巧:库函数是编译器优化过的,比手动写 ASCII 范围更高效,还能兼容不同编码,优先用库函数!
2. 🔄 字符转换函数(ctype.h):大小写 “一键切换”
如果说分类函数是 “判断”,转换函数就是 “直接修改”——tolower(大写转小写)和toupper(小写转大写),一句话搞定转换!
2.1 基础使用示例
#include <stdio.h>
#include <ctype.h>
int main()
{
printf("%c\n", toupper('a')); // 输出:A(小写转大写)
printf("%c\n", tolower('B')); // 输出:b(大写转小写)
return 0;
}
2.2 优化版:小写转大写(用 toupper 更简洁)
int main()
{
char arr[] = "I Am a Student";
int i = 0;
while (arr[i] != '\0')
{
if (islower(arr[i]))
{
arr[i] = toupper(arr[i]); // 直接调用转换函数,不用记32!
}
i++;
}
printf("%s\n", arr); // 输出:I AM A STUDENT
return 0;
}
3. 📏 strlen:字符串长度计算器(string.h)
strlen的核心是 “统计\0之前的字符个数”,但它有个 “致命坑”—— 返回值是size_t(无符号整数),新手很容易在这里翻车!
3.1 函数特性 + 坑点演示
- 原型:
size_t strlen(const char* str) - 规则:只数
\0前的字符,字符串必须以\0结尾(否则结果随机); - ❌ 大坑:无符号数减法,3-6=-3 会被当成超大正数!
#include <stdio.h>
#include <string.h>
int main()
{
// 坑点:无符号数减法翻车
if (strlen("abc") - strlen("abcdef") > 0)
{
printf(">\n"); // 实际输出:>(错!因为3-6=-3是无符号大正数)
}
else
{
printf("<\n");
}
// 避坑:强制转成int
if ((int)strlen("abc") - (int)strlen("abcdef") > 0)
{
printf(">\n");
}
else
{
printf("<\n"); // 输出:<(对!)
}
return 0;
}
3.2 三种模拟实现(吃透底层逻辑)
写法 1:计数器法(最直观,新手首选)
#include <stdio.h>
#include <assert.h> // 断言:防止传入空指针
int my_strlen(const char* str)
{
int count = 0;
assert(str); // 若str是NULL,程序直接报错,避免空指针解引用崩溃
while (*str) // 等价于*str != '\0',遇到\0循环终止
{
count++;
str++; // 指针后移,遍历每个字符
}
return count;
}
写法 2:指针 - 指针法(无额外变量,优雅)
int my_strlen(const char* s)
{
assert(s);
char* p = s; // 保存字符串起始地址
while (*p != '\0')
{
p++;
}
return p - s; // 指针相减=元素个数(字符占1字节)
}
写法 3:递归法(无循环,练递归思维)
int my_strlen(const char* str)
{
assert(str);
if (*str == '\0')
{
return 0; // 递归终止条件:遇到\0返回0
}
else
{
return 1 + my_strlen(str + 1); // 每递归一次,长度+1
}
}
4. 🚚 strcpy:字符串 “搬家工”(string.h)
strcpy是 “字符串拷贝”,把源字符串(含\0)完整 “搬” 到目标空间。核心禁忌:不能直接给数组名赋值!
4.1 函数特性 + 易错点
- 原型:
char* strcpy(char* destination, const char* source) - 规则:
- 目标空间必须可修改(不能是常量字符串,如
char* p = "xxx"); - 目标空间必须足够大(否则越界崩溃);
- ❌ 严禁直接给数组名赋值(数组名是地址常量,不能被修改)。
- 目标空间必须可修改(不能是常量字符串,如
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[20] = { 0 };
// arr2 = arr1; // 编译报错!数组名是常量,不能赋值(等价于“10=20”)
strcpy(arr2, arr1); // 正确:把arr1的内容(含\0)拷贝到arr2
printf("%s\n", arr2); // 输出:abcdef
// 坑点:目标是常量字符串(只读)
char* p = "xxxxxxxxxx"; // 常量字符串存放在只读区,不能修改
// strcpy(p, arr1); // 运行崩溃!修改只读内存
return 0;
}
4.2 模拟实现(基础版 + 优化版)
基础版:分步拷贝(易理解)
#include <assert.h>
void my_strcpy(char* dest, char* src)
{
assert(src != NULL && dest != NULL); // 双重断言,避免空指针
// 拷贝\0前的字符
while (*src != '\0')
{
*dest = *src;
dest++;
src++;
}
// 关键:拷贝\0(否则目标字符串无结束符,后续操作会越界)
*dest = *src;
}
优化版:一行拷贝(简洁 + 支持链式调用)
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest; // 保存目标起始地址(用于返回)
assert(src && dest); // 简化断言写法
// 核心逻辑:*dest++ = *src++ 先赋值,后指针后移;遇到\0时,赋值后循环终止
while (*dest++ = *src++)
{
; // 空语句即可
}
return ret; // 支持链式调用,比如printf("%s", my_strcpy(dest, src))
}
// 测试
int main()
{
char arr1[] = "hello bit";
char arr2[20] = "xxxxxxxxxxxxxx";
my_strcpy(arr2, arr1);
printf("%s\n", arr2); // 输出:hello bit
return 0;
}
5. 🧩 strcat:字符串 “拼接器”(string.h)
strcat是 “字符串追加”,把源字符串追加到目标字符串的\0位置,相当于 “拼接两个字符串”。
5.1 函数特性 + 使用示例
- 原型:
char* strcat(char* destination, const char* source) - 规则:
- 源字符串必须有
\0(知道拷贝到哪停); - 目标字符串必须有
\0(知道从哪开始追加); - 目标空间足够大、可修改;
- ❌ 不能自己追加自己(会覆盖
\0,陷入死循环)。
- 源字符串必须有
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[20] = "hello "; // 目标空间足够大,末尾有\0
char arr2[] = "world"; // 源字符串,末尾有\0
strcat(arr1, arr2); // 追加:hello + world → hello world
printf("%s\n", arr1); // 输出:hello world
return 0;
}
5.2 模拟实现 + 自追加禁忌解析
#include <assert.h>
char* my_strcat(char* dest, const char* src)
{
assert(dest && src);
char* ret = dest;
// 步骤1:找到目标字符串的\0(追加起始位置)
while (*dest != '\0')
{
dest++;
}
// 步骤2:拷贝源字符串(含\0),和strcpy逻辑一致
while (*dest++ = *src++)
{
;
}
return ret;
}
// ❌ 为什么不能自追加?
int main()
{
char arr1[20] = "abcdef";
// my_strcat(arr1, arr1); // 运行崩溃/死循环!
/* 原因:
初始:arr1 = a b c d e f \0
找\0后,dest指向\0位置,src指向a;
拷贝时:a覆盖\0 → arr1变成 a b c d e f a,此时src还在往后走,
永远找不到\0,会越界访问内存!
*/
return 0;
}
6. ⚖️ strcmp:字符串 “裁判”(string.h)
strcmp是 “字符串比较”,比较的是字符的 ASCII 值,不是字符串长度!很多人误以为 “长的字符串更大”,其实错了~
6.1 函数规则 + 易错点
- 原型:
int strcmp(const char* str1, const char* str2) - 规则:
- 逐字符比较 ASCII 值,直到找到不同字符或
\0; - str1 > str2 → 返回大于 0 的值(VS 中是 1);
- str1 < str2 → 返回小于 0 的值(VS 中是 - 1);
- str1 == str2 → 返回 0;
- 逐字符比较 ASCII 值,直到找到不同字符或
- ❌ 大坑:别用
arr1 == arr2比较字符串内容(比较的是数组首地址,不是内容)。
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abcdef";
// 错误:比较的是数组首地址(arr1和arr2地址不同)
if (arr1 == arr2)
{
printf("相等\n");
}
else
{
printf("不相等\n"); // 输出:不相等
}
// 正确:用strcmp比较内容
int ret = strcmp(arr1, arr2);
if (ret == 0)
{
printf("相等\n"); // 输出:相等
}
else if (ret > 0)
{
printf("arr1 > arr2\n");
}
else
{
printf("arr1 < arr2\n");
}
return 0;
}
6.2 模拟实现(基础版 + 优化版)
基础版:返回 ±1/0(贴合 VS 实现)
#include <assert.h>
int my_strcmp(char* str1, char* str2)
{
assert(str1 && str2);
// 逐字符比较,直到不同或遇到\0
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0; // 全部相等,返回0
}
str1++;
str2++;
}
// 字符不同,返回比较结果
return *str1 > *str2 ? 1 : -1;
}
优化版:返回 ASCII 差值(更通用)
int my_strcmp(char* str1, char* str2)
{
assert(str1 && str2);
while (*str1 == *str2)
{
if (*str1 == '\0')
{
return 0;
}
str1++;
str2++;
}
return *str1 - *str2; // 比如'abq'和'abc'返回 'q'-'c'=16
}
7. 🔒 安全版函数:strncpy/strncat/strncmp
strcpy/strcat/strcmp是 “长度不受限” 的函数,容易越界崩溃。C 标准库提供了 “长度受限” 的安全版,核心是多了一个num参数,控制操作的字符个数。
7.1 strncpy:指定长度拷贝
- 原型:
char* strncpy(char* dest, const char* src, size_t num) - 规则:
- 最多拷贝
num个字符; - 若 src 长度 < num → 剩余位置补
\0; - 若 src 长度 ≥ num → 只拷贝 num 个字符,不补 \0(需手动添加)。
- 最多拷贝
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "abcdef";
char arr2[20] = "xxxxxxxxxxxxxx";
strncpy(arr2, arr1, 8); // src只有6个有效字符+1个\0,剩余1位补\0
printf("%s\n", arr2); // 输出:abcdef(\0截断了后面的x)
return 0;
}
7.2 strncat:指定长度追加(比 strncpy 安全)
- 原型:
char* strncat(char* dest, const char* src, size_t num) - 规则:
- 最多追加
num个字符; - 无论追加多少,末尾自动补 \0(不用手动加);
- 若 src 长度 < num → 只拷贝到 src 的 \0 为止。
- 最多追加
int main()
{
char arr1[20] = "xx\0xxxxxxxx";
char arr2[] = "abcdef";
strncat(arr1, arr2, 3); // 追加前3个字符:a b c + \0
printf("%s\n", arr1); // 输出:xxabc
return 0;
}
7.3 strncmp:指定长度比较
- 原型:
int strncmp(const char* str1, const char* str2, size_t num) - 规则:只比较前
num个字符,比较逻辑和 strcmp 一致。
int main()
{
char arr1[] = "abcdef";
char arr2[] = "abqdefghi";
int ret = strncmp(arr1, arr2, 2); // 比较前2个字符(ab=ab)→ 0
int ret2 = strncmp(arr1, arr2, 3); // 比较前3个字符(abc < abq)→ -1
printf("%d %d\n", ret, ret2); // 输出:0 -1
return 0;
}
8. 🔍 strstr:字符串 “找字匠”(string.h)
strstr的作用是 “在字符串 str1 中找字符串 str2 第一次出现的位置”,找不到返回 NULL(空指针)。
8.1 函数使用示例
#include <stdio.h>
#include <string.h>
int main()
{
char arr1[] = "this is an apple";
char* p1 = "is";
char* ret1 = strstr(arr1, p1);
printf("%s\n", ret1); // 输出:is is an apple(找到第一个is)
char* p2 = "Appl"; // 大小写敏感,找不到
char* ret2 = strstr(arr1, p2);
if (ret2 == NULL)
{
printf("找不到\n"); // 输出:找不到
}
return 0;
}
8.2 模拟实现(暴力匹配法,易理解)
工业级实现用 KMP 算法(效率更高),这里先讲暴力匹配,适合新手理解核心逻辑:
#include <assert.h>
char* my_strstr(const char* str1, const char* str2)
{
assert(str1 && str2);
const char* cur = str1; // 记录每次匹配的起始位置
const char* s1 = NULL; // 遍历str1
const char* s2 = NULL; // 遍历str2
// 特殊情况:str2是空字符串,直接返回str1
if (*str2 == '\0')
{
return (char*)str1;
}
// 外层循环:移动起始位置
while (*cur)
{
s1 = cur;
s2 = str2;
// 内层循环:逐字符匹配
while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
{
s1++;
s2++;
}
// 匹配成功:s2走到\0
if (*s2 == '\0')
{
return (char*)cur;
}
// 匹配失败:起始位置后移一位
cur++;
}
// 所有位置都匹配失败
return NULL;
}
// 测试
int main()
{
char arr1[] = "abcdefabcdef";
char arr2[] = "cdef";
char* ret = my_strstr(arr1, arr2);
if (ret != NULL)
{
printf("%s\n", ret); // 输出:cdefabcdef
}
else
{
printf("找不到\n");
}
return 0;
}
9. ✂️ strtok:字符串 “分割刀”(string.h)
strtok是 “字符串分割神器”,比如把zpengwei@yeah.net拆成zpengwei、yeah、net,把192.168.1.1拆成四个网段。规则有点特殊,必须吃透!
9.1 函数核心规则
- 原型:
char* strtok(char* str, const char* sep) - 规则:
sep是 “分隔符集合”(比如@.表示分隔符是 @和.);- 第一次调用:
str传要分割的字符串,函数找到第一个标记,把分隔符改成\0,并保存当前位置; - 后续调用:
str传 NULL,函数从保存的位置继续找下一个标记; - 找不到标记返回 NULL;
- ❌ 会修改原字符串(建议先拷贝一份再分割)。
9.2 实战:分割邮箱地址
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = "zpengwei@yeah.net";
char arr2[30] = { 0 };
strcpy(arr2, arr); // 拷贝原字符串,避免修改原数据
const char* sep = "@."; // 分隔符集合:@和.
char* ret = NULL;
// 循环分割:简洁写法
for (ret = strtok(arr2, sep); ret != NULL; ret = strtok(NULL, sep))
{
printf("%s\n", ret); // 输出:zpengwei → yeah → net
}
return 0;
}
10. 📖 strerror/perror:错误信息 “翻译官”
C 语言程序运行时,若发生错误(比如打开不存在的文件),会把错误码存到全局变量errno(头文件errno.h)。strerror和perror能把错误码转成人类能懂的字符串。
10.1 strerror:错误码→字符串
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
// 打印0~10的错误码对应的信息
for (int i = 0; i <= 10; i++)
{
printf("%d: %s\n", i, strerror(i));
}
/* 输出示例:
0: No error
1: Operation not permitted
2: No such file or directory
3: No such process
...
*/
return 0;
}
10.2 perror:直接打印错误信息(调试神器)
perror = printf + strerror,自动拼接 “自定义提示 + 冒号 + 错误信息”,比 strerror 更常用。
#include <stdio.h>
#include <errno.h>
int main()
{
// 尝试打开不存在的文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("打开文件失败"); // 输出:打开文件失败: No such file or directory
return 1; // 异常退出
}
// 读文件(省略)
fclose(pf);
pf = NULL;
return 0;
}
11. 🚨 终极避坑:0/NULL/'\0'/'0' 的区别
这四个 “0” 是 C 语言新手的 “高频混淆点”,一张表彻底分清:
| 符号 | 类型 | ASCII 值 | 核心含义 | 用法场景 |
|---|---|---|---|---|
| 0 | 整型常量 | - | 数字 0 | 数值计算(int a=0) |
| NULL | 指针常量 | 0 | 空指针(指向地址 0) | 指针初始化(char* p=NULL) |
| '\0' | 字符常量 | 0 | 字符串结束符 | 字符串末尾(char arr []="a\0") |
| '0' | 字符常量 | 48 | 字符形式的数字 0 | 字符判断(isdigit ('0')) |
✅ 一句话记:数值用 0,指针用 NULL,字符串结束用 '\0',字符数字用 '0'!
🎯 最后总结
- 字符函数(ctype.h):优先用库函数(islower/toupper),比手动写 ASCII 范围更通用;
- 字符串函数:无长度限制的函数(strcpy/strcat)易越界,优先用安全版(strncpy/strncat);
- 模拟实现:吃透指针遍历、\0 处理、断言防空指针,才能避开核心坑;
- 错误处理:调试时用 perror,快速定位文件、IO 操作的错误;
- 避坑关键:分清无符号返回值(strlen)、数组名是常量(不能赋值)、四个 “0” 的区别。
如果这篇博客帮你打通了字符 / 字符串函数的 “任督二脉”,欢迎点赞收藏🌟~
C语言字符串函数详解与避坑指南
1282

被折叠的 条评论
为什么被折叠?



