C提高~复合数据结构类型

复合数据结构类型

引言

C语言中复杂的组合数据类型,如数组、字符串、结构体、共用体、枚举等其他组合类型,它们是如何在内存中开辟空间的,以及这些组合数据类型的特点又是如何在内存中体现出来的。现有的操作系统计算机上,为了实现内存的高效利用,操作系统对所有物理内存进行了统一的内存管理,所以应用程序表现出来的都是虚拟内存

管理方式

在C语言程序中,存放数据所能使用的内存空间大概分为四种情况:栈(stack)、堆(heap)、数据区(.data和.bss区)和常量区(.ro.data)

栈内存特点

空间实现自动管理运行时空间自动分配,运行结束空间自动回收。栈是自动管理的,程序员不需要手工干预,方便简单,因此栈又称为自动管理区

能够被反复使用:栈内存在程序中用的都是一块内存空间,程序通过自动开辟和自动释放,会反复使用这一块空间。

脏内存栈内存由于反复使用每次使用后程序不会去清空内容,当下一次该空间再次被分配时上一次使用的值会还在

临时性:函数不能返回栈变量的指针,因为该空间在函数运行结束之后就会被释放

/*函数不能返回局部变量的地址,因为该函数执行完后,存于函数栈的局部变量就已经不存了,如果返回地址
其访问该空间的话,该空间很有可能已经被别人获取正在使用了*/
#include <stdio.h>

int func(void){
  /*a是局部变量,分配在栈上又叫栈变量(临时变量)*/
  int a = 4;
  printf("&a = %p\n",&a);
  return &a;
}

void func(void){
  int a = 33;
  int b = 33;
  int c = 33;
  printf("int func2, &a = %p\n",&a);
}

int main(void){
  int *p = NULL;
  p = func();
  func2();
  func2();
  printf("p = %p\n");
  printf("*p = %d.\n",*p);    //运行之后*p等于33,证明栈内存用完之后是脏的,临时的

  return 0;
}

运行结果

&a = 000000000062FDDC
int func2, &a = 000000000062FDD4
int func2, &a = 000000000062FDD4
p = 00007FFDCD56FA30
*p = 0.

func()函数运行结束函数体内定义的局部变量自动释放,这时看出p地址和&a是不一致的,*p值为0,未初始化的变量默认值为0;

/*栈出溢出:因为操作系统事先给定了栈的大小,
  如果在函数中无穷尽的分配局部变量,栈内存总能用完*/
#include <stdio.h>

void stack_overflow(void){
  int a[10000000] = {0};
  a[10000000 -1]  = 12;
  return ;
}

//下面函数为递归函数
void stack_overflow2(void){
  int a = 2;
  stack_overflow2();
  return ;
}


int main(void){
  //stack_overflow(); //core dumped
  stack_overflow2();
  return 0;
}

上面两个函数的运行结果Segmentation fault(core dumped)证明栈溢出了。

堆内存特点

灵活:堆时另一种管理形式的内存区域,堆内存的管理灵活。

内存量大:堆内存空间很大,进程可以按需手动申请,使用完手动释放。

程序手动申请和释放:写代码去申请malloc和释放free。

脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此惹事脏的。

临时性:堆内存在malloc后和free之前的这期间可以被访问。在malloc之前free之后不能访问,否则会有不可预料的后果。

#include <stdio.h>
#include <stdlib.h>

int main(void){
  //需要一个1000个int类型元素的数组
  //第一步:申请和绑定
  int *p = (int *)malloc(1000*sizeof(int));
  //第二步:检验申请是否成功
  if(p == NULL){
    printf("malloc error.\n");
    return -1;
  }
  //第三步:使用申请的内存
  *(p+0) = 1;
  *(p+1) = 2;
  
  //第四步:释放
  free(p);
  return 0;
}

如果最后没有将分配的堆内存空间释放的话,这块内存空间会被一直占用,只有当整个程序终止后才会释放。所以对堆内存来说,使用完后及时使用free释放空间就显得非常重要,否则会导致内存泄露,即内存空间都还在 ,但是该空间被之前程序使用后,后续不再使用了,但是它却一直占着

malloc

