ZMonster's Blog 巧者劳而智者忧,无能者无所求,饱食而遨游,泛若不系之舟

C语言字符串操作

string.h

其实在C语言的标准库中,也有非常丰富的字符串操作函数。当然了,由于C语言中 字符串 并不是基本数据类型,也没有 这个概念,相对来说操作上可能没有Python/Java之类的语言方便。不过了解一下C语言中的字符串操作还是有意义的。

C语言中的字符串操作函数在 string.h 中,不过要了解都有什么函数,阅读string.h并不是什么好的方式。如果是在Unix/Linux系统下,只要执行:

man string

就可以了解都有哪些字符串操作函数了,如下图所示:

string_manual.png

其中一些常用的函数及其大致功能如下(具体细节后面再细说):

  • 字符串拷贝

    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()有两点区别:

  1. 使用strcasecmp()应该包含 strings.h 而不是 string.h
  2. 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(),不再赘述。