二、总结内存知识

二、总结内存知识

1 笔试积累

  • 根据代码和数据存放位置分为:哈佛结构和冯诺依曼结构,记忆:哈佛(half,即分成两半)是分开存放。
  • 内存RAM(random access memory),随机的意思是什么?举反例:磁带需要按序读取
  • 掉电丢失数据,DRAM,动态体现在需要刷新电路不断刷新电荷。除速度低于SRAM外,其他一般都比SRAMA好。
  • 栈在逻辑上是遵循FILO(first in last out)原则的(by the way 队列则为FIFO)。
  • 在32位系统和64位系统下只有指针类型和长整型字节数有所差别,其余全部相同。
  • 现有的平台上,每个内存硬件地址数值所能分配的大小统一为1个字节。注意区分软件意义上的地址,例如数组地址。

2 理解及思想

2.1 为啥需要内存

  • 内存用于存放代码和数据,CPU通过取出某些数据并经过某些代码加工后,可以得到我们需要的结果或者新的数据内容。因此内存是计算机解决实际问题时,必不可少的容器
  • 举例:数学问题,给出已知条件的数据,需要经过代码加工,即算法的实现后,得到结果(如判断某年是否为闰年)或者新数据(如对一幅图像去噪声后的新图像数据)。

2.2 堆和栈区别

  • 首先需要明确,没有堆栈一词,只有堆、栈。
  • 栈的管理由编译器进行,一般在编译前可以通过某些汇编文件对堆、栈大小进行适当调整。
  • 栈之所以需要FILO原则,来源于函数调用时需要保存现场,若多级连续调用,则多级现场压栈,逐级返回时,逐级弹栈,现场也逐级恢复,这符合人类的正常思维流程。
  • 堆内存虽然号称可以动态内存分配,但是动态只是相对于栈而言,体现在编译之后可以根据代码分配一段内存,但是这段内存在分配后,并不能动态变化!即使使用API重新赋值内存大小,之前的数据仍然丢失掉了,而且分配的动态内存需要程序员时刻牢记,用完归还,再借不难。by the way,C#/Java等语言会自动归还垃圾内存。
  • **特别注意:**栈内存是反复被使用的,因此在使用栈内存前请先确保该空间是干净的。
  • **特别注意:**由于系统返给用户堆内存是与堆指针绑定的,因此在释放内存前,请保证该指针的指向性,即不能在未释放内存前就弄丢了该堆内存指针,否则该段内存将泄漏,直到整个程序结束。
  • 用户向系统申请堆内存时,系统会以一个最小单元的整数倍(本机gcc上测试16字节)取值后返回一个void*指针,用户指定时需要自行强制转换成其他指针,以便编译器对内存数据合理解释。可连续分配两块相连内存并将地址相减可得该最小单元字节数。由于申请和释放操作并不是连续的,例如可以连续申请几块内存后,释放其中一块内存,再次申请一块较小的内存,这样就会产生内存碎片,故堆内存的一个缺点是需要处理内存碎片问题。
  • 由于指针的外挂性质,只要获取了一个指针,既可以通过指针运算(移动指针)去访问不属于该指针管辖的内存,这一点是非常危险的,语言本身并未提供可靠机制确保程序员的非法访问。一旦访问到用于空间其他可以操作的内存,可能会篡改其原本的内容。