使用malloc分配空间时,返回实际上是一个void *类型的指针(地址),该地址是本次申请内存空间的首字节地址,失败返回NULL(使用前需检查是否为NULL),使用完需用free释放void类型(空类型)不表示没有类型,而表示万能类型,在需要时在具体指定void*表示是一个无类型指针,对于32位系统,指针本都是4个字节

值得注意的是:调用free归还p所指向的堆内存之前指向这段内存的指针p的指向需要发生改变指向了其他的地方的话,必须通过一个中间指针变量先记住p指向的堆空间,之后free时才能通过这个中间变量释放之前p所指向的堆空间,否则就会造成之前p所指的堆空间无法释放导致内存泄露的发生

内存中的各个段

代码段、数据段、bss段

编译器在编译程序时,程序会按照一定的结构被划分成各个不同的段进行组织,即.text、.bss、.data段等

代码段(.text):代码存放程序的代码部分,程序中各种函数的指令就存放在该段

数据段:又称数据区、静态数据区、静态区程序中的静态变量空间开辟于此,值得注意:全局变量是整个程序的公共财产,而局部变量只是函数的私有财产

.bss段:又叫ZI(Zero Initial)段,所有未初始化的静态变量的空间就开辟于此,这个段会自动将未初始化静态空间初始化为0

注意:数据段(.data)和.bss段实际上没有本质区别,都是用来存放程序中的静态变量,只是.data存放显式初始化为非0的静态数据,而.bss中存放那些显式初始化为0或者未显式初始化的静态数据

特殊数据会被放到代码段

#include <stdio.h>
int main(void){
  char *p = "linux";
  *(p+0) = 'f';        //运行报错,因为是字符串常量,不能被修改
  printf("p = %s.\n",p);

  return 0;
}

内存管理方式小结

栈、堆和静态这三种内存管理方式都可以为程序提供内存空间。栈空间用于开辟局部变量空间,实现自动内存管理;对于堆内存,程序中需要使用malloc进行手动申请,使用完后必须使用free进行释放,实现手动内存管理静态数据区的数据段,专门用于开辟全局变量和静态变量不需要程序员参与管理。

只是在函数内部临时使用作用范围希望被局限在函数内部,即定义局部变量

堆内存和数据段几乎拥有完全相同的属性,大部分时候是可以相互替换。但是他们生命周期不同堆内存的生命周期是从malloc开始到free结束,而静态变量程序一开始执行就被开辟,直到整个程序结束才回收,伴随程序运行一直存在。所以,若变量只是在程序的一个阶段期间有用适合使用堆内存空间;若变量需要在程序运行的整个过程中一直存在适合使用全局变量

字符串类型

C语言使用指针来管理字符串,例如char *p = "linux",此时p为字符串,但p本质上是一个指针变量p中存放了字符串的第一个字符的地址,该地址即为字符串的地址

字符串的本质

字符串的本质为指向字符串的存放空间的指针,C中使用ASCII码对字符进行编码,编码后用char型变量来表示一个字符,所以字符串就是由多个字符打包在一起共同组成的,本质上和字符数组没有什么区别,只是使用了‘\0’字符作为结尾符反映在内存中字符串是由多个字节连续分布构成的,每个字符占用了一个字节。其中涉及要点如下:

  1. 用一个指针指向字符串头
  2. 固定尾部(字符串总是以'\0'来结尾);
  3. 组成字符串的各字符的地址彼此连续

指向字符串的指针变量空间和字符串存放的空间是分开的

还是看这个例子char *p = "linux"p是一个字符指针变量,占4个字节。p可以是全局变量或局部变量;而"linux"存储于代码段,占6个字节,实际上总共消耗了10个字节,其中4个字节用于存放字符串第一个字符的地址,5个字节用于存放linux这五个字符,最后一个用于存放‘\0’字符串结尾符

存储多个字符的两种方式----字符串和字符数组

当有多个连续字符需要存储时,有两种方式,第一种是字符串,第二种是字符数组,如下:

#include <stdio.h>
int main(void){
  char *p = "linux";    //字符串
  char a[] = "linux";   //字符数组
  return 0;
}

sizeof

