丽水市住房和城建建设局网站,火车头导入wordpress,网络广告代理渠道,下载小程序一篇文章带你详细了解C智能指针 为什么要有智能指针内存泄漏1.什么是内存泄漏#xff0c;它的危害是什么2.内存泄漏的分类3.如何避免内存泄漏 智能指针的使用及原理1.RAII2.智能指针的原理3.auto_ptr4.unique_ptr5.shared_ptr6.weak_ptr 为什么要有智能指针
C引入智能指针的主… 一篇文章带你详细了解C智能指针 为什么要有智能指针内存泄漏1.什么是内存泄漏它的危害是什么2.内存泄漏的分类3.如何避免内存泄漏 智能指针的使用及原理1.RAII2.智能指针的原理3.auto_ptr4.unique_ptr5.shared_ptr6.weak_ptr 为什么要有智能指针
C引入智能指针的主要目的是为了解决手动管理内存的问题提高程序的健壮性和可维护性。在C中内存管理由程序员手动完成包括内存的分配和释放。手动管理内存可能导致一些常见的问题如内存泄漏、释放已经释放的内存二次释放、野指针等。
智能指针是一种封装了指针的类它可以自动管理内存的生命周期使得内存的分配和释放更加安全和方便。
我们考虑一个简单的情景展示为什么需要智能指针以及它是如何解决问题的。 假设有一个Person类表示一个人该类有一个成员变量是name并且我们在动态内存中为其分配内存 #include iostream
#include cstringclass Person {
public:Person(const char* n) {name new char[strlen(n) 1];strcpy(name, n);}~Person() {delete[] name;}void printName() const {std::cout name std::endl;}private:char* name;
};int main() {Person* personPtr new Person(John);personPtr-printName();delete personPtr; // 忘记释放内存return 0;
}在这个例子中我们通过 new 在堆上创建了一个 Person 对象但在程序结束前忘记了调用 delete 来释放内存。这会导致内存泄漏因为自定义对象的析构函数不会被调用从而无法释放 name 的内存。
这里我们可以使用智能指针中的unique_ptr来管理Person对象
#include iostream
#include memory
#include cstringclass Person {
public:Person(const char* n) {name new char[strlen(n) 1];strcpy(name, n);}~Person() {delete[] name;}void printName() const {std::cout name std::endl;}private:char* name;
};int main() {std::unique_ptrPerson personPtr std::make_uniquePerson(John);personPtr-printName(); // 在作用域结束时自动释放内存return 0;
}当然这里只是常规的情况普通的内存泄漏问题很多人觉得只要多注意一些就可以了但是如果是下面的情况呢 考虑以下场景其中 Person 类有一个成员变量 bestFriend 表示另一个 Person 对象两个人互为最好的朋友 #include iostreamclass Person {
public:Person(const char* n) : name(n), bestFriend(nullptr) {}~Person() {std::cout name destroyed. std::endl;}void setBestFriend(Person* friendPtr) {bestFriend friendPtr;}private:const char* name;Person* bestFriend;
};int main() {Person* john new Person(John);Person* mary new Person(Mary);john-setBestFriend(mary);mary-setBestFriend(john);// delete john;// delete mary;return 0;
}在这个例子中John 和 Mary 形成了循环引用因为它们彼此引用对方作为最好的朋友。如果我们尝试使用原始指针进行 delete它们的析构函数将永远不会被调用导致内存泄漏。这里我们就可以使用智能指针中的shared_ptr就可以解决这个问题因为 std::shared_ptr 使用引用计数来跟踪对象的引用数量当引用计数为零时对象会被正确地销毁。然而使用原始指针来管理这种情况会导致无法释放的内存。
**如果两个或多个对象相互引用形成循环引用而使用原始指针管理内存可能导致内存泄漏因为循环引用会导致引用计数无法归零从而无法释放对象。**所以在对对象的管理中我们会遇到很多原始指针解决不了的问题所以才有了智能指针的由来。
内存泄漏
1.什么是内存泄漏它的危害是什么
内存泄漏是指在程序运行过程中分配的内存空间在不再需要时没有被释放导致系统中的可用内存减少。内存泄漏可能发生在各种编程语言中包括C、Java、C#等。内存泄漏的危害主要包括
资源浪费 内存泄漏导致未释放的内存无法被重新使用导致系统中的可用内存逐渐减小。这可能导致系统性能下降、程序变得缓慢甚至在极端情况下导致系统崩溃。程序性能下降 随着时间的推移内存泄漏会导致程序使用的内存越来越多从而增加了垃圾回收的负担使得程序运行变得更加缓慢。这对于长时间运行的服务或应用程序来说尤其是一个严重的问题。系统稳定性下降 内存泄漏可能导致系统内存耗尽最终导致系统不稳定甚至崩溃。这对于一些关键系统、服务器或嵌入式系统来说可能是致命的。难以调试 内存泄漏通常不容易被发现因为程序在运行时没有显著的错误提示。随着内存泄漏的累积程序可能会在某一刻因为内存不足而崩溃而这个问题的根本原因可能很难追踪。安全隐患 内存泄漏也可能导致安全问题。恶意攻击者可以利用未释放的内存来执行缓冲区溢出等攻击从而破坏程序的正常执行甚至入侵系统。
2.内存泄漏的分类
内存泄漏可以分为几种常见的类型每种类型都有不同的原因和表现。以下是一些常见的内存泄漏分类
堆内存泄漏Heap Memory Leak 堆内存泄漏是指在动态分配内存时没有正确释放这些内存导致的泄漏。这通常发生在使用 new 或 malloc 分配内存后忘记使用 delete 或 free 进行释放。栈内存泄漏Stack Memory Leak 栈内存泄漏通常是由于在函数或代码块中分配的局部变量没有在该函数或代码块结束时被正确释放。栈内存泄漏通常较为轻微因为在函数结束时栈上的局部变量会自动被销毁。全局/静态内存泄漏Global/Static Memory Leak 全局变量和静态变量在程序的整个生命周期内存在如果没有在程序结束时释放相关内存就会导致全局或静态内存泄漏。循环引用Circular Reference 循环引用是指两个或多个对象相互引用形成一个循环结构并且它们的引用计数无法归零。这种情况下即使没有其他引用这些对象也无法被垃圾回收。虚拟机泄漏Memory Leak in Managed Runtimes 在使用托管运行时如Java虚拟机、.NET运行时等的环境中有时会出现虚拟机泄漏即运行时本身没有正确释放的内存。资源泄漏Resource Leak 除了内存之外资源泄漏还可以包括其他类型的资源例如文件句柄、网络连接等。如果这些资源在使用完毕后没有被释放就会导致资源泄漏。
3.如何避免内存泄漏
避免内存泄漏是编程中非常重要的任务之一。以下是一些常见的方法和最佳实践有助于减少或避免内存泄漏 使用智能指针 使用C的智能指针如std::shared_ptr、std::unique_ptr等可以自动管理内存的释放。这样可以减少手动释放内存的机会防止忘记释放或重复释放的问题。 // 使用 std::shared_ptr
std::shared_ptrint smartPtr std::make_sharedint(42);RAII资源获取即初始化原则 使用对象生命周期管理资源。确保在对象创建时分配资源在对象销毁时释放资源。智能指针正是基于这个原则设计的。 避免手动管理内存 尽量避免使用 new 和 delete 进行手动内存管理。使用标准容器和智能指针等抽象层级更高的工具它们能够更安全地管理内存。 使用析构函数 在类的析构函数中释放在构造函数中分配的资源。确保资源的释放操作被正确实现。 class ResourceHolder {
public:ResourceHolder() {// 分配资源resource new Resource;}~ResourceHolder() {// 释放资源delete resource;}private:Resource* resource;
};避免循环引用 当存在循环引用时使用弱引用std::weak_ptr来打破循环引用关系防止引用计数无法归零。 #include iostream
#include memoryclass Person;class Car {
public:void setOwner(std::shared_ptrPerson person) {owner person;}private:std::shared_ptrPerson owner;
};class Person {
public:void buyCar() {car std::make_sharedCar();car-setOwner(shared_from_this());}private:std::shared_ptrCar car;
};使用工具进行静态和动态分析 使用工具如静态分析器如Clang Static Analyzer、Cppcheck、动态分析器如Valgrind等帮助发现潜在的内存泄漏问题。 使用现代C特性 C11及其后续版本引入了许多现代C特性如移动语义、智能指针、Lambda表达式等这些特性有助于更安全和高效地管理内存。 良好的编程习惯 养成良好的编程习惯注重代码的规范性和清晰性有助于及早发现潜在的问题并减少内存泄漏的发生。
智能指针的使用及原理
1.RAII
RAIIResource Acquisition Is Initialization是一种C编程范式是一种基于对象生命周期管理资源的策略。RAII的核心思想是在对象的构造函数中获取资源如内存、文件句柄、网络连接等而在对象的析构函数中释放这些资源。这样资源的生命周期与对象的生命周期绑定在一起从而确保资源在适当的时候被正确释放。
下面我们看一个使用RAII思想设计的SmartPtr类
templateclass T
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout delete: _ptr endl;delete _ptr;}private:T* _ptr;
};int div()
{int a, b;cin a b;if (b 0)throw invalid_argument(除0错误);return a / b;
}void Func()
{SmartPtrint sp1(new int);SmartPtrint sp2(new int);cout div() endl;
}int main()
{try{Func();}catch (exception e){cout e.what() endl;}return 0;
}在这里我们可以看到通过SmartPtr类对div类新对象的建立我们可以在其创建新对象时自动将指针进行托管最终不用我们手动去调用析构函数而是在作用域将结束时通过SmartPtr指向的div对象指针自动调用析构从而防止了内存泄漏的问题。
2.智能指针的原理
上述的SmartPtr还不能将其称为智能指针因为它还不具有指针的行为。指针可以解引用也可以通过-去访问所指空间中的内容因此此处模板类中还得需要将* 、-进行重载才可让其像指针一样去使用 。
templateclass T
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout delete: _ptr endl;delete _ptr;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;
};struct Date
{int _year;int _month;int _day;
};
int main()
{SmartPtrint sp1(new int);*sp1 10;cout *sp1 endl;SmartPtrint sparray(new Date);sparray-_year 2023;sparray-_month 1;sparray-_day 1;
}需要注意的是这里应该是sparray.operator-()-_year 2018;应该是sparray--_year这里语法上为了可读性省略了一个-
3.auto_ptr
auto_ptr 是 C98 标准中引入的一种智能指针用于管理动态分配的内存。它是第一个尝试提供自动内存管理的 C 标准库智能指针然而由于其独特的拥有权转移语义导致了一些问题因此在后续的 C 标准中被更现代的智能指针如 std::unique_ptr 和 std::shared_ptr所取代。
以下是 auto_ptr 的一些关键特点和历史 独占所有权 auto_ptr 具有独占所有权的特性即当一个 auto_ptr 拥有某块动态分配的内存时其他任何 auto_ptr 都不能指向同一块内存。这种独占性导致了一些潜在的问题特别是在涉及到复制和拷贝构造函数时。拥有权的转移 auto_ptr 支持拥有权的转移即一个 auto_ptr 对象的所有权可以转移到另一个对象而被转移的对象会变成空指针。这样的语义在某些情况下可能导致程序员不经意间的错误。不适用于容器 由于拥有权的转移语义auto_ptr 不能安全地用于标准库容器因为容器的操作可能导致拷贝或复制 auto_ptr 对象从而引发悬空指针和内存泄漏问题。被现代智能指针替代 由于 auto_ptr 存在的问题C11 引入了更为安全和灵活的智能指针如 std::unique_ptr 和 std::shared_ptr。这些智能指针提供了更好的内存管理语义和更丰富的功能取代了 auto_ptr。被标记为废弃 C11 标准中将 auto_ptr 标记为废弃deprecated并建议使用更为安全的替代方案。在 C17 标准中auto_ptr 被完全移除。 简化模拟实现auto_ptr
namespace yulao
{templateclass Tclass auto_ptr {public:auto_ptr(T* ptr nullptr): _ptr(ptr){}auto_ptr(auto_ptrT ap):_ptr(ap._ptr){ap._ptr nullptr;}auto_ptrT operator(auto_ptrT ap){if (this ! ap){if (_ptr){cout Delete: _ptr endl;delete _ptr;}_ptr ap._ptr;ap._ptr nullptr;}return *this;}~auto_ptr(){if (_ptr){cout Delete: _ptr endl;delete _ptr;}}T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;};
}C98中 auto_ptr 管理权转移被拷贝对象的出现悬空问题很多公司是明确的要求了不能使用它
比如下面的情况
class A
{
public:~A(){cout ~A() endl;}int _a1 0;int _a2 0;
};int main()
{auto_ptrA ap1(new A);ap1-_a1;ap1-_a2;auto_ptrA ap2(ap1);ap1-_a1;ap1-_a2;return 0;
}这里使用了 auto_ptr 来管理动态分配的对象 A 的内存。然而需要注意的是auto_ptr 具有拥有权转移的语义因此在将一个 auto_ptr 赋值给另一个后原始的 auto_ptr 将变为 nullptr 指针。这会导致后续对原始指针成员 _a1 和 _a2 的访问可能导致未定义行为。auto_ptr 在现代C中已经被废弃有了更为安全的 unique_ptr 来管理动态分配的内存。unique_ptr 没有拥有权转移的问题并提供了更好的所有权管理。
4.unique_ptr
在C11出现之前其实已经有了智能指针不是指auto_ptr是在第三方的一个C库中名叫boost库。
Boost库是一个由C社区开发和维护的开源库集合提供了许多高质量、可移植且通用的C工具和组件。在C11引入标准智能指针之前Boost库已经提供了类似的功能。
Boost库中的智能指针主要是boost::shared_ptr和boost::scoped_ptr。以下是它们在C98到C11之间的历史发展 boost::scoped_ptr 在C98时代Boost库引入了boost::scoped_ptr。这是一个独占所有权的智能指针其目的是在其生命周期结束时自动释放所管理的资源。然而由于它的独占性质boost::scoped_ptr不能共享资源。 #include boost/scoped_ptr.hppint main() {boost::scoped_ptrint myInt(new int);// myInt 的生命周期结束时所管理的内存将被释放return 0;
}boost::shared_ptr 随着C98的发展Boost库还引入了boost::shared_ptr。这是一个引用计数智能指针它允许多个shared_ptr对象共享对同一对象的所有权并在最后一个shared_ptr离开作用域时释放资源。 #include boost/shared_ptr.hppint main() {boost::shared_ptrint sharedInt(new int);// 多个 sharedInt 对象共享同一块内存资源return 0;
}C11引入标准智能指针 随着C11标准的发布标准库中引入了std::unique_ptr、std::shared_ptr和std::unique_ptr提供了更为强大和灵活的智能指针实现。C11的智能指针取代了Boost库中的对应版本并成为标准语言特性。
unique_ptr的实现原理简单粗暴的防拷贝下面简化模拟实现了一份unique_Ptr来了解它的原理
templateclass T
class unique_ptr
{
private:// 防拷贝 C98// 只声明不实现 //unique_ptr(unique_ptrT ap);//unique_ptrT operator(unique_ptrT ap);
public:unique_ptr(T* ptr nullptr): _ptr(ptr){}// 防拷贝 C11unique_ptr(unique_ptrT ap) delete;unique_ptrT operator(unique_ptrT ap) delete;~unique_ptr(){if (_ptr){cout Delete: _ptr endl;delete _ptr;}}T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;
};这里我们可以看到为了防止出现auto_ptr的拷贝空指针的情况我们可以直接将拷贝构造和赋值重载直接delete或者设为私有声明后不实现两种方式。
int main()
{yulao::unique_ptrint sp1(new int);yulao::unique_ptrint sp2(sp1);//此时拷贝会报错return 0;
}5.shared_ptr
shared_ptr与unique_ptr最大的区别就是支持拷贝
shared_ptr的原理是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。 shared_ptr在其内部给每个资源都维护了着一份计数用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用)就说明自己不使用该资源了对象的引用计数减一。如果引用计数是0就说明自己是最后一个使用该资源的对象必须释放该资源如果不是0就说明除了自己还有其他对象在使用该份资源不能释放该资源否则其他对象就成野指针了。 引用计数支持多个拷贝管理同一个资源最后一个析构对象释放资源
templateclass T
class shared_ptr
{
public:shared_ptr(T* ptr nullptr): _ptr(ptr), _pCount(new int(1)){}void Release(){if (--(*_pCount) 0){cout Delete: _ptr endl;delete _ptr;delete _pCount;}}~shared_ptr(){Release();}// sp1(sp2)shared_ptr(const shared_ptrT sp): _ptr(sp._ptr), _pCount(sp._pCount){(*_pCount);}shared_ptrT operator(const shared_ptrT sp){if (_ptr sp._ptr){return *this;}Release();// 共管新资源计数_ptr sp._ptr;_pCount sp._pCount;(*_pCount);return *this;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}private:T* _ptr;// 引用计数int* _pCount;
};引用计数管理 使用 _pCount 来追踪对象的引用计数每个 shared_ptr 指向同一个对象时它们共享同一个 _pCount。构造函数和析构函数 在构造函数中你初始化 _pCount 为1表示当前有一个 shared_ptr 指向这个对象。在析构函数中你通过 Release() 函数来释放资源如果引用计数为0则删除对象和引用计数。拷贝构造函数和拷贝赋值运算符 当一个 shared_ptr 被拷贝构造或赋值给另一个 shared_ptr 时引用计数会增加。这样多个 shared_ptr 可以共享同一块内存当最后一个 shared_ptr 被销毁时对象才会被释放。重载 operator * 和 operator- 使得 shared_ptr 的使用方式类似于原始指针。删除资源的时机 在你的代码中资源的释放是在 Release() 函数中进行的该函数在析构函数和赋值运算符中被调用。这确保了资源在最后一个引用被释放时被正确删除。
int main(){shared_ptrA sp1(new A);shared_ptrA sp2(sp1);shared_ptrA sp3(sp1);sp1-_a1;sp1-_a2;std::cout sp2-_a1 : sp2-_a2 std::endl;sp2-_a1;sp2-_a2;std::cout sp1-_a1 : sp1-_a2 std::endl;
}我们通过这段代码可以看到shared_ptr可以很好的支持对对象指针的拷贝并进行管理实现多指针管理同一个对象但不会造成多次调用析构的情况。
但是在C中使用 shared_ptr 来管理资源当形成循环引用时可能导致对象无法正确释放。下面是一个简化的双向链表的例子
#include memory
#include iostreamtemplatetypename T
class Node {
public:T data;std::shared_ptrNodeT next;std::shared_ptrNodeT prev;Node(const T val) : data(val), next(nullptr), prev(nullptr) {std::cout Node constructed with value: val std::endl;}~Node() {std::cout Node destructed with value: data std::endl;}
};int main() {// 创建一个双向链表节点1auto node1 std::make_sharedNodeint(1);// 创建一个双向链表节点2auto node2 std::make_sharedNodeint(2);// 形成循环引用node1-next node2;node2-prev node1;// 输出每个节点的引用计数std::cout Reference counts: node1 node1.use_count() , node2 node2.use_count() std::endl;// 节点1和节点2的引用计数不为零它们不会被释放return 0;
}在上面的例子中node1 和 node2 形成了双向链表的循环引用。node1 持有 node2 的 shared_ptr而 node2 同时持有 node1 的 shared_ptr。这导致两个节点的引用计数不会变为零它们的析构函数也不会被调用。这就是循环引用的问题。
为了解决这个问题可以使用 weak_ptr 来打破循环引用。在上面的例子中可以将 prev 和 next 成员改为 std::weak_ptr 类型。这样即使形成了循环引用weak_ptr 不会增加引用计数也不会影响节点的析构。
6.weak_ptr
首先我们要知道weak_ptr不是常规智能指针没有RAII不支持直接管理资源weak_ptr主要用shared_ptr构造用来解决shared_ptr循环引用问题这里将它单独列出只是为了更好的讲清楚这个问题
简单的模拟实现weak_ptr
// 辅助型智能指针使命配合解决shared_ptr循环引用问题
templateclass T
class weak_ptr
{
public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptrT sp):_ptr(sp.get()){}weak_ptr(const weak_ptrT wp):_ptr(wp._ptr){}weak_ptrT operator(const shared_ptrT sp){_ptr sp.get();return *this;}T operator*(){return *_ptr;}T* operator-(){return _ptr;}
public:T* _ptr;
};构造函数 默认构造函数以及从 shared_ptr 和另一个 weak_ptr 构造的构造函数。这是 weak_ptr 常见的构造方式。赋值运算符 从 shared_ptr 赋值的运算符。这允许将 shared_ptr 赋值给 weak_ptr使得 weak_ptr 可以观察 shared_ptr 所指向的对象但并不影响引用计数。重载 operator * 和 operator- 这使得 weak_ptr 的使用方式类似于原始指针。注意事项 在实际使用中weak_ptr 通常用于解决循环引用问题而不是直接与裸指针交互。weak_ptr 不会增加对象的引用计数因此不会影响对象的生命周期。
以下是一个简单的示例演示了如何使用 weak_ptr 和 shared_ptr 避免循环引用
#include iostream
#include memoryclass B; // 前向声明class A {
public:shared_ptrB b_ptr;A() { std::cout A constructed std::endl; }~A() { std::cout A destructed std::endl; }
};class B {
public:weak_ptrA a_ptr; // 使用 weak_ptr 避免循环引用B() { std::cout B constructed std::endl; }~B() { std::cout B destructed std::endl; }
};int main() {shared_ptrA a make_sharedA();shared_ptrB b make_sharedB();a-b_ptr b;b-a_ptr a;return 0;
}在这个例子中A 和 B 类相互引用但通过使用 weak_ptr 避免了循环引用。
最后再提一句shared_ptr的线程安全问题需要等到后面作者将Linux文章更新到多线程再进行讲解希望大家能够持续关注我