C语言的符号表和类型系统1

本文探讨了C语言编译器中符号表的设计与实现,包括其在内存中的存储结构、支持的作用域特性以及如何处理同名变量等问题。此外还详细介绍了C语言类型系统的表示方法,包括说明符和修饰符的定义。

阅读博客的朋友可以到我的网易云课堂中,通过视频的方式查看代码的调试和执行过程:
http://study.163.com/course/courseMain.htm?courseId=1002830012

绝大多数编程语言都可以分成三部分:声明(declaration),表达式(expression),语句模块(statement). 每部分都有专门的语法来定义,在上一节中,我们通过语法定义了C语言的变量声明,并通过解析器成功实现了变量声明的语法解析。

对于C语言中的一段函数代码,便可分割成对应于上面所说的三部分。函数声明中的函数名,返回值和输入参数例如:
int fun(int arg1, int arg2);
就可以对应于上面三部分中的声明部分。函数的主体则对应于表达式和语句模块部分。

在对代码的声明部分进行语法解析时,我们需要构建一种数据结构,以便于支持具体的代码生成,这种数据结构,就是我们接下来要研究的符号表。符号表本质上是一种数据库,用来存储代码中的变量,函数调用等相关信息。该表以key-value 的方式存储数据。变量和函数的名字就用来对应表中的key部分,value部分包含一系列信息,例如变量的类型,所占据的字节长度,或是函数的返回值。当我们的解析器读取源代码,遇到声明部分时,便给符号表添加一条记录,如果变量或函数脱离了它的作用范围时,便将他们对应的记录从表中删除。例如:

{
int variable = 0;
}

在上面代码中,进入大括号时,解析器遇到变量的声明,于是便把变量variable 的相关信息写入符号表。当解析器读取到右括号时,便把variable在符号表中的信息给删除,因为出了variable的作用范围只在括号之内。

符号表还可以用来存储类型定义(typedef)和常量声明,在词法解析的过程,词法解析器还需要和符号表交互,用于确定一个变量名是否属于一种类型定义,例如:

typedef char SingleByte

当词法解析器读取SingleByte这个字符串后,会在符号表中查询这个字符串所对应的记录,由于每个记录都有一个标志位,用来表明该字符串是否属于变量声明,于是词法解析器从记录中读取这个标志位,发现SingleByte对应的标志位被设置为1,因此词法解析器就不会把SingleByte当做普通的变量处理,而是当做关键字来处理。

符号表作为一种数据库,它必须具备以下特点:

  1. 速度。由于符号表会被编译器频繁写入和读取,因此记录的写入,查询速度必须足够快。为了保证速度,整个符号表会直接存储在内存中,由此符号表的设计必须仔细考虑内存消耗。
  2. 维护性。符号表几乎是编译器中,最复杂的数据结构。它的设计必须灵活可扩展,使得除了编译器外,其他应用程序或模块也能良好的访问符号表。
  3. 灵活性。C语言的变量声明系统很复杂,例如它允许类型关键字的相互组合等(long int, long doube *…), 因此符号表必须能支持各种不同的变量声明方式。
  4. 重复性支持。由于对大多数编程语言而言,在不同的间套下,重复的变量名是允许的:

int variable = 0;
{
int variable = 1;
}

例如上面例子中,两个变量虽然拥有相同的名字,但却是合法的。在大括号内的变量会覆盖(shadow) 外层同名变量。因此符号表必须支持同一个key, 但却可以映射到不同的value.

  1. 易删除。由于变量可能随时超出作用范围,因此一旦语法解析器发现变量失效后,必须能快速的将其从符号表中删除。

符号表的数据结构设计

为了应对上面的需求,我们可通过哈希表来实现符号表的设计。由于哈希表的插入和删除平均耗时是O(1), 因此它能满足快速的插入和删除这一要求,如果遇到作用域不同的同名变量,他们必然被哈希到同一个位置,那么我们可以用链表把哈希到同一个地方的记录串联起来,这样就解决了支持重复性的问题。举个具体例子:

int Godot;
void waiting(int vladmir, int estragon) {
int pozzo;
while (condition) {
int pozzo, lucky;
}
}

在上面的代码中,Godto 和 waiting是属于第一层的变量,函数waiting的参数vladmir, estragon ,和内部变量 pozzo 属于第二层的变量。while 体内的变量 pozzo, 和 lucky 属于第三层的变量,而且两个pozzo是同名变量。

于是通过链式哈希表来实现符号表的过程如下:

这里写图片描述