sizeof是C语言中一个关键字,也是一个运算符,使用方法为sizeof(类型或变量名)sizeof运算符返回的是类型或者变量所占用的字节数。一像int、double等原生类型占用字节数和平台有关,使用sizeof可测试不同平台下类型所占用的字节数;二除了ADT之外还有UDT,用户自定义类型中占用字节数可以使用sizeof运算符查看。

strlen库函数

size_t strlen(const char *s);

用于计算并返回字符串你的实际长度,该函数接受一个字符串的指针,返回值为字符串的长度(以字节为单位)。注意:strlen返回的字符串长度不包含结尾'\0'。

#include <stdio.h>
#include <string.h>
int main(){
  char *p = "linux";
  int len = strlen(p);

  printf("sizeof(p) = %d.\n",sizeof(p));
  printf("len = %d\n",len);    //len = 5
  return 0;
}
//64为操作系统
sizeof(p) = 8. 
len = 5

小结:在sizeof是测试字符指针变量p本身的长度,和字符串长度无关,而strlen是用来计算字符串中字符个数的(不包含'\0'),所以strlen的结果为5。
       sizeof(数组名)得到的永远是数组字节数与有无初始化没有任何关系strlen用于计算字符串的长度只有传递合法的字符串地址才有效,若随便传递一个字符指针,但该字符指针指向的并不是一个字符串,是没有意义的,如下例:

#include <stdio.h>
#include <string.h>

int main(void){
  char a[] = "windows";    //a[0] = 'w';a[1]='i';.....a[6] = 's';a[7]='\0';
  printf("sizoef(a) = %d.\n",sizeof(a));    //8
  printf("strlen(a) = %d.\n",strlen(a));    //7
  
  char b[5] = "windows";    //字符串“Windows”个数大于5所以编译器会将字符串里的字符'w'和's'去掉
  printf("sizoef(b) = %d.\n",sizeof(b));    //5
  printf("strlen(b) = %d.\n",strlen(b));    //5

  char c[5] = {0};                        //c[0] = 0;
  printf("sizoef(c) = %d.\n",sizeof(c));    //5
  printf("strlen(c) = %d.\n",strlen(c));    //0
   
  return 0;
}

运行结果

char b[5] = "windows"; [Warning] initializer-string for array of chars is too long
sizoef(a) = 8.
strlen(a) = 7.
sizoef(b) = 5.
strlen(b) = 5.
sizoef(c) = 5.
strlen(c) = 0.

小结:若在定义数组时,没有明确给出数组大小,需要在初始化时给定,编译器会根据初始化时的字符个数去自动计算数组空间大小。

字符数组与字符串

字符数组char a[] = "linux";定义一个数组a,数组a占6个字节右值"linux"本身只存在于编译器中编译器用它来初始化字符数组a后就弃掉,该字符串的字符被存放于数组中,等价于 char a [] = {'l','i','n','u','x'};

 

字符串char *p = "linux"定义了一个字符指针p,p占4个字节,分配在栈上;同时定义了一个字符串“linux”,分配在代码段中,然后把代码段中的字符串的首地址(即‘l’的地址)赋值给p;

 

总结对比,字符数组自带内存空间,可以直接存放字符数据,而字符串只是一个字符指针变量,只占4个字节,字符只能存到别地方,然后把其首地址存在p中

结构体

定义结构体时需要先声明结构体类型,然后在用结构体类型来定义结构体变量,也可以在定义结构体类型的同时定义结构体变量

//1 定义类型
struct people{
  char name[20];
  int age;
};

//2 定义类型的同时定义变量
struct student{
  char name[20];
  int age;
};

//3 将结构体struct student重命名为s1,s1是一个类型名,不是变量
typedef struct student{
  char name[20];
  int age;
}s1;

数组与结构体比较

数组有两个缺陷,第一个定义时必须明确给出大小且以后大小无法更改第二个数组要求所有元素类型必须一致结构体用来解决数组第二个缺陷,可将结构体理解为其中元素类型可以不相同的数组;只是通常请款下数组使用较简单;

