C++初阶之内存分布(上)

简介: C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。

#我的2023年上半年总结#


44f17d7660924a03b2e15962387825b5.png


C/C++内存分布


我们先来看下面的一段代码 :


int g_val=100;
int g_unval;
int main(int argc,char* argv[],char* env[])
{
     printf("code addr         :%p\n",main);
     const char* p="hello";
     printf("read only         :%p\n",p);
     printf("global val        :%p\n",&g_val);
     printf("global uninit  val:%p\n",&g_unval);
     char* q1=(char*)malloc(10);
     char* q2=(char*)malloc(10);
     char* q3=(char*)malloc(10);
     char* q4=(char*)malloc(10);
     printf("heap addr         :%p\n",q1);
     printf("heap addr         :%p\n",q2);
     printf("heap addr         :%p\n",q3);
     printf("heap addr         :%p\n",q4);
     printf("stack addr        :%p\n",&q1);
     printf("stack addr        :%p\n",&q2);
     printf("stack addr        :%p\n",&q3);
     printf("stack addr        :%p\n",&q4);
     static int i=0;
     printf("static addr       :%p\n",&i);
     printf("args addr         :%p\n",argv[0]);
     printf("env addr          :%p\n",env[0]);                                                             
     return 0;                       
 } 


实际输出和对应的内存分布


3deaac2d2a9541faad2954bc0edac6f4.png


我在Linux进程一文当中对内存分布和虚拟内存有详细讲解,不了解的小伙伴可以去看看这篇文章:


Linux进程


说明:


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

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

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

4.数据段(初始化数据和未初始化数据区)–存储全局数据和静态数据。

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


C语言中动态内存管理方式:malloc/calloc/realloc/free


1.malloc和free


C语言提供了一个动态内存开辟的函数:

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。


1.如果开辟成功,则返回一个指向开辟好空间的指针。

2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

4.如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。


C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:


void free (void* ptr);


free函数用来释放动态开辟的内存。


1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

2.如果参数 ptr 是NULL指针,则函数什么事都不做。


malloc和free都声明在 stdlib.h 头文件中。


2.calloc


C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:


void* calloc (size_t num, size_t size);


1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

2.与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。


int main()
{
  int* p = (int*)calloc(10, sizeof(int));
  if (NULL != p)
  {
  }
  free(p);
  p = NULL;
  return 0;
}

02866700040f4822a3d440c21192fd7a.png


所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。


3.realloc


1.realloc函数的出现让动态内存管理更加灵活。

2.有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。


函数原型如下:


void* realloc (void* ptr, size_t size);


1.ptr 是要调整的内存地址


2.size 调整之后新大小


3.返回值为调整之后的内存起始位置。


4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。


5.realloc在调整内存空间的是存在两种情况:


情况1:原有空间之后有足够大的空间

情况2:原有空间之后没有足够大的空间


b586967f037b4f88b5793edf034c9b99.png


情况1

当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。


情况2

当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。


由于上述的两种情况,realloc函数的使用就要注意一些。


4.常见的动态内存错误


对NULL指针的解引用操作


void test()
{
  int* p = (int*)malloc(INT_MAX / 4);
  *p = 20;//如果p的值是NULL,就会有问题
  free(p);
}


对动态开辟空间的越界访问


void test()
{
  int i = 0;
  int* p = (int*)malloc(10 * sizeof(int));
  if (NULL == p)
  {
    exit(EXIT_FAILURE);
  }
  for (i = 0; i <= 10; i++)
  {
    *(p + i) = i;//当i是10的时候越界访问
  }
  free(p);
}


对非动态开辟内存使用free释放


void test()
{
  int a = 10;
  int *p = &a;
  free(p);
}


使用free释放一块动态开辟内存的一部分


void test()
{
  int* p = (int*)malloc(100);
  p++;
  free(p);//p不再指向动态内存的起始位置
}


对同一块动态内存多次释放


void test()
{
  int* p = (int*)malloc(100);
  free(p);
  free(p);//重复释放
}


动态开辟内存忘记释放(内存泄漏)


void test()
{
  int* p = (int*)malloc(100);
  if (NULL != p)
  {
    *p = 20;
  }
}
int main()
{
  test();
  while (1);
}


忘记释放不再使用的动态开辟的空间会造成内存泄漏。


切记:动态开辟的空间一定要释放,并且正确释放。


C++内存管理方式


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


1.new/delete操作内置类型


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;
   // C++11支持
   A* ptr7 = new A[2]{1,2};
   A* ptr8 = new A[2]{ A(1), A(2) };
}


cfaa9c0eef25498db9d6e754965c7107.png


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


2.new和delete操作自定义类型


class A
{
public:
    A(int a = 0)
        : _a(a)
    {
        cout << "A():" << this << endl;
    }
    ~A()
    {
        cout << "~A():" << this << endl;
    }
private:
    int _a;
};
int main()
{
    // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
    //还会调用构造函数和析构函数
    A* p1 = (A*)malloc(sizeof(A));
    A* p2 = new A(1);
    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];
    free(p5);
    delete[] p6;
    return 0;
}


92c23c8d78384150b03c3985a4a2cb31.png


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


3.new和malloc使用上的区别


以下代码在32位环境下实验,在64位环境下需开辟更大空间


malloc


#include <iostream>
using namespace std;
int main()
{
  // 失败返回NULL
  char* p1 = (char*)malloc(1024u*1024u*1024u*2 - 1);
  printf("%p\n", p1);
  return 0;
}


new