所有的变量都存储到哈希表中,同名变量pozzo被哈希到同一个位置,所有用队列连接起来,由于我们使用变量名做哈希,因此不同变量名也有可能哈希到同一个地方,假定vladmir哈希到与pozzo相同的地方,所以vladmir也在同一个队列中。

在头顶还有一个队列,用来存储不同层次的变量起始指针,例如Godot, waiting 属于第一层次的变量,因此头部队列的第一个元素存储指针,指向第一个变量Godot, 然后Godot自己又引出一个指针,指向同一层的另一个变量wait, 由此,同一层的变量实际上是通过一个队列连接起来,这个队列的头指针就存储在Cross link 列表中。

第二层三个变量vladmir, estragon, pozzo , 也组成一个队列:vladmir->estragon->pozzo, 这个队列的头指针就存放在cross link列表的第二个元素。第三层以此类推。

符号表中的一个记录,我们可以用如下java代码表示(symbol.java):

public class Symbol {
    String  name;
    String  rname;

    int       level;  //变量的层次
    boolean   implicit;  //是否是匿名变量
    boolean   duplicate;   //是否是同名变量

    Symbol    args;   //如果该符号对应的是函数名,那么args指向函数的输入参数符号列表

    Symbol    next;  //指向下一个同层次的变量符号
}

哈希表中的记录,我们用SymbolEntry来表示,代码如下:


public class SymbolEntry {
    private Symbol symbol;
    private SymbolEntry prev, next;

    public SymbolEntry(Symbol sym) {
        this.symbol = sym;
    }

    public void setPrev(SymbolEntry prev) {
        this.prev = prev;
    }

    public void setNext(SymbolEntry next) {
        this.next = next;
    }

    public SymbolEntry getPrev() {
        return prev;
    }

    public SymbolEntry getNext() {
        return next;
    }

}

用于解决哈希冲突的链表是双向链表,所以SymbolEntry中有两个对象指针,prev 和 next, 分别指向当前符号的前缀和后缀,这种双向链表的设计有利于在队列中对元素进行删除。

类型系统

接下来的问题是,如何表示变量的类型,如果语言足够简单,那么类型可以用一些整形数组来表示,例如0表示整数,1表示浮点数。指针类型,例如int* 可以用数值3来表示。这种类型系统,称之为限制行类型系统,因为这种方法只能表示有限中类型。

像C语言这种拥有复杂类型表示方式的语言,上面的方法就捉襟见肘了。因此要表示C语言的类型系统,必须设计足够灵活的数据结构。C语言的变量声明包括两部分,一部分叫说明符(specifier),这部分对应各种数据类型关键字,int long, extern, struct等。另一部分叫修饰符,由变量名以及星号,中括号组成,例如 *a, a[10] 等。

说明符部分是有限的,毕竟关键字的数量有限,因此关键字的组合方式也有限,但是,修饰符部分就相当灵活,例如星号*就可以有多个,*和[]又可以相互组合,因此C语言的类型必须由两部分组成,一部分表示说明符部分,另一部分表示修饰符部分。整个类型系统就由包含这两种结构的链表构成。例如声明语句:
short in Quasimodo;
long *Gringoire;

他们的类型系统如下:
这里写图片描述

类型系统中,说明符部分只有一个,而修饰符部分可以有多个,当然也可以没有,同时,说明符始终放在链表的末尾。通过把链表顺序念下来,就可以读出变量声明语句,例如对于第二个队列,顺序读下来就是:Gringoire is a pointer to long.

如果是一个长整形数组,例如 long Coppenole[10],类型系统的表示如下:
这里写图片描述

一个指向10个长整形元素的数组指针,long (*Frollo)[10],类型系统表示如下:
这里写图片描述

这种类型系统有个显著特点是,容易促进代码生成。后面我们可以看到这个效果。

类型系统的实现

修饰符的实现比较简单,代码如下:


public class Declarator {
    public static int  POINTER = 0;
    public static int  ARRAY = 1;
    public static int  FUNCTION = 2;

    private int  declareType = POINTER;
    private int  numberOfElements = 0;

    public Declarator(int type) {
        if (type < POINTER) {
            declareType = POINTER;
        } 

        if (type > FUNCTION) {
            declareType = FUNCTION;
        }
    }

    public void setElementNum(int num) {
        if (num < 0) {
            numberOfElements = 0;
        } else {
            numberOfElements = num;
        }
    }

    public int getType() {
        return declareType ;
    }

    public int getElementNum() {
        return numberOfElements;
    }
}

上面代码中,declareType 用来表示要修饰的变量是一个指针,数组,还是函数,如果是数组的话,numberOfElements 这个成员用来表示数组含有多少个元素。

说明符的实现稍微麻烦些,代码如下:


