c++11 部分特性的讨论

c++参考手册:https://zh.cppreference.com/w/cpp

说明:要使用c++11的新特性要在编译的时候添加“-std=c++11”。

A、智能指针:

使用智能指针的背景:由于用户new出来的指针是要自己管理的,所以new出来之后,在指针不再使用的时候用户要自己delete这个指针,否则会导致内存泄漏。智能指针提供了一个方法,可以使得new出来的对象寄生于栈对象,当栈对象退出栈的时候,就可以自动地把new出来的对象delete掉,从而避免了内存泄漏。

智能指针有4种:auto_ptr、shared_ptr、unique_ptr、weak_ptr。其中auto_ptr标准已经弃用。

1、共享的智能指针shared_ptr:

使用方法:

#include <iostream>
#include <memory>

//空引用资源的shared_ptr
std::shared_ptr<int> sp0;

//shared_ptr可以用于bool运算符重载。
//同时当shared_ptr有资源引用的时候,bool运算符返回true,否则为false
//下面显示0
if (sp0)
    std::cout << 1 << std::endl;
else
    std::cout << 0 << std::endl;

//对sp0设置资源,同时释放旧有资源
sp0.reset(new int(100));
//下面显示1
if (sp0)
    std::cout << 1 << std::endl;
else
    std::cout << 0 << std::endl;

//对sp0的引用指针释放资源,sp0变成一个空引用资源的shared_ptr
sp0.reset();
//下面显示0
if (sp0)
    std::cout << 1 << std::endl;
else
    std::cout << 0 << std::endl;

//下面给两个是等价,优先使用make_shared,因为这个比较高效
auto sp1 = std::make_shared<int>(100);
std::shared_ptr<int> sp2(new int(100));

//给shared_ptr指定删除器
std::shared_ptr<int> sp3(new int(100), [](int * p){
    delete p;
});
//对于数组的释放,以通过指定删除器来实现
std::shared_ptr<int> sp4(new int[10], [](int * p){
    delete []p;
});

注意事项:

a、不建议使用get方法去获取shared_ptr的原始指针,以防止不规范的使用。

b、make_shared不能指定删除器,但是make_shared在c++20中有对数组的操作。

c、make_shared高效在于只需要申请一次内存,而另外一个需要申请两次。因为make_shared的数据部分和引用计数部分的内存是一起生成的,而另外一个是申请数据部分的内存和申请计数部分的内存。

d、不要用一个原始指针初始化多个shared_ptr,例如:

int * ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);

f、有些场合我们需要返回this指针,但是返回的this是一个裸指针。这样子就违背了智能指针管理指针的方案了。那么这个时候,不能直接如下:

class A
{
public:
    std::shared_ptr<A> GetSelf()
    {
        return std::shared_ptr<A>(this);
    }
};

因为这里会引起注意事项d的问题,一个资源多个shared_ptr管理。解决方案如下:

class A : pulbic std::enable_shared_from_this<A>
{
public:
    std::shared_ptr<A> GetSelf()
    {
        return shared_from_this();
    }
};

2、独占的智能指针unique_ptr:

使用方法:

#include <iostream>
#include <memory>

//空引用资源的unique_ptr
std::unique_ptr<int> up0;

//unique_ptr可以用于bool运算符重载。
//同时当unique_ptr有资源引用的时候,bool运算符返回true,否则为false
//下面显示0
if (up0)
    std::cout << 1 << std::endl;
else
    std::cout << 0 << std::endl;

//对up0设置资源,同时释放旧有资源
up0.reset(new int(100));
//下面显示1
if (up0)
    std::cout << 1 << std::endl;
else
    std::cout << 0 << std::endl;

//对up0的引用指针释放资源,up0变成一个空引用资源的unique_ptr
up0.reset();
//下面显示0
if (up0)
    std::cout << 1 << std::endl;
else
    std::cout << 0 << std::endl;

//下面给两个是等价,优先使用make_unique,new的版本会使得new的代码重复键入
auto up1 = std::make_unique<int>(100);
std::unique_ptr<int> up2(new int(100));

//给unique_ptr指定删除器(与shared_ptr略有不同)
std::unique_ptr<int, void (*)(int *)> up3(new int(100), [](int * p){
    delete p;
});
//对于数组的释放,以通过指定删除器来实现
std::unique_ptr<int, void (*)(int *)> up4(new int[10], [](int * p){
    delete []p;
});

注意事项:

unique_ptr不能赋值,智能通过move语义来移动资源。

3、弱引用智能指针weak_ptr:

weak_ptr主要用于解决循环引用shared_ptr的问题,例如:

#include <iostream>
#include <memory>

class B;

class A
{
public:
    //std::weak_ptr<B> m_pb;
	std::shared_ptr<B> m_pb;
	
	~A()
	{
		std::cout << "release A" << std::endl;
	}
};

class B
{
public:
    //std::weak_ptr<A> m_pa;
	std::shared_ptr<A> m_pa;
	
	~B()
	{
		std::cout << "release B" << std::endl;
	}
};

int main()
{
	{
    	std::shared_ptr<A> pa = std::make_shared<A>();
		std::shared_ptr<B> pb = std::make_shared<B>();
		pa->m_pb = pb;
		pb->m_pa = pa;
	}
	
	std::cout << "exit" << std::endl;
	
    return 0;
}

上述代码中A的对象于B的对象都没有调用析构函数去析构对象。因为当pa与pb退出栈的时候,只是把pa和pb对资源的引用计数减了1,但是pa的m_pb与pb的m_pa对于资源的引用还是存在的。所以它们对资源的计数不为0,所以没有调用析构函数。弱引用指针就是为了解决这个问题而存在的。

使用方法:

如上面代码中m_pa与m_pb的注释一样。把shared_ptr注释掉,然后把weak_ptr开出来,那么就能看到析构函数调用了。weak_ptr不会把引用计数增加,但是会引用资源。

注意事项:

a、在使用weak_ptr的时候,要先用weak_ptr的成员函数expire来判断weak_ptr的资源是否过期,如果过期就不能访问资源的成员,如果没有,那么就先调用weak_ptr的成员函数lock来获得shared_ptr对象,这个时候相当于把资源的引用计数加1了,然后通过shared_ptr来访问成员。

4、对于智能指针的线程安全问题:

a、shared_ptr的引用计数是安全的,但是对于智能指针本身是不安全的。例如多个线程共用同一个智能指针对象,所以我们用智能指针的时候如果要传入函数,那么函数的参数不要用引用。例如:

#include <iostream>
#include <memory>

//这里不要用void Fun(std::shared_ptr<int> & sp)
void Fun(std::shared_ptr<int> sp)
{}

int main()
{
	std::shared_ptr<int> sp1 = std::make_shared<int>(100);
	Fun(sp1);
	
    return 0;
}

b、对于资源来说是不安全的。

B、右值引用与move语义

先看看代码:

#include <iostream>

class A
{
public:
	A(){}
	A(const A & a) { std::cout << "copy construct" << std::endl; }
	A(A && a) { std::cout << "move construct" << std::endl; }
	
	A & operator=(const A & a)
	{
		std::cout << "copy oper =" << std::endl;
		return *this;
	}
	
	A & operator=(A && a)
	{
		std::cout << "move oper =" << std::endl;
		return *this;
	}
};

A Fun(const bool is_a)
{
	A a;
	A b;
	std::cout << "Fun" << std::endl;
	if (is_a)
		return a;
	else
		return b;
}

int main()
{
    A a1;
	A a2 = a1;
	A a3 = std::move(a1);
	A a4;
	A a5;
	A a6;
	a4 = a1;
	a5 = std::move(a1);
	a6 = Fun(true);
	std::cout << "main" << std::endl;
	A a7 = Fun(false);
    return 0;
}

上面的代码输出如下:

copy construct
move construct
copy oper =
move oper =
Fun
move construct
move oper =
main
Fun
move construct


从上面的代码和输出可以知道,&&这个是作为右值引用的标志,如果定义了以&&右值引用为参数的构造函数或者=操作符重载,那么当传入的是右值引用的时候,就调起右值引用的版本。右值就是在内存没有确定存储地址,没有变量名,表达式结束就会销毁的值。例如:函数返回值、表达式返回值和字面常量。

在上述代码中有个地方要注意,函数Fun如果没有ifelse的两个返回值,例如只返回a这个路径。那么输出的最后一条move construct就不会打印出来,这个时候也不会打印copy construct。这个是因为返回值优化(RVO)。如上面的代码,当Fun返回值只是a的时候,RVO会在a7分配内存后,把地址传入并替代a,那么在Fun里面对a的操作都会作用与a7。

C、forward完美转发

#include <iostream>

template<typename T>
void print(T & t)
{
    std::cout << "L " << t << std::endl;
}

template<typename T>
void print(T && t)
{
    std::cout << "R " << t << std::endl;
}

