C++11的新特性:智能指针、左值引用、移动语义、匿名函数lambda

目录

一:智能指针

1:智能指针解决的问题

2:C++11中的三个智能指针

2.1:shared_ptr

2.1.1、shared_ptr的初始化

2.1.2、内存模型

2.1.3、shared_ptr共享的智能指针

2.1.4、shared_ptr的常用函数

2.1.5、指定删除器

2.1.6、使用shared_ptr要注意的问题

2.2、unique_ptr独占的智能指针

2.2.1、unique_ptr是一个独占型的智能指针

2.2.2、unique_ptr可以指向一个数组

2.2.3、unique_ptr需要确定删除器的类型

2.3、weak_ptr弱引用的智能指针

2.3.1、什么是weak_ptr?

2.3.2、weak_ptr的基本用法

2.3.3、weak_ptr返回this指针

2.3.4、weak_ptr解决循环引用问题

2.3.5、weak_ptr使用注意事项

2.4、智能指针安全性问题

二 :右值引用和移动语义

1、什么是左值,右值?

2、什么是左值引用、右值引用?

3、std::move

4、emplace_back 减少内存拷贝和移动

三、匿名函数lambda

1、基本语法

2、捕获列表

2.1、值捕获

2.2、引用捕获

2.3、隐式捕获

2.4、空捕获列表

2.5、表达式捕获

2.6、泛型 Lambda

2.7、可变lambda


一:智能指针

1:智能指针解决的问题

        1.1:内存泄漏:裸指针需要手动进行释放,但是智能指针可以自动释放。

        1.2:共享所有指针的传播和释放,比如多线程使用同一个对象时析构的问题。

2:C++11中的三个智能指针

        unique_ptr:独占对象的所有权,由于没有引用计数,因此性能较好。

        shared_ptr:共享对象的所有权,但性能略差。

        weak_ptr:配合shared_ptr,解决循环引用的问题。

2.1:shared_ptr

2.1.1、shared_ptr的初始化
//下面的两种方法都可以
auto sp1 = make_shared<int>(100);    //1

shared_ptr<int> sp1 = make_shared<int>(100);  //2

//相当于
shared_ptr<int> sp1(new int(100));

//如果直接将原始指针(裸指针)赋值给智能指针,是错误的
std::shared_ptr<int> p = new int(1);
2.1.2、内存模型

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

当出现下面的情况的时候,这个图就变成了这样

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2(p1);

2.1.3、shared_ptr共享的智能指针

        std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析构的时候,内存才会被释放。 shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个 shared_ptr对象销毁时,被管理对象自动销毁。简单来说分为下面两种:

一个指向堆上创建的对象的裸指针,raw_ptr

一个指向内部隐藏的、共享的管理对象。share_count_object

2.1.4、shared_ptr的常用函数

(1)、s.reset(…):重置shared_ptr;

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

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

//比如下面的,重新赋值,那么指向100的引用计数减一,而指向200的引用计数加一
auto s = make_shared<int>(100);
s.reset(new int(200));


s.use_count();        //返回shared_ptr的强引用计数;
s.unique();           //若use_count()为1,返回true,否则返回false。(查看是否唯一)

(2)、s.get():返回shared_ptr中保存的裸指针;

当需要获取原始指针时,可以通过get方法来返回原始指针,代码如下所示:

std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get();  // 获取裸指针
//如果现在不小心 delete p;,那么智能指针就成了空指针

我们使用这个get命令一般遵循下面的约定:

1.不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的

2.保存为裸指针,不知什么时候就会变成空悬指针,保存为shared_ptr,则产生了独立指针

3.不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

2.1.5、指定删除器

        如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象,因此也需要我们指定删除器。下面是具体用法:

 //这里是指定删除器
 void DeleteIntPtr(int *p) {
     cout << "call DeleteIntPtr" << endl;
     delete p;
 }

 int main()
 {
     std::shared_ptr<int> p(new int(1), DeleteIntPtr);
     return 0;
 }

//这里是动态数组的删除器,使用了lambda表达式,更简洁,后面会讲
std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});
2.1.6、使用shared_ptr要注意的问题

(1)、不要用一个原始指针初始化多个shared_ptr

 int *ptr = new int;
 shared_ptr<int> p1(ptr);
 shared_ptr<int> p2(ptr); // 逻辑错误

