C语言_程序环境和预处理

目录

1. 程序的翻译环境

2. 程序的执行环境

3. C语言程序的翻译+链接

4. 预编译过程详解

4.1 预定义符号介绍

4.1.1 __FILE__ //进行编译的源文件

4.1.2 __LINE__//文件当前的行号

4.1.3 __DATE__//文件被编译的日期

4.1.4 __TIME__//文件被编译的时间

4.1.5 __STDC__//如果文件遵循ANSIC,其值就是1,否则未定义

4.2 预处理指令 #define

4.2.1 #define 定义标识符   

4.2.2 #define 定义宏

4.2.3 #define 替换规则

4.2.4 带有副作用的宏参数

4.3 宏和函数对比

4.4 预处理操作符#和##的介绍

4.4.1 # 的作用:在字符串中访问宏

4.4.2 ## 的作用:把位于它两边的符号合并成一个

4.5 命令行定义

4.6 预处理指令#include

4.7 预处理指令#undef

4.8 条件编译

4.8.1 常见的条件编译指令:


1. 程序的翻译环境

翻译环境是指把通常我们写的test.c源文件变成可编程程序test.exe的过程,我们称作翻译环境。

在翻译环境下源代码被转换为可执行的机器指令;也可以说是把C代码(我们写的代码)转换为二进制代码的过程。因为机器能读懂二进制代码,只能处理二进制下的代码,所以想要我们写的C程序执行,必须把C程序转换为机器可以读懂的语言,也就是二进制语言。

执行环境是指把可编程程序test.exe运行的过程,我们称作执行环境。

通常我们在工程中会写不知一个 .c 源文件,比如:main.c、contact.c、gpio.c等等,C语言的环境会首先把我们写的源文件通过编译器变成目标文件object,简称 .obj 目标文件,所有的目标文件+链接库通过链接器变成可执行的文件;

这也就是我们写下的C代码的运行过程。依托链接器和编译器的共同作用完成可执行文件的生成。

1. 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object)。

2. 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。

3. 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且他可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。

将test.c编译成可执行文件test.exe文件的过程所经历的环境称作翻译环境;翻译环境又可以细分为编译+链接,而编译的过程依托于编译器,链接的过程依托于链接器;如果细分,编译的过程又可以分为预编译+编译+汇编

预编译也称为预处理阶段:在Linux系统下,gcc编译器执行预处理的过程为 gcc-E test.c,预处理的过程包括#include 头文件的包含、注释删除( // 后面的注释删除,gcc编译器下注释是通过空格来代替的)、#define。总的来说预编译过程就是执行文本操作的过程

编译过程:把预处理过程产生的test.i 文件通过Linux系统下的 gcc-S test.i 操作进行编译;生成 test.s 文件。test.s 文件就是把 test.i 文件通过编译生成的。总的来说就是把C语言代码编译成汇编代码,以便于汇编过程的使用。编译过程包括语法分析、词法分析、语义分析、符号汇总。

Linux环境下生成的 test.o 文件就是window环境下的 .obj(object目标文件),test.o 是Linux环境下 gcc-S test.s生成的;

汇编过程:就是把Linux环境的 test.s 文件转换成 test.o 文件,也就是目标文件;最终目标文件+链接库在链接器的作用下生成可执行的程序;把汇编指令转换成二进制指令。汇编过程包括形成符号表

符号表就是在test.c 文件到目标文件test.o 的过程中,会调用不同的函数,至少有main函数,当然也可能会有add.c、gpio.c、led.c等等,每个函数在该过程中都会最终形成一个表格,该表包含所有函数以及每个函数所对应的地址,最终整理形成符号表。

整个的过程可以说是:Linux环境下--->test.c--->test.i--->test.s--->test.o   ;    分别通过Linux环境下的gcc-E test.c--->gcc-S test.i--->gcc-S test.s过程所实现。

通过编译过程生成的Linux环境下的test.o文件(window环境下test.obj目标文件)会通过链接过程将目标程序转变为可执行程序 test.exe 文件。

