一、const, static与volatile
1、CONST
const的用法(定义和用途):Const就是常量修饰符, const 变量应该在声明的时候就进行初始化,如果在声明 常量的适合没有提供值,则该常量的值是不确定的,且无法修改。
基本作用:const
用于定义常量,如果一个变量被 const
修饰,那么它的值就不能再被改变。
修饰:一般变量
const int n = 5;
// 或者
int const n = 5;
// n的值不能被改变 如n = 6;
修饰:常量指针与指针常量
int a = 5;
const int *n = &a;
// 或者
int const *n = &a;
// 或者
int *const n = &a;
// 或者
const int *const n = &a;
//const int *n = &a; 与 int const *n = &a;,这两种写法是等价的,都表示 n 所指向的地址可以被改变,但指向地址中的值不能被改变,被称为常量指针:
n = &b;//n 所指向的地址可以被改变;
//int *const n = &a;,表示 n 所指向地址中值可以被改变,但指向的地址不能被改变,被称为指针常量
*n = b;//n 所指向地址的值可以被改变
//const int *const n = &a;,是以上两种的结合版,表示 n 的所指向的地址,以及所指向地址中的值,均不能被改变,被称为指向常量的常指针:
// 错误,n 所指向的地址不能被改变;
// n = &b;
// *n = b;
修饰:函数参数与修饰函数返回值
当 const 关键字修饰函数的参数和返回值时,作用同样是防止修改。
修饰:函数参数
在传值调用时,const 修饰参数表示防止修改参数的值。
在传址调用时,const 修饰参数的具体意义根据该参数具体是常量指针还是指针常量或是指向常量的常量指针有所不同。
常量指针作为参数是防止在函数中修改其指向常量的值,例如:char *strcpy(char *dest, const char *src);
假如定义该函数时,修改参数 src 的指向地址中的值,编译器就会报错。指针常量作为参数,是防止在函数中修改其指向的地址,例如:void swap ( int * const p1 , int * const p2 )
假如在定义该函数时,修改了参数 p1 和 p2 所指向的地址,编译器就会报错。指向常量的常量指针作为参数,既是防止在函数中修改其指向的地址,也是防止在函数中修改其指向地址中的值。
修饰:函数返回值
与 const 修饰参数时的作用相同,区别在于表示的是返回值的某些特性不可修改。
修饰:全局变量与修饰局部变量
这里的区别在于,const 修饰局部变量和全局变量时,该变量所存储的区域有所区别
修饰:全局变量
当 cost 修饰全局变量时,该变量会被存放在内存的常量区,而非一般全局变量所在的静态全局区。
修饰:局部变量
当 const 修饰局部变量时,该变量会被存放在内存的栈区,当该函数执行完毕时,该变量所占用的内存会被回收释放。
2、STATIC
const的用法(定义和用途):
在 C 语言中, static作用:“改变生命周期”或者“改变作用域”。有以下特性 :
基本作用:static
可以修饰局部变量,全局变量或者函数,改变其作用域或存储位置。
为变量进行初始化
用 static 修饰的变量,会被自动初始化,对应的内存空间中,会被 0 值填充。
static int a;
static char c;
static void *p;
// a = 0
// c = 0
// p = 00000000
static 修饰在.c 文件中
// a.c
static int a = 100;
void print_a()
{
printf("in a.c, static int a = %d\n", a);
}
//main.c
#include "a.h"
int main()
{
// 错误,静态变量 a 只在 a.c 中生效
// printf("a = %d\n", a);
print_a();
return 0;
}
// 输出
// in a.c, static int a = 100
static 修饰在.h 文件中
// a.h
static int a = 100;
extern void print_a();
// a.c
void print_a()
{
printf("in a.c, static int &a = %p\n", &a);
}
//main.c
#include <stdio.h>
#include "a.h"
int main()
{
printf("in main.c, static int &a = %p\n", &a);
print_a();
return 0;
}
// 输出:
// in main.c, static int &a = 0x56569008
// in a.c, static int &a = 0x5656900c
建议不要在 .h
文件中下定义任何变量,如果是全局的只使用 extern
声明出来,在 .c
下声明全局变量。当 static 修饰局部变量时,该局部变量只会初始化一次。在函数运行结束后,该变量不会被销毁。 该变量的值会得到保留,下次继续调用该函数时,会用保留的值继续执行。
static 所修饰的局部变量不在存储在栈区,而是存储在静态全局区。
3、VOLATILE
volatile的含义:volatile的本意是“易变的”。在程序执行过程中,访问寄存器通常要比访问内存单元快得多。因此,编译器往往会进行优化,以减少对内存的存取次数,但这有可能导致读取到脏数据。
volatile的作用:当使用volatile声明变量时,系统会确保每次都需要从该变量所在的内存地址重新读取数据,即使前面的指令刚刚从该地址读取过数据。换句话说,遇到这个关键字声明的变量时,编译器将不会对访问该变量的代码进行优化,从而保证了对该特殊地址的稳定访问。如果不使用volatile,编译器则可能会对相关的代码进行优化
3.1 在C语言中,volatile
关键字是一种类型修饰符,用于告诉编译器不应对该变量的访问进行优化,以确保每次访问都是从内存中直接读取数据,而不是从缓存或寄存器中读取。
下面是一个简单的例子,用于验证volatile
关键字的作用。在这个例子中,我们将使用volatile
关键字来防止编译器优化掉一个看似无用的循环,该循环实际上是在等待一个由外部事件(如中断)更改的变量。
#include <stdio.h>
#include <unistd.h> // 包含usleep函数
volatile int flag = 0; // 使用volatile修饰符
void waitForFlag() {
while (!flag) {
// 空循环,等待flag被设置为1
}
printf("Flag is set!\n");
}
void setFlag() {
flag = 1; // 设置flag为1,这将导致waitForFlag函数中的循环退出
}
int main() {
printf("Waiting for flag to be set...\n");
// 在另一个线程或中断服务例程中,setFlag函数可能会被调用
// 但为了简化,我们在这里使用sleep来模拟延迟
usleep(2000000); // 等待2秒
setFlag(); // 设置flag,这将导致waitForFlag中的循环退出
waitForFlag(); // 实际上,由于setFlag的调用,这个函数会立即返回
return 0;
}
在这个例子中,flag
变量被声明为volatile
,这意味着编译器在编译时不会优化掉waitForFlag
函数中的循环,即使看起来它像是在做无用功。如果去掉volatile
修饰符,编译器可能会优化掉这个循环,因为它认为flag
的值在循环内部没有改变,这会导致程序永远挂起,等待一个永远不会发生的条件。通过使用volatile
,我们确保了每次循环迭代时都会从内存中重新读取flag
的值,从而能够正确响应外部事件。
3.2 在嵌入式开发中,使用 volatile
关键字是非常常见的,它常用于以下几种情况:
中断服务程序中的变量:当一个变量在中断服务程序(ISR)中被修改,并且这个变量的值需要被主程序或其他程序检测时,应该使用 volatile 关键字。这是因为中断可能在任何时间发生,而编译器优化可能会使得主程序无法正确地读取到最新的变量值。
多任务环境下的共享标志:在多任务环境中,不同任务之间可能需要通过某些共享的标志来进行通信。这些标志应该被声明为 volatile,以确保每次访问都是直接从内存中读取最新值,而不是从缓存或寄存器中获取可能已经过时的值。
存储器映射的硬件寄存器:对于映射到特定硬件寄存器的变量,它们可能在任意时刻由硬件异步修改。volatile 可以确保对这类变量的每次读写都会直接与硬件寄存器交互,防止编译器进行不恰当的优化。这样可以确保每次读写操作都不会被编译器优化掉,从而保证程序能够正确地与硬件设备通信。
// 1. 中断服务程序中修改的供其它程序检测的变量需要加volatile
volatile int flag = 0; // 定义一个volatile变量flag
void interrupt_service_routine() {
flag = 1; // 在中断服务程序中修改flag的值
}
int main() {
// 主程序检测flag的值
if (flag) {
printf("Interrupt occurred!\n");
}
return 0;
}
// 2. 多任务环境下各任务间共享的标志应该加volatile
volatile int shared_flag = 0; // 定义一个volatile变量shared_flag
void task1() {
// 任务1中修改shared_flag的值
shared_flag = 1;
}
void task2() {
// 任务2中检测shared_flag的值
if (shared_flag) {
printf("Task 2 detected the change in shared_flag!\n");
}
}
// 3. 存储器映射的硬件寄存器通常也要加volatile说明,因为每次对它的读写都可能由不同意义
volatile uint32_t *hardware_register = (uint32_t *)0x40000000; // 存储器映射的硬件寄存器地址
void read_register() {
uint32_t value = *hardware_register; // 读取硬件寄存器的值
printf("Hardware register value: %u\n", value);
}
void write_register(uint32_t value) {
*hardware_register = value; // 写入硬件寄存器的值
}
此外,volatile 关键字的作用是防止编译器对于被修饰的变量进行错误的优化。由于嵌入式系统通常涉及到实时性要求很高的操作,如中断处理和硬件控制,因此确保变量的每次访问都是最新的值至关重要。
在嵌入式开发中,正确使用 volatile 关键字对于确保系统的可靠性和稳定性是非常重要的。它帮助开发者避免因编译器优化而导致的潜在问题,确保程序能够准确地反映硬件状态和执行预期的操作。
二、总结
const
- 定义和用途:
const
是常量修饰符,用于声明一个变量的值在初始化之后不能被修改。 - 主要用法:
- 修饰变量:定义时就初始化,之后不能更改。
- 修饰函数形参:表示在函数内不能修改该参数的值。
- 修饰类成员函数:表明该函数不会修改类的任何成员变量(常成员函数)。
- 修饰指针:区分常量指针(指针指向的内容不可变)和指针常量(指针本身不可变)。
- 修饰函数返回值:表明返回值的某些特性不可修改(如返回指向常量的指针)。
- 存储位置:
- 修饰局部变量时,存储在栈区。
- 修饰全局变量时,存储在常量区(与普通全局变量存储于静态全局区不同)。
static
- 定义和用途:
static
用于改变变量的生命周期或作用域,或者改变函数的连接属性。 - 主要用法:
- 修饰局部变量:延长局部变量的生命周期,使其在函数调用结束后依然保留在内存中。
- 修饰全局变量:限制全局变量的作用域,使其只在定义它的文件内部可见。
- 修饰函数:限制函数的连接属性,使其只在定义它的文件内部可调用。
- 初始化:被
static
修饰的变量会自动初始化为0(对于数值类型和指针)。 - 存储位置:无论修饰局部变量还是全局变量,
static
变量都存储在静态存储区。
volatile
- 定义和用途:
volatile
告诉编译器该变量的值可能会以编译器难以预测的方式被改变,因此每次访问该变量时都必须直接从其内存地址中读取,而不能使用缓存中的值。 - 主要用法:
- 用于中断服务程序中被修改的变量,确保主程序能读取到最新值。
- 用于多任务环境中的共享标志,确保各任务间通信的可靠性。
- 用于存储器映射的硬件寄存器,确保每次读写都直接与硬件交互。
- 作用:防止编译器对变量的访问进行优化,确保程序的实时性和准确性,特别是在嵌入式系统和硬件交互密集的场景中。
综上const, static与volatile,分别用于确保数据的不可变性(const
)、控制变量的生命周期和可见性(static
),以及确保变量访问的实时性和准确性(volatile
)。