《C语言深度解剖》(12):C语言库函数的使用方法和底层原理实现

🤡博客主页:醉竺

🥰本文专栏:《C语言深度解剖》

😻欢迎关注:感谢大家的点赞评论+关注,祝您学有所成!


✨✨💜💛想要学习更多C语言深度解剖点击专栏链接查看💛💜✨✨ 


目录

0. 前言

1. 函数介绍 

1.1 strlen 

1.2 strcpy 

1.3 strncpy  

1.4 strcat 

1.5 strncat 

1.6 strcmp 

1.7 strncmp 

1.8 strstr 

1.9 strtok

1.10 strerror 

1.11 memcpy 

1.12 memmove

1.13 memcmp

1.14 memset 

2. 库函数的模拟实现 

2.1 模拟实现 strlen 

2.2 模拟实现 strcpy 

2.3 模拟实现 strcat

2.4 模拟实现 strstr 

2.5 模拟实现 strcmp 

2.6 模拟实现 memcpy 

2.7 模拟实现 memmove 

2.8 模拟实现 memcmp 

2.9 模拟实现 memset


0. 前言

C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在 常量字符串中或者字符数组中。

字符串常量 适用于那些对它不做修改的字符串函数。


1. 函数介绍 

1.1 strlen 

strlen 是用于计算以空字符结尾的字符串的长度。 

size_t strlen(const char* str);

函数功能:strlen 计算并返回字符串中字符的数量,不包括终止的空字符'\0'。它用于获取字符串的实际长度,以区别于字符串数组的容量。

参数:

  • str: 指向以 '\0' 结尾的字符串的指针。
  • 字符串已'\0' 作为结束标志,strlen函数返回的是在字符串中 '\0' 前面出现的字符个数(不包含 '\0' )。
  • 返回值: 返回 size_t 类型的值,表示字符串的长度(即不包括终止的空字符)。

注意事项:

  • strlen 假定输入的字符串是有效且以 '\0' 结尾的。如果字符串中没有终止的空字符,将会导致未定义行为,可能引发内存访问错误。
  • 使用 strlen 时应确保字符串以 '\0' 结尾,以保证函数能正确返回字符串长度。
  • strlen 的返回类型是 size_t,这是一个无符号整数类型,预期能表示平台上最大可能的数组大小。 

1.2 strcpy 

strcpy 是一个标准的C库函数,用于将一个以空字符结尾的字符串复制到另一个缓冲区。 

char *strcpy(char *dest, const char *src);

函数功能: strcpy 将源字符串的内容复制到目标缓冲区中,包括终止的空字符 '\0'。

参数:

  • dest: 指向目标缓冲区的指针,复制后的字符串将存放在那里。
  • src: 指向源字符串的指针,该字符串以 '\0' 结束。
  • 返回值: 返回指向目标缓冲区的指针 (dest)。

注意事项: 

  • strcpy 不会检查源字符串是否比目标缓冲区长,目标缓冲区 dest 必须足够大,以确保能够容纳源字符串中的所有字符加上结尾的空字符 '\0',否则可能导致缓冲区溢出。
  • 目标空间必须可变(不可以被const修饰,否则怎么复制到目标空间)
  • 为了安全使用,当实际长度未知且存在潜在风险时,建议使用 strncpy 或其他安全版本的字符串复制函数。

1.3 strncpy  

strncpy 是一个标准的C库函数,用于将一个字符串的指定数量字符复制到另一个缓冲区中。 

char *strncpy(char *dest, const char *src, size_t n);

 函数功能:strncpy 将源字符串的最多 n 个字符复制到目标缓冲区中。如果源字符串的长度小于 n,则函数会在目标中用空字符 '\0' 填充,直到总共复制 n 个字符。

参数:

  • dest: 指向目标缓冲区的指针,应有足够的空间来容纳 n 个字符。
  • src: 指向源字符串的指针,该字符串以 '\0' 结束。
  • n: 要复制的最大字符数。
  • 返回值: 返回指向目标缓冲区的指针 (dest)。

