C语言字符串函数用法与实现总结
本文介绍了C语言中常用的字符串处理函数及其实现原理。主要包括:1)字符串拷贝函数strcpy/strncpy及其安全实现;2)字符串拼接函数strcat/strncat;3)字符串比较函数strcmp;4)子串查找函数strstr;5)字符串分割函数strtok;6)错误信息函数strerror。文章详细阐述了各函数的工作原理,并提供了自定义实现代码,特别强调了防止越界访问的安全性问题。通过分析函
字符串是数据的重要组成部分,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;
}
返回错误信息,表示没有这个文件。

更多推荐

所有评论(0)