相信每一位计算机专业的学生都可能有过这样的体验:在大学的某个阶段,突然之间,过去所学的各门课程——从数字电路中的逻辑门,到计算机组成原理,再到汇编语言、C语言、C++乃至Java——仿佛一下子贯通起来,形成了一个有机的整体。这一刻,每一种语言的出现不再显得孤立,而是呈现出清晰的历史脉络与内在逻辑。


阅读指引

要理解本文,建议读者具备以下基础:

  • 拥有一定的编程实践,代码量不少于5万行;

  • 理解汇编语言的基本概念,如静态数据段、代码段和堆栈段。

若还具备以下知识更佳:

  • 了解C语言的编译过程、C++对象模型及MFC反射机制;

  • 熟悉Java解释器的工作原理;

  • 使用过JavaScript、Python或PHP等动态语言,体会过运行时类型系统的灵活性;

  • 掌握计算机组成原理。


序言:若干基本问题

  1. 程序设计语言的根本目标是什么?

  2. 为什么大多数语言都以“控制流”(顺序执行与跳转)为基础?这种模式与实际应用需求(如教务管理系统或打车应用)之间存在怎样的差距?

  3. 在C语言中,为何类型声明需与控制流分离?

  4. 当代主流语言最基本的构成元素是什么?

  5. 是否存在类型结构可在运行时修改的语言?动态性的含义是什么?


何为动态性?

动态性涉及以下三方面:

  1. 编译阶段确定哪些信息,之后不再变更;

  2. 运行时能够修改或添加什么;

  3. 运行时是否保留类型信息。


程序中的信息分类

  1. 数据信息
    a) 编译期元数据(类型框架、内存占用等,仅于编译期间维护,结束后丢弃)
    b) 运行期元数据(继承关系、用于new或反射操作等)(需注意区分编译与运行时的元数据)
    c) 堆栈段中的地址偏移(例如C++的switch-case语句中不可声明变量,或因共享内存限制)
    d) 静态段中的地址

  2. 指令信息
    a) 代码段(实现动态性需操作系统或虚拟机支持,如动态链接库、动态类加载、Lisp中的代码自生成)


各类语言分析

汇编语言

汇编语言是否具备动态性?答案是否定的。原因在于:其一,寄存器、数据段、堆栈和代码段完全由程序员显式控制,所有结构在编写时即已固定;其二,遵循冯·诺依曼体系的基本工作方式:取指、执行,周而复始。

既然已有数据段,为何还需要堆栈段?堆栈的出现实为模块化发展的产物。最初的汇编并无堆栈,直到上世纪60年代模块化思想兴起,堆栈才随之引入。在汇编中,模块体现为子程序,但其管理仍完全依赖于程序员。

堆栈与模块化的优点包括:

  • 支持递归调用;

  • 将功能拆分为可复用的模块;

  • 实现了作用域封装。

其缺点则有:

  • 时间开销:保存与恢复现场带来的额外成本(部分高级语言通过“尾递归消除”优化降低此开销);

  • 空间风险:存在堆栈溢出的可能。

C语言

C语言相较于汇编引入了哪些新元素?

  1. 编译器

  2. 表达式(可处理更多操作数)

  3. 函数与模块化结构(真正实现了模块化,堆栈操作对程序员透明)

  4. 类型系统(包括基本类型、结构体、数组和指针)

  5. 头文件与库机制

本质上,C语言并未增加汇编无法实现的功能,而是通过编译器将汇编中的繁琐操作抽象化,减轻了程序员的负担。

编译器的主要作用包括:

  • 解析表达式与控制流(汇编指令通常仅1-3个操作数,而表达式可包含多个);

  • 实现函数与模块的抽象(自动处理现场保存与恢复);

  • 管理类型与变量(将所有变量转换为直接地址访问,保证最高访问效率);

  • 代码优化。

变量如何被替换为直接地址?
a) 编译期元数据(如结构体成员、数组长度等,用于生成指令流,编译结束后丢弃);
b) 计算各变量在模块内的偏移地址(一旦确定即固化于执行码中);
c) 管理变量存储(所有内存布局在编译期确定,执行码中不保留类型信息,仅通过地址操作数据,与汇编类似。寄存器分配则由底层算法处理)。

