C++11新特性学习笔记

文章目录

    • 主要内容
    • 编译方法
    • 智能指针
      • 智能指针主要解决以下问题:
      • shared_ptr内存模型
      • 智能指针使用案例
        • 智能指针可以自动释放占用的内存
      • Notes
      • shared_ptr共享的智能指针
      • shared_ptr的基本用法和常用函数
        • 初始化make_shared/reset
        • 获取原始指针get
        • 指定删除器
      • 使用shared_ptr注意的问题
        • 不要用一个原始指针初始化多个shared_ptr
        • 不要在函数实参中创建shared_ptr
        • 通过shared_from_this()返回this指针
        • 避免循环引用(难点,细品)
      • unique_ptr——独占的智能指针
      • weak_ptr——弱引用的智能指针
        • 什么是weakptr
        • weak_ptr作用
        • weak_ptr的基本用法
        • weak_ptr返回this指针
        • weak_ptr解决循环引用问题
        • weak_ptr使用注意事项
      • 智能指针安全性问题
        • case1:多线程代码操作的是同一个shared_ptr对象,此时不安全
        • case2:多线程代码操作的不是同一个shared_ptr对象
    • 2 右值引用和移动语义
      • 左值和右值
      • 左值引用和右值引用
        • 左值引用
        • 右值引用
        • 右值引用可以指向左值吗?
        • 左值引用、右值引用本身是都是左值
        • 右值引用优化性能,避免深拷贝
        • forward完美转发
      • emplace_back 减少内存拷贝和移动
      • 小结
    • 3 匿名函数lambda
      • 值捕获的变量在lambda表达式被创建时就被拷贝而非调用时拷贝
      • 表达式捕获
      • 泛型Lambda
      • 可变lambda
      • 其他方式
        • 1. [&, identifier_list]
        • 2. [=, identifier_list]
    • 拓展资料

主要内容

  • 智能指针
  • 右值引用
  • 匿名函数
  • STL容器
  • 正则表达式

编译方法

主要是要加上 -std=c++11或者更高的版本,比如 -std=c++14
编译范例:

 g++ -o test test.cpp -std=c++11

智能指针

智能指针主要解决以下问题:

  1. 内存泄漏:内存手动释放,使用智能指针可以自动释放
    malloc free; new delete
  2. 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题
    C++里面的四个智能指针: auto_ptr,shared_ptr,unique_ptr, weak_ptr 其中后三个是C++11支持,并且
    第一个已经被C++11弃用。
    几个指针的特点:
    unique_ptr独占对象的所有权,由于没有引用计数,因此性能较好。
    shared_ptr共享对象的所有权,但性能略差。
    weak_ptr配合shared_ptr解决循环引用的问题。

shared_ptr内存模型

C++11新特性学习笔记_第1张图片

shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个
引用计数(reference count), 一个弱计数(weak count)和其它一些数据。

C++11新特性学习笔记_第2张图片

智能指针使用案例

智能指针可以自动释放占用的内存
{
    shared_ptr<Buffer> buf = make_shared<Buffer>("auto free memory"); // Buffer
    对象分配在堆上,但能自动释放
    对比
    Buffer *buf = new Buffer("auto free memory");// Buffer对象分配在堆上,但需要手动
    delete释放
}

Notes

  1. 智能指针是否是线程安全的?

ref_count是安全的(原子计数)

