IE盒子

搜索
查看: 135|回复: 0

全面理解C++指针和内存管理(三)

[复制链接]

5

主题

14

帖子

29

积分

新手上路

Rank: 1

积分
29
发表于 2023-4-19 19:50:21 | 显示全部楼层 |阅读模式
内存池是一种用于管理内存的高效技术,它可以在程序启动时一次性地分配一块大内存,在程序运行过程中反复使用这块内存,避免了频繁地申请和释放内存,从而提高了程序的性能。

内存池的实现可以采用链表、数组等数据结构,内存池中的内存块可以被分配给对象或其他数据结构使用,当内存块不再被使用时,可以将其返回到内存池中,以便后续的分配。
下面是一个简单的内存池实现的例子:
#include <iostream>#include <vector>class MemoryPool{public:    MemoryPool(std::size_t blockSize, std::size_t blockCount)        : m_blockSize(blockSize), m_blockCount(blockCount)    {        m_memory.resize(m_blockSize * m_blockCount);        for (std::size_t i = 0; i < m_blockCount; ++i)        {            void* p = &m_memory[i * m_blockSize];            m_freeList.push_back(p);        }    }    void* allocate(std::size_t size){        if (size > m_blockSize)            return nullptr;        if (m_freeList.empty())            return nullptr;        void* p = m_freeList.back();        m_freeList.pop_back();        return p;    }    void deallocate(void* p){        m_freeList.push_back(p);    }private:    std::size_t m_blockSize;    std::size_t m_blockCount;    std::vector<char> m_memory;    std::vector<void*> m_freeList;};class MyClass{public:    MyClass(int x, int y) : m_x(x), m_y(y) {}    int getX() const { return m_x; }    int getY() const { return m_y; }private:    int m_x;    int m_y;};int main(){    MemoryPool pool(sizeof(MyClass), 10);    MyClass* p1 = new(pool.allocate(sizeof(MyClass))) MyClass(1, 2);    MyClass* p2 = new(pool.allocate(sizeof(MyClass))) MyClass(3, 4);    std::cout << p1->getX() << ", " << p1->getY() << std::endl;    std::cout << p2->getX() << ", " << p2->getY() << std::endl;    pool.deallocate(p1);    pool.deallocate(p2);    return 0;}上面的程序定义了一个MemoryPool类,它可以用于管理大小为blockSize的内存块,总共有blockCount个内存块。程序创建了一个MemoryPool对象,然后使用allocate()函数分配内存块,并使用deallocate()函数将内存块返回到内存池中。程序还定义了一个MyClass类,它包含两个整型成员变量m_x和m_y,并在内存池中分配了两个MyClass对象。1. 内存分配器
内存分配器是一种用于管理内存的高效技术,它可以为程序中的对象和数据结构动态地分配和释放内存,并确保内存的合理利用,避免了内存泄漏和内存碎片等问题,提高了程序的性能。
C++ STL提供了多种内存分配器,其中最常用的是std::allocator。std::allocator是一个模板类,它可以为任意类型的对象分配内存,并在对象被销毁时自动释放内存。下面是一个使用std::allocator的例子:
#include <iostream>#include <vector>int main(){    std::allocator<int> alloc;    int* p = alloc.allocate(10); // 分配10个int类型的内存空间    for (int i = 0; i < 10; ++i)        alloc.construct(&p, i); // 在分配的内存空间中构造10个int对象    for (int i = 0; i < 10; ++i)        std::cout << p << " ";    std::cout << std::endl;    for (int i = 0; i < 10; ++i)        alloc.destroy(&p); // 销毁10个int对象    alloc.deallocate(p, 10); // 释放10个int类型的内存空间    return 0;}上面的程序创建了一个std::allocator<int>对象,使用allocate()函数分配了10个int类型的内存空间,使用construct()函数在内存空间中构造了10个int对象,使用destroy()函数销毁了10个int对象,最后使用deallocate()函数释放了10个int类型的内存空间。除了std::allocator,C++ STL还提供了其他内存分配器,如std::scoped_allocator_adaptor、std::pmr::polymorphic_allocator等。这些内存分配器具有不同的特点和用途,可以根据具体的需求选择适当的内存分配器。
2. 智能指针
智能指针是一种可以自动管理动态内存的指针,可以避免手动管理内存的复杂性和错误,从而提高程序的安全性和可靠性。
C++ 11引入了智能指针的概念,它们可以自动管理内存的分配和释放,并提供了一些方便的方法来访问指针所指向的对象,如operator->、operator*等。
C++ STL提供了多种智能指针,其中最常用的是std::unique_ptr和std::shared_ptr。
std::unique_ptr是一个独占式的智能指针,它拥有指向对象的唯一所有权,即只能由一个std::unique_ptr对象管理同一个对象。当std::unique_ptr对象销毁时,它会自动释放它所拥有的对象,并确保不会出现内存泄漏。
#include <memory>#include <iostream>int main() {  // 创建一个unique_ptr,管理一个int类型的对象  std::unique_ptr<int> p(new int(42));  std::cout << *p << std::endl; // 输出42  // unique_ptr不允许多个对象管理同一个内存  // std::unique_ptr<int> q = p; // 错误:拷贝构造函数被删除  // 通过std::move转移对象所有权  std::unique_ptr<int> q = std::move(p);  std::cout << *q << std::endl; // 输出42  // p不再管理对象,所以p指向nullptr  if (p == nullptr) {    std::cout << "p is nullptr" << std::endl;  }  // q在销毁时会自动释放它所管理的对象  return 0;}std::shared_ptr是一个共享式的智能指针,它可以被多个std::shared_ptr对象管理同一个对象,并且在最后一个std::shared_ptr对象销毁时,它会自动释放它所拥有的对象。
#include <memory>#include <iostream>int main() {  // 创建一个shared_ptr,管理一个int类型的对象  std::shared_ptr<int> p(new int(42));  std::cout << *p << std::endl; // 输出42  // 可以多个shared_ptr对象管理同一个内存  std::shared_ptr<int> q = p;  std::cout << *q << std::endl; // 输出42  // 输出对象被多少个shared_ptr对象管理  std::cout << "p.use_count() = " << p.use_count() << std::endl; // 输出2  // q在销毁时,对象引用计数减1  // 因为p也管理同一个对象,所以对象并没有被销毁  return 0;}除了std::unique_ptr和std::shared_ptr,C++ STL还提供std::weak_ptr是一个弱引用的智能指针,它可以观测一个std::shared_ptr所管理的对象,但不会增加对象的引用计数。当最后一个std::shared_ptr对象销毁后,弱引用将变得无效。#include <memory>#include <iostream>int main() {  std::shared_ptr<int> p(new int(42));  // 创建一个weak_ptr观测p所管理的对象  std::weak_ptr<int> q = p;  std::cout << *p << std::endl; // 输出42  std::cout << *q.lock() << std::endl; // 输出42  // 输出对象被多少个shared_ptr对象管理  std::cout << "p.use_count() = " << p.use_count() << std::endl; // 输出1  // 销毁p所管理的对象  p.reset();  // weak_ptr观测的对象已经不存在,q.lock()返回nullptr  if (q.lock() == nullptr) {    std::cout << "q is nullptr" << std::endl;  }  return 0;}3. 内存泄漏
内存泄漏指的是程序在运行过程中分配了内存,但没有及时释放,导致内存资源得不到释放,最终耗尽系统的内存资源,使得系统运行缓慢,甚至崩溃。
在使用动态内存分配时,如果不恰当地使用指针,就会导致内存泄漏。例如,在使用new分配内存时,如果忘记使用delete释放内存,就会出现内存泄漏的情况。
为了避免内存泄漏,可以使用智能指针来管理动态内存的分配和释放,或者遵循以下一些内存管理的最佳实践:

  • 及时释放不再使用的内存,避免不必要的内存占用;
  • 在动态内存分配和释放时使用 RAII(资源获取即初始化)技术,通过构造函数和析构函数自动管理内存;
  • 在使用指针时遵循规范,确保每个new操作都有一个对应的delete操作,避免内存泄漏的发生。