C编译生成的执行码与汇编输出有无区别?在优化后,两者几乎无法区分。正如无法通过猫的叫声判断其品种一样,也无法通过执行码区分是由汇编还是C语言编写。

C语言在效率上的额外成本体现在:

  • 编译时间;

  • 模块堆栈操作。

但经过优化后,C程序的执行效率可与汇编媲美。从原理上讲,C语言并未引入新的运行时机制,因此可将其视为一种高级的汇编宏。

C++语言

(推荐阅读《深度探索C++对象模型》)

C++在C基础上增加了:

  1. 成员函数

  2. 类型继承体系

  3. 虚函数与虚继承

  4. 模板

  5. 多范式支持(语言更为抽象,逐渐脱离冯氏结构的束缚)

其中,面向对象思想使程序更贴近现实事物,设计模式的广泛应用也减轻了程序设计的负担。

编程范式可理解为一整套设计思想与行为准则(例如C属于过程式,Haskell为函数式,Java为面向对象式,Python为多范式混合,Shell用于命令调用,Lua用于扩展C程序,PHP专注于网页开发,Prolog则用于逻辑推理。此外,UML用于规范描述,XML用于数据存储,CSS和HTML分别负责网页样式与内容,JavaScript则较为独特)。

C++支持的范式包括:

  1. 过程式(使用STL进行类似C风格的编程)

  2. ADT式(自定义抽象数据类型,采用继承但不使用new或virtual;通过拷贝构造函数和RAII机制管理资源)

  3. 面向对象式(使用new和virtual,通过指针或引用实现多态)

  4. 泛型编程(参考《Modern C++ Design》,多用于底层库开发,业务代码中较少涉及)

C++编译器的实现方式?早期C++编译器cfront将C++代码转换为C代码,再由C编译器编译。C编译器无法区分代码源自C++还是C。

C++特性的实现方式简要说明:

  1. 成员变量:与C结构体类似,最终转换为地址直接访问;

  2. 成员函数:通过名称编码方案转换为C函数,并添加this指针参数(如___clsA12345func001(..., clsA *this));

  3. 类型继承体系:依靠编译期元数据实现,编译为C后该信息丢失;

  4. 虚函数与虚继承:通过虚函数表和虚基类表实现多态(运行时通过查表实现);

  5. 模板:通过代码复制实现,每次编译均需重新实例化,无法直接编译为库文件。

C++编译准则与虚机制:

  • 追求与C相当的效率,希望避免运行时间接调用,不使用运行期元数据,所有数据均按C结构体方式在编译期布局。

  • 上述目标在过程式和ADT范式中成立。

  • 但在面向对象范式中,需保持继承体系成员结构的一致性,以实现运行时多态:通过统一接口访问父类或子类的成员,无论实际对象类型。

虚机制的实现方式:
a) 虚函数:每个含虚函数的类型维护一张虚函数表(运行期元数据),通过查表调用相应函数;
b) 多重继承:编译器根据指针类型调整偏移量;
c) 虚继承:通过虚基类指针访问共享部分,缺点包括:子孙类需携带额外指针(Microsoft引入虚基类表优化),长链继承会导致访问层次加深。

跨平台层次:

  • 源代码跨平台(如C/C++,但受系统API差异影响,需大量宏支持);

  • 二进制跨平台(如Java)。
    C++虽在源代码层面可跨平台,但不同操作系统的API差异大大增加了难度,跨平台责任落于程序员肩上。编译器仅负责编译与链接。

如何高效使用C++?

  • 避免过多使用虚函数、虚继承和拷贝构造(带来额外开销);

  • 虚函数需通过指针寻址,效率低于内联函数;

  • 虚继承同样增加寻址成本(空虚基类可被优化,因此Java允许继承多个接口);

  • 避免在复杂继承层次中使用虚函数;

  • 虚机制会增加编译时间。

Java语言

(推荐阅读《本地Java代码的静态编译和动态编译问题》)

Java相较于C++的增加:

  1. 虚拟机(支持跨平台、动态编译和动态特性)

  2. 取消指针

  3. 仅支持接口的多继承

  4. 统一的类库

Java在编译方面迈出重要一步,其跨平台特性和运行时的灵活性为自身及后续语言发展提供了更多可能性。

