[C] 字符串库函数 及模拟实现


C语言中的函数分为两个大类:

  1. 库函数
    库函数中又分为:标准库 / 第三方库
  2. 自定制函数

在本篇博客中,我们就深入函数基理,解剖函数功能。运用基本代码模拟实现各个函数,这样就便于调试代码,客观的看看函数是如何实现和运作的。


字符串长度比较函数:

strlen

[strlen的官方文档]:http://www.cplusplus.com/reference/cstring/strlen/?kw=strlen

函数的作用是:【计算字符串的字节长度】。

size_t strlen ( const char * str );
  • size_tunsigned int的缩写,在32位系统中占4个字节,在64位系统中为 unsigned long int(无符号长整型),占8个字节。
    strlensizeof函数都返回的是size_t类型的数据。
    _t是通过typedef定义的含义。
typedef unsigned int size_t;
  • const修饰的指针类型表示指针指向的内容不可更改。
  • C 风格字符串以\0作为结束标志,strlen函数的指针参数指向的字符串必须以\0结尾。
    函数返回的数据是:在字符串中\0之前出现的字符个数且不包含\0,打印方式是%lu,无符号长整型格式数据。

模拟实现

size_t Strlen(const char *str) {
	assert(str != NULL);	//参数合法性校验
	size_t val = 0;			//计数器
	while (*str) {			//指针指向\0,字符串结束,跳出
		val++;			
		str++;				//指针指向下一个字符
	}
	return val;
}

【注】:

  1. 进行函数参数合法性校验是非常必要的一个环节,如果传入函数的是一个无效参数,例如NULL空指针。
  2. 如果没有校验这一步,在函数之后的逻辑中存在对指针参数解引用的操作,解引用一个空指针就造成访问越界的未定义行为了,结果不可预期。
  3. 代码中assert语句称为断言,它的本质就是一个,宏之中是一个表达式。

“断言”:
如果表达式为,断言通过,进行之后的逻辑。
如果表达式为,断言失败,程序主动停止,直接退出。

大致相当于如下操作:

if(str == NULL){
	return 0;
}

二者的区别在于:

断言命中,直接程序结束。
if…else命中,函数结束,继续执行主函数中之后的逻辑。

由此可见:

  • 断言:适合于 参数会造成非常严重后果的场景,同时也要考虑程序一旦停止后的成本因素(例如生产环境中的服务器程序)。
  • if…else语句:适合参数造成不太严重的错误,可以在以后解决的问题。

长度不受限制的字符串函数:

strcpy

[strcpy的官方文档]:http://www.cplusplus.com/reference/cstring/strcpy/?kw=strcpy

函数的作用是:【将一个字符串拷贝到另一个字符串中】。

char * strcpy ( char * destination, const char * source );
  • source:使用中可以简写为src(源),const修饰说明作为目标的源字符串不能修改。
  • destination:使用中可以简写为dest(目标),没有被const修饰说明他的内容是可变的,且必须可变,同时也要足够大的空间存放源字符串中内容。
  • 返回值是char *类型数据,文档中写到destination is returned,说明目标参数同时被返回了。
  • 源字符串必须以'\0'结尾,函数会将源字符串中的 ‘\0’ 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串,避免溢出,且必须可变
  • 字符串之间不能像如下这样相互赋值,只能在定义的时候通过=初始化,不能直接赋值,通过字符串更改另一个字符串的内容要通过strcpy
char str[] = "String1";
str = "String2";		//更改字符串内容不能通过直接赋值
strcpy(str,"String2");	//正确写法

模拟实现

char* Strcpy(char *dest, const char *src) {
	assert(dest != NULL);
	assert(src != NULL);
	//这里不建议写为 assert(src != NULL && dest != NULL);
	//因为一旦断言成功,无法得知是何者为无效指针
	
	int i = 0;
	for (; src[i] != '\0'; i++) {
		dest[i] = src[i];
	}
	dest[i] = '\0';
	return dest;
}

