Ⅰ、C语言内存管理回顾
01 内存分布位置
首先我们先观察下面一段代码,并回答问题:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
选择题:
选项: A.栈 B.堆 C.数据段 D.代码段
globalVar在哪里?____ staticGlobalVar在哪里?____
staticVar在哪里?____ localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?___
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____
答案:CCCAA AAADAB
栈区
非静态局部变量/函数参数/返回值等等,栈是向下增长的。
执行函数时,函数内部局部变量的存储单元都可以在栈上创建。
函数执行结束后这些存储单元会被自动释放。栈内存分配运算内置于处理器的指令集中,
拥有很高的效率,但是分配的内存容量是有限的。
栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
堆区
用于程序运行时动态内存分配,堆是可以上增长的。
一般由程序员自主分配释放,若程序员不主动释放,程序结束时可能由操作系统回收。
其分配方式类似于链表。
数据段
静态存储区,数据段存放全局变量和静态数据,程序结束后由系统释放。
代码段
可执行的代码 / 只读常量。代码段存放类成员函数和全局函数的二进制代码。
一个程序起来之后,会把它的空间进行划分,而划分是为了更好地管理。
函数调用,函数里可能会有很多变量,函数调用建立栈帧,栈帧里存形参、局部变量等等。
内存映射段
内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。
用户可使用系统接口创建共享共享内存,做进程间通信。
02 C语言中动态内存管理方式
malloc / calloc / realloc的区别?
malloc
void* malloc(size_t size);
该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。具体情况如下:
1、如果申请成功,则返回一个指向开辟好空间的指针。
2、如果申请失败,则返回 NULL 指针。
3、返回值类型为 void*,由使用者自己决定。
4、如果 size 为0,malloc的行为是标准未定义的,结果取决于编译器。
calloc
void* calloc(size_t num, size_t size);
该函数的功能是为 num 个大小为 size 的元素开辟一块空间,并把空间的每个字节初始化为0,返回指向它的指针。
与malloc相比,第一、calloc 有两个参数,分别为元素的个数和元素的大小。第二、calloc 会在返回地址前把申请的空间的每个字节初始化为0。
先看malloc:
int main()
{
//malloc
int* p = (int*)malloc(40); //开辟40个空间
if (p == NULL)
return 1;
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
运行结果是10个随机值
再看calloc:
int main()
{
//calloc
int* p = (int*)calloc(10, sizeof(int)); //开辟10个,大小为int的空间
if (p == NULL)
return 1;
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
运行结果为:0 0 0 0 0 0 0 0 0 0
总结:说明calloc会对开辟的内存进行初始化,并将每个字节初始化为0 。如果对于申请的内存空降需要初始化,我们就可以使用calloc函数。
realloc
void* realloc(void* ptr, size_t size);
该函数用于重新调整之前调用 malloc 或 calloc 所分配的内存大小,可以对动态内存开辟的空间大小进行调整。具体情况如下:
1、ptr 指向要调整的内存地址。
2、size 为调整之后的新大小。
3、返回值为调整之后的内存起始位置,请求失败则返回空指针。
4、realloc 函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc 在调整空间时存在三种情况:
情况一:原有空间之后有足够大的空间
情况二:原有空间之后没有足够大的空间
情况三:realloc 有可能找不到合适的空间来调整大小
情况一: 当原有空间之后有足够大的空间时,直接在原有空间之后追加空间,原来的数组不发生变化。
情况二:当原有空间之后没有足够大的空间时,会在堆空间上另找一个大小合适的连续的空间来使用。函数的返回值将是一个新的内存地址。
情况三:如果找不到合适的空间,就会返回空指针。
Ⅱ、C++动态内存管理
01 C++ 兼容 C 的动态内存管理方式
c 语言的内存管理方式在 C++ 中同样适用,但有些地方使用起来并不是那么顺手。
为了解决这个问题,C++ 进化出属于自己的内存管理方式,通过使用 new 和 delete 进行动态内存管理。
02 使用 new 开辟空间
使用 new 开辟空间:
void test()
{
//动态申请一个int类型的空间
int* p1 = new int;
//动态申请一个int类型的空间并初始化为10
int* p2 = new int(10);
//动态申请10个int类型的空间
int* p3 = new int[10];
}
同时 new 不需要强制类型转换。
03 使用 delete 释放空间
使用 delete 释放空间:
void test()
{
//动态申请一个int类型的空间
int* p1 = new int;
//动态申请一个int类型的空间并初始化为10
int* p2 = new int(10);
//动态申请10个int类型的空间
int* p3 = new int[10];
//单个对象,直接 delete
delete p1;
delete p2;
//多个对象,使用 delete[]
delete[] p3;
//最好再全部置空
p1 = nullptr;
p2 = nullptr;
p3 = nullptr;
}
04 初始化 new 数组
C++ 98 并不支持初始化 new 数组
int* p = new int[5];
但 C++11 允许大括号初始化,我们可以用 {} 列表初始化
int* p1 = new int[5]{1,2} // 1 2 0 0 0
int* p2 = new int[5]{1,2,3,4,5}; // 1 2 3 4 5
05 new 和 delete 操作自定义类型
我们知道了,malloc / free 和 new / delete 对于内置类型来说没有本质区别,那么它存在的意义仅仅是用法更简洁,更方便使用嘛? 当然不是,我们接着往下看
malloc 和 new 的对比
对于自定义类型来说,你也是可以使用 malloc 的。
用 malloc 创建对象:
class A
{
public:
A()
:_a(0)
{
cout << "A():" << endl;
}
~A()
{
cout << "~A():" << endl;
}
private:
int _a;
};
int main()
{
A* a1 = (A*)malloc(sizeof(A));
A* a2 = (A*)malloc(sizeof(A) * 5);
}
现在来看看 C++ 的:
int main()
{
A* a1 = (A*)malloc(sizeof(A));
A* a2 = (A*)malloc(sizeof(A) * 5);
A* a3 = new A;
A* a4 = new A[5];
}
但仅仅只有书写更简洁嘛? 让我们调试观察一下:
new 这里不仅会发内存,还会调用对应的构造函数初始化,如果是一个数组,它会依次对创建的对象进行初始化。
free 和 delete 的对比
我们来对比一下 free 和 delete:
class A
{
public:
A()
:_a(0)
{
cout << "A():" << endl;
}
~A()
{
cout << "~A():" << endl;
}
private:
int _a;
};
int main()
{
A* a1 = (A*)malloc(sizeof(A));
A* a2 = (A*)malloc(sizeof(A) * 5);
A* a3 = new A;
A* a4 = new A[5];
free (a1);
free (a2);
delete a3;
delete[] a4;
}
相应的,free 只是把 p1 p2 指向的空间释放掉。
而 delete 不仅会把 p1 p2 指向的空间释放掉,还会调用相应的析构函数。
总结:在申请自定义类型的空间时, new 会调用构造函数, delete 会调用析构函数,而 malloc 和 free 不会
new:在堆上申请空间 + 调用构造函数输出
delete:调用指针类型的析构函数 + 释放空间给堆
06 new/delete 和malloc/free 要匹配使用
不建议大家混着用,有时编译器会报错
malloc / free
delete / delete
new[] / delete[]
Ⅲ、new 和 delete 的底层
01 operator new 和 operator delete 函数
new 在底层调用 operator new来申请空间
delete 在底层调用 operator delete 来释放空间
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,
尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void* p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{
_CrtMemBlockHeader* pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述两个全局函数的实现可以知道:
operator new 实际上也是通过调用 malloc 来申请空间
operator delete 最终也是通过调用 free 来释放空间
如果 malloc 申请空间成功就直接返回,否则执行用户提供的空间不足的应对措施,
如果用户提供该措施就继续申请,否则就抛异常。
面向过程的语言处理错误方法:
int main()
{
char* p1 = (char*)malloc(1024u * 1024u * 1024u *2u);
if (p1 == nullptr)
{
printf("%d\n", errno);
perror("malloc fail");
exit(-1);
}
else
{
printf("%p\n", p1);
}
return 0;
}
面向对象的语言处理错误方法:
一般是抛异常----try catch
int main()
{
char* p2 = nullptr;
try
{
char* p2 = new char[1024u * 1024u * 1024u * 2u - 1];
} catch (const exception& e) {
cout << e.what() << endl;
}
printf("%p\n", p2);
return 0;
}
02 operator new 与 operator delete 的类专属重载
我们先看看C的方式写链表的结点的申请:
struct ListNode
{
int _val;
ListNode* _next;
ListNode* _prev;
};
int main()
{
struct ListNode* cur = (struct ListNode*)malloc(sizeof(struct ListNode));
if (cur == NULL)
{
perror("malloc fail");
exit(-1);
}
cur->_next = NULL;
cur->_prev = NULL;
cur->_val = 0;
return 0;
}
我们再来看看C++ 的方式:
struct ListNode
{
int _val;
ListNode* _next;
ListNode* _prev;
/*构造函数*/
ListNode(int val = 0)
:_val(val)
,_next(nullptr)
,_prev(nullptr)
{}
};
int main()
{
ListNode* cur = new ListNode(0);
return 0;
}
在 C++ 里,因为 new 会自动调用构造函数去完成初始化,就很舒服。
而且还不需要去检查是否开辟失败,因为 new 失败不会返回空,而是抛异常。
再来看个熟悉的例子----Stack:
class Stack
{
public:
Stack(int capacity = 4)
:_arr(new int[capacity])
,_top(0)
,_capacity(0)
{}
~Stack() {
delete[] _arr;
_arr = nullptr;
_capacity = _top = 0;
}
private:
int* _arr;
int _top;
int _capacity;
};
int main()
{
Stack st;
Stack* pst2 = new Stack;
delete pst2;
return 0;
}
Ⅳ、new 和 delete 的实现原理
01 对于内置类型
如果申请的是内置类型的空间,new 和 malloc ,free 和 delete 基本相似。
不同的是,new / delete 申请和释放的是单个元素的空间
new[] 和 delete[] 申请和释放的是连续空间,new 在申请空间失败时会抛异常
A* a3 = new A;
A* a4 = new A[5];
operator new 和 operator delete 就是对 malloc 和 free 的封装
operator new 调用 malloc 申请空间,失败后,改为抛异常
02 对于自定义类型
new 的原理:
① 调用 operator new 申请空间
② 在申请空间上调用构造函数,完成构造
delete 的原理:
① 在空间上调用析构函数,完成对象中资源的清理工作
② 调用 operator delete 释放空间
new T[N] 的原理:
① 调用 operator new[] ,在 operator new[] 中实际调用 operator new 完成 N 个对象空间的申请
② 在申请空间上调用 N 次构造函数,完成初始化
delete[] 的原理:
① 在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源的清理工作
② 调用 operator delete[] ,在 operator delete[] 中实际调用 operator delete 释放空间
Ⅴ、定位 new
01 定位 new 表达式
定位 new 表达式是在已分配的原始空间中调用构造函数初始化一个对象
简单来说,定位 new 表达式可以在已有的空间进行初始化
分为带参和不带参
new(目标地址指针)类型 // 不带参
new(目标地址指针)类型(该类型的初始化列表) // 带参
02 定位 new 的使用场景
假如开的空间是从内存池来的,如果想初始化,我们就可以使用定位 new
因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调用构造函数进行初始化。
03 定位 new 的使用演示
不带参:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A():" << endl;
}
~A()
{
cout << "~A():" << endl;
}
private:
int _a;
};
int main()
{
A* a1 = (A*)malloc(sizeof(A));
new(a1)A;
return 0;
}
带参:
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A():" << endl;
}
~A()
{
cout << "~A():" << endl;
}
private:
int _a;
};
int main()
{
A* a2 = (A*)malloc(sizeof(A));
new(a2)A(10);
return 0;
}
模拟一下 new 的行为:
A* a3 = new A(3);
//等价于:
A* a4 = (A*)operator new(sizeof(A));
new(a4)A(3);
析构函数
析构函数可以显示调用(构造函数不可以)
int main()
{
A* a1 = new A(3);
delete a1;
//等价于:
A* a2 = (A*)operator new(sizeof(A));
new(a2)A(3);
a2->~A();
operator delete (a2);
return 0;
}