【C语言】预处理、宏定义、内联函数 _
一、由源码到可执行程序的过程
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
汇编代码。为了让编译器聚焦核心功能,就将一些非核心功能剥离到预处理器去了,也就是所让预处理器帮编译器做一些编译前的准备工作。
二、编程中常见的预处理
- 头文件包含:
#include
- 注释
- 条件编译:
#if #elif #endif...
- 宏定义
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
宏定义的预处理
- 在预处理阶段由预处理器进行机械替换,而不做类型检查
- 宏定义替换会递归进行,直到替换出来的值本身不再是一个宏为止
如下是在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) |
这个宏需要注意的是
- 当一个数字直接出现在程序中时,它的是类型默认是
int
- 一年有多少秒,这个数字超过了
int
类型存储的范围- 加
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 noinline
和always_inline
当函数的函数体很小,而且被大量频繁调用,应该做内联展开时,就可以使用内联函数。但编译器会不会作内联展开,编译器也会有自己的权衡(不合理的内联函数会降低CPU
寻址效率、函数运行效率、降低代码的可移植性…)。如果想告诉编译器一定要展开,或者不作展开,就可以使用noinline
或always_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语言】预处理、宏定义、内联函数 _的更多相关文章
- C/C++之宏、内联函数和普通函数的区别
内联函数的执行过程与带参数宏定义很相似,但参数的处理不同.带参数的宏定义并不对参数进行运算,而是直接替换:内联函数首先是函数,这就意味着函数的很多性质都适用于内联函数,即内联函数先把参数表达式进行运算 ...
- c++ _宏与内联函数
第一部分:宏为什么要使用宏呢?因为函数的调用必须要将程序执行的顺序转移到函数所存放在内存中的某个地址,将函数的程序内容执行完后,再返回到转去执行该函数前的地方.这种转移操作要求在转去执行前要保存现场并 ...
- 特殊用途语言特性(默认实参/内联函数/constexpr函数/assert预处理宏/NDEBUG预处理变量)
默认实参: 某些函数有这样一种形参,在函数的很多次调用中它们都被赋予一个相同的值,此时,我们把这个反复出现的值称为函数的默认实参.调用含有默认实参的函数时,可以包含该实参,也可以省略该实参. 需要特别 ...
- C++语言基础(7)-inline内联函数
函数调用是有时间和空间开销的.程序在执行一个函数之前需要做一些准备工作,要将实参.局部变量.返回地址以及若干寄存器都压入栈中,然后才能执行函数体中的代码:函数体中的代码执行完毕后还要清理现场,将之前压 ...
- __inline定义的内联函数和宏的区别
转自:http://blog.csdn.net/lw370481/article/details/7311668 函数与宏 #define TABLE_COMP(x) ((x)>0?(x):0) ...
- (转载)内联函数inline和宏定义
(转载)http://blog.csdn.net/chdhust/article/details/8036233 内联函数inline和宏定义 内联函数的优越性: 一:inline定义的类的内联函 ...
- [C++] inline内联函数使用方法
C++支持内联函数,目的是为了提高函数的执行效率,类似于C语言中的宏定义 内联函数在调用时将它在程序中的每个调用点展开,不用额外分配栈空间 内联函数的定义在一个源文件中出现一次,但在多个源文件中可以同 ...
- 【C语言天天练(二一)】内联函数
引言:调用函数时,一般会由于建立调用.传递參数.跳转到函数代码并返回等花费掉一些时间,C语言的解决的方法是使用类函数宏.在C99中,还提出了第二种方法:内联函数. 内联 ...
- 嵌入式C语言自我修养 10:内联函数探究
10.1 属性声明:noinline & always_inline 这一节,接着讲 __atttribute__ 属性声明,__atttribute__ 可以说是 GNU C 最大的特色.我 ...
随机推荐
- MM32F0020 UART1中断接收
目录: 1.MM32F0020简介 2.初始化MM32F0020 UART1和NVIC中断 3.编写MM32F0020 UART1中断接收函数 4.编写MM32F0020 UART1发送字节和ASCI ...
- 四、Java基础
Java基础 在开始学习Java基础之前,我们先来学习一下IDEA 打开IDEA,新建一个项目(New Project),选择空项目(Empty Project),填写项目名(Project name ...
- vue--按需加载的3种方式(解决网页首次加载速度慢的问题)
一.vue的异步组件加载 使用异步组件加载,打包的时候会将每个组件分开打包到不同的js文件中: {path: '/index', name: 'index', meta:{ title:'首页', r ...
- 安装CentOS时,推荐的分区方案
最佳分区设置取决于 Linux 系统的用途.
- 基于 vite2 + Vue3 写一个在线帮助文档工具
提起帮助文档,想必大家都会想到 VuePress等,我也体验了一下,但是感觉和我的思路不太一样,我希望的是那种可以直接在线编辑文档,然后无需编译就可以直接发布的方式,另外可以在线写(修改)代码并且运行 ...
- quartz框架(六)-ThreadPool
ThreadPool 本篇博文,博主将介绍Quartz框架中ThreadPool线程池相关的内容.线程池顾名思义,就是一个可以帮助我们来进行线程资源管理的对象.在web开发中,常见的就有数据库连接池, ...
- linux作业--第四周
1.自建yum仓库,分别为网络源和本地源 所有Yum仓库的配置文件均需以 .repo 结尾并存放在/etc/yum.repos.d/目录中的 [base] : yum仓库唯一标识符,避免与其它仓库冲突 ...
- Triple Shift
来源:Atcoder ARC 136 B - Triple Shift (atcoder.jp) 题解:这道题我们不可能去硬模拟(大多数这种题都不能这样去模拟的),然后我们就要去发现特性, 发现把 a ...
- php 23种设计模式 - 迭代器模式
迭代器模式 迭代器模式 (Iterator),又叫做游标(Cursor)模式.提供一种方法访问一个容器(Container)对象中各个元素,而又不需暴露该对象的内部细节. 当你需要访问一个聚合对象,而 ...
- ArcGIS下载安装
鉴于各位在安装过程中碰到诸多问题,博主打算分享下安装过程 ,仅供参考 一.注意:两个版本安装之前都必须的步骤 安装license Manager10.2,下载完成后,打开安装点击SetUp.exe,如 ...