比较C/C++、Java与Python编译运行的异同
Author:Redamancy
编译原理
1. C/C++
C/C++的编译运行过程可以分为以下几个主要步骤:
- 源代码编写:
- 开发者编写C/C++源代码文件,通常以
.c
(C语言)或.cpp
(C++语言)为扩展名。
- 开发者编写C/C++源代码文件,通常以
- 预处理(Preprocessing):
- 预处理器处理源代码中的预处理指令(如
#include
,#define
,#ifdef
等)。 - 展开宏定义、包含头文件内容,并处理条件编译指令。
- 生成一个没有预处理指令的纯文本文件。
- 预处理器处理源代码中的预处理指令(如
- 编译(Compilation):
- 词法分析(Lexical Analysis): 将预处理后的代码分解为 tokens,即语言的最小语法单位(如关键字、标识符、运算符等)。
- 语法分析(Syntax Analysis): 将 tokens 序列转换为抽象语法树(AST),并检查代码的语法是否正确。
- 语义分析(Semantic Analysis): 检查代码的语义正确性,如类型检查、作用域解析等。
- 中间代码生成(Intermediate Code Generation): 将语法树转换为中间代码,便于优化和跨平台编译。
- 代码优化(Code Optimization): 对中间代码进行优化,以提高最终代码的效率。
- 目标代码生成(Code Generation): 将优化后的中间代码转换为目标机器的汇编代码。
- 汇编(Assembly):
- 汇编器将汇编代码转换为机器码,生成目标文件(通常是
.o
或.obj
文件)。
- 汇编器将汇编代码转换为机器码,生成目标文件(通常是
- 链接(Linking):
- 链接器将一个或多个目标文件与库文件链接在一起,解析符号引用,生成最终的可执行文件(通常是
.exe
文件)。 - 这一步包括符号重定位、库函数链接等操作。
- 链接器将一个或多个目标文件与库文件链接在一起,解析符号引用,生成最终的可执行文件(通常是
- 运行(Execution):
- 可执行文件生成后,可以在目标平台上直接运行。
- 操作系统加载可执行文件到内存中,并执行程序的指令。
示例流程
假设有一个简单的C程序文件 main.c
:
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
编译运行过程:
- 预处理:
- 预处理器处理
#include <stdio.h>
指令,将stdio.h
头文件的内容包含到main.c
中。 - 生成预处理后的文件,如
main.i
。
- 预处理器处理
- 编译:
- 编译器将
main.i
编译为汇编代码,生成汇编文件,如main.s
。
- 编译器将
- 汇编:
- 汇编器将
main.s
汇编为机器码,生成目标文件,如main.o
。
- 汇编器将
- 链接:
- 链接器将
main.o
与标准库(如libc
)链接,生成可执行文件,如main
或main.exe
。
- 链接器将
- 运行:
- 操作系统加载并执行
main
或main.exe
,程序输出Hello, World!
。
- 操作系统加载并执行
命令行示例(使用GCC)
-
预处理:
gcc -E main.c -o main.i
-
编译:
gcc -S main.i -o main.s
-
汇编:
gcc -c main.s -o main.o
-
链接:
gcc main.o -o main
-
运行:
./main
通过这些步骤,C/C++源代码被转换为可执行文件,并可以在目标平台上运行。
2. java
Java的编译运行过程与C/C++有些不同,它涉及到Java特有的编译器和运行时环境。以下是Java程序的编译和运行过程的详细步骤:
- 源代码编写:
- 开发者编写Java源代码文件,通常以
.java
为扩展名。Java源代码由多个类组成,每个类通常对应一个.java
文件。
- 开发者编写Java源代码文件,通常以
- 编译(Compilation):
- Java源代码首先需要通过Java编译器(
javac
)进行编译。 - 编译器将Java源代码文件编译为字节码文件(
.class
文件)。字节码是一种中间表示形式,与平台无关。 - 编译过程包括:
- 词法分析:将源代码分解为tokens。
- 语法分析:将tokens转换为抽象语法树(AST)。
- 语义分析:检查代码的语义正确性,如类型检查、作用域解析等。
- 中间代码生成:将AST转换为字节码。
- 代码优化:对字节码进行优化。
- Java源代码首先需要通过Java编译器(
- 字节码文件:
- 编译后的字节码文件(
.class
文件)包含了Java虚拟机(JVM)可以执行的字节码指令。 - 这些字节码文件是与平台无关的,理论上可以在任何支持JVM的平台上运行。
- 编译后的字节码文件(
- 类路径设置(Classpath Setup):
- 在运行Java程序之前,需要设置类路径(classpath),告诉JVM在哪里可以找到这些字节码文件。
- 类路径可以通过命令行参数(
-classpath
或-cp
)或环境变量(CLASSPATH
)来设置。
- 运行(Execution):
- 使用Java运行时环境(JRE)中的Java虚拟机(JVM)来运行字节码文件。
- 执行过程包括:
- 类加载:JVM加载字节码文件到内存中。
- 字节码校验:JVM校验字节码的正确性和安全性。
- 解释执行(Interpreting):JVM解释执行字节码指令,将其转换为机器码并执行。
- JIT编译(Just-In-Time Compilation):在运行时,JVM可以选择将频繁执行的字节码编译为本机机器码,以提高执行效率。
示例流程
假设有一个简单的Java程序文件 HelloWorld.java
:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
编译运行过程:
-
编译:
javac HelloWorld.java
- 编译器将
HelloWorld.java
编译为HelloWorld.class
字节码文件。
- 编译器将
-
运行:
java HelloWorld
- JVM加载
HelloWorld.class
文件,并执行其中的main
方法。 - 程序输出
Hello, World!
。
- JVM加载
详细步骤解释
-
编译(javac):
javac HelloWorld.java
- 生成的
HelloWorld.class
文件包含了字节码指令。
- 生成的
-
类路径设置:
- 默认情况下,JVM会在当前目录下查找
.class
文件。如果需要指定类路径,可以使用:
java -cp path/to/classes HelloWorld
-
默认情况下,JVM会在当前目录下查找
.class
文件: -
当运行 Java 程序时,JVM 默认会在当前工作目录(即你运行
java
命令时所在的目录)中查找指定的类文件。 -
例如,如果你在终端或命令提示符中进入项目根目录并运行
java HelloWorld
,JVM 会在该目录及其子目录中查找HelloWorld.class
文件。 -
如果需要指定类路径,可以使用
-cp
或--classpath
参数: -
在某些情况下,类文件可能不在当前目录中,或者你需要从不同的目录中加载多个类文件。这时候,可以通过
-cp
或--classpath
参数来指定类路径。 -
这个参数告诉 JVM 去指定的路径下查找类文件,而不是默认的当前目录。
-
命令行使用示例:
java -cp path/to/classes HelloWorld
-
-cp
或--classpath
是参数的关键字,表示类路径。 -
path/to/classes
是指定的目录路径,JVM 将在这个目录及其子目录中查找类文件。 -
HelloWorld
是要运行的类名。 -
执行效果:JVM 会在
path/to/classes
目录下查找HelloWorld.class
文件并执行它。 -
类路径可以包含多个路径:
-
多个路径之间用分号(Windows)或冒号(Linux/macOS)分隔。例如:
java -cp path1/to/classes;path2/to/classes HelloWorld
-
分号
;
用于 Windows 系统。 -
冒号
:
用于 Linux 和 macOS 系统。 -
支持通配符:
-
路径中可以使用通配符来匹配多个目录或 JAR 文件。例如:
java -cp lib/* HelloWorld
lib/*
表示lib
目录下的所有 JAR 文件。
-
-
默认情况下,JVM 会在当前目录中查找类文件。如果类文件在其他目录,或者你需要从一个或多个目录中加载类文件,可以使用
-cp
或--classpath
参数来指定类路径。这个参数允许你指定一个或多个目录或 JAR 文件,JVM 将在这些路径中查找指定的类文件。
- 默认情况下,JVM会在当前目录下查找
-
运行(java):
java HelloWorld
- JVM加载
HelloWorld.class
文件,并执行其中的main
方法。 - JVM解释执行字节码指令,如果启用了JIT编译,还会在运行时将部分字节码编译为本机机器码。
- JVM加载
通过这些步骤,Java源代码被编译为字节码并由JVM执行。Java的这种机制使得它的程序具有良好的跨平台性,因为字节码可以在任何安装了JVM的平台上运行。
3. Python
Python 的编译运行过程与其他编程语言(如 C/C++ 和 Java)有些不同。Python 是一种解释型语言,执行过程通常不涉及中间字节码的生成。因此,Python 的编译和运行过程一般可以分为以下几个步骤:
1. 源代码编写
-
开发者编写 Python 源代码文件,通常以
.py
为扩展名。 -
例如,创建一个名为
hello.py
的文件,内容为:
print("Hello, World!")
2. 解释执行
- Python 使用解释器来执行代码,通常是 CPython(Python 的官方实现)。
- 当执行 Python 程序时,解释器会按以下步骤处理代码:
a. 词法分析(Lexical Analysis)
- 源代码会被解析为一系列的词法单元(tokens),这些 tokens 是易于处理的代码组成部分,如关键字、标识符、操作符等。
b. 语法分析(Syntax Analysis)
- 词法分析后的 tokens 将被解析为一个语法树(Parse Tree或Abstract Syntax Tree,AST),以便于理解代码的结构。
- 在这一步,解释器会检查代码是否符合 Python 的语法规则。
c. 语义分析(Semantic Analysis)
- 解释器检查逻辑和语义上的正确性,如变量的定义和使用是否一致,类型匹配等。
d. 字节码生成(Bytecode Generation)
- 语法树将被编译为字节码(bytecode),字节码是与平台无关的中间代码,适合于在 Python 虚拟机(PVM,Python Virtual Machine)中执行。
- 通常,字节码存储在
.pyc
文件中(在执行时会自动生成)。
e. 解释执行(Bytecode Execution)
- 解释器将字节码传递给 Python 虚拟机(PVM)进行执行。
- 在这一阶段,真正的代码执行发生。PVM 将字节码逐条解释执行,并在需要时调用相应的 C 函数。
3. 模块和包的处理
- Python 支持模块和包的导入,导入时会执行对应模块的字节码,如果模块未编译成字节码文件,则会运行源代码进行编译。
示例流程
假设有一个简单的 Python 文件 hello.py
:
print("Hello, World!")
执行过程:
-
执行 Python 程序:
python hello.py
-
词法分析:
- 解释器将源代码分解为 tokens。
-
语法分析:
- 解析 tokens 生成 AST。
-
语义分析:
- 检查代码的语义正确性。
-
字节码生成:
- 生成字节码并存储为
.pyc
文件(如果使用了__pycache__
目录来缓存)。
- 生成字节码并存储为
-
解释执行:
- PVM 逐条执行字节码,最终输出:
Hello, World!
总结
Python 的执行过程比较灵活,不同于 Java 的严格编译和运行步骤。Python 直接将源代码转换为字节码,并在运行时解释执行。这种机制使得 Python 具有良好的交互性和易用性,但在执行效率方面相较于编译型语言会稍显劣势。
- 逐行执行:Python 代码由 Python 解释器逐行读取并执行,这和编译型语言需要将整个代码转换为机器语言形成的二进制文件的过程不同。
- 交互性:Python 允许在交互式环境(如 REPL)中逐行输入和执行代码,这使得开发和调试更加方便。
虽然 Python 没有传统意义上的编译器(将源代码编译成机器码),但它确实包含了编译过程并通过字节码实现了跨平台的兼容性。由于 Python 是解释型语言,用户可以快速测试代码而无需手动编译,增加了开发的便捷性。
实践体会
1. C/C++
Visual Studio:
在使用 Visual Studio 开发 C/C++ 项目时,通常不需要下载 GCC,因为 Visual Studio 自带了自己的编译器和工具链。这些工具包括 Microsoft 的 C/C++ 编译器(MSVC),可以直接用于编译和调试 C/C++ 代码。
关键点
- 内置编译器:
- Visual Studio 提供了 MSVC 编译器,开发者可以在其环境中直接编写、编译和调试 C/C++ 代码,而无需额外安装 GCC。
- 可选择的工具链:
- 如果你希望使用 GCC 编译器,可以通过安装 MinGW(Minimalist GNU for Windows)或 Cygwin 来获得 GCC 编译器。但这不是使用 Visual Studio 的必要步骤。
- 使用 Visual Studio 进行开发:
- 一旦在 Visual Studio 中创建了 C/C++ 项目,你可以直接在 IDE 中编写代码,然后使用自带的 MSVC 编译器进行编译和运行。
- 使用 WSL(Windows Subsystem for Linux):
- 如果你对使用 Linux 版本的 GCC 感兴趣,可以在 Windows 10 和更高版本中启用 WSL(Windows Subsystem for Linux),并在 WSL 环境中安装 GCC。在这种情况下,你可以在 Visual Studio 中设置外部工具或使用终端运行代码。
如果你主要使用 Visual Studio 开发 C/C++ 项目,使用自带的 MSVC 编译器是最简单的做法,不需要额外下载和安装 GCC。但是,如果你有特定的需求需要使用 GCC,可以考虑安装 MinGW 或 WSL。
Visual Studio Code:
在 Visual Studio Code(VSCode)中编写和编译 C/C++ 程序,通常需要单独安装 GCC 编译器或其他编译工具链。**VSCode 本身是一个轻量级的代码编辑器,支持多种编程语言,但不包含任何编译器或解释器。**以下是具体的步骤和建议:
下载和安装 GCC
gcc
和g++
都是GNU编译器套件(GNU Compiler Collection,简称GCC)中的编译器前端,但它们主要用于编译不同的编程语言代码。
gcc
是GNU C Compiler,用于编译C语言代码(扩展名为.c
的文件)。默认包含C语言的标准库头文件(如<stdio.h>
、<stdlib.h>
等)。g++
是GNU C++ Compiler,用于编译C++语言代码(扩展名为.cpp
或.cxx
的文件)。默认包含C++语言的标准库头文件(如<iostream>
、<vector>
等),并且还会链接C++标准库(如libstdc++
)
gcc
编译命令:gcc hello.c -o hello
g++
编译命令:g++ hello.cpp -o hello
混合使用
需要注意的是,
gcc
也可以编译C++代码(扩展名为.cpp
的文件),但可能需要手动链接C++标准库:gcc hello.cpp -o hello -lstdc++
然而,更推荐使用
g++
来编译C++代码,因为它会自动处理C++相关的内容。
- 选择合适的 GCC 发行版:
- MinGW(Minimalist GNU for Windows):提供了适用于 Windows 的 GCC 编译器。适合大多数简单的 C/C++ 开发。
- Cygwin:提供 GNU 环境,可以选择更完整的功能,但安装较复杂。
- WSL(Windows Subsystem for Linux):如果你在 Windows 10 或更高版本,可以启用 WSL,安装 Linux 发行版,然后在上面安装 GCC。这使得你能够在 VSCode 中开发符合 Linux 标准的 C/C++ 项目。
- 安装 MinGW:
在 VSCode 中配置 GCC
安装 C/C++ 插件:
- 打开 VSCode,点击左侧的扩展(Extensions)图标,搜索并安装 Microsoft 的 C/C++ 插件。这将为你提供代码高亮、智能提示和调试功能。
配置任务(tasks.json):
- 为了便于编译和运行 C/C++ 代码,你需要在你的工作区中创建一个任务配置文件。可以通过按
Ctrl + Shift + P
打开命令面板,然后输入并选择Tasks: Configure Default Build Task
来创建或编辑tasks.json
文件。示例
tasks.json
配置如下:{ "version": "2.0.0", "tasks": [ { "label": "build", "type": "shell", "command": "gcc", "args": [ "-g", "${file}", "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": ["$gcc"] } ] }
编写和运行代码:
- 你可以创建一个新的 C 或 C++ 文件,编写代码,然后按
Ctrl + Shift + B
来运行你配置的构建任务。- 编译成功后,在 VSCode 的终端中运行生成的可执行文件。例如,如果你在 Windows 中使用 MinGW,生成的可执行文件可能在当前文件夹中,你可以输入类似
.\your_program.exe
的命令运行它。总结
- 对于在 VSCode 中编写和编译 C/C++ 代码,确实需要单独下载和安装 GCC 或其他编译器。
- 安装和配置后,你可以充分利用 VSCode 的功能进行 C/C++ 开发,方便地编写、编译和调试代码。
-
编写Hello.txt文件
在计算机中,文件的扩展名(后缀)主要用于指示文件的类型,以便操作系统和应用程序知道如何处理该文件。然而,文件的实际内容和格式才是决定文件能否被正确解析和运行的关键因素。
为何可以通过更改后缀来“编译运行” ?
-
扩展名的作用:
- 标识文件类型:扩展名不仅帮助操作系统识别文件类型,它还告诉编程环境或编译器该如何处理这个文件。例如,
.c
表示是C语言源代码,.cpp
表示是C++语言源代码,.java
表示是Java源代码,.py
表示是Python源代码。 - 与编辑器的关联:许多文本编辑器和IDE会根据文件的扩展名来选择合适的语法高亮和代码补全功能。
- 标识文件类型:扩展名不仅帮助操作系统识别文件类型,它还告诉编程环境或编译器该如何处理这个文件。例如,
-
文件内容的决定因素:
- 无论文件的扩展名是什么,编译器或解释器最终都关注的是文件的实际内容是否符合特定编程语言的语法规则。如果一个
.txt
文件包含符合C语言语法的代码,那么将其扩展名改为.c
并交给C编译器(如gcc
)时,它可以成功编译和运行。
- 无论文件的扩展名是什么,编译器或解释器最终都关注的是文件的实际内容是否符合特定编程语言的语法规则。如果一个
-
示例:
假设你有一个.txt
文件hello.txt
,内容如下:#include <stdio.h> int main() { printf("Hello, World!\n"); return 0; }
如果你将这个文件重命名为
hello.c
,然后使用如下命令编译:gcc hello.c -o hello
然后运行:
./hello
程序将会正常输出:
Hello, World!
通过更改文件的扩展名可以使其被编译器或解释器识别并运行,但核心是文件的内容是否符合特定编程语言的语法。良好的编程习惯是使用正确的文件扩展名,以便于代码管理和团队协作。
#include<iostream> using namespace std; int main() { cout << "Helloooooooo!!!"; }
-
-
把Hello.txt文件后缀改为.cpp
-
终端输入预处理、编译、汇编、链接指令
- 预处理:
g++ -E Hello.cpp -o Hello.i
-
编译:
gcc -S Hello.i -o Hello.s
-
汇编:
gcc -c Hello.s -o Hello.o
-
链接:
gcc Hello.o -o Hello
-
运行程序
2. Java
-
编写Hello.txt文件
public class Hello { public static void main(String args[]) { System.out.println("Helooooo!"); } }
-
把Hello.txt文件后缀改为.java
-
编译java文件
javac Hello.java
4. 运行java程序
为什么
java Hello
是对的,而java Hello.class
报错呢?
java Hello
命令正确执行而java Hello.class
报错的原因,可以从以下几个方面来解释:
- 命令格式与约定:
- 在Java中,使用
java
命令来运行一个程序时,应该跟随的是类的名称,而不是文件名称或文件扩展名。因此,正确的命令格式是java ClassName
,其中ClassName
是想要运行的Java类的名称。- Java虚拟机的执行方式:
- Java虚拟机(JVM)通过
java
命令来启动,并期望找到一个类的名称作为参数,以便它可以加载并执行该类中的main
方法(如果存在)。- 当执行
java Hello
时,JVM会查找名为Hello
的类,并尝试加载和执行它。如果找到了相应的.class
文件,并且该文件包含了一个有效的main
方法,那么程序就会成功运行。- 然而,当执行
java Hello.class
时,JVM会尝试查找名为Hello.class
的类,这通常是不存在的,因为.class
是文件扩展名,而不是类名的一部分。因此,JVM无法找到对应的类来执行,从而导致报错。- 错误消息:
- 如果尝试执行
java Hello.class
,可能会收到类似“Error: Could not find or load main class Hello.class”的错误消息。这是因为JVM无法根据提供的名称找到或加载主类。综上所述,
java Hello
是正确的命令格式,因为它遵循了Java的命名约定和JVM的执行方式;而java Hello.class
则是不正确的,因为它包含了文件扩展名,导致JVM无法找到对应的类来执行。
java Hello
3. Python
-
编写Hello.txt文件
print("Heloooooo!")
-
直接解释运行Hello.txt
python Hello.txt
- 把Hello.txt文件后缀改为.py
- 解释运行Hello.py
-
直接逐行解释运行