✅作者简介:嵌入式入坑者,与大家一起加油,希望文章能够帮助各位!!!!
📃个人主页:@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-什么是寄存器
结论:条件编译与宏定义在嵌入式单片机这种底层开发非常重要一定一定要熟练掌握
本文就到这啦,如果觉得本文对你有所帮助赶快收藏起来叭!!!