脑急转弯程序的“Hello World”是如何工作的?

148

有人把这个发送给我,并声称这是Brainfuck语言的Hello World(我希望是这样...)

++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.

我知道它的基本原理是通过移动指针并增减数据来工作...

但我仍然想知道,它到底是如何工作的?它是如何在屏幕上打印任何东西的?它是如何编码文本的?我完全不理解...


39
用这种语言编写的应用程序肯定很难维护。 - Déjà vu
26
@ring0:不,那是一种只写语言。 - LetMeSOThat4U
1
它有什么实际用途? - Yash Kumar Verma
11
@YashVerma 不需要一个... - Insane
65
在语言名称中已经明确指出了。 - Mateen Ulhaq
6个回答

300

1. 基础知识

要理解Brainfuck,你需要想象一个由0初始化的无限单元格数组。

...[0][0][0][0][0]...

当brainfuck程序启动时,它会指向任何一个单元格。

...[0][0][*0*][0][0]...

如果将指针向右移动>,就是将指针从单元格X移动到单元格X+1。

...[0][0][0][*0*][0]...

如果增加单元格数值+,你将得到:

...[0][0][0][*1*][0]...

如果您再次增加单元格的值+,您将得到:

...[0][0][0][*2*][0]...

如果你减少单元格的值-,你会得到:
...[0][0][0][*1*][0]...

如果你将指针向左移动 <,那么你就是将指针从单元格 X 移动到单元格 X-1。
...[0][0][*0*][1][0]...

2. 输入

要读取字符,您需要使用逗号 ,。它的作用是:从标准输入中读取字符,并将其十进制 ASCII 码写入实际单元格。

请参考 ASCII 表。例如,! 的十进制码是 33,而 a 的十进制码是 97

好的,现在想象一下您的BF程序内存看起来像:

...[0][0][*0*][0][0]...

假设标准输入代表 a,如果您使用逗号 , 运算符,则BF会将十进制ASCII码 97 读入内存:

...[0][0][*97*][0][0]...

你通常希望那样想,但事实要更加复杂一些。事实上,BF 不是读一个字符,而是读一个字节(无论这个字节是什么)。让我举个例子:
在 Linux 中
$ printf ł

输出:

ł

这是一个特定的波兰字符,无法由 ASCII 编码表示。在此情况下使用 UTF-8 编码,因此它需要在计算机内存中占用多个字节。我们可以通过进行十六进制转储来证明:

$ printf ł | hd

这显示:

00000000  c5 82                                             |..|

零偏移。第一个字节是82,第二个字节是c5,表示我们要读取的字符为ł。其中|..|是不可能在这种情况下出现的图形化表示。
如果您将ł作为输入传递给只读取单个字节的BF程序,则程序内存将如下所示:
...[0][0][*197*][0][0]...

为什么是197?因为十进制的197等于十六进制的c5。感觉很熟悉吗?当然了。它就是字母ł的第一个字节!

3. 输出

要打印字符,你需要使用点号.。它的作用是:假设我们把实际单元格的值视为十进制ASCII码,将相应的字符打印到标准输出。

好了,现在让我们假设你的BF程序内存看起来像这样:

...[0][0][*97*][0][0]...

如果现在使用点(.)运算符,BF会输出:

a

因为 ASCII 中的a十进制代码是 97

例如,BF程序如下(97个加号和2个点):

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++..

将使所指向的单元格的值增加到97,并将其打印出来两次。

aa

4. 循环

在 BF 中,循环由循环开始符号 [ 和循环结束符号 ] 组成。您可以将其视为 C/C++ 中的 while,其中条件是实际单元格值。

请看下面的 BF 程序:

++[]

++ 会使实际单元格的值增加两次:

...[0][0][*2*][0][0]...

[] 就像 while(2) {},所以它是一个无限循环。

假设我们不想让这个循环无限制地执行下去,我们可以采取以下措施:

++[-]

每次循环时,它都会减少实际单元格值。一旦实际单元格值为0,循环就会结束。
...[0][0][*2*][0][0]...        loop starts
...[0][0][*1*][0][0]...        after first iteration
...[0][0][*0*][0][0]...        after second iteration (loop ends)

让我们再考虑一个有限循环的例子:

++[>]

这个例子表明,我们不必在循环开始的单元格结束循环。
...[0][0][*2*][0][0]...        loop starts
...[0][0][2][*0*][0]...        after first iteration (loop ends)

