C语言内存问题总结

内存机制

计算机程序的运行过程,其实就是程序中很多个函数相继运行的过程。程序是由很多个函数组成的,程序的本质就是函数,函数的本质是加工数据的动作。

为什么需要内存呢?

内存是用来存储可变数据的,数据在程序中表现为全局变量、局部变量等(在gcc中,其实常量也是存储在内存中的)(大部分单片机中,常量是存储在flash中的,也就是在代码段),对我们写程序来说非常重要,对程序运行更是本质相关。

所以内存对程序来说几乎是本质需求。越简单的程序需要越少的内存,而越庞大越复杂的程序需要更多的内存。内存管理是我们写程序时很重要的话题。我们以前学过的了解过的很多编程的关键其实都是为了内存,譬如说数据结构(数据结构是研究数据如何组织的,数据是放在内存中的)和算法(算法是为了用更优秀更有效的方法来加工数据,既然跟数据有关就离不开内存)。

什么是内存?
从硬件角度:内存实际上是电脑的一个配件(一般叫内存条)。根据不同的硬件实现原理还可以把内存分成SRAM和DRAM。
从逻辑角度:内存是这样一种东西,它可以随机访问(随机访问的意思是只要给一个地址,就可以访问这个内存地址)、并且可以读写(当然了逻辑上也可以限制其为只读或者只写);内存在编程中天然是用来存放变量的(就是因为有了内存,所以C语言才能定义变量,C语言中的一个变量实际就对应内存中的一个单元)。


从逻辑角度来讲,内存实际上是由无限多个内存单元格组成的,每个单元格有一个固定的地址叫内存地址,这个内存地址和这个内存单元格唯一对应且永久绑定。
以大楼来类比内存是最合适的。逻辑上的内存就好象是一栋无限大的大楼,内存的单元格就好象大楼中的一个个小房间。每个内存单元格的地址就好象每个小房间的房间号。内存中存储的内容就好象住在房间中的人一样。
逻辑上来说,内存可以有无限大(因为数学上编号永远可以增加,无尽头)。但是现实中实际的内存大小是有限制的,譬如32位的系统(32位系统指的是32位数据线,但是一般地址线也是32位,这个地址线32位决定了内存地址只能有32位二进制,所以逻辑上的大小为2的32次方)内存限制就为4G。实际上32位的系统中可用的内存是小于等于4G的。

如何管理内存(无OS时,有OS时)?

对于计算机来说,内存容量越大则可能性越大,所以大家都希望自己的电脑内存更大。我们写程序时如何管理内存就成了很大的问题。如果管理不善,可能会造成程序运行消耗过多的内存,这样迟早内存都被你这个程序吃光了,当没有内存可用时程序就会崩溃。所以内存对程序来说是一种资源,所以管理内存对程序来说是一个重要技术和话题。

先从操作系统角度讲:操作系统掌握所有的硬件内存,因为内存很大,所以操作系统把内存分成1个1个的页面(其实就是一块,一般是4KB),然后以页面为单位来管理。页面内用更细小的方式来以字节为单位管理。操作系统内存管理的原理非常麻烦、非常复杂、非常不人性化。那么对我们这些使用操作系统的人来说,其实不需要了解这些细节。操作系统给我们提供了内存管理的一些接口,我们只需要用API即可管理内存。譬如在C语言中使用malloc free这些接口来管理内存。

没有操作系统时:在没有操作系统(其实就是裸机程序)中,程序需要直接操作内存,编程者需要自己计算内存的使用和安排。如果编程者不小心把内存用错了,错误结果需要自己承担。
    
再从语言角度来讲:不同的语言提供了不同的操作内存的接口。

譬如汇编:根本没有任何内存管理,内存管理全靠程序员自己,汇编中操作内存时直接使用内存地址(譬如0xd0020010),非常麻烦;

譬如C语言:C语言中编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过API(malloc free)来访问系统内存。裸机程序中需要大块的内存需要自己来定义数组等来解决。
譬如C++语言:C++语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C++语言对内存的管理比C要高级一些,容易一些。但是C++中内存的管理还是靠程序员自己来做。如果程序员new了一个对象,但是用完了忘记delete就会造成这个对象占用的内存不能释放,这就是内存泄漏。
Java/C#等语言:这些语言不直接操作内存,而是通过虚拟机来操作内存。这样虚拟机作为我们程序员的代理,来帮我们处理内存的释放工作。如果我的程序申请了内存,使用完成后忘记释放,则虚拟机会帮我释放掉这些内存。听起来似乎C# java等语言比C/C++有优势,但是其实他这个虚拟机回收内存是需要付出一定代价的。所以说语言没有好坏,只有适应不适应。当我们程序对性能非常在乎的时候(譬如操作系统内核)就会用C/C++语言;当我们对开发程序的速度非常在乎的时候,就会用Java/C#等语言。  