指向对象的数据如果被修改,则是不安全的(如果要数据安全,还要自己加锁机制

  1. shared_ptr和unique_ptr不可混用!

shared_ptr共享的智能指针

std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析
构的时候,内存才会被释放。

shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个shared_ptr对象销毁时,被管理对象自动销毁。

简单来说,shared_ptr实现包含了两部分,
一个指向堆上创建的对象的裸指针,raw_ptr
一个指向内部隐藏的、共享的管理对象。share_count_object

use_count:当前这个堆上对象被多少对象引用了,简单来说就是引用计数。

shared_ptr的基本用法和常用函数

设shared_ptr有一个对象s:

  • s.get():返回shared_ptr中保存的裸指针;
  • s.reset(…):重置shared_ptr;多次调用无参的reset没有特殊效果
  • s.use_count() :返回shared_ptr的强引用计数;
  • s.unique() :若use_count()为1,返回true,否则返回false。

reset( )不带参数时,若智能指针s是唯一指向该对象的指针,则释放,并置空。若智能指针P不是唯
一指向该对象的指针,则引用计数减少1,同时将P置空。
reset( )带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指
针,则只减少引用计数,并指向新的对象。如

auto s = make_shared<int>(100);
s.reset(new int(200));
初始化make_shared/reset
// 智能指针初始化
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 = make_shared<int>(100);
//相当于
shared_ptr<int> sp1(new int(100));

不能将一个原始指针直接赋值给一个智能指针,例如,下面这种方法是错误的:

std::shared_ptr<int> p = new int(1);

shared_ptr不能通过“直接将原始这种赋值”来初始化,需要通过构造函数和辅助方法来初始化。

  • 对于一个未初始化的智能指针,可以通过reset方法来初始化;
  • 当智能指针有值的时候调用reset会引起引用计数减1
#include 
#include 
using namespace std;

int main()
{
    std::shared_ptr<int> p1;
    p1.reset(new int(1)); //将p1所指对象重置
    std::shared_ptr<int> p2 = p1;
    // 引用计数此时应该是2
    cout << "p2.use_count() = " << p2.use_count()<< endl;
    p1.reset();
    cout << "p1.reset()\n";
    // 引用计数此时应该是2
    cout << "p2.use_count()= " << p2.use_count() << endl;
    if(!p1) {
    	cout << "p1 is empty\n";
    }
    if(!p2) {
    	cout << "p2 is empty\n";
    }
    p2.reset();
    cout << "p2.reset()\n";
    cout << "p2.use_count()= " << p2.use_count() << endl;
    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()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,
遵守以下几个约定:

  • 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的
  • 保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
  • 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误
指定删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。

#include 
#include 
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表达

当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对
象,代码如下所示:

std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});

使用shared_ptr注意的问题

不要用一个原始指针初始化多个shared_ptr

会导致多个shared_ptr单独计数,如:

int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);
cout << p1.use_count() << endl; //1
cout << p2.use_count() << endl; //1

内存模型如下

C++11新特性学习笔记_第3张图片

不要在函数实参中创建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 
#include 
using namespace std;
class A
{
    public:
    shared_ptr<A> GetSelf()
    {
    return shared_ptr<A>(this); // 不要这么做
    }
    ~A()
    {
    cout << "Destructor A" << endl;
    }
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf(); //这里也是相当于拿裸指针初始化多个智能指针
    return 0;
}

上面代码会导致运行两次析构。因为sp1 和 sp2单独计数,离开作用域时分别会调用析构。

正确的返回this的shared_ptr做法是:让目标类继承std::enable_shared_from_this类,然后再子类中使用基类的成员函数shared_from_this来返回this的 shared_ptr

class A:public std::enable_shared_from_this<A>
{
public:
    	shared_ptr<A>GetSelf()
        {
			return shared_from_this();
        }
		~A()
        {
			cout<<"Destructor A"<<endl;
        }
};

int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf(); //ok
    
	return 0;
}
避免循环引用(难点,细品)
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;
}

分析如下:

  1. 当离开作用域前,obj A 的ref_count = 2, obj B 的ref_count = 2;

  2. 由于ap和bp(shared_ptr 局部变量)是存在栈上,故离开作用域时会先调用bp(注意是指针变量析构而不是指针变量指向对象的析构函)的析构函数,使得obj B的ref_count = 2 - 1 = 1 ;

然后调用ap的构造函数,使得 obj A的ref_count = 2 - 1 = 1 ;

  1. 此时obj A和 obj B的ref_count都不为0,故不会调用A和B的析构,从而也不会调用到B::aptr和A::bptr的析构,最终导致objA 和obj B都不会被析构

解法:把A和B的任何一个成员变量改为weak_ptr。

unique_ptr——独占的智能指针

  1. unique_ptr是一个独占型的智能指针,不能将其赋值给另一个unique_ptr
  2. unique_ptr可以指向一个数组
  3. 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独占特性,它和shared_ptr还有一些区别:

  • unique_ptr可以指向一个数组:
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]);

C++ 17及之后,shared_ptr也支持指向数组

  • unique_ptr需要确定删除器的类型,故不能像shared_ptr那样直接指定删除器,可这样写:
std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete p;});

