将字符串字面值分配给字符数组,字符串字面值如何复制到堆栈上?

5

我明白当你使用char array[] = "string"时,字符串常量"string"会从数据段复制到堆栈。这个字符串常量是逐个字符地复制吗?还是编译器获取字符串常量的起始和结束地址并一次性将整个字符串复制到堆栈中?

谢谢。


那是一个实现细节;检查生成的机器代码以获取答案。 - John Bode
这完全取决于编译器想做什么。 - Bo Persson
一个快速在线检查gcc、clang和icc的方法是使用http://gcc.godbolt.org/,它们似乎都有不同的功能。 - Shafik Yaghmour
有比这个更值得担心的事情。 - user93353
4个回答

8
编译器可以任意执行操作,只要观察到的结果相同。有时根本就没有复制。
C标准没有规定如何进行复制,因此C实现可以采用任何方法来实现结果。 C标准强制执行的唯一要求是可观察结果(例如写入标准输出的文本)必须如定义的那样。
当工程师设计高质量的C实现时,他们将花费一些时间考虑在这种情况下复制字符串的最佳方法,并且他们将设法设计出一个编译器,在每种情况下选择最佳方法。使用“移动立即值”指令可以在原地构建短字符串。长字符串可能通过调用memcpy进行复制。中间字符串可以通过内联调用memcpy进行复制,有效地移动若干字节的少量指令。
当工程师设计便宜的C实现时,即只需完成工作以便代码可以移植到机器上但不需要速度时,他们将执行最简单的操作。
有时编译器根本不会复制该字符串:如果编译器可以确定您不需要复制,则没有理由进行复制。例如,如果编译器看到您仅向printf传递字符串并且根本不修改它,则编译器可以通过将原始字符串传递给printf而不进行复制来获得相同的结果。

2
...或者它可能会将未使用的数组,包括初始化,全部优化出代码。+1 感谢您花时间撰写这个答案。 - autistic

1

没有理由认为有副本存在。

以以下代码为例。

int main() {
  char c[] = "hi";
}

对我来说,这将生成(未优化的)汇编代码:
main:
    pushq   %rbp
    movq    %rsp, %rbp
    movw    $26984, -16(%rbp)
    movb    $0, -14(%rbp)
    movl    $0, %eax
    popq    %rbp
    ret

数组的内存通过将其设置为值26984进行初始化。这个值恰好由两个字节0x68和0x69表示,它们是“h”和“i”的ASCII值。没有任何字符串的数据段表示,并且该数组不是通过逐个字符复制任何内容或通过任何其他聪明的复制方法来初始化的。
当然,这只是一个编译器的实现(g++ 4.8),其他编译器可以做任何他们想做的事情,只要符合语言规范。

1

这取决于编译器和目标架构。

可能存在非常简单的目标架构,例如微控制器,它们没有支持复制内存块的指令。可能存在专门用于教学的非常简单的编译器,即使在支持更有效方法的架构上也会生成逐字节复制。

然而,在这种情况下,您可以假设生产级别的编译器会执行合理的操作,并为大多数流行的架构生成最快的代码,因此您不需要担心它。

不过,检查的最佳方法是阅读编译器生成的汇编代码。

以此测试代码(stack_array_init.c)为例:

#include <stdio.h>

int
main()
{
    char a[]="Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed\n"
             "do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n";

    printf("%s", a);

    return 0;
}

使用gcc编译成汇编代码,并进行大小优化(以减少读取量),如下所示:

gcc -Os -S stack_array_init.c

这是x86-64的输出:
        .file   "stack_array_init.c"
        .section        .rodata.str1.1,"aMS",@progbits,1
.LC1:
        .string "%s"
.LC0:
        .string "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed\ndo eiusmod tempor incididunt ut labore et dolore magna aliqua.\n"
        .section        .text.startup,"ax",@progbits
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        subq    $136, %rsp
        .cfi_def_cfa_offset 144
        movl    $.LC0, %esi
        movl    $126, %ecx
        leaq    2(%rsp), %rdi
        xorl    %eax, %eax
        rep movsb
        leaq    2(%rsp), %rsi
        movl    $.LC1, %edi
        call    printf
        xorl    %eax, %eax
        addq    $136, %rsp
        .cfi_def_cfa_offset 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (Debian 4.7.2-5) 4.7.2"
        .section        .note.GNU-stack,"",@progbits

这里,“rep movsb”是将字符串复制到堆栈的指令。

这是来自ARMv4汇编的摘录(可能更容易阅读):

main:
    @ Function supports interworking.
    @ args = 0, pretend = 0, frame = 128
    @ frame_needed = 0, uses_anonymous_args = 0
    str lr, [sp, #-4]!
    sub sp, sp, #132
    mov r2, #126
    ldr r1, .L2
    mov r0, sp
    bl  memcpy
    mov r1, sp
    ldr r0, .L2+4
    bl  printf
    mov r0, #0
    add sp, sp, #132
    ldr lr, [sp], #4
    bx  lr
.L3:
    .align  2
.L2:
    .word   .LC0
    .word   .LC1
    .size   main, .-main
    .section    .rodata.str1.4,"aMS",%progbits,1
    .align  2
.LC1:
    .ascii  "%s\000"
    .space  1
.LC0:
    .ascii  "Lorem ipsum dolor sit amet, consectetur adipisicing"
    .ascii  " elit, sed\012do eiusmod tempor incididunt ut labor"
    .ascii  "e et dolore magna aliqua.\012\000"
    .ident  "GCC: (Debian 4.6.3-14) 4.6.3"
    .section    .note.GNU-stack,"",%progbits

据我对ARM汇编的理解,这段代码似乎是调用memcpy将字符串复制到堆栈数组中。虽然这里没有展示memcpy的汇编代码,但我预计它会使用其中一种最快的方法。

0

我不确定你在“逐个字符”和“整个字符串”复制方法之间的区别上的意思。字符串通常不是机器级实体,这意味着它不可能被复制为“整个字符串”。你希望如何实现这一点?

字符串始终会被“逐个字符”复制,至少在概念上是这样。现在,当涉及到复制扩展内存区域时,编译器可以通过尽可能执行按字(而不是按字节)复制来优化复制过程。类似的优化可能会在处理器微架构层面上实现。

但无论如何,在一般情况下,复制都是作为一个“迭代”过程实现的,而不是作为“整个字符串”的某个原子操作。

此外,聪明的编译器可能会意识到,在某些情况下根本不需要复制。例如,如果您的代码不修改array对象并且不依赖于其地址标识,则编译器可能会简单地决定直接使用原始字符串文字,而不进行任何复制(即基本上将您的char array [] =“string”静默替换为const char *array =“string”)。


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