为什么数组中需要const大小?

时间:2018-04-09 22:35:55

标签: c++ arrays

假设您有以下代码:

const size_t size = 5; 
int array[size]{1,2,3,4,5}; // ok to initialize since size is const 


size_t another_size = 5;
int another_array[another_size]; // can't do int another_array[another_size]{1,2,3,4,5};

another_array[0] = 1;
another_array[1] = 9090;
another_array[2] = 76;
another_array[3] = 90;
another_array[4] = 100;

由于array是使用const大小创建的,因此可以初始化它。但是,another_array无法初始化,因为它没有const大小。

如果我在声明数组后能够为another_array分配值,为什么我首先无法初始化another_array?编译器不应该知道大小吗?代码运行时由arrayanother_array创建的内容是什么?我假设编译器允许您使用非another_array大小创建const意味着编译器确实知道大小?

1 个答案:

答案 0 :(得分:2)

评论部分介绍了如何使用std::vector获取可变长度数组。我想仔细看看正在发生什么为什么

要回答你的问题,是的,编译器确实知道 - 并且知道它知道another_size的值。为简单起见,我们将首先解决这个答案中最基本的概念,然后我们将从那里开始教学,所以对于初学者,请考虑以下代码:

#include <iostream>

int main()
{
    std::size_t n = 5;

    int array[n] { 1, 2, 3, 4, 5 };

    for (auto i = 0; i < 10; ++i) {
        std::cout << array[i] << ' ';
    }
}

在gcc 7.3上,这会产生以下输出: [-std=c++17 -Wall -Wextra -Weffc++ -pedantic -O3]

<source>: In function 'int main()':
<source>:9:16: ISO C++ forbids variable length array 'array' [-Wvla]
    int array[n] { 1, 2, 3, 4, 5 };
               ^
Compiler returned: 0

如果您注意到,编译器的错误消息没有说明没有识别another_size标识符,甚至没有传递一个荒谬的值,因为它可能是假设未初始化或初始化错误。

错误只是说:

  

ISO C ++禁止可变长度数组&#39;数组&#39; [-Wvla]

奇怪的是,这正是它的意思。问题不在于编译器认为您缺少数组大小的表达式,因为编译程序时,词法分析器会对文件进行标记,并且解析器生成一个树,表示从您的代码中推导出的语义和#39;语法。 You'd be surprised how much the compiler can deduce from your code,并且非常了解标识符another_size以及相关值(5)。但是,C ++标准明确禁止可变长度数组,并且有充分理由,我们很快就会看到。但实际的限制是可以被视为“人工”的​​限制。一个,因为它实际上并不是因为技术上限制了编译器推断你的意图的能力。

除了上述所有内容之外,很多时候你还不知道你有多少堆栈空间,所以分配一个大小为n的数组正在玩带有内存错误的俄罗斯轮盘将是extremely difficult to find。 (also this

作为我之前的观点的必然结果,如果你 实际上已经记录了你有多少堆栈空间,我敢说你没有在正确的抽象层次上编程。 / p>

但为什么呢?

如果这种限制是由标准而非技术限制强加的,那么逻辑后续问题就是“为什么?&#34;

首先,我们必须解决允许可变长度数组的主要问题:它主要不是开发人员在源代码中编码非const值。 (虽然 错误,但请参阅:What is a magic number and why is it badconst-correctness)问题实际上是围绕这样一个事实:如果你可以根据a设置堆栈分配的数组的大小当然,根据墨菲定律,大数定律等等,一些可怜的,倒霉的,毫无防备但善意的初级开发人员将允许用户自己输入数组的大小,我们要参加比赛。相反,要求数组大小为整数文字或const变量都不允许这样做。

有趣的是,可变长度数组在其他语言中实际上是合法的,尤其是在C99标准的C中。尽管如此,他们仍然气馁。变长数组的最大问题是它们是堆栈分配的,而堆栈分配通常被认为是一件好事,在这种情况下它代表了一种负担。

由于address space layout randomization之类的问题以及对所涉及风险的认识不断增强,堆栈粉碎作为一个漏洞得到了缓解,但它远未解决问题。由于涉及这种特定情况,从用户接收输入时接受的做法是限制写入传入缓冲区的字节数。在这种情况下,我们作为开发人员的优势之一是知道这个缓冲区实际有多大。我们最不希望的是让潜在的入侵者能够自己设置堆栈分配的数组的大小。

更重要的是,获取用户输入是非常危险的,需要采取很多措施来正确消毒并控制输入。拥有一个可变长度数组,需要输入运行时值来设置其大小,这只是一个出错的机会。

那么,代码运行时会创建什么?

要回答这个问题,请考虑以下代码:

#include <iostream>


int main()
{
    std::size_t n = 5;

    int array[n] { 1, 7, 5, 0, 1 };

    for (auto i = 0; i < 5; ++i) {
        std::cout << array[i] << ' ';
    }
}

正如您所看到的,我们已经堆叠了一个非const值,并以给出错误的确切方式初始化了您的数组。我的编译器也警告我有关数组的信息,但是我只使用-std=c++17 -pedantic -O3编译,所以尽管有这个警告,编译仍在继续,产生以下代码,为了清晰和简洁而删节:

main:
  push rbp
  push rbx
  sub rsp, 56
  movdqa xmm0, XMMWORD PTR .LC0[rip]
  lea rbx, [rsp+16]
  lea rbp, [rsp+56]
  mov DWORD PTR [rsp+32], 1
  movaps XMMWORD PTR [rsp+16], xmm0
.L2:
  mov esi, DWORD PTR [rbx]
  mov edi, OFFSET FLAT:std::cout
  add rbx, 4
  call std::basic_ostream<char, std::char_traits<char> >::operator<<(int)
  lea rsi, [rsp+15]
  mov edx, 1
  mov rdi, rax
  mov BYTE PTR [rsp+15], 32
  call std::basic_ostream<char, std::char_traits<char> >& std::__ostream_insert<char, std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*, long)
  cmp rbx, rbp
  jne .L2
  add rsp, 56
  xor eax, eax
  pop rbx
  pop rbp
  ret