链接过程:链接过程主要完成合并段表以及符号表的合并和符号表的重定位。

合并段表:合并段表的过程如下图,链接过程主要是吧test.o目标文件转换为test.exe可执行文件,其中涉及到通过链接器将不同的单一的文件捆绑在一起,形成可执行文件test.exe的过程;首先,我们需要知道,test.o 目标文件是有特定的文件格式的,不同的目标文件中对应的段如表简要表示;合并段表就是把不同的目标文件中相同的段表进行合并,最终,通过链接器形成可执行文件test.exe;因为最终只能生成一个可执行程序;

符号表的合并和符号表的重定位:在汇编过程中,我们已经形成了符号表;但是不同的目标文件是有不同的符号表的(函数可能相同,但函数对应的地址是不同的),将不同目标函数的符号表进行合并和重定位的过程我们称作符号表的合并和符号表的重定位;

具体的合并过程是将所有的函数罗列到一个表中,对应的函数地址不同时,以有效的地址作为函数的地址;有效的地址是说有些函数的地址只是起声明作用,这样的函数地址是无效的,真正有效的地址是函数执行相应功能时的地址;简单来说就是:头文字中的函数只是声明作用,其地址就无效,真是的地址在 .c文件中。

符号表的意义:当存在外部声明的函数时,主函数调用外部声明的函数是通过符号表调用的,更精确的来说是在符号表中查找声明函数的地址,如果声明函数的地址无效,程序就会报错,找不到外部声明函数;所以符号表对于放大的程序进行模块化、外部声明具有显著的意义;链接过程中程序的错误我们称作链接错误;

2. 程序的执行环境

执行环境是指把可编程程序test.exe运行的过程,我们称作执行环境。

执行环境用于实际的执行指令。执行环境就是在test.exe执行文件已经产生的前提下,程序是如何运行起来的过程。

1. 程序必须载入内存中。在有操作系统的环境下;一般这个过程由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存中完成的。简单的理解就是:比如说我们拿到一个开发板,老师让我们把程序弄到板子上,需要把我们写的程序烧录到开发板上,真正意义上烧录的过程就是程序写进内存的过程。

2. 程序执行由此开始;接着便调用main函数;写到内存上以后,程序就可以开始运行了。

3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值;意思就是说:当程序运行到main函数中以后,就会为我们main函数中的变量、静态变量、返回值开辟空间。

4. 终止程序。正常终止main函数;也可能是意外终止;

3. C语言程序的翻译+链接

将test.c编译成可执行文件test.exe文件的过程所经历的环境称作翻译环境;翻译环境又可以细分为编译+链接,而编译的过程依托于编译器,链接的过程依托于链接器;如果细分,编译的过程又可以分为预编译+编译+汇编

预编译也称为预处理阶段:在Linux系统下,gcc编译器执行预处理的过程为 gcc-E test.c,预处理的过程包括#include 头文件的包含、注释删除( // 后面的注释删除,gcc编译器下注释是通过空格来代替的)、#define。总的来说预编译过程就是执行文本操作的过程

编译过程:把预处理过程产生的test.i 文件通过Linux系统下的 gcc-S test.i 操作进行编译;生成 test.s 文件。test.s 文件就是把 test.i 文件通过编译生成的。总的来说就是把C语言代码编译成汇编代码,以便于汇编过程的使用。编译过程包括语法分析、词法分析、语义分析、符号汇总。

Linux环境下生成的 test.o 文件就是window环境下的 .obj(object目标文件),test.o 是Linux环境下 gcc-S test.s生成的;

汇编过程:就是把Linux环境的 test.s 文件转换成 test.o 文件,也就是目标文件;最终目标文件+链接库在链接器的作用下生成可执行的程序;把汇编指令转换成二进制指令。汇编过程包括形成符号表

