C语言字符串操作
2014-03-06目录
string.h
其实在C语言的标准库中,也有非常丰富的字符串操作函数。当然了,由于C语言中 字符串 并不是基本数据类型,也没有 类 这个概念,相对来说操作上可能没有Python/Java之类的语言方便。不过了解一下C语言中的字符串操作还是有意义的。
C语言中的字符串操作函数在 string.h 中,不过要了解都有什么函数,阅读string.h并不是什么好的方式。如果是在Unix/Linux系统下,只要执行:
man string
就可以了解都有哪些字符串操作函数了,如下图所示:
其中一些常用的函数及其大致功能如下(具体细节后面再细说):
字符串拷贝
stcpy, strncpy
字符串比较
strcmp, strncmp, strcasecmp, strncasecmp
字符串连接
strcat, strncat
字符查找
strchr, strrchr, strchrnul, strpbrk
建立字符串副本
strdup, strndup, strdupa, strndupa
字符串分割
strsep, strtok, strtok_r
字符串匹配
strstr
下面根据功能的不同来展示各个函数的用法。这里我会用一些实例来进行示范,同时,其结果由org-babel对代码块求值得到。
字符串拷贝(strcpy, strncpy)
strcpy
函数原型:
char *strcpy(char *dest, const char *src);
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char dest[1024] = {0}; char *src = "abcde"; strcpy(dest, src); printf("%s\n", dest); return 0; }
结果:
abcde
strcpy()函数会将源字符串中的结束符('\0')也拷贝到目的字符串中。
注意,strcpy()可能会导致溢出。
strncpy
函数原型:
char *strncpy(char *dest, const char *src, size_t n);
该函数从源字符串中拷贝n个字符到目的字符串;如果源字符串长度不足,则用 NULL 填充,以保证将n个字符写入目的字符串中;如果源字符串中前n个字符不包含字符串结束符,函数不会为目的字符串添加上结束符。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char dest[7] = {0}; char *src = "abcde"; dest[5] = 'A'; strncpy(dest, src, 5); printf("%s\n", dest); return 0; }
结果:
abcdeA
所以如果有需要,应该在拷贝后自己在目的字符串尾部添加结束符。
字符串比较(strcmp, strncmp, strcasecmp, strncasecmp)
strcmp
函数原型:
int strcmp(const char *s1, const char *s2);
如果s1小于s2,函数返回一个负数;如果s1等于s2,函数返回0;否则返回一个正数。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s1 = "abcde"; char *s2 = "abcef"; char *s3 = "ad"; printf("compare(%s, %s) -> %d\n", s1, s2, strcmp(s1, s2)); printf("compare(%s, %s) -> %d\n", s1, s3, strcmp(s1, s3)); printf("compare(%s, %s) -> %d\n", s2, s3, strcmp(s2, s3)); return 0; }
结果:
compare(abcde, abcef) -> -1 compare(abcde, ad) -> -2 compare(abcef, ad) -> -2
从这个结果可以发现,strcmp()是根据字典序来对字符串进行比较的。进一步的,还可以发现strcmp()的返回值是比较过程中最后一次比较时两个字符的值的差,如比较"abcde"和"abcef",有:
a - a = 0 b - b = 0 c - c = 0 d - e = -1 #按字典序,大小已分,不再比较
strncmp
函数原型:
int strncmp(const char *s1, const char *s2, size_t n);
和strcmp()的区别是,strncmp()只对s1和s2的前n个字节进行比较。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s1 = "abcde"; char *s2 = "abcfg"; char *s3 = "abd"; printf("compare(%s, %s, 4) -> %d\n", s1, s2, strncmp(s1, s2, 4)); printf("compare(%s, %s, 2) -> %d\n", s1, s3, strncmp(s1, s3, 2)); printf("compare(%s, %s, 3) -> %d\n", s2, s3, strncmp(s2, s3, 3)); return 0; }
结果:
compare(abcde, abcfg, 4) -> -2 compare(abcde, abd, 2) -> 0 compare(abcfg, abd, 3) -> -1
strcasecmp
函数原型:
int strcasecmp(const char *s1, const char *s2);
strcasecmp()也是用来比较字符串的,和strcmp()有两点区别:
- 使用strcasecmp()应该包含 strings.h 而不是 string.h
- strcasecmp()在比较时不区分大小写
示例:
#include <stdio.h> #include <string.h> #include <strings.h> int main(int argc, char *argv[]) { char *s1 = "AbcdE"; char *s2 = "abcdE"; printf("compare(%s, %s) with case -> %d\n", s1, s2, strcmp(s1, s2)); printf("compare(%s, %s) ignore case -> %d\n", s1, s2, strcasecmp(s1, s2)); return 0; }
结果:
compare(AbcdE, abcdE) with case -> -32 compare(AbcdE, abcdE) ignore case -> 0
strncasecmp
strncasecmp()之于strcasecmp()就如strncmp()之于strcmp(),不再赘述。
字符串连接(strcat, strncat)
strcat
函数原型:
char *strcat(char *dest, const char *src);
strcat()首先会覆盖掉目的字符串的结束符,然后把源字符串的内容追加到后面,并在最后添加结束符。如果目的字符串缓冲区长度不够,将导致溢出。
strcat()在操作完成后,返回目的字符串的首地址,这样可以方便地进行链式操作。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char dest[1024] = "hello "; char *src = "world!"; printf("%s\n", strcat(dest, src)); return 0; }
结果:
hello world!
strncat
函数原型:
char *strncat(char *dest, const char *src, size_t n);
strncat()将最多n个字节的内容追加到目的字符串尾部,并且会在追加后添加终止符号。
同strcat()一样,它返回目的字符串的首地址。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char dest[1024] = "hello "; char *src = "world!lkjsdljsd"; printf("%s\n", strncat(dest, src, 6)); return 0; }
结果:
hello world!
字符查找(strchr, strrchr, strchrnul, strpbrk)
strchr
函数原型:
char *strchr(const char *s, int c);
strchr()返回一个字符指针,指向指定字符在指定字符串中第一次出现的位置。如果在指定字符串中没有找到指定字符,则返回 NULL 。该函数的第二个参数按理来说应当是一个字符,不过标准库中确实是int类型。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "hello world!"; char c = 'l'; printf("%s\n", strchr(s, c)); return 0; }
结果:
llo world!
strrchr
函数原型:
char *strrchr(const char *s, int c);
strrchr()和strchr()类似,但它返回的是指定字符在指定字符串中最后一次出现的位置。如果未找到,同样返回 NULL 。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "hello world!"; char c = 'l'; printf("%s\n", strrchr(s, c)); return 0; }
结果:
ld!
strchrnul
函数原型:
char *strchrnul(const char *s, int c);
strchrnul()的功能和strchr()只有细微的区别,那就是,当没有找到指定字符时,strchrnul()不返回 NULL ,而是返回字符串结束符的位置。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "abcde"; char c = 'm'; printf("%p, %p\n", s, strchrnul(s, c)); return 0; }
结果
0x40065c, 0x400661
这里由于strchrnul()的特性,没办法通过打印字符串来了解strchrnul()的操作,不过观察这两个指针的值,会发现:
0x400661 - 0x40065c = 0x5
而字符串s的第六个元素(从0开始,5即第六个),正好是结束符。
strpbrk
函数原型:
char *strpbrk(const char *s, const char *accept);
strpbrk()和strchr()的区别在于,strchr()是从字符串里搜索 一个字符 ,而strpbrk()则是在字符串里搜索 一个字符集中的字符 ,看第二个参数就明白了。strpbrk()遍历字符串,如果发现某个字符在指定的 字符集 中,则立即返回指向该字符的指针。如果最后没有找到任何在指定字符集中的字符,则返回 NULL 。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "Hello World!"; char *accept = "Wo"; printf("%s\n", strpbrk(s, accept)); return 0; }
结果:
o World!
字符串分割(strtok, strtok_r, strsep)
strtok
函数原型:
char *strtok(char *str, const char *delim);
strtok()根据第二个参数指定的分隔符(可能存在多个不同的分隔符)将指定字符串分割成多个子串。通过多次调用strtok(),可以依次获得字符串的多个子串的首地址。要注意的是,除了第一次调用时将待分割字符串作为第一个参数,后续的调用要将第一个参数置为 NULL 。当字符串已经无法再分割时,strtok()返回 NULL 。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char s[1024] = "abc;lsdk:lskdj,;slsj"; char *delm = ";:,"; char *result = NULL; int len = strlen(s); int i = 0; result = strtok(s, delm); while (result != NULL) { printf("Source:%s, Sub:%s\n", s, result); result = strtok(NULL, delm); } return 0; }
结果:
Source:abc Sub:abc Source:abc Sub:lsdk Source:abc Sub:lskdj Source:abc Sub:slsj
除了上面说过的strtok()的用法外,还要注意的是,作为待分割的字符串,它必须是 可更改的 。否则虽然可以通过编译,但运行会出错。要理解这个现象,首先要了解strtok()的内部机制。
了解其机制,没必要去寻找其实现源代码,只要对它的操作过程进行剖析就知道了。先看下面的代码:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char s[64] = "To be or not to be"; char *delm = " ,."; /* 分隔符:空格 */ char *result = NULL; int i = 0, len = strlen(s); for (i = 0; i < len; ++i) { /* 逐个打印s中的字符 */ printf("%c ", s[i]); } printf("\n"); for (i = 0; i < len; ++i) { /* 逐个打印s中字符的数值 */ printf("%d ", (int)s[i]); } printf("\n"); result = strtok(s, delm); while (result != NULL) { /* 观察s中字符数值的变化 */ for (i = 0; i < len; ++i) { printf("%d ", (int)s[i]); } printf("\n"); result = strtok(NULL, delm); } return 0; }
结果:
T | o | b | e | o | r | n | o | t | t | o | b | e | |||||
84 | 111 | 32 | 98 | 101 | 32 | 111 | 114 | 32 | 110 | 111 | 116 | 32 | 116 | 111 | 32 | 98 | 101 |
84 | 111 | 0 | 98 | 101 | 32 | 111 | 114 | 32 | 110 | 111 | 116 | 32 | 116 | 111 | 32 | 98 | 101 |
84 | 111 | 0 | 98 | 101 | 0 | 111 | 114 | 32 | 110 | 111 | 116 | 32 | 116 | 111 | 32 | 98 | 101 |
84 | 111 | 0 | 98 | 101 | 0 | 111 | 114 | 0 | 110 | 111 | 116 | 32 | 116 | 111 | 32 | 98 | 101 |
84 | 111 | 0 | 98 | 101 | 0 | 111 | 114 | 0 | 110 | 111 | 116 | 0 | 116 | 111 | 32 | 98 | 101 |
84 | 111 | 0 | 98 | 101 | 0 | 111 | 114 | 0 | 110 | 111 | 116 | 0 | 116 | 111 | 0 | 98 | 101 |
84 | 111 | 0 | 98 | 101 | 0 | 111 | 114 | 0 | 110 | 111 | 116 | 0 | 116 | 111 | 0 | 98 | 101 |
可以看到,s中的分隔符,逐次地被置为'\0'即字符串结束符。这就是strtok()分割字符串的内部原理了。而strtok()返回的指针,其实就是s中各个子串的起始位置了。如果s指向的内容是无法被修改的,那么strtok()自然也就无法将原先的分隔符置为字符结束符了。
当然了,由于源字符串会被修改,在实际中,如果需要,可以用strdup()来建立一个源字符串的副本。
strtok_r
函数原型:
char *strtok_r(char *str, const char *delim, char **saveptr);
strtok_r()是Linux下的strtok()的可重入版本(线程安全版本),它比strtok()多了一个参数 saveptr ,这个参数用于在分割字符串时保存上下文。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char s[64] = "Hello world"; char *delm = " "; char *result = NULL, *ptr = NULL; printf("Source:%p\n", s); result = strtok_r(s, delm, &ptr); while (result != NULL) { printf("Result:%p\t", result); printf("Saveptr:%p\n", ptr); printf("---%s\t", result); printf("---%s\n", ptr); result = strtok_r(NULL, delm, &ptr); } return 0; }
结果:
Source:0x7fff180f3de0 Result:0x7fff180f3de0 Saveptr:0x7fff180f3de6 ---Hello ---world Result:0x7fff180f3de6 Saveptr:0x7fff180f3deb ---world ---
可以看到,saveptr这个指针在每次调用strtok_r()后就指向了未分割的部分的首地址。相对地,strtok()则是在内部有一个静态缓冲区,通过这个静态缓冲区来记录未处理的起始位置,所以strtok()不是线程安全的。
strsep
函数原型:
char *strsep(char **stringp, const char *delim);
strsep()同样是字符串分割函数,它和strtok()的不同之处在于,它会直接修改待分割的指针的值,让它始终指向未处理部分的起始位置。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char s[64] = "To be or not to be"; char *source = s; char *delm = " "; char *result = NULL; while (source != NULL) { printf("Source:%s | ", source); result = strsep(&source, delm); printf("result:%s\n", result); } return 0; }
结果
Source:To be or not to be | result:To Source:be or not to be | result:be Source:or not to be | result: or Source:not to be | result: not Source:to be | result: to Source:be | result: be
因为和strtok()的这个不同之处,strsep不需要区分第一次调用后后续的连续调用,可以用统一的操作来对字符串进行分割。
字符串匹配(strstr)
函数原型:
char *strstr(const char *haystack, const char *needle);
strstr()返回字符串needle在字符串haystack中第一次出现的位置;如果没有匹配,则返回 NULL 。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "To be or not to be."; char *p = "be"; printf("%s\n", strstr(s, p)); return 0; }
结果:
be or not to be.
字符串副本创建(strdup, strndup, strdupa, strndupa)
strdup
函数原型:
char *strdup(const char *s);
strdup()调用malloc()分配一块内存并将字符串s的内容拷贝进去,产生s的副本。要注意的是,在最后应该调用free()来释放副本。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "abcde"; char *dup = strdup(s); printf("%s\n", dup); free(dup); return 0; }
结果:
abcde
strndup
函数原型:
char *strndup(const char *s, size_t n);
strndup()和strdup()类似,但最多只拷贝s的前n个字节。如果s的长度大于n,还会在副本后添加终止符。
示例:
#include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { char *s = "abcde"; char *dup = strndup(s, 4); printf("%s\n", dup); free(dup); return 0; }
结果:
abcd
strdupa
函数原型:
char *strdupa(const char *s);
strdupa()和strdup()类似,但在分配内存时,它使用alloca()而不是malloc()。
strndupa
函数原型:
char *strndupa(const char *s, size_t n);
strndupa()之于strdupa()就如strndup()之于strdup(),不再赘述。