数组访问方式有两种下标方式和指针方式,但实质上都是指针方式结构体变量中的元素访问方式只有一种,用句点.或箭头 ->方式,其两种方式实质一样使用地址进行访问;当使用指针时可使用句点.访问,只是写法复杂,而使用箭头替代,更加简洁

结构体对齐访问

结构体中元素访问,本质上还是指针方式,结构该元素在整个结构体中偏移量和该元素类型来访问,但实际上结构体的元素偏移量比较复杂,还需考虑元素对齐访问,结构体实际占用的字节数与所有成员占用字节数总和不一定相等

#include <stdio.h>
struct s{
  char c;
  int a;
};

int main(){
  printf("sizeof(struct s) = %d.\n",sizeof(struct s));    //8
  
  return 0;
}

访问结构体元素时需要对齐访问,主要为了配合硬件,即硬件本身有物理上的限制,对齐排布和访问可以提高访问效率内存本身是一个物理器件(DDR内存芯片,SOC上的DDR控制器),本身有一定的局限性:如果内存每次访问时按照4个字节对齐访问,效率最高(牺牲内存空间换取速度性能);若不对齐访问,效率要低很多。

结构体对齐的规则和运算

编译器本身可设置内存对齐规则,以下规则需要记住:

32位编译器,一般编译器默认对齐方式是4个字节

#include <stdio.h>
struct mystruct1{            //1字节对齐        //4字节对齐
  int a;                     // 4              // 4
  char b;                   //  1             //  2
  short c;                 //   2            //   2
};

int main(void){
  //4字节对齐
  printf("sizeof(struct mystruct1) = %d.\n",sizeof(struct mystruct1));
  return 0;
}

运行结果

sizeof(struct mystruct1) = 8.

整个结构体变量4个字节对齐是有编译器保证的,第一个元素a的第一个字节地址为整合结构体的起始地址,所以自然是4字节对齐的。但是a的结束地址由下一个元素说了算,然后第二元素b,因为上一个元素a本身占4个字节,本身就是对齐的。所以留给b的开始地址也是4个字节对齐地址。所以b可直接放b放的位置就决定了a一共占4个字节,因为不需要填充b的起始地址定了后,结束地址不能定(因为可能需要填充),结束地址要由下一个元素来定。然后第三个元素c,short类型需要2个字节必须放在类似0、2、4、8这样地址处,不能存放在1,3这样的奇数地址处,因此c不能紧挨着b存放,需要在b之后添加1字节的填充(padding),然后在开始放c当整个结构体的所有元素都对齐存放后,还没结束,因为整个结构体大小还要是4的整数倍

结构体对齐总结

当编译器将结构体设置为4个字节,称之为自动对齐,反之,使用#program进行对齐是就是手动对齐。设置手动对齐的命令有两种:

  • 第一种#program pack(),设置编译器1个字节对齐(即取消对齐);
  • 第二种#program pack(n),表示n=4表示4字节对齐,若为n=8即为8字节对齐;
#include <stdio.h>
#pragma pack(4)           //4字节对齐

struct mystruct1{
  int a;      // 4
  char b;     // 1(+1) 2
  short c;    // 2
};

struct mystruct2{
  char a;     //1(+3) 4
  int b;     // 4
  short c;   // 2(+2) 4
};

typedef struct mystruct5{
  int a;                // 4
  struct mystruct1 s1;  // 8
  double b;            //  8
  int c;               //  4
}MyS;

struct stu{
  char sex;           //1(+3) 4
  int length;         // 4
  char name[10];     // 1*10(+2) 12
};
#pragma pack() 

int main(void){
  printf("sizeof(struct mystruct1) = %d.\n",sizeof(struct mystruct1));
  printf("sizeof(struct mystruct2) = %d.\n",sizeof(struct mystruct2));
  printf("sizeof(MyS) = %d.\n",sizeof(MyS));
  printf("sizeof(struct stu) = %d.\n",sizeof(struct stu));
  return 0;
}

运行结果

sizeof(struct mystruct1) = 8.
sizeof(struct mystruct2) = 12.
sizeof(MyS) = 24.
sizeof(struct stu) = 20.

没有字节对齐(1字节对齐)

