程序环境和预处理
一,程序的环境
1.程序的翻译环境
在翻译环境中源代码将被转换为可执行的机器指令。
2.程序的执行环境
执行环境就是用于实际执行代码。
二,编译和链接
1.翻译环境
程序的翻译环境是指用于将一种编程语言的源代码翻译为另一种编程语言或者目标代码的工具和环境。它通常由编译器或解释器组成,以及相关的库和工具。
编译器是一种将高级编程语言转换为机器语言或者低级中间代码的程序。编译器将源代码作为输入,经过词法分析、语法分析、语义分析、优化和代码生成等阶段,最终生成可执行的目标代码或者字节码等形式。
解释器则是一种逐行解释并执行源代码的程序。解释器将源代码逐行翻译成机器语言或者中间代码,并立即执行。解释器通常不需要生成可执行的目标代码,因此可以更快地执行源代码,但速度可能相对较慢。
除了编译器和解释器,翻译环境还包括其他工具和库,用于辅助编译、调试和优化代码。例如,调试器可以帮助开发人员查找和修复程序中的错误,静态分析工具可以对代码进行静态分析,找出潜在的问题和优化机会。
编译可以分为以下几个阶段:
- 词法分析:词法分析器(也称为扫描器)将源代码分割成一个个的词法单元(token),如变量名、关键字、运算符等。这个阶段主要用于识别和提取源代码中的基本单词。
- 语法分析:语法分析器(也称为解析器)使用词法分析器生成的词法单元,根据语法规则检查代码的结构,并创建一个抽象语法树(Abstract Syntax Tree,AST)。抽象语法树表示代码的语法结构,方便后续的语义分析和代码生成。
- 语义分析:语义分析器对抽象语法树进行检查,确保代码在语义上是正确的。它会检查变量的声明和使用是否一致,类型是否匹配,函数调用是否正确等。此阶段还可以进行一些语法糖的转换和优化。
- 优化:优化器对代码进行优化,以提高程序的执行效率和性能。它可以进行各种优化,如常量折叠、循环展开、代码块重排等,以减少执行时间和内存消耗。
- 代码生成:代码生成器将优化后的代码转换为目标机器的可执行代码。这个阶段根据目标平台的特定规则和约束,将抽象语法树转换为汇编语言或机器语言指令,生成可执行的目标代码。
2.运行环境
程序的运行环境是指程序在计算机系统中执行时所需要的软件和硬件环境。它包括以下几个方面:
- 操作系统:程序的运行环境需要一个操作系统来管理计算机的硬件资源和提供基本的服务。操作系统负责分配内存、管理进程和线程、提供文件系统访问等功能,为程序提供必要的运行支持。
- 运行时库:程序可能依赖于特定的运行时库(Runtime
Library)来提供一些常用的功能和服务。运行时库是一组预编译的代码库,包含了常见的函数、类和数据结构,可以在程序运行时被动态链接或静态链接到程序中。 - 虚拟机或解释器:对于一些高级编程语言,程序的运行环境可能需要一个虚拟机或解释器来执行程序。虚拟机是一个软件层,它模拟了一个计算机系统,能够解释和执行特定语言的字节码或中间代码。解释器则逐行解释执行源代码。
- 编程语言支持:程序的运行环境需要相应的编程语言支持,包括编译器、解释器或即时编译器等。这些工具将源代码或中间代码转换为可执行的机器代码或字节码,以便程序能够在计算机系统上执行。
- 硬件资源:程序的运行环境需要计算机系统提供必要的硬件资源,如中央处理器(CPU)、内存、硬盘空间、输入输出设备等。这些硬件资源用于程序的执行和与外部环境的交互。
程序的运行过程可以大致分为以下几个步骤:
- 编译或解释:如果程序使用的是编译型语言,首先需要将源代码通过编译器转换为目标代码或可执行文件。而对于解释型语言,程序会逐行地由解释器或虚拟机直接解释执行。
- 加载:在程序运行之前,操作系统会将程序及其所依赖的库或资源加载到内存中。这包括将可执行文件或字节码加载到内存中,并为程序分配所需的内存空间。
- 初始化:程序在加载到内存后,需要进行初始化操作。这可能包括变量的初始化、库的加载、建立数据结构等。程序在初始化过程中准备好运行所需的环境和资源。
- 执行:一旦程序初始化完成,操作系统会为程序创建一个进程,并将控制权交给程序的入口点。程序按照代码的顺序执行,依次执行每条语句或指令。根据代码的逻辑,程序可能会进行计算、读取或写入文件、与用户交互等操作。
- 终止:程序的运行过程可能在不同的条件下终止。这可以是程序执行完所有语句,或者遇到了错误导致程序崩溃,或者是用户主动终止程序运行。在终止过程中,程序会释放所占用的内存和资源,并将控制权返回给操作系统。
注意:
程序的运行过程可能涉及到多线程、并发、异常处理等复杂的概念和操作。具体的运行过程也可能因编程语言、操作系统和程序本身的特性而有所不同。以上是一个一般性的程序运行过程的简要描述,实际情况可能因具体的环境和实现方式而有所差异。
三,预处理
1.预定义符号
在编程中,开发人员可以利用这些预定义符号来简化代码的编写和理解。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
2.#define
1.#define定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
例如:
#define name( parament-list ) stuff
注意:
参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
例如:
#define SQUARE( x ) x * x
//...
//使用这个宏时
int a = 5;
printf("%d\n" ,SQUARE( a + 1) );
打印结果后,结果为11
这是以为替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );
想要结果打印为36,只需要在宏定义上加两个括号就可以了
#define SQUARE(x) (x) * (x)
此时预处理时就是:
printf ("%d\n",(a + 1) * (a + 1) );
2.#define替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先
被替换。 - 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上
述处理过程。
注意:
宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
3.#和##
4.宏和函数对比
宏通常被应用于执行简单的运算。
比如在两个数中找出较大的一个。
#define MAX(a, b) ((a)>(b)?(a):(b))
相比于函数,宏有以下好处:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于>来比较的类型。
- 宏是类型无关的。
宏也有以下缺点:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序
的长度。 - 宏是没法调试的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
3.#undef
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除
4.命令行定义
许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。命令行定义是指在命令行界面(Command Line Interface,CLI)中使用的命令和参数的规定和格式。通过命令行定义,用户可以在命令行中输入特定的命令和参数来执行相应的操作或请求系统提供特定的服务。
5.条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令
常见的条件编译指令:
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
6.文件的包含
1.包含方式
本地文件包含
#include "filename"
这种文件的包含方式对应的查找策略是先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误
库文件包含
#include <filename.h>
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
对于库文件也可以用 #include " "的形式去包含,但是效率就比较低下,也不容易区分本地文件还是库文件。
2.嵌套文件包含
当出现嵌套文件包含的情况时,可以使用条件编译的方式解决
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__
也可以
pragma once
去避免头文件的重复使用。