面试问答:Hello World 解释

27

这个经典的 IOCCC 参赛作品是用 C 编写的 Hello World 程序。有没有人可以解释一下它是如何工作的?

int i;
main() {
    for (;i["]<i;++i){--i;}"]; read('-'-'-',i+++"hello, world!\n",'/'/'/'));
}
read(j, i, p) {
    write(j / p + p, i---j, i / i);
}

稍微整理了一下:

int i;
main()
{
  for ( ; i["]<i;++i){--i;}"]; read('-' - '-', i++ + "hello, world!\n", '/' / '/'));
}

read(j, i, p)
{
  write(j / p + p, i-- - j, i / i);
}

38
面试中为什么会有人问混淆代码的问题?! - Brian Rasmussen
2
从纯语言角度来看,完全不是这样的,因为write()没有被定义。;-)(我知道这可能是要调用POSIX write()函数,但这并没有在任何地方说明。) - DevSolar
1
更不用说 read() 的 K&R 风格声明了。它可能无法直接在现代编译器中编译通过。如果这出现在面试中,它只有作为一个关于人们在代码周围玩游戏的谈话开端才有意义,并且可以看到受访者如何解决这个难题... - RBerteig
1
是的,有一些假设。@Brian:这可能是因为解释这个工作原理需要很好的理解,而一个平庸的程序员可能没有这种知识...也许。或者面试官认为他真的很聪明。 - JoshD
3
@Fabian:从语法上讲,它们是一样的。[]运算符是可交换的。 - DevSolar
显示剩余4条评论
5个回答

56

for循环条件

i["]<i;++i){--i;}"]

这个表达式利用了在C语言中数组索引是可交换的这一事实。它等价于:

"]<i;++i){--i;}"[i]

因此,当位置i处的字符为\0,即字符串结束时,循环将终止,该字符串长度为14个字符(恰好与"hello, world!\n"的长度相同)。 因此,可以将for循环条件重写为:

i != 14

字符算术

read('-' - '-', i++ + "hello, world!\n", '/' / '/')

char是一种整数类型,因此:

  • '-' - '-'等于0
  • '/' / '/'等于1

    read(0, i++ + "hello, world!\n", 1)


修复了所有编译器警告(例如隐式整型转换为指针),并简化了上述内容后,代码变成了:

#include <unistd.h>

int i = 0;

void read2(int, char*, int);

int main()
{
   while (i != 14)
   {
      read2(0, i++ + "hello, world!\n", 1);
   }

   return 0;
}

void read2(int j, char* i, int p)
{
   write(j / p + p, i-- - j, 1);
}

(我将read重命名为read2,以避免与Unix的read函数发生冲突。)

请注意,对于read2函数,jp参数是不需要的,因为该函数总是以j=0和p=1的形式调用。

#include <unistd.h>

int i = 0;

void read2(char*);

int main()
{
   while (i != 14)
   {
      read2(i++ + "hello, world!\n");
   }

   return 0;
}

void read2(char* i)
{
   write(1, i--, 1);
}

write(1, i--, 1)的调用会把i中的一个字符写到文件描述符1(标准输出),而后缀--是多余的,因为这个i是一个局部变量,在之后没有被引用。所以这个函数等同于putchar(*i)

read2函数嵌入到主循环中可得:

#include <stdio.h>

int i = 0;

int main()
{
   while (i != 14)
   {
      putchar(*(i++ + "hello, world!\n"));
   }

   return 0;
}

其意义显而易见。


第三个代码块中有错别字:void read2(char* i) { write(1, i--, 1); }应该改为:void read2(char* i) { write(0, i--, 1); }其中,0 是标准输出的文件描述符。 - Jonathan Branam
1
@JonathanBranam 不是,0 是标准输入的文件描述符。1 是标准输出,2 是标准错误输出。 - Pedro Cunha

17

并不想完全梳理这个程序,但是有一些提示:

  • '-' - '-' 被混淆成了 0
  • '/' / '/' 和 read() 函数中的 i / i 被混淆成了 1

请记住 [] 是可交换的,比如说 i["]<i;++i){--i;}"]"]<i;++i){--i;}"[i] 是一样的意思(其中有一个字符数组并且指向它的第 i 个元素)。字符串的内容并不重要,只用到它的长度来定义循环的次数。任何其他相同长度的字符串(巧合的是,和输出的长度一样...)都可以使用。在循环次数之后,"string"[i] 返回终止字符串的 null 字符。Zero == false,循环结束。

就此,应该相对容易弄清楚其余部分。

编辑:由于赞使我对此更感兴趣。

当然,i++ + "Hello, world!\n""Hello, world!\n"[ i++ ] 是一样的。

codelark 已经指出 0 是 stdout 的 fid。(给+1,而不是给我。只是为了完整性而提到它。)

read() 函数中的 i 当然是本地变量,即 read() 中的 i-- 不会影响 main() 中的 i。由于随后的 i / i 总是等于 1,所以 -- 运算符根本没有任何作用。

哦,还要告诉面试官解雇写这段代码的人。它没有对write()的返回值进行错误检查。我可以忍受臭名昭著的糟糕代码(而且已经这样忍受了很多年),但是不检查错误比糟糕更糟糕,那是有缺陷的。 :-)

剩下的只是一些小学数学问题。我会把它交给实习生去做。 ;-)


10

在i["..."]中,字符串索引导致循环执行字符串长度的次数。i++ 加上 hello world 字符串意味着每次迭代将字符串从第一个字母开始传递给本地读取函数。

第一次迭代 = "hello.." 第二次迭代 = "ello.."

读取函数的第一个参数简化为0,表示stdout文件描述符。最后一个参数简化为1,因此每次调用只写入一个字符。这意味着每次迭代都会将传递给stdout的字符串的第一个字符写入。因此,根据上面的示例字符串:

第一次迭代 = "h" 第二次迭代 = "e"

for循环中的字符串索引与hello world字符串的长度相同,由于[]是可交换的,并且字符串是空终止的,最后一次迭代将返回空字符并退出循环。


4

虽然我不是C语言专家,但我会尽力:

i["]<i;++i){--i;}"]
// is actually
char* x = "]<i;++i){--i;}"
*(i + x)
// and will turn to zero when i == 14 (will point to string's ending zero)
// '-' - '-' and '/' / '/' are always 0 and 1
// int i is initiated to zero
// i++ will return original value, so main turns to
main()
{
    char* hello = "hello, world!\n";
    for (i = 0 ; i != 14; i++) read(0, hello[i], 1);
}

// read becomes
// i-- does nothing here, i is in read's scope, not the global one
read(j, i, p)
{
  write(1, i, 1);
}

// and at last
main()
{
    char* hello = "hello, world!\n";
    for (i = 0 ; i<14; i++) write(1, hello[i], 1);
}

1
又是一个面试问题,似乎更多地反映了面试官而不是被面试者。 关键问题如下: * j始终为0(('-' - '-') == 0) * p始终为1(('/' / '/') == 1) * i(在read函数中)是(i++ +“hello world”)== hello world中的第i个字符(然后递增i)
因此,read函数变成了:
read(0, NextChar, 1)
{
  write(1, NextChar , 1);
}

另一个关键点是for循环中[]运算符的可交换性。

理解和解析这种代码在我看来真的没有什么价值。


2
在我看来,[] 运算符的可交换性是这段代码中唯一有价值的知识。 - DevSolar
@DevSolar - 没错 - 很公平; 我编辑了我的答案以使我的意思更加清晰。 - Elemental
当我第一次遇到这个问题时,有一件事情真的让我感到惊讶。尽管我有C++的背景。 - JoshD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接