一、由源码到可执行程序的过程

1. 预处理: 源码经过预处理器的预处理变成预处理过的.i中间文件

 
1 gcc -E test.c -o test.i

2. 编译: 中间文件经过编译器编译形成.s的汇编文件

 
1 gcc -S test.i -o test.s

3. 汇编: 汇编文件经过汇编器生成目标文件.o(机器语言)

 
1 gcc -c test.s -o test.o

4. 链接: 链接器将目标文件链接成.exe可执行程序(Linux下是.elf)

 
1 gcc test.o -o test.exe

在整个过程中,预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他额外的可能会用到的工具,合起来叫编译工具链。gcc/g++就是一个编译工具链,在实际工程中并不会去手动生成那么多中间文件,而是直接一步到位:

 
1 gcc test.c -o test.exe

其中,编译器的主要目的是编译源代码,即将.c的源代码(.i本质上就是预处理过的.c)转化成.s汇编代码。为了让编译器聚焦核心功能,就将一些非核心功能剥离到预处理器去了,也就是所让预处理器帮编译器做一些编译前的准备工作。

二、编程中常见的预处理

  1. 头文件包含:#include
  2. 注释
  3. 条件编译:#if #elif #endif...
  4. 宏定义

2.1 头文件包含

2.1.1 #include <>和 #include ""的区别

  • #include <>专门用来包含系统提供的头文件,如果使用<>,编译器只会到系统指定目录(编译器中配置的或OS配置的目录,如在Ubuntu中是/usr/include,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件,如果找不到就会提示这个头文件不存在。
  • #include ""用来包含程序员写的头文件,编译器默认先在当前目录下寻找相应的头文件,如果没找到再到系统指定目录去寻找,如果还没找到则提示文件不存在。
使用原则
  • 系统自带的用<>
  • 程序员写的放在当前目录下用""
  • 程序员写的集中放专门存放头文件的目录下,在编译器中用-I参数寻找用<>

2.1.2 头文件包含在预处理时的处理方式

在预处理的时候,预处理器将所包含的头文件的内容原处展开替换这#include语句。
如下所示分别是同一目录下的.c文件和.h文件:

对其进行预编译生成.i文件后,.i文件的内容如下所示:

2.2 注释

我们在.c源文件中写的注释,预处理器在预处理阶段会将其擦除(在.c文件依然存在,在.i文件中不存在)。其实这也正顺应了注释是写给使用程序的人看的,而不是给编译器看的。如下所示:

2.3 条件编译

一般情况下,源程序中所有行都参与编译,但有时希望程序有多种配置,对一部分内容指定编译条件(如产品的调试版与正式版),这就是条件编译(conditional compile),条件编译有以下两种判定方法:

2.3.1 #if

这种判定方法类似于if...else...语句,格式为#if (条件表达式),它的判定标准是()中的表达式是true还是flase,在进行预编译的过程中,只会保留条件表达式为真的那部分内容,如下所示测试代码:

 
 
 1 #include <stdio.h>
2 #define DEBUG 1
3
4 int main(void)
5 {
6 #if(DEBUG == 1)
7 printf("Version: DEBUG!");
8 #elif(DEBUG == 2)
9 printf("Version: TEST!");
10 #else
11 printf("Version: LAST!");
12 #endif
13
14 return 0;
15 }
 

预编译结果如下所示:

我们也可以不在源码中对DEBUG进行宏定义,而在编译的时候可以用如下方法对其进行宏定义并指定值:

 
1 gcc -E -D DEBUG=2 test.c -o test.i

结果如下所示:

2.3.2 #ifdef

#ifdef XXX判定条件成立与否时主要是看XXX这个符号在本语句之前有没有被定义,只要定义了,判断就成立,并不关心XXX的宏值为多少。测试代码及结果参见下文3.1非带参宏的内容

三、宏定义

3.1 非带参宏

非带参宏主要结合条件编译使用,比较简单,其定义格式为

 
  #define 宏名 替换列表(替换列表可有可无)

如下所示:

 
1 #define DEBUG
2 #define TEST 1
3 #define TEST2 TEST

宏定义的预处理

  1. 在预处理阶段由预处理器进行机械替换,而不做类型检查
  2. 宏定义替换会递归进行,直到替换出来的值本身不再是一个宏为止

如下是在STM32开发过程中常用的打印调试信息的一个调试代码段:

 
 
 1 #include <stdio.h>
2
3 #define USER_TIMER_DEBUG
4 #ifdef USER_TIMER_DEBUG
5 #define user_timer_printf(format, ...) printf( format "\r\n", ##__VA_ARGS__)
6 #define user_timer_info(format, ...) printf("[\ttimer]info:" format "\r\n", ##__VA_ARGS__)
7 #define user_timer_debug(format, ...) printf("[\ttimer]debug:" format "\r\n", ##__VA_ARGS__)
8 #define user_timer_error(format, ...) printf("[\ttimer]error:" format "\r\n",##__VA_ARGS__)
9 #else
10 #define user_timer_printf(format, ...)
11 #define user_timer_info(format, ...)
12 #define user_timer_debug(format, ...)
13 #define user_timer_error(format, ...)
14 #endif
 

