C++资源管理浅谈

引言:      

        在计算机编程语言的学习与实践中,自然避免不了与计算机的资源管理打交道。所谓的资源就是,一旦用了它,将来就必须还给系统,如果用户不这么做,那糟糕的事情便会发生。在开始谈及C++的资源管理之前,先来聊聊何为计算机的资源,以及为何要管理计算机的资源。

        在编程中,资源指的是计算机系统中具有有限性共享性的各种实体或能力,它们通常是系统运行时需要使用的硬件或软件要素。

        为何要进行资源管理?因为资源是有限的,不可以无止尽进行索取。正犹如存在一间房间,其容积便是用户可以使用的资源。如果不断添加家具而不清理旧家具,空间会变得拥挤,最终直至于无地栖身。程序上也是一样的,用户不断向系统索取资源,而不将其进行释放归还给系统,久而久之,系统表现愈来愈差,最后便可能会崩溃,卡住。因此,为了使程序能够良好运作,对资源进行合理的管理是必不可少的。


自动内存管理:

垃圾回收机制(Garbage Collection)

        垃圾回收是自动内存管理最广泛使用的机制,主要工作在堆内存上。许多现代编程语言,如 Java、C#、Python 和 JavaScript,都使用垃圾回收机制。垃圾回收器的工作原理是自动跟踪不再使用的内存并回收这些内存。这种机制的优点很明显,能避免用户犯下一些低级的错误。当然,也带来了额外的性能开销。此处不深入讨论。

栈内存的自动管理

        在编写C/C++程序中,我们可以使用mallocnew等语句来在堆区(heap)上开辟空间,也有相应的语句对其开辟的空间进行释放。但是在栈区上却并非如此,我们不需要手动在栈区上分配空间,更不需要手动释放,而这一切都得利于栈空间中的自动内存管理模式:由系统全权管理,局部变量通常在栈上分配,函数执行完毕时,栈帧自动销毁,释放内存。由于栈是 "LIFO"(后进先出)结构,因此内存的分配和回收是非常高效的。

部分语言也有自己独特的自动内存管理机制,此处就不一一列举。


手动内存管理:

常规使用new,delete手动进行管理

        在 C++ 中,newdelete 是用于动态内存管理的关键字,分别负责在堆上分配和释放内存。用户在使用这些关键字管理内存时,应当留意以下事项:

  • newdelete的使用必须成对!

void dynamic_memory_allocate()
{
    /*在堆区开辟大小为4字节的空间用于存储一个int型数据
    int型指针iptr指向该空间*/ 
    int* iptr = new  int;  
    *iptr = 10;
    /*忘记了delete行为,导致内存泄漏*/
}

        

void dynamic_memory_allocate(int* PInv)
{
    int *iptr = new int[5];
    *PInv = 20;
    *(iptr+2) = *PInv;
    delete[] iptr;  //delete遗漏,导致内存泄漏
}

int main(int argc,const char* argv[])
{
    dynamic_memory_allocate(new int);

    return 0;
}
  • 释放对象后防止指针悬空!

void dynamic_memory_allocate()
{
    int *PInv = new int;
    delete PInv;
    *PInv = 15;	//悬空指针
    /*PInv所指向的内存被释放后仍然操纵指针。这可能会引发未定义行为*/
}

解决办法之一便是释放对象后,手动将指针置空。

void dynamic_memory_allocate()
{
    int *PInv = new int;
    delete PInv;
    PInv = nullptr;
}
  • 成对的newdelete形式应相同!

        在用new开辟空间时,其所针对的是单个对象还是一个对象数组?这个问题十分重要,因为c++中释放存在两种形式:delete | delete[]

        前者用于释放动态分配的单个对象,而后者则用于释放动态分配的对象数组,二者不可混用!因为newdelete会自动调用构造与析构函数,这块内存中存在多少个对象?这个问题的答案直接影响了该有多少个析构函数该被调用起来。倘若混用,则很可能会导致未定义行为发生。

void dynamic_memory_allocate()
{
    typedef std::string MyTelephoneNum[5];
    std::string *PInv = new MyTelephoneNum;
    //delete PInv;  错误,会导致未定义行为
    delete[] PInv;  //正确,正常运行
    PInv = nullptr;
}

智能指针对象自动管理

        为了减少手动管理资源时的复杂性和常见错误,C++中引入了智能指针的概念。所谓智能指针,是指封装了原始指针的类,通过 RAII(Resource Acquisition Is Initialization)机制自动管理内存。

  • std::unique_ptr

        其前身为std::auto_ptr,在C++11之后auto_ptr便被废除,取而代之的则是unique_ptrstd::unique_ptr 是一个独占所有权的智能指针,意味着只有一个 unique_ptr 可以拥有某块内存。当 unique_ptr 离开其作用域时,它会自动释放所拥有的内存。相比 auto_ptrunique_ptr 提供了更安全、清晰的语义。unique_ptr 不允许拷贝,但可以通过移动语义来转移所有权,这使得资源管理更加清晰和安全。

