梦开始的地方——c语言预处理+编译过程(代码片段)

爱敲代码的三毛 爱敲代码的三毛     2022-12-16     783

关键词:

文章目录


C语言程序的编译(预处理)

在C语言标准规定在C的任何一种实现中,存在两个不同的环境。

第一种是翻译环境,在这个环境中被转换为可执行的机器指令,第二种是执行环境,它用于实际执行代码。

1.编译和链接

假设有一个test.c的源代码,它需要经过编译——>链接——>可执行程序

如果有多个.c的源代码文件,它们每个都会单独的进行编译再通过链接器最后变成可执行程序

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

1) 编译的几个阶段

注意:此时我的环境是Centos7.6的gcc编译器

代码:

test.c

#include <stdio.h>
#define MAX 666666
//声明外部函数
extern int add(int x, int y);
int main()

	int a = 10;
	int b = 20;
    int tmp = MAX;
	printf("%d\\n", a + b);

	return 0;

add.c

int add(int x, int y)

	return x + y;

翻译环境中的编译又可以分为3个阶段

  • 预编译
  • 编译
  • 汇编

预编译阶段

在预编译期间编译器会做那么几件事

  1. 头文件的包含
  2. 注释的删除
  3. #define定义符号的替换
  4. 预处理指令

我们在Linux上使用gcc -E test.c > test.itest.c文件进行预编译,预编译之后立马就会停下来,预编译的解结果保存到test.i文件中方便查看

我们会发现,预编译后。我们的写的注释不见了,写的头文件也不见了,多了一堆函数声明(这只是部分截图)。

我们发现几点

  1. #include <stdio.h>头文件不见了
  2. 写的注释被删除了
  3. #define定义的MAX也被替换了

我们可以验证以下头文件的包含,在我的Linux系统中的/usr/include/stdio.h保存了stdio.h文件,查看后发现里面的函数信息的确是我们上面所看到了。所以预处理接段就会把头文件中的内容包含到源文件中。

编译阶段

在编译阶段会把C代码翻译成汇编代码,做这么几件事

  1. 语法分析
  2. 词法分析
  3. 语义分析
  4. 符号汇总

语法分析简单就是检查代码是否有语法错误

词法分析:把C语言代码一个个拆分开来,建立一个语法树之类的东西

语义分析:简单来说就是把C语言的代码怎么转换成对应的汇编代码,C语言的额一个语义

通过gcc -S test.ctest.c文件进行编译,编译完后会停下来将结果保存到test.s文件中。test.s中保存的就是汇编代码。

符号汇总是编译阶段一个非常终要的过程。

符号汇总就是把文件中重要的符号给提取出来

我们简单修改一下test.c文件

#include <stdio.h>
#define MAX 666666
//声明外部函数
extern int add(int x, int y);
int count = 0;
void print()

    

int main()

	int a = 10;
	int b = 20;
        int tmp = MAX;
	printf("%d\\n", a + b);

	return 0;

在Linux文件下通过命令gcc -c test.c生成一个test.o的目标文件对于前面所讲的windows中的.obj文件

再通过readelf -s test.o命令查看里面的内容发现,只记录另外关键的一些全局的函数和变量

再来看add.c的源文件,这个文件中只有一个add函数

把它们的符号进行汇总,把主要的符号进行汇总,就会符号汇总。

汇编阶段

再Linux环境下通过命令gcc -c test.stest.s中的汇编代码转换为二进制指令,生成一个test.o的二进制文件

再汇编阶段还会形成符号表,在前面的编译阶段只是将符号进行汇总。而这里汇编阶段会会生成一个.o的文件(windows中是.obj文件),把前面汇总的符号形成一个符号表,符号表中记录的了汇总的符号并给它们分配了一个地址。

注意:main函数里的add只是一个声明,给这个add分配的这个地址是没有任何意义的,相当于就是一个标识符,这函数有没有还是取决去前面是否定义这个add函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p0rmGJpA-

2) 链接

在Linux环境下通过gcc test.o对目标文件进行链接,生成一个a.out的可执行文件(相当于windos中的.exe文件)

在链接期间主要会做那么两件事情

  1. 合并段表
  2. 符号表的合并和重定位

