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

这篇具有很好参考价值的文章主要介绍了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 字节就是保存了该内存块的描述信息,比如有该内存块的大小。
C++基础(三) —— 内存分配

这样当执行 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层。==内存池是专为应用程序提供的专属的内存管理器,它属于应用程序层。==所以程序申请内存的时候就不需要通过标准库和操作系统,明显降低了开销。文章来源地址https://www.toymoban.com/news/detail-476742.html

到了这里,关于C++基础(三) —— 内存分配的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • [Linux]环境变量 进程地址空间(虚拟内存与物理内存的关系)

    hello,大家好,这里是bang_bang,今天我们来讲一下语言层级上的程序地址空间和系统层级上的进程地址空间的区别,在下面中我举的例子会设计到环境变量,所以开篇我先讲讲环境变量。 目录 1️⃣环境变量 🍙 基本概念 🍙环境变量相关命令 🍥查看环境变量echo 🍥添加全局环

    2024年02月15日
    浏览(56)
  • Linux内存管理 | 四、物理地址空间设计模型

    我的圈子: 高级工程师聚集地 我是董哥,高级嵌入式软件开发工程师,从事嵌入式Linux驱动开发和系统开发,曾就职于世界500强企业! 创作理念:专注分享高质量嵌入式文章,让大家读有所得! 前面几篇文章,主要讲解了虚拟内存空间的布局和管理,下面同步来聊聊物理内

    2024年02月08日
    浏览(52)
  • 2.3.1操作系统-存储管理:页式存储、逻辑地址、物理地址、物理地址逻辑地址之间的地址关系、页面大小与页内地址长度的关系、缺页中断、内存淘汰规则

    在存储管理当中,操作系统会负责将外存的一些文件调入到内存当中,以便给CPU调用,如果调用的内容不在内存当中,那么会产生一种中断,叫做缺页中断。然后从外存调数据,调完数据再返回,接着访问之前的断点部分。 在调用的过程当中,如果是一个几十G的文件,调入

    2024年02月03日
    浏览(47)
  • C++——内存分配与动态内存管理

    🌸作者简介: 花想云 ,在读本科生一枚,致力于 C/C++、Linux 学习。 🌸 本文收录于 C++系列 ,本专栏主要内容为 C++ 初阶、C++ 进阶、STL 详解等,专为大学生打造全套 C++ 学习教程,持续更新! 🌸 相关专栏推荐: C语言初阶系列 、 C语言进阶系列 、 数据结构与算法 本章我们

    2023年04月17日
    浏览(56)
  • c++ 内存管理一:初识内存分配工具

    前言 侯捷 c++内存管理学习总结笔记。 在C++中,有几种常用的内存分配工具可以帮助进行动态内存管理。 从c++应用程序自上而下,通常会有这样的几种分配内存的方式,当然最终都是直接或间接的调用系统的API。 1 new 和 delete new 和 delete :new操作符用于在堆上分配内存,de

    2024年02月11日
    浏览(45)
  • 『Linux』第九讲:Linux多线程详解(一)_ 线程概念 | 线程控制之线程创建 | 虚拟地址到物理地址的转换

    「前言」文章是关于Linux多线程方面的知识,讲解会比较细,下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句」  我与春风皆过客, 你携秋水揽星河。 ——网络流行语,诗词改版 用现在的话来说:我不

    2024年02月04日
    浏览(47)
  • C++中内存的分配

    一个由C/C++编译的程序占用的内存分为以下几个部分     1、栈区(stack)—   由编译器自动分配释放   ,存放函数的参数值,局部变量的值等。   2、堆区(heap)   —   一般由程序员分配释放,   若程序员不释放,程序结束时可能由OS回收。    3、全局区(静态区)

    2024年02月10日
    浏览(36)
  • C++ 指针进阶:动态分配内存

    malloc 是 stdlib.h 库中的函数,原型为 void *__cdecl malloc(size_t _Size); : 作用 : malloc 函数沿空闲链表(位于内存 堆空间 中)申请一块满足需求的内存块,将所需大小的内存块分配给用户剩下的返回到链表上; 并返回指向该内存区的首地址的指针,意该指针的类型为 void * ,因此

    2024年02月05日
    浏览(46)
  • C++类和动态内存分配

    C++能够在程序运行时决定内存的分配,而不是只在编译阶段,因此,就可以根据程序的需要,而不是根据一系列严格的存储类型规则来使用内存,C++使用new和delete运算符来动态控制内存,但是,在类中使用这些运算符会导致许多新的问题,在这种情况下,析构函数就是必不可

    2024年04月16日
    浏览(43)
  • C++ 类的内存分配是怎么样的?

    首先通过一段代码来引入动态内存分配的主题。一个名为 StringBad 的类以及一个功能更强大的 String 类。 介绍一下这些定义,第一个char指针来表示一段字符串,这就意味着类声明没有为字符串本身分配存储空间。而是要在构造函数里通过new来为字符串分配空间,这就避免了在

    2024年03月24日
    浏览(40)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包