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

microchip dspic33 系列教程(3):深入解析IO操作与中断配置 本文深入解析了Microchip dsPIC33系列微控制器的IO操作与中断配置。从点亮LED的基础GPIO控制(涉及TRISx、LATx、PORTx寄存器)入手,详细讲解了数字输入、上拉电阻配置及模拟功能切换。重点剖析了外部中断(INT0)和变化通知(CN)中断的配置步骤、中断服务函数编写及优先级管理,并提供了使用MPLAB X IDE和PICKit3进行调试的实用技巧与常见问题排查清单。 阅读详情

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个函数的小总结:

strcpy、strcat、strcmp这三个函数是长度不受限的函数。长度不受限指的是,它们会在函数没有达到结束条件时一直执行下去,直到遇到\0;
strncpy、strncat、strncmp这三个函数是长度受限的函数。长度受限指的是,它们只会处理指定数量的字符;
其中strcpy、strcat、strncpy、strncat都没有定义两个字符串出现重合的行为,当出现这种情况时,输出结果取决于使用的编译器;
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任何可打印字符,包括图形字符和空白字符

常用的是isdigit,islower,isupper,isalpha

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语言学习有帮助。

C 语言中如何实现字符串的查找和替换? 空字符串、字符串长度为 0、内存不足等情况。在实际编程中,应该添加适当的错误检查和处理代码,以提高程序的健壮性。 C 语言中实现字符串的查找和替换可以通过基本的循环和字符串操作来完成,也可以利用标准库提供的函数结合一些辅助操作来实现。根据具体的需求和场景,选择合适的方法可以提高程序的效率和可读性。 阅读详情

相关推荐

UX3328S Datasheet中文版_V1.2

UX3328S datasheet 中文版 v1.2

从未初始化的字符串说起

未赋值的字符串为什么会显示“烫”呢?在这里,我会简单的向大家说明一下,欢迎有兴趣的朋友来看看和一起讨论一下

Cantaloupe77的博客 1436

gun_hu33.zip_6hu33_gun_www.6hu33.com

ldpc码的编解码实现,采用加权网络中节点强度和权重都是幂率分布的模型,Pisarenko谐波分解算法。

TMS320VC33 学习笔记 开发环境

TMS320VC33属于TI早期的DSP产品,da

HuangHui的专栏 5570

论文阅读笔记:Extended Vision Transformer (ExViT) for Land Use and Land Cover Classification

注意力机制驱动的深度模型最近取得了成功,如最具代表性的视觉转换器(ViT),吸引了一波高级研究来探索它们对更广泛领域的适应。然而,遥感(RS)领域中基于Transformer的方法更关注单模态数据,这可能会在充分利用不断增长的多模态地球观测数据时失去可扩展性。为此,我们提出了一种新的多模式深度学习框架,通过在最小修改的情况下扩展传统的ViT,针对土地利用和土地覆盖(LULC)分类任务。

嗑研狗的日常 945

《解锁GCC版本选择的奥秘:从入门到精通》

GCC 版本的选择是一项复杂而关键的决策,犹如一场精心策划的战略布局,贯穿于软件开发的整个生命周期。从项目需求与兼容性的深度考量,到性能与稳定性的权衡抉择,再到硬件与系统适配的精准对接,每一个环节都紧密相连,相互影响。在游戏开发行业,对性能的极致追求促使开发者青睐较新的 GCC 版本,以实现更流畅的游戏体验和更震撼的视觉效果;科学计算领域则依赖于 GCC 版本对复杂数学运算的高效优化,以应对大规模数据处理的挑战;嵌入式开发行业因硬件资源的限制和对稳定性的严苛要求,通常选择经过长期验证的稳定版本;

zheng_ruiguo的专栏 1416

嵌入式C语言实战教程

目录第1章嵌入式C语言概述1.1 嵌入式C语言简介1.2 嵌入式C语言的发展历史1.3 嵌入式C语言的主要特点1.4 单片机的汇编语言与嵌入式C语言比较1.5 嵌入式C语言与标准C语言的异同1.6 嵌入式C语言总结嵌入式C语言程序的基本结构2.1 嵌入式C语言入门实例2.2 程序工作原理2.3 源程序1. 源程序 12. 源程序 2嵌入式C语言程序的基本结构头文件2.6 主函数2.7 函数3.1 嵌入式C语言的基本数据类型3.2 基本数据类型3.3 常量与变量3.4 自定义变量类型 typedef4.1 运

m0_67931155的博客 1375

ADRV902x射频评估实战:手把手教你用TES GUI生成ARM bin与JESD204C配置文件

