扫二维码与项目经理沟通
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流
程序加载到内存后,操作系统会给不同的内存指定不同的权限,拥有读取和执行权限的内存块是代码,拥有读取和写入权限的内存块是数据
CPU一般通过地址来取得内存中的代码和数据
CPU访问内存时需要的是地址,而不是变量名和函数名,当源文件被编译和链接成可执行程序后,都会被替换成地址
(变量名是数据,函数、字符串名、数组名是代码块或数据的首地址)
程序在硬盘中,要载入内存才能运行
CPU只能从内存中读取数据和指令
对于CPU,内存时存放指令和数据,并不能完成计算功能
计算机:硬盘->内存->CPU:缓存->>
CPU=运算器+寄存器+缓存寄存器:一个寄存器能够存储32/64位的数据,CPU一般由上百个寄存器(多少位的计算机,表示的是寄存器是多少位的)。寄存器用来进行数学运算或流程控制
缓存:内存读取速度小于CPU,因此将频繁使用的数据暂时读取到缓存区。CPU只能从缓存中读取到部分数据,对于使用不是很频繁的数据会直接从内存中读取。
指令集
虚拟内存/地址编译、汇编之后会变成一条条的CPU指令,
虚拟地址通过CPU的转换才能对应到物理地址:虚拟地址–>内存映射机制 -->物理地址 ;
每次程序运行的时候,操作系统都会重新安排虚拟地址和物理地址的对应关系,哪一段物理内存空闲就使用哪一段
虚拟地址和物理地址的映射关系由操作系统决定,虚拟地址空间的大小由操作系统决定
虚拟地址可以使不同程序的地址空间相互隔离,防止被互相篡改
程序A 和程序B虽然都可以访问同一个地址,但是他们对应的物理地址是不同的,因此不会互相修改对方的内存,因此程序不应该直接使用物理内存地址
虚拟地址 作为一个中间层,用来屏蔽复杂的底层细节,只给用户提供简单的接口
内存映射 ???
只要能够控制这个虚拟地址到物理地址的映射过程,就可以保证程序每次运行时都可以使用相同的地址???
内存分页 ???
内存空间/进程一个进程用于一个独立的地址空间
一个程序可能会有多个进程,而一个进程对应一个独立的地址空间,所以一个程序可能有多个地址空间
存放操作系统内核代码和数据,被所有程序共享
在程序中修改内核空间的数据不仅会影响操作系统本身,还会影响其他程序(一般操作系统进制用户访问内核空间)
内核空间的访问需要操作系统的API函数,执行内核提供的代码
系统调用 访问内核空间,执行内核代码(内核也是程序)叫做 内核模式 (发生系统调用时会暂停用户程序,转而执行内核代码)
内核用于管理硬件,提供接口供上层使用
用户空间保存的是引用程序的代码和数据,程序私有
执行程序叫做用户模式
下面的内存分区实际上说的是用户空间中的内存分区
内存分区 – 用户地址空间当运行在用户模式的应用程序需要输入输出、申请内存等底层操作时,就需要调用系统API函数进入内核模式,执行结束后,又回到用户模式
数据以二进制的形式保存在内存中,字节是最小的可操作单位
在内存管理中,为每个字节分配了一个编号,使用该字节时,只要知道编号就可以,这个编号,就是地址
注意:内存分区和变量的作用域不是完全等价的,甚至是没有关系的
获取地址
&data
,(data可以是数值、字符)
编译器自动分配
局部变量、形参、返回值,const
定义的局部变量也存放在栈区
操作系统自动管理
栈区上的内容只在函数范围内存在,函数结束自动销毁
栈区内存地址:由高到低,即后定义的变量地址低于先定义的变量地址
先进后出
属于动态内存分配
函数中定义的局部变量按照先后定义的顺序一次压入栈中,即,相邻变量的地址之间不会存在其他变量(debug/release不同)
见函数调用栈的例子
实际上,程序启动时会为栈区分配一块大小适当的内存(包括局部函数调用栈的时候),这对于一般的函数调用就已经足够了,函数进出栈只是ebp、esp等寄存器指向的变换,或者是向已有的内存中写入数据,其实不涉及内存的分配和释放
只要当函数中有较大的局部数组时,编译器才会在函数代码中插入针对栈的动态内存分配函数,这样函数被调用时才分配内存,不调用就不分配
因此栈内存的分配效率要高于堆,也就是大部分情况下并没有真的分配栈内存,而是对已有内存的操作
程序员分配内存和释放,若开发人员不释放,程序结束时有系统回收
malloc()
free()
new
delete
堆区内存地址:由低到高
大小由系统内存/虚拟内存上限决定
属于动态内存分配
注意:后申请的内存空间并不一定在先申请的内存空间的后面,因为先申请的内存空间一旦释放,后申请的内存空间则会利用先前被释放的内存,从而导致先后分配的内存空间在地址上不存在先后关系
堆中存储的数据若未释放,则生命周期等同于程序的生命周期
由于系统是通过链表来存储空闲的内存地址,因此堆数据时不连续的内存空间(这个堆区域数据结构的堆不一样,是通过链表实现的)
对于堆分配,操作系统有一个记录空闲内存地址的链表,当申请内存时,会遍历该链表,寻找第一个空间大于所申请空间的堆节点,然后将该节点从空闲节点链表中删除,并将该节点的内存空间分配给程序。另外,对于大多数系统,会在这块内存空间中的首地址出记录本次分配的大小,这样使用delete才能正确释放内存空间。由于找到的堆节点的大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空间链表
free§并不能改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为NULL
free§ 只是释放掉动态分配的内存,p的值并不是NULL,仍然指向之前这个被释放掉的内存,所以if§仍然会执行,但是输出p指向的内存会报错
// c 用malloc
// 注意指针的类型转换
char* p1=(char*)malloc(10);
free(p1);
//free(p)并不能改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为NULL
// C++ 用new
char* p2=new char[10];
delete[] p2
malloc 动态内存分配 内存池 – 还没看
未初始化全局(静态)区 .bass
已初始化全局(静态)区 .data
在编译期间就能确定 存储大小的变量 的存储区,且在运行期间可以进行修改
包括全局变量,静态变量(包括静态全局变量、静态局部变量)
属于静态内存分配
这块内存具有的读写权限
静态数据区的变量只能初始化一次,也就是可修改但是不能初始化(即使二次初始化了,也会被视为无效)
.bass
读写
未初始化的全局变量或未初始化的静态变量
初始化为0的全局变量或初始化为0的静态变量
.bass段不占用可执行文件空间,由操作系统初始化
.data
读写
已初始化的全局变量 (但初始化不为0的)
已初始化的静态变量 (但初始化不为0的)
.data段占用可执行文件空间,由程序初始化
.rodata
字符、字面值、字符串等常量
const 修饰的全局变量(const修饰的局部变量存放在栈区)
这部分内存只有读取权限,没写入权限,即运行期间,常量区的内容不可被修改
属于静态内存分配
存放程序的代码
.txt二进制
只读,不可修改
属于静态内存分配
静态/动态内存在程序运行期间加载和卸载动态连接库
代码区、常量区、全局数据区的内存在程序启动时就已经分配好了,大小固定、不能由程序分配或释放,只能等到程序运行结束由操作系统回收
栈区、堆区的内存在程序运行期间可以根据实际需求来分配和释放,不用在程序刚启动时就备足所有内存
char *str1="hello"; // hello字符串在常量区,str1在全局数据区
int n; // 全局数据区
char *func()
{char *str="world"; // world字符串在常量区,str在栈区
return str;
}
int main()
{int a; // 栈区
char arr[10]; // 栈区
char *pstr=func(); // 栈区
}
内存泄漏一块内存没有被指针指向它,(程序和内存失去了联系,再无法对他进行任何操作),这块内存直到程序结束被系统回收
char *p=(char *)malloc(100*sizeof(char)); // 这段内存存在内存泄漏,无法被释放,只有等程序运行结束由操作系统回收
p=(char *)malloc(50*sizeof(char));
free(p);
p=NULL;
栈
栈溢出第一种栈溢出
(整个)用于栈的内存空间是有限的,超出大值(跟编译器有关)就会栈溢出
一个程序可以包含多个线程,每个线程都有自己的栈,栈的大是是针对线程的
第二种栈溢出
调用栈 -函数相关栈中局部变量的内存空间有限,占用了栈中其他内存空间的地址,导致栈发生错误
函数调用与栈有关
栈帧
又叫做活动记录
函数调用过程中,存储全部的信息的栈叫做栈帧
一个不典型的例子:由高到底地址分别为:实参、返回地址、xxx、一块内存(包括局部变量、返回值等)、xxx
函数调用惯例
调用方和被调用方之间遵守的约定
- 函数参数的传递方式,是通过栈传递还是通过寄存器传递
- 参数参数传递的方式,是从左到右入栈还是从右到左入栈
- 参数弹出方式,函数调用结束后需要将压入栈中的参数全部弹出,使得栈在函数调用前后保持一致,(这个弹出的工作可以由调用方完成,也可以由被调用方完成)
函数调用惯例可以进行修改 –没看,见文档
函数调用栈实例 – 见文档 很重要!!!
内存对齐局部变量分配内存,是否初始化也与调用栈有关
https://blog.csdn.net/weixin_46251230/article/details/123755070
CPU 通过地址总线访问内存,一次能处理几个字节的数据,就命令地址总线读取几个字节的数据(32位CPU一次可以处理4个字节数据,64位CPU一次可以处理8个字节)
寻址步长4个字节或8个字节
将一个数据尽量放在一个步长内,避免跨步长存储,这称为内存对齐
32位默认4字节对齐,64位默认8字节对齐
动态内存分配 – 内存池 --还没看 野指针/空指针/void指针 空指针具体对齐方式,与编译器有关
一般来讲全局变量会自动内存对齐,而局部变量不会进行内存对齐
对未初始化的指针赋值为NULL
空指针是不指向任何数据的指针,是无效指针
// NULL 其实是一个宏定义,指向了内存的0地址,
#define NULL ((Void*)0)
char* s=NULL;
if (p==NULL){// ...
}
void指针
void*
表示一个有效指针,指向实实在在的数据,只是数据的类型尚未确定,在后序使用过程中需要进行强制类型转换
char* s=(char*)malloc(sizeof(char)*30);
野指针如果一个指针指向的内存没有访问权限,或者指向一块已经释放掉的内存,那么就无法对该指针进行操作,这样的指针就是野指针
free
free§并不能改变指针p的值,p依然指向以前的内存,为了防止再次使用该内存,建议将p的值手动置为NULL
free§ 只是释放掉动态分配的内存,p的值并不是NULL,仍然指向之前这个被释放掉的内存,所以if§仍然会执行,但是输出p指向的内存会报错
避免野指针
缓存与缓冲区初始化为NULL
free之后,赋值为NULL
内存中用于临时保存输入输出数据
缓冲区 :内存空间的一部分
作用: 减少磁盘的读写次数 ;
分类输入缓冲区/输出缓冲区
根据数据刷新时机全缓冲:缓冲区被填满时(一般是对硬盘的读写)
行缓冲:遇到换行符时 (标准I/O),prinf(“\n”),scanf回车
无缓冲: getche(),getch() 这两个函数没有缓冲
(程序结束的时候 也会刷新缓冲区)
(输出之后有输入的操作,也会刷新缓冲区)注意:不同的操作系统对缓冲区的定义时不同的,printf在windows下无需缓冲,在linux下有缓冲
不刷新缓冲区的例子
// 这个例子,进入死循环
// 缓冲区没有被填满、没有遇到换行符、程序没有结束,所以标准输出始终没有显示字符串
int main()
{printf("hello world"); // 只要加上printf("hello world\n") 立即刷新
while (1);
return 0;
}
刷新
行缓冲,全缓冲:缓冲区满时自动刷新
行缓冲:遇到’\n’时,自动刷新
关闭文件时自动刷新
程序关闭时自动刷新
使用刷新函数
清空输出缓冲区
fflush(stdout) // 一般用于linux系统下,清空标准输出缓冲区,清空屏幕缓冲区
清空输入缓冲区
int c
while (c=getchar()!='\n' && c!=EOF)
FILE 与缓冲区FILE *fp;
FILE 结构体
int cn // 剩余字符,缓冲区中还有多少个字符未被读取
char *ptr //下一个要被读取的字符的地址
char *base // 缓冲区的基地址
int flag // 读写状态标志位
int fd // 文件描述符
FILE是文件缓冲区的结构体,fp是指向文件缓冲区的指针
缓冲区的刷新表示将 缓冲区的指针变为缓冲区的基地址
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
我们在微信上24小时期待你的声音
解答本文疑问/技术咨询/运营咨询/技术建议/互联网交流