1.源文件转换为可执行文件

源文件经过以下几步生成可执行文件:

  • 1、预处理(preprocessor):对#include、#define、#ifdef/#endif、#ifndef/#endif等进行处理
  • 2、编译(compiler):将源码编译为汇编代码
  • 3、汇编(assembler):将汇编代码汇编为目标代码
  • 4、链接(linker):将目标代码链接为可执行文件

编译器和汇编器创建的目标文件包含:二进制代码(指令)、源码中的数据;链接器将多个目标文件链接成一个;装载器吧目标文件加载到内存。

图1 源文件到可执行文件的步骤

2.可执行程序组成及内存布局

通过上面的小节,我们知道将源程序转换为可执行程序的步骤,典型的可执行文件分为两部分:

  • 代码段(Code),由机器指令组成,该部分是不可改的,编译之后就不再改变,放置在文本段(.text)。
  • 数据段(Data),它由以下几部分组:
    • 常量(constant),通常放置在只读read-only的文本段(.text)
    • 静态数据(static data),初始化的放置在数据段(.data);未初始化的放置在BSS段(.bss,Block Started by Symbol,BSS段的变量只有名称和大小却没有值)
    • 动态数据(dynamic data),这些数据存储在堆(heap)或栈(stack)

源程序编译后链接到一个以0地址为始地址的线性或多维虚拟地址空间。而且每个进程都拥有这样一个空间,每个指令和数据都在这个虚拟地址空间拥有确定的地址,把这个地址称为虚拟地址(Virtual Address)。将进程中的目标代码、数据等的虚拟地址组成的虚拟空间称为虚拟存储器(Virtual Memory)。典型的虚拟存储器中有类似的布局:

  • Text Segment (.text)
  • Initialized Data Segment (.data)
  • Uninitialized Data Segment (.bss)
  • The Stack
  • The Heap

如下图所示:

图2 进程内存布局

当进程被创建时,内核为其提供一块物理内存,将虚拟内存映射到物理内存,这些都是由操作系统来做的。

3.数据存储类别

讨论C/C++中的内存布局,不得不提的是数据的存储类别!数据在内存中的位置取决于它的存储类别。一个对象是内存的一个位置,解析这个对象依赖于两个属性:存储类别、数据类型。

  • 存储类别决定对象在内存中的生命周期。
  • 数据类型决定对象值的意义,在内存中占多大空间。

C/C++中由(auto、 extern、 register、 static)存储类别和对象声明的上下文决定它的存储类别。

3.1 自动对象(automatic objects)

auto和register将声明的对象指定为自动存储类别。他们的作用域是局部的,诸如一个函数内,一个代码块{***}内等。操作了作用域,对象会被销毁。

  • 在一个代码块中声明一个对象,如果没有执行auto,那么默认是自动存储类别。
  • 声明为register的对象是自动存储类别,存储在计算机的快速寄存器中。不可以对register对象做取值操作“&”。

3.2 静态对象(static objects)

静态对象可以局部的,也可以是全局的。静态对象一直保持它的值,例如进入一个函数,函数中的静态对象仍保持上次调用时的值。包含静态对象的函数不是线程安全的、不可重入的,正是因为它具有“记忆”功能。

  • 局部对象声明为静态之后,将改变它在内存中保存的位置,由动态数据--->静态数据,即从堆或栈变为数据段或bbs段。
  • 全局对象声明为静态之后,而不会改变它在内存中保存的位置,仍然是在数据段或bbs段。但是static将改变它的作用域,即该对象仅在本源文件有效。此相反的关键字是extern,使用extern修饰或者什么都不带的全局对象的作用域是整个程序。

4.一个实例

下面我们分析一段代码:

 #include <stdio.h>
#include <stdlib.h> int a;
static int b; void func( void )
{
char c;
static int d;
} int main( void )
{
int e;
int *pi = ( int *) malloc ( sizeof ( int ));
func ();
func ();
free (pi );
return ();
}

