c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第三式】字符函数和字符串函数

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第三式】字符函数和字符串函数

【心法】
【第零章】c语言概述
【第一章】分支与循环语句
【第二章】函数
【第三章】数组
【第四章】操作符
【第五章】指针
【第六章】结构体
【第七章】const与c语言中一些错误代码
【禁忌秘术】
【第一式】数据的存储
【第二式】指针
【第三式】字符函数和字符串函数
【第四式】自定义类型详解(结构体、枚举、联合)
【第五式】动态内存管理
【第六式】文件操作
【第七式】程序的编译



前言

c语言中对字符和字符串的处理非常频繁,但c语言本身是没有字符串类型的,字符串通常存储在常量字符串字符数组中。
常量字符串适用于不需要对其进行修改的字符串函数。


一、函数介绍

1. strlen

在这里插入图片描述

  • strlen函数的作用是获得一个字符串的长度;
  • 接收一个const修饰的字符指针变量,这个指针指向的字符串必须有结束符\0
  • 返回一个无符号(易错点)的int类型的数值,表示这个字符串的长度(不包括结束符\0);如果没有返回值,则表示出错;
// 使用示例
#include <stdio.h>
#include <string.h>

int main()
{
	char *a = "abcdefg";
	printf("a的长度 = %d\n", strlen(a));

	return 0;
}

在这里插入图片描述

易错点

#include <stdio.h>
#include <string.h>

int main()
{
	char* a = "abcdefg";
	char* b = "bbb";
	if ((strlen(b) - strlen(a)) >= 0)
	{
		printf("b >= a\n");
	}
	else
	{
		printf("b < a\n");
	}

	return 0;
}

上面这段代码按照设计思路,此时应该输出b < a。我们来看看,它的执行结果是否是这样。

运行结果:
在这里插入图片描述
可以看到输出仍是b >= a。这是因为strlen函数的返回值类型为size_t也就是unsigned int,是一个无符号数,是恒大于等于0的,所以上面代码只会输出b >= a

2. strcpy

在这里插入图片描述

  • strcpy函数的作用是拷贝一个字符串;
  • 接收两个参数,一个为字符指针类型,一个是由const修饰的字符指针类型,前者是拷贝的目的串,这片空间必须可变,后者是用于复制的串,这个字符串必须是有结束符\0的字符串;
  • 返回目的字符串的地址;如果没有返回值,就代表出错;
  • 注意,这个函数没有溢出检验,如果你目的位置的长度不足,无法接收你要复制字符串,它不会管你这那的,一定会将其拷贝过去,这就会产生错误;除此之外,该函数并没有定义源串和目的串在内存中产生重合时的情况,所以这时也无法完成拷贝的任务;

在这里插入图片描述

// 使用示例
#include <stdio.h>
#include <string.h>

void init(char* d)
{
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		d[i] = 1;
	}
}

int main()
{
	const char *s = "test of strcpy"; // 源字符串必须有结束字符 \0
	char d[20]; // d的长度一定要大于 s的长度,以确保能放下源字符串
	init(d);
	// strcpy函数会将源字符串中的 \0一起拷贝过去
	strcpy(d, s);
	printf("d = %s\n", d);

	return 0;
}

在这里插入图片描述
从上图中可以看到,目的字符串中确实有了结束符 \0;

出现越界
有两种情况会出现越界访问的问题:

  1. 目的串长度不够:
#include <stdio.h>
#include <string.h>

int main()
{
	const char *s = "test of strcpy"; 
	char d[3] = { 0 }; 
	strcpy(d, s);
	printf("d = %s\n", d);

	return 0;
}

在这里插入图片描述
使用strcpy函数拷贝字符串后,使用printf函数格式化输出字符串d时,将整个字符串都输出了,所以的确将整个字符串复制了过去,但因为目的位置d没有足够的空间而产生了越界,所以产生了错误;

  1. 源字符串没有结束符:
    在这里插入图片描述

出现重合

#include <stdio.h>
#include <string.h>

int main()
{
	char s[20] = "test of strcpy"; 
	strcpy(&s[3], s);
	printf("d = %s\n", &s[3]);

	return 0;
}

在这里插入图片描述
可以看到此处的结果,和我们的预期并不相符;这里的输出不一定是这个,因为,c语言中没有定义这种行为,这里的结果由编译器决定。

3. strcat