静态内存分配和动态内存分配

静态内存是指程序开始运行时,由编译器自动分配和释放空间。程序中的各种变量,在程序编译时都需要分配空间,当函数调用完,空间自动释放。此时开发者不需要关心空间的申请与释放问题。

静态内存管理由两个特点:

1.空间开辟的大小时固定的
2.数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

而动态内存分配,可以很好的解决上面的问题,当需要开辟的空间不确定时,静态内存管理就不好定义变量的大小,这是使用动态内存,可以得到一个变常数组,大小可以由自己开辟。并且动态内存可以提供大块的内存。
注意:动态内存是在堆上开辟的,必须由开发者申请释放

C语言如何操作内存

c语言中用变量名来访问内存,譬如在C语言中:

int a;
a = 5;
a += 4;  // a == 9;

结合内存来解析C语言语句的本质:
int a;    // 编译器帮我们申请了1个int类型的内存格子,长度是4字节,地址是确定的,但是只有编译器知道,我们是不知道的,也不需要知道。并且把符号a和这个格子绑定。
a = 5;    // 编译器发现我们要给a赋值,就会把这个值5丢到符号a绑定的那个内存格子中。
a += 4;    // 编译器发现我们要给a加值,a += 4 等效于 a = a + 4;编译器会先把a原来的值读出来,然后给这个值加4,再把加之后的和写入a里面去。

C语言中数据类型的本质含义是:表示一个内存格子的长度和解析方法。
数据类型决定长度的含义:我们一个内存地址(0x30000000),本来这个地址只是1个字节的地址,但是实际上我们可以通过给他一个类型(int),让他有了长度(4),这样这个代表内存地址的数字(0x30000000)就能表示从这个数字(0x30000000)开头的连续的n(4)个字节的内存格子了(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)。
数据类型决定解析方法的含义:譬如我有一个内存地址(0x30000000),我们可以通过给这个内存地址不同的类型来指定这个内存单元格子中二进制数的解析方法。譬如我 (int)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个int型数据;那么我(float)0x30000000,含义就是(0x30000000 + 0x30000001 + 0x30000002 + 0x30000003)这4个字节连起来共同存储的是一个float型数据;

C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。

可以用指针来间接访问内存。

用数组来管理内存
数组管理内存和变量其实没有本质区别,只是符号的解析方法不同。(普通变量、数组、指针变量其实都没有本质差别,都是对内存地址的解析,只是解析方法不一样)。
int a;        // 编译器分配4字节长度给a,并且把首地址和符号a绑定起来。
int b[10];    // 编译器分配40个字节长度给b,并且把首元素首地址和符号b绑定起来。

数组中第一个元素(a[0])就称为首元素;每一个元素类型都是int,所以长度都是4,其中第一个字节的地址就称为首地址;首元素a[0]的首地址就称为首元素首地址。

程序的内存到底从哪来?

对程序来说,内存就是程序的立足之地(程序是被放在内存中运行的);程序运行时需要内存来存储一些临时变量。

 
内存管理最终是由操作系统完成的
(1)内存本身在物理上是一个硬件器件,由硬件系统提供。
(2)内存是由操作系统统一管理。为了内存管理方便又合理,操作系统提供了多种机制来让我们应用程序使用内存。这些机制彼此不同,各自有各自的特点,我们程序根据自己的实际情况来选择某种方式获取内存(在操作系统处登记这块内存的临时使用权限)、使用内存、释放内存(向操作系统归还这块内存的使用权限)。
三种内存来源:栈(stack)、堆(heap)、数据区(.data)
在一个C语言程序中,能够获取的内存就是三种情况:栈(stack)、堆(heap)、数据区(.data)

