刨析C语言常用的字符串函数与内存函数及模拟实现

x33g5p2x  于2021-11-21 转载在 其他  
字(10.4k)|赞(0)|评价(0)|浏览(277)

一、C语言相关字符串库函数一览表

二、strlen函数(求字符串长度)

strlen函数介绍

strlen函数是用于求字符串长度的(不包括\0),例如字符串“abc”的长度为3,在代码中我们如何使用strlen函数呢?

#include <stdio.h>
int main()
{
 const char*str1 = "abcdef";
 const char*str2 = "bbb";
 if(strlen(str2)-strlen(str1)>0)
 {
 printf("str2>str1\n");
 } 
 else
 {
 printf("srt1>str2\n");
 }
 return 0; }

程序结果如图:

我们的程序的结果可能会感到吃惊,字符串str2的长度为3,字符串str1的长度为6,那为什么最后打印的却是str2>str1呢?

经过查阅资料发现,strlen函数返回值的类型为size_t(无符号整型),因此无符号整型之间的运算结果仍然是无符号整型。3-6的确等于-3,而最终打印的是-3的补码,而-3是二进制原码得到的。-3的原码为10000000000000000000000000000011而-3的补码为01111111111111111111111111111101,是个正数,因此strlen(str2)-strlen(str1)的结果是大于0的。

strlen的特点及注意事项:

  • 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
  • 参数指向的字符串必须要以 ‘\0’ 结束。
  • 注意函数的返回值为size_t,是无符号的。( 易错 )
  • 调用strcpy函数所需的头文件为string.h

strlen函数的模拟实现

方法一:计数器法

#include <stdio.h>
#include <string.h>
int my_strlen(const char* str)
{
	int count = 0;
	while (*str++!='\0')
	{
		count++;
	}
	return count;
}
int main()
{
	char* str1 = "abcdef";
	int ret = my_strlen(str1);
	printf("%d", ret);
	return 0;
}

代码运行结果:(以下的运行结果跟下图一致)

方法二:递归方法

#include <stdio.h>
#include <string.h>
int my_strlen(const char* str)
{
    if(*str == '\0')
       return 0;
    else
       return 1+my_strlen(str+1);
}
int main()
{
	char* str = "abcdef";
	int ret = my_strlen(str);
	printf("%d", ret);
	return 0;
}

方法三:指针减指针的方式

int my_strlen(char *s) 
{
       char *p = s;
       while(*p != ‘\0’ )
              p++;
       return p-s; 
}
int main()
{
	char* str = "abcdef";
	int ret = my_strlen(str);
	printf("%d", ret);
	return 0;
}

三、strcpy函数(字符串拷贝)

strcpy函数介绍

先看strcpy函数的返回值类型与传值。

查阅资料我们可以看到,strcpy函数传入的第一个参数为目标地址处,第二个参数为源地址处,返回值类型为char*类型。

例如以下代码:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[20] = "abcdef";
	char str2[] = "zjr";
	strcpy(str1, str2);
	printf("%s", str1);
	return 0;
}

代码执行结果如图:

strpy函数的特点及注意事项:

  • 源字符串必须以 ‘\0’ 结束。
  • 会将源字符串中的 ‘\0’ 拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。

strcpy函数的模拟实现

char *my_strcpy(char *dest, const char*src)
{ 
 char *ret = dest;
 assert(dest != NULL);
 assert(src != NULL);
 while((*dest++ = *src++))
 {
    ;
 }
 return ret;
}
#include <stdio.h>
#include <string.h>
int main()
{
	char str1[20] = "abcdef";
	char str2[] = "zjr";
	my_strcpy(str1, str2);
	printf("%s", str1);
	return 0;
}

在my_strcpy函数的形参中的源字符串地址使用了const的原因是:保证修改的是目的地字符串的内容而不是源字符串的内容。my_strcpy函数内部使用了assert函数的目的是保证传入的目的地字符串地址与源字符串地址都不为空指针,如果为空指针则程序代码执行时会报错。

如上代码运行结果为:

简单介绍下assert函数:

头文件为assert.h

传入的为空指针就会报错,否则不影响程序执行。

四、strcat函数(字符串追加)

strcat函数介绍

strcat函数的返回值类型为char*,可以返回被追加的字符串的起始地址。而第一个参数为目的地字符串的地址,第二个参数为源字符串的地址。

利用strcat函数所需的头文件:

