我知道当你执行char array[] =“string”时,字符串字面量“string”会从数据段复制到堆栈。字符串是一个字符接一个字符地复制吗?或者编译器获取字符串字面量的开始和结束地址,并将整个字符串一次复制到堆栈?谢谢
70gysomp1#
编译器做任何它“想”做的事情,只要观察到的结果是相同的。有时候根本没有副本。C标准没有指定如何进行复制,因此C实现可以通过任何方式自由地实现结果。C标准强加的唯一要求是可观察的结果(例如写入标准输出的文本)必须是定义的。当工程师设计一个高质量的C实现时,他们会花一些时间考虑在这种情况下复制字符串的最佳方法,他们会寻求设计一个在每种情况下选择最佳方法的编译器。一个短字符串可以通过使用“移动立即值”指令来构建。长字符串可以通过调用memcpy来复制。一个中间字符串可以通过一个对memcpy的内联调用来复制,实际上是几个指令,每个指令移动几个字节。当工程师设计一个廉价的C实现时,一些只是完成工作的东西,这样代码就可以移植到机器上,但不需要太快,他们会做任何对他们来说最简单的事情。有时编译器根本不会复制字符串:如果编译器可以告诉你不需要一个副本,那么就没有理由复制一个副本。例如,如果编译器发现您只是将字符串传递给printf,而根本没有修改它,那么编译器将通过将原始字符串传递给printf来获得相同的结果,而不进行复制。
memcpy
printf
zpqajqem2#
完全没有理由认为有复制品。以下面的代码为例。
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'的ascurry值。字符串根本没有数据段表示,数组也不是通过一个字符接一个字符地复制任何东西来初始化的,或者通过任何其他聪明的复制方法来初始化。当然,这只是一个编译器的实现(g++ 4.8),其他编译器可以做任何他们想做的事情,只要符合语言规范。
iugsix8n3#
这取决于编译器和目标体系结构。可能有非常简单的目标架构,如微控制器,它没有支持复制内存块的指令。可能存在为教学设计的非常简单的编译器,即使在支持更有效方法的体系结构上也会生成逐字节复制。但是,您可以假设生产级编译器会做合理的事情,并在这种情况下为大多数流行的体系结构生成最快的代码,并且您真的不需要担心它。不过,最好的检查方法是读取编译器生成的程序集。以下面的测试代码(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
这里,“repmovsb”是将字符串复制到堆栈的指令。下面是一个ARMv 4程序集的摘录(可能更容易阅读):
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的程序集,但我希望它使用可用的最快方法之一。
13z8s7eq4#
我不太清楚你所说的“逐字符”和“整个字符串”复制方法之间的区别是什么意思。字符串通常不是机器级实体,这意味着它不可能被复制为“整个字符串”。你觉得这怎么可能发生?字符串将始终被“逐个字符”复制,至少在概念上是这样。现在,当涉及到复制扩展内存区域时,编译器可以通过尽可能执行逐字(而不是逐字节)复制来优化复制过程。类似的优化可以在处理器微体系结构级实现。但无论如何,在一般情况下,复制是作为一个 * 迭代 * 过程实现的,而不是作为对“整个字符串”的一些原子操作。最重要的是,一个聪明的编译器可能会意识到,在某些情况下,复制是没有必要的。例如,如果您的代码不修改array对象,并且不依赖于其地址标识,则编译器可能会直接决定使用原始字符串字面量,而根本不进行任何复制(即基本上是悄悄地用const char *array = "string"替换您的char array[] = "string")
array
const char *array = "string"
char array[] = "string"
4条答案
按热度按时间70gysomp1#
编译器做任何它“想”做的事情,只要观察到的结果是相同的。有时候根本没有副本。
C标准没有指定如何进行复制,因此C实现可以通过任何方式自由地实现结果。C标准强加的唯一要求是可观察的结果(例如写入标准输出的文本)必须是定义的。
当工程师设计一个高质量的C实现时,他们会花一些时间考虑在这种情况下复制字符串的最佳方法,他们会寻求设计一个在每种情况下选择最佳方法的编译器。一个短字符串可以通过使用“移动立即值”指令来构建。长字符串可以通过调用
memcpy
来复制。一个中间字符串可以通过一个对memcpy
的内联调用来复制,实际上是几个指令,每个指令移动几个字节。当工程师设计一个廉价的C实现时,一些只是完成工作的东西,这样代码就可以移植到机器上,但不需要太快,他们会做任何对他们来说最简单的事情。
有时编译器根本不会复制字符串:如果编译器可以告诉你不需要一个副本,那么就没有理由复制一个副本。例如,如果编译器发现您只是将字符串传递给
printf
,而根本没有修改它,那么编译器将通过将原始字符串传递给printf
来获得相同的结果,而不进行复制。zpqajqem2#
完全没有理由认为有复制品。
以下面的代码为例。
对我来说,这会产生(未优化的)装配:
数组的内存通过将其设置为值26984来初始化。这个值恰好由两个字节0x68和0x69表示,它们是'h'和'i'的ascurry值。字符串根本没有数据段表示,数组也不是通过一个字符接一个字符地复制任何东西来初始化的,或者通过任何其他聪明的复制方法来初始化。
当然,这只是一个编译器的实现(g++ 4.8),其他编译器可以做任何他们想做的事情,只要符合语言规范。
iugsix8n3#
这取决于编译器和目标体系结构。
可能有非常简单的目标架构,如微控制器,它没有支持复制内存块的指令。可能存在为教学设计的非常简单的编译器,即使在支持更有效方法的体系结构上也会生成逐字节复制。
但是,您可以假设生产级编译器会做合理的事情,并在这种情况下为大多数流行的体系结构生成最快的代码,并且您真的不需要担心它。
不过,最好的检查方法是读取编译器生成的程序集。
以下面的测试代码(stack_array_init. c)为例:
并使用gcc将其编译为汇编,并优化大小(以减少读取量),如下所示:
以下是x86-64的输出:
这里,“repmovsb”是将字符串复制到堆栈的指令。
下面是一个ARMv 4程序集的摘录(可能更容易阅读):
根据我对ARM汇编的理解,这看起来像是代码调用memcpy将字符串复制到堆栈数组中。虽然这并没有显示memcpy的程序集,但我希望它使用可用的最快方法之一。
13z8s7eq4#
我不太清楚你所说的“逐字符”和“整个字符串”复制方法之间的区别是什么意思。字符串通常不是机器级实体,这意味着它不可能被复制为“整个字符串”。你觉得这怎么可能发生?
字符串将始终被“逐个字符”复制,至少在概念上是这样。现在,当涉及到复制扩展内存区域时,编译器可以通过尽可能执行逐字(而不是逐字节)复制来优化复制过程。类似的优化可以在处理器微体系结构级实现。
但无论如何,在一般情况下,复制是作为一个 * 迭代 * 过程实现的,而不是作为对“整个字符串”的一些原子操作。
最重要的是,一个聪明的编译器可能会意识到,在某些情况下,复制是没有必要的。例如,如果您的代码不修改
array
对象,并且不依赖于其地址标识,则编译器可能会直接决定使用原始字符串字面量,而根本不进行任何复制(即基本上是悄悄地用const char *array = "string"
替换您的char array[] = "string"
)