在这里插入图片描述

  • strcat是将一个字符串附加到另一个字符串的后面;
  • 返回目的串(它的首字符地址),没有返回值则表示出错;
  • strcat有两个参数,两个都是指向有结束符\0的字符串的字符指针;
  • 该函数在为目的地址追加字符串后,会在新字符串后面加上结束符\0;
  • 初始的源字符串会将目的串中的结束符覆盖掉;
  • 没有溢出检查,同样的,c语言中并未定义,添加的源串和目的串有重合的情况;

在这里插入图片描述
上图中展示了strcat的工作过程。

#include <stdio.h>
#include <string.h>

int main()
{
	char a[20] = "hello ";
	const char *b = "world";
	strcat(a, b);
	printf("%s\n", a);

	return 0;
}

运往结果:
在这里插入图片描述
错误使用:

  1. 没有结束字符\0
    在这里插入图片描述
    在这里插入图片描述
  2. 出现重合
    在这里插入图片描述

4. strcmp

在这里插入图片描述

  • strcmp函数的功能是比较两个字符串的大小;比较规则,同一位置的字母进行比较,越靠前越小,如果相同则继续往后比较,直到分出结果;
  • 该函数有两个参数,分别接收一个const修饰的字符指针变量,这两个指针指向有结束符的字符串;
  • 标准规定,返回值<0,表示str1比str2小;返回值=0,表示str1和str2相等;返回值>0,表示str1比str2大;
#include <stdio.h>
#include <string.h>

int main()
{
	const char *a = "abcde";
	const char *b = "abc";
	printf("%d\n", strcmp(a, b));

	return 0;
}

在这里插入图片描述
此时字符串a和字符串b的前三个字符相同,a比b更长,所以a>b,所以strcmp的返回值>0;这里返回1是因为VS编译器设计成这样。其它的编译器就不一定。

当比较的字符串没有结束符时:
在这里插入图片描述

5. strncpy

在这里插入图片描述

  • 这个函数的作用与strcpy大致相同,也是将一个字符串复制到另一个字符串中,但是这个函数能够指定复制多少个字符到目的字符串中;
  • 与strcpy不同的是,strncpy有三个参数。前两个参数与strcpy相同,都是两个字符串,但两个字符串不要求有结束字符\0了,第三个参数为无符号类型的整型数值,表示复制的字符个数;
  • 返回值为目的字符串,没有返回值表示出错;
  • strncpy是复制src字符串最开始的count个字符到dest字符串,并且返回dest串;
  • count应该小于或等于src的长度,但它不会自动添加结束字符\0;
  • 如果count比src的长度更长,它会在后面添加\0到足够长度;
  • 同样的,c语言中并未定义两个字符串出现重合的情况;

正常使用情况:

#include <stdio.h>
#include <string.h>

int main()
{
	char a[20] = "abcdef";
	const char *b = "zyxw";
	strncpy(a, b, 2); // 将字符串 b的前两个字符复制到字符串 a
	printf("%s\n", a); // 预期结果为输出 zycdef

	return 0;
}

运行结果:
在这里插入图片描述
从这个例子中,我们也能看出,strncpy这个函数并不会自动在复制完字符串内容后添加\0。

count比src的长度更长
在这里插入图片描述
出现重合
在这里插入图片描述
在这里插入图片描述

6. strncat

在这里插入图片描述

  • strncat的功能是在一个字符串后添加字符;
  • 有3个参数,前两个表示目的字符串和源字符串,第三个是要从源字符串添加的字符个数;
  • 返回值为指向目的字符串的指针。没有返回值则表示出错;
  • 目的字符串和源字符串必须是有结束字符\0的字符串;
  • count是一个无符号的整型数;
  • 该函数只会将源字符串前面的count个字符追加到目的串后;
  • src串的第一个字符会覆写dest串的\0;
  • 当src的长度比count更小时(换句话说,在复制的过程中,还没复制到第count个字符就遇到了\0),遇到这种情况,strncat只会将整个src字符串追加过去就结束(复制到\0就结束);
  • 同样的,该函数并未对两个字符串出现重合的情况进行定义;

正常使用示例:

#include <stdio.h>
#include <string.h>

int main()
{
	char a[20] = { "abcdef" };
	const char *b  = "qwert";
	strncat(a, b, 3);
	printf("%s\n", a);

	return 0;
}