strcat函数的使用:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[20] = "abcd";
	char str2[] = "efgh";
	char* ret = strcat(str1, str2);
	printf("%s", ret);
	return 0;
}

代码运行结果为:

strcat函数的特点及注意事项:

  • 源字符串必须以 ‘\0’ 结束。
  • 目标空间必须有足够的大,能容纳下源字符串的内容。
  • 目标空间必须可修改。

strcat函数的模拟实现

char *my_strcat(char *dest, const char*src)//保证源字符串不被修改
 {
 char *ret = dest;
 assert(dest != NULL);
 assert(src != NULL);
 while(*dest)
 {
    dest++;//先到目的地字符串的\0地址处
 }
 while((*dest++ = *src++))//先解引用后++,因此最后能将源字符串内的\0也追加进去
 {
     ;
 }
 return ret; 
 }
#include <stdio.h>
#include <string.h>
int main()
{
	char str1[20] = "abcd";
	char str2[] = "efgh";
	char* ret = my_strcat(str1, str2);
	printf("%s", ret);
	return 0;
}

代码执行结果:

五、strcmp函数(字符串的比较)

strcmp函数介绍

strcmp函数的返回值类型为int,第一个参数为一个字符串首字母的地址,第二个参数为另一个字符串首字母的地址,依次往后作比较,这strcmp函数比较的是字符串的内容,而不是字符串的长度。

对于strcmp函数的返回值,C语言规定如果第一个字符串的内容小于第二个字符串的内容,则返回小于0的值;如果第一个字符串的内容等于第二个字符串的内容,则返回0,如果第一个字符串的内容大于第二个字符串的内容,则返回大于0的值。

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[] = "abcd";
	char str2[] = "abc";
	int ret = strcmp(str1, str2);
	printf("%d", ret);
	return 0;
}

代码运行结果:(结果虽然返回大于0的值都为1,但是每个编译器此处规定返回大于0的值有所不同,此处会有差异)。

假如以上例子对strcmp函数是比较字符串内容不够清晰,以下例子能更好地观察:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[] = "abcd";
	char str2[] = "abq";
	int ret = strcmp(str1, str2);
	printf("%d", ret);
	return 0;
}

代码运行结果:

strcmp函数的模拟实现

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

my_strcmp(const char* s1, const char* s2)
{
	assert(s1 && s2);
	while (*s1 == *s2)
	{
		if (*s1 == '\0')
			return 0;
		s1++;
		s2++;
	}
	return *s1 - *s2;//比较字符串内容
}

#include <string.h>
int main()
{
	char str1[] = "abcd";
	char str2[] = "abq";
	int ret = my_strcmp(str1, str2);
	printf("%d", ret);
	return 0;
}

代码运行结果:

strcpy、strcat、strcmp都是长度不受限制的字符串函数,所以就显得不够安全。所以就有了strncpy、strncat、strncmp函数。

六、strncpy函数(受限制字符串拷贝)

strncpy函数介绍

strncpy函数的返回值类型为char*类型,该函数第一个参数为目的地字符串的首字母地址,第二个参数为源字符串的首字母地址,第三个参数为需要拷贝过去的字符个数。

使用strncpy函数所需的头文件:

strncpy的使用:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[20] = "abcd";
	char str2[] = "kpl";
	char* ret = strncpy(str1, str2, 2);
	printf("%s", ret);
	return 0;
}

代码运行结果:

strncpy函数的模拟实现

#include <stdio.h>
#include <string.h>
char* my_strncpy(char* dest, char* src, size_t count)
{
	while (count--)//先使用count再减1
	{
		*(dest + count) = *(src + count);
	}
	return dest;
}
int main()
{
	char str1[20] = "abcd";
	char str2[] = "kpl";
	char* ret = my_strncpy(str1, str2, 2);
	printf("%s", ret);
	return 0;
}

七、strncat函数(受限制字符串追加)

strncat函数介绍

strncat函数与其他的受限制的库函数的返回值类型与参数几乎一样,此处不再进行说明。

使用strncat函数所需头文件:

strncat函数的模拟实现

#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strncat(char* dest, char* src, size_t count)
{
	assert(dest && src);
	char* ret = dest;
	while (*dest)
	{
		dest++;//到目的地字符串的\0位置
	}
	int s =count ;
	while (count--)
	{
		*(dest + count) = *(src + count);//字符串追加
	}
	*(dest+s) = '\0';
	return ret;
}
int main()
{
	char str1[20] = "abcd";
	char str2[] = "kpl";
	char* ret = my_strncat(str1, str2, 2);
	printf("%s", ret);
	return 0;
}