sizeof(struct mystruct1) = 7.
sizeof(struct mystruct2) = 7.
sizeof(MyS) = 23.
sizeof(struct stu) = 15.

GCC推荐对齐指令

使用_attribute_((packed))和_attribute_((aligned(n)))时,直接放在类型定义后面,那么该类型就以指定的方式进行对齐。packed作用是取消对齐,aligned(n)表示对齐方式。接下来看个例子

#include <stdio.h>
//不使用内存对齐
struct mystruct11{
  int a;
  char b;
  short c;
}_attribute_((packed));

//使用内存对齐
/*
 aligned(n):当n(1、2、4)小于或等于4时,结构体struct mystruct22对齐字节是22(4、2、2、4)
            当n大于4时,结构体struct mystruct22的对齐字节:n
*/
struct mystruct22{
  int a;
  char b;
  short c;
  short d;
}_attribute_((aligned(1024))) My22;


offsetof宏与container_of宏

结构体变量访问各个元素,本质上是通过指针方式来访问的,形式上是句点“.”的方式来访问(这时其实是编译器帮我们自动计算了偏移量)。

offsetof宏的作用是计算结构中某个元素相对结构首地址的偏移量,实质是通过编译器来帮用户计算。原理是虚拟一个TYPE类型的结构体变量,然后TYPE.MEMBER的方式访问MEMBER元素,继而得到MEMBER相对于整个变量首地址的偏移量。

#include <stdio.h>

#define offsetof(TYPE,MEMBMER)  ((int)&((TYPE *)0)->MEMBMER)

struct mystruct{
  char a;
  int b;
  short c;
};

/*
TYPE是结构体类型,MEMBMER是结构体中一个元素的元素名
这个宏返回的是MEMBMER元素相对于整个结构体变量的首地址的偏移量,类型是int
*/

int main(void){
  struct mystruct s1;
  s1.b = 12;

  int *p = (int *)((char *)&s1 + 4);
  printf("*p = &d.\n", *p);    //根据结构体对齐计算得出的
  
  return 0;
}

运行结果

*p = 12.

(TYPE *)0是一个强制类型转换,把0地址强制类型转换成一个指针, 该指针指向一个TYPE类型的结构体变量,实际上这个结构体变量可能不存在,但是只要不去解引用这个指针就不会出错

对于((TYPE)0)->MEMBMER来说,(TYEP *)0表示一个TYPE类型的结构体指针。通过指针来访问这个结构体变量的MEMBMER元素&((TYPE *))->MEMBMER等效于&(((TYPE *)0)->MEMBMER)-&(((TYPE *)0)),这就得到成员的偏移量

container_of宏

/*
  ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素名
  该宏返回是指向该结构体变量的指针,类型是(type *)
*/
#define container_of(ptr,type,member)({
const typedef ((type *)0->member)*__mptr = (ptr); \
(type *)((char* )__mptr - offsetof(type,member));
})

作用:知道一个结构体变量中某个成员的指针反推该结构体变量的指针。有了container_of宏,可以从一个成员的指针得到整个结构体变量的指针,继而得到结构体中其他成员的指针

typeof关键字的作用:通过typeof(a)由变量a得到a的类型,所以typeof的作用是由变量得到变量的数据类型

宏的工作原理:先用typeof得到member成员类型将member成员的指针转成自己类型的指针,然后用该指针减去该成员相对于整个结构体变量的偏移量偏移量用offset宏得到),之后得到整个结构体变量的首地址,在把该地址强制类型转换为type*

小结

  1. 基本要求:必须会用这两个宏,知道他们接收什么参数,返回什么值,会用两个宏来写代码,理解别人代码中两个宏的意思
  2. 升级要求:能理解这两个宏的工作原理,能表述出来。
  3. 高级要求:能自己写出这两个宏

共用体(union)

共用体例子分析

#include <stdio.h>
struct mystruct{
  int a;
  char b;
};
/*
a和b其实指向同一块内存空间,只是对这块内存空间的两种不同的解析方式。
若使用u1.a,那么就按照int类型来解析这个内存空间;
若使用u1.b,那么就按照char类型来解析这块内存空间;
*/
union myunion{
  int a;
  char b;
};

