Linux C/C++服务器
C++智能指针与右值引用
智能指针、右值引用、STL
编译
版本比较低的编译器要指明c++11或c++14
g++ -o main main.cpp -std=c++11
g++ -o main main.cpp -std=c++14
1. 智能指针
智能指针主要解决以下问题:
- 内存泄漏:内存手动释放,使用智能指针可以自动释放
- 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题
C++里面的四个智能指针:auto_ptr,shared_ptr,unique_ptr,weak_ptr其中后三个是C++11支持,并且第一个已经被C++11弃用。
- unique_ptr独占对象所有权,由于没有引用计数,因此性能较好。
- shared_ptr共享对象所有权,但性能略差。
- weak_ptr配合shared_ptr,解决循环引用的问题。
1.1 shared_ptr共享智能指针
1.1.1 内存模型
shared_ptr内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块包含一个引用计数(reference count),一个弱计数(weak count)和一些其他数据。
share_ptr的实现包含了两部分,
- 一个是指向堆上创建的对象裸指针,Ptr to T
- 一个指向内部隐藏的、共享的控制块,Control Block,Reference Count指当前这个堆上对象被多少对象引用了,也被称为引用计数,当销毁一个引用对象引用计数就减一,当引用计数为0后,堆上数据T自动销毁。
1.1.2 基本用法
- 初始化make_shared/reset
std::shared_ptr<int> p1 = make_shared<int>(1);
std::shared_ptr<int> p2(new int(1));
std::shared_ptr<int> p3 = p2;
std::shared_ptr<int> p4;
p4.reset(new int(1));
我们优先使用make_shared来构建智能指针,因为它更高效
不能将一个原始指针直接赋值给一个智能指针
std::shared_ptr<int> p = new int(1); //错误赋值
- 指针获取/指定删除器
//如果用share_ptr管理非new对象或是没有析构函数的类时。应为其传递合适的删除器;
void DeleteIntPtr(int *p){
delete p;
}
int main(){
std::shared_ptr<int> p(new int(1), DeletIntPtr);
}
//当p的引用计数为0时,自动调用DeletIntPtr来释放对象的内存。删除器可以是一个lambda表达式,上面的写法可以改写为:
std::shared_ptr<int> p(new int(1), [](int *p) {delete p;});
//当我们使用shared_ptr管理动态数组时,需指定删除器,**因为shared_ptr的默认删除器不支持数组对象**
std::shared_ptr<int> p(new int[10], [](int *p) {delete [] p;});
虽然可以通过get()来获取原始指针,但最好不要用p.get()这种方法,很容易不小心delte p;如果你不知道get()的危险性则永远不要使用get()函数
1.1.3 需要注意的问题
- 不要用一个原始指针初始化多个shared_ptr
int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); //逻辑错误
- 不要在函数实参中创建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_form_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); // 不要这么做
return shared_from_this(); // 正确做法
}
~A()
{
cout << "Destructor A" << endl;
}
};
int main()
{
shared_ptr<A> sp1(new A);
shared_ptr<A> sp2 = sp1->GetSelf();
return 0;
}
- 避免循环引用
循环引用造成内存泄漏
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::shared_ptr<A> aptr;
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl; //循环引用导致ap bp退出了作用域没有析构
return 0;
}
循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不会减为0,导致两个指针都不会被析构,产生内存泄漏。
解决办法是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr章节
1.2 unique_ptr独占智能指针
1.2.1 基本用法
- unique_ptr是一个独占的智能指针,不能将其赋值给另一个unique_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; // 报错,不能复制
- unique_ptr可以指向一个数组
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]); // 这个是不合法的,必须要有数组删除器
- unique_ptr需要确定删除器的类型
unique_ptr指定删除器和shared_ptr有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){deletep;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){deletep;}); // 错误
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;}); //需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器
关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。
如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
1.3 weak_ptr弱引用智能指针
share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也会被释放。
1.3.1 基本用法
- use_count()、expired()方法
shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl; //1.通过use_count()方法获得当前观察资源的引用计数 结果输出1 wp(sp)并不会增加引用计数!
if(wp.expired()) //2.通过expired()方法判断所观察资源是否已经释放
cout << "无效,资源已释放";
- 通过lock方法获取监视的shared_ptr
std::weak_ptr<int> gw;
void f()
{
cout << "lock\n";
auto spt = gw.lock(); //锁好资源再去判断是否有效
std::this_thread::sleep_for(std::chrono::seconds(2));
if(gw.expired()) {
cout << "gw Invalid, resource released\n";
}
else {
cout << "gw Valid, *spt = " << *spt << endl;
}
}
int main()
{
{
auto sp = std::make_shared<int>(42);
gw = sp;
std::thread([&](){
std::this_thread::sleep_for(std::chrono::seconds(1));
cout << "sp reset\n";
sp.reset(); //释放sp资源
}).detach();
f(); //gw任然有效,因为lock
}
f();
return 0;
}
- weak_ptr解决循环引用问题
在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr
#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
std::shared_ptr<B> bptr;
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::weak_ptr<A> aptr; // 修改为weak_ptr
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
{
std::shared_ptr<A> ap(new A);
std::shared_ptr<B> bp(new B);
ap->bptr = bp;
bp->aptr = ap;
}
cout<< "main leave" << endl;
return 0;
}
这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,ap会被析构,析构后其内部的bptr绑定的bp的引用计数会由2减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。
2. 右值引用与移动语义
C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。
什么是左值、右值?
- 左值可以取地址,位于等号左边;
- 右值没法取地址,位于等号右边,比如常量、表达式、返回右值函数等;
int a = 6;
- a可以通过 & 取地址,位于等号左边,所以a是左值。
- 6位于等号右边,6没法通过 & 取地址,所以6是个右值。
2.1 左值引用
int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值,但是const左值引用可以指向右值
const int &ref_a = 5; // 编译通过
void push_back (const value_type& val); //函数传常数 vec.push_back(5) 就是左值引用的问题,我们平时可能都在用只是不知道叫法
2.2 右值引用
右值,就是在内存没有确定存储地址、没有变量名,表达式结束就会销毁的值。
右值引用,就是绑定到右值的引用,通过&&来获得右值引用。
可以将右值引用归纳为:非常量右值引用只能绑定到非常量右值上;常量右值引用可以绑定到非常量右值、常量右值上。
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=20; //常量左值(有确定存储地址,也有变量名)
const int a2=20; //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&b1=a; //错误,a是一个非常量左值,不可以被非常量右值引用绑定
int &&b2=a1; //错误,a1是一个常量左值,不可以被非常量右值引用绑定
int &&b3=10; //正确,10是一个非常量右值,可以被非常量右值引用绑定
int &&b4=a1+a2; //错误,(a1+a2)是一个常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=a; //错误,a是一个非常量左值,不可以被常量右值引用绑定
const int &&c2=a1; //错误,a1是一个常量左值,不可以被常量右值引用绑定
const int &&c3=a+a1; //正确,(a+a1)是一个非常量右值,可以被常量右值引用绑定
const int &&c4=a1+a2; //正确,(a1+a2)是一个常量右值,不可以被常量右值引用绑定
从上述可以发现,常量左值引用可以绑定到右值上,但右值引用不能绑定任何类型的左值,若想利用右值引用绑定左值该怎么办呢?
2.3 move移动语义
C++11中提供了一个标准库move函数获得绑定到左值上的右值引用,即直接调用std::move告诉编译器将左值像对待同类型右值一样处理,但是被调用后的左值将不能再被使用。
int a=10; //非常量左值(有确定存储地址,也有变量名)
const int a1=20; //常量左值(有确定存储地址,也有变量名)
//非常量右值引用
int &&d1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被非常量右值引用绑定
int &&d2=std::move(a1); //错误,将常量左值a1转换为常量右值,不可以被非常量右值引用绑定
//常量右值引用
const int &&c1=std::move(a); //正确,将非常量左值a转换为非常量右值,可以被常量右值引用绑定
const int &&c2=std::move(a1); //正确,将常量左值a1转换为常量右值,可以被常量右值引用绑定
可以发现,编译器利用std::move将左值强制转换为相同类型的右值之后,引用情况跟右值是一模一样的。
- 左值引用绑定到有确定存储空间以及变量名的对象上,表达式结束后对象依然存在;右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式等临时对象上,赋值表达式结束后就对象就会被销毁。
- 左值引用后可以利用别名修改左值对象;右值引用绑定的值不能修改。
2.4 引入右值引用有什么意义?
- 减少内存拷贝,提高效率,某些情况下,需要拷贝一个对象然后将其销毁,如:临时类对象的拷贝就要先将旧内存的资源拷贝到新内存,然后释放旧内存,引入右值引用后,就可以让新对象直接使用旧内存并且销毁原对象(右值引用也是引用),这样就减少了内存和运算资源的使用,从而提高了运行效率;
- 移动含有不能共享资源的类对象,像IO、unique_ptr这样的类包含不能被共享的资源(如:IO缓冲、指针),因此,这些类对象不能拷贝但可以移动。这种情况,需要先调用std::move将左值强制转换为右值,再进行右值引用。
2.5 forward完美转发
forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值。
现存在一个函数
Template<class T>
void func(T &&val);
根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!
看下面例子
int &&a = 10;
int &&b = a; //错误
注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这是不对的。
因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),就会按照参数原来的类型转发;
int &&a = 10;
int &&b = std::forward<int>(a);
3. STL容器
容器、迭代器
- 标准容器类:
array(固定大小数组)、deque(队列)、forward_list(单链表)、list(双向链表)、vector(变长数组) - 有序关联容器(红黑树实现):
set:快速查找,无重复元素
multiset:快速查找,可有重复元素
map:一对一映射,无重复元素,基于键快速查找
multimap:一对一映射,可有重复元素,基于键快速查找 - 无序容器(hash):
unordered_set:快速查找,无重复元素
unordered_multiset:快速查找,可有重复元素
unordered_map:一对一映射,无重复元素,基于键快速查找
unordered_multimap:一对一映射,可有重复元素,基于键快速查找 - 标准库容器类:
stack:后进先出(LIFO)
queue:先进先出(FIFO)
priority_queue:优先级最高的元素先出,堆排序