C++基础(四) —— 内存分配


概念

物理地址内存的分配与释放

主要采用链表结构

使用了一个名叫page的结构体管理物理内存,结构体中包括了页的大小、页的状态以及指向相邻页的指针。

Linux内核使用这些指针来构建了一个逻辑链表,当需要分配内存的时候,会从链表中查找第一个空闲页并把它标记为已使用。

释放内存的时候,会把相应的页标记为空闲,并把它插入到链表对应的位置

虚拟用户进程空间内存的分配与释放

C++语言层次
new delete
智能指针 栈上的对象出作用域自动析构 自动管理内存的分配与释放

C语言层次
malloc free
malloc() 分配的是虚拟内存。
如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。

系统调用
sbrk() brk() mmap()
管理进程的堆(heap)空间。

问:什么场景下 malloc() 会通过 brk() 分配内存?又是什么场景下通过 mmap() 分配内存?
malloc() 源码里默认定义了一个阈值:
如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;

allocator模板类

#include <iostream>
#include <memory>

int main() {
    std::allocator<int> allocator;

    // 在堆上动态的分配大小为5*sizeof(int)的内存
    int* ptr = allocator.allocate(5);
    int* ptrnum = new int[5];
    int abc[5];  // abc也是指针

    // 构造对象
    for (int i = 0; i < 5; ++i) {
        allocator.construct(ptr + i, i);
        allocator.construct(ptrnum + i, i);
        allocator.construct(abc + i, i);
    }

    // 访问对象
    for (int i = 0; i < 5; ++i) {
        std::cout << ptr[i] << " ";
        std::cout << ptrnum[i] << " ";
        std::cout << abc[i] << " ";
    }
    std::cout << std::endl;

    // 销毁对象
    for (int i = 0; i < 5; ++i) {
        allocator.destroy(ptr + i);
        allocator.destroy(ptrnum + i);  
    }

    // 释放内存
    allocator.deallocate(ptr, 5);
    delete ptrnum;
    ptrnum = nullptr;

    return 0;
}

new delete

堆上分配内存

T* ptr = new T; // 分配单个对象的内存并构造对象
T* arr = new T[N]; // 分配对象数组的内存并构造对象
delete ptr; // 释放单个对象的内存并调用析构函数
delete[] arr; // 释放对象数组的内存并调用每个对象的析构函数

new 运算符在堆上分配的内存可以通过相应的 delete 运算符来释放,从而销毁对象并释放内存。

动态:
为了简化内存管理,C++11 引入了智能指针(如 std::shared_ptr 和 std::unique_ptr),它们提供了更安全和更方便的内存管理机制。智能指针可以自动管理动态分配的内存,避免显式使用 delete,从而减少了内存泄漏和资源管理的错误。

malloc free

堆上分配内存
void* malloc(size_t size);
malloc() 返回一个指向分配内存块的指针,该内存块大小为 size 字节。分配的内存块在堆上连续存储,可以手动管理其使用和释放。

void free(void* ptr);
free输入的是指向内存块的指针

问1:malloc(1) 会分配多大的虚拟内存
malloc() 在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。
具体会预分配多大的空间,跟 malloc 使用的内存管理器有关系,我们就以 malloc 默认的内存管理器(Ptmalloc2)来分析。

#include <stdio.h>
#include <malloc.h>

int main() {
  printf("使用cat /proc/%d/maps查看内存分配\n",getpid());
  
  //申请1字节的内存
  void *addr = malloc(1);
  printf("此1字节的内存起始地址:%x\n", addr);
  printf("使用cat /proc/%d/maps查看内存分配\n",getpid());
 
  //将程序阻塞,当输入任意字符时才往下执行
  getchar();

  //释放内存
  free(addr);
  printf("释放了1字节的内存,但heap堆并不会释放\n");
  
  getchar();
  return 0;
}

程序输出:
此1字节的内存起始地址d73010