小结
C++的指针和内存管理是 C++ 编程中必须掌握的基础知识。指针提供了一种灵活的内存访问方式,但也带来了指针悬空、野指针等问题。为了保证内存的安全性和可靠性,需要合理地使用指针,并且使用智能指针、RAII等技术来自动管理动态内存的分配和释放。同时,还需要注意内存泄漏等问题,遵循内存管理的最佳实践,保证程序的性能够有效利用指针和管理内存是 C++ 程序员的重要技能之一,下面再介绍一些与指针和内存管理相关的高级话题。
4. 指针与多线程
在多线程编程中,指针的使用需要特别小心,因为不同线程之间的内存是共享的,如果没有正确处理,就可能导致竞争条件和数据竞争等问题。
指针的并发使用主要存在以下几个问题:

  • 竞争条件:多个线程对同一个指针进行读写操作,导致结果与预期不符;
  • 空悬指针:一个指针所指向的内存被释放后,另一个线程还在使用该指针,导致访问非法内存;
  • 数据竞争:多个线程对同一个变量进行读写操作,导致结果与预期不符。
为了避免这些问题,可以采用以下几种方法:

  • 在使用指针时,采用读写锁、互斥锁、信号量等机制来保证多个线程对指针的访问顺序和互斥性;
  • 使用原子操作来对指针进行读写操作,保证操作的原子性和线程安全性;
  • 避免在多个线程之间共享指针,使用局部变量或线程安全的容器等机制来保证数据的安全性。