符号表就是在test.c 文件到目标文件test.o 的过程中,会调用不同的函数,至少有main函数,当然也可能会有add.c、gpio.c、led.c等等,每个函数在该过程中都会最终形成一个表格,该表包含所有函数以及每个函数所对应的地址,最终整理形成符号表。

整个的过程可以说是:Linux环境下--->test.c--->test.i--->test.s--->test.o   ;    分别通过Linux环境下的gcc-E test.c--->gcc-S test.i--->gcc-S test.s过程所实现。

通过编译过程生成的Linux环境下的test.o文件(window环境下test.obj目标文件)会通过链接过程将目标程序转变为可执行程序 test.exe 文件。

链接过程:链接过程主要完成合并段表以及符号表的合并和符号表的重定位。

合并段表:合并段表的过程如下图,链接过程主要是吧test.o目标文件转换为test.exe可执行文件,其中涉及到通过链接器将不同的单一的文件捆绑在一起,形成可执行文件test.exe的过程;首先,我们需要知道,test.o 目标文件是有特定的文件格式的,不同的目标文件中对应的段如表简要表示;合并段表就是把不同的目标文件中相同的段表进行合并,最终,通过链接器形成可执行文件test.exe;因为最终只能生成一个可执行程序;

符号表的合并和符号表的重定位:在汇编过程中,我们已经形成了符号表;但是不同的目标文件是有不同的符号表的(函数可能相同,但函数对应的地址是不同的),将不同目标函数的符号表进行合并和重定位的过程我们称作符号表的合并和符号表的重定位;

具体的合并过程是将所有的函数罗列到一个表中,对应的函数地址不同时,以有效的地址作为函数的地址;有效的地址是说有些函数的地址只是起声明作用,这样的函数地址是无效的,真正有效的地址是函数执行相应功能时的地址;简单来说就是:头文字中的函数只是声明作用,其地址就无效,真是的地址在 .c文件中。

符号表的意义:当存在外部声明的函数时,主函数调用外部声明的函数是通过符号表调用的,更精确的来说是在符号表中查找声明函数的地址,如果声明函数的地址无效,程序就会报错,找不到外部声明函数;所以符号表对于放大的程序进行模块化、外部声明具有显著的意义;链接过程中程序的错误我们称作链接错误;

4. 预编译过程详解

我们已经学习了:test.c--->test.exe(翻译环境)包括翻译和链接;翻译又包括预编译、编译、汇编;这一节我们重点解析预编译过程

4.1 预定义符号介绍

预定义符号是本身就有的符号,通过 #define MAX 100 定义的符号是自己定义的,本身MAX是没有的,两者是不一样的。

__FILE__ //进行编译的源文件

__LINE__//文件当前的行号

__DATE__//文件被编译的日期

__TIME__//文件被编译的时间

__STDC__//如果文件遵循ANSIC,其值就是1,否则未定义

4.1.1 __FILE__ //进行编译的源文件

 __FILE__ :打印出的是main.c文件存在的绝对路径;

4.1.2 __LINE__//文件当前的行号

 __LINE__:打印出的是__LINE__这一行代码在编译器中对应的行数。

如果默认VS编译器左侧没有显示行数,可以通过以下流程设置行数;

