c语言高级专题——内存

朱有鹏老师笔记结合自己理解方便后期查阅编写的博客
1.1.程序运行为什么需要内存
1.1.1、计算机程序运行的目的
我们总是在编写代码,程序 那么我们编写程序又是为了什么呢?
程序运行的目的不外乎2个 :结果和过程
就是对数据加工的过程,和对数据加工后得到的结果

例如以函数为例
int add(int a, int b)
    {
        return a + b;
    }           // 这个函数的执行就是为了得到结果
    void add(int a, int b)
    {
        int c;
        c = a + b;
        printf("c = %d.\n", c);
    }   //只是在乎过程

程序是由很多个函数组成的,程序的本质就是函数,函数的本质是加工数据的动作。

1.1.3、冯诺依曼结构和哈佛结构
冯诺依曼结构是:数据和代码放在一起。(统一编址)——————方便
哈佛结构是:数据和代码分开存在。(独立编址)———————安全
程序和数据都放在内存中,且不彼此分离的结构称为冯诺依曼结构。譬如Intel的CPU均采用冯诺依曼结构。
程序和数据分开独立放在不同的内存块中,彼此完全分离的结构称为哈佛结构。譬如大部分的单片机(MCS51、ARM9等)均采用哈佛结构。
冯诺依曼结构中程序和数据不区分的放在一起,因此安全和稳定性是个问题,好处是处理起来简单。一些黑客就是利用存储在一起来进行黑数据的,
哈佛结构中程序(一般放在ROM、flash中)和数据(一般放在RAM中)独立分开存放,因此好处是安全和稳定性高,缺点是软件处理复杂一些(需要统一规划链接地址等)嵌入式方面安全是十分重要的因此采用哈弗结构

1.1.4。对内存的管理
有操作系统的时候,系统会对内存进行管理,则我们使用只需使用系统提供的API(如c语言中的malloc 和 free)
无操作系统的时候就是 例如裸机过程中 我们需要自己计算内存位置使用安排的,