2.3 位宽寻址

  • 理解位宽寻址,需要从字出发,每个地址可以寻到的最大位宽即表示整机寻址位宽,也就是软硬件达到统一操作内存的条件,对硬件而言,每次操作某段内存,寻址地址都是以整机位宽为单位进行操作,例如32位系统,则每次操作的是4个字节的地址空间,即使软件只操作了该块空间中的一部分,为了达到最高读写效率,硬件在分配内存时仍然以4个字节为单位进行操作(64位系统时操作8个字节)。下面举例说明:

    #include <stdio.h>
    
    int main()
    {
        char *pchar = NULL;
        char test1_char = 0;
        char test2_char = 0;
        int test_int = 0;
        long test1_long = 0;
        long test2_long = 0;
    
        printf("size of point [%ld]\n",sizeof(pchar));
        printf("test1_char [%p]\n",&test1_char);
        printf("test2_char [%p]\n",&test2_char);
        printf("test_int [%p]\n",&test_int);
        printf("test1_long [%p]\n",&test1_long);
        printf("test2_long [%p]\n",&test2_long);
    
       	return 0;
    }
    
  • 2.3.1 在32位系统上运行

    • 结果:

      size of point [4]

      test1_char [0x7ebba243]
      test2_char [0x7ebba242]
      test_int [0x7ebba23c]
      test1_long [0x7ebba238]
      test2_long [0x7ebba234]

    • 分析:

      1、从打印出来的地址数值长度和size of测出的实际值就可以确认在32位系统上,指针类型的长度为4个字节,即系统只为每个指针类型的变量提供4个字节的空间大小,值得一提的是,指向任意类型(char* , int *,short , long等等)的指针变量本身都是如此,而与其指向的空间变量没有半毛钱关系。具体的底层逻辑,请看下节介绍的微机原理中关于寻址的底层逻辑。

      2、看地址值递减规律,可以得到给这些变量分配内存的是一个向下增长的栈。

      3、看下面的表格找规律(截取了地址后两位):

      变量名称地址空间占位统计
      test1_char43--------431
      test2_char42--------421
      仅用于填充41--------402
      test_int3F--------3C4
      test1_long3B--------384
      test2_long37--------344

      4、除了可以轻易看到各类型在32位系统上占内存空间大小外,更重要的是发现中间有用于填充的2个字节的空间碎片,即被系统浪费掉的空间(对程序员而言,他并不知道这2个字节被自己占用了),为啥会这样?正如前文叙述,这是硬件为了达到最高效率而做出的必要牺牲(术语是内存对齐)。

      5、如何填充呢?当系统给test1_char、test2_char各分配完1个字节空间后,紧接着需要给test_int分配4个字节,但此时发现前面已经分配了2个字节,若在test2_char后继续开辟4个字节,则这整段内存为6个字节,不符合最大访问效率的原则,于是舍弃了test2_char后的2个字节用于填充,使其成为整机位宽大小。而test_int则在下一个整机位宽大小的内存单元内分配4个字节,从而保证该变量可以一次完整访问,否则test_int需要分两次访问后拼凑起来才能得到完整的数值。

  • 2.3.2 在64位系统上运行

    • 结果:

      size of point [8]

      test1_char [0x7ffdba20bd47]
      test2_char [0x7ffdba20bd46]
      test_int [0x7ffdba20bd40]
      test1_long [0x7ffdba20bd38]
      test2_long [0x7ffdba20bd30]

    • 分析:

      1、从打印出来的地址数值长度和size of测出的实际值就可以确认在64位系统上,指针类型的长度为8个字节,即系统只为每个指针类型的变量提供8个字节的空间大小,值得一提的是,指向任意类型(char* , int *,short , long等等)的指针变量本身都是如此,而与其指向的空间变量没有半毛钱关系。具体的底层逻辑,请看下节介绍的微机原理中关于寻址的底层逻辑。

      2、看地址值递减规律,可以得到给这些变量分配内存的是一个向下增长的栈。

      3、看下面的表格找规律(截取了地址后两位):

      变量名称地址空间占位统计
      test1_char47--------471
      test2_char46--------461
      仅用于填充45--------442
      test_int43--------404
      test1_long3F--------388
      test2_long37--------308

      4、除了可以轻易看到各类型在64位系统上占内存空间大小外,更重要的是发现中间有用于填充的2个字节的空间碎片,即被系统浪费掉的空间(对程序员而言,他并不知道这2个字节被自己占用了),为啥会这样?正如前文叙述,这是硬件为了达到最高效率而做出的必要牺牲(术语是内存对齐)。

      5、如何填充呢?当系统给test1_char、test2_char各分配完1个字节空间后,紧接着舍弃了test2_char后的2个字节用于填充,之后给test_int分配4个字节,此时前面一共分配了8个字节,即一个整机位宽大小的空间。若继续给test1_long变量分配内存时,就自然必须在下一个整机位宽内存单元里分配了,而该变量本身就占一个整机位宽大小,自然也就无需使用内存碎片进行填充了。

    6、留下疑问:为什么填充的内存碎片不放在test_int后,而要选择放在其之前?

2.4 代码存储知识

2.4.1 总结概括
  • 本文总结代码编译后以及在运行时如何分配各变量内存空间的相关知识。

  • 代码中需要的内存可以来自:栈(stack)堆(heap)数据区(.data)

  • 烧录到硬件中的可执行程序被分成多个段进行存取:代码段数据段bss段

  • bss段又叫ZI(zero initial)段,本质上属于数据区(.data段和.bss段)。这两个段空间是紧密相连的。

  • 代码段中的数据有只读属性,字符串等常量一般存放于此,也有资料说明其存放在数据区,防止篡改。但不是绝对不可更改,依具体平台而定。

  • BSS(Block Started by Symbol),即在系统启动时将该区所有变量值清零。

