新津网站建设,政务服务网站建设整改报告,房地产电商网站建设,网站建设 金手指排名霸屏目录 一、程序的翻译环境和执行环境 二、编译和链接详解 2、1 翻译环境 2、2 编译过程详解 2、3 执行环境 三、预处理详解 3、1 预定义符号 3、2 #define 3、2、1 #define定义的符号 3、2、2 #define 定义宏 3、2、3 #define 替换规则 3、3 宏和函数的对比 3、4 条件编译 3、5… 目录 一、程序的翻译环境和执行环境 二、编译和链接详解 2、1 翻译环境 2、2 编译过程详解 2、3 执行环境 三、预处理详解 3、1 预定义符号 3、2 #define 3、2、1 #define定义的符号 3、2、2 #define 定义宏 3、2、3 #define 替换规则 3、3 宏和函数的对比 3、4 条件编译 3、5 头文件的包含 3、5、1 头文件被包含的方式 3、5、2 嵌套文件包 标题C语言的程序环境和预处理详解 作者Ggggggtm 寄语与其忙着诉苦不如低头赶路奋路前行终将遇到一番好风景 我们平常写的代码都是通过编译器来运行的。我们有没有想过编译器是怎么将代码转化为各种指令最后输出结果呢这篇文章会详细解释编译器的运行的整个过程的细节希望会对你有所帮助。 一、程序的翻译环境和执行环境 我们可以简单认为编译器把代码首先进行翻译然后再执行。所以在ANSIC的任何一种实现中存在两个不同的环境 第1种是翻译环境在这个环境中源代码被转换为可执行的机器指令。 第2种是执行环境它用于实际执行代码。 那编译器具体是怎么翻译和执行的呢这就要看编译和链接的过程了。我们接着往下看。 二、编译和链接详解
2、1 翻译环境 我们先来看一下整个的翻译过程翻译环境大致可分为以下三个步骤 组成一个程序的每个源文件通过编译过程分别转换成目标代码object code。 每个目标文件由链接器linker捆绑在一起形成一个单一而完整的可执行程序。 链接器同时也会引入标准C函数库中任何被该程序所用到的函数而且它可以搜索程序员个人的程序库将其需要的函数也链接到程序中。 具体我们也可结合下图一起理解 我们再具体看其中的编译和执行的细节。 2、2 编译过程详解 我们先来看一段代码 sum.c
int g_val 2016;
void print(const char *str)
{printf(%s\n, str);
}test.c
#include stdio.h
int main()
{extern void print(char *str);extern int g_val;printf(%d\n, g_val);print(hello bit.\n);return 0;
} 我们可以看到上述代码中有两个源文件分别是sum.c 和 test.c。在对上述代码进行编译的时候具体又分为以下步骤 预编译预处理。主要是处理预处理指令有头文件的包含#include、定义符号的替换和删除#define、注释的删除等等。编译。把C语言代码翻译成汇编代码。其中有语法分析、词法分析、语义分析、符号汇总。汇编。把汇编代码翻译成二进制指令同时形成符号表。 链接。符号表的合并和重定位、合并段表。 在上述编译的过程中第2点的符号汇总是指讲全局变量函数名称当作符号汇总然后再汇编阶段将汇总的全局变量函数名称与其地址形成一个符号表。最后由于有多个源文件会生成多个符号表在链接阶段对符号表进行合并和重定位。链接完后会生成可执行程序。 上述代码的编译过程中的符号表生成如下图 注意多个源文件隔离编译生成各自的符号表。最后会在链接时对符号表进行汇总和重定位。 2、3 执行环境 上述讲述了编译和链接后生成可执行文件那么我们再看一下程序执行的过程 程序必须载入内存中。在有操作系统的环境中一般这个由操作系统完成。在独立的环境中程序的载入必须由手工安排也可能是通过可执行代码置入只读内存来完成。 程序的执行便开始。接着便调用main函数。 开始执行程序代码。这个时候程序将使用一个运行时堆栈stack存储函数的局部变量和返回地址。程序同时也可以使用静态static内存存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。 终止程序。正常终止main函数也有可能是意外终止。 三、预处理详解
3、1 预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C其值为1否则未定义 上述的符号均为预处理符号。在文件的预处理阶段均会被替换成相应的数据。我们结合以下代码理解。 #includestdio.h
int main()
{printf(line:%d\n, __LINE__);printf(%s\n, __DATE__);printf(%s\n, __TIME__);return 0;
} 上述代码的运行结果为如下图 3、2 #define
3、2、1 #define定义的符号 我们直接看#define的使用方法代码如下 //用法
#define name stuff//例子
#define MAX 1000
#define reg register //为 register这个关键字创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长可以分成几行写除了最后一行外每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf(file:%s\tline:%d\t \date:%s\ttime:%s\n ,\__FILE__,__LINE__ , \__DATE__,__TIME__ ) 我们对上述的例子进行一一解释。第一个就是用 MAX 代替了 1000。第二个我们在使用register 关键字时会感到很麻烦因为这个关键字太长了。于是用了 reg 代替了regisert。第三个其实是死循环。第四个效果更加明显。当我们使用switch语句时可能经常忘记break于是用CASE 代替了 breakcase。第五个就很简单直接代替了一个打印语句。 注意在define定义标识符的时候在最后不要加上 。因为define定义标识符时进行替换的加上 时可能会出现意想不到的错误。 3、2、2 #define 定义宏 #define 机制包括了一个规定允许把参数替换到文本中这种实现通常称为宏macro或定义宏define macro。 语法#define name( parament-list ) stuff 其中的 parament-list 是一个由逗号隔开的符号表它们可能出现在stuff中。 注意 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在参数列表就会被解释为stuff的一部分。 我们举一个例子代码如下 #define SQUARE( x ) x * x
int main()
{printf(%d, SQUARE(5));return 0;
} 将上面的代码进行预处理后打印的是5*5的值。我们再来看一段代码 #define SQUARE( x ) x * x
int main()
{//printf(%d, SQUARE(5));int a 5;printf(%d\n, SQUARE(a 1));return 0;
} 我们的本意是想打印出a1的平方但是结果并非如此。结果如下图: 我们来分析一下。替换文本时参数x被替换成a 1,所以这条语句实际上变成了 printf (%d\n,a 1 * a 1 )。自然而然结果就是11。 这样就比较清晰了由替换产生的表达式并没有按照预想的次序进行求值。 在宏定义上加上两个括号这个问题便轻松的解决了 #define DOUBLE( x) ( ( x ) * ( x ) ) 3、2、3 #define 替换规则 在程序中扩展#define定义符号和宏时需要涉及几个步骤 在调用宏时首先对参数进行检查看看是否包含任何由#define定义的符号。如果是它们首先被替换。 替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值所替换。 最后再次对结果文件进行扫描看看它是否包含任何由#define定义的符号。如果是就重复上述处理过程。 注意 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏不能出现递归。 当预处理器搜索#define定义的符号的时候字符串常量的内容并不被搜索。 3、3 宏和函数的对比 宏通常被应用于执行简单的运算。 比如在两个数中找出较大的值。 #define MAX(a, b) ((a)(b)?(a):(b)) 那么为什么不用函数呢其有如下两个原因 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。 所以宏比函数在程序的规模和速度方面更胜一筹。 更为重要的是函数的参数必须声明为特定的类型。 所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以 用于来比较的类型。 宏是类型无关的。 当然宏和函数对比也是有不足的有如下几点 每次使用宏的时候一份宏定义的代码将插入到程序中。除非宏比较短否则可能大幅度增加程序的长度。 宏可能会带来运算符优先级的问题导致程容易出现错。 宏是没法调试的。 宏由于类型无关也就不够严谨。 更加具体的宏和函数的对比总结如下表格 属 性 #define定义宏 函数 代 码 长 度 每次使用时宏代码都会被插入到程序中。除了非常 小的宏之外程序的长度会大幅度增长。 函数代码只出现于一个地方每 次使用这个函数时都调用那个地方的同一份代码。 执 行 速 度 更快。 存在函数的调用和返回的额外开 销所以相对慢一些。 操 作 符 优 先 级 宏参数的求值是在所有周围表达式的上下文环境里除非加上括号否则邻近操作符的优先级可能会产生不可预料的后果所以建议宏在书写的时候多些括号。 函数参数只在函数调用的时候求值一次它的结果值传递给函数。表达式的求值结果更容易预测。 带 有 副 作 用 的 参 数 参数可能被替换到宏体中的多个位置所以带有副作 用的参数求值可能会产生不可预料的结果。 函数参数只在传参的时候求值一 次结果更容易控制。 参 数 类 型 宏的参数与类型无关只要对参数的操作是合法的 它就可以使用于任何参数类型。 函数的参数是与类型有关的如 果参数的类型不同就需要不同 的函数即使他们执行的任务是相同的。 调 试 宏是不方便调试的。 函数是可以逐语句调试的。 递 归 宏是不能递归的。 函数是可以递归的。 3、4 条件编译 在编译一个程序的时候我们如果要将一条语句一组语句编译或者放弃是很方便的。因为我们有条件编译指令。 比如调试性的代码删除可惜保留又碍事所以我们可以选择性的编译。代码如下 #include stdio.h
#define __DEBUG__
int main()
{int i 0;int arr[10] {0};for(i0; i10; i){arr[i] i;#ifdef __DEBUG__printf(%d\n, arr[i]);//为了观察数组是否赋值成功。 #endif //__DEBUG__}return 0;
} 条件编译指令有很多我们来看一下常见的条件编译指令 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 OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
3、5 头文件的包含 我们已经知道 #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。 这种替换的方式很简单 预处理器先删除这条指令并用包含文件的内容替换。 这样一个源文件被包含10次那就实际被编译10次。 3、5、1 头文件被包含的方式 头文件的包含方式有两种 本地文件包含。如#include filename 。 查找策略先在源文件所在目录下查找如果该头文件未找到编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。 库文件包含。如#include filename.h 。 查找头文件直接去标准路径下去查找如果找不到就提示编译错误。这样是不是可以说对于库文件也可以使用 “” 的形式包含答案是肯定的可以。但是这样做查找的效率就低些当然这样也不容易区分是库文件还是本地文件了。 3、5、2 嵌套文件包 如果出现如下场景 comm.h和comm.c是公共模块。 test1.h和test1.c使用了公共模块。 test2.h和test2.c使用了公共模块。 test.h和test.c使用了test1模块和test2模块。 这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。 如何解决这个问题 答案条件编译。 每个头文件的开头写如下代码即可 #ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif //__TEST_H__或者
#pragma once 预处理指令的内容就讲解到这里希望以上内容对你有所帮助ovo~