C语言预处理指令-单片机必备技能

✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@rivencode的个人主页
🔥系列专栏:玩转C语言
💬推荐一款模拟面试、刷题神器,从基础到大厂面试题👉点击跳转刷题网站进行注册学习

一.C代码的编译连接运行

一个c语言的代码要转变成一个可执行程序要经过 编译——>链接——>执行三个步骤
1. 程序的翻译环境和执行环境

  • 翻译环境:在这个环境中源代码(文本代码)被转换为可执行的机器指令(二进制代码)。
  • 执行环境:在这个环境下执行代码。

1.预编译处理

  • 头文件的包含(将test.c所有引入的头文件里面的内容拷贝到test.c
  • 删除test.c中的注释(使用空格替换)
  • #define宏定义的替换
  • 条件编译

2.编译
将C语言代码转化为汇编代码

  • 语法分析
  • 词法分析
  • 语义分析
  • 符号汇总

符号汇总包括全局变量和函数名

3.汇编

  • 将汇编代码转化成二进制代码(指令
  • 形成符号表

4.链接

  • 合并段表
  • 符号表的合并和重定位

详情如图

在这里插入图片描述
在这里插入图片描述

看到这相信你对源文件编译和链接有了一定的认识接下来开始我们的重点,预编译里的预处理指令

二.预处理指令的简介

所有的预处理指令都是以#开始的,而且不用以;结尾,常用的有 #include
#define #if #ifdef #ifndef 。

1.单片机用宏定义与条件编译的好处

在单片机编程中要想写出一个可读性强且,漂亮的代码,宏定义是必然是要用到的,单片机人基本是红(宏)孩儿

  • 增加程序的可移植性

一般的可以将外设初始化函数所用到的端口号,引脚号,时钟,函数名等都可以用宏定义因为单片机的型号有很多,如果你换了一个板子上面所说的外设参数很可能会发生变化,此时只要修改宏定义的参数就完成的替换,若不用宏定义则得一个一个在代码中改,而且容易出错
在这里插入图片描述

  • 增加程序可读性

如果代码需要一些指令(一般是一些十六进制的数)或者是一些标志如果你直接填十六进制的数进去,过两天你能确保你还知道这些十六进制数是什么指令嘛。
在这里插入图片描述

  • 还有很多条件编译

利用条件编译防止头文件重复包含后面会详细讲,然后随便打开一个外设的头文件里面都有条件编译必须得看懂意思
在这里插入图片描述

三.#define宏定义

1.无参#define

格式: #define 宏名 替换列表

  • 宏名:一般全部大写
  • 替换列表:可以是一个数值常量,字符常量,字符串常量,或一段代码
  • 预编译阶段代码中的宏名全部用替换列表内容替换
    直接将替换列表原模原样替换宏名
#define MAX 1000
#define REG register          //为 register这个关键字,创建一个简短的名字
#define do_forever while(1)     //用更形象的符号来替换一种实现
#define SRT  "adcef"       //一段字符串

//带参宏定义控制灯的亮灭后面细讲
#define  NO      1
#define  OFF     0

//\续行符后面不能有任何东西
#define  LED_R(a)        if(a)\
	GPIO_ResetBits(LED_R_GPIO_POTR,LED_R_GPIO_PIN);\
	else GPIO_SetBits(LED_R_GPIO_POTR,LED_R_GPIO_PIN );
LED_R(NO)//点灯

如果定义的替换列表过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。

1.在define定义标识符的时候,要不要在最后加上 ;
看个例子就明白了
不加 ;成功打印MAX的值
在这里插入图片描述
加上;出现错误在这里插入图片描述
所有为了避免此种问题不建议在后面添加;
2.宏定义如何替换直接将替换列表原模原样替换宏名

在这里插入图片描述
这里有没有算成100哈哈,这里替换列表 5+5 只是原模原样替换下来并不是先计算再替换。
3.若宏名出现在字符串中则不替换
在这里插入图片描述

1.1.#undef

就是一个取消宏定义的指令
在这里插入图片描述

2.带参#define

  • 注意:
  • 参数列表的左括号必须与宏名紧邻。
  • 如果两者之间有任何空白存在,参数列表就会被解释为替换列表的一部分。
    1.如何传参 与函数类似
    定义
    #define 宏名(形参) 替换列表
    调用
    宏名(实参)
    先将实参传给形参,再传给替换列表的形参,再把宏名整个替换成替换列表,是不是听起来有点绕直接上代码
    在这里插入图片描述
    2. 但宏定义传参与函数传参到底有啥区别呢
    先看代码我传一个算术表达式
    在这里插入图片描述
    你算对了嘛是不是有人算成36,但结果并非如此,接下来就解释一下与前面替换列表原模原样替换宏名一样形参的内容也是原模原样传给实参
    在这里插入图片描述
    函数传参
    在这里插入图片描述
    显然这次得到了我们想要的结果,函数传算术表达式时先计算后传参

由于宏定义的表达式与我们传进去表达式操作符的优先级不同导致不是我们想要的结果,解决方法就是将宏定义的表达式里的参数加上括号
在这里插入图片描述
这不就得得到我们想要的效果嘛

还并没有结束再看一段代码
在这里插入图片描述
我们想要的结果应该是10*(2+2)=40,但单单只让参数加上括号达不到我们的目的,此时只要我们整体在加上括号就带到我们的目的
在这里插入图片描述

总结:为了避免宏定义的表达式与我们传进去表达式操作符的优先级不同导致出现歧义,在定义宏的时将宏定义中表达式参数加上括号,再整体加上括号

3. 宏定义的嵌套-宏参数和#define 定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
在这里插入图片描述
是不是很好理解,到这就可以解释一下上文宏定义点灯的代码
在这里插入图片描述

当然点亮一个灯肯定不止这两段代码,还包括GPIO结构体的初始化,时钟的开启等等,若有兴趣的朋友就关注一下叭,后面会出单片机的入门到进阶详细教学,其实单片机的本质就是操作寄存器学会这个后面库函数不就是如鱼得水

2.1.#与##的用法

2.1.1 #的作用:将传过来形参转化为字符串
在这里插入图片描述
2.1.1 ##-链接符:可以把位于它两边的符号合成一个符号
在这里插入图片描述

2.2.命令行定义

有一些符号可以是在编译的时候在进行定义或者赋值
直接上代码叭,由于window环境无法执行带参命令,则我们到linux底下执行
请添加图片描述
请添加图片描述

3.函数与带参#define区别

3.1.带副作用的宏参数

x+1;//不带副作用
x++;//带副作用

x++使用后++
先看一个题,你们算算结果是什么
在这里插入图片描述
是不是11 11 12呢,答案是否定的,上代码
在这里插入图片描述
在这里插入图片描述
是不是感觉十分危险,我们再来看看函数运算该式子会不会出现问题

在这里插入图片描述
函数并没有出现此问题,还是那句话宏定义的传参:
形参的内容原模原样传给实参

3.2.宏参数没有指定的数据类型

例.只用一个宏定义就可以比较不同数据类型的数值
上代码
在这里插入图片描述
而函数参数有指定的类型如果你要比较各种数据类型数值的大小要写多个函数。
像这样
在这里插入图片描述

3.3.以宏比函数在程序的规模小和执行速度更快

接下来就来看一下函数的调用与返回需要多少条指令

请添加图片描述
在这里插入图片描述
在看一下宏用了多少条叭,就怎么几条
在这里插入图片描述
结论:用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多,宏比函数在程序的规模小和执行速度更快

3.4.宏与函数相比有如下缺点

  • 每用一次宏原文件中就会替换增加一段代码,而函数不会可以反复调用
  • 宏无法进行调试预编译阶段会将宏整体替换,此时你看到的代码与调试的代码不相同,难以进行调试
  • 上文提到的运算符优先级的问题

3.5.函数与宏的命名规则

  • 把宏名全部大写
    例:MAX
  • 函数名每个单词首字母大写其余小写
    例:Bubble Sort()

虽然不一定要按照规定来,但别的程序员都这么写你不怎么写,写出来的代码可能就不兼容,建议按照规定来

三.条件编译

如果你正在初学单片机,有些辛辛苦苦写的测试代码因为你要学下一个知识点而不得不删除或屏蔽它,屏蔽代码不会还在用 “//” “/* */” 屏蔽代码叭,那你得反思一下,这样屏蔽即显得不美观又乱

直接用条件编译,若用条件编译在预编译阶段会选择性编译代码

1.#if #elif #endif

格式:

#if   整型常量表达式1
  //代码段1
#elif 整型常量表达式2
  //代码段2
.... //可以填多个#elif
#else
  //代码段3
#endif
  • 这里的整形常量表达式可以带宏定义,但不能有变量的出现这也是与if else 的区别
  • 执行过程现判断整型常量表达式1是否为真(非0),若为真,预编译阶段编译代码段1,其他代码段不参与编译,若为假判断整型常量表达式2是否为真后面以此类推
  • 与if else一样#else也是与它最近的#if进行匹配
  • 在这里插入图片描述

2.#ifdef

格式:

#ifdef 标识符   
//代码段1
#else
//代码段2
#endif

//其中的#else也可以没有
#ifdef 标识符
//代码段
#endif
  • 当标识符已经被定义过 ( 一般是用#define命令定义),则对代码段1进行编译,否则编译代码段2。
  • #ifdef 标识符 另外一种写法 #if define(标识符) 这两种写法是等价的
    上代码理解
    在这里插入图片描述

2.1.stm32f10x单片机宏定义选择芯片型号

stm32f10x根据容量和性能等有很多型号,在打开keil5软件后在完成新建库函数工程模板后,要选择芯片的型号,才能接下来的开发
我先随便打开一个头文件(stm32f103_rcc.h)看看里面的条件编译
在这里插入图片描述
接下来就是宏定义单片机的型号
该文件是"stm32f10x.h"
在这里插入图片描述
在这里插入图片描述

3.#ifndef

#ifndef 标识符   
//代码段1
#else
//代码段2
#endif

//其中的#else也可以没有
#ifndef 标识符
//代码段
#endif
  • 当标识符未定义 ( 一般是用#define命令定义),则对代码段1进行编译,否则编译代码段2。
  • #ifndef 标识符 另外一种写法 #if !define(标识符) 这两种写法是等价的
  • 与#ifdef 用法基本一致只是定义->就编译 改成未定义->就编译
    上代码,基本用法一样就不赘述啦
    在这里插入图片描述

四.#include文件包含

在预编译阶段,源文件包含了头文件 预处理器先删除这条指令(#include "文件名"或 include<文件名>) ,并用该头文件的内容放到包含它的源文件。 这样一个头文件被包含10次,那就实际被编译10次。

1.文件包含方式

  • 本地文件包含
#include  "文件名"

查找策略:先在源文件所在目录下(自己所建的工程目录)查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准路径(软件的安装目录)查找头文件。
如果找不到就提示编译错误。

  • 库文件包含
#include  <文件名>

查找头文件直接去标准路径(件的安装目录)下去查找, 如果找不到就提示编译错误。
**

此时问题来了,既然本地文件包含两个路径都会去查找可以都用本地文件包含的方式嘛,答案是肯定的,但是速度会变慢,若是一个很大的项目工程,其本地的头文件就非常多,要先找完本地文件才去标准路径下查找岂不是浪费很多时间,所有不建议这样

2.嵌套文件包含

头文件有头文件这样很容易重复包含,所以条件编译必不可少
在这里插入图片描述

2.1.文件重复包含解决办法

1.让我们来看看预编译时候到底是不是将头文件的内容复制到包含头文件的test.c源文件中

  • 首先写好test.c 源文件 与 swap.c 头文件

在这里插入图片描述

  • 在linux底下用预编译命令 gcc - E test.c > test.i 将test.c预编译然后编译好代码重定位到 test.i 文件 打开test.i文件查看结果
    在这里插入图片描述
    在这里插入图片描述
  • 如果test.c多次包含swap.h 头文件会发生什么
    在这里插入图片描述
    在这里插入图片描述

如果头文件重复包含,会导致代码重复被包含(代码冗余),而且有时候会报重复定义的错误在keil5中

  • 解决方法用条件编译#ifndef
    在这里插入图片描述
    此时在进行预编译打开test.i文件查看结果
    在这里插入图片描述
    此时有人就会说我没那么蠢在一个源文件包含三次头文件
    其实在stm32编程中不可避免会重复包含头文件。
    在这里插入图片描述
    条件编译也可以用#pragma once 添加在头文件开头,该头文件也只会编译一次
    在这里插入图片描述

重新进行预编译打开test.i查看
在这里插入图片描述

五.预处理指令汇总

已经敲键盘累了,此图出自百度
在这里插入图片描述
单片机入门已经更新 STM32-什么是寄存器

结论:条件编译与宏定义在嵌入式单片机这种底层开发非常重要一定一定要熟练掌握 本文就到这啦,如果觉得本文对你有所帮助赶快收藏起来叭!!!

评论 32
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

rivencode

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值