0%

智能指针

1. 智能指针

  • 智能指针是一种用于管理动态分配的内存的工具,能够帮助避免内存泄漏悬挂指针等问题。
  • 智能指针是C++11标准引入的一个重要特性,它们基于RAII(资源获取即初始化)原则,利用对象生命周期的概念,在对象生命周期结束时自动释放资源。
  • 智能指针实际上是一个类对象,它封装了原始指针,并在其生命周期结束时负责释放指针所指向的内存。
  • 智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。
  • C++11引入了三种指针:std::shared_ptrstd::unique_ptrstd::weak_ptr

2. std::shared_ptr

共享指针,允许多个指针指向同一块内存,它使用引用计数来跟踪有多少个指针指向了该对象。并且会在最后一个指针不再指向该内存区域时自动释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <memory>
int main(){
// 使用make_shared创建shared_ptr
std::shared_ptr<int> ptr = std::make_shared<int>(42);
//或构造函数初始化
std::shared_ptr<int>(new int(42));
// 拷贝复制
std::shared_ptr<int> ptr2 = ptr1; //引用计数+1
//获取引用计数
std::cout <<"引用计数:" << ptr1.use_count() << std::endl;

return 0;// 当ptr1和ptr2都超出作用域时,所管理的内存会被自动释放
}
  • 创建shared_ptr

    使用std::make_shared函数来创建std::shared_ptr,它会自动分配内存并构造对象,同时返回一个指向该对象的std::shared_ptr

  • 拷贝和赋值

    将一个std::shared_ptr赋值给另一个时,引用计数会增加。当所有指向该对象的std::shared_ptr都被销毁时,引用计数会减少。只有当引用计数为0时,资源才会被释放。

  • 引用计数

    std::shared_ptr内部维护了一个引用计数器,用于跟踪有多少个std::shared_ptr指向了相同的资源。可以通过use_count()方法获取引用计数。

3. std::weak_ptr

循环引用指的是两个或多个对象彼此之间相互引用,导致它们的引用计数永远不会变为零,从而造成内存泄漏。在使用std::shared_ptr时,循环引用是一个常见的问题,因为std::shared_ptr的引用计数机制可能导致对象永远无法被释放。如以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <memory>

class A {
public:
std::shared_ptr<A> next;

A() {
std::cout << "A 构造函数" << std::endl;
}

~A() {
std::cout << "A 析构函数" << std::endl;
}
};

int main() {
std::shared_ptr<A> a1 = std::make_shared<A>();
std::shared_ptr<A> a2 = std::make_shared<A>();

a1->next = a2;
a2->next = a1; // 循环引用

return 0;
}

在这个例子中,a1a2相互引用,即a1next指针指向a2,而a2next指针又指向a1。这样一来,它们之间的引用计数永远不会变为零,因为彼此都在互相引用。即使在main函数结束时,a1a2的引用计数也不会为零,导致A类对象永远无法被销毁,造成内存泄漏。

std::weak_ptrstd::shared_ptr的一种弱引用,它不会增加引用计数。用于解决std::shared_ptr的循环引用带来的内存泄漏问题。可以通过lock()方法获得一个std::shared_ptr,如果原来的std::shared_ptr已经被销毁,则返回一个空指针。

上述示例中,可以将next成员改为std::weak_ptr类型,这样就不会导致循环引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <memory>
#include <iostream>

class A {
public:
std::weak_ptr<A> next; // 使用weak_ptr

A() {
std::cout << "A 构造函数" << std::endl;
}

~A() {
std::cout << "A 析构函数" << std::endl;
}
};

int main() {
std::shared_ptr<A> a1 = std::make_shared<A>();
std::shared_ptr<A> a2 = std::make_shared<A>();

a1->next = a2;
a2->next = a1; // 循环引用被打破

return 0;
}

在这个修改后的示例中,A类的next成员现在是std::weak_ptr类型,这意味着a1->nexta2->next不会增加A对象的引用计数。因此,即使a1a2相互引用,它们之间的循环引用也被打破了。这样在main函数结束时,A类对象的引用计数会变为零,对象会被正确地销毁,从而避免了内存泄漏问题。

4. std::unique_ptr

std::unique_ptr是一种独占所有权的智能指针,它确保在其生命周期内,只有一个指针可以指向该对象。当std::unique_ptr被销毁时,它所管理的对象也会被自动释放。

  • 创建unique_ptr
1
2
3
4
5
6
7
8
9
10
11
#include <memory>

int main() {
// 使用make_unique创建unique_ptr
std::unique_ptr<int> ptr = std::make_unique<int>(42);

// 或者使用构造函数
std::unique_ptr<int> ptr2(new int(42));

return 0;
}
  • 移动语义

std::unique_ptr是独占所有权的,因此它不支持拷贝语义,但支持移动语义。这意味着可以通过移动操作将资源所有权从一个std::unique_ptr转移到另一个。

1
2
3
4
5
6
7
8
9
10
11
#include <memory>

int main() {
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr1); // 移动所有权到ptr2

// 此时ptr1不再拥有资源,它指向nullptr
// ptr2拥有资源,可以安全地使用

return 0;
}
  • 释放资源

std::unique_ptr超出作用域时,它所管理的资源会被自动释放。

1
2
3
4
5
6
7
8
9
10
11
#include <memory>

void someFunction() {
std::unique_ptr<int> ptr = std::make_unique<int>(42);
// 在这里可以安全地使用ptr
} // ptr超出作用域,所管理的内存会被自动释放

int main() {
someFunction();
return 0;
}
  • 自定义删除器

std::unique_ptr支持自定义删除器,可以指定在释放资源时调用的函数或者函数对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <memory>
#include <iostream>

void customDeleter(int* ptr) {
std::cout << "自定义删除器被调用" << std::endl;
delete ptr;
}

int main() {
std::unique_ptr<int, decltype(&customDeleter)> ptr(new int(42), customDeleter);

return 0;
}