简单来说就是它会把多个.o的目标文件进行链接,因为一个项目编译后会有多个目标文件,这些文件又没有任何关系,通过它们的函数声明进行链接,把这些文件都关联起来。

如果一个函数没有被定义就会出现的链接错误(无法解析的外部命令)

和并段表和符号表,简单理解就是多个目标文件中相同的段只保留一个,比如合并符号表保留add函数的符号和地址。链接期间就是检查外部的一些函数和符号定义是否合法。

链接完毕后就生成了可执行程序。

图解过程

运行环境

  1. 编译完后的可执行程序,程序必须加载到内存中,在有操作系统的环境中,这个操作一般由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成
  2. 程序开始执行,接着就要调用main函数
  3. 开始执行程序代码,这个时候程序将为函数开辟栈帧,存储函数的局部变量何返回地址。程序同时开始也可以使用静态(static)内存。存储在静态内存中的变量在整个执行过程一直保留着它们的值
  4. 终止程序,正常终止main函数,也有可能意外终止

2. 预处理

1) 预定义符号

C语言中由一些预定义的符号,它们分别保存这一些信息,它们也是在预处理阶段被直接替换的。

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

int main()

	printf("进行编译的源文件: %s\\n",__FILE__);
	printf("文件当前的行号: %d\\n",__LINE__);
	printf("文件被编译的日期: %s\\n",__DATE__);
	printf("文件被编译的时间: %s\\n",__TIME__);

	return 0;

在vs2019中没有__STDC__没有定义整个符号,说明vs2019对ANSI C的支持是不好的,而我在Linux环境下正常输出1说明在LInux环境下是严格遵循C语言标准的。

2) #define

通过#define可以定义标识符,也可以定义宏

#fefine定义标识符

#include <stdio.h>
#define MAX 100000
#define STR "hello"
#define PRINTLN printf("\\n")
int main()

	printf("%d", MAX);
	PRINTLN;
	printf("%s", STR);
	
	

	return 0;

运行结果

100000
hello

#define定义宏的时候,后面要不要加分号;?

建议是不加,加上分号分号也会被替换过去,需要的时候加就可以了。

#define 定义宏

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

写一个宏来计算两个数的和

#include <stdio.h>
#define ADD(x,y) x+y
int main()

	int a = 10;
	int b = 20;
	printf("%d\\n", add(a, b));
	
	

	return 0;

这中写法是存在问题的,当写出这样的代码的时候就会出现问题

#include <stdio.h>
#define ADD(x,y) x+y
int main()

	int a = 10;
	int b = 20;
	printf("%d\\n", add(a, b)*add(a,b));



	return 0;

打印结果

230

这并不是我们想要的结果,因为在替换后发生了优先级的问题

printf("%d\\n", ADD(a, b)*ADD(a,b));
	//等价于printf("%d\\n", 10+20*10+20);

解决方法就是给宏的每一个参数加上括号,整体再加上括号

#include <stdio.h>
#define ADD(x,y) ((x)+(y))
int main()

	int a = 10;
	int b = 20;
	printf("%d\\n", ADD(a, b)*ADD(a,b));


	return 0;

所以以后用宏求这种数值表达式的时候,最后把每一个参数加上括号,避免再使用宏。

define替换宏的规则

在程序中进行宏替换的时候,需要涉及到以下几个步骤

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

注意

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

比如下面这种写法是没有问题的

#define MAX 1000
#define add(x,y) ((x)+(y))*MAX

这种宏里写#define定义的符号没有问题,但宏是不支持自己调用自己的

3) #和##

如何把参数插入到字符串中?

解答这个问题前先来看一下C语言的另外一种字符串写法


#include <stdio.h>

int main()

	char* str = "hello" "world;";
	printf("%s\\n", str);
	printf("123" "abc\\n");
	return 0;

打印结果

helloworld;
123abc

把两个字符串写一起,在编译阶段它们会自动拼接成一个字符串。

现想完成这么一个打印,把一个变量的变量名和值打印出来,且插入在字符串中,我们发现这并不好实现。这个时候就可以用到宏。

#include <stdio.h>

