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;
};
}