在射频前端系统设计中,ADRV902x系列收发器凭借其宽带性能和灵活配置能力,已成为5G基站、微波回传等场景的核心器件。然而当工程师面对非官方硬件平台时,如何高效利用Transceiver Evaluation Software(TES)完成关键配置,成为项目推进的第一道门槛。本文将彻底解析脱离ADS9母板后的完整离线工作流,从参数预设到文件生成,手把手解决真实工程中的配置痛点。在Demo模式下,所有硬件状态检测都会被跳过,但配置文件生成功能完全可用。作为基础模板,因其配置最为简洁,便于后续自定义修改。

weixin_33704591的博客 429

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针

字符指针数组指针指针数组数组传参和指针传参几种数组、指针之间的比较函数指针函数指针数组指向函数指针数组的指针回调函数指针和数组的笔试题指针是一个存储地址的变量,地址是一块内存空间的唯一标识;指针的大小是固定的(4字节或8字节);指针是有类型的,它的类型决定它 + - 整数时移动的步长、*解引用操作时的权限;指针是能够进行运算的,+ - 整数、指针相减、指针的关系运算;接下来,就让我们继续探讨指针的进阶主题。对于普通的指针,我们都已经很熟悉了。

PLKDTHFS的博客 1518

在虚拟机中对阿克曼转向车进行导航及避障的仿真测试

一、    在我使用rosdep install teb_local_planner去安装teb_local_planner的时候,报错:Command ‘rosdep’ not found, but can be installed with:sudo apt install python3-rosdep2    我花大半个月好不容易建立起的一系列ROS环境,被python3-rosdep2这条万恶的语句连同ROS一起给我删了。。。悲伤来的那么猝不及防    随着ROS版本的迭代,相关资料很多都不再适用

慕羽★的博客 3806

把静态前端托管进 SAP BTP:HTML5 Application Deployer 与企业级内容部署全攻略

本文介绍了在SAP BTP Cloud Foundry环境中使用HTML5 Application Repository托管前端静态资源的工程化实践。通过HTML5 Application Deployer工具,结合MTA描述、路由配置和服务实例,实现前后端解耦和标准化部署。文章详细阐述了目录结构、关键配置文件、MTA编排以及构建部署流程,并针对大体积前端资源提供了GACD和异步上传两种解决方案。最后说明了如何与AppRouter协同工作,使部署内容可供终端用户访问。这套方法使前端更新无需重启后端,支持团队

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 155

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第一式】数据的存储

本章节的重点是数据类型详细介绍整型在内存中的的存储:原码、反码、补码大小端字节序介绍及判断浮点型在内存中的存储解析char // 字符数据类型short // 短整型int // 整型long // 长整型long long // 更长的整型float // 单精度浮点数double // 双精度浮点数// c语言有没有字符串类型?// 没有以及他们所占存储空间的大小。使用这个类型开辟内存空间的大小(大小决定了使用范围)如何看待内存空间的视角什么是大端小端?

PLKDTHFS的博客 1487

OpenClaw Windows 安装指南

好的,我来一步一步带你在 Windows 上安装 OpenClaw。

m0_60605453的博客 549

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第六式】文件操作

为什么使用文件什么是文件文件的打开和关闭文件的顺序读写文件的随机读写文本文件和二进制文件文件读取结束的判定文件缓冲区磁盘上的文件就是文件。但是在程序设计上,我们谈到的文件一般有两种:程序文件、数据文件;本节详细的对c语言中数据文件的使用进行了详细的介绍,包括对文件读写操作使用的函数,以及通过文件位置指针的操作函数来对文件进行随机读写,并指出了feof函数可能出现的使用错误,在最后,介绍了文件缓冲区的概念;

PLKDTHFS的博客 1226

PINN求解PDE

PINN求解Dirichlet泊松方程 泊松方程引入 −Δu=f,x∈Ω=(0,1)2-\Delta u=f,x \in \Omega=(0,1)^2−Δu=f,x∈Ω=(0,1)2 u=g,x∈∂Ωu=g,x \in \partial \Omegau=g,x∈∂Ω import torch import time import numpy as np import matplotlib.pyplot as plt import torch.nn as nn def UU(X, order,prob):

forrestguang的博客 3804

手把手教你学Simulink--基于微电网的建模与仿真场景实例:独立运行模式下微电网的能量管理系统设计

本文详细介绍了使用Simulink对独立运行微电网能量管理系统进行建模与仿真的完整流程。主要包括:系统结构设计(分布式能源、储能系统、负荷等模块)、建模步骤(从创建项目到实现能量管理策略)、仿真设置与结果分析。通过建立基于规则的控制逻辑和优化调度算法,实现微电网的自主运行和能量优化管理。文章还提供了参数优化和多目标改进建议,为新能源和智能电网领域的研究与应用提供了实用指导。

MHD0815的博客 8582

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第五式】动态内存管理