int main()

	float f = 4.5f;
	printf("the value of f is %f\\n", f);

	int a = 10;
	printf("the value of a is %d\\n", a);

	int b = 20;
	printf("the value of b is %d\\n", b);

	return 0;

通过宏定义可以把代码写成这样,也能达到上面代码的效果,避免了代码的冗余。

#include <stdio.h>
#define PRINT(data, format) printf("the value of "#data" is %"#format"\\n",data)
int main()

	float f = 4.5f;
	PRINT(f, f);

	int a = 10;
	PRINT(a, d);


	int b = 20;
	PRINT(b,d);

	return 0;

#data等价于“data”,在预编译期间就会被替换成对于的字符。

##的作用

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

#include <stdio.h>
#define APPEND(str,number) str##number
int main()

	int day100 = 2022;
	printf("%d\\n", APPEND(day,100));

	return 0;

运行结果

2022

4) 带副作用的宏参数

当宏参数的定义出现超过一次的时候,如果参数带有副作用,那么在使用这个宏的时候就可能出现危险,导致不可预测的后果。

比如下面这个代码救会出现副作用

#include <stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()

	int a = 10;
	int b = 20;
	printf("%d\\n", MAX(a++, b++));
	printf("a=%d b=%d\\n", a, b);

	return 0;

这里的**b++**被执行了两次,相当于替换后的表达式就是

printf("%d\\n", ((a++) > (b++) ? (a++) : (b++)));

这就是带有副作用的宏参数

5) 宏和函数对比

宏通常用来做一些简单的运算,比如我们求两个数的和

#define ADD(x,y) ((x)+(y))

那为什么不用函数来完成这个任务呢?

int add(int x, int y)

    return x + y;

宏对比函数的优势

  1. 用调用函数和从函数返回的代码可能比实际执行这么一个小型计算工作所需要的时间更多,所以宏比函数的规模和速度上更胜一筹

    来看一段代码对比

    这是通过宏来计算两数之和的汇编代码

然后再看下通过函数计算两数之和代码转换为汇编代码的代码量

我们发现通过宏实现代码量只有7条,而通过函数实现则由十几行汇编代码。

宏在预编译期间就把定义的代码进行替换后面进行运算就可以了,而函数则存在一个调用+运算+返回三个过程。

  1. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可
    以适用于整形、长整型、浮点型等可以用于>来比较的类型。宏是类型无关的

    上面的代码宏能计算各种类型的和,而函数只能只能计算整形的和。

    再举个列子,我们常用的malloc函数用来开辟空间,我们可以写一个宏来开辟空间,而传递类型函数是做不到的。

    #include <stdio.h>
    #include <stdlib.h>
    #define MALLOC(size,type) (type*)(malloc(sizeof(type)*size)) 
    
    int main()
    
    	int* arr = MALLOC(10, int);
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	
    		arr[i] = i;
    	
    	for (i = 0; i < 10; i++)
    	
    		printf("%d ", arr[i]);
    	
    	return 0;
    
    

宏对比函数的劣势

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度
  2. 宏是没法调试的
  3. 宏由于类型无关,也就不够严谨
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错

对比总结

#define定义宏函数
代码每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操 作 符优先级宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带 有 副 作参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果函数参数只在传参的时候求值一次,结果更容易控制
参 数宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是不同的
调式宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

3. 常见预处理命令

1) #undef

这条指令用来一处一个宏定义

#include <stdio.h>
#define MAX 1000

int main()

	int tmp = MAX;
#undef MAX
	int ret = MAX;//报错

	
	return 0;

2) 命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 例如:当我们根据同一个源文件要编译出不同的一个程序的不同版本的时候,这个特性有点用处。

#include <stdio.h>


int main()

	int arr[SIZE] =  0 ;
	int i = 0;
	for (i = 0; i < SIZE; i++)
	
		arr[i] = i;
	
	for (i = 0; i < SIZE; i++)
	
		printf("%d ", arr[i]);
	

	
	return 0;

再Linux64位环境下通过命令gcc -D SIZE=10 test.ctest.c文件进行编译,生成a.out文件,运行就是一个大小为10的数组