public class Specifier {
    //type
    public static int  INT = 0;
    public static int  CHAR = 1;
    public static int  VOID = 2;
    public static int  STRUCTURE = 3;
    public static int  LABEL = 4;

    //storage
    public static int  FIXED = 0;
    public static int  REGISTER = 1;
    public static int  AUTO = 2;
    public static int  TYPEDEF = 3;
    public static int  CONSTANT = 4;

    public static int  NO_OCLASS = 0;  //如果内存类型是auto, 那么存储类型就是NO_OCLASS
    public static int  PUBLIC = 1;
    public static int  PRIVATE = 2;
    public static int  EXTERN = 3;
    public static int  COMMON = 4;

    private  int  basicType;
    public   void setType(int type) {
        basicType = type;
    }
    public int getType() {
        return basicType;
    }

    private int storageClass;
    public  void setStorageClass(int s) {
        storageClass = s;
    }
    public int getStorageClass() {
        return storageClass;
    }

    private int outputClass;
    public void setOutputClass(int c) {
        outputClass = c;
    }
    public int getOutputClass() {
        return outputClass;
    }

    private boolean isLong = false;
    public void setLong(boolean l) {
        isLong = l;
    }
    public boolean getLong() {
        return isLong;
    }

    private boolean isSigned = false;
    public void setSign(boolean signed) {
        isSigned = signed;
    }
    public boolean isSign() {
        return isSigned;
    }

    private boolean isStatic = false;
    public void setStatic(boolean s) {
        isStatic = s;
    }
    public boolean isStatic() {
        return isStatic;
    }

    private boolean isExternal = false;
    public void setExternal(boolean e) {
        isExternal = e;
    }
    public boolean isExternal() {
        return isExternal;
    }

    private int  constantValue = 0;
    public void setConstantVal(int v) {
        constantValue = v;
    }
    public int getConstantVal() {
        return constantValue;
    }

    private StructDefine  vStruct;
}

basicType用来表明变量属于什么类型,当前要做的编译器暂时只支持4种类型,int , char, void , struct, label. storageClass 表示变量的存储方式,FIXED表示变量只能存放在固定的内存地址,AUTO表示当前变量是局部变量,可以存放在堆栈上。如果当前变量是经过typedef修饰的,那么他的值也会设置成TYPEDEF, 例如:

typedef char  single;

那么变量single对应的说明符中,storageClass的值等于TYPEDEF。
CONSTANT 用来标识常量类型,加入你声明了一个枚举类型:
enum days {
MON, TUE, WED, TUR, FRI, SAU, SUN
};

编译器会将MON,TUE等当做int类型的常值变量加入符号表:

constant int MON = 0;

于是MON, TUE, 等对应的Specifier类中,storageClass的值就是CONSTANT. 同时constantValue也会做相应的设置,例如MON对应的Specifier类,constantValue 等于0, TUE对应的specifier类的constantValue 等于1.

对于字符串常量,编译器会把它转换成一个初始化了的char数组,例如

“contents of string”;

会转换成:
char s1[] = “contents of string”;
这样,编译器就可以建立一个类型列表来描述字符串常量。

Specifier最后还有一个StructDefine类型的成员,如果当前的变量是一个结构体的话,vStruct就不是null, StructDefine的具体定义,我们后面再给出。

isLong用来表示当前变量占据多大字节,默认下int类型占据2字节,long int 占据4字节,因此:

long int x;

变量x对应的Specifier类,isLong就会设置为true.由于编译器默认没有long修饰的变量都占据2字节,所以short关键字会被自动忽略。

isSigned, isStatic, isExternal 用来表明变量是否被对应的关键字所修饰,例如:

external unsigned long int y;

那么变量y对应的Specifier 中,isExternal 等于true, isSigned 等于false, isLong等于true.

类型系统是一个复杂而且繁琐的技术要点,一节不可能讲清楚,本节我们先探讨一部分,在后面的章节中,我们继续就类型系统的理论和代码实现进行深入的了解。