栈的详解
运行时自动分配&自动回收:栈是自动管理的,程序员不需要手工干预。方便简单。
反复使用:栈内存在程序中其实就是那一块空间,程序反复使用这一块空间。
脏内存:栈内存由于反复使用,每次使用后程序不会去清理,因此分配到时保留原来的值。
临时性:(函数不能返回栈变量的指针,因为这个空间是临时的)
栈会溢出:因为操作系统事先给定了栈的大小,如果在函数中无穷尽的分配栈内存总能用完。 


堆内存详解
操作系统堆管理器管理:堆管理器是操作系统的一个模块,堆管理内存分配灵活,按需分配。
大块内存:堆内存管理着总量很大的操作系统内存块,各进程可以按需申请使用,使用完释放。
程序手动申请&释放:手工意思是需要写代码去申请malloc和释放free。
脏内存:堆内存也是反复使用的,而且使用者用完释放前不会清除,因此也是脏的。
临时性:堆内存只在malloc和free之间属于我这个进程,而可以访问。在malloc之前和free之后都不能再访问,否则会有不可预料的后果。

数据段、bss段、代码段
编译器在编译程序的时候,将程序中的所有的元素分成了一些组成部分,各部分构成一个段,所以说段是可执行程序的组成部分。
数据段(也被称为数据区、静态数据区、静态区):数据段就是程序中的数据,直观理解就是C语言程序中的全局变量。(注意:全局变量才算是程序的数据,局部变量不算程序的数据,只能算是函数的数据)
bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。

代码段:代码段就是程序中的可执行部分,直观理解代码段就是函数堆叠组成的。


数据段(.data)和bss段的区别和联系:二者本来没有本质区别,都是用来存放C程序中的全局变量的。区别在于把显示初始化为非零的全局变量存在.data段中,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量值默认为0)的全局变量存在bss段。

另外,和普通局部变量分配在栈上不同,静态局部变量,也就是static修饰的局部变量,也在数据段。

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

如果我需要一段内存来存储数据,我究竟应该把这个数据存储在哪里?(或者说我要定义一个变量,我究竟应该定义为局部变量还是全局变量还是用malloc来实现)。不同的存储方式有不同的特点,简单总结如下:
* 函数内部临时使用,出了函数不会用到,就定义局部变量
* 堆内存和数据段几乎拥有完全相同的属性,大部分时候是可以完全替换的。但是生命周期不一样:

堆内存的生命周期是从malloc开始到free结束,而全局变量是从整个程序一开始执行就开始,直到整个程序结束才会消灭,伴随程序运行的一生。启示:如果你这个变量只是在程序的一个阶段有用,用完就不用了,就适合用堆内存;如果这个变量本身和程序是一生相伴的,那就适合用全局变量。(堆内存就好象租房、数据段就好象买房。堆内存就好象图书馆借书,数据段就好象自己书店买书)你以后会慢慢发现:买不如租,堆内存的使用比全局变量广泛。

C语言中的栈和堆

我们在C中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。


注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。


然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作是弹栈(出栈)。出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。这个动作也是自动的,也不用人写代码干预。


栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。


分析一个细节:C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?


定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的),所以说使用栈来实现的局部变量定义时如果不显式初始化,值就是脏的。如果你显式初始化怎么样?
C语言是通过一个小手段来实现局部变量的初始化的。

int a = 15;        // 局部变量定义时初始化

C语言编译器会自动把这行转成:
 

int a;            // 局部变量定义
a = 15;            // 普通的赋值语句

栈的不足之处:
首先,栈是有大小的。所以栈内存大小不好设置。如果太小怕溢出,太大怕浪费内存。(这个缺点有点像数组)
其次,栈的溢出危害很大,一定要避免。所以我们在C语言中定义局部变量时不能定义太多或者太大(譬如不能定义局部变量时 int a[10000]; 使用递归来解决问题时一定要注意递归收敛)

堆(heap)是一种内存管理方式。内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,其次内存需求在时间和大小块上没有规律(操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,申请或者释放的内存块大小随意)。
堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内存管理单元)来管理的,然后向使用者(用户进程)提供API(malloc和free)来使用堆内存。
我们什么时候使用堆内存?需要内存容量比较大时,需要反复使用及释放时,很多数据结构(譬如链表)的实现都要使用堆内存。

堆管理内存的特点(大块内存、手工分配&使用&释放)
特点一:容量不限(常规使用的需求容量都能满足)。
特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存),称为内存泄漏。在C/C++语言中,内存泄漏是最严重的程序bug,这也是别人认为Java/C#等语言比C/C++优秀的地方。


