介绍
这是是用来记录我的 thinking 和 feeling 的地方.
Made by Zhang Haoze (https://github.com/iamzhz)
电子邮件:iamzhz@foxmail.com
逃 Error
“逃 ERROR”,顾名思义。本页记录了一些 Abandon 的一些错误。不包含语法错误,包含好不容易 de 掉的 编译器/连接器 错误、运行时错误、逻辑错误.
include 的顺序
在 src/include.h
下包含了这个 project 的所有头文件,其中有标准库的,也有 Abandon 中某个部分的头文件,之后让其他的每个编译单元都 include 这个文件。如果该文件的某个顺序错了,那就会让编译器 g++ 报一些某某符号 undefined 的错.
我甚至为此写一个注释:
// pay attention to the order!! (to myself)
不要在头文件中定义变量!
Once upon a time, 我在某个头文件中加入了类似以下的代码
PointerManager pmTree;
然而,因为上面提过的原因,每个编译单元都 include“主头文件”,而“主头文件”会包含每个其他的头文件,所以就会产生多个定义.
当然,在编译阶段没报错,但链接时就会报一大堆多重定义的错:multiple definition of pmTree
。
我刚开始想到把它放到实现文件(或 源文件)中,但也不行,这样编译器就报错了.
Emmmm, 我又想起了一个词—— extern .
于是,解决这个问题也很简单, 在头文件的定义前加上 extern
,把定义(Definition)变为声明(Declaration),在把定义放在实现文件中.
extern PointerManager pmTree;
这就是空指针鸭
“我要写 parser 辣”:我用一个写了一个类 Tree
用来存储抽象语法树(Abstract Syntax Tree, AST ),在我的 Parser
类中,我有一些用于解析非终结符的成员函数,每个函数返回一个指向 new 过的一个树的指针,so ,
我在遇到错误的语法时会返回 nullptr,而在这之后我又直接引用了这个空指针,
于是,make 过了,运行时系统输出了类似这样的错:
[1] 2841 segmentation fault (core dumped) ./abandon hello.abn
并且当前目录还多了个名为core.2841
的文件,现请教 GPT-4o 得知,
在Linux系统中,出现“segmentation fault (core dumped)”错误时,系统通常会生成一个核心转储文件(core dump file)。这个文件包含程序在崩溃时的内存映像和寄存器状态,帮助开发者进行调试和分析。
具体到你的例子中:
● [1] 2841 表示进程ID为2841的程序在后台运行。
● segmentation fault (core dumped) 表示程序由于非法访问内存地址而崩溃,并且系统生成了一个核心转储文件。
生成的文件 core.2841 是核心转储文件,其中 2841 对应的是崩溃的进程ID。该文件包含了程序崩溃时的内存状态、寄存器状态、堆栈内容等信息。开发者可以使用调试工具,如gdb(GNU Debugger),来分析这个核心转储文件,以确定崩溃的原因。
(我这时使用的是一个云 space,运行的 Linux)这明显是一个比较明显的问题,但当时这个明显的问题显得不那么明显,
就是说“不知庐山真面目,只缘身在此山中”,我当时想不到这上面,害的我开始埋怨别的函数。
最后才领悟“啊,我 return 了个空指针,我又引用了这个空指针”.
解决办法就是不用 nullptr 来表示语法错误,于是我又定义了一个全局变量来表示:
Tree* noneTreeClass = createTree(treeTypeNode_None);
不要吞我的 token
在试写 parser 后,我的输入是 3+7
,而输出的树是
Node Expr
├── Node Term
│ ├── Node Factor
│ │ └── Token Int [3]
│ └── Node ε
└── Node ε
其文法是
Expr -> Term Expr'
Expr' -> + Term Expr' | - Term Expr' | ε
Term -> Factor Term'
Term' -> * Factor Term' | / Factor Term' | ε
Factor -> ( Expr ) | Number
哎,我加号呢?我 7 呢? debug 后,发现原来是“吞 token”了.
按照我的规定,parser 里的函数都是先使用现成的 token,后为下一个函数获取新 token; 每个函数最后必须有且只能有一个未使用的 token
而我的一些函数因为多读了造成了 bug (我称之为“吞 token”,不知道是否有这个名词)
比如以下代码,为 Expr' 写的一个函数 (含有 bug 的)
Tree* Parser::parse_Expr_() {
Token tk = this->lx->current; // 获取当前 token
this->lx->getNextToken(); // 为下一个 token 提前读
Tree* tr = createTree(treeTypeNode_Expr_); // 构建一棵 Expr' 的树 (作为返回值)
Tree* tr_Term;
Tree* tr_Expr_;
if (tk.matchSign("+") || tk.matchSign("-")) { // 当前 token 是 + 或 - 的符号
tr->add(createTree(tk)); // 将当前 token 封装为树加入到 tr 的子结点
} else {
return epsilonTreeClass; // 返回 ε
}
tr_Term = this->parse_Term(); // 继续解析 Term
if (tr_Term == noneTreeClass) return noneTreeClass; // 如果 Term 解析失败, 返回失败
tr->add(tr_Term); // 否则将 Term 的树加入到 tr 的子结点
tr_Expr_ = this->parse_Expr_(); // 继续解析 Expr'
if (tr_Expr_ == noneTreeClass) return noneTreeClass; // 如果 Expr' 解析失败, 返回失败
tr->add(tr_Expr_); // 否则将 Expr' 的树加入到 tr 的子结点
return tr; // 返回解析过后的 Expr' 树
}
不难发现, 如果输入是 ε 的, 那么它会依次执行
Token tk = this->lx->current; // 未使用的第一个 token (先标记为 token1)
this->lx->getNextToken(); // 未使用的第二个 token
Tree* tr = createTree(treeTypeNode_Expr_);
return epsilonTreeClass;
/* 因为定义不会被执行,且对应该示例无用,故弃之*/
这样它就会一下子读了两个 token,但却都没有使用,会导致其他代码看不到 token1
解决方案:
将代码改为只有返回的不是 ε 时,才 this->lx->getNextToken()
如下
Tree* Parser::parse_Expr_() {
Token tk = this->lx->current;
// 减去了 this->lx->getNextToken();
Tree* tr = createTree(treeTypeNode_Expr_);
Tree* tr_Term;
Tree* tr_Expr_;
if (tk.matchSign("+") || tk.matchSign("-")) {
tr->add(createTree(tk));
this->lx->getNextToken(); // 增加了
} else {
return epsilonTreeClass;
}
tr_Term = this->parse_Term();
if (tr_Term == noneTreeClass) return noneTreeClass;
tr->add(tr_Term);
tr_Expr_ = this->parse_Expr_();
if (tr_Expr_ == noneTreeClass) return noneTreeClass;
tr->add(tr_Expr_);
return tr;
}
其实也就是把 this->lx->getNextToken()
换了个位置,
其他函数也有这个 bug,解决方法亦同
鈹溾攢鈹€
是的,标题你没有看错,也不是你编码的问题
Just now,我的 parse 加减乘除写好了
问题描述
我在 Web 上的 MarsCode(Linux Debian) 写的了一些代码,在上面 push 到了 GitHub,此时此刻,我要把它 pull 到本地(Windows 11)
嗯?有 Error?算了重新 clone 吧
clone 好了,run 一下
emmmm.. 什么东西?
$ ./abandon hello.abn
Input File: hello.abn
Output File:
Node Expr
鈹溾攢鈹€ Node Term
鈹?
正确结果应该是
➜ Abandon git:(main) ./abandon hello.abn
Input File: hello.abn
Output File:
Node Expr
├── Node Term
│ ├── Node Factor
│ │ └── Node Expr
│ │ │ ├── Node Term
│ │ │ │ ├── Node Factor
│ │ │ │ │ └── Token Int [10]
│ │ │ │ └── Node ε
│ │ │ └── Node Expr'
│ │ │ │ ├── Token Sign [-]
│ │ │ │ ├── Node Term
│ │ │ │ │ ├── Node Factor
│ │ │ │ │ │ └── Token Int [2]
│ │ │ │ │ └── Node ε
│ │ │ │ └── Node ε
│ └── Node Term'
│ │ ├── Token Sign [*]
│ │ ├── Node Factor
│ │ │ └── Token Int [4]
│ │ └── Node ε
└── Node ε
[free] 0x564e6680b5e0
[free] 0x564e6680b640
[free] 0x564e6680b6c0
[free] 0x564e6680b750
[free] 0x564e6680b7b0
[free] 0x564e6680b860
[free] 0x564e6680b8c0
[free] 0x564e6680b920
[free] 0x564e6680b9a0
[free] 0x564e6680ba90
[free] 0x564e6680bb10
[free] 0x564e6680bb70
[free] 0x564e6680bbd0
[free] 0x564e6680bc70
[free] 0x564e6680bcf0
[free] 0x564e6680bd50
[free] 0x564e6680bdb0
[free] 0x564e6680bf40
[free] 0x564e6680bfa0
[free] 0x564e6680c040
[free] 0x564e6680c0d0
并且,并且!我这之中的我的hello.abn
中的文件内容完全一样
现在(2024/8/5 19:28)还没有解决,稍等
解决过程
- 19:28 刚刚开始
- 19:41 暂无头绪
- 19:47 我一气之下把在 Windows 下的文件的 UTF-8 编码全改成 Chinese 2312,现在好了(我就知道跟编码有关
只是我不解我代码里全是 ASCII 中的字符,为什么会与编码... - 吃了顿饭
- 21:05 全部又改为 UTF-8,又可以... 这就是玄学吗?
如果我有幸让您看到了这几段字,并且您知道原因的话(不知道也没关系)...请给我发 email iamzhz@foxmail.com
,感谢,如果没人看到的话就算了
P.S. 我所 clone 的 commit 的 hash 值是 ae8546c4b3c72b075b3ac2c1675ee70ae8145557
8/6 更新
我才发现我好像确实用了 ASCII 外的字符 ε 和 制表符(不是指 tab),是我没注意到
好吧我悟了
我我乱码改为 ε,重新 make 了
于是
PS D:\zhz\v\Abandon> ./abandon hello.abn
Input File: hello.abn
Output File:
Node Expr
锟斤拷锟斤拷锟斤拷 Node Term
锟斤拷 锟斤拷锟斤拷锟斤拷 Node Factor
锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Expr
锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Term
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Factor
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Token Int [10]
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node 蔚
锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Expr'
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Token Sign [-]
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Term
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Factor
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Token Int [2]
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node 蔚
锟斤拷 锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node 蔚
锟斤拷 锟斤拷锟斤拷锟斤拷 Node Term'
锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Token Sign [*]
锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node Factor
锟斤拷 锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Token Int [4]
锟斤拷 锟斤拷 锟斤拷锟斤拷锟斤拷 Node 蔚
锟斤拷锟斤拷锟斤拷 Node 蔚
这就是传说中的 锟斤拷 吗?
但是我想用 UTF-8 编码啊?!
搜索“Windows gcc utf-8”,emmmmmm,只要在编译时加上 -finput-charset=UTF-8 -fexec-charset=GBK
,就可以啦。于是我的 makefile 变成这样了 (不过我不会把它 push 到 Github 上)
cc := g++
std := -std=c++11
abandon: src/*.cc src/*/*.cc
$(cc) $^ -o $@ $(std) -g -finput-charset=UTF-8 -fexec-charset=GBK
完美
2024/8/31 更新
对应 Windows,又有一种新方法: 可以不修改 makefile,保持二进制文件的编码为 UTF-8,在运行程序前,使用
chcp 65001
将当前命令行编码切换为 UTF-8,这样就可以顺利地 run 了
段错误
嗯.... 经过没几天的 code 之后,发现好多的段错误其实都是引用了空指针(nullptr
)
完毕
怎么会跳到这里面?
在我往 parser 中加 if else 后,也修改了对 Sentence 的解析,但是,会出现这种问题 Abandon 代码是这样的(可能与写到最后的 Abandon 代码有出入,但是理想中现在可以解析的代码)
fn int main(void) {
if (x) {
@print("It is true"); // 在这里的分号处报错
} else {
@print("It is not true");
}
@hello("hello");
x = 1 + 2;
}
而执行的命令后是这样的:
PS D:\zhz\v\Abandon> ./abandon hello.abn
Input File: hello.abn
Output File:
[Error] 3,29 ( or something other expected
[free] 69 pointer(s)
因为我修改了 parse_Sentence 函数,所以应该这应该与该函数有关,通过 gdb 对该函数打断点,发现了端倪
每次到这里判断都会跳进那个如果是关键字且是 if 的判断里去:
Tree* Parser::parse_Sentence() {
Tree* tr = createTree(treeTypeNode_Sentence);
Tree* tr_Expr;
if (this->current.matchKeyword("if")) { // 通过测试,每次到这里判断都会跳进这个if判断
Tree* tr_If = this->parse_If();
ERROR_noneTreeClass(If);
tr->add(tr_If);
return tr;
} else if (this->current.matchKeyword("else")) {
Tree* tr_Else = this->parse_Else();
ERROR_noneTreeClass(Else);
tr->add(tr_Else);
return tr;
}
tr_Expr = this->parse_Expr();
ERROR_noneTreeClass(Expr);
tr->add(tr_Expr);
if (!this->current.matchSign(";")) {
EXPECTED_ERROR(";");
}
tr->add(createTree(this->current));
this->getNextToken();
return tr;
}
所以我怀疑与 Token::matchKeyword
有关。那就看一下:
bool Token::matchKeyword(std::string content) {
if (this->type == tokenTypeKeyword) {
if (this->content == content) return true;
}
return true;
}
好吧。。。。我也不知道什么时候写了这几行 bad code。。哎,看看我的 git 提交,emm,Jul 1,两个月前, 现在是 2024/8/26
正如我现在正在写的 markdown 文件名 - all_true.md
,你应该看到的是 all_true.html
或者是什么的
完毕
Parser
记录了关于语法分析器的东西
文法?文法!
前情提要 (可以跳过)
TBH,我在未写语法分析时,认为:
文法?有必要吗?我觉得我到时候写 parser 的时候能够应该能顺着自己的心写出来,
我还需要它?
(真实心理)
于是,
我写 parser 的之前,思考了一下实现方法,
于是,
我只能又捧读龙书,重视起文法了.
上下文无关文法
- 上下文无光文法(Context-Free Grammar,CFG),以下简称为文法.
- 终结符 (Non-terminal) 语法树中的最某端的节点,代表一个 token.
- 非终结符 (Terminal) 不是语法树中的最某端的节点的节点,代表一群树状的 token(s).
文法通常表示为 \(A\to \alpha\), 左侧部分 \(A\) 称为 非终结符,右侧的 \(\alpha\) 被称为生产式(Production) 或 右部 (Right-hand side, RHS)
- 生产式 是由 终结符 和/或 非终结符 组成的符号序列:也就是一堆由 终结符 和(或) 非终结符 堆成的东西. 生产式 表示了左侧非终结符.
以下是处理加减乘除及括号运算的文法(式1)
Expr -> Expr + Term | Expr - Term | Term
Term -> Term * Factor | Term / Factor | Factor
Factor -> ( Expr ) | Number
其中,|
用于分割相同非终结符中可能的不同的生产式,可以理解为 “或”.
虽然但是,式1包含左递归.
假如用此式最左推导(Leftmost Derivation)某些终结符符号(如1+2
),它会陷入 Expr 的无穷循环.
因此放入到代码环境需要改为无左递归版本,如下(式2)
Expr -> Term Expr'
Expr' -> + Term Expr' | - Term Expr' | ε
Term -> Factor Term'
Term' -> * Factor Term' | / Factor Term' | ε
Factor -> ( Expr ) | Number
其中,ε
为空集,也就是啥都不需要,
像这样把它变为右递归的做法,叫做消除左递归
CST 与 AST
原来树与树是不同的?我还以为所有的都叫 AST 抽象语法树
CST
CST(Concrete Syntax Tree,具体语法树)
CST 是对输入源代码的一种更接近原始语法结构的直接表示,它保留了更多的语法细节,包括可能的冗余和不必要的信息
像这样
Node Expr
├── Node Term
│ ├── Node Factor
│ │ └── Node Expr
│ │ ├── Node Term
│ │ │ ├── Node Factor
│ │ │ │ └── Token Int [10]
│ │ │ └── Node ε
│ │ └── Node Expr'
│ │ ├── Token Sign [-]
│ │ ├── Node Term
│ │ │ ├── Node Factor
│ │ │ │ └── Token Int [2]
│ │ │ └── Node ε
│ │ └── Node ε
│ └── Node Term'
│ ├── Token Sign [*]
│ ├── Node Factor
│ │ └── Token Int [4]
│ └── Node ε
└── Node ε
AST
AST (Abstract Syntax Tree,抽象语法树)
也就是把 CST 去掉没用的 ε 与只包含一个子节点的节点,但保留子节点
像这样
Node Expr
├── Node Factor
│ ├── token Int [10]
│ └── Node Expr'
│ ├── Token Sign [-]
│ └── Token Int [2]
└── Node Term'
├── Token Sign [*]
└── Token Int [4]
这么样?看着是不是清净很多?
Thinking
CST 可以直接生成的,也可以直接生成 AST,不过可能会没直接生成 CST 简单. 我生成的就是 CST, display 出来的树比较难看, 所以, 我还想往 Tree 类再加一个成员函数, 起名为 cst2ast()
, 它在 CST 生成之后执行, 通过遍历一棵树将使得其变得更抽象, 应该也不算难
半晌后更新
已经写成了, 输出:
Node Term
├── Node Expr
│ ├── Token Int [10]
│ └── Node Expr'
│ │ ├── Token Sign [-]
│ │ └── Token Int [2]
└── Node Term'
│ ├── Token Sign [*]
│ └── Token Int [4]
code:
Tree* Parser::cst2ast(Tree* tr) {
if (tr->type == treeType_Token) return tr;
std::vector<Tree*> & c = tr->children; // 酱紫写起来比较好看
for (auto it = c.begin(); it != c.end(); /*这里是空的*/) {
if ((*it)->type == treeType_Node && (*it)->label == treeTypeNode_Epsilon) { // 如果是空集
it = c.erase(it); // “擦”去它
} else { // 否则 (废话)
Tree* result = cst2ast(*it); // 将一个子节点扔给另一个自己处理
if (result == nullptr) it = c.erase(it); // 如果其返回 nullptr, 则擦去它
else { // 否则
*it = result; // 使用它
it ++; // 调到下一个迭代器
}
}
}
if (c.empty()) return nullptr; // 如果是空的,直接返回 nullptr (算是标记吧)
if (c.size() == 1) return c[0]; // 如果只要一个子节点,就单独返回它,不包括自己
return tr; // 最后剩下的一个完美的 Tree
}
注意: 在循环中, 如果是空集(treeTypeNode_Epsilon), 并且 it = c.erase(it)
, 那么就不需要it ++
了, 所以 for 循环的第三个表达式是空的, 之后根据情况再在循环体内写
它的结果与我自己动手做成的完全一样, 不过只有那些线不一样, 这就该埋怨我的 Tree::display() 了, 详见打印一棵树
两天后
其实这样输出的也并不是十分的 AST, 因为撇来撇去的,所以我想再加上一些语句把一些撇去掉,只保留...那叫什么东西
反正就是,原来是这样的
Node Expr
├── Token Int [1]
└── Node Expr'
│ ├── Token Sign [+]
│ └── Token Int [3]
操作之后就变成
Node Expr
├── Token Int [1]
├── Token Sign [+]
└── Token Int [3]
我想你懂我的意思。感觉也简单,就是在 return tr;
前加上一些判断的代码, 再做一些什么操作,不过要有一个函数判断是不是带撇的
稍等
不过,突然感觉没那个必要了,刚写的代码放这里纪念一下(说不定中间代码生成时有用)
bool treeTypeNodeLabelIs_(treeTypeNodeLabel label) {
switch(label) {
case treeTypeNode_Main: return false;
case treeTypeNode_None: return false;
case treeTypeNode_Epsilon: return false;
case treeTypeNode_Expr: return false;
case treeTypeNode_Expr_: return true;
case treeTypeNode_Term: return false;
case treeTypeNode_Term_: return true;
case treeTypeNode_Factor: return false;
}
return false;
}
bool treeTypeNodeLabelTogether(treeTypeNodeLabel l1, treeTypeNodeLabel l2) {
if (l1 == treeTypeNode_Expr && l2 == treeTypeNode_Expr_) return true;
if (l1 == treeTypeNode_Term && l2 == treeTypeNode_Term_) return true;
return false;
}
小结:parse 加减乘除
截止现在(2024/8/5 14:18), Abandon 已经能够将任意的加减乘除解析为 CST 了
比如输入(10-2)*4
, 能够解析出树:
Node Expr
├── Node Term
│ ├── Node Factor
│ │ └── Node Expr
│ │ │ ├── Node Term
│ │ │ │ ├── Node Factor
│ │ │ │ │ └── Token Int [10]
│ │ │ │ └── Node ε
│ │ │ └── Node Expr'
│ │ │ │ ├── Token Sign [-]
│ │ │ │ ├── Node Term
│ │ │ │ │ ├── Node Factor
│ │ │ │ │ │ └── Token Int [2]
│ │ │ │ │ └── Node ε
│ │ │ │ └── Node ε
│ └── Node Term'
│ │ ├── Token Sign [*]
│ │ ├── Node Factor
│ │ │ └── Token Int [4]
│ │ └── Node ε
└── Node ε
打印一棵树
介绍一哈
我的树大概长这样
class Tree {
treeType type;
Token tk;
treeTypeNodeLabel label;
std::vector<Tree*> children;
};
然后有一堆枚举
enum treeType {
treeType_End,
treeType_Token,
treeType_Node
};
enum treeTypeNodeLabel {
treeTypeNode_Main,
treeTypeNode_None,
treeTypeNode_Epsilon,
treeTypeNode_Expr,
treeTypeNode_Expr_,
treeTypeNode_Term,
treeTypeNode_Term_,
treeTypeNode_Factor,
};
在树中,根据 label
判断是一个是否是树中的叶子节点, type
是表示非子节点的类型, children
是表示非子节点的子节点(们), tk
表示 Token, 是一个叶子结点的值.
我的这个 Tree 一般存储堆中, 使用 createTree()
函数(们)创建.
在用这颗树的时候, 先判断 label
, 然后在使用 [type
与 children
] 或 [tk
].
打印一棵树
因此可以这样:
void Tree::display(int indent, bool last) {
for (int i = 0; i < indent - 1; ++i) { // 打印缩进
std::cout << (i < indent - 1 ? "│ " : " ");
}
if (indent > 0) { // 打印树枝
std::cout << (last ? "└── " : "├── ");
}
if (this->type == treeType_Token) { // 打印 Token
std::cout << "Token " << this->tk.typeToText()
<< " [" << this->tk.content << ']' << std::endl; // 打印 Token 类型及内容
} else { // 打印非 Token 节点(Node)
std::cout << "Node " << treeTypeNodeLabelToText(this->label) << std::endl; // 打印节点类型
for (size_t i = 0; i < this->children.size(); ++i) { // 打印子节点(们)
this->children[i]->display(indent + 1, i == this->children.size() - 1); // 递归打印
}
}
}
完毕!
一些对我来说比较新鲜的东西
GDB 这么好用?
以前,我只有一个时候使用 gdb,那就是-查看程序的汇编代码的时候
我确实是大材小用了,
我以前调试程序都是直接在代码中 print 变量的值,再简单点就是加一个宏,比如
#define DEBUG(info) std::cout << "[INFO] " << info << std::endl;
这时候我的宏的参数都是不加括号的,因为可以这样玩 DEBUG("Point 1: " << var)
在写这个 project 的时候我才真正使用 gdb 调试程序
GDB
- 编译源程序时加上
-g
参数,生成调试信息 - 运行
gdb your_program
命令,进入 gdb 调试环境
这里的your_program
是你编译后的可执行文件 - 输入
break
命令,设置断点,程序运行到这里会自动暂停 比如在 启动函数func
处设置断点,输入break func
这里的break
可以简写成b
- 输入
run
命令运行程序,程序会在 gdb 调试环境中运行
这里的run
可以简写成r
- 输入
next
命令,单步执行程序,遇到断点会自动暂停 这里的next
可以简写成n
- 输入
continue
命令,继续运行程序,遇到断点会自动暂停
这里的continue
可以简写成c
- 输入
print
命令,查看变量的值
这里的print
可以简写成p
- 输入
backtrace
命令,查看函数调用栈
这里的backtrace
可以简写成bt
- 输入
quit
命令,退出 gdb 调试环境
这里的quit
可以简写成q
截止目前所有的上下文无关文法
Expr -> Assign
Assign -> Compare Assign'
Assign' -> = Compare Assign' | ε
Compare -> Add Compare'
Compare' -> == Add Compare' | > Add Compare' | < Add Compare' | >= Add Compare' | <= Add Compare' | != Add Compare' | ε
Add -> Term Expr'
Add' -> + Term Expr' | - Term Expr' | ε
Term -> Factor Term'
Term' -> * Factor Term' | / Factor Term' | ε
Factor -> ( Expr ) | Int | DefineVariable
ExprList -> Expr ExprList' | ε
ExprList' -> , Expr ExprList' | ε
FunctionCall -> @ Id ( ExprList )
Sentence -> Expr ; | If | Else | While | DoWhile | For | Break | Continue | Return
Sentences -> Sentence Sentences | Sentence
Statements -> { Sentences } | Sentence
DefineFuction -> fn Type Id ( ExprList ) Statements
DefineVariable -> Type Id
If -> if ( Expr ) Statements
Else -> else Statements
While -> while ( Expr ) Statements
DoWhile -> do Statements while ( Expr ) ;
For -> for (Expr ; Expr ; Expr ) Statements
Break -> break ;
Continue -> continue ;
Return -> return Expr ;
这个类似文档的东西是怎么来的
刚开始,我想写着代码(Abandon),有了逻辑错误,感觉有点懒了,那就....写写其他的?
比如文档.
刚开始我用语雀写,写了一个“逃 Error”,然后,分享....
哎,要会员,要实名.
那就算了,我说的是语雀,
我看看其他的吧,
用什么?Gitbook?登不上,
Notion?有点慢
自建一个静态网页放到 Github Pages 上?
(Github Pages 在河南好像挺快的)
网上搜了搜,看到了 nextra,界面看起来挺好看的,
试了试,烦,node.js 还没装
装上了,安装其他库,
好了,现在 node_modules
变成无底洞了
好了,又 Error 了一大堆
逃吧
最后,我用了这个叫做 mdbook 的东西,
虽然界面比不上 nextra,
但重要的是不需要 node.js 了,
它是用 Rust 写的,但有编译好的二进制文件,直接 download 下来了,
我这就把 node 卸了
稍等
好了,现在就可以愉快地写了
总之就是 \(语雀\to Gitbook\to Notion\to Nextra\to mdbook\)
没事,我就试试 LaTeX 公式
好啦,现在我要 commit + push 了