程序中声明的变量a、b、c、d、e、pi的存储类别和生命期如下所述:

  • a是一个未初始化的全局变量,作用域为整个程序,生命期是整个程序运行期间,在内存的bbs段
  • b是一个未初始化的静态全局变量,作用域为本源文件,生命期是整个程序运行期间,在内存的bbs段
  • c是一个未初始化的局部变量,作用域为函数func体内,即仅在函数体内可见,生命期也是函数体内,在内存的栈中
  • d是一个未初始化的静态局部变量,作用域为函数func体内,即仅在函数体内可见,生命期是整个程序运行期间,在内存的bbs段
  • e是一个未初始化的局部变量,作用域为函数main体内,即仅在函数体内可见,生命期是main函数内,在内存的栈中
  • pi是一个局部指针,指向堆中的一块内存块,该块的大小为sizeof(int),pi本身存储在内存的栈中,生命期是main函数内
  • 新申请的内存块在堆中,生命期是malloc/free之间

用图表示如下:

图3 例子的内存布局

综合1~4,介绍了C/C++中由源程序到可执行文件的步骤,和可执行程序的内存布局,数据存储类别,最后还通过一个例子来说明。

可执行程序中的变量在内存中的布局可以总结为如下:

  • 变量(函数外):如果未初始化,则存放在BSS段;否则存放在data段
  • 变量(函数内):如果没有指定static修饰符,则存放在栈中;否则同上
  • 常量:存放在文本段.text
  • 函数参数:存放在栈或寄存器中

内存可以分为以下几段:

  • 文本段:包含实际要执行的代码(机器指令)和常量。它通常是共享的,多个实例之间共享文本段。文本段是不可修改的。
  • 初始化数据段:包含程序已经初始化的全局变量,.data。
  • 未初始化数据段:包含程序未初始化的全局变量,.bbs。该段中的变量在执行之前初始化为0或NULL。
  • 栈:由系统管理,由高地址向低地址扩展。
  • 堆:动态内存,由用户管理。通过malloc/alloc/realloc、new/new[]申请空间,通过free、delete/delete[]释放所申请的空间。由低地址想高地址扩展。

1~4引自吴秦先生的博文。

作者:吴秦
出处:http://www.cnblogs.com/skynet/
本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名吴秦(包含链接).

5.内存对齐

5.1 一个例子

先来看一个例子:

 #include<iostream>
using namespace std; class test
{
private:
char c = ''; // 1byte
int i; // 4byte
short s = ; // 2byte
}; int main()
{
cout << sizeof(test) << endl;
return ;
}

输出是12.

 #include<iostream>
using namespace std; class test
{
private:
int i; // 4byte
char c = ''; // 1byte
short s = ; // 2byte
}; int main()
{
cout << sizeof(test) << endl;
return ;
}

输出是8.

我们可以看到,类test和test2的成员变量完全一样,只是定义顺序不一样,却造成了2个类占用内存大小不一样。这就是编译器内存对齐的缘故。

5.2 对齐规则

1、第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

2、在数据成员完成各自对齐之后,类(结构或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

很明显#pragma pack(n)作为一个预编译指令用来设置多少个字节对齐的。值得注意的是,n的缺省数值是按照编译器自身设置,默认为8。其语法如下:

where:

1 | 2 | 4 | 8 | 16 Members of structures are aligned on the specified byte-alignment, or on their natural alignment boundary, whichever is less, and the specified value is pushed on the stack.
nopack No packing is applied, and "nopack" is pushed onto the pack stack
pop The top element on the pragma pack stack is popped.
(no argument specified) Specifying #pragma pack() has the same effect as specifying #pragma pack(pop).

5.3 例子分析

5.3.1 对于类test的内存空间

内存分配过程:

1)char和编译器默认的内存缺省分割大小比较,char比较小,分配一个字节给它。

2)int和编译器默认的内存缺省分割大小比较,int比较小,占4字节。只能空3个字节,重新分配4个字节。

3)short和编译器默认的内存缺省分割大小比较,short比较小,占2个字节,分配2个字节给它。

