C++ 内存管理

简介: C++ 内存管理

一. C/C++中的内存分布


在此之前我们已经很多次的提及了栈区堆区静态区的概念


栈 又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。

堆 用于程序运行时动态内存分配,堆是可以上增长的。

(静态区)数据段–存储全局数据和静态数据。

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


那么复习完了上面的概念 我们来做几道题练练手


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在哪里?____  staticGlobalVar在哪里?____
staticVar在哪里?____  localVar在哪里?____
num1 在哪里?____
char2在哪里?____  *char2在哪里?___
pChar3在哪里?____    *pChar3在哪里?____
ptr1在哪里?____     *ptr1在哪里?____
2. 填空题:
sizeof(num1) = ____; 
sizeof(char2) = ____;    strlen(char2) = ____;
sizeof(pChar3) = ____;   strlen(pChar3) = ____;
sizeof(ptr1) = ____;


其中前面1~5没有什么难点


全局变量在静态区


被Static修饰的变量在静态区


局部变量放到栈区


故答案为 C C C A A


我们再来看看后面六个


指针是在栈区(局部变量)


动态开辟出的空间是在堆区


只要记住这两点上面的题目基本不会错


唯一值得注意的是这一行代码


char char2[] = "abcd";

*char2的在哪里?


因为这时候的char2是一个指向首元素的地址 (不懂的可以去复习下C语言数组章节)


对于它解引用出来的是首元素 也就是字符‘a’ 和常量的字符串类型不匹配


于是这个时候就会在栈上拷贝一份临时的数据


所以说它在栈区中


故答案为


A A A A D A B

4f77adda3e2c46cd9e8f149c04de55d4.png 

看看这张图 之后我们便开始做填空题


sizeof(num1) = ____; 
sizeof(char2) = ____;    strlen(char2) = ____;
sizeof(pChar3) = ____;   strlen(pChar3) = ____;
sizeof(ptr1) = ____;

这里唯一需要注意的一点是 字符串后面的‘/0’ 在计算大小是算作是一个大小 在计算长度时忽略不计


所以说sizeof(char2) = 5


其他的题目都很简单 这里直接给出答案


40 5 4 4 4 4 (32位系统)


这里提出一个问题 sizeof和strlen的区别是什么?


答:一个是计算大小占多少个字节 一个是计算字符串长度是多少


二. C语言中的动态内存管理方式


C语言中实现内存管理主要是通过下面这四个函数分别是


malloc


calloc


realloc


free


其中常见的面试题有


malloc calloc realloc之间的区别是什么 ?


这里可以直接参考萌新以前写的博客


C语言动态内存开辟


着重要注意下malloc和calloc的区别


一个是不初始化 一个是全部初始化为0


三. C++中动态内存管理方式


我们说C++是基于C语言的优化 所以说C语言中的动态内存管理方式在C++中也是可以使用的


但是呢在某些特殊场景下 C语言中的动态内存管理会不能操作 或者说操作起来很麻烦


所以说这个时候我们C++就诞生了新的内存管理方式


3.1 new和delete操作符操作内置类型


注意我的副标题写的是什么? 操作符


说明我们这里的两个关键字是操作符 而C语言中的malloc ralloc cealloc free都是函数 这里要注意

void Test()
{
 // 动态申请一个int类型的空间
 int* ptr4 = new int;
 // 动态申请一个int类型的空间并初始化为10
 int* ptr5 = new int(10);
 // 动态申请10个int类型的空间
 int* ptr6 = new int[3];
 delete ptr4;
 delete ptr5;
 delete[] ptr6;
}


5144be9e7e9745e09e2a071dc2751134.png


我们来看看debug过程

db63cb57356242259517b203de3042f2.png


从下面的监视窗口同学们应该就能理解了初始化和new的基本使用了


我们这里继续走下去

7aba430495fe454fa33b49820b0f85e0.png


delete之后发现全部被删除了


这里我们总结下


总结


对于申请和释放单个元素的空间 我们使用new和delete

如果想要初始化 我们在后面加上括号 括号里面写上我们要初始化的值就好

对于申请和释放连续的空间的时候我们使用 new[] 和delete[]


这里要注意的一点是 在C++11更新的标准中增加了对于连续空间的初始化方式


