为什么此代码给出输出C++Sucks?它背后的概念是什么?
1 2 3 4 5 6 7
| #include <stdio.h>
double m [] = {7709179928849219.0, 771};
int main () {
m [1]--?m [0]*=2,main ():printf((char*)m );
} |
在这里测试一下。
- @从技术上讲,是的,但是它在c99:ideone.com/izokql中运行完全相同。
- @ Nijn森,我的观点是,在C中递归调用EDCOX1 0是合法的,而不是在C++中。
- @Sid我在某人的Topcoder配置文件中将此代码作为状态读取..对其进行了测试,并对输出感到惊讶..试图分析它,但有点远,但无法找到确切的解释,因此在此处询问它
- 我想你也会喜欢阅读2006年的问答:模糊C代码竞赛
- @雷管123哦,你不知道堆垛溢出机制是如何工作的?那么我非常感谢你,给你:http://stackoverflow.com/help/why-vote。
- @我也有类似的想法。但这不是欧普的错,而是人们投票支持这无用的知识。承认,这种代码模糊的东西可能很有趣,但在谷歌中输入"模糊",你会在你能想到的每种正式语言中得到大量的结果。别误会我,我觉得在这里问这样一个问题是可以的。这只是一个被高估的问题,因为这个问题不太有用。
- @雷管123"你一定是新来的"——如果你看看关闭的原因,你会发现情况并非如此。您的问题中显然缺少了所需的最小理解——"我不理解这个,请解释一下",这在堆栈溢出时是不受欢迎的。如果你自己先尝试了什么,这个问题会不会被解决?对于谷歌"双表示C"之类的公司来说,这是微不足道的。
- @ BenCollins什么时候是空的主()C++?
- @Josefx:因为它不是c.stackoverflow.com/questions/2108192/&hellip;中的main()函数允许的签名。呃,在我们的时候,C++也不允许这样做。
- 我的大端PowerPC机打印了skcuS++C。
- 我的话,我讨厌这样做作的问题。这是一个内存中的位模式,恰好与一些愚蠢的字符串相同。它对任何人都没有用处,但是它为提问者和回答者都赢得了数百个代表点。同时,对人们有用的难题可能会获得一些分数(如果有的话)。这有点像是一个有问题的典型儿童。
数字7709179928849219.0具有以下二进制表示形式,即64位double:
1 2
| 01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- -------- |
+表示符号的位置;指数的^和尾数的-(即没有指数的值)。
由于表示使用二进制指数和尾数,因此将数字加倍将使指数增加1。你的程序精确地做了771次,所以从1075开始的指数(10000110011的十进制表示)最后变成1075+771=1846;1846的二进制表示是11100110110。结果模式如下:
1 2 3
| 01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C' |
此图案与您看到的打印字符串相对应,只是向后。同时,数组的第二个元素变为零,提供了空终止符,使得字符串适合传递给printf()。
- 为什么绳子是向后的?
- @Derek x86是Little Endian
- @Derek这是因为特定于平台的endianness:抽象的IEEE754表示的字节以递减的地址存储在内存中,因此字符串正确打印。在具有大endianness的硬件上,需要从一个不同的数字开始。
- C++标准是否需要在IEEE 754中表示浮点数?维基百科说不…
- @Alvinwong您是正确的,本标准不需要IEEE 754或任何其他特定格式。这个程序差不多是不可移植的,或者离它很近。
- @DasBlinkenlight如何生成二进制文件?只是读取位模式?
- @我使用了双精度IEEE754计算器:我粘贴了7709179928849219值,并得到了二进制表示。
- 不知道我是应该给你+1来计算它,还是-1来考虑它是值得的时间。
- @爱德华德福尔克一开始写这本书花了不少时间,花了不到五分钟的时间,所有的计算器都可以在网上找到。
- 递归使其从后向前打印。:)
- @德里克,你不应该取笑他的体型。
- @吉米巴特,那些技术水平低的人有什么不可思议的?人们必须从某个地方开始。你也不是天生的C++大师。
- @美国2012年,吉姆是绝对正确的"快车道"的学习风格。了解如何在硬件上表示和处理数据是在体面的课程中教授的第一件事,或者是由真正感兴趣和好奇的学生发现的第一件事。自称为"IT大师"的人,炫耀他们使用一些高级框架或语言作为"知识"或"专家",这是令人厌恶的,是对他们所站的巨人肩膀的伤害。
- 值得一看的娱乐价值。
- @你是个巫师吗?你是怎么知道的?
- @我把数字粘贴到IEEE754计算器上,查找尾数的位置,然后用计算器做了一点算术。其他的一切都到位了。
更具可读性的版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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个字节上的情况下进行的,因此每个字符占用1个字节。
如果没有递归和非法的main()调用,它将如下所示:
1 2 3 4 5 6
| double m [] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i ++)
{
m [0] *= 2;
}
printf((char*) m ); |
- 这是修正后的减量。所以它将被称为771次。
- @你说得对,谢谢。我已经编辑了我的答案。
- 这应该是公认的答案,真正清除了原始代码的"模糊"。
免责声明:这个答案被张贴到问题的原始形式,它只提到C++,并包含了C++头。这个问题转化为纯C是由社区完成的,没有原始提问者的意见。
形式上讲,不可能对这个程序进行推理,因为它是不正确的(即它不是合法的C++)。它违反了C++ 11 [Basic,St.Madi] P3:
The function main shall not be used within a program.
除此之外,它还依赖于这样一个事实:在一台典型的消费计算机上,double的长度为8字节,并且使用某种众所周知的内部表示。对数组的初始值进行计算,以便在执行"算法"时,第一个double的最终值将是这样的:内部表示(8字节)将是8个字符C++Sucks的ASCII码。数组中的第二个元素是0.0,其第一个字节在内部表示中是0,使其成为有效的C样式字符串。然后使用printf()发送到输出。
在上面的一些内容没有保留的硬件上运行这个命令将导致垃圾文本(或者甚至可能导致访问越界)。
- 我要补充的是,这不是一个C++ 11的发明——C++ 03也有EDCOX1,0,3.3.1/3,用同样的措辞。
- 这个小例子的要点是说明C++可以做什么。使用ub技巧或"经典"代码的大型软件包的魔力示例。
- @Sharptooth感谢您添加此功能。我不是故意的,我只是引用了我使用的标准。
- @安吉:是的,我明白,我只是想说,这个措词很旧。
- 是的,它违反了C++ 11,但是如果它在C++ 11之前写了很长很长的时间呢?
- "形式上讲,不可能对这个程序进行推理"——只有C++标准是形式逻辑的基础…但事实并非如此,所以你的陈述是错误的。使用各种众所周知的事实,很有可能对这个程序进行推理。人们可以很容易地对程序做出条件语句,例如"如果运行的机器有这样那样的共同特征,那么程序就会这样做"。实际上,我们知道程序在运行时做了什么,所以我们可以从那里正式地推理到实现的特性。
- "函数main不应在程序中使用。"——您没有注意、掌握或理解此语句的上下文。"应"适用于严格符合要求的程序…违反这种约束的程序不是严格遵守C++程序。但大多数程序没有。
- @Jimbalter注意到我说过"正式地说,不可能推理,"而不是"不可能正式地推理"。你说得对,有可能对程序进行推理,但你需要知道用于推理的编译器的详细信息。它完全在编译器的权限内,可以简单地取消对main()的调用,或者用API调用替换它来格式化硬盘驱动器,或者其他什么。
- @ Svp齿指出的VSZ,同样的措辞也出现在C++ 03中。我无法访问C++ 98标准,但在那里也不会感到惊讶。
- C++允许的是无关紧要的,因为这是一个C程序,而不是C++。当然,单凭这一点并不能从形式上未定义的行为中拯救这个程序。
- @ BerndJendrissek,当我回答这个问题时,它仍然用C++来标记,在标题中只提到了C++,包括EDCOX1,2,等等。这不是我的错,其他人编辑它只是提到了C,没有OP的意见。检查问题的编辑历史,更不用说消息是"C++吸吮"了。
- @安吉,哦,哇,这是可怕的编辑(还有一个原因,我不喜欢非操作编辑)。不好意思爱上它,以xkcd.com/386的风格对你大喊大叫。
- @Berndjendrissek没问题,至少有人通知我Q的变化,并添加了一个合适的免责声明。
也许理解代码的最简单的方法是逆向处理。我们将用一个字符串打印出来——为了平衡,我们将使用"C++岩石"。关键点:和原版一样,它只有八个字符长。因为我们要像原稿一样(粗略地)打印出来,然后按相反的顺序打印出来,我们将从相反的顺序开始。对于第一步,我们将把该位模式看作一个double,并打印出结果:
1 2 3 4 5 6 7 8
| #include <stdio.h>
char string [] ="skcoR++C";
int main (){
printf("%f
", *(double*)string );
} |
这就产生了3823728713643449.5。所以,我们想用一些不明显,但很容易逆转的方法来操纵它。我将半任意地选择乘256,这给了我们978874550692723072。现在,我们只需要编写一些模糊的代码,将其除以256,然后按相反的顺序打印出各个字节:
1 2 3 4 5 6 7 8 9 10 11 12
| #include <stdio.h>
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语句转换成一个表达式,所以主体如下:
1 2
| x [1] && (x [0] /= 2, main (--x [1], (char **)++y ));
putchar(*--y ); |
对于任何不习惯模糊代码(和/或代码高尔夫)的人来说,这看起来确实很奇怪——计算并丢弃一些毫无意义的浮点数的逻辑and和main的返回值,这些值甚至都没有返回值。更糟糕的是,如果没有意识到(并思考)短路评估是如何工作的,那么它甚至可能无法立即明显地避免无限递归。
我们的下一步可能是将打印每个字符与查找该字符分开。通过生成正确的字符作为main的返回值,并打印出main返回的内容,我们可以很容易地做到这一点:
1 2
| x [1] && (x [0] /= 2, putchar(main (--x [1], (char **)++y )));
return *--y ; |
至少在我看来,这已经足够模糊了,所以我就不谈了。
它只是构建了一个双数组(16字节),如果被解释为字符数组,则为字符串"c++吮吸"建立ASCII代码。
但是,代码不适用于每个系统,它依赖于以下一些未定义的事实:
下面的代码打印了C++Suc;C,所以整个乘法过程只针对最后两个字母。
1 2 3
| double m [] = {7709179928849219.0, 0};
printf("%s
", (char *)m ); |
其他人已经很彻底地解释了这个问题,我想补充一点,根据标准,这是未定义的行为。
C++ 11 3.61/3主函数
The function main shall not be used within a program. The linkage (3.5) of main is implementation-defined. A program that defines main as deleted or that declares main to be inline, static, or constexpr is ill-formed. The name main is not otherwise reserved. [ Example: member functions, classes, and enumerations can be called main, as can entities in other namespaces. —end example ]
- 我会说它甚至是不正确的(就像我在回答中所做的那样),它违反了"应该"。
代码可以这样重新编写:
1 2 3 4 5 6 7 8 9 10
| void f ()
{
if (m [1]-- != 0)
{
m [0] *= 2;
f ();
} else {
printf((char*)m );
}
} |
它所做的是在EDCOX1×0数组EDCOX1(7)中产生一组字节,它对应于字符"C++吸吮",后面跟着一个空终止符。他们通过选择一个双精度值来模糊代码,在标准表示法中,当该值翻倍771次时,将产生一组字节,该字节带有数组第二个成员提供的空终止符。
请注意,此代码不能在不同的endian表示下工作。另外,不允许打电话给main()。
- 为什么你的f返回int?
- 呃,'因为我在复制问题中的int返回是愚蠢的。我来解决这个问题。
首先,我们应该记住,双精度数字以二进制格式存储在内存中,如下所示:
(i)符号1位
(ii)指数为11位
(iii)52位
位的顺序从(i)降到(iii)。
首先将十进制分数转换为等价的分数二进制数,然后将其表示为二进制的数量级形式。
因此,编号7709179928849219.0变为
1 2 3 4
| (11011011000110111010101010011001010110010101101000011)base 2
=1.1011011000110111010101010011001010110010101101000011 * 2^52 |
现在,同时考虑数量位1。忽略不计,因为所有数量级方法应以1开头。
所以量级部分变成:
1
| 1011011000110111010101010011001010110010101101000011 |
现在2的幂是52,我们需要把偏压数加成2^(指数-1的位)-1。即2^(11-1)-1=1023,所以我们的指数变成52+1023=1075。
现在我们的代码用2771倍的倍数使这个数字的指数增加了771倍。
所以我们的指数是(1075+771)=1846,它的二进制等价物是(11100110110)。
现在我们的数字是正数,所以我们的符号位是0。
所以我们修改的数字变成:
符号位+指数+大小(位的简单串联)
1
| 0111001101101011011000110111010101010011001010110010101101000011 |
由于m被转换成char指针,我们将把位模式从lsd中分为8块。
1
| 01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 |
(十六进制等效值为:)
1
| 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 |
从字符图中可以看出:
现在,一旦将其设置为m[1]为0,这意味着一个空字符
现在假设您在一个小的endian机器上运行这个程序(低位存储在低位地址中),那么指针m指向低位地址位,然后在8个chuck中取位(类型转换为char*),最后一个chunk中遇到00000000时printf()停止……
但此代码不可移植。