虚拟机的优势:

  • 跨平台:在操作系统与字节码间添加隔离层,实现无缝跨平台;

  • 动态编译:部分信息延迟到运行时确定,支持动态特性;

  • 运行期维护类型信息,支持动态加载(如CORBRA依赖此机制)。

Java编译执行过程:

  1. 编译生成基于堆栈的字节码;

  2. 依赖JRE在不同OS上运行;

  3. 早期JRE采用解释执行,效率较低:
    a) 取指;
    b) 解码;
    c) 从操作数栈获取操作数;
    d) 执行操作;
    e) 结果写回堆栈。

如何提升执行效率?引入JIT(即时编译)机制:

  • 按函数编译,转换为中间表示并优化,生成本地代码;

  • 编译与执行线程分离,避免阻塞;

  • 通过Profiler分析热点代码,进一步优化(如对象池复用)。

动态编译的优点:

  • 根据程序行为优化代码(如热点方法、数组拷贝优化、多态调用优化);
    缺点:

  • 初始编译影响启动性能;

  • 运行期编译和分析占用资源;

  • 效率稳定需时间;

  • 实时图形程序难以容忍编译和GC延迟。

如何满足实时需求?采用AOT(预先编译):

  • 缺点:对反射和动态类加载支持较差。

JIT与AOT对比:Java更适合长期运行的服务端应用,如Web服务器或守护进程。

函数式语言

常见函数式语言包括:

  • Lisp、Scheme、Haskell(纯函数式)、F#
    支持函数式特性的语言:

  • Python、JavaScript、Java、C(部分支持)

函数式语言的特性:

  1. 函数无副作用,仅依赖输入输出;

  2. 支持高阶函数与lambda演算;

  3. 声明式语法,更易理解和自解释;

  4. 基于列表的编程,函数更具通用性;

  5. 惰性求值;

  6. 可形式化验证,可能证明正确性;

  7. 适合并行或分布式计算:
    a) 数据不可变;
    b) 惰性求值;
    c) 隐含计算依赖顺序,易于并发(过程式语言需通过锁、Actor等模式同步)。

总之,函数式语言更加抽象,更接近数学表达,远离冯·诺依曼体系的结构。

缺点:

  • 运行效率较低;

  • 开发环境和工具链较简单;

  • 应用范围较窄。

逻辑程序设计

Prolog语言用于人工智能和逻辑推理,本文暂不深入讨论。


总结

动态性的体现:

  1. 多态:根据实际对象类型调用方法;

  2. 反射:运行期维护类型信息;

  3. 动态类加载:运行中加载新类型;

  4. 动态链接:按需链接库文件。

编译与解释语言的界限?语言本身并无属性之分(如Java既可编译也可解释),少数特性依赖运行环境。

为何解释语言需虚拟机或运行环境?动态编译和元数据管理等功能具有通用性,抽离为运行时环境可避免重复实现。

脚本语言与非脚本语言:脚本语言用于调度其他程序(如Shell调用命令,Lua调用C)。

跨平台的层次:

  1. 源代码跨平台(如C/C++,但受系统API限制);

  2. 二进制跨平台(如Java,或直译执行的JavaScript/PHP)。

发展历史(推荐《近看图灵碗》)及学术语言:

  • Fortran、ALGOL58、ALGOL60、Lisp、Smalltalk

常用语言分类:

  • 过程式:C、ALGOL、Pascal

  • 面向对象:C++、Smalltalk、Java、Delphi

  • 函数式:Lisp、Scheme、Haskell

  • 逻辑式:Prolog

  • 脚本:PHP、Python、Ruby

  • 数据描述:XML、CSS、HTML


问题回答

  1. 程序设计语言的目的是什么?

    • 控制数据与控制指令流。

  2. 为何大多数语言以控制流为基础?

    • 历史原因,受冯·诺依曼体系结构影响。

  3. 为何C语言将类型声明与控制流分离?

    • 编译时需将类型信息转换为地址偏移,并替换控制流中的变量。

  4. 主流语言的基本元素是什么?

    • 控制流与类型系统。

  5. 是否存在运行时可修改类型结构的语言?

    • JavaScript使用原型继承,可在运行时为对象添加成员;Python、Java等则在运行时保留类型信息。


综上所述

  • 控制流对应指令执行顺序;

  • 类型系统用于计算变量地址;

  • 需明确区分编译期与运行期元数据。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