注意事项:

  • strncpy 不会自动在复制完成后为目标字符串添加终止空字符 '\0',除非源字符串较短且剩余的空间用空字符填充。因此,为确保目标字符串总是以 '\0' 结束,通常需要手动添加。
  • 目标缓冲区大小必须至少为 n,以避免缓冲区溢出。
  • 如果 n 大于源字符串的长度,剩余的目标缓冲区部分将以 '\0' 填充。
  • strncpy 不会检查源和目标缓冲区是否重叠,重叠使用可能导致未定义行为。

1.4 strcat 

strcat 用于将一个以空字符结尾的字符串连接到另一个以空字符结尾的字符串末尾。 

char *strcat(char *dest, const char *src);

函数功能:strcat 将源字符串的内容追加到目标字符串的末尾处(目标字符串中的终止空字符 '\0' 被源字符串的第一个字符覆盖,),并保证最终结果仍然是以空字符 '\0' 结尾的。 

参数:

  • dest: 指向目标字符串的指针,应有足够的空间来容纳最终组合的字符串,包括源字符串和终止空字符。
  • src: 指向源字符串的指针,该字符串以 '\0' 结束,将被追加到目标字符串的末尾。
  • 返回值: 返回指向目标字符串的指针 (dest)。

注意事项:

  • strcat 不会检查源字符串是否能安全附加到目标字符串,这可能导致安全问题如溢出。目标缓冲区 dest 必须足够大,以确保能够容纳原有的目标字符串、源字符串和终止的空字符 '\0',否则可能导致缓冲区溢出。
  • 自己给自己追加的时候不要用这个函数,用strncat
  • 为减少风险,可以使用更安全的替代函数 strncat,它在附加内容时考虑目标缓冲区的最大大小。

1.5 strncat 

strncat 用于将一个字符串的最多前 n 个字符追加到另一个以空字符结尾的字符串的末尾。 

char *strncat(char *dest, const char *src, size_t n);

函数功能:strncat 将源字符串 src 的最多 n 个字符追加到目标字符串 dest 的末尾,并在结果字符串的末尾添加一个终止空字符 '\0'。 

参数:

  • dest: 指向目标字符串的指针,应该有足够的空间来容纳连接后的字符串。
  • src: 指向源字符串的指针,该字符串以 '\0' 结束。
  • n: 指定从源字符串追加的最多字符个数。
  • 返回值: 返回指向目标字符串的指针 (dest)。

注意事项

  • 目标缓冲区 dest 必须有足够的空间来容纳其原有内容、从源字符串 src 追加的字符以及最终的终止空字符 '\0',否则可能导致缓冲区溢出。
  • strncat 会在连接后的字符串末尾自动添加一个终止空字符。
  • n 要附加的最大字符数。如果 src 的长度小于 n,则整个 src 会被附加到 dest;如果 src 的长度大于或等于 n,则只有前 n 个字符会被附加到 dest 中,并且 strncat 会自动在末尾添加终止符 '\0'。 

1.6 strcmp 

strcmp  用于比较两个以空字符结尾的字符串。

int strcmp ( const char * str1, const char * str2 );

函数功能: strcmp 逐个字符地比较两个字符串,直到找到不同的字符或者达到字符串的末尾。比较依据是字符的ASCII值。

参数:

  • str1: 指向第一个以 '\0' 结尾的字符串的指针。
  • str2: 指向第二个以 '\0' 结尾的字符串的指针。

返回值:

  • ​​​​如果 s1 和 s2 相等,返回 0。
  • 如果 s1 小于 s2(即 s1 的第一个不匹配字符的 ASCII 值小于 s2 对应字符的 ASCII 值),返回一个小于 0 的值。
  • 如果 s1 大于 s2(即 s1 的第一个不匹配字符的 ASCII 值大于 s2 对应字符的 ASCII 值),返回一个大于 0 的值。 

注意事项

  • strcmp 在比较时区分字母的大小写。即“apple”和“Apple”会被判定为不相等。
  • 一旦找到不同的字符,strcmp 会立即返回结果,而不再继续比较后续字符。
  • 要确保输入的字符串是以 '\0' 结尾的,否则可能会导致未定义行为。

1.7 strncmp 

strncmp 用于比较两个以空字符结尾的字符串的前几个字符。 

int strncmp ( const char * str1, const char * str2, size_t num );