weak_ptr——弱引用的智能指针

什么是weakptr

weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理对象。进行该对象内存管理的是那个强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段。

weak_ptr作用

当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。

weak_ptr设计目的是为了配合shared_ptr而引入的一种协助shared_ptr工作的智能指针。

它只能从一个shared_ptr或另一个weak_ptr对象构造,它的构造和析构不会引起应用技术的增加或减少。

weak_ptr的基本用法
  1. 通过use_count()方法获取当前观察资源的引用计数

    shared_ptr<int> sp(new int(10));
    weak_ptr<int> wp(sp);
    cout << wp.use_count() << endl; //结果讲输出1
    
  2. 通过expired()方法判断所观察资源是否已经释放

    shared_ptr<int> sp(new int(10));
    weak_ptr<int> wp(sp); //用shared_ptr初始化weak_ptr
    if(wp.expired())
    	cout << "weak_ptr无效,资源已释放";
    else
    	cout << "weak_ptr有效";
    
  3. lock() 的使用(难点

    lock()功能是锁住资源:1.资源仍在,后面expired()判断为true,即便其他线程释放资源,这里调用lock()返回的资源仍然可以用;2.资源已释放,则后面expired()判断为true

    std::weak_ptr<int> gw;
    //线程1
    void f2()
    {
        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;
            //线程2
            std::thread([&](){
                		//等待1s,转去执行线程1中内容
                       std::this_thread::sleep_for(std::chrono::seconds(1));
    
                       cout << "sp reset\n";
                       sp.reset();
                   }).detach(); //detach分离线程,让其在后台运行
    
            f2();
        }
        f2();
        return 0;
    }
    

    假设在多线程环境下,子线程为 std::thread创建出来,

    首先进入main()主线程创建出sp指向的资源,并且让weak_ptr指向sp所指向的资源,然后进入子线程等1秒

    主线程进入f2()调用lock()锁住并获得sp指向的 资源,并等待2s

    回到子线程,执行reset(),但资源不会被释放(因为上一步锁住并获得资源)

    回到主线程的f2()调用位置,执行expired()判断是否过期,结果是false,并输出gw Valid, *spt = 42

    然后回到子线程,释放sp指向的资源

    在主线程再次执行f2()中expired(),结果则为true。

weak_ptr返回this指针

shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生
std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是
std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用
shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回,再看前面的例子

using namespace std;
class A: public std::enable_shared_from_this<A>
{
public:
    shared_ptr<A>GetSelf()
    {
    return shared_from_this(); //
    }
    ~A()
    {
    cout << "Destructor A" << endl;
    }
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf(); // ok
    return 0;
}

输出结果:

Destructor A

在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内
部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,sp1的引用计数减为0,A对象会
被析构,不会出现A对象被析构两次的问题。

需要注意的是,获取自身智能指针的函数尽在shared_ptr的构造函数被调用之后才能使用,因为
enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造。

weak_ptr解决循环引用问题

智能指针的循环引用可能导致内存泄漏,可通过weak_ptr解决该问题,只要将A或B任意一个成员变量改为weak_ptr即可

class A;
class B;
class A {
public:
    std::weak_ptr<B> bptr; // 修改为weak_ptr
    ~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;
	return 0;
}

对A成员赋值时,即执行ap->bptr = bp;时,由于bptr是weak_ptr,并不会增加引用计数,故bp引用计数仍然为1,在离开作用域之后,bp引用计数减为0,B指针被析构,析构之后其内部的aptr计数减为1,然后在离开作用域时,ap引用计数从1减为0,故A对象被析构,不会发生内存泄漏。

weak_ptr使用注意事项

weak_ptr在使用前需要检查其合法性

weak_ptr<int> wp;
{
shared_ptr<int> sp(new int(1)); //sp.use_count()==1
wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;

因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了。
得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。
因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后
一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法知道到自己所容纳的那个指针
资源的当前状态。

智能指针安全性问题

引用计数本身是安全的,至于智能指针是否安全需要结合实际使用分情况讨论

case1:多线程代码操作的是同一个shared_ptr对象,此时不安全

如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr

std::thread td([&sp1]()){....});

又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用

void fn(shared_ptr<A>&sp) {
...
}
..
std::thread td(fn, sp1);
case2:多线程代码操作的不是同一个shared_ptr对象

