C语言突破进阶-字符串与内存函数

image-20220122171516999

0. Intro

我们平常经常使用的字符串和字符函数有如下几个

🐉求字符串长度
🐲strlen
🐉长度不受限制的字符串函数
🐲strcpy
🐲strcat
🐲strcmp
🐉长度受限制的字符串函数介绍
🐲strncpy
🐲strncat
🐲strncmp
🐉字符串查找
🐲strstr
🐲strtok
🐉错误信息报告
🐲strerror
🐉字符操作
🐉内存操作函数
🐲memcpy
🐲memmove
🐲memset
🐲memcmp

本文的目标就是力求理解这些函数,并尝试模拟实现它们

1. strlen

image-20220120201221757

size_t strlen ( const char * str );

strlen的返回值是size_t,这size_t也就是unsigned int

模拟实现strlen可以用那些方法呢?

或许可以有以下几种方法

♠️计数器的方法

❤️递归的方法

♣️指针-指针

1.0 strlen注意事项

🌿字符串已经\0 作为结束标志,strlen函数返回的是在字符串中\0 前面出现的字符个数(不包含\0 )。
🍁参数指向的字符串必须要以\0 结束。
🍁注意函数的返回值为size_t,是无符号的(易错)

1.1 证明返回值的无符号型

int main()
{
	if (strlen("abc") - strlen("abcdef") > 0)
		printf(">");
	else
		printf("<=");

	return 0;
}

按理说3-6<0,但是如上代码的输出是>,所以说其实size_t是一个无符号值

1.2 计数器法模拟

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

int main() 
{
	int len = my_strlen("abcdef");
	printf("%d\n", len);
	
	return 0;
}

1.3 递归法

int my_strlen(const char *str)
{
    assert(str);
    if (*str=='\0')
        return 0;
    else
        return 1 + my_strlen(str + 1);
}
int main()
{
    char arr[] = "abcd";
    int ret = 0;
    ret = my_strlen(arr);
    printf("ret=%d\n", ret);
    return 0;
}

1.4 指针-指针

int my_strlen(const char* p)
{
	const char*  ret = NULL;//指针位置可以改变,但是不可以改变指向的内容
	assert(p);
	ret = p;
	while (*ret != '\0')
	{
		ret++;
	}
	return ret - p;
}
int main()
{
	char arr[] = "abcdef";
	int ret = 0;
	ret = my_strlen(arr);
	printf("ret=%d\n", ret);

	return 0;
}

2. strcpy

image-20220120214826752

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

在认识strcpy之前,首先认识一下长度受限函数和长度不受限函数

2.0 长度受限函数和长度不受限函数

对于strcpy来说,由于长度受限,strcpy受限于和被拷贝字符串的\0作为终止符,假如传入的被拷贝字符串没有\0的话,就会导致问题发生,比如出现乱码

int main()
{
	char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f'};
	char arr2[20] = "xxxxxxxxxxxx";
	strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}

image-20220120212803683

所以要使用好该函数,必须了解他的注意事项

2.1 strcpy 注意事项

🌿Copies the C string pointed by source into the array pointed by destination,including the terminating null character (and stopping at that point).
🍁源字符串必须以’\0’ 结束。
🍁会将源字符串中的’\0’ 拷贝到目标空间。
🍁目标空间必须足够大,以确保能存放源字符串。
🍁目标空间必须可变。
🍁学会模拟实现。

2.1.1 目标空间足够大

int main()
{
	char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f','\0'};
	char arr2[3] = " ";
	strcpy(arr2, arr1);
	printf("%s\n", arr2);
	return 0;
}

空间不够大就会报错

image-20220120213242722

2.1.2 目标空间要可变

int main()
{
	char arr1[] = {'a', 'b', 'c', 'd', 'e', 'f','\0'};
	const char* p = "xxxxxxxxxx";
	strcpy(p, arr1);
	printf("%s\n",p);
	return 0;
}

强行要改是没法改的

image-20220120213646164

2.2 strcpy 模拟实现

参照库函数参数要求模拟实现

注意要实现链式访问效果

char* my_strcpy( char* dest, const char* src)
{
	char* ret = dest;
	assert(src && dest);
	while (*dest++ = *src++)
	{//*src =\0 时dest也改变为\0同时跳出
		;
	}
	return ret;
}

