如何写一个简单编译器的编译器

如何写一个简单的编译器_百度知道用C语言来实现一个简单的虚拟机
投稿:goldensun
字体:[ ] 类型:转载 时间:
这篇文章主要介绍了用C语言来实现一个简单的虚拟机,其中栈数组的部分非常值得学习,需要的朋友可以参考下
&必要的准备工作及注意事项:
在开始之前需要做以下工作:
&&& 一个C编译器——我使用了 clang 3.4,也可以用其它支持 c99/c11 的编译器;
&&& 文本编辑器——我建议使用基于IDE的文本编辑器,我使用 E
&&& 基础编程知识——最基本的变量,流程控制,函数,数据结构等;
&&& Make 脚本——能使程序更快一点。
为什么要写个虚拟机?
有以下原因:
&&& 想深入了解计算机工作原理。本文将帮助你了解计算机底层如何工作,虚拟机提供简洁的抽象层,这不就是一个最好的学习它们原理的方法吗?
&&& 更深入了解一些编程语言是如何工作。例如,当下多种经常使用那些语言的虚拟机。包括JVM,Lua VM,FaceBook 的 Hip—Hop VM(PHP/Hack) 等。
&&& 只是因为有兴趣学习虚拟机。
我们将要实现一种非常简单的自定义的指令集。我不会讲一些高级的如位移寄存器等,希望在读过这篇文章后掌握这些。
我们的虚拟机具有一组寄存器,A,B,C,D,E, 和F。这些是通用寄存器,也就是说,它们可以用于存储任何东西。一个程序将会是一个只读指令序列。这个虚拟机是一个基于堆栈的虚拟机,也就是说它有一个可以让我们压入和弹出值的堆栈,同时还有少量可用的寄存器。这要比实现一个基于寄存器的虚拟机简单的多。
言归正传,下面是我们将要实现的指令集:
PSH 5 pushes 5 to the stack
PSH 10 pushes 10 to the stack
ADD pops two values on top of the stack, adds them pushes to stack
POP pops the value on the stack, will also print it for debugging
SET A 0 sets register A to 0
HLT stop the program
这就是我们的指令集,注意,POP 指令将会打印我们弹出的指令,这样我们就能够看到 ADD 指令工作了。我还加入了一个 SET 指令,主要是让你理解寄存器是可以访问和写入的。你也可以自己实现像MOV A B(将A的值移动到B)这样的指令。HTL 指令是为了告诉我们程序已经运行结束。
虚拟机是如何工作的呢?
现在我们已经到了本文最关键的部分,虚拟机比你想象的简单,它们遵循一个简单的模式:读取;解码;执行。首先,我们从指令集合或代码中读取下一条指令,然后将指令解码并执行解码后的指令。为简单起见,我们忽略了虚拟机的编码部分,典型的虚拟机将会把一个指令(操作码和它的操作数)打包成一个数字,然后再解码这个指令。
开始编程之前,我们需要设置好我们的项目。第一,你需要一个C编译器(我使用 clang 3.4)。还需要一个文件夹来放置我们的项目,我喜欢将我的项目放置于~/Dev:
$cd ~/Dev/
如上,我们先 cd 进入~/Dev 目录,或者任何你想放置的位置,然后新建一个目录(我称这个虚拟机为"mac")。然后再 cd 进这个目录并新建我们 src 目录,这个目录用于放置代码。
makefile 相对直接,我们不需要将什么东西分成多个文件,也不用包含任何东西,所以我们只需要用一些标志来编译文件:
SRC_FILES = main.c
CC_FLAGS = -Wall -Wextra -g -std=c11
CC = clang
${CC} ${SRC_FILES} ${CC_FLAGS} -o mac
这对目前来说已经足够了,你以后还可以改进它,但是只要它能完成这个工作,我们应该满足了。
指令编程(代码)
现在开始写虚拟机的代码了。第一,我们需要定义程序的指令。为此,我们可以使用一个枚举类型enum,因为我们的指令基本上是从0到X的数字。事实上,可以说你是在组装一个汇编文件,它会使用像 mov 这样的词,然后翻译成声明的指令。
我们可以只写一个指令文件,例如 PSH, 5 是0, 5,但是这样并不易读,所以我们使用枚举器!
typedef enum {
} InstructionS
现在我们可以将一个测试程序存储为一个数组。我们写一个简单的程序用于测试:将5和6相加,然后将他们打印出来(用POP指令)。如果你愿意,你可以定义一个指令将栈顶的值打印出来。
指令应该存储成一个数组,我将在文档的顶部定义它;但你或许会将它放在一个头文件中,下面是我们的测试程序:
const int program[] = {
上面的程序将会把5和6压入栈,调用 ADD 指令,这将会把栈顶的两个值弹出,相加后将结果压回栈中,接下来我们弹出结果,因为 POP 指令将会打印这个值,但是你不必自己再做了,我已经做好并测试过了。最后,HLT 指令结束程序。
很好,这样我们有了自己的程序。现在我们实现了虚拟机的读取,解码,求值的模式。但是要记住,我们没有解码任何东西,因为我们给出的是原始指令。也就是说我们只需要关注读取和求值!我们可以将它们简化成两个函数 fetch 和 evaluate。
取得当前指令
因为我们已经将我们的程序存成了一个数组,所以很简单的就可以取得当前指令。一个虚拟机有一个计数器,一般来说叫做程序计数器,指令指针等等,这些名字是一个意思取决于你的个人喜好。在虚拟机的代码库里,IP 或 PC 这样的简写形式也随处可见。
如果你之前有记得,我说过我们要把程序计数器以寄存器的形式存储...我们将那么做——在以后。现在,我们只是在我们代码的最顶端创建一个叫 ip 的变量,并且设置为 0。
int ip = 0;
ip 变量代表指令指针。因为我们已经将程序存成了一个数组,所以使用 ip 变量去指明程序数组中当前索引。例如,如果创建了一个被赋值了程序 ip 索引的变量 x,它将存储我们程序的第一条指令。
[假设ip为0]
int ip = 0;
int main() {
int instr = program[ip];
如果我们打印变量 instr,本来应是 PSH 的它将显示为0,因为在他是我们枚举里的第一个值。我们也可以写一个取回函数像这样:
int fetch() {
return program[ip];
这个函数将会返回当前被调用指令。太棒了,那么如果我们想要下一条指令呢?很容易,我们只要增加指令指针就好了:
int main() {
int x = fetch(); // PSH
ip++; // increment instruction pointer
int y = fetch(); // 5
那么怎样让它自己动起来呢?我们知道一个程序直到它执行 HLT 指令才会停止。因此我们使用一个无限的循环持续直到当前指令为HLT。
// INCLUDE &stdbool.h&!
bool running =
int main() {
while (running) {
int x = fetch();
if (x == HLT) running =
这工作的很好,但是有点凌乱。我们正在循环每一条指令,检查是否 HLT,如果是就停止循环,否则“吃掉”指令接着循环。
判断一条指令
因此这就是我们虚拟机的主体,然而我们想要确实的评判每一条指令,并且使它更简洁一些。好的,这个简单的虚拟机,你可以写一个“巨大”的 switch 声明。让 switch 中的每一个 case 对应一条我们定义在枚举中的指令。这个 eval 函数将使用一个简单的指令的参数来判断。我们在函数中不会使用任何指令指针递增除非我们想操作数浪费操作数。
void eval(int instr) {
switch (instr) {
因此如果我们在回到主函数,就可以像这样使用我们的 eval 函数工作:
bool running =
int ip = 0;
// instruction enum here
// eval function here
// fetch function here
int main() {
while (running) {
eval(fetch());
ip++; // increment the ip every iteration
很好,那会很完美的完成这个工作。现在,在我们加入其他指令之前,我们需要一个栈。幸运的是,栈是很容易实现的,我们仅仅需要使用一个数组而已。数组会被设置为合适的大小,这样它就能包含256个值了。我们也需要一个栈指针(常被缩写为sp)。这个指针会指向栈数组。
为了让我们对它有一个更加形象化的印象,让我们来看看这个用数组实现的栈吧:
[] // empty
PSH 5 // put 5 on **top** of the stack
[] // empty
那么,在我们的程序里发生了什么呢?
我们首先把5压入了栈
然后压入6:
接着添加指令,取出这些值,把它们加在一起并把结果压入栈中:
// pop the top value, store it in a variable called a
a = // a contains 6
[5] // stack contents
// pop the top value, store it in a variable called b
b = // b contains 5
[] // stack contents
// now we add b and a. Note we do it backwards, in addition
// this doesn't matter, but in other potential instructions
// for instance divide 5 / 6 is not the same as 6 / 5
result = b +
push result // push the result to the stack
[11] // stack contents
那么我们的栈指针在哪起作用呢?栈指针(或者说sp)一般是被设置为-1,这意味着这个指针是空的。请记住一个数组是从0开始的,如果没有初始化sp的值,那么他会被设置为C编译器放在那的一个随机值。
如果我们将3个值压栈,那么sp将变成2。所以这个数组保存了三个值:
sp指向这里(sp = 2)
&0& 1& 2 &- 数组下标
现在我们从栈上出栈一次,我们仅需要减小栈顶指针。比如我们接下来把9出栈,那么栈顶将变为5:
sp指向这里(sp = 1)
&0& 1 &- 数组下标
所以,当我们想知道栈顶内容的时候,只需要查看sp的当前值。OK,你可能想知道栈是如何工作的,现在我们用C语言实现它。很简单,和ip一样,我们也应该定义一个sp变量,记得把它赋为-1!再定义一个名为stack的数组,代码如下:
int ip = 0;
int sp = -1;
int stack[256]; // 用数组或适合此处的其它结构
// 其它C代码
现在如果我们想入栈一个值,我们先增加栈顶指针,接着设置当前sp处的值(我们刚刚增加的)。注意:这两步的顺序很重要!
// sp = -1
sp++; // sp = 0
stack[sp] = 5; // 栈顶现在变为5
所以,在我们的执行函数eval()里,可以像这样实现push出栈指令:
void eval(int instr) {
switch (instr) {
case HLT: {
case PSH: {
stack[sp] = program[++ip];
现在你看到,它和我们之前实现的eval()函数有一些不同。首先,我们把每个case语句块放到大括号里。你可能不太了解这种用法,它可以让你在每条case的作用域里定义变量。虽然现在不需要定义变量,但将来会用到。并且它可以很容易得让所有的case语句块保持一致的风格。
其次是神奇的表达式program[++ip]。它做了什么?呃,我们的程序存储在一个数组里,PSH指令需要获得一个操作数。操作数本质是一个参数,就像当你调用一个函数时,你可以给它传递一个参数。这种情况我们称作压栈数值5。我们可以通过增加指令指针(译者注:一般也叫做程序计数器)ip来获取操作数。当ip为0时,这意味着执行到了PSH指令,接下来我们希望取得下一条指令——即压栈的数值。这可以通过ip自增的方法实现(注意:增加ip的位置十分重要,我们希望在取得指令前自增,否则我们只是拿到了PSH指令),接下来需要跳到下一条指令否则会引发奇怪的错误。当然我们也可以把sp++简化到stack[++sp]里。
对于POP指令,实现非常简单。只需要减小栈顶指针,但是我一般希望能够在出栈的时候打印出栈值。
我省略了实现其它指令的代码和swtich语句,仅列出POP指令的实现:
// 记得#include &stdio.h&!
case POP: {
int val_popped = stack[sp--];
printf("popped %d\n", val_popped);
现在,POP指令能够工作了!我们刚刚做的只是把栈顶放到变量val_popped里,接着栈顶指针减一。如果我们首先栈顶减一,那么将得到一些无效值,因为sp可能取值为0,那么我们可能把stack[-1]赋给val_popped,通常这不是一个好主意。
最后是ADD指令。这条指令可能要花费你一些脑细胞,同时这也是我们需要用大括号{}实现case语句内作用域的原因。
case ADD: {
// 首先我们出栈,把数值存入变量a
int a = stack[sp--];
// 接着我们出栈,把数值存入变量b
// 接着两个变量相加,再把结果入栈
int result = a +
sp++; // 栈顶加1 **放在赋值之前**
stack[sp] = // 设置栈顶值
寄存器是虚拟机中的选配件,很容易实现。之前提到过我们可能需要六个寄存器:A,B,C,D,E和F。和实现指令集一样,我们也用一个枚举来实现它们。
typedef enum {
A, B, C, D, E, F,
NUM_OF_REGISTERS
小技巧:枚举的最后放置了一个数 NUM_OF_REGISTERS。通过这个数可以获取寄存器的个数,即便你又添加了其它的寄存器。现在我们需要一个数组为寄存器存放数值:
int registers[NUM_OF_REGISTERS];
接下来你可以读取寄存器内的值:
printf("%d\n", registers[A]); // 打印寄存器A的值
我没有在寄存器花太多心思,但你应该能够写出一些操作寄存器的指令。比如,如果你想实现任何分支跳转,可以通过把指令指针(译者注:或叫程序计数器)和/或栈顶指针存到寄存器里,或者通过实现分支指令。
前者实现起来相对快捷、简单。我们可以这样做,增加代表IP和SP的寄存器:
typedef enum {
A, B, C, D, E, F, PC, SP,
NUM_OF_REGISTERS
现在我们需要实现代码来使用指令指针和栈顶指针。一个简单的办法——删掉上面定义的sp和ip变量,用宏定义实现它们:
#define sp (registers[SP])
#define ip (registers[IP])
译者注:此处应同Registers枚举中保持一致,IP应改为PC
这个修改恰到好处,你不需要重写很多代码,同时它工作的很好。
您可能感兴趣的文章:
大家感兴趣的内容
12345678910
最近更新的内容
常用在线小工具编译相关(5)
一.makefile的作用
& & & & & Makefile是用于自动编译和链接的,一个工程有很多文件组成,每一个文件的改变都会导致工程的重新链接,但是不是所有的文件都需要重新编译,Makefile中记录有文件的信 息,在make时会决定在链接的时候需要重新编译哪些文件。Makefile的宗旨就是:让编译器知道要编译一个文件需要依赖其他的哪些文件。当那些依赖文件有了改变,编译器会自动发现最终的生成文件已经过时,而应该重新编译相应的模块。 makefile带来的好处就是—&自动化编译&,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。默认的情况下,make命令会在当前目录下按顺序找寻文件名为&GNUmakefile&、&makefile&、&Makefile&的文件,找到了解释这个文件。当然也可以使用make
-f DIR/makefile 来指定用于makefile文件
二.makefile的几点基础知识
& & & 1.赋值符号的区别
& & & & &= &是最基本的赋值
& & & := 是覆盖之前的值
& & ?= 是如果没有被赋值过就赋予等号后面的值
& & += 是添加等号后面的值
2.自动变量
& & $& & &第一个依赖文件的名称
& & $? & &所有的依赖文件,以空格分开,这些依赖文件的修改日期比目 & &标的创建日期晚
& & $@ &目标的完整名称
& & $^ & &所有的依赖文件,以空格分开,不包含重复的依赖文件
3.几个常用的函数
1. $(patsubst %.c,%.o,x.c.c bar.c)
把字串“x.c.cbar.c”符合模式[%.c]的单词替换成[%.o],返回结果是“x.c.obar.o”
2.$(filter &pattern...&,&text& )
以&pattern&模式过滤&text&字符串中的单词,保留符合模式&pattern&的单词。可以有多个模式。&
3.$(filter-out &pattern...&,&text& )
4.$(foreach &var&,&list&,&text& )
把参数&list&中的单词逐一取出放到参数&var&所指定的变量中, 然后再执行&text&所包含的表达式。每一次&text&会返回一个字符串,循环这个过程。
5.shell函数,例如files := $(shell echo *.c)
三.通用Makefile的编译过程
& & & &从顶层开始递归进入子目录,当进入到一个目录的最底层时,开始使用编译器编译,再将该层的所有.o文件打包成build-in.o,返回它的上一层目录再递归进入子目录,当编译完所有的子目录后,就开始编译顶层的.c文件,最后将顶层的.o文件和顶层每个子目录的build-in.o链接成我们的目标文件。
思维导图:
假如有这样一个目录结构的工程
顶级Makefile:
CROSS_COMPILE =
= $(CROSS_COMPILE)as
= $(CROSS_COMPILE)ld
= $(CROSS_COMPILE)gcc
= $(CC) -E
= $(CROSS_COMPILE)ar
= $(CROSS_COMPILE)nm
= $(CROSS_COMPILE)strip
= $(CROSS_COMPILE)objcopy
= $(CROSS_COMPILE)objdump
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP
CFLAGS := -Wall -O2 -g
LDFLAGS :=
export CFLAGS LDFLAGS
TOPDIR := $(shell pwd)
export TOPDIR
TARGET := test
obj-y += main.o
obj-y += a/
obj-y += b/
obj-y += c/
make -C ./ -f $(TOPDIR)/Makefile.build
$(CC) $(LDFLAGS) -o $(TARGET) built-in.o
rm -f $(shell find -name &*.o&)
rm -f $(shell find -name &*.d&)
rm -f $(TARGET)
.PHONY:all clean
这里前面就是定义一些变量,all是工程默认的目标,它是一个伪目标,进入伪目标后执行的命令就是执行Makefile.build,这里就会引起递归调用,在Makefile.build中又会调用Makefile.build.一直到Makefile.build返回以后,会使用Makefile.build最后生成的built-in.o生成最终的目标文件。
Makefile.build:
PHONY := build
subdir-y :=
include Makefile
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
#dep_files := $(wildcard $(dep_files))
#ifneq ($(dep_files),)
include $(dep_files)
PHONY += $(subdir-y)
build : $(subdir-y) built-in.o
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^
dep_file = .$@.d
$(CC) $(CFLAGS) -Wp,-MD,$(dep_file) -c -o $@ $&
.PHONY : $(PHONY)Makefile.build会加载顶级目录下的Makefile,在顶级目录下的Makefile中已经给obj-y添加了一些条目,subdir-y就是获取子目录,然后对每一个子目录又调用Makefile.build。
当递归到没有子目录的目录时,Makefile.build开始返回,并使用$(CC)对源文件进行编译,将所有的.c生成.o文件,并将当前目录下的.o和子目录下的build-in.o连接成当前目录下的build-in.o,并回返上级目录,一次往复,最终返回到顶级目录,在顶级目录下生成build-in.o。返回到顶级目录后,Makefile.build返回到了Makefile中,Makefile在使用build-in.o生成指定的目标文件。至此,递归结束,整个系统编译完成。
这里的-MD选项和.$@.d的作用是 生成头文件的依赖关系时,把依赖关系写入到这个文件中去。
要想彻底看懂的话,建议认真推倒下每一个变量的值,比对目录与文件,会更好理清编译的过程。
子目录下的Makefile:
obj-y += a.o
obj-y += d/
子目录的Makefile就是添加该目录中的文件与子目录,子目录以/结尾。
打印如下:
make -C ./ -f /home/dragon/liujinwei/uni-makefile/Makefile.build
make[1]: Entering directory `/home/dragon/liujinwei/uni-makefile'
make -C a -f /home/dragon/liujinwei/uni-makefile/Makefile.build
make[2]: Entering directory `/home/dragon/liujinwei/uni-makefile/a'
make -C d -f /home/dragon/liujinwei/uni-makefile/Makefile.build
make[3]: Entering directory `/home/dragon/liujinwei/uni-makefile/a/d'
gcc -Wall -O2 -g -Wp,-MD,.d.o.d -c -o d.o d.c
ld -r -o built-in.o d.o
make[3]: Leaving directory `/home/dragon/liujinwei/uni-makefile/a/d'
gcc -Wall -O2 -g -Wp,-MD,.a.o.d -c -o a.o a.c
ld -r -o built-in.o a.o d/built-in.o
make[2]: Leaving directory `/home/dragon/liujinwei/uni-makefile/a'
make -C b -f /home/dragon/liujinwei/uni-makefile/Makefile.build
make[2]: Entering directory `/home/dragon/liujinwei/uni-makefile/b'
gcc -Wall -O2 -g -Wp,-MD,.b.o.d -c -o b.o b.c
ld -r -o built-in.o b.o
make[2]: Leaving directory `/home/dragon/liujinwei/uni-makefile/b'
make -C c -f /home/dragon/liujinwei/uni-makefile/Makefile.build
make[2]: Entering directory `/home/dragon/liujinwei/uni-makefile/c'
gcc -Wall -O2 -g -Wp,-MD,.c.o.d -c -o c.o c.c
ld -r -o built-in.o c.o
make[2]: Leaving directory `/home/dragon/liujinwei/uni-makefile/c'
gcc -Wall -O2 -g -Wp,-MD,.main.o.d -c -o main.o main.c
ld -r -o built-in.o main.o a/built-in.o b/built-in.o c/built-in.o
make[1]: Leaving directory `/home/dragon/liujinwei/uni-makefile'
-o test built-in.o
#### make completed successfully
make之后的目录:
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:23498次
排名:千里之外
原创:48篇
评论:21条
(3)(15)(5)(6)(9)(3)(1)(2)(1)(2)初学编译原理,想写一个简单的编译器。
你可以先看看这个_(:з」∠)_……llvm官方教程让人翻译成中文了……不过好像翻译的不全,你自己可以看英文原版……嫌写词法分析语法解析程序麻烦可以找找lex+yacc的教程,网上到处都是,或者直接拿ply上……反正初学整那么麻烦也没啥意思
这个题目有点久了,现在才想起来答,主要还是因为暑假才找到空闲的时间,把我的C11编译器写完了。&br&这个编译器,(&a href=&///?target=https%3A///wgtdkp/wgtcc& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&GitHub - wgtdkp/wgtcc: a tiny C compiler in C++&i class=&icon-external&&&/i&&/a&) 一方面是为了学习 C11, 一方面为了练习C++。&br&在约11k代码中实现了:&br&&ol&&li&几乎完整的C11语法解析(除去变长数组);&/li&&li&语义与类型检查(仿gcc的错误提示)&/li&&li&预处理器&/li&&li&x86-64汇编代码生成, 多谢 &a data-hash=&a06cfb38e37dacdf4d7f032& href=&///people/a06cfb38e37dacdf4d7f032& class=&member_mention& data-editable=&true& data-title=&@RednaxelaFX& data-hovercard=&p$b$a06cfb38e37dacdf4d7f032&&@RednaxelaFX&/a& 的回答&a href=&/question/& class=&internal&&寄存器分配问题? - 编译器&/a&,把我从无休止的手动优化中拯救回来&/li&&li&精简的x86-64 ABI&/li&&/ol&心形很流行嘛,wgtcc编译的M大( &a data-hash=&1e2cccc3ce33& href=&///people/1e2cccc3ce33& class=&member_mention& data-editable=&true& data-title=&@Milo Yip& data-hovercard=&p$b$1e2cccc3ce33&&@Milo Yip&/a& ) 在 &a href=&/question/& class=&internal&&如何用C语言画一个“心形”? - C(编程语言)&/a&回答里的代码:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&cp&&#include &stdio.h&&/span&
&span class=&kt&&int&/span& &span class=&nf&&main&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&k&&for&/span& &span class=&p&&(&/span&&span class=&kt&&float&/span& &span class=&n&&y&/span& &span class=&o&&=&/span& &span class=&mf&&1.5f&/span&&span class=&p&&;&/span& &span class=&n&&y&/span& &span class=&o&&&&/span& &span class=&o&&-&/span&&span class=&mf&&1.5f&/span&&span class=&p&&;&/span& &span class=&n&&y&/span& &span class=&o&&-=&/span& &span class=&mf&&0.1f&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&k&&for&/span& &span class=&p&&(&/span&&span class=&kt&&float&/span& &span class=&n&&x&/span& &span class=&o&&=&/span& &span class=&o&&-&/span&&span class=&mf&&1.5f&/span&&span class=&p&&;&/span& &span class=&n&&x&/span& &span class=&o&&&&/span& &span class=&mf&&1.5f&/span&&span class=&p&&;&/span& &span class=&n&&x&/span& &span class=&o&&+=&/span& &span class=&mf&&0.05f&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&kt&&float&/span& &span class=&n&&a&/span& &span class=&o&&=&/span& &span class=&n&&x&/span& &span class=&o&&*&/span& &span class=&n&&x&/span& &span class=&o&&+&/span& &span class=&n&&y&/span& &span class=&o&&*&/span& &span class=&n&&y&/span& &span class=&o&&-&/span& &span class=&mi&&1&/span&&span class=&p&&;&/span&
&span class=&n&&putchar&/span&&span class=&p&&(&/span&&span class=&n&&a&/span& &span class=&o&&*&/span& &span class=&n&&a&/span& &span class=&o&&*&/span& &span class=&n&&a&/span& &span class=&o&&-&/span& &span class=&n&&x&/span& &span class=&o&&*&/span& &span class=&n&&x&/span& &span class=&o&&*&/span& &span class=&n&&y&/span& &span class=&o&&*&/span& &span class=&n&&y&/span& &span class=&o&&*&/span& &span class=&n&&y&/span& &span class=&o&&&=&/span& &span class=&mf&&0.0f&/span& &span class=&o&&?&/span& &span class=&sc&&'*'&/span& &span class=&o&&:&/span& &span class=&sc&&' '&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&n&&putchar&/span&&span class=&p&&(&/span&&span class=&sc&&'\n'&/span&&span class=&p&&);&/span&
&span class=&p&&}&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&&img src=&/cb0d2dba53e20_b.png& data-rawwidth=&645& data-rawheight=&567& class=&origin_image zh-lightbox-thumb& width=&645& data-original=&/cb0d2dba53e20_r.png&&&br&&br&C11 中有一些非常实用或者好玩的新特性,如 compound literal. 一个典型的用途是当我们想获得一个数据的另一种表示的时候, 我们可能会这么做:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&kt&&float&/span& &span class=&n&&f&/span& &span class=&o&&=&/span& &span class=&mf&&1.5&/span&&span class=&p&&;&/span&
&span class=&kt&&int&/span& &span class=&n&&i&/span& &span class=&o&&=&/span& &span class=&o&&*&/span&&span class=&p&&(&/span&&span class=&kt&&int&/span&&span class=&o&&*&/span&&span class=&p&&)&/span&&span class=&o&&&&/span&&span class=&n&&f&/span&&span class=&p&&;&/span&
&/code&&/pre&&/div&然而gcc 在开 -O2 时会报 &i&&b&break strict-aliasing rules &/b&&/i&的warning。 有了 compound literal, 我们可以这么做:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&cp&&#define CAST(s_t, d_t, sv) \&/span&
&span class=&cp&&
(union {s_ d_}){sv}.dv&/span&
&span class=&kt&&float&/span& &span class=&n&&f&/span& &span class=&o&&=&/span& &span class=&mf&&1.5&/span&&span class=&p&&;&/span&
&span class=&kt&&int&/span& &span class=&n&&i&/span& &span class=&o&&=&/span& &span class=&n&&CAST&/span&&span class=&p&&(&/span&&span class=&kt&&float&/span&&span class=&p&&,&/span& &span class=&kt&&int&/span&&span class=&p&&,&/span& &span class=&n&&f&/span&&span class=&p&&);&/span&
&/code&&/pre&&/div&而且这是一个模板呢~&br&&br&C11 也支持在identifier 中使用unicode字符了,中文编程很exciting:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&cp&&#define 整型
int&/span&
&span class=&cp&&#define 输出
printf&/span&
&span class=&cp&&#define 面函数
main&/span&
&span class=&cp&&#define 返回
return&/span&
&span class=&cp&&#define 定义
typedef&/span&
&span class=&cp&&#define 不可变
const&/span&
&span class=&cp&&#define 字符
char&/span&
&span class=&cp&&#define 指针
&span class=&cp&&#define 为&/span&
&span class=&err&&定义&/span& &span class=&err&&不可变&/span& &span class=&err&&字符&/span& &span class=&err&&指针&/span& &span class=&err&&为&/span& &span class=&err&&字面值&/span&&span class=&p&&;&/span&
&span class=&err&&整型&/span& &span class=&err&&面函数&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&err&&字面值&/span& &span class=&err&&蛤蛤&/span& &span class=&o&&=&/span& &span class=&s&&&&/span&&span class=&se&&\u82df\u5229\u56fd\u5bb6\u751f\u6b7b\u4ee5\uff0c&/span&&span class=&s&&&&/span&
&span class=&s&&&&/span&&span class=&se&&\u5c82\u56e0\uf\u907f\u8d8b\u4e4b&/span&&span class=&s&&&&/span&&span class=&p&&;&/span&
&span class=&err&&输出&/span&&span class=&p&&(&/span&&span class=&s&&&%s&/span&&span class=&se&&\n&/span&&span class=&s&&&&/span&&span class=&p&&,&/span& &span class=&err&&蛤蛤&/span&&span class=&p&&);&/span&
&span class=&err&&返回&/span& &span class=&mi&&0&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&这些例子在&i&example/&/i&目录下可以找到。&br&&br&说说写这个小编译器总结的方法吧:&br&&ol&&li&以最快的速度做到能够解析下面这段代码:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&kt&&int&/span& &span class=&nf&&main&/span&&span class=&p&&(&/span&&span class=&kt&&int&/span& &span class=&n&&argc&/span&&span class=&p&&,&/span& &span class=&kt&&char&/span&&span class=&o&&**&/span& &span class=&n&&argv&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&kt&&int&/span& &span class=&n&&i&/span&&span class=&p&&;&/span&
&span class=&k&&return&/span& &span class=&mi&&0&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&
在此之前可以写一个简单的scanner,能够达到解析常见的token就行。&/li&&li&以最快的速度看到&i&hello,world&/i&。&/li&&li&开始对照语言标准一个一个实现特性,并同步做单元测试。因为已经看到&i&hello world,&/i&这一步虽然工作量有点大,但是因为有了前面的经验,可以很快。&/li&&li&解析声明是最复杂的,所以先写解析声明。&/li&&li&龙书是必需的,一个好的参考也非常重要(如&a href=&///?target=https%3A///rui314/8cc& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&GitHub - rui314/8cc: A Small C Compiler&i class=&icon-external&&&/i&&/a&)。&/li&&li&尝试自举(因为我用的C++,所以没法自举)。&/li&&br&&br&写一个编译器的坏处是,以后写一段代码都试图在脑子里面翻译成汇编。。。&br&&br&// Update_1&br&// Date: 09/20/2016&br&匆忙写的回答,感觉仅仅是抛出结果和小结,实在是太不负责任了=-=。没有实现过的同学可能还是不知如何入手,下面就写写我是如何一步一步做的吧(某些内容只限于C编译器)&br&&br&1. 初始状态,你&b&必须&/b&有一本第二版的龙书。其它的答案可能会推荐《编译器实现》或者《编程语言实现模式》。《编译器实现》因为中文翻译的比较生硬,读了几章,发现还是龙书比较好,后来基本没有看《编译器实现》了。如果你是直接读原版,那么还是推荐龙书,毕竟都有决心读原版了,干脆彻底一点读龙书原版好了^_^。其实龙书并没有那么难,公式记不住,不理解,跳过就好了。《编程语言实现模式》其实很好的,各种实现方法都有所涉及。唯一不足的是,作者没有向《代码大全》的作者那样,对我耳提面命 ----- 直接告诉我怎么做就好了(相信这是新手最需要的...)。&br&&br&2. 你必须有C11 standard。open-std 上面的draft就够了(&a href=&///?target=http%3A//www.open-std.org/jtc1/sc22/wg14/www/docs/n1548.pdf& class=& external& target=&_blank& rel=&nofollow noreferrer&&&span class=&invisible&&http://www.&/span&&span class=&visible&&open-std.org/jtc1/sc22/&/span&&span class=&invisible&&wg14/www/docs/n1548.pdf&/span&&span class=&ellipsis&&&/span&&i class=&icon-external&&&/i&&/a&)。(如果是其他语言的编译器,对应之。如果是自己设计,那么应该学着standard那样,将grammar,constraints等写下来)&br&&br&3. 一个简单的不带优化的编译器,基本只需要3个步骤:词法分析,语法分析,代码生成;对应到代码里面,就是3个class:Scanner, Parser, G &br&&br&4. 对于C语言编译器,支持预处理器,相当于又支持了一门新的简单语言了。所以一开始不必去支持预处理器。在测试时,只使用不包含预处理器的代码就好了。或者,用gcc进行预处理,将其输出作为你的编译器的输入。&br&&br&5. 在真的动手开始写Scanner之前,有必要读n节,对个C源程序的编译步骤有基本的了解。其实ad-hoc的词法解析一点也不比写一个split()函数更复杂,尤其是开始的时候不必去记录 Source Location(当然后面如果写错误提示,可以再加进来)。实现的时候,也不要吝惜内存了,直接将所有的token一次解析掉存起来就好。因为这对于后面Parser要回溯的时候会方便一点。&br&&br&6. 已经得到Token List 之后,就可以开始做Parser部分了。暂时不做错误恢复,即遇到错误即exit。这里有一个很实用的设计建议:准备好下面这四个函数:&br&a. Peek() 返回下一个Token(只测试该Token,不向前移动Token List的offset指针)&br&b. Next() 消费下一个Token&br&c. Expect(expectedToken) , 这代替了下面的这段错误处理:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&Peek&/span&&span class=&p&&()&/span& &span class=&o&&!=&/span& &span class=&n&&expectedToken&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&n&&Error&/span&&span class=&p&&(&/span&&span class=&s&&&expect %s, but got %s&/span&&span class=&se&&\n&/span&&span class=&s&&&&/span&&span class=&p&&,&/span& &span class=&n&&expectedToken&/span&&span class=&p&&,&/span& &span class=&n&&Peek&/span&&span class=&p&&());&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&d. Try(expectedToken), 这代替了下面这段代码:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&k&&if&/span& &span class=&p&&(&/span&&span class=&n&&Peek&/span&&span class=&p&&()&/span& &span class=&o&&==&/span& &span class=&n&&expectedToken&/span&&span class=&p&&)&/span& &span class=&p&&{&/span&
&span class=&n&&Next&/span&&span class=&p&&();&/span& &span class=&c1&&// 消费之&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&
这很有用,因为Parser里面可能会有上百个这段代码,在我的Parser里面,有84个Expect()调用,81个Peek()(Peek和Test), 39个Next(),62个Try()。相信我,这4个函数会让你的代码干净一倍。&br&&br&7. C的语言组成,大致可以分为 Expression, Declaration, Statement and Block 三个部分。这里面Statement and Block是最简单的,Declaration难一点。按照我前面的心得体验,应该从简单的入手,但是我们非先做Declaration不可。因为Statements都是在函数内的啊,不搞定Declaration就没法继续了呢~ 其实,Declaration也好做,对着n1548的7.A.2.2节一个一个将grammar翻译为Parser里面的逻辑就好了(是的,除去语义和类型检查,Parser就是真么简单)。这里有一个小的tip:&br&&div class=&highlight&&&pre&&code class=&language-text&&int arr[10][5];
&/code&&/pre&&/div&读到第一个']'时,我们看到的arr的类型是:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&c1&&// Array&/span&
&span class=&c1&&//
length: 10&/span&
&span class=&c1&&//
derivedType: Integer&/span&
&/code&&/pre&&/div&读到第二个']'时,我们看到的arr的类型是:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&c1&&// Array&/span&
&span class=&c1&&//
length: 5&/span&
&span class=&c1&&//
derivedType: Array(length:10, derivedType:Integer)&/span&
&/code&&/pre&&/div&(不要纠结于这里的derivedType,和面向对象语言的不一样,但是standard就是这么叫的=——=)&br&是的,这么顺序解析,我们得到了一个错误的类型,因为arr的正确类型应该是:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&c1&&// Array&/span&
&span class=&c1&&//
length: 10&/span&
&span class=&c1&&//
derivedType: Array(length:5, derivedType:Integer)&/span&
&/code&&/pre&&/div&我的方法是在声明结束时对type做fix, 递归地提升derivedType为当前Type。哈哈,我竟然一次就写对了^_^。当然,你也可以在每一次遇到&[]& &()&(函数声明后缀)时就做fix。&br&做完Declaration,你还没有往AST上添加任何node,是的,仅仅是Declaration,是没有一行代码需要生成的。所有的成就都在符号表里面。这里又有tip:暂时不要做Initializer,它有一点烦人的(C标准把它搞得好繁琐)。&br&&br&8. struct/union 类型;如果只是支持一个小小的子集,那么大可以跳过这一部分不做。struct会引入一些工作量,一方面,需要为tag(tag 和普通的identifier不在同一个命名空间)另开一个符号表(我使用一个小trick避免了这个麻烦);另一方面,它也是使Initalizer变复杂的原因之一,尤其是匿名struct/union。tip:对struct/union的支持步骤是:普通嵌套的结构,匿名无tag的 struct成员,位域,union;这里把union放到最后是因为,它和struct除去存储布局之外,别无二致(所以你甚至不必区分这两个类型);你也可以在任何一步就开始支持union ^_^&br&&br&9. 数组和函数;除去作为sizeof关键字的操作数,数组总是会被cast成一个指针;你一定写过这样的代码:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&k&&typedef&/span& &span class=&nf&&void&/span& &span class=&p&&(&/span&&span class=&o&&*&/span&&span class=&kt&&func_t&/span&&span class=&p&&)(&/span&&span class=&kt&&void&/span&&span class=&p&&);&/span&
&span class=&kt&&func_t&/span& &span class=&n&&f&/span& &span class=&o&&=&/span& &span class=&n&&func&/span&&span class=&p&&;&/span&
&span class=&n&&f&/span&&span class=&p&&();&/span& &span class=&c1&&// 难道不应该是 (*f)(); ?&/span&
&/code&&/pre&&/div&其实函数也是被cast成指针了,所以上面的调用方式都对,更微妙的是,任何一个函数调用都会被先cast成指针,再解引用(至少我的实现是这样的);&br&&br&10. storage 和 linkage;起初只实现所有的对象分配在栈空间;这会大大简化代码生成部分,因此对于“以最快速度看到&i&hello world&/i&”是非常重要的;linkage对于前向声明是重要的,如果你没有打算支持,那么也可以跳过,等你看到&i&hello world&/i&再回来支持,或者等你的函数和标准库的冲突了^_^&br&&br&11. expression;这个最简单,该怎么样就怎么样=-=。tip:联合赋值运算符:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&n&&a&/span& &span class=&o&&*=&/span& &span class=&mi&&5&/span&&span class=&p&&;&/span&
&span class=&n&&a&/span& &span class=&o&&=&/span& &span class=&n&&a&/span& &span class=&o&&*&/span& &span class=&mi&&5&/span&&span class=&p&&;&/span&
&/code&&/pre&&/div&是不是总是被告知效果和下面那行效果相等?那么不要害羞,就这么翻译!(嗯,这么做是会产生bug(如果左值表达式有副作用),但是可以通过额外的检查规避掉;对于带优化的编译器,这根本不是问题,因为它们怎么会对公共子表达式求两遍值呢?)&br&&br&12. statement;这是我最喜欢的部分,不仅仅是因为它简单,而且让我明白那些控制语句是怎么生成到汇编代码的(对应请看龙书6.6和6.7节);如最简单的&b&&i&while&/i&&/b&循环的展开:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&cm&&/*&/span&
&span class=&cm&& * while (expression) statement&/span&
&span class=&cm&& * 展开后:&/span&
&span class=&cm&& * cond: &/span&
&span class=&cm&& *
if (expression) then&/span&
&span class=&cm&& *
empty&/span&
&span class=&cm&& *
else&/span&
&span class=&cm&& *
goto end&/span&
&span class=&cm&& *
statement&/span&
&span class=&cm&& *
goto cond&/span&
&span class=&cm&& * end:&/span&
&span class=&cm&& */&/span&
&/code&&/pre&&/div&这里,我将 &b&if &/b&语句保留为基本的翻译单元,因为将其他的控制结构翻译为 &b&if &/b&语句会带来很大的便利。tip:支持顺序:if-else, while/do-while, for, switch- &br&&br&这些基本是一个C语言Parser的动手步骤了,现在你可以parse这段代码了:&br&&div class=&highlight&&&pre&&code class=&language-c&&&span class=&kt&&int&/span& &span class=&nf&&main&/span&&span class=&p&&()&/span& &span class=&p&&{&/span&
&span class=&n&&puts&/span&&span class=&p&&(&/span&&span class=&s&&&hello world&/span&&span class=&se&&\n&/span&&span class=&s&&&&/span&&span class=&p&&);&/span&
&span class=&k&&return&/span& &span class=&mi&&0&/span&&span class=&p&&;&/span&
&span class=&p&&}&/span&
&/code&&/pre&&/div&你可以手动将 puts插入到符号表以应付过去(某些builtin的函数还真就是这么干的),或者你就要实现对struct/union的支持, 不然是没有办法 #include &stdio.h& 的了。这里没有使用 printf,因为暂时没有必要实现变参函数。&br&这样,你与hello world只有一步之遥了:汇编代码生成。&br&// TODO(wgtdkp): 汇编代码生成&br&// End of update_1&br&&br&// Update_2&br&// Date: 09/21/2016&br&因为按照&以最快的速度看到hello world&来做的话,语义检查和类型检查可以暂且放一放,或者只是实现parse过程中必不可少的一部分。下面是以x86-64 汇编代码生成为例,说说代码生成。这里又可以跳过中间代码生成,直接由AST生成汇编代码~&br&&br&1. intel x86-64 手册;显然这是&b&必需&/b&的,虽然我拿到这3000多页的手册时,也是虎躯一震的。不过,实实在在地讲,我只看了大概30页的内容;更多的时候,我是对照着gcc生成的汇编代码照虎画猫; tip:对于某些指令,如乘除法,移位,对照gcc进行翻译是非常有效的;但你不应该企图生成像gcc那么高效的代码!(具体方法见下面)&br&&br&2. System V x64 ABI;你&b&必须&/b&至少看chapter 3(chapter 3就够用了, 不过只有30多页,放心吧^_^);至少掌握stack frame的结构和对齐。注意x86-64的调用规约会稍微复杂一点,不过你可以做一些大胆的简化:&br&
a. scalar type (除去struct/union,剩下的都是)按照 ABI里面的就好;&br&
b. struct/union 是比较复杂的,这是可以直接按照stack传参(而不是寄存器传参去做),毕竟又有多少代码会直接传递struct/union 呢?^_^,等到你决意要做一个full featured的编译器时,再来考虑它吧&br&可以参考这里&a href=&///?target=https%3A//www3.nd.edu/%7Edthain/courses/cse40243/fall2015/intel-intro.html& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&Introduction to X86-64 Assembly for Compiler Writers&i class=&icon-external&&&/i&&/a&&br&&br&3. visitor 模式;相信这个不必赘述,取决于怎么使用它,可以有很多作用;比如Parser中会需要做常量表达式求值,我们会用到它;获得一个表达式的左值地址时,我们又需要用到它;(考虑你该如何翻译赋值表达式)&br&&br&4. 数据类型;在代码生成这一步,我们的数据类型只有3种:整型,浮点型,struct/union(集合);我将能够用通用寄存器存储的数据称为整型,包括指针;struct/union的处理比较特殊,对于一个类型为struct/union的表达式,visit该表达式,总是得到此struct/union对象的地址值(存储于%rax寄存器中);只要我们在所有代码中遵守这一规则,那么它确实很管用,即使会有一些冗余的拷贝;&br&&br&5. 翻译函数定义;一个函数的翻译可以分为下面几个步骤:&br&&ol&&li&保存栈指针;&/li&&li&确定函数参数的位置(在哪个寄存器或者stack上的位置),将它们复制到当前stack frame的上,更新每个参数(object)的offset成员(object的地址)。更新当前stack frame的偏移量;&/li&&li&确定当前作用域内的object的地址, 这只需要扫描当前scope内的所有object,并线性地分配到stack frame上面就好;注意不包括内层scope内的定义的object。这是一种优化,能够充分利用栈空间,而且实现更简单。更新当前的stack frame偏移量。&/li&&li&翻译函数体;&/li&&li&在return 语句和函数结尾处,恢复栈指针并退出函数;&/li&&/ol&&br&6. 翻译函数调用;也可以分为下面几个步骤:&br&&ol&&li&确定函数地址;这可能是函数的名字,也可能是一个寄存器;这里就需要一个能够计算表达式左值地址的Evaluator了^_^(后面会讲到);&/li&&li&对函数参数求值,暂存之(push到stack上);&/li&&li&确定函数参数的位置,即,应该在哪个寄存器或stack的位置;拷贝参数值到对应位置;&/li&&li&调整栈指针以正确地对齐(这个很重要,不然会有segment fault的,都是泪);&/li&&li&调用之~&/li&&/ol&&br&7. 翻译赋值表达式;对左值表达式的赋值,需要获得左值表达式的地址,而不是值;因此我们需要一个 LValGenerator 来取得左值表达式的地址,然后将右操作数的值load到该地址中;&br&&br&8. 翻译表达式;建议采用1-TOSCA方法,不懂的可以看看R大的回答&a href=&/question/& class=&internal&&寄存器分配问题? - 编译器&/a&;这里的tip:不要被gcc生成的各种高效代码蛊惑了而去做大量的手动优化,那是一个很大的坑,尤其是我们没有生成中间代码,是做不到全局寄存器分配的效果的。&br&&br&// End of update_2&br&&br&&br&// 谢谢大家的支持^_^&/ol&
这个题目有点久了,现在才想起来答,主要还是因为暑假才找到空闲的时间,把我的C11编译器写完了。 这个编译器,() 一方面是为了学习 C11, 一方面为了练习C++。 在约11k代码中实现了: 几乎完整的C11语法解…
以学习为目的写的一个简单的编译器原型(详情见github)。&a href=&///?target=https%3A///arctanx0/Woo& class=& wrap external& target=&_blank& rel=&nofollow noreferrer&&GitHub - arctanx0/Woo: a simple compiler written by c++&i class=&icon-external&&&/i&&/a&。&br&&br&上学期学编译原理的时候,先是看着轮子哥 &a data-hash=&ecc0ec035f& href=&///people/ecc0ec035f& class=&member_mention& data-editable=&true& data-title=&@vczh& data-hovercard=&p$b$ecc0ec035f&&@vczh&/a& 的博客写了一个简单的正则引擎。之后看到这个问题和 &a data-hash=&406e39f95acc6ff8ede4& href=&///people/406e39f95acc6ff8ede4& class=&member_mention& data-editable=&true& data-title=&@Leviathan& data-hovercard=&p$b$406e39f95acc6ff8ede4&&@Leviathan&/a& 的答案,备受鼓舞,开始写这个编译器。&br&&br&现在回过头来看,学到的东西最多的还是来自轮子哥的文章,可惜后面关于语法分析器的系列文章我没能潜心看下去,而是偷懒参考了llvm tutorial里的那种方法。后端是自己构造的一个简易的虚拟机。&br&&br&这是我第一个完整的c++项目,代码还没有重构,拿出来只是为了分享下战胜自己的喜悦。写的时候会产生很多种畏难情绪,但是仔细一想,觉得半途而废的感觉更加恶心,所以坚持下来了。&br&&br&写完后,第一件事就是拿 &a data-hash=&406e39f95acc6ff8ede4& href=&///people/406e39f95acc6ff8ede4& class=&member_mention& data-editable=&true& data-title=&@Leviathan& data-hovercard=&p$b$406e39f95acc6ff8ede4&&@Leviathan&/a&的那段心形代码测试了一下,看到这个结果还是挺开心的。&br&&img src=&/ee5a3c2fc8f50dfdfef2386_b.png& data-rawwidth=&718& data-rawheight=&464& class=&origin_image zh-lightbox-thumb& width=&718& data-original=&/ee5a3c2fc8f50dfdfef2386_r.png&&错误提示功能:&br&&img src=&/acd3dee850caec015ef24_b.png& data-rawwidth=&1081& data-rawheight=&825& class=&origin_image zh-lightbox-thumb& width=&1081& data-original=&/acd3dee850caec015ef24_r.png&&===========================&br&收到这么多赞真是不胜惶恐。。。&br&多谢前辈 &a data-hash=&ecc0ec035f& href=&///people/ecc0ec035f& class=&member_mention& data-editable=&true& data-title=&@vczh& data-hovercard=&p$b$ecc0ec035f&&@vczh&/a& 的支持,你一直是我的精神领袖。。
以学习为目的写的一个简单的编译器原型(详情见github)。。 上学期学编译原理的时候,先是看着轮子哥
的博客写了一个简单的正则引擎。之后看到这个问题和
的答案,备受鼓舞,开…
已有帐号?
无法登录?
社交帐号登录
幼生期程序猿_(:з」∠)_

我要回帖

更多关于 简单的c语言编译器 的文章

 

随机推荐