【C++初阶】:C&C++内存管理

island1314 2024-08-06 08:05:12 阅读 93

✨                                        理想主义的少年永远不会被现实招安      🌏

📃个人主页:island1314

🔥个人专栏:C++学习

🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏  💞 💞 💞


1、1.C/C++内存分布

我们先来看下面的一段代码和相关问题

<code>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);

}

1. 选择题:

选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)

globalVar在哪里?__C__ staticGlobalVar在哪里?__C__

staticVar在哪里?__C__ localVar在哪里?__A__

num1 在哪里?__A__

分析:

globalVar全局变量在数据段 staticGlobalVar静态全局变量在静态区

staticVar静态局部变量在静态区 localVar局部变量在栈区

num1局部变量在栈区

char2在哪里?__A__ *char2在哪里?__A__

pChar3在哪里?__A__ *pChar3在哪里?__D__

ptr1在哪里?__A__ *ptr1在哪里?__B__

分析:

char2局部变量在栈区

char2是一个数组,把后面常量串拷贝过来到数组中,数组在栈上,所以*char2在栈上

pChar3局部变量在栈区 *pChar3得到的是字符串常量字符在代码段

ptr1局部变量在栈区 *ptr1得到的是动态申请空间的数据

【说明】

1、栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的,栈可以通过函数_alloca进行动态分配,不过注意,所分配空间不能通过free或delete进行释放,频繁的申请空间和释放空间,容易造成内存碎片,甚至内存泄漏,栈区由于是自动管理,不存在此问题

2、内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(现在只需要了解, 后面在linux中会提到)

3、堆用于程序运行时动态内存分配,堆是可以上增长的。堆无法静态分配,只能动态分配

4、数据段–存储全局数据和静态数据。

5、代码段–可执行的代码/只读常量。


2、 C语言中动态内存管理方式

方式:malloc/calloc/realloc/free

<code>

void Test()

{

int* p1 = (int*)malloc(sizeof(int));

free(p1);

// 1.malloc/calloc/realloc的区别是什么?

int* p2 = (int*)calloc(4, sizeof(int));

int* p3 = (int*)realloc(p2, sizeof(int) * 10);

// 这里需要free(p2)吗?

free(p3);

}

 malloc/calloc/realloc的区别?

三者都是分配内存,都是stdlib.h库里的函数,但是也存在一些差异。

(1)malloc函数。其原型void *malloc(unsigned int num_bytes);

              num_byte为要申请的空间大小,需要我们手动的去计算,如int *p = (int*)

malloc( 20*sizeof(int)),如果编译器默认int为4字节存储的话,那么计算结果是80Byte,一次申请一个80Byte的连续空间,并将空间基地址强制转换为int类型,赋值给指针p,此时申请的内存值是不确定的。

(2)calloc函数,其原型void *calloc(size_t n, size_t size);

        其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,会int *p = (int *)calloc(20, sizeof(int)),这样就省去了人为空间计算的麻烦。但这并不是他们之间最重要的区别,malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0;

既然calloc不需要计算空间并且可以直接初始化内存避免错误,那为什么不直接使用calloc函数,那要malloc要什么用呢?

实际上,任何事物都有两面性,有好的一面,必然存在不好的地方。这就是效率。calloc函数由于给每一个空间都要初始化值,那必然效率较malloc要低,并且现实世界,很多情况的空间申请是不需要初始值的,这也就是为什么许多初学者更多的接触malloc函数的原因。

(3)realloc函数和上面两个有本质的区别,其原型void realloc(void *ptr, size_t new_Size)

用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作,ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小。


3、C++内存管理方式

       C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

🌈3.1 new/delete操作内置类型

void Test()

{

//内置类型

// 管理对象

// 动态申请一个int类型的空间

int* ptr4 = new int;

// 动态申请一个int类型的空间并初始化为10

int* ptr5 = new int(10);

//管理对象数组

// 动态申请10个int类型的空间

int* ptr6 = new int[3];

//动态申请10个int类型的空间,并初始化为1 2 3 4

int* ptr7 = new int[10] {1, 2, 3, 4}; //后面的会默认初始化为0

//释放一个对象

delete ptr4;

delete ptr5;

//释放多个对象

delete[] ptr6;

delete[] ptr7;

}

注意:申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:匹配起来使用。

🌈3.2 new和delete操作自定义类型

       new / delete malloc / free最大区别是 new / delete 对于【自定义类型】除了开空间还会调用构造函数和析构函数

class A

{

public:

A(int a = 0)

: _a(a)

{

//cout << "A():" << this << endl;

cout << "A():" << endl;

}

A(int a1, int a2)

{

cout << "A(int a1, int a2)" << endl;

}

A(const A& aa)

: _a(aa._a)

{

cout << "A(const A& aa)" << endl;

}

A& operator=(const A& aa)

{

cout << "A& operator=(const A& aa)" << endl;

if (this != &aa)

{

_a = aa._a;

}

return *this;

}

~A()

{

//cout << "~A()" << this << endl;

cout << "~A()" << endl;

}

private:

int _a;

};

