引言

在C语言程序开发中,字符串处理和内存操作是最基础也最常用的功能。无论是开发系统软件、嵌入式应用还是日常工具,我们都需要频繁使用各种字符串和内存操作函数。然而,这些看似简单的函数如果使用不当,往往会导致程序崩溃、内存泄漏甚至安全漏洞等严重问题。

本文将全面介绍C语言标准库中最常用的字符串函数(来自<string.h>)和内存操作函数,通过清晰的示例代码和实际应用场景,帮助开发者掌握它们的正确使用方法。我们不仅会讲解每个函数的基本用法,还会重点分析使用时的注意事项和常见陷阱,让您能够编写出更加健壮可靠的C语言代码。

为了帮助读者更好地理解和掌握这些关键函数,我们将采用结构化讲解方式,通过函数原型、功能说明、代码示例和典型错误分析四个维度,系统性地剖析每个函数的使用要点。这种层层递进的讲解方式既能帮助初学者快速上手,也能让有经验的开发者查漏补缺,全面规避常见的使用陷阱。


字符分类函数

C语⾔中有⼀系列的函数是专⻔做字符分类的,也就是⼀个字符是属于什么类型的字符的。在此仅为读者做简单介绍而不做介绍,若感兴趣读者可进行自行理解,推荐使用平台https://legacy.cplusplus.com/进行更深入理解。这些函数的使⽤都需要包含⼀个头⽂件是<ctype.h>。

函数如果他的参数符合下列条件就返回真
iscntrl任何控制字符
isspace空白字符:空格 ‘’,换页 ‘\f’,换行‘\n’,回车 ‘\r’,制表符‘\t’或者垂直制表符‘\v’
isdigit十进制数字 ‘0’ ~ ‘9’ 字符
isxdigit十六进制数字,包括所有十进制数字字符,小写字母a~f,大写字母A~F
islower小写字母a~z
isupper大写字母A~Z
isalpha字母a~z或A~Z
isalnum字母或者数字,a~z,A~Z,0~9
ispunct标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph任何图形字符
isprint任何可打印字符,包括图形字符和空白字符

字符串函数基本介绍

字符操作函数

1.strlen函数

1.1函数原型

size_t strlen(const char* str);//函数原型

1.2功能说明

该函数通过计算起始地址到终止空字符'\0'之间的字符数量来确定字符串长度(不包含终止符本身)。

1.3代码示例

1.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abcde";
	int ret = strlen(arr);
	printf("%d", ret);
	return 0;
}
1.3.2strlen和sizeof的区别

sizeof 计算内存字节总数(含'\0'),是静态运算符,适用于所有变量类型;

strlen 统计字符串有效字符数(不含'\0'),是运行时函数,仅处理字符串。

#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "abc";
	printf("%d\n", strlen(arr));//结果为3
	printf("%d\n", sizeof(arr));//结果为4
	return 0;
}
1.3.3strlen函数模拟实现
unsigned int my_strlen(const char* str)
{
	assert(str != NULL);
	int count = 0;
	while (*str != '\0')
	{
		str++;
		count++;
	}
	return count;
}

1.4典型错误

1.4.1strlen()的"越界阅读"隐患及代码说明

当字符串未以'\0'终止时,strlen()会继续向后读取内存,直到偶然遇到'\0'为止,这会导致:

  • 返回错误的长度值

  • 可能引发段错误(Segmentation Fault)

  • 存在信息泄露风险

	char arr[] = {'a','b','c'};//未标明'\0'的位置
	int ret = strlen(arr);
1.4.2与有符号整数混用具有隐式类型转换风险

size_t是无符号类型(通常是unsigned long),当赋值给int时: 若字符串长度 > INT_MAX,会发生截断 后续比较运算可能产生意外行为

// 错误1:长度比较失效
if (strlen(str) > -1) { // 永远为真!因为size_t无符号
    printf("This will always execute!\n");
}

// 错误2:循环条件问题
for (int i = 0; i < strlen(str); i++) { 
    // 当strlen > INT_MAX时,i会溢出

2.strcpy函数

 2.1函数原型

char* strcpy(char * destination, const char * source );//函数原型

2.2功能说明

该函数通过将源指针所指向的C字符串复制到目标指针所指向的数组中来确定字符串内容,包括终止空字符(并在此处停止复制)。

2.3代码示例

2.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[10] = { 0 };
	char arr2[] = "abcdefg";
	strcpy(arr1, arr2);
	printf("%s", arr1);
	return 0;
}
2.3.2strcpy函数模拟实现
char* my_strcpy(char* arr1,const char* arr2)
{
	assert(arr1 && arr2);
	char* ret = arr1;
	while (*arr1++ = *arr2++)
	{
		;
	}
	return ret;
}