3. strcat

3.0 认识strcat

image-20220120220525235

实现的效果是字符串追加到目标中

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

3.1 strcat 注意事项

🌿Appends a copy of the source string to the destination string. The terminating null character
in destination is overwritten by the first character of source, and a null-character is included
at the end of the new string formed by the concatenation of both in destination.
🍁源字符串必须以’\0’ 结束。
🍁目标空间必须有足够的大,能容纳下源字符串的内容。
🍁目标空间必须可修改。

但是字符串自己给自己追加,如何?

3.1.1 源字符串必须以’\0’ 结束

这个原因和之前的一样

3.1.2 目标空间必须有足够的大

也和之前一样,被追加字符串空间要足够

3.1.3 目标空间必须可修改。

空间还是要可以修改才可以追加

3.1.4 strcat不能直接自我追加

image-20220120222423392

但是可以先保存一份在临时变量中,再拼接

3.2 模拟实现 strcat

char *my_strcat(char* dest, const char* src)
{
	assert(dest&&src);
	char* ret = dest;
	//1. 找 目标空间中的'\0'
	while (*dest)
	{
		dest++;
	}
	//2. 追加内容到目标空间
	while (*dest++ = *src++)
	{
		;
	}
	return ret;
}

4. strcmp

4.0 认识strcmp

image-20220121085705772

比较的是对应位置上字符的大小

int strcmp ( const char * str1, const char * str2 );

4.1 标准规定

🌿This function starts comparing the first character of each string. If they are equal to each
other, it continues with the following pairs until the characters differ or until a terminating
null-character is reached.
🍁第一个字符串大于第二个字符串,则返回大于0的数字
🍁第一个字符串等于第二个字符串,则返回0
🍁第一个字符串小于第二个字符串,则返回小于0的数字

4.2 模拟实现 strcmp

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

5. 长度不受限函数

5.1 strncpy

image-20220121091655248

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

注意参数比原先多了一个

5.1.1 strncpy 注意事项

🌿Copies the first num characters of source to destination. If the end of the source C string
(which is signaled by a null-character) is found before num characters have been copied,
destination is padded with zeros until a total of num characters have been written to it.
🍁拷贝num个字符从源字符串到目标空间。
🍁如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个。

5.1.2 源码

char * __cdecl strncpy (
        char * dest,
        const char * source,
        size_t count
        )
{
        char *start = dest;

        while (count && (*dest++ = *source++) != '\0')    /* copy string */
                count--;

        if (count)                              /* pad out with zeroes */
                while (--count)
                        *dest++ = '\0';

        return(start);
}

5.2 strncat

image-20220121092132535

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

5.2.1 strncat 注意事项

🌿Appends the first num characters of source to destination, plus a terminating null-character.
🌿If the length of the C string in source is less than num, only the content up to the
terminating null-character is copied.

5.2.2 源码

char * __cdecl strncat (
        char * front,
        const char * back,
        size_t count
        )
{
        char *start = front;

        while (*front++)
                ;
        front--;

        while (count--)
                if ((*front++ = *back++) == 0)
                        return(start);

        *front = '\0';
        return(start);
}

5.3 strncmp

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

image-20220121095443807

5.3.1 strncmp注意事项

:🍁比较到出现另个字符不一样或者一个字符串结束或者num个字符全部比较完。

5.3.2 源码

int __cdecl strncmp
(
    const char *first,
    const char *last,
    size_t      count
)
{
    size_t x = 0;

    if (!count)
    {
        return 0;
    }

    /*
     * This explicit guard needed to deal correctly with boundary
     * cases: strings shorter than 4 bytes and strings longer than
     * UINT_MAX-4 bytes .
     */
    if( count >= 4 )
    {
        /* unroll by four */
        for (; x < count-4; x+=4)
        {
            first+=4;
            last +=4;

            if (*(first-4) == 0 || *(first-4) != *(last-4))
            {
                return(*(unsigned char *)(first-4) - *(unsigned char *)(last-4));
            }

            if (*(first-3) == 0 || *(first-3) != *(last-3))
            {
                return(*(unsigned char *)(first-3) - *(unsigned char *)(last-3));
            }

            if (*(first-2) == 0 || *(first-2) != *(last-2))
            {
                return(*(unsigned char *)(first-2) - *(unsigned char *)(last-2));
            }

            if (*(first-1) == 0 || *(first-1) != *(last-1))
            {
                return(*(unsigned char *)(first-1) - *(unsigned char *)(last-1));
            }
        }
    }

    /* residual loop */
    for (; x < count; x++)
    {
        if (*first == 0 || *first != *last)
        {
            return(*(unsigned char *)first - *(unsigned char *)last);
        }
        first+=1;
        last+=1;
    }
    return 0;
}