函数功能:strncmp 比较两个字符串的前 num 个字符,逐字符比较,直到达到指定的字符数或找到不同的字符。比较的依据是字符的ASCII值。

参数:

  • str1: 指向第一个以 '\0' 结尾的字符串的指针。
  • str2: 指向第二个以 '\0' 结尾的字符串的指针。
  • num: 要比较的字符的最大数量。

返回值:

  • 如果 str1 和 str2 的前 n 个字符相等,返回 0。
  • 如果 s1 的前 num 个字符小于 str2 的前 num 个字符(即 str1 的某个字符的 ASCII 值小于 str2 对应字符的 ASCII 值),返回一个小于 0 的值。
  • 如果 str1 的前 num 个字符大于 str2 的前 num 个字符(即 str1 的某个字符的 ASCII 值大于 str2 对应字符的 ASCII 值),返回一个大于 0 的值。

注意事项:

  • 如果 num 的值超过了字符串的长度,strncmp 会在遇到空字符 '\0' 时停止比较。
  • strncmp 同样区分大小写,“apple”和“Apple”将被判定为不相等。
  • 即使实际字符串长度小于 num,函数也会有效地比较直到结尾的空字符。

1.8 strstr 

strstr 是一个标准的C库函数,用于在一个字符串中查找另一个字符串的首次出现。 

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

函数功能: strstr 在给定的字符串 str1 中查找字符串 str2 的第一次出现,并返回一个指向该位置的指针。如果找到子字符串,strstr 函数返回指向 str1 中子字符串第一次出现的指针;如果没有找到,返回 NULL

参数: 

  • str1: 要搜索的源字符串,以 '\0' 结尾。
  • str2: 要查找的子字符串,也以 '\0' 结尾。

返回值:

  • 返回指向 str1 中 str2 首次出现位置的指针。
  • 如果 str2 不在 str1 中,返回 NULL
  • 如果 str2 是空字符串,strstr 返回 str1。

注意事项:

  • strstr 的搜索是区分大小写的,因此 “Hello” 和 “hello” 将被视为不同的字符串。
  • 如果 str2 是空字符串,函数将返回 str1,即认为空字符串在任何字符串中都是“首次出现”的。
  • 此函数返回的指针指向 str1 中的位置,因此原始字符串 str1 不会被改变。

1.9 strtok

strtok 是一个标准的C库函数,用于将字符串拆分为一系列标记(tokens),使用指定的分隔符集合进行分割。 

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

函数功能:strtok 在一个字符串中搜索分隔符集合,逐个返回其分隔出的子字符串(标记),直到处理完整个字符串。 

参数:

  • str: 要拆分的字符串。如果传递的是 NULL,则函数将继续使用上一次调用后保存的字符串。
  • sep: 包含分隔符字符的字符串,这些字符用于分割目标字符串。
  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回 NULL 指针。

返回值:

  • 返回指向在目标字符串中的下一个标记的指针。
  • 如果没有更多标记可返回,则返回 NULL。 

1.10 strerror 

strerror 是一个标准的C库函数,用于将错误代码转换为描述错误的可读字符串。 

char * strerror ( int errnum );

函数功能:strerror 接收一个错误代码(通常是从errno中获得),并返回一个指向以null结尾的字符串的指针,该字符串描述了与之相关联的错误信息。 

参数:

errnum: 表示错误代码的整数值,通常是全局变量 errno 的值。


字符分类函数: 

函数如果他的参数符合下列条件就返回真
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.11 memcpy 

memcpy用于从源内存块复制指定数量的字节到目标内存块。  

void *memcpy(void *dest, const void *src, size_t n);

函数功能:memcpy 函数从源地址 src 开始,复制 n 字节的数据到目标地址 dest。

参数:

  • dest: 指向目标内存块的指针,复制后的数据将存放在那里。
  • src: 指向源内存块的指针,从那里复制数据。src 和 dest 的类型是一个无类型指针(void pointer),这意味着它们可以指向任何类型的数据。
  • n: 需要复制的字节数。
  • 返回值: 返回指向目标内存块的指针 (dest)。 