vs2013(编译器)-->工具-->选项-->找到对应的编译环境(C/C++/C#)-->在常规一栏中勾选行数即可;具体流程因编译器而定。

4.1.3 __DATE__//文件被编译的日期

__DATE__:获得编译代码的日期

4.1.4 __TIME__//文件被编译的时间

__TIME__:获得编译代码的时间

4.1.5 __STDC__//如果文件遵循ANSIC,其值就是1,否则未定义

__STDC__:文件若遵循ANSIC,则返回1;

4.2 预处理指令 #define

#开头的指令都称为预处理指令;

4.2.1 #define 定义标识符   

语法:#define name stuff

 不止如此:

#define ret register
//#define ret "hehe"
int main()
{
    ret int a;//两个是等价的ret可以代替register
    register int a;
}

#define 定义的标识符最后一般不要加分号;加分号可能会出现语法错误;

4.2.2 #define 定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。-----参数替换到文本中的意思是:宏定义中包含参数

宏的申明方式:#define name(parament-list)stuff;其中parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中;注意:参数列表的左括号必须与name紧邻。如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

 巨坑:注意:宏是替换的,而不是直接传参的,这跟数学上是有一定的差异的;

#define SQUART(x) x*x
int main()
{
	int ret = SQUART(5+1);
	printf("%d\n", ret);
	return 0;
}//11
//这个时候可能郁闷,为什么不是6*6=36;而是11
//注意,x是一个整体,整体代换以后会是5+1*5+1=11

//#define SQUART(x) (x)*(x)计算的是36

#define SQUART(x)  (x)*(x) 

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

4.2.3 #define 替换规则

1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果有的话,它们是首先被替换的;

2. 替换文本随后被插入到程序中原本文本的位置。对于宏,参数名被它们的值替换。

3. 最后,再次对结果文件进行扫描,看看他是否包含任何由#define定义的符号。如果是,就重复上述的检查;

注意:

1. 宏参数和#define定义中可以出现其他#define定义的常量。但是对于宏,不能出现递归。

int ret = SQUART(MAX+MAX);
//#define定义的宏SQUART中出现#define定义的另一个宏MAX;

 2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

printf("MAX=%d\n", ret);//字符串里的MAX不被替换的意思是指“MAX"字符串里的内容不被搜索

4.2.4 带有副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副作用就是表达式求值的时候出现永久性效果。

a++是带有副作用的;牢牢铭记:宏的参数是替换进去的,而不是算好之后带进去的;(提前将整个式子带进去)

 为了彻底的避免宏中参数带来的副作用:尽量少的在宏中使用带有副作用的参数。

4.3 宏和函数对比

求a和b的最大值?比较宏和函数实现哪个更好?

在真实的程序运行过程中,调用函数不仅仅是一步就可以完成的;每当调用一个函数的同时,需要多步的准备工作来调用该函数。所以运行一个程序时,调用函数是比较费时的;而宏定义没有繁琐的步骤,相比而言是比较是比较省时的

不用函数的原因有二:

1. 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹

2. 更重要的一点是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之宏是与类型无关的

宏相比于函数的劣势有四:

1. 每当使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。

2. 宏没有办法进行调试。

3. 宏由于和类型无关。所以使用较多的宏也会使得程序的严谨性变低。

4. 宏可能会带来运算符优先级的问题,导致程序很容易出现错误。

宏有时候可以做函数永远做不到的事情:

1. 宏的参数可以出现类型,但是函数做不到。简单来说宏定义的参数可以单纯是函数的类型,但是使用外部函数时,参数绝不可以仅仅设置成一个函数类型;

#define SIZEOF(type)  sizeof(type)
int main()
{
	int ret = SIZEOF(int);
	//int ret=sizeof(int);
	printf("%d\n", ret);//printf("%d\n",sizeof(int));
	return 0;
}//4

4.4 预处理操作符#和##的介绍

首先介绍一个C语言中字符串的天然连接现象:纵使我把一个完整的字符串分开为多个字符串,C语言中依然很可以完整的打印出来

4.4.1 # 的作用:在字符串中访问宏

4.2.3 #define 替换规则中:注意2说:当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

通过 # 是可以访问字符串中的宏的;具体如下:

4.4.2 ## 的作用:把位于它两边的符号合并成一个

##  可以把位于它两边的符号合成一个符号。他允许宏定义从分离的文本片段创建标识符。

4.5 命令行定义

命令行定义是C编程器提供的一种能力,允许在命令行中定义符号。用于启动编程过程。在执行命令的过程中给它加上一个参数进行设置下面我们通过一个例子来具体的讲解?

4.6 预处理指令#include

#include指令可以使另一个文件被编译。

不只是在C语言的学习中,在单片机的学习过程中也是如此,#include有时候使用  <   >  引用的,但有时候是用双引号  "   "  引用的;这是如何区分的呢?

实际上,当我们引用文件时,有时候引用的是库文件,ag. stdio.h;有时候是本地文件,也就是我们自己写的文件,ag. LED.h、GPIO.h等

当我们引用的是库文件,此时就是#include <stdio.h>;如果是本地文件,就是#include "LED.h"

#include "LED.h"文件(双引号文件)在引用头文件时,程序首先访问的是工程所在的目录,如果在工程所在的目录下没有找到头文件,程序会紧接着访问编译器(VS)的安装路径,如果在安装路径下还是没有找到被引用的头文件,程序就会报错;Linux环境下标准头文件的路径:/ usr / include ;

#include <stdio.h>文件(尖括号)只会在标准路径下(安装路径)查找,如果找不到被引用的头文件,就会报错。因为双引号的查找范围比较大,所以相应的查找效率也会比较低,对于那些明知道在安装路径下的文件,可以直接用#include < >,加快程序的运行效率;

嵌套结构:

所谓嵌套是指,当程序工程非常庞大时,main函数中可能会引用多个头文件,而每个头文件之间又相互引用;当执行主程序时,某个头文件被多次引用,这就是嵌套。

解决这一问题的方法是:在头文件中进行定义。

#ifndef __LED_H__
#define __LED_H__

void contract(void);

#endif

这么定义的意思是#ifndef  如果 LED.h 没有被定义,则执行下述程序;第一次调用该头文件,显然LED.h没有被定义,则执行下述程序;当第二次调用该头文件时,#ifndef,LED.h 已经被定义了,则下述程序不会被执行,#endif 条件编译结束。这样头文件被重复多次包含的问题就被解决了。

还有一种解决头文件被重复多次包含在头文件中声明函数上方写这样一个程序:

#pragma once

4.7 预处理指令#undef

宏的命名约定:一般来讲函数的宏的使用语法很相似。  把宏名全部大写  函数名不要全部大写

#undef:指令用于移除一个宏定义;

通常我们使用#define来定义一个宏定义,当定义完一个宏定义以后,可以用#undef来移除宏定义。

4.8 条件编译

条件编译:在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们可以借助条件编译指令

有些调试的代码,我们删掉可惜,保留下来又比较碍事,因此我们通常会借助条件编译指令来解决这一问题。

4.8.1 常见的条件编译指令:

#if  常量表达式

#endif

int main()
{
	int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = 0;
		//#if条件编译指令,后面的常量表达式如果为真,则执行
		//如果为假,则不执行
#if 1//0
		printf("%d ", arr[i]);
#endif
	}
	return 0;
}

