c4:500 行代码的 C 语言解释器
C4(C in four functions)是 Robert Swierczek 写的一个极简主义练习:用大约 500 行 C 代码实现一个能运行 C 程序的虚拟机和解释器。整个项目就四个函数:next() 词法分析、program() 语法分析生成代码、eval() 虚拟机执行、main() 组装。 它有多强1234gcc -o c4 c4.c./c4 hello.c # 解释执行一个 C 程序./c4 -s hello.c # 输出汇编指令./c4 c4.c hello.c # 用 c4 解释运行 c4 自己,再由它运行 hello.c 最后一行是它的名场面:c4 可以解释执行 c4 自己(甚至再套一层), demonstrates 了”自举(self-hosting)”的概念。 我做了什么以 c4 为基础学习整个解释器的骨架:词法分析如何把源码切成 token、虚拟机的指令集(IMM/ADJ/PUSH/JMP/…)、符号表的组织。my_gcc.c 是学习过程中加入自己注释和修改的版...
宏定义的高级用法
研究 Boost.Preprocessor 的时候整理的一套”宏元编程”套路:用纯宏实现在编译期展开出 N 个变量。不涉及任何编译器扩展,纯标准 C 预处理器。 目标12int BOOST_PP_ENUM_PARAMS(20, a);// 展开后:int a0, a1, a2, ... , a19; 坑一:拼接参数必须经过两层宏123#define BOOST_PP_CAT(a, b) BOOST_PP_CAT_I(a, b)#define BOOST_PP_CAT_I(a, b) BOOST_PP_CAT_II(~, a##b)#define BOOST_PP_CAT_II(p, res) res 为什么不能直接写 a##b?因为 ## 会阻止参数展开——直接 CAT(BOOST_PP_REPEAT_, 1) 拼出来的是字面量 BOOST_PP_REPEAT_1 而不是它展开后的内容。标准解法就是延迟一层:先拼出 ~, BOOST_PP_REPEAT_1,再扫描一轮时 ~ 被丢弃、参数正常展开。 坑二:循环靠”手写递归表”预处理器没有循环,只能靠宏递归展开模拟。但宏不能直接...
用 C 语言写 C++
这是个整活项目,但整得非常认真:用 C 语言写出 C++ 的语法。目标包括 new Animal("Tom", 1) 这种”像话吗”的代码,甚至还有个山寨 iostream 头文件。项目分两代——第一代(2023 年 3 月)老老实实用结构体内嵌实现继承多态;第二代(2024 年 4 月起)开始放飞,靠 GCC 的两个扩展和几个胆大包天的宏,让 C 长出了 C++ 的样子。 第一步:抹平关键字C 和 C++ 最表面的区别是一堆关键字。那就 #define 掉——项目里躺着一个名叫 iostream 的文件(对,没有扩展名),内容如下: iostream12345678910111213141516171819202122#ifndef __iostream_h__#define __iostream_h__#include <stdio.h>#include <string.h>#include <Print.h>#define using#define namespace#define std#define class s...
函数定义远古知识
挖到一个真正的”远古知识”:K&R 风格的函数定义。这是 C 语言还没标准化的年代(K&R 那本《The C Programming Language》第一版)的写法,现在的编译器为了兼容老代码依然支持。 现代写法 vs 远古写法12345678910111213// 现代写法(ANSI C / C89 之后)double alt_style(int a, double *real){ return *real + a;}// 远古写法(K&R 风格)double alt_style(a, real) double *real; int a;{ return *real + a;} 特征很明显:参数列表里只写参数名,参数类型声明放在函数体大括号之前。类型声明还可以乱序(先写 real 再写 a),因为它们只是”声明区”,和参数名按位置对应。 为什么被淘汰了K&R 风格最大的问题是:不带原型。调用方在没有任何声明的情况下调用它,编译器不会检查参数个数和类型,传错了照样编译通过,运行时才...
C语言奇怪的冷知识
最近学到几个 C 语言的冷知识,全是我平时踩坑或者见到的”怪现象”。挑几个有意思的分享一下,每个都能让你重新认识这门熟悉又陌生的语言。 一、数组越界导致的死循环12345678910int i, array[3];printf("%p\n", &i);printf("%p\n\n", &array[3]);for (i = 0; i <= 3; i++){ array[i] = 0;}printf("%d\n", i); 这段代码是个经典炸弹:array 只有 3 个元素,循环却写到下标 3。诡异的是它可能不会崩溃,而是死循环——因为变量 i 和 array 在栈上相邻,i 恰好落在 array[3] 的位置上,每次 array[3] = 0 都会把循环变量 i 清零,i 永远到不了 4。 注意这个现象和变量定义顺序、编译器优化级别都有关系:开了 O2 之后编译器会重新排列栈空间,死循环可能就消失了。所以这类代码属于”未定义行为”,怎么表现都看编译器心情。 二、有符号数和...
用栈实现四则运算计算器
计算器是栈的教科书级应用。这个项目从 V1.0 迭代到 V1.2,最终版约 1100 行,从”能算 1+2*3“一路做到:+ - * / ^ 五则运算、{} [] () 三种括号混用、小数,甚至 sin(1+sin(1+sin(1))) 这样的嵌套函数调用——全部纯 C 手写。 核心算法:两个栈消灭优先级人脑算 3 + 4 * 2 时知道先乘除后加减,机器靠两个栈来消灭优先级问题: 数字栈:存放操作数 符号栈:存放运算符,利用优先级决定何时计算 整个算法分两步: 中缀转后缀:将 3 + 4 * 2 转为 3 4 2 * +(后缀表达式/逆波兰式),后缀表达式不需要任何优先级判断 后缀求值:逐个读 token,数字入栈,运算符弹出两个数计算后与压回,最后栈里剩下的就是答案 先造工具:My_Stack写计算器之前先造轮子。栈用 calloc 分配、realloc 扩容,base/top/max 三个指针管理一段连续内存: 1234567struct my_Stack{ ll stack_size; Elemtype* bas...
字符串公式求值器
一个有意思的小工具:给它一个字符串,比如 "sin(1+1)+max(2,3)" 这种形式,它能自动找出里面的数学函数调用、解析括号里的表达式,然后调用对应的库函数把结果算出来。全程纯 C,核心用了三个技术点。 一、函数指针”查找表”C 没有反射,怎么把字符串 "sin" 变成 sin() 函数?答案是字符串比较 + 函数指针: 123456789101112typedef double (*math_fun)(double);math_fun get_math_fun(const char *s){ if (strcmp(s, "sin") == 0) return sin; if (strcmp(s, "cos") == 0) return cos; if (strcmp(s, "tan") == 0) return tan; if (strcmp(s, "sqrt") == 0) return sqrt; if...
Print.h 开发记(七)V1.7/V1.8
系列导航:上一篇:Print.h 开发记(六)V1.6 系列最终篇:V1.7 加入字体效果,V1.8 修掉遗留问题并支持无参调用——print 库到此定稿。 V1.7:字体宏除了颜色,ANSI 转义序列还能控制字体效果: 123456789101112// 原始字体宏#define NONE(str) str"\033[m"// 高亮文字#define HIGHTLIGHT(str) "\033[1m"NONE(str)// 斜体文字#define ITALIC(str) "\033[3m"NONE(str)// 下划线文字#define _(str) "\033[4m"NONE(str)// 闪烁文字#define FLASGING(str) "\033[5m"NONE(str)// 反显文字 - 交换前景色和背景色#define Reversedisplay(str) "\033[7m&quo...
C语言数组冷知识
这次分享几个数组的冷门玩法:柔性数组、负数下标和复合字面量。 一、柔性数组:通信数据包神器123456struct package{ int version; int size; char data[0]; // 柔性数组(C99 标准写法是 char data[])}; data[0] 不占任何空间,sizeof(struct package) 只有 8 字节(两个 int)。用法是一次性 malloc 出”结构体 + 数据”的连续内存: 12345my_package = malloc(sizeof(struct package) + data_len);my_package->version = 1;my_package->size = data_len;for (int i = 0; i < data_len; i++) my_package->data[i] = 'a' + i; 这样包头和数据在一块内存里,一次 free 就能释放,序列化网络包时直接整块发送——网络...
AutoWrite 自动打字机开发记:从模拟按键到发送消息
总有些场合,文字得靠键盘一个字一个字”敲”进去——目标输入框不吃复制粘贴,或者你只是想让朋友亲眼看着一段话”自己”出现在聊天框里。AutoWrite 就是干这个的:你把文本交给它,它替你把每个键按下去。项目做了两版,正好是 Windows 下模拟输入的两条技术路线——第一版把自己变成键盘,第二版直接给窗口发消息。第一版整理后传上了 GitHub:AutoWrite(MIT 协议)。 第一版思路:把自己变成键盘最早的版本(V0.0.1,2023 年 4 月)核心只有一个念头:程序不碰目标窗口,只负责假装自己是键盘。Windows 提供的 keybd_event 就是干这个的——给它一个虚拟键码(VK code),系统就当作这个键被按了一下: 12345void write_word(int vk)//输入数字、字母{ keybd_event(vk,0,0,0); // 按下 keybd_event(vk,0,KEYEVENTF_KEYUP,0); // 弹起} 按下、弹起,两个事件,一个键就”敲”完了。字母数字都好办,麻烦的是符号——它们和主键区不是一对一的关...