代码表示如下

int* ptr6 = new int[3]{1,2,3};

84c79366fd804038ade104a3ab621dcb.png


在C++11标准以后 我们也可以使用大括号对于连续的空间进行初始化


3.2 new和delete操作符操作自定义类型 (重点之一)


还是一样 我们先来看代码


class A
{
public:
  A(int a = 0)
  : _a(a)
  {
  cout << "A():" << this << endl;
  }
  ~A()
  {
  cout << "~A():" << this << endl;
  }
private:
  int _a;
};
struct ListNode
{
  ListNode* _next;
  int _val;
  ListNode(int val)
  :_next(nullptr)
  ,_val(val)
  {}
};
int main()
{
  //自定义类型
  //new和delete相比malloc/free,除了空间管理还会调用构造函数和析构函数
  A* p1 = new A;
  A* p2 = (A*)malloc(sizeof(A));
  delete p1;
  free(p2);
  ListNode* n1 = new ListNode(1);
  ListNode* n2 = new ListNode(2);
  ListNode* n3 = new ListNode(3);
  ListNode* n4 = new ListNode(4);
  n1->_next = n2;
  return 0;
}

729d80cf883d4787ac32613ef312c74f.png

我们这里发现会自动调用构造函数和析构函数


这就是为什么C++中会使用new和delete操作符的原因之一


之后我们再来看看malloc出来的空间 能不能调用构造和析构

(其实这里已经能猜出来了 不能 )

9168f13a902d4d81b30f88bb80914e6b.png


这里什么都没有发生 符合预期

71be4cbb89f14c429f25d3d96fa9103b.png


我们发现对于Listnode来说 也是new出来就初始化了


比起malloc来说方便不少


3.3 面向过程和面向对象的错误区别(重点之一)


面向过程的语言的错误是通过错误码识别的

面向对象的语言的错误是通过抛出异常识别的


还记得C语言中我们malloc失败是怎么报错的嘛?


代码表示如下


A* p2 = (A*)malloc(sizeof(A));
  if (p2==NULL)
  {
  perror("malloc fail");
  exit(-1);
  }


但是在C++中我们的报错是这样子的


(这里的语法记住就好 不要求现在掌握)


int main()
{
  try
  {
  while (1)
  {
    new char[1024u * 1024u * 1024u];
    cout << "yes" << endl;
  }
  cout << "test" << endl;
  }
  catch (exception& e)
  {
  cout << e.what() << endl;
  }
  return 0;
}

df81a810b6274504b79f426c08ffef85.png


这里我们可以发现两个点


1 new空间失败之后会直接抛出一个异常到catch 捕获异常

2 new空间失败后后面的语句不会执行


为了验证第二点我们可以这样试试

829ff3475a724ad4aae5065ba5814017.png


可以完美验证我们的思路


3.4 类型不匹配遇到的错误


如果我们使用了new 我们销毁要使用delete

如果我们使用了new[] 我们销毁要使用delete[]

如果我们使用了 malloc realloc calloc 我们要使用free


不能乱


横穿马路有风险 你这次侥幸没被创不代表你永远不会被创


四. operator new和operator delete函数


new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是

系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过

operator delete全局函数来释放空间。


其实底层就是用malloc来实现的


这里简单画图方便大家理解下

bb11d5ac753e4507995ba4057a49c46b.png


源码表示如下 (这里看看就好)