八、strncmp函数(受限制字符串内容比较)

strncmp函数介绍

strncmp函数的返回值类型为int型,返回值的规则跟strcmp函数相同,此处不再细讲。第一个与第二个参数:因为两个字符串只是用来作比较,如果修改则不满足strncmp函数的使用规则,因此在前面两个参数前面加上const保证字符串内容不被修改。第三个参数为比较的字符个数。

使用strncmp函数所需的头文件:

strncmp函数的使用:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[] = "abcd";
	char str2[] = "abkl";
	int ret = strncmp(str1, str2, 2);
	printf("%d", ret);
	return 0;
}

代码运行结果:

strncmp函数的模拟实现

#include <stdio.h>
#include <assert.h>
#include <string.h>
int my_strncmp(char* s1, char* s2, size_t count)
{
	assert(s1 && s2);
	while (count--)
	{
		if (*s1 == *s2)
		{
			s1++;
			s2++;
		}
	}
	return *s1 - *s2;
}
int main()
{
	char str1[] = "abcd";
	char str2[] = "abkl";
	int ret = my_strncmp(str1, str2, 2);
	printf("%d", ret);
	return 0;
}

代码执行结果:

九、strstr函数(字符串查找函数)

strstr函数的返回值类型为char类型,第一个参数是被查找的字符串,第二个参数是在第一个参数中需要查找的字符串。例如:第一个字符串为i am a student,第二个字符串为a,则返回的char类型用字符串形式打印的结果为am a student 。

使用strstr函数所需的头文件:

strstr函数的使用:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[] = "abcdefdgh";
	char str2[] = "d";
	char* ret = strstr(str1, str2);
	printf("%s", ret);
	return 0;
}

代码执行结果:

strstr函数的模拟实现

#include <stdio.h>
#include <string.h>
char* my_strstr(const char* str1, const char* str2)
{
	char* s1, * s2;
	char* cp = str1;
	if (*str2 == '\0')
	{
		return str1;
	}
	while (*cp)
	{
		s1 = cp;	
		s2 = str2;
		while (*s1 && *s2 && *s1 == *s2)//开始遍历寻找
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
		{
			return cp;
		}
		cp++;//当字符查找途中发现有不相同的字符,则从刚开始查找处的下一个字符开始查找
	}
	return NULL;
}
int main()
{
	char str1[] = "abcdefdgh";
	char str2[] = "d";
	char* ret = my_strstr(str1, str2);
	printf("%s", ret);
	return 0;
}

代码执行结果:

查找字符串有两种情况:
第一种:例如在abcd字符串中查找cd,则两个字符串一 一对应一次即可找到。

第二种:需要遍历几次才可能找到。

当第一次遍历时查找到字符c时才发现被查找的字符串中没有bbc的字符串,但实际上被查找的字符串中是有的,只需要再多遍历一次即可。并且从下一个字符处开始寻找。

十、strtok函数(用于字符串分割)

strtok函数的返回值类型为char*类型,即返回字符串的地址。第一个是用来存放待分割的字符串地址,第二个是用来存放分割符的集合地址。

使用strtok函数的头文件:

strtok函数的使用:

#include <stdio.h>
#include <string.h>
int main()
{
	char str1[] = "by@bite.cn";
	char str2[200] = { 0 };
	char sep[] = "@.";
	strcpy(str2, str1);
	char* ret = NULL;
	for (ret = strtok(str2, sep); ret != NULL; ret = strtok(NULL, sep))
	{
		printf("%s ", ret);
	}
}

注:

  • sep参数是个字符串,定义了用作分隔符的字符集合
  • 第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
  • strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
  • strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
  • strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
  • 如果字符串中不存在更多的标记,则返回 NULL 指针。

由于strtok函数使用情况较少,所以这里没有对strtok进行模拟实现

十一、strerror(错误信息报告)

strerror函数返回值类型为char*,参数为int。

strerror函数的使用:

#include <stdio.h>
#include <string.h>
int main()
{
    FILE* pFile;
    pFile = fopen("unexist.ent", "r");
    if (pFile == NULL)
        printf("Error opening file unexist.ent: %s\n", strerror(errno));
    //errno: Last error number
    return 0;
}

十二、memcpy函数(内存拷贝函数并且内存不重叠)

memcpy函数的存在意义

因为strcpy与strncpy函数都是字符串间的拷贝,对于整型、结构体等其它类型的拷贝均不适用,因此memcpy函数的拷贝油然而生。