[root@aliyun code]# ./a.out 
0 1 2 3 4 5 6 7 8 9

3) 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。
比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译

#include <stdio.h>
#define DEBUG 1

int main()

	printf("hello world!\\n");
#ifdef DEBUG
	printf("test");
#endif // DEBUG


	
	return 0;

如果把DEBUG设置为0,打印test的那一行代码就不会进行编译

当然也可以多个分支

#include <stdio.h>
#define DEBUG 0

int main()

	int a = 0;
	printf("hello world!\\n");
#if DEBUG
	printf("test");
#elif a
	printf("false");
#else
	printf("haha");
#endif // DEBUG

	return 0;

嵌套定义

#include <stdio.h>
#define DEBUG 0

int main()

	int a = 0;
	printf("hello world!\\n");
#if defined(DEBUG)
	#if 0
	printf("0");
	#elif a-1
	printf("0");
	#else a+1
	printf("1");
	#endif

#endif



	
	return 0;

4) 文件包含

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。
这种替换的方式很简单: 预处理器先删除这条指令,并用包含文件的内容替换。 这样一个源文件被包含10次,那就实际被编译10次 。

头文件被包含方式

  • 本地文件包含

    #include "add.h"
    

    查找方式:先再源文件所在目录查找add.h的头文件,如果头文件未查找到,编译器就像查找库函数头文件一样再标准位置查找头文件,如果找不到就提示编译错误。

    Linux环境标准头文件路径/usr/include/

  • 库文件包含

    #include <stdio.h>
    

    查找文件直接取标准路径下查找,如果找不到就提示编译错误。

    用<

    梦开始的地方——c语言文件操作详解(代码片段)

    文章目录C语言文件操作1.什么是文件?2.文件指针3.文件的打开和关闭4.文件的顺序读写fgetc&fputcfgets&fputsfread&fwritefscanf&fprintfscanf/fscanf/sscanf对比printf/fprintf/sprintf5.文件的随机读写(fseek&ftell&rewind)6.文件结 查看详情

    梦开始的地方——c语言指针入门(代码片段)

    文章目录指针入门1.指针概念2.指针和指针类型3.野指针造成野指针的原因如何避免野指针4.指针的运算指针加减整数指针的运算关系指针的关系运算5.指针和数组6.二级指针7.指针数组指针入门1.指针概念指针(Pointer)是编程语言中... 查看详情

    梦开始的地方——c语言指针练习题(代码片段)

    指针练习注意我的环境是:win10下的VS2019-X86环境练习1intmain()inta[5]=1,2,3,4,5;int*ptr=(int*)(&a+1);printf("%d,%d",*(a+1),*(ptr-1));return0;&a取出整个数组的地址,但它的地址和首元素的地址还是一样的&#x 查看详情

    梦开始的地方——c语言常用字符函数汇总(代码片段)

    文章目录字符库函数1.strlen(求字符串长度)2.strcpy(字符串拷贝)3.strcat(字符串追加函数)4.strcmp(字符串比较)5.strncpy(字符串拷贝)6.strncat(字符串追加)7.strncmp(字符串比较)8.strstr(字符串查找)9.strtok(字符串分割)10.strerror(错误信息)字符库... 查看详情

    梦开始的地方——c语言中那些细节(代码片段)

    文章目录static关键字1.static修饰局部变量2.static修饰全局变量3.static修饰函数函数的默认返回值隐式类型转换1.整形提升2.整型提升的意义3.算数转换static关键字1.static修饰局部变量生命周期延长:该变量不随函数结束而结束ÿ... 查看详情

    梦开始的地方——c语言动态内存管理(malloc+calloc+realloc+free)(代码片段)

    文章目录动态内存管理1.为什么需要动态内存分配?2.动态内存函数malloc&freecallocrealloc3.常见的动态内存错误对NULL解引用对动态开辟空间的越界访问对非动态开辟内存使用free释放使用free释放一块动态开辟内存的一部分对同... 查看详情

    梦开始的地方——c语言数据在内存中的存储(整形+浮点型)(代码片段)

    文章目录整形在内存中的存储1.数值类型的基本分类2.整形在内存中的存储1.原码、反码、补码2.内存中为什么要存放补码?3.大小端存储4.无符号有符号数练习5.有符号数无符号数小结浮点型在内存中的存储IEEE754整形在内存中... 查看详情

    c语言编译过程,满满的干货!!!(代码片段)

    程序环境和预处理一、程序翻译和运行环境二、预处理详解1.预定义符号2.define定义宏3.#和##的区别4.宏和函数好坏比较5.命名约定6.头文件中<>和""区别7.条件编译一、程序翻译和运行环境翻译环境:在翻译环境中ÿ... 查看详情

    c语言----程序编译(预处理)(代码片段)

    ...到运行结果的大体过程编译1)预编译gcctest.c-E>test.i预处理后停止完成文本操作完成头文件包含#define定义的符号和宏的替换去除注释2)编译gcctest.i-S生成test.s的文件把C语言代码转化为汇编代码编译部分深入学习编译原... 查看详情

    c语言攻略-从零开始的c语言生活----初阶篇(代码片段)

    各位大佬大家好啊!从今天开始正式的学习C语言,就废话不多说我所使用编译器:【VisualStudio2019】目录了解什么是C语言    C语言的发展史第一个C程序——梦开始的地方数据类型数据类型所占内存大小(sizeof关... 查看详情

    c语言编译过程--预处理探索(代码片段)

    ...34;helloworld\\n"); return0;通过下面的命令对hello.c文件进行预处理gcc-Ehello.c>hello.e可以看到预处理后的文件hello.e的文件大小是hello.c的大概160倍因为文件太大,为了简单了解这个文件里面有什么内容,我不得不节选一些重... 查看详情

    c语言预处理(代码片段)

    预处理(或称预编译)是指在进行编译的第一遍扫描(词法扫描和语法分析)之前所作的工作。预处理指令指示在程序正式编译前就由编译器进行的操作,可放在程序中任何位置。当我们写了一个代码,从一个文本文件的代码... 查看详情

    makefile第三课:c语言的编译(代码片段)

    ...i文件gcc-Emain.cgcc-Emain.c-ohelloworld.i-E选项告诉编译器只进行预处理操作-o选项把预处理的结果输出到指定文件2.2GeneratingAssemblyLanguagegcc-Smain.cgcc-Smain.c 查看详情

    梦开始的地方——c语言:函数指针+函数指针数组+指向函数指针数组的指针(代码片段)

    文章目录一、函数指针1.函数指针定义2.函数指针的使用3.解读函数指针代码二、函数指针数组三、指向函数指针数组的指针四、回调函数一、函数指针1.函数指针定义整形指针是指向整形的指针:存放整形的地址数组指针是... 查看详情

    梦开始的地方,从最小二乘法开始学机器学习(代码片段)

    梦开始的地方,从最小二乘法开始学机器学习从这篇博客开始,我们将逐步实现不同的机器学习代码,以此来深入学习了解不同的机器学习背后的原理~文章目录梦开始的地方,从最小二乘法开始学机器学习00.参考... 查看详情

    梦开始的地方,从最小二乘法开始学机器学习(代码片段)

    梦开始的地方,从最小二乘法开始学机器学习从这篇博客开始,我们将逐步实现不同的机器学习代码,以此来深入学习了解不同的机器学习背后的原理~文章目录梦开始的地方,从最小二乘法开始学机器学习00.参考... 查看详情

    c语言中程序的编译(预处理操作)+链接详解(详细介绍程序预编译过程)(代码片段)

    ...目录1.前言2.翻译环境和运行环境2.1翻译环境2.2运行环境3.预处理详解3.1预定义符号3.2#define定义的标识符常量和宏3.2.1#define定义的标识符常量3.2.2#define定义的宏3.2.3#define替换规则3.2.4#和##3.2.5带副作用的宏参数3.3宏和函数的对比4.... 查看详情

    makefile(代码片段)

    ...完后有一些常见的输出文件.a 静态库(文档).c 需要预处理的C语言源代码.h C语言源代码的头文件.i 经过预处理后的C语言源代码.o 目标文件(经过汇编产生).s 经过编译后产生的汇编语言代码编译过程.c->.i->.s->.o1... 查看详情