什么是智能指针
C++引入智能指针的主要目的是为了解决手动管理内存的问题,提高程序的健壮性和可维护性。在C++中,内存管理由程序员手动完成,包括内存的分配和释放。手动管理内存可能导致一些常见的问题,如内存泄漏、释放已经释放的内存(二次释放)、野指针等。
智能指针是一种封装了指针的类,它可以自动管理内存的生命周期,使得内存的分配和释放更加安全和方便。
示例:假设有一个Person
类表示一个人,该类有一个成员变量是name
,并且我们在动态内存中为其分配内存。
#include <iostream>
#include <cstring>
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 <iostream>
#include <memory>
#include <cstring>
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 <iostream>
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 使用引用计数来跟踪对象的引用数量,当引用计数为零时,对象会被正确地销毁。然而,使用原始指针来管理这种情况会导致无法释放的内存。
如果两个或多个对象相互引用,形成循环引用,而使用原始指针管理内存,可能导致内存泄漏,因为循环引用会导致引用计数无法归零,从而无法释放对象。所以在对对象的管理中我们会遇到很多原始指针解决不了的问题,所以才有了智能指针的由来。
内存泄漏
什么是内存泄漏
内存泄漏是指在程序运行过程中,分配的内存空间在不再需要时没有被释放,导致系统中的可用内存减少。内存泄漏的危害主要包括导致程序使用的内存越来越多,从而增加了垃圾回收的负担,使得程序运行变得更加缓慢。这对于长时间运行的服务或应用程序来说尤其是一个严重的问题。
内存泄漏的分类
内存泄漏可以分为以下几种常见的类型:
1.堆内存泄漏: 堆内存泄漏是指在动态分配内存时,没有正确释放这些内存导致的泄漏。这通常发生在使用 new 或 malloc 分配内存后忘记使用 delete 或 free 进行释放。
2.栈内存泄漏: 栈内存泄漏通常是由于在函数或代码块中分配的局部变量没有在该函数或代码块结束时被正确释放。栈内存泄漏通常较为轻微,因为在函数结束时,栈上的局部变量会自动被销毁。
3.全局/静态内存泄漏: 全局变量和静态变量在程序的整个生命周期内存在,如果没有在程序结束时释放相关内存,就会导致全局或静态内存泄漏。
4.循环引用: 循环引用是指两个或多个对象相互引用,形成一个循环结构,并且它们的引用计数无法归零。这种情况下,即使没有其他引用,这些对象也无法被垃圾回收。
5.虚拟机泄漏: 在使用托管运行时(如Java虚拟机、.NET运行时等)的环境中,有时会出现虚拟机泄漏,即运行时本身没有正确释放的内存。
6.资源泄漏: 除了内存之外,资源泄漏还可以包括其他类型的资源,例如文件句柄、网络连接等。如果这些资源在使用完毕后没有被释放,就会导致资源泄漏。
如何避免内存泄漏
避免内存泄漏是编程中非常重要的任务之一,以下是一些常见的方法:
1.使用智能指针: 使用C++的智能指针,如std::shared_ptr、std::unique_ptr等,可以自动管理内存的释放。这样可以减少手动释放内存的机会,防止忘记释放或重复释放的问题。
// 使用 std::shared_ptr
std::shared_ptr<int> smartPtr = std::make_shared<int>(42);
2.RAII(资源获取即初始化)原则: 使用对象生命周期管理资源。确保在对象创建时分配资源,在对象销毁时释放资源。智能指针正是基于这个原则设计的。
3.避免手动管理内存: 尽量避免使用 new 和 delete 进行手动内存管理。使用标准容器和智能指针等抽象层级更高的工具,它们能够更安全地管理内存。
4.使用析构函数: 在类的析构函数中释放在构造函数中分配的资源。确保资源的释放操作被正确实现。
class ResourceHolder {
public:
ResourceHolder() {
// 分配资源
resource = new Resource;
}
~ResourceHolder() {
// 释放资源
delete resource;
}
private:
Resource* resource;
};
5.避免循环引用: 当存在循环引用时,使用弱引用(std::weak_ptr)来打破循环引用关系,防止引用计数无法归零。
#include <iostream>
#include <memory>
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;
};
6.使用工具进行静态和动态分析: 使用工具如静态分析器(如Clang Static Analyzer、Cppcheck)、动态分析器(如Valgrind)等,帮助发现潜在的内存泄漏问题。
7.使用现代C++特性: C++11及其后续版本引入了许多现代C++特性,如移动语义、智能指针、Lambda表达式等,这些特性有助于更安全和高效地管理内存。
8.良好的编程习惯: 养成良好的编程习惯,注重代码的规范性和清晰性,有助于及早发现潜在的问题,并减少内存泄漏的发生。
智能指针的使用及底层原理
1.RAII
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;
}
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;
}
我们可以看到,通过SmartPtr
类对div
类新对象的建立,我们可以在其创建新对象时自动将指针进行托管,最终不用我们手动去调用析构函数,而是在作用域将结束时,通过SmartPtr
指向的div
对象指针自动调用析构,从而防止了内存泄漏的问题。
2.auto_ptr
auto_ptr 是 C++98 标准中引入的一种智能指针,用于管理动态分配的内存。它是第一个尝试提供自动内存管理的 C++ 标准库智能指针,然而,由于其独特的拥有权转移语义,导致了一些问题,因此在后续的 C++ 标准中被更现代的智能指针(如 std::unique_ptr 和 std::shared_ptr)所取代。 auto_ptr
具有独占所有权的特性,即当一个 auto_ptr
拥有某块动态分配的内存时,其他任何 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;
};
}
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 没有拥有权转移的问题,并提供了更好的所有权管理。
3.unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝,下面简化模拟实现了一份unique_Ptr来了解它的原理。
template<class T>
class unique_ptr
{
private:
// 防拷贝 C++98
// 只声明不实现
//unique_ptr(unique_ptr<T>& ap);
//unique_ptr<T>& operator=(unique_ptr<T>& 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;
}
4.shared_ptr
shared_ptr与unique_ptr最大的区别就是支持拷贝。
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1.shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2.在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3.如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4.如果不是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;
};
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 <memory>
#include <iostream>
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 不会增加引用计数,也不会影响节点的析构。
5.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;
};
#include <iostream>
#include <memory>
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
避免了循环引用。
注意: 在实际使用中,weak_ptr
通常用于解决循环引用问题,而不是直接与裸指针交互。weak_ptr
不会增加对象的引用计数,因此不会影响对象的生命周期。