memcpy函数介绍

因为能够使得多种相同类型之间可以拷贝,因此memcpy函数的返回值为void* 型,第一个参数也为void* 型,是被拷贝的地址;第二个参数也为void* 型,是待拷贝内容的地址;第三个参数是size_t(无符号整型)的count,这里的count是整个需要拷贝的元素大小(单位:字节)。

memcpy函数的使用:

#include <stdio.h>
#include <string.h>
int main()
{
	int arr1[100] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr2[100] = { 0 };
	int i = 0;
	memcpy(arr2, arr1, 10 * sizeof(int));//arr1的大小为40个字节,可以利用sizeof计算
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr2[i]);
	}
	return 0;
}

代码运行结果:

memcpy函数的特点:

  • 函数memcpy从source的位置开始向后复制num个字节的数据到destination的内存位置。
  • 这个函数在遇到 ‘\0’ 的时候并不会停下来。
  • 如果source和destination有任何的重叠,复制的结果都是未定义的。

memcpy函数的模拟实现

#include <stdio.h>
#include <string.h>
#include <assert.h>
void* my_memcpy(void* dst, const void* src, size_t count) 
{
	void* ret = dst;
	assert(dst);
	assert(src);
	while (count--) {
		*(char*)dst = *(char*)src; //每次复制一个字节的内容
		dst = (char*)dst + 1;
		src = (char*)src + 1;
	}
	return(ret);
}
int main()
{
	int arr1[100] = { 1,2,3,4,5,6,7,8,9,10 };
	int arr2[100] = { 0 };
	int i = 0;
	my_memcpy(arr2, arr1, 10 * sizeof(int));
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr2[i]);
	}
	return 0;
}

代码运行结果:

在本程序中是把两个没有内存重叠的两个数组进行的相互拷贝,但是在内存出现重叠的情况时,memcpy函数在某些编译器下是无法实现的,所以我们在下面引入memmove函数。

十三、memmove函数(内存拷贝函数并能解决内存重叠)

memmove函数介绍

使用memmove函数所需的头文件:

memmove函数的返回值与参数跟memcpy函数一样,此处不再细讲。

memmove函数的模拟实现

#include <stdio.h>
#include <string.h>
#include <assert.h>
void* my_memmove(void* dst, const void* src, size_t count) 
{
	void* ret = dst;
	assert(dst);
	assert(src);
	if (src > dst)
	{
		while (count--)
		{
			*(char*)dst = *(char*)src;
			dst = (char*)dst + 1;
			src = (char*)src + 1;
		}
	}
	else
	{
		while (count--)
		{
			*((char*)dst + count) = *((char*)src + count);
		}
	}
	return ret;
}
int main()
{
	int arr1[] = { 1,2,3,4,5,6,7,8,9,10 };
	int i = 0;	
	my_memmove(arr1+4,arr1+2 ,16);
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr1[i]);
	}
	return 0;
}

代码执行结果:

如果用我们模拟实现memcpy函数的代码的结果为:

可见此处连续出现了三次3 4,这是什么原因呢?
以下用图解来帮助大家理解:

从前往后拷贝的意思是需要拷贝的内存内容是从前往后拷贝的,即从3到6开始拷贝,如果从后往前拷贝则是从6到3进行拷贝。因为5和6被赋值为3和4后再进行拷贝,则在5和6位置处拷贝的仍然是3,4的内容。

那么有哪几种情况呢?怎么解决?
一共有三种情况:

我们发现,当src大于dst时,只能从前往后拷贝,否则会出现上面用memcpy函数执行的错误的结果。当src小于dst时,只能从后往前拷贝,并且被拷贝的内存内容也是从后往前被拷贝。当第三中情况dst大于src并且没有内存内容没有重叠时,从前往后或者从后往前拷贝都是可以的,因此我们可以将这三种情况用两种解决方案解决。理解memmove模拟函数中的以下代码极为关键。

if (src > dst)
	{
		while (count--)
		{
			*(char*)dst = *(char*)src;
			dst = (char*)dst + 1;
			src = (char*)src + 1;
		}
	}
	else
	{
		while (count--)
		{
			*((char*)dst + count) = *((char*)src + count);//count进入循环内部已经减1,刚好到最需要拷贝的内容的最后一个值处
		}
	}

如果这篇文章对你有帮助,麻烦点个赞支持下谢谢!

相关文章

微信公众号

最新文章

更多