5. 智能指针与循环引用
智能指针是一种强大的工具,可以帮助我们自动管理动态内存的分配和释放。但是,当出现循环引用的情况时,智能指针的引用计数就会出现问题,导致内存泄漏。
循环引用是指两个或多个对象之间相互持有对方的智能指针,形成了一个环形引用结构。在这种情况下,如果没有采取特殊的处理方式,就会出现内存泄漏的问题。
例如,下面的代码中,对象A和B相互持有对方的智能指针,导致它们的引用计数都不为0,最终导致内存泄漏。
#include <memory>class A;class B;class A {public:    std::shared_ptr<B> b;};class B {public:    std::shared_ptr<A> a;};int main() {    std::shared_ptr<A> a(new A);    std::shared_ptr<B> b(new B);    a->b = b;    b->a = a;    return 0;}为了避免循环引用导致的内存泄漏,可以采用以下几种方法:

  • 将智能指针转换为弱引用(std::weak_ptr)来解决循环引用问题。弱引用不会增加引用计数,当引用计数为0时,自动释放指针所指向的内存。
  • 使用 std::shared_ptr 的自定义删除器(deleter)来手动释放内存。删除器是一个函数对象,当智能指针引用计数为0时,会调用该函数来释放内存。例如,可以定义一个删除器,当智能指针引用计数为0时,先释放 B 对象的智能指针,再释放 A 对象的智能指针,从而避免循环引用的问题。
  • 使用智能指针的环形引用解决方案,例如 boost::intrusive_ptr 或 std::enable_shared_from_this 等。这些方案都是通过增加引用计数的方式来解决循环引用问题的。