运行结果:
在这里插入图片描述
可以strncat是会自动在添加的字符串后面添加结束字符\0;

count比src更长
在这里插入图片描述
出现重合
在这里插入图片描述
在这里插入图片描述

7. strncmp

在这里插入图片描述

  • 与strcmp一样,strncmp的功能也是比较两个字符串的大小(字典序比较);
  • 有三个参数,前两个字符指针指向要比较的两个字符串,第三个参数count表示比较这两个字符串的前count个字符;
  • 返回值和strmp相同,str1大于str2就返回大于0的数,str1小于str2就返回小于0的数,str1等于str2就返回0;

使用示例:

#include <stdio.h>
#include <string.h>

int main()
{
	char *a = "abcdef";
	char *b = "abcde";
	printf("%d\n", strncmp(a, b, 5));

	return 0;
}

运行结果:
在这里插入图片描述
可以看到这里的结果是两个字符串相等,这是因为strcmp中的第三个参数为5,只比较这两个字符串的前5个字符,所以得到的结果是0,如果用strcmp比较,这两个字符串肯定不相等。

对上面6个函数的小总结:

strcpystrcatstrcmp这三个函数是长度不受限的函数。长度不受限指的是,它们会在函数没有达到结束条件时一直执行下去,直到遇到\0;
strncpystrncatstrncmp这三个函数是长度受限的函数。长度受限指的是,它们只会处理指定数量的字符;
其中strcpystrcatstrncpystrncat都没有定义两个字符串出现重合的行为,当出现这种情况时,输出结果取决于使用的编译器;
strcpy没有溢出检测,所以需要src串有结束标志\0,且src串的长度不能大于dest串的长度;
strncpy也没有溢出检测,但两个字符串都不一定要有\0,因为该函数复制的字符数由使用者自己决定,需要使用者在使用时考虑清楚,标准规定当count超过src的长度时,后面补\0;
同时,这两个函数,在对重合字符串进行复制的时候,当src串的位置在dest的后面,则可以正常使用,反之就可能会出错;
这两个函数都不会自动在结束时添加\0。strcpy是将src的\0也一起复制过来,所以才会结束;
strcat中两个字符串都要有结束标志\0,这是因为,cat是在dest串的后面追加,需要找到dest的尾部,追加的是整个src字符串,所以需要知道src什么时候结束;
strncat中两个字符串也都要有结束标志\0,dest的原因和上面一样,而src也要有的原因是,标准规定当count超过src的长度时,复制到src的\0就结束追加,所以这里也需要知道src什么时候结束;
strcat根据规定,当出现重合时,肯定会出错,因为它的结束条件是遇到\0,但无论是src在dest的前面还是后面,都会将后面的\0覆写成有效字符,所以永远都没有\0,肯定会导致越界访问,也不可能得到预期结果;
strncat则是会出现结果与预期相符的情况;与前几个函数不同,strncat会自动在结束位置放上\0;

8. strstr

在这里插入图片描述

  • strstr的功能是在一个字符串中找子串;
  • 它有两个参数,前者是字符串,有结束标志\0;后者是要找的子串,有结束标志\0;
  • 它返回找到的第一个子串的字符指针,不包括结束标志\0,只是返回它的起始位置;没找到则返回NULL指针,如果子串的长度的0(空串),返回字符串本身;
#include <stdio.h>
#include <string.h>

int main()
{
	char *a = "abcdefgqwertbcdee";
	char *b = "bcde";
	printf("%s\n", strstr(a, b));

	return 0;
}

运行结果:
在这里插入图片描述

可以看到strstr返回的确实是一个指向找到的第一个与子串相同的字符的位置,并没有在这个子串后面添加\0;
在这里插入图片描述

9. strtok

