Logo
Section 1 / 7

使用字符数组表示内存

从内存的角度了解C语言中的字符串。

导入

使用字符数组表示内存

从内存的角度了解C语言中的字符串。

引言

在 C 语言中,我们使用 char 数组来表示字符串。因此,我们在“指针与数组”一章中讨论的所有内容都适用于本章。

然而,字符数组有一些特定的特性,接下来我们将进行讨论。

在本章中,我们将把字符数组称为字符串。

使用字符数组的第一个代码示例

让我们使用 ARRAY_SIZE 宏来处理一个字符数组,并尝试推断其元素数量。

此外,我们还将打印字符串所占的字节数。

1#include <stdio.h> 2 3#define ARRAY_SIZE(arr) (sizeof(arr) / sizeof(arr[0])) 4 5int main() 6{ 7 char str[32]; 8 printf("sizeof(str) = %u\n", sizeof(str)); 9 printf("ARRAY_SIZE(str) = %u\n", ARRAY_SIZE(str)); 10 11 return 0; 12}

输出结果如下:

1sizeof(str) = 32 2ARRAY_SIZE(str) = 32
  • sizeof:数组大小为 32 字节。字符串有 32 个元素,每个元素是占用 1 字节的 char 类型。1 × 32 = 32 字节。
  • ARRAY_SIZE:数组中有 32 个元素,因为每个元素占 1 字节。

之前我们注意到整数数组的总大小和元素数量之间存在差异,这是因为 int 类型占 4 字节。而 char 类型占 1 字节,因此总大小与元素数量相等。

结论:对于字符数组,sizeof 和 ARRAY_SIZE 是等价的。在本章中,我们将使用 sizeof 来确定 char 数组的元素数量。再次强调,对于其他数据类型,必须使用 ARRAY_SIZE。

内存布局图示

假设我们有如下字符串声明:

1char str[4];

我们可以像初始化其他数组一样进行初始化:

1char str[4] = {'a', 'b', 'c'};

以下是该字符串的内存布局图。

注意:字符串在内存中并非分段存储,而是作为一个连续块分配,大小为 sizeof(char) × 元素个数。虚线仅用于示意每个元素的起始位置,实际并无分隔。这仅用于可视化目的。

...
str 的内存布局

这与编译器分配数组的方式基本一致。注意,char str[4] 声明了一个包含四个元素(索引 0、1、2、3)的 char 类型数组 str。然后我们按顺序将 'a'、'b'、'c' 填入前三个元素。

令人惊讶的是,在图示中,最后一个元素被设置为 '\0',尽管我们的初始化列表中并未显式指定它({'a', 'b', 'c'})。

注意:每个字符数组都必须以字符串结束符 '\0' 结尾。它也被称为空终止符或空字符。

如果数组长度大于初始化列表中的元素个数,多出的元素会被自动零初始化,相当于在末尾补上了 '\0';但如果初始化列表恰好填满数组,就不会自动补 '\0'(见后文示例)。

以下两种初始化方式是等价的:

1char str1[4] = {'a', 'b', 'c'}; // 编译器自动添加 `\0` 2char str2[4] = {'a', 'b', 'c', '\0'};

我们可以通过以下代码看出它们的等价性:对于 str1,我们没有手动添加 '\0'(由编译器零初始化补上);对于 str2,我们手动添加了。但两者打印结果完全相同。

1#include <stdio.h> 2 3int main() 4{ 5 char str1[4] = {'a', 'b', 'c'}; // 编译器自动添加 `\0` 6 char str2[4] = {'a', 'b', 'c', '\0'}; 7 8 printf("str1 = [%s]\n", str1); 9 printf("str2 = [%s]\n", str2); 10 11 return 0; 12}

注意:打印字符串时,始终用 [] 包裹字符串。这样可以更容易发现可能存在的空格或其他不可见字符。

为什么需要 '\0'?

如前所述,数组名会退化为指向其首元素的指针。在上面的代码中,str1 和 str2 都是指向字符串第一个字符的指针。

