C++程序如何根据编译选项打印0或1



我有以下C++代码。它应该打印1。只有一个非法内存访问,它是读取的,即对于for循环中的i=5,我们获得c[5]=cdata[5],而cdata[5]不存在。尽管如此,所有的写入调用都是合法的,即程序从不尝试写入某些未分配的数组单元。如果我用g++ -O3编译它,它会打印0。如果我在没有-O3的情况下编译,它会打印1。这是我在g++中见过的最奇怪的错误。

稍后编辑以解决一些问题。未定义的行为应限于读取cdata[5]并将其写入c[5]。分配CCD_ 13可以从CCD_。好吧,但这应该只将垃圾复制到c[5],而不在其他地方写任何东西!

#include <iostream>
#include <cstdlib>
using namespace std;
int n,d;
int *f, *c;
void loadData(){
int fdata[] = {7,  2, 2, 7, 7, 1};
int cdata[] = {66, 5, 4, 3, 2};
n = 6;
d = 3;
f = new int[n+1];
c = new int[n];
f[0] = fdata[0];
c[0] = cdata[0];
for (int i = 1;i<n;i++){
f[i] = fdata[i];
c[i] = cdata[i];
}
cout<<f[5]<<endl;
}
int main(){
loadData();
}

在优化前后,很难准确了解代码发生了什么。正如你自己已经知道并指出的那样,你试图脱离数组的界限,这会导致未定义的行为。

然而,您很好奇为什么它(显然)是-O3标志;原因;问题!

让我们首先说,实际上是标志-O -fpeel-loops导致代码重新组织,从而使错误变得明显。-O3将启用几个优化标志,其中-O -fpeel-loops

您可以在这里阅读有关编译器标志在优化的哪个阶段是什么的信息。

简而言之,-fpeel-loops轮重新组织了循环,因此第一次和最后几次迭代实际上是从循环本身中取出的,并且一些变量实际上是从内存中清除的。小线圈甚至可能被完全拆开!

考虑到这一点,试着用-O -fpeel-loops甚至用-O3:运行这段代码

#include <iostream>
#include <cstdlib>
using namespace std;
int n,d;
int *f, *c;
void loadData(){
int fdata[] = {7,  2, 2, 7, 7, 1};
int cdata[] = {66, 5, 4, 3, 2};
n = 6;
d = 3;
f = new int[n+1];
c = new int[n];
f[0] = fdata[0];
c[0] = cdata[0];
for (int i = 1;i<n;i++){
cout << f[i];
f[i] = fdata[i];
c[i] = cdata[i];
}
cout << "nFINAL F[5]:" << f[5]<<endl;
}
int main(){
loadData();
}

您将看到,无论您的优化标志如何,它都将打印1

这是因为语句:cout << f[i],它将改变fpeel-loops的操作方式。

另外,用这个代码块进行实验:

f[0] = fdata[0];
c[0] = cdata[0];
c[1] = cdata[1];
c[2] = cdata[2];
c[3] = cdata[3];
c[4] = cdata[4];
for (int i = 1; i<n; i++) {
f[i] = fdata[i];
c[5] = cdata[5];
}
cout << "nFINAL F[5]:" << f[5] <<endl;

您将看到,即使在这种情况下,使用所有优化标志,输出也是1,而不是0。即使在这种情况下:

for (int i = 1; i<n; i++) {
c[i] = cdata[i];
}
for (int i = 1; i<n; i++) {
f[i] = fdata[i];
}

产生的输出实际上是1。这也是因为我们已经改变了循环的结构,fpeel-loops无法像以前那样重新组织它,就像产生错误一样。这也是将其包装成while循环的情况:

int i = 1;
while (i < 6) {
f[i] = fdata[i];
c[i] = cdata[i];
i++;
}

尽管在while循环中,由于-Waggressive-loop-optimizations的原因,-O3将阻止此处的编译,因此您应该使用-O -fpeel-loops进行测试

因此,我们不能真正确定编译器是如何为您重组该循环的,但它使用了所谓的规则,就好像规则一样,您可以在这里阅读更多信息。

当然,编译器会根据您遵守设置规则的事实,为您自由重构代码。编译器的like规则将始终产生所需的输出,前提是程序员没有导致未定义的行为。在我们的例子中,我们确实违反了规则,超出了该数组的界限,因此重构该循环的策略是基于这样的想法,即这不可能发生,但我们做到了。

所以,是的,并不是所有的事情都像说你所有的问题都是由在未分配的内存地址读取造成的那样简单明了。当然,这也是问题最终得到证实的原因,但您读取的cdata超出了范围,这是一个不同的数组!因此,这并不像说仅仅是读取超出数组范围就导致了问题那么简单,就好像你这样做一样:

int i = 0;
f[i] = fdata[i];
c[i] = cdata[i];
i++;
f[i] = fdata[i];
c[i] = cdata[i];
i++;
f[i] = fdata[i];
c[i] = cdata[i];
i++;
f[i] = fdata[i];
c[i] = cdata[i];
i++;
f[i] = fdata[i];
c[i] = cdata[i];
i++;
f[i] = fdata[i];
c[i] = cdata[i];
i++;

它将工作并打印1!即使我们使用cdata数组肯定会越界!因此,造成问题的不仅仅是在未分配的内存地址进行读取。

同样,实际上是fpeel-loops循环重构策略,它认为您不会超出该数组的范围,并且相应地更改了循环。

最后,优化标志将导致0的输出这一事实与您的机器密切相关。0没有任何意义,它不是实际逻辑运算的产物,因为它实际上是一个垃圾值,在未分配的内存地址中找到,或者是对垃圾值执行的逻辑运算的乘积,导致NaN。

如果您想了解发生这种情况的原因,可以检查生成的程序集代码。它位于编译器资源管理器中:https://godbolt.org/z/bs7ej7

请注意,对于-O3,生成的汇编代码并不完全易于读取。

正如您所提到的,您的代码中存在内存问题。

"cdata";具有5个成员,其最大索引为4。但在循环中,当";i=5〃;,读取的数据无效,可能会导致未定义的行为。

c[i] = cdata[i];

请这样修改您的代码,然后重试。

for (int i = 1; i < n; i++)
f[i] = fdata[i];
for (int i = 1; i < 5; i++)
c[i] = cdata[i];

答案很简单——您访问的内存超出了数组的范围。谁知道那里有什么?它可能是程序中的其他变量,可能是随机垃圾,完全未定义。

所以,当你编译的时候,有时你碰巧得到了有效的数据,而其他时候你编译的数据并不是你所期望的。

您所期望的数据是否存在纯属巧合,并且取决于编译器的操作将决定存在哪些数据,即完全不可预测。

至于内存访问,读取无效内存仍然是一个很大的禁忌。读取潜在的未初始化值也是如此。你同时在做这两件事。它们可能不会导致分割错误,但它们仍然是大的"否"。

我的建议是使用valgrind或类似的内存检查工具。如果你通过valgrind运行这个代码,它会对你大喊大叫。这样,您就可以捕获代码中编译器可能无法捕获的内存错误。这种记忆错误是显而易见的,但有些很难追踪,几乎所有的错误都会导致不明确的行为,这让你想用一把生锈的钳子把牙齿拔出来。

简而言之,不要访问越界元素,并祈祷你正在寻找的答案恰好存在于该内存地址。

最新更新