6. 内存池
内存池是一种管理内存的高效方式,它通过预分配一定数量的内存块,然后动态分配这些内存块来避免频繁的内存分配和释放操作。内存池可以减少内存碎片和提高内存分配的效率,特别是在程序需要频繁分配和释放内存时,内存池的效果更加显著。
下面是一个简单的内存池实现示例:
#include <vector>#include <mutex>class MemoryPool {public:    MemoryPool(size_t block_size, size_t block_count)        : block_size_(block_size), block_count_(block_count) {        blocks_.reserve(block_count_);        for (size_t i = 0; i < block_count_; ++i) {            blocks_.push_back(new char[block_size_]);        }    }    ~MemoryPool() {        for (auto block : blocks_) {            delete[] block;        }    }    void* allocate() {        std::lock_guard<std::mutex> lock(mutex_);        if (!free_blocks_.empty()) {            void* block = free_blocks_.back();            free_blocks_.pop_back();            return block;        }        return new char[block_size_];    }    void deallocate(void* block) {        std::lock_guard<std::mutex> lock(mutex_);        free_blocks_.push_back(block);    }private:    size_t block_size_;    size_t block_count_;    std::vector<char*> blocks_;    std::vector<void*> free_blocks_;    std::mutex mutex_;};这个内存池的实现方式很简单,它使用一个 std::vector 来保存预分配的内存块,使用一个 std::vector 来保存空闲的内存块。在分配内存时,如果存在空闲的内存块,则从空闲块中取出一个,否则就动态分配一个新的内存块。在释放内存时,将内存块添加到空闲块列表中即虚拟内存。7.  虚拟内存虚拟内存是操作系统中一个重要的概念,它将物理内存和进程中使用的逻辑地址分离开来,让每个进程都可以独立地访问一块连续的内存空间,而不必考虑物理内存的具体位置。
在虚拟内存中,每个进程都有自己的地址空间,地址空间通常被分为几个部分,包括代码段、数据段、堆、栈等。虚拟内存使用分页机制来实现,将进程的逻辑地址划分为固定大小的页(通常为4KB),每个页都有一个对应的物理页帧(也为4KB)。进程的逻辑地址空间被映射到物理地址空间,当进程需要访问某个逻辑地址时,操作系统会将其映射到对应的物理地址。
虚拟内存的主要好处是它可以让进程使用比物理内存更大的地址空间。当进程需要访问一个逻辑地址,而该地址所对应的物理页不在内存中时,操作系统会触发一个缺页异常(page fault),将该页从磁盘中读入内存,然后再将逻辑地址映射到物理地址。虚拟内存还可以提高系统的安全性,因为每个进程都有自己独立的地址空间,不会相互干扰。
8. 垃圾回收
垃圾回收是一种自动内存管理技术,它通过自动识别不再被使用的内存对象,并自动释放它们来减轻程序员的负担。垃圾回收可以有效地避免内存泄漏和悬挂指针等问题,特别是在大型软件系统中,手动管理内存会变得非常困难。
垃圾回收的实现方式有很多种,其中最常见的是基于引用计数的垃圾回收算法。引用计数算法使用一个计数器来记录每个对象被引用的次数,当计数器为0时,该对象就可以被回收。例如,在 C++ 中,智能指针(如 std::shared_ptr)就是基于引用计数的垃圾回收机制,它会自动跟踪引用计数,并在引用计数为0时自动释放内存。
除了引用计数算法,还有其他的垃圾回收算法,如标记-清除算法、标记-复制算法、分代算法等。这些算法都有各自的优缺点,选择哪种算法取决于具体应用场景和需求。
9. C++11 的智能指针
C++11 引入了三种智能指针,分别是 std::unique_ptr、std::shared_ptr 和 std::weak_ptr。这些智能指针可以自动管理动态分配的内存,并且能够避免内存泄漏和悬挂指针等问题。
std::unique_ptr 是一种独占型智能指针,它拥有对动态分配的对象的唯一所有权。当 std::unique_ptr 被销毁时,它会自动释放内存。std::unique_ptr 可以通过 std::move 函数进行移动,从而实现所有权的转移。
std::shared_ptr 是一种共享型智能指针,它可以被多个 std::shared_ptr 对象共享拥有权。当最后一个 std::shared_ptr 对象被销毁时,它会自动释放内存。std::shared_ptr 内部维护了一个引用计数,用于记录当前有多少个 std::shared_ptr 对象共享该对象。std::shared_ptr 还支持自定义删除器(deleter),用于在释放内存时执行自定义的操作。
std::weak_ptr 是一种弱型智能指针,它不会增加引用计数,也不会影响对象的生命周期。std::weak_ptr 可以通过 std::shared_ptr 来创建,当需要访问对象时,可以通过 std::weak_ptr 调用 std::lock 方法来获取一个 std::shared_ptr 对象,如果对象已经被释放,则返回一个空的 std::shared_ptr 对象。
这些智能指针可以极大地简化内存管理的工作,但也需要注意它们的使用和适当的选择。在使用 std::shared_ptr 时,应注意避免循环引用(circular reference),即两个或多个对象相互引用,导致引用计数永远不为0,从而导致内存泄漏。在这种情况下,可以使用 std::weak_ptr 来打破循环引用。
10. C++ 的内存池
内存池是一种预先分配一定数量的内存块,并在需要时分配给程序使用的技术。C++ 中可以通过自定义内存分配器来实现内存池,从而提高程序的效率。
一般来说,内存池可以分为静态内存池和动态内存池两种类型。静态内存池在程序启动时就会预先分配好一定数量的内存块,并在程序运行期间不再新增或释放内存。动态内存池则可以根据程序的需要动态地分配或释放内存块。
在 C++ 中,可以通过重载 new 和 delete 运算符来自定义内存分配器。在自定义的分配器中,可以使用 malloc 或 new 等系统调用来分配内存块,并使用一个类似于链表的结构来管理内存块的分配和释放。
下面是一个简单的内存池示例:
class MemoryPool {public:    MemoryPool(size_t block_size, size_t block_count);    ~MemoryPool();    void* allocate();    void deallocate(void* ptr);private:    struct Block {        Block* next;    };    size_t block_size_;    size_t block_count_;    char* data_;    Block* head_;};MemoryPool::MemoryPool(size_t block_size, size_t block_count)    : block_size_(block_size), block_count_(block_count) {    data_ = new char[block_size * block_count];    head_ = reinterpret_cast<Block*>(data_);    Block* p = head_;    for (size_t i = 1; i < block_count; ++i) {        p->next = reinterpret_cast<Block*>(data_ + i * block_size);        p = p->next;    }    p->next = nullptr;}MemoryPool::~MemoryPool() {    delete[] data_;}void* MemoryPool::allocate() {    if (head_ == nullptr) {        return nullptr;    }    Block* p = head_;    head_ = head_->next;    return p;}void MemoryPool::deallocate(void* ptr) {    if (ptr == nullptr) {        return;    }    Block* p = static_cast<Block*>(ptr);    p->next = head_;    head_ = p;}这个示例中,MemoryPool 类用于管理内存池,其中 allocate 方法用于分配内存块,deallocate 方法用于释放内存块。Block 结构体用于维护链表结构。
11. C++ 的内存泄漏检测工具
内存泄漏是一个常见的问题,在大型的 C++ 项目中尤为突出。C++ 中可以使用一些内存泄漏检测工具来帮助开发者找出内存泄漏的原因。
常用的内存泄漏检测工具包括:

  • Valgrind:一种开源的内存泄漏检测工具,支持多种操作系统和 CPU架构,能够检测 C++ 程序的内存使用情况、线程问题和其他错误。Valgrind 包含多个工具,其中最常用的是 Memcheck 工具,可以检测出内存泄漏和访问已释放内存的错误。
  • AddressSanitizer:一种由 Google 开发的工具,可以检测内存泄漏、缓冲区溢出、使用未初始化的内存等问题。AddressSanitizer 是在编译时通过对二进制代码的修改来实现的,因此需要使用特定的编译器和链接器。
  • LeakSanitizer:一种由 Google 开发的工具,专门用于检测内存泄漏问题。与 AddressSanitizer 类似,LeakSanitizer 是在编译时通过对二进制代码的修改来实现的。
  • Visual Leak Detector:一种专门用于检测 Windows 平台上内存泄漏问题的工具,可以与 Visual Studio 集成使用。Visual Leak Detector 可以检测出由于未正确释放内存而导致的内存泄漏问题。
  • Purify:一种商业软件,由 IBM 公司开发,可以在多种操作系统和 CPU 架构上使用。Purify 可以检测出内存泄漏、内存越界、使用未初始化的内存等问题。