4)对齐结束类本身也要对齐,所以最后空余的2个字节也被test占用。

5.3.2 对于类test2的内存空间

1)int和编译器默认的内存缺省分割大小比较,int比较小,占4字节。分配4个字节给int。

2)char和编译器默认的内存缺省分割大小比较,char比较小,分配一个字节给它。

3)short和编译器默认的内存缺省分割大小比较,short比较小,此时前面的char分配完毕还余下3个字节,足够short的2个字节存储,所以short紧挨着。分配2个字节给short。

4)对齐结束类本身也要对齐,所以最后空余的1个字节也被test占用。

5.3.3 使用#pragma pack(n)

 #include<iostream>
using namespace std; #pragma pack(1)//设定为1字节对齐 class test
{
private:
char c = ''; //1byte
int i; //4byte
short s = ; //2byte
}; class test2
{
private:
int i; //4byte
char c = ''; //1byte
short s = ; //2byte
}; int main()
{
cout << sizeof(test) << endl;
cout << sizeof(test2) << endl;
return ;
}

输出结果:

可以看到,当我们把编译器的内存分割大小设置为1后,类中所有的成员变量都紧密的连续分布。

5.4 内存对齐的作用

要严重参考一IBM的文章:Data alignment: Straighten up and fly right,PDF版本可从这里下载得到。

平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

性能原因:经过内存对齐后,CPU的内存访问速度大大提升。具体原因稍后解释。

图一:

这是普通程序员心目中的内存印象,由一个个的字节组成,而CPU并不是这么看待的。

图二:

CPU把内存当成是一块一块的,块的大小可以是2,4,8,16字节大小,因此CPU在读取内存时是一块一块进行读取的。块大小成为memory access
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性能。

这还属于乐观情况了,上文提到内存对齐的作用之一为平台的移植原因,因为以上操作只有有部分CPU肯干,其他一部分CPU遇到未对齐边界就直接罢工了。

5.5 内存对齐对结构体成员变量访问影响

  先看下边一小程序:

 #include <iostream>
using namespace std; struct MyStruct
{
int a;
int b;
int c;
}; int main()
{
struct MyStruct myStruct = {, , }; struct MyStruct *ptr = &myStruct;
cout << ptr->a << endl;
cout << ptr->b << endl;
cout << ptr->c << endl; int *pstr = (int *)&myStruct;
cout << *pstr << endl;
cout << *(pstr + ) << endl;
cout << *(pstr + ) << endl; return ;
}

  上边程序中第16~18和第21~23行输出的结果是一样的。但如果我们考虑到字节填充的问题时,采用pstr那种访问方式就不大对了。所以要采用ptr那种访问方式。

6.参考资料

  C/C++ Memory Layout

  Data alignment: Straighten up and fly right

  内存对齐的规则以及作用

  C++内存对齐总结

  #pragma pack

  更多关于C++内存布局请参考:

  C++ 对象的内存布局

