0

在 Visual C++ 2017 中,在尝试违反规则时会发生什么时,我发现如果我将 const int 强制转换为 int *,然后将值重新分配给 int *,调试器将更改 const 的值,但运行时执行不会。

无论我是在调试模式下运行它还是作为已发布的可执行文件运行,都会发生这种情况。我知道它是未定义的,但我正在寻找有关这些值所在位置的见解,因为它们似乎是相同的位置。

const int j = 100;
//int *q = &j; //Compiler disallows
int *q = (int*)&j; //By some magic, now allowed
*q = 300; //After this line, j = 300 in debugger
cout << "j = " << j << endl; //300 in debugger, 100 in console
//^ What is happening here? Where are the two values stored?
cout << "*q = " << *q << endl; //300 in both

//Output:
//  j = 100
//  *q = 300

这两个值存储在哪里?这就像有一个桶同时装满两种不同的液体。

我知道这是未定义的行为,但我想知道是否有人可以在内部阐明正在发生的事情。

4

3 回答 3

1

前提是有缺陷的。调试器按照相同的 C++17 规则工作,因此它也可以假设没有未定义的行为。这意味着它可以检查源代码并知道 j==100. 没有理由必须检查运行时值。

于 2019-01-20T01:37:18.453 回答
0

如果一个对象在const存储中,编译器可能会在空闲时用两个或多个具有相同内容的对象替换它,前提是它可以判断出地址从不进行比较。如果两个对象的地址都暴露给外界,编译器通常无法做到这一点,但在一个对象暴露而另一个对象没有暴露的情况下可能会这样做。

例如,考虑:

const char Hey[4] = "Hey";

void test(int index)
{
  char const *HeyPtr = Hey;
  putchar(HeyPtr[index]);
}

编译器处理test将能够看到 的值HeyPtr永远不会以任何方式暴露给外部代码,并且在某些平台上可能会受益于让test函数使用其自己的字符串副本。在地址为 64 位的平台上,如果test不包含其自己的字符串副本,则需要 8 个字节来包含Hey. 存储字符串的额外副本所需的四个字节的成本将低于保存地址所需的八个字节。

在某些情况下,标准提供的保证比程序员通常需要的要强。例如,给定:

const int foo[] = {1,2,3,4};
const int bar[] = {1,2,3,4};

除非程序碰巧将foo(或从它派生的地址)与bar(同样)进行比较,否则对两个对象使用相同的存储将节省 16 个字节,而不会影响程序语义。然而,该标准没有提供程序员可以指示代码不会比较这些地址,或者如果它们碰巧比较相等不会受到不利影响的方法,因此编译器只能在它可以的情况下进行这种替换告诉替换对象的地址不会暴露给可能执行此类比较的代码。

于 2019-01-21T16:56:04.390 回答
0

好吧,看看生成的程序集......

    const int j = 100;
00052F50  mov         dword ptr [j],64h  
    //int *q = &j; //Compiler disallows
    int *q = (int*)&j; //By some magic, now allowed
00052F58  lea         rax,[j]  
00052F5D  mov         qword ptr [q],rax  
    *q = 300; //After this line, j = 300 in debugger
00052F62  mov         rax,qword ptr [q]  
00052F67  mov         dword ptr [rax],12Ch  
    cout << "j = " << j << endl; //300 in debugger, 100 in console
00052F6D  lea         rdx,[__xt_z+114h (07FF679CC6544h)]  
00052F74  lea         rcx,[std::cout (07FF679D31B80h)]  
00052F7B  call        std::operator<<<std::char_traits<char> > (07FF679B43044h)  
00052F80  mov         edx,64h  
00052F85  mov         rcx,rax  
00052F88  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B417E9h)  
00052F8D  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF679B42C25h)]  
00052F94  mov         rcx,rax  
00052F97  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B445F7h)  
    //^ What is happening here? Where are the two values stored?
    cout << "*q = " << *q << endl; //300 in both
00052F9C  lea         rdx,[__xt_z+11Ch (07FF679CC654Ch)]  
00052FA3  lea         rcx,[std::cout (07FF679D31B80h)]  
00052FAA  call        std::operator<<<std::char_traits<char> > (07FF679B43044h)  
00052FAF  mov         rcx,qword ptr [q]  
00052FB4  mov         edx,dword ptr [rcx]  
00052FB6  mov         rcx,rax  
00052FB9  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B417E9h)  
00052FBE  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF679B42C25h)]  
00052FC5  mov         rcx,rax  
00052FC8  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B445F7h)  

注意从__xt_z+114h. 这是从全局初始化器末尾的偏移量(__xt_z可能是调试器找到的最近的符号),很可能是只读数据部分(.rdata)。

这就是 Debug 版本放置的地方100(毕竟它是一个常数)。

然后,MSVC 调试版本总是在堆栈上分配局部变量和常量,因此您会得到一个单独的j变量,您甚至可以修改它(注意编译器在您读取时不必从中读取j,因为它知道j是一个常量包含100)。

如果我们在发布模式下尝试相同的操作,我们会看到编译器进行了值传播并优化了这两个变量,只需将值内联到代码中:

    const int j = 100;
    //int *q = &j; //Compiler disallows
    int *q = (int*)&j; //By some magic, now allowed
    *q = 300; //After this line, j = 300 in debugger
    cout << "j = " << j << endl; //300 in debugger, 100 in console
000C101D  lea         rdx,[string "j = " (07FF72FAC3298h)]  
000C1024  mov         rcx,qword ptr [__imp_std::cout (07FF72FAC30A8h)]  
000C102B  call        std::operator<<<std::char_traits<char> > (07FF72FAC1110h)  
000C1030  mov         edx,64h  
000C1035  mov         rcx,rax  
000C1038  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC30A0h)]  
000C103E  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72FAC12E0h)]  
000C1045  mov         rcx,rax  
000C1048  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC3098h)]  
    //^ What is happening here? Where are the two values stored?
    cout << "*q = " << *q << endl; //300 in both
000C104E  lea         rdx,[string "*q = " (07FF72FAC32A0h)]  
000C1055  mov         rcx,qword ptr [__imp_std::cout (07FF72FAC30A8h)]  
000C105C  call        std::operator<<<std::char_traits<char> > (07FF72FAC1110h)  
000C1061  mov         edx,12Ch  
000C1066  mov         rcx,rax  
000C1069  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC30A0h)]  
000C106F  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72FAC12E0h)]  
000C1076  mov         rcx,rax  
000C1079  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC3098h)]  

在这两种情况下,输出是相同的。const变量保持不变。

有没有关系?不,您不应该依赖这种行为,也不应该修改常量。

于 2019-01-21T17:30:00.363 回答