|

skk2rppqhah64024997041.gif
: J5 d+ [1 u0 j) \: r& G点击上方蓝色字体,关注我们* ^6 S% c- S, s0 m
9 B% v& _) j& v2 B1 U2 g
15 f6 B, ^: K: i5 Y
volatile关键字
% g& h. i! ~0 s: xvolatile是一个特征修饰符,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,告诉编译器对该变量不做优化,都会直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。6 W7 @* s `( |* B x* w
4 T0 G& Y7 O9 [! i! O% x; w/ c
常用场景:中断服务与主程序共享变量。示例代码如下:/ c* R5 i4 n7 K, v @0 S
( x0 r u8 K% f* \
//volatile uint8_t flag=1;uint8_t flag=1;
, J3 @) ? O& r9 b# \5 N/ Uvoid test(void){ while(flag) { //do something }}
0 n* a: z. b# t) R//interrupt service routinevoid isr_test(void){ flag=0;}& T Y( P' r8 a* r# I- B* C
如果没使用volatile定义flag,可能在优化后test陷入死循环,因为test里使用的flag并没修改它,开启优化后,编译器可能会固定从某个内存取值。7 z/ I9 R x+ u/ N( \* _
* T. T) m& U8 @0 }, k0 u8 n# T2$ L) u2 N% ?' \9 D# Y
const关键字
% P; U1 L9 @3 C9 `const 是 constant 的缩写,意思是“恒定不变的”,它是定义常变量的关键字。3 k3 n, j4 S! Z, f9 c, B3 i# ~" ^0 r
通常有4种用法。
4 ]$ c" D1 m/ O% D& E0 K) a- t8 V! z" a! J7 r
5 p! B; C$ j# a- {* E
1、修饰变量
3 T" S: v' D' D$ \' O
, f8 r4 N# W, i1 x, N采用const修饰变量,即变量声明为只读,保护变量值以防被修改。
X8 t, Y+ j ?( } ]% Z9 ? f- a/ t: E8 j" L4 R- U
const int i = 1;或者int const i=1;2 H1 d+ Y3 _ a1 ?
变量i具有只读特性,不能够被更改;若想对i重新赋值,如i = 10,属于错误操作。* z* F; x. S% ?0 V. P
( B: Z8 A1 t, ]$ m' A
0 }! G( `5 @9 b4 ^
2、修饰数组
+ C' f: |* S0 L. P( J+ F5 T8 z% A
2 A) _: e1 Q0 x3 O# [数组元素与变量类似,具有只读属性,不能被更改,一旦更改,编译时就会报错。2 F6 N; @1 r3 H) V3 K+ T8 j! _% w0 F
" Z: K2 |/ V7 M* _5 ]$ ]' `const int array[5] = {1,2,3,4,5};array[0] = array[0]+1; //错误,array是只读的,禁止修改5 W8 C9 ~6 y' S% F3 W f
使用大数组存储固定的信息,例如查表(表驱动法的键值表),可以使用const节省ram。编译器并不给普通const只读变量分配空间,而是将它们保存到符号表中,无需读写内存操作,程序执行效率也会提高。
- j! l, C' j! l! G
+ c- K1 m3 r3 i: z# P
: ^5 u+ ~9 m7 U; Y) [% f3、修饰指针
c; u0 W4 H0 c6 V: O! T8 ? S6 n) G' L5 f: v
C语言中const修饰指针要特别注意,共有两种形式,一种是用来限定指向空间的值不能修改;另一种是限定指针不可更改。
+ M& e+ `, ~4 |4 i
4 a) M6 C% K6 u2 f, r! ?0 zint i = 1;int j = 2;
# e# m3 v& B k6 \const int *p1 = &i;int* const p2 = &j;1 h) V, p0 F, E) @: S- [+ J
上面定义了两个指针p1和p2,区别是const后面是指针本身还是指向的内容。7 M" ~1 \# z! v$ U8 V
6 O& i$ T/ Q/ S0 G4 i z在定义1中const限定的是*p1,即其指向空间的值不可改变,若改变其指向空间的值如*p1=10,则程序会报错;但p1的值是可以改变的,对p1重新赋值如p1=&k是没有任何问题的。$ e$ B9 [8 e2 d& t. p c0 ~9 w1 I' D5 Q
q' D4 p: B. m% t
在定义2中const限定的是指针p2,若改变p2的值如p2=&k,程序将会报错;但*p2,即其所指向空间的值可以改变,如*p2=20是没有问题的,程序正常执行。% m4 y! I% u$ c
% O- Z: W( l3 }7 n- Z4 ~
! @1 L( U# u# q6 f" D) y4、 修饰函数参数/ n2 {& G' x3 v% W$ W) X
const关键字修饰函数参数,对参数起限定作用,防止其在函数内部被修改,所限定的函数参数可以是普通变量,也可以是指针变量。- ^, U- b, _" N0 E% m/ z$ j: B
! q) x6 F9 E; `2 k: {2 z9 N1 b
void fun(const int i){ …… i++; //对i的值进行了修改,程序报错}
+ Z" v# j7 h6 K! C常用的函数如strlen。
& c1 v, Y/ C" b4 q% @
6 g: p, C+ e/ _0 N. esize_t strlen(const char *string);' h. n8 O9 }/ F7 x
const在库函数中使用非常普遍,是一种自我保护的安全编码思维。
" ]2 P( H3 a1 ]: H& @3 G3 K& m8 Z3! O4 ^; A% D2 K; N+ k: ^ X
static关键字. S. B: s6 G9 S! q" |" U" r
1、static修饰全局变量,该变量只在本文件内被访问,不能在其他文件被直接访问。2 P# G' N) g8 K4 E. `3 U {
' M1 C' B! i9 x) ~2、static修饰函数,该函数只能在本文件内被访问,不能被其他文件访问。但是可以通过嵌套的方式调用,变相的封装的表现。! Q1 j6 S g6 @3 z; Z
( u& Q7 E+ l" B( S, X8 b# t3、static修饰局部变量,更改该局部变量的生命周期。 c9 e. U0 l4 Q6 i* R
生命周期:将临时变量的生命周期变成全局变量的生命周期。作用域不变:作用域仍然是在本代码块内。
+ i5 v% R6 C* T4 B4 ?. [! ]5 I, H/ }& J U: I' P0 B& O. p6 X* p
48 W, R k7 s; z7 g* t% _! a
struct与union5 m7 ?& [" I0 E: o- \4 r- T
可以使用struct结构体来存放一组不同类型的数据。5 X7 M3 g0 ^" P+ Z
0 T2 H- ?8 B" H8 \" X# L
struct 结构体名{ 结构体所包含的变量或数组};
" y/ ]; V# s. \& O结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员,通常我们使用结构体定义和解析协议,如下所示:( l( S1 b' T$ o, ~
3 [* e, H; V: s( [/ V7 @// 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()! h$ P5 P% n0 G; r0 f, ?; ]* ]
union共用体关键字,定义union下面的成员变量共享一块内存,每一个成员在任一时刻有且只有一个成员使用此块内存。
1 z; j, Y1 N" I1 c0 o! [& P g- r
6 {& [2 T& E* W: Nunion 共用体名{ 成员列表};
- ?2 Q0 [$ s1 I% R# R1 |结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
- d7 P0 Y7 R( G0 B% b% _# a1 `, u5 W通常使用共用体做一些标志位操作,例如以下示例,可以非常灵活的访问Val中的bit位。
* p2 f0 T7 Q& Z; ?4 P+ T1 e
( N9 }0 {9 I" @/ Vtypedef 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;% M: b" m6 R M; w' K) O1 A
或者使用共用体实现单字节与多字节的转化和拼接,如下所示:
6 k* P/ l) V2 k- X1 @% W
& ^/ N; T4 m; j# S! k3 H#include "stdio.h"8 w+ U7 R8 L: x. h% G, O
typedef struct{ union { struct { unsigned char low; unsigned char high; }; unsigned short result; };}test_t;
# W3 R# x8 a* z7 p8 Xint main(int argc, char *argv[]){ test_t hello;, Z5 A- j5 L' K3 G1 T- d p
hello.high=0x12; hello.low=0x34;
, m& V" F5 M# {' B/ d# g printf("result=%04X\r
: k: {8 T. U1 a( _6 T& B",hello.result);//输出 result=1234
. R3 ?& U X1 T# H; N return 0;}
! e! W, Q1 x& g% Q
# w0 B$ l, v& |- j5 \56 H% s9 T/ m! |* O# I+ |3 }
预定义标识符! m. E; u- l& l) _7 p. V
一般编译器都支持预定义标识符,这些标识符结合printf等打印信息帮助程序员调试程序是非常有用的,一般编译器会自动根据用户指定完成替换和处理。& R* ^. E" M+ ^4 H
: c7 Y! E' z& t
常用的预定义标识符如下所示:! p& t; Z) c+ t5 G4 `2 [0 r
! E7 W2 G& f: ^; I. {5 w3 T__FILE__ //表示编译的源文件名__LINE__ //表示当前文件的行号__FUNCTION__ //表示函数名__DATE__ //表示编译日期__TIME__ //表示编译时间
; Y: `, ^( @6 B: U% w在Debug打印日志时候经常会用到,如下所示:
4 \6 x( N- }; d9 c, K; b7 o, f) [" n, x C6 U |
printf("file:%s,line:%d,date:%s,time:%s",__FILE__,__LINE__,__DATE__,__TIME__);
6 _! B! c' _+ T& Y! i
" q- o% }* f/ B3 M6" h& K" o. t5 Q! c. t
#与##
[1 m: N: m; b3 h6 |#:是一种运算符,用于带参宏的文本替换,将跟在后面的参数转成一个字符串常量。
) j; _. r9 q$ p' [" t3 N$ y! e' _0 d- f
* U* O9 |. Z5 a5 M) W" ]##:是一种运算符,是将两个运算对象连接在一起,也只能出现在带参宏定义的文本替换中。
8 c. H7 r- W- A; R
# U5 ^0 Y1 B: X, H( L#include "stdio.h"; k0 B$ I) |& z, N4 G
#define TO_STR(s) #s#define COMB(str1,str2) str1##str24 g. C6 ~( S9 g' N( U" ^. e
int main(int argc, char *argv[]){ int UART0= 115200;* o; v0 T- d; ~4 | g: C
printf("UART0=%d
, M4 C9 C! {1 B2 S", COMB(UART, 0));//字符串合并为变量UART0 printf("%s
( J9 u. Q: O3 q F; p! C% H6 r", TO_STR(3.14));//将数字变成字符串
& t+ p* j) [8 ]6 ?4 J+ g% e return 0;}
' O0 B: V+ O8 n- N6 {
p; T) ]" Q! E: a* ]7
6 @3 I: H+ {: Rvoid 与 void*关键字
4 h7 ~$ X: P3 Y$ u0 L% uvoid表示的是无类型,不能声明变量或常量,但是可以把指针定义为void类型,如void* ptr。void* 指针可以指向任意类型的数据,在C语言指针操作中,任意类型的数据地址都可转为void* 指针。因为指针本质上都是unsigned int。
|1 e: W o0 c/ F& G4 f
+ R& M( | V- ?1 f, O' T常用的内存块操作库函数:
! a) V9 Q( Q$ T' Q& y; b9 f6 N7 E: d2 r9 D
void * memcpy( void *dest, const void *src, size_t len );void * memset( void *buffer, int c, size_t num);2 U5 p* {7 Q, Z4 X" Q: x
数据指针为void* 类型,对传入任意类型数据的指针都可以操作。另外其中memcpy第二个参数,const现在也如前文所述,拷贝时对传入的原数据内容禁止修改。
) }$ S) M l, n
& Y3 b; u; \% X9 p特殊说明,指针是不能使用sizeof求内容大小的,在ARM系统固定为int 4字节。对于函数无输入参数的,也尽量加上void,如下所示:1 |) C7 C5 ^- n: W$ b+ I
: ?# M- [$ T2 H0 C
void fun(void);
/ F5 ~+ z/ E9 L$ U1 y5 j' F. o: I1 T7 f4 \& V
8* n U, E+ v! u% ]3 j4 G
weak关键字
- b3 ^ N9 C4 p7 L一般简化定义如下所示:# x4 ]. q7 j* H9 Y. \7 Y
' x5 a& {! o# R& v6 }( W#define _WEAK __attribute__((weak))
& \% H: ?- q$ D6 W! V( L' S函数名称前面加上__WEAK属性修饰符称为“弱函数”,类似C++的虚函数。链接时优先链接为非weak定义的函数,如果找不到则再链接带weak函数。( X0 V) T" d# q- p
+ |& T9 U0 t4 i* s1 I0 S_WEAK void fun(void) { //do this}
0 P, R- c. G/ c//不在同一个.c,两同名函数不能在同一个文件void fun(void) { //do that}. H2 x# M9 j- N+ y3 {
这种自动选择的机制,在代码移植和多模块配合工作的场景下应用较多。例如前期移植代码,需要调用某个接口fun,但当前该接口不存在或者未移植完整使用,可以使用weak关键字定义为空函数先保证编译正常。9 w: y6 V0 n3 Z: C4 i* p$ g3 a9 P9 g
4 n* X* k) g% o后续移植完成实现了fun,即软件中有2个fun函数没有任何错误,编译器自动会识别使用后者。当然也粗暴的#if 0屏蔽对fun的调用,但要确保后续记得放开。* r4 [8 A4 o) L0 S9 c
+ }! |6 o n9 \
z3iwwlzuh1q64024997141.png
y( }1 V& M- k! Y0 l' `6 r
往期推荐什么是内存碎片?
# i3 S3 v3 v b0 P, W! z详解UDS CAN诊断:什么是UDS(ISO 14229)诊断?4 T- W/ C9 J r' Z9 z% J
磁耦合共振无线供电装置
+ y: [5 Q( Q* H& Q- w" ]C语言:十六进制(HEX)和浮点类型(float、double)转换
J( h) S" b6 W/ F' |+ G: y- }( ]7 [HarmonyOS 分布式多端应用一站式开发平台(DevEco Studio 安装)& ]$ \6 k" M8 m2 d2 L# C& C) C0 G0 j
3 D6 r. m; ]; I. v5 s7 H# B1 s6 Q R
fuk2mic3ad164024997241.jpg
6 r/ G- H: }, u' w- ], n) V4 a
rx0bwuyifqw64024997341.gif
" u3 }% F) Z8 D0 u$ {8 M( l. N
点击阅读原文,更精彩~ |
|