c语⾔相关性匹配_⼲货⼤⽜谈嵌⼊式C语⾔的⾼级⽤法
原标题:⼲货 | ⼤⽜谈嵌⼊式C语⾔的⾼级⽤法
EEWorld
1
内存管理
我们需要知道——变量,其实是内存地址的⼀个抽像名字罢了。在静态编译的程序中,所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的,只知道地址。
内存的使⽤时程序设计中需要考虑的重要因素之⼀,这不仅由于系统内存是有限的(尤其在嵌⼊式系统中),⽽且内存分配也会直接影响到程序的效率。因此,我们要对C语⾔中的内存管理,有个系统的了解。
在C语⾔中,定义了4个内存区间:代码区;全局变量和静态变量区;局部变量区即栈区;动态存储区,即堆区;具体如下:
1>栈区(stack)— 由编译器⾃动分配释放 ,存放函数的参数值,局部变量的值等。其操作⽅式类似于数据结构中的栈。
2>堆区(heap) — ⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配⽅式倒是类似于链表。
3>全局区(静态区)(static)—全局变量和静态变量的存储是放在⼀块的,初始化的全局变量和静态变量在⼀块区域, 未初始化的全局变量和未初始化的静态变量在相邻的 另⼀块区域。 - 程序结束后由系统释放。
4>常量区 —常量字符串就是放在这⾥的。 程序结束后由系统释放。
5>程序代码区—存放函数体的⼆进制代码。
我们来看张图:
图1
⾸先我们要知道,源代码编译成程序,程序是放在硬盘上的,⽽⾮内存⾥!只有执⾏时才会被调⽤到内存中!我们来看看程序结构,ELF是是Linux的主要可执⾏⽂件格式。ELF⽂件由4部分组成,分别是ELF头(ELF header)、程序头表(Program header table)、节(Section)和节头表(Section header table)。具体如下:
1>Program header描述的是⼀个段在⽂件中的位置、⼤⼩以及它被放进内存后所在的位置和⼤⼩。即要加载的信息;
2>Sections保存着object ⽂件的信息,从连接⾓度看:包括指令,数据,符号表,重定位信息等等。在图中,我们可以看到Sections中包括:
text ⽂本结 存放指令;
rodata 数据结 readonly;
data 数据结 可读可写;
3>Section头表(section header table)包含了描述⽂件sections的信息。每个section在这个表中有⼀个⼊⼝;每个⼊⼝给出了该section 的名字,⼤⼩,等等信息。相当于 索引!
⽽程序被加载到内存⾥⾯,⼜是如何分布的呢?我们看看上图中:
正⽂和初始化的数据和未初始化的数据就是我们所说的数据段,正⽂即代码段;
2>正⽂段上⾯是常量区,常量区上⾯是全局变量和静态变量区,⼆者占据的就是初始化的数据和未初始化的数据那部分;
3>再上⾯就是堆,动态存储区,这⾥是上增长;
4>堆上⾯是栈,存放的是局部变量,就是局部变量所在代码块执⾏完毕后,这块内存会被释放,这⾥栈区是下增长;
5>命令⾏参数就是001之类的,环境变量什么的前⾯的⽂章已经讲过,有兴趣的可以去看看。
我们知道,内存分为动态内存和静态内存,我们先讲静态内存。
静态内存
存储模型决定了⼀个变量的内存分配⽅式和访问特性,在C语⾔中主要有三个维度来决定:存储时期 、作⽤域 、链接。
1、存储时期
存储时期:变量在内存中的保留时间(⽣命周期)
存储时期分为两种情况,关键是看变量在程序执⾏过程中会不会被系统⾃动回收掉。
1) 静态存储时期 Static
在程序执⾏过程中⼀旦分配就不会被⾃动回收。
通常来说,任何不在函数级别代码块内定义的变量。
⽆论是否在代码块内,只要采⽤static关键字修饰的变量。
2) ⾃动存储时期 Automatic
除了静态存储以外的变量都是⾃动存储时期的,或者说只要是在代码块内定义的⾮static的变量,系统会肚脐⾃动⾮配和释放内存;
2、作⽤域
作⽤域:⼀个变量在定义该变量的⾃⾝⽂件中的可见性(访问或者引⽤)
在C语⾔中,⼀共有3中作⽤域:
1) 代码块作⽤域
在代码块中定义的变量都具有该代码的作⽤域。从这个变量定义地⽅开始,到这个代码块结束,该变量是可见的;
2) 函数原型作⽤域
出现在函数原型中的变量,都具有函数原型作⽤域,函数原型作⽤域从变量定义处⼀直到原型声明的末尾。
3) ⽂件作⽤域
⼀个在所有函数之外定义的变量具有⽂件作⽤域,具有⽂件作⽤域的变量从它的定义处到包含该定义的⽂件结尾处都是可见的;3、链接
链接:⼀个变量在组成程序的所有⽂件中的可见性(访问或者引⽤);
C语⾔中⼀共有三种不同的链接:
1) 外部链接
如果⼀个变量在组成⼀个程序的所有⽂件中的任何位置都可以被访问,则称该变量⽀持外部链接;
2) 内部链接
如果⼀个变量只可以在定义其⾃⾝的⽂件中的任何位置被访问,则称该变量⽀持内部链接。
3) 空链接
如果⼀个变量只是被定义其⾃⾝的当前代码块所私有,不能被程序的其他部分所访问,则成该变量⽀持空链接
我们来看⼀个代码⽰例:
#include
int a = 0;// 全局初始化区
char *p1; //全局未初始化区
int main()
{
int b; //b在栈区
char s[] = "abc"; //栈
char *p2; //p2在栈区
char *p3 = "123456"; //1234560在常量区,p3在栈上。
static int c =0; //全局(静态)初始化区
p1 = (char *)malloc(10);
p2 = (char *)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1, "123456"); //1234560放在常量区,编译器可能会将它与p3所指向的"123456"优化成⼀个地⽅。
}
1.2动态内存
当程序运⾏到需要⼀个动态分配的变量时,必须向系统申请取得堆中的⼀块所需⼤⼩的存储空间,⽤于存储该变量。当不在使⽤该变量时,也就是它的⽣命结束时,要显⽰释放它所占⽤的存储空间,这样系统就能对该空间 进⾏再次分配,做到重复使⽤有线的资源。下⾯介绍动态内存申请和释放的函数。
1.2.1 malloc 函数
malloc函数原型:
size是需要动态申请的内存的字节数。若申请成功,函数返回申请到的内存的起始地址,若申请失败,返回NULL。我们看下⾯这个例⼦:
使⽤该函数时,有下⾯⼏点要注意:
1)只关⼼申请内存的⼤⼩;
2)申请的是⼀块连续的内存。记得⼀定要写出错判断;
嵌入式系统是什么意思3)显⽰初始化。即我们不知这块内存中有什么东西,要对其清零;
1.2.2 free函数
在堆上分配的额内存,需要⽤free函数显⽰释放,函数原型如下:
使⽤free(),也有下⾯⼏点要注意:
1)必须提供内存的起始地址;
调⽤该函数时,必须提供内存的起始地址,不能够提供部分地址,释放内存中的⼀部分是不允许的。
2)malloc和free配对使⽤;
编译器不负责动态内存的释放,需要程序员显⽰释放。因此,malloc与free是配对使⽤的,避免内存泄漏。
p = NULL是必须的,因为虽然这块内存被释放了,但是p仍指向这块内存,避免下次对p的误操作;
3)不允许重复释放
因为这块内存被释放后,可能已另分配,这块区域被别⼈占⽤,如果再次释放,会造成数据丢失;
1.2.3 其它相关函数
calloc函数分配内存需要考虑存储位置的类型。
realloc函数可以调整⼀段动态分配内存的⼤⼩
1.3堆和栈⽐较
1)申请⽅式
stack: 由系统⾃动分配。 例如,声明在函数中⼀个局部变量 int b; 系统⾃动在栈中为b开辟空间
heap: 需要程序员⾃⼰申请,并指明⼤⼩,在c中malloc函数 ,如p1 = (char *)malloc(10);
2)申请后系统的响应
栈:只要栈的剩余空间⼤于所申请空间,系统将为程序提供内存,否则将报异常提⽰栈溢出。
堆:⾸先应该知道操作系统有⼀个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻第⼀个空间⼤于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于⼤多数系统,会在这块内存空间中的⾸地址处记录本次分配的⼤⼩,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于到的堆结点的⼤⼩不⼀定正好等于申请的⼤⼩,系统会⾃动的将多余的那部分重新放⼊空闲链表中。
3)申请⼤⼩的限制
栈:栈是向低地址扩展的数据结构,是⼀块连续的内存的区域。这句话的意思是栈顶的地址和栈的最⼤容量是系统预先规定好的,栈的⼤⼩是2M(也有的说是1M,总之是⼀个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提⽰overflow。因此,能从栈获得的空间较⼩。
堆:堆是向⾼地址扩展的数据结构,是不连续的内存区域。这是由于系统是⽤链表来存储的空闲内存地址的,⾃然是不连续的,⽽链表的遍历⽅向是由低地址向⾼地址。堆的⼤⼩受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间⽐较灵活,也⽐较⼤。
4)申请效率的⽐较
栈由系统⾃动分配,速度较快。但程序员是⽆法控制的。
堆是由new分配的内存,⼀般速度⽐较慢,⽽且容易产⽣内存碎⽚,不过⽤起来最⽅便。
5)堆和栈中的存储内容
栈:在函数调⽤时,第⼀个进栈的是主函数中后的下⼀条指令(函数调⽤语句的下⼀条可执⾏语句)的地址,然后是函数的各个参数,在⼤多数的C编译器中,参数是由右往左⼊栈的,然后是函数中的局部变量。注意静态变量是不⼊栈的。 当本次函数调⽤结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下⼀条指令,程序由该点继续运⾏。
堆:⼀般是在堆的头部⽤⼀个字节存放堆的⼤⼩。堆中的具体内容由程序员安排。
6)存取效率的⽐较
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在运⾏时刻赋值的;
⽽bbbbbbbbbbb是在编译时就确定的;
但是,在以后的存取中,在栈上的数组⽐指针所指向的字符串(例如堆)快。
⽐如:
对应的汇编代码
第⼀种在读取时直接就把字符串中的元素读到寄存器cl中,⽽第⼆种则要先把指针值读到edx中,再根据edx读取字符,显然慢了。
7)最后总结
堆和栈的区别可以⽤如下的⽐喻来看出:
栈就像我们去饭馆⾥吃饭,只管点菜(发出申请)、付钱、和吃(使⽤),吃饱了就⾛,不必理会切菜、洗菜等准备⼯作和洗碗、刷锅等扫尾⼯作,他的好处是快捷,但是⾃由度⼩。
堆就象是⾃⼰动⼿做喜欢吃的菜肴,⽐较⿇烦,但是⽐较符合⾃⼰的⼝味,⽽且⾃由度⼤。
2
内存对齐
2.1 #pragma pack(n) 对齐⽤法详解
1.什么是对齐,以及为什么要对齐
现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照⼀定的规则在空间上排列,⽽不是顺序的⼀个接⼀个的排放,这就是对齐。
对齐的作⽤和原因:各个硬件平台对存储空间的处理上有很⼤的不同。⼀些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况, 但是最常见的是如果不按照适合其平台要求对数据存放进⾏对齐,会在存取效率上带来损失。⽐如有些平台每次读都是从偶地址开始,如
果⼀个int型(假设为 32位系统)如果存放在偶地址开始的地⽅,那么⼀个读周期就可以读出,⽽如果存放在奇地址开始的地⽅,就可能会需要2个读周期,并对两次读出的结果的⾼低字节进⾏拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
2.对齐的实现
通常,我们写程序的时候,不需要考虑对齐问题。编译器会替我们选择时候⽬标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令⽽改变对指定数据的对齐⽅法。 但是,正因为我们⼀般不需要关⼼这个问题,所以因为编辑器对数据存放做了对齐,⽽我们不了解的话,常常会对⼀些问题感到迷惑。最常见的就是struct数据结构的sizeof结果,出乎意料。为此,我们需要对对齐算法所了解。
作⽤:
指定结构体、联合以及类成员的packing alignment;
语法:
#pragma pack( [show] | [push | pop] [, identifier], n )
说明:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。