智能指针、左值引用右值引用、lambda表达式

文章目录

  • 1. 智能指针(shared_ptr、unique_ptr、weak_ptr)
    • 1.1 智能指针用来解决什么问题
    • 1.2 shared_ptr
      • 1.2.1 shared_ptr内存模型
      • 1.2.2 shared_ptr的基本用法
      • 1.2.3 使用shared_ptr要注意的问题
    • 1.3 unique_ptr
    • 1.4 weak_ptr
      • 1.4.1 weak_ptr的基本用法
    • 1.5 智能指针的安全性问题
  • 2. 左值引用与右值引用
      • 2.1 移动构造函数
      • 2.2 移动(move)语义
      • 2.3 forward 完美转发
  • 3. lambda 表达式
    • 3.1 匿名函数的基本用法

1. 智能指针(shared_ptr、unique_ptr、weak_ptr)

1.1 智能指针用来解决什么问题

 智能指针主要解决以下问题:
   1. 内存泄漏:在堆上申请的内存手动释放(new/malloc),使用智能指针内存可以自动释放。
   2.共享所有权指针的传播和释放:比如多线程用同一个对象的析构问题。
智能指针、左值引用右值引用、lambda表达式_第1张图片
 智能指针的区别:
   shared_ptr共享对象的所有权,性能越差;
   unique_ptr独享对象所有权,由于没有引用计数,因此性能较好;
   weak_ptr配合shared_ptr,解决循环引用的问题。

1.2 shared_ptr

1.2.1 shared_ptr内存模型

  std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。再最后一个shared_ptr析构的时候,内存才会被释放。
  std::shared_ptr内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个引用计数(reference count), 一个弱计数(weak count)和其它一些数据。
  use_count,当前这个堆上对象被多少对象引用了,简单来说就是引用计数。
智能指针、左值引用右值引用、lambda表达式_第2张图片

1.2.2 shared_ptr的基本用法

·初始化

  1. 初始化make_shared/reset

  1. 通过构造函数,std::shared_ptr辅助函数和reset方法来初始化shared_ptr;
  2. 优先使用make_shared来构造指针指针,因为他更高效。
  3. 不能讲一个原始指针赋值给一个智能指针。
  4. 当智能指针有值时调用reset()会引起引用计数减1.

//shared_ptr的初始化:通过构造函数、make_shared、reset()方法
int main(){
	shared_ptr<int> p1(new int(1));	//通过构造函数初始化
	shared_ptr<int> p2 = p1;	//拷贝构造函数
	shared_ptr<int> p3;
	p3.reset(new int(100));		//reset()方法

	shared_ptr<int> p4 = make_shared<int>(100);

	//不能将一个裸指针赋值给一个智能指针
	//shraed_ptr p5 = new int(5);	//错误的

	if(p1){
		cout << "p1 use_count():  " << p1.use_count() << endl;	// 2
		p2.reset();		//引用计数-1
		cout << "p1 use_count():  " << p1.use_count() << endl;	// 1
	}
	return 0;
	
}

  1. 获取原始指针get
	//使用get()方法获取裸指针p,delete p会导致裸指针被释放两次
	{
		shared_ptr<int> ptr = make_shared<int>(100);
		int *p = ptr.get();				
		//delete p;					//double free or corruption (out)
	}
  1. 指定删除器
int main(){

	//如果使用shared_ptr管理非new对象或者是没有析构函数的类时,应当为其传递合适的删除器
	shared_ptr<int> p1(new int(10), DeleteIntPtr);

	//删除器可以是一个lambda表达式
	shared_ptr<int> p2(new int(20), [](int *p)->void{
		cout << "call lambda delete p" << endl;
		delete p;
	});

	//当我们使用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持对数组对象。
	shared_ptr<int> p3(new int[10], [](int *p){
		cout << "call lambda delete p[]" << endl;
		delete []p;
	});
	
}

1.2.3 使用shared_ptr要注意的问题

  1. 不要使用一个原始指针初始化多个shared_ptr,会出现同一个资源被多次释放的错误。
  2. 不要在函数实参中创建shared_ptr,不同编译器函数的参数计算顺序是不是一样的,可能会存在已经new创建的对象资源,由于异常shared_ptr智能指针还来不及创建,出现内存泄漏。
  3. 通过shared_from_this()返回this指针,this指针本身就是一个裸指针,函数返回shared_ptr的this指针,为其他的shared_ptr指针初始化,其本质相当于使用同一个原始指针初始化多个shared_ptr,会导致资源被释放多次。
  4. 避免循环引用,循环引用导致智能指针的引用计数为2,但是离开作用域之后智能指针的引用计数减为1,并没有减为0,导致两个指针都不会被析构,造成内存泄漏。
    解决的办法是把A和B任何一个成员变量改为weak_ptr
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;  // 析构函数后,才去释放成员变量
    }
};
void cycle_shared_ptr(){
	shared_ptr<A> pa(new A());
	shared_ptr<B> pb(new B());

	pa->bptr = pb;
	pb->aptr = pa;
}
int main(){
	//使用一个原始指初始化多个shared_ptr,会导致对象被多次释放
	int *ptr = new int(10);
	shared_ptr<int> p1(ptr);
	//shared_ptr p2(ptr);	//free(): double free detected in tcache 2

	/*
		不要在函数实参中创建shared_ptr,因为不同编译器对函数参数的计算顺序约定是不一样的。
		可能是从右到左,也可能是从左到右。
		所以,有可能是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建,则会导致内存泄漏。
	*/
	//function(shared_ptr(new int), g()); //有缺陷

	//循环引用,没有调用析构函数
	cycle_shared_ptr();
}

