字符串初始值设定项会不会有点浪费内存?

Will a string initializer somewhat waste memory?

要初始化一个char数组,通常我这样写:

char string[] = "some text";

但是今天,我的一位同学说应该使用:

char string[] = {'s', 'o', 'm', 'e', ' ', 't', 'e', 'x', 't', '[=11=]'};

我告诉他放弃可读性和简洁性是疯狂的,但他认为用字符串初始化 char 数组实际上会创建两个字符串,一个在堆栈中,另一个在只读内存中。在使用嵌入式设备时,这会导致不可接受的内存浪费。

当然,字符串初始值设定项看起来更清晰,所以我将在我的程序中使用它们。但问题是,字符串初始化程序会创建两个相同的字符串吗?或者字符串初始值设定项只是语法糖?

经过您的编辑,这两个定义之间没有区别。两者都会产生一个十个字符的数组并初始化为相同的内容。

这实际上很容易验证:首先检查 sizeof 为您提供的两个数组,然后您可以使用例如memcmp 比较两个数组。

第二个初始化几乎等于第一个,有一个关键的区别:第二个数组没有作为字符串终止。

第一个创建一个包含十个字符(包括终止符)的数组,第二个创建一个包含九个字符的数组。如果您不将数组用作字符串,那么是的,您将在第二次初始化时保存一次元素。

will a string initializer create two same string? Or string initializers are just syntax sugars?

两种情况完全不同:

第一种情况:

char string[] = "some text";  // <-- string initialization

此语法特定于字符串,不能应用于任何其他数据类型。它会自动在末尾添加一个 [=12=] 字符,因此可以保证 printf 等库函数知道在哪里结束输出(使用 %s 控制字符串)。


第二种情况:

char string[] = {'s', 'o', 'm', 'e', ' ', 't', 'e', 'x', 't'};  // <--  array initialization

此语法用于初始化 array 但不是 string。该语法可用于初始化其他类型的数组(如intlong等)。它永远不会在数组末尾自动添加 [=12=] 。所以使用 %s 控件 printf 这个字符数组是错误的。


简而言之,这是用于不同目的的两种不同的初始化语法。如果你需要字符串,那么使用第一种语法,如果你使用字符数组——那么使用第二种。

C 标准有一个 "special case" 允许您使用字符串文字初始化数组:

§6.7.9/14 An array of character type may be initialized by a character string literal or UTF−8 string literal, optionally enclosed in braces. Successive bytes of the string literal (including the terminating null character if there is room or if the array is of unknown size) initialize the elements of the array.

就是这样。它没有说任何其他内容,这将是平台和编译器的实现细节。与明确给出字符串文字静态存储持续时间的 C++ 不同,C 标准 不会 。这是隐含的。有一些常见的扩展允许你修改字符串文字,这意味着它不能保证它会被放在只读内存中。

char string[] = "some text";

100% 等同于

char string[] = {'s', 'o', 'm', 'e', ' ', 't', 'e', 'x', 't', '[=11=]'};

你的朋友很困惑:如果 string 是一个局部变量,那么在 both 的情况下你创建了两个字符串。驻留在堆栈上的变量 string 和驻留在只读内存 (.rodata) 中的只读字符串文字。

没有办法避免只读存储,因为所有数据都必须分配到某个地方。您不能凭空分配字符串文字。您所能做的就是将它从一个只读内存段移动到另一个内存段,这最终会给您带来完全相同的程序大小。

一般首选前一种风格,因为它更具可读性。它确实是一种语法糖。

但它也是首选,因为它可能会简化一些称为 "string pooling" 的编译器优化,这允许编译器以更节省内存的方式存储字符串文字 "some text"。如果逐个字符地初始化字符串,编译可能会或可能不会意识到它是一个只读字符串常量。

在语义上,这两行是相同的。但实际结果将取决于编译器。

试验 http://gcc.godbolt.org/ 显示了多种策略:

  • 使用一系列带有立即操作数的 movb 指令(或等效指令)一次一个字符填充数组。

  • 使用 movabsq / movq 对在数组中一次填充一个双字,其中第一个有一个立即双字操作数。

  • 将数据从存储在 .rodata 部分的字符串常量复制到数组中。

不同的编译器对这两种情况使用不同的策略。特别是,gcc 仅针对 char string[] = "string literal"; 的情况发现了 movabsq 优化,这使得您朋友的策略有点笨重(因为生成的代码有更多字节)。

尝试不同的优化设置可能会产生更多变化。

很明显,基础数据必须存储在程序中某处,无论是在数据部分还是作为可执行代码中的一系列直接操作数。由于弄清楚或猜测特定风格如何影响给定编译器的优化能力是不切实际的,唯一合理的方法是使用最容易阅读和维护的风格。 (有用的推论是,编译器可能也最容易使用最常见的样式。)

万一这实际上对性能至关重要,您将不得不检查所使用的实际编译器生成的代码。但是你应该首先问问自己是否真的需要一个初始化的可变缓冲区。

  • 在第一种情况下,string[] 使用长度为 10 字节的 文字字符串常量 初始化,它将在读取中实例化 -只有细分。

  • 在第二种情况下 string[] 使用长度为 10 字节的 文字字符常量 的常量数组进行初始化,它将在只读段。

这两种情况在语义和内存要求上都是相同的。第一个只是第二个的语法糖(更方便,更不容易出错)。

如果您需要使用编译时常量数据初始化非只读数据,无论使用何种语法,都必须编译常量初始化程序。你不能不劳而获。但是,如果数据是常量,则可以通过声明使用单个只读副本:

const char* string = "some text" ;

这将创建一个指向 常量字符串 的指针 string,并且与 say:

相比可以节省内存
#define string "some text"

这可能会在使用宏 string 的任何地方生成多个 "some text" 的副本。 (尽管大多数现代 compiler/linker 工具链在任何情况下都能够删除重复的字符串)。在第一种情况下,您可以获取 string 的地址,并确保所有引用的值都相同,而每个未优化的引用的宏都不同。另一个语义差异是,对于 const char* stringsizeof(string) 是指针的大小,而对于 string[],它是初始化程序的长度(包括 nul 终止符)