int main(void){
  struct mystruct s1;
  s1.a = 23;
  printf("s1.b = %d.\n",s1.b);    //s1.b = 0;结论s1.a和s1.b是独立无关的

  union myunion u1;               //共用体变量的定义
  u1.a = 24;
  printf("u1.b = %d.\n",u1.b); 
  u1.a = 'c';                      //共用体元素的使用
  printf("u1.b = %c.\n",u1.b);    //u1.b =23;结论是u1.b和u1.a是相关的,a和b地址一样
                                 //充分说明a和b指向同一块内存,只是对这块内存解析规则有所不同
  u1.b = 'd';  
  printf("u1.b = %c.\n",u1.b); 
  return 0;
}

运行结果

s1.b = 0.
u1.b = 24.
u1.b = c.
u1.b = d.

共用体总结

共用体union和结构体struct类型声明、变量定义和使用方法上很相似

共用体和结构体不同结构体类似于一个包裹,其中的成员彼此是独立存在的,分布在内存的不同单元中,它们只是被打包成了一个整体叫结构体;共用体中的各个成员其实是一体的,彼此不独立,使用同一个内存单元。可理解为:同一个内存空间有多种解释方式(有时候为这个元素,有时候是那个元素)。

union的sizeof测到大小实际是union中各个元素里占用内存最大的那个元素的大小

union中元素不存在内存对齐问题,因为union中实际只有一个内存空间,都是从一个地址开始的开始地址就是整个union占用的内存空间的首地址,所以不涉及内存对齐。

共用体和结构体区别

  1. 相同点:操作语法几乎相同;
  2. 不同点:struct是多个独立元素(内存空间)打包在一起;union是一个元素(内存空间)的多种解析方式

主要用途

  1. 在那种对同一单元进行多种不同规则解析的情况下;
  2. C语言中可用指针和强制类型转换代替共用体完成同样的功能,但共用体的方式更简单、便捷、好理解。

大小端模式

大端模式(big endian)和小端模式(little endian),最早是在串口等通信中一次只能发送1个字节。当要发送一个int类型的数就遇到问题,int类型有4个字节,按照:byte0,byte1,byte2 byte3这样顺序发送,还是按照byte3 byte2 byte1 byte0顺序发送?规则就是发送方和接受方必须按照同样的字节顺序来通信,否则就会出现错误,这就叫通信系统中的大小端模式

现在将大小端米模式更多指计算机存储系统的大小端,在计算机内存/硬盘/Nand中,存储系统是32位但数据仍是按照字节为单位存放的。于是32位的二进制在内存中存储时有两种分布方式高字节对应低地址(大端模式)、高字节对应高地址(小端模式)以大端模式存储,其内存布局如下所示:

 

 

大端模式

以小端存储,其内存布局如下:

小端模式

大端模式和小端模式本身没有对错、没有优劣,理论上按照大端或小端都可以,但是要求存储时和读取模式是必须一致。实际应用中,大端如C51单片机,小端如ARM(大部分用小端模式,大端模式不算多)。写代码时,需要用代码来检测当前系统的大小端。如用C语言写一个函数来测试当前机器的大小端模式

使用union来测试机器的大小端模式