1.3 unique_ptr

  1. unique_ptr 是一个独占型的智能指针,不能将一个unique_ptr对象赋值给另一个unique_ptr对象。
  2. unique_ptr 可以指向一个数组对象。
  3. unique_pt 需要指定删除器的类型。
  4. 如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr。
int main()
{
	//unique_ptr是一个独享型智能指针,不能将一个unique_ptr赋值给另外一个unique_ptr
  	auto p1 = make_unique<int>(10);	
	//unique_ptr p2 = p1;		//错误
	//使用std::move强unique_ptr对象转移
	unique_ptr<int> p3 = move(p1);	//正确
	if(!p1){
		cout << "p1 is null" << endl;
	}else{
		cout << "p1 is not null" << endl;
	}

	//unique_ptr可以指向一个数组
	unique_ptr<int []> ptr1(new int[10]);	//正确
	shared_ptr<int []> ptr2(new int[10]);	//错误

	//unique_ptr需要指定删除器的类型
	std::unique_ptr<int, void(*)(int*)> ptr3(new int(1), [](int *p){
		cout << "unique_ptr call lambda delete p" << endl;
		delete p;
	});

	std::shared_ptr<int> ptr4(new int(1), [](int *p){
		cout << "shared_ptr call lambda delete p" << endl;
		delete p;
	});
    return 0;
}

1.4 weak_ptr

  weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。它只能从一个shared_ptr 或 另一个weak_ptr对象构造。
  weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在。weak_ptr还可以返回this指针和解决循环引用的问题。

int main()
{
	shared_ptr<int> p = make_shared<int>(10);
	//weak_ptr p1(new int(10));	//错误的,只能从一个shared_ptr 或 weak_ptr对象构造
  	weak_ptr<int> p2(p);
	weak_ptr<int> p3(p2);
	//p use_count(): 1
	cout << "p use_count(): " << p3.use_count() << endl;	//weak_ptr不会改变引用计数器
	
    return 0;
}

1.4.1 weak_ptr的基本用法

  1. 通过use_count()方法获取当前观察对象的引用计数
  2. 通过expired()方法判断观察者资源是否已经释放
  3. 通过lock方法获取监视的shared_ptr
#include 
#include 
using namespace std;


class A;
class B;

class A {
public:
    std::weak_ptr<B> bptr; // 修改为weak_ptr
    int *val;
    A() {
        val = new int(1);
    }
    ~A() {
        cout << "A is deleted" << endl;
        delete  val;
    }
};

class B {
public:
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B is deleted" << endl;
    }
};
//weak_ptr解决循环引用的问题
void test(){
	std::shared_ptr<A> aptr(new A);
	std::shared_ptr<B> bptr(new B);
	aptr->bptr = bptr;
	bptr->aptr = aptr;
}
int main(){
	std::weak_ptr<A> wp;
    
        std::shared_ptr<A> ap(new A);
		std::shared_ptr<A> ap2 = ap;
        wp = ap;
    
    cout<< "wp.use_count() " << wp.use_count() << " , wp.expired(): " << wp.expired() << endl;
    if(!wp.expired()) {
        // wp不能直接操作对象的成员、方法
        //cout << "wp->val" << *(wp->val) << endl;	//错误,error: base operand of ‘->’ has non-pointer type ‘std::weak_ptr
        std::shared_ptr<A> ptr = wp.lock(); // 需要先lock获取std::shared_ptr
        *(ptr->val) = 20;  // 是不是有问题了?
        cout << "ptr->val  " << *(ptr->val) << endl;
    }
	
	test();
}

  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智能指针返回。

#include 
#incldue <merroy>
using namespace std;

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

int main(){
	shared_ptr<A> ptr1 = make_shared<A>();
	shared_ptr<A> ptr2 = ptr1.GetSelf();
}

1.5 智能指针的安全性问题

