当前位置: 首页 > 知识库问答 >
问题:

从编译器的角度来看,数组的引用是如何处理的,为什么不允许按值传递(而不是衰减)?

慕河
2023-03-14

我们知道,在C语言中,我们可以将数组的引用作为参数传递,比如f(int)(

然后我写了一个演示,希望从汇编语言中看到一些东西:

void foo_p(int*arr) {}
void foo_r(int(&arr)[3]) {}
template<int length>
void foo_t(int(&arr)[length]) {}
int main(int argc, char** argv)
{
    int arr[] = {1, 2, 3};
    foo_p(arr);
    foo_r(arr);
    foo_t(arr);
   return 0;
}

最初,我猜它仍然会衰减到指针,但是会通过寄存器隐式传递长度,然后在函数体中变回数组。但是汇编代码告诉我这不是真的

void foo_t<3>(int (&) [3]):
  push rbp #4.31
  mov rbp, rsp #4.31
  sub rsp, 16 #4.31
  mov QWORD PTR [-16+rbp], rdi #4.31
  leave #4.32
  ret #4.32

foo_p(int*):
  push rbp #1.21
  mov rbp, rsp #1.21
  sub rsp, 16 #1.21
  mov QWORD PTR [-16+rbp], rdi #1.21
  leave #1.22
  ret #1.22

foo_r(int (&) [3]):
  push rbp #2.26
  mov rbp, rsp #2.26
  sub rsp, 16 #2.26
  mov QWORD PTR [-16+rbp], rdi #2.26
  leave #2.27
  ret #2.27

main:
  push rbp #6.1
  mov rbp, rsp #6.1
  sub rsp, 32 #6.1
  mov DWORD PTR [-16+rbp], edi #6.1
  mov QWORD PTR [-8+rbp], rsi #6.1
  lea rax, QWORD PTR [-32+rbp] #7.15
  mov DWORD PTR [rax], 1 #7.15
  lea rax, QWORD PTR [-32+rbp] #7.15
  add rax, 4 #7.15
  mov DWORD PTR [rax], 2 #7.15
  lea rax, QWORD PTR [-32+rbp] #7.15
  add rax, 8 #7.15
  mov DWORD PTR [rax], 3 #7.15
  lea rax, QWORD PTR [-32+rbp] #8.5
  mov rdi, rax #8.5
  call foo_p(int*) #8.5
  lea rax, QWORD PTR [-32+rbp] #9.5
  mov rdi, rax #9.5
  call foo_r(int (&) [3]) #9.5
  lea rax, QWORD PTR [-32+rbp] #10.5
  mov rdi, rax #10.5
  call void foo_t<3>(int (&) [3]) #10.5
  mov eax, 0 #11.11
  leave #11.11
  ret #11.11

现场演示

我承认我不熟悉汇编语言,但很明显,这三个函数的汇编代码是相同的!所以,在汇编程序编码之前一定会发生一些事情。总之,与数组不同,指针对长度一无所知,对吗?

  1. 编译器在这里是如何工作的
  2. 既然标准允许通过引用传递数组,这是否意味着实现起来很简单?如果是这样,为什么不允许按值传递

对于Q2,我的猜测是前C和C代码的复杂性。毕竟,函数参数中的int[]等于int*一直是一个传统。也许一百年后,它会被弃用


共有3个答案

钮瀚
2023-03-14

关于:

我承认我不熟悉汇编语言,但很明显,这三个函数的汇编代码是一样的!

汇编代码可能是相同的,也可能是不同的——这取决于单个的C实现(以及您调用它们的选项)。只要保持可观察的行为(这是仔细定义的),C标准有一个总体的假设规则来允许任何生成的机器代码

在你的问题中,不同的语法只是源代码级别和翻译过程中的语法和一些语义差异。它们中的每一个在标准中都有不同的定义——例如函数参数的确切类型会不同(如果你要使用像提升::type_index

韦安怡
2023-03-14

这都是关于向后兼容性的。C从C获得数组,C从B语言获得数组。在B中,数组变量实际上是指针。丹尼斯·里奇写过这篇文章。

数组参数衰减为指针帮助Ken Thompson在将UNIX迁移到C时重用了他以前的B源代码

后来,人们认为这可能不是最好的决定,但却认为改变C语言为时已晚。因此,数组的衰减被保留,但后来添加的结构是按值传递的。

结构的引入也为您真正想要按值传递数组的情况提供了一种变通方法:

为什么在C中声明一个只包含数组的结构?

傅胡媚
2023-03-14

甚至C99intfoo(intarr[static 3])在asm中仍然只是一个指针。static语法向编译器保证,即使C抽象机不访问某些元素,它也可以安全地读取所有3个元素,因此,例如,它可以使用无分支的cmov作为if

调用方不会在寄存器中传递长度,因为它是编译时常量,因此在运行时不需要。

可以按值传递数组,但前提是数组位于结构或联合内部。在这种情况下,不同的呼叫约定有不同的规则。根据AMD64 ABI,数组是什么样的C11数据类型。

你几乎不想通过值传递数组,所以C没有语法是有道理的,C也从来没有发明过语法。通过常量引用(即const int*arr)传递要高效得多;只有一个指针arg。

我将您的代码放在Godbolt编译器资源管理器上,使用gcc-O3-fno内联函数进行编译-fno内联函数调用一次-fno内联小函数来阻止它内联函数调用。这消除了-O0调试构建和帧指针样板文件中的所有噪音。(我只是在手册页上搜索了内联,并禁用了内联选项,直到得到我想要的。)

您可以在函数定义上使用GNU C\uuuu attribute\uuu((noinline))来禁用特定函数的内联,即使它们是静态的。

我还添加了对没有定义的函数的调用,因此编译器需要在内存中具有正确值的arr[],并在其中两个函数的arr[4]中添加了一个存储。这让我们可以测试编译器是否警告超出数组边界。

__attribute__((noinline, noclone)) 
void foo_p(int*arr) {(void)arr;}
void foo_r(int(&arr)[3]) {arr[4] = 41;}

template<int length>
void foo_t(int(&arr)[length]) {arr[4] = 42;}

void usearg(int*); // stop main from optimizing away arr[] if foo_... inline

int main()
{
    int arr[] = {1, 2, 3};
    foo_p(arr);
    foo_r(arr);
    foo_t(arr);
    usearg(arr);
   return 0;
}

gcc7。3-O3-Wall-Wextra不带函数内联,在Godbolt上:由于我关闭了代码中未使用的args警告,我们得到的唯一警告是来自模板,而不是来自foo_r

<source>: In function 'int main()':
<source>:14:10: warning: array subscript is above array bounds [-Warray-bounds]
     foo_t(arr);
     ~~~~~^~~~~

asm输出为:

void foo_t<3>(int (&) [3]) [clone .isra.0]:
    mov     DWORD PTR [rdi], 42       # *ISRA.3_4(D),
    ret
foo_p(int*):
    rep ret
foo_r(int (&) [3]):
    mov     DWORD PTR [rdi+16], 41    # *arr_2(D),
    ret

main:
    sub     rsp, 24             # reserve space for the array and align the stack for calls
    movabs  rax, 8589934593     # this is 0x200000001: the first 2 elems
    lea     rdi, [rsp+4]
    mov     QWORD PTR [rsp+4], rax    # MEM[(int *)&arr],  first 2 elements
    mov     DWORD PTR [rsp+12], 3     # MEM[(int *)&arr + 8B],  3rd element as an imm32
    call    foo_r(int (&) [3])
    lea     rdi, [rsp+20]
    call    void foo_t<3>(int (&) [3]) [clone .isra.0]    #
    lea     rdi, [rsp+4]      # tmp97,
    call    usearg(int*)     #
    xor     eax, eax  #
    add     rsp, 24   #,
    ret

foo_p()的调用仍然得到了优化,可能是因为它没有做任何事情。(我没有禁用过程间优化,甚至noinlinenoclone属性也没有阻止这一点。)添加*arr=0对函数体的结果是从main调用函数体(在rdi中传递一个指针,就像其他2个一样)。

注意去映射函数名上的clone. ras.0注释:gcc定义了一个函数,该函数的指针指向arr[4],而不是基本元素。这就是为什么有一个lea rdi,[rsp 20]来设置arg,以及为什么存储区使用[rdi]来定义没有位移的点。__attribute__((noclone))会阻止它。

这种过程间优化非常简单,在这种情况下可以节省1字节的代码大小(在克隆的寻址模式下,只需disp8),但在其他情况下也很有用。调用者需要知道这是函数修改版本的定义,比如void foo_clone(int*p){*p=42;} ,这就是为什么它需要将其编码为损坏的符号名称。

如果您在一个文件中实例化了模板,并从另一个看不到定义的文件中调用它,那么如果没有链接时间优化,gcc将不得不调用常规名称,并像编写的函数那样传递指向数组的指针。

IDK为什么gcc这样做是为了模板,而不是参考。这可能与它警告模板版本有关,但不是参考版本。或者可能与推导模板有关?

顺便说一句,让main使用mov-rdi,rsp而不是lea-rdi,[rsp 4]的IPO实际上会让它运行得稍微快一点。i、 e.采取

但这只对像main这样的调用者有帮助,他们已经在rsp之上分配了4个字节的数组,我认为gcc只寻找使函数本身更高效的首次公开募股,而不是在一个特定调用者中的调用序列。

 类似资料:
  • 我是新来的Spring。我试图以函数的方式为SpringWebFlux制作一个示例应用程序。为什么我们的处理函数不能传递通量。是否有任何方法可以让路由器函数接受它,因为据说路由器函数接受服务器响应的子类型。 显示处理程序代码 这里,我正在通过单声道 路由器功能代码 所以有没有办法做到这一点,我已经阅读了不同的文章。我只能找到Mono,但如果我使用基于注释的webflux,我可以传递flux。

  • 本文向大家介绍什么是PHP中的按引用传递和按值传递?,包括了什么是PHP中的按引用传递和按值传递?的使用技巧和注意事项,需要的朋友参考一下 在本文中,我们将学习PHP中的按值传递和按引用传递。  现在,让我们详细了解这两个概念。 通常,在PHP中,我们遵循通过按值传递方法将参数传递给函数的方法。我们之所以遵循这种做法,是因为如果函数中参数的值被更改,则在函数外部它不会被更改。 在某些情况下,我们可

  • 现在,调用可能会调用的copy构造函数(它很可能是copy省略的,所以情况并非如此)。但将导致复制。如果包含大量数据,则可能是一个问题。 我们将通过将其作为常量引用()传递来改进它,以消除不需要的副本。只要是就可以。如果不是,则调用将导致编译错误(丢失限定符)。 所以,为什么要费心使用常量引用,只使用reference()。这很好,但对于第一种情况就不起作用了,因为将r-value绑定到(非常量)

  • 问题内容: 数组不是Java中的原始类型,但它们也不是对象,因此它们是按值还是按引用传递?它是否取决于数组包含的内容,例如引用或原始类型? 问题答案: 。如果是Array(只不过是Object),则数组引用按值传递。(就像对象引用按值传递)。 当你将数组传递给其他方法时,实际上是复制对该数组的引用。 通过该引用对数组内容进行的任何更改都会影响原始数组。 但是,将引用更改为指向新数组不会更改原始方法

  • 问题内容: 我的运行方式如下: 哪里是这个要点。 当我查看结果时,它是一个1D数组而不是2D数组: 它似乎是一个元组数组: 如果我从调用中删除转换器规范,它将正常工作并生成2D数组: 问题答案: 返回的结果称为 结构化ndarray ,例如,请参见此处:http : **//docs.scipy.org/doc/numpy/user/basics.rec.html** 。这是因为您的数据不是同质的

  • 我创建了一个MapReduce作业,该作业将计算键的数量,然后根据它们出现的次数对它们进行排序 处理输入时,如 最终目标将是一个类似于 我的地图阶段输出a 我的减少阶段有3个阶段:设置,我初始化一个数组列表来保存我的 数组列表中的值是我创建的一个对象myObject的值,它将文本和Int保存在一个元组中可写,我发现一个奇怪的地方是当我这样做的时候 key是传递到reducer的键,count是我通