|
pqjd2o1bjtm64011336636.gif
! v. `) k% C6 A$ i
点击上方蓝色字体,关注我们' @4 N5 F+ J$ Y
- w' Z* g! d% q/ ?: F1
' i. Q% E7 E* E4 e! [* Y3 Fvolatile关键字
( Y! l1 z( `7 S) d, i% lvolatile是一个特征修饰符,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。4 a6 W, M9 h$ ~7 ?
' ^- V% u; _/ @$ N, D3 _8 p常用场景:中断服务与主程序共享变量。示例代码如下:
' k) W5 b! `7 C4 T" f2 O
8 o/ H6 @$ G% Z( ?7 J& C; S//volatile uint8_t flag=1;uint8_t flag=1;
V& k, D1 R' B X! M/ D8 T7 Vvoid test(void){ while(flag) { //do something }}
4 R6 e- I8 y: m( h//interrupt service routinevoid isr_test(void){ flag=0;}& i: v k2 E' I
如果没使用volatile定义flag,可能在优化后test陷入死循环,因为test里使用的flag并没修改它,开启优化后,编译器可能会固定从某个内存取值。
3 F0 O* j; P7 p& f* U8 e! ?$ ^( e6 M+ n1 |+ w7 I, [" p
2
5 L: R' y# B4 Q/ \6 k9 H sconst关键字# M- c; B! c5 Q* f, R$ B V8 n) \. U
const 是 constant 的缩写,意思是“恒定不变的”,它是定义常变量的关键字。
9 @/ t' b* k0 G. F( [通常有4种用法。( U5 I$ d2 W' d, U6 _
- y( p( Q1 V3 m; g( f% g/ j+ S% a, x9 ^( @
1、修饰变量
* E0 n) ]2 F7 b' t3 b& P8 f( |& Y/ ?& w/ K4 ~8 N8 U
采用const修饰变量,即变量声明为只读,保护变量值以防被修改。
) O, B z' _9 n4 M6 L
* P' }3 x9 {6 {$ _$ `const int i = 1;或者int const i=1;
+ w% @4 k6 t: b" H变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10,属于错误操作。
, w8 R; z( `, w" A) e! r, O" y" F3 W( p: u+ e
3 |9 Q8 s5 @2 q% p& e8 I8 w4 k& ^2、修饰数组
2 b- y2 u) i( F, @ s3 j5 k" z% j2 N# [7 q/ r4 D6 @
数组元素与变量类似,具有只读属性,不能被更改,一旦更改,编译时就会报错。
' i7 g! V* ?6 E2 `1 i+ e1 R! ?9 n1 D8 \( D) T6 p6 I. ^! c
const int array[5] = {1,2,3,4,5};array[0] = array[0]+1; //错误,array是只读的,禁止修改! r9 ^! U" j) w( p
使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。$ g$ v3 g1 b( e- C+ }
# L5 O) g7 f; q' Z
$ j3 o. ^, [0 l! V% l* i3、修饰指针1 X* M, [3 O0 O% v; a
* t/ d0 c$ s* Y; kC语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。( u% O" c a9 N
% t3 y7 v* Z" N/ p( V+ ?# f, ?
int i = 1;int j = 2;- D! R4 q s! [
const int *p1 = &i;int* const p2 = &j;
4 M. Q W }% l4 J5 V上面定义了两个指针p1和p2,区别是const后面是指针本身还是指向的内容。
$ r- u; q' J% ~/ y5 g
' K' A8 P5 ~0 r" U' [& b; `5 [在定义1中const限定的是*p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=10,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。
5 `/ V; T3 O4 _$ ]% p. V7 j$ G! r* ]: w
在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如*p2=20是没有问题的,程序正常执行。( B- R4 ~7 P: p5 q* ]
2 h V" l5 b/ _4 k
$ W! }3 P( s! j4 F* q4、 修饰函数参数" z- Q8 S2 P- B! U) q6 W4 {
const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改,所限定的函数参数可以是普通变量,也可以是指针变量。' I2 x H* P2 s9 j4 U
, _& A2 N! U% y6 J' ? wvoid fun(const int i){ …… i++; //对i的值进行了修改,程序报错}
3 S* m- c/ I+ s常用的函数如strlen。
6 E* A, Y& j1 g1 T3 c0 t$ a0 m7 P3 r3 r% I" E
size_t strlen(const char *string);0 O6 g( k4 `( [6 _/ ^
const在库函数中使用非常普遍,是一种自我保护的安全编码思维。( h y3 \) }5 s: t% _9 l! e
3% S: a' N9 }* A5 m9 Q% P6 p
static关键字
: U' m, h6 m$ r- W. R1、static修饰全局变量,该变量只在本文件内被访问,不能在其他文件被直接访问。
2 ?$ }3 j8 P! W U6 R$ U2 L8 ~- Z( I
2、static修饰函数,该函数只能在本文件内被访问,不能被其他文件访问。但是可以通过嵌套的方式调用,变相的封装的表现。
2 c- c+ ` ^6 G _+ u8 k; I5 K' p9 a6 N/ i
3、static修饰局部变量,更改该局部变量的生命周期。
$ |3 a0 K: J6 \8 d# o: ^3 A生命周期:将临时变量的生命周期变成全局变量的生命周期。作用域不变:作用域仍然是在本代码块内。3 |" T; `$ ~5 b
5 P$ }2 s% c% E3 I4
Q2 h' @* t8 l v9 z! V; _( L8 Vstruct与union. e' S# q* [' F. B6 }
可以使用struct结构体来存放一组不同类型的数据。3 Z4 C1 f( f6 r+ m& _- H
- W0 C" L4 W# [struct 结构体名{ 结构体所包含的变量或数组};
I" D, x5 B% A' R% _: S结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员,通常我们使用结构体定义和解析协议,如下所示:; z9 W$ L: R$ c5 C4 W
3 }8 j/ j7 G, S4 D2 ^
// WiFi接收数据帧,控制切换模式#pragma pack(1)typedef struct receive_data_mode_t{ uint8_t device_head; // 数据帧头:0XA0+功能码(FUNCTION_ID3),A款产品智能插座 uint16_t device_len; // 数据包总长度 uint16_t device_id; // 节点ID 0X0001~0XFFFE char software_version[15]; // 软件版本 SMART_SW_A1_1.0 A款产品软件1.0版本 char hardware_version[15]; // 硬件版本 SMART_HW_A1_1.0 A款产品硬件1.0版本 uint8_t switch_mode; // 切换模式 0:运行模式,1:配置模式,2:节点升级,3:节点重启 uint16_t crc; // 校验位}ReceiveData_Mode_t;#pragma pack()
- v7 G8 A" E8 \; X! kunion共用体关键字,定义union下面的成员变量共享一块内存,每一个成员在任一时刻有且只有一个成员使用此块内存。; [- D, g5 w$ s* s7 ^; W' [
' p5 Z# }- ]8 Q* F$ i
union 共用体名{ 成员列表};4 j& l) P% |; p
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。2 E. Y- Q' b K; g8 y
通常使用共用体做一些标志位操作,例如以下示例,可以非常灵活的访问Val中的bit位。
7 [# K5 z" x) A& L/ g4 E6 G" e7 O9 D# Z+ d# B9 @' s" G# S
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;
3 {+ ]0 E2 z9 B N或者使用共用体实现单字节与多字节的转化和拼接,如下所示:
7 S7 z7 ]! N; E" i* }. j/ L& n* D# @: B- m( F3 E" \* U" M
#include "stdio.h"7 N8 ?) D& e% P. t, I
typedef struct{ union { struct { unsigned char low; unsigned char high; }; unsigned short result; };}test_t;* B9 E2 N' T) y6 {2 @5 W
int main(int argc, char *argv[]){ test_t hello;
. Z2 Y: I1 \1 H& H7 q, o0 ~ hello.high=0x12; hello.low=0x34;' ^- e* P4 i4 j' |! N) X
printf("result=%04X\r
/ ~8 e9 O( e" o* ^# r",hello.result);//输出 result=1234
* U& ~; Q4 W) R+ q, v" t- c4 B return 0;}, N& L! u; o6 R+ c+ _
8 m- N* F3 k a& Z% t5- \2 f* \% b" E$ B o3 C
预定义标识符
4 q1 `! | d& y* w2 L一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。' n. H$ F" y3 \) C5 C0 ^
+ G- c0 u' [: C- o* `2 V& A常用的预定义标识符如下所示:
X' B5 o' o& a- E
( k- m. g! |4 Z) i__FILE__ //表示编译的源文件名__LINE__ //表示当前文件的行号__FUNCTION__ //表示函数名__DATE__ //表示编译日期__TIME__ //表示编译时间
6 q# d y; k: O1 f5 @1 ^7 A在Debug打印日志时候经常会用到,如下所示:
' z- E' h* r" @4 P% ^; c
4 I; o/ K- E1 ^6 S2 Wprintf("file:%s,line:%d,date:%s,time:%s",__FILE__,__LINE__,__DATE__,__TIME__);
: B: }" z3 O c, \& K- u8 q Q% X# @& [0 M/ b8 v4 Y8 z
6$ h9 d8 {* W7 Z' B( b0 [; q+ p5 n
#与##
3 M& r! C1 w; H6 `3 i1 P#:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。
3 o/ N3 g4 c0 f# D+ c
( R: b2 B2 B) P$ L) i##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。
& t! j' o9 k1 }
; ]( I! o0 w7 _' D( G#include "stdio.h"
; U4 g, `: l7 Z% B2 e' }# }#define TO_STR(s) #s#define COMB(str1,str2) str1##str2$ X7 E) e# m) p! _
int main(int argc, char *argv[]){ int UART0= 115200;
2 Y6 X: `% k( i printf("UART0=%d' E/ a$ u" q, z, r1 q1 J. U$ Y
", COMB(UART, 0));//字符串合并为变量UART0 printf("%s( ?% n' ]* |( V& J% V8 `3 c: |
", TO_STR(3.14));//将数字变成字符串# h/ J% n$ g ?( y( e8 w3 |
return 0;}4 M. L. j# H: o3 O
5 C; X( ^3 ~$ | }( `3 q7
, s: J0 o& g: ?4 yvoid 与 void*关键字! f1 a4 @" p, r( A8 T) B3 F3 p0 @
void表示的是无类型,不能声明变量或常量,但是可以把指针定义为void类型,如void* ptr。void* 指针可以指向任意类型的数据,在C语言指针操作中,任意类型的数据地址都可转为void* 指针。因为指针本质上都是unsigned int。. a$ T9 [ d* N# `
+ W+ u2 F7 P1 \常用的内存块操作库函数:' \: m6 p2 ], o; p
, p! J9 x2 r6 W
void * memcpy( void *dest, const void *src, size_t len );void * memset( void *buffer, int c, size_t num);
6 R) v5 S9 ~* Y& l8 ^. }+ w! x数据指针为void* 类型,对传入任意类型数据的指针都可以操作。另外其中memcpy第二个参数,const现在也如前文所述,拷贝时对传入的原数据内容禁止修改。3 q# {$ c& r! k" n g
, z1 i8 J+ @; M4 _7 s: d4 d
特殊说明,指针是不能使用sizeof求内容大小的,在ARM系统固定为int 4字节。对于函数无输入参数的,也尽量加上void,如下所示:
+ z. S' s4 i" Q$ Z3 Q# N
7 r% J# B, L5 s8 hvoid fun(void);" f" i7 L% X. `& H1 ~
3 q) C5 K# R/ x) j [4 Y8 @
8
8 K! J& S3 c8 U' ?; lweak关键字# c' [4 \0 E5 B: O& d
一般简化定义如下所示:1 h9 j g' x* K3 I0 U
. h8 W( w8 I1 P3 R% L. t#define _WEAK __attribute__((weak))2 w6 v6 X! Y s) R4 @+ j
函数名称前面加上__WEAK属性修饰符称为“弱函数”,类似C++的虚函数。链接时优先链接为非weak定义的函数,如果找不到则再链接带weak函数。
. o$ C% C, p/ x. P5 R$ Q- _( G; E( q
$ U* B6 c) L0 s D_WEAK void fun(void) { //do this} 6 {) H$ E9 S$ t0 y k
//不在同一个.c,两同名函数不能在同一个文件void fun(void) { //do that}. I) A. J2 t! ~
这种自动选择的机制,在代码移植和多模块配合工作的场景下应用较多。例如前期移植代码,需要调用某个接口fun,但当前该接口不存在或者未移植完整使用,可以使用weak关键字定义为空函数先保证编译正常。
" A- l7 H; x( q# A) t7 X! A' r
; o+ N- `' _1 o2 X6 o9 v0 U后续移植完成实现了fun,即软件中有2个fun函数没有任何错误,编译器自动会识别使用后者。当然也粗暴的#if 0屏蔽对fun的调用,但要确保后续记得放开。" d$ H) p& c7 g/ l
$ Y, Q; Z# E) ^0 P8 v. T- f, u
4az1cr4tckv64011336736.png
" H- s# n& v# O5 Z+ x
往期推荐什么是内存碎片?
2 h, n# y& T2 o7 ^4 e详解UDS CAN诊断:什么是UDS(ISO 14229)诊断?( ?6 v$ K) w' j2 k) x0 ?& c
磁耦合共振无线供电装置4 w+ u" ^6 Y7 t& u0 q; |( s7 E
C语言:十六进制(HEX)和浮点类型(float、double)转换4 ]7 J' l) l" B+ O8 K* L
HarmonyOS 分布式多端应用一站式开发平台(DevEco Studio 安装): X/ A/ d' Q- g/ g9 q* U1 T' S
/ r, q# S$ n1 k! i3 R/ \1 d8 `! @. ?, H
vrrnfc2hjre64011336836.jpg
' {$ J$ y# I/ j! x9 C' u
fqzxfmocn5o64011336936.gif
2 T# ~) N1 z$ @$ ~3 I点击阅读原文,更精彩~ |
|