/*
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)


五. new和delete的实现原理


5.1 内置类型


在实现内置类型开辟销毁空间的时候基本和malloc free相似


唯一不同的是如果开辟失败


new会抛出一个异常


malloc会返回一个空指针


5.2 自定义类型


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是先开辟空间再调用构造函数


delete是先调用析构函数再销毁空间


六. 定位new表达式(placement-new)


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


它的使用格式如下


new (place_address) type(无参)


或者说


new (place_address) type(initializer-list)(有参)


我们来看看代码


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* p3 = (A*)malloc(sizeof(A));
  if (p3 == nullptr)
  {
    perror("malloc fail");
    exit(-1);
  }
  //new(p3)A(1);
  return 0;
}


来看看debug窗口是什么样子的


2e8e6fa801054ff7af8ce237a1750f5a.png


这里的p1初始化了 但是p3没有初始化


但是我们应该怎么调用构造函数呢?


这里给出一个叫做定位new的解决方案


new(p3)A(1);


ead6b7c3ad4640faad77b6a38805b959.png

我们发现 这里确实初始化成功了


而析构函数的调用就简单多了 直接指针指向析构函数就可以


c5f858ee317740fda5e4302d5002e87b.png


七. 常见面试题


7.1 malloc和new的区别 delete和free的区别


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

方是:


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

这个我们再前面就讲解过了


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

这里就是为什么C++中要创造new 两大创造new的原因之一


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

这个是语法上的特点


4.malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型也是语法上的特点


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

这个应该是要记住的 两大创造new的原因之一


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

这个是malloc和delete的自定义类型实现原理 我们在上面讲解过了


应该怎么理解并记忆呢?


首先是语法上的三个特点

int*p = (int *)malloc(sizeof(4))
int* p = new int(3);


观察这两段代码我们不难发现 2 4


之后如果开辟多个空间的话 我们不难发现 3


int* p = new int[5];


回答上来这三点之后我们就开始想 为什么C++要创建new


一个是初始化 一个是抛出异常


最后我们回顾下new实现自定义类型的原理就好


7.2 内存泄漏


什么是内存泄漏呢?


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

该段内存的控制,因而造成了内存的浪费。


内存泄漏的危害是什么呢?


长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现

内存泄漏会导致响应越来越慢,最终卡死。


大家可以写出这段代码 并且打开资源管理器


我们可以发现 这个程序占用的内存越来越高了


int main()
{
  while (1)
  {
  int* p = new int[1024];
  cout << p << endl;
  }
  return 0;
}

8afca21a619242f6b4f540ce8412dda7.png

关于怎么预防内存泄漏其实都是我们后面要学的内容


老师上课的时候也没有重点讲解 所以说博主这里就不提及了 后面遇到再说


总结


本篇博客博主主要讲解了C++中的内存管理


由于博主水平有限 所以错误在所难免 希望大佬看到之后可以指正


如果这篇博客帮助到了你 别忘了一键三连啊


阿尼亚 哇酷哇酷!

相关文章
|
25天前
|
存储 Java C++
C++ 引用和指针:内存地址、创建方法及应用解析
C++中的引用是现有变量的别名,创建时需用`&`运算符,如`string &meal = food;`。指针存储变量的内存地址,使用`*`创建,如`string* ptr = &food;`。引用必须初始化且不可为空,而指针可初始化为空。引用在函数参数传递和提高效率时有用,指针适用于动态内存分配和复杂数据结构操作。选择使用取决于具体需求。
38 9
|
29天前
|
存储 Linux C语言
【C++初阶】6. C&C++内存管理
【C++初阶】6. C&C++内存管理
34 2
|
2月前
|
存储 程序员 Linux
1024程序员节特辑 | C++入门指南:内存管理(建议收藏!!)
1024程序员节特辑 | C++入门指南:内存管理(建议收藏!!)
41 0
|
2月前
|
存储 监控 算法
【C++ 软件设计思路】高效管理历史任务记录:内存与磁盘结合的策略解析
【C++ 软件设计思路】高效管理历史任务记录:内存与磁盘结合的策略解析
60 0
|
12天前
|
存储 缓存 算法
C++从入门到精通:4.6性能优化——深入理解算法与内存优化
C++从入门到精通:4.6性能优化——深入理解算法与内存优化
|
12天前
|
存储 程序员 编译器
C++从入门到精通:3.4深入理解内存管理机制
C++从入门到精通:3.4深入理解内存管理机制
|
13天前
|
存储 人工智能 程序员
【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节
【重学C++】【内存】关于C++内存分区,你可能忽视的那些细节
44 1
|
13天前
|
C语言 C++
【C++基础(九)】C++内存管理--new一个对象出来
【C++基础(九)】C++内存管理--new一个对象出来
|
14天前
|
存储 编译器 Linux
c++的学习之路:8、内存管理与模板
c++的学习之路:8、内存管理与模板
11 0
|
19天前
|
存储 Linux C语言
C/C++之内存旋律:星辰大海的指挥家
C/C++之内存旋律:星辰大海的指挥家
23 0