template<typename T>
void Fun(T && t)
{
    print(t);
    print(std::move(t));
    print(std::forward<T>(t));
}

void FunError(int && i)
{
    print(i);
    print(std::move(i));
    print(std::forward<int>(i));
}

int main(int argn, char ** argc)
{
    Fun(11);

    int x = 12;
    Fun(x);
    //FunError(x);

    int y = 13;
    Fun(std::forward<int>(y));
}

以上的代码输出如下:

L 11
R 11
R 11
L 12
R 12
L 12
L 13
R 13
R 13

在讨论上述的现象之前先讨论万能引用的概念。上面代码的模板函数Fun的参数是一个万能引用而不是一个T的右值引用变量,而函数FunError的参数就是一个int的右值引用变量,所以如上代码如果调用注释部分的FunError(x),那么就会出现编译问题,原因是x是一个左值,但是函数参数是个右值引用。在Fun(x)调用中就不会存在这个问题,因为是万能引用所以,根据调用x是左值,那么Fun的t变量就是一个左值引用T也是一个左值引用类型。

那么根据上述代码可以看出,右值引用变量其实也是一个左值(因为有名字和确定的内存地址)。通过观察Fun(11)和Fun(x)的第3条输出可以知道std::forward的其中一个用法就是可以维持万能引用原来的出入参数的引用属性。通过Fun(std::forward<int>(y))的输出可以看出std::forward的另外一个用法就是可以把左值转化为右值。

D、emplace_back减少内存拷贝和移动(对于stl容器,c++11后引入了emplace_back接口)

emplace_back与push_back的用法相同,但是emplace_back没有创造中间临时值,而是直接原地构造对象。

E、unordered container无序容器

c++11增加了无序容器unordered_map/unordered_multimap与unordered_set/unordered_multiset。map和set的内部是用红黑树结构来存储和排序元素的,而unordered_map和unordered_set是用散列表(hash table)来存储元素。

F、匿名函数lambda表达式

//lambda的语法:[捕获列表](参数列表)->返回类型{函数体}
void Test1()
{
    auto fun1 = [](int a, int b)->int
    {
        return a + b;
    };
}

//相对于fun1,fun2没有了返回类型,编译器可以通过return来推导返回类型
//建议用fun1形式,代码更加清晰,容易阅读
void Test2()
{
    auto fun2 = [](int a, int b)
    {
        return a + b;
    };
}

//捕获列表可以使得函数体使用外部变量
//res的值是21而不是31,因为捕获列表的这种参数传入方式是传值而且是只读的。
void Test3()
{
    int c = 10;
    int d = 11;
    auto fun3 = [c, d](int a, int b)->int
    {
        //c = a; 这个会报错,传值是不能修改的。
        return c + d;
    };

    c = 20;

    int res = fun3(100, 101);
}

//捕获列表可以使得函数体使用外部变量
//res的值是31,因为这个时候捕获列表传入的参数是引用。
void Test4()
{
    int c = 10;
    int d = 11;
    auto fun4 = [&c, d](int a, int b)->int
    {
        return c + d;
    };

    c = 20;

    int res = fun4(100, 101);
}

注意事项:

上图演示了捕获列表的按值捕获按引用捕获的用法,还有一种隐式捕获。以下是隐式捕获的演示:

void Test1()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    
    auto fun1 = []()->int
    {
        int res = 0;
        //res = a + b;  error 因为“[]”表示不捕获外部函数的局部变量。
        return res;    
    };
}

void Test2()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    
    auto fun2 = [=]()->int
    {
        int res = 0;
        res = a + b + c + d; //ok 因为“[=]”表示按值捕获外部函数的所有局部变量
        return res;
    };
}

void Test3()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    
    auto fun3 = [&]()->int
    {
        int res = 0;
        res = a + b + c + d; //ok 因为“[&]”表示按引用捕获外部函数的所有局部变量
        return res;
    };
}

void Test3()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    
    auto fun3 = [&, c, d]()->int
    {
        int res = 0;

        //除了c和d是按值捕获,其它的外部函数的局部变量都是按引用捕获
        a = 5; //ok 
        //c = 1; error 

        res = a + b + c + d;
        return res;
    };
}

void Test4()
{
    int a = 1;
    int b = 2;
    int c = 3;
    int d = 4;
    
    auto fun4 = [=, c, d]()->int
    {
        int res = 0;

        //除了c和d是按引用捕获,其它的外部函数的局部变量都是按值捕获
        //a = 5; error 
        c = 1; //ok

        res = a + b + c + d;
        return res;
    };
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值