6. strstr

6.0 Intro of strstr

image-20220121100049042

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

6.1 使用与注意事项

演示使用strstr

若找到返回值是传找到的地址

int main()
{
	char arr1[] = "abbbcdef";
	char arr2[] = "bbcd";

	char* ret = strstr(arr1, arr2);

	if (NULL == ret)
		printf("没找到\n");
	else
		printf("%s\n", ret);

	return 0;
}

🌿Returns a pointer to the first occurrence of str2 in str1, or a null pointer if str2 is not part of str1.

6.2 模拟实现strstr

先讨论几种情况

找的字符串不可以是空字符串

if (*substr == '\0')
{
	return (char*)str;
}

image-20220121103820120

当指针指向的字符是相等的时候substr指针往后走,str也往后走,如果sub走到空说明匹配完了,能找到

但是可能还会有特殊情况

特殊情况

上面的方法会使得bbb阻断bbc的判断

image-20220121104203484

所以应该多两个指针,最好让str和substr不动,新指针向后走,同时也要创一个cur指针,默认指向源字符串的首地址,它的作用是表示判断的起始位置

	const char* s1 = str;
	const char* s2 = substr;
	const char* cur = str;

然后实现循环

image-20220121105638788

什么时候终止循环?

s2到\0或s1到\0都要终止

	while (*s1 &&  *s2 && *s1 == *s2)

假如s2能走完那最好直接返回,说明找到

	if (*s2 == '\0')
			return (char*)cur;
char* my_strstr(const char* str, const char* substr)
{
	const char* s1 = str;
	const char* s2 = substr;
	const char* cur = str;

	assert(str && substr);
	if (*substr == '\0')
	{
		return (char*)str;
	}
	while (*cur)
	{
		s1 = cur;
		s2 = substr;
		while (*s1 &&  *s2 && *s1 == *s2)
		{
			s1++;
			s2++;
		}
		if (*s2 == '\0')
			return (char*)cur;
		cur++;
	}
	return NULL;
}

7. strtok

7.0 Intro of strtok

image-20220122151901674

char * strtok ( char * str, const char * sep );

7.1 strtok 快速入门

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

也就是说,strtok找第一个标记的时候,第一个不是NULL,strtok找不是第一个标记的时候,第一个标记是NULL

看一个🌰


int main()
{
	const char* p = "@.#";
	char arr[] = "Danerious@Targerian.123#";
	char buf[50] = { 0 };
	strcpy(buf, arr);
	char* str = strtok(buf, p);//Danerious
	printf("%s\n", str);
	str = strtok(NULL, p);//Targerian
	printf("%s\n", str);
	str = strtok(NULL, p);//123
	printf("%s\n", str);
	//strtok - 开始返回NULL
	return 0;
}

为了防止我原来的字符串被改变,我想存在一个新的数组中,创建一份拷贝值

防止每次切割的时候都要重复调用一次,利用for循环


int main()
{
	const char* p = "@.#";
	char arr[] = "Danerious@Targerian.123#jkewfjw.@#";
	char buf[50] = { 0 };
	strcpy(buf, arr);
	char* str = NULL;

	for (str = strtok(buf, p); str != NULL; str=strtok(NULL, p))
	{
		printf("%s\n", str);
	}
	return 0;
}

image-20220122153758680

根据这样的特性我们猜测,strtok背后是由以一个静态变量来保存的,因此可以实现剪切

8. strerror

8.0 Intro of strerror

image-20220122154256194

char * strerror ( int errnum );

🍁返回错误码,所对应的错误信息。

8.1 strerror快速入门

通过这段代码可以看到strerroer对应不同数字的报错形式,一一对应

int main()
{
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%s\n", strerror(i));
	}
	return 0;
}

image-20220122154704070

8.2 strerror使用情况

