本文引注:
1、volatile
volatile修饰表示变量是易变的,编译器中的优化器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份,有效的防止编译器自动优化,从而与软件设计相符合。
中断服务与主程序共享变量:
c
//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并没修改它,开启优化后,编译器可能会固定从某个内存取值。例如:
c
for(int i=0; i<100000; i++);
//对比
for(volatile int i=0; i<100000; i++);
前者可能被优化掉,虽然编码本意是需要执行操作延时,但编译器认为代码无意义。
总的来说,volatile是告知编译器,不管代码如何,必须保留,而且使用时需要重新从内存读取更新,不能使用先前读取的缓存,一般在驱动代码中使用较多。
第一个循环中的变量 i 是一个普通的整数变量,而第二个循环中的变量 i 是一个被标记为 volatile 的整数变量。volatile 关键字的使用可以影响编译器对变量的优化行为,确保每次访问变量时都从内存中读取最新的值。这在多线程或并发编程中特别有用。
举例:
c
#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修饰变量,即变量声明为只读,保护变量值以防被修改。例如
c
const int i = 1;
上面这个例子表明,变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10;属于错误操作。
特别说明,定义变量的同时进行初始化,写成int const i=1,是正确的。
2、 修饰数组
C语言中const还可以修饰数组,举例如下:
c
const int array[5] = {1,2,3,4,5};
array[0] = array[0]+1; //错误,array是只读的,禁止修改
数组元素与变量类似,具有只读属性,不能被更改;一旦更改,编译时就会报错。
使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。
3、 修饰指针
C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。举例如下:
c
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关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改。所限定的函数参数可以是普通变量,也可以是指针变量。例如:
c
void fun(const int i)
{
......
i++; //对i的值进行了修改,程序报错
}
常用的函数如strlen
c
size_t strlen(const char *string);
const在库函数中使用非常普遍,是一种自我保护的安全编码思维。
3、struct与union
对于struct 结构体和union共联体在嵌入式领域是使用得非常频繁的,一些可编程芯片提供的寄存器库都是采用结构体和共联体结合的方式来提供给软件人员进行开发,同时在平时的编码过程中这两个数据类型的灵活应用也能够实现代码更好的封装与简化。
如下面的简单示例,就可以非常灵活的访问Val中的bit位。
c
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)。
c
#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,为保证上层读取基站信息接口唯一,使用共用体就非常合适,否则需定义两套接口。