|
0rmtcxkrehg64015933843.gif
( h7 P# t5 i9 k* y* D& K点击上方蓝色字体,关注我们6 v. Z( M) T; J: _
8 Z: |5 G/ u+ q
1
# A- t$ ]# t' |0 Z- n4 L" _volatile关键字8 z; \7 y, ^$ }
volatile是一个特征修饰符,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。. y5 W+ | w( L! R$ V5 e
7 v7 l1 R& Y" S: E. ]
常用场景:中断服务与主程序共享变量。示例代码如下:
3 @6 ]; e) e# l2 [1 A% d
) V9 E3 z- e! n* f# O' ^, H- m//volatile uint8_t flag=1;uint8_t flag=1;) k' L7 M& q; D& D4 P- O
void test(void){ while(flag) { //do something }}
# F+ c+ {: l: y6 {//interrupt service routinevoid isr_test(void){ flag=0;}
" ^% K" w: V( K# c0 O如果没使用volatile定义flag,可能在优化后test陷入死循环,因为test里使用的flag并没修改它,开启优化后,编译器可能会固定从某个内存取值。0 S6 q- }( S. b9 ~
4 w9 u2 B) G+ p' I! G% Z2
5 Y( q2 b9 \. t- O* Jconst关键字
; e, s& f* f* Nconst 是 constant 的缩写,意思是“恒定不变的”,它是定义常变量的关键字。& s% S, m5 {. E; G
通常有4种用法。
; q9 h& f7 r( d* X( v
8 ~+ e7 |# P% i) }" o; N
" l: z- ]# ~! T3 ?, p, F1、修饰变量
& D8 ~! T( ^5 w6 |$ @1 k+ ~
' R9 ?+ k5 N, ~: n采用const修饰变量,即变量声明为只读,保护变量值以防被修改。
- [8 j1 Q Y2 Q! r" n% @
8 ^5 ?4 Q5 N" x7 B# v7 Mconst int i = 1;或者int const i=1;
1 i, E+ G8 g+ ]9 w* j9 d变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10,属于错误操作。
]/ k8 k/ Q( `2 C1 b- h* B% P' q% _* N/ L; H3 O/ j; Q1 x
/ |2 h2 _! a- H ~$ o2 I& Q* Q
2、修饰数组
6 I! z" Z, |: E7 c
5 k' _2 T4 F) g) D0 r数组元素与变量类似,具有只读属性,不能被更改,一旦更改,编译时就会报错。
: s, k$ Q. s# L8 j# C- h* a. |$ i' h% Y) I. o" U
const int array[5] = {1,2,3,4,5};array[0] = array[0]+1; //错误,array是只读的,禁止修改
. x4 ]1 G5 j5 a使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。
4 _7 R$ g3 G1 c+ [3 v/ D9 Y, G: S# z- V& w" f2 S+ \! I
$ B/ `, e$ {! F: O% t5 T& ^! m3、修饰指针( p4 N- k9 H! ? [
7 k" L* Q) ]8 l# t: l
C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。
8 E4 b' U1 z8 @) V) k' j
- q1 ^9 o; |% l+ P' A- T8 ^int i = 1;int j = 2;% p! b2 S# D: x e
const int *p1 = &i;int* const p2 = &j;
) H& a2 L; d/ `" B' L0 j: [上面定义了两个指针p1和p2,区别是const后面是指针本身还是指向的内容。
1 |' p M/ Y9 j* A5 }$ o }' {/ k0 L& V6 Q, g
在定义1中const限定的是*p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=10,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。# l3 @5 g2 o3 t, f
& r8 f w, W) H F, q) O% L0 H- i
在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如*p2=20是没有问题的,程序正常执行。
8 y6 s/ ]6 a% n# k' T! D
" }8 F+ L- g4 Q6 i) t( a$ R" K2 _3 B( `" [9 z
4、 修饰函数参数
" r1 U6 K7 S2 v- G4 b. _const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改,所限定的函数参数可以是普通变量,也可以是指针变量。. e6 Z+ m; \2 f" B3 T' D& u; ^
, N* p$ |, F9 `, C. @, e$ {$ \3 {void fun(const int i){ …… i++; //对i的值进行了修改,程序报错}
$ b; j3 H; Y3 {+ Y' B常用的函数如strlen。4 O9 o m, C7 h6 ?8 w. }0 }( [
7 p. P Q6 Z9 e8 I& V! a" I: Q- g
size_t strlen(const char *string);% P5 s1 |- [2 g9 k; V
const在库函数中使用非常普遍,是一种自我保护的安全编码思维。
: J: B7 t3 R8 p0 w0 y! O _3
+ b2 o V7 y9 h" N' Fstatic关键字
! i6 L* T3 T; @# t1、static修饰全局变量,该变量只在本文件内被访问,不能在其他文件被直接访问。+ p. s2 L M9 e2 i, S
4 o9 b9 E1 c% x: I
2、static修饰函数,该函数只能在本文件内被访问,不能被其他文件访问。但是可以通过嵌套的方式调用,变相的封装的表现。5 r6 S D. @# C' T% r- J
, Z e$ Q1 O1 w3、static修饰局部变量,更改该局部变量的生命周期。
' H2 u1 M" [" D: ]8 U生命周期:将临时变量的生命周期变成全局变量的生命周期。作用域不变:作用域仍然是在本代码块内。* U7 j# v, g1 Q/ c& Z# S
5 |) Y# z0 e2 C+ h* L) l
47 f+ R& {% W$ ^, D! D2 x
struct与union! y( j2 P! d* W2 ^+ S
可以使用struct结构体来存放一组不同类型的数据。4 C8 k5 ]; @; `
0 \ F6 o. R) ?struct 结构体名{ 结构体所包含的变量或数组};
5 J! R. p \/ l& H8 G# f结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员,通常我们使用结构体定义和解析协议,如下所示:' b3 p0 e0 z8 @) F3 [# ]- q0 r& I
2 U) G* E7 g0 W7 q0 h
// 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()
2 g$ V$ f5 b- K9 Y T9 B2 hunion共用体关键字,定义union下面的成员变量共享一块内存,每一个成员在任一时刻有且只有一个成员使用此块内存。
- {, v% |* U% N$ m0 z" j& _& V
/ W% [' p- L* N9 M l+ Z% punion 共用体名{ 成员列表};" e0 R" s e( w |" h
结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
* Y8 j+ ]4 p) \& E1 N通常使用共用体做一些标志位操作,例如以下示例,可以非常灵活的访问Val中的bit位。- Z( A1 h. Z' U, y: S0 v! `+ W
1 L) q+ k L% B" R7 f
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 V4 L0 Q7 h# K1 @0 x9 s! i( |
或者使用共用体实现单字节与多字节的转化和拼接,如下所示:
% }/ @# e0 F! B5 L% z6 t& p" O$ m" r8 E6 g
#include "stdio.h"! T7 P& D. _; W3 W
typedef struct{ union { struct { unsigned char low; unsigned char high; }; unsigned short result; };}test_t;# w; ]+ A& u( W. Q
int main(int argc, char *argv[]){ test_t hello;. N5 Q: J ?5 Z* B7 y4 h4 G: V
hello.high=0x12; hello.low=0x34;
! K8 B! K! C0 A printf("result=%04X\r8 q5 Y' d, [% `. A1 ]5 G
",hello.result);//输出 result=1234
4 v! y# Y. l/ `7 j return 0;}
: y$ Q. g: ]3 Y }7 C
7 k1 ~7 t0 _- a8 K5 o; w. W# l56 W8 J4 }8 W P; E+ e6 X6 ?
预定义标识符, r/ r5 C2 |- E6 V& G3 \
一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。
- b4 M) b* _% R& i1 ~$ i8 @* v
5 G3 ~% x, F7 F. {+ H- D/ n4 @常用的预定义标识符如下所示:$ c( d$ W+ [6 U( r( J. h5 T
3 S& W+ p w& h3 p__FILE__ //表示编译的源文件名__LINE__ //表示当前文件的行号__FUNCTION__ //表示函数名__DATE__ //表示编译日期__TIME__ //表示编译时间
1 v" B* S7 ~1 i在Debug打印日志时候经常会用到,如下所示:
2 t8 ^6 F$ S- G: Z2 G9 N% D2 h: U5 _) ^* R- n u* M4 ` w
printf("file:%s,line:%d,date:%s,time:%s",__FILE__,__LINE__,__DATE__,__TIME__);) |6 n5 B l$ U W, L7 f
v5 t2 Z( f) J/ S* A
6
0 q9 {2 n, V- l- N! d: p f: j0 O& U3 d+ |' `#与##- D1 E- d, A( R6 c2 i$ o& a
#:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。
* `) h. G w. T7 f
9 [9 w1 R- [- C4 Q; G9 E8 n' P7 s##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。
' x" Z/ }1 h& A" h! V- ^
# ~- S6 }# I8 z! l9 g#include "stdio.h"4 W) s# j6 y, \" H/ }: D) r
#define TO_STR(s) #s#define COMB(str1,str2) str1##str24 }3 t2 }/ E+ k% y* }0 b8 G4 R# c, x
int main(int argc, char *argv[]){ int UART0= 115200;
% w# y. ?. ~& t printf("UART0=%d {8 X# X. b$ L0 }3 |# s
", COMB(UART, 0));//字符串合并为变量UART0 printf("%s
5 D ~0 D/ c5 q; f, t", TO_STR(3.14));//将数字变成字符串3 \0 N$ |" q/ k$ \9 C" l7 q
return 0;}
% U. L& P' l2 O1 a) }2 o/ Y# ]# c% }. J1 N, i1 y) H |
7
" R3 \: i' S5 Z, G6 m0 rvoid 与 void*关键字5 O% F4 l( x- E( y) }
void表示的是无类型,不能声明变量或常量,但是可以把指针定义为void类型,如void* ptr。void* 指针可以指向任意类型的数据,在C语言指针操作中,任意类型的数据地址都可转为void* 指针。因为指针本质上都是unsigned int。
2 [. H/ {* F. s1 w
( @; l! a9 p* {常用的内存块操作库函数:
) p& V- k! Y$ v ~9 s) {: C8 {" u q5 \) C+ \1 A& O0 w
void * memcpy( void *dest, const void *src, size_t len );void * memset( void *buffer, int c, size_t num);4 r+ X) f8 Q; ]& b
数据指针为void* 类型,对传入任意类型数据的指针都可以操作。另外其中memcpy第二个参数,const现在也如前文所述,拷贝时对传入的原数据内容禁止修改。
: h7 }. [% J# N7 p U
. r6 {2 b9 I d& e: ]特殊说明,指针是不能使用sizeof求内容大小的,在ARM系统固定为int 4字节。对于函数无输入参数的,也尽量加上void,如下所示:$ Q, R( S! @! _
: D4 M5 m4 [# n: V5 \; J
void fun(void);" S Z# E" k' R( W
4 E6 r: G; b" @. M1 H8 S
8* W5 a* _! P/ M6 `! i" F
weak关键字
8 q/ F7 ^2 Q! Z, j- K' B% T一般简化定义如下所示:8 z9 E. ^, `$ V& u
" X% Y' a6 A9 ^( P9 w5 u#define _WEAK __attribute__((weak))
+ ^ I4 d6 [6 p! A% ]6 m函数名称前面加上__WEAK属性修饰符称为“弱函数”,类似C++的虚函数。链接时优先链接为非weak定义的函数,如果找不到则再链接带weak函数。: |6 _% t9 _5 R3 H6 b7 q
$ {% o; t( V- j5 B- P' `_WEAK void fun(void) { //do this}
7 M/ R) x% \+ x5 _- c' Q( m/ _//不在同一个.c,两同名函数不能在同一个文件void fun(void) { //do that}" q, G. _/ z* C
这种自动选择的机制,在代码移植和多模块配合工作的场景下应用较多。例如前期移植代码,需要调用某个接口fun,但当前该接口不存在或者未移植完整使用,可以使用weak关键字定义为空函数先保证编译正常。# }$ U' i: J/ n* l- ^) E
1 v* S/ j8 ]4 {9 G
后续移植完成实现了fun,即软件中有2个fun函数没有任何错误,编译器自动会识别使用后者。当然也粗暴的#if 0屏蔽对fun的调用,但要确保后续记得放开。
' y" y. @/ z$ f& \! J: e0 O4 V4 r" r7 o, E- \) n/ z6 E+ C
gsy3dti3fgp64015933943.png
1 s) R" z% B2 X* _3 k5 _# Q0 v
往期推荐什么是内存碎片?8 k( C3 s- Y c
详解UDS CAN诊断:什么是UDS(ISO 14229)诊断?
8 l. u! O3 Y7 {0 D8 T8 U- B$ Z3 ~磁耦合共振无线供电装置
' Z1 u6 o1 E4 s" O+ a7 LC语言:十六进制(HEX)和浮点类型(float、double)转换. ^ R% ~' v0 y( H) `
HarmonyOS 分布式多端应用一站式开发平台(DevEco Studio 安装)
# {7 t1 C4 [0 J, L- Z: r2 V: Y) i# L& u3 g
! h" G h3 e o0 }3 A& x. d* }) o
h1kyevj50zz64015934043.jpg
1 ~- ^" J) H- [- e( U
dnweybl2r1c64015934143.gif
# o6 q Z: g6 b V( k& z+ E
点击阅读原文,更精彩~ |
|