#if   //如果条件语句为真,则执行该语句

#elif   // 如果该条件语句为真,则执行该语句

#else  //如果前两个条件语句都不为真,则执行该语句

#endif  //结束条件编译语句

 ③ 判断是否被定义

#if defined(symbol)

#ifdef symbol  //和上一条的语句是同一个意思,如果symbol被定义,则执行该语句对应的程序

#if !defined(symbol)

#ifndef symbol //和上一条语句是同一个意思;和最上边的两个的意思恰恰相反;如果symbol没有被定义,则执行该语句对应的程序

 ④ 嵌套指令

#if defined(OS_UNIX)  //如果OS_UNIX被定义,则执行下述语句段,语句段中再根据OPTION1和OPTION2的真假进行判断执行哪个语句

        #ifdef OPTION1    //如果OPTION1为真,则执行 unix_version_option1();

                unix_version_option1();

        #endif

        #ifdef OPTION2  //如果OPTION2为真,则执行 unix_version_option2();

                unix_version_option2();

        #endif

#elif defined(OS_MSDOS)  //如果OS_UNIX为假,则跳过上述语句段;如果OS_MSDOS为真,则执行下述语句段

        #ifdef OPTION2    //如果OPTION2为真,则执行msdos_version_option2();

                msdos_version_option2();

        #endif

#endif

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值