在这里插入图片描述

  • strtok的功能是找到字符串中下一个词;
  • 它有两个参数,前者是待分割字符串,后者是分割字符集合;
  • 返回值为在字符串中找到的下一个词的首字符的指针。如果返回值为NULL,则表示字符串中所有的词全部已找到,找到这个字符串的结束标志\0。每次找到一个词时,该函数都会修改这个字符串(将这个分隔字符替换成\0);
  • 第二个参数strDelimit明确指出当前调用中字符串中可能出现的分隔字符;
  • 在首次调用strtok时,这个函数会跳过所有的先导分隔符(找到字符串中第一个非分隔符的字符,从它开始找第一个词),并返回这个字符串中的第一个词,并将它后面的分隔符修改成\0;
  • 可以通过调用一系列的strtok将剩余的字符串中其他的词全部分出来;
  • 每次调用strtok都会修改目标字符串,因为它会将找到的分隔符替换成\0;(所以调用该函数时,通常都会复制一个字符串,并使用这个复制出来的字符串来进行分割)。
  • 要找一个字符串中的下一词,要再次调用strtok,但是,第一个参数应设为NULL;当第一个参数为NULL时,strtok会在被修改过的字符串中去找下一个词;(使用static修饰的局部变量保存这个地址);
  • 每次调用时的分隔符集合都可以不相同,也就是有多种分隔符集;
  • 如果多重或同时调用这个函数,这就会导致有高概率出现数据损坏和得到不正确的结果;因此需要注意不要同时调用这个函数来处理不同的字符串;同时也应该意识到当我们在一个循环中调用这个函数时,该循环中是否还有其他语句会调用这个函数。
  • 但是多个线程同时调用这个函数就不会产生不良影响;
#include <stdio.h>
#include <string.h>

int main()
{
	char str[] = "- This is, a sample string.";
	char *pch = NULL;
	printf("Splitting string \"%s\" into tokens:\n", str);
	
	pch = strtok(str, "-,. ");
	while (pch != NULL)
	{
		printf("%s\n", pch);
		pch = strtok(NULL, "-,. ");
	}

	return 0;
}

运行结果:
在这里插入图片描述
从上面的输出可以看出,字符串最开始出现的分隔符被跳过了,直接从第一个有效字符开始找第一个token。
然后我们再来看看str这个数组的内容:
在这里插入图片描述

10. strerror

在这里插入图片描述

  • strerror_strerror前者用来获取一个系统的错误信息,后者是用来打印用户提供的错误信息;
  • strerror只有一个参数,表示错误信息的错误码;
  • _strerror也只有一个参数,这是用户提供的信息;
  • 这两个函数都是返回一个指向错误信息字符串的指针;下一次调用这两个函数会覆写这个字符串;
  • 注意:strerror_strerror这两个函数都不会将信息打印出来,需要使用其他输出函数才行;
  • 如果_strerror函数的参数为NULL,它就会返回上一个调用库函数产生的错误的信息;
  • _strerror的错误信息的结束标志是换行符\n,如果,_strerror的参数不是NULL,则它返回一个指向错误信息字符串的指针。这个字符串的结构组成按顺序为,你提供的信息、一个冒号、一个空格、最后一次调用库函数产生的错误、一个换行符\n,它会自动在字符串后面加一个\n;
  • 你提供的信息长度不能超过94个字节;
  • _strerror函数使用的实际错误码是存放在变量errno中;
  • 系统的错误信息是从变量_sys_errlist中获取到的,这是一个根据错误码的顺序排列的错误信息数组;
  • _strerror通过使用error的值作为索引来访问_sys_errlist中的对应错误信息;
  • 变量_sys_nerr的值被定义为数组_sys_errlist中的最大元素个数;
  • 为了获取到准确的结果,应该在一个库例程出错后立刻调用_strerror,否则对strerror_strerror的后续调用会改变errno的值;
  • 需注意_strerror是VS对strerror的扩展,其他编译器并不支持;并且可以使用perror函数来替代它;
#include <stdio.h>
#include <string.h>
#include <errno.h> // 包含errno这个全局变量

int main()
{
	FILE* pf = fopen("test", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		printf("%s", _strerror("test"));
		perror("test");
	}

	// 使用文件

	fclose(pf);
	pf = NULL;

	return 0;
}

运行结果:
在这里插入图片描述
当前目录中并没有test这个文件,所以FILE类型的指针pf指向的文件为NULL,此时文件打开失败,开始输出错误信息;
可以看到,_strerror这个函数比strerror多输出了一个test: ,这里由用户自己提供的信息;可以看到用printf函数输出_strerror得到的信息得到的结果与perror函数输出的结果相同,且perror函数不需要调用printf之类的输出函数,就能直接将信息输出到屏幕;
并且perror函数是ANSI标准中的库函数,所以为了可移植性,推荐使用perror

11.字符分类函数

函数如果它的参数符合下列条件则返回真
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任何可打印字符,包括图形字符和空白字符

常用的是isdigitislowerisupperisalpha

12. memcpy

