14.C语言的代码内存布局详解
一个程序本质上都是由 BSS 段、data段、text段三个组成的。这样的概念在当前的计算机程序设计中是很重要的一个基本概念,而且在嵌入式系统的设计中也非常重要,牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题。
程序编译后生成的目标文件至少含有这三个段,这三个段的大致结构图如下所示:
在采用段式内存管理的架构中.text即为代码段,为只读。.bss段包含程序中未初始化的全局变量。数据段包含三个部分:heap(堆)、stack(栈)和静态数据区。
1)堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。其概念与数据结构中“堆”的概念不同。
2)栈 (stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变 量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以 栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
3)静态数据区存放的是程序中已初始化的全局变量、静态变量和常量。
在采用段式内存管理的架构中(比如intel的80x86系统),BSS 段(Block Started by Symbol segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,一般在初始化时 BSS 段部分将会清零。BSS 段属于静态内存分配,即程序一开始就将其清零了。
比如,在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段中,未初始化的全局变量保存在.bss 段中。
text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中),由系统从可执行文件中加载;而BSS段不在可执行文件中,由系统初始化。
内存中堆和栈的区别
栈 堆
操作系统自动分配,释放。 程序员手动分配,释放,不释放要等程序结束了才回收。
存放程序临时创建的局部变量 存放进程运行中被动态分配的内存段
内存中位置不同,向低地址扩展,连续内存,最大容量是系统预设好的,空间较小,2,3M。 向高地址扩展,不连续的内存,用链表来管理,可以申请大的空间。
效率上,栈系统分配,速度快。 速度慢,容易产生内存碎片。
堆和栈中的存储内容
栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可
执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈
的,然后是函数中的局部变量。注意静态变量是不入栈的。
当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地
址,也就是主函数中的下一条指令,程序由该点继续运行。
堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。
16.用户进程内存空间
详情见此
http://blog.jobbole.com/45733/
17.类型转换有哪些?(四种类型转换,分别举例说明)
1)static_cast: 一般的转换(no run-time check)通常,如果你不知道该用哪个,就用这个。
编译器在编译期处理
将地址a转换成类型T,T和a必须是指针、引用、算术类型或枚举类型。
表达式static_cast<T*>(a), a的值转换为模板中指定的类型T。在运行时转换过程中,不进行类型检查来确保转换的安全性。
static_cast它能在内置的数据类型间互相转换,对于类只能在有联系的指针类型间进行转换。可以在继承体系中把指针转换来、转换去,但是不能转换成继承体系外的一种类型。
为什么要用static_cast转换而不用c语言中的转换?
static_cast不能进行无关类型(如非基类和子类)指针之间的转换。
应使用static_cast取代c风格的强制类型转换,较安全。
2)dynamic_cast: 通常在基类和派生类之间转换时使用
在运行期,会检查这个转换是否可能。
完成类层次结构中的提升。T必须是一个指针、引用或无类型的指针。a必须是决定一个指针或引用的表达式。
dynamic_cast 仅能应用于指针或者引用,不支持内置数据类型
表达式dynamic_cast<T*>(a) 将a值转换为类型为T的对象指针。如果类型T不是a的某个基类型,该操作将返回一个空指针。
它不仅仅像static_cast那样,检查转换前后的两个指针是否属于同一个继承树,它还要检查被指针引用的对象的实际类型,确定转换是否可行。
比如基类向派生类转换(它实际确实是派生类才能成功)。
3)const_cast: 主要针对const和volatile的转换
编译器在编译期处理
去掉类型中的常量,T和a必须是相同的类型。
4)reinterpret_cast: 用于进行没有任何关联之间的转换,比如一个字符指针转换为一个整形数。
编译器在编译期处理
任何指针都可以转换成其它类型的指针,T必须是一个指针、引用、算术类型、指向函数的指针或指向一个类成员的指针。
表达式reinterpret_cast<T*>(a)能够用于诸如char* 到 int*,或者One_class* 到 Unrelated_class*等类似这样的转换,因此可能是不安全的。
18.内存对齐原则
1:数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储。
2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补齐.
#pragma pack(1),告诉编译器,所有的对齐都按照1的整数倍对齐,换句话说就是没有对齐规则. Vc,Vs等编译器默认是#pragma pack(8),所以测试我们的规则会正常;注意gcc默认是#pragma pack(4),并且gcc只支持1,2,4对齐。套用三原则里计算的对齐值是不能大于#pragma pack指定的n值。
为什么要内存对齐
(1)平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取得某些特定类型的数据,否则抛出硬件异常。
(2)性能原因:经过内存对齐后,CPU的内存访速度大大提升。具体原因稍后解释。
图二:
CPU把内存当成是一块一块的,块的大小可以是2,、4、8、16字节的大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory granularity(粒度),本人把它翻译为“内存读取粒度”。假设CPU要读取一个int型4字节大小的数据到寄存器中,分为两种情况讨论:
(1)数据从0字节开始
(2)数据从1字节开始
再次假设内存读取粒度为4。
当该数据是从0字节开始时,CPU只需读取内存一次即可把这4字节的数据完全读取到寄存器中。
当该数据是从1字节开始时,问题变得有些复杂,此时该int型数据不是位于内存读取边界上,这就是一类内存未对齐的数据。
图四:
此时,CPU先访问一次内存,读取0-3字节的数据进寄存器,并再次读取4-5字节的数据进寄存器,接着把0字节和6、7、8字节的数据剔除,最后合并1,2,3,4字节的数据进寄存器。对一个内存未对齐的数据进行了这么多额外的操作,大大降低了CPU的性能。
19.内联函数(讲了一下内联函数的优点以及和宏定义的区别)
1.内联函数在运行时可调试,而宏定义不可以;
2.编译器会对内联函数的参数类型做安全检查或自动类型转换(同普通函数),而宏定义则不会;
3.内联函数可以访问类的成员变量,宏定义则不能;
(以上都可以理解为内联函数毕竟是函数,它就会有函数能做的,宏不能)
以上
在类中声明同时定义的成员函数,自动转化为内联函数。
宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
宏不是函数,只是在编译前将程序中有关字符串替换成宏体。
解释:
什么时候用到内联函数:
(1)一个函数被不断的重复调用
(2)函数只是简单的几行,而且函数内不包含for,while,switch语句。
(就是可以很快的运行完,且会反复调用,用普通函数的话切换的时间会与运行时间有可比性了)
20.typedef和#define的用法与区别
一、typedef的用法
在C/C++语言中,typedef常用来定义一个标识符及关键字的别名,它是语言编译过程的一部分,但它并不实际分配内存空间,实例像:
typedef可以增强程序的可读性,以及标识符的灵活性,但它也有“非直观性”等缺点。
二、#define的用法
#define为一宏定义语句,通常用它来定义常量(包括无参量与带参量),以及用来实现那些“表面似和善、背后一长串”的宏,它本身并不在编译过程中进行,而是在这之前(预处理过程)就已经完成了,但也因此难以发现潜在的错误及其它代码维护问题,它的实例像:
在Scott Meyer的Effective C++一书的条款1中有关于#define语句弊端的分析,以及好的替代方法,大家可参看。
三、typedef与#define的区别
从以上的概念便也能基本清楚,typedef只是为了增加可读性而为标识符另起的新名称(仅仅只是个别名),而#define原本在C中是为了定义常量,到了C++,const、enum、inline的出现使它也渐渐成为了起别名的工具。有时很容易搞不清楚与typedef两者到底该用哪个好,如#define INT int这样的语句,用typedef一样可以完成,用哪个好呢?我主张用typedef,因为在早期的许多C编译器中这条语句是非法的,只是现今的编译器又做了扩充。为了尽可能地兼容,一般都遵循#define定义“可读”的常量以及一些宏语句的任务,而typedef则常用来定义关键字、冗长的类型的别名。
宏定义只是简单的字符串代换(原地扩展),而typedef则不是原地扩展,它的新名字具有一定的封装性,以致于新命名的标识符具有更易定义变量的功能。请看上面第一大点代码的第三行:
以及下面这行:
效果相同?实则不同!实践中见差别:pINT a,b;的效果同int *a; int *b;表示定义了两个整型指针变量。而pINT2 a,b;的效果同int *a, b;表示定义了一个整型指针变量a和整型变量b(define仅仅是代换)。
注意:两者还有一个行尾;号的区别哦!
21.const 和宏定义#define的区别
1.差别:const与#define最大的差别在于:前者在堆栈分配了空间,而后者只是把具体数值直接传递到目标变量罢了,#define不占用内存单元,每次调用都会分配内存。
或者说,const的常量是一个Run-Time的概念,他在程序中确确实实的存在可以被调用、传递。而#define常量则是一个Compile-Time概念,它的生命周期止于编译期:在实际程序中他只是一个常数、一个命令中的参数,没有实际的存在。const常量存在于程序的数据段.#define常量存在于程序的代码段。
2.#define宏是在预编译阶段进行替换,而const修饰的只读变量是在编译的时候确定其值。
3.#define宏没有类型,而const修饰的只读变量具有特定的类型
22.链接指示:extern “C”
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
这个功能十分有用处,因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。