【注】:

  1. 程序通过逐个字符赋值的方法完成了字符串的“拷贝”,最后在字符数组最后一位放置\0结束符,完成C风格字符串的拷贝。
  2. 【警告】int类型在32位机器上占存4字节,表示的最大范围为21亿,折合约为2G大小的内存。如果工程上字符串长度超过2G大小,该类型无法正确表示,会发生溢出而变成负数。
    所以保险起见,建议换为8个字节的int64_t类型,它无论32还是64位机器上都是占用8个字节的内存空间。(同时要包含头文件<stdint.h>
  3. 【警告】存在一定几率的缓冲区重合问题,所以只能算是简陋的strcpy

strcat

[strcat的官方文档]:http://www.cplusplus.com/reference/cstring/strcat/?kw=strcat

函数的作用是:【拼接两个字符串】。

char * strcat ( char * destination, const char * source );
  • 源文件被拼接在了目标文件之
  • 目标空间要足够大同时容纳源字符串和目标字符串加和的大小。
  • 因为其返回值是char *,同时返回的是dest目标,所以可以通过一行表示多次拼接:
strcat(str1,str2);
strcat(str1,"abc");
strcat(str1,"def");

//下面这行代码与前三行是等效的
strcat(strcar(strcat(str1,str2),"abc"),"def");

模拟实现

char *Strcat(char *dest, const char *src) {
	assert(dest != NULL);
	assert(src != NULL);
	int i = 0;
	for (; dest[i] != '\0'; ++i);		//找到 dest 的末尾
	for (int j = 0; src[j] != '\0'; ++j, ++i) {
		dest[i] = src[j];		//开始拷贝数据
	}
	dest[i] = '\0';
	return dest;
}

strcmp

[strcmp的官方文档]:http://www.cplusplus.com/reference/cstring/strcmp/?kw=strcmp

函数的作用是:【比较两个字符串字节大小】。

int strcmp ( const char * str1, const char * str2 );
  • 两个指针参数都是const修饰的,说明要比较的两个字符串在函数调用期间内容都是不可改变的。因为我们是简单的比较大小,是不会影响到内容的。
  • 返回值是int类型参数,如果返回值小于零<0,说明str2长于str1字符串。如果返回值大于零>0,说明str1长于str2字符串,如果返回值为0,说明两字符串长度相同。
  • 因为C语言中字符串是用字符数组来表示的,而字符数组传参可以隐式转换为指针,所以此处的大小比较就不是两个字符串内容的比较了,而是两个指针保存的两个地址的比较。

模拟实现

int Strcmp(const char * str1, const char * str2) {
	assert(str1 != NULL);
	assert(str2 != NULL);
	while (*str1 != '\0' && *str2 != '\0') {
		if (*str1 < *str2) {
			break;
		}
		else if (*str1 > *str2) {
			break;
		}
		else {
			++str1;
			++str2;
		}
	}
	
	//写法一		(对于写法二稍微好一些)
	if (*str1 < *str2) {
		return -1;
	}
	else if (*str1 > *str2) {
		return 1;
	}
	else {
		return 0;
	}
//********************************************	
	//写法二		(略复杂)
	if (*str1 == '\0' && *str2 != '\0') {
		return -1;
	}
	if (*str1 != '\0' && *str2 == '\0') {
		return 1;
	}
	if (*str1 == '\0' && *str2 == '\0') {
		return 0;
	}
}

【注】:

  • 程序先在while逻辑中通过字典序的比较方式比较了两字符串的大小,再在后面的代码判断循环结束情况。
  • 写法一通过ASCII码的方式进行了比较,'\0'ASCII码为0,又因为ASCII都为正值,所以就可以通过比较来查看何者先遇到了'\0'这种情况。同时也可以处理二者字面大小胜负已分的比较情况。一举两得。

另一种写法:

int Strcmp(const char * str1, const char * str2) {
	while(*str1 && (*str1 == *str2)){
		str1++;
		str2++;
	}
	return *str1 - *str2;
}

长度受限制的字符串函数:

strncpy

[strncpy的官方文档]:http://www.cplusplus.com/reference/cstring/strncpy/?kw=strncpy

函数的作用是:【将一个字符串中n个字符拷贝到另一个字符串】。

char *strncpy (char * destination, const char * source, size_t num );
  • 拷贝n个字符从源字符串到目标空间。限制拷贝数量的目的就是:防止目标空间太小而超出缓冲区容纳范围,造成越界
  • 如果源字符串的长度小于n,则拷贝完源字符串之后,在目标的后面追加0,直到n个。

模拟实现

char *Strncpy(char * dest, const char * src, size_t n) {
	assert(dest != NULL);
	assert(src != NULL);
	size_t i = 0;
	for (; i < n; ++i) {
		dest[i] = src[i];
	}
	dest[i] = '\0';
	return dest;
}

【小结】:strncpystrcpy函数原理相同,只是循环终止条件有些差异。strcpy函数找到'\0'的字符数组元素才终止拷贝,而strncpy达到第n个字符下标就停止拷贝,二者异曲同工。


strncat

[strncat的官方文档]:http://www.cplusplus.com/reference/cstring/strncat/?kw=strncat
函数的作用是:【将一个字符串中n个字符拼接到另一个字符串之后】。

char *strncat (char * destination, const char * source, size_t num );

模拟实现

char* Strncat(char* dest, const char* str, size_t n){
	assert(dest != NULL);
	assert(str != NULL);
	char *ret = dest;	//创建ret指针储存dest指针的初始位置
	while (*dest) {
		dest++;
	}
	while (n--) {
		if (!(*dest++ = *str++)) {	//指针str指向'\0'时才进行if内逻辑
			return ret;
		}
	}
	*dest = '\0';
	return ret;
}
  • 创建ret指针存储dest指针的初始位置这一步必要吗?必要,因为在我们找到dest指针指向的目标字符串尾点过程中需要改变指向,在改变之初给它设立一个备份就可以避免再次寻找原来位置的过程。
  • 函数返回有两种情景:
    一是str指针指向的源字符串元素到达了'\0'if语句的条件通过,执行了其中的return语句。
    二是完成了n个字符的拼接,成功实现。

以上这两种返回情况都是有效的。


strncmp

[strncmp的官方文档]:http://www.cplusplus.com/reference/cstring/strncmp/?kw=strncmp
函数的作用是:【比较两个字符串n个字节内的内容】。

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

模拟实现

int Strncmp(const char * str1, const char * str2, size_t num) {
	assert(str1 != NULL);
	assert(str2 != NULL);
	while (num--) {
		if (*str1 > *str2) {
			break;
		}
		else if (*str1 < *str2) {
			break;
		}
		else {
			str1++;
			str2++;
		}
	}
	if (*str1 > *str2) {
		return 1;
	}
	else if (*str1 < *str2) {
		return -1;
	}
	else{
		return 0;
	}
}

另一种写法:

int my_strncmp(const char* str1, const char* str2, size_t count){
	if(!count){		//如果count=0, 直接返回零
		return 0;
	}
	while(--count && *str1 && (*str1 == *str2)){
		str1++;
		str2++;
	}
	return *str1 - *str2;
}

strerror

要想使用strerror函数,首先需要知道errno的含义:错误码。默认情况下,errno值为0 ,即No error,无错误。

  • strerror 函数根据返回的错误码,返回对应的错误信息。
  1. 要想使用errno需要包含头文件<error.h>
  2. 在其基础上想使用strerror函数需要包含头文件<string.h>

默认样例如下:

int main() {
	printf("%d\n", errno);	//默认输出了 0 
	printf("%s\n", strerror(errno));	//输出 No error
	system("pause");
	return 0;
}

如果使用了某些库函数或者操作系统函数,这些函数在执行失败的时候就设置errno的值:

int main() {
	FILE *fp = fopen("aaa.txt", "r");
	printf("%d\n", errno);	
	printf("%s\n", strerror(errno));
	system("pause");
	return 0;
}

此程序输出结果为:
2
No such file or directory
表示错误码为2,此错误码的原因是没有找到此文件或目录

通过errno返回错误信息的这种错误处理和目前的许多编程语言相比就显得十分笨拙了,当前许多语言并没有采用这种方案,而是一种异常处理的校验机制,这也是C语言的经(luo)典(hou)性的表现之一。


  • 小结

库函数中有关字符串的函数就介绍完毕了,希望读者看完本篇博客能够掌握每个字符串库函数的运行逻辑并可以自我编程实现,在以后的程序生涯中正确使用这些函数。

猜你喜欢

转载自blog.csdn.net/qq_42351880/article/details/86751404