不能直接用 std::map 当符号表,因其扁平结构无法支持作用域嵌套下的变量遮蔽查找;需用作用域栈(vector)配合各Scope内unordered_map实现就近匹配,并分层处理函数重载、类型别名及类作用域等语义需求。

为什么不能直接用 std::map<:string astnode></:string> 当符号表
因为作用域嵌套时,同名变量会遮蔽外层定义,而 std::map 是扁平结构,查不到“当前作用域可见的最近定义”。比如函数内声明了 x,就不能再访问同名的全局 x —— 这需要栈式作用域管理,不是单层哈希能解决的。
实际做法是维护一个作用域栈(std::vector<scope></scope>),每个 Scope 是一个独立的 std::unordered_map<:string symbol></:string>。进入新作用域(如 {、函数体、if 分支)就 push 一个新 Scope;退出时 pop。查找符号从栈顶开始向下遍历,遇到第一个匹配即返回。
-
Symbol至少包含类型(Type*)、是否可变(const)、定义位置(用于报错) - 不要在
Scope析构时自动释放Symbol所指的 AST 节点——语义分析阶段不负责内存回收,留给后续阶段统一处理 - 函数参数要显式插入到函数体作用域中,不能只存在签名里;否则函数体内无法查到
param
怎么处理函数重载和同名但不同签名的函数
纯靠名字查符号表会冲突。必须把函数名 + 参数类型列表作为键,例如用 std::string 拼接 "foo(int,float)",或更稳妥地用 std::tuple<type type></type> 作 std::map 的 key(需自定义哈希)。但注意:C++ 语义分析器通常不在此阶段做重载决议,而是先收集所有同名函数声明,等调用发生时再比对实参类型。
所以建议分两层存储:
- 全局符号表存函数声明:键为函数名,值为
std::vector<functiondecl></functiondecl> - 每个函数体作用域只存局部变量和参数,不存函数本身
- 函数调用表达式
call_expr的语义检查,需遍历该名字对应的所有FunctionDecl*,尝试匹配参数个数与类型兼容性(注意隐式转换规则)
lookup("x") 返回空指针?常见漏掉的三种情况
不是代码写错了,而是语义流程没走全:
- 变量声明语句没触发
insert():比如只解析了VarDecl节点,但忘记在当前作用域调用current_scope->insert(name, symbol) - 作用域未正确切换:
for (int i = 0; ...)中的i应在for初始化语句后立即进入新作用域,但很多手写 parser 在for节点处理中漏掉push_scope() - 类作用域未启用:如果支持
class A { int x; };,则类定义本身应创建一个ClassScope,且成员查找需额外支持scope->lookup_in_class("x", this_class),不能只依赖栈式查找
要不要支持 typedef 和类型别名的符号解析
要,而且必须早于变量声明处理。否则 typedef int myint; myint x; 中的 myint 查不到,导致 x 类型推导失败。
关键点:
- 类型别名也存进当前作用域,但标记为
Symbol::TYPE_ALIAS类型,和变量区分开 - 遇到
typedef或using时,先查右值类型(如int)是否已知,再将左值名(如myint)插入符号表 - 类型查找(如解析
myint x)需专门调用lookup_type("myint"),它只遍历作用域栈中带TYPE_ALIAS标记的项,避免和变量名冲突
作用域管理本身不复杂,难的是边界条件:break/continue 不影响作用域,但 lambda 表达式要建新作用域并捕获外层变量;模板实例化时符号查找要跨翻译单元缓存……这些留到扩展阶段再加,初始版本守住函数/块/类三层作用域就足够驱动基础类型检查了。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











