在以下示例中:
int main(void) {
int a=7;
{
int a=8;
}
}
在没有优化的情况下,生成的程序集将是这样的(来自编译器资源管理器):
main:
pushq %rbp
movq %rsp, %rbp
movl $7, -4(%rbp) // outer scope: int a=7
movl $8, -8(%rbp) // inner scope: int a=8
movl $0, %eax
popq %rbp
ret
如果存在重复命名的变量,编译器如何知道变量在哪里?也就是说,当在内部范围中时,存储器地址在%rbp-8
,而当在外部范围中时地址在%rbp-4
。
有很多方法可以实现本地作用域规则。这里有一个简单的例子:
- 编译器可以保留一个嵌套作用域列表,每个作用域都有自己的符号定义列表
- 该列表最初具有用于全局范围的单个元素
- 当它解析函数定义时,它会在函数参数名称的作用域列表前面添加一个新的作用域元素,并将每个参数名称与该作用域元素的标识符列表中的相应信息一起添加
- 对于每个新块,它在作用域列表前面添加一个新的作用域元素。
for (
在其第一个子句中也引入了一个新的定义范围 - 离开scope(在块的末尾)后,它从scope列表中弹出scope元素
- 当它解析声明或定义时,如果对应的符号已经在当前作用域的列表中,则它是本地重新定义,这是被禁止的(
extern
正向声明除外)。否则,符号将添加到范围列表中 - 当它在表达式中遇到符号时,它会在当前作用域的符号列表中查找该符号,并在作用域列表中查找每个连续的作用域,直到找到为止。如果找不到该符号,则它是未定义的,根据最新的C标准,这是一个错误。否则,符号信息被用于进一步的解析和代码生成
对类型和对象名称执行上述步骤,为struct
、union
和enum
标记保留一个单独的符号列表。
在所有这些发生之前,在程序翻译的单独阶段进行预处理。
C编程语言有一些规范,比如n1570(或更新的规范)。该规范在§6.2.1中定义了标识符的范围。
因此,任何C编译器都应该遵循该规范。
C编译器是如何实现该规范的,这需要一本很好的书来解释。我推荐龙的书。
一些简单或复杂的C编译器是开源的。查看TinyCC、nwcc、Clang或GCC的源代码,了解它们是如何实现该规范的(它们有符号表,但细节针对每个编译器)。
如果存在重复命名的变量,编译器如何知道变量在哪里?
它管理符号表,并在解析块时更新它们。通常,编译器会为编译后的源代码构建一些抽象的语法树,树中表示变量的叶引用一些符号表。GCC编译器记录其Generic Tree和GIMPLE数据结构,并提供输出它们的转储选项。您还可以将foo.c
编译为gcc -S -O -fverbose-asm foo.c
,并查看发出的汇编代码foo.s
。
最后,您的示例可以被认为是较差的编程风格。一些编码准则(如MISRA-C或GNU编码标准)不允许或不鼓励它。你的代码审查过程应该捕捉到这样的代码(在我看来,你的例子是一个很难阅读的代码)。
我的感觉是,单字母变量的范围应该很小——最多十几行
我建议查看(寻找灵感)现有自由软件项目的C代码内部(如GNUbash或GNUmake)。已经注意选择可以理解的名字。
利用现代源代码编辑器,如GNUemacs或vim。您可以将它们配置为只需按下几下键盘即可键入长标识符(它们具有自动完成功能;一些输入库(如GNU readline)也提供了这一功能)。由于您(或您的同事)花在阅读源代码上的时间要比花在键入上的时间多得多,因此这样的努力(命名好您的变量和标识符)值得您花费宝贵的时间。
如果使用GCC作为编译器,请将其作为gcc -Wall -Wextra -g
调用,以获得大量警告和调试信息。您也可以使用静态源代码分析工具,如Frama-C或Clang静态分析器。
对于现实生活中的软件项目(例如GTK),您将有一个指定编码约定的文档,您可以编写一些GCC插件来检查其中的大部分。另请参见DECODER项目。
对于软件项目的某些部分,您可以使用C代码生成器,如SWIG或GNUbison。在某些情况下,您将拥有自己的C代码生成器。然后确保生成长C标识符,以减少名称冲突的可能性。
一些代码混淆工具正在重命名大多数C标识符。如果你发送的C源代码没有注释,并且大多数标识符都是像_0TwK4TkhEG
一样生成的,那么生成的C代码可以在你的客户端进行编译,实际上是不可读的。从技术上讲,您可以编写一个代码混淆器,将可读的C代码转换为神秘的C代码。