_GLOBAL__sub_I_main:
  sub rsp, 8
  mov edi, OFFSET FLAT:std::__ioinit
  call std::ios_base::Init::Init()
  mov edx, OFFSET FLAT:__dso_handle
  mov esi, OFFSET FLAT:std::__ioinit
  mov edi, OFFSET FLAT:std::ios_base::Init::~Init()
  add rsp, 8
  jmp __cxa_atexit
.LC0:
  .long 1
  .long 7
  .long 5
  .long 0

我建议您自行尝试,生成自己的汇编代码(使用-S进行汇编和-masm=intel,默认为&amp; t语法)。虽然我在const上使用n修饰符未能包含此代码的版本,但代码完全相同。不基本上完全相同,字面完全相同,至少在gcc上有这些选项。

另外,我想澄清一下,如果您在禁用优化的情况下编译此代码,您可能会获得更直观的结果,因为您编写的代码与程序集之间可能存在更多的一对一对应关系编译器输出的指令。话虽如此,我认为分析一个完全优化的程序,即使它只是一个玩具示例,也更有用,因为它可以帮助您了解编译器使用的优化,特别是因为x84-64不同于x86以一些非平凡的方式。此外,一些汇编指令隐含地引用了特定的寄存器,如果您不期望它,这可能会令人困惑。

那么这段代码意味着什么呢?让我们分解吧。

解释汇编输出

输入main后,rbprbx寄存器将被压入堆栈。回想一下,在x86-64中rbp可以用作通用寄存器,而不必充当基指针。相反,处理器使用rsp来支持函数调用和返回。

释放rbprbx寄存器后,我们现在继续实际分配堆栈。正如我们在开始时提到的那样,当您将非const值指定为another_array数组的大小时,编译器确切地知道您的意思。尽职尽责,堆栈使用main命令为sub rsp, 56分配必要的空间。

请记住rsp包含内存地址,因此当我们从rsp中减去56时,我们将其移动向下 < / i>值为56.在64位体系结构中,这将代表7个字节的堆栈分配,因为the stack grows down

分配堆栈内存后,我们看到这一行:

movdqa xmm0, XMMWORD PTR .LC0[rip]

movdqa指令意味着Move Aligned Double Quadword,想要从某处移动128位到xmm0寄存器。这里有几点需要指出。首先,movdqa指令对其源和目标都采用xmm寄存器。正如您所看到的,如果您愿意,来自.LC0地址的来源正在被播放。这种转换是必要的,因为指令期望源大小为128位,而地址在x86-64中由64位表示。另外,请注意我如何使用&#34; cast&#34;用引号?这是因为汇编语言的大约是 size ,而不是 type 本身。在vanilla汇编语言中没有类型检查;它是您正在使用的编程语言提供的抽象。实际上,传递给函数的参数数量也不会与函数的声明arity进行比较。这是您的语言编译器提供的另一个安全措施。您编写的代码只会执行,如果您搞砸了某些内容,可能会导致分段错误。

历史记录:在过去,这是一个巨大的交易,因为操作系统或处理器没有提供内存保护。如果你编写了一个意外分配或写入太多内存的程序,那么很可能不仅会覆盖你的个人资料,比如文档和程序,还会覆盖你的内核。我们现在拥有奢侈的保护模式和虚拟内存,但有趣的是,计算机仍然以实模式启动,然后初始化为保护模式。

回到movdqa指令,有趣的是编译器选择在此程序中使用xmm寄存器。从我们的C ++代码中可以看出,我们的数组只保存整数,那么为什么要使用浮点寄存器呢?编译器利用打包,将所有数字填入单个寄存器。如果你也注意到,在.LC0指令中,只定义了四个元素,即使我们在程序中声明了五个整数。编译器优化了其中一个&#39;并将剩余的四个值中的每一个转换为long

这很完美,因为x64中的xmm寄存器是128位。 C++ standard defines long为&#34;至少32位&#34;,这看起来确实如此。这四个32位long现在被打包到一个128位寄存器中。

回到我们的分析,接下来的两条指令非常简单:

lea rbx, [rsp+16]
lea rbp, [rsp+36]

lea指令加载有效地址,在本例中为[rsp+16]。这很有用,因为我们相对于堆栈指针传递地址。

现在,它可能不会立即明显,但[rsp+16]是数组的第一个元素,[rsp+36]是最后一个元素。在.L2中,您可以看到该程序调用{​​{1}}。它正在测试cmp rbx, rbp指向的地址是否等于rbp指向的地址。如果结果为false,则指令指针移回rbx的开头,将.L2增加4个字节(从而使其等于此整数数组中的下一个值),并再次重复循环

这不是特定于你关于数组的问题,所以我会快速前进,但我确实希望快速达到两点:

首先,请注意,如果rbx为真,我们会跳过跳回cmp rbx, rbp。然后,我们通过向.L2添加56来释放我们先前分配的堆栈内存。

其次,请注意最后一次通话:rsp。在x86中,调用约定是将函数的结果放入xor eax, eax。由于eax在成功执行时默认返回0,因此对同一寄存器的逻辑异或操作将始终等于零。然后我们从堆栈中弹出mainrbx并返回。

结论

总而言之,VLA可以为您提供真正没有额外好处,使代码对读者不那么直观,并且可以代表可能(且代价高昂)易受攻击的攻击向量,但是使用它们是可能的,作为限制是由标准而不是技术建立的。