目录
内存分区模型
C/C++程序在运行时,将内存大方向划分为4个区域
- 代码区:存放函数体的二进制代码,由操作系统进行管理的
- 全局区:存放全局变量和静态变量以及常量
- 栈区:由编译器自动分配释放,存放函数的参数值,局部变量等
- 堆区:由程序员分配和释放,若程序员不释放,那么程序结束时由操作系统回收。
内存分区的意义:不同区域存放的数据被赋予不同的生命周期,给我们更大的灵活编程
C/C++内存开辟
按照程序运行前后分区
程序运行前
前言:程序编译后,生成exe可执行程序,未执行该程序前分为2个区域
代码区
- 代码区里面存放CPU执行的机器指令
- 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可
- 代码区是只读的,使其只读的原因是防止程序意外的修改他的指令
全局区
- 全局变量和静态变量存放在此
- 全局区还包含了常量区,字符串常量和其他常量(非const修饰的局部常量)也存放在此
- 该区域的数据在程序结束后由操作系统释放
程序运行后
前言:在程序执行后又划分为2个区域
栈区
- 由编译器自动分配释放,存放函数的参数值,局部变量等
- 注意返回值不要返回局部变量的地址,因为栈区开辟的数据会由编译器自动释放
具体案例
#include <iostream>
using namespace std;
int* func() {
int a = 10;
return &a;
}
void main() {
int* p = func();
//编译器给我们做了一次保留
cout << "a的值为:" << *p << endl;
//再次打印失效
cout << "a的值为:" << *p << endl;
}
堆区
- 由程序员分配和释放,若程序员不释放则程序结束时由操作系统回收
- 在C++中主要利用new在堆区开辟空间,在C语言中主要用动态内存分配函数
关于栈
栈:一种数据结构,遵循先进后出原则,类似弹夹
理解:
- 往栈里放数据就类似于往弹夹里装子弹称为压栈也称入栈
- 从栈里取数据就类似于从弹夹里取子弹称为弹栈也称出栈
栈区的使用习惯:先用高地址,再用低地址
注意:每一个函数调用都需要在栈区上给自己分配一个空间(栈内遵循先进后出原则)
绝大多数编译器传参顺序从右往左,并且传参的过程就是压栈的过程
整个过程
在栈内开辟main函数的空间——main函数对空间进行管理分别为局部变量分配空间——调用函数时从右往左进行实参的压栈并对Add函数分配栈内空间——Add函数对空间进行管理分别为局部变量分配空间——a'+b'赋值给z——将z赋值给c——销毁Add函数空间以及a',b'空间(弹栈)。
栈与静态区(数据段)
实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁所以生命周期变长。
动态内存分配
动态内存分配,顾名思义就是可以使我们更加灵活的分配和运用内存,动态内存的开辟的释放都是按照我们自身的需要进行的,我们通常直接使用的内存是在栈上的,开辟和释放内存都是由系统自动进行的,而动态内存分配的内存是在堆上。
那么使用动态内存的好处在哪?
栈上的空间是自动分配自动回收的,因此栈上的数据的生存周期只是在函数的运行过程当中,运行后就释放掉,不能够再访问。在堆上开辟的内存,不会被系统自动释放,只能通过我们自己释放或者当程序结束的时候被自动回收,只要我们有指向由我们申请在堆上的空间的指针,我们就能对它进行修改数据和使用。
常见的内存使用方式
1.创建一个变量
//(变量)局部变量放在栈区,全局变量放在静态区
int a = 10;
//(数组)局部变量放在栈区,全局变量放在静态区
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
上述开辟空间有2个特点
- 空间开辟大小是固定的
- 数组在声明的时候,必须指定数组的长度,他所需要的内存在编译时分配
但对于空间的需求不仅仅是上述情况,有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式便不再满足,这时候就需要试试动态内存分配了
为什么有动态内存分配
原因:当前我们这两种(创建变量和创建数组的方式)不能满足我们来创建一个我们想要创建多大空间就能创造多大空间的需求
动态内存分配函数
malloc函数
这个函数向内存中申请一块连续可用的size字节的空间(值为随机,没进行初始化),并返回指向这个空间的指针
free函数
该函数用来做动态内存的释放和回收,传入的ptr为申请的动态内存空间的地址
对上面两个函数的使用:
引入头文件:#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
void main() {
//向堆内存中申请10个整形的空间
int* p=(int*)malloc(10 * sizeof(int));
if (p == NULL) {
//打印错误原因的一个方式
perror("malloc");
}
else
{
//正常使用空间
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;//对内存进行修改
printf("%d\n", *(p + i));//0,1,2,3,4,5,6,7,8,9
}
}
//当动态内存空间不使用后,就应该还给操作系统
free(p);//空间虽已经释放,但还可以通过p找到该地址
p = NULL;
}
注意:如果没有free函数,那么执行到main函数的最末尾,malloc所申请的空间依然存在,直到main函数结束(不是普通的函数)——程序结束(生命周期到了),其也会把malloc函数申请的这块内存还给操作系统。
calloc函数
在内存中开辟一个数组,并且数组的每个元素初始化为0,里面的num为元素的个数,size为每个元素的大小,之后会返回该数组所在的地址,如果空间不够用,同样会返回null
使用:
引入头文件:#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
void main() {
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL) {
printf("%s\n", strerror(errno));
}
else {
int i = 0;
for (i = 0; i < 10; i++) {
printf("%d\n",*(p + 1));//0,0,0,0,0,0,0,0,0,0(初始化全为0)
}
}
//释放空间
free(p);//用来释放动态开辟的空间
p = NULL;
}
realloc函数
调整动态开辟内存的大小
- realloc函数的出现让动态内存管理更加灵活
- 有时我们发现过去申请的空间太小了,有时候我们又觉得申请的空间太大了,那为了合理的控制内存,我们一定会对内存的大小做灵活的调整,realloc函数就可以做到对动态开辟内存大小的调整。
- ptr是要调整的内存地址
- size为调整之后的新大小
- 返回值为调整之后内存的起始位置
- 这个函数调整原内存空间大小的基础上,还会将原内存的数据移动到新空间
realloc函数使用注意事项
- 如果p(开始内存空间的地址)指向的的空间之后有足够的内存空间可以追加,那么realloc函数会直接追加并返回p
- 如果p(开始内存空间的地址)指向的的空间之后没有足够的内存空间可以追加那么realloc函数会重新找一个新的内存区域,开辟一块满足需求的空间,并把原来内存中的数据拷贝回来,释放旧的内存空间,最后返回新开辟内存空间的地址
使用:
引入头文件:#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
void main() {
int* p = (int*)malloc(20);
if (p == NULL) {
perror("malloc");
}
else {
int i = 0;
for (i = 0; i <5; i++) {
*(p + i) = i;
printf("%d ", *(p + i));//0 1 2 3 4
}
printf("\n");
}//假设到这里20字节不能满足使用要求了,欲再填20字节
//为了避免realloc函数调用失败把p地址搞丢执行下3行
int* ptr=(int*)realloc(p, 40);
if (ptr != NULL) {
p = ptr;
}
int i = 0;
for ( i = 0; i < 10; i++)
{
printf("%d ", *(p + i));//0 1 2 3 4 -842150451 -842150451 -842150451 -842150451 -842150451(随机值)
}
//释放内存空间
free(p);
p = NULL;
}
常见动态内存错误
- 对空指针解引用操作
- 对动态开辟内存的越界访问
- 对非动态空间free
- 使用free释放动态开辟内存的一部分
- 对同一块动态内存多次释放
- 对动态开辟的内存空间忘记释放
#include <stdio.h>
#include <stdlib.h>
void main() {
int* p = (int*)malloc(20);
if (p == NULL) {
perror("malloc");
}
else {
int i = 0;
for (i = 0; i <5; i++) {
*p++ = i;
}
}
//回收空间,此时p已经不是首地址了,释放空间报错
free(p);
p = NULL;
}
柔性数组
定义:C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员
柔性数组特点
举例:
#include <stdio.h>
#include <stdlib.h>
struct Stu
{
int n;
int arr[];//未知大小的-柔性数组成员-数组的大小是可以调整的(也可以这样写——int arr[0])
};
void main() {
struct Stu s;
printf("%d\n", sizeof(s));//4
//由此观之,在计算含有柔性数组的结构体的大小时,计算的大小不包含柔性数组的大小
//为结构体Stu开辟空间,并且保证柔性数组有5个int类型变量的空间
struct Stu* ps = (struct Stu*)malloc(sizeof(struct Stu) + 5 * sizeof(int));
ps->n = 100;
for (int i = 0; i < 5; i++)
{
ps->arr[i] = i;//0,1,2,3,4
printf("%d ", ps->arr[i]);
}
printf("\n");
//如今对于数组来说5个整形不够了,要再添5个
struct Stu* ptr = (struct Stu*)realloc(ps, 44);
if (ptr!=NULL)
{
ps = ptr;
}
else
{
perror("realloc");
}
for (int i = 5; i < 10; i++)
{
ps->arr[i] = i;
printf("%d ", ps->arr[i]);//5,6,7,8,9
}
printf("\n");
free(ps);
ps = NULL;
}
替代方式
#include <stdio.h>
#include <stdlib.h>
struct Stu
{
int n;
int* arr;
};
void main() {
struct Stu* ps = (struct Stu*)malloc(sizeof(struct Stu));
ps->arr = malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++)
{
ps->arr[i] = i;
printf("%d ", ps->arr[i]);
}
printf("\n");
//再加20个字节空间
int* ptr = (int*)realloc(ps->arr, 10 * sizeof(int));
if (ptr!=NULL)
{
ps->arr = ptr;
}
else {
perror("realloc");
}
for (int i = 5; i < 10; i++)
{
ps->arr[i] = i;
printf("%d ", ps->arr[i]);
}
printf("\n");
free(ps->arr);//先释放ps->arr
ps->arr = NULL;
free(ps);
ps = NULL;
}
柔性数组的优点
- 方便内存释放——只需要一次的内存释放
- 有利于访问速度——连续内存有益于提高访问速度,也有利于减少内存碎片(一次分配内存即可)