【C++杂货铺】内存管理(一)

简介: 【C++杂货铺】内存管理(一)

73c948691b594621be63079e5c662153.gif

一、C/C++内存分布

从用途和存储的角度来看,在C/C++程序中有局部数据、静态数据、全局数据、常量数据、动态申请的数据五种主要的数据,各种数据的特点如下:

局部数据:随用随创建,存储在栈区,作用域只在局部,生命周期在局部,出了作用域就销毁。

静态数据:存储在数据段,作用域只在局部或当前文件,生命周期伴随程序“一生”。

全局数据:存储在数据段,作用域在全局,生命周期伴随程序“一生”。

常量数据:存储在代码段。

动态申请的数据:存储在堆区。

fcd0b615e213435bb7a7d79b178991bd.jpg小Tips:这里需要注意char char2[] = "abcd";,"abcd"是常量字符串,存储在代码段,但是char2只是一个局部域的数组,它会把这个常量字符串拷贝一份,存储在栈区,然后char2是数组名,表示数组首元素的地址,这里表示的就是拷贝过来存储在栈区的字符串的首元素地址,并不是表示代码段中常量字符串的首元素地址。其次,const并不会改变变量的存储区域,因为pChar3是一个局部变量,所以它还是存储在栈区,前面加const修饰,是因为它存的是代码段中常量字符串的首地址。可见只要是在局部创建的变量,只要前面没有加static修饰,那这个变量一定是存储在栈区。

📖总结:

栈:又叫堆栈,用于存放非静态局部变量、函数参数、返回值等等,栈是向下生长的。

内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。

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

数据段:用于存储全局数据和静态数据。

代码段:用于存储可执行的代码/只读的常量。

小Tips:数据段也可以叫做静态区,代码段也可以叫做常量区。其中数据段和代码段是站在操作系统的角度进行划分的,而静态区和常量区是站在语言的角度进行划分的。

二、C++内存管理方式

在学习C语言时,提到的4个内存管理函数malloc、realloc、calloc、free大家还记得嘛👀?忘了的话可以走传送门,回去看看。在C++中我们任然可以使用C语言中的内存管理方式,但有些地方就无能为力了,而且使用起来比较麻烦,因此C++中提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。

2.1 new和delete操作内置类型

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

小Tips:new对内置类型来说,和C语言一样,只是纯粹的开空间,对于动态申请到的空间,如果不进行人为初始化的话,里面存的都是随机值。动态申请并初始化,类型后面跟的是圆括号(),动态申请多个连续空间,类型后面跟的是方括号[],如果要对这多个连续的空间初始化,可以在[]的后面跟{},里面是初始化的数据,可以只初始化一部分,后面默认补0。申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间使用new[]和delete[]

2.2 new和delete操作自定义类型

📖C语言的玩法

先来回顾一下,C语言中针对自定义类型是怎么玩的,以动态申请一个链表节点为例:

//节点类型
struct List_Node
{
  int _val;
  struct List_Node* _next;
};
//申请节点的函数
List_Node* BuyListNode(int x)
{
  List_Node* tmp = (List_Node*)malloc(sizeof(List_Node));
  if (nullptr == tmp)
  {
    perror("malloc");
    exit(-1);
  }
  tmp->_val = x;
  tmp->_next = nullptr;
}
int main()
{
  //动态申请三个节点
  List_Node* p1 = BuyListNode(1);
  List_Node* p2 = BuyListNode(2);
  List_Node* p3 = BuyListNode(3);
  return 0;
}

如上面的代码所示,在C语言中,要想为自定义类型动态申请一块空间,我们单独封装了一个函数BuyListNode,这个函数完成了空间申请、合理性检查、初始化等工作,然后通过调用这个函数实现自定义类型的动态内存申请。

📖C++的玩法

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类型变量
  A* p2 = new A(1);
  delete p2;
  //通过new动态申请3个连续空间,存储3个A类型的数据
  A* p6 = new A[3];
  delete[] p6;
  return 0;
}

8ef8af968868439988eb91913ab65a64.png

通过打印结果可以看出,new/delete对于自定义类型出了开空间,还会调用构造函数和析构函数。

小Tips:上面代码中A* p2 = new A(1);,我们进行了人为的初始化,即用圆括号传了一个1,由于new会去调用构造函数,所以这个1最终会被A类构造函数的形参接受,用于初始化对象中的_a成员变量;而A* p6 = new A[3];申请的3个连续空间(数组),我们并没有进行人为的初始化,这在当前环境下也是可以的,因为构造函数的形参有缺省值,属于默认构造函数,new去调用构造函数的时候,可以不需要传实参直接使用缺省值,但是如果A类没有默认构造函数,那我们在使用new动态申请空间的时候,一定要进行人为的传值初始化。

7a0cef59db0d44e58ffade38d040a921.png

如果A类没有默认构造函数,我们在new的时候就需要人为的传参进行初始化,像下面这样。

//通过new动态申请一块空间,存储A类型变量
A* p2 = new A(1);
//通过new动态申请10个连续空间,存储10个A类型的数据
A* p6 = new A[3]{ 1, 2, 3 };//通过隐式类型转换去初始化
A* p6 = new A[3]{ A(1), A(2), A(3) };//通过匿名对象去初始化
//A* p6 = new A[3]{ A(1), A(2)};//申请了三块空间,只初始化了两个,这是不被允许的

