|
ehkf2mbeaxy6406095230.gif
1 y* q: X7 F4 b$ {. Y
点击上方蓝色字体,关注我们
1 C& U% o7 G, ~
- t* D( G: x" s1 w9 Y* N0 w1# x) a# d) {9 p% w* B- t ~" N$ p* l
volatile关键字
6 ~0 k# S9 ]0 g. k, D! Ivolatile是一个特征修饰符,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。& g }" J3 U0 l/ C" Y
% N G. j( F5 o7 m" Q* q. G0 M. }0 h
常用场景:中断服务与主程序共享变量。示例代码如下:" S3 r, p9 M+ t6 V
h0 R' b P4 O4 ]# |& L
//volatile uint8_t flag=1;uint8_t flag=1;
1 y* h. U. o" A5 n4 o& s. {void test(void){ while(flag) { //do something }}
. H4 d5 U( W' h# p//interrupt service routinevoid isr_test(void){ flag=0;}- `) Z( _' }, @% y6 ]; D. l
如果没使用volatile定义flag,可能在优化后test陷入死循环,因为test里使用的flag并没修改它,开启优化后,编译器可能会固定从某个内存取值。
9 N8 M; z: D+ b: ^# Y* h- b" a; }2 U+ i. d9 b
24 P0 W @) F0 _: f) {
const关键字& f3 W( ^8 }3 C( i/ f% u( J
const 是 constant 的缩写,意思是“恒定不变的”,它是定义常变量的关键字。
4 r! {4 @; Z5 [, o: K( |2 w$ d: A通常有4种用法。
& H- G' C- ~+ G/ v& p% }" j# _7 ?5 {0 O) L; U( B0 \
\% ]+ [0 f' S/ T) h% o3 K
1、修饰变量2 X* c: Y; F9 n/ B7 Z+ X$ v; E
' H& K g( t! e7 D5 q采用const修饰变量,即变量声明为只读,保护变量值以防被修改。
; Z/ n k9 h' i6 ?# d8 |: G
2 Z R8 p, ^# R) ]" `" l2 vconst int i = 1;或者int const i=1;
, o7 S* J9 l4 [2 g6 U2 l- V, l变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10,属于错误操作。
( k5 g6 v- X, c3 w: c: [- ]" t+ m1 ]$ \, w$ t9 }
% j: Q: s5 d D, j
2、修饰数组
+ N; w1 D2 _4 \2 p
, j, x# ?; t* q8 j) c) z数组元素与变量类似,具有只读属性,不能被更改,一旦更改,编译时就会报错。7 B, d+ D. P4 B: k$ z4 N3 s
: t1 b9 {# U& |: ~2 e$ g" r- \0 o- R
const int array[5] = {1,2,3,4,5};array[0] = array[0]+1; //错误,array是只读的,禁止修改% Z+ Z6 G& E3 Y# H
使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。
1 G n% t* u& U+ a; y- I
. Z1 L* L% P6 O" L% Z
' A& ~- Z5 P- A6 t R/ N( d3、修饰指针% R: e. Q4 L* o! g4 p0 c
3 n. {0 L9 a0 I2 GC语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。
2 o( |8 {, `5 A" I( E( }- I) x- T7 x$ N$ g& [" f
int i = 1;int j = 2;
% z; T" _: }$ e: u, `; nconst int *p1 = &i;int* const p2 = &j;
' i6 n0 u U0 c: P9 z0 ]6 A5 s上面定义了两个指针p1和p2,区别是const后面是指针本身还是指向的内容。/ m, t0 e5 j: u2 T
' P% ^/ {* G+ F7 j6 G1 S+ q S在定义1中const限定的是*p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=10,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。
( D5 u4 n* S/ {$ Y$ q: V0 ~1 M! Y
在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如*p2=20是没有问题的,程序正常执行。
( K" M( c" g; H; g% ~( R5 ~1 D2 e5 d. c
$ w0 ~- ~# i* h) U; G2 m
4、 修饰函数参数- I- T9 y% }! _, A5 ]% Z
const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改,所限定的函数参数可以是普通变量,也可以是指针变量。
7 K- C3 I6 o# C7 o' I( j
8 {" g5 W% G9 `' ivoid fun(const int i){ …… i++; //对i的值进行了修改,程序报错}
) M8 ]0 |# M3 d( f常用的函数如strlen。7 j& d, U% @& v, j' t# C% Q
! i" v5 F' C' q' u1 ]size_t strlen(const char *string);- p7 o" G( F. y, K1 z! Q% D
const在库函数中使用非常普遍,是一种自我保护的安全编码思维。, L U Q# x s3 Z% M4 z
3
- E6 ?8 t8 Z) M$ D/ K/ r) f- nstatic关键字
3 Z* b& P2 \& i. ~: W1 V& ]1、static修饰全局变量,该变量只在本文件内被访问,不能在其他文件被直接访问。
+ J8 h# P) A; e' ]4 i; K/ p" D" k8 g/ ~+ R! V/ C& V! |
2、static修饰函数,该函数只能在本文件内被访问,不能被其他文件访问。但是可以通过嵌套的方式调用,变相的封装的表现。) O" k' u P! N( f$ m% y
1 P4 v. [3 B- E" Q, L, _
3、static修饰局部变量,更改该局部变量的生命周期。
& J, {2 w3 g) u生命周期:将临时变量的生命周期变成全局变量的生命周期。作用域不变:作用域仍然是在本代码块内。7 _5 N' F. A/ O' x2 Q. Q$ y
5 {" a8 O* ~; E4/ F+ d: {) s7 b0 g% m
struct与union) S+ U0 Y& l; a7 d4 h
可以使用struct结构体来存放一组不同类型的数据。
: k* C( @& M1 q, z' y, \! N" h5 I2 _0 _! q Q
struct 结构体名{ 结构体所包含的变量或数组};
& e1 ?4 K# D' s$ k9 r( l6 E. g结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员,通常我们使用结构体定义和解析协议,如下所示:
0 T- ]. _9 }3 x) {2 l }( e* s! g3 M, R8 N7 o, H5 x' h; ?4 ~) N" A( c
// 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(), o5 W, u% N: a" f( d: I7 c; Y
union共用体关键字,定义union下面的成员变量共享一块内存,每一个成员在任一时刻有且只有一个成员使用此块内存。
+ @$ E% W$ j0 j* \5 a# y7 t- j- j) v7 _5 @/ [- E" V6 a! o
union 共用体名{ 成员列表};
# J6 Q0 U. {/ ?3 j结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。& p# X* p; E7 Z/ @) V
通常使用共用体做一些标志位操作,例如以下示例,可以非常灵活的访问Val中的bit位。
/ D, ?& y4 |6 o" V
/ w, p7 e3 [9 B7 \3 r! n G2 r7 }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;, [* i5 q- F7 A7 p
或者使用共用体实现单字节与多字节的转化和拼接,如下所示:
* H2 [5 x. o$ c; ?
; s9 v; Q- X- o/ R+ `#include "stdio.h"
/ L2 i0 l. m/ {2 T$ ztypedef struct{ union { struct { unsigned char low; unsigned char high; }; unsigned short result; };}test_t;
, m. y' U" {( fint main(int argc, char *argv[]){ test_t hello;
1 k; k# }) c1 o- `9 w9 r hello.high=0x12; hello.low=0x34;
( a! P) N; m, t0 F% Z printf("result=%04X\r
5 R: d0 p3 }/ J& p+ T",hello.result);//输出 result=1234
# W# L4 p+ R+ T return 0;}) S2 l# P1 \. T9 H% d
* j% I: l6 Z2 K5
z3 H" g* \- }; j预定义标识符
! |) [7 O" N/ a* T一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。. _ ]; U. }9 z% z) X0 Y
* O9 w3 l7 e; h* r( ?: \常用的预定义标识符如下所示:
# P# m8 O# a( t. p9 W' U3 |
5 T/ c/ g% j+ ^9 d0 S) f) k: g& Z__FILE__ //表示编译的源文件名__LINE__ //表示当前文件的行号__FUNCTION__ //表示函数名__DATE__ //表示编译日期__TIME__ //表示编译时间8 F/ e8 c0 a4 }7 @- Z. I2 j8 Q
在Debug打印日志时候经常会用到,如下所示:7 t# p% U# \* ]+ P
$ ?: S! |0 x3 Q7 J
printf("file:%s,line:%d,date:%s,time:%s",__FILE__,__LINE__,__DATE__,__TIME__);
# h9 S I2 y# }- {( o* N" C
1 g- b/ ?6 H" E( H) A1 L6 Y9 d' l3 m$ {6
: g8 v4 k+ b* s# X( M$ f& B#与##( }4 p+ O5 Y$ ` q% c7 g
#:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。$ l" g( O4 t1 ^* L$ f9 Y0 m
6 @# C7 O, V6 ^/ \
##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。' Q# b1 R' S6 P2 k3 l
5 }1 D& r: [& J; I. O* V$ |
#include "stdio.h"
5 m$ R9 l% p9 Q% ^1 `" [#define TO_STR(s) #s#define COMB(str1,str2) str1##str2 E! x$ V1 T% F6 P
int main(int argc, char *argv[]){ int UART0= 115200;
$ P5 ?& O. }3 t) ^7 m printf("UART0=%d
! T$ A2 d% t8 @ F" A Z( Z1 L", COMB(UART, 0));//字符串合并为变量UART0 printf("%s
) c( f" Z! {5 N4 f", TO_STR(3.14));//将数字变成字符串( J* B$ q2 D m! {" c: Z$ X
return 0;}9 i. X2 B( H3 ]$ n8 M& g
7 f% s+ F$ Q" p: o4 ] e7 A
7
6 z8 y) m" u0 Cvoid 与 void*关键字7 o1 \% C9 L$ I. K) i
void表示的是无类型,不能声明变量或常量,但是可以把指针定义为void类型,如void* ptr。void* 指针可以指向任意类型的数据,在C语言指针操作中,任意类型的数据地址都可转为void* 指针。因为指针本质上都是unsigned int。
' a! x6 e: X! p3 R5 W! J1 N- T4 P/ \0 y0 K0 h+ w
常用的内存块操作库函数:
% m: k# k8 g$ ]; d) y
1 |! l4 M. g9 p" h% A! I! g- Nvoid * memcpy( void *dest, const void *src, size_t len );void * memset( void *buffer, int c, size_t num);
! x% K" f( U1 i* ]0 V5 C5 M数据指针为void* 类型,对传入任意类型数据的指针都可以操作。另外其中memcpy第二个参数,const现在也如前文所述,拷贝时对传入的原数据内容禁止修改。* L- s/ p0 a9 O2 B1 M
: k4 C8 g9 B8 g3 }' i# t特殊说明,指针是不能使用sizeof求内容大小的,在ARM系统固定为int 4字节。对于函数无输入参数的,也尽量加上void,如下所示:, ?4 x$ I, e, E) Z2 V! M6 r8 G% c
0 r; E; A; y7 [$ P
void fun(void);- i2 f/ K# ^' y0 P! P
7 J7 h7 x! h4 u" v8
4 b" V6 I" e! }5 Nweak关键字8 V4 W( X- N. k( k$ c
一般简化定义如下所示:
# u2 E+ h+ c2 ^" w G* }; g9 j3 Q- F: u, G& S$ J$ l
#define _WEAK __attribute__((weak))
! ]4 i2 r% g5 n函数名称前面加上__WEAK属性修饰符称为“弱函数”,类似C++的虚函数。链接时优先链接为非weak定义的函数,如果找不到则再链接带weak函数。
$ e5 _2 X: x7 x5 Q# w8 C T5 C8 r# g/ U' C
_WEAK void fun(void) { //do this} & u: u2 h& z3 w7 F
//不在同一个.c,两同名函数不能在同一个文件void fun(void) { //do that}
: N* u y- `0 t' \( r, k这种自动选择的机制,在代码移植和多模块配合工作的场景下应用较多。例如前期移植代码,需要调用某个接口fun,但当前该接口不存在或者未移植完整使用,可以使用weak关键字定义为空函数先保证编译正常。& q) q& I' {7 P0 [) H! p
" j7 B+ O. O% z% u# U/ ~后续移植完成实现了fun,即软件中有2个fun函数没有任何错误,编译器自动会识别使用后者。当然也粗暴的#if 0屏蔽对fun的调用,但要确保后续记得放开。& j) @* W& _- ]- i1 a
; p$ j5 d1 f4 r2 W% G+ T
obzzf0ax3dx6406095330.png
. l4 }- j ^4 O# ]6 k
往期推荐什么是内存碎片?
, r7 z7 N. c' h" @' Q4 R详解UDS CAN诊断:什么是UDS(ISO 14229)诊断?
2 k6 d X; I* i( i& F磁耦合共振无线供电装置8 v' w' ~9 i+ L! ^( P
C语言:十六进制(HEX)和浮点类型(float、double)转换
0 q, @+ ~ I$ M) N0 PHarmonyOS 分布式多端应用一站式开发平台(DevEco Studio 安装)6 P* s3 a" O F6 g# r6 [% N
, j( z2 Y1 L; D: `( x8 X' Y, y
& ]( Q- v; I9 z
0gfwuj32of06406095430.jpg
3 G! I8 C4 F, q7 t+ G) P
isieitqkphf6406095530.gif
4 b, C. S3 h. c8 I( l点击阅读原文,更精彩~ |
|