2.4典型错误

2.4.1strcpy()的"越界阅读"隐患及代码说明

当源字符串的长度超过目标数组的容量时,会发生缓冲区溢出。

  • 数据损坏:溢出部分会覆盖紧邻目标数组之后的内存数据,这些数据可能是其他变量、函数参数或控制信息,导致它们值被意外修改,程序逻辑出错。

  • 程序崩溃:如果覆盖了关键的内存结构(如函数返回地址、堆栈帧指针),会导致程序突然崩溃,产生段错误(Segmentation Fault)或栈溢出(Stack Overflow)等错误。

  • 安全漏洞:这是最危险的后果。黑客可以精心构造一个超长字符串,利用溢出数据覆盖函数的返回地址,将其指向恶意代码,从而劫持程序执行流程,获取系统控制权。这是许多蠕虫病毒和攻击的根源。

char arr1[5] = { 0 };//只能容纳4个字符 + '\0'
	char arr2[] = "abcdefg";//远大于5
	strcpy(arr1, arr2);
2.4.2如果传递给 strcpy 的任何一个指针是 NULL,会导致程序访问非法内存。
  • 立即崩溃:对 NULL 指针进行解引用操作是非法内存访问,操作系统会强行终止程序,在绝大多数系统上会直接引发段错误(Segmentation Fault)。
  • 调试困难:如果崩溃发生在大型项目或库函数中,错误日志可能只指向 strcpy 这一行,而不是指向将指针设置为 NULL 的真正源头,增加排查难度。

char *dest = NULL;
char *src = "Hello";
strcpy(dest, src); // 对 NULL 指针解引用,通常导致段错误 (Segmentation Fault)

3.strcat函数

3.1函数原型

char* strcat(char * destination, const char * source );//函数原型

3.2功能说明

该函数通过将源字符串的完整内容(包括终止空字符)追加到目标字符串的末尾来确定最终字符串,并返回目标字符串的起始地址。

3.3代码示例

3.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[10] = "abcd";
	char arr2[] = "efg";
	strcat(arr1, arr2);
	printf("%s", arr1);
	return 0;
}
3.3.2strcat函数模拟实现
char* my_strcat(char* arr1, const char* arr2)
{
	char* ret = arr1;
	while (*arr1)
	{
		arr1++;
	}
	while (*arr1++ = *arr2++)
	{
		;
	}
	return ret;
}

3.4典型错误

此处典型错误与前文strcpy函数极为相似,故在此省略,读者朋友可以结合前文进行思考学习。

4.strcmp函数

4.1函数原型

char* strcmp(const char* str1, const char* str2 );//函数原型

4.2功能说明

该函数通过逐字符比较两个字符串的内容(从起始地址到终止空字符'\0')来确定它们的字典序关系,并返回一个表示比较结果的整数值。

4.3代码示例

4.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[10] = "abcde";
	char arr2[10] = "abq";
	int ret = strcmp(arr1, arr2);
	printf("%d", ret);
	return 0;
}
4.3.2strcmp函数模拟实现
int my_strcmp(const char* arr1, const char* arr2)
{
	while (*arr1 && (*arr1 == *arr2))
	{
		arr1++;
		arr2++;
	}
	if (*arr1 > *arr2)
		return 1;
	else if ((*arr1 < *arr2))
		return -1;
	else
		return 0;
}

4.4比较规则

  • 第⼀个字符串⼤于第⼆个字符串,则返回⼤于0的数字。
  • 第⼀个字符串等于第⼆个字符串,则返回0。
  • 第⼀个字符串⼩于第⼆个字符串,则返回⼩于0的数字。
  • 那么如何判断两个字符串?⽐较两个字符串中对应位置上字符ASCII码值的⼤⼩。

5.(6.)(7.)strncpy(strncat)(strncmp)函数

基于前文的知识和下面三个函数的相似性及特殊性,我决定将这三个函数写在同一部分,方便读者对比理解。

如前文所述,strcpy、strcat、strcmp等函数因其无法限制操作长度,极易导致缓冲区溢出这一严重的安全漏洞。为了解决这一问题,C标准库提供了它们的安全增强版本:strncpy, strncat, strncmp。这组函数通过增加一个关键参数 n 来显式指定操作的最大字符数,从而有效防止数组越界。

5.1函数原型

//strncpy
char * strncpy ( char * destination, const char * source, size_t num );
//strncat
char * strncat ( char * destination, const char * source, size_t num );
//strncmp
int strncmp ( const char * str1, const char * str2, size_t num );

