文章目录
- 一、字符数组与“字符串数组”?
- 二、常见的字符串操作函数有哪些?
- 1、strlen()
- 2、strcpy()
- 3、strcat()
- 4、strcmp()
- 5、strstr()
一、字符数组与“字符串数组”?
许多初学者面对字符串总是有一种莫名的恐惧感。C语言并没有纯字符串数组的说法,但其本质上不过就是“字符们”的集合,于是我们从字符数组的定义出发是可以定义“字符串”数组的。但需要注意的是,当不给定长度时,当以单个字符定义数组时,字符后没有\0作为终止符,而当以字符串定义数组时,在字符串末尾会以\0
作为字符串的结束标志。
如:
char a[]={'a','b','c','d','e','f'};
//在不指定字符数组长度时,以单个字符进行定义时,在末尾无\0作为终止符
char b[]="abcdef";
//在不指定字符数组长度时,以字符串进行定义时,在末尾用\0作为终止符
不要小看这个\0
,它接下来扮演着解开字符串神秘面纱的重要角色。
我知道这时候有的小伙伴会问,上面是不指定字符数组长度,那如果改成指定长度呢?
如:
char A[6]={'a','b','c','d','e','f'};
char B[6]="abcdef";
//当给定数组长度并且用指定元素内容占满时,
//无论以字符串定义或是字符定义的数组在末尾都不会直接存放\0,
//此时用字符串操作函数如strlen对长度进行检索,将发生难以预料的风险。
char A1[6]={'a','b','c','d','e'};
char B1[6]="abcde";
//此时在e的后面数组A1与B1都会存放\0来维持长度6
//所以当指定长度定义字符数组时,应当在满足长度元素之前的一个单位就结束元素存放
综上所述,\0
在字符数组中占据一个元素单位,并且会作为字符的终止标志。
当然还有另一种定义字符串的方法:
char*str="abcdef";
这是一个用 char* 类型指针维护的字符串,char*str 指针指向字符串首元素a,但当对指针变量解引用操作时,将会访问整个字符串。这种方式与上述定义方式不同的是,这种定义方式的字符串不可修改,以指针形式维护的字符串将创建在内存中的静态区。(如果对内存分区不太理解的小伙伴可以参看我的函数栈帧那篇文章)
在实际应用中,我们更愿意用上述b数组的定义方式来定义字符串,在不给定长度的情况下,编译器会在字符串末尾自动填充\0作为字符串的终止符,并且该字符串可修改。
二、常见的字符串操作函数有哪些?
我们需要存储或处理一些字符类型数据,所以我们创建了字符串。但我们还不满足于此,我们还希望这些字符串之间可以进行某些操作来实现一些功能,比如说字符串的长度是多少?字符串之间是否相等?在一个字符串后面追加一个字符串等等…为了实现上述种种,字符串函数应运而生。下面我们将对常见字符串一一介绍,并从逻辑的角度出发来自定义模拟实现这些库函数。
1、strlen()
strlen函数是一个常见的C语言的求字符串长度的库函数,它在库中的定义如下:
size_t strlen(const char*string)
它的参数为一个const
修饰的字符类型指针,它的返回类型为无符号整型size_t
,当然我们更习惯另一种写法:unsigned int
。这表明当用strlen计算字符串长度时,返回类型为一个无符号整型。
一种常见的错误如下:
int main()
{
const char* str1="abcdef";
const char* str2="abc";
if(strlen(str2)-strlen(str1)>0)
{
printf("str2>str1\n");
}
else
{
printf("str1>str2\n");
}
return 0;
}
在VS2019编译环境下,会输出str2>str1。strlen函数会返回一个无符号整型。当strlen(str2)
长度的3(unsigned)
减去strlen(str1)
的长度6(unsigned)
时得到-3(补码)
,但是此补码会以无符号形式输出,那么将会是一个很大的正数。
**那么strlen是如何实现的呢?**在上述中我们提到,在字符串的最后一个元素后有\0
作为终止符号。strlen函数就是依靠此机制进行工作的,我们模仿库函数中的长度检索机制,模拟三种实现方式:
(1)计数器形式
unsigned int mystrlen(const char* arr)// strlen模拟实现,计数器方式
{
assert(arr != NULL);
int count = 0;
while (*arr != '\0')
{
count++;
arr++;
}
return count;
}
(2)递归方式
unsigned int my_strlen(const char *arr)//strlen模拟实现 递归方式
{
assert(arr);
if (*arr != '\0')
{
return 1 + my_strlen(arr + 1);
}
else
return 0;
}
(3)指针相减方式
unsigned int Mystrlen(const char*s)//strlen模拟实现 使用指针相减的方式
{
assert(s);
const char* p = NULL;
p = s;
while (*p != '\0')
{
p++;
}
int ret = p-s;
return ret;
}
实际上,三种方式都将找到\0
作为结束标志,不同的是,在某些场景下,函数递归方式可以不创建临时变量从而实现strlen
函数功能。
2、strcpy()
字符串拷贝,或者叫复制粘贴,是编程实际中难以避免的字符运算。比如将学生信息进行拷贝,对备忘录进行拷贝操作等等
如:将char a []="Tom"
;拷贝到char a2[20]="name"
中,即批量的将a2改变为Tom时,strcpy函数就派上用场。在C语言库中strcpy函数定义如下:
char *strcpy( char *strDestination, const char *strSource );
我们可以看到strcpy的参数分别为char*类型指针指向的目标地址(strDestination),以及const修饰的char*类型的指针指向源地址(strSource)。值得一提的时,在使用strcpy时有如下注意事项:
- 源字符串必须有\0作为终止标志
- 在进行拷贝时\0也会拷贝到目标空间
- 目标空间必须足够大,否则将无法拷贝成功
- 目标地址必须可变(即不能采用char*指针在静态区创建字符串)
模拟实现strcpy的核心思想就是以\0
作为循环结束标志,并不断更新指针访问空间进行从源地址向目标地址元素的赋值
strcpy模拟实现
char* my_strcpy( char*dest, const char* str)
{
assert(dest != NULL);
assert(str != NULL);
char* ret = dest;
while (*dest++ = *str++)
{
;
}
return ret;
}
//strcpy模拟实现 切记 返回值类型为char*
strncpy是一种指定长度的字符串拷贝函数,与strcpy稍有不同,即在传递参数时需要传递n的值来指定拷贝长度,其他参数与strcpy一致
strncpy模拟实现:
char* my_strncpy(char*dest, const char* src, size_t count)
{
assert(dest && src);
char* ret = dest;
while (count)
{
*dest++ = *src++;
count--;
}
return ret;
}
//strncpy 限制字符长度的 字符串复制 函数的模拟实现
3、strcat()
试想这样一段编程实际场景,需要对一个字符串后进行补充一段字符串内容。strcat可以将源字符串全部追加到目标字符串处,在C库中它的定义如下:
char *strcat( char *strDestination, const char *strSource );
strcat与strcpy长得极像,无论是返回值类型还是参数类型。两者差异就在于strcpy是进行字符串之间的拷贝,strcat是将字符串追加到另一字符串之后。
其对字符串的要求与strcpy相同,同样是目标空间需要足够大以及字符串可修改并有\0
作为终止符号
strcat模拟实现:
char* my_strcat( char*dest, const char* src)//strcat模拟实现
{
assert(dest && src);
char* ret = dest;
while (*dest != '\0')
{
dest++;
}
while (*dest++ = *src++)
{
;
}
return ret;
}
可以看到,与strcpy
唯一的不同就在于,strcat需要先找到目标字符串的末尾位置,然后进行赋值(拷贝)操作。由于dest指针的不断修改,我们需要事先定义一个char* ret对dest进行存放,最终返回ret,即返回dest的首地址,在主函数中就可以对目标字符串进行打印。
与strncpy
类似,strncat
也是一种可以指定长度追加的字符串追加函数
strncat模拟实现;
char* my_strncat(char*dest, const char* src, size_t count)
{
assert(dest && src);
char* ret = dest;
while (*dest != '\0')
{
dest++;
}
while (count)
{
*dest++ = *src++;
count--;
}
return ret;
}
//strncat 限制字符的字符串追加函数模拟实现
4、strcmp()
int strcmp( const char *string1, const char *string2 );
字符串比较函数strcmp
,用来比较两个字符串的大小。这里的比较方法是:英文字母按照对应的ASCII码值进行逐一比较,直至发现不同的字符或者全部比较完毕。如果两个字符串相等那么就会返回0,如果string1大于string2将返回一个大于0的数,反之则会返回一个小于0的数。在实际编程中,对于密码的比对就可以使用strcmp函数来比较两个字符串。
strcmp模拟实现:
int my_strcmp(const char* s1,const char*s2)//字符串比较函数模拟实现
{
assert(s1&&s2);
while (*s1 == *s2)
{
if (*s1 == '\0')
{
return 0;
}
s1++;
s2++;
}
return *s1 - *s2;
}
除此之外还可以用strncmp
来指定长度字符比较,这种方式较为安全。C语言库函数中有此函数,VS2019编译环境下推荐使用strncmp
来代替strcmp
,来取消字符串长度而带来的风险。
strncmp模拟实现如下:
int my_strncmp(char* s1, char* s2,size_t count)
{
assert(s1 && s2);
size_t i = 0;
for (i = 0; i < count; i++)
{
if ( ((*s1) == (*s2)) && i == count - 1)
{
return 0;
}
else if(*s1!=*s2)
{
return *s1 - *s2;
}
s1++;
s2++;
}
}
//strncat模拟实现 限制字符长度的 字符串比较函数
可以看到,在使用strncmp
时,除了传递比较的字符串之外,还需要传递比较的长度n。
5、strstr()
strstr
函数的主要功能是判断strCharSet字符串是否在string字符串中(即一个字符串是否为另一个字符串的子串)在C标准库中的定义如下:
char *strstr( const char *string, const char *strCharSet );
//注意这里说的是判断strCharSet是否为string的子串
与strcat
、strcpy
不同,strstr
要求参数需要均为const
修饰的常量。返回值与strcat
、strcpy
一致均为char*
类型的指针变量,返回string字符串中第一个与strCharSet相同的元素的地址
比如:
char a[]="abcdefbcd";
char b[]="bcd";
char* ret=strstr(a,b);
printf("%s\n",ret);
//此时将打印 bcdefbcd,即返回与bcd匹配的第一个b出现的位置
若简单将此功能模拟实现并不难,只需要按照strcpy或者strcat的逻辑进行检索并返回就可以,但是如果需要判别的字符串如下所示呢?
char string[]="abbbcde"
char strCharSet[]="bbc";
这里有人一定会讲:那就用KMP字符串匹配算法!
好主意,但是对于初学者来说有没有简单的逻辑实现方式呢?
完成匹配的关键其实就是:当strCharSet字符串出现c的时候字符串string中仍是b,那么之前的两次检索仿佛“前功尽弃”。其实不然,这里只需要在开始检索时设置一个cp指针,当发生检索失败时cp+1,再以cp为起点开始检索就可以。
strstr简单匹配算法逻辑如下:
模拟实现:
char* my_strstr(const char* str1, const char* str2)//strstr函数模拟实现,判断字符串是否在另一个字符串中,若在返回第一次出现位置的首地址
{
assert(str1 && str2);
char*s1;
char*s2;
char*cp;
cp = str1;
if (*str2 == '\0')
{
return str1;
}
while (*cp)
{
s1 = cp;
s2 = str2;
while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2)
{
s1++;
s2++;
}
if (*s2 == '\0')
return cp;
cp++;
}
return NULL;
}
除此之外还有strtok
,strerror
等字符串操作函数分别可对字符串进行切分,以及打印报错号,这里不再进行一一介绍。刚兴趣的小伙伴可以自行学习查看。