C语言操作堆内存的接口(malloc free)
堆内存释放时最简单,直接调用free释放即可。void free(void *ptr);
堆内存申请时,有3个可选择的类似功能的函数:malloc, calloc, realloc
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);    // nmemb个单元,每个单元size字节
void *realloc(void *ptr, size_t size);        // 改变原来申请的空间的大小的

譬如要申请10个int元素的内存:
malloc(40);            malloc(10*sizeof(int));
calloc(10, 4);         calloc(10, sizeof(int));

数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户;

堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc接口。realloc的实现原理类似于上面说的Java中的可变大小的数组的方式。


堆的优势和劣势(管理大块内存、灵活、容易内存泄漏)
优势:灵活;
劣势:需要程序员去处理各种细节,所以容易出错,严重依赖于程序员的水平。

linux下C程序的内存映像

代码段(只读数据段)

对应着程序中的代码(函数),代码段在linux中又叫文本段(.text)

只读数据段就是在程序运行期间只能读不能写的数据,const修饰的常量有可能是存在只读数据段的(但是不一定,const常量的实现方法在不同平台是不一样的)

数据段、bss段
数据段存:1、显式初始化为非0的全局变量;2、显式初始化为非0的static局部变量
bss段存:1、显式初始化为0或者未显式初始化的全局变量;2、显式初始化为0或未显式初始化的static局部变量。


C语言中什么样变量存在堆内存中?C语言不会自动向堆中存放东西,堆的操作是程序员自己手工操作的。程序员根据需求自己判断要不要使用堆内存,用的时候自己申请,自己使用,完了自己释放。

文件映射区
文件映射区就是进程打开了文件后,将这个文件的内容从硬盘读到进程的文件映射区,以后就直接在内存中操作这个文件,读写完了后在保存时再将内存中的文件写到硬盘中去。


栈内存区,局部变量分配在栈上;函数调用传参过程也会用到栈。

内核映射区
内核映射区就是将操作系统内核程序映射到这个区域了。
对于linux中的每一个进程来说,它都以为整个系统中只有它自己和内核而已。它认为内存地址0xC0000000以下都是它自己的活动空间,0xC0000000以上是OS内核的活动空间。
每一个进程都活在自己独立的进程空间中,0-3G的空间每一个进程是不同的(因为用了虚拟地址技术),但是内核是唯一的。

OS下和裸机下C程序加载执行的差异
C语言程序运行时环境有一定要求,意思是单独个人写的C语言程序没法直接在内存中运行,需要外部一定的协助,这段协助的代码叫加载运行代码(或者叫构建C运行时环境的代码,这一段代码在操作系统下是别人写好的,会自动添加到我们写的程序上,这段代码的主要作用是:给全局变量赋值、清bss段)。
ARM裸机第十六部分,写shell时有一次定义了一个全局变量初始化为0但是实际不为0,后来在裸机的start.S中加了清bss段代码就变0了。这就说明在裸机程序中没人帮我们来做这一段加载运行时代码,要程序员自己做(start.S中的重定位和清bss段就是在做这个事);在操作系统中运行程序时程序员自己不用操心,会自动完成重定位和清bss,所以我们看到的现象:C语言中未初始化的全局变量默认为0。
数据段的全局变量或静态局部变量都是有非0的初值的,这些初值在main函数运行之前就已经被初始化了,是重定位期间完成的初始化。

补充:堆内存申请和释放

void *malloc(size_t size)
分配所需的内存空间,并返回一个指向它的指针。

void free(void *ptr)
释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。

void *是个指针类型,malloc返回的是一个void *类型的指针,实质上malloc返回的是堆管理器分配给我本次申请的那段内存空间的首地址(malloc返回的值其实是一个数字,这个数字表示一个内存地址)。为什么要使用void *作为类型?主要原因是malloc帮我们分配内存时只是分配了内存空间,至于这段空间将来用来存储什么类型的元素malloc是不关心的,由我们程序自己来决定。


malloc的返回值:成功申请空间后返回这个内存空间的指针,申请失败时返回NULL。所以malloc获取的内存指针使用前一定要先检验是否为NULL。


malloc申请的内存时用完后要free释放。free(p);会告诉堆管理器这段内存我用完了你可以回收了。堆管理器回收了这段内存后这段内存当前进程就不应该再使用了。因为释放后堆管理器就可能把这段内存再次分配给别的进程,所以你就不能再使用了。


