随笔——字符串函数

最近浅学了字符串函数,考虑到目前用字符串函数的场景不多,特别写了这篇博客用来回顾,防止过一阵子全忘光。

字符分类函数

顾名思义,这类函数就是用来对字符进行分类的,它们都有一个共同的头文件:ctype.h

函数如果输入的字符符合下列条件就返回真
iscntrl任何控制字符
isspace空白字符
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任何可打印字符,包括图形字符和空白字符

如果不符合条件就返回假。

这里以isupper为例:
写⼀个代码,将字符串中的大写字⺟转小写,其他字符不变。

#include<stdio.h>
#include<ctype.h>

int main()
{
	char str[] = "HeLlo WoRd";
	int i = 0;
	printf("转换前:%s\n", str);
	while (str[i])
	{
		if (isupper(str[i]))
		{
			str[i] += 32;
		}
		i++;
	}
	printf("转换后:%s\n", str);
	return 0;
}

字符转化函数

和字符分类函数一样,它们的头文件也是ctype.h,用来把大小写字符相互转化

函数作用
tolower若转入大写字母,则返回对应小写字母,若传入小写字母,则返回传入的小写字母
toupper若转入小写字母,则返回对应大写字母,若传入大写字母,则返回传入的大写字母

有了字符转化函数上面的代码就可以这样写了:

#include<stdio.h>
#include<ctype.h>

int main()
{
	char str[] = "HeLlo WoRd";
	int i = 0;
	printf("转换前:%s\n", str);
	while (str[i])
	{
		if (isupper(str[i]))
		{
			str[i] = tolower(str[i]);
		}
		i++;
	}
	printf("转换后:%s\n", str);
	return 0;
}

考虑到本篇博客重点为字符串函数,所以对于上述的两大字符函数就不扩展了,如果想了解更多信息,可以访问C libraryC/C++参考手册这两个网站。
你可以在C library中直接对上述函数进行搜索,也可以通过C/C++参考手册查看头文件ctype.h。


C library界面:C library界面


C/C++参考手册(1):C/C++参考手册(1)


C/C++参考手册(2):C/C++参考手册(2)


字符串函数

字符串函数的头文件都是string.h

strlen

size_t strlen ( const char * str );
  • 字符串以 ‘\0’ 作为结束标志,strlen函数返回的是在字符串中 ‘\0’ 前⾯出现的字符个数(不包含 ‘\0’ )。
  • 参数指向的字符串必须要以 ‘\0’ 结束。
  • 注意函数的返回值为size_t,是⽆符号的( 易错 )
  • strlen的使⽤需要包含头⽂件

笔者刚开始学习C语言是就曾遇到这道题:
预测下列代码的运行结果:

#include<stdio.h>
#include<string.h>

int main()
{
	char* str1 = "hello word";
	char* str2 = "I am a student.";
	if (strlen(str1) - strlen(str2))
	{
		printf("字符串1比字符串2长");
	}
	else
	{
		printf("字符串2比字符串1长");
	}
	return 0;
}

A.字符串1比字符串2长
B.字符串2比字符串1长
题目大致是这个意思,我当初就直接选了B;
实际上由于syrlen返回的是size_t(⽆符号),除非两个字符串长度一样,否则strlen(str1) - strlen(str2)都是逻辑真。


strlen的模拟实现:

#include<stdio.h>
#include<assert.h>
#include<string.h>
 //会写三种方法:前两种会创建新变量

 //第一种方法:计数器
size_t my_strlen_1(const char* cource)
{
	assert(cource);
	int count = 0;
	while (*cource)
	{
		count++;
		cource++;
	}
	return (size_t)count;
}

//第二种方法:指针-指针
size_t my_strlen_2(const char* const cource)
{
	assert(cource);
	char* destination = cource;
	while (*destination)
	{
		destination++;
	}
	return (size_t)(destination - cource);
}

//第三种方法:递归
size_t my_strlen_3(const char* cource)
{
	if (*cource)
	{
		return 1 + my_strlen_3(++cource);
	}
	else
	{
		return 0;
	}
}

int main()
{
	char str[] = { "hello word" };
	printf("标准库:%zd\n", strlen(str));
	printf("%d:%zd\n", 1, my_strlen_1(str));
	printf("%d:%zd\n", 2, my_strlen_2(str));
	printf("%d:%zd\n", 3, my_strlen_3(str));
	return 0;
}

strcpy

