大纲
智能指针
智能指针的入门案例
unique_ptr 对象的介绍
unique_ptr
是 C++ 11 提供的用于防止内存泄漏的智能指针中的一种实现,独享被管理对象指针所有权的智能指针。unique_ptr
对象包装了一个原始指针,并负责其生命周期。当该对象被销毁时,会在其析构函数中删除关联的原始指针。unique_ptr
实现了 ->
和 *
运算符的重载,因此它可以像普通指针一样使用。
unique_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 26 27 28 29 30
| #include <iostream>
using namespace std;
class Task {
public: Task(int id) { this->id = id; cout << "构造函数被调用" << endl; }
~Task() { cout << "析构函数被调用" << endl; }
int getId() { return this->id; }
private: int id;
};
int main() { unique_ptr<Task> taskPtr(new Task(23)); cout << "id = " << taskPtr->getId() << endl; return 0; }
|
程序运行输出的结果如下:
unique_ptr<Task>
对象 taskPtr
接受原始指针作为参数。当 main
函数退出时,该对象超出作用范围就会自动调用自身的析构函数。在 unique_ptr<Task>
对象 taskPtr
的析构函数中,会删除关联的原始指针,这样就不用专门执行 Task 对象的 delete
操作了。以后不管函数正常退出还是异常退出(由于某些异常),也会始终调用 taskPtr
对象的析构函数。因此,原始指针将始终被删除并防止内存泄漏。
unique_ptr 对象独享所有权
unique_ptr
对象始终是关联的原始指针的唯一所有者,因此开发者无法通过拷贝构造函数或赋值运算符复制 unique_ptr
对象的副本,只能移动它。由于每个 unique_ptr
对象都是原始指针的唯一所有者,因此在其析构函数中,它可以直接删除关联的指针,不需要任何参考计数。
智能指针的基础操作
获取被管理对象的原始指针
在 unique_ptr
对象上调用 get()
函数,可以获取管理对象的原始指针
1
| Task *p1 = taskPtr.get();
|
检查 unique_ptr 对象是否为空
有两种方法创建一个空的 unique_ptr
对象,因为没有与之关联的原始指针,所以它是空的
1
| unique_ptr<int> ptr = nullptr;
|
有两种方法可以检查 unique_ptr
对象是否为空或者是否有与之关联的原始指针
1 2 3
| if (!ptr) { cout<<"ptr is empty"<<endl; }
|
1 2 3
| if (ptr == nullptr){ cout<<"ptr is empty"<<endl; }
|
使用原始指针创建 unique_ptr 对象
要创建非空的 unique_ptr
对象,需要在创建对象时在其构造函数中传递原始指针
1
| unique_ptr<Task> taskPtr(new Task(22));
|
或者
1
| unique_ptr<Task> taskPtr(new unique_ptr<Task>::element_type(23));
|
不能通过赋值的方法创建 unique_ptr
对象
1
| unique_ptr<Task> taskPtr = new Task();
|
智能指针的进阶操作
重置 unique_ptr 对象
在 unique_ptr
对象上调用 reset()
函数可以重置它,即它会 delete
已关联的原始指针,并将 unique_ptr
对象设置为空
unique_ptr 对象不允许复制
由于 unique_ptr
不可复制,只能移动。因此,无法通过拷贝构造函数或赋值运算符创建 unique_ptr
对象的副本
1 2 3 4 5 6
| unique_ptr<Task> taskPtr1(new Task(22)); unique_ptr<Task> taskPtr2(new Task(35));
unique_ptr<Task> taskPtr4 = taskPtr1;
taskPtr2 = taskPtr1;
|
转移 unique_ptr 对象的所有权
不允许复制 unique_ptr
对象,但可以转移它们。这意味着 unique_ptr
对象可以将自身关联的原始指针的所有权转移给另一个 unique_ptr
对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| unique_ptr<Task> taskPtr1(new Task(55));
unique_ptr<Task> taskPtr2 = move(taskPtr1);
if (taskPtr1 == nullptr) { cout << "taskPtr1 is empty" << endl; }
if (taskPtr2 != nullptr) { cout << "taskPtr2 is not empty" << endl; }
cout << taskPtr2->getId() << endl;
|
程序运行输出的结果如下:
1 2 3
| taskPtr1 is empty taskPtr2 is not empty 55
|
释放 unique_ptr 对象关联的原始指针
在 unique_ptr
对象上调用 release()
函数,将释放其关联的原始指针的所有权,并返回原始指针,同时设置 unique_ptr
对象为空。特别注意,这里是释放其关联的原始指针的所有权,并没有 delete
原始指针,而调用 reset()
函数则会 delete
原始指针
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| unique_ptr<Task> taskPtr1(new Task(55));
if (taskPtr1 != nullptr) { cout << "taskPtr1 is not empty" << endl; }
Task* ptr = taskPtr1.release();
if (taskPtr1 == nullptr) { cout << "taskPtr1 is empty" << endl; }
cout << "id = " << ptr->getId() << endl;
|
程序运行输出的结果如下:
1 2 3
| taskPtr1 is not empty taskPtr1 is empty id = 55
|
C++ 14 使用原始指针创建 unique_ptr 对象
C++ 引入了新的语法,可以使用 make_unique
来创建 unique_ptr
对象,省去了 new
关键字的使用
1
| unique_ptr<Task> taskPtr = make_unique<Task>(34);
|
原子操作的使用
原子操作简介
所谓的原子操作,取的就是 “原子是最小的、不可分割的最小个体” 的意义,它表示在多个线程访问同一个全局资源的时候,能够确保在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。在以往的 C++ 标准中并没有对原子操作进行规定,开发人员往往是使用汇编语言,或者是借助第三方的线程库,例如 Intel 的 pthread
来实现。在新标准 C++ 11 中,引入了原子操作的概念,并通过这个新的头文件提供了多种原子操作数据类型,例如 atomic_bool
、atomic_int
等等。如果在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问;这样就可以保证多个线程访问这个共享资源的正确性,从而避免了锁的使用,提高了效率。在新标准 C++ 11 中,atomic
对 int
、char
、bool
等基础数据结构进行了原子性封装,在多线程环境中,对 atomic
对象的访问不会造成资源竞争,利用 atomic
可实现数据结构的无锁设计。
atomic 的简介
在新标准 C++ 11 中,新增了 atomic
关键字,可以使用它定义一个原子类型,详见 C++ 参考手册一、C++ 参考手册二。
成员函数 | 说明 |
---|
store | 原子地以非原子对象替换原子对象的值 |
load | 原子地获得原子对象的值 |
operator= | 存储值于原子对象 |
is_lock_free | 检查原子对象是否免锁 |
operator T | 从原子对象加载值 |
exchange | 原子地替换原子对象的值,并获得它先前持有的值 |
compare_exchange_weak、compare_exchange_strong | 原子地比较原子对象与非原子参数的值,若相等则进行交换,若不相等则进行加载 |
特化成员函数 | 说明 |
---|
fetch_add | 原子地将参数加到存储于原子对象的值,并返回先前保有的值 |
fetch_sub | 原子地从存储于原子对象的值减去参数,并获得先前保有的值 |
fetch_and | 原子地进行参数和原子对象的值的逐位与,并获得先前保有的值 |
fetch_or | 原子地进行参数和原子对象的值的逐位或,并获得先前保有的值 |
fetch_xor | 原子地进行参数和原子对象的值的逐位异或,并获得先前保有的值 |
operator++ 、operator++(int) 、operator-- 、operator--(int) | 令原子值增加或者减少一 |
operator+= 、operator-= 、operator&= 、operator^= | 加、减,或者与原子值进行逐位与、异或 |
值得一提的是,所谓特化函数,也就是 atomic
自身提供的,可以进行原子操作的函数。使用这些函数进行的操作,都是原子的。
atomic 的使用案例
加锁不使用 atomic
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| #include <iostream> #include <ctime> #include <mutex> #include <vector> #include <thread>
using namespace std;
mutex mtx; size_t total = 0;
void threadFun() { for (int i = 0; i < 1000000; i++) { unique_lock<mutex> lock(mtx); total++; } }
int main(void) { clock_t start_time = clock();
vector<thread> threads; for (int i = 0; i < 10; i++) { threads.push_back(thread(threadFun)); } for (auto& thad : threads) { thad.join(); }
cout << "total number:" << total << endl;
clock_t end_time = clock(); cout << "耗时:" << end_time - start_time << "ms" << endl;
return 0; }
|
程序运行输出的结果如下:
1 2
| total number:10000000 耗时:615ms
|
不加锁使用 atomic
与加锁相比,使用原子操作(atomic)能大大地提高程序的运行效率。
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 26 27 28 29 30 31 32 33 34 35 36 37 38
| #include <iostream> #include <ctime> #include <mutex> #include <vector> #include <thread>
using namespace std;
atomic<size_t> total(0);
void threadFun() { for (int i = 0; i < 1000000; i++) { total++; } }
int main(void) { clock_t start_time = clock();
vector<thread> threads; for (int i = 0; i < 10; i++) { threads.push_back(thread(threadFun)); } for (auto& thad : threads) { thad.join(); }
cout << "total number:" << total << endl;
clock_t end_time = clock(); cout << "耗时:" << end_time - start_time << "ms" << endl;
return 0; }
|
程序运行输出的结果如下:
1 2
| total number:10000000 耗时:321ms
|
为什么要定义一个原子类型
举个例子,int64_t
类型,在 32 位机器上为非原子操作。更新时该类型的值时,需要进行两步操作(高 32 位、低 32 位)。如果多线程操作该类型的变量,且在操作时未加锁,可能会出现读脏数据的情况。解决该问题的话,可以使用加锁,或者提供一种定义原子类型的方法。
1 2
| std::atomic<int64_t> value;
|
1 2
| int64_t x = value.load(std::memory_order_relaxed);
|
1 2
| int64_t x = 10; value.store(x, std::memory_order_relaxed)
|
atomic 不能与 string 一起使用
特别注意,atomic
关键字不能与 string
类型一起使用,因为 string
不是可简单复制的类型(TriviallyCopyable),详见 C++ 参考文档:
The primary std::atomic template may be instantiated with any TriviallyCopyable type T satisfying both CopyConstructible and CopyAssignable.
1 2 3 4 5 6
| #include <iostream>
int main() { std::atomic<std::string> str{ "Hello" }; return 0; }
|
上述代码编译后,C++ 编译器会出现编译错误,如下所示:
1
| error C2338: atomic<T> requires T to be trivially copyable, copy constructible, move constructible, copy assignable, and move assignable.
|
关于 C++ 编译器为什么会对 std::atomic<std::string>
给出简单的可复制错误,在 Stack Overflow 上找到了一个类似的问题可供参考。
参考博客