#include <iostream>
using namespace std;
int main()
{
    char* p2 = new char[1024u * 1024u * 1024u * 2 - 1];
    printf("%p\n", p2);
    delete(p2);
  return 0;
}


a2ede99b203f4720a072e9fa8068ffe2.png


通过两段代码我们不难发现,两者最大的区别就是在开辟空间失败后,malloc只能返回NULL,而new抛异常,这样我们更容易发现问题存在,当然这里还不算最大的区别和其最大作用体现,他真正的作用体现在类中,下面我们会提到。


operator new与operator delete函数


1.operator new与operator delete函数


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


我们可以看看上一段函数new和delete函数在调用时的反汇编代码


7342bf2011774c99a109afe6a532dd0c.png


operator new与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来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。==operator delete 最终是通过free来释放空间的。 ==


2.重载operator new与operator delete函数


没错,operator new与operator delete函数还可以自己进行重载


#include <iostream>
using namespace std;
class A
{
public:
  A(int a = 0)
    : _a(a)
  {
    cout << "A():" << this << endl;
  }
  ~A()
  {
    cout << "~A():" << this << endl;
  }
private:
  int _a;
};
 //重载operator new,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个字节
void* operator new(size_t size, const char* fileName, const char* funcName, size_t lineNo)
{
  void* p = ::operator new(size);
  cout <<"new:"<< fileName << "||" << funcName << "||" << lineNo << "||" << p << "||" << size << endl;
  return p;
}
// 重载operator delete,在释放空间时:打印再那个文件、哪个函数、第多少行释放
void operator delete(void* p, const char* fileName, const char* funcName, size_t lineNo)
{
  cout <<"delete:"<<fileName << "||" << funcName << "||" << lineNo << "||" << p << endl;
  ::operator delete(p);
}
//#ifdef _DEBUG
#define new new(__FILE__, __FUNCTION__, __LINE__)
#define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__)
//#endif
int main()
{
  A* p1 = new A;
  delete(p1);
  return 0;
}


b2f8cc97b8e440da84b1c66e23aaded6.png


可以看到我们重载的new是会自动调用构造函数的,但是delete函数并没有调用析构函数,这是因为我们在重载的时候,宏定义改变了其调用形式,导致编译时其不认为还有delete函数的机制。


3.实现一个类专属的operator new和operator delete


#include <iostream>
using namespace std;
struct ListNode
{
  int _val;
  ListNode* _next;
  // 内存池
  static allocator<ListNode> alloc;
  void* operator new(size_t n)
  {
    cout << "operator new -> STL内存池allocator申请" << endl;
    void* obj = alloc.allocate(1);
    return obj;
  }
  void operator delete(void* ptr)
  {
    cout << "operator delete -> STL内存池allocator申请" << endl;
    alloc.deallocate((ListNode*)ptr, 1);
  }
  struct ListNode(int val)
    :_val(val)
    , _next(nullptr)
  {}
};
//allocator在STL后才会提到,这里我们先不探讨它
allocator<ListNode> ListNode::alloc;
int main()
{
  // 频繁申请ListNode. 想提高效率 -- 申请ListNode时,不去malloc,而是自己定制内存池
  ListNode* node1 = new ListNode(1);
  delete node1;
  return 0;
}


20c69d4c61d74c7b892644e16cfc6695.png


这里用到了allocator,提到了内存池的概念,这个要到后面的STL才会学习,这里我们只是用这个做一个演示。利用operator new和operator delete的这种特性,我们可以为每一个类打造属于自己专属的operator new和operator delete,彼此互不干扰,且使用方式也是正常调用,如果出现内存泄漏我们也可以很好的观察到。

相关文章
|
8天前
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
25 2
|
2月前
|
存储 编译器 C语言
【C++】C\C++内存管理
【C++】C\C++内存管理
【C++】C\C++内存管理
|
4天前
|
安全 C++
超级好用的C++实用库之环形内存池
超级好用的C++实用库之环形内存池
19 5
|
2月前
|
存储 安全 编译器
Go 内存分布
该文章深入分析了Go语言中值的内存分布方式,特别是那些分布在多个内存块上的类型,如切片、映射、通道、函数、接口和字符串,并讨论了这些类型的内部结构和赋值时的行为,同时指出了“引用类型”这一术语在Go中的使用可能会引起的误解。
48 5
Go 内存分布
|
4天前
|
C++
超级好用的C++实用库之动态内存池
超级好用的C++实用库之动态内存池
11 0
|
2月前
|
编译器 C++
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
virtual类的使用方法问题之C++类中的非静态数据成员是进行内存对齐的如何解决
|
2月前
|
存储 Java C语言
【C++】C/C++内存管理
【C++】C/C++内存管理
|
1月前
|
C语言 C++
C++(二)内存管理
本文档详细介绍了C++中的内存管理机制,特别是`new`和`delete`关键字的使用方法。首先通过示例代码展示了如何使用`new`和`delete`进行单个变量和数组的内存分配与释放。接着讨论了内存申请失败时的处理方式,包括直接抛出异常、使用`try/catch`捕获异常、设置`set_new_handler`函数以及不抛出异常的处理方式。通过这些方法,可以有效避免内存泄漏和多重释放的问题。
|
2月前
|
存储 编译器 C语言
C++内存管理(区别C语言)深度对比
C++内存管理(区别C语言)深度对比
72 5
|
2月前
|
存储 程序员 编译器
c++学习笔记08 内存分区、new和delete的用法
C++内存管理的学习笔记08,介绍了内存分区的概念,包括代码区、全局区、堆区和栈区,以及如何在堆区使用`new`和`delete`进行内存分配和释放。
40 0