5.2功能说明

  • strncpy函数通过从源地址复制最多 n 个字符到目标数组来确定字符串内容,如果未遇到源字符串的终止符则不会追加空字符。
  • strncat函数通过从源地址追加最多 n 个字符到目标字符串的末尾(覆盖其终止符)来确定最终字符串,并始终在结果末尾添加终止空字符。
  • strncmp函数通过逐字符比较两个字符串的最多前 n 个字符(或直到遇到终止符)来确定它们的字典序关系。

综上所述,strncpy、strncat 和 strncmp 在功能上与前述的基本函数(strcpy、strcat、strcmp)一脉相承,但其关键改进在于引入了一个具有计数器性质的参数 num(或 n)。通过显式指定操作的上限字符数,程序员能够对函数的行为实施精确控制,从而从根本上规避因缺乏边界检查而导致的缓冲区溢出问题及其引发的一系列严重后果。

8.strstr函数

8.1函数原型

char * strstr ( const char * str1, const char * str2);

8.2功能说明

该函数通过在前一个字符串(haystack)中从左到右查找后一个字符串(needle)首次出现的起始地址,来确定子串的位置,若未找到则返回空指针。

8.3代码示例

8.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[] = "abbbcdef";
	char arr2[] = "bbcde";
	char* ret = strstr(arr1, arr2);
	if (ret == NULL)
		printf("找不到\n");
	else
		printf("%s\n", ret);
	return 0;
8.3.2strstr函数模拟实现
(1)暴力法
//暴力法
char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	if (*str2 == '\0')
		return str1;
	char* s1 = str1;
	char* s2 = str2;
	char* cp = str1;
	while (*cp)
	{
		s1 = cp;
		s2 = str2;
		while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
			return cp;
		if (*s1 == '\0')
			return NULL;
		cp++;
	}
}
(2)KMP算法

鉴于 KMP 算法涉及的预处理机制(构建部分匹配表)和匹配流程相对复杂,其实现原理与细节已超出本文的讨论范围。为确保内容的专注与深度,作者将基于自身的学习沉淀与时间规划,另行撰写一篇专题博文来系统性地剖析 KMP 算法的核心思想与实现。

9.strtok函数

9.1函数原型

char * strtok ( char * str, const char * sep);

9.2功能说明

该函数通过根据指定的分隔符集合(delimiters)将字符串(str)分割为一系列令牌(token),并通过多次调用逐次返回每个令牌的起始地址,以此来确定字符串中的逻辑片段。

9.3代码示例

9.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr[] = "yunzhui@cccc.com";
	char* p = "@.";
	char* ret = NULL;
	for (ret = strtok(arr, p); ret != NULL; ret = strtok(NULL, p))
	{
		printf("%s\n", ret);
	}
	return 0;

9.4典型错误

9.4.1错误传入源字符串

strtok 会在找到的分隔符位置写入 '\0' 来终止每个令牌,从而永久性地修改你传入的字符串。

如果你传入的是一个字符串字面量(如 "hello,world")或一个常量指针,会导致未定义行为(通常是程序崩溃)。所以应该始终传入一个可修改的字符数组的副本。

// 错误!会导致崩溃
char *str = "hello,world"; // 字符串字面量存储在只读区域
strtok(str, ",");

// 正确!
char str[] = "hello,world"; // 在栈上创建了一个可修改的副本
char str_copy[100];
strcpy(str_copy, original_string); // 创建副本再使用
strtok(str_copy, ",");
9.4.2线程不安全

strtok 在内部使用一个静态变量来记住上一次调用后字符串的位置。如果在多线程环境中同时调用 strtok,这个内部状态会被多个线程竞争和破坏,导致令牌丢失、重复或程序崩溃。

10.strerror函数

10.1函数原型

char* strerror ( int errnum );

10.2功能说明

该函数通过将错误编号(errnum)映射到其对应的系统错误描述字符串的起始地址,来确定错误的可读解释。

10.3代码示例

10.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	FILE* pf = fopen("test.exe", "r");
	printf("%s\n", strerror(errno));
	return 0;
}
10.2strerror和perror的区别

strerror:将错误码转换为描述字符串并返回指针,需自行处理输出格式和目标。

perror:基于当前 errno 值直接将错误描述打印到标准错误流,会自动附加自定义前缀信息。

需要注意的是两个函数使用时需要包含的头文件如下,在此也给出了作者的观点