<think>嗯,用户想了解C语言符号表的相关信息。之前系统已经给出了清晰的引用资料,需要整合这些内容给出专业且易懂的解释。 用户可能是编译器相关的学习者或开发者,提问很专业但缺乏具体方向。从引用[1][3]看,需要重点解释符号表如何存储C语言特有的复杂类型信息,比如结构体内部的函数指针多维数组。特别要注意引用[2]提到的符号表与语法分析的区别——用户可能混淆了这两个阶段。 考虑设计四个核心部分的讲解:基本信息、存储内容、类型系统关联、结构设计。开头先用一句话定义符号表是"编译器存储标识符属性的数据库",这样最直观。在类型系统部分要强调C语言的独特性,比如struct argotiers那个例子很典型,可以拆解它的三个成员来说明符号表如何递归存储嵌套类型。 技术细节上需要解释修饰符(declarator)的解析过程。引用[1]提到的"说明符+修饰符"模式是关键,比如int (*a)[10]就要拆解为基本类型int两层修饰符(指针+数组)。可以画个示意图说明符号表条目如何像洋葱一样层层包裹这些修饰符。 最后考虑补充两个实际价值点:为什么Java符号表设计更简单(C缺少垃圾回收等机制),以及调试器中符号表的作用。这样能帮助用户理解抽象知识的应用场景。 准备用加粗标题分段呈现,把struct argotiers的案例单列强调。避免直接贴代码但可以描述数据结构字段,结尾的关联问题要引导用户关注实现难点而非表面功能。 </think>以下是对C语言符号表的详细介绍,结合引用内容的核心要点: --- ### **一、符号表的基本作用** 符号表编译器在**语义分析阶段**的核心数据结构,用于存储程序中所有**标识符的属性信息**,包括: 1. **变量名**、函数名、结构体/联合体标签 2. **类型信息**(基本类型、指针、数组、结构体等) 3. **存储类别**(`extern`、`static`、`auto`) 4. **作用域与生命周期** 5. **内存地址**(编译期或链接期确定) > 引用[2]明确其核心任务:**记录所有声明(变量/函数),并在语义层面检查合法性**。例如 `int 1;` 在语法分析阶段可通过(因符合 `int [token]` 规则),但语义分析会因 `1` 非合法标识符而报错[^2]。 --- ### **二、符号表存储的关键内容** | **条目类型** | **存储信息示例** | **说明** | |--------------|--------------------------------------|-----------------------------------| | 变量 | `int x;` → 类型`int`, 地址, 作用域 | 支持基本类型复杂类型 | | 函数 | `void f(int);` → 返回值类型、参数列表 | 包含参数数量类型检查 | | 结构体 | `struct S {int a;};` → 成员列表 | 递归存储嵌套结构(如成员的类型) | | 类型别名 | `typedef int MyInt;` → 等价类型映射 | 建立自定义类型到基础类型的关联 | > 引用[3]的案例 `struct argotiers` 展示了符号表如何递归存储**嵌套类型**(如函数指针 `int (*Clopin)();` 多维数组 `double Mathias[5];`)[^3]。 --- ### **三、类型系统的关键设计** C语言的类型声明由两部分构成(引用[1]): 1. **说明符(Specifier)** - 基础类型:`int`, `long`, `char` - 存储类别:`extern`, `static` - 复合类型:`struct/union/enum` 标签 2. **修饰符(Declarator)** - 组合操作符:`*`(指针), `[]`(数组), `()`(函数) - 变量名:如 `*a`, `b[10]` **符号表需动态组合二者**。例如: - `int (*p)[3];` → 说明符 `int` + 修饰符 `(*p)[3]` - 符号表需解析出:**指向3个int数组的指针** --- ### **四、符号表的数据结构设计** 由于C语言的**类型组合复杂性**(如多级指针、嵌套结构体),符号表需采用灵活设计: 1. **树形结构** - 根节点:标识符名称 - 子节点:类型属性(指针层级、数组维度、结构体成员链表) 2. **类型图(Type DAG)** - 避免重复存储相同类型(如多个 `int*` 指向同一类型节点) 3. **作用域栈** - 实现块作用域:进入作用域压栈,退出时弹出局部符号 > 引用[4]强调:符号表实现**高度依赖编译器设计目标**,可扩展为支持多语言[^4]。 --- ### **五、符号表的实际应用场景** 1. **语义检查** - 检查类型兼容性(如赋值 `int*` 到 `char*` 是否合法) 2. **中间代码生成** - 为变量分配临时地址或寄存器 3. **调试信息** - 生成调试符号(如GDB使用的DWARF格式) 4. **链接器符号解析** - 解决跨文件的 `extern` 引用 --- ### 典型符号表条目示例(以结构体为例) ```c struct argotiers { int (*Clopin)(); // 成员1:函数指针 double Mathias[5]; // 成员2:double数组 struct argotiers *Guillaume; // 成员3:指向自身的指针 } gipsy; // 全局变量 ``` 符号表需递归存储: 1. `gipsy` 的类型为 `struct argotiers` 2. 成员 `Clopin` 的类型为 **函数指针** → `int (*)()` 3. 成员 `Guillaume` 的类型为 **递归指针** → `struct argotiers*` ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值