C/C++内存布局及对齐的更多相关文章

  1. 内存对齐与ANSI C中struct型数据的内存布局 【转】

    转自:http://blog.chinaunix.net/uid-25909619-id-3032209.html 当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将 ...

  2. 内存对齐与ANSI C中struct型数据的内存布局

    当在C中定义了一个结构类型时,它的大小是否等于各字段(field)大小之和?编译器将如何在内存中放置这些字段?ANSI C对结构体的内存布局有什么要求?而我们的程序又能否依赖这种布局?这些问题或许对不 ...

  3. 浅析内存对齐与ANSI C中struct型数据的内存布局-内存对齐规则

    这些问题或许对不少朋友来说还有点模糊,那么本文就试着探究它们背后的秘密. 首先,至少有一点可以肯定,那就是ANSI C保证结构体中各字段在内存中出现的位置是随它们的声明顺序依次递增的,并且第一个字段的 ...

  4. 重磅硬核 | 一文聊透对象在 JVM 中的内存布局,以及内存对齐和压缩指针的原理及应用

    欢迎关注公众号:bin的技术小屋 大家好,我是bin,又到了每周我们见面的时刻了,我的公众号在1月10号那天发布了第一篇文章<从内核角度看IO模型的演变>,在这篇文章中我们通过图解的方式以 ...

  5. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  6. C++类内存布局图(成员函数和成员变量分开讨论)

    一.成员函数 成员函数可以被看作是类作用域的全局函数,不在对象分配的空间里,只有虚函数才会在类对象里有一个指针,存放虚函数的地址等相关信息. 成员函数的地址,编译期就已确定,并静态绑定或动态的绑定在对 ...

  7. c++ 对象的内存布局

    之前介绍过了普通对象比如系统自带的int等对象的对齐方式,在学习类型转换的时候遇到了自定义类型的继承体系中的downcast与upcast. 于是顺藤摸瓜,摸到了这里.发现还是 陈皓的博客里面写的最早 ...

  8. C++ Data Member内存布局

    如果一个类只定义了类名,没定义任何方法和字段,如class A{};那么class A的每个实例占用1个字节的内存,编译器会会在这个其实例中安插一个char,以保证每个A实例在内存中有唯一的地址,如A ...

  9. C++中派生类对象的内存布局

    主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Objec ...

随机推荐

  1. iOS界面不能点击(tableView 的cell 不能使用点击事件,tableView也不能上下滚动)

    iOS开发中间遇到了很多问题,有些后来又遇到,竟然忘记怎么处理了,所以还是来写下博客记录自己遇到的问题,方便自己也方便别人. 之前想的是项目小,就用storyboard在绘制界面,但是项目慢慢的扩展, ...

  2. PLSQL实现分页查询

    --集合实现游标查询 CREATE OR REPLACE PACKAGE emppkg IS TYPE t_record IS RECORD( rn INT, empno emp.empno%TYPE ...

  3. [lua]luasocket.c:20:17: fatal error: lua.h: No such file or directory

    安装luasocket的时候出现了如下的错误 问题 $ tar xzf luasocket-2.0.2.tar.gz $ cd luasocket-2.0.2 $ $ make cd src; mak ...

  4. Android开发_TextView跑马灯

    关键代码: android:singleLine="true" android:ellipsize="marquee" android:focusable=&q ...

  5. 【问题汇总】ScrollView嵌套GridView的问题

    在开发中遇到了ScrollView嵌套GridView的情况,由于这两款控件都自带滚动条,当它们碰到一起的时候便会出问题,即GridView会显示不全. 解决办法,自定义一个GridView控件. [ ...

  6. JRE System Library [JavaSE-1.7](unbound)

    window > preferences > java > Install jars >如果没有jdk1.7 ,点击下面的search,会自动找到已经安装对jdk1.7,选择, ...

  7. <<精通iOS开发>>第14章例子代码彻底清除警告

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) 上一篇我们解决了<<精通iOS开发>> ...

  8. iOS9 中关闭ATS的方法

    大熊猫猪·侯佩原创或翻译作品.欢迎转载,转载请注明出处. 如果觉得写的不好请多提意见,如果觉得不错请多多支持点赞.谢谢! hopy ;) iOS9中增加了系统的安全性,你会发现默认情况下打开非http ...

  9. 随机采样和随机模拟:吉布斯采样Gibbs Sampling实现文档分类

    http://blog.csdn.net/pipisorry/article/details/51525308 吉布斯采样的实现问题 本文主要说明如何通过吉布斯采样进行文档分类(聚类),当然更复杂的实 ...

  10. Java基础---集合框架---迭代器、ListIterator、Vector中枚举、LinkedList、ArrayList、HashSet、TreeSet、二叉树、Comparator

    为什么出现集合类? 面向对象语言对事物的体现都是以对象的形式,所以为了方便对多个对象的操作,就对对象进行存储,集合就是存储对象最常用的一种方式. 数组和集合类同是容器,有何不同? 数组虽然也可以存储对 ...