小Tips:对于动态申请的自定义类型数组,如果该自定义类型没有默认构造,则需要进行人为的初始化,并且申请了几块空间就要初始化几个,不像动态申请的内置类型数组,可以只初始化前面一部分,后面默认是0。此外,如果该类的构造函数有多个形参(超过一个),就不支持隐式类型转换去初始化了,可以继续使用匿名对象去初始化。

📖一些建议

这里建议大家malooc、free、new、delete、new[]、delete[]不要混用,严格按照下面的对应关系去使用:

malloc——free

new——delete

new[]——delete[]

对于内置类型,混用了可能没什么,但是对于自定义类型可就不一定了。

d9c47d757fd3438c89899bda1f903712.png


三、operator new与operator delete函数

new和delete是用户进行动态内存申请和释放的操作符,operator new和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过调用operator delete全局函数来释放空间。

小Tips:operator new和operator delete并不是简单的运算符重载哦。

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和operator delete的实现,从中我们可以发现,operator new实际上也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete最终是通过free来释放空间的。

小Tips:operator new、operator delete和malloc、free在功能上相似,都是到堆上申请空间,然后释放空间,它们的区别在于,前者失败了会抛异常,而后者失败了是返回错误码,通过返回值来表示这一块有问题。抛异常是面向对象语言的一大特色,关于异常春人会在后面的文章中给大家分享,感兴趣的友友可以关注一下春人哟。


目录
相关文章
|
5月前
|
安全 C语言 C++
比较C++的内存分配与管理方式new/delete与C语言中的malloc/realloc/calloc/free。
在实用性方面,C++的内存管理方式提供了面向对象的特性,它是处理构造和析构、需要类型安全和异常处理的首选方案。而C语言的内存管理函数适用于简单的内存分配,例如分配原始内存块或复杂性较低的数据结构,没有构造和析构的要求。当从C迁移到C++,或在C++中使用C代码时,了解两种内存管理方式的差异非常重要。
194 26
|
10月前
|
存储 程序员 编译器
玩转C++内存管理:从新手到高手的必备指南
C++中的内存管理是编写高效、可靠程序的关键所在。C++不仅继承了C语言的内存管理方式,还增加了面向对象的内存分配机制,使得内存管理既有灵活性,也更加复杂。学习内存管理不仅有助于提升程序效率,还有助于理解计算机的工作原理和资源分配策略。
|
6月前
|
C语言 C++
c与c++的内存管理
再比如还有这样的分组: 这种分组是最正确的给出内存四个分区名字:栈区、堆区、全局区(俗话也叫静态变量区)、代码区(也叫代码段)(代码段又分很多种,比如常量区)当然也会看到别的定义如:两者都正确,记那个都选,我选择的是第一个。再比如还有这样的分组: 这种分组是最正确的答案分别是 C C C A A A A A D A B。
113 1
|
12月前
|
存储 缓存 编译器
【硬核】C++11并发:内存模型和原子类型
本文从C++11并发编程中的关键概念——内存模型与原子类型入手,结合详尽的代码示例,抽丝剥茧地介绍了如何实现无锁化并发的性能优化。
499 68
|
9月前
|
存储 Linux C语言
C++/C的内存管理
本文主要讲解C++/C中的程序区域划分与内存管理方式。首先介绍程序区域,包括栈(存储局部变量等,向下增长)、堆(动态内存分配,向上分配)、数据段(存储静态和全局变量)及代码段(存放可执行代码)。接着探讨C++内存管理,new/delete操作符相比C语言的malloc/free更强大,支持对象构造与析构。还深入解析了new/delete的实现原理、定位new表达式以及二者与malloc/free的区别。最后附上一句鸡汤激励大家行动缓解焦虑。
|
10月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
447 0
|
11月前
|
存储 程序员 编译器
什么是内存泄漏?C++中如何检测和解决?
大家好,我是V哥。内存泄露是编程中的常见问题,可能导致程序崩溃。特别是在金三银四跳槽季,面试官常问此问题。本文将探讨内存泄露的定义、危害、检测方法及解决策略,帮助你掌握这一关键知识点。通过学习如何正确管理内存、使用智能指针和RAII原则,避免内存泄露,提升代码健壮性。同时,了解常见的内存泄露场景,如忘记释放内存、异常处理不当等,确保在面试中不被秒杀。最后,预祝大家新的一年工作顺利,涨薪多多!关注威哥爱编程,一起成为更好的程序员。
493 0
|
存储 缓存 C语言
【c++】动态内存管理
本文介绍了C++中动态内存管理的新方式——`new`和`delete`操作符,详细探讨了它们的使用方法及与C语言中`malloc`/`free`的区别。文章首先回顾了C语言中的动态内存管理,接着通过代码实例展示了`new`和`delete`的基本用法,包括对内置类型和自定义类型的动态内存分配与释放。此外,文章还深入解析了`operator new`和`operator delete`的底层实现,以及定位new表达式的应用,最后总结了`malloc`/`free`与`new`/`delete`的主要差异。
219 3
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
625 4
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。