2.4.2 编程实验
#include <stdio.h>

int g_init_0 = 0;                           /*全局初始化为0*/
int g_init_not_0 = 123;                     /*全局初始化非0*/
int g_uninit;                               /*全局未初始化*/
static int g_s_init_0 = 0;                  /*全局静态初始化为0*/
static int g_s_init_not_0 = 234;            /*全局静态初始化非0*/
static int g_s_uninit;                      /*全局静态未初始化*/
char *g_str_p = "xiangwei come on!";        /*全局字符串指针变量*/
char g_str_array[] = "xiangwei come on!";   /*全局字符数组变量*/

void test(void)
{
     int l_init_0 = 0;                           /*局部初始化为0*/
    int l_init_not_0 = 123;                     /*局部初始化非0*/
    int l_uninit;                               /*局部未初始化*/
    static int l_s_init_0 = 0;                  /*静态局部初始化为0*/
    static int l_s_init_not_0 = 234;            /*静态局部初始化非0*/
    static int l_s_uninit;                      /*静态局部未初始化*/
    char *l_str_p = "xiangwei come on!";        /*局部字符串指针变量*/
    char l_str_array[] = "xiangwei come on!";   /*局部字符数组变量*/

    printf("[%14s] addr---->[%p]\n", "g_init_0", &g_init_0);
    printf("[%14s] addr---->[%p]\n", "g_init_not_0", &g_init_not_0);
    printf("[%14s] addr---->[%p]\n", "g_uninit", &g_uninit);
    printf("[%14s] addr---->[%p]\n", "g_s_init_0", &g_s_init_0);
    printf("[%14s] addr---->[%p]\n", "g_s_init_not_0", &g_s_init_not_0);
    printf("[%14s] addr---->[%p]\n", "g_s_uninit", &g_s_uninit);
    printf("[%14s] addr---->[%p]\n", "g_str_p", g_str_p);
    printf("[%14s] addr---->[%p]\n", "g_str_array", &g_str_array);

    printf("[%14s] addr---->[%p]\n", "l_init_0", &l_init_0);
    printf("[%14s] addr---->[%p]\n", "l_init_not_0", &l_init_not_0);
    printf("[%14s] addr---->[%p]\n", "l_uninit", &l_uninit);
    printf("[%14s] addr---->[%p]\n", "l_s_init_0", &l_s_init_0);
    printf("[%14s] addr---->[%p]\n", "l_s_init_not_0", &l_s_init_not_0);
    printf("[%14s] addr---->[%p]\n", "l_s_uninit", &l_s_uninit);
    printf("[%14s] addr---->[%p]\n", "l_str_p", l_str_p);
    printf("[%14s] addr---->[%p]\n", "l_str_array", &l_str_array);

}

int main(int argc, char const *argv[])
{
    test();
    return 0;
}
/****************** 运行结果为:********************
[      g_init_0] addr---->[0x7f7592001044]
[  g_init_not_0] addr---->[0x7f7592001010]
[      g_uninit] addr---->[0x7f7592001058]
[    g_s_init_0] addr---->[0x7f7592001048]
[g_s_init_not_0] addr---->[0x7f7592001014]
[    g_s_uninit] addr---->[0x7f759200104c]
[       g_str_p] addr---->[0x7f7591e009c4]
[   g_str_array] addr---->[0x7f7592001020]
[      l_init_0] addr---->[0x7fffc3c0bafc]
[  l_init_not_0] addr---->[0x7fffc3c0bb00]
[      l_uninit] addr---->[0x7fffc3c0bb04]
[    l_s_init_0] addr---->[0x7f7592001050]
[l_s_init_not_0] addr---->[0x7f7592001034]
[    l_s_uninit] addr---->[0x7f7592001054]
[       l_str_p] addr---->[0x7f7591e009c4]
[   l_str_array] addr---->[0x7fffc3c0bb10]
****************************************************/
  • 按地址从小到大排列后整理为:
