gpt4 book ai didi

c++ - 为什么数组需要const大小?

转载 作者:行者123 更新时间:2023-11-30 03:24:23 26 4
gpt4 key购买 nike

假设您有以下代码:

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,这意味着编译器确实知道大小吗?

最佳答案

评论部分介绍了如何使用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++ forbids variable length array 'array' [-Wvla]



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

除了上述所有内容外,很多时候您实际上并不真正知道有多少可用堆栈空间,因此分配大小为 n的数组正在玩俄罗斯轮盘赌,而其中的内存错误将是 extremely difficult to find。 ( also this)

作为我先前观点的推论,如果您实际上是在跟踪拥有的堆栈空间,我敢说您没有在正确的抽象级别进行编程。

但是为什么呢?

如果此限制是由标准而不是技术限制所强加的,则逻辑上的后续问题是“为什么?”

好吧,首先,我们必须解决允许使用可变长度数组的主要问题:这主要与开发人员在源代码中编码非常量值有关。 (尽管这是错误的,请参阅: What is a magic number and why is it badconst-correctness)问题实际上围绕着以下事实:如果您可以基于非常量值设置堆栈分配的数组的大小,那么可以肯定地根据墨菲定律,大量等等,一些贫穷,不幸,毫无戒心但善意的初级开发人员将允许用户自己输入数组的大小,我们开始了比赛。相反,要求数组大小为整数文字或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,默认为at&t语法)。虽然我不会在 const上使用 n修饰符包含此代码的版本,但代码完全相同。至少在使用这些选项的gcc上,基本上不完全相同,实际上不完全相同。

另外,我想澄清一下,如果您要在禁用优化的情况下编译此代码,则可能会得到更直观的结果,即您编写的代码与编译器的汇编指令之间可能存在一对一的对应关系。输出。话虽这么说,我认为分析一个完全优化的程序(即使只是一个玩具示例)更加有用,因为它可以帮助您了解编译器使用的优化方式,特别是因为x84-64与x86在一些非平凡的方式。另外,一些汇编指令隐式引用了特定的寄存器,如果您不期望这样做,可能会造成混淆。

那么,这段代码是什么意思呢?让我们分解一下。

解释装配输出

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

释放了 rbprbx寄存器后,我们现在开始实际分配堆栈。正如我们在一开始所提到的,编译器完全了解您为 another_array数组的大小分配非常量值时的含义。认真地,堆栈使用 main命令为 sub rsp, 56分配了必要的空间。

请记住, rsp拥有一个内存地址,因此当我们从 rsp中减去56时,会将其向下移动值56。在64位体系结构中,由于 the stack grows down,它将表示7个字节的堆栈分配。

分配堆栈内存后,我们看到以下行:
movdqa xmm0, XMMWORD PTR .LC0[rip]
movdqa指令的意思是 Move Aligned Double Quadword,可以将128位从某处移到 xmm0寄存器。这里有几点要指出。首先, movdqa指令为其源和目标使用 xmm寄存器。如您所见,源将通过 .LC0地址进行“广播”。此转换是必需的,因为该指令期望源大小为128位,而x86-64中的地址由64位表示。另外,请注意我如何在引号中使用“cast”?那是因为用汇编语言进行的转换只取决于大小,而不是键入本身。 Vanilla 汇编语言中没有类型检查;它是您正在使用的编程语言提供的一种抽象。实际上,传递给函数的参数数量也不会与函数的声明arity相比较。这是语言编译器提供的另一种保护措施。您编写的代码将仅执行,如果您弄乱了某些内容,可能会导致分段错误。

历史记录:在过去,这是一笔大数目,因为操作系统或处理器无法为您提供内存保护。如果您编写的程序意外分配或写入了过多的内存,则很有可能不仅会覆盖您的个人资料(如文档和程序),还会覆盖您的内核。如今,我们拥有 protected 模式和虚拟内存,但是有趣的是,计算机仍然以实模式启动,然后初始化为 protected 模式。

回到 movdqa指令,有趣的是编译器选择对该程序使用 xmm寄存器。从C++代码中可以看到,数组仅保存整数,那么为什么要使用浮点寄存器呢?编译器利用打包的优势,将所有数字填充到单个寄存器中。如果您也会注意到,即使在程序中声明了五个整数,在 .LC0指令中也仅定义了四个元素。编译器优化了其中一个值,并将其余四个值都转换为 long

这是完美的,因为x64中的 xmm寄存器为128位。 C++ standard defines long是“至少32位”,当然看起来就是这种情况。现在,将这四个32位 long打包到单个128位寄存器中。

回到我们的分析,接下来的两个指示非常简单:
lea rbx, [rsp+16]
lea rbp, [rsp+36]
lea指令加载一个有效地址,在这种情况下为 [rsp+16]。这很有用,因为我们要传递相对于堆栈指针的地址。

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

这并不是您关于数组的问题所特有的,所以我会继续前进,但是我确实想快速达到两点:

首先,请注意,如果 cmp rbx, rbp为true,我们将跳过跳转回 .L2的过程。然后,我们通过向 rsp添加56来取消分配先前分配的堆栈内存。

第二,注意最后一个调用: xor eax, eax。在x86中,调用约定是将函数的结果放入 eax中。由于 main在成功执行后默认返回0,因此对同一寄存器进行逻辑异或运算将始终等于零。然后,我们从堆栈中弹出 rbxrbp并返回。

结论

总而言之,VLA确实没有给您带来任何额外的好处,使代码对读者来说不那么直观,并且可以表示可能的(且代价很高的)易受攻击的攻击媒介,但是可以使用它们,因为限制是由标准而非技术确定的。

关于c++ - 为什么数组需要const大小?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49742686/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com