文章目录
虚拟地址空间
在多任务操作系统中,每个进程都运行在属于自己的内存沙盘中。这个沙盘就是虚拟地址空间(Virtual Address Space),在32位模式下它是一个4GB的内存地址块。在Linux系统中, 内核进程和用户进程所占的虚拟内存比例是1:3,而Windows系统为2:2(通过设置Large-Address-Aware Executables标志也可为1:3)。这并不意味着内核使用那么多物理内存,仅表示它可支配这部分地址空间,根据需要将其映射到物理内存。
虚拟地址通过页表(Page Table)映射到物理内存,页表由操作系统维护并被处理器引用。在Linux中,内核空间是持续存在的,并且在所有进程中都映射到同样的物理内存。内核代码和数据总是可寻址,随时准备处理中断和系统调用。与此相反,用户模式地址空间的映射随进程切换的发生而不断变化。
#include<iostream>
using namespace std;
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVal = 1;
int localVar = 1;
char a2[] = "abcd";
char*a3 = "abcd";
int*p1 = (int*)malloc(sizeof(int)* 4);
int*p2 = (int*)calloc(4, sizeof(int));
int*p3 = (int*)realloc(p2, sizeof(int)* 4);
int*p4 = new int;
int*p5 = new int(3);
int*p6 = new int[3];
free(p1);
free(p3);
delete p4;
delete p5;
delete[] p6;
}
上面定义的这些变量的存储位置如图所示:
如何一次性在堆上申请4G的空间?
- 在32位操作系统,堆上一次性开辟4G的空间是不可能实现的,32根地址总线,可以表示的最大范围是 2 32 2^{32} 232= 2 2 ∗ 2 10 ∗ 2 10 ∗ 2 10 2^{2}*2^{10}*2^{10}*2^{10} 22∗210∗210∗210= 4 ∗ 1024 ∗ 1024 ∗ 1024 4*1024*1024*1024 4∗1024∗1024∗1024= 4 G 4G 4G,Linux系统内核态占 1 G 1G 1G,用户空间只有 3 G 3G 3G,32位系统堆上可以开辟的最大空间大概是 2 G 2G 2G。
- 在64位系统中是很容易实现的,因为64位地址总线可以表示的最大范围是 2 64 2^{64} 264= 2 32 2^{32} 232* 2 32 2^{32} 232,也就是 2 32 2^{32} 232个 4 G 4G 4G。其用户空间远远超过了 4 G 4G 4G,当然是很容易实现一次性开辟 4 G 4G 4G的空间。
malloc、calloc、realloc
malloc、calloc、realloc以及free函数都是存储在<cstdlib>
库中的,都是C语言用来在堆上管理内存的函数接口。
malloc函数
void* malloc (size_t size);
- C语言的malloc函数是用来在堆上动态开辟内存的。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 这里动态开辟了40个字节,这里
sizeof(int)
是在手动计算一个整形的字节数,开辟了10个整形的空间。 - 返回值是
void*
,所以需要类型转化为int*
。
#include<cstdlib>
int main()
{
int n = 10;
int *p = (int*)malloc(sizeof(int)*n);
if (p != nullptr)//申请失败,p指向空
{
for (int i = 0; i < n; i++)//malloc申请的空间都未曾初始化
{
*(p + i) = 0;
}
}
free(p);
p = nullptr;//空间释放,必须让p指向空,否则存在野指针问题
system("pause");
return 0;
}
free函数
void free (void* ptr);
- free函数也只是释放掉了这段内存空间,将其以
void*
类型归还给系统。 - 空间释放,必须让p指向空,否则存在野指针问题。
calloc函数
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 字节的元素开辟一块空间。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
#include <cstdlib>
#include<iostream>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if (nullptr != p)
{
for (int i = 0; i < 10; i++)
std::cout << *(p + i) <<std::endl;
}
free(p);
p = nullptr;
system("pause");
return 0;
}
realloc函数
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
void* realloc (void* ptr, size_t size);
1、ptr 是要调整的内存地址,size 调整之后新大小,如果重新分配成功则返回指向被分配内存的指针,否则返回空指针,原内存空间的大小和数据维持不变。
ptr=nullptr | realloc等同于malloc,开辟未初始化的空间 |
---|---|
size=0 | realloc等同于free,将ptr所指向的空间归还给系统 |
2、新的大小可大可小(如果新的大小大于原内存大小,则新分配部分不会被初始化;如果新的大小小于原内存大小,可能会导致数据丢失。
3、realloc在扩大内存空间的是存在两种情况
原有空间之后有足以增容的空间 | 要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。 |
---|---|
原有空间之后没有足够多的空间用来扩容时 | 扩展在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。 |
#include <stdio.h>
#include <stdlib.h>
int main()
{
char *p, *np;
p = (char *)malloc(10);
np = (char *)realloc(p, 15);
printf("%p\n", p);
printf("%p\n", np);
free(np);
np = nullptr;
system("pause");
return 0;
}
笔试题
- 同一块内存释放多次
void test()
{
int *p = (int *)malloc(100);
int* ptr = p;
free(p);
free(ptr); //重复释放
}
- 使用free释放一块动态开辟内存的一部分
void test()
{
int *p = (int *)malloc(100);
p++;
free(p); //p不再指向动态内存的起始位置
}
- GetMemory函数返回了局部变量,Test函数中的printf使用了非法空间。
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
- 在GetMemory函数中,p指向了malloc新开辟的空间,在该函数执行结束后,p指向的空间没有被释放,存在内存泄漏问题。str始终指向NULL,将
"hello world"
拷贝给一个空指针,必定产生错误。
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
- 上一题中因为需要改变指针内容,即需要改变指针指向,所以应该传二级指针就可以实现。
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(str);
str = nullptr;
}
new和delete
int*p1=new int;
int*p2=new int(3);//给变量初始化为3
int*p3=new int[3];//开辟了容量为3的数组
delete p1;
delete p2;
delete[] p3;
new和delete不是函数,而是操作符。 只需要给定变量类型个数,new会自己算出空间大小。new动态开辟了的空间,还调用了构造函数为变量进行初始化。delete调用析构函数进行清理工作后,释放空间。
new/delete 和operator new/operator delete和malloc/free的关系
operator new和operator delete和函数重载没有毛关系,new和delete不是函数,是内置操作符。
new操作做的事情
1、调用operator new函数动态开辟内存,申请内存失败,抛异常。operator new实质是对malloc的封装。
2、调用构造函数进行初始化对象。
delete操作做的事情
1、调用析构函数,清理对象空间。
2、调用operator delete进行空间释放,operator new实质是对free的封装。
new[ ]底层处理机制
operator new 的参数count是申请空间的大小(单位字节),也就是说new []实际是调用了operator new去一次性开辟好自己需要的内存大小。
new type[],type类型有显示定义析构函数,会多开4个字节存储对象个数 ,如果没有定义析构函数,系统默认的构造函数,系统保存了对象个数,不需要关心。
自己定义的析构函数,系统需要知道这块地址的元素个数会开辟一个整形的位置专门来存储这个个数。在小端机器,这个整形存储在这段数据的低地址处。
如果new type[ ],type为显示调用了析构函数的自定义类型,使用delete去释放空间,则会导致内存泄漏,反之亦然。