大家在使用数组时肯定都会有过这样的疑问,如果一个数组的长度不足了,可以延长这个数组吗?使其能够满足存储的需求。又或者是现在不确定数组应该设为多长,先把它放着,等之后再设置它的长度。上面的这些需求,数组显然是无法实现的,那么c语言中有什么东西能够做到这一点呢?动态内存分配!你现在不知道需要多大的空间,没关系,之后你明确了的时候,使用动态内存分配,将你要的空间分配给即可。为什么存在动态内存分配动态内存函数的介绍mallocfreecallocrealloc常见的动态内存错误。

PLKDTHFS的博客 1293

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第七式】程序的编译

程序的翻译环境程序的执行环境c语言程序的编译+链接预定义符号介绍预处理指令#define宏和函数的对比预处理操作符#和##的介绍命令定义预处理指令#include预处理指令#undef条件编译__FILE__ // 进行编译的源文件__LINE__ // 文件当前行号__DATE__ // 文件被编译的日期__TIME__ // 文件被编译的时间__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义。

PLKDTHFS的博客 1195

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)

c语言中虽然定义得有许多的类型,如char、int、double等等,但是现实生活中需要描述的问题,仅仅使用这些已有类型是不够的,所以在c语言中,程序员可以根据自己的需要,定义出自己的类型结构,以满足需求。结构体:结构体类型的声明结构的自引用结构体变量的定义和初始化结构体内存对齐结构体传参结构体实现位段(位段的填充&可移植性)枚举枚举类型的定义枚举的优点枚举的使用联合联合类型的定义联合的特点联合的使用有了结构体类型,之后应该如何定义变量呢?

PLKDTHFS的博客 989

C语言修炼——还不会的字符函数和字符串函数给你一次讲明白!

在编程的过程中,我们经常要处理字符和字符串,为了⽅便操作字符和字符串,C语⾔标准库中提供了⼀系列库函数,接下来我们就学习⼀下这些函数。的模拟实现需要注意,我们应该在两字符串相同位序下字符不同的时候去比较,所以我们可以直接用一个循环结构遍历字符串,直到遇见相异字符或。中,⽽⼀个错误码的数字只是一个整数,我们很难理解其代表的含义,所以每⼀个错误码都是有对应的错误信息的。语句判断的作用其实是节省内存,提高效率。,表⽰没有错误,当我们在使⽤标准库中的函数的时候发⽣了某种错误,就会将对应的错误码,存放在。

Meanlong_的博客 1948

C语言修炼秘籍【第零章】c语言概述

c语言是一门编程语言。语言的作用就是交流,人们可以利用它与计算机进行交流,让计算机知道你需要它完成什么工作。本系列文章是对笔者在c语言学习的总结记录。本文会先粗略的介绍c语言中需要学习的内容。

PLKDTHFS的博客 1664

c语言修炼秘籍【第二章】函数

本文会从函数是什么开始,逐一介绍函数的语法结构,如何声明和定义一个函数,函数的参数类型,调用函数时的传参类型,函数的调用方式,函数递归,并在最后对汉诺塔、青蛙跳台阶和八皇后问题进行详细分析(包含全部代码)

PLKDTHFS的博客 1443

c语言修炼秘籍【第三章】数组

本文将会介绍常用到的一维数组和二维数组是如何使用及它们在内存中是如何存储,说明何为数组越界,并讨论数组在作为函数参数时应该如何使用。在最后附带三子棋和扫雷的代码实现详解。本文对一维和二维数组的初始化、创建、使用,作出的简要的介绍,并通过冒泡排序算法的实现,详细说明了当数组作为函数参数时的性质 - - 传递的是首元素地址。在最后,给出了两个数组的使用实例,附带了完整的实现代码,并配有详细注释。

PLKDTHFS的博客 1257

C 语言中如何实现字符串的拼接?

C 语言中字符串的拼接 在 C 语言中,实现字符串的拼接有多种方法。下面我们将详细介绍几种常见的方法,并通过示例代码来帮助理解。 # 一、使用 `strcat` 函数 `strcat` 函数用于将两个字符串连接在一起。它的函数原型在 `` 头文件中,声明如下: ```c char *strcat(char *dest, const char *src); ``` `strcat` 函数会将 `src` 所指向的字符串追加到 `dest` 所指向的字符串的末尾,并返回 `dest

技术笔记 1万+

C语言进阶--万字讲解带你学会字符串函数

万字讲解带你学会字符串函数

微笑,来自内心坚不可摧的自信。 1435
上一篇: c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第二式】指针
下一篇: c语言修炼秘籍 - - 禁(进)忌(阶)秘(技)术(巧)【第四式】自定义类型详解(结构体、枚举、联合)
YKPG
博客等级 码龄6年 344粉丝 37原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值