char* strcpy(char * destination, const char * source );
  • 源字符串必须以 ‘\0’ 结束。
  • 会将源字符串中的 ‘\0’ 拷⻉到⽬标空间。
  • ⽬标空间必须⾜够⼤,以确保能存放源字符串。
  • ⽬标空间必须可修改。
  • 返回目标字符串的起始地址

刚接触strcpy的时候我就有个现在看来很奇怪的想法:如果要使用strcpy,那这个目标字符串(严格来说应该是目标字符串的载体)必须是空的,我写这篇博客的时候已经不知道这个奇怪想法是怎么产生的了;因为strcpy会把源字符串中的 ‘\0’ 也拷⻉到⽬标空间,所以即使目标字符串(的载体)是有内容的,也不会有太大影响。
比如:

#include<stdio.h>
#include<string.h>

int main()
{
	char  str1[50] = "xxxxxxxxxxxxxxxxxxxxxx";
	char* str2 =     "I am a student.";
	strcpy(str1, str2);
	printf("%s\n", str1);
	return 0;
}

最后还是会打印"I am a student.",后面那些没改的x是不会打印出来的。
我们也可以用调试,看得更清楚:

随笔——字符串函数-关于strcpy

目标空间必须可修改可能有些不好理解,就我个人的理解,上述代码中的str1和str2虽然都可被称为字符串,但实际是不一样的,str1是字符数组,这个字符数组储存着字符串"xxxxxxxxxxxxxxxxxxxxxx",str1只是这个字符串的载体,str1就像是一个杯子,其内容物是可以改变的,如果一个杯子之前装水,那现在也可以装可乐,(当然这个比喻也有不严谨的地方:一个本来装水的杯子如果现在要装可乐,那要先把水倒掉,再装可乐,当要把一个本来装着字符串"xxxxxxxxxxxxxxxxxxxxxx"的数组改成装字符串"I am a student."的数组,就没有"倒掉"这一步,而是直接修改原字符串的储存空间),str2就不一样了,它就是字符串本身,就是水或者可乐呀,而不是杯子,现实生活中也没有“把水直接变成可乐”这种说法吧;


至于const这种修饰那应该不用讲了吧:
const写在解引号前就是修饰指针指向的内容,写在解引号后就是修饰指针本身
比如:

		const char* p和char const* p就是修饰指针指向的内容
		char * const p就是修饰指针本身

strcpy的模拟实现:

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

 //初代版
char* my_strcpy_0(char* destination, const char* source)
{
	assert(destination && source);
	char* ret = destination;
	while (*source)
	{
		*destination = *source;
		destination++;
		source++;
	}
	*destination = *source;
	return ret;
}

//一代优化
char* my_strcpy_1(char* destination, const char* source)
{
	assert(destination && source);
	char* ret = destination;
	do
	{
		*destination++ = *source++;

	} while (*(source - 1));
	return ret;
}

//仿标准库型
char* my_strcpy(char* destination, const char* source)
{
	assert(destination && source);
	char* ret = destination;
	while (*destination++ = *source++)
	{
		;
	}

	return ret;
}

int main()
{
	char to[] = { "chnwsduihjfgsadygfuyshui" };
	char* go = "hello word";
	char* rs = "chnwsduihjfgsadygfuyshui";
	printf("标准库:%s\n", strcpy(to, go));
	strcpy(to, rs);
	printf("初代版:%s\n", my_strcpy_0(to, go));
	strcpy(to, rs);
	printf("优化版:%s\n", my_strcpy_1(to, go));
	strcpy(to, rs);
	printf("仿标准库版:%s\n", my_strcpy(to, go));
	strcpy(to, rs);

	return 0;
}

strcat

char * strcat ( char * destination, const char * source );
  • 源字符串必须以 ‘\0’ 结束。
  • ⽬标字符串中也得有 \0 ,否则没办法知道追加从哪⾥开始。
  • ⽬标空间必须有⾜够的⼤,能容纳下源字符串的内容。
  • ⽬标空间必须可修改。

注意:strcat不会把源字符串的’\0’也拷贝过来:
让我们来看调试:

随笔——字符串函数-关于strcat


strcat的模拟实现

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

char* my_strcat(char* destination, const char* source)
{
	assert(destination && source);
	char* ret = destination;
	while (*destination)
	{
		destination++;
	}
	while (*destination++ = *source++)
	{
		;
	}
	return ret;
}

