14. C++ malloc&calloc&recalloc

一、malloc函数

谈到malloc函数相信学过c语言的人都很熟悉,但是malloc底层到底做了什么又有多少人知道。

1.1 关于malloc相关的几个函数

可以这样认为(window下)原型:

c++ 复制代码
extern void *malloc(unsigned int num_bytes);

如果分配成功:则返回指向被分配内存空间的指针,不然返回指针NULL 。同时,当内存不再使用的时候,应使用free()函数将内存块释放掉。

关于:void*,表示未确定类型的指针,c,c++规定void*可以强转为任何其他类型的指针,关于void还有一种说法就是其他任何类型都可以直接赋值给它,无需进行强转,但是反过来不可以 。

malloc:

malloc分配的内存大小至少为参数所指定的字节数,malloc的返回值是一个指针,指向一段可用内存的起始位置,指向一段可用内存的起始地址,多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)实现malloc时应同时实现内存大小调整和内存释放函数(realloc和free) malloc和free是配对的,如果申请后不释放就是内存泄露,如果无故释放那就是什么也没做,释放只能释放一次,如果一块空间释放两次或者两次以上会出现错误(但是释放空指针例外,释放空指针也等于什么也没做,所以释放多少次都是可以的。)

1.2 malloc和new

new返回指定类型的指针,并且可以自动计算所需要的大小。

c++ 复制代码
int *p;
p = new int;//返回类型为int* ,分配的大小是sizeof(int)
p = new int[100];//返回类型是int*类型,分配的大小为sizeof(int)*100

而malloc需要我们自己计算字节数,并且返回的时候要强转成指定类型的指针。

c++ 复制代码
int *p;
p = (int *)malloc(sizeof(int));
  1. malloc的返回是void*,如果我们写成了:p=malloc(sizeof(int));间接的说明了(将void转化给了int*,这不合理)
  2. malloc的实参是sizeof(int),用于指明一个整型数据需要的大小,如果我们写成p=(int*)malloc(1),那么可以看出:只是申请了一个一个字节大小的空间。
  3. malloc只管分配内存,并不能对其进行初始化,所以得到的一片新内存中,其值将是随机的。

一般意义上:我们习惯性的将其初始化为NULL,当然也可以使用memset函数。

简单的说:

malloc函数其实就是在内存中找一片指定大小的空间,然后将这个空间的首地址给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上是连续的,而在物理上可以不连续。我们作为程序员,关注的是逻辑上的连续,其他的操作系统会帮着我们处理。

1.3 malloc实现原理

虚拟内存地址和物理内存地址

为了简单,现代操作系统在处理物理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序层面,当涉及内存地址时,都是使用的虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下每个进程的虚拟地址空间为264Byte。

这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能如此大的空间,实际能用到的空间大小取决于物理内存的大小。 由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转化为物理内存地址,才能实现对内存数据的操作。这个转换一般由一个叫MMU的硬件完成。

页与地址构成

在现代操作系统中,不论是虚拟内存还是物理内存,都不是以字节为单位进行管理的,而是以页为单位。一个内存页是一段固定大小的连续的连续内存地址的总称,具体到Linux中,典型的内存页大小为4096 Byte。所以内存地址可以分为页号和页内偏移量。下面以64位机器,4G物理内存,4K页大小为例,虚拟内存地址和物理内存地址的组成如下:

上面是虚拟内存地址,下面是物理内存地址。由于页大小都是4k,所以页内偏移都是用低12位表示,而剩下的高地址表示页号 MMU映射单位并不是字节,而是页,这个映射通过差一个常驻内存的数据结构页表来实现。现在计算机具体的内存地址映射比较复杂,为了加快速度会引入一系列缓存和优化,例如TLB等机制,下面给出一个经过简化的内存地址翻译示意图:

内存页与磁盘页

我们知道一般将内存看做磁盘的缓存,有时MMU在工作时,会发现页表表名某个内存页不在物理内存页不在物理内存中,此时会触发一个缺页异常,此时系统会到磁盘中相应的地方将磁盘页载入到内存中,然后重新执行由于缺页而失败的机器指令。关于这部分,因为可以看做对malloc实现是透明的,所以不再详述 :

真实地址翻译流程:

内存排布:明白了虚拟内存和物理内存的关系及相关的映射机制,下面看一下具体在一个进程内是如何排布内存的。 以Linux 64位系统为例。理论上,64bit内存地址空间为0x0000000000000000-0xFFFFFFFFFFFFFFF,这是个相当庞大的空间,Linux实际上只用了其中一小部分。

具体分布如图所示:

对用户来说主要关心的是User Space。将User Space放大后,可以看到里面主要分成如下几段:

  • Code:这是整个用户空间的最低地址部分,存放的是指令(也就是程序所编译成的可执行机器码) Data:这里存放的是初始化过的全局变量
  • BSS:这里存放的是未初始化的全局变量
  • Heap:堆,这是我们本文主要关注的地方,堆自底向上由低地址向高地址增长
  • Mapping Area:这里是与mmap系统调用相关的区域。大多数实际的malloc实现会考虑通过mmap分配较大块的内存空间,本文不考虑这种情况,这个区域由高地址像低地址增长 Stack:栈区域,自高地址像低地址增长 。
  • Heap内存模型:一般来说,malloc所申请的内存主要从Heap区域分配,来看看Heap的结构是怎样的。

Linux维护一个break指针,这个指针执行堆空间的某个地址,从堆开始到break之间的地址空间为映射好的,可以供进程访问,而从break往上,是未映射的地址空间,如果访问这段空间则程序会报错。

1.4 brk与sbrk

由上文知道,要增加一个进程实际上的可用堆大小,就需要将break指针向高地址移动。Linux通过brk和sbrk系统调用操作break指针。两个系统调用的原型如下:

c++ 复制代码
int brk(void *addr);
void *sbrk(inptr_t increment);

brk将break指针直接设置为某个地址,而sbrk将break从当前位置移动increment所指定的增量brk在执行成功时返回0,否则返回-1并设置为errno为ENOMEM,sbrk成功时返回break移动之前所指向的地址,否则返回(void)-1*;

资源限制和rlimirt

系统为每一个进程所分配的资源不是无限的,包括可映射的空间,因此每个进程有一个rlimit表示当前进程可用的资源上限,这个限制可以通过getrlimit系统调用得到,下面代码获取当前进程虚拟内存空间的rlimit 其中rlimt是一个结构体

c++ 复制代码
struct rlimit
{
    rlimt_t rlim_cur;
    rlim_t rlim_max;
};

每种资源有硬限制和软限制,并且可以通过setrlimit对rlimit进行有条件限制作为软限制的上限,非特权进程只能设置软限制,且不能超过硬限制

二、calloc函数

calloc函数也是与free()函数配套使用的,使用方式与malloc几乎相同,也是在堆区申请动态内存空间。头文件:stdlib.h,返回类型为空指针,size_t num为元素个数,size_t size为每个元素的字节大小。

calloc函数的原型:

c++ 复制代码
void* calloc(size_t num ,size_t size)
2.1 calloc函数的使用
c++ 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	//calloc与malloc的区别
	//1.参数的使用方式不同
	//2.calloc会在返回起始地址之前,把在堆区申请的动态内存空间的每个字节都初始化为0
	int* p=(int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	else
	{
		int i;
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));//0 0 0 0 0 0 0 0 0 0
		}
	}
	//注意要释放calloc申请的那块空间
	//还给操作系统,并把指针置为空
	free(p);
	p = NULL;
	return 0;
}
2.2 calloc与malloc的区别

1.参数的使用方式不同

malloc(单位:字节):malloc(10 * sizeof(int));或malloc(40)
calloc:calloc(10 , sizeof(int))

2.malloc的使用效率较高,因为calloc在返回在堆区申请的那块动态内存的起始地址之前,会将每个字节都初始化为0。

三、realloc函数
3.1 什么是realloc()

realloc()是C库的功能,用于为已分配的内存块增加更多的内存大小。C语言中重新分配的目的是扩展当前的存储块,同时保留原始内容。realloc()函数有助于通过malloc或calloc函数减少先前分配的内存大小。realloc代表内存的重新分配。

在C中realloc的语法:

c++ 复制代码
ptr = realloc (ptr,newsize);

上面的语句在变量newsize中分配具有指定大小的新内存空间。执行完函数后,指针将返回到存储块的第一个字节。新的大小可以大于或小于以前的内存。我们不能确定新分配的块是否将指向与先前存储块相同的位置。

C语言中的realloc函数将在新区域中复制所有先前的数据。它确保数据将保持安全。

例如:

#includeint main () {
   char *ptr;
   ptr = (char *) malloc(10);
   strcpy(ptr, "Programming");
   printf(" %s,  Address = %un", ptr, ptr);

   ptr = (char *) realloc(ptr, 20); //ptr is reallocated with new size
   strcat(ptr, " In 'C'");
   printf(" %s,  Address = %un", ptr, ptr);
   free(ptr);
   return 0;
} 
3.2 如何使用realloc()

下面的C语言程序演示了如何在C语言中使用realloc来重新分配内存。