在这里插入图片描述

  • memcpy函数的作用是在内存之间复制字符;
  • 它有3个参数,两个void *类型的指针变量,前者指向目的空间,后者指向源空间,第三个参数num表示要复制的字节数;
  • 返回指向目的空间的指针;
  • dest和src指向空间的底层类型并不影响该函数的功能,它是对二进制的数据进行操作;
  • 这个函数不会检查src中的任何空字符\0,它总是会复制num个字节;
  • 为了避免溢出,dest和src的占据的字节数最少也要等于num,且这两片内存不应该出现重合;

可能大家看完上面的一系列函数已经有点迷糊了,会出现前面不是有了strcpy和strncpy这两个复制函数吗,还要这个memcpy干嘛?

来想一个场景,有两个int类型的数组arr1、arr2,它们的长度都是10(能保存10个int类型数据),现在需要将arr1前5个元素复制到arr2,该怎么办?
能使用strcpy或strncpy吗?
不能,因为这两个函数的参数类型为字符指针,返回值也为字符指针,与int数组类型不符;
使用循环?可以。但还有其它方法吗?
使用memcpy,因为这个函数是以字节为单位进行处理,什么类型都能处理,也就是说,前面使用strcpy或strncpy的地方也能用memcpy实现;

#include <stdio.h>
#include <string.h>

int main()
{
	int arr1[10] = {1,2,3,4,5,6,7,8,9,10};
	int arr2[10] = {0};
	memcpy(arr2, arr1, 5 * sizeof(int));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr2[i]);
	}

	return 0;
}

运行结果:
在这里插入图片描述

验证\0不会影响复制
在这里插入图片描述
检验不会检查溢出
在这里插入图片描述
出现重合
在这里插入图片描述
在这里插入图片描述
memcpy出现重合时与strncpy出现重合时的情况类似,有的符合预期,有的不行。

13. memmove

在这里插入图片描述

  • memmove的功能和memcpy相同,都是将一片内存空间的内容复制到另一个地方;不过,memmove可以处理两片内存空间出现重合的情况;
  • 两个指针的类型仍为void *,原因相同;
  • 不检查\0,总是会复制num个字节的数据;
  • 为了不出现溢出,num应该小于dest和src长度;

在这里插入图片描述

14. memcmp

在这里插入图片描述

  • 与strcmp和strncmp的功能相似,memcmp是比较两片内存空间中的内容是否相同(以字节为单位);
  • 比较ptr1指向空间的前num个字节和ptr2指向空间的前num个字节是否相同,相同返回0,大于返回值>0,小于返回值<0;
  • 与strcmp和strncmp不同的是,memcmp遇到\0也不会停止比较,而一定会比较完num个字符或比较出结果;
#include <stdio.h>
#include <string.h>

int main()
{
	char *str1 = "abcde\0fg";
	char *str2 = "abcde";
	printf("strcmp:\n");
	if (strcmp(str1, str2) > 0)
	{
		printf("str1 > str2\n");
	}
	else if (strcmp(str1, str2) < 0)
	{
		printf("str1 < str2\n");
	}
	else
	{
		printf("str1 = str2\n");
	}
	printf("strncmp:\n");
	if (strncmp(str1, str2, 7) > 0)
	{
		printf("str1 > str2\n");
	}
	else if (strncmp(str1, str2, 7) < 0)
	{
		printf("str1 < str2\n");
	}
	else
	{
		printf("str1 = str2\n");
	}
	printf("memcmp:\n");
	if (memcmp(str1, str2, 7) > 0)
	{
		printf("str1 > str2\n");
	}
	else if (memcmp(str1, str2, 7) < 0)
	{
		printf("str1 < str2\n");
	}
	else
	{
		printf("str1 = str2\n");
	}

	return 0;
}

运行结果:
在这里插入图片描述
从上图可以验证,memcmp确实不会因为\0而停止比较,而是会在没比较出结果或没比较到第num个字节时,一直比较下去。

15. memset

在这里插入图片描述

  • memset的功能是设置一片内存空间的值,用指定值设置ptr指向空间的前num个字节的内容,指定值value是这个填充字符的ASCII码值;
#include <stdio.h>
#include <string.h>

int main()
{
	char str[20];
	// 对str进行赋值
	memset(str, '*', 15);

	int arr[10] = {0};
	// 使用memset将arr的元素的值设为 1,2,3,4,5,6,7,8,9,10
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		// 小端字节序
		memset((char *)arr + (i * sizeof(int)), i + 1, 1);
	}

	return 0;
}