int main()
{
	char to[50] = "hello ";
	char* go = "word";
	char* rs = "hello ";
	printf("%s\n", strcat(to, go));
	strcpy(to, rs);
	printf("%s\n", my_strcat(to, go));
	strcpy(to, rs);
	return 0;
}

strcmp

int strcmp ( const char * str1, const char * str2 );
  • 第⼀个字符串⼤于第⼆个字符串,则返回⼤于0的数字
  • 第⼀个字符串等于第⼆个字符串,则返回0
  • 第⼀个字符串⼩于第⼆个字符串,则返回⼩于0的数字

VS 2022上的strcmp与C标准中的strcmp不同,VS 2022上的strcmp是第⼀个字符串⼤于第⼆个字符串,则返回1, 第⼀个字符串等于第⼆个字符串,则返回0,第⼀个字符串⼩于第⼆个字符串,则返回-1。


strcmp的模拟实现

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

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;
}

int main()
{
	char* str1 = "abcdef";
	char* str2 = "abcdfe";
	if (my_strcmp(str1, str2) > 0)
	{
		printf("字符串1比字符串2大\n");
	}
	else
	{
		printf("字符串2比字符串1大\n");
	}
	return 0;
}

如果你在VS上使用strcpy,strcat,strcmp,VS会报警的,和scanf报错类似,VS认为它们都是不安全的,因为它们都不会判断目标空间是否足够大以存储输入的信息,容易造成越权访问,所以就有了strncpy,strncat,strncmp,相比strcpy,strcat,strcmp,strncpy,strncat,strncmp相对安全,因为它们多了一个参数,要求你人为规定输入的字符串长度,减少了越权访问的可能性。


strncpy

char * strncpy ( char * destination, const char * source, size_t num );
  • 拷⻉num个字符从源字符串到⽬标空间
  • 如果源字符串的⻓度⼩于num,则拷⻉完源字符串之后,在⽬标的后边追加ASCII的0(\0),直到num个
  • 如果源字符串的⻓度大于等于num,则拷⻉完源字符串之后,不会在⽬标的后边追加一个\0

看看调试,效果更明显:

随笔——字符串函数——关于strncpy

strncpy的模拟实现
第一版:

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


char* my_strncpy(char* destination, const char* source, size_t num)
{
	assert(destination && source);
	char* ret = destination;
	size_t len = strlen(source);
	if (len < num)
	{
		while (*destination++ = *source++)
		{
			;
		}
		size_t i = 0;
		for (; i < num - len - 1; i++)
		{
			*destination++ = 0;
		}
	}
	else
	{
		while (num--)
		{
			*destination++ = *source++;
		}
	}
	return ret;
}

int main()
{
	char arr[50] = "xxxxxxxxxxxxxxxxxxxxxxx";
	char* str = "hello word";
	size_t n = strlen(str);
	my_strncpy(arr, str, n - 2);
	printf("%s\n", arr);
	return 0;
}

第二版:

#include<stdio.h>
#include<assert.h>
#include<string.h>
char* MyStrncpy(char* destination, const char* source, size_t num)
{
	assert(destination && source);
	char* ret = destination;
	size_t i = 0;
	for (; source[i] != '\0' && i < num; i++)
	{
		destination[i] = source[i];
	}
	if (num > i)
	{
		for (; i < num; i++)
		{
			destination[i] = 0;
		}
	}
	return ret;
}


int main()
{
	char arr[50] = "xxxxxxxxxxxxxxxxxxxxxxx";
	char* str = "hello word";
	size_t n = strlen(str);
	MyStrncpy(arr, str, n);
	printf("%s\n", arr);
	return 0;
}

strncat

char * strncat ( char * destination, const char * source, size_t num );
  • 将source指向字符串的前num个字符追加到destination指向的字符串末尾,再追加⼀个 \0 字
  • 如果source指向的字符串的⻓度⼩于num的时候,只会将字符串中到\0 的内容追加到destination指向的字符串末尾

还是看看调试:

随笔——字符串函数——关于strncat

strncat的模拟实现

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

char* my_strncat(char* destination, const char* source, size_t num)
{
	assert(destination && source);
	char* ret = destination;
	while (*destination)
	{
		destination++;
	}
	size_t len = strlen(source);
	if (len > num)
	{
		while (num--)
		{
			*destination++ = *source++;
		}
		*destination = 0;
	}
	else
	{
		while (*destination++ = *source++)
		{
			;
		}
	}

	return ret;
}

