网站建设未来发展的趋势,asp网站开发软件,百度灰色词优化排名,wordpress 段子模板文章目录前言程序环境翻译环境翻译环境分类编译预处理预处理符号预定义符号#define#undef命令行定义条件编译文件包含头文件包含查找规则嵌套文件包含其他预处理指令编译阶段汇编链接#x1f389;welcome#x1f389; ✒️博主介绍#xff1a;博主大一智能制造在读#xff…
文章目录前言程序环境翻译环境翻译环境分类编译预处理预处理符号预定义符号#define#undef命令行定义条件编译文件包含头文件包含查找规则嵌套文件包含其他预处理指令编译阶段汇编链接welcome ✒️博主介绍博主大一智能制造在读热爱C/C会不定期更新系统、语法、算法、硬件的相关博客浅浅期待下一次更新吧 博客制作不易点赞⭐收藏➕关注 前言 在我之前的一篇文章中写到了目前主流语言的优缺点那其实对于语言来说剖析到最底层都是二进制只是语法不同那计算机是怎么区分语言在程序写好到结束中间发生了哪些事情本篇文章从C语言角度出发剖析一下从写好程序到运行发生了哪些事情。 程序环境 在ANSI C下的任何程序当中都有两种不同的环境 翻译环境这个环境当中可以将程序的源代码转换成可执行的机器指令。执行环境它用于实际执行代码。 翻译环境 翻译环境可以将程序翻译成可执行的机器指令对于其他语言也是这样只有翻译成可执行的机器指令计算机才可以识别那C语言的翻译阶段是这样的 在C语言中翻译的大致过程就如上图所示在程序写好进行编译的时候会编译器集成开发环境会对每个程序文件.c单独进行翻译翻译成一个目标文件在windows环境下面后缀是.obj每个目标文件都进过链接器链接器外部接链接库通过链接库和链接器生成一个可执行程序.exe。 翻译环境分类 翻译环境大致分为编译和链接两个阶段 编译 编译时翻译环境最开始的阶段他分成三个步骤 预编译预处理编译处理汇编 预处理 预处理阶段会进行对头文件的包含对于用#define定义的符号进行替换和删除还有注释的删除和文本操作其中#define定义和头文件的包含都用到了预处理符号。 预处理符号
预定义符号 预定义符号是语言内置的符号有以下几种 __FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C其值为1否则未定义#define #define在前面学习常量的时候是有进行简略的介绍的用#define定义的标识符常量但是#define是不仅仅可以定义标识符常量的还可以定义一些宏那这些宏具体可以干什么呢可以把他理解为另类的函数宏的定义方法如下 #define name(parament-list) stuff;其中name如何函数命一样parament-list是一个符号表可以理解为函数的参数stuff可以理解为要实际做的事情符号表内的符号会出现在stuff里面对于宏来说他实际是把name(parament-list)进行替换替换成后面的是stuff可以用代码进行验证一下 #includestdio.h#define ADD(x,y) xyint main()
{printf(%d, 3 * ADD(3, 4));return 0;
}表达式的结果式13但是按照猜想得到的结果应该是21347在和3相乘那13怎么得到的上面说到宏是进行的替换将后面的34替换下来那这个表达式实际上是3*34就是94那就是13那可不可以让34先算在乘3呢只需要加括号对于宏来说不要吝啬括号那对上面代码进行修改 #includestdio.h#define ADD(x,y) (xy)int main()
{printf(%d, 3 * ADD(3, 4));return 0;
}那如果现在是一个乘法的宏呢 #includestdio.h#define MUL(x,y) (x*y)int main()
{printf(%d, 3 * MUL(33, 44));return 0;
}和我们要得到的结果是不一样的我们想要得到的是144但是得到的是57那我将内容替换到程序当中3344也就是3124 得出19193得到57但是我们想要的是先相加在相乘的那就还需要加括号如下所示 #includestdio.h#define MUL(x,y) ((x)*(y))int main()
{printf(%d, 3 * MUL(33, 44));return 0;
}这样替换下来的就是33*44结果是48和我们想要得到的结果是一样 所以对于宏而言不需要吝啬括号。 #define定义宏的替换步骤 在程序中扩展#define定义符号和宏时需要涉及几个步骤。 在调用宏时首先对参数进行检查看看是否包含任何由#define定义的符号。如果是它们首先 被替换。替换文本随后被插入到程序中原来文本的位置。对于宏参数名被他们的值所替换。最后再次对结果文件进行扫描看看它是否包含任何由#define定义的符号。如果是就重复上 述处理过程。 #和##的作用 u在正常使用printf打印的时候可以将不用%s打印两个字符串吗是可以的 #includestdio.h
int main()
{printf(这个数字是%d, 10);return 0;
}那在宏中可以吗也是可以的 #includestdio.h
#define PRINT(A ,B) printf(数字是A\n,B);
int main()
{PRINT(%d,10);return 0;
}但是这样只是对于字符串是参数的时候才能将字符串放进去还有一种方法就是利用#它的作用是将一个宏参数变成字符串如果现在要计算一个加法表达式的结果就可以用这个来更直观的表达 #includestdio.h
#define PRINT(A ,B) printf(#B的结果是A\n,B);int main()
{PRINT(%d,12);return 0;
}##的作用 ##可以把位于它两边的符号合成一个符号。 它允许宏定义从分离的文本片段创建标识符。 #includestdio.h
#define _ADD(A ,B) num##AB;int main()
{int num5 5;_ADD(5, 10);printf(%d, num5);return 0;
}宏的副作用 对于宏来说有些是有副作用的比如操作符看下面的代码 #includestdio.h#define MAX(A,B) ((A)(B)?(A):(B))int main()
{int a 1;int b 2;int c MAX(a, b);printf(a %d b %d c%d, a, b, c);return 0;
}结果是什么按照猜想的结果a是2b是3然后比大小赋给c3比2大所以b在执行一次是4那现在a是2b是4c也是4结果是正确的吗运行起来看看 c是3这就是因为宏本质上还是替换赋值给c的是((a)(b)?(a):(b))这个表达式的结果是b而b是先进行了一次得到3表达式结果还是b但是是后置那就是先使用在那就是先赋值3在进行。 宏对比函数的优缺点 那宏和函数都可以实现某种功能那他们有什么区别吗就是单纯的书写格式不一样吗不仅仅是这样宏的优点在于宏的速度是优于函数的并且对于宏是不需要去定义类型的那宏就没有缺点吗有当我们使用宏的时候一份宏定义的代码将插入到程序中除非宏比较短否则可能大幅度增加程序的长度而且宏是没法调试的我们是不能直接进入宏调试的因为宏是替换到程序当中编译器是认得但是我们是不知道内部有无问题的而且上面说到宏没有定义类型也就不够严谨并且宏可能会带来运算符优先级的问题导致我们想的和实际跑出来的内容不一样。 宏的命名 对于宏的命名而言通常是全部大写的这也是一个约定俗成的东西而函数的命名就通常不是大写的这也可以让其他程序员在看程序的时候一眼看出来哪个是宏哪个是函数。 #undef 如果在写程序的时候宏目前的功能不满足当前的程序或者不满足当前我们想要得到的效果但是我们知道直接修改宏内代码是个不太好的习惯那有没有办法可以不动我们程序内本身就有的东西然后修改掉宏实现的内容呢这里就有一个新的操作符——#undef它的作用并不是修改一个宏而是移除那怎么去使用的它的语法格式是这样的#undef NAME那现在可以使用一下看看#undef的功能是不是和我说的是一样的 #includestdio.h#define MAX(A,B) ((A)(B)?(A):(B))
#undef MAXint main()
{int a 1;int b 2;int c MAX(a, b);printf(a %d b %d c%d, a, b, c);return 0;
}可以看到是有报错的那现在在编译器眼中就没有了MAX这个宏这个时候就可以在写一个MAX的宏来实现我们现在想要实现的内容了。 #includestdio.h#define MAX(A,B) ((A)(B)?(A):(B))
#undef MAX
#define MAX(A,B) ((A)(B)?(B):(A))int main()
{int a 1;int b 2;int c MAX(a, b);printf(a %d b %d c%d, a, b, c);return 0;
}命令行定义 C语言的大部分编译器都提供了一种功能允许在命令行当中定义符号用于编译过程的程序通常是在Linux环境下使用在windows环境下一般是不可以的那这个是什么意思呢举个例子 #includestdio.h
int main()
{int arr[SIZE];int i0;for(i0;iSIZE;i){arr[i]i;}for(i0;iSIZE;i){printf(%d ,arr[i]);}printf(\n);return 0;
}可以看到上面的程序当中有一个没有初始化和定义的变量SIZE那程序正常情况下是跑不起来的是会报错的那应该怎么在没有修改程序的情况下让它跑起来在Linux环境下面可以在命令行规定规定格式是这样的 gcc -D SIZE10 test.c这样编译器就会知道SIZE是10程序就可以正常运行起来。 条件编译 在编译一个程序的时候当我们有部分语句不需要的时候可以进行注释那有没有其他方法呢有可以用条件编译指令那条件编译指令具体怎么使用往下看在我们的程序当中会有一些调试性的代码这个时候这些代码就和鸡翅一样食之无味弃之可惜那现在就可以使用选择性的编译 #includestdio.h#define _ DEBUG _int main()
{int i 0;int arr[10] { 0 };for(i 0; i10;i){arr[i] i;printf(%d, arr[i]);//为了知道上面的数是否放到数组里面}return 0;
}可以看到上面代码中for循环内的printf是用于验证我们的数字成功放到了数组当中没有那现在我们知道放成功了不需要它了但是又不想删除和注释这个时候使用条件编译指令当中的一个 #includestdio.h#define _ DEBUG _int main()
{int i 0;int arr[10] { 0 };for(i 0; i10;i){arr[i] i;#ifdef _ DEBUF _//如果_ DEBUF _为真进入到里面执行语句printf(%d, arr[i]);//为了知道上面的数是否放到数组里面#endif//结束标志}return 0;
}那这样写对于_ DEBUF _它的值是为假的那就不会走ifdef那就不会运行printf这样就实现了我们不想删除也不想注释但是让程序不跑某些代码的功能了常见条件编译指令还有很有 //1
#if //常量表达式
//...
#endif
//这里的常量表达式由预处理求值
//比如上面程序当中的_ DEBUG _编译器会知道是什么但是必须要提前声明
//2.多个分支的条件编译
#if //常量表达式
//...
#elif //常量表达式
//...
#else
//...
#endif
//3.判断是否被定义
#if defuned(symbol)
#ifdef symbol#if !defined(symbol)
#ifndef symbol
//4.嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPION2msdos_version_option2();#ednif
#endif文件包含 我们知道#include是用于包含头文件的在进入这个头文件的时候就对头文件指向的文件进行了编译这个时候在编译器眼里#include的地方就是包含的头文件的内容所以我们包含了多少次头文件就会替换多少次所以我们包含了多少次头文件就编译了多少次头文件所以最好不要重复包含同一个头文件很多次 头文件包含查找规则 在包含头文件的时候我们发现有两种包含方式一种是另一种是那它们两个包含方式有什么区别呢对于而言它会先在源文件所在目录下查找如果没有找到这个头文件编译器就像查找库函数头文件一样在标准位置查找头文件如果没有在标准位置查找到就提示编译错误而标准位置就在编译器的下载目录当中比如我用的vs2020那标准位置就在我的vs2020的默认下载路径。而会直接在标准位置查找找不到就报错对于库函数的头文件也可以用来引但是就会降低效率因为本来就存放在标准位置直接就可以查但是用“”先查本地相比较就会慢一点而且也不容易区分本地的头文件和库函数的头文件所以对于头文件用什么引要根据情况而定。 嵌套文件包含 如上图所示comn.h和comn.c是公共模板test1.h和test1.c使用了公共模块test2.h和test2.c也使用了公共模块test.h和test.c使用了test1模块和test2的模块。那这样test是不是同时包含了两份comn.h的内容那就重复包含了那有没有什么方法解决这个问题呢使用条件编译在每个头文件最前面加上条件编译指令如下所示 #ifdef _TEST_H_
#define
//头文件的包含
#endif或者 #pragma once这样可以避免掉我们的头文件重复包含 其他预处理指令 还有一些预处理指令 #error
#pragma
#line
...编译阶段 预处理结束后就要开始我们的编译阶段编译阶段会生成一个test.s的文件这个文件里面会放我们程序转成的汇编代码编译器通常会在这个阶段进行语法分析、词法分析、语义分析符号汇总那前面三个分析的作用可以猜到是将我们的C语言程序进行分析然后转换成汇编代码那符号汇总是什么它会干些什么事符号汇总其实就是将我们的全局变量和全局函数进行汇总然后对其进行汇编然后就会到汇编这一步。 汇编 汇编阶段会在编译阶段之后进行它会生成一个.obj.obj是在windows环境下生成的在Linux环境下生成.o文件那这些文件内容是什么里面全是二进制指令还会生成符号表符号表就和编译阶段的符号汇总有关那我们的.obj或者.o文件是二进制指令能不能打开看看呢我们是直接打不开的可以使用一个工具readelf可以打开文件看到这些二进制指令那符号表是什么呢符号表就是用于判断在编译过程中是否有使用符号。 链接 链接阶段是链接库和我们的生成的.obj或者.o文件生成一个.exe文件这个文件可以执行能看到我们程序实际跑出的结果是什么。