(2)、不要在函数实参中创建shared_ptr

        因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也 可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还 没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针。

 function(shared_ptr<int>(new int), g()); //有缺陷

 //正确写法
 shared_ptr<int> p(new int);
 function(p, g()); 

(3)、通过shared_from_this()返回this指针

        不要将this指针作为shared_ptr返回出来,因为this指针本质上是一个裸指针,因此,这样可能会导致重复析构。正确返回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;
 }

(4)、避免循环引用

 class A;
 class B;

 class A {
 public:
     std::shared_ptr<B> bptr;    //在a中包含b
     ~A() {
         cout << "A is deleted" << endl;
     }
 };

 class B {
 public:
     std::shared_ptr<A> aptr;    //在b中包含a
   ~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,导致两个指针都不会被析构,产生内存泄漏。
}

2.2、unique_ptr独占的智能指针

2.2.1、unique_ptr是一个独占型的智能指针

        unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将 一个unique_ptr赋值给另一个unique_ptr。

        unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其 他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。

        std::make_shared是c++11的一部分,但std::make_unique不是。它是在c++14里加入标准库的。

//不允许进行赋值操作,独占
unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr;  // 报错,不能复制

//可以使用move转移,并且自身就不再拥有,但是不能复制
unique_ptr<T> my_ptr(new T);                     // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr);  // 正确
unique_ptr<T> ptr = my_ptr;                      // 报错,不能复制

//使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的
//一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。
auto upw1(std::make_unique<Widget>());     // with make func
std::unique_ptr<Widget> upw2(new Widget);  // without make func
2.2.2、unique_ptr可以指向一个数组
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;

std::shared_ptr<int []> ptr2(new int[10]);  // 这个是不合法的

//需要用到删除器
std::shared_ptr<int> ptr2(new int[10], [](int *p) { delete [] p;});
2.2.3、unique_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;}); // 错误

std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete  p;}); //正确,需要指定删除器的类型

2.3、weak_ptr弱引用的智能指针

2.3.1、什么是weak_ptr?

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

        weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从 一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

2.3.2、weak_ptr的基本用法

        通过use_count()方法获取当前观察资源的引用计数。通过expired()方法判断所观察资源是否已经释放。通过lock方法获取监视的shared_ptr。

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl;  //结果讲输出1
if(wp.expired())
    cout << "weak_ptr无效,资源已释放";
else
    cout << "weak_ptr有效";
 std::weak_ptr<int> gw;
 void f()
 {
 	auto spt = gw.lock();
 	if(gw.expired()) {
 		cout << "gw无效,资源已释放";
 	}
 	else {
 		cout << "gw有效, *spt = " << *spt << endl;
	 }
 }
 int main()
 {
 	{
 		auto sp  = std::make_shared<int>(42);
 		gw = sp;
 		f();
 	}
 	f();
 	return 0;
 }
2.3.3、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返回。

2.3.4、weak_ptr解决循环引用问题

        在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过 weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr,这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发生内存泄漏。

 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中包含a
   ~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;    
}
2.3.5、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;

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

2.4、智能指针安全性问题

(1)、多线程代码操作的是同一个shared_ptr的对象,此时是不安全的。 比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用,这时候必然不是线程安全的。

(2)、多线程代码操作的不是同一个shared_ptr的对象 这里指的是管理的数据是同一份,而shared_ptr不是同一个对象。比如多线程回调的lambda的是按值捕获的对象。另个线程传递的shared_ptr是值传递,而非引用:这时候每个线程内看到的sp,他们所管理的是同一份数据,用的是同一个引用计数。但是各自是不同的 对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的。

(3)、需要注意:所管理数据的线程安全性问题。显而易见,所管理的对象必然不是线程安全的,必然 sp1、 sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,那对象的数据安全必然是需要对象 A自己去保证。

二 :右值引用和移动语义

1、什么是左值,右值?

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

int a = 6;

2、什么是左值引用、右值引用?

2.1、左值引用:能指向左值,不能指向右值的就是左值引用,引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。const左值引用不会修改指向的值,因此可以指向右值,这也是为什么要使用原因之一,如 std::vector 的 push_back。

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

//const左值引用是可以指向右值的
const int &ref_a = 5;  // 编译通过

void push_back (const value_type& val);

2.2、右值引用:它的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值。

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