注意事项:

  • memcpy 不会检查目标和源内存是否重叠。重叠可能会导致未定义行为。
  • 确保目标内存块足够大以接收 n 字节,以避免缓冲区溢出。
  • memcpy 直接操作内存,因此它不负责处理字符串结束的空字符(‘\0’)。即它只拷贝指定数量的字节,并不会在目标数组中添加空字符,也不会因为遇到空字符而停止拷贝。
  • 使用 memcpy 函数时需要小心,因为它不检查数组边界或重叠。如果目标和源内存区域重叠,拷贝的结果是未定义的。如果需要处理可能重叠的内存区域,应该使用 memmove 函数。 

在这个例子中,字符串 "Hello World" 被复制到 dest 数组中,包括字符串结束的空字符。函数返回的是 dest 的指针,但在实际使用中通常不需要接收这个返回值。 

1.12 memmove

memmove 用于在内存中移动数据块。与 memcpy 不同,memmove 可以安全地处理源和目标重叠的情况,这是它的一个重要特性。

void *memmove(void *dest, const void *src, size_t n);

函数功能: memmove 从源地址 src 开始,安全地复制 n 字节的数据到目标地址 dest。即使源和目标内存区域重叠,memmove 也能保证正确的结果。 

参数:

  • dest: 指向目标内存块的指针,复制后的数据将存放在那里。
  • src: 指向源内存块的指针,从那里复制数据。
  • n: 需要复制的字节数。
  • 返回值: 返回指向目标内存块的指针 (dest)。 

  1. 在这个例子中,memmove将字符串 “can be very” 从索引15开始的位置复制到索引20开始的位置,覆盖了 “can be very” 后面的字符。最终打印的字符串将是 “memmove can be very useful”。
  2. 请注意,memmove和 memcpy的功能类似,但是 memcpy不保证在源地址和目标地址重叠时能正确工作,而 memmove可以。因此,当你不确定源地址和目标地址是否重叠时,应该使用 memmove来确保程序的正确性。
  3. 虽然 memmove 可以处理重叠的内存区域,但如果您确定内存区域不会重叠,使用 memcpy 函数会更高效,因为 memcpy 不会处理重叠的情况,因此通常执行速度更快。

1.13 memcmp

memcmp 用于比较两个内存块的内容。这个函数可以用于判断两个内存区域(通常是数组或结构体的内存表示)是否相等,或者哪个大于另一个。  

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

函数功能:memcmp 逐字节比较两个内存块的数据,比较范围为指定的字节数。如果比较的内存内容完全相同,则返回0;如果不同,则返回负值或正值,具体取决于不同的字节比较顺序。

参数说明:

  • ptr1: 指向第一个内存块的指针。
  • ptr2: 指向第二个内存块的指针。
  • n: 要比较的字节数。

返回值:

  • 0:表示两个内存块的内容在指定的字节数范围内相同。
  • <0:表示在比较过程中,第一个内存块的某个字节小于第二个内存块的相应字节。
  • >0:表示在比较过程中,第一个内存块的某个字节大于第二个内存块的相应字节。

注意事项: 

  • memcmp 执行内存的逐字节比较,而不会考虑内存块中的数据类型。
  • 不处理字符串末尾的空字符 ‘\0’,比较完全由 n 决定。
  • 因为 memcmp 是按字节处理的,所以两个内存块的开始到结束不一定需要是相同的类型,只需要有足够的大小来比较。

1.14 memset 

memset 用于将指定值填充到一块内存区域中。它通常用于初始化数组或结构体的内存块。 

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

函数功能memset 函数会将 ptr 指向的内存区域的前 num 个字节设置为 value 的值。这个函数通常用于初始化内存块,或者将内存块中的数据清零。函数的返回值是指向被设置内存区域的指针,即 ptr。 


2. 库函数的模拟实现 

2.1 模拟实现 strlen 

三种方式:

方式1:计数器方式

int my_strlen(char* str)
{
	int length = 0;

	while (*str != '\0')
	{
		length++;
		str++;
	}

	return length;
}

方法2:(不能创建临时变量计数器)递归

int my_strlen(char* str)
{
	if (*str == '\0')
		return 0;
	else
		return 1 + my_strlen(str + 1);
}

方法3:指针-指针的方式 