这里指的是管理的数据是同一份,而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕
获的对象。

std::thread td([sp1]()){....});

另一个线程传递的shared_ptr是值传递而非引用:

void fn(shared_ptr<A>sp) {
...
}
..
std::thread td(fn, sp1);

这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的
对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。

2 右值引用和移动语义

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能

左值和右值

可以从2个角度判断:

  • 左值可以取地址、位于等号左边
  • 而右值没法取地址,位于等号右边。
int a = 6;
  • a可以通过 & 取地址,位于等号左边,所以a是左值。
  • 6位于等号右边,6没法通过 & 取地址,所以6是个右值。

再比如

struct A {
	A(int a = 0) {
		a_ = a;
	}
	int a_;
};
A a = A();

同样的,a可以通过 & 取地址,位于等号左边,所以a是左值。
A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值

左值引用和右值引用

引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝

左值引用

能指向左值,不能指向右值的就是左值引用:

int a = 5;
int &ref_a = a; // 左值引用指向左值,编译通过
int &ref_a = 5; // 左值引用指向了右值,会编译失败

引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。
但是,const左值引用是可以指向右值的:

const int &ref_a = 5; // 编译通过

const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const & 作为函数参数的原
因之一,如std::vectorpush_back

void push_back (const value_type& val);

若没有constvec.push_back(5)这样的代码就无法编译通过

右值引用

再看下右值引用,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值:

int &&ref_a_right = 5; // ok
int a = 5;
int &&ref_a_left = a; // 编译不过,右值引用不可以指向左值
ref_a_right = 6; // !!!右值引用的用途:可以修改右值
右值引用可以指向左值吗?

可以,使用std::move

int a = 5; // a是个左值
int &ref_a_left = a; // 左值引用指向左值
int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
cout << a; // 打印结果:5

在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值
了?并不是,打印出a的值仍然是5。

std::move 是一个非常有迷惑性的函数:

  • 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量;

  • 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左
    值。其实现等同于一个类型转换: static_cast(lvalue) 。 所以,单纯的std::move(xxx)
    不会有性能提升。

同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move
指向该左值:

int &&ref_a = 5;
ref_a = 6;
等同于以下代码:
int temp = 5;
int &&ref_a = std::move(temp);
ref_a = 6;
// 此时temp等于6!!!
左值引用、右值引用本身是都是左值

因为被声明出的左右值引用是有地址的,也位于等号左边

// 形参是个右值引用
void change(int&& right_value) {
	right_value = 8;
}
int main() {
    int a = 5; // a是个左值
    int &ref_a_left = a; // ref_a_left是个左值引用
    int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
    change(a); // 编译不过,a是左值,change参数要求右值
    change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
    change(ref_a_right); // !!!编译不过,右值引用ref_a_right本身也是个左值
    change(std::move(a)); // 编译通过
    change(std::move(ref_a_right)); // 编译通过
    change(std::move(ref_a_left)); // 编译通过
    change(5); // 当然可以直接接右值,编译通过
    cout << &a << ' ';
    cout << &ref_a_left << ' ';
    cout << &ref_a_right;
    // 打印这三个左值的地址,都是一样的
}

  • 右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值
  • 或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合前面章节对左值,
  • 右值的判定方式:其实引用和普通变量是一样的, int &&ref = std::move(a) 和 int a = 5 没有什
    么区别,等号左边就是左值,右边就是右值。

结论

  1. 从性能上讲,左右值引用没区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过 std::move指向左值,而左值引用只能指向左值(const左值引用也能指向右值)
  3. 作为函数形参时,右值引用更灵活。const左值虽然也可以做到左右值都接受,但它无法修改。
void f(const int& n) {
	n += 1; // 编译失败,const左值引用不能修改指向变量
}
void f2(int && n) {
	n += 1; // ok
}
int main() {
	f(5);
	f2(5);
}
右值引用优化性能,避免深拷贝

浅拷贝导致二次析构

class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	~A(){
        cout << "destructor A, m_ptr:" << m_ptr << endl;
        delete m_ptr;
        m_ptr = nullptr;
	}
private:
	int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}
int main()
{
    {
       //a和Get()获得的对象拥有相同值的指针m_ptr,导致离开作用域时二次析构
  		A a = Get(false); // 运行报错
    }
	cout << "main finish" << endl;
	return 0;
}