变量名称变量地址意义
全局和局部字符串指针变量值【0x7f79e3e0开头】保存在代码段
g_str_p0x7f79e3e01044"xiangwei come on!"
l_str_p0x7f79e3e01044"xiangwei come on!"
初始化变量(全局和静态局部)【0x7f759200开头】保存在数据段
g_init_not_00x7f7592001010全局初始化非零变量
g_s_init_not_00x7f7592001014全局静态初始化非零变量
g_str_array0x7f7592001020全局初始化字符数组变量
l_s_init_not_00x7f7592001034静态局部初始化非零变量
未初始化非零变量(全局和静态局部)【0x7f759200开头】保存在bss段
g_init_00x7f7592001044全局初始化为零变量
g_s_init_00x7f7592001048全局静态初始化为零变量
g_s_uninit0x7f759200104c全局静态未初始化变量
l_s_init_00x7f7592001050静态局部初始化为零变量
l_s_uninit0x7f7592001054静态局部未初始化变量
g_uninit0x7f7592001058全局未初始化变量
局部非静态变量【0x7f f f c3c0开头】保存在栈空间
l_init_00x7f f f c3c0bafc局部初始化为零变量
l_init_not_00x7f f f c3c0bb00局部初始化为非零变量
l_uninit0x7f f f c3c0bb04局部未初始化变量
l_str_array0x7f f f c3c0bb10局部字符数组
2.4.5 分析总结
  • 搞清楚几个影响变量存储空间的因素,然后排列组合即可。

    1、字符串常量、全局变量、局部变量、静态变量、初始化变量、未初始化变量、零

    2、全局作用性>静态作用性>局部变量

  • 首先排除字符串常量:只要是字符串常量,一定存于代码段。注意一个细节,若字符串常量一样,则即使定义多个字符串常量指针,各指针指向的地址一致。

  • 然后讨论静态变量:首先明确静态变量一定在数据区(.data段或.bss段),总之,其一定不在栈上,也就是说其生命周期是整个程序运行期间,而作用域取决于其定义的位置。另外,对于全局变量作用时,静态不起作用,请按全局变量规则分析。最后对于静态局部变量,除初始化为非零的变量位于.data段,其余变量均位于.bss段

  • 再看全局变量:除未初始化为非零的变量位于.bss段,其余变量均位于.data段

  • 最后分析局部变量:一般的局部变量,即非静态局部变量,则其分配在栈空间上。

2.4.6 排雷专区
  • 注意上述分析中将全局变量中初始化为零和未初始化变量均放在了.bss段,也就是说全局变量中即使初始化为零也被视为需要重新被系统清零的变量。
  • 如何证明:在连续定义多个初始化为零以及未初始化的全局变量后,读出各变量地址,可以发现初始化为零的变量存放在未初始化的变量之前,即上述代码中g_init_0l_s_init_not_0之间间隔超出了一个整型的空间,而g_init_0与后面的变量却是无间隔相连的。

3 微机寻址

3.1 CPU与内存的关系

  • 首先拿出一张大学微机原理课本的图(《微型计算机原理与接口技术》冯博琴(第三版) 第五章存储器系统)

    存储器系统

  • 介绍下已知条件:该图可以大致分为三个部分

    1、最左边的1号区域为CPU的系统总线,从命名看可以分为三大部分,一类为数据总线(D0D7),一类为地址总线(A0A10),剩下的作为生成片选信号的总线,值得注意的是,该总线实际上是利用了CPU地址总线部分地址线来达到目的的,这部分地址线(A11~A18)对于存储器(3、4号区域)而言是无感的。

    2、中间的2号区域为LS138俗称3-8译码器,用于生成最终的片选信号,从而决定CPU到底选择哪个存储器进行操作,其中的具体原理,不属于微机原理部分,此处不做探讨,请参考数字电路原理对LS138进行详细探究。

    3、右边的3、4号区域为存储器,其中CS引脚电平用于确认该存储器是否被CPU选中。

3.2 CPU寻址和位宽寻址的关系

  • 以上图系统为例,CPU寻址中的实际上也包括了生成片选信号的A11A18部分,而编程语言中的`位宽寻址`概念中的`址`主要指存储器上用于识别数据存放位置的A0A10部分。
  • 在上述存储器系统中可以看出,该存储器的位宽为8位,即D0D7数据位宽,当A0A10给定一个地址,则CPU会从该存储器中取出一个字节的数据,也就是说CPU操作一次存储器只能取出一个位宽数据大小的数据。

