无情的搬砖机器
一、关键字
1.1 struct
该关键字看另外博客:结构体
1.2 union
该关键字看另外博客:大小端和联合体
1.3 switch
switch注意事项:
- case语句中的值只能是整型或字符型的常量或常量表达式(想想字符型数据在内存里是怎么存的)
- default语句只能用于处理真正的默认情况
- 每个case语句分支必须有break,否则分支重叠
Ps:switch可用于冗长的程序逻辑进程,每一个case代表一个进程,只有其中进程执行完,才对switch(Temp)判断的值进行改动(例如:Temp++);然后进行下一个流程,方便梳理逻辑。
1.4 enum
相比 #define
标识符常量必须由程序员手工赋值。enum
使程序更容易维护,因为枚举常量是由编译程序自动生成的。
Ps:可搭配switch
作为case
值方便扩展
1.5 static & extern
1.5.1 static
1 | static uint32_t Shatang; |
常见用法:屏蔽其他源文件对本源文件static修饰的变量
- 修饰局部变量:用来延长变量生命周期,防止再次调用函数时需要再初始化修饰的变量,保留原有数据(常见用法)
- 修饰全局变量:用static对全局变量进行修饰改变了其作用域的范围,防止其他源文件对本源文件的变量调用
static 全局变量
:、- static全局变量只初始化一次,防止在其他文件单元中被引用;
- 只在定义该变量的源文件内有效,在同一源程序的其它源文件中不能使用它。
static 局部变量
:- static局部变量只被初始化一次,下一次依据上一次结果值;
- 限制了它的使用范围
static 函数
:- static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为(static)内部函数,内部函数应该在当前源文件中说明和定义。
- static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝
Ps:指针是变量
总结:从上面用法描述来看,static不会在 .h文件修饰变量(.c文件都调用不了你,要你何用),正常情况下都会在 .c文件修饰变量、函数定义(声明往往在.h文件),防止其他 .c文件调用;
1.5.2 extern
1 | extern uint32_t Shatang; |
常见用法:用在变量或者函数的声明前,用来说明“此变量/函数”是在别处定义的,要在此处引用。
问题描述:因为要使用CAN进行数据传输,因此在主程序”test.c”中采用#include “can.h”,调用”can.h”中的函数和变量。结果编译后出现许多
Error L6200E: symbol xxx multiply defined ...
原因:因为在
"can.h"
中定义(不仅声明还进行定义)了许多变量,"can.c"
文件中采用#include "can.h"
,调用"can.h"
中的变量定义;在主函数"test.c"
中也采用#include "can.h"
,调用"can.h"
中的变量定义,导致”can.h”中的变量被重复定义。解决方法:首先,不应该在”can.h”中定义”can.c”中使用的变量(正常情况都是这样),而是在”can.c”中定义所需的变量。在”can.h”中把变量先进行
extern uint8_t Shatang;
声明,然后 在”can.c”文件再进行定义;此时,主程序”test.c”中将所调用”can.h”中的变量(声明) 将会去”can.c”查找其定义,因此不会存在变量重复定义,问题得到解决。结论:extern往往用于 .h文件变量声明,然后在相应的 .c文件进行定义,这样防止 .h文件被多个 .c文件调用时产生重复定义的错误
特殊案例:往往图片、字库是按照 .h文件变量定义的,没有.c文件;当你的(lcd.c)文件功能需要调用 某些图片数组.h文件,不能在自身(lcd.h)文件调用 image.h文件,当其他 .c文件调用lcd.h
文件,就会产生上面同样的重复定义;因此对于(只有) .h文件进行变量定义,往往采用直接在(只能在单个) lcd.c 文件调用
1.5.3 static 和 extern 的关系
static
表示是本文件内的变量(在函数中的是静态变量),extern
表示是其他文件定义的变量,显然两者是矛盾的;只有全局变量并且没有被static声明的变量才能声明为extern。
1.6 const & 指针
常见用法:const 修饰变量为只读变量(不是常量!!!)。
- 为了防止传递的函数参数不被修改,在调用函数的形参中用const关键字
- const可以用来创建数组常量、指针常量、指向常量的指针等
如何辨别const是修饰什么?方法:无括号的情况下,可以先忽略类型名。
const
int *p; //const修饰*p,p是指针,*p是指针指向的对象,不可变int const *p; //const修饰*p,p是指针,*p是指针指向的对象,不可变int *const p; //const修饰p,p不可变,p指向的对象可变const
int *const p; //前一个const修饰*p,后一个const修饰p,指针p和p指向的对象都不可变
这里或许就有人要问:const int *const p;
这种都不可变的有什么用?只不过是教语法时用到的花里胡哨。
诚然,指针p和p指向的对象都不可变,但是p指向的对象还可以是个指针啊!因此 const int *const p;
往往是用于二级指针。思路反过来,当别人代码这样写的时候:你就应该明白,这里是二级指针。
1 | //特殊情况 |
以上的三种情况是等效的。对于const (int *)
,因为int *
是一个整体,相当于一个类型(如 char
),因此,这个const
是限定指针不可变。
Ps:只读变量 和 常量 是有区别的,内存存放区域都不同。详情看内存分配一章。
1.7 void
void 常与 (函数)指针 搭配使用:
- C语言规定只有相同类型的指针才可以相互赋值
- void*指针作为左值用于“接收”任意类型的指针
- void指针作为右值赋值给其他指针时需要强制类型转换
1.8 volatile
volatile用于告诉编译器必须每次去内存中取变量值:
- volatile主要修饰可能被多个线程访问的变量
- volatile也可以修饰可能被(硬件)未知因数更改的变量
Ps:volatile往往只用于最底层驱动(防止数值被修改,例如通信协议);最好不要用于封装其他高级应用层的代码,用处不大,而且不利于其他人对你的程序进行移植拓展(极端情况下可能取值问题出错)。
举例如下:
1 | int square(volatile int *ptr) |
由于*ptr 的值可能被意想不到地该变,这段代码可能返不是你所期望的平方值!
1.9 sizeof
1 | sizeof(int) //计算(int类型)内存大小 |
常见用法:计算内存大小。
在嵌入式c代码中,用于计算数组等大小;它往往会和 struct、#define、#pragma pack 连用,用来计算结构体的偏移量,方便调用结构体成员。
1 | /* |
PRINT_PARAM_NUM
可用于实现结构体成员的(for循环)赋值;GET_PRINTF_PARAM_OFFSET(member)
计算出来的结构体偏移量可方便 结构体扩展
1.10 typedef
typedef用于给一个已经存在的数据类型重命名。
下边是一个能够说明typedef的语法例子:
1 | // simple typedef |
typedef 定义结构体类型
1
2
3
4
5
6
7
8typedef struct
{
int iNum;
long lLength;
}MyStruct;
MyStruct stu1;
MyStruct *stu1;
MyStruct class[50];typedef 定义数组类型
1
2
3
4
5
6
7
8typedef int(AINT5)[5];
typedef float(AFLOAT10)[10];
typedef char(ACHAR9)[9];
AINT5 a1;
AFLOAT10* pf = &fArray;
ACHAR9 cArray;
AINT5 i = {0,1,2,3,4};typedef 定义(常见)指针类型
1
2
3typedef char* pstr;
pstr p;typedef 定义函数指针类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using namespace std;
typedef void(*pFunc)();
void myFunc()
{
cout << "Hello World!" << endl;
}
int main()
{
pFunc func;
unc = &myFunc;
func();
return 0;
}
进阶:typedef 定义函数指针类型后,可以再用 该函数指针类型 定义 数组/结构体成员,让 每个元素 或 结构体成员 为函数指针。
综合例程,如下:
1 |
|
1.11 enum & const & typedef & #define
#define
:标识符常量必须由程序员手工赋值enum
:枚举常量是由编译程序自动生成的,使程序更容易维护- 这两个都是可以用来定义常量
const
:修饰 只读变量,不会变的变量。从内存分配区上来看,就已经不一样了。typedef
:跟#define
极其相似,但实际有很大区别
举例区分
typedef
&#define
例子1. 以下p1,p2,p3,p4有什么区别?
1 | //第一段 |
答案:p4是一个char类型
例子2. 以下是 指针为只读变量 还是 指针指向的内容不可变?
1 | typedef char* pstr; |
答案: const pstr p;<==> char* const p;
错误的原因在于将 typedef 当做文本扩展了(#define 才是真正的文本扩展!)。声明 const pstring 时,const修饰的是pstring的类型,这是一个指针。因此,该声明语句应该是把cstr定义为指向string 类型对象的const指针。
1 | typedef char* pstr; |
以上的四种情况是等效的。
二、符号
2.1 单引号、双引号
- 单引号引起来的都是字符常量
- 双引号引起来的都是字符串常量
举例:1 ,’1’ , “1”
第1个是整数常量,32位系统下占4字节;
第2个是字符常量,占1字节;
第3个是字符串常量,占2字节。
字符在内存里是以 ASCII码 存储的,所以字符常量还可以与整形常量或变量进行运算,如:'A' + 1
。
2.2 逻辑运算符使用分析
案例分析:
1 |
|
输出的结果为1,0。
2.3 逻辑运算符 & 按位运算符
举例:以下例子函数的DATAx
为宏定义stm32各个IO管脚,封装函数实现8pin并口数据输出;以下函数均能在C编译器编译通过
1 | //error |
当调用该函数时,错误案例是无法实现相应的输出的
1 | DATA_Process(0xff); |
提示:因为是 按位& ,所以得出来的值 并非 1 或0 ;只有 && 条件判断 才是得出来的值 1或0。
2.4 优先级
三、预处理
预处理是在编译环节中最早开始执行的,并且后面的代码(因条件变化)都不会影响到任何预处理的一些操作(例如:#define)
3.1 宏定义
1 |
举例如下
1 |
嵌入式系统编程,要求程序员能够利用C语言访问固定的内存地址。
- 既然是个地址,那么按照C语言的语法规则,这个表示地址的量应该是指针类型。
- 所以,知道要访问的内存地址后,比如0x5F,第一步是要把它强制转换为指针类型
(unsigned char*)0x5F
,AVR的SREG是八位寄存器,所以0x5F强制转换为指向unsigned char
类型。 - volatile(可变的)这个关键字说明这变量可能会被意想不到地改变,这样编译器就不会去假设这个变量的值了。这种“意想不到地改变”,不是由程序去改变,而是由硬件去改变——意想不到。
- 所以,知道要访问的内存地址后,比如0x5F,第一步是要把它强制转换为指针类型
- 第二步,对指针变量解引用,就能操作指针所指向的地址的内容了
*(volatile unsigned char*)0x5F
- 第三步,小心地把#define宏中的参数用括号括起来,这是一个很好的习惯,所以
#define SREG ((volatile unsigned char)0x5F)
类似的,如果使用一个32位处理器,要对一个32位的内存地址进行访问,可以这样定义:
1 |
然后就可以用C语言对这个内存地址进行读写操作了.
读:tmp = RAM_ADDR;
写:RAM_ADDR = 0x55;
3.2 条件编译
1 |
条件编译的意义,实际工程中条件编译主要用于以下情况:
- 不同的产品线公用一份代码
- 区分编译产品的调试版和发布版
- 方便变动程序,例如不同的软件驱动方式
举例:
1 |
四、函数的设计技巧
- 参数名要能够体现参数意义
- 如果说传递的参数为指针,且仅仅作输入参数用,则应在类型前加const,以防止该指针在函数体内被恶意修改
- 不要省略返回值的类型,如果函数没有返回值,那么应当声明为void
- 在函数体的“入口处”,对参数的有效性进行检查,对指针的检查尤为重要
- 语句不可返回指向“栈内存”的“指针”,因为该内存会在函数结束后销毁
- 相同的输入应当产生相同的输出,尽量避免函数带有“记忆”功能少用static
- 避免函数有太多的参数,参数个数应当控制在4个以内
- 有时候函数不需要返回值,但是增加灵活性,可以附加返回值