目录
- 1智能指针
-
- 1.`shared_ptr`
-
- 1.1 `shared_ptr`的基本用法
- 使用`shared_ptr`要注意的问题
- 运用
- 2.`unique_ptr`独占的智能指针
-
- 示例:管理动态内存
- 3.`weak_ptr`弱引用的智能指针
-
- `weak_ptr`的基本用法
- lock 的作用:
- weak_ptr返回this指针
- weak_ptr解决循环引用问题
- `weak_ptr`使用注意事项
- 4.智能指针安全性问题
-
- 情况 1:多线程代码操作的是同一个 `shared_ptr` 的对象
- 情况 2:多线程代码操作的不是同一个 shared_ptr 的对象
- 2右值引用和移动语义
-
-
- 2.0&&的特性
- 2.1 右值引用优化性能,避免深拷贝
- 2.2 移动(move )语义
- 2.4 `forward` 完美转发
- 2.5 emplace_back 减少内存拷贝和移动
- 2.6 `unordered container` 无序容器
-
- 2.6.1 `map`和`unordered_map`的差别
- 2.7 小结
-
- 3匿名函数`lambda`
-
- 3.1 匿名函数的基本语法为:
- 3.2 捕获列表
- 3.3 匿名函数的简写
- 3.4 Lambda捕获列表
- 4 C++11标准库(STL)
-
- 4.1 容器简介
- **4.2 迭代器简介**
- 4.3 算法简介
- 5正则表达式
1智能指针
C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
1.shared_ptr
std::shared_ptr
使用引用计数,每一个shared_ptr
的拷贝都指向相同的内存。再最后一个shared_ptr
析构的时候,内存才会被释放。shared_ptr
共享被管理对象,同一时刻可以有多个shared_ptr
拥有对象的所有权,当最后一个shared_ptr
对象销毁时,被管理对象自动销毁。
简单来说,shared_ptr
实现包含了两部分,
- 一个指向堆上创建的对象的裸指针,
raw_ptr
裸指针(Raw Pointer)是指常规的指针,它直接指向内存中的某个地址,不带有任何智能或管理机制。与智能指针(如std::shared_ptr
和std::unique_ptr
)不同,裸指针没有自动管理资源的能力,因此它需要程序员手动管理内存的分配和释放。裸指针通常是 C++ 中最基本的指针类型,用于指向对象或数据。 - 一个指向内部隐藏的、共享的管理对象。
share_count_object
第一部分没什么好说的,第二部分是需要关注的重点:
use_count
,当前这个堆上对象被多少对象引用了,简单来说就是引用计数。
1.1 shared_ptr
的基本用法
通过构造函数、std::shared_ptr
辅助函数和reset
方法来初始化shared_ptr
,代码如下:
// 智能指针初始化
std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));
if(p3) {
cout << "p3 is not null";
}
我们应该优先使用make_shared
来构造智能指针,因为他更高效。
auto sp1 = make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100));
不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:
std::shared_ptr<int> p = new int(1);
shared_ptr
不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。 对于一个未初始化的智能指针,可以通过reset
方法来初始化,当智能指针有值的时候调用reset
会引起引用计数减1。
另外智能指针可以通过重载的bool
类型操作符来判断。
#include <iostream>
#include <memory>
using namespace std;
int main() {
// 创建一个空的 shared_ptr
std::shared_ptr<int> p1;
// 使用 reset() 初始化 p1,指向一个动态分配的 int 对象,值为 1
p1.reset(new int(1));
// 创建另一个 shared_ptr p2,指向与 p1 相同的对象
std::shared_ptr<int> p2 = p1;
// 此时引用计数应该是 2
cout << "p2.use_count() = " << p2.use_count() << endl;
// 调用 p1 的 reset(),释放对原对象的所有权,p1 不再指向任何对象
p1.reset();
cout << "p1.reset()\n";
// 引用计数此时应该是 1,因为 p2 仍然指向原对象
cout << "p2.use_count() = " << p2.use_count() << endl;
// 判断 p1 是否为空
if (!p1) {
cout << "p1 is empty\n";
}
// 判断 p2 是否为空
if (!p2) {
cout << "p2 is empty\n";
}
// 调用 p2 的 reset(),释放 p2 对对象的所有权,p2 不再指向任何对象
p2.reset();
cout << "p2.reset()\n";
// 此时 p2 不再指向对象,所以引用计数是 0
cout << "p2.use_count() = " << p2.use_count() << endl;
// 判断 p2 是否为空
if (!p2) {
cout << "p2 is empty\n";
}
return 0;
}
- 获取原始指针
当需要获取原始指针时,可以通过get
方法来返回原始指针,代码如下所示:
std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); //
不小心 delete p;
谨慎使用p.get()
的返回值,如果你不知道其危险性则永远不要调用get()函数。
p.get()
的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:
- 不要保存
p.get()
的返回值 ,无论是保存为裸指针还是shared_ptr
都是错误的 - 保存为裸指针不知什么时候就会变成空悬指针,保存为
shared_ptr
则产生了独立指针 - 不要
delete p.get()
的返回值 ,会导致对一块内存delete
两次的错误
指定删除器
- 如果用
shared_ptr
管理非new
对象或是没有析构函数的类时,应当为其传递合适的删除器。
示例代码如下:
//1-1-delete
#include <iostream>
#include <memory>
using namespace std;
void DeleteIntPtr(int *p) {
cout << "call DeleteIntPtr" << endl;
delete p;
}
int main()
{
std::shared_ptr<int> p(new int(1), DeleteIntPtr);
return 0;
}
当p
的引用计数为0时,自动调用删除器DeleteIntPtr
来释放对象的内存。删除器可以是一个lambda
表达式,上面的写法可以改为:
std::shared_ptr<int> p(new int(1), [](int *p) {
cout << "call lambda delete p" << endl;
delete p;});
当我们用shared_ptr
管理动态数组时,需要指定删除器,因为shared_ptr
的默认删除器不支持数组对象,代码如下所示:
std::shared_ptr<int> p3(new int[10], [](int *p) {
delete [] p;});
使用shared_ptr
要注意的问题
- 不要用一个原始指针初始化多个
shared_ptr
,例如下面错误范例:
int *ptr = new int; // 使用裸指针分配内存
shared_ptr<int> p1(ptr); // p1 管理裸指针
shared_ptr<int> p2(ptr); // p2 也尝试管理同一块内存
在这段代码中,ptr
是一个裸指针,它指向通过 new
动态分配的内存。然后你用两个 shared_ptr(p1 和 p2)
来管理这块内存。
问题所在:
std::shared_ptr
会通过引用计数来管理资源的生命周期。当shared_ptr
被销毁时,会减少引用计数,并在引用计数为零时自动释放资源(调用 delete)。- 如果你直接用裸指针(ptr)初始化两个
shared_ptr
,这会导致 双重删除 的问题,因为 p1 和 p2 都会在销毁时调用
delete,而裸指针已经在外部被 new 分配过一次,这样会导致两次删除同一块内存,从而发生未定义行为(例如程序崩溃)。
- 不要在函数实参中创建
shared_ptr
,对于下面的写法:
function(shared_ptr<int>(new int), g()); //有缺陷
因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int
,然后调用g()
,如果恰好g()
发生异常,而shared_ptr
还没有创建, 则int
内存泄漏了,正确的写法应该是先创建智能指针,代码如下:
shared_ptr<int> p(new int);
function(p, g());
- 通过
shared_from_this()
返回this
指针。不要将this
指针作为shared_ptr
返回出来,因为this
指针本质上是一个裸指针,因此,这样可能会导致重复析构,看下面的例子。
#include <iostream>
#include <memory>
using namespace std;
class A {
public:
shared_ptr<A> GetSelf() {
return shared_ptr<A>(this); // 不要这么做:错误的做法
}
~A() {
cout << "Deconstruction A" << endl;
}
};
int main() {
shared_ptr<A> sp1(new A); // 创建 shared_ptr sp1 来管理 A 的实例
shared_ptr<A> sp2 = sp1->GetSelf(); // 错误:使用 this 指针创建一个新的 shared_ptr
return 0;
}
运行后调用了两次析构函数。
在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,而他们之间是没有任何关系的,在离开作用域之后this
将会被构造的两个智能指针各自析构,导致重复析构的错误。
正确返回this的shared_ptr
的做法是:让目标类通过std::enable_shared_from_this
类,然后使用基类的成员函数shared_from_this()
来返回this
的shared_ptr
,如下所示。
#include <iostream>
#include <memory>
using namespace std;
class A : public std::enable_shared_from_this<A> // 继承 enable_shared_from_this
{
public:
shared_ptr<A> GetSelf()
{
return shared_from_this(); // 返回指向当前对象的 shared_ptr
}
~A()
{
cout << "Deconstruction A" << endl;
}
};
int main()
{
// 创建 shared_ptr sp1 来管理 A 类的实例
shared_ptr<A> sp1 = make_shared<A>(); // 使用 make_shared 更加安全和高效
// 使用 sp1 调用 GetSelf() 返回当前对象的 shared_ptr
shared_ptr<A> sp2 = sp1->GetSelf(); // sp2 和 sp1 共享同一个对象的所有权
// 程序结束时,sp1 和 sp2 会自动销毁并释放内存
return 0;
}
在weak_ptr
章节我们继续讲解使用shared_from_this()
的原因。
- 避免循环引用。循环引用会导致内存泄漏,比如:
#include <iostream>
#include <memory>
using namespace std;
class B; // 提前声明 B 类
class A {
public:
shared_ptr<B> bptr; // A 持有 B 的 shared_ptr
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
shared_ptr<A> aptr; // B 持有 A 的 shared_ptr
~B() {
cout << "B is deleted" << endl;
}
};
int main() {
{
shared_ptr<A> ap(new A); // 创建 shared_ptr A
shared_ptr<B> bp(new B); // 创建 shared_ptr B
ap->bptr = bp; // A 指向 B
bp->aptr = ap; // B 指向 A
}
cout << "main leave" << endl; // 由于循环引用,A 和 B 并没有析构
return 0;
}
运行结果:
main leave
循环引用 是指对象 A 和对象 B 通过shared_ptr
相互持有对方,形成一个环。这样,A 和 B 的引用计数都会增加,但由于它们相互引用,它们的引用计数永远不会归零。这会导致它们的析构函数永远不会被调用,内存永远不会释放,从而产生内存泄漏。
循环引用导致ap
和bp
的引用计数为2,在离开作用域之后,ap
和bp
的引用计数减为1,并不回减为0,导致两个指针都不会被析构,产生内存泄漏。
解决的办法是把A和B任何一个成员变量改为weak_ptr
,具体方法见weak_ptr
章节。
解释:
A 和 B 的相互引用:
- A 类持有一个
shared_ptr<B> bptr
,即 A 引用 B。 - B 类持有一个
shared_ptr<A> aptr
,即 B 引用 A。
引用计数增加:
- 当你创建
shared_ptr<A> ap(new A)
时,A 的引用计数是 1。 - 当你创建
shared_ptr<B> bp(new B)
时,B 的引用计数是 1。
然后,通过以下代码:
ap->bptr = bp; // A 持有指向 B 的 shared_ptr
bp->aptr = ap; // B 持有指向 A 的 shared_ptr
- A 对象的 bptr 持有 B 对象的
shared_ptr
,这意味着 B 对象的引用计数增加到 2。 - B 对象的 aptr 持有 A 对象的
shared_ptr
,这意味着 A 对象的引用计数也增加到 2。
循环引用的结果:
- 在
main()
函数结束时,ap
和bp
会超出作用域并应该被销毁。 - 然而,由于 A 和 B 互相持有
shared_ptr
,它们的引用计数永远不会减少到 0。 - A 对象的引用计数是 2(由 ap 和 bptr 引用),而 B 对象的引用计数也是 2(由
bp
和aptr
引用)。 - 由于循环引用,A 和 B 的引用计数永远不会归零,因此它们的析构函数不会被调用,对象的内存也永远无法被释放,导致内存泄漏。
运用
资源管理(自动释放内存)
shared_ptr
可以有效管理资源(如动态分配的内存、文件句柄、数据库连接等),确保资源在不再需要时被自动释放,避免内存泄漏。
示例:动态内存管理
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() {
std::cout << "MyClass Constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass Destructor" << std::endl;
}
};
int main() {
{
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
std::shared_ptr<MyClass> ptr2 = ptr1; // 引用计数变为 2
std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl;
} // ptr1 和 ptr2 在作用域结束时被销毁,MyClass 对象被自动释放
std::cout << "Out of scope." << std::endl;
return 0;
}
输出:
MyClass Constructor
ptr1 use count: 2
MyClass Destructor
Out of scope.
在这个例子中,当 ptr1
和 ptr2
超出作用域时,它们的引用计数减少到零,MyClass 对象会被自动销毁,析构函数被调用,释放了资源。
2.unique_ptr
独占的智能指针
unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个unique_ptr
赋值给另一个unique_ptr
。下面的错误示例。
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr; // 报错,不能复制
unique_ptr
不允许复制,但可以通过函数返回给其他的unique_ptr
,还可以通过std::move
来转移到其他的unique_ptr
,这样它本身就不再拥有原来指针的所有权了。例如
unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr<T> ptr = my_ptr; // 报错,不能复制
std::make_shared是c++11的一部分,但std::make_unique
不是。它是在c++14里加入标准库的。
auto upw1(std::make_unique<Widget>()); // with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func
使用new的版本重复了被创建对象的键入,但是make_unique
函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。
除了unique_ptr
的独占性, unique_ptr
和shared_ptr
还有一些区别,比如
unique_ptr
可以指向一个数组,代码如下所示
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的
std::shared_ptr
是一个引用计数的智能指针,它允许多个指针共享对同一资源的所有权,并在最后一个指针超出作用域时释放资源。shared_ptr
通常用于单个对象,但对于数组类型的资源,它不直接支持。标准库中的 shared_ptr
是设计为管理单个对象的内存,并不直接支持数组类型(即 int[]
),即使你用 new
分配的是数组。
unique_ptr
指定删除器和shared_ptr
有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){
delete p;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){
delete p;}); // 错误
unique_ptr
需要确定删除器的类型,所以不能像shared_ptr
那样直接指定删除器,可以这样写:
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){
delete p;}); // 正确
关于shared_ptr
和unique_ptr
的使用场景是要根据实际应用需求来选择。如果希望只有一个智能指针管理资源或者管理数组就unique_ptr
,如果希望多个智能指针管理同一个资源就用shared_ptr
。
示例:管理动态内存
我们将创建一个 Person
类,类的构造函数中分配一个动态字符串并且在析构函数中自动释放它。
代码实现
#include <iostream>
#include <memory>
#include <string>
class