3.3 对齐访问的意义

  • 假设现在我需要存储这样两个数据,一个数据a,其需要占位4位,一个数据b,其需要占位6位,如何给变量a,b分配内存空间?
  • 按依次分配不留碎片空间的方法:第一块位宽内存单元里分配了数据a完整的4位空间,而变量b被拆分在第一块内存单元里4位空间,第二块内存单元里2位空间,那么当CPU需要取出或写入变量b时,则需要给出两个地址,操作两次存储器才能将完整的b数据取出或写入,其中还要涉及到两次数据的拼凑操作,这需要额外的CPU硬件空间投入以及时间投入,大大降低了操作效率。
  • 以位宽大小为单位分配空间的方法:第一块位宽内存单元里分配了数据a完整的4位空间,将剩下的4位空间填充,并将变量b分配到第二块内存单元空间中,占6位,同理若变量b后需要分配一个超过2(计算8-6)位大小的变量,则仍然继续填充这两位空间。显然这样做能够提高CPU操作效率,唯一表面上看带来的“坏处”是给存储器空间浪费了2位空间而已。
  • 两害相权取其轻,CPU运行效率显然更加重要,何况CPU运行速度远远高于存储器,当CPU运行了这个操作后,还需要去完成很多其他工作,不会一直等待存储器完成操作。对CPU而言,完成一次存取操作是异步的,从而提高CPU整体的性能。另外从现实世界的成本来看,存储器成本不断降低,而世界更重效率和用户体验。

4 内存数据的解释

4.1 数据类型

  • 数据类型是编程语言中的概念,其定义了一段内存中数据的组织方式,即某个类型对应的数据应如何以二进制的形式存取在内存之中。
  • 不同的数据类型占据的内存大小不一样或者即使占据的内存大小一样,但是存取的数据格式并不相同,例如64位系统中long类型和double类型同样占据8个字节的大小空间,即使在这两段空间里存放完全相同的二进制数据,但由于其数据格式不一样,导致读取该段内存数据时会产生不一样的解释方式,即得到了不同的结果。
  • 由于每个类型规定了占用内存空间的大小,也就确认了可以表示的数据范围,例如char类型占据了8位空间,即一共可以表示的数据个数为:2^8=256个,若char类型表示为signed char,则实际表示的范围为(-)128到(+)127,也就是说如果你强制给该类型赋值超出范围的值例如128将会溢出,内存中会存放溢出后剩下的低8位数据,而进位数据将会被舍弃。

4.2 编译器类型检查

  • C语言编译器提供了编译时类型检查,若程序员定义的变量类型与使用时需要的类型不匹配,则会抛出警告或者错误,以提示程序员修改程序。
  • 如果程序员非常清楚自己正在做什么,有些警告可以忽略。例如给一个规定为const函数形参传入一个同类型的非const的变量,则编译器会抛出警告,但仍然可以正常运行(编译器会做一些工作)。
  • 众所周知,抛出错误,就只能修改到无错误为止,否则无法生成最终的可执行程序,也就无法运行程序。

4.3 强制类型转换

  • 首先需要强调的是在C语言编程中,强制类型转换分两种操作:数据直接转换取地址间接转换

  • 数据直接转换涉及对内存数据的二次解释,先按原先的数据类型解释取该数据后,再根据需要的类型进行二次解释,这样不会出现在不兼容类型间的数据解释出错的情况,例如:long类型的1234可以无报错的转换成double类型的1234.000000。但是注意:高精度类型转向低精度类型时,会丢掉部分精度,例如浮点型转整型时,只取出整数部分

  • 温馨提示:使用printf函数打印浮点型数据时默认只显示小数点后六位,如需显示更多位数,请使用%.xlf进行显示位数x的指定。

  • 取地址间接转换不涉及内存数据的二次解释,而是直接使用需要的类型进行一次解释,这样会出现不兼容类型见的数据解释出错的情况,例如:long类型的1234可以无报错的转换成double类型0.000000。

  • 具体的代码段如下:

    #include <stdio.h>
    
    int main()
    {
        char *pchar = NULL;
     	long test_long = 1234;
        
        printf("size of point [%ld]\n",sizeof(pchar));
        printf("long====[%ld]\n", test_long);				/*没有进行转换*/
        printf("double====[%lf]\n", (double)test_long);		/*数据直接转换*/
        printf("double====[%lf]\n", *(double*)&test_long);	/*取地址间接转换*/
        
       	return 0;
    }
    
    /*************运行结果为:***************
    
    long====[1234]
    double====[1234.000000]
    double====[0.000000]
    ***************************************/
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fffxxx222

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值