这些工具都可以在程序运行期间对内存使用情况进行监控和分析,从而帮助开发者找出内存泄漏的原因。不同工具的实现原理和使用方法略有不同,具体使用时需要根据自己的需求进行选择。
除了使用工具检测内存泄漏,还有一些编程技巧可以帮助避免内存泄漏,比如:

  • 使用 RAII(Resource Acquisition Is Initialization)技术:RAII 是一种 C++ 编程技巧,利用对象的构造和析构函数,在对象构造时获取资源,在对象析构时释放资源,从而避免资源泄漏。常见的 RAII 实现包括智能指针、文件句柄类等。
  • 注意对象的生命周期:在编写程序时,需要仔细考虑对象的生命周期,确保对象的析构函数能够在适当的时候被调用。比如,当使用 new 操作符分配内存时,应该在不再使用该对象时手动调用 delete 操作符释放内存,而不是将内存泄漏到程序结束时由操作系统回收。
  • 使用 STL 容器:STL 容器(如 vector、list、map 等)是 C++ 标准库中提供的高级数据结构,具有自动管理内存的功能。使用 STL 容器可以避免手动管理内存,从而减少内存泄漏的风险。
  • 使用智能指针:智能指针是一种特殊的指针类,能够自动管理对象的生命周期。C++11 标准引入了两种智能指针:std::unique_ptr 和 std::shared_ptr。其中,std::unique_ptr 只能拥有一个指向对象的指针,std::shared_ptr 可以拥有多个指向同一个对象的指针。使用智能指针可以避免手动管理内存,从而减少内存泄漏的风险。
