引言:
在计算机编程语言的学习与实践中,自然避免不了与计算机的资源管理打交道。所谓的资源就是,一旦用了它,将来就必须还给系统,如果用户不这么做,那糟糕的事情便会发生。在开始谈及C++的资源管理之前,先来聊聊何为计算机的资源,以及为何要管理计算机的资源。
在编程中,资源指的是计算机系统中具有有限性和共享性的各种实体或能力,它们通常是系统运行时需要使用的硬件或软件要素。
为何要进行资源管理?因为资源是有限的,不可以无止尽进行索取。正犹如存在一间房间,其容积便是用户可以使用的资源。如果不断添加家具而不清理旧家具,空间会变得拥挤,最终直至于无地栖身。程序上也是一样的,用户不断向系统索取资源,而不将其进行释放归还给系统,久而久之,系统表现愈来愈差,最后便可能会崩溃,卡住。因此,为了使程序能够良好运作,对资源进行合理的管理是必不可少的。
自动内存管理:
垃圾回收机制(Garbage Collection)
垃圾回收是自动内存管理最广泛使用的机制,主要工作在堆内存上。许多现代编程语言,如 Java、C#、Python 和 JavaScript,都使用垃圾回收机制。垃圾回收器的工作原理是自动跟踪不再使用的内存并回收这些内存。这种机制的优点很明显,能避免用户犯下一些低级的错误。当然,也带来了额外的性能开销。此处不深入讨论。
栈内存的自动管理
在编写C/C++程序中,我们可以使用malloc
,new
等语句来在堆区(heap)上开辟空间,也有相应的语句对其开辟的空间进行释放。但是在栈区上却并非如此,我们不需要手动在栈区上分配空间,更不需要手动释放,而这一切都得利于栈空间中的自动内存管理模式:由系统全权管理,局部变量通常在栈上分配,函数执行完毕时,栈帧自动销毁,释放内存。由于栈是 "LIFO"(后进先出)结构,因此内存的分配和回收是非常高效的。
部分语言也有自己独特的自动内存管理机制,此处就不一一列举。
手动内存管理:
常规使用new,delete手动进行管理
在 C++ 中,new
和 delete
是用于动态内存管理的关键字,分别负责在堆上分配和释放内存。用户在使用这些关键字管理内存时,应当留意以下事项:
-
new
与delete
的使用必须成对!
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;
}
-
成对的
new
与delete
形式应相同!
在用new
开辟空间时,其所针对的是单个对象还是一个对象数组?这个问题十分重要,因为c++中释放存在两种形式:delete
| delete[]
前者用于释放动态分配的单个对象,而后者则用于释放动态分配的对象数组,二者不可混用!因为new
与delete
会自动调用构造与析构函数,这块内存中存在多少个对象?这个问题的答案直接影响了该有多少个析构函数该被调用起来。倘若混用,则很可能会导致未定义行为发生。
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_ptr
。std::unique_ptr
是一个独占所有权的智能指针,意味着只有一个 unique_ptr
可以拥有某块内存。当 unique_ptr
离开其作用域时,它会自动释放所拥有的内存。相比 auto_ptr
,unique_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将对象与资源管理相绑定,合理运用能够极大减轻资源管理的压力以及难度。
标签:std,浅谈,C++,ptr,内存,new,PInv,资源管理,delete From: https://blog.csdn.net/R6bandito/article/details/142285334