字符串是数据的重要组成部分,C语言中有许多操作字符串的函数,我将其中几个列出来:

//均需要包含头文件string.h

strcpy()            //string copy
strncpy()           //string n copy
strcat()            //string catch
strncat()           //string n catch
strcmp()            //string compare
strlen()            //string length
strstr()            //string string
strtok()            //string token
strerror()          //string error

strcpy()用于将一个字符串的内容拷贝到另一个字符串,具体用法:

char *strcpy(char *dest, const char *src);
//此处dest是destination(目标空间)的简写,src是source(来源)的简写
//函数有返回值,为dest地址
//传入的是两个字符串的指针,将src指向内容拷贝到dest指向空间
#include <stdio.h>
#include <string.h>

int main()
{
	char str1[] = "xxxxxxxxxxx";
	char str2[] = "abcd";
	strcpy(str1, str2);            //将str2指向内容放到str1指向的空间里
	printf("%s", str1);
	return 0;
}

这个函数的原理容易想到,是将src指向空间的内容\0前面的部分一个一个拷贝到dest指向空间,基于此,我们可以自己尝试还原这个函数:

char* my_strcpy(char* dest, const char* src)    //src不应该被改变,所以加const保护
{
	char* dest_cpy = dest;      //后面会改变dest指针,先留下备份
	//终止条件是src指向空间遇到\0,因此可以用while循环
	while (*src != '\0')        // \0 ASC码值为0,故也可写成while(*src)
	{
		*dest = *src;           //拷贝一位
		dest++;                 //指向下一位
		src++;               
		//这三行可简化为*dest++ = *src++;
	}

	//此时src的\0还没有被拷贝到dest,可手动加上
	*dest = *src;               //src已经指向\0,直接拷贝就行
	return dest_cpy;
}

这样就大致还原了strcpy函数的功能。可以在此基础上进一步简化代码:

char* my_strcpy(char* dest, const char* src)   
{ 
	char* dest_cpy = dest;      //后面会改变dest指针,先留下备份
	//终止条件是src指向空间遇到\0,因此可以用while循环
	while (*dest++ = *src++)        // 先赋值再地址加一,最后\0赋给*dest后跳出循环
        ;                          //此时while后已经不需要语句,用空语句就行                                                               

	return dest_cpy;
}

以上都是目标空间足够的前提下。若目标空间不足以装下要拷贝的字符串,再用上述代码就会导致越界访问,在上一篇总结过,这种行为会造成野指针,产生安全问题。为使程序安全性更高,可以使用strncpy()函数。

char *strcpy(char *dest, const char *src,int num);  //原型

这个函数限定了最多只能拷贝n个字符,只要提前掌握目标空间长度,就能有效避免野指针问题。

char str1[] = "xxxxx";
char str2[] = "abcdefg";
strncpy(str1,str2,3);
printf("%s",str1);
//输出结果是abcxx

你可能觉得这个函数的还原用for循环更好,就像下面这样:

char* my_strncpy(char* dest, const char* src,int num)   
{ 
	char* dest_cpy = dest;      //后面会改变dest指针,先留下备份
	//希望用循环赋值
    int i;
	for (i = 0;i < num;i++)
    {
        *(dest+i) = *(src+i);
    }                                                             

	return dest_cpy;
}

这样是不行的,因为拷贝源头可能不足n个字符,可能导致野指针。

使用strncpy如果拷贝源头不足n个字符,会自动补\0,补足n个。

char str1[] = "xxxxxxxx";
char str2[] = "abcd";
strncpy(str1,str2,6);
printf("%s",str1);
//打印abcd
//可监视str1,为abcd\0\0xx\0
//原本为8个x和一个\0,拷贝了str2的5个字符又补了一个\0,后面不变

所以我们要还原仍要用while循环。

char* my_strncpy(char* dest, const char* src,int num)    //src不应该被改变,所以加const保护
{
	char* dest_cpy = dest;      //后面会改变dest指针,先留下备份
	//每复制一个num减一,先检测n是否归零
	while (num-- && (*dest++ = *src++))
		;

	//拷贝到\0,但num可能还没归零,要确保num归零
	while (num--)
	{
		*dest++ = '\0';             //补 \0 补足num个字符
	}

	return dest_cpy;
}