当库函数使用的时候,发生错误会把errno这个全局的错误变量设置为本次执行库函数产生的错误码
errno是C语言提供的一个全局变量,可以直接使用,放在errno.h文件中的

#include <errno.h>
int main()
{
	//打开文件
	FILE* pf = fopen("test.txt", "r");
	if (NULL == pf)
	{
		//出错误的原因是什么
		printf("%s\n", strerror(errno));
		return 0;
	}
	//读文件
	//关闭文件
	fclose(pf);
	pf = NULL;
	return 0;
}

9. 字符分类字符

函数如果他的参数符合下列条件就返回真
iscntrl任何控制字符
isspace空白字符:空格‘ ’,换页‘\f’,换行’\n’,回车‘\r’,制表符’\t’或者垂直制表符’\v’
isdigit十进制数字 0~9
isxdigit十六进制数字,包括所有十进制数字,小写字母af,大写字母AF
islower小写字母a~z
isupper大写字母A~Z
isalpha字母az或AZ
isalnum字母或者数字,az,AZ,0~9
ispunct标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph任何图形字符
isprint任何可打印字符,包括图形字符和空白字符

9.1 字符分类函数快速入门

isspace

#include <ctype.h>

int main()
{
	char ch = 'w';
	if (isspace(ch))
	{
		//空白字符
	}
	else
	{

	}
	return 0;
}

isupper

int main()
{
	char ch = 0;
	ch = getchar();
	if (islower(ch))
	{
		ch = toupper(ch);
	}
	else
	{
		ch = tolower(ch);
	}
	printf("%c\n", ch);
	return 0;
}

10 .memcpy

10.0 Intro of memcpy

image-20220122160345514

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

memcpy可以解决更多拷贝问题,比如说原来是strcpy只能拷贝char[],但是现在memcpy可以拷贝int[]

由于设计者不知道memcpy会传入声明类型的指针,所以干脆采用void*的指针作为类型

10.1 memcpy快速入门

int main()
{
	int arr1[] = { 1,2,7,4,5 };
	int arr2[] = { 1,2,3,4,5 };
	int ret = memcpy(arr1, arr2, 9);

	printf("%d\n", ret);
	return 0;
}

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

10.2 模拟实现memcpy

void* my_memcpy(void* dest, const 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;
}

但是如果当我们用下面的测试用例来尝试的话,会得到不正确的结果

关键在于,传入的参数部分是重叠的,所以可能产生覆盖

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

image-20220122163905745

其实memcpy的任务只要能复制不重复的字符串就可以了,实现重复拷贝可以使用另外一个函数

于是就引出了另一个函数memmove

11. memmove

11.0 Intro of memmove

image-20220122163248056

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

11.1 memmove 快速入门

void test1()
{
	int arr3[] = { 1,2,3,4,5,6,7,8,9,10 };
	memmove(arr3, arr3+2, 20);
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", arr3[i]);
	}
}
int main()
{
	test1();
	return 0;
}

image-20220122163733834

🍁和memcpy的差别就是memmove函数处理的源内存块和目标内存块是可以重叠的。
🍁如果源空间和目标空间出现重叠,就得使用memmove函数处理。

11.2 模拟实现memmove

image-20220122164017568

分两种情况就可以了

image-20220122164526551

void* my_memmove(void* dest, const void* src, size_t num)
{
	void* ret = dest;
	assert(dest && src);	
	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);
		}
	}
	return ret;
}

12. memset

12.0 Intro of memset

image-20220122165125540

void *memset(void *dest,int c ,size_t count);

12.1 memset 快速入门

🍁memset修改的时候是按照字节来修改的

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	memset(arr, 'x', 10);
	return 0;
}

image-20220122165804340

13. memcmp

image-20220122170016162

int memcmp ( const void * ptr1,
const void * ptr2,
size_t num );

13.1 memcmp快速入门

🍁比较从ptr1和ptr2指针开始的num个字节

image-20220122170211634

int main()
{
	int arr1[] = { 1,2,7,4,5 };
	int arr2[] = { 1,2,3,4,5 };
	int ret = memcmp(arr1, arr2, 9);

	printf("%d\n", ret);
	return 0;
}

小结:

关于字符串函数和字符内存相关函数就学到这里

老铁们有收获的话一定要给个赞,多多评论哦

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

言之命至9012

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值