int my_strlen(char* str)
{
	char* start = str;
	while (*str != '\0')
	{
		str++;
	}

	return str - start;
}
int my_strlen(char* str)
{
	char* start = str;
	while (*str++ != '\0')
	{
		;
	}

	return str - start - 1;
}

2.2 模拟实现 strcpy 

char* my_strcpy(char* dest, const char* src)
{
	assert(dest && src);
	char* ret = dest;
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

2.3 模拟实现 strcat

int my_strcmp(const char* str1, const char* str2)
{
	assert(str1 && str2);
	
	while (*str1 == *str2)
	{
		if (*str1 == '\0')
			return 0;
		str1++;
		str2++;
	}

	return *str1 - *str2;

	/*if (*str1 > *str2)
		return 1;
	else
		return -1;*/
}

2.4 模拟实现 strstr 

char* my_strstr(const char* str1, const char* str2)
{
	assert(str1 && str2);
	char* s1 = NULL;
	char* s2 = NULL;
	char* cp = (char*)str1;

	while (*cp)
	{
		s1 = cp;
		s2 = (char*)str2;
		while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
		{
			s1++;
			s2++;
		}

		if (*s2 == '\0')
			return cp;
		cp++;
	}
	return NULL;
}

2.5 模拟实现 strcmp 

int my_strcmp(const char* str1, const char* str2)
{
	assert(str1 && str2);
	
	while (*str1 == *str2)
	{
		if (*str1 == '\0')
			return 0;
		str1++;
		str2++;
	}

	return *str1 - *str2;

	/*if (*str1 > *str2)
		return 1;
	else
		return -1;*/
}
int my_strcmp(const char* src, const char* dst)
{
    int ret = 0;
    assert(src != NULL);
    assert(dest != NULL);
    while (!(ret = *(unsigned char*)src - *(unsigned char*)dst) && *dst)
        ++src, ++dst;
    if (ret < 0)
        ret = -1;
    else if (ret > 0)
        ret = 1;
    return(ret);
}

2.6 模拟实现 memcpy 

void* my_memcpy(void* dest, void* src, size_t num)
{
	void* ret = dest;
	assert(dest && src);

	while (num--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}

	return ret;
}

2.7 模拟实现 memmove 

void* my_memove(void* dest, void* src, size_t num)
{
	assert(dest && src);
	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.8 模拟实现 memcmp 

#include <stddef.h>  // 为了使用 size_t

int my_memcmp(const void *ptr1, const void *ptr2, size_t num) {
    const unsigned char *p1 = (const unsigned char *)ptr1;
    const unsigned char *p2 = (const unsigned char *)ptr2;

    for (size_t i = 0; i < num; ++i) {
        if (p1[i] != p2[i]) {
            return p1[i] - p2[i];
        }
    }
    return 0;
}

解释:

类型转换:

由于 void 类型不能直接进行计算或偏移操作,首先将指针转换为 unsigned char *。使用 unsigned char 来处理字节级别的比较,避免任何符号扩展问题。

循环比较:

遍历所需比较的字节数 num,逐字节比较两个内存区域的内容。若发现不同,立即返回差值,表示两内存块不相等。

返回值:

如果所有 num 个字节均相等,则返回 0,表示两内存块相等。

这个实现逻辑直接模拟了标准 memcmp 函数的行为,能够比较任何两个内存区域,适用于多种数据类型的比较。此外,它处理二进制数据的问题,如不同数据大小或非文本字符等,这在二进制文件读取或网络传输中十分有效。

2.9 模拟实现 memset

#include <stddef.h> // 为了使用 size_t

void *my_memset(void *ptr, int value, size_t num) {
    unsigned char *p = (unsigned char *)ptr;
    unsigned char val = (unsigned char)value;

    for (size_t i = 0; i < num; ++i) {
        p[i] = val;
    }

    return ptr;
}

解释:

类型转换:

首先将 void * 转换为 unsigned char *,以便逐字节操作内存。unsigned char 是标准的字节大小单位,确保没有符号扩展问题。

将 value 转换为 unsigned char 类型,将其作为用于填充的单字节值。

填充循环:

使用一个循环遍历指定的字节数 num,并将内存位置的每个字节设置为 value

返回值:

返回最初传入的指针 ptr,与标准 memset 的行为一致,便于链式调用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醉竺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值