提问



为什么这段代码会给出输出C++Sucks?它背后的概念是什么?


#include 

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}


在这里测试。[50]

最佳参考


数字7709179928849219.0具有以下二进制表示形式为64位double:


01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------


+显示了标志的位置;指数的^和尾数的-(即没有指数的值)。


由于表示使用二进制指数和尾数,因此将指数加倍会使指数递增1。你的程序精确地完成了771次,所以从1075开始的指数(10000110011的十进制表示)最后变为1075 + 771=1846; 1846年的二进制表示是11100110110。结果模式如下所示:


01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'


此模式对应于您看到的打印字符串,仅向后。同时,数组的第二个元素变为零,提供空终止符,使字符串适合传递给printf()

其它参考1


更易阅读的版本:


double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}


它以递归方式调用main() 771次。


最开始,m[0] = 7709179928849219.0代表C++Suc;C。在每次通话中,m[0]加倍,修复最后两个字母。在最后一次调用中,m[0]包含C++Sucks的ASCII字符表示,m[1]仅包含零,因此它C++Sucks字符串的空终止符。所有假设m[0]存储在8个字节上,因此每个char占用1个字节。 [51] [52]


没有递归和非法main()调用它将如下所示:


double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

其它参考2


免责声明:此答案已发布到问题的原始形式,该问题仅提及C ++并包含C ++标题。问题是转换为纯C是由社区完成的,没有原始提问者的意见。





从形式上讲,不可能推理这个程序,因为它的结构不合理(即它不是合法的C ++)。它违反了C ++ 11 [[basic.start.main]] p3:



??函数main不得在程序中使用。



除此之外,它依赖于这样的事实,即在典型的消费者计算机上,double长度为8个字节,并使用某种众所周知的内部表示。计算数组的初始值,以便在执行算法时,第一个double的最终值将使内部表示(8个字节)成为8个字符的ASCII码[[C++Sucks。然后数组中的第二个元素是0.0,其内部表示中的第一个字节是0,使其成为有效的C风格字符串。然后使用printf()将其发送到输出。


在HW上运行此操作,其中上述某些操作不会导致垃圾文本(或者甚至是访问超出范围)。

其它参考3


也许理解代码的最简单方法是反过来处理事情。我们将从一个字符串开始打印 - 为了平衡,我们将使用C ++ Rocks。关键点:就像原版一样,它的长度恰好是八个字符。由于我们(大致)会像原版那样做,并以相反的顺序打印出来,我们将从相反的顺序开始。对于我们的第一步,我们只是将位模式视为double,并打印出结果:


#include 

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}


这产生3823728713643449.5。因此,我们希望以某种方式操纵它,这不是很明显,但很容易逆转。我将半任意选择乘以256,这给了我们978874550692723072。现在,我们只需要编写一些混淆代码来除以256,然后以相反的顺序打印掉它的各个字节:


#include 

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}


现在我们有很多转换,将参数传递给(递归)main,这些参数被完全忽略(但是获得增量和减量的评估是完全至关重要的),当然还有完全随意的数字来掩盖这样一个事实:我们正在做的事情非常简单明了。


当然,由于整点都是混淆,如果我们觉得这样,我们也可以采取更多步骤。例如,我们可以利用短路评估,将if语句转换为单个表达式,因此main的主体看起来像这样:


x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);


对于那些不习惯混淆代码(和/或代码高尔夫)的人来说,这开始看起来确实很奇怪 - 计算和丢弃某些无意义的浮点数的逻辑andmain的返回值]],它甚至没有返回一个值。更糟糕的是,如果没有意识到(并思考)短路评估是如何工作的,那么它如何避免无限递归甚至可能不会立即显而易见。


我们的下一步可能是将每个角色的打印与找到该角色分开。通过从main生成正确的字符作为返回值,并打印出main返回的内容,我们可以很容易地做到这一点:


x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;


至少对我来说,这似乎足够混淆了,所以我会留下它。

其它参考4


它只是构建一个双数组(16个字节) - 如果解释为char数组 - 为字符串C ++ Sucks构建ASCII代码


但是,代码不适用于每个系统,它依赖于以下一些未定义的事实:



  • double正好有8个字节

  • 端序


其它参考5


以下代码打印C++Suc;C,因此整个乘法仅适用于最后两个字母[53]


double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

其它参考6


其他人已经非常彻底地解释了这个问题,我想根据标准添加一个注释,这是未定义的行为


C ++ 11 3.6.1/3 主要功能



??函数main不得在程序中使用。 main的链接(3.5)是实现定义的。将main定义为已删除或声明main为inline,static或constexpr的程序格式不正确。名称main不以其他方式保留。 [[示例:成员函数,类和枚举可以称为main,其他名称空间中的实体也可以称为main。 - 示例]]


其它参考7


代码可以像这样重写:


void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}


它正在做的是在double数组m中生成一组字节,这些字节恰好对应于字符C ++ Sucks,后跟一个空终止符。它们对代码进行了模糊处理通过选择一个double值,当加倍771次时,在标准表示中产生具有由数组的第二个成员提供的null终止符的字节集。


请注意,此代码不会在不同的endian表示下工作。此外,不严格允许调用main()

其它参考8


它基本上只是一种聪明的方法来隐藏第一个double值中的字符串C ++ Sucks(注意8个字节),递归地乘以2直到秒的双值达到零(771次)。


如果将double的字节值解释为字符串,则printf()将使用双精度数乘以7709179928849219.0 * 2 * 711会导致C ++糟透。并且printf()不会失败,因为第二个double值是0并被printf()解释为\\ 0。