<string.h>:使用 strerror 函数必须包含此头文件,它提供了该函数的原型声明。
<stdio.h>:使用 perror 函数必须包含此头文件,它提供了该函数的原型声明。
<errno.h>:使用这两个函数时都必须包含此头文件,原因如下:
对于 strerror:它的参数(错误码)通常来源于全局变量 errno 或 EACCES 等标准错误宏,这些都定义在 <errno.h> 中。不包含会导致编译错误。
对于 perror:
可读性与可维护性:虽然 perror 内部会自动读取 errno,但如果你需要在调用前后根据 errno 的值进行逻辑判断(例如 if (errno == EAGAIN)),就必须包含 <errno.h> 来访问 errno 和错误码宏。
兼容性与稳定性:在某些旧版编译器或特定实现中,<stdio.h> 可能不会自动包含 <errno.h> 的全部定义。显式包含 <errno.h> 可以确保 errno 在任何环境下都被正确定义,避免因隐式依赖而潜在的编译风险,是编写稳健、可移植代码的良好习惯。

内存操作函数

前述的 strcpy、strcat 等函数及其安全版本 strncat、strncpy 虽然解决了边界问题,但其设计初衷仍是处理以空字符 '\0' 结尾的文本字符串。当我们需要操作图片数据、结构体、加密数据或任何可能包含 '\0' 的内存块时,字符串函数就无能为力了。为此,C语言提供了一组更为底层的 内存操作函数:memcpy, memmove, memcmp, memset。这组函数完全忽略数据内容,仅以字节为单位进行处理,为我们提供了对内存的绝对控制。

1.memcpy函数

1.1函数原型

void * memcpy ( void * destination, const void * source, size_t num );

1.2功能说明

该函数通过从源内存地址复制指定数量的字节(n)到目标内存地址,来确定目标内存块的内容,此操作不检查重叠。

1.3代码示例

1.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	int arr1[20] = { 0 };
	int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
	memcpy(arr1, arr2, 20);
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		printf("%d ", arr1[i]);
	}
	return 0;
}
1.3.2memcpy函数模拟实现
void* my_memcpy(void* dest, const void* src, size_t num)
{
	void* ret = dest;
	while (num--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

1.4典型错误

1.4.1源内存和目标内存有重叠
char data[] = "abcdefghi";
// 尝试将数据向前移动(重叠区域)
memcpy(data, data + 2, 5); // 行为未定义!应该用 memmove
// 可能的结果:"cdedefghi" 或其他混乱内容

2.memmove函数

2.1函数原型

void * memmove ( void * destination, const void * source, size_t num );

2.2功能说明

该函数通过从源内存地址复制指定数量的字节(n)到目标内存地址,来确定目标内存块的内容,此操作能安全处理内存重叠。

2.3代码示例

2.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	int arr1[20] = { 0 };
	int arr2[] = { 1,2,3,4,5,6,7,8,9,10 };
	memmove(arr1, arr2, 20);
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		printf("%d ", arr1[i]);
	}
	return 0;
}
2.3.2memmove函数模拟实现
void* my_memmove(void* dest, const void* src, size_t num)
{
	void* ret = dest;
	if (dest < src)
	{
		while (num--)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	else
	{
		while (num--)
		{
			*((char*)dest + num) = *((char*)src + num);
		}

	}
	return ret;
}

2.4与memcpy的比较

  • 核心操作是安全内存复制:功能与 memcpy 相同,但保证了重叠内存复制的正确性。
  • 处理内存重叠:通过额外的逻辑(临时缓冲区或方向判断)确保即使源和目标内存重叠,也能复制出正确的结果。
  • 性能权衡:通常比 memcpy 稍慢,但提供了安全保障。

3.memcmp函数

3.1函数原型

int memcmp ( const void * ptr1, const void * ptr2, size_t num );

3.2功能说明

该函数通过逐字节比较两个内存块的前n个字节,来确定它们的二进制内容是否相等。

3.3代码示例

3.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	int arr1[] = { 1,2,3,9 };
	int arr2[] = { 1,2,5,7 };
	int ret = memcmp(arr1, arr2, 16);
	printf("%d", ret);
	return 0;
}

3.4比较规则

与前文strcmp的比较规则一致,但区别是memcmp函数是逐字节进行比较

4.memset函数

4.1函数原型

void * memset ( void * ptr, int value, size_t num );

4.2功能说明

该函数通过使用指定的值(转换为unsigned char)填充目标内存块的前n个字节,来初始化或设置内存内容。

4.3代码示例

4.3.1使用范例
#include<stdio.h>
#include<string.h>
int main()
{
	char arr1[10] = "abcdefghi";
	memset(arr1 + 3, 'x', 3);
	printf("%s", arr1);
	return 0;
}

打印结果: abcxxxghi

结语:掌握基础,方能远行

通过本文的讲解,相信您已经对C语言中常用的字符串和内存操作函数有了更深入的理解。从最基础的strlen、strcpy,到更安全的strncpy、memmove,每一个函数都像工匠手中的工具,只有了解其特性和使用场景,才能打造出坚固可靠的程序。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