在调用free归还这段内存之前,指向这段内存的指针p一定不能丢(也就是不能给p另外赋值)。因为p一旦丢失这段malloc来的内存就永远的丢失了(内存泄漏),直到当前程序结束时操作系统才会回收这段内存。


gcc中的malloc默认最小是以16B为分配单位的。如果malloc小于16B的大小时都会返回一个16字节的大小的内存。

补充:单片机中的内存管理

注意,以上是在操作系统(Liunx)下的内存管理。

在单片机中,内存管理方式虽然类似,但并非完全一样。

单片机的程序分为code(代码存储区)、RO-data(只读数据存储区)、RW-data(读写数据存储区)和ZI-data(零初始化数据区)。在MDK编译器下可以观察到在代码中这4个量的值。

Code:代码,程序中所有的函数都位于此处

RO-data:只读数据 (const 修饰的 ,字符串常量)

RW-data:读写数据 ,程序中定义并且初始化为非0的全局变量和静态变量位于此处

ZI—data :初始化为0数据 (包括栈空间和堆空间)

这里没有提到局部变量,局部变量在哪?也就是说,栈在哪?

代码烧录时,code、RO-data和RW-data存储在flash中,所以三者之和为单片机中flash需要分配给它们的空间大小,另外RW-data和ZI-data存储在sram中,同样两者之和为单片机中sram需要分配给它们的空间大小。

如果你查看.map文件,如下例子:

==================================================================

    Total RO  Size (Code + RO Data)                 2980 (   2.91kB)
    Total RW  Size (RW Data + ZI Data)               104 (   0.10kB)
    Total ROM Size (Code + RO Data + RW Data)       2988 (   2.92kB)

==================================================================

Total ROM Size (Code + RO Data + RW Data)这样所写的程序占用的ROM的字节总数,也就是说程序所下载到ROM flash 中的大小。为什么Rom中还要存RW,因为掉电后RAM中所有数据都丢失了,每次上电RAM中的数据是被重新赋值的,每次这些固定的值就是存储在Rom中的,为什么不包含ZI段呢,是因为ZI数据都是0,没必要包含,只要程序运行之前将ZI数据所在的区域一律清零即可,包含进去反而浪费存储空间。

 实际上,ROM中的指令至少应该有这样的功能:
1. 将RW从ROM中搬到RAM中,因为RW是变量,变量不能存在ROM中。
2. 将ZI所在的RAM区域全部清零,因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零。ZI中也是变量,同理:变量不能存在ROM中。
在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量。否则只能运行不含变量的代码。

以上总结一下就是:固化的内容放在falsh中,变量放在内存中。

我的疑惑,为什么变量会存在RAM中,要说运行的时候存在里面是可以的,但是一开始不应该都下载到flash中,然后加载到内存中吗?

我觉得这里的意思应该是,要加载到内存中的意思,即一开始全烧录到flash中,然后运行时只把rw和zi区加载到内存,可是取指令难道从flash中取?

参考一些说法:

cpu从rom开始执行指令,rom中的指令再操作ram,当然也可以将指令拷贝到ram中。计算机操作系统就是从rom开始,将磁盘中的数据拷贝到ram中去执行的。

8位单片机都是直接在ROM中运行。某些十六位的也是比如96单片机;32位的单片机基本就不是在ROM中运行了,而是在动态RAM中运行。

常规上ROM是用来存储固化程序的,RAM是用来存放数据的。由于FLASH ROM比普通的ROM读写速度快,擦写方便,一般用来存储用户程序和需要永久保存的数据。譬如说,现在家用的电子式电度表,它的内核是一款单片机,该单片机的程序就是存放在ROM里的。电度表在工作过程中,是要运算数据的,要采集电压和电流,并根据电压和电流计算出电度来。电压和电流是一个实时的数据,用户不关心,它只是用来计算电度用,计算完后该次采集的数据就用完了,然后再采集下一次,因此这些值就没必要永久存储,就把它放在RAM里边。然而计算完的电度,是需要永久保存的,单片机会定时或者在停电的瞬间将电度数存入到FLASH里。

继续查资料:

下文来说明单片机与x86cpu的具体途径:

pc机在运行程序的时候先把程序从硬盘中,调入RAM中运行,cpu从RAM中读取程序和数据。

