数组&字符串&结构体&共用体&枚举
程序中内存从哪里来1
程序执行需要内存支持
- 对程序来说,内存就是程序的立足之地(程序是被放在内存中运行的);程序运行时需要内存来存储一些临时变量。
内存管理最终是由操作系统完成的
- (1)内存本身在物理上是一个硬件器件,由硬件系统提供。
- (2)内存是由操作系统统一管理。为了内存管理方便又合理,操作系统提供了多种机制来让我们应用程序使用内存。这些机制彼此不同,各自有各自的特点,我们程序根据自己的实际情况来选择某种方式获取内存(在操作系统处登记这块内存的临时使用权限)、使用内存、释放内存(向操作系统归还这块内存的使用权限)。
三种内存来源:栈(stack)、堆(heap)、数据区(.data)
- (1)在一个C语言程序中,能够获取的内存就是三种情况:栈(stack)、堆(heap)、数据区(.data)
栈的详解
- 运行时自动分配&自动回收:栈是自动管理的,程序员不需要手工干预。方便简单。
- 反复使用:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。
- 脏内存:栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。
- 临时性:(函数不能返回栈变量的指针,因为这个空间是临时的)
- 栈会溢出:因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完。
程序中内存从哪里来2
堆内存详解
- 操作系统堆管理器管理:堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配。
- 大块内存:堆内存管理者总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放。
- 程序手动申请&释放:手工意思是需要写代码去申请malloc和释放free。
- 脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的。
- 临时性:堆内存只在malloc和free之间属于我这个进程,而可以访问。在malloc之前和free之后
- 都不能再访问,否则会有不可预料的后果。
堆内存使用范例
(1)void * 是一个指针类型,malloc返回的是一个void *类型的指针,实质上malloc返回的是堆管理器分配给我本次申请的内存空间的首地址(malloc 返回的值其实是一个数字,这个数字表示一个内存地址)。为什么要使用void *作为类型?主要原因是malloc帮我们分配内存是只分配了内存空间,至于这段空间将来被用来存储什么类型的元素malloc是不关心的,由程序自己来决定。
(2)什么是void类型。早起被翻译成空型。这个翻译非常不好,会误导人。void类型不表示没有类型,而表示万能类型。void的意思是这个数据类型当前是不确定的,在需要的时候可以再去指定它的具体类型。void *类型是一个指针类型,这个指针本身占4个字节,但是指针指向的类型是不确定的。换句话说,指针在需要的时候可以被强制转化为任何一种确定类型的指针,也就是说这个指针可以指向任何类型的指针。
(3)malloc的返回值:成功申请空间后返回这个内存空间的地址,申请失败的时候返回NULL。所以malloc的内存指针使用前一定要检验
(4)malloc申请的内存使用完后,要注意内存释放free。free(p);会告诉堆管理器这段内存已经使用完,堆管理器可以回收了。堆管理器回收了这段内存后,这段内存当前进程就不应该再使用了。因为释放后,堆管理器就可能吧这段内存分配给别的进程,所以你就不能再使用了。
(5)再调用free归还这段内存之前,指向这段内存的指针p一定不能丢(也就是p不能另外赋值)。因为p一旦丢失这段malloc来的内存就永远的丢失了(内存泄露)直到当前程序结束时,操作系统才会回收这段内存。
malloc的一些细节表现
- malloc(0):malloc申请0个字节本身就是一件无厘头的事情,一般不会碰到这个需要。如果真的malloc(0)返回的是NULL还是一个有效指针,答案是分配了16Byte的一段内存,并且返回了这段内存的地址。这个答案是不确定的,因为C语言并没有规定malloc(0)的表现,由各malloc函数库的实现来定义。
- malloc(4):gcc中malloc默认最小是16个字节位默认分配单位,如果malloc小于16字节,都会返回一个16字节的内存。malloc实现时任意字节的分配,而是允许一些大小的块来分配。
- malloc(20)去访问25、第250、第2500字节会怎么样。实战中120字节正确、1200字节正确、继续下去总有一个字节处开始出现段错误。
程序中内存从哪里来3
代码段、数据段、bss段
编译器在编译程序的时候,将程序中的所有的元素分成了一些组成部分,各部分构成了一个段。所以说段是可执行程序的组成部分。
- (1)代码段:代码段是程序中可执行部分,直观理解就是代码段就是函数堆叠组成的。
- (2)数据段(也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,至关立即额就是C语言程序中的全局变量。(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算函数的数据)
- (3)bss段(又叫做ZI(Zero initial)段):bss段的特点就是被初始化为0,bss段本质上也属于数据段,就是被初始化为0 的数据段
注意区分:数据段(.data)和bss段的区别和联系:二者本来没有本质区别,都是用来存放C语言中的全局变量的。区别在于我们把显示的初始化为非0 的全局变量存在.data段,而把显式初始化为0 或者并未显式初始化的全局变量存在.bss段
有些特殊数据会被放在代码段
- (1)C语言中,使用char *p = "linux";定义字符串时,字符串“linux”实际被分配在代码段,也就是说这个“linux”字符串实际上是一个常量字符串,而不是变量字符串。
- (2)const型的常量:C语言中const这个关键字用来定义常量,常量就是不能用来改变的量。const的实现方法至少有两种:第一种就是编程时把const修饰的变量放在代码段以实现不能修改(普遍见于各种单片机的编译器)。第二种就是由编译器来检查以确保const *的常量不会被修改。实际上我们const常量进而普通的常量放在数据段的(gcc中就是这样实现的)
显式初始化位非0 的全局变脸和静态局部变量放在数据段
- 放在.data段的变量有2种:第一种是显式初始化位非0的全局变量。第二种是静态局部变量,也就是static修饰的局部变量。(普通局部变量分配在站上,静态局部变量分配在.data段上)
未初始化或者显示初始化为0 的全局变量放在bss段
- (1)bss段和.data段并没有本质区别,几乎可以不用去区分这两种。
总结:C语言中所有变量和常量所使用的内存无非以上三种情况
- (1)相同点:三种获取内存的方法,都可以会给程序提供可用内存,都可以定义变量给程序用。
- (2)不同点:栈内存对应C中的普通局部变量(别的变量还用不了栈,而且栈是自动的,由编译器和运行时环境提供服务的,程序员无法来手工控制栈),堆内存完全独立于我们的程序存在和管理的,程序需要内存是可以手工去申请malloc,使用完成后必须尽快free释放。(堆内存对程序就相当于公共图书馆对于人);数据段对于程序来说对应C语言中的全局变量和静态的局部变量,他的维护和管理也是自动的。
- (3)如果我需要一段内存来存储数据,我究竟要把这个数据存在哪里?(或者说我要定义一个变量,我就究竟应该定义为局部变量还是全局变量还是用malloc来实现)。不同的存储方式有不同的特点。简单总结如下:
-
-
- 函数内部临时使用,出了函数就不会用到,就定义局部变量,
- 堆内存和数据段几乎拥有完全相同的属性,大部分时候是可以完全替换的,但是声明周期不一样。堆内存的生命周期是malloc开始free结束,而全局变量是从整个程序一开始执行就开始,直到整个程序结束才会消灭,伴随程序运行一生。其启示:如果变量只不过在一个阶段有用,用完就不用了,那么就适合用堆内存。如果这个变量和这个程序是一生相伴的,那么就使用全局变量来实现。(堆内存就好像是租房,而全局变量就好像是买房。堆内存就好像就好像是向图书馆介绍,全局变量就好像去书店买书)。你以后会发现买不如租,堆内存的使用比全局变量广泛。
-
-
C语言的字符串类型
C语言没有原生字符串类型
- (1)很多高级语言像java、C#等就有字符串类型,有个string来表示字符串,用法和int这些很像。可以string s1 = "linux";来定义字符串的变量
- (2)C语言没有string类型,C语言中的字符串时通过字符指针来间接实现的
C语言使用指针来管理字符串
- (1)C语言中定义字符串方法:char *p = "linux";此时p就叫做字符串,但是实际上p只是一个字符指针(本质上就是一个指针变量,只是p指向了一个字符串的其实地址而已)。
C语言中字符串的本质:指针指向头,固定尾部的地址相连的一段内存
- (1)字符串就是一串字符。字符反映在现实中就是文字、符号、数字等人用来表达的字符,反映在编程中就是字符类型的变量。C语言中使用ASCII编码对字符进行编程,编码后可以用char型变量来表示一个字符。字符串就是多个字符打包在一起共同组成的。
- (2)字符串在内存中其实就是多个字节连续分布构成的(类似于数组,字符串和字符数组非常像)
- (3)C语言中字符串有3个核心要点:第一是用一个指针指向字符串的头,第二是固定尾部(字符串总是以‘\0’来结尾)。第三是组成字符串的各字符的地址相连
- (4)'\0'是一个ASCII字符,其实就是编码为0的那个字符,(真正的0和字符0是不同的,数字0有自己的ASCII编码 )要注意区分'\0'和'0'、0的 区别。(0等同于'\0')
- (5)'\0'作为一个特殊的数字被字符串定义为字符串的结尾标志。产生的副作用就是字符串无法包含'\0'这个字符。这种思路就是“魔数”,就是选出来的一个特殊数字,这个数表示一个特殊含义,你的正式内容不能包含这个魔数作为内容。
注意:指向字符串的指针和字符串本身是分开的两个东西。
- (1)char *p = "linux";在这段代码中,p本质上是一个字符指针,占4个字节;“linux”分配在代码段占6个字节;实际上总共耗费了10个字节,这10个字节中:4字节的指针p就叫做字符串指针(用来指向字符串,理解为字符串的引子,但是本身不是字符串)5字节的用来存linux这5个字符的内存才是真正的字符串,最后一个用来存'\0'的内存是字符串的结尾标志。
存储多个字符的2种方式:字符串和字符数组
- (1)我们有多个连续字符(典型就是linux这个字符串)需要存储,实际上有两种方式:第一种就是字符串;第二种就是字符数组
字符串和字符数组的细节
字符数组初始化与sizeof、strlen
- (1)sizeof是C语言的一个关键字,也是C语言的一个运算符(sizeof使用的时候是sizeof(类型或者变量名),所以很多人误以为sizeof是函数,其实不是)。sizeof运算符用来返回一个类型或者是变量所占用的内存字节数。为什么需要sizeof运算符?主要原因之一是:int、double等原生类型占几个字节和平台有关;二是C语言中除了ADT之外还有UDT,这些用户自定义类型占几个字节无法一眼看出,所以用sizeof运算符来让编译器帮忙运算。
- (2)strlen函数是一个C语言库函数,这个库函数的原型是:size_t strlen(const char *s);这个函数接收一个字符串的指针返回这个字符串的长度,以字节为单位。注意一点:strlen返回的字符串长度是不包含字符串结尾的'\0'的。我们为什么需要strlen函数?因为从字符串定义(指针指向头、结尾是'\0'、中间依次相连)可以看出无法直接得到字符串的长度,需要用strlen来计算得到字符串的长度
- (3)sizeof(数组名)得到的永远是数组的元素个数(也就是数组大小),和数组中有无初始化,初始化多、少等式没有关系的;strlen是用来计算字符串的长度的,只能传递合法字符串进去才有意义,如果随便传递进去一个字符指针,但是这个字符指针并不是字符串时没有意义的。
- (4)当我们定义数组是,如果没有明确给出数组大小,则必须同时给出初始化式,编译器会根据初始化式自动计算数组大小(数组定义时必须同时给出大小、要么直接给。要么给初始化式)
字符串初始化与sizeof、strlen
(1)char *p = "linux";
- sizeof(p)得到的永远是4,因为这时候sizeof测的是字符指针p本身的长度,和字符串长度是无关的
(2)strlen刚好用来计算字符串长度
字符数组与字符串的本质差异(内存分配角度)
- (1)字符数组char a[ ] = "linux";来说,定义一个数组a,数组a占6字节,右值“linux”本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有linux这个字符串的)。这句就相当于是char a[ ] = {'l', 'i', 'n', 'u', 'x', '\0'}
- (2)字符串char *p = "linux";定义了一个字符指针p,p占4个字节,分配在栈上。同时还定义了一个字符串“linux”,分配在代码段。然后把代码中的字符串(一共占6字节)的首地址也就是字符‘l’的首地址赋值给p。
总结对比:字符数组和字符串有本质区别。字符数组本身是数组,数组自身自带内存空间,可以用来存东西(所以数组类似于容器),而字符串本身是指针,本身永远占4字节还不能用来存有效数据,所以只能把有效数据存到别的地方,然后把地址存在p。也就是字符数组自己存那些字符,而字符串需要额外的内存来存那些字符,字符串本身只存真正的那些字符所在的内存空间的首地址。
C语言之结构体概述
结构体类型是一种自定义类型
(1)C语言中的2中类型:原生类型和自定义类型
结构体使用时先定义结构体类型再用类型来定义变量
(1)结构体定义时需要先先定义结构体类型再用类型来定义变量
(2)也可以在定义结构体类型的同时定义变量
数组到结构体的进步之处
(1)结构体可以认为是从数组发展而来的。其实数组和结构体都算作数据结构的范畴,数组就是最简单的数据结构、结构体比数组更复杂一些,链表、哈希表之类的比结构体又复杂一些,二叉树、图等又更复杂一些。
(2)数组由2个明显的缺陷:第一个是定义时必须明确给出大小,且这个大小在以后不能更改。第二个是数组要求所有的元素类型必须一致更复杂的数据结构中就致力于解决这两个缺陷
(3)结构体用于解决数组的第二个缺陷,可以将结构体理解成一个其中元素类型可以不同的数组。结构体完全可以取代数组,只是在数组可用的范围内,数组更简单
结构体变量中的元素如何访问?
数组中元素的访问方式:表面上有两种方式(数组下标方式和指针方式),实质上都是指针方式访问
结构体变量中的元素访问方式:只有一种,用.或者->方式访问(.和->访问结构体元素的实质是一样的,只是C语言规定了用结构体变量来访问就用.而结构体指针的方式访问就用->,实际上高级语言已经不分了,都用.)
结构体的访问访问方式有点类似于数组下标方式
思考:结构体变量的点号或者->访问元素的实质是什么?其实本质上是指针来访问的。
结构体的对齐访问
举例说明什么是结构体对齐访问
- (1)上节讲过结构体中元素的访问,其实本质上海市指针方式访问,结合这个元素在整个结构体中的偏移量和这个元素的类型来进行访问的。
- (2)但是实际上结构体的元素的偏移量比上节讲的还要复杂,因为结构体要考虑内存对齐访问的问题,所以每个元素实际占的字节数和自己本身的类型所占的自己数不一定完全一样(譬如char c实际占字节数可能是1,也可能是2,也可能是3,也可能是4········)
- (3)一般来说,我们用.的方式来访问结构体元素时,我们是不用考虑内存对其的问题的。因为编译器会帮我们处理这个细节。但是因为C语言本身是很底层的语言,而且嵌入式开发经常要从内存的角度,以指针的方式来处理结构体及其中的元素,因此还是需要掌握结构体对齐规则。
结构体为什么要对齐访问?
- (1)结构体中元素对齐访问主要原因是为了配合硬件,也就是说硬件本身有物理上的限制,如果对齐排布和访问会提高效率,否则会大大降低效率
- (2)内存本身是一个物理层器件(DDR内存芯片,SoCDDR控制器),本身有一定的局限性:如果内存每次访问时按照4字节对齐访问,那么效率是最高的。如果不对齐访问效率会低很多
- (3)还有很多别的因素需要我们对齐访问,譬如icache的一些缓存特性,还有其他硬件(譬如MMU、LCD显示器)的一些依赖特性,所以会要求内存对齐访问
- (4)对比对齐访问和非对齐访问:对齐访问牺牲了内存空间换取了速度和性能,而非对齐访问牺牲了访问速度和性能,换取了内存空间的完全利用。
gcc支持但是不推荐的对齐指令:#pragma pack () #pragma park(n) (n=1/2/4/8)
(1)编译器本身可以设置内存对齐规则,有以下的规则需要记住,
第一个:32位编译器,一般编译器默认对齐方式是4字节对齐
总结下:结构体对齐的分析要点和关键:
- (1)结构体对齐要考虑:结构体整体本身必须安置在4字节处,结构体对齐后的大小必须是4的倍数,编译器设置4字节对齐时,如果编译器设置位8字节对齐,这里的4就是8
- (2)结构体中每个元素本身都必须对齐存放,而每个元素本身都有自己的对齐规则。
- (3)编译器考虑结构体存放时就以满足以上2点要求的最小需要来排布
gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8)
(1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器默认的对齐方式是4,但是有时候我们不希望对齐方式是4字节,而是希望设置为别的(譬如设置为1,也可能希望是8,甚至可能是128字节)
(2)常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack( ),这种就是设置编译器1字节对齐(有些人喜欢讲:编译器 不对齐访问,还有些讲取消编译器对齐访问);第二种是#pragma pack (4),这个括号中的数字就表示我们希望有多少字节对齐。
(3)我们需要#pragma pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对去参数就是n
#include<stdio.h> #pragma pack(1) struct student { //1字节对齐 //4字节对齐 char name; //1 //4 int age; //4 //4 short num; //2 //4 }; #pragma pack() int main(void) { struct student s1; printf("sizeof(s1) = %d.\n", sizeof(s1)); return 0; }
结果:sizeof(s1) = 7.
gcc推荐的对齐命令有两种:第一种是__attribute__((packed)) __attribute__((aligned(n)))
(1)_attribute_((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。
(2)_attribute_((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。他的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐)
#include<stdio.h> struct mystruct1 { //1字节对齐 //4字节对齐 int a; //4 字节 //4字节 char b; //1字节 //2字节 short c; //2字节 //2字节 }__attribute__((packed)); int main(void) { struct mystruct1 s1; printf("sizeof(s1) = %d.\n", sizeof(s1)); return 0; } 结果:sizeof(s1) = 7.
参考阅读blog:
http://www.cnblogs.com/dolphino520/archive/2011/09/17/2179466.html
http://blog.scdn.net/sno_guo/article/details/8042332
offsetof宏与结构体元素访问的本质
由结构体指针而访问各元素的原理
- (1)通过结构体整体变量来访问其中各个元素,本质上是通过指针方式访问的,形式上是通过.的方式来访问的(这时候其实是编译器自动帮我们计算了偏移量)
offset宏
- offset宏的作用是:用宏来计算结构体中某个元素和结构体的首地址的偏移量(其实质是通过编译器帮我们计算)
- offset宏的原理:我们虚拟一个type类型的结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于我们整个变量首地址的偏移量
- 学习思路:第第一步先学会用offsetof宏,第二步再去理解这个宏
- (TYPE *)0 这是一个强制类型转换,把0地址强制类型转换为一个指针,且这个指针指向一个type 类型的结构体变量,实际上这个结构体变量可能不存在,但是只要不去解引用就不会出错。
- ((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE 类型的结构体变量指针,通过指针来访问结构体变量的member元素。
- &((TYPE *)0)->MEMBER 等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址,但是因为我们整个结构体变量的首地址是0 ,所以该地址减去0值不变,但是含义却就是偏移量
#include<stdio.h> struct mystruct { char a; int b; short c; }; //TYPE是结构体类型,而MEMBER是结构体中一个元素名 //这个宏返回的是member元素相对于整个结构体的偏移量 #define offsetof(TYPE,MEMBER) ((int)&((TYPE *)0)->MEMBER) int main(void) { struct mystruct s1; s1.b = 12; int offsetofa = offsetof(struct mystruct, a); printf("offsetofa = %d.\n", offsetofa); int offsetofb = offsetof(struct mystruct, b); printf("offsetofb = %d.\n", offsetofb); int offsetofc = offsetof(struct mystruct, c); printf("offsetofc = %d.\n", offsetofc); return 0; } 结果:offsetofa = 0. offsetofb = 4. offsetofc = 8.
container_of宏:
- (1)作用:知道一个结构体某一元素的指针,反推这个结构体变量的指针。有了 container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针
- (2)typeof关键字的作用是:typeof(a)时,由变量a得到a的类型,typeof就是由变量名得到变量数据类型的
- (3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后就得到整个结构体变量的首地址,再把这个地址强制类型转换成type*即可。
#include<stdio.h> struct mystruct { char a; int b; short c; }; #define container_of(ptr, type, member) ({ \ const typeof(((type *)0)->member) * _mptr =(ptr); \ (type *)((char *)_mptr - offsetof(type, member));}) int main(void) { struct mystruct s1; struct mystruct *pS = NULL; short *p = &(s1.c); // p就是指向结构体中某个member的指针 printf("s1的指针等于:%p.\n", &s1); //问题是要通过p来计算得到s1的指针 pS = container_of(p, struct mystruct, c); printf("pS等于:%p.\n", pS); return 0; }
学习指南和要求:
- (1)最基本的要求:必须要学会这两个宏的使用。就是说能知道这两个宏接收什么参数,返回什么值,会用这两个宏来写代码。看见代码中别人用着两个红要能明白是什么意思
- (2)升级要求,能理解这两个宏的工作原理,能表述出来(有些面试笔试题会这样要求)
- (3)更高的要求:能自己写出这两个宏(不要着急,慢慢来)
逆推:知道结构体变量中某个元素的指针,如何逆推得到结构体的指针。
共用体union
共用体类型的定义,变量定义和使用
- (1)共用体union和结构体struct在类型定义、变量定义、使用方法上很相似。
- (2)共用体和结构体不同:结构体类似于一个包裹,结构体中的成员彼此是独立存在的,分布在内存的不同单元中,他们只是被打包成一个整体叫做结构体而已。而共用体中的各个成员其实是一体的,彼此不独立,他们使用同一个内存单元。可以理解为:有时候是这个元素,有时候是那个元素。更准确的说说法是同一个内存空间有多种解释方式。
- (3)共用体union就是对同一块内存中存储的二进制的不同 理解方式
- (4)有些书中把union翻译成联合(联合体),这个名字不好。现在翻译成共用体。
- (5)union的sizeof测到的大小实际上是union中各个元素的里面所占字节数最大的那个元素的大小。因为可以存的下这个就一定能存的下其他的元素。
- (6)union中的元素不存在内存对齐的问题,因为实际上,union中只存在一个内存空间,都是从同一个地址开始的(开始地址就是整个union占有的内存空间的首地址),所以,不涉及到内存对齐。
共用体和结构体的相同和不同
- (1)相同点就是操作语法基本相同
- (2)不同点时本质上的不同。struct是多个独立的元素(内存空间)打包在一起;union是一个元素(一个内存空间)的多种不同的解析方式
共用体的主要用途
- (1)共用体就用在那种对同一个内存单元进行多种不同规则解析的情况下。
- (2)C语言中可以没有共用体,用指针和强制类型转换可以完成同样的功能,但是共用题的方式可以更简单,更便捷,更容易理解。
大小端模式1
什么是大小端模式
- (1)大端模式(big endian)和小端模式(little endian)。最早是小说中出现的词,和计算机本身没有关系。
- (2)后来计算机通信发展起来后,遇到一个问题:就是串口等串行通信中,一次只能发送1个字节。这时候我要发送一个int类型的数就遇到一个问题。int类型有4个字节,我是按照byte0、byte1、byte2、byte3这样的顺序发送还是按照反过来byte3、byte2、byte1、byte0这样的顺序发送。规则就是发送方和接收方必须按照同样的字节顺序来通信,否则就会出现错误。这就是通信系统中的大小端模式。这是大小端和计算机挂钩的最早的问题。
- (3)现在我们讲的这个大小端模式,更多的是指计算机存储系统的大小端。在计算机内存/硬盘/Nand中。因为存储系统是32位的,但是数据仍然是按照字节为单位的。于是一个32位二进制在内存中存储时有两种分布方式:高字节对应高地址就是大端模式、高字节对应低地址就是小端模式。
- (4)大端模式和小端模式本身没有对错,没有优劣,理论上按照大端或小端都可以,但是要求存储和读取时,按照同样的大小端模式来进行,否则会出错。
- (5)显示的情况就是有些CPU公司用大端(譬如51单片机)有些CPU用小端,(譬如ARM)。大部分是小端模式。大端模式的不多。所以我们在写代码时,当不知道当前环境是大端还是小端模式时就需要用代码来检测当前系统的大小端。
经典笔试题:用C语言写一个函数来测试当前机器的大小端模式?
- 用union来测试机器的大小端模式
- 用指针方式来测试大小端模式?
#include<stdio.h> //共用体中很重要的一点:a和b都是从u1的低地址开始的 //假设u1所在的4字节地址分别是:0,1,2,3的话 union myunion { int a; char b; }; //如果是小端模式则返回1,如果是大端模式则返回0 //union方式 int is_little_endlian(void) { union myunion u1; u1.a = 1; return u1.b; } //指针方式 int is_little_endlian2(void) { int a = 1; char b = *((char *)(&a)); } int main(void) { int i = is_little_endlian2(); if(i == 1) { printf("小端模式.\n"); } else printf("大端模式.\n"); return 0; }
大小端模式2
看似可行实测不行的测试大小端方式:
(1)位与运算。
结论:位与无法测试机器的大小端模式。(表现就是大端机器和小端机器的&运算后的值相同)
理论分析:位与运算是编译器提供运算,这个运算是高于内存层次的(或者说位与运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节)
#include<stdio.h> int main(void) { int a = 1; int b = a &0xff; printf("b=%d.\n",b); return 0; }
这种方式是不对的。
(2)移位
结论:移位的方式也无法测试机器的大小端
理论分析:原因和位与运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节是高位还是低位无关
(3)强制类型转换
结论:这种也是不可以的。
理论分析:同上
通信中的大小端(数组的大小端)
- (1)譬如要通过串口发送一个0x12345678给对方,但是因为串口本身限制,只能以字节为单位来发送,所以需要发4次;接受方分4次接收,内容分别是0x12、0x34、0x56、0x78接收方接收到4字节后需要去重组得到0x12345678(而不是得到0x78563412)
- (2)所以通信双方需要默契,先发/先接是高位还是低位的问题?这就是通信中的大小端的问题。
- (3)一般来说:先发低字节叫做小端;先发高字节叫做大端。实际操作中,通信协议中,会去定义大小端,明确告诉你是先发高字节还是低字节。
- (4)在通信协议中,大小端是非常重要的,大家使用别人定义的通信协议还是自己要去定义通信协议,一般都要注意表明标清楚你的通信协议中大小端的问题。
枚举
枚举是用来干嘛的?
- (1)枚举在C语言中其实是一些符号常量集,直白点说,枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码。编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。
- (2)枚举中的枚举值都是常量,怎么验证?
- (3)枚举符号常量和其对应的常量数字相对来说,数字不重要,符号才重要。符号对应的数字只要彼此不同即可,没有别的要求。所以一般情况下我们一般都不明确指定这个符号所对应的数字,而让编译器自动分配。(编译器自动分配的原则是从0开始,依次增加。如果用户自己定义了一个值,则从那个值开始依次增加)
C语言为什么需要枚举?
- (1)C语言没有枚举也是可以的。使用枚举其实就是对1和0 浙西而数字进行编码,这样的好处就是变成是可以不用看数字而直接看符号。符号的意义是显然的,一眼可以看出,而数字所代表的含义除非看注释或者文档。
- (2)宏定义的目的和意义:不用数字而是用符号。从这里可以看出宏定义和枚举存在内在联系。宏定义和枚举经常用来解决类似的问题,他们两个基本可以互换,但还是有一些细微的差别的。
宏定义和枚举的区别?
- (1)枚举是将多个有关联的符号封装在一个枚举中,而宏定义是散的。
- (2)什么情况下用枚举?当我们要定义的常量是一个有限集合时(譬如一周7天,一年12个月之类的)最适合用枚举
- (3)不能用枚举的情况(定义的常量无关联。或者是无限的)就用宏定义。
总结:宏定义先出现用来解决符号常量的问题,后来发现有时候彼此之间的符号常量之间有关联(多选一的关系),用宏定义虽然可以做,但是不太好,于是发明了枚举。
枚举的定义和使用?
看文档