#include <memory>	//使用时应当注意包含头文件<memory>
void dynamic_memory_allocate()
{
    std::unique_ptr<std::string> PInv(new std::string);
    *PInv = "Hello,Cpp";
    /*将开辟的空间与unique_ptr对象挂钩,不需要手动delete,当离开该作用域指针变量被销毁时
    自动调用delete进行释放*/
}
  • std::shared_ptr

        顾名思义,既然unique是独占所有权指针,不可以有多个unique指针维护同一个对象。那shared_ptr则允许用户用多个指针维护同一个对象。即std::shared_ptr 是一个共享所有权的智能指针,多个 shared_ptr 可以同时拥有一块内存,该型指针内部维护有一个计数器,记录维护同一块空间的指针对象数目,当该数目清0时,方才调用delete进行释放。也就是说,只有当维护一块空间的最后一个指针变量离开其作用域,才会执行释放动作。

std::shared_ptr<int> dynamic_memory_allocate()
{
    std::shared_ptr<int> PInv(new int);
    std::shared_ptr<int> PInv_2 = PInv; //此种赋值在unique_ptr是不被允许的
    /*PInv_2与PInv共同维护一块内存*/
    return PInv_2;
    /*返回后PInv与PInv_2被销毁,但是计数器并未归0,因此对象并没有被释放*/
}

int main(int argc,const char* argv[])
{
    /*PInv_3同样维护这块空间,只有当该作用域结束,PInv_3被销毁时方才会真正释放这片空间*/
    std::shared_ptr<int> PInv_3 = dynamic_memory_allocate();

    return 0;
}
  • std::weak_ptr

  weak_ptr是伴随着shared_ptr而产生的一种智能型指针。用于解决 std::shared_ptr 相互引用时可能导致的循环引用问题。考虑代码如下:

class B;
class A {
public:
    std::shared_ptr<B> PInvA;
    A() : PInvA(new B) {}
};

class B {
public:
    std::shared_ptr<A> PInvB;
    B() : PInvB(new A) {}
};

void createCycle() {
    std::shared_ptr<A> a(new A());
    std::shared_ptr<B> b(new B());
    a->PInvA = b;
    b->PInvB = a;
    /*a 和 b 在 createCycle 函数结束时会被销毁,但由于它们相互引用
,引用计数不会降为 0,导致内存不会被释放。
    从而导致内存泄漏*/
}

其解决办法便是使用weak进行观察:

class B;
class A {
public:
    std::shared_ptr<B> PInvA;
    A() : PInvA(new B) {}
};

class B {
public:
    std::weak_ptr<A> PInvB; // 使用 weak_ptr 代替 shared_ptr
    B() : PInvB(std::shared_ptr<A>(new A)) {}
};

这样,当 shared_ptr 被销毁时,引用计数会正确降为 0,内存会被释放。

在此,智能指针我们只做如此说明。


RAII(Resource Acquisition Is Initializing)资源获取即初始化机制

        RAII,与其说是机制,更是一种思想。也常被称为资源取得时机便为初始化时机。它用于确保资源在对象的生命周期内得到安全、自动化的管理。RAII的核心思想是将资源(如内存、文件句柄、锁等)的获取和释放与对象的生命周期绑定,从而避免手动管理资源可能引发的问题,如资源泄漏或不正确的释放。其核心可用一句话说明:在构造函数中获取资源,在析构函数中释放资源

        在c++编程中,RAII思想的运用尤为广泛,在动态内存、文件、锁或网络连接等方面均有应用。其基本原理便是在构造函数中关联资源的分配,在其析构函数中关联对象的释放。利用c++中对象的特殊机制,达到相对自动化管理内存的目的。

class ManageMyString
{
private:
    char *PInv;

public:
    ManageMyString(const char *str)
    {
        if (str)
        {
            PInv = new (char[strlen(str) + 1]);
            strcpy(this->PInv,str);
        }
        else
        {
            PInv = new(char[1]);
            *PInv = '\0';
        }
    }
    //禁用拷贝与赋值
    ManageMyString(const ManageMyString&) = delete;
    ManageMyString& operator=(const ManageMyString&) = delete;

    ~ManageMyString()
    {
        delete[] this->PInv;
    }
};

        以上代码便是RAII思想的一例具体应用案例:构造函数中根据传入的字符串分配对应的内存,并且将字符串复制到所分配的内存中。因此,一个ManageMyString对象便维护了一个指向字符串的指针变量。当该对象被销毁时,析构函数中调用delete释放掉所维护的内存。

        该行为与智能指针,以及在栈区上的内存管理机制相同。RAII将对象与资源管理相绑定,合理运用能够极大减轻资源管理的压力以及难度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值