C++引入智能指针的主要目的是为了解决手动管理内存的问题,提高程序的健壮性和可维护性。在C++中,内存管理由程序员手动完成,包括内存的分配和释放。手动管理内存可能导致一些常见的问题,如内存泄漏、释放已经释放的内存(二次释放)、野指针等。
智能指针是一种封装了指针的类,它可以自动管理内存的生命周期,使得内存的分配和释放更加安全和方便。
我们考虑一个简单的情景,展示为什么需要智能指针以及它是如何解决问题的。
假设有一个
Person
类表示一个人,该类有一个成员变量是name
,并且我们在动态内存中为其分配内存:
#include
#include
class 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
#include
#include
class 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_ptr<Person> personPtr = std::make_unique<Person>("John");
personPtr->printName(); // 在作用域结束时自动释放内存
return 0;
}
当然这里只是常规的情况,普通的内存泄漏问题,很多人觉得只要多注意一些就可以了,但是如果是下面的情况呢?
考虑以下场景,其中
Person
类有一个成员变量bestFriend
表示另一个Person
对象,两个人互为最好的朋友:
#include
class 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
使用引用计数来跟踪对象的引用数量,当引用计数为零时,对象会被正确地销毁。然而,使用原始指针来管理这种情况会导致无法释放的内存。
**如果两个或多个对象相互引用,形成循环引用,而使用原始指针管理内存,可能导致内存泄漏,因为循环引用会导致引用计数无法归零,从而无法释放对象。**所以在对对象的管理中我们会遇到很多原始指针解决不了的问题,所以才有了智能指针的由来。
内存泄漏是指在程序运行过程中,分配的内存空间在不再需要时没有被释放,导致系统中的可用内存减少。内存泄漏可能发生在各种编程语言中,包括C++、Java、C#等。内存泄漏的危害主要包括:
内存泄漏可以分为几种常见的类型,每种类型都有不同的原因和表现。以下是一些常见的内存泄漏分类:
new
或 malloc
分配内存后忘记使用 delete
或 free
进行释放。避免内存泄漏是编程中非常重要的任务之一。以下是一些常见的方法和最佳实践,有助于减少或避免内存泄漏:
使用智能指针: 使用C++的智能指针,如std::shared_ptr
、std::unique_ptr
等,可以自动管理内存的释放。这样可以减少手动释放内存的机会,防止忘记释放或重复释放的问题。
// 使用 std::shared_ptr
std::shared_ptr<int> smartPtr = std::make_shared<int>(42);
RAII(资源获取即初始化)原则: 使用对象生命周期管理资源。确保在对象创建时分配资源,在对象销毁时释放资源。智能指针正是基于这个原则设计的。
避免手动管理内存: 尽量避免使用 new
和 delete
进行手动内存管理。使用标准容器和智能指针等抽象层级更高的工具,它们能够更安全地管理内存。
使用析构函数: 在类的析构函数中释放在构造函数中分配的资源。确保资源的释放操作被正确实现。
class ResourceHolder {
public:
ResourceHolder() {
// 分配资源
resource = new Resource;
}
~ResourceHolder() {
// 释放资源
delete resource;
}
private:
Resource* resource;
};
避免循环引用: 当存在循环引用时,使用弱引用(std::weak_ptr
)来打破循环引用关系,防止引用计数无法归零。
#include
#include
class Person;
class Car {
public:
void setOwner(std::shared_ptr<Person> person) {
owner = person;
}
private:
std::shared_ptr<Person> owner;
};
class Person {
public:
void buyCar() {
car = std::make_shared<Car>();
car->setOwner(shared_from_this());
}
private:
std::shared_ptr<Car> car;
};
使用工具进行静态和动态分析: 使用工具如静态分析器(如Clang Static Analyzer
、Cppcheck
)、动态分析器(如Valgrind
)等,帮助发现潜在的内存泄漏问题。
使用现代C++特性: C++11及其后续版本引入了许多现代C++特性,如移动语义、智能指针、Lambda表达式等,这些特性有助于更安全和高效地管理内存。
良好的编程习惯: 养成良好的编程习惯,注重代码的规范性和清晰性,有助于及早发现潜在的问题,并减少内存泄漏的发生。
RAII(Resource Acquisition Is Initialization)
是一种C++编程范式,是一种基于对象生命周期管理资源的策略。RAII的核心思想是,在对象的构造函数中获取资源(如内存、文件句柄、网络连接等),而在对象的析构函数中释放这些资源。这样,资源的生命周期与对象的生命周期绑定在一起,从而确保资源在适当的时候被正确释放。
下面我们看一个使用RAII
思想设计的SmartPtr
类
template<class 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()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(new int);
cout << div() << endl;
}
int main()
{
try
{
Func();
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}
在这里我们可以看到,通过SmartPtr
类对div
类新对象的建立,我们可以在其创建新对象时自动将指针进行托管,最终不用我们手动去调用析构函数,而是在作用域将结束时,通过SmartPtr
指向的div
对象指针自动调用析构,从而防止了内存泄漏的问题。
上述的SmartPtr
还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此:此处模板类中还得需要将*
、->
进行重载,才可让其像指针一样去使用 。
template<class 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()
{
SmartPtr<int> sp1(new int);
*sp1 = 10;
cout << *sp1 << endl;
SmartPtr<int> sparray(new Date);
sparray->_year = 2023;
sparray->_month = 1;
sparray->_day = 1;
}
需要注意的是这里应该是sparray.operator->()->_year = 2018
;应该是sparray->->_year
这里语法上为了可读性,省略了一个->
auto_ptr
是 C++98 标准中引入的一种智能指针,用于管理动态分配的内存。它是第一个尝试提供自动内存管理的 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
存在的问题,C++11 引入了更为安全和灵活的智能指针,如std::unique_ptr
和std::shared_ptr
。这些智能指针提供了更好的内存管理语义和更丰富的功能,取代了auto_ptr
。- 被标记为废弃: C++11 标准中将
auto_ptr
标记为废弃(deprecated),并建议使用更为安全的替代方案。在 C++17 标准中,auto_ptr
被完全移除。
简化模拟实现auto_ptr
namespace yulao
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
auto_ptr(auto_ptr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& 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;
};
}
C++98中 auto_ptr
管理权转移,被拷贝对象的出现悬空问题,很多公司是明确的要求了不能使用它
比如下面的情况
class A
{
public:
~A()
{
cout << "~A()" << endl;
}
int _a1 = 0;
int _a2 = 0;
};
int main()
{
auto_ptr<A> ap1(new A);
ap1->_a1++;
ap1->_a2++;
auto_ptr<A> 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
没有拥有权转移的问题,并提供了更好的所有权管理。
在C++11出现之前其实已经有了智能指针(不是指auto_ptr
),是在第三方的一个C++库中,名叫boost库。
Boost库是一个由C++社区开发和维护的开源库集合,提供了许多高质量、可移植且通用的C++工具和组件。在C++11引入标准智能指针之前,Boost库已经提供了类似的功能。
Boost库中的智能指针主要是boost::shared_ptr
和boost::scoped_ptr
。以下是它们在C++98到C++11之间的历史发展:
boost::scoped_ptr
: 在C++98时代,Boost库引入了boost::scoped_ptr
。这是一个独占所有权的智能指针,其目的是在其生命周期结束时自动释放所管理的资源。然而,由于它的独占性质,boost::scoped_ptr
不能共享资源。
#include
int main() {
boost::scoped_ptr<int> myInt(new int);
// myInt 的生命周期结束时,所管理的内存将被释放
return 0;
}
boost::shared_ptr
: 随着C++98的发展,Boost库还引入了boost::shared_ptr
。这是一个引用计数智能指针,它允许多个shared_ptr
对象共享对同一对象的所有权,并在最后一个shared_ptr
离开作用域时释放资源。
#include
int main() {
boost::shared_ptr<int> sharedInt(new int);
// 多个 sharedInt 对象共享同一块内存资源
return 0;
}
C++11引入标准智能指针: 随着C++11标准的发布,标准库中引入了std::unique_ptr
、std::shared_ptr
和std::unique_ptr
,提供了更为强大和灵活的智能指针实现。C++11的智能指针取代了Boost库中的对应版本,并成为标准语言特性。
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份unique_Ptr来了解它的原理
template<class T>
class unique_ptr
{
private:
// 防拷贝 C++98
// 只声明不实现
//unique_ptr(unique_ptr& ap);
//unique_ptr& operator=(unique_ptr& ap);
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
// 防拷贝 C++11
unique_ptr(unique_ptr<T>& ap) = delete;
unique_ptr<T>& operator=(unique_ptr<T>& 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_ptr<int> sp1(new int);
yulao::unique_ptr<int> sp2(sp1);//此时拷贝会报错
return 0;
}
shared_ptr与unique_ptr最大的区别就是支持拷贝
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
==引用计数支持多个拷贝管理同一个资源,最后一个析构对象释放资源 ==
template<class 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_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
{
(*_pCount)++;
}
shared_ptr<T>& operator=(const shared_ptr<T>& 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_ptr<A> sp1(new A);
shared_ptr<A> sp2(sp1);
shared_ptr<A> 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
#include
template<typename T>
class Node {
public:
T data;
std::shared_ptr<Node<T>> next;
std::shared_ptr<Node<T>> 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() {
// 创建一个双向链表节点1
auto node1 = std::make_shared<Node<int>>(1);
// 创建一个双向链表节点2
auto node2 = std::make_shared<Node<int>>(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
不会增加引用计数,也不会影响节点的析构。
首先我们要知道weak_ptr
不是常规智能指针,没有RAII
,不支持直接管理资源,weak_ptr
主要用shared_ptr
构造,用来解决shared_ptr
循环引用问题,这里将它单独列出,只是为了更好的讲清楚这个问题
简单的模拟实现weak_ptr
// 辅助型智能指针,使命配合解决shared_ptr循环引用问题
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr(const weak_ptr<T>& wp)
:_ptr(wp._ptr)
{}
weak_ptr<T>& operator=(const shared_ptr<T>& 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
#include
class B; // 前向声明
class A {
public:
shared_ptr<B> b_ptr;
A() { std::cout << "A constructed" << std::endl; }
~A() { std::cout << "A destructed" << std::endl; }
};
class B {
public:
weak_ptr<A> a_ptr; // 使用 weak_ptr 避免循环引用
B() { std::cout << "B constructed" << std::endl; }
~B() { std::cout << "B destructed" << std::endl; }
};
int main() {
shared_ptr<A> a = make_shared<A>();
shared_ptr<B> b = make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;
return 0;
}
在这个例子中,A
和 B
类相互引用,但通过使用 weak_ptr
避免了循环引用。
最后再提一句shared_ptr
的线程安全问题,需要等到后面作者将Linux文章更新到多线程再进行讲解,希望大家能够持续关注我!!!