C语言笔记归纳15:字符串函数

C语言字符串函数详解与避坑指南

字符串函数

目录

字符串函数

1. 🧾 字符分类函数(ctype.h):给字符 “贴身份标签”

1.1 常用函数速查表(必记!)

1.2 实战:小写字母转大写(两种写法对比)

2. 🔄 字符转换函数(ctype.h):大小写 “一键切换”

2.1 基础使用示例

2.2 优化版:小写转大写(用 toupper 更简洁)

3. 📏 strlen:字符串长度计算器(string.h)

3.1 函数特性 + 坑点演示

3.2 三种模拟实现(吃透底层逻辑)

写法 1:计数器法(最直观,新手首选)

写法 2:指针 - 指针法(无额外变量,优雅)

写法 3:递归法(无循环,练递归思维)

4. 🚚 strcpy:字符串 “搬家工”(string.h)

4.1 函数特性 + 易错点

4.2 模拟实现(基础版 + 优化版)

基础版:分步拷贝(易理解)

优化版:一行拷贝(简洁 + 支持链式调用)

5. 🧩 strcat:字符串 “拼接器”(string.h)

5.1 函数特性 + 使用示例

5.2 模拟实现 + 自追加禁忌解析

6. ⚖️ strcmp:字符串 “裁判”(string.h)

6.1 函数规则 + 易错点

6.2 模拟实现(基础版 + 优化版)

基础版:返回 ±1/0(贴合 VS 实现)

优化版:返回 ASCII 差值(更通用)

7. 🔒 安全版函数:strncpy/strncat/strncmp

7.1 strncpy:指定长度拷贝

7.2 strncat:指定长度追加(比 strncpy 安全)

7.3 strncmp:指定长度比较

8. 🔍 strstr:字符串 “找字匠”(string.h)

8.1 函数使用示例

8.2 模拟实现(暴力匹配法,易理解)

9. ✂️ strtok:字符串 “分割刀”(string.h)

9.1 函数核心规则

9.2 实战:分割邮箱地址

10. 📖 strerror/perror:错误信息 “翻译官”

10.1 strerror:错误码→字符串

10.2 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)
  • 规则:
    1. 目标空间必须可修改(不能是常量字符串,如char* p = "xxx");
    2. 目标空间必须足够大(否则越界崩溃);
    3. ❌ 严禁直接给数组名赋值(数组名是地址常量,不能被修改)。
#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)
  • 规则:
    1. 源字符串必须有\0(知道拷贝到哪停);
    2. 目标字符串必须有\0(知道从哪开始追加);
    3. 目标空间足够大、可修改;
    4. ❌ 不能自己追加自己(会覆盖\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)
  • 规则:
    1. 逐字符比较 ASCII 值,直到找到不同字符或\0
    2. str1 > str2 → 返回大于 0 的值(VS 中是 1);
    3. str1 < str2 → 返回小于 0 的值(VS 中是 - 1);
    4. str1 == str2 → 返回 0;
  • ❌ 大坑:别用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)
  • 规则:
    1. 最多拷贝num个字符;
    2. 若 src 长度 < num → 剩余位置补\0
    3. 若 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)
  • 规则:
    1. 最多追加num个字符;
    2. 无论追加多少,末尾自动补 \0(不用手动加);
    3. 若 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拆成zpengweiyeahnet,把192.168.1.1拆成四个网段。规则有点特殊,必须吃透!

9.1 函数核心规则

  • 原型:char* strtok(char* str, const char* sep)
  • 规则:
    1. sep是 “分隔符集合”(比如@.表示分隔符是 @和.);
    2. 第一次调用:str传要分割的字符串,函数找到第一个标记,把分隔符改成\0,并保存当前位置;
    3. 后续调用:str传 NULL,函数从保存的位置继续找下一个标记;
    4. 找不到标记返回 NULL;
    5. ❌ 会修改原字符串(建议先拷贝一份再分割)。

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)。strerrorperror能把错误码转成人类能懂的字符串。

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'!

🎯 最后总结

  1. 字符函数(ctype.h):优先用库函数(islower/toupper),比手动写 ASCII 范围更通用;
  2. 字符串函数:无长度限制的函数(strcpy/strcat)易越界,优先用安全版(strncpy/strncat);
  3. 模拟实现:吃透指针遍历、\0 处理、断言防空指针,才能避开核心坑;
  4. 错误处理:调试时用 perror,快速定位文件、IO 操作的错误;
  5. 避坑关键:分清无符号返回值(strlen)、数组名是常量(不能赋值)、四个 “0” 的区别。

如果这篇博客帮你打通了字符 / 字符串函数的 “任督二脉”,欢迎点赞收藏🌟~ 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值