正文:
当使用 集成开发环境(IDE) 进行c语言的编程时,点击”编辑“,整个C程序从源代码便可执行可运行程序的生成过程的,在程序执行的过程中,IDE会在后台位我们执行好所有的编译过程,虽然IDE在后台执行有着许多底层的细节,要想看到他的程序过程也不是不行,接下来就带领大家深入了解的编程的整个过程。
一、程序的编译环境和执行环境
编译环境:就是在当前环境下把源代码转化可可执行的机器指令。
执行环境:代码在经过编译环境后生成二进制指令代码,由当前环境执行生成。
编译环境
注意:
- 一个C语言的项目可能有多个.c文件一起构成,生成一一对应的目标文件(obj)。
- 在windows环境下的目标后缀名是.obj,Linux 环境下目标我呢见后缀名是.o 。
- 多个目标文件和链接库一起经过一起处理生成最终的到一个可执行程序。
- 链接库就是平时用标准库呐,比如stdio.h 、string.h、stdlib.h等等或者本地库 。
翻译环境: 本意就是将C语言代码转化为二进制的指令。而它还能再从编译中细分为,预编译(也有人叫预处理)、编码、汇编 以及链接,这些都是循序渐进的一步步代入,或许有点绕口,接下会再造个知识梳理图,更深层次的吸收消化。
编译过程概述:
- 预处理(Preprocessing):首先,对源文件进行预处理。预处理器将处理源代码中的预处理指令,比如以#开头的指令,如#include、#define等,并展开宏定义。预处理后的代码会生成一个.i文件,通常是在临时目录中。
- 编译(Compiling):接下来,编译器前端会将预处理后的源代码编译成汇编代码(.s文件)。此阶段会检查语法和语义错误,并进行优化,但不会生成可执行代码。
- 汇编(Assembling):汇编器(as)将汇编代码转换成机器代码,并生成目标文件(.o文件)。
- 链接(Linking):最后,链接器(ld)将目标文件与所需的库文件链接在一起,生成最终的可执行文件。
以上便是,便是编译环境每个部分所该完成的任务。
运行环境
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码植入只读内存完成。
- 程序的执行开始,紧接着就是调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储在静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也由可能时意外终止。
预定义符号
C语言设置了一些预定义的符号,可以直接使用的,预定义符号也是在预处理期间处理的。
__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__STDC__ //如果编译器循序 ANSI C,其值为1,否则未定义(但vs 编译是不循序的哈)
int main()
{
printf("file:%s line:%d\n", __FILE__, __LINE__);
printf("date:%s\n", __DATE__);
printf("time:%s\n", __TIME__);
//printf("stdc:%s\n", __STDC__); //这个是实现的不了哈 只能在gcc下实现,返回1
return 0;
}
#define 定义常量
基本的语法:
#define name stuff // 第一个参数就是名称,而第二个参数便就是内容了
举例:
#define MAX 100
#define MIN "ABCD"
int main()
{
printf("%d\n", MAX);
printf("%s\n", MIN);
return 0;
}
还要别的很多各种举例方法,这边大家可以参考参考:
# 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__ )
在define定义标识符的时候,不要自作主张加切记!!!否则会出现不必要的错误
比如:
还需要注意一点在很多各种举例,上面的反斜杠后不能添加空格要直接回车才能触发作用。
#define 定义宏
第一种定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro),或定义宏(define macro)。
基本宏的申明方式:
#define name( paramen_list ) stuff
#define MAX(x,y) (x>y?x:y)
int main()
{
int a = 10;
int b = 20;
int c = MAX(a, b); //其实语句可以替换为 // int c = (a>b?a:b);
printf("%d\n", c);
return 0;
}
其中的 parament-list 是⼀个由
逗号隔开的符号表
,它们可能出现在stuff中。
其次:就是要注意一些有关括号的问题,接下继续带领大家来看下面的代码例子。
第二种定义宏
这是另一种宏的定义:
#define DOUBLE(x) (x) + (x)
来看下面的例子:
看上面的代码会打印什么结果呢? 我们让代码跑起来看一下吧:结果是100好像并没有问题。
但是如果我们对这个代码稍微的修改一下,结果还会是100吗?
哦吼?跑起来可以看到怎么算成19呢?接下来我们可以俩者替换一下看看,出现了啥问题?
可以看到在预处理后 a+1 直接替换过去心想是10*10,但实际因字符的优先级不对,导致成了9+1*9+1,所以结果计算为19,可想而知在定义宏的时候最好不要吝啬自己括号。
修改后就把原先的(x)*(x),添加上括号即可,这样在出现式子为参数后不会因优先级导致的错误
注意:
带有副作用的宏参数
x+1; //不带副作用x++; //带有副作用
宏和函数对比
# 和 ##
#运算符
#运算符将宏的一个的参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为 “字符串化”
#define PRINT(val,format) printf("the value of "#val" is "format"\n",val)
int main()
{
int a = 10;
PRINT(a, "%d");
int b = 20;
PRINT(b, "%d");
float f = 3.5f;
PRINT(f, "%.1f");
return 0;
}
PRINT(val,format); //当我们把这俩个参数替换到宏体内,就需要用到 #val 来进行转换位“val”。
## 运算符
##可以把位与俩边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。##也被称为记号粘合。
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
举例:写一个求2个数的较大值的时候,不同的数据类型就得写不同函数的。
可以看到每次要是因为类型变了,又要重新定义函数,不觉得很麻烦嘛?接下试试定义宏的效果怎么样?
//定义宏
#define GENERIC_MAX(type) \
type type##_max(type x,type y) \
{ \
return (x>y?x:y); \
} //最后一个就不需要反斜杆了哈
GENERIC_MAX(int)
GENERIC_MAX(float)
int main()
{
//定义宏
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%.2f\n", fm);
return 0;
}
但这个##在实际开发的过程中是用的比较少,但大家也就看看就行哈。需要尝试可以复制上去看看运行看看哈。
命名约定
一般来说怕函数的宏使用语法很相似。所以会让二者语言不好区分。平时就是可以养成一个习惯:
把宏名全写成大写
函数名不要全部写成大写,可以学骆驼命名
#undef
这条指令用就是移除一个宏定义。其实跟注释很相似的用法
#undef MAX
//当用上这条指令时,上面一条的宏定义就失效,就得再重新定义一条
条件编译
在编译⼀个程序的时候我们如果要将⼀条语句(⼀组语句)编译或者放弃是很⽅便的。因为我们有条件编译指令。⽐如说:调试性的代码,删除可惜,保留⼜碍事,所以我们可以选择性的编译 。
基本解析:
#define 定义一个预处理宏
#undef 取消宏的定义#if 编译预处理中的条件命令,相当于C语法中的if语句
#ifdef 判断某个宏是否被定义,若已定义,执行随后的语句
#ifndef 与#ifdef相反,判断某个宏是否未被定义
#elif 若#if, #ifdef, #ifndef或前面的#elif条件不满足,则执行#elif之后的语句,相当于C语法中的else-if
#else 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif #if, #ifdef, #ifndef这些条件命令的结束标志.
defined 与#if, #elif配合使用,判断某个宏是否被定义
还有许多常见的条件编译指令:
1. if 常量表达式//...# endif// 常量表达式由预处理器求值。如:# define __DEBUG__ 1# if __DEBUG__//..# endif2. 多个分⽀的条件编译# if 常量表达式//...# elif 常量表达式//...# else//...# endif3. 判断是否被定义# if defined(symbol)# ifdef symbol# if !defined(symbol)# ifndef symbol4. 嵌套指令# if defined(OS_UNIX)# ifdef OPTION1unix_version_option1();# endif# ifdef OPTION2unix_version_option2();# endif# elif defined(OS_MSDOS)# ifdef OPTION2msdos_version_option2();# endif# endif
头文件被包含的方式
本地文件包含:
我们在来看下面的例子:
#include "add.h"
#include "test.h"
#include "data.h"
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。如果找不到就提示编译错误
- 库文件包含
我们在来看下面的例子:
#include <stdio.h>
#include <string.h>
#include <stdilb.h>
查找策略:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。
嵌套文件包含
我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。如果直接这样写,test.c⽂件中将test.h包含5次,那么test.h⽂件的内容将会被拷⻉5份在test.c中。如果test.h ⽂件⽐较⼤,这样预处理后代码量会剧增。如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果真的不堪设想。 所以便能引用上:条件编译每个头⽂件的开头写:#ifdef __TEST_H__#define __TEST_H__//头文件的内容#endif // __TEST_H__或者可以用到#pragma once //也是比较常见的 一般都是IDE 编译的内部才能看到这样的用法// 头文件的内容
总结
以上就是C语言预处理的详解,从对编译环境到执行环境的了解,以及再深层次的编译环境的深度解剖,也对预编译(预处理)、编译、汇编以及链接的生成文件还要文件编译的用法呐。学到这里相必对预编译这章节有着一定的理解和收获。
每一篇都在很用新的写,如果觉得不错的话,可以用你发财的小手点点赞!!!