一次又一次地初始化时,指针在循环内做了啥?

Posted

技术标签:

【中文标题】一次又一次地初始化时,指针在循环内做了啥?【英文标题】:What does pointer do inside loop when initialized again and again?一次又一次地初始化时,指针在循环内做了什么? 【发布时间】:2019-09-05 04:26:01 【问题描述】:

我正在使用 GNU ARM 嵌入式工具链处理 STM32 uC。我试图找出在循环内初始化指针时会发生什么。一个非常简单的例子如下(部分是伪代码):

while(1)

    char* msg = "my message";
    transmit_via_uart(msg, strlen(msg));
    delay(1000);

每次再次初始化指针msg时,处理器是否为堆上的字符串分配新空间?还是会覆盖“旧”指针msg 指向的空间(未分配新空间)?

我知道,我可以将初始化行放在 while 循环上方,我只是好奇会发生什么,无法弄清楚。

感谢您的快速答复! T.

编辑:对不起!当然编译器不会分配任何东西...... :)

【问题讨论】:

编译器不在堆上分配任何东西。您的程序在调用malloc 和朋友时会执行此操作。没有malloc 调用,所以没有堆分配。我建议买一本关于 C 的好书。 真正了解发生了什么的唯一方法是编译代码并查看程序集。您得到的答案仅对特定编译器(包括使用的选项)和使用的特定系统有效。例如,您可以使用godbolt.org 进行检查。也就是说,您不太可能看到任何内存分配。 几乎所有现代编译器都会优化对strlen的调用,即这个函数调用将被编译为transmit_via_uart(address_of_string_literal, 10);,如seen here。编写const char* msg = "my message"; 也是一个好主意,以确保您的代码不会尝试改变字符串文字。 【参考方案1】:

在 C 中,所有文字字符串实际上都是只读的字符数组,当然,数组包括空终止符。当您获得指向此类字符串的指针时,您将获得指向其第一个元素的指针,即字符串中的第一个字符。

这个数组的确切存储位置无关紧要,但每个字符串文字通常只有一个副本。

至于变量msg本身,很可能编译器在调用函数时为它分配空间,以及函数内部的所有其他局部变量。在进入循环之前,变量的空间可能未初始化。然后一个好的编译器会对其进行优化,使变量只初始化一次。

【讨论】:

关于“通常只有一个副本”:在 C 的抽象计算机中,必须只有一个副本,因为字符串文字的生命周期是程序的整个执行过程。它是一个对象;每次代码到达它时,都会引用同一个对象。 (从技术上讲,源代码中的字符串字面量是用来初始化一个静态存储时长的数组,被引用的就是那个数组。) Re“很可能是编译器为其分配空间……”:在C的抽象计算机内部,每次循环迭代时肯定会分配空间(生命周期是while的块,而不是功能块)。在 C 的抽象计算机之外,很可能没有为 msg 分配空间;传递给transmit_via_uart 的值可能是动态构建的,使用“加载地址”、“加载/添加立即数”或类似指令的某种形式或组合。【参考方案2】:

除非您明确使用 malloc 系列函数,否则 C 程序永远不会在堆上分配。

字符串文字 "my message" 存储在 ROM 中(可能在大多数系统上称为 .rodata.text 的部分中)。它是在程序启动时分配的。

msg 指针仅指向 ROM 中的该地址。指针本身分配在堆栈或 CPU 寄存器中。

然而,编译器足够聪明,即使你在循环中重复调用它,地址也不会改变。所以它很可能会优化掉变量msg,并简单地将可以找到字符串的原始硬编码ROM地址传递给函数。

您可以将初始化放在循环上方就好了,除非您使用的是石器时代、30 岁的 C90 编译器。


顺便说一句,编写代码的更好方法是:

char msg[] = "my message";
transmit_via_uart(msg, sizeof(msg)-1);

通过这种方式,您可以在编译时计算字符串文字的大小,因为它是常量且已知的。通过使用strlen,您可以强制执行编译器可能不够聪明而无法优化的运行时计算。

【讨论】:

在优化时编译器很可能会删除对strlen 的调用 事实上,gcc 和 clang optimize the call to strlen 即使禁用了优化,而 MSVC 将需要 /O2 使用sizeof 需要将声明更改为const char msg[] = "my message";,否则您将获得指针的大小。对吗? 实际上,C 标准中没有任何内容阻止局部变量在堆上分配。该标准甚至没有提到堆栈或堆。这完全取决于实现。我不知道有什么不同的系统,但你的说法不一定正确。 … (d) C 标准中没有任何内容阻止实现使用堆或其动态分配子系统来自动分配对象,只要它是自动执行的。 (e) 如果实现选择为msg 使用 CPU 寄存器,则通常不被视为“分配”,因为该寄存器通常不为特定对象保留,而只是临时使用,将寄存器的使用与其他人并在不同时间对同一对象使用不同的寄存器……【参考方案3】:

编译器是否在堆上为字符串分配新空间? 指针msg再次初始化的时间?或者它是否覆盖 “旧”指针 msg 指向的空间(未分配新空间)?

没有。 literal 字符串在编译时是已知的,因此编译器能够将其存储在可执行文件的特殊部分(通常是.text)中。当它需要它时,编译器可以简单地使用指向存储文字的那块内存的指针。 Ne 需要自己执行字符的任何副本。

这是您的代码在编译后的样子 (source here),经过完全优化,即 -O3

.LC0:
        .string "my message"
ff():
        sub     rsp, 8
.L2:
        mov     esi, 10
        mov     edi, OFFSET FLAT:.LC0
        call    transmit_via_uart(char const*, int)
        mov     edi, 1000
        call    delay(unsigned int)
        jmp     .L2

每个循环(.L02 部分)唯一被初始化的是指针,该指针使用以下指令获取.LC0 处已知内存块的地址:mov edi, OFFSET FLAT:.LC0

没有内存是动态分配的,如果您考虑一下,既然我们需要的所有信息在编译时都已知道,为什么还要这样做呢?

【讨论】:

对我来说,这看起来不像是 OPs 代码的汇编。您确定您发布的代码正确吗? @DavideSpataro:编译器为什么要优化代码? 您可能应该提到您更改了代码...顺便说一句:delay @DavideSpataro:你可以add an extern,c 编译器在链接器之前运行,所以它并不关心你在做什么。如果该函数是静态且内联的(如您的示例中的那个),那么它当然会完全删除调用,并且如果没有全局副作用则能够将其删除。【参考方案4】:

每次指针msg再次初始化时,处理器是否在堆上为字符串分配新空间?

源代码中的字符串文字表示在整个程序执行过程中存在的字符数组。所以当程序开始执行时,就会为其提供空间。

编译器执行此操作的典型方法是将字符串放入程序的常量数据部分。

或者它是否覆盖了“旧”指针 msg 指向的空间(没有分配新空间)?

在 C 语义中,每次达到msg 的定义时,都会创建一个名为msg 的对象,并将其初始化为指向字符数组。

在实践中,优秀的编译器,尤其是在启用优化时,会认识到这对于实现源代码的最终效果并不是必需的。对于调用transmit_via_uart(msg, strlen(msg));,一个好的编译器会知道msg的值(相对于存储字符串的程序部分)和strlen(msg)的值,它会生成指令将这些值传递给@ 987654326@ 无需为msg 对象使用实际存储空间。

对于编译器来说,可以使含义更加明显:

while(1)

    static const char msg[] = "my message";
    transmit_via_uart(msg, sizeof msg - 1);
    delay(1000);

msg 声明为staticconst 显式告诉编译器msg 是不变数据的永久数组,使用sizeof 告诉编译器该值是对象的固定属性,不是可以在运行时使用strlen 计算的东西(尽管它在技术上仍然是运行时表达式,而不是编译时常量)。未能优化原始代码的低质量编译器可能会使用此代码做得更好。

【讨论】:

以上是关于一次又一次地初始化时,指针在循环内做了啥?的主要内容,如果未能解决你的问题,请参考以下文章

为什么数据会在Firebase数据库的Loop中一次又一次地添加?

Git rebase 一次又一次地回到同一个地方

一次又一次地解析错误[关闭]

Swift,如何通过按下按钮一次又一次地播放声音

Swift:每次重新加载时都会一次又一次地添加表格视图单元格内容?

一次又一次地在新的相同标签中打开一个网址