当前位置: 首页 > article >正文

C语言-了解程序环境和预处理看这一篇(超详解)

1.程序环境

在ANSIC的任何一种实现中,都会存在两个不同的环境。第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令,第2种是执行环境,它用于实际执行代码。如下图所示:

1.1 翻译环境

翻译环境会分几个步骤:

  • 组成一个程序的每个源文件通过编译器的编译过程会分别转换成目标代码
  • 每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序
  • 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中

由上我们便可以知道翻译环境分成编译器、链接器两个部分分别完成相应的功能,那么编译器这一部分又可以分为预编译(预处理)、编译、汇编这三个部分,如下图:

预编译:预编译过程主要处理的是那些源代码文件中以"#"开始的预编译指令,如#include、#define等,具体有哪些如下:

  • 将所有的"#define"删除,并且展开所有的宏定义。
  • 处理所有条件预编译指令,比如"#if"、"#ifdef"、"#elif"、"#else"、"#endif"
  • 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这
  • 个过程是递归进行的,也就是说被包含的文件可能还包含其他文件
  • 删除所有的注释
  • 保留所有的#pragma编译器指令,因为编译器须要使用它们
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号

编译:编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析、生成相应的符号汇总及优化后生产相应的汇编代码文件

汇编:汇编就是将汇编代码转变成机器可以执行的指令,每一个汇编语句几乎都对应一条机器指令,以及生成符号表

符号汇总,符号表:

编译中的符号汇总就是会把源文件当中类似于全局变量,函数名等汇总起来,局部变量不会总因为局部变量只有在程序执行时才会定义,生命周期短。

汇编当中的符号表就是把各原文件当中所汇总的符号整合到一起,把全局变量,以及函数等它们的真正地址都整合起来,一边在链接器链接时找到它们的真正位置。具体如下所示:

链接(链接器):把多个目标文件和连接库进行链接的,主要进行合并段表和符号表的合并与重定位 。

1.2 执行环境

  • 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
  • 程序的执行便开始。接着便调用main函数。 
  • 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
  • 终止程序。正常终止main函数;也有可能是意外终止。

2.预处理

2.1 预定义符号

__FILE__   //进行编译的源文件
__LINE__   //文件当前的行号
__DATE__   //文件被编译的日期 
__TIME__   //文件被编译的时间 
__STDC__   //如果编译器遵循ANSI C,其值为1,否则未定义

所谓预定义就是在我们预处理之前就已经定义好了,可以直接使用,这些能用来干什么呢?一般可以用来我们在写代码的时候用作标记,当工程比较复杂的时候,我们可以在其中穿插这样的代码,类似于写日志,写入文件当中,以便编译时发现其中的错误。如下代码所示:

int main()
{
	FILE* pf = fopen("log.txt", "a+");
	if (pf == NULL)
	{
		perror("fopen");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		fprintf(pf, "%s %d %s %s\n ", __FILE__, __LINE__, __DATE__, __TIME__);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

 

2.2 #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__ )   
  • #define MAX 1000 这个就是我们最常用的把一个常量定义给MAX方便我们后续的修改
  • #define reg register 对于寄存器定义的变量直接写register会比较长,可以定义成这种形式方便我们定义变量
  • #define do_forever for(;;) 这是一种死循环,在代码中直接写入do_forever就会进行死循环
  • #define CASE break;case 有的编程语言switch语句中的case不需要break,但是c语言当中是要求break的,所以就有了这样的定义,在switch语句中CASE就可以不用写break,但需要注意第一个case需要小写,实际上#define就是替换。case:  CASE(break;case): 
  • 需要注意:#define后面尽量不要加入      

#define定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。            语法:#define name(parament-list) stuff 

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。需要注意:参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。看如下代码:

//#define SQUARE( x )  x * x
//#define SQUARE( x ) (x) + (x)
#define SQUARE( x ) ((x) + (x))
int main()
{
	printf("%d \n", SQUARE(5));//25
	printf("%d \n", SQUARE(2 + 3));//11实际上printf("%d \n", 2+3*2+3)
	//改正#define SQUARE( x )  (x) * (x)
	//但又会遇到新的问题如下
	printf("%d \n", 10*SQUARE(2 + 3));//想要的结果是10*(5+5)=100
	                                  //实际上10*5+5=55
	//因此#define定义宏括号要尽量加上
	return 0;
}

所以需要注意:用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。

#define替换规则

#define定义的符号和宏,替换的时候会涉及如下几个步骤:                     

  • 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先 被替换
  • 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值替换
  • 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上 述处理过程。

需要注意:

  •  宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归
  • 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

2.3 #和##

上面我们知道,字符串常量的话,#define所定义的符号它是不会替换的,那怎么样让它去替换呢?下面我们讲述比较奇妙的知识点,#和##。

#define PRINT(V, FORMAT) printf("the value of "#V" is " FORMAT"\n", V )
int main()
{
	//如果我们想要打印the value of a is 0
    //the value of b is 1,the value of c is 2
    //定义函数的话,其中的abc是不是不能以一个通用的方式去替换
	int a = 0;
	PRINT(a, "%d");
	float b = 1;
	PRINT(b, "%f");
	return 0;
}


#define VALUE(x, y) x##y
int main()
{
	int value111 = 100;
	printf("%d\n", VALUE(value, 111));//100
	return 0;
}

#的含义就是把宏定义的参数转换成所对应的字符串,之后才插入字符串之中。##就是把两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。需要注意:

  • #和##只能在宏定义下才能去使用。
  • 只有当字符串作为宏参数的时候才可以把字符串放在字符串中,如果直接在字符串中去替换,而我们的参数不是字符串,就不会替换
  • ##这样的连接必须产生一个合法的标识符。否则其结果就是未定义的

2.4 带副作用的宏参数 

带有副作用的宏参数和上述加不加括号所带来的影响是不同的。具体的就是宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么就有可能会出现危险。副作用就是表达式求值的时候出现的永久性效果。例如:

x+1//正常的加法运算没有副作用
x++//这会产生副作用,表达式使用的是x原本的值,但是执行之后x+1了不再是原来的值

产生的副作用看下例:

#define MAX(a, b)  ( (a) > (b) ? (a) : (b) )
int main()
{
	int x = 5;
	int y = 8;
	int z = MAX(x++, y++);
	printf("x=%d y=%d z=%d\n", x, y, z);//结果是不是我们想的6 9 8呢?
	//事实上结果是6 10 9 
}

事实上的结果与我们所想的大相径庭,这是为什么呢?这就是由于副作用的表达式在宏的定义中出现的不止一次,对最后的结果产生了较大的影响,最终的运算见下图:                                  MAX(x++, y++)  ( (x++) > (y++) ? (x++) : (y++) )

2.5 宏和函数的对比

一定程度下我们是不是会感觉宏和函数的功能差不多,但是呢它们还是有一定的区别,区别是:

  • 当一个运算较为简单的时候,用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  • 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。但是这个宏可以适用于整形、长整型、浮点型等可以用于>来比较的类型,如上例的ab整型我们可以传,字符型的我们也可以传。因此宏是类型无关的
  • 宏的参数可以出现类型,但是函数做不到。如下代码所示
#define MALLOC(num, type)\
    (type *)malloc(num * sizeof(type))
int main()
{
    MALLOC(10, int);//类型作为参数
    //预处理器替换之后:(int*)malloc(10 * sizeof(int));
    return 0;
}

当然,有时候优点也是缺点,是一把双刃剑和函数相比它的缺点如下:

  • 每次使用宏的时候,一份宏定义的代码都会在预处理阶段插入到程序中。运算简单时无可厚非,但当代码足够复杂时,这样的宏使用多次,我们的代码将会大幅度增加。
  • 由于它是类型无关,那也就不够严谨
  • 宏由于预处理阶段就会插入的程序当中去,因此在编译运行进行调试的时候也就看不到我们所定义的这些符号和宏,因此也就没办法进行调试。
  • 如上面加不加括号所引起的运算符优先级也是一个问题,很容易导致出错

这里有一个不成文的规定, 一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者,因此呢C语言程序员的一个习惯就是:把宏名全部大写,函数名不要全部大写。我们也尽量这样做,这样会显得高级!

2.6 #undef

这个指令就和#define正好相反,前者定义一个符号或宏,后者取消定义,如果在代码中我们想让这个符号重新被定义,就应该先把之前的移除。 

2.7 命令行定义

什么意思呢?就是说我们可以在命令行中进行定义,应用场景也比较有限,一般就是在一些内存空间比较有限,会根据不同的机器我们去定义一个不同大小的变量,gcc编译环境下就可以实现这一的一个功能。

2.8 条件编译

顾名思义就是满足条件就编译,不满足就不编译,比如一些调试性代码删除可惜,保留又碍事,所以我们可以选择性的编译。具体有以下几种条件编译指令:

//1
#if 0
	printf("张三");//真就执行,假就不执行
#endif
	return 0;
//2
#if  0
	printf("张三");//真就执行,假就不执行
#elif 1
	printf("李四");//真就执行,假就不执行,但如果第一条执行了这个即使是真也不执行
#elif 0
	printf("王二");//真就执行,假就不执行,但如果前两条执行了这个即使是真也不执行
#endif
//3
#define NAME  0
int main()
{
#ifdef NAME  // 等价于#if defined (NAME)
	printf("张三");//检测的是否被定义,即使定义的为假也执行
#endif
#ifndef NAME//==#if !defined (NAME)
	printf("张三");//检测的是否被定义不定义才执行
#endif
	return 0;
}
//4嵌套,类似于判断语句条件编译指令也是支持嵌套的

2.9 文件包含

我们在写C语言代码时常常会写一句#include<stdio.h>,什么意思呢?其实这条语句的意思就是包含头文件 stdio.h,在预处理阶段这条代码就会被替换成我们所包含文件的代码。在前面所介绍的通讯录代码中是不是还有另外一种包含头文件的方式#include"",那种方式我们用来包含我们自己定义的头文件。

所以头文件包含有两种方式:#include<>和#include""。它们的区别就是查找策略不同,如下:

  • #include<>:查找头文件直接去标准路径下去查找,如果找不到就提示编译错误
  • #include"":先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样再去标准位置查找头文件

那我们对于库函数下的头文件可不可以用#include""进行包含呢?当然也是可以的,但是它是不是会先在我们的源文件的目录下去查找,再去标准位置去查找,这样效率也就低了。因此我们要按照标准去编写我们的代码,这样我们的代码会显得高级! 

2.10 嵌套文件包含

通过对预处理的了解,你想没想到过这样的场景呢?

可能我们并没有在意这些,但当我们仔细去观察就会发现,在一些时候我们还是写出了这样的代码的, comm.h和comm.c是公共模块,add.h和add.c使用了公共模块,sub.h和sub.c使用了公共模块,test.h和test.c使用了add模块和sub模块。那我们可想而知在预处理之后,common的代码是不是就在test中出现了两次,一旦这些代码足够的长,那效率就会降下来了。怎么样预防这样问题的出现呢?有两种解决方式:

//第一种是在头文件中写入
#pragma once
//第二种
#ifndef __TEST_H__
#define __TEST_H__
//endif之前是头文件的内容
#endif   
//这三条语句的意思就是1.ifndef __TEST_H__不定义就执行
//因此#define __TEST_H__和头文件内容就会插入在程序当中
//当有第二个头文件要插入时遇到了#define __TEST_H__
//因为定义了#ifndef __TEST_H__就不会再执行

http://www.kler.cn/news/343400.html

相关文章:

  • std::future::then的概念和使用方法
  • Java SSL使用Openssl ECC加密生成证书遇到的坑
  • Python和C++及MATLAB低温磁态机器学习模型
  • 【解决办法】git clone报错unable to access ‘xxx‘: SSL certificate problem
  • 第19周JavaWeb编程实战-MyBatis实现OA系统 面试题解析
  • Go语言学习代码记录
  • C++继承深度剖析:从理论到实践的跨越
  • rzszscp端对端文件传输整理
  • 【SpringSecurity】基本流程
  • 职场上的人情世故你知多少
  • VARCHAR(50) 与 CHAR(50) 的区别
  • 活动预告|博睿数据将受邀出席GOPS全球运维大会上海站!
  • KVM虚拟化技术
  • 启动hadoop后没有 NodeManager和 ResourceManager
  • Spring Boot RESTful API开发教程
  • 滚柱导轨适配技巧与注意事项!
  • linux线程 | 线程的控制
  • 关于mac下的nvm设置淘宝镜像源
  • 文件与目录的基本操作
  • 无人机侦测:手提式无线电侦测设备技术详解