这个函数通常要与求字符串长度的函数strlen()配合使用,接下来介绍strlen函数。

size_ t strlen(const char* str)      //原型
char str[] = "abcdef";
printf("%zu", strlen(str));          //打印字符串str长度

strlen的原理是求字符串\0之前的字符个数,返回一个size_t类型的值,我们可以自己还原一个:

size_t my_strlen(const char* str)
{
	int count = 0;
	while (*str++)         //指针每右移一位count加一,直到指向\0
		count++;
	return count;
}

strcpy会覆盖目标空间原来的内容,而strcat会在目标空间的末尾追加来源空间的内容。

char* strcat(char* dest,char* src)         //原型,返回值与strcpy一致
char str1[20] = "xxxxxxx";                  //确保目标空间长度足够,避免野指针
char str2[] = "abcd";
strcat(str1,str2);
printf("%s",str1);                 //打印结果是xxxxxxxabcd

str1原本末尾有\0,如果追加后还有的话肯定无法正常打印,所以追加时str2其实覆盖了\0。那么这个函数是否可以转化成用strcpy,但是传入的是目标空间末尾的指针?基于此,可以还原这个函数:

char* my_strcat(char* dest, char* src)
{
	char* dest_cpy = dest;
	int s = my_strlen(dest);          //确定从第几个开始
	my_strcpy(dest + s, src);         //从\0开始拷贝
	return dest_cpy;
}

这里为了说明原理直接用了上面的函数(才不是懒呢)。strcat也有越界访问的风险,推荐使用更加安全的strncat,类似于strncpy,限制最多追加的个数,但不会补\0。这个函数也能自己实现:

char* my_strncat(char* dest, char* src,int num)
{
	char* dest_cpy = dest;
	int s = my_strlen(dest);          //确定从第几个开始
	dest += s;
	//一种情况是追加了n个,另一种情况是src提前指向\0
	while (num-- && (*dest++ = *src++))
		;
	return dest_cpy;
}

接下来是一个常用的字符串比较函数。字符串比较不能直接用==,而要用strcmp()函数。

int strcmp(const char* str1,const char* str2)    //原型
//只是比较,不希望改变内容,所以加const保护
//比较的是字典序,从前到后一个一个字母比较ASC码,直到遇到不同的,不同的ASC码大小决定字符串大小
//比如abc和abd,前两位相同,c小于d ASC码值,所以abc更小
//str1大,返回正数
//str1大,返回负数
//二者相等,返回0

我们可以自己实现这个函数。

int my_strcmp(const char* str1, const char* str2)
{
	//二者不同且都不指向\0的时候右移指针
	while (*str1 && *str2 && *str1 == *str2)
	{
		str1++;
		str2++;            //典型错误是写*str1++ == *str2++,这样出循环是会指向下一个字符
	}
	
	//此时已有一个指针指向\0或指向不同字符
	if (*str1 > *str2)
		return 1;
	else if (*str1 < *str2)
		return -1;
	else
		return 0;
}

可以用ASC码的运算进一步简化,因为最后使用函数时只看正负。

int my_strcmp(const char* str1, const char* str2)
{
	//二者不同且都不指向\0的时候右移指针
	while (*str1 && *str2 && *str1 == *str2)
	{
		str1++;
		str2++;
	}
	
	//此时已有一个指针指向\0或指向不同字符
	return (int)(*stri - *str2);
    //char强制转int
}

strstr()函数用于寻找一个字符串里有没有另一个字符串,并返回第一次出现的位置。

char *strstr(const char *str1, const char *str2);  //原型
//str1指向待查找的主字符串,str2指向要查找的子串
//返回子串第一次出现的位置,没找到返回null,如:
char str1[] = "abcdefghijkl";               
char str2[] = "cd";
printf("%s", strstr(str1, str2));   //输出cdefghijkl
printf("%s", strstr(str1,"cf"));    //输出(null)

这个函数要实现起来比较复杂,我们通常会想到这样去比:

abcdefg
cd
//对齐比较第一位,即a和c,不同,则子串右移
abcdefg
 cd
//不同,再右移
abcdefg
  cd
//比较第一位,都是c,然后比第二位,都是d,相同,返回指向主字符串c的地址