预编译结果如图所示:

3.2 带参宏

宏可以带参数,称为带参宏。带参宏的使用和带参函数非常相似,只是在使用上和处理上有一些差异,其定义格式为:

 
  #define 标识符(参数1,参数2,...,参数n) 替换列表

在定义带参宏时,每一个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可。

不带括号的后果:

 
 
1 #include <stdio.h>
2 #define M(a, b) a * b
3 int main(void)
4 {
5 int result = M(2 + 3, 5)
6 printf("%d", result);
7 return 0;
8 }
 

如上测试代码,我们想得到(2 + 3) * 5的结果,但是由于宏在预处理的时候也是进行机械替换,int result = M(2 + 3, 5)变成了int result = 2 + 3 * 5,这及其容易出现逻辑上的错误

3.2.1 带参宏示例

1.MAX宏: 求2个数中较大的一个

 
  #define MAX(a, b) (((a)>(b)) ? (a) : (b))

2.SEC_PER_YEAR宏 用宏定义表示一年中有多少秒

 
  #define SEC_PER_YEAR (365*24*60*60UL)

这个宏需要注意的是

  1. 当一个数字直接出现在程序中时,它的是类型默认是int
  2. 一年有多少秒,这个数字超过了int类型存储的范围
  3. UL将其转为无符长整型

3.2.2 带参宏和带参函数的区别

1.时间与空间
  • 宏定义在预处理期间处理,进行简单的内容替换,无需额外空间
  • 函数是在编译期间处理的,调用时需要为形参分配空间并将实参的值赋给形参
2.执行速度
  • 宏只进行文本替换,函数运行阶段参数需要进行出入栈的操作,速度比宏慢
3.类型检查
  • 宏定义不会检查参数的类型,返回值也不会附带类型
  • 而数有明确的参数类型和返回值类型。当调用函数时编译器参数的静态类型检查,如果编译器发现实际传参和参数声明不同时会报警告或错误。
综合比较

宏和函数各有千秋,最大的特点是:用函数的时候程序员不太用操心类型不匹配因为编译器会检查,用宏的时候程序员必须注意实际传参和宏所希望的参数类型一致,或者自行加入类型检查,否则可能编译不报错但是运行有误。

如对MAX宏加入类型检查:

 
 
1 #define MAX(a, b) ({\
2 typeof(a) _a = (a); \
3 typeof(b) _b = (b); \
4 (void) (&_a == &_b);\
5 _a > _b ? _a : _b;})
 

测试代码:

 
 
 1 #include <stdio.h>
2
3 #define MAX(a, b) ({\
4 typeof(a) _a = (a); \
5 typeof(b) _b = (b); \
6 (void) (&_a == &_b);\
7 _a > _b ? _a : _b;})
8
9 #define MAX2(a, b) a > b ? a : b
10
11 int main(void)
12 {
13 int a = 2;
14 float b = 3.1;
15 int result2 = MAX2(a, b);
16 typeof(MAX(a, b)) result = MAX(a, b);
17 printf("result = %f\n", result);
18 printf("result2 = %d\n", result2);
19 return 0;
20 }
 

四、内联函数

内联函数本质上是函数,通过在函数定义前加inline关键字实现,是编译器负责处理的,可以做参数的静态类型检查。同时也有带参宏的展开特性,运行时没有调用开销。

4.1 与常规函数对比

  • 当函数体很短的时候,使用常规函数会造成很大的调用开销,内联函数采用原地展开的方式,没有调用开销
  • 当函数体长的时候,由于内联函数展开会降低寻址效率,所以长函数体不会使用内联函数
  • 内联函数本质上是函数,函数的性质内联函数都有

4.2 与宏的对比

  • 参数类型检查。编译过程中,编译器仍可以对其内联函数进行参数检查
  • 便于调试。函数支持的调试功内联函数同样支持,而宏不支持
  • 接口封装。有些内联函数可以用来封装一个接口,而宏不具备这个特性

4.3 noinlinealways_inline

当函数的函数体很小,而且被大量频繁调用,应该做内联展开时,就可以使用内联函数。但编译器会不会作内联展开,编译器也会有自己的权衡(不合理的内联函数会降低CPU寻址效率、函数运行效率、降低代码的可移植性…)。如果想告诉编译器一定要展开,或者不作展开,就可以使用noinlinealways_inline对函数作一个属性声明。

 
1 static inline int func(int *, int);//编译器权衡是否内联展开
2 static inline __attribute__((noinline)) int func(int *, int);//不内联展开
3 static inline __attribute__((always_inline)) int func(int *, int);//内联展开

static修饰呢是因为内联函数不一定会内联展开,当多个文件都包含同一个内联函数的定义时,如果没有static将函数的作用域限制在各自本地文件内,编译时就有可能报重定义错误