int main()
{
	char arr[100] = "xxxxxxxxxxx\0xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
	char* str = "Hope Is the Thing With Feathers";
	size_t n = strlen(str);
	my_strncat(arr, str, n + 2);
	printf("%s\n", arr);
	return 0;
}

strncmp

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

⽐较str1和str2的前num个字符,如果相等就继续往后⽐较,最多⽐较num个字⺟,如果提前发现不⼀样,就提前结束,⼤的字符所在的字符串⼤于另外⼀个。如果num个字符都相等,就是相等返回0。


strstr

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

用于在字符串str1中查找字符串str2

  • 如果*str2是’\0’,则返回str1
  • 如果能找到,就返回字符串str2在字符串str1中第⼀次出现位置的地址
  • #如果找不到,就返回NULL

strstr的模拟实现:

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

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

	}
	return NULL;
}

int main()
{
	char* str1 = "hello word";
	char* str2 = "llo w";
	printf("%s\n", my_strstr(str1, str2));
	return 0;
}


strtok

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

strtok比较抽象,实际用的也不多

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

这样说可能还是不好理解,我个人的理解是strtok用于将一个字符串分割成不同的部分,比如对于字符串wind3344764904@gmail.com(这是我随便造的gmail邮箱,不是笔者的邮箱)来说,这个字符串被分成了三部分:第一部分wind3344764904,第二部分gmail,第三部分就是com,其中字符’@‘和字符"."就充当分隔符的角色。
strtok第一次调用是strtok(str, sep);str告诉函数要对那个字符串进行分割,sep告诉函数,到底那些字符才被算作分隔符,之后strtok会逐个遍历字符串str,只要找到一个分隔符,就会把这个分隔符修改为’\0’,并会用一个静态变量记住这个分隔符的地址,方便下次调用,然后返回这部分的起始地址;第二次调用第一个参数就不用再输入str了,而是NULL,这样strtok就会把之前那个储存的静态变量拿出来用作起始地址再往后遍历,直到再找到一个分隔符,把这个分隔符修改为’\0’,并把这个分隔符的地址赋给静态变量,返回第二部分的起始地址,依次类推,如果想换个字符串进行分割,第一个参数就输入另一个字符串,如果字符串中已经没有分隔符了就会返回NULL
以下是示例代码:

#include<stdio.h>
#include<string.h>

int main()
{
	char* str = "wind3344764904@gmail.com";
	char* sep = "@.";
	char arr[50] = "\0";
	char* store = NULL;
	size_t num = strlen(str);
	strncpy(arr, str, num);
	for (store = strtok(arr, sep); store != NULL; store = strtok(NULL, sep))
	{
		printf("%s\n", store);
	}
	return 0;
}

调试控制台的结果:
调试控制台的结果


strerror

char * strerror ( int errnum );

strerror函数可以把参数部分错误码对应的错误信息的字符串地址返回来。
在不同的系统和C语⾔标准库的实现中都规定了⼀些错误码,⼀般是放在 errno.h 这个头⽂件中说明的,你可以用Everything来看,C语⾔程序启动的时候就会使⽤⼀个全⾯的变量errno来记录程序的当前错误码,只不过程序启动的时候errno是0,表⽰没有错误,当我们在使⽤标准库中的函数的时候发⽣了某种错误,就会讲对应的错误码,存放在errno中,⽽⼀个错误码的数字是整数很难理解是什么意思,所以每⼀个错误码都是有对应的错误信息的。strerror函数就可以将错误对应的错误信息字符串的地址返回。

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
	//foprn以读的形式(r)打开文件,如果项目路径下文件不存在,就打开失败
	FILE* pf = fopen("main.txt", "r");
	if (pf == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	return 0;
}

调试控制台的结果:
调试控制台的结果
No such file or directory:没有这样的文件或目录
除此之外还有一个函数叫做perror,头文件是stdio.h,它集成了printf和strerror,就一个参数,这个参数是个字符串,用于对这个错误信息进行标记,防止有多个错误信息存在时无法一一对应,代码如下:

#include<stdio.h>
#include<string.h>
#include<errno.h>

int main()
{
	//foprn以读的形式(r)打开文件,如果项目路径下文件不存在,就打开失败
	FILE* pf = fopen("main.txt", "r");
	if (pf == NULL)
	{
		perror("第一处");
		return 1;
	}
	return 0;
}

调试控制台的结果:

调试控制台的结果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值