输出结果:

constructor A
constructor A
ready return
destructor A, m_ptr:0xf87af8
destructor A, m_ptr:0xf87ae8
destructor A, m_ptr:0xf87af8
main finish

有一次拷贝赋值 A a = Get(false);

深拷贝防止二次析构

class A
{
public:
    A() :m_ptr(new int(0)) {
    	cout << "constructor A" << endl;
    }
    //!!!深拷贝,直接重新分配内存拷贝一份指针成员所指的数据
    A(const A& a) :m_ptr(new int(*a.m_ptr)) {
   		cout << "copy constructor A" << endl;
    }
    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr << endl;
        delete m_ptr;
        m_ptr = nullptr;
    }
private:
	int* m_ptr;
};
// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
	A a;
	A b;
	cout << "ready return" << endl;
	if (flag)
		return a;
	else
		return b;
}
int main()
{
    {
        A a = Get(false); // 正确运行
	}
	cout << "main finish" << endl;
	return 0;
}

输出结果:

constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:0xea7af8
destructor A, m_ptr:0xea7ae8
destructor A, m_ptr:0xea7b08
main finish

移动构造函数(核心特性是剪切)

上面代码中拷贝构造是不必要的,上面代码中的Get函数会返回临时变量,然后通过这个临时变量拷贝构造一个新对象b,临时变量在拷贝构造完成后就销毁了,如果堆内存很大,那么这个拷贝构造的代价就很大,带来了额外的性能损耗,于是便可采用移动构造函数:

class A
{
public:
	A() :m_ptr(new int(0)) {
		cout << "constructor A" << endl;
	}
	A(const A& a) :m_ptr(new int(*a.m_ptr)) {
		cout << "copy constructor A" << endl;
	}
    // !!!移动构造函数,可以浅拷贝
    A(A&& a) :m_ptr(a.m_ptr) {
    	a.m_ptr = nullptr; // 为防止a析构时delete data,提前置空其m_ptr
    	cout << "move constructor A" << endl;
    }
    ~A(){
    	cout << "destructor A, m_ptr:" << m_ptr << endl;
    	if(m_ptr)
    	delete m_ptr;
    }
private:
	int* m_ptr;
};

// 为了避免返回值优化,此函数故意这样写
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
    	return a;
    else
    	return b;
}
int main()
{
    {
   		A a = Get(false); // 正确运行
    }
	cout << "main finish" << endl;
	return 0;
}

输出结果:

constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0
destructor A, m_ptr:0xfa7ae8
destructor A, m_ptr:0xfa7af8
main finish

上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现
中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了
对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时
值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷
贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重
要目的是用来支持移动语义的。

  • 移动语义可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少
    不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护
    (创建和销毁)对性能的影响。

  • 注意:传临时对象也是被认为是右值引用,进入移动构造函数

  • 如果没实现移动构造,但使用move,则会调用拷贝构造

forward完美转发

forward完美转发实现了参数在传递过程中保持其属性的功能。即若是左值,则传递之后仍是左值;若是右值则传递之后仍为右值。

注:现存在如下函数

Template<class T>
void func(T &&val);

根据前面所描述的,这种引用类型既可以对左值引用,亦可以对右值引用。
但要注意,引用以后,这个val值它本质上是一个左值!

int &&a = 10;
int &&b = a; //错误,无法将右值绑定到左值(a)

注意这里,a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,再用右值引用引用a这
是不对的。

因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但如果我们用std::forward (val),
就会按照参数原来的类型转发:

int &&a = 10;
int &&b = std::forward<int>(a);

通过下面例子,理解forward

template <class T>
void Print(T &t)
{
	cout << "L" << t << endl;
}
template <class T>
void Print(T &&t)
{
	cout << "R" << t << endl;
}
template <class T>
void func(T &&t)
{
    Print(t);
    Print(std::move(t));
    Print(std::forward<T>(t));
}
int main()
{
    cout << "-- func(1)" << endl;
    func(1);
    int x = 10;
    int y = 20;
    cout << "\n-- func(x)" << endl;
    func(x); // x本身是左值
    
    cout << "\n-- func(std::forward(y))" << endl;    
    func(std::forward<int>(y)); T为int,以右值方式转发y
    
    cout << "\n-- func(std::forward(y))" << endl;    
    func(std::forward<int&>(y));
    
    cout << "\n-- func(std::forward(y))" << endl;
    func(std::forward<int&&>(y));
    return 0;
}

