探索字符与字符串:基本库函数的使用(二)

目录

文章目录

前言

函数模拟实现

strlen

strcpy

strcat

 strstr

strcmp

 memcpy

memmove

总结


前言

继接上文,本片文章我将带领大家去模拟实现一些基本的库函数。


函数模拟实现

strlen

前文我们已将基本了解了strlen函数是用于计算字符串长度的,那么接下来我们来模拟实现一下该函数

strlen函数统计的是\0之前的字符个数,函数返回类型是int,这里我们有三种实现方法:

方法一:计数器

int my_strlen(const char *ch)
{
	
	int i = 0;
	while(*ch != '\0') {
		ch++;
		i++;
	}
	return i;
}

 方法二:递归

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

 方法三:指针运算

int my_strlen(char *s)
{
char *p = s;
while(*p != ‘\0’ )
p++;
return p-s;
}

 这些方法的代码实现都较为简单,就不再细介绍。

strcpy

在模拟实现strcpy函数时需要注意以下几点:

  • 函数参数顺序
  • 函数的功能,停止条件
  • assert
  • const修饰指针
  • 函数返回值

 我们再来看一下函数的原型:

char* strcpy(char * destination, const char * source );

 注意const修饰的参数模拟实现代码:

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

 assert断言,为了防止传进来为NULL,后置++的优先级高于*,所以在*dest++中先后置++,再解引用。当赋值到\0时循环就会结束,返回目标字符串的起始位置。

strcat

函数的原型与strcpy函数原型类似。

strcat函数的功能是连接两个字符串,我们可以依据strcpy的思路将源字符串的内容copy到目标字符串的末尾,我们只需找到目标字符串中\0的位置即可

代码实现:

char *my_strcat(char *dest, const char*src)
{
char *ret = dest;
assert(dest != NULL);
assert(src != NULL);
while(*dest)
{
dest++;
}
while((*dest++ = *src++))
{
;
}
return ret;
}

 那为什么strcat最好不要自己给自己追加。

看到模拟实现的strcat我们或许就会发现问题:

探索字符与字符串:基本库函数的使用(二)_第1张图片

如果自己给自己追加,初始时dest和src就指向同一个字符串,当dest 向后移动找到\0以后就会停止(需要追加的位置),然后就会开始追加,将src指向的字符赋给dest指向的位置,这就会把\0给替换掉:

探索字符与字符串:基本库函数的使用(二)_第2张图片

 但是\0对于src来说又很重要,这是循环结束的标志,而此时\0被替换,循环就无法停止,便会一直向后赋值,知道越界访问程序崩溃停止。

 strstr

strstr函数的功能是在一个字符串中查找指定的子串,并返回该子串在原字符串中的起始位置。

函数原型:

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

 strstr在不依靠库函数的情况下,对其模拟实现可能有点复杂。

具体代码:

char* my_strstr(char* str1, char* str2)
{
	char* s1 = str1;
	char* s2 = str2;
	char* cp = str1;
	while (*cp)
	{
		s1 = cp;
		s2 = str2;
		while (*s1 && * s2 && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
			return cp;
		cp++;
	}
	return NULL;
}

以a b b b c d e f和b b c为例:

探索字符与字符串:基本库函数的使用(二)_第3张图片

 初始时两指针指向两字符串的首元素地址,问题在于要确定开始匹配的位置,那么我们就需要一个指针来存放开始匹配的位置,我们设为cp,初始阶段开始匹配,cp指向的位置和str1相同。

cp不断的向后寻找开始匹配的位置,如果*cp=\0,就说明cp遍历了整个字符串都没有找到匹配点,说明str2不属于str1的子字符串,返回NULL。

以此为前提才能开始匹配。从第一个字符开始向后匹配,匹配失败后我们就需要移动cp向后移动尝试从下一个位置开始,所以进行cp++;

s1和s2是进行字符比对的指针,起初s1和s2都是从字符串的首元素开始:

探索字符与字符串:基本库函数的使用(二)_第4张图片

 然后s1和s2同步进行移动,对字符进行一一比对,

while (*s1 == *s2)
		{
			s1++;
			s2++;
		}

 如果匹配失败,cp就向后移动,s1从cp指向的位置开始继续比对(s1=cp),s2从str2指向的字符串的首元素开始(s2=str2),如果匹配成功,此时的s2指向的一定是字符串末尾的\0由此我们就可以以此为判定条件,判断是否匹配成功:

while (*s1 && * s2 && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
			return cp;

匹配成功就可以提前结束程序,返回首次匹配成功的初始位置(return cp)。遍历结束都没有找到则返回空指针。

strcmp

strcmp函数的功能是比较两字符串大小。

具体功能:

  • 比较两个字符串的大小。
  • 返回一个整数值,表示两个字符串的大小关系。
  • 如果字符串相等,返回值为0。
  • 如果字符串1大于字符串2,返回值大于0。
  • 如果字符串1小于字符串2,返回值小于0

 由此我们来模拟实现一下,首先我们依次比对两个字符串对应位置字符的大小。如果有一个遍历结束,就返回0。但切记while循环里不可以这样写*str1++ == *str2++。

如果str1和str2不相等退出循环后仍会向后走一步,这样就跳过了对应位置上第一个不相等的字符,就会导致在判断大小时出现错误。

所以我们选择在循环里进行++操作。

在两字符不相等的前提下,如果str1指向的字符大于str2指向的字符,就返回1,否则就返回-1

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

 memcpy

函数原型:

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

从source的位置开始向后复制num个字节的数据到destination的内存位置。

此外我们还要注意:

  • 这个函数在遇到 '\0' 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

 这里注意,memcpy的函数类型是void* 类型,这表明memcpy函数可以用于所以类型的数据。

那么在模拟实现时,我们就要设计一个可以适用于任何类型的函数。最终的返回结果是目标dest数据的起始地址。

于是我们就可以这样设计

模拟代码演示:

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;

}

 num为需要copy的字节大小,可以作为循环的次数,不管传入任何类型的数据,我们都将其转换为char*类型,进行一个字节一个字节的复制。最终返回目标dest数据的起始地址。只需创建一个变量存储即可。

memmove

函数原型:

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

 dest是目标内存的起始地址,src是源内存的起始地址,n是要移动的字节数

注意:函数原型中的参数类型都是void*,表示传入的参数是任意类型的指针,可以用于处理不同类型的数据。返回值类型是void*,表示返回一个指向目标内存的指针。

那么我们要如何设计呢?

有人可能会想,这个简单,我们模仿上边的memcpy函数进行复制就好了,但这样真的可以吗?

在模拟的memcpy中确实可以实现部分的数据移动,但它并不完善。这里我们需要考虑到数据覆盖问题。

如果是将数据从高地址移动到同一数组的低地址处,这样可以实现如:

memcpy(arr, arr + 2, 20);

但如果是将前边的数据向后移动呢?

memcpy(arr+2, arr , 20);

探索字符与字符串:基本库函数的使用(二)_第5张图片

当我们想把arr处20个字节的数据向后移动2个单位时,1赋值给了3,2赋值给了4,那到3赋值时3的值就已经被覆盖,这就会导致最终结果错误。

那我们应该怎么解决呢?

这时我们就可以考虑从源数据段的最后边开始赋值,20个字节,5个元素,先将5赋给7,4赋给6,3赋给5……

探索字符与字符串:基本库函数的使用(二)_第6张图片

 

这样就可以实现向后移动。

回到memmove函数,它是一个可以接收任何类型数据的函数,那么次时我们就需要将数据类型转换为char*类型,进行一个字节一个字节的赋值。

代码实现:

my_memmove(void* dest, const void* src, size_t n)
{
	if (dest < src)
	{
		while (n--)
		{
			*(char*)dest = *(char*)src;
			dest = (char*)dest + 1;
			src = (char*)src + 1;
		}
	}
	else
	{
		while (n--)
		{
			*((char*)dest+n) = *((char*)src+n);
		}
	}

}

 根据src和dest的大小进行判断,选择合适的方法。

(char*)dest+n和(char*)src+n将两个指针移动到需要后移的数据最后,从后向前依次赋值。每移动一个字节,n--,dest和src从最后向前一个字节,继续赋值……,直到需要移动的字节移动完毕停止。


总结

好的本期内容到此结束,模拟实现这些内容虽然很不常用,但是却可以帮助我们更好的理解和正确的使用这些库函数,同样也可以帮助我们提升一定的算法能力,对于处理一些字符操作题目很有帮助。最后,感谢阅读!

你可能感兴趣的:(算法,c语言,其他,经验分享)