数组&字符串&结构体&共用体&枚举

《朱老师物联网大讲堂》学习笔记

学习地址:www.zhulaoshi.org


(1).
程序运行需要内存来存储一些临时变量,内存是程序的立足之地,


内存由操作系统统一管理,操作系统提供了各种机制来为我们使用内存提供服务,


栈,
自动管理,
反复使用,所以是脏的,
临时性,
大小有限,所以可能会溢出,


(2).
堆内存由堆管理器管理,堆管理器属于操作系统的一部分,
大块内存,
灵活使用,手动分配和释放,
脏的,
临时性,合法使用范围是malloc和free之间,


malloc(4),
gcc中的malloc分配默认以16B为最小分配单位,


(3).
编译器在编译程序的时候,将程序中的所有的元素分成了一些组成部分,各部分构成一个段,所以说段是可执行程序的组成部分,
代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。
数据段(也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,直观理解就是C语言程序中的全局变量,(全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据),
bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段就是被初始化为0的数据段。
数据段(.data)和bss段的都是用来存放C程序中的全局变量的,区别在于把显示初始化为非零的全局变量存在.data段中,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量值默认为0)的全局变量存在bss段,


char *p = "linux";"linux"实际被分配在代码段,"linux"实际上是一个常量字符串,而不是变量字符串,
const常量,常量就是不能被改变的量,const的实现方法至少有2种:第一种就是编译将const修饰的变量放在代码段去以实现不能修改(普遍见于各种单片机的编译器);第二种就是由编译器来检查以确保const型的常量不会被修改,实际上const型的常量还是和普通变量一样放在数据段的(gcc中就是这样实现的)。


显式初始化为非零的全局变量和静态局部变量放在数据段,
放在.data段的变量有2种:第一种是显式初始化为非零的全局变量。第二种是静态局部变量,也就是static修饰的局部变量。(普通局部变量分配在栈上,静态局部变量分配在.data段)


未初始化或显式初始化为0的全局变量放在bss段,
bss段和.data段并没有本质区别,


局部变量使用栈内存,
堆内存我们只能通过操作系统提供的malloc和free来分配和释放,其生命周期是malloc和free之间,
数据段对应程序中的全局变量和静态局部变量,与程序同生共死,




(4).


C语言没有原生字符串类型,java,c#等语言有字符串类型,用法如String s1 = “linux”,
C语言中的字符串是通过字符指针间接实现的,


char *p = "linux";此时p就叫做字符串,实际上p只是一个字符指针(本质上就是一个指针变量,指向了一个字符串的起始地址),


C语言中字符串的本质:指针指向头、固定尾部的地址相连的一段内存,
字符串就是多个字符一起共同组成的,在内存中是多个字节连续分布的,以‘\0'结尾,类似数组,


'\0'是一个ASCII字符,是编码为0的字符(数字0有它自己的ASCII编码)。要注意区分'\0'和'0'和0.(0等于'\0','0'等于48),
字符串中无法包含'\0'这个字符,


char *p = "linux";p本质上是一个字符指针,占4字节;"linux"分配在代码段,占6个字节;总共耗费了10个字节,最后存'\0'的内存是字符串结尾标志,本质上不属于字符串,


存储多个字符的2种方式:字符串和字符数组,


(5).
sizeof返回一个类型或者是变量所占用的内存字节数,C语言中用户自定义类型可以用sizeof让编译器计算,
strlen是一个C语言库函数:size_t strlen(const char *s);这个函数接收一个字符串的指针,返回这个字符串的长度(以字节为单位),strlen返回的字符串长度是不包含字符串结尾的'\0'的,




char *p = "linux"; sizeof(p)得到的永远是4,
strlen刚好用来计算字符串的长度,


字符数组char a[] = "linux";定义了一个数组a,数组a占6字节,右值"linux"本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有"linux"这个字符串的);这句就相当于是:char a[] = {'l', 'i', 'n', 'u', 'x', '\0'};
字符串char *p = "linux";定义了一个字符指针p,p占4字节,分配在栈上;同时还定义了一个字符串"linux",分配在代码段;然后把代码段中的字符串(一共占6字节)的首地址(也就是'l'的地址)赋值给p,
字符数组本身是数组,数组自身自带内存空间用来存东西,而字符串本身是指针,永远只占4字节,这4个字节不能用来存有效数据,有效数据存在其它地方,然后把地址存在p中,


(6).
数组2个缺陷,
1.定义时必须明确给出大小,且这个大小在以后不能再更改,
2.数组所有的元素的类型必须一致,
更复杂的数据结构中就致力于解决数组的这两个缺陷,


