const, static与volatile
const
基本作用
const
用于定义常量,如果一个变量被 const
修饰,那么它的值就不能再被改变。
修饰变量
一般变量
写法:
const int n = 5;
// 或者
int const n = 5;
以上两种写法都表示变量 n
的值不能被改变
// 错误,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
所指向的地址可以被改变,但指向地址中的值不能被改变,被称为常量指针:int a = 5; int b = 10; const int *n = &a; // 或者 // int const *n = &a; // 正确,n 所指向的地址可以被改变; n = &b; // 错误,n 所指向地址的值不能被改变 // *n = b; printf("n = %d\n", *n); // 输出 n = 10
-
int *const n = &a;
,表示n
所指向地址中值可以被改变,但指向的地址不能被改变,被称为指针常量:int a = 5; int b = 10; int *const n = &a; // 错误,n 所指向的地址不能被改变; // n = &b; // 正确,n 所指向地址的值可以被改变 *n = b; printf("n = %d\n", *n); // 输出 n = 10
-
const int *const n = &a;
,是以上两种的结合版,表示n
的所指向的地址,以及所指向地址中的值,均不能被改变,被称为指向常量的常指针:int a = 5; const int *const n = &a; // 错误,n 所指向的地址不能被改变; // n = &b; // 错误,n 所指向地址的值不能被改变 // *n = b; printf("n = %d\n", *n); // 输出 n = 10
修饰函数参数与修饰函数返回值
当 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 修饰局部变量时,该变量会被存放在内存的栈区,当该函数执行完毕时,该变量所占用的内存会被回收释放。
static
基本作用
static
可以修饰局部变量,全局变量或者函数,改变其作用域或存储位置。
为变量进行初始化
用 static 修饰的变量,会被自动初始化,对应的内存空间中,会被 0 值填充。
#include <stdio.h>
static int a;
static char c;
static void *p;
int main() {
printf("a = %d\n",a);
printf("c = %d\n",c);
printf("p = %p\n",p);
return 0;
}
// 输出
// a = 0
// c = 0
// p = 00000000
通过改变变量或函数的作用域来隐藏变量或函数
static 修饰在.c 文件中
当一个在 .c
文件中的变量或函数被 static
修饰时,该变量或函数的作用域就被限制在了该 .c
文件中,其他文件无法访问该变量,该变量也无法被 extern
修饰。
// a.h
#ifndef _A_H
#define _A_H
extern void print_a();
#endif //_A_H
// a.c
#include <stdio.h>
#include "a.h"
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 文件中
当一个在 .h
文件中的变量或函数被 static
修饰时,该变量会随着预处理过程中头文件展开时,在引用的不同的 .c
文件中,生成不同的变量。
// a.h
#ifndef _A_H
#define _A_H
static int a = 100;
extern void print_a();
#endif //_A_H
// a.c
#include <stdio.h>
#include "a.h"
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
修饰局部变量时,该局部变量只会初始化一次。在函数运行结束后,该变量不会被销毁。 该变量的值会得到保留,下次继续调用该函数时,会用保留的值继续执行。
static
所修饰的局部变量不在存储在栈区,而是存储在静态全局区。
#include <stdio.h>
void print_x()
{
static int x = 0;
printf("static int x = %d\t", x);
printf("static int &x = %p\n", &x);
++x;
}
int main()
{
print_x();
print_x();
print_x();
return 0;
}
// 输出
// static int x = 0 static int &x = 0x565a3010
// static int x = 1 static int &x = 0x565a3010
// static int x = 2 static int &x = 0x565a3010
static 修饰的全局变量与一般全局变量的区别
static
修饰的全局变量和一般的全局变量都存储在程序的静态全局区,但是其作用域有所不同。
static
修饰的全局变量只在当前 .c
文件中有效,每一个 .c
文件中变量的存储地址不同,因此可以在不同的 .c
文件中以 static
形式来声明同名的全局变量。
在不同的 .c
文件中声明同名的一般全局变量,使用的是相同的存储空间,只能有一个 .c
文件中对此变量进行初始化,此时连接不会出错。
volatile
在嵌入式开发中,使用 volatile
关键字是非常常见的,它通常用于以下几种情况:
- 中断服务程序中的变量:当一个变量在中断服务程序(ISR)中被修改,并且这个变量的值需要被主程序或其他程序检测时,应该使用
volatile
关键字。这是因为中断可能在任何时间发生,而编译器优化可能会使得主程序无法正确地读取到最新的变量值。 - 多任务环境下的共享标志:在多任务环境中,不同任务之间可能需要通过某些共享的标志来进行通信。这些标志应该被声明为
volatile
,以确保每次访问都是直接从内存中读取最新值,而不是从缓存或寄存器中获取可能已经过时的值。 - 存储器映射的硬件寄存器:对于映射到特定硬件寄存器的变量,它们可能在任意时刻由硬件异步修改。
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; // 写入硬件寄存器的值
}