C 风格字符串

我们一直在输出中使用一种语法:

cout << "Hello, world!" << endl;

这里面,我一直没有讲“双引号引起的东西”到底是什么。这一节的内容将解释它的本质。

字符数组

首先从字符数组说起。字符数组,指的就是由若干个字符类型变量构成的数组。即:

char a[5]{};

这里 a 就是一个字符数组。嗯,字符数组就是个数组,这没什么特殊的。比如下面的代码就带空格地输出了 a 这个数组中的元素 H e l l o

#include <iostream>
using namespace std;
int main() {
    char a[]{'H', 'e', 'l', 'l', 'o'}; // 这里省略了大小 5
    for (int i{0}; i < 5; i++) {
        cout << a[i] << " ";
    }
}

现在来看这样神奇的代码:

#include <iostream>
using namespace std;
int main() {
    char a[]{"Hello"}; // 这里
    for (int i{0}; i < 5; i++) {
        cout << a[i] << " ";
    }
}

这里的初始化器里面放上了“双引号引起的一句话”——有点奇怪,但是这样的代码是没问题的。它的运行结果和之前也一样,也是 H e l l o——聪明的你一定能大概猜到这种初始化方法做了什么:用引号引起的字符逐一初始化字符数组内的元素——但真的如此吗?

#include <iostream>
using namespace std;
int main() {
    char a[]{"Hello"};
    cout << sizeof(a) << endl;
}

你会发现这段代码输出的是 6,而不是 5。也就是说 {"Hello"} 这个初始化器初始化了 6 个元素(因为一个 char 占 1 字节,sizeof(a) 为 6 说明有 6 个 char)。明明引号里只有五个字符,为什么会有六个初始化值呢?

答案在于 "Hello" 确实包含六个字符。读者会说,你这是不识数啊。嗯——我们先来看看到底是哪 6 个字符:

元素a[0]a[1]a[2]a[3]a[4]a[5]
'H''e''l''l''o''\0'

问题出在结尾多了一个字符叫做 \0。如果你足够细心,就能发现其实它是转义字符的一种。这个字符叫做空字符(Null character)。空字符是 char 类型的零值。

因为 '\0' 是零值,所以 '\0' 转换为 int 类型得到的是 0,转换为 bool 类型得到的是 false

下面重点来了:由字符数组中连续、相邻的若干个字符,且只有最后一个字符是空字符构成的序列称为 C 风格字符串(C string),这里简称为字符串。请看以下示例:

char a[6]{'H', 'e', 'l', 'l', 'o', '\0'}; // a 是一个字符串,因为它是 \0 结尾的
char b[5]{'H', 'e', 'l', 'l', 'o'};       // b 不是字符串,因为最后一个字符非空
char c[5]{'H', 'i', '\0', '\0', '\0'};    // c 的前三个元素构成字符串
char d[5]{'\0', '\0', '@', '\0', '\0'};   // d[2] 和 d[3] 两个元素构成字符串
char e[2]{'#', '\0'};                     // e 是一个字符串
char f[1]{'$'};                           // f 不是字符串(结尾非空)
char g[1]{'\0'};                          // g 是字符串

字符串字面量

由双引号引起的转义或非转义字符

"转义或非转义字符"

称为字符串字面量(String literal)。这种字面量的值为 const char[N] 类型的数组,其中 N转义或非转义字符 的个数 + 1,称为它的大小。数组中存储的元素分别为字面量中的字符,并追加一个空字符 '\0'(因此字符串字面量是一个典型的 C 风格字符串)。比如:

"foobar"; // 这个字符串字面量完全等价于下面的数组 a:
char a[7]{'f', 'o', 'o', 'b', 'a', 'r', '\0'};

注意字符串字面量的元素是只读的,因为字面量都是常量,在编译期间确定,且运行期间的值不会发生改变。

特别地,字符数组可以用字符串字面量初始化:

char a[7]{"foobar"};

但要求数组的大小不得小于字符串字面量。比如:

char a[6]{"Hello"}; // OK,字符串字面量大小为 6,与数组大小一致
char b[9]{"Hello"}; // OK,数组大小更大
char c[5]{"Hello"}; // 错误,数组大小不够

对于更大的那种情况,它和之前用较少值的列表初始化数组的效果是一致的:

char a[9]{"Hello"}; /* 等价于:
char a[9]{'H', 'e', 'l', 'l', 'o', '\0'}; */

那么剩下的 a[6]a[8] 采用零初始化。刚刚提到过,char 类型的零值正是 '\0',故而零初始化将剩下的元素也都初始化为 '\0'

// 上例中 a 等价于:
char a[9]{'H', 'e', 'l', 'l', 'o', '\0', '\0', '\0', '\0'};

类似地,用字符串字面量初始化字符数组时,可以省略数组大小:

char a[]{"Hello"}; // a 的大小为 6,因为 "Hello" 大小为 6
char b[]{""} // b 的大小为 1,因为 "" 只包含一个空字符,大小为 1

输出字符串

字符串字面量可以直接输出。比如 cout << "Hello, world!"; 这样,我们已经见过很多次了。其实,如果一个数组存放的内容恰为一个字符串,则这个字符串也可以被输出。具体来讲是这样的:

#include <iostream>
using namespace std;
int main() {
    char a[20]{'a', 'b', 'c', 'd', '\0'}; // 这里 '\0' 是可省略的
    cout << a << endl;
    char b[20]{"hello"};
    cout << b << endl;
}

正如预期那样,输出了

abcd
hello

当你将一个字符数组放在 cout << 后面的时候,程序会从头挨个输出这个数组内的元素,直到空字符为止。这就是为什么输出数组 a 的时候,只会将前四个元素输出;因为第五个元素是空字符。

这样输出字符串的时候,空字符并不会被输出。然而,即便你强制输出空字符,你也看不到任何变化——因为空字符是不可见字符,输出之后你一般看不见。(但是这些不可见字符可能会在评测系统中引起麻烦,所以要避免输出它们。)

注意事项

字符数组和普通数组无异,只是多了一些使用方法而已。但是字符数组同样不能被赋值

char a[]{"Hello"};
char b[];
b = a; // 错误
b = "Hello"; // 错误:将数组(字符串字面量)赋值给数组 b

a 是一个字符串时,由于它是空字符结尾的,所以可以这样赋值:

#include <iostream>
using namespace std;
int main() {
    char a[]{"Hello"};
    char b[20]{};      // b 全部初始化为 '\0'
    for (int i{0}; a[i] != '\0'; i++) {
        b[i] = a[i];
    }
    cout << b << endl;
}

但这个方法只能用于字符串;如果字符数组中不含空字符,就会发生意外的错误。略感幸运的是,我们有一些更简便的方法来操作 C 风格字符串,参见稍微靠后的章节

最近更新:
代码未运行