#include <stdio.h>
#include <stdlib.h>
    int main() {
        int i, * ptr, sum = 0;
        ptr = malloc(100);
        if (ptr == NULL) {
            printf("Error! memory not allocated.");
            exit(0);
        }
        
        ptr = realloc(ptr,500);
    if(ptr != NULL)
           printf("Memory created successfullyn");
           
    return 0;

    }

C示例中的realloc结果:

Memory created successfully

每当重新分配导致操作失败时,它都会返回空指针,并且先前的数据也将被释放。

  • 1、函数原型:void *realloc(void *ptr,size_f size);ptr是指向需要修改的内存块的指针,size是请求修改的大小
  • 2、realloc用来修改已分配的内存块的大小
  • 3、如果调整成功,返回值是调整大小后内存的起始位置;如果失败,则返回NULL,所以要对返回值进行判空。
  • 4、在扩大内存空间时会出现的两种情况:

(1)ptr所指向的内存后有足够的内存空间用来扩展

(2)ptr所指向的内存后没有足够的内存空间用来扩展

则在堆上重新找一个大小合适的连续空间来使用,这样函数返回的是一个新的内存地址。如果新的内存空间申请成功,则会将ptr所指向的内存中的内容拷贝到新的内存空间中,ptr所指向的内存会被释放,返回新的内存地址;如果不成功,ptr所指向的内存不会被释放,函数返回NULL。

  • 5、p = realloc(ptr,size)函数返回值不为空时,释放内存不需要写free(ptr),只需要写free(p)
  • 6、申请的内存空间不会进行初始化
四、free函数
  • 1、用来释放动态开辟的内存
  • 2、函数原型:void free(void *ptr);指针参数是指向malloc等函数动态申请的内存地址,这块内存释放后会返还给堆,虽然指针指向这块区域,但可以重新分配这块数据
  • 3、一般来说,free释放的是最新开辟的一个内存空间如果程序中malloc了,但是没有free,则会造成内存泄漏(即在程序运行过程中,系统会一直被申请内存,造成可用内存不断减少)
  • 4、在free之后,需要将ptr再次置空,即ptr = NULL;,如果不置空,后面程序如果通过ptr会再次访问到已经释放/无效的/已经被回收再利用的内存。
  • 5、free不能同时释放一块内存。

在使用以上函数时,需要加头文件#include <stdlib.h>

将free与malloc函数的联用情况:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
  int* p = (int*)malloc(40);
  int* ptr = p;
  if (ptr == NULL)
  {
  printf("%s\n", strerror(errno));
  return 1;
  }
  //使用
    //自行添加使用的代码!!
  //释放
  free(p);  
  p = NULL;
}

在这个代码中,就是将malloc函数与free函数初步联用!!所以才能更合理的分配内存!!

但是在malloc函数与free函数联用的情况,由于代码的不规范,也会出现或多或少的错误!!

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int test ()
{
  int* p= (int*)malloc(40);
  if (p == NULL)
  {
  printf("%s\n", strerror(errno));
  return 1;
  }
 
  //使用
  if (1)
  {
  //某个成立的条件!
  return 2;
  }
  //释放
  free(p);
  p = NULL;
}//该段代码,存在内存泄露的问题!
int main()
{
  test();
  return 0;

其实,在该段代码中,可能出现内存泄漏的问题!!

原因在于:在该段代码中:

//使用
  if (1)
  {
  //某个成立的条件!
  return 2;
  }

== NULL)

{

printf("%s\n", strerror(errno));

return 1;

}

//使用

if (1)

{

//某个成立的条件!

return 2;

}

//释放

free§;

p = NULL;

}//该段代码,存在内存泄露的问题!

int main()

{

test();

return 0;

其实,在该段代码中,可能出现内存泄漏的问题!!

原因在于:在该段代码中:

//使用

if (1)

{

//某个成立的条件!

return 2;

}

如果条件成立,直接返回该值,但并不会继续执行代码,导致,后续的释放(//释放 free(p); p = NULL;)出现问题!
相关推荐
宅小海9 分钟前
scala String
大数据·开发语言·scala
qq_3273427312 分钟前
Java实现离线身份证号码OCR识别
java·开发语言
锅包肉的九珍13 分钟前
Scala的Array数组
开发语言·后端·scala
心仪悦悦16 分钟前
Scala的Array(2)
开发语言·后端·scala
yqcoder39 分钟前
reactflow 中 useNodesState 模块作用
开发语言·前端·javascript
baivfhpwxf20231 小时前
C# 5000 转16进制 字节(激光器串口通讯生成指定格式命令)
开发语言·c#
许嵩661 小时前
IC脚本之perl
开发语言·perl
长亭外的少年1 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
直裾1 小时前
Scala全文单词统计
开发语言·c#·scala
心仪悦悦1 小时前
Scala中的集合复习(1)
开发语言·后端·scala