在这里插入图片描述

int main()
{
	int arr[10] = {0};
	// 使用memset将arr的元素的值设为 1,2,3,4,5,6,7,8,9,10
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		memset(arr + i, i + 1, 1);
	}

	return 0;
}

这段代码与前面的代码功能相同,但一个是前者是将其转换为char *类型,一个字节一个字节的处理,后者是一个4个字节4个字节的处理,void *类型的参数接收到的指针的底层类型为int *,移动步长为4个字节;

二、库函数模拟实现

1.my_strlen

#include <assert.h>
typedef unsigned int uint;

// 使用计数器
uint my_strlen(const char* str)
{
	assert(str);
	int ret = 0;
	while (*str++)
	{
		ret++;
	}
	return ret; // \0不算在字符串长度中
}

// 不使用临时变量,递归
uint my_strlen(char* str)
{
	assert(str);
	if (*str == '\0')
	{
		return 0; // \0不算在字符串长度中
	}
	return 1 + my_strlen(str + 1);
}

// 使用指针相减
uint my_strlen(const char* str)
{
	assert(str);
	char *eos = str;
	while (*eos++)
	{ }
	return eos - str - 1; // \0不算在字符串长度中
}

2.my_strcpy

#include <assert.h>

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

	return start;
}

// 递归
char* my_strcpy(char* dest, const char* src)
{
	assert(dest && src);
	char* start = dest;
	if ((*dest++ = *src++) == '\0'){}
	else my_strcpy(dest, src);

	return start;
}

3.my_strcat

#include <assert.h>

char* my_strcat(char* dest, const char* src)
{
	assert(dest && src);
	char* start = dest;
	// 找dest串的尾部
	while(*dest++)
	{ }
	dest--;
	// 追加字符串src
	while (*dest++ = *src++)
	{ }

	return start;
}

4.my_strcmp

#include <assert.h>

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

	return (*str1 - *str2);
}

5.my_strncpy

#include <assert.h>

typedef unsigned int uint;

char* my_strncpy(char* dest, const char* src, uint num)
{
	assert(dest && src);
	char* start = dest;
	// 先判断num是否为0,如为0,则直接结束,不需判断当前src指向的字符是否\0
	while (num && (*dest++ = *src++)) // dest,src永远指向下一个字符复制的位置
	{
		num--;
	}

	if (num) // num不为0,此时只能是*(src-1)指向了\0
	{
		while (--num)// dest复制了一个src中的\0,但还没有减少num的值,所以需要先减一次
		{
			*dest++ = '\0'; // 当复制字符数量不足num个时,填充\0
		}
	}

	return start;
}

6.my_strncat

#include <assert.h>
typedef unsigned int uint;

char* my_strncat(char* dest, const char* src, uint num)
{
	assert(dest && src);
	char* start = dest;

	while(*dest++) // 找到dest的尾部
	{ }
	dest--; // 回到dest的\0
	
	while(num && (*dest++ = *src++))
	{ 
		num--;
	}
	if (num == 0) // 此时追加的字符串没有\0
	{
		*dest = '\0'; // 补上\0
	}

	return start;
}

// 另一种写法
char* my_strncat(char* dest, const char* src, uint num)
{
	assert(dest && src);
	char* start = dest;

	while (*dest++) // 找到dest的尾部
	{
	}
	dest--; // 回到dest的\0
	
	while (num--)
	{
		if ((*dest++ = *src++) == 0)
		{
			return start;
		}
	}
	// 此时是因为num==0,所以追加的字符串没有\0
	*dest = '\0'; // 补上\0

	return start;
}

7.my_strncmp

#include <assert.h>
typedef unsigned int uint;

int my_strncmp(const char* str1, const char* str2, uint num)
{
	assert(str1 && str2);
	while ((*str1 == *str2) && (*str1 != 0))
	{
		num--;
		if (num == 0)
		{
			return 0;
		}
		str1++;
		str2++;
	}

	return *str1 - *str2;
}

8.my_strstr

#include <string.h>
#include <assert.h>

