常用字符串函数和内存函数


本文介绍常见的字符串函数和内存函数,模拟实现这些函数以及交代这些函数使用时需要注意的地方

一.字符串函数

1.1.strlen函数

函数声明:
size_t strlen ( const char * str );
功能:求字符串的长度。
这个函数大家应该经常使用,但有以下几点需要特别注意

  1. 字符串已经 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前面出现的字符个数(不包含 ‘\0’ )。
  2. size_t strlen ( const char * str );参数指向的字符串必须要以 ‘\0’ 结束。
  3. 注意函数的返回值为size_t,是无符号的

关于第三点举个例子

#include <string.h>
int main()
{
	if (strlen("abc") - strlen("abcd") > 0)
	{
		printf(">");
	}
	else
	{
		printf("<=");
	}
	return 0;
}

最终结果是”>“,因为strlen(“abc”)是无符号的3,strlen(“abcd”)是无符号的4,二者相减,得到的还是一个无符号数,结果>0。

实现strlen
实现strlen有三种方式:计数器,递归,指针相减

递归:

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

计数器

int MyStrlen(const char * str)
{
	 int count = 0;
	 while(*str)
	 {
		  count++;
		  str++;
	 }
	 return count;
}

指针相减

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

1.2.strcpy函数

函数声明:
char* strcpy(char * destination, const char * source);
功能是把源头空间的字符串拷贝到目标字符串中去。
注意:

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

关于第四点:例如

const char arr[10] = "abcdef";

arr就不能作为destination传参,因为arr数组的元素是不能改变的。

模拟实现

#include <assert.h>
char* MyStrcpy(char* dest, const char* src)
{
	assert(dest != NULL && src != NULL);
	char* ret = dest;
	while ((*dest++ = *src++) != '\0')
	{
		;
	}
	return ret;
}

这里的assert()是一个库函数,头文件<assert.h>。功能是断言括号里的内容一定成立,如果不成立程序就会停止运行。因为NULL是不能解引用的,加上断言后如果程序出现这个错误,编译器就会告诉你断言失败的位置,方便我们定位错误,所以这个函数对程序员来说是非常友好的,应当多多使用。

这里的while ((*dest++ = *src++) != '\0')写法非常简洁,值得仔细品味。

1.3.strcat函数

函数声明:
char * strcat ( char * destination, const char * source );
功能是把源头字符串追加目标字符串的后面
注意:

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

模拟实现

char* mystrcat(char* dest, const char* src)
{
	assert(dest && src);
	char* ret = dest;
	//找到目标字符串的\0
	while (*dest != '\0')
	{
		dest++;
	}
	//拷贝
	while ((*dest++ = *src++) != '\0')
	{
		;
	}
	return ret;
}

思路就是先找到目标字符串的‘\0’,从这个位置开始执行拷贝,直到遇到源字符串的‘\0’停下,‘\0’也要拷贝。

思考一个问题,使用strcat,自己给自己追加可以吗?

char arr[20] = "abcdef";
strcat(arr, arr);

我们预期达到的效果是拼接成”abcdefabcdef“这样一个字符串,但是当你这样做时,会发现程序陷入死循环最终崩了。
在这里插入图片描述
你会发现,子又生孙,孙又生子,永远都找不到‘\0’,子子孙孙,无穷匮也…………

要想自己给自己追加,可以使用strncat这个函数,感兴趣可自行研究

1.4.strstr函数

函数声明:
char * strstr ( const char *str1, const char * str2);
这个函数的比较有趣,它的功能是在str1这个字符串中找str2这个字符串,如果找到了就返回第一次出现的地址,如果找不到就返回空指针。

模拟实现

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

指针cp是可能满足条件的地址,cp最开始指向str1字符串的第一个字符,然后用s1从cp开始往后遍历,s2从str2首字符开始往后遍历,如果*s1 == *s2,就让他们继续往后移动,如果还是相等,继续移动。

若二者始终相等,一直到把str2遍历完了,说明找到了子字符串,返回最初记录的位置cp;
若在遍历的过程中出现不相等的情况,说明cp不是我们要找的地址,所以让cp往后移动,s1回到cp位置,s2也回到起始位置,重新开始遍历。

如果cp把str1遍历完了还没有被返回,说明找不到了,返回NULL。

二.内存函数

字符串函数只能处理字符串,而内存函数可以处理所有类型的数据

2.1.memcpy函数

