【编译原理笔记】2.1 Programming Language Basics
对应龙书中1.6节
一、静态与动态特性(Static vs Dynamic)
1.1 基本定义
-
静态问题(Static Issues):在编译时就能确定和处理的问题
-
动态问题(Dynamic Issues):需要在运行时才能解决的问题
1.2 静态特性示例
// C/Java中的声明作用域int x; // 编译时确定作用域static class MyClass { // 静态类对象的位置在编译时确定// ...}
1.3 动态特性示例
void function() {int local_var; // 局部变量的位置在运行时确定(栈分配)}
1.4 关键区别
特性类型 | 处理时机 | 示例 | 影响 |
---|---|---|---|
静态 | 编译时 | 作用域、静态类型检查 | 编译效率、错误检测 |
动态 | 运行时 | 局部变量分配、多态 | 运行灵活性、性能开销 |
二、环境与状态(Environments and States)
2.1 两层映射模型
上图展示了名字到值的两阶段映射
2.2 环境(Environment)
-
功能:将名字(Names) 映射到存储位置(Locations)
-
性质:相对静态,在编译时部分确定
-
示例:变量名到内存地址的映射
2.3 状态(State)
-
功能:将存储位置(Locations) 映射到值(Values)
-
性质:高度动态,在运行时不断变化
-
示例:内存地址存储的实际数据值
2.4 实际意义
int x = 10; // 环境:x → 内存地址0x1000// 状态:0x1000 → 值10x = 20; // 状态变化:0x1000 → 值20(环境不变)
三、静态作用域与块结构(Static Scopes/Block Structures)
3.1 静态作用域规则
3.2 最近嵌套规则(Most Closely Nested Rule)
-
名字查找顺序:从最内层块开始,逐层向外
-
遮蔽(Shadowing):内层声明遮蔽外层同名声明
-
作用域确定:在编译时通过程序结构确定
3.3 块结构特性
-
嵌套性:块可以嵌套在其他块中
-
局部性:块内声明只在块内有效
-
确定性:作用域在编译时完全确定
四、显式访问控制(Explicit Access Control)
4.1 访问修饰符
-
Public:公开访问,任何代码都可访问
-
Private:私有访问,只在类内部可访问
-
Protected:保护访问,类及其子类可访问
4.2 封装性设计
class MyClass {public int publicVar; // 任何地方可访问private int privateVar; // 仅本类内可访问 protected int protVar; // 本类及子类可访问}
4.3 设计目标
-
信息隐藏:隐藏实现细节
-
接口隔离:明确公开的API
-
维护性:减少外部依赖带来的影响
五、动态作用域(Dynamic Scope)
5.1 定义规则
"A use of a name x refers to the declaration of x in the most recently called procedure with such a declaration."
最近调用原则:名字x引用的是最近调用的包含x声明的过程中的声明。
5.2 与静态作用域对比
// 静态作用域示例int x = 1;void foo() { printf("%d", x); } // 总是输出1// 动态作用域伪代码 int x = 1;void bar() { int x = 2; foo(); } // 如果动态作用域,foo()输出2
5.3 实际应用场景
-
宏扩展(Macro Expansion)
-
面向对象编程中的方法解析
-
某些脚本语言(如早期LISP)
5.4 动态作用域的问题
-
不可预测性:行为依赖于运行时调用序列
-
调试困难:相同代码在不同上下文行为不同
-
维护复杂:难以理解程序逻辑
六、参数传递机制(Parameter Passing Mechanisms)
6.1 传值调用(Call by Value)
void f(int x) {x = x + 1; // 修改形参xprint(x); // 输出增加后的值}int main() {int y = 1;f(y + 1); // 传递表达式y+1的值// y的值不变,仍为1}
特点:
-
传递实际参数的副本
-
函数内修改不影响原始变量
-
适用于基本数据类型
6.2 传引用调用(Call by Reference)
void swap(int &x, int &y) { // C++引用参数int temp = x;x = y;y = temp;}int main() {int a = 1, b = 2;swap(a, b); // a和b的值被交换}
应用场景:
-
数组传递
-
指针参数
-
类对象传递(大多数面向对象语言)
特点:
-
传递实际参数的引用
-
函数内修改影响原始变量
-
适用于需要修改参数值的场景
6.3 传名调用(Call by Name)
#define MAX(a, b) ((a) > (b) ? (a) : (b))int main() {int x = 1, y = 2;int z = MAX(++x, y); // 展开为: ((++x) > (y) ? (++x) : (y))// x可能被多次求值(这里x变为3)}
特点:
-
类似宏替换,参数表达式在调用点展开
-
可能多次求值同一表达式
-
现代语言较少使用(Algol 60特性)
七、别名问题(Aliasing)
7.1 别名定义
"An interesting consequence of call-by-reference parameter passing. It is possible that two formal parameters can refer to the same location."
别名:多个名字指向同一个内存位置。
典型示例
void q(int x[], int y[]) {x[10] = 2; // 修改x// 如果x和y是同一数组,y[10]也变为2
}void p() {int a[100];q(a, a); // 传递同一数组的两个别名
}
7.2 别名产生场景
-
传引用调用:多个参数指向同一对象
-
指针别名:不同指针指向同一内存
-
数组重叠:数组切片或子数组
7.3 别名的问题
-
优化障碍:编译器难以确定内存独立性
-
正确性风险:意外修改共享数据
-
调试困难:难以追踪数据流
解决方案
// 使用const限制修改
void q(const int x[], int y[]) {// x[]只读,避免意外修改y[10] = 2;
}// 使用restrict关键字(C99)
void q(int *restrict x, int *restrict y) {// 向编译器保证x和y不重叠x[10] = 2;
}
八、核心概念总结
静态vs动态的权衡
方面 | 静态优势 | 动态优势 |
---|---|---|
性能 | 编译时优化 | 运行时灵活性 |
错误检测 | 早期发现 | 适应性强 |
确定性 | 行为可预测 | 支持多态 |
作用域规则演进
动态作用域(早期语言) → 静态作用域(现代语言) → 显式访问控制(面向对象)
参数传递发展
传名调用(Algol) → 传值调用(函数式) → 传引用调用(面向对象)
现代语言设计趋势
-
默认安全:传值为主,显式传引用
-
静态检查:尽可能在编译时发现问题
-
明确语义:减少隐式行为和别名
-
封装性:通过访问控制管理复杂性
这些基础概念为理解编程语言设计、编译器实现和程序正确性提供了理论基础,是计算机科学教育的核心组成部分。