而单片机的程序则是固化在flash 中,cpu运行的时候直接从flash 中读取程序,从RAM中读取数据。

造成这种差别的原因:

x86架构的cpu是基于冯.诺伊曼体系的,即数据和程序储存在一起,而且pc机的RAM资源相当丰富,从几十M到几百M甚至是几个G,客观上能承受大量的程序数据。

而单片机的架构大多是哈弗体系的,即程序和数据分开储存,而且单片机的片内RAM资源是相当有限的,内部的RAM过大会带来成本的大幅度提高。

单片机的程序能存储在RAM中吗?
通过上面的分析可得知:单片机的程序能存储于flash中是基于两点考虑,即体系结构和RAM资源的多少。因此,在技术不断进步,片内RAM容量不断增多的今天,RAM资源已经不再是制约这种差别的主要因素,而对于体系机构我们只要更改cpu读取程序的方式就可以。


进一步理解变量:

我仔细想了下,一开始,我们只有一个程序,代码就是固定的代码,常量是在编译的时候确定的,那么变量呢?

注意:变量在没有运行之前是不存在的!

变量是在程序的运行过程中产生的,执行相应的指令,就生成相应的变量,然后分配对应的内存。

这就是,为什么说变量是存在内存中的原因。

变量都是临时的,只要程序代码没变,那么,每次执行,生成的变量和数据都是一致的,无论执行多少次都不会变化。

所以,不要再觉得代码是代码,变量是变量了,得执行代码,才有变量。

结合以上内容,总结如下:

烧录时,固化部分(code+ro)被下载到flash中,开始执行,执行期间的所有变量都临时保存在内存中,如果有哪个变量结果需要被永久保存下来,就写入到flash中。

强调

C语言中的全局变量存储在静态存储区,静态存储区又称为全局数据区或全局区。静态存储区是由编译器在程序启动时分配的内存区域,其大小是程序编译时确定的。在静态存储区中声明的变量在程序的整个运行期间都存在,并且在程序启动时就已经初始化。

静态存储区由两部分组成:

1. 数据段(Data Segment)

数据段用于存储程序中已经初始化的全局变量和静态变量。数据段中的变量在程序启动时就已经分配了内存空间,变量的初值都已经确定。数据段的大小由程序中已经初始化的全局变量和静态变量的总大小决定。

2. BSS段

BSS段用于存储程序中未初始化的全局变量和静态变量。BSS段中的变量在程序启动时没有实际的值,操作系统会为其分配一段空间,并在内部设置为0。因此,BSS段中的变量会占用一定的内存空间,但不会占用程序的可执行文件的磁盘空间。BSS段的大小由程序中未初始化的全局变量和静态变量的总大小决定。

总的来说,C语言中的全局变量存储在静态存储区中,静态存储区由数据段和BSS段组成,全局变量的存储位置取决于变量是否被初始化。已经初始化的全局变量存储在数据段中,未初始化的全局变量存储在BSS段中。

内存分配图

代码区
代码区(Code Segment)是存放可执行程序代码的内存区域,也称为文本区。在程序运行时,操作系统会将可执行文件中的机器指令加载到该区域,并将其设为只读(Read-Only),保证代码的安全性和一致性。

由于该区域的内容只需要在程序启动时进行加载,因此代码区的大小通常是固定的,不会随着程序运行的过程而发生变化。

需要注意的是,由于代码区是只读的,程序无法对该区域的内容进行修改。如果程序试图修改代码区的内容,将会导致访问异常错误。

以上都是针对PC系统中的,在嵌入式系统中,有所区别,主要体现在只读区,也就是代码段或者文本段。

对于PC来说,可执行文件存储在磁盘上,当执行的时候,将文件内容(代码和数据)加载到内存中去执行。

而对于单片机来说,程序文件存在ROM中,执行的时候,指令代码不用加载到RAM即可执行。

MCU 内部的 Flash 是可以直接运行代码的,也就是说,可执行文件的 Code 和 RO Data不会被加载到 RAM中。

也就是说,单片机中的代码和常量数据都是直接存放在flash中的。

具体可参考这篇:单片机可以从Flash中直接运行代码的原理 - 知乎

这里其实就是单片机内部flash和内部ram的起始地址以及大小,比如上面大小分别为512k和128k字节大小,就对应了单片机自带的存储空间。

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值