#include <stdio.h>
/*
共用体中: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;
  return u1.b;
}

int main(void){
  int i = is_little_endian();
  if(1 == i){
    printf("小端模式\n");
  }else{
    printf("大端模式\n");
  }

  return 0;
}

运行结果(x86-GCC-64)

小端模式

分析:

如果是以小端模式存储,u1.a内存布局如下所示:

则u1.b = 1;共用体中元素使用同一块内存空间;

如果是以大端模式存储,u1.a的内存布局如下:

则u1.b等于0;为什么将只使用一个字节?b是char类型变量,占一个字节。 

用指针方式来测试机器的大小端

#include <stdio.h>

int is_little_endian2(void){
  int a = 1;
  char b = *((char *)(&a));        //指针方式其实就是共用体的本质
  
  return b;
}

int main(void){
  int i = is_little_endian2();
   if(1 == i){
     printf("小端模式\n");   
  }else{
    printf("大端模式\n");
  }

  return 0;
}

分析:char b = *((char *)(&a))

 

如果a = 1是以小端模式,则b等于1;如果a = 1是以大端模式,则b等于在0;

信系统中的大小端(数组的大小端)

如果通过串口发送一个0x12345678给接收方,但是因为串口本身限制,只能以字节为单位来发送,所以需要发4次接受方分4次接收,内容分别是0x12、0x34、0x56、0x78。接收方接受发到这四个字节之后需要去重组得到0x12345678,而不是得到0x78563412。所以通信双方需要有一个约定,先发/先接的是高位还是低位?这就是通信中的大小端问题,一般,先发低字节较小端;先发高字节叫大端。实际操作中,在通信协议里会去定义大小端,明确告诉你先发的是低字节还是高字节。

在通信协议中大小端是非常重要的,不管是使用别人定义的通信协议而还是自己定义的通信协议,一定都要注意在通信协议中注明大小端问题

枚举enum

枚举的作用

枚举在C语言中其实就是一些符号常量集,枚举定义了一些符号,该符号的本质是Int类型的常量,每个符号和一个常量绑定。符号表示一个自定义的识别码,编译码对枚举的认知是符号常量所绑定的那个int类型的数字。

#include <stdio.h>
//这个枚举用来表示函数返回值,ERROR表示错,RIGHT表示对
enum return_value{
  ERROR,
  RIGHT,
};
enum return_value func1(void);

int main(void){
  enum return_value r = func1();
  if(r == RIGHT)        //不是r.RIGHT,也不是return_value.RIGHT
  {
     printf("函数执行正确\n");
  }
  else{
     printf("函数执行错误\n");
  }
  printf("ERROR = %d.\n",ERROR);    //ERROR = 0
  printf("RIGHT = %d.\n",RIGHT);    //RIGHT = 1
  
  return 0;
}

enum return_value_func1(void){
  enum return_value r1;
  r1 = ERROR;
  
  return r1;
} 

运行结果

函数执行错误
ERROR = 0.
RIGHT = 1.

对于枚举符号变量来讲,数字不重要,符号才重要符号对应的数字只要彼此不相同即可,没有别的要求。所以一般情况下我们都不会明确指定这个符号所对应的数字,而是让编译器自动分配编译器自动分配原则是,从0开始依次增加,如果用户定义一个值,则从定义的那个值开始往后一次增加

宏定义和枚举区别

枚举是将很多有关联的符号封装在一个枚举中,而宏定义是完成分散的。什么时候用枚举?当要定义的常量是一个有限集合时(如一个星期有7天,一个月有31天,一年有12个月等)最适合枚举。不能用枚举的情况下(定义常量符号之间无关联或者无限的)用宏定义。

宏定义最先出现,用来解决符号常量的问题,后来发现有时候定义的符号常量彼此之间有关联(多选一的关系),可以用宏定义,但不贴切,于是发现了枚举来解决该情况。

枚举定义

//1 分别定义类型和变量
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,
};
  enum week today1;
// 3 定义类型的同时定义变量
enum {
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT
};

//4 用typedef定义枚举类型别名,并在后面使用别名进行遍历定义
typedef enum week{
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT
};
  week today3;
// 5 用typedef定义枚举的别名
typedef enum{
  SUN,        //SUN = 0
  MON,        //MON = 1
  TUE,
  WEN,
  THU,
  FRI,
  SAT

}week;

不能有重名的枚举类型

即在一个文件中不能有两个或两个以上的enum被typedef成相同的别名。因为将两种不同类型重名为相同的别名,这会让gcc在还原别名时遇到困惑。如下定义:

typedef int INT; typedef char INT;

那么INT到底被译为int还是char,就无法确定。

不能有重名的枚举成员

两个struct类型内的成员可以重名,而两个enum类型中成员却不可以重名。因为struct类型成员的访问方式为“变量名.成员”,而enum成员的访问方式为“成员名”,因此若两个enum类型中有重名的成员,那代码中访问这个成员时到底访问的是enum中的哪个成员呢,无法确定。但是两个#define宏定义是可以重名的,该宏名真正的值取决于最后一次定义的值。编译器会给出警告单不会出现error

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值