// 暴力匹配
char* my_strstr(const char* str, const char* substr)
{
	assert(str && substr);
	const char* a = str;
	const char* b = substr;
	const char* c = str;
	if (strlen(substr) == 0)
	{
		return a;
	}
	while (*a) 
	{
		while (*b != 0 && *c == *b)
		{
			c++;
			b++;
		}
		if (*b == 0)
		{
			return a;
		}
		a++;
		c = a;
		b = substr;
	}
	return NULL;
}
// 除了暴力匹配法外,还可以使用KMP算法,此处就不再详细展示

9.my_strtok

#include <stdio.h>
#include <string.h>
#include <assert.h>

int isDelimit(const char *a, const char* strDelimit)
{
	int length = strlen(strDelimit);
	int i = 0;
	for (i = 0; i < length; i++)
	{
		if (*a == *(strDelimit + i))
		{
			return 1;
		}
	}
	return 0;
}

char* my_strtok(char* str, const char* strDelimit)
{
	assert(strDelimit);
	static char* cp = NULL; // 记录位置的指针
	char* start = NULL;
	// 继续上一次的处理
	if (str == NULL)
	{
		// 跳过先导分隔符
		while (isDelimit(cp, strDelimit))
		{
			cp++;
		}

		start = cp;
		while (*cp && !isDelimit(cp, strDelimit))
		{
			cp++;
		}
		if (*cp != 0)
		{
			*cp++ = 0; // 将分隔符改成\0
		}
	}
	else
	{
		// 跳过先导分隔符
		while (isDelimit(str, strDelimit))
		{
			str++;
		}

		start = str;
		// 开始分隔
		// 有两种情况会停下来
		// 1.遇到\0
		// 2.遇到分隔符
		while (*str && !isDelimit(str, strDelimit))
		{
			str++;
		}
		
		if (*str == 0)
		{
			cp = str;
		}
		else
		{
			cp = str + 1;
			*str = 0; // 将分隔符改成\0
		}
	}
	if (*start == 0)
	{
		return NULL;
	}
	else
	{
		return start;
	}
}

10.my_memcpy

#include <assert.h>

typedef unsigned int uint;

void* my_memcpy(void* dest, const void* src, uint num)
{
	assert(dest && src);
	void* start = dest;
	while (num--)
	{
		*(((char*)dest)++) = *(((char*)src)++);
	}

	return start;
}

11.my_memmove

memmove函数能够处理两片空间出现重合的情况,所以在这里我们就具体分析一下,它是如何实现的。
当重合时出现结果与预期不符是因为,后续要复制的数据被前面的复制修改了。也就是当src在dest的前面时,src后面要复制给dest的值在此之前就被前面的复制操作修改了。
在这里插入图片描述
在这里插入图片描述
所以当src在dest的后面时,从前往后复制是没有问题的。那么当src在dest的前面应该如何复制,来保证前面的复制不会影响到之后的复制呢?
从后往前。
在这里插入图片描述
但是注意src在dest的后面时,使用从后往前的方式又会导致错误,所以memmove应该分两种情况来处理,

  1. src在dest前面:从后往前复制;
  2. src在dest后面:从前往后复制;
#include <assert.h>

typedef unsigned int uint;

void* my_memmove(void* dest, const void* src, uint num)
{
	assert(dest && src);
	void *start = dest;
	// src在前
	if (src < dest)
	{
		// 从后往前复制
		while (num--)
		{
			*((char*)dest + num) = *((char*)src + num);
		}
	}
	else
	{
		while (num--)
		{
			*(((char*)dest)++) = *(((char*)src)++);
		}
	}
	return start;
}

12.my_memcmp

#include <assert.h>

typedef unsigned int uint;

int my_memcmp(const void* ptr1, const void* ptr2, uint num)
{
	assert(ptr1 && ptr2);
	while (num && *(char*)ptr1 == *(char*)ptr2)
	{
		num--;
		((char*)ptr1)++;
		((char*)ptr2)++;
	}
	if (num == 0)
	{
		return 0;
	}
	return (*(char*)ptr1 - *(char*)ptr2);
}

13.my_memset

#include <assert.h>

typedef unsigned int uint;

void* my_memset(void* ptr, int value, uint num)
{
	assert(ptr);
	void* start = ptr;
	int i = 0;
	for (i = 0; i < num; i++)
	{
		*((char*)ptr + i) = value;
	}

	return start;
}

总结

本文对c语言中会使用到的字符串函数进行了详细介绍说明,并给出了模拟实现的代码,希望对你们的c语言学习有帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值