【C语言】预处理、宏定义、内联函数 _的更多相关文章

  1. C/C++之宏、内联函数和普通函数的区别

    内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算 ...

  2. c++ _宏与内联函数

    第一部分:宏为什么要使用宏呢?因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去执行前要保存现场并 ...

  3. 特殊用途语言特性(默认实参/内联函数/constexpr函数/assert预处理宏/NDEBUG预处理变量)

    默认实参: 某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参.调用含有默认实参的函数时,可以包含该实参,也可以省略该实参. 需要特别 ...

  4. C++语言基础(7)-inline内联函数

    函数调用是有时间和空间开销的.程序在执行一个函数之前需要做一些准备工作,要将实参.局部变量.返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码:函数体中的代码执行完毕后还要清理现场,将之前压 ...

  5. __inline定义的内联函数和宏的区别

    转自:http://blog.csdn.net/lw370481/article/details/7311668 函数与宏 #define TABLE_COMP(x) ((x)>0?(x):0) ...

  6. (转载)内联函数inline和宏定义

    (转载)http://blog.csdn.net/chdhust/article/details/8036233 内联函数inline和宏定义   内联函数的优越性: 一:inline定义的类的内联函 ...

  7. [C++] inline内联函数使用方法

    C++支持内联函数,目的是为了提高函数的执行效率,类似于C语言中的宏定义 内联函数在调用时将它在程序中的每个调用点展开,不用额外分配栈空间 内联函数的定义在一个源文件中出现一次,但在多个源文件中可以同 ...

  8. 【C语言天天练(二一)】内联函数

            引言:调用函数时,一般会由于建立调用.传递參数.跳转到函数代码并返回等花费掉一些时间,C语言的解决的方法是使用类函数宏.在C99中,还提出了第二种方法:内联函数.         内联 ...

  9. 嵌入式C语言自我修养 10:内联函数探究

    10.1 属性声明:noinline & always_inline 这一节,接着讲 __atttribute__ 属性声明,__atttribute__ 可以说是 GNU C 最大的特色.我 ...

随机推荐

  1. win7重装系统过程关机 电脑开机黑屏 硬盘无法识别 无法使用u盘启动

    问题:win7重装系统中强制重启导致硬盘无法识别,开机后无法选择使用u盘启动盘启动,电脑黑屏,将硬盘拆掉可以使用u盘启动,使用SATA转接口在win7中有反应但无法识别 无法识别原因:重装系统过程中断 ...

  2. Qt:QCustomPlot使用教程(二)——基本绘图

    0.说明 本节翻译总结自:Qt Plotting Widget QCustomPlot - Basic Plotting 本节内容是使用QCustomPlot进行基本绘图. 本节教程都使用custom ...

  3. package.xml使用说明

    1. package.xml使用说明 a. pacakge.xml 包含了package的名称. 版本号. 内容描述. 维护人员. 软件许可. 编译构建工具. 编译依赖. 运行依赖等信息. 2. pa ...

  4. Django中的Session和cookie

    Session和cookie 参考文献:https://www.cnblogs.com/wupeiqi/articles/5246483.html 1.问题引入 1.1 cookie是什么? 保存在客 ...

  5. 接口自动化测试框架(Java 实现)

    目录 需求分析 开发设计 分层与抽象 技术选型 主要类设计 测试文件设计 工程目录设计 工程实现 github 地址 运行示例 需求分析 需求点 需求分析 通过 yaml 配置接口操作和用例 后续新增 ...

  6. Python:pyglet学习(1):想弄点3D,还发现了pyglet

    某一天,我突然喜欢上了3D,在一些scratch教程中见过一些3D引擎,找了一个简单的,结果z轴太大了,于是网上一搜,就发现了pyglet 还是先讲如何启动一个窗口 先看看官网: Creating a ...

  7. tp限制访问频率

    作用 通过本中间件可限定用户在一段时间内的访问次数,可用于保护接口防爬防爆破的目的. 安装 composer require topthink/think-throttle 安装后会自动为项目生成 c ...

  8. MySQL优化之索引解析

    索引的本质 MySQL索引或者说其他关系型数据库的索引的本质就只有一句话,以空间换时间. 索引的作用 索引关系型数据库为了加速对表中行数据检索的(磁盘存储的)数据结构 索引的分类 数据结构上面的分类 ...

  9. 对于处理datetime数据类型的一些常用方法:

    datetime数据类型常用方法: 在项目中从数据库中取出数据后通常需要先绘制图像进行数据的观察,此过程中使用到的方法: 1.时间数据类似于 2022-03-23 14:21:45 可以先转换为dat ...

  10. LGP4841题解

    无向联通图计数板子 首先,这个太难了,先让我们来求一个简单的: 无向图计数. 一共 \(\frac {n \times (n+1)} 2\) 条可能存在的边,枚举一条边是否存在,就有 \(2^{\fr ...