但我们还需要知道字符串的结束位置。例如,当我们调用 printf 时,数组会退化为指针,同时丢失其大小信息。此时 %s 格式符无法知道字符串何时结束,从而无法停止打印内容。

C 语言规定所有字符串必须以 '\0' 结尾,以此避免额外传递长度参数。我们从起始地址开始打印,直到遇到 '\0' 为止。它本质上是一个标记,告诉我们字符串的结束位置。

这种类型的字符串称为 C 风格字符串。

注意:这意味着字符串的实际大小必须比我们要存储的数据多 1 字节(用于存放 '\0')。

也有其他约定,将长度信息与字符串数据一起存储。

如果移除 '\0' 会发生什么?

如果移除了 '\0',我们将无法确定字符串的长度,这会导致未定义行为,并开始打印随机内存数据。

为了说明这一点,考虑 strlen 函数。它返回字符串的长度,即从字符串开头到 '\0' 之间的字符数量。为此,它会持续计数直到遇到 '\0'。

1#include <stdio.h> 2#include <string.h> 3 4int main() 5{ 6 char str1[4] = {'a', 'b', 'c'}, str2[4] = {'a', 'b', 'c', 'd'}; 7 8 printf("strlen(str1) = %d\n", strlen(str1)); 9 printf("strlen(str2) = %d\n", strlen(str2)); 10 11 return 0; 12}

输出结果如下:

1strlen(str1) = 3 2strlen(str2) = 7

strlen 对 str1 的处理是正常的。它返回 3,即 "abc" 的长度。我们设置了 'a'、'b'、'c',最后一个元素由编译器自动设为 '\0'。

但 strlen 对 str2 的处理不正确。我们试图存储 "abcd",长度为 4。然而,str2 只能容纳 4 个字符(3 个有效字符 + 1 个空终止符)。当我们写入 4 个字符时,没有为 '\0' 留出空间,而 strlen 依赖于 '\0' 来计算长度,因此结果错误地显示为 7。

记住:务必确保字符串有足够的空间容纳数据和空终止符 '\0'。永远不要覆盖空终止符。

strlen 与 sizeof 的区别

我们必须区分 strlen 和 sizeof:

  • strlen 返回字符串中实际设置的字符数量。
  • ARRAY_SIZE 或 sizeof 返回字符串的总大小,其中部分元素可能为空。

例如,对于以下声明:

1char str[10] = {'a', 'b', 'c'};

strlen 函数返回 3,即 "abc" 的长度和已占用的字符数。
而 sizeof 或 ARRAY_SIZE 返回 10,即数组的总大小。

...
字符串未完全填充的示例

我们也可以通过以下代码清楚地看到两者的区别:

1#include <stdio.h> 2#include <string.h> 3 4int main() 5{ 6 char str[10] = {'a', 'b', 'c'}; 7 8 printf("strlen(str) = %d\n", strlen(str)); 9 printf("sizeof(str) = %d\n", sizeof(str)); 10 11 return 0; 12}

初始化的快捷方式

到目前为止,我们以逐个字符的方式初始化字符串,这种方式较为繁琐。C 语言提供了一种更简便的写法。

以下两种声明是等价的:

1char str[10] = {'a', 'b', 'c'}; 2char str[10] = "abc";

我们还可以省略数组大小,让编译器自动推断。如下代码中的 str2:

1#include <stdio.h> 2 3int main() 4{ 5 char str1[10] = "abc"; 6 char str2[] = "abc"; 7 8 printf("sizeof(str1) = %d | sizeof(str2) = %d\n", sizeof(str1), sizeof(str2)); 9 10 return 0; 11}

输出结果如下:

1sizeof(str1) = 10 | sizeof(str2) = 4

str1 的大小是我们指定的 10。
编译器自动将 str2 的大小设为足以容纳 "abc" 和空终止符的大小。"abc" 的长度为 3,加上 1 个空字符,总共为 4 字节。

使用链表练习所有内容

指向常量字符串的指针