情况1:多线程代码操作的是同一个shared_ptr的对象,此时是不安全的。
情况2:多线程代码操作的不是同一个shared_ptr的对象 。

为什么多线程读写 shared_ptr 要加锁?陈硕的博客-CSDN博客多线程读写

2. 左值引用与右值引用

  1. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  2. 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  3. 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。

2.1 移动构造函数

#include 
using namespace std;

class A{
public:
	A():m_ptr(new int(10)){
		cout << "A constructor" << endl;
	};
	A(const A& a):m_ptr(new int(*a.m_ptr)){
		cout << "A copy constructor" << endl;
	}
	A(A&& a):m_ptr(a.m_ptr){
		a.m_ptr = nullptr;
		cout << "move constructor" << endl;
	}
	~A(){ 
		cout << "Destruvtor A, " << m_ptr<< endl;
		if(m_ptr) delete m_ptr;
		m_ptr = nullptr;
	}
private:
	int *m_ptr;
	
};

A Get(bool flag){
	A a;
	A b;
	cout << "================" << endl;
	if(flag)
		return a;
	else
		return b;
}

int main()
{
	{
		A a = Get(false);
	}
	cout << "main finish" << endl;
	return 0;
}

执行结果:调用默认的浅拷贝构造函数

  对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除.
智能指针、左值引用右值引用、lambda表达式_第3张图片

执行结果:调用深拷贝拷贝构造函数

  深拷贝构造函数虽然避免了对内存重复删除的问题;但是在Get函数中会返回一个临时变量,然后通过这个临时变量拷贝构造了一个新的对象b,临时变量在拷贝构造完成后就销毁了,如果对内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。
智能指针、左值引用右值引用、lambda表达式_第4张图片

执行结果:调用移动构造函数
  移动构造函数可以将资源通过浅拷贝的方式从一个对象转移到另一个对象,避免了临时对象的创建。移动构造函数的参数是一个右值引用类型&&,用来支持移动语义的。
智能指针、左值引用右值引用、lambda表达式_第5张图片

2.2 移动(move)语义

  move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。要move语义起作用,核心在于需要对应类型的构造函数支持。
智能指针、左值引用右值引用、lambda表达式_第6张图片

#include 
#include 
#include 
#include 
#include 
using namespace std;

class MyString {
private:
    char* m_data;
    size_t   m_len;
    void copy_data(const char *s) {
        m_data = new char[m_len+1];
        memcpy(m_data, s, m_len);
        m_data[m_len] = '\0';
    }
public:
    MyString() {
        m_data = NULL;
        m_len = 0;
    }

    MyString(const char* p) {
        m_len = strlen (p);
        copy_data(p);
    }

    MyString(const MyString& str) {
        m_len = str.m_len;
        copy_data(str.m_data);
        std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
    }
    MyString& operator=(const MyString& str) {
        if (this != &str) {
            m_len = str.m_len;
            copy_data(str.m_data);
        }
        std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
        return *this;
    }

    // 用c++11的右值引用来定义这两个函数
    MyString(MyString&& str) {
        std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
        m_len = str.m_len;
        m_data = str.m_data; //避免了不必要的拷贝
        str.m_len = 0;
        str.m_data = NULL;
    }

    MyString& operator=(MyString&& str) {
        std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
        if (this != &str) {
            m_len = str.m_len;
            m_data = str.m_data; //避免了不必要的拷贝
            str.m_len = 0;
            str.m_data = NULL;
        }
        return *this;
    }

    virtual ~MyString() {
        if (m_data) free(m_data);
    }
};


int main()
{
    MyString a;
    a = MyString("Hello");      // move
    MyString b = a;             // copy
    MyString c = std::move(a);  // move, 将左值转为右值
    return 0;
}

在这里插入图片描述

2.3 forward 完美转发

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

#include 
using namespace std;
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 Print(T &&t){
	Ptint(t);
	Print(std::move(t));
	Print(std::forword<T>(t));
}

int main(){
	int &&a = 5;
	int &&b = a;	//错误的,变量a是左值
	int &&b = forward<int>(a);	//正确的,forward参数传递过程中保持其值的功能
	
}

– func(1)
L1 #参数本身是一个左值
R1 #move转为右值
R1#1本身是一个右值
– func(x)
L10
R10
L10#x本身是一个左值
– func(std::forward(y))
L20
R20
R20#forward转发为右值

3. lambda 表达式

3.1 匿名函数的基本用法

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

  一般情况下,编译器可以自动推断出lambda表达式的返回类型,所以我们可以不指定返回类型,但是如果函数体内有多个return语句时编译器无法自动推断出返回类型,此时必须指定返回类型。
智能指针、左值引用右值引用、lambda表达式_第7张图片
推荐一个零声学院免费公开课程,个人觉得老师讲得不错,分享给大家:Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习:

你可能感兴趣的:(c++)