3、std::move

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

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

        std::move 是一个非常有迷惑性的函数: 不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量; 但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:  static_cast(lvalue) 。 所以,单纯的std::move(xxx) 同样的,不会有性能提升。右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move 指向该左值。

        move移动语义:move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持。

4、emplace_back 减少内存拷贝和移动

        对于STL容器,C++11后引入了emplace_back接口。 emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。

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

        上述语句足够简单易懂,将一个string对象添加到testVec中。底层实现: 首先,string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程。 其次,vector内会创建一个新的string对象,这是第二次构造。 最后在push_back结束时,最开始的临时对象会被析构。加在一起,这两行代码会涉及到两次 string构造和一次析构。

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

三、匿名函数lambda

1、基本语法

        语法规则:lambda表达式可以看成是一般函数的函数名被略去,返回值使用了一个 -> 的形式表示。唯 一与普通函数不同的是增加了“捕获列表”。一般情况下,编译器可以自动推断出lambda表达式的返回类型,所以我们可以不指定返回类型,但是如果函数体内有多个return语句时,编译器无法自动推断出返回类型,此时必须指定返回类型。

[捕获列表](参数列表) mutable(可选) 异常属性 -> 返回类型 {
// 函数体
}


//[捕获列表](参数列表)->返回类型{函数体}
 int main()
 {
     auto Add = [](int a, int b)->int {
         return a + b;
     };
     std::cout << Add(1, 2) << std::endl;        
     return 0;

     auto Add = [](int a, int b) {    //这样是自动推导
         return a + b;
     };
 }

2、捕获列表

        有时候,需要在匿名函数内使用外部变量,所以用捕获列表来传递参数。根据传递参数的行为,捕获列 表可分为以下几种:

2.1、值捕获

        与参数传值类似,值捕获的前提是变量可以拷贝,不同之处则在于,被捕获的变量在 lambda表达式被 创建时拷贝,而非调用时才拷贝:

void test3()
 {
     cout << "test3" << endl;
     int c = 12;
     int d = 30;        //这里是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;    
//会cout出30,返回12.
 }

2.2、引用捕获

与引用传参类似,引用捕获保存的是引用,值会发生变化。

 void test5()
 {
     cout << "test5" << endl;
     int c = 12;
     int d = 30;
     auto Add = [&c, &d](int a, int b)->int {
         c = a; // 编译对的
        cout << "d = " << d  << endl;
         return c;    //这里返回a的值
     };
     d = 20;
     std::cout << Add(1, 2) << std::endl;
//那么会cout出20,然后返回1.
 }

2.3、隐式捕获

        手动书写捕获列表有时候是非常复杂的,这种机械性的工作可以交给编译器来处理,这时候可以在捕获 列表中写一个 & 或 = 向编译器声明采用引用捕获或者值捕获。

void test7()
 {
    int c = 12;
    int d = 30;
    // 把捕获列表的&改成=再测试
    auto Add = [&](int a, int b)->int {
            c = a; // 编译对的
            cout << "d = " << d  << endl;
             return c;
     };
     d = 20;
     std::cout << Add(1, 2) << std::endl;
     std::cout << "c:" << c<< std::endl;
 }

2.4、空捕获列表

        捕获列表'[]'中为空,表示Lambda不能使用所在函数中的变量。

void test8()
 {
     cout << "test7" << endl;
     int c = 12;
     int d = 30;
    // 把捕获列表的&改成=再测试
    // [] 空值,不能使用外面的变量
    // [=] 传值,lambda外部的变量都能使用
    // [&] 传引用值,lambda外部的变量都能使用
    auto Add = [](int a, int b)->int {
         cout << "d = " << d  << endl; // 编译报错
         return c;// 编译报错
    };
     d = 20;
     std::cout << Add(1, 2) << std::endl;
     std::cout << "c:" << c<< std::endl;
 }

2.5、表达式捕获

        上面提到的值捕获、引用捕获都是已经在外层作用域声明的变量,因此这些捕获方式捕获的均为左值, 而不能捕获右值。 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;
 }

2.6、泛型 Lambda

        在C++14之前,lambda表示的形参只能指定具体的类型,没法泛型化。从 C++14 开始, Lambda 函数的形式参数可以使用 auto关键字来产生意义上的泛型。

//泛型 Lambda C++14
 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;
 }

2.7、可变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
 }

C++11的新特性就讲解到这里,感谢大家的收看!https://xxetb.xetslk.com/s/2D96kH

  • 27
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值