这里表现的是子串右移,实际上我们不能让子串右移,而让主字符串左移。想象有一个指针指向主字符串和子串开头,主字符串左移,指针就指向了主字符串第二个元素前的位置,指针是相对主字符串右移的。

也就是说,比较第一位不同后,让指向主字符串的指针右移一位,直到第一位相同。第一位相同后,我们要比较第二位是否相同,因此,让两个指针都向右移。如果第二位相同继续比后面的(如果还有),直到比到不同的或比完。如果比完了都相同,那么就找到了。比到不同的就退出比较,之后又从第一位开始比较。基于此,可写出:

char* my_strstr(const char* str1, const char* str2)
{
    const char* str1_cpy;
	const char* str2_cpy;
	while (*str1)
	{
		if (*str1 == *str2)
		{
			//现在比较后面的位
			str1_cpy = str1;
            str2_cpy = str2;
			//提前拷贝指针,用拷贝的指针找,方便返回
			while (*str1_cpy == *str2_cpy && *str1_cpy && *str2_cpy)
			{
				str1_cpy++;
				str2_cpy++;
			}
			//在找到的情况下,str2会指向末尾\0
			if (*str2_cpy == '\0')
				return (char*)str1;
			//没返回就是没找到,可能是str1已经到尽头
			if (*str1_cpy == '\0')
				return NULL;
			//还要从之前的位置继续找,这正是设置拷贝指针的意义,为了原指针不动
		}
		str1++;                       //直到遇到第一位相同的或str1已指向最后
	}
	//到这就是最后都没找到
	return NULL;
}

整个代码比较复杂,适合画图进行理解。

strtok是一个字符串切割函数,用来去掉指定字符,可以多次使用。

char* strtok(char* str1,const char* str2)     //原型
//str1出现str2指向字符,会被替换成\0
//返回指向子串的指针
strtok("abc","b");
//切割后变为{'a','\0','c','\0'},子串为"a"

连续切割同一字符串时,第二次可以不用传入被切字符串的地址,可传入NULL,这说明这个函数能记忆地址。

int main()
{
	char str1[] = "abcdbcdebcfg";
	char str2[] = "b";       
	strtok(str1, str2);       //第一次需传str1
	strtok(NULL, str2);       //第二次传NULL即可
	strtok(NULL, str2);
	return 0;
}

这段代码执行后可以监视str1 ,可以看到3个b确实都变成了\0。

根据后面可以记忆的特性,我们能写出完美切割代码:

char* p = NULL;
for (p = strtok(str1, str2); p != NULL; p = strtok(NULL, str2))
{
	printf("%s\n", p);       //能打印出所有片段
}
//会不断切割直到无法再割

str2内有多个字符时,每个字符都作为分隔符,将遇到的第一个分隔符改成\0,第二次切割时,从\0开始,如果后面还有分隔符,会先跳过所有分隔符,找到一个分隔符外的字符,再重复操作,举个例子:

char str1[] = "abcdcbdebcbfg";
char str2[] = "bc";
char* p = NULL;
for (p = strtok(str1, str2); p != NULL; p = strtok(NULL, str2))
{
	printf("%s\n", p);
}
//第一次将b改为\0,并跳过分隔符c,拆分出a
//第二次将第二个c改成\0,并跳过分隔符b,拆分出d
//第三次将第三个b改成\0,并跳过分隔符c,b,拆分出fg
//第四次拆分出fg
//代码最后打印四个子串 a d de fg

最后是strerror()。C语言的库函数在使用时如果发生了错误,会返回一个错误码(errno)。将errno传入strerror会得到错误信息(字符串)。

char* strerror(int errornum)    //原型

#include <errno.h>                      //使用errno需包含头文件errno.h
int main()
{
	printf("%s", strerror(errno));       //打印错误信息
	return 0;
}

没错时,就会打印No error。这是查找错误来源的有效方法。

perror() 相当于printf() 加上 strerror ,perror内部放函数名,会直接打印关于使用这个函数的错误信息。

int main()
{
	fopen("test.txt", "r");    //尝试打开test.txt这个文件
	perror("fopen");
	return 0;
}

返回错误信息,表示没有这个文件。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