结构体可以解决数组的第二个缺陷的,结构体算是一个其中元素类型可以不相同的数组,结构体完全可以取代数组,只是在数组可用的范围内数组比结构体更简单,


数组中下标访问和指针访问的本质,都是以指针的方式来实现的,
结构体变量中的元素访问方式:只有一种,用.或者->的方式来访问,


结构体变量的点号或者->访问元素,本质上还是用指针来访问的,


(7).
结构体访问的本质还是指针,
不过要考虑不同类型变量的偏移量,而且还要考虑元素的对齐访问, 
所以每个元素占用的字节数可能和自身大小会有出入,
Ps;对齐访问牺牲了内存空间来换取速度性能,非对齐访问牺牲访问速度来换取空间的利用率,


32位编译器,一般默认4字节对齐,
结构体整体要4四字节对齐,大小是4字节倍数,
结构体中每个元素也要对其访问,


对齐访问指令,
#pragma pack()   #pragma pack(n) (n=1/2/4/8),
两个指令之间的部分对齐访问,


gcc推荐的对齐指令__attribute__((packed))  __attribute__((aligned(n)))


参考阅读blog:
http://www.cnblogs.com/dolphin0520/archive/2011/09/17/2179466.html
http://blog.csdn.net/sno_guo/article/details/8042332




(8).
结构体访问各个元素,本质上是通过指针方式的,用.的方式是编译器帮我们计算了偏移量,
offsetof宏:用宏来计算结构体中某个元素和结构体首地址的偏移量,
offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量,


(TYPE *)0 这是一个强制类型转换,把0地址强制类型转换成一个指针,这个指针指向一个TYPE类型的结构体变量,实际上这个结构体变量可能不存在,但是只要我不去解引用这个指针就不会出错,
((TYPE *)0)->MEMBER (TYPE *)0是一个TYPE类型结构体变量的指针,通过指针指针来访问这个结构体变量的member元素,


&((TYPE *)0)->MEMBER  等效于&(((TYPE *)0)->MEMBER),意义就是得到member元素的地址。但是因为整个结构体变量的首地址是0,


container_of宏:知道一个结构体中某个元素的指针,反推这个结构体变量的指针,
有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针,
typeof关键字的作用是:typepef(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的,
这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可,


(9).
结构体类似于一个包裹,结构体中的成员彼此是独立存在的,分布在内存的不同单元中,他们只是被打包成一个整体叫做结构体而已,
共用体中的各个成员其实是一体的,彼此不独立,他们使用同一个内存单元,可以理解为:有时候是这个元素,有时候是那个元素,更准确的说法是同一个内存空间有多种解释方式,
union中的元素不存在内存对齐的问题,


struct是多个独立元素(内存空间)打包在一起,
union是一个元素(内存空间)的多种不同解析方式,


共用体就用在那种对同一个内存单元进行多种不同规则解析的这种情况下,
C语言用指针和强制类型转换可以替代共用体完成同样的功能,共用体的方式更方便,


(10).
大端模式(big endian)和小端模式(little endian),
在计算机内存/硬盘/Nnad中,存储系统是32位的,数据是按照字节为单位的。一个32位的二进制在内存中存储时有2种分布方式:高字节对应高地址(大端模式)、高字节对应低地址(小端模式),
理论上按照大端或小端都可以,但是要求必须存储时和读取时按照同样的大小端模式来进行,否则会出错,
有些CPU公司用大端(譬如C51单片机);有些CPU用小端(譬如ARM),


经典笔试题:用C语言写一个函数来测试当前机器的大小端模式,


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


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


(11).
看似可行实际不行的测试大小端方式:位与、移位、强制类型转化,
位与运算,
结论:位与的方式无法测试机器的大小端模式,(表现就是大端机器和小端机器的&运算后的值相同的),
理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关),


移位,
结论:移位的方式也不能测试机器大小端,
理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的,
右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的,


强制类型转换,
同上,


在通信协议中,大小端是非常重要的,一定都要注意标明通信协议中大小端的问题,




(12).
枚举是一些符号常量集和int型常量的绑定,
枚举中的枚举值都是常量,怎么验证?
枚举中符号对应的数字彼此不能相同,编译器默认自动从0开始分配,




枚举是对1、0这些数字进行符号化编码,


枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的,也就是说枚举其实是多选一,
定义的常量是一个有限集合时(譬如一星期有7天,譬如一个月有31天,譬如一年有12个月····),最适合用枚举,


宏定义先出现,后来发现有时候定义的符号常量彼此之间有关联(多选一的关系),于是乎发明了枚举,


枚举的定义和使用,












你可能感兴趣的:(C语言)