然而,回到起点是个好习惯。为什么呢?因为如果循环结束在它开始的另一个单元格,我们就不能确定单元格指针的位置。说实话,这种做法可以让Brainfuck变得不那么晦涩难懂。

4
好的,现在我明白了 :) - speeder
33
那是一个完美的解决方案,适合初学者理解这种语言思想。恭喜你,写得很好。 - Casey ScriptFu Pharr
4
我看过的最好的Brainfuck介绍。老实说,你的帖子有点让BF失去了它的魅力。 - Boyang
3
我想如果你需要一个业余时间的项目,你可以为Brainfuck添加Unicode支持。 - Álvaro González
3
在你的帖子之后,BF不再是BF了! - thanos.a
显示剩余4条评论

57

维基百科有一份注释版的代码。

+++++ +++++             initialize counter (cell #0) to 10
[                       use loop to set the next four cells to 70/100/30/10
    > +++++ ++              add  7 to cell #1
    > +++++ +++++           add 10 to cell #2 
    > +++                   add  3 to cell #3
    > +                     add  1 to cell #4
    <<<< -                  decrement counter (cell #0)
]                   
> ++ .                  print 'H'
> + .                   print 'e'
+++++ ++ .              print 'l'
.                       print 'l'
+++ .                   print 'o'
> ++ .                  print ' '
<< +++++ +++++ +++++ .  print 'W'
> .                     print 'o'
+++ .                   print 'r'
----- - .               print 'l'
----- --- .             print 'd'
> + .                   print '!'
> .                     print '\n'

为了回答你的问题,,.字符用于输入/输出。文本采用ASCII编码。 维基百科文章也进一步深入讨论了这个话题。
第一行将a [0] = 10初始化为从0开始递增十次。第2行的循环有效地设置了数组的初始值:a [1] = 70(接近字符“H”的ASCII代码72),a [2] = 100(接近字符“e”的101或'e'),a [3] = 30(接近空格的代码32)和a [4] = 10(换行符)。循环通过在每次循环中分别向单元格a [1]a [2]a [3]a [4]添加7、10、3和1来工作,总共对每个单元格进行10次加法运算(得到a [1]=70等)。循环结束后,a [0]为零。>++。然后将指针移动到a [1],其值为70,将其加上2(产生72,即大写字母H的ASCII字符代码),并输出它。
下一行将数组指针移动到a [2]并将其加1,生成101,即小写字母'e',然后输出。
由于'l'恰好是'e'之后的第七个字母,因此需要将另外七个(+++++++)添加到a [2]中,然后将结果输出两次。
'o'是'l'之后的第三个字母,因此需要将a [2]再增加三次并输出结果。
程序的其余部分以相同的方式继续进行。对于空格和大写字母,会选择不同的数组单元格并根据需要进行递增或递减。

但是为什么它会打印?或者说它是如何打印的?注释向我解释了这行代码的意图,现在需要知道它具体做了什么。 - speeder
9
它之所以会打印输出,是因为编译器知道逗号和句点用于输入/输出,就像 C 通过使用 putchar 来打印输出一样。这是由编译器处理的实现细节。 - ken
1
同时也因为它将“Hello World”中ASCII字符所需的单元格设置为整数值。 - slugonamission
1
我期望有更深入的解释...但是 :/ - speeder
1
@speeder - 我在答案中添加了来自维基百科的代码详细解释。您可以查看链接的文章获取更多信息。 - ken

11

Brainfuck,顾名思义。它只使用了 8 个字符> [ . ] , - +,这使得它是最快速学习的编程语言,但也是最难实现和理解的编程语言。最终你会被逼疯。

它在数组中存储值:[72] [101] [108] [111]

初始化指针指向数组的第1个单元格:

  1. > 将指针向右移动1个位置

  2. < 将指针向左移动1个位置

  3. + 将单元格的值加1

  4. - 将单元格的值减1

  5. . 输出当前单元格的值

  6. , 输入数据到当前单元格

  7. [ ] 循环,如 +++[-],表示循环3次,- 运算符将计数器的值减1

存储在单元格中的值是 ASCII 值:

因此,参考上面的数组:[72] [101] [108] [108] [111],如果你匹配 ASCII 值,你会发现它写的是 Hello

恭喜!你已经学会了 BF 的语法。

——-更多内容———

让我们编写第一个程序,即 Hello World,之后你就能用这种语言写下自己的名字了。

+++++ +++++[> +++++ ++ >+++++ +++++ >+++ >+ <<<-]>++.>+.+++++ ++..+++.++.+++++ +++++ +++++.>.+++.----- -.----- ---.>+.>.

分解成若干部分:

+++++ +++++[> +++++ ++ 
                  >+++++ +++++ 
                  >+++ 
                  >+ 
                  <<<-]

创建一个包含4个元素的数组(元素数量为>),并设置一个类似于10的计数器: —-伪代码—-
array =[7,10,3,1]
i=10
while i>0:
 element +=element
 i-=1

因为计数器的值存储在单元格0中,>移动到单元格1并将其值更新为+7,>移动到单元格2并将其先前的值增加10,以此类推...。 <<< 返回到单元格0并将其值减1。
因此,在循环完成后,我们有一个数组:[70,100,30,10]。
>++. 

移动到第一个元素并将其值增加2(两个'+'),然后使用该ASCII值打印('.')字符。例如在Python中:

chr(70+2) # 打印'H'

>+.

移动到第二个单元格,将其值增加1到101并打印('.')其值,即chr(101) chr(101)#打印'e' 现在下一段中没有>或<,因此它只取最新元素的当前值并将其递增

+++++ ++..

最新元素 = 101 因此,101 + 7 并将其打印两次(因为有两个“..”)chr(108)#打印l两次 可以用作

for i in array:
    for j in range(i.count(‘.’)):
           print_value

———它在哪里被使用?——-

这只是一种用于挑战程序员的玩笑语言,实际上没有被广泛应用。


9
为了回答关于它如何知道打印什么的问题,我在打印代码右侧添加了ASCII值的计算:
> just means move to the next cell
< just means move to the previous cell
+ and - are used for increment and decrement respectively. The value of the cell is updated when the increment/decrement happens

+++++ +++++             initialize counter (cell #0) to 10

[                       use loop to set the next four cells to 70/100/30/10

> +++++ ++              add  7 to cell #1

> +++++ +++++           add 10 to cell #2 

> +++                   add  3 to cell #3

> +                     add  1 to cell #4

<<<< -                  decrement counter (cell #0)

]            

> ++ .                  print 'H' (ascii: 70+2 = 72) //70 is value in current cell. The two +s increment the value of the current cell by 2

> + .                   print 'e' (ascii: 100+1 = 101)

+++++ ++ .              print 'l' (ascii: 101+7 = 108)

.                       print 'l' dot prints same thing again

+++ .                   print 'o' (ascii: 108+3 = 111)

> ++ .                  print ' ' (ascii: 30+2 = 32)

<< +++++ +++++ +++++ .  print 'W' (ascii: 72+15 = 87)

> .                     print 'o' (ascii: 111)

+++ .                   print 'r' (ascii: 111+3 = 114)

----- - .               print 'l' (ascii: 114-6 = 108)

----- --- .             print 'd' (ascii: 108-8 = 100)

> + .                   print '!' (ascii: 32+1 = 33)

> .                     print '\n'(ascii: 10)

5
所有答案都很详细,但它们缺少一个小细节:打印。 在构建您的Brainfuck翻译器时,您还要考虑字符,这实际上就是Brainfuck中打印语句的样子。因此,当您的Brainfuck翻译器遇到字符时,它应该打印当前指向的字节。
例如: 假设您有--> char *ptr = [0] [0] [0] [97] [0]... 如果这是一个Brainfuck语句:>>>.,则您的指针应该向右移动3个空格,落在:[97],所以现在*ptr = 97,完成后您的翻译器遇到,然后应该调用
write(1, ptr, 1)

或者任何等效的打印语句来打印当前指向的字节,该字节的值为97,并且字母a将被打印在std_output上。


2
我认为你想知道的是Brainfuck如何处理所有代码。解析器是用高级语言编写的,例如Python,以解释代码中点号或加法符号的含义。
因此,解析器将逐行读取您的代码,并说:“好的,有一个>符号,所以我必须推进内存位置。这段代码很简单,如果(该内存位置中的内容)==>,则memlocation = + memlocation,这是用高级语言编写的。同样,如果(内存位置中的内容)==“。”,那么打印(内存位置的内容)。
希望这能澄清问题。tc

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