int main()

{

// new/delete 和 malloc/free最大区别是

// new/delete对于【自定义类型】除了开空间,还会调用构造函数和析构函数

A* p1 = (A*)malloc(sizeof(A));

A* p2 = new A(2);

free(p1);

delete p2;

// 内置类型是几乎是一样的

int* p3 = (int*)malloc(sizeof(int)); // C

int* p4 = new int;

free(p3);

delete p4;

A* p5 = (A*)malloc(sizeof(A) * 10);

A* p6 = new A[10]; //调用10次构造函数

free(p5);

delete[] p6; //调用10次析造函数

//多个对象

A aa1(1);

A aa2(2);

A aa3(3);

A* p7 = new A[10]{ aa1,aa2,aa3 };

delete[]p7;

// 多参数

A* p7 = new A[10]{ 1,2,3,4,5,{6,7}};//用1生成临时对象,然后被编译器合二为一(隐式对象转换)

delete[]p7;

return 0;

}

注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会

4.operator new与operator delete函数

1.new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数

2.new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局 函数来释放空间。

5. new和delete的实现原理

💫5.1 内置类型   

  如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:

new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

💫5.2 自定义类型

new的原理

1. 调用operator new函数申请空间

2. 在申请的空间上执行构造函数,完成对象的构造

delete的原理

1. 在空间上执行析构函数,完成对象中资源的清理工作

2. 调用operator delete函数释放对象的空间

new T[N]的原理

1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对

象空间的申请

2. 在申请的空间上执行N次构造函数

delete[]的原理

1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理

2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释

放空间

总结:new == operator new -> (malloc) + 构造函数

           ~~~~~~~~~~          delete == 析构函数 + operator delete -> (free)

           ~~~~~~~~~~          特别的:new失败了,抛异常, 不需要再检查返回值

💫5.3 delete注意事项

ClassA *pclassa=new ClassA[5];

delete pclassa;

析构函数此时只会调用1次,要想完整释放数组空间,需要使用[]

注意:程序可能崩溃

6、定位new表达式(placement-new)

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式:

new (place_address) type或者new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

使用场景:

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化

class A

{

public:

A(int a = 0)

: _a(a)

{

cout << "A():" << this << endl;

}

~A()

{

cout << "~A()" << this << endl;

}

private:

int _a;

};

int main()

{

//A* p1 = new A; //和下面等价

A* p1 = (A*)operator new(sizeof(A));

//p1->A(); //不支持这样显示调用构造

new(p1)A; //对已有空间显示调用构造

// delete p1 //和下面等价

p1->~A();

operator delete(p1);

return 0;

}

7、一些内存管理的问题

7.1 malloc/free和new/delete的区别

malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

1、 malloc和free是函数,new和delete是操作符

2、malloc申请的空间不会初始化,new可以初始化

3、 malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可

4、 malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型

5、malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需

要捕获异常

6、 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理

7.2 内存泄漏

🧩7.2.1 什么是内存泄漏,内存泄漏的危害

       什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

       内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

class A

{

public:

A(int a = 0)

: _a(a)

{

cout << "A():" << this << endl;

}

~A()

{

cout << "~A()" << this << endl;

}

private:

int _a;

};

int main()

{

// new []

A* p2 = (A*)operator new(sizeof(A) * 10);

//new(p2)A[10]; //对已有空间显示调用构造

//new(p2)A[10]{ 1,2,3,4 }; //从语法上来说是可以的

for (int i = 0; i < 10; ++i) new(p2 + i)A(i); //用for循环来初始化每个对象

//delete []

for (int i = 0; i < 10; i++) (p2 + i)->~A(); //析构每个对象

operator delete[](p2);

return 0;

}

🧩7.2.2 内存泄漏分类

C/C++程序中一般我们关心两种方面的内存泄漏:

堆内存泄漏(Heap leak):

       堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

系统资源泄漏:

       指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

🧩7.2.3 如何检测内存泄漏

在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks() 函数进行简单检测,该

函数只报出了大概泄漏了多少个字节,没有其他更准确的位置信息。

🧩 7.2.4如何避免内存泄漏

1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:

这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智

能指针来管理才有保证。

2. 采用RAII思想或者智能指针来管理资源。

3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。

4. 出问题了使用内存泄漏工具检测。

总结一下:

内存泄漏非常常见,解决方案分为两种:

1、事前预防型。如智能指针等。

2、事后查错型。如泄漏检测工具。

📖总结

以上就是 C & C++ 内存管理的全部内容啦

本篇到此就结束,希望我的这篇博客可以给你提供有益的参考和启示,感谢大家支持!!!

祝大家天天顺心如意



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。