本文引注:
https://mp.weixin.qq.com/s/YCbHFZ0SQmp9COEBD5kQJQ
1、volatile
volatile修饰表示变量是易变的,编译器中的优化器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份,有效的防止编译器自动优化,从而与软件设计相符合。
中断服务与主程序共享变量:
//volatile uint8_t flag=1;
uint8_t flag=1;
void test(void)
{
while(flag)
{
//do something
}
}
//interrupt service routine
void isr_test(void)
{
flag=0;
}
如果没使用volatile定义flag,可能在优化后test陷入死循环,因为test里使用的flag并没修改它,开启优化后,编译器可能会固定从某个内存取值。例如:
for(int i=0; i<100000; i++);
//对比
for(volatile int i=0; i<100000; i++);
前者可能被优化掉,虽然编码本意是需要执行操作延时,但编译器认为代码无意义。
总的来说,volatile是告知编译器,不管代码如何,必须保留,而且使用时需要重新从内存读取更新,不能使用先前读取的缓存,一般在驱动代码中使用较多。
第一个循环中的变量 i 是一个普通的整数变量,而第二个循环中的变量 i 是一个被标记为 volatile 的整数变量。volatile 关键字的使用可以影响编译器对变量的优化行为,确保每次访问变量时都从内存中读取最新的值。这在多线程或并发编程中特别有用。
举例:
#include <iostream>
int main() {
// 第一个循环
for (int i = 0; i < 5; i++);
std::cout << "First loop: " << i << std::endl;
// 第二个循环
for (volatile int i = 0; i < 5; i++);
std::cout << "Second loop: " << i << std::endl;
return 0;
}
2、const
const是恒定不变的意思,其修饰的各种数据类似只读效果。
1、 修饰变量
采用const修饰变量,即变量声明为只读,保护变量值以防被修改。例如
const int i = 1;
上面这个例子表明,变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10;属于错误操作。
特别说明,定义变量的同时进行初始化,写成int const i=1,是正确的。
2、 修饰数组
C语言中const还可以修饰数组,举例如下:
const int array[5] = {1,2,3,4,5};
array[0] = array[0]+1; //错误,array是只读的,禁止修改
数组元素与变量类似,具有只读属性,不能被更改;一旦更改,编译时就会报错。
使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。
3、 修饰指针
C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。举例如下:
int i = 1;
int j = 2;
const int *p1 = &i;
int* const p2 = &j;
上面定义了两个指针p1和p2,区别是const后面是指针本身还是指向的内容。
在定义1中const限定的是* p1,即其指向空间的值不可改变,若改变其指向空间的值如* p1=10,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。
在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但* p2,即其所指向空间的值可以改变,如* p2=20是没有问题的,程序正常执行。
4、 修饰函数参数
const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改。所限定的函数参数可以是普通变量,也可以是指针变量。例如:
void fun(const int i)
{
……
i++; //对i的值进行了修改,程序报错
}
常用的函数如strlen
size_t strlen(const char *string);
const在库函数中使用非常普遍,是一种自我保护的安全编码思维。
3、struct与union
对于struct 结构体和union共联体在嵌入式领域是使用得非常频繁的,一些可编程芯片提供的寄存器库都是采用结构体和共联体结合的方式来提供给软件人员进行开发,同时在平时的编码过程中这两个数据类型的灵活应用也能够实现代码更好的封装与简化。
如下面的简单示例,就可以非常灵活的访问Val中的bit位。
typedef union
{
BYTE Val;
struct __packed
{
BYTE b0:1;
BYTE b1:1;
BYTE b2:1;
BYTE b3:1;
BYTE b4:1;
BYTE b5:1;
BYTE b6:1;
BYTE b7:1;
} bits;
}BYTE_VAL, BYTE_BITS;
其中:1表示按位操作。不只是位-字节可以,单字节与多字节也可以简化拼接。
这段代码定义了一个联合体(union)和一个结构体(struct)。联合体的名称是BYTE_VAL,结构体的名称是BYTE_BITS。它们都用来表示一个字节(BYTE)的值。
联合体中包含了两个成员:Val和bits。Val是一个字节(BYTE)的整体值,而bits是一个结构体类型的成员。
结构体(struct)被定义为__packed,这表示其成员按照最小内存对齐方式进行排列,不会有额外的填充字节。
结构体中的成员是按位(bit)进行定义的。它们分别是b0、b1、b2、b3、b4、b5、b6和b7,每个成员占据一个位(bit)的空间。
通过使用这个联合体和结构体,可以以两种不同的方式访问和操作一个字节(BYTE)的值:作为整体的值(Val),或者按位访问和操作(bits)。
#include "stdio.h"
typedef struct
{
union
{
struct
{
unsigned char low;
unsigned char high;
};
unsigned short result;
};
}test_t;
int main(int argc, char *argv[])
{
test_t hello;
hello.high=0x12;
hello.low=0x34;
printf("result=%04X\r\n",hello.result);//输出 result=1234
return 0;
}
运行输出 result=1234 (win7系统下QT开发环境),原本需要 (high<<8)|low 运算,可以简化为共用体类型自动完成,但必须注意平台的字节顺序,属于大端还是小端模式。
注:
在大端模式中,数据的高位字节(Most Significant Byte,MSB)存储在内存的低地址处,而低位字节(LeastSignificantByte,LSB)存储在内存的高地址处。在小端模式中,数据的高位字节(MSB)存储在内存的高地址处,而低位字节(LSB)存储在内存的低地址处。
在应用层面,如果明确某个数据可能存在两种可能,而且两种结果不会同时存在,也可以使用结构体与共用体组合的方式,确保模块对外接口统一。
例如移动通信模块,使用数据结构保存其基站信息,因为制式不同,模块可能工作在2G-GSM,也可能在4G-Cat1,为保证上层读取基站信息接口唯一,使用共用体就非常合适,否则需定义两套接口。
4、预定义标识符
一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。
部分标识:
__FILE__ //表示编译的源文件名
__LINE__ //表示当前文件的行号
__FUNCTION__ //表示函数名
__DATE__ //表示编译日期
__TIME__ //表示编译时间
使用范例:
printf("file:%s,line:%d,date:%s,time:%s",__FILE__,__LINE__,__DATE__,__TIME__);
这些比较常见,主要用于日志分析、版本记录,便于调试。
5、#与##
#:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。
##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。
#include "stdio.h"
#define TO_STR(s) #s
#define COMB(str1,str2) str1##str2
int main(int argc, char *argv[])
{
int UART0= 115200;
printf("UART0=%d\n", COMB(UART, 0));//字符串合并为变量UART0
printf("%s\n", TO_STR(3.14));//将数字变成字符串
return 0;
}
6、void 与 void*
void表示的是无类型,不能声明变量或常量,但是可以把指针定义为void类型,如void* ptr。void* 指针可以指向任意类型的数据,在C语言指针操作中,任意类型的数据地址都可转为void* 指针。因为指针本质上都是unsigned int。
常用的内存块操作库函数:
void * memcpy( void *dest, const void *src, size_t len );
void * memset( void *buffer, int c, size_t num);
注:函数返回 void* 类型的指针,指向目标内存区域的起始位置。
数据指针为void* 类型,对传入任意类型数据的指针都可以操作。另外其中memcpy第二个参数,const现在也如前文所述,拷贝时对传入的原数据内容禁止修改。
特殊说明,指针是不能使用sizeof求内容大小的,在ARM系统固定为int 4字节。对于函数无输入参数的,也尽量加上void,如
void fun(void);
7、weak
一般简化定义
#define _WEAK __attribute__((weak))
注:__attribute__() 是GCC编译器的一个特性,用于指定给变量、函数、类型等添加一些属性或约束。
函数名称前面加上__WEAK属性修饰符称为“弱函数”,类似C++的虚函数。链接时优先链接为非weak定义的函数,如果找不到则再链接带weak函数。
_WEAK void fun(void)
{
//do this
}
//不在同一个.c,两同名函数不能在同一个文件
void fun(void)
{
//do that
}
这种自动选择的机制,在代码移植和多模块配合工作的场景下应用较多。例如前期移植代码,需要调用某个接口fun,但当前该接口不存在或者未移植完整使用,可以使用weak关键字定义为空函数先保证编译正常。后续移植完成实现了fun,即软件中有2个fun函数没有任何错误,编译器自动会识别使用后者。当然也粗暴的#if 0屏蔽对fun的调用,但要确保后续记得放开。