字符串文字在内存中的存储(C++)

3

我读到字符串字面值总是存储在只读内存中,这很有道理。

然而,如果我使用一个字符串字面值来初始化字符数组,它仍然会将字符串字面值存储在只读内存中,然后将其复制到字符数组的内存位置。

我的问题是,在这种情况下,为什么要在第一次存储字符串字面值时存储在只读内存中,而不是直接存储在字符数组的内存位置中。


1
为什么?你需要一个字符串来初始化字符数组的来源。字符数组可能是函数中的局部变量,每次调用函数时都必须对其进行初始化。 - harper
1
除了之前的评论,我还要指出的是,唯一的方法是在运行时初始化一个字符串,并且将字符不存储在只读内存中,而是存储在编译时未知的内存位置(即大多数位置,尤其是使用-fPIC选项),这样做的唯一方式就是将字符串存储在指令的“立即”(payload)值中。但实际上,这与将其存储在只读内存中几乎没有太大区别... 事实上,除了一些低级技术细节(指令缓存,数据缓存)之外,它几乎没有任何区别。 - Andrej Podzimek
1
我认为这并不完全准确。从C++的角度来看,并没有特殊的只读内存存在。字符串字面量的类型是char const[N],其中N是一个正整数。编译器可以按照自己的意愿处理它。C++所要表达的就是,“不要对这个对象进行写操作”。 - bitmask
1
我读到过字符串字面量总是存储在只读内存中,但实际上并没有这样的要求,这是编译器作者的选择。有些情况下,字符串字面量可以被覆盖,因为它的初始值在运行时并没有被使用,就像这个全局可变变量:char foo[10] = "FooFoo" - Marek R
1
我读到过字符串字面量总是存储在只读内存中,但实际上并没有这样的要求,这是编译器作者的选择。有些情况下,字符串字面量是可以被覆盖的,因为它的初始值在运行时并没有被使用,就像这个全局可变变量:char foo[10] = "FooFoo" - undefined
显示剩余6条评论
1个回答

2
我读到字符串字面量总是存储在只读内存中,这是有道理的。
字符串字面量的存储位置是由实现定义的。如果编译器决定发出一个大的字符串字面量,它通常会位于静态内存的只读部分,比如.rodata。
然而,是否有必要这样做取决于编译器。编译器可以根据“as-if规则”对你的代码进行优化,所以如果程序的行为与字面量存储在其他地方或根本不存储时相同,也是允许的。
示例1
int sum() {
    char arr[] = "ab";
    return arr[0] + arr[1];
}

使用以下汇编输出:
sum():
     mov eax, 195
     ret

在这种情况下,因为所有内容都是编译时常量,根本没有字符串字面量或数组。编译器对其进行了优化,并将我们的代码转换为return 195;,通过求和两个ASCII字符ab得到结果。

示例2

void consume(const char*);

void short_string() {
    char arr[] = "short str";
    consume(arr);
}

short_string():
        sub     rsp, 24
        movabs  rax, 8391086215229565043
        mov     qword ptr [rsp + 8], rax
        mov     word ptr [rsp + 16], 114
        lea     rdi, [rsp + 8]
        call    consume(char const*)@PLT
        add     rsp, 24
        ret

再次强调,没有生成任何代码来将字符串保存在只读内存中,但也没有完全进行优化。编译器看到字符串short str非常短,因此将其ASCII字节视为数字8391086215229565043,并直接将其内存mov到堆栈上。使用指向堆栈内存的指针调用consume()函数。

示例3

void long_string() {
    char arr[] = "Lorem ipsum dolor [...] est laborum.";
    consume(arr);
}

long_string():
        push    rbx
        sub     rsp, 448
        lea     rsi, [rip + .L__const.long_string().arr]
        mov     rbx, rsp
        mov     edx, 446
        mov     rdi, rbx
        call    memcpy@PLT
        mov     rdi, rbx
        call    consume(char const*)@PLT
        add     rsp, 448
        pop     rbx
        ret
.L__const.long_string().arr:
        .asciz  "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

我们的字符串现在太长了,不能再被视为一个数字或两个数字。整个字符串现在将被写入静态内存中,很可能是链接后的.rodata段。它仍然有用,因为我们可以使用memcpy将其从静态内存复制到堆栈上初始化arr。
结论
如果你担心编译器在这里做了一些浪费的事情,不要担心。现代编译器非常擅长优化和决定哪些符号放在哪里,如果它们发出一个字符串字面量,通常是因为它必须存在于其他代码中才能工作,或者因为它使数组的初始化更容易。

查看{{link1:使用Compiler Explorer的实时示例}}


你能解释一下它是如何简化数组的初始化过程的吗? - Humble Penguin
2
在Example 3中,数组必须存在于堆栈上,因为在调用consume(arr)时我们要获取它的地址。由于数组存在于静态内存中,我们通过sub rsp, 448将堆栈增加448字节,然后调用memcpy将448个字符串字面量字节复制到堆栈上。如果没有这个静态内存中的字符串字面量,我们将不得不生成大量代码将其放在那里,而不仅仅是调用memcpy函数。 - Jan Schultke
我很感激你的回答。现在更清楚了。谢谢! - Humble Penguin
另一个值得一提的案例是static char long_string[] = "...";或者全局变量:它们可以像静态存储中的任何其他非常量数组一样存在于可读写的.data段中。此外,这与源代码中的字符串字面量无关。我们可以通过int foo[] = {1,2,3,4,...,99};来达到相同的效果,并且编译器可以选择从.rodata段进行memcpy,或者在数组需要存在于堆栈上时存储立即数,否则有其他选项。(@HumblePenguin) - Peter Cordes
“String literals: Where do they go?”(https://stackoverflow.com/q/2589949)看起来更像是我正在寻找的那种问题的更好的重复。 - undefined
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接