# 字符串

网道（WangDoc.com），互联网文档计划

## 简介 #

C 语言没有单独的字符串类型，字符串被当作字符数组，即`char`类型的数组。比如，字符串“Hello”是当作数组`{'H', 'e', 'l', 'l', 'o'}`处理的。

编译器会给数组分配一段连续内存，所有字符储存在相邻的内存单元之中。在字符串结尾，C 语言会自动添加一个全是二进制`0`的字节，写作`\0`字符，表示字符串结束。字符`\0`不同于字符`0`，前者的 ASCII 码是0（二进制形式`00000000`），后者的 ASCII 码是48（二进制形式`00110000`）。所以，字符串“Hello”实际储存的数组是`{'H', 'e', 'l', 'l', 'o', '\0'}`。

所有字符串的最后一个字符，都是`\0`。这样做的好处是，C 语言不需要知道字符串的长度，就可以读取内存里面的字符串，只要发现有一个字符是`\0`，那么就知道字符串结束了。

    char localString[10];

上面示例声明了一个10个成员的字符数组，可以当作字符串。由于必须留一个位置给`\0`，所以最多只能容纳9个字符的字符串。

字符串写成数组的形式，是非常麻烦的。C 语言提供了一种简写法，双引号之中的字符，会被自动视为字符数组。

    {'H', 'e', 'l', 'l', 'o', '\0'}

    // 等价于
    "Hello"

上面两种字符串的写法是等价的，内部存储方式都是一样的。双引号里面的字符串，不用自己添加结尾字符`\0`，C 语言会自动添加。

注意，双引号里面是字符串，单引号里面是字符，两者不能互换。如果把`Hello`放在单引号里面，编译器会报错。

    // 报错
    'Hello'

另一方面，即使双引号里面只有一个字符（比如`"a"`），也依然被处理成字符串（存储为2个字节），而不是字符`'a'`（存储为1个字节）。

如果字符串内部包含双引号，则该双引号需要使用反斜杠转义。

    "She replied, \"It does.\""

反斜杠还可以表示其他特殊字符，比如换行符（`\n`）、制表符（`\t`）等。

    "Hello, world!\n"

