文章主题:程序环境和费用详解🌏专栏专栏:深入理解C语言📔 ;作者简介:更新有关深入理解C语言知识的博主一枚,记录分享自己对C语言的深入解读。😆个人主页:[₽]的个人主页< /字体>🏄🌊
程序的运行环境起相对应的环境,其中翻译环境中的编译中的修复环境又是我们了解甚少的一个环境,下面就是关于程序环境和修复环境的详细解说。😆
翻译环境:在环境中源代码被转换为执行这个机器指令。执行环境:它用于实际执行代码。
程序编译过程:
组成一个程序的每个源文件通过编译过程分别转换成目标代码(目标代码)。每个目标文件由链接器(linker
)捆绑在一起,形成一个单一而完整的执行程序。链接器同时也引入标准C函数库中的任何被该程序所用的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
示例:
sum.c
int g_val = 2016 ;void 打印(const char *str) {printf(< /span>"%s\n", str);}
test.c
#包括 int main(){extern void 打印(char *str);extern < span class="8b11-8e4d-0443-5456 token 关键字">int g_val;printf("%d\ n", g_val);< span class="2fe0-98fb-ea3e-be9c token function">print("hello bit.\n");返回 0< /span>;}
发生的编译与链接:
如何查看编译期间的每一步安装了软件包?(vs code文本编辑器)
< code>test.c
#包括 int main(){int i = 0;for(i=0; i<10; i++){printf("%d", i);} span>返回 0;}
减肥选项gcc -E test.c -o test.i
分手完成之后就停下来,重建之后产生的结果都放在test.i文件中。编译选项gcc -S test.c
编译完成之后就停止来,结果保存在test .s
中。编译gcc -c test.c
完成之后就停止来,结果保存在test.o
中。 blockquote>运行环境
程序执行的过程:
程序必须加载内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的加载必须由手工安排,也可能是通过执行代码置入内存来完成。程序的执行便开始。接着便调用main
函数。开始执行程序代码。这个程序代码。程序将使用一个运行时堆栈(stack
),存储函数的局部变量和返回地址。程序同时也可以使用静态(static
)内存,存储于静态内存中的变量在程序的整个执行过程中一直保留它们的值。终止程序。正常终止main
函数;也有可能是意外终止。
初步详细解
预定义符号
__FILE__ //进行编译的源文件__LINE__ //文件当前的行号__DATE__ //文件被编译的日期__TIME__ //文件被编译的时间__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
< /pre>这些预定义符号都是语言内置的。
例:printf("文件:%s行:%d\n" , __FILE__, __LINE__);
define
#define 定义标识符
标识符对应的值也是文本,所以不仅仅是复制粘贴后会被解析成数值的数字文本,其余的文本也迫切,甚至与不写也可以(但不会像非预编译指令中的那样标记赋随机值,逻辑不同,对应的)也不是内存,可能其中根本上就不会存在值),因为宏定义的标识符有时作用就相当于标识符一个文本的作用或者整个文件的作用,一般会和条件编译语句中检测是否用预定义指令定义了一个标识符的结构搭配一起使用,这就是标识符标识符的作用就真的是检测其是否定义的作用了,这是什么定义了官方的标识符的程序创建当一开始就带创建和变化的预定义符号中的最后一个
__STDC__
,或者符合ANSI C标准为1。或者就为未定义的原因了,估计也是为了符合标识符这一预定义指令的风格,方便和条件编译指令通过标识符判断定义是否表示了搭配在一起的效果,该编译器内部是否用ANSI C的标准就可以直接通过条件编译的指令把这个语法:#定义 名称 内容
例:
#定义 MAX 1000#定义 reg 注册 //用于注册该关键字,创建一个简短的名称#定义 do_forever< /span> for(; ;) //用更形象的符号来替换一种实现#< /span>定义 CASE break;case //在写case语句的时候自动把break写上。//如果定义的东西过长,可以拆几行写,除了最后一行外,每行行的后面//面都加一个反斜杠(续行符)。宏和定义预多行写入时不会用到续行符。< span class="cfb4-c0f7-2fe0-98fb token 宏属性">#定义 DEBUG_PRINT printf("文件:%s\t行:%d\t \ 日期:%s\t时间:%s\n" ,\ __FILE__,__LINE__ , \ __DATE__,__TIME__ )
提问:
在
定义
定义标识符的时候,不要在最后加上;
?例:
<代码 class="51b7-d772-ecf9-8b11 prism language-c">#define MAX 1000;#定义 MAX 1000建议不要加上
;
,这样很容易导致问题,即使不产生问题也影响程序运行的效率,是不好的习惯
如:if(条件)max = MAX ;elsemax = 0;
因为在C语言中
else
前面的语句一定得是if
,所以这里重建的阶段经过文本替换以后else
前面的语句实际上是;< /code>,就会造成后面编译的错误。
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(
下面macro
)或定义宏(define macro
)。是宏的申明方式:< /p>
#定义 名称( parament< span class="51b7-d772-ecf9-8b11 token operator">-列表 ) stuff
其中的
parament-list
是一个由逗号隔开的符号表,它们可能出现在stuff
中。
注:
参数列表的左括号必须与name
紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
如:#定义 正方形( x ) x * x 这个宏接收一个参数
x< /code> 。如果在上述声明之后,你把
SQUARE( 5 );
安置在程序中,赔偿器就可以用5 * 5
这个表达式替换上面
但是,这个宏存在一个问题:
观察下面的代码段:int a = 5;printf("%d\n " ,正方形( a + 1) );
乍一看,你可能觉得可能代码将打印 36 这个值。
事实上,将会打印 11。
为什么?替换文本时,参数 x 被替换生成
a + 1
,那么这条语句实际上变成了:printf ("%d\n",a + 1 * a + 1 );
这样就比较响了,由替换产生的表达式并没有按照预想的顺序进行求值。
在宏上加上两个逗号,这个问题便定义了轻松的解决方案了:#定义 SQUARE(x) (x) * (x)
这样修复之后就产生了预期的效果:
< span class="c0f7-2fe0-98fb-ea3e token function">printf ("%d\n",(a + 1< /span>) * (a + 1) ) span>;
这里还有一个宏定义:
#定义 DOUBLE(< /span>x) (x) + (x) code>
定义了我们使用了逗号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5;printf("%d\n" ,10 * 双(a));
这将打印什么值呢?
预计,希望打印 100,但实际上打印的是 55。
之后我们发现替换:printf ("%d\n",10 * (5) + (5) span>);
乘法轰炸先于宏定义的加法,所以出现了
55
。
这个问题,的解决方法是在定义宏表达式加上一对双人就可以了。#定义 双(x) ( ( x ) span> + ( x ) )
提示:
所以用于针对数值表达式进行求值的宏定义都应该用这种方式加上逗号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可有的优先级之间的应答。# Define 的替换规则
在程序中扩展
在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。(指在程序中使用宏时参数,如果用的同样等于时的标识符时,会先把标识符中的数据替换之后再把宏中的文本替换,因为都是在重建时就处理的东西,所以做的到赶在它文本重建之前把参数中的内容赶上它替换替换的细节给替换进去了,不是替换处的变量就做不到这一点,只能在预处理它的文本替换后,才能再把变量的值放在编译之后的完整编译语言中,然后在编译过程中把语言转换成二进制语言,然后在很后面的运行过程中才能在内存操作中把变量的值引用里面)替换文本被插入到程序中原来文本的位置。对于宏,参数名被它们的值所替换。最后,再次对结果文件进行扫描,看看它是否包含任何由#define
定义符号和宏时,需要涉及几个步骤。#define code>定义的符号。如果是,就重复上述处理过程。(可能因为有些原因没有处理的
#define
定义的符号就会在第二次做完,只选两次,如果还没有判断完就不会继续判断了,一般也很少会出现这种情况。
注意:宏参数和#define
定义中可以出现其他#define
定义的符号。对于宏,不能出现电位。(即宏可以被描绘(针对于同类而言,不同类不能用雕塑这种说法),但只能用作设计师,不能像一样的宏)当雕塑器搜索#define
定义的符号的时候,字符串常量的内容并不被搜索,当然标识符不了则也一样可以判断出同性质的宏参数也不行,得用后面提到的#
的知识点来解决该问题。(可以组成不是同一层的理解文本效果,字符与字符串中与宏相同的符号为避免与宏中的标识符相冲突,并且不会被替换,而是字符(字符串)中是什么文本就依然用什么文本)#和##
前面说过与预定义相关的所有文本都不会在字符串中的内容给检测到,但可以在宏中的参数有一种方法让这种参数插入到字符串中。如何把参数插入到字符串中?
首先我们看看这样的代码:
char* p = "hello""bit\n";printf("你好""位\n"); span>printf("%s", p);
这里输出的是不是
hello bit
?
答案是确定的:是。
我们发现字符串是有自动连接的特点的。
那我们是不是可以写这样的代码?:#定义 打印(格式, VALUE)\printf("值为 "格式"\n", VALUE);。..打印("%d", 10);
这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
另外一个技巧是:
使用#
,把一个不是字符串的宏参数变成对应的字符串。
例:int i = 10; #定义 PRINT(格式, VALUE)\printf(< span class="ac8b-d90e-7467-cfb4 token string">"的值" # span>值"是"格式"\n", VALUE);。。。打印("%d", i+
3);/ /产生什么效果? 代码中的
#VALUE
会被消耗器处理为:
"VALUE "
。
最终输出的结果应该是:i+ 的值3 是 13
## 的
##
可以把位于其两侧的作用符号合成一个符号。
它允许宏定义从分离的文本中片段创建标识符。#定义 ADD_TO_SUM(num, value) \sum##num += value;...ADD_TO_SUM< span class="7467-cfb4-c0f7-2fe0 token punctuation">(5, 10);//作用是:给sum5增加10.
注:
这样的连接必须产生一个合法的标识符。否则其结果就是未定义的。< br /> 宏参数虽然也是直接文本复制粘贴,但他还是无法直接把两个宏参数组合到一起的整个字符当做一个变量名去用时,这种情况下就必须要借用## code>来帮助其实现这种效果了。
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数带副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。 副作用就是表达式求值的时候就会出现的永久性效果(主要即指参数中的一些变量的值执行一次语句后只因为标记结果发生了变化而影响后面的宏语句的情况)(宏是直接将文本复制粘贴的结果会被大量生成而不是形成函数的一个赋值形参的逻辑,所以将一次标记结果的语句带入一个因为出现多个的宏中时,参数的后果同时发生,造成危险情况发生。
例:x+1;//不产生结果x++; // 带标记的副作用
MAX宏可以证明带有副作用的参数所引起的问题。
#定义 MAX(a, b) ( (a) > (b) ?< /span> (a) : (b) )...x = 5;y = 8;z = MAX (x++, y ++);printf("x=%d y=%d z=%d\n",< /span> x, y, z);//输出的结果是什么?
所以输出的结果是:
x=6 y=10 z= 9
宏和函数对比
宏通常被评估执行简单的侵犯(书写复杂的侵犯和逻辑为防止参数的作曲错误执行会比函数书写起来复杂很多)。
比如在两个数中查找增加的一个。#define MAX(a, b) (( a)>(b)?(a ):(b))
那为什么不用函数来完成这个任务?
用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多
原因有二:
所以宏比函数在程序的规模和速度方面更胜一筹。(只是文本的复制粘贴需要繁杂的调用返回操作) 最重要的是函数的参数必须声明为特定的类型。
所以函数只能在类型合适的表达式上使用。反之这个宏则可以适用于整形、长整型、浮点型等,可以用于>来比较的类型(参数的范围和执行参数与操作的范围比函数要大倍数)。宏是类型无关的。
宏的缺点:
当然和函数相比宏也有劣势的地方:
3.增加使用宏的时候,一部分宏定义的代码将插入到程序中。除非宏比较短,否则可能增加带宽程序
的长度。
4. 宏是无法调试的(因为是在剩余阶段就执行完了,且宏中可以有多个语句,也可以一个完整的语句都没有,调试以宏显示的文本来看的话,为复杂,编译器的效果就干脆处理成是直接一步就过去了,这也是为什么一般在宏中只写简单语句的原因,复杂的宏体出错后难以通过调试出现问题的原因)。
5. 3.宏由于类型无关,尚不够严谨(一些对参数类型要求严谨的逻辑不能由宏来书写)。
6.宏可能会带来引出优先级的问题,导致程序容易出现错误(且排除这种情况时补充过量又使更复杂的结构就繁杂不清)。
宏有时可以做函数做比如:宏的参数可以出现类型(通过类型命名的文本来表示意思),但是函数做不到(函数的参数不是文本,而是量,最多也只)能通过参数的大小来大概的洞察类型的情况,或者字符串,但这些都远没有宏方便)。#定义 MALLOC(num, type)\(类型 *)malloc(num * sizeof(类型))。..//使用MALLOC span>(10, int);//类型作为参数//重建器替换之后:(int< /span> *)malloc(10 * sizeof(int));
宏和函数的一个对比
属性 #define定义宏 函数 代码长度 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会增加 函数代码只出现在一个地方;每次使用这个函数时,都调用那个地方的同一个代码 执行速度 更快 存在函数的调用和返回的额外费用,所以相对慢一些 操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加上逗号,否则相邻操作符的优先级可能会产生不可预见的后果,所以建议宏在书写的时候多一些逗号。 函数参数只在函数调用的时候求值一次(相当于将参数处的代码用一个表达式将其算出结果然后再将其传入到函数中对参数创建的形参中),其结果值传递给函数。表达式的求值结果应该更容易预测。 标记的结果参数 参数可能被替换到宏体中的多个位置,所以带标签的文本参数求值可能会产生意想不到的结果。(带标签的文本参数被替换)直接复制粘贴宏中多少次就会被执行多少次) 函数参数只在传参的时候求值一次,结果更容易控制。(有答案也只需在将值带入函数形参中被求值的那一次在整个函数运行中)造成一次交互) 参数类型 宏的参数与类型无关(因为本质上就是把该参数当成为一个文本时(参数的作用本质上就是把其无效文本的形式复制粘贴到宏体的多个位置)),只要对参数的操作是合法的,它就可以使用于任何参数类型。 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使它们执行的任务是相同的。(小则警告,大则导致与参数有关)的逻辑运算出来的结果错误) 调试 宏是不方便调试的(调试时直接)一步跳过,内部的具体逻辑是看不到的) 函函数是可以逐句语句调试的(因为是调用到那一个函数体调用了,每一步具体的)逻辑都可以直接被看到) 静脉 宏是不能静脉的(即自己洼自己,不行但宏可以自己调用标识符,此时标识符会被先执行) 函数是可以分层的(可以自己调用自己,执行多次) 命名约定
一般来说函数和宏的使用语法很相似。所以语言本身不能帮助我们区分两者。
我们平时的一个习惯是:把宏名全部大写
函数名不要全部大写(但钻孔是一些库中自带的宏基本上字母全部又都是小写的,自己写的时候稍稍注意一下要大写即可)#undef
用于移动的这条指令除一个宏定义。
#< span class="7467-cfb4-c0f7-2fe0 token指令关键字">undef NAME//如果有一个名字需要被重新定义,//那么它的旧第一个名称要被删除。//(或者后续不用该名称)此时,也可以先采用删除操作)
命令行定义
许多C的编译器提供了一种能力,允许在命令行中中定义符号。用于启动编译过程。
例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假设某个程序中声明了一个某个存储容量,如果机器内存有限,我们需要一个高性能的存储,但是另外一个机器内存很大,我们需要一个存储能够很大。(即通过在命令行中通过命令行的语法对预定义的标识符)符进行一些赋值来实现方便较早的不同版本的测试作用,在一些没有这个写功能的编译器下尝试一下然后因为标识符未定义而在后续如果推断了该标识符的话错误(在VS中会被错误的检测生成语法错误,但本质上应该是运行错误,因为你实际上没有在C语言中语法用这个标识符的话,就完全不会出现错误))#include
int main(){ int 数组 [ARRAY_SIZE];< /span> int i = 0; for(i =< /span> 0; i< ARRAY_SIZE ; i ++) { 数组[i] = i; } 用于 (i = 0; i< ARRAY_SIZE; i ++< /span>) { printf span>("%d " ,数组[i]); } printf("\n" ); 返回 0;}< /code>编译指令:
//linux环境演示(linux具有命令行预定义标识符的能力)gcc -D ARRAY_SIZE=10 programe.c< /code>
条件编译
在编译一个程序的时候我们如果执行一条语句(一组语句)编译放弃或者是很方便的。因为我们有条件编译指令
也就是说:可调试的代码,舍弃可惜,又获得干扰事,所以我们可以选择性的编译。
< code class="c0f7-2fe0-98fb-ea3e prism language-c">#包括#定义 __DEBUG__ //这里就说明了标识符预定义之后不确定其代表的常量值时 / /是符合语法规范的,既可以在条件编译时担当 //#ifdef的条件,又可以在命令行去重新赋值,因为只要不将< /span> //其直接拿来用就行int main span>() {int i = 0;int arr[10] span> = {0};用于(i=0; i<10; i++){arr[i] = i;< /span>#ifdef __DEBUG__printf("%d\n", arr[i]);//为了观察负载是否分配。成功#endif //__DEBUG__< /span>}返回 0;} 常见的条件编译指令:
<跨度类="令牌编号">1.#if< /span> 常量表达式//...#
endif //常量表达式由修复器求值。如:#define __DEBUG__ 1#if __DEBUG__//..#< /span><跨度类s="tokendirectivekeyword">endif 2.多个分支的条件编译 #if 常量表达式< span class="be9c-51b7-d772-ecf9 token comment">//...#elif 常量表达式//...#else //...#endif 3.判断是否被定义#< span class="be9c-51b7-d772-ecf9 token 指令关键字">if 已定义(symbol) //define(symbol)和!define(symbol)是C语言中属于独条件编译的#ifdef 符号//具有真假判断性的表达式,且与普通定义不同,音符紧挨其后(与关#if !定义(符号) //键字大小相同,函数和宏不同不一定紧挨其后但是一定保健,#ifndef 符号 //所有的创作指令都是清一色的可以用续行符来达到分行写的目的,4.剪影指令 //且续行符的语法规则是续行符两边的空格全都可以忽略不计)在#if中< span class="2fe0-98fb-ea3e-be9c token 指令哈希">#if 已定义< /span>(OS_UNIX)//还能够被分别简写为#ifdef和#ifndef#ifdef 选项1unix_version_option1(< /span>);#< span class="ea3e-be9c-51b7-d772 token 指令关键字">endif#ifdef 选项2unix_version_option2 ();#endif# elif 已定义(OS_MSDOS) #ifdef 选项2< /span>msdos_version_option2()< span class="cfb4-c0f7-2fe0-98fb token punctuation">;#endif #endif< /span> 文件包含
我们已经知道,
#include
指令可以使另外一个文件被编译。就像它实际出现的那样于#include
指令的地方一样。
这种替换的方式很简单:
搭建器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那么实际被编译了10次(与函数每次都调用定义处的原理不同,与预定义逻辑相同的很简单的复制粘贴逻辑,构造指令均是复制时对编辑后的C语言代码文本的文本操作,所以不是复制粘贴而是在原文本的删除操作(即条件编译实现简单编译效果的底层逻辑))。头文件被包含的方式:
本地文件包含#包括 "文件名"//同源码文件位置相同处自己写的文件就被调用本地, /因为/和写源码的地方几个< /span>
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误。
Linux环境的标准头文件的路径:/usr/include
VS环境的标准头文件的路径:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include//这是VS2013的路径默认
注意按照自己的安装路径前往,如果安装路径自然改变存储的位置和编译器查找的位置会发生变化。
库文件包含#include
查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用""
的形式包含?
答案是肯定的,可以
。
但是这样查找的效率就低一些,当然这样也能成功区分是库文件还是本地文件了。描述符文件包含
如果出现这样的场景:
comm.h和comm.c是公共模块。
test1.h和test1.c使用了公共模块。
test2.h和test2.c使用了公共模块。
test.h和test.c使用了test1模块和test2模块。
这样最终程序中就会出现两份comm.h的内容。这样就造成了文件内容的重复。
如何解决这个问题?
答案:条件编译。
每个头文件的起始写:#ifndef __TEST_H__#定义 __TEST_H__//头文件的内容#endif span> //__TEST_H__//因为如果引用了相同头文件的文件则因为复制粘贴了相同的结构,// __TEST_H__标识符就被定义了,防止在下一次引用时被重复重写了,// __TEST_H__定义一旦被引用过就会被预定义了就一定可以通过条件编译在// 剩余阶段就防止相同的文本出现在C语言的文章文件中,相同的文章文件 //更不会因此被编译了
或者:
#pragma 一次//检测在同一程序中的引用情况,让其在同一程序中只引用一次
< /pre>就可以避免头文件的重复引入。
笔试题:头文件中的 ifndef/define/endif 是用的? #include和 #include "filename.h" 有什么区别? 答:
1.ifndef属于条件编译指令,检测是标识符的未定义,如果#ifndef 后面的标识符未定义的话就编译#ifndef 基础下的 C 语言文本,反之则不编译其基础下的 C 语言文本,如果其后初始化条件编译语句就继续判断符合条件可编译的语句,如果直接是#endif就直接没有可编译的语句。define属于撤销指令中的预定义指令,用于定义标识符和宏。#endif条件属于编译指令,是单个条件编译结构结束的标志
2 . #include属于库文件包含,直接去标准路径中查找头文件,没找到则提示编译错误,#include "filename.h"属于本地文件包含,先在源文件所在的目录下(本地)查找,如果未找到则像库文件查找一样在标准位置查找头文件,没找到则提示编译错误,其中标准库中的头文件也可以被本地文件查找到,但这样效率很高,且轻松区分谁是库文件谁是本地文件了。
其他脚本
#错误#pragma #行...//不做介绍,自己去了解。#pragma 打包() span>//在结构体部分介绍过
#pragma pack()详细配方这里:深入理解C语言(3):自定义类型详解
总结
以上就是博主对程序环境和预处理的详解,😄希望对你的C语言学习有所帮助!作为刚学编程的小白,可能在设计逻辑方面有些不足,欢迎评论区指导正!看都这个看到了,点一个空间赞或者关注一下吧(当然三连也可以~),您的支持就是博主更新最大的动力!让我们一起成长,共同进步!
深入理解C语言(5):程序环境和重构详解原创由知识百科栏目发布,感谢您对的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“深入理解C语言(5):程序环境和重构详解原创”