
上一个专题我们详细的分享了c语言里面的结构体用法,读者在看这些用法的时候,可以一边看一边试验,掌握了这些基本用法就完全够用了,当然在以后的工作中,如果有遇到了更高级的用法,我们可以再来总结学习归纳。好了,开始我们今天的主题分享。 一、共用体union: 1、什么是共用体union? 这个共用体,估计大家平时在代码也比较少见,我去看了一下stm32的例程里面没怎么看到这个用法(下面的示例分享是在stm32里面找的);其实这个共用体union(也叫联合体)跟我们上次分享的结构体定义是非常像的,比如说:类型定义、变量定义、使用方法上很相似。就像下面两个例子一样,把许多类型联合在一起(不过虽然形式上类似,但是具体用法还是有区别的,下面会讲他们之间的区别): union st{+ }7 [! S, ^: }+ Rint a;/ k7 l w: I& s% C7 @9 k! ^! p/ P char b; }; 4 H* U: P1 ?4 {7 A) ^! y ![]() * [; F. \. a) L. c% w1 A 2、共用体与结构体的区别: 结构体类似于一个包裹,结构体中的成员彼此是独立存在的,分布在内存的不同单元中,他们只是被打包成一个整体叫做结构体而已;共用体中的各个成员其实是一体的,彼此不独立,他们使用同一个内存单元。可以理解为:有时候是这个元素,有时候是那个元素。更准确的说法是同一个内存空间有多种解释方式。所以共用体用法总结如下: * X9 ^: z2 }1 p# b
3、代码实战: * O+ j9 d7 c$ o' f: C #include <stdio.h>. I& }& o$ U( _ typedef union{ int a; char c; //int a;+ ~# |# l- x6 w0 Q9 A // int b;2 g. [% |' S, e. V6 D$ r& i' ?6 S# n) L }st; int main(void) { st haha; haha.c='B';& Q! F3 W2 B9 J( f9 z; [ // haha.a=10;6 ^, a0 u& b1 u0 W' U- | //haha.b=60;$ q6 A2 x D6 n: B 3 o' b6 Q! Q8 `0 W& x2 ? printf("the haha size is %d\n",sizeof(haha));. Q6 u( I" j N printf("haha.c=%d\n",haha.c);* p3 y3 E+ @( ~" h$ O. v ( @9 I+ K3 ~8 h1 h& ? h$ ~ return 0; } 2 o0 B$ A" R) Z- m ) Q6 l- z1 u! f, p+ d8 i 9 C, I# c0 b# }) j4 r typedef union{3 }! v. _4 Y8 \5 ?, W1 M5 o) S % }5 E# X- n. T$ {+ p0 ` int a; char c;& ^0 r! g4 i* [9 \$ R+ j int b;; ` u5 c5 C' M9 [; W }st; int main(void) {, _0 D1 ~# _. P8 E! Y# U st haha;$ g* C2 e2 f4 U9 l: d$ ]8 M& z haha.c='B'; haha.a=10;# n! m0 o- _) ~6 m haha.b=60; 4 `6 K' r9 P5 k5 e printf("the haha size is %d\n",sizeof(haha));4 i% y/ w0 n3 {1 u: V' M& |, T8 m2 n printf("haha.c=%d,haha.a=%d,haha.b=%d\n",haha.c,haha.a,haha.b);* q$ J) p3 k% x printf("the a is 0x%x\n",&haha.a); printf("the c is 0x%x\n",&haha.c); printf("the b is 0x%x\n",&haha.b);, R+ b g3 C3 |8 G- _/ P6 v; O % d2 o5 P, D* D return 0;1 k& n5 p6 _8 x- ] } 0 E9 [1 B* M9 W J3 O( Z+ L; F 演示结果: 0 u' C/ Y' S+ t+ o2 Z# U the haha size is 4haha.c=666 @0 X* l3 N5 e* ~ haha.c=60,haha.a=60,haha.b=60 the a is 0x61feac7 E v8 ~& p& n the c is 0x61feac the b is 0x61feac/ P! O) b( r, M q0 R- c ) o/ y8 H4 @: ]6 R 说明:* d1 e, A* W9 G- d& d. W 通过上面的代码示例,读者可以发现这个共用体的大小,并不是像我们之前结构体那样是把每个成员所占内存大小加起来,而是我们上面说的那样,共用体由成员占用内存大小最大的那个决定的,上面的示例中int 占用4个字节大小,为最大的,所以sizeof(haha)得出结果就是4个字节大小,而且读者细心可以发现到打印出来的结果a和b都是60,它是访问内存占用大小最大的那个成员的数值,因为那个'B'的acii码值是是66;通过示例,我们也发现共用体访问其成员方式跟结构体是一样的(上面也有说到过)。下面是和结构体做对比的代码示例: ( e3 l2 {7 l @2 T o7 o #include <stdio.h>7 Z7 g, m3 l$ M3 Y# \7 U! f// 共用体类型的定义 struct mystruct { int a;/ Q1 g4 z2 V l4 [: ?3 [) q char b; };# L9 J6 R+ M% _/ z) U9 s // a和b其实指向同一块内存空间,只是对这块内存空间的2种不同的解析方式。4 A+ `' Z/ i. D( { // 如果我们使用u1.a那么就按照int类型来解析这个内存空间;如果我们使用 // u1.b那么就按照char类型 // 来解析这块内存空间。4 V( L% w6 d+ I union myunion1 d% D# V' e8 ^" _ { int a;* a; N4 a g" r, t char b; double c; };2 j& b5 ]* b3 P 4 Y R; J1 W& f9 ^/ ^ int main(void): E: {9 r) j6 P5 }/ W3 d& |% a {4 w8 q, j8 h+ ~2 d. \8 X. q! |4 k # R: K! V) j9 G& y; @% H " A3 l. k5 X x5 S struct mystruct s1;3 s8 k% N' r9 Y0 i# [7 X4 ? s1.a = 23;' q Y: O5 ^- ?" T0 L printf("s1.b = %d.\n", s1.b); // s1.b = 0. 结论是s1.a和s1.b是独立无关的 printf("&s1.a = %p.\n", &s1.a);2 u- i- g5 J) A$ I$ e5 K printf("&s1.b = %p.\n", &s1.b);& c9 v! c& A2 c+ H union myunion u1; // 共用体变量的定义 u1.a = 23; u1.b='B';2 w8 G) ^, L% d) s u1.a=u1.b; // 共用体元素的使用 printf("u1.a = %d.\n", u1.a);$ [7 M$ c7 }' ]1 F printf("u1.b = %d.\n", u1.b); printf("u1.c = %d.\n", u1.c); // u1.b = 23.结论是u1.a和u1.b是相关的 // a和b的地址一样,充分说明a和b指向同一块内存,只是对这块内存的不同解析规则 # Y/ R' K3 m5 e0 e printf("&u1.a = %p.\n", &u1.a);8 G8 X) i' o$ A printf("&u1.b = %p.\n", &u1.b); printf("the sizeof u1 is %d\n",sizeof(u1));1 s. ?' F1 R0 e0 F# q3 z4 H( Q . G1 p1 d1 q! M0 N9 q return 0; }! z5 G. Y/ D! j; [8 r) n# y 4 e& G$ x9 ^4 w& v- O 演示结果: s1.b = 22.&s1.a = 0061FEA8. &s1.b = 0061FEAC. u1.a = 66. u1.b = 66.1 w6 K E) r/ X& n u1.c = 66.4、 &u1.a = 0061FEA0. &u1.b = 0061FEA0., r% k' E; M* j the sizeof u1 is 8 3 _7 z* J9 y V( q& m9 G- s/ v 4、小结:
二、枚举 8 Z( t9 s! `5 q! u8 f7 N. L 1、什么是枚举?! L6 a' Z0 q8 a) R B . G: t2 V( d1 s( ~4 ?: D 枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。枚举符号常量和其对应的常量数字相对来说,数字不重要,符号才重要。符号对应的数字只要彼此不相同即可,没有别的要求。所以一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。(编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加)。 6 D5 N9 C) Y; `% v4 n0 i8 g 2、为什么要用枚举,和宏定义做对比: (1)C语言没有枚举是可以的。使用枚举其实就是对1、0这些数字进行符号化编码,这样的好处就是编程时可以不用看数字而直接看符号。符号的意义是显然的,一眼可以看出。而数字所代表的含义除非看文档或者注释。 3 w3 q, q" \0 C2 b) b: J( B (2)宏定义的目的和意义是:不用数字而用符号。从这里可以看出:宏定义和枚举有内在联系。宏定义和枚举经常用来解决类似的问题,他们俩基本相当可以互换,但是有一些细微差别。 (3)宏定义和枚举的区别:
(4)使用枚举情况:
总结: 3 Y& y5 m2 ?% v 宏定义先出现,用来解决符号常量的问题;后来人们发现有时候定义的符号常量彼此之间有关联(多选一的关系),用宏定义来做虽然可以但是不贴切,于是乎发明了枚举来解决这种情况。 3 Z% j! \: H4 J3 X. I9 ~ 3、代码示例: 8 Y8 |: d% P) S5 _' k1 r a、几种定义方法: enum week {/ ^& N2 V. p/ E* A9 R SUN, // SUN = 0 MON, // MON = 1; TUE,0 p0 K2 x! t2 y WEN, THU,* k M; j% I3 E, j7 H0 _ FRI,: b6 ]- O A4 { J) \2 z SAT,7 T8 c: z' Z5 d0 c# A. Z& B }; enum week today; W0 q. q, J( b0 u' [" L */1 [. L0 H" s' a& q1 I , o2 i% }4 x9 L- n: E. r- ? /* // 定义方法2,定义类型的同时定义变量 enum week( i' L! J/ U8 ?3 f6 U- c9 [* O {9 _4 o& d1 i7 D& q) _& Z& q SUN, // SUN = 0, m; N% \) M, Z2 M" t MON, // MON = 1;8 [3 B3 j- p" L" U. ?5 e( t TUE,! A% C7 @; x. ]1 I } WEN,9 X8 X' p' a- { THU,/ b( r& i0 a+ ]( N: L FRI, SAT, }today,yesterday;; ?% x1 F: N9 s% j) B% f */ /* // 定义方法3,定义类型的同时定义变量 enum {# C) j% c/ U+ ?6 F9 i; p @ SUN, // SUN = 0 MON, // MON = 1; TUE, 6 |: s& s. o+ P. ~ WEN, THU,) U% ^% J. J- @ FRI, SAT,8 x9 l5 c( b. w7 N. S( X }today,yesterday; */ /* // 定义方法4,用typedef定义枚举类型别名,并在后面使用别名进行变量定义 typedef enum week. i( |. ~0 q0 y2 ~" E( i { SUN, // SUN = 0. V/ x$ o7 R4 A$ _, x m9 X MON, // MON = 1;1 h( Z! K% {: | TUE,4 j" ]% t5 Z7 a WEN, THU,! `9 e T5 L% U1 A4 f FRI,$ z5 Y' k7 E/ L) {" W6 _7 o SAT, }week; */ /* // 定义方法5,用typedef定义枚举类型别名,并在后面使 6 r) M, X. e1 q2 L: n 用别名进行变量定义 typedef enum 3 Q# O; j5 }2 K# Y" x7 T$ p {1 c: a8 ^) Y: t) m SUN, // SUN = 08 K+ W2 ?* P1 K' l& ` MON, // MON = 1; TUE, WEN, THU,1 ], i; b6 I. `8 D6 W FRI, SAT, }week;/ w3 `: _6 C# n6 a0 r3 \ b、错误类型举例(下面的举例中也加入了结构体作为对比): ; [4 q$ |) Q* o+ b0 G, C. j /* // 错误1,枚举类型重名,编译时报错:error: conflicting// types for ‘DAY’- I4 x1 _1 P+ t) P4 j typedef enum workday { MON, // MON = 1; TUE, WEN,0 F1 }8 P2 x x* |8 u* X5 f3 } THU, FRI,8 M* d! A2 R% m" {; X' g" g* ` }DAY; typedef enum weekend { SAT,) \+ c$ o9 v2 X0 G3 [4 S/ A SUN, }DAY; */ /* // 错误2,枚举成员重名,编译时报错:redeclaration //of // enumerator ‘MON’* {6 `1 E$ p B: m- Z: w. _5 l typedef enum workday; N6 d/ b( G4 j, c {/ M$ ~& e9 [) E7 T$ c' e: G' m, u MON, // MON = 1; TUE, WEN, `' {+ l9 U: h THU, FRI, }workday; typedef enum weekend% o# N5 z! F8 } {4 W9 D# h: ]9 [9 P4 ~ MON,. \) Y/ H! f9 c' f SAT, SUN,* k' y* }( @5 V }weekend;! w) `- i$ r- x' h // 结构体中元素可以重名4 @, u. z0 O: Q typedef struct { int a;. ?, _1 M3 ^/ B! O char b; ]/ n6 y5 ?3 T8 u) b# \' e }st1; typedef struct {) g& W2 B$ J0 R9 [ f int a; char b; }st2; */ 1 Y% f5 H" h; { 说明: 经过测试,两个struct类型内的成员名称可以重名,而两个enum类型中的成员不可以重名。实际上从两者的成员在访问方式上的不同就可以看出了。struct类型成员的访问方式是:变量名.成员,而enum成员的访问方式为:成员名。因此若两个enum类型中有重名的成员,那代码中访问这个成员时到底指的是哪个enum中的成员呢?所以不能重名。但是两个#define宏定义是可以重名的,该宏名真正的值取决于最后一次定义的值。编译器会给出警告但不会error,下面的示例会让编译器发出A被重复定义的警告。 * @% a5 @" q5 s8 [" Q" L/ b #include <stdio.h>#define A 5 #define A 7; U( o, R1 ]) H! j6 } & E7 W4 R* A3 h int main(void) { printf("hello world\n");- f" U$ n" h8 ` return 0;* o k0 q1 N1 m& Q Q5 v) ] } ; n1 p" m B% W3 ]4 z % y9 {3 u: i4 Y7 Q2 Y0 W# t c、代码实战演示: #include <stdio.h>0 t2 ~+ V. b6 m+ W5 n9 l" u+ {7 L typedef enum week: I1 o$ x" q2 i/ r1 {" X# m7 a; Q9 V { SUN, // SUN = 0+ z; B+ S' }+ ~$ ~ MON, // MON = 1; TUE, //2 WEN, //3 THU, FRI, SAT, }week; 6 K; L# S6 x* M( q+ M8 Z int main(void)$ Z: C0 ~1 Y5 O8 j { 2 `6 F' N2 S6 l6 S" I2 C4 { // 测试定义方法4,5 week today; today = WEN; printf("today is the %d th day in week\n", today);" F) ]) h3 E3 s" R. U) J x) [1 i/ Q) N return 0;' m- I z6 t+ u+ A: {2 o$ n } : h1 x* l' V" B. p2 Y' r 演示结果: today is the 3 th day in week! R9 @4 l2 M8 X4 p2 u d、接着我们把上面枚举变量改变它的值(不按照编译模式方式来),看看会发生什么变化: * I" g5 L: M! k" c: R #include <stdio.h>typedef enum week { SUN, // SUN = 0$ u& N/ H# Z) H) N' P- Y MON=8, // MON = 1; TUE, //2: z; l1 K! V7 ~6 v" W/ J+ m$ V- M WEN, //3 THU,3 _2 r/ A& I4 s4 Q- Z2 o FRI, SAT," p& y+ _8 S \+ m$ z }week; int main(void) { 8 Z( c/ o1 P( q // 测试定义方法4,51 |8 k- l0 G7 T G/ ? week today,hh;# Z( I% B, \: r$ B2 n3 ^ today = WEN;$ x- R+ n# o# M$ X. q! g hh=SUN; printf("today is the %d th day in week\n", SUN); printf("today is the %d th day in week\n", today);$ R; \$ K( o- V, h2 D return 0; } 演示结果(我们可以看到改变了枚举成员值,它就在这个基础递增下面的成员值): today is the 10 th day in week: c6 ]/ \* L! z$ b; s4 t 注意:
8 [( X9 j/ H5 S5 a6 ?6 ? 三、大小端模式:/ c9 t- t8 f2 \3 h 1、什么是叫大小端模式? $ G H3 ?& D2 l: J a、什么叫大端模式(big-endian)? $ q. K5 Y9 J+ n6 U6 X$ s 在这种格式中,字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。 / m5 x3 b8 K4 A4 N7 Z b、什么叫小端模式(little-endian)? $ X" m: N j/ K8 n/ o1 o# } 与大端存储格式相反,在小端存储格式中,低地址中存放的是字数据的低字节,高地址存放的是字数据的高字节。 2、实际解释: ----- 我们把一个16位的整数0x1234存放到一个短整型变量(short)中。这个短整型变量在内存中的存储在大小端模式由下表所示: $ z E& m: T! b) h# J1 N$ ]& k1 G
- m+ g/ a; }2 z9 h9 O6 c/ l9 K; x 说明: 由上表所知,采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将低位存放在低地址。 6 w4 b. V, L. R. }$ a& [ 3、代码实战来判断大小端模式: 7 O! h3 b' i; B! X; i6 I #include <stdio.h>5 @. n* A- h" `# K' y6 m$ L2 {% C( o& X$ O5 l& Y8 v // 共用体中很重要的一点:a和b都是从u1的低地址开始的。 // 假设u1所在的4字节地址分别是:0、1、2、3的话,那么a自然就是0、1、2、3; // b所在的地址是0而不是3., j3 q. i+ V4 f# H# _ union myunion { int a; char b;* x9 ~ }) a% I };# ]& n( G k' i // 如果是小端模式则返回1,小端模式则返回04 Q W# u% m0 A9 E/ d. \9 E$ S* F: z int is_little_endian(void) {' @$ u- s+ Z; ?7 W union myunion u1;5 F* z. l0 P' g& Z l$ V u1.a = 1; // 地址0的那个字节内是1(小端)或者0(大端)% r5 m9 Y9 ^0 v y$ }! t+ T return u1.b;3 n7 F2 X3 P0 T- f F }8 ]& R/ i4 B( O1 @% V int is_little_endian2(void) {& @5 K$ C6 b# @3 g. S int a = 1; char b = *((char *)(&a)); // 指针方式其实就是共用体的本质3 H* g* x' k1 D B, i ; h* L; t# l4 x$ h% h/ F- Z return b; }# G& j, w" t3 r6 h 0 j9 j/ ^, Y# R " z1 S- ~. }* K! D* r int main(void) { int i = is_little_endian2(); if (i == 1)8 r. n0 Z% H- t" Q' s, G5 P { printf("小端模式\n");6 T4 ^$ R0 G& w0 U! Z } else { printf("大端模式\n"); }$ ]# k7 @& n% c4 G4 m& [$ T) K 4 g" z6 y9 S9 A2 u* L1 X/ x return 0; } 演示结果: : X0 f- m& n5 K1 d% v. z, B 4、看似可行实则不行的测试大小端方式:位与、移位、强制类型转化: & l$ H4 b' Y7 i #include <stdio.h>% [/ e/ E# T3 O0 T. d int main(void)" p3 F7 h3 l) O: k3 ~; B { f3 c6 b- N$ L& d1 r. s8 i$ r // 强制类型转换. Q! M- p1 H- N& s/ p4 e- ] int a;4 q$ v9 t6 F' b, Q9 x* c+ c# S char b; a = 1; b = (char)a; printf("b = %d.\n", b); // b=1* [% m2 {% Z9 R# v" \ # R& h! |# z( z- d s- p# @. E /*8 C+ @4 h5 `3 [! L" ] // 移位$ s5 U8 z1 j8 M: o int a, b;) s( z9 j- [/ U& i! t0 s0 x) q a = 1;! J9 h, S1 x- }! {! c: P b = a >> 1;2 {* s4 Z" U6 c, S printf("b = %d.\n", b); //b=0 */ /*& B. E- q. ~: N0 v3 y, L$ d& g9 u // 位与 int a = 1; int b = a & 0xff; // 也可以写成:char b% {- U, L' G' U& c& m! H printf("b = %d.\n", b); //b=1 */ return 0;5 f0 y& k8 K) w; g }: a5 \& K) N; W2 J & g! B7 q Y* Q3 @7 q+ I+ Z 说明:! L' ^( c! H- U (1)位与运算: 结论:位与的方式无法测试机器的大小端模式。(表现就是大端机器和小 端机器的&运算后的值相同的) 理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关)。 " B, S/ A( b3 G9 r (2)移位: 4 s8 e, T( P' C, a 结论:移位的方式也不能测试机器大小端。 理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。 (3)强制类型转换和上面分析一样的。 5、通信系统中的大小端(数组的大小端) (1)譬如要通过串口发送一个0x12345678给接收方,但是因为串口本身限制,只能以字节为单位来发送,所以需要发4次;接收方分4次接收,内容分别是:0x12、0x34、0x56、0x78.接收方接收到这4个字节之后需要去重组得到0x12345678(而不是得到0x78563412)。 + ^, a# W4 c. e+ {$ I% S (2)所以在通信双方需要有一个默契,就是:先发/先接的是高位还是低位?这就是通信中的大小端问题。 (3)一般来说是:先发低字节叫小端;先发高字节就叫大端。在实际操作中,在通信协议里面会去定义大小端,明确告诉你先发的是低字节还是高字节。 (4)在通信协议中,大小端是非常重要的,大家使用别人定义的通信协议还是自己要去定义通信协议,一定都要注意标明通信协议中大小端的问题。 四、总结: 上面分享了一些我们常用的一些用法,掌握了这些就可以了,当日后工作中有其他用法,再总结归纳,完善自己的知识体系。 + d5 q9 ^! K% Q$ c |