如果字符串过长，可以在需要折行的地方，使用反斜杠（`\`）结尾，将一行拆成多行。

    "hello \
    world"

上面示例中，第一行尾部的反斜杠，将字符串拆成两行。

上面这种写法有一个缺点，就是第二行必须顶格书写，如果想包含缩进，那么缩进也会被计入字符串。为了解决这个问题，C 语言允许合并多个字符串字面量，只要这些字符串之间没有间隔，或者只有空格，C 语言会将它们自动合并。

    char greeting[50] = "Hello, ""how are you ""today!";
    // 等同于
    char greeting[50] = "Hello, how are you today!";

这种新写法支持多行字符串的合并。

    char greeting[50] = "Hello, "
      "how are you "
      "today!";

`printf()`使用占位符`%s`输出字符串。

    printf("%s\n", "hello world")

## 字符串变量的声明 #

字符串变量可以声明成一个字符数组，也可以声明成一个指针，指向字符数组。

    // 写法一
    char s[14] = "Hello, world!";

    // 写法二
    char* s = "Hello, world!";

上面两种写法都声明了一个字符串变量`s`。如果采用第一种写法，由于字符数组的长度可以让编译器自动计算，所以声明时可以省略字符数组的长度。

    char s[] = "Hello, world!";

上面示例中，编译器会将数组`s`的长度指定为14，正好容纳后面的字符串。

字符数组的长度，可以大于字符串的实际长度。

    char s[50] = "hello";

上面示例中，字符数组`s`的长度是`50`，但是字符串“hello”的实际长度只有6（包含结尾符号`\0`），所以后面空出来的44个位置，都会被初始化为`\0`。

字符数组的长度，不能小于字符串的实际长度。

    char s[5] = "hello";

上面示例中，字符串数组`s`的长度是`5`，小于字符串“hello”的实际长度6，这时编译器会报错。因为如果只将前5个字符写入，而省略最后的结尾符号`\0`，这很可能导致后面的字符串相关代码出错。

字符指针和字符数组，这两种声明字符串变量的写法基本是等价的，但是有两个差异。

第一个差异是，指针指向的字符串，在 C 语言内部被当作常量，不能修改字符串本身。

    char* s = "Hello, world!";
    s[0] = 'z'; // 错误

上面代码使用指针，声明了一个字符串变量，然后修改了字符串的第一个字符。这种写法是错的，会导致难以预测的后果，执行时很可能会报错。

如果使用数组声明字符串变量，就没有这个问题，可以修改数组的任意成员。

    char s[] = "Hello, world!";
    s[0] = 'z';

为什么字符串声明为指针时不能修改，声明为数组时就可以修改？原因是系统会将字符串的字面量保存在内存的常量区，这个区是不允许用户修改的。声明为指针时，指针变量存储的值是一个指向常量区的内存地址，因此用户不能通过这个地址去修改常量区。但是，声明为数组时，编译器会给数组单独分配一段内存，字符串字面量会被编译器解释成字符数组，逐个字符写入这段新分配的内存之中，而这段新内存是允许修改的。

为了提醒用户，字符串声明为指针后不得修改，可以在声明时使用`const`说明符，保证该字符串是只读的。

    const char* s = "Hello, world!";

上面字符串声明为指针时，使用了`const`说明符，就保证了该字符串无法修改。一旦修改，编译器肯定会报错。

第二个差异是，指针变量可以指向其它字符串。

    char* s = "hello";
    s = "world";

上面示例中，字符指针可以指向另一个字符串。

但是，字符数组变量不能指向另一个字符串。

    char s[] = "hello";
    s = "world"; // 报错

上面示例中，字符数组的数组名，总是指向初始化时的字符串地址，不能修改。

同样的原因，声明字符数组后，不能直接用字符串赋值。

    char s[10];
    s = "abc"; // 错误

上面示例中，不能直接把字符串赋值给字符数组变量，会报错。原因是字符数组的变量名，跟所指向的数组是绑定的，不能指向另一个地址。

为什么数组变量不能赋值为另一个数组？原因是数组变量所在的地址无法改变，或者说，编译器一旦为数组变量分配地址后，这个地址就绑定这个数组变量了，这种绑定关系是不变的。C 语言也因此规定，数组变量是一个不可修改的左值，即不能用赋值运算符为它重新赋值。

想要重新赋值，必须使用 C 语言原生提供的`strcpy()`函数，通过字符串拷贝完成赋值。这样做以后，数组变量的地址还是不变的，即`strcpy()`只是在原地址写入新的字符串，而不是让数组变量指向新的地址。

    char s[10];
    strcpy(s, "abc");

上面示例中，`strcpy()`函数把字符串`abc`拷贝给变量`s`，这个函数的详细用法会在后面介绍。

## strlen() #

`strlen()`函数返回字符串的字节长度，不包括末尾的空字符`\0`。该函数的原型如下。

    // string.h
    size_t strlen(const char* s);

它的参数是字符串变量，返回的是`size_t`类型的无符号整数，除非是极长的字符串，一般情况下当作`int`类型处理即可。下面是一个用法实例。

    char* str = "hello";
    int len = strlen(str); // 5

`strlen()`的原型在标准库的`string.h`文件中定义，使用时需要加载头文件`string.h`。

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

    int main(void) {
      char* s = "Hello, world!";
      printf("The string is %zd characters long.\n", strlen(s));
    }

注意，字符串长度（`strlen()`）与字符串变量长度（`sizeof()`），是两个不同的概念。

    char s[50] = "hello";
    printf("%d\n", strlen(s));  // 5
    printf("%d\n", sizeof(s));  // 50

上面示例中，字符串长度是5，字符串变量长度是50。

如果不使用这个函数，可以通过判断字符串末尾的`\0`，自己计算字符串长度。

    int my_strlen(char *s) {
      int count = 0;
      while (s[count] != '\0')
        count++;
      return count;
    }

## strcpy() #

字符串的复制，不能使用赋值运算符，直接将一个字符串赋值给字符数组变量。

    char str1[10];
    char str2[10];

    str1 = "abc"; // 报错
    str2 = str1;  // 报错

上面两种字符串的复制写法，都是错的。因为数组的变量名是一个固定的地址，不能修改，使其指向另一个地址。

如果是字符指针，赋值运算符（`=`）只是将一个指针的地址复制给另一个指针，而不是复制字符串。

    char* s1;
    char* s2;

    s1 = "abc";
    s2 = s1;

上面代码可以运行，结果是两个指针变量`s1`和`s2`指向同一字符串，而不是将字符串`s1`的内容复制给`s2`。

C 语言提供了`strcpy()`函数，用于将一个字符串的内容复制到另一个字符串，相当于字符串赋值。该函数的原型定义在`string.h`头文件里面。

    strcpy(char dest[], const char source[])

`strcpy()`接受两个参数，第一个参数是目的字符串数组，第二个参数是源字符串数组。复制字符串之前，必须要保证第一个参数的长度不小于第二个参数，否则虽然不会报错，但会溢出第一个字符串变量的边界，发生难以预料的结果。第二个参数的`const`说明符，表示这个函数不会修改第二个字符串。

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

    int main(void) {
      char s[] = "Hello, world!";
      char t[100];

      strcpy(t, s);

      t[0] = 'z';
      printf("%s\n", s);  // "Hello, world!"
      printf("%s\n", t);  // "zello, world!"
    }

上面示例将变量`s`的值，拷贝一份放到变量`t`，变成两个不同的字符串，修改一个不会影响到另一个。另外，变量`t`的长度大于`s`，复制后多余的位置（结束标志`\0`后面的位置）都为随机值。

`strcpy()`也可以用于字符数组的赋值。

    char str[10];
    strcpy(str, "abcd");

上面示例将字符数组变量，赋值为字符串“abcd”。

`strcpy()`的返回值是一个字符串指针（即`char*`），指向第一个参数。

    char* s1 = "beast";
    char s2[40] = "Be the best that you can be.";
    char* ps;

    ps = strcpy(s2 + 7, s1);

    puts(s2); // Be the beast
    puts(ps); // beast

上面示例中，从`s2`的第7个位置开始拷贝字符串`beast`，前面的位置不变。这导致`s2`后面的内容都被截去了，因为会连`beast`结尾的空字符一起拷贝。`strcpy()`返回的是一个指针，指向拷贝开始的位置。

`strcpy()`返回值的另一个用途，是连续为多个字符数组赋值。

    strcpy(str1, strcpy(str2, "abcd"));

上面示例调用两次`strcpy()`，完成两个字符串变量的赋值。

另外，`strcpy()`的第一个参数最好是一个已经声明的数组，而不是声明后没有进行初始化的字符指针。

    char* str;
    strcpy(str, "hello world"); // 错误

上面的代码是有问题的。`strcpy()`将字符串分配给指针变量`str`，但是`str`并没有进行初始化，指向的是一个随机的位置，因此字符串可能被复制到任意地方。

如果不用`strcpy()`，自己实现字符串的拷贝，可以用下面的代码。

    char* strcpy(char* dest, const char* source) {
      char* ptr = dest;
      while (*dest++ = *source++);
      return ptr;
    }

    int main(void) {
      char str[25];
      strcpy(str, "hello world");
      printf("%s\n", str);
      return 0;
    }

上面代码中，关键的一行是`while (*dest++ = *source++)`，这是一个循环，依次将`source`的每个字符赋值给`dest`，然后移向下一个位置，直到遇到`\0`，循环判断条件不再为真，从而跳出循环。其中，`*dest++`这个表达式等同于`*(dest++)`，即先返回`dest`这个地址，再进行自增运算移向下一个位置，而`*dest`可以对当前位置赋值。

`strcpy()`函数有安全风险，因为它并不检查目标字符串的长度，是否足够容纳源字符串的副本，可能导致写入溢出。如果不能保证不会发生溢出，建议使用`strncpy()`函数代替。

## strncpy() #

`strncpy()`跟`strcpy()`的用法完全一样，只是多了第3个参数，用来指定复制的最大字符数，防止溢出目标字符串变量的边界。

    char* strncpy(
      char* dest,
      char* src,
      size_t n
    );

上面原型中，第三个参数`n`定义了复制的最大字符数。如果达到最大字符数以后，源字符串仍然没有复制完，就会停止复制，这时目的字符串结尾将没有终止符`\0`，这一点务必注意。如果源字符串的字符数小于`n`，则`strncpy()`的行为与`strcpy()`完全一致。

    strncpy(str1, str2, sizeof(str1) - 1);
    str1[sizeof(str1) - 1] = '\0';

上面示例中，字符串`str2`复制给`str1`，但是复制长度最多为`str1`的长度减去1，`str1`剩下的最后一位用于写入字符串的结尾标志`\0`。这是因为`strncpy()`不会自己添加`\0`，如果复制的字符串片段不包含结尾标志，就需要手动添加。

`strncpy()`也可以用来拷贝部分字符串。

    char s1[40];
    char s2[12] = "hello world";

    strncpy(s1, s2, 5);
    s1[5] = '\0';

    printf("%s\n", s1); // hello

上面示例中，指定只拷贝前5个字符。

## strcat() #

`strcat()`函数用于连接字符串。它接受两个字符串作为参数，把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串，但是第二个字符串不变。

该函数的原型定义在`string.h`头文件里面。

    char* strcat(char* s1, const char* s2);

`strcat()`的返回值是一个字符串指针，指向第一个参数。

    char s1[12] = "hello";
    char s2[6] = "world";

    strcat(s1, s2);
    puts(s1); // "helloworld"

上面示例中，调用`strcat()`以后，可以看到字符串`s1`的值变了。

注意，`strcat()`的第一个参数的长度，必须足以容纳添加第二个参数字符串。否则，拼接后的字符串会溢出第一个字符串的边界，写入相邻的内存单元，这是很危险的，建议使用下面的`strncat()`代替。

## strncat() #

`strncat()`用于连接两个字符串，用法与`strcat()`完全一致，只是增加了第三个参数，指定最大添加的字符数。在添加过程中，一旦达到指定的字符数，或者在源字符串中遇到空字符`\0`，就不再添加了。它的原型定义在`string.h`头文件里面。

    char* strncat(
      const char* dest,
      const char* src,
      size_t n
    );

`strncat()`返回第一个参数，即目标字符串指针。

为了保证连接后的字符串，不超过目标字符串的长度，`strncat()`通常会写成下面这样。

    strncat(
      str1,
      str2,
      sizeof(str1) - strlen(str1) - 1
    );

`strncat()`总是会在拼接结果的结尾，自动添加空字符`\0`，所以第三个参数的最大值，应该是`str1`的变量长度减去`str1`的字符串长度，再减去`1`。下面是一个用法实例。

    char s1[10] = "Monday";
    char s2[8] = "Tuesday";

    strncat(s1, s2, 3);
    puts(s1); // "MondayTue"

上面示例中，`s1`的变量长度是10，字符长度是6，两者相减后再减去1，得到`3`，表明`s1`最多可以再添加三个字符，所以得到的结果是`MondayTue`。

## strcmp() #

如果要比较两个字符串，无法直接比较，只能一个个字符进行比较，C 语言提供了`strcmp()`函数。

`strcmp()`函数用于比较两个字符串的内容。该函数的原型如下，定义在`string.h`头文件里面。

    int strcmp(const char* s1, const char* s2);

按照字典顺序，如果两个字符串相同，返回值为`0`；如果`s1`小于`s2`，`strcmp()`返回值小于0；如果`s1`大于`s2`，返回值大于0。

下面是一个用法示例。

    // s1 = Happy New Year
    // s2 = Happy New Year
    // s3 = Happy Holidays

    strcmp(s1, s2) // 0
    strcmp(s1, s3) // 大于 0
    strcmp(s3, s1) // 小于 0

注意，`strcmp()`只用来比较字符串，不用来比较字符。因为字符就是小整数，直接用相等运算符（`==`）就能比较。所以，不要把字符类型（`char`）的值，放入`strcmp()`当作参数。

## strncmp() #

由于`strcmp()`比较的是整个字符串，C 语言又提供了`strncmp()`函数，只比较到指定的位置。

该函数增加了第三个参数，指定了比较的字符数。它的原型定义在`string.h`头文件里面。

    int strncmp(
      const char* s1,
      const char* s2,
      size_t n
    );

它的返回值与`strcmp()`一样。如果两个字符串相同，返回值为`0`；如果`s1`小于`s2`，`strcmp()`返回值小于0；如果`s1`大于`s2`，返回值大于0。

下面是一个例子。

    char s1[12] = "hello world";
    char s2[12] = "hello C";

    if (strncmp(s1, s2, 5) == 0) {
      printf("They all have hello.\n");
    }

上面示例只比较两个字符串的前5个字符。

## sprintf()，snprintf() #

`sprintf()`函数跟`printf()`类似，但是用于将数据写入字符串，而不是输出到显示器。该函数的原型定义在`stdio.h`头文件里面。

    int sprintf(char* s, const char* format, ...);

`sprintf()`的第一个参数是字符串指针变量，其余参数和`printf()`相同，即第二个参数是格式字符串，后面的参数是待写入的变量列表。

    char first[6] = "hello";
    char last[6] = "world";
    char s[40];

    sprintf(s, "%s %s", first, last);

    printf("%s\n", s); // hello world

上面示例中，`sprintf()`将输出内容组合成“hello world”，然后放入了变量`s`。

`sprintf()`的返回值是写入变量的字符数量（不计入尾部的空字符`\0`）。如果遇到错误，返回负值。

`sprintf()`有严重的安全风险，如果写入的字符串过长，超过了目标字符串的长度，`sprintf()`依然会将其写入，导致发生溢出。为了控制写入的字符串的长度，C 语言又提供了另一个函数`snprintf()`。

`snprintf()`只比`sprintf()`多了一个参数`n`，用来控制写入变量的字符串不超过`n - 1`个字符，剩下一个位置写入空字符`\0`。下面是它的原型。

    int snprintf(char*s, size_t n, const char* format, ...);

`snprintf()`总是会自动写入字符串结尾的空字符。如果你尝试写入的字符数超过指定的最大字符数，`snprintf()`会写入 n - 1 个字符，留出最后一个位置写入空字符。

下面是一个例子。

    snprintf(s, 12, "%s %s", "hello", "world");

上面的例子中，`snprintf()`的第二个参数是12，表示写入字符串的最大长度不超过12（包括尾部的空字符）。

`snprintf()`的返回值是写入格式字符串的字符数量（不计入尾部的空字符`\0`）。如果`n`足够大，返回值应该小于`n`，但是有时候格式字符串的长度可能大于`n`，那么这时返回值会大于`n`，但实际上真正写入变量的还是`n-1`个字符。如果遇到错误，返回一个负值。因此，返回值只有在非负并且小于`n`时，才能确认完整的格式字符串写入了变量。

## 字符串数组 #

如果一个数组的每个成员都是一个字符串，需要通过二维的字符数组实现。每个字符串本身是一个字符数组，多个字符串再组成一个数组。

    char weekdays[7][10] = {
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    };

上面示例就是一个字符串数组，一共包含7个字符串，所以第一维的长度是7。其中，最长的字符串的长度是10（含结尾的终止符`\0`），所以第二维的长度统一设为10。

因为第一维的长度，编译器可以自动计算，所以可以省略。

    char weekdays[][10] = {
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    };

上面示例中，二维数组第一维的长度，可以由编译器根据后面的赋值，自动计算，所以可以不写。

数组的第二维，长度统一定为10，有点浪费空间，因为大多数成员的长度都小于10。解决方法就是把数组的第二维，从字符数组改成字符指针。

    char* weekdays[] = {
      "Monday",
      "Tuesday",
      "Wednesday",
      "Thursday",
      "Friday",
      "Saturday",
      "Sunday"
    };

上面的字符串数组，其实是一个一维数组，成员就是7个字符指针，每个指针指向一个字符串（字符数组）。

遍历字符串数组的写法如下。

    for (int i = 0; i < 7; i++) {
      printf("%s\n", weekdays[i]);
    }

本教程采用知识共享 署名-相同方式共享 3.0协议。

分享本文