#头条创作挑战赛#
scf,是我用C语言写的一个编译器框架。
它可以把类似C语言的简单代码编译成可执行文件,暂时只支持x64的CPU和Linux系统。
语法的设计基本上参照了C语言,有一点点的改动:
1,取消了typedef 这类因为C语言的历史遗留而导致的关键字。
2,结构体类型在定义变量时不需要带struct关键字。
C语言如果没有提前用typedef 声明结构体类型的话,在定义变量时必须带struct,例如:
struct A* p;
不能直接用A* p; 除非前面用typedef struct {} A; 声明过A。
这个问题也是C语言的历史遗留问题。
实际上在语法分析时直接忽略掉声明语句中的struct并不难,C++就是这样的,但C语言或许是为了版本兼容还是添加了typedef关键字。
我在scf里把它去掉了,只在定义结构体时才需要struct,在定义变量或指针时不需要struct。
3,对结构体的所有操作c++结构体初始化,在实现上都通过指针。
结构体,实际上是个成员变量的字节数不固定的“数组”。
struct A {
double d;
int i;
char c;
};
int a[3];
结构体A和数组a都有3个成员,区别只是结构体的成员的字节数不一样,而数组的所有成员的字节数一样。
访问结构体的元素,实际上跟访问数组的元素差不多,都是首地址+偏移量。
只是数组的偏移量,是单个成员的字节数*成员的索引号。
结构体的偏移量,是当前成员前面的其他成员加起来的字节数。
如果结构体当函数实参的话,传指针就行,额外把结构体复制一份没意义。
传参、取结构体的成员等场景,scf全使用的指针,不支持复制结构体。
所以,结构体的运算符一律使用的->,没有使用点号.
即使这样:
struct A { int x, y; };
A a;
也是a->x = 1;
而不是C语言的a.x = 1;
我在scf框架的底层没有区分这两种运算,不管是结构体取成员、还是结构体的指针取成员,都一律按结构体的指针取成员。
实际上就算是a.x = 1,到了生成机器指令的时候:也是先加载a的地址,然后根据x的偏移量赋值为1,实际还是(&a)->x = 1.
C语言对->和.做区分,我觉得还是为了后端考虑:
使用p->x = 1是指针,生成机器指令直接计算p与x的偏移量就行;
使用a.x = 1是结构体,生成机器指令时要先加载a的地址,然后再计算&a与x的偏移量;
不同的运算符,在这里是很强的提示信息,后端代码要好写一些。
如果使用同样的运算符->,编译器就要记录p和a的变量类型(p是指针,a是结构体),并做区分处理。
但是,编译器本来就要记录变量类型的(这个区分并不难),所以我就把点号运算去掉了[呲牙]
从少打字的角度考虑,->和.里选点号更好。
但是C程序员都打惯了->了,所以我还是选了->。
4,结构体的赋值运算,
结构体的赋值,和结构体指针的赋值解引用,我在设计语法时纠结了很久!
例如:
A* p;
A* q;
A a;
A b;
p和q是结构体的指针,a和b是结构体。
如果a和b之间赋值的话c++结构体初始化,可以这么写a = b;
如果p和q之间赋值的话,就存在2个语义:
1)指针赋值,
p = q;
不涉及到结构体成员的拷贝,只是让8个字节的指针变量p = q就行。
2)指针的赋值解引用,实际是结构体赋值。
例如,*p = *q;
它的抽象语法树如下:
*p = *q的抽象语法树
要想处理这种情况,不得不去查看=两边的类型。
两个星号*的运算结果都是结构体,而不是结构体的指针。
但是,这种情况没法直接处理等号=的两个子节点,而必须接着往下找,才能找到真正要处理的变量p和q。
a=b和p=q的抽象语法树
并且,a = b, p = q, *p = *q,这三个的抽象语法树非常的接近,在处理时还得判断等号的两边到底是结构体、结构体指针、还是星号运算符。
我一开始并不想做这么复杂的判断,纠结了好久也没想到捷径,最后还是把语法设计成这样了。
另外,赋值运算符还可能被重载了,也需要在这里考虑。
所以,赋值运算符=的语义分析也写得比较复杂。
scf编译器只有1种情况直接使用结构体,而不是使用它的指针,就是对结构体的赋值:
struct A { int x, y; };
A a, b;
a = b;
A c = {1, 2};
毕竟结构体类型的变量占的内存,跟结构体的指针占的内存字节数不一样。
其他情况都是使用结构体的指针,例如:传参,访问成员变量,指针赋值,etc.
5,结构体的声明和定义,都必须在全局作用域。
不支持在结构体定义里再嵌套成员结构体的定义。
但是,支持在结构体定义里嵌套结构体类型的成员变量:这个不得不支持,否则数据结构就没法写了[捂脸]
如下这样的代码是不支持的:
struct A{
struct B {} b;
struct C {};
C c;
};
虽然C语言支持,但我觉得没必要搞这么复杂:
1)把所有结构体的定义都放在全局作用域,反而更简单。
2)把结构体类型的定义和结构体变量的声明分开,反而更简单。
所以,我设计的语法是如下这样的:
struct B {};
struct C {};
struct A {
B b;
C c;
};
6,运算符和构造函数的重载,
为了让数学公式写起来方便,我加了运算符和构造函数的重载。
但是,没有支持虚函数、继承、封装,这3个OOP机制。
C++的祖传代码,都是因为虚函数、继承、封装[捂脸]所以我把这3样全去掉了。
我觉得,保留运算符和构造函数的重载就可以了。
至于OOP机制,还是用结构体+函数指针吧[呲牙]
struct A;
struct Aops {
int open(A** pp);
void close(A* p);
};
struct A {
Aops* ops;
void* priv;
};
我觉得,C风格的结构体+函数指针的OOP就很好。
尽量把A->ops的初始化放在open()函数里,不要半截里再改变。
半截里改变函数指针,是很让人烦的。
7,去掉了头文件,也没支持宏,
如果一个文件里写得只是函数声明和类型定义,那么它就是头文件。
没有特意用.c和.h区分。
任何2个文件之间都可以包含,只要不构成递归包含!
所以,只保留了include用来包含文件,而且把它作为了关键字。
C语言的花样宏定义,我都把它去掉了。
8,struct和class这两个关键字,是完全没有差别的。
C++里的struct和class也只是封装权限的差别。
我既然把封装去掉了,这两个就没有差别了。
9,自动内存管理,
scf有两种内存管理模式:自动管理,手动管理。
如果是使用create关键字创建的(结构体)类对象,那么scf会自动管理内存:
在变量离开作用域时,会调用程序员写的__release()函数释放内存。
scf的类的构造函数和析构函数:
int __init(A* this, …); // 构造函数
void __release(A* this); // 析构函数
一个叫__init(),一个叫__release()。
为了不使用异常,构造函数有个int返回值,可以返回错误码。
析构函数就只能是void了,毕竟就算有错误码,程序也没法收到。
如果是malloc / calloc 创建的,那就手动free()吧。
如果不使用create关键字创建结构体(类)对象,那么就可以纯手动管理内存:不会触发自动内存管理。
10,协程,
我倒是写完了协程的模块代码,但一直没整合到scf里。
一旦使用了协程,程序员就必须在main()函数里主动运行协程的主线程函数。
总的来说,协程更像个编程框架,而不是编程语言。
int main()
{
co f0();
co f1();
co_run();
return 0;
}
这种代码并不会马上运行,而是在最后执行co_run()时才会运行。
co f0() 和 co f1() 只是给框架添加协程对象的结构体,相当于给epoll框架添加事件,最后还是要在末尾运行epoll_wait()时才会真正启动事件的处理。
在底层的语言里,协程都是作为框架的,而不是语言本身的机制。
11,最后给个main()函数,
main()函数
输入输出还是使用的C标准库:
不使用不行,否则就得自己去通过write()系统调用去打印hello world了。
1)scf的代码在gitee上,见我的置顶微头条,
2)拉下来之后,在parse目录下直接make,就可以获得编译器的可执行文件scf,它也在parse目录。
3)用命令./scf hello.c编译上图的代码文件,默认会生成2个文件:
1.elf,是连接前的.o文件,
1.out,是连接后的可执行文件,
用chmod +x ./1.out 给它加上运行权限,
然后运行./1.out 就可以打印出hello world了。
PS:大家如果发现BUG了记得给我说一下,我去修改scf的代码!
连接后的程序头和动态库信息
main函数的汇编指令和运行结果
连接进来的C动态库函数
限 时 特 惠: 本站每日持续更新海量各大内部创业教程,一年会员只需98元,全站资源免费下载 点击查看详情
站 长 微 信: muyang-0410