|
上一个专题我们详细的分享了c语言里面的结构体用法,读者在看这些用法的时候,可以一边看一边试验,掌握了这些基本用法就完全够用了,当然在以后的工作中,如果有遇到了更高级的用法,我们可以再来总结学习归纳。好了,开始我们今天的主题分享。 一、共用体union: 1、什么是共用体union? 这个共用体,估计大家平时在代码也比较少见,我去看了一下stm32的例程里面没怎么看到这个用法(下面的示例分享是在stm32里面找的);其实这个共用体union(也叫联合体)跟我们上次分享的结构体定义是非常像的,比如说:类型定义、变量定义、使用方法上很相似。就像下面两个例子一样,把许多类型联合在一起(不过虽然形式上类似,但是具体用法还是有区别的,下面会讲他们之间的区别): union st{int a; char b; };
2、共用体与结构体的区别: 结构体类似于一个包裹,结构体中的成员彼此是独立存在的,分布在内存的不同单元中,他们只是被打包成一个整体叫做结构体而已;共用体中的各个成员其实是一体的,彼此不独立,他们使用同一个内存单元。可以理解为:有时候是这个元素,有时候是那个元素。更准确的说法是同一个内存空间有多种解释方式。所以共用体用法总结如下:
3、代码实战: typedef union{ int a; char c; //int a; // int b; }st; int main(void) { st haha; haha.c='B'; // haha.a=10; //haha.b=60; printf("the haha size is %d\n",sizeof(haha)); printf("haha.c=%d\n",haha.c); return 0; } typedef union{ int a; char c; int b; }st; int main(void) { st haha; haha.c='B'; haha.a=10; haha.b=60; printf("the haha size is %d\n",sizeof(haha)); printf("haha.c=%d,haha.a=%d,haha.b=%d\n",haha.c,haha.a,haha.b); 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); return 0; } 演示结果: haha.c=66 haha.c=60,haha.a=60,haha.b=60 the a is 0x61feac the c is 0x61feac the b is 0x61feac 说明: 通过上面的代码示例,读者可以发现这个共用体的大小,并不是像我们之前结构体那样是把每个成员所占内存大小加起来,而是我们上面说的那样,共用体由成员占用内存大小最大的那个决定的,上面的示例中int 占用4个字节大小,为最大的,所以sizeof(haha)得出结果就是4个字节大小,而且读者细心可以发现到打印出来的结果a和b都是60,它是访问内存占用大小最大的那个成员的数值,因为那个'B'的acii码值是是66;通过示例,我们也发现共用体访问其成员方式跟结构体是一样的(上面也有说到过)。下面是和结构体做对比的代码示例: // 共用体类型的定义 struct mystruct { int a; char b; }; // a和b其实指向同一块内存空间,只是对这块内存空间的2种不同的解析方式。 // 如果我们使用u1.a那么就按照int类型来解析这个内存空间;如果我们使用 // u1.b那么就按照char类型 // 来解析这块内存空间。 union myunion { int a; char b; double c; }; int main(void) { struct mystruct s1; s1.a = 23; printf("s1.b = %d.\n", s1.b); // s1.b = 0. 结论是s1.a和s1.b是独立无关的 printf("&s1.a = %p.\n", &s1.a); printf("&s1.b = %p.\n", &s1.b); union myunion u1; // 共用体变量的定义 u1.a = 23; u1.b='B'; u1.a=u1.b; // 共用体元素的使用 printf("u1.a = %d.\n", u1.a); 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指向同一块内存,只是对这块内存的不同解析规则 printf("&u1.a = %p.\n", &u1.a); printf("&u1.b = %p.\n", &u1.b); printf("the sizeof u1 is %d\n",sizeof(u1)); return 0; } 演示结果: s1.b = 22.&s1.a = 0061FEA8. &s1.b = 0061FEAC. u1.a = 66. u1.b = 66. u1.c = 66.4、 &u1.a = 0061FEA0. &u1.b = 0061FEA0. the sizeof u1 is 8 4、小结:
二、枚举 1、什么是枚举? 枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。枚举符号常量和其对应的常量数字相对来说,数字不重要,符号才重要。符号对应的数字只要彼此不相同即可,没有别的要求。所以一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。(编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加)。 2、为什么要用枚举,和宏定义做对比: (1)C语言没有枚举是可以的。使用枚举其实就是对1、0这些数字进行符号化编码,这样的好处就是编程时可以不用看数字而直接看符号。符号的意义是显然的,一眼可以看出。而数字所代表的含义除非看文档或者注释。 (2)宏定义的目的和意义是:不用数字而用符号。从这里可以看出:宏定义和枚举有内在联系。宏定义和枚举经常用来解决类似的问题,他们俩基本相当可以互换,但是有一些细微差别。 (3)宏定义和枚举的区别:
(4)使用枚举情况:
总结: 宏定义先出现,用来解决符号常量的问题;后来人们发现有时候定义的符号常量彼此之间有关联(多选一的关系),用宏定义来做虽然可以但是不贴切,于是乎发明了枚举来解决这种情况。 3、代码示例: a、几种定义方法: enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }; enum week today; */ /* // 定义方法2,定义类型的同时定义变量 enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }today,yesterday; */ /* // 定义方法3,定义类型的同时定义变量 enum { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }today,yesterday; */ /* // 定义方法4,用typedef定义枚举类型别名,并在后面使用别名进行变量定义 typedef enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }week; */ /* // 定义方法5,用typedef定义枚举类型别名,并在后面使 用别名进行变量定义 typedef enum { SUN, // SUN = 0 MON, // MON = 1; TUE, WEN, THU, FRI, SAT, }week; b、错误类型举例(下面的举例中也加入了结构体作为对比): // types for ‘DAY’ typedef enum workday { MON, // MON = 1; TUE, WEN, THU, FRI, }DAY; typedef enum weekend { SAT, SUN, }DAY; */ /* // 错误2,枚举成员重名,编译时报错:redeclaration //of // enumerator ‘MON’ typedef enum workday { MON, // MON = 1; TUE, WEN, THU, FRI, }workday; typedef enum weekend { MON, SAT, SUN, }weekend; // 结构体中元素可以重名 typedef struct { int a; char b; }st1; typedef struct { int a; char b; }st2; */ 说明: 经过测试,两个struct类型内的成员名称可以重名,而两个enum类型中的成员不可以重名。实际上从两者的成员在访问方式上的不同就可以看出了。struct类型成员的访问方式是:变量名.成员,而enum成员的访问方式为:成员名。因此若两个enum类型中有重名的成员,那代码中访问这个成员时到底指的是哪个enum中的成员呢?所以不能重名。但是两个#define宏定义是可以重名的,该宏名真正的值取决于最后一次定义的值。编译器会给出警告但不会error,下面的示例会让编译器发出A被重复定义的警告。 #define A 5 #define A 7 int main(void) { printf("hello world\n"); return 0; } c、代码实战演示: #include <stdio.h>typedef enum week { SUN, // SUN = 0 MON, // MON = 1; TUE, //2 WEN, //3 THU, FRI, SAT, }week; int main(void) { // 测试定义方法4,5 week today; today = WEN; printf("today is the %d th day in week\n", today); return 0; } 演示结果: today is the 3 th day in weekd、接着我们把上面枚举变量改变它的值(不按照编译模式方式来),看看会发生什么变化: typedef enum week { SUN, // SUN = 0 MON=8, // MON = 1; TUE, //2 WEN, //3 THU, FRI, SAT, }week; int main(void) { // 测试定义方法4,5 week today,hh; today = WEN; hh=SUN; printf("today is the %d th day in week\n", SUN); printf("today is the %d th day in week\n", today); return 0; } 演示结果(我们可以看到改变了枚举成员值,它就在这个基础递增下面的成员值): today is the 10 th day in week 注意:
三、大小端模式: 1、什么是叫大小端模式? a、什么叫大端模式(big-endian)? 在这种格式中,字数据的高字节存储在低地址中,而字数据的低字节则存放在高地址中。 b、什么叫小端模式(little-endian)? 与大端存储格式相反,在小端存储格式中,低地址中存放的是字数据的低字节,高地址存放的是字数据的高字节。 2、实际解释: ----- 我们把一个16位的整数0x1234存放到一个短整型变量(short)中。这个短整型变量在内存中的存储在大小端模式由下表所示:
说明: 由上表所知,采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将低位存放在低地址。 3、代码实战来判断大小端模式: // 共用体中很重要的一点:a和b都是从u1的低地址开始的。 // 假设u1所在的4字节地址分别是:0、1、2、3的话,那么a自然就是0、1、2、3; // b所在的地址是0而不是3. union myunion { int a; char b; }; // 如果是小端模式则返回1,小端模式则返回0 int is_little_endian(void) { union myunion u1; u1.a = 1; // 地址0的那个字节内是1(小端)或者0(大端) return u1.b; } int is_little_endian2(void) { int a = 1; char b = *((char *)(&a)); // 指针方式其实就是共用体的本质 return b; } int main(void) { int i = is_little_endian2(); if (i == 1) { printf("小端模式\n"); } else { printf("大端模式\n"); } return 0; } 演示结果: 4、看似可行实则不行的测试大小端方式:位与、移位、强制类型转化: int main(void) { // 强制类型转换 int a; char b; a = 1; b = (char)a; printf("b = %d.\n", b); // b=1 /* // 移位 int a, b; a = 1; b = a >> 1; printf("b = %d.\n", b); //b=0 */ /* // 位与 int a = 1; int b = a & 0xff; // 也可以写成:char b printf("b = %d.\n", b); //b=1 */ return 0; } 说明: (1)位与运算: 结论:位与的方式无法测试机器的大小端模式。(表现就是大端机器和小 端机器的&运算后的值相同的) 理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关)。 (2)移位: 结论:移位的方式也不能测试机器大小端。 理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。 (3)强制类型转换和上面分析一样的。 5、通信系统中的大小端(数组的大小端) (1)譬如要通过串口发送一个0x12345678给接收方,但是因为串口本身限制,只能以字节为单位来发送,所以需要发4次;接收方分4次接收,内容分别是:0x12、0x34、0x56、0x78.接收方接收到这4个字节之后需要去重组得到0x12345678(而不是得到0x78563412)。 (2)所以在通信双方需要有一个默契,就是:先发/先接的是高位还是低位?这就是通信中的大小端问题。 (3)一般来说是:先发低字节叫小端;先发高字节就叫大端。在实际操作中,在通信协议里面会去定义大小端,明确告诉你先发的是低字节还是高字节。 (4)在通信协议中,大小端是非常重要的,大家使用别人定义的通信协议还是自己要去定义通信协议,一定都要注意标明通信协议中大小端的问题。 四、总结: 上面分享了一些我们常用的一些用法,掌握了这些就可以了,当日后工作中有其他用法,再总结归纳,完善自己的知识体系。 |
微信公众号
手机版