从语言角度分析
譬如汇编:汇编中操作内存时直接使用内存地址(譬如0xd00200
譬如C语言:编译器帮我们管理直接内存地址,我们都是通过编译器提供的变量名等来访问内存的,操作系统下如果需要大块内存,可以通过API(malloc free)来访问系统内存。裸机程序中需要大块的内存需要自己来定义数组等来解决。
譬如C++语言:C++语言对内存的使用进一步封装。我们可以用new来创建对象(其实就是为对象分配内存),然后使用完了用delete来删除对象(其实就是释放内存)。所以C++语言对内存的管理比C要高级一些,容易一些。但是C++中内存的管理还是靠程序员自己来做。如果程序员new了一个对象, 但是用完了忘记delete就会造成这个对象占用的内存不能释放,这就是内存泄漏。
Java/C#等语言:这些语言不直接操作内存,而是通过虚拟机来操作内存。

现在进入正题 就是我们会用到的知识点

计算机中最小的单位是bit位,但是我们常常是以字节Byte为单元单位的 都是以字节为单位进行计算的
1Byte = 8bit

以32位计算机为例:他的内存分布实图是什么样子呢 ?也就是我们在分析内存问题时要怎么模拟出内存来进行操作呢?
这里写图片描述

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

根据上面的实体图理解
内存编址方法:这个内存地址(一个数字)和这个格子的空间(实质是一个空间)是一一对应且永久绑定的。这就是内存的编址方法。
所以说内存单元的2个概念:地址和空间是内存单元的两个方面。
地址就是0x的标号 空间就是我们真正的内存,存储数据就是存储在空间中

再次强调:内存编址是以字节为单位的

内存和数据类型的关系:
数据类型和内存的关系就在于:数据类型是用来定义变量的,而这些变量需要存储、运算在内存中。所以数据类型必须和内存相匹配才能获得最好的性能,否则可能不工作或者效率低下。
在32位系统中定义变量最好用int,因为这样效率高。因为刚好是32位总线 每个时钟节拍都是传输32位,而int类型的刚好也是32位的数据 因此刚好匹配,因此有时候我们定义bool类型也会将其转换成int来存储读取,来提高效率、

内存对齐;
这里写图片描述

2:接下来真正进入我们从内存对编程进行了解了,
在汇编中 对内存地址直接使用,(例如:我们要操作一个内存单元 那么就直接通过起始地址0x,对其的空间直接放值)
ldr r1, = 0x00001000
ldr r2, = 要放入的数值
str r2, [r1]
就这样来进行存储的

在c语言中 编译器就对内存进行了封装 ,
int a; // 编译器帮我们申请了1个int类型的内存格子(长度是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型数据;
之前讲过一个很重要的概念:内存单元格子的编址单位是字节。
这里写图片描述

采用内存角度重新理解数据类型
普通变量上面已经分析过了,其实其余特殊一点点的类型都是类似的方法。

用指针来间接访问内存:
指针类型其实也是普通变量 指针长度为4,解析方法就是把存储的4个字节的数据按照是地址的方法来解析出来
因为内存空间中存放的是地址 ,我们就可以通过这个地址来访问这个地址的内存空间 来达到间接访问内存,

用数组来管理内存
数组管理内存和变量其实没有本质区别,只是规定的长度和符号的解析方法不同。
规定的长度要看怎么定义的
这里写图片描述

结构体内嵌指针实现面向对象
面向过程与面向对象。
总的来说:C语言是面向过程的,但是C语言写出的linux系统是面向对象的。
非面向对象的语言,不一定不能实现面向对象的代码。
只是说用面向对象的语言来实现面向对象要更加简单一些、直观一些、无脑一些。
用C++、Java等面向对象的语言来实现面向对象简单一些,因为语言本身帮我们做了很多事情;
但是用C来实现面向对象很麻烦,看起来也不容易理解,这就是为什么大多数人学过C语言却看不懂linux内核代码的原因。
struct s
{
int age; // 普通变量
void (*pFunc)(void); // 函数指针,指向 void func(void)这类的函数
};
使用这样的结构体就可以实现面向对象。
这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,
结构体中的函数指针类似于class中的成员方法。

栈的应用举例:局部变量
C语言中的局部变量是用栈来实现的。
我们在C中定义一个局部变量时(int a),编译器会在栈中分配一段空间(4字节)
给这个局部变量用(分配时栈顶指针会移动给出空间,给局部变量a用的意思就是,
将这4字节的栈内存的内存地址和我们定义的局部变量名a给关联起来),对应栈的操作是入栈。
注意:这里栈指针的移动和内存分配是自动的(栈自己完成,不用我们写代码去操作)。
然后等我们函数退出的时候,局部变量要灭亡。对应栈的操作是弹栈(出栈)。
出栈时也是栈顶指针移动将栈空间中与a关联的那4个字节空间释放。
这个动作也是自动的,也不用人写代码干预。

栈的优点:栈管理内存,好处是方便,分配和最后回收都不用程序员操心,C语言自动完成。
分析一个细节:C语言中,定义局部变量时如果未初始化,则值是随机的,为什么?
定义局部变量,其实就是在栈中通过移动栈指针来给程序提供一个内存空间和这个局部变量名绑定。
因为这段内存空间在栈上,而栈内存是反复使用的(脏的,上次用完没清零的)

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

内存管理之堆
4.1.8.1、什么是堆
堆(heap)是一种内存管理方式。
内存管理对操作系统来说是一件非常复杂的事情,因为首先内存容量很大,
其次内存需求在时间和大小块上没有规律(操作系统上运行着的几十、几百、几千个进程随时都会申请或者释放内存,
申请或者释放的内存块大小随意)。
堆这种内存管理方式特点就是自由(随时申请、释放;大小块随意)。
堆内存是操作系统划归给堆管理器(操作系统中的一段代码,属于操作系统的内存管理单元)来管理的,
然后向使用者(用户进程)提供API(malloc和free)来使用堆内存。
我们什么时候使用堆内存?需要内存容量比较大时,需要反复使用及释放时,
很多数据结构(譬如链表)的实现都要使用堆内存。
4.1.8.2、堆管理内存的特点(大块内存、手工分配&使用&释放)
特点一:容量不限(常规使用的需求容量都能满足)。
特点二:申请及释放都需要手工进行,手工进行的含义就是需要程序员写代码明确进行申请malloc及释放free。
如果程序员申请内存并使用后未释放,这段内存就丢失了(在堆管理器的记录中,这段内存仍然属于你这个进程,
但是进程自己又以为这段内存已经不用了,再用的时候又会去申请新的内存块,这就叫吃内存),
称为内存泄漏。在C/C++语言中,内存泄漏是最严重的程序bug,这也是别人认为Java/C#等语言比C/C++优秀的地方。
4.1.8.3、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中的可变大小的数组的方式。
4.1.8.4、堆的优势和劣势(管理大块内存、灵活、容易内存泄漏)
优势:灵活;
劣势:需要程序员去处理各种细节,所以容易出错,严重依赖于程序员的水平。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值