使用字符数组表示内存
从内存的角度了解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) × 元素个数。虚线仅用于示意每个元素的起始位置,实际并无分隔。这仅用于可视化目的。
这与编译器分配数组的方式基本一致。注意,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 字节。