总之,内存泄漏是一种常见的编程错误,可以使用工具和编程技巧进行检测和避免。开发者需要对内存管理的相关知识有深入的了解,并在编写代码时时刻注意内存使用情况,避免内存泄漏和其他内存相关错误。
除了避免内存泄漏,还有一些其他的内存管理技巧和策略,可以提高程序的性能和可靠性。下面列举几个常见的技巧:

  • 内存池技术:内存池是一种提前分配好一块连续的内存,然后按需分配给程序使用的技术。由于内存池中的内存是连续的,可以减少内存碎片和动态分配内存的开销,从而提高程序的性能。内存池的实现方式有很多种,比如使用 std::allocator 或自己实现内存池类。
  • 内存对齐:内存对齐是指将数据结构中的成员变量按照特定规则对齐,使得成员变量的起始地址能够被特定大小的整数整除。内存对齐可以提高程序的性能,因为对齐的数据访问速度更快。在 C++ 中,可以使用 alignas 关键字或编译器提供的 pragma 指令来指定对齐方式。
  • 内存分配器:内存分配器是一种管理内存的对象,用于分配和释放内存。C++ 标准库中提供了一些内存分配器,比如 std::allocator 和 std::pmr::memory_resource。也可以自己实现内存分配器,以满足特定的需求。
  • 大块内存的分配和释放:当需要分配大块内存时,可以使用 mmap 或 VirtualAlloc 等系统调用来分配内存,而不是使用 malloc 或 new。这样可以避免内存碎片和动态分配内存的开销。类似地,当不再需要大块内存时,应该使用 munmap 或 VirtualFree 等系统调用来释放内存。
总之,内存管理是一个复杂的问题,需要开发者深入理解内存模型和内存管理机制,并采用适当的技术和策略来提高程序的性能和可靠性。同时,开发者需要注意内存使用情况,避免内存泄漏和其他内存相关错误,以确保程序的正确性和稳定性。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表