之后,使用cat /proc/…/maps查看内存分布情况。我在 maps 文件通过此 1 字节的内存起始地址过滤出了内存地址的范围。

[root@xiaolin ~]# cat /proc/3191/maps | grep d730
00d73000-00d94000 rw-p 00000000 00:00 0                                  [heap]

可以看到,堆空间的内存地址范围是 00d73000-00d94000,这个范围大小是 132KB,也就说明了 malloc(1) 实际上预分配 132K 字节的内存。
但是程序里打印的内存起始地址是 d73010,而 maps 文件显示堆内存空间的起始地址是 d73000,为什么会多出来 0x10 (16字节)呢?这个问题在问2中。

问2:free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?
malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节,这个多出来的 16 字节就是保存了该内存块的描述信息,比如有该内存块的大小。
在这里插入图片描述

这样当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。

strcpy 与 memcpy 与 memset

内存数据拷贝函数
strcpy 和 memcpy 是 C 语言中的库函数,用于内存数据的拷贝操作。它们有不同的使用方式:
memset 是 C 语言中的库函数,用于将一块内存区域设置为指定的值

strcpy是提供了对字符串的复制,memcpy是内存的复制,对复制的内容没有限制,使用范围更广!!!
strcpy和memcpy主要有以下3方面的区别。
复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。
复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。
用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。

strcpy

char* strcpy(char* dest, const char* src);
strcpy 用于将一个以 null 结尾的字符串从源地址 src 复制到目标地址 dest,并返回目标地址的指针。
实例

char source[] = "Hello, World!";
char destination[20];
strcpy(destination, source);

memcpy

void* memcpy(void* dest, const void* src, size_t n);
memcpy 用于将源地址 src 的前 n 字节的数据复制到目标地址 dest,无返回值。
示例:

int source[] = {1, 2, 3, 4, 5};
int destination[5];
memcpy(destination, source, sizeof(source));

需要注意的是,使用这两个函数时,需要确保目标地址 dest 具有足够的空间来容纳要复制的数据。

一个数据报文的构成实例:

   char buffer[kBufferSize];
    memset(buffer, 0, sizeof(buffer));

    // Header
    buffer[0] = 0x5A;
    buffer[1] = 0xA5;

    // CMD_ID
    const char* CMD_ID = "00000000000000001";
    memcpy(buffer+2, CMD_ID, strlen(CMD_ID));

    // Frame_Type
    buffer[19] = 0xD1;

    // Packet_Type
    buffer[20] = 0x01;

    // Frame_No
    buffer[21] = 0x01;

    // Sub_Packet_Type
    buffer[22] = 0x00;
    buffer[23] = 0x00;

    // Time_Stamp
    srand(time(NULL));
    int time_stamp = rand() % 1000000;
    memcpy(buffer+24, &time_stamp, sizeof(int));

    // X
    float x = 123.456f;
    memcpy(buffer+28, &x, sizeof(float));

    // Y
    float y = 789.012f;
    memcpy(buffer+32, &y, sizeof(float));

    // Z
    float z = 345.678f;
    memcpy(buffer+36, &z, sizeof(float));

    // Version
    int version = 1;
    memcpy(buffer+40, &version, sizeof(int));

    // CRC16
    uint16_t crc = 0;
    for (int i = 0; i < 42; i++) {
        crc += (uint8_t)buffer[i];
    }
    memcpy(buffer+42, &crc, sizeof(uint16_t));

    // End
    buffer[44] = 0x96;

memset

memset 是 C 语言中的库函数,用于将一块内存区域设置为指定的值
void* memset(void* ptr, int value, size_t num);
memset 将指针 ptr 指向的内存区域的前 num 字节都设置为值 value。它返回指向 ptr 的指针。
它可以用来快速地将一块内存区域设置为特定的值,例如将数组全部设置为零或将某个标记数组全部设置为特定的标记值。

示例:

int array[5];
memset(array, 0, sizeof(array));  // 将数组全部设置为零


char str[10];
memset(str, 'A', sizeof(str));  // 将 str 数组的每个元素都设置为字符 'A'
for (int i = 0; i < sizeof(str); i++) printf("%c ", str[i]);

需要注意的是,memset 的参数 value 是一个整数,会被解释为无符号字符。因此,如果需要将内存区域设置为非零的特定值,需要确保该值在无符号字符的范围内。

在 C++ 中,也可以使用 std::fill 算法或使用初始化语法来实现相似的功能,以提供更安全和易用的方式来初始化和设置内存。

内存泄露

首先,在C++中,我们通常将内存分为三个主要的部分:data段、heap堆和stack栈。
Data段:data段是存储全局变量和静态变量的区域。这些变量在程序的整个执行过程中都存在,并且在程序启动时就会被分配内存。data段在程序的内存布局中通常是静态分配的一部分。

Heap堆:heap堆是用于动态分配内存的区域。在堆上分配的内存可以在程序运行时动态地进行分配和释放。使用动态内存分配的方式,如使用new和malloc,可以在堆上分配对象或数据,并在不需要时手动释放。堆上的内存分配和释放需要显式地管理,如果没有正确释放分配的内存,就会产生内存泄漏。

Stack栈:stack栈是用于存储局部变量和函数调用的区域。每当函数被调用时,栈会分配一块内存用于存储函数的局部变量和函数参数。当函数执行结束时,栈会自动释放这些内存。栈上的内存分配和释放是自动管理的,无需手动干预。
需要注意的是,递归函数的每一层调用都会在栈上创建一个新的栈帧,而递归函数的嵌套层数过多可能导致栈空间的耗尽。当递归的深度过大时,可能会发生栈溢出错误(Stack Overflow Error),因为栈空间是有限的。

说回内存泄露,在 C++ 中,内存泄露(Memory Leak)是指程序在运行过程中未能正确释放已经分配的内存,导致这部分内存无法再被程序所访问和回收的情况。

内存泄露通常发生在以下情况下:

  • 忘记释放动态分配(堆区)的内存:在使用 new 或 malloc 分配内存后,没有使用对应的 delete 或 free 进行释放。
  • 误用指针导致内存无法释放:指针被错误地重复赋值,导致原先分配的内存无法被释放。
  • 异常导致资源清理不完整:在异常抛出的情况下,没有正确处理分配的内存,导致内存泄露。

解决方案

  • 显式释放内存:在使用 new 或 malloc 动态分配内存后,确保在不再需要时调用 delete 或 free 来显式释放内存。这是最基本的解决内存泄露问题的方法。确保每个动态分配的内存都有相应的释放操作。

  • 智能指针(Smart Pointers):使用智能指针类(如 std::unique_ptr、std::shared_ptr)来管理动态分配的内存。智能指针可以自动管理内存的生命周期,并在不再需要时自动释放内存。使用智能指针可以避免手动调用 delete 或 free,减少内存泄露的风险。

补充,内存管理不当的情况

  • 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
  • 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
  • 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。

内存池

内存池原理
程序可以通过系统的内存分配方法预先分配一大块内存来做一个内存池,之后程序的内存分配和释放都由这个内存池来进行操作和管理,当内存池不足时再向系统申请内存。

我们通常使用malloc等函数来为用户进程分配内存。它的执行过程通常是由用户程序发起malloc申请内存的动作,在标准库找到对应函数,对不满128k的调用brk()系统调用来申请内存(申请的内存是堆区内存),接着由操作系统来执行brk系统调用。

我们知道malloc是在标准库,真正的申请动作需要操作系统完成。所以由应用程序到操作系统就需要3层。==内存池是专为应用程序提供的专属的内存管理器,它属于应用程序层。==所以程序申请内存的时候就不需要通过标准库和操作系统,明显降低了开销。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

秋雨qy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值