函数声明如下:
void * memcpy ( void * destination, const void * source, size_t num );
该函数的功能是把source指向的那块内存中的数据,拷贝到destination指向的那块空间,拷贝多少数据呢?拷贝num个字节的数据。

首先看看使用的例子:

    int arr1[] = { 1,2,3,4,5,6,7,8,9,10};
	int arr2[8] = { 0 };
	memcpy(arr2, arr1, 20);

这段代码就表示把arr1数组的前20个字节的数据,拷贝到arr2数组中去。
拷贝后的结果如下
在这里插入图片描述

memcpy函数相较于strcpy函数,它的优点在于能拷贝任意类型的数据,浮点型,整型甚至结构体,万物皆能拷。

我们可以尝试自己实现这个函数
首先看看这个函数的参数,destination的类型是void*,void*是一个通用类型的指针,他能接收任意类型数据的地址,但是如果要对他解引用,或者进行加减运算,就必须先要强制类型转换。因为只有这样编译器才知道解引用访问几个字节,+1要跳过几个字节。

还可以发现source指针前加了一个const,这表示不能通过指针来更改指针指向的那块空间的数据。因为既然是拷贝,那么源头的数据肯定时不需要更改的,在实现这个函数前我就先在参数列表中加上const,如果到时候头脑发热把源头数据给改了,编译器就会报警告提示我。

实现如下

#include <assert.h>
void* MyMemcpy(void* dest, void* src, size_t num)
{
	assert(dest != NULL && src != NULL);
	void* ret = dest;
	while (num--)
	{
		*(char*)dest = *(char*)src;
		dest = (char*)dest + 1;
		src = (char*)src + 1;
	}
	return ret;
}

核心思路就是把void* 强制转换为char*,一次访问一个字节,拷贝num次

2.2.memmove函数

void * memmove ( void * destination, const void * source, size_t num );
该函数的功能是,把source指向的那块内存中的num个字节的数据,拷贝destination到指向的那块空间。
通过仔细对比你会发现,memmove的功能不是和memcp一样吗,而且函数声明也是一毛一样,那二者的不同之处在哪呢?

在内存重叠的时候,使用memcpy可能会出现意想不到的效果,此时建议使用memove
举例说明:

    int arr[20] = { 1,2,3,4,5,6,7,8 };
	memcpy(arr + 2, arr, 16);

在这里插入图片描述
我的本意是用1,2,3,4来覆盖3,4,5,6,最终得到{1,2,1,2,3,4,7,8}这样一个数组,但结果却是{1,2,1,2,1,2,7,8}。这是因为重叠的那片区域,既是源头数据,也是目标空间的数据,在从前向后拷贝的过程中,重叠部分的数据被改成1,2,而3,4根本就没有机会拷贝。

这是memcpy函数的一个不足之处,而memmove就弥补了这一缺陷。

下面我们自己来实现memmove
memmcpy之所以会出现问题,是因为它总是从前向后拷贝数据,当内存重叠时就可能会出现问题,既然如此,那我们就从后向前拷贝呗!从上面的例子来看,从后向前拷贝确实能解决问题。但上面的情况是重叠部分在源头空间的后部,若重叠部分在源头空间的前部时就需要从前往后拷贝了。而如果两块内存没有重叠区域,从后往前和从前往后均可。因此,需要分类讨论。

1.dest > src从后往前拷贝
在这里插入图片描述
2.dest < src从前往后拷贝

在这里插入图片描述
实现代码:

#include <assert.h>
void* MyMemmove(void* dest, const void* src, size_t num)
{
	assert(dest != NULL && src != NULL);
	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);
		}
	}
}

还有一点需要说明,当我们使用VS编译器去测试memcpy时,发现即使内存重叠时也不会出现问题,这是为什么呢?
其实C语言本身只是规定了各种库函数的功能,它规定memcpy的拷贝数据的功能,前提是内存不重叠。同时又规定一个功能更加强大的库函数memmove,它可以处理内存重叠的情况。
规定下来后,各大编译器厂商实现自家的库函数,只需完成函数的基本功能。而开发VS的程序员在写memcpy时显然就超额完成了任务,就连内存重叠也能完成拷贝。但是话说回来,C语言本身并没有要求memcpy有这么强大的功能,所以如果你在别的编译器底下运行时可能就会出现问题。
简单来说,C语言要求memmove拿100分,而memcpy只要打60分就行,但是VS底下的memcpy和memmove都是100分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值