注意:std::forward 则会得到T&&

输出结果

-- func(1)
L1
R1
R1

-- func(x)
L10
R10
L10

-- func(std::forward<int>(y))
L20
R20
R20  //注意到如果转发函数模板参数没给引用类型,那默认就是按照右值引用转发。

-- func(std::forward<int&>(y))
L20
R20
L20

-- func(std::forward<int&&>(y))
L20
R20
R20

emplace_back 减少内存拷贝和移动

emplace_back是就地构造,不用构造后再次复制到容器中。故效率比push_back

如:

vector<string> testVec;
testVec.push_back(string(16, 'a'));

上述代码中push_back底层实现为:

  1. string(16, ‘a’) 会创建一个string类型临时对象,这涉及一次调用string构造函数
  2. vector内会调用拷贝构造函数创建对象,这是第二次调用构造
  3. 最后push_back结束时,最开始的临时对象会被析构。故总共是两次构造和一次析构

emplace_back可以直接在vector中构建一个对象,而非创建一个临时对象再放进vector,再销毁。emplace_back可省略一次构造和一次析构,从而达到优化目的。

小结

  1. 右值引用仅仅通过改变资源所有者(剪切)来避免内存拷贝,从而提升性能。
  2. forward 能根据参数的实际类型转发给正确的函数
  3. emplace 系列函数通过直接构造对象的方式避免了内存的拷贝和移动。

3 匿名函数lambda

值捕获的变量在lambda表达式被创建时就被拷贝而非调用时拷贝

void test3()
{
    cout << "test3" << endl;
    int c = 12;
    int d = 30;
    auto Add = [c, d](int a, int b)->int {
        cout << "d = " << d << endl;
        return c;
    };
    d = 20;
    std::cout << Add(1, 2) << std::endl;
}

表达式捕获

上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值,
而不能捕获右值。
C++14之后支持捕获右值,允许捕获的成员用任意的表达式进行初始化,被声明的捕获变量类型会根据
表达式进行判断,判断方式与使用 auto 本质上是相同的:

void test9()
{
    cout << "test9" << endl;
    auto important = std::make_unique<int>(1);
    auto add = [v1 = 1, v2 = std::move(important)](int x, int y) -> int {
    return x + y + v1 + (*v2);
	};
	std::cout << add(3,4) << std::endl;
}

泛型Lambda

从C++14开始,lambda函数的形式参数可用auto关键字来生成广义泛型

void test10()
{
    cout << "test10" << endl;
    auto add = [](auto x, auto y) {
    	return x+y;
	};
	std::cout << add(1, 2) << std::endl;
	std::cout << add(1.1, 1.2) << std::endl;
}

可变lambda

  • 采用值捕获的方式,lambda不能修改其值,若果想修改,必须加mutable修饰(感觉有点反常理)
  • 采用引用捕获的方式,lambda可直接修改其值
void test12() {
    cout << "test12" << endl;
    int v = 5;
    // !!!值捕获方式,使用mutable修饰,可以改变捕获的变量值
    auto ff = [v]() mutable {return ++v;};
    v = 0;
    auto j = ff(); // j为6
}
void test13() {
    cout << "test13" << endl;
    int v = 5;
    // 采用引用捕获方式,可以直接修改变量值
    auto ff = [&v] {return ++v;};
    v = 0;
    auto j = ff(); // v引用已修改,j为1
}

其他方式

1. [&, identifier_list]

identifier_list是一个逗号分隔的列表,包含0个或多个来自所在函数的变量,这些变量采用值捕获的方式,其他变量则被隐式捕获,采用引用方式传递,
identifier_list中的名字前面不能使用&。

2. [=, identifier_list]

identifier_llist中变量采用引用方式被捕获。其他变量则被隐式捕获,采用引用传递。

identifier_list中的名字前面必须加&

拓展资料

  1. C++11新特性,补充读物
  2. https://blog.csdn.net/solstice/article/details/8547547 为什么多线程读写shared_ptr要加锁
  3. 其他内容见pdf课件

你可能感兴趣的:(C/C++,c++,学习,开发语言)