本文从五个方面详述C++11特性:
- 关键字与变量的使用
- 面向对象特性(委托构造函数,继承构造函数,可调用对象包装器、邦定器,lambda表达式等)
- 移动语义
- 智能指针
- 多线程
关键字与变量的使用
字符串原始字面量
c++11中添加了字符串的原始字面量,定义方式为
1 | R"注释(字符串)注释" |
()两边的字符除按可以省略,程序会直接打印括号中的字符串,不会在乎类似转移字符之类。
如下面的代码
1 | string path1 = "\root\tmp\hello.cpp"; |
运行结果
ooth1: mphello.cpp
path2:\root\tmp\hello.cpp
path3:\root\tmp\hello.cpp
可能会奇怪,path1字符串输出的是什么?为什么明明是想输出path1结果输出了ooth1?原因是但斜杠与一些字母代表转义字符,\r表示回到当前行的行首,而不会换到下一行,如果接着输出的话,本行以前的内容会被逐一覆盖,\t是制表符,相当于按下一次tab键,即将”path1:“被”(\r)oot(\t) “替换,导致出现运行结果中的现象。这种现象的解决可以像path2那种写法,出现反斜杠时在加一个反斜杠;或者像path3那种写法处理。
另外一种功能是使用了字符串的原始字面量后,字符串可以占用多行了,代码的可读性得到了改善。
1 | string str = R"( |
long long 类型
long long实际上与 signed long long, long long int, signed long long int 是一回事。unsigned long long 实际上即为 unsigned long long int。在使用long long类型时,要在数据后添加LL后缀;unsigened long long后添加ULL后缀。
使用宏定义LLONG_MAX,LLong_MIN,ULLONG_MAX,分别可以表示long long数据类型的最大最小值,unsigned long long类型的最大值。
1 | cout << "long long max: " << LLONG_MAX << endl; |
运行结果
long long max: 9223372036854775807
long long min: -9223372036854775808
unsigned long long min: 18446744073709551615
静态断言
C++11提供了静态断言, 相比assert,静态断言不需要包含新的头文件,并且可以输出自己希望的输出信息。
1 | static_assert(sizeof(long) == 4,"当前机子不是64位"); |
值得一提的是,静态断言中的判定表达式必须要是常量表达式。在VS环境下,程序执行之前便判断出常量表达式的真假,当常量表达式为假时,VS会提示”静态断言失败“。
异常与noexcept
noexcept是C++11新增特性,相当于throw(),即不向函数外发送任何异常。
关于throw()的用法可查C++ throw(抛出异常)详解 (biancheng.net)
1 |
|
运行结果
来自test4的异常
main结束
数值 字符串间的转换
数值->字符串
1
2
3
4
5
6
7
8
9
10
string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val);字符串->数值
1 |
|
代码演示
1 |
|
运行结果
2022
转换完成字符个数:4
8226
转换完成字符个数:4
- 更快的转换方式
<sstream> 主要用来进行数据类型转换,由于<sstream> 使用 string 对象来代替字符数组(snprintf 方式),避免了缓冲区溢出的危险;而且,因为传入参数和目标对象的类型会被自动推导出来,所以不存在错误的格式化符号的问题。简单说,相比 C 编程语言库的数据类型转换,<sstream> 更加安全、自动和直接。
1 |
|
自动类型推导
auto自动推导
auto能自动推导变量的类型,但使用auto自动类型推导必须给变量初始化,不然无法推导。auto可以与指针、引用结合,也可以与const、volatile限定符配合使用。只有当变量是指针或者引用类型时,推导结果才会保留const,volatile关键字。
1 | int animal = 1024; |
auto一般适用于替代迭代器的定义和泛型编程中。
- 如果变量的定义太冗长,可以用auto替代
1 | vector<int>val{ 1,2,3 }; |
- 在泛型编程中,如果要接收一个模板变量,是很难确定变量的类型,所以用什么数据类型接收就是一件非常头疼的事。解决这个问题,不仅要在模板声明中添加多个变量,在模板函数调用中还要多写多个参数。但如果有了auto关键字,就迎刃而解了。只需将不确定类型的变量用auto接收即可完成任务。
1 | template <class T,class P> |
auto的限制
- 不能在函数参数中使用
1 | int test(auto a){ //*错误* |
- 不能用于初始化类的非静态成员变量
又因为静态非常量成员不能在类内初始化,所以在类内auto只能修饰静态常量成员
1 | class test { |
- 不能定义数组
1 | int arr[]={1,2,3}; |
- 不能当作模板参数
1 | template <typename T> |
decltype
语法格式:decltype(表达式)
在变量类型不确定又不想声明的情况下可以用到decltype。decltype对变量类型的推导是在编译期完成的,只用于表达式类型的推导而不计算表达式的值。
推导规则
表达式为变量,推导得到的类型与变量类型一致
表达式为函数,推导得出的类型与函数返回值一致
如果const修饰的是纯右值(纯数据)时,const不会保留,比如const int类型会推导成int。
- 左值表达式或者被括号修饰,推导结果为表达式类型的引用,如果有const、volatile是不能省略的。
1 | //括号修饰 |
decltype同auto一样,广泛应用于泛型编程中。比如说,写了个类模板,里面用到了容器,想用迭代器遍历容器中的数据,迭代器如何定义?如果用 T::iterator it; 之类的语法是错误的,因为编译器无法识别。最好的方案就是用decltype类型推导。
1 | template <class T> |
总结一下,auto的使用必须初始化,decltype可以不用初始化。
融合!返回值类型后置
上述文章阐述了auto和decltype的用法,此处讲解一个两者结合的使用方法。
在设计模板函数或者模板类的时候,难免会出现模板变量类型不一样的情况。加入需要两个数值类型的加和运算的模板函数,返回两者的加和。我们知道数值类型有很多,short,int,float,double,long等等,甚至字符变量也能掺和掺和。不同的组合的返回类型也是不同的,但在设计模板函数时在一开头就要给出返回类型,在没有确定参数类型时给出返回类型似乎是一件不可能的事情,C++14中编译器才可以自己推导任何函数的返回类型,如果C++11的编译器用auto做函数返回类型是错误的,因为没有初始化。所以就想到了decltype,然而decltype的使用需要知道传递的参数,如果这么写
1 | decltype(t+u) add(T t, U u) |
肯定是错误的,因为decltype中的t和u还没给出,编译器无法识别直接报错。如果能让返回类型放在参数列表之后定义就好了。所以便有了下述的代码思路
1 | template <class T,class U> |
这就是返回值类型后置。
初始化列表
C++11列表初始化新玩法
c++11标准中,可以通过{ }直接初始化对象,之前标准{}前需要一个’=’。
1 | int old_standard[]={1,2,3}; |
使用new 操作符创建新对象时也可以使用列表初始化
1 | int* new_point = new int{ 1024 }; |
return的返回值也可以返回一个匿名对象。
1 | int test() { |
自定义类型的初始化
在初始化以下类时需要注意
- 若自定义类型中有私有成员,无法使用初始化列表初始化
1 | struct T1 {//不可用初始化列表初始化 |
但这样是可以的
1 | struct T { |
运行结果
t3.x= 2
t3.t.x= 1
- 类中有非静态成员可以通过列表初始化进行初始化,但不能初始化静态成员变量
1 | struct T4{ |
- c++14之前不支持用列表初始化的方式初始化包含已有初始化的非静态变量的结构体\类
1 | struct T5 |
- 如果有构造函数能初始化自定义类型中的变量,那么也可以使用列表初始化
1 | struct T6 { |
std::initializer_list
如果想要自定义一个函数并且接收任意个数的参数,只需要将函数参数指定为 std::initializer_list,使用初始化列表 { } 作为实参进行数据传递即可。
1 | void test(initializer_list<int> list) { |
using
引子:typedef 和 函数指针
- 函数指针
顾名思义,函数指针就是指向函数地址的变量。函数指针的使用往往会和指针函数混淆。在C++规则中,括号优先级是大于*优先级的,所以表达式**关键字(*函数名)**表达就是一个指针类型。下面拿int类型设计一个指针函数和一个函数指针加以区分。
1 | int* func1(int val);//指针函数 |
typedef
typedef用于给变量起别名,增加代码的可读性。最基本的用法就如以下代码所示。
1 | typedef int type_int; //type_int相当于int的别名 |
运行结果
a= 2
p= 0000006D208FF764
另外,定义新类型时添加括号并不是语法错误,如
1 | type_array(arr) = { 2,3,4 };//定义了一个数组,数组内容为{2,3,4} |
接下来看看这种形式
1 | typedef int nickname(int,string); |
它的作用相当于给返回类型为int
,含一个int
类型和一个string
类型的参数的函数起了别名nickname
。下面的一段程序将演示它的使用方式。
1 | typedef int nickname(int,string); |
为了可读性,更多的用typedef给函数指针起别名,这样在编程过程中能将原来的函数名修改成一个应景的名字,增加可读性。而对普通函数起别名多用于在函数定义的过程中。
using 的使用
using
的基本使用方法跟typedef大同小异,就不详述了。区别是using习惯于用赋值的方式定义变量的新名称。
1 | using type_int = int; |
using相比于typedef有什么优势呢?下面举一个用typedef给模板取别名的例子。
1 | template<class T> |
类似语句编译器会提示” 此处不能指定typedef “,如果想用typedef指定模板,需要用一个类或者结构体将模板包含进去。像这样
1 | template<class T> |
但是用using的方式的话就会简单很多,不需要先建立结构体或者类,直接定义即可
1 | template<class T> |
之后在看一看在初始化的区别, 先定义了一个仿函数Myprint
,用于打印输出
1 | template<class T> |
1 | MyVec<string>::typedef_vector test1 = { "张三","李四","王五","赵六" };//typedef |
运行结果
typedef:
张三 李四 王五 赵六
using:
张三 李四 王五 赵六
可以明显看出,用typedef给模板起别名的时候,明显地冗杂。而用using不仅相对轻巧,代码的可读性也大大的提高。但不能用函数类型定义函数的实体。
面向对象特性
类成员变量的快速初始化
在C++98中支持”就地声明”初始化类中的静态成员常量,即通过等号=赋予初始值,非静态成员变量的初始化必须在构造函数中声明。非const static
则需要在类外初始化。但在C++11中,可以通过就地方式来实现非静态变量的初始化。但设想一下,如果一个类内都定义了就地初始化和构造函数声明初始化,最后一个执行的是谁呢?
1 |
|
运行结果
构造函数声明初始化
可以看出,字符串变量b1
保留的是构造函数声明初始化的值,所以编译器是先进行就地初始化,再进行构造函数声明初始化。
final和override关键字的使用
final
final
放在类或虚函数的后面,限制某个类不能被继承或虚函数不能被重写。注意,修饰函数时只能修饰虚函数。
1 | class Animal { |
override
C++11新标准中提供override
关键字来说明派生类中的虚函数。主要的用途是便于在调试的时候发现派生类同名函数的写错,因为这种情况在调试中是非常难发现的,并且是程序员的意图更加清晰。但如果用override
标记了未覆盖已存在的虚函数,编译器将会报错。
1 | class Animal { |
委托构造函数
委托构造函数的引入
小明和李华给外国友人写信从高中写到了考研写累了,突然想唱歌,为了方便这哥俩唱歌,咱写了个唱歌类给他们。这哥俩唱的不好,就限制他俩只能唱三句。因为有1~3句的选择,就写三个构造函数。
1 | class SingSong { |
之后把麦给他俩,放飞自我就行
1 | cout << "小明想唱歌,唱了一句夜曲" << endl; |
很不碰巧,被周杰伦看到了,周杰伦看他俩唱的这样就像洗洗耳朵。周杰伦一代天王,可以给他重写个演唱会类。
1 | class concert { |
之后周杰伦就能唱歌了
1 | concert Jay( |
以上代码都能正确运行。但仔细看一下代码,就能发现小明和李华的唱歌类有好多冗余的代码,而周杰伦的演唱会类就精简了很多。演唱会类的实现是用了C++11标准中的委托构造函数。委托构造函数的优点就是,减少了重复代码的出现,从两个类对比可以发现,concert
类可以充分利用之前写的构造函数来实现有更多功能的构造函数。
需要注意的问题
- 不能出现”委托环“,一个类中若有多个委托函数,只能按链状调用,首尾不能相连。
- 如果要使用委托构造函数,不能在函数体内部调用构造函数,否则会提示形参重定义。要在初始列表中调用构造函数。
1 | class error { |
- 委托函数初始化一个变量后,就不能再次初始化该变量
继承构造函数
1 | class Father { |
继承构造函数可以简化派生类构造函数的编写,在C++11标准前,子类要通过Child1(int v):Father(v){}的方式继承父类的构造函数,而且一次只能继承一个构造函数。C++11标准可以通过using Father::Father便可继承父类的所有构造函数。
另外,c++规则中,如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。通过using的方式可以在子类中使用父类的隐藏函数。但如果子类函数与父类函数同名同参数或者虚函数依旧是按照子类函数调用。
1 | int main() { |
运行结果
调用子类的func_same
调用父类的func_similar函数
调用子类的func_virtual函数
可调用对象包装器,绑定器
C++ 11中的std::bind和std::function - _FeiFei - 博客园 (cnblogs.com)
lambda表达式
lambda表达式时C++11引入的一个新特性,相比其他编程语言,C++引入lambda表达式算晚的。引入lambda表达式后,可以就地匿名定义目标函数或函数对象,并捕获一定范围内的变量。他的使用语法如下
1 | [capture](params) opt -> ret {body;}; |
capture 是捕获列表
- [] - 不捕捉任何变量
- [&] - 捕获外部作用域中所有变量,并作为引用在函数体内使用 (按引用捕获)
- [=] - 捕获外部作用域中所有变量,并作为副本在函数体内使用 (按值捕获)
拷贝的副本在匿名函数体内部是只读的 - [=, &foo] - 按值捕获外部作用域中所有变量,并按照引用捕获外部变量 foo
- [bar] - 按值捕获 bar 变量,同时不捕获其他变量
- [&bar] - 按引用捕获 bar 变量,同时不捕获其他变量
- [this] - 捕获当前类中的 this 指针
让 lambda 表达式拥有和当前类成员函数同样的访问权限
如果已经使用了 & 或者 =, 默认添加此选项
params 是参数表,可以省略不写
opt 是函数选项
mutable
: 因为用[=]方式捕获的数据副本在匿名函数体内部是只读不可修改的,通过 mutable 选项可以修改按值传递进来的拷贝1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int a = 10, b = 20;
auto func1 = [=] {
a++;//错误,使用值拷贝的方式捕获外部变量,可读不能写
};
//实现可读写外部捕获的值有两种方式
//默认状态下 lambda 表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获
auto func2 = [&] {
a++;//正确
cout << a << endl;
};
//被mutable修改是lambda表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的外部变量的只读(const)属性
auto func3 = [=]() mutable{
a++;//正确
cout << a << endl;
};
exception
:指定函数抛出的异常
ret 是返回值类型,通常采用返回类型后置的方法定义
- C++11 中允许省略 lambda 表达式的返回值
1
2
3
4
5// 忽略返回值的lambda表达式定义
auto f = [](int a)
{
return a+10;
};- 编译器会根据 return 语句自动推导返回值的类型,但labmda表达式不能通过列表初始化自动推导出返回值类型。
1
2
3
4
5// error,不能推导出返回值类型
auto f1 = []()
{
return {1, 2}; // 基于列表初始化推导返回值,错误
}body 是函数体
移动语义
右值引用
在介绍右值引用之前,先阐述一下左值和右值的概念。左值可以不严格的理解为等号左边的值,右值不严格的理解为等号右边的值。比如int a=2
,a
即为左值,2
是右值。左值严格上讲是在内存中存储,可取地址的数据;而右值是可以提供的数据,不可取地址。a
在内存中是有4B的存储空间的,而2
只是一个数,a
所指的内存空间中存放着2,内存中没有数字2对应的存储地址。所以区分左右值的一个便捷方法就是看他能否取地址。
C++11中的右值可以分为两种
- 纯右值 非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等。如
int value=11
中的11. - 将亡值 与右值引用相关的表达式,比如,T&& 类型函数的返回值、
std::move
的返回值等
右值是没有名称的,使用右值只能借助引用的方式,但如果对右值进行修改,只通过左值引用是行不通的。C++11通过”&&”进行右值引用。
1 | int && value=10;//右值引用 |
引用类型 | 可以引用的 | 值类型 | 使用场景 | ||
---|---|---|---|---|---|
非常量左值 | 常量左值 | 非常量右值 | 常量右值 | ||
非常量左值引用 | Y | N | N | N | 无 |
常量左值引用 | Y | Y | Y | Y | 常用于类中构建拷贝构造函数 |
非常量右值引用 | N | N | Y | N | 移动语义、完美转发 |
常量右值引用 | N | N | Y | Y | 无实际用途 |
移动构造函数
函数中如果有类的局部变量,调用结束后会自动调用析构函数。析构函数中一般要包含对开辟的对空间的回收。假设一个类中包含着指针类型变量,这个指针指向一段新开辟的堆空间,当我们把这个类返回值传给主函数中的一个接收变量(如下列程序中 demo obj1 = test();
语句),调用 demo obj1 = test();
的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 obj1
,在 test()
函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了(VS、codeblocks对编译器做了优化,不会出现上述现象)。这显然与预期的结果是不符的。解决这个问题就需要移动构造函数出场了。
1 | class demo { |
移动构造函数的参数为右值引用类型,函数并没有开辟新的堆空间,进行的是浅拷贝。当obj1
接收到了临时变量中的num
地址后,讲临时变量中的*num
指向NULL,之后进行析构时是将临时变量中的*num
所指地址回收,回收的是NULL,而obj1
中的指针所指地址并未受到影响。
右侧对象是一个临时对象才会调用移动构造函数,如果没有移动构造函数会调用拷贝构造函数。
转移和完美转发
转移方法move
使用std::move
方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝。
move
的应用场景有两个
- 右值引用传递变为左值,如果想传递右值引用,可以使用
move
。
定义两个方法输出左值还是右值
1 | void printValue(int& i) { |
使用move
前:
1 | void forward(int&& i) { |
运行结果
printValue(i):左值
printValue(214):右值
oassValue(214):左值
使用move后
1 | void passValue(int&& i) { |
运行结果
printValue(i):左值
printValue(214):右值
oassValue(214):右值
- 另一个用途就是当一个变量不在使用后,如果新的变量需要使用老变量的数据,可以使用
move
方法将老变量的数据转移给新变量。\
1 | vector<int>v1{ 1,2,3 }; |
运行结果
v1:
v2:1 2 3
v3:
完美转发方法forward
右值引用类型是独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。如果需要按照参数原来的类型转发到另一个函数,可以使用 C++11 提供的 std::forward ()
函数,该函数实现的功能称之为完美转发。
转发的规则是,左值引用转换为左值,非左值引用统统转换成右值。
写一个判断左右值的方法
1 | template<class T> |
举例如下
1 | //完美转发 |
运行结果
左值
右值
右值左值
右值
左值左值
右值
右值左值
右值
左值左值
右值
右值
智能指针
在现代 C++ 编程中,标准库包含智能指针,该指针用于确保程序不存在内存和资源泄漏且是异常安全的。智能指针是在<memory>
头文件中的 std
命名空间中定义的。
C++ 中没有垃圾回收机制,必须自己释放分配的内存。智能指针的引用可以帮助程序员管理动态分配的内存,自动释放申请来的内存,避免内存泄漏。
共享智能指针 share_ptr
共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针 shared_ptr 是一个模板类。
share_ptr
通过引用计数的方式实现多个share_ptr对象之间的共享资源。在对象销毁时,引用计数减一,当引用计数为0时,释放该资源。
share_ptr的初始化
- 通过构造函数初始化
1 | std::shared_ptr<int> ptr1(new int(11)); |
- 通过拷贝构造函数
1 | std::shared_ptr<int>ptr2(ptr1); |
- 通过移动构造函数
1 | std::shared_ptr<int>ptr4(std::move(ptr1)); |
移动构造函数在移动语义中阐述过,通过这种方式初始化,相当于将ptr1的资源转让给ptr4。一个很好的演示方法是智能指针的use_count()
函数。
1 | std::shared_ptr<int> ptr1(new int(11)); |
运算结果
ptr1引用计数1
ptr1引用计数0
ptr2引用计数1
- 通过make_shared初始化
std::make_shared()
就可以完成内存对象的创建并将其初始化给智能指针
1 | std::shared_ptr<int> ptr5 = std::make_shared<int>(2023); |
- 通过reset初始化
std::shared_ptr::reset
对于一个未初始化的共享智能指针,可以通过reset
方法来初始化,当智能指针中有值的时候,调用reset
会使引用计数减 1。
1 | ptr5.reset(); |
运行结果
ptr5引用计数0
ptr5引用计数1
创建和析构类对象
因为share_ptr
是一个模板类,它对任何数据类型都适用。当它创建一个类对象时,何时调用构造函数,何时调用析构函数,下面的代码展示出了通过make_shared
方法构造智能指针和通过reset
方法释放及重定向智能指针的例子。
1 | class Test |
1 | //智能指针创建和析构类对象 |
运行结果
创建类对象, str = C++
ptr_class3引用计数2
创建类对象, str = java
ptr_class3引用计数1
析构类对象,str = C++
ptr_class3引用计数0
创建类对象, str = C++11
析构类对象,str = java
ptr_class2引用计数1
析构类对象,str = C++11
通过运行结果可以看出,通过make_shared
创建的智能指针对象会调用类的构造函数,通过智能指针的拷贝构造函数创建新的智能指针并不再调用类的构造函数。在引用计数为0时才调用类的析构函数。如果通过reset
重定向智能指针并开辟新的堆空间,也会调用类的构造函数。
获取原始指针
共享智能指针类提供get
方法可以得到原始地址。得到的结果就是一个指针,通过指针的使用的方法可以调用该数据结构里的各种数据和方法。还是以上一小节中的test
类为例。
1 | std::shared_ptr<Test>ptr_class = make_shared<Test>("C++"); |
指定删除器
当智能指针管理的内存对应的引用计数变为 0 的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数。
删除器可以用函数实现,如
1 | void deleteIntPtr(int* p) |
也可以用 lambda 表达式实现
1 | std::shared_ptr<Test>ptr_del(new Test, [](Test* t) { |
lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。C++11提供了一个默认的删除器std::default_delete<T>()
,通过delete
来实现。但如果智能指针管理的是数组对象,需要在默认删除器中指定数组类型。(但在C++11之后的标准中支持未指定删除器的智能指针数组的写法)
1 | shared_ptr<Test> ptr(new Test[2], default_delete<Test[]>()); |
运行结果
创建类对象
创建类对象
析构类对象,str =
析构类对象,str =
独占智能指针unique_ptr
std::unique_ptr
是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个 unique_ptr 赋值给另一个 unique_ptr。
由于unique_ptr
不允许复制,所以它的初始化方式就两种。一种通过构造函数new
一段堆空间,另一种是通过前面讲述的move
方法转移所有权。
1 | //独占智能指针的两种初始化 |
在指定删除器上,unique_ptr
指定删除器的时候需要确定删除器的类型,不能像 shared_ptr
那样直接指定删除器。
1 | unique_ptr<int> ptr1(new int(10), [](int* p) {delete p; });//错误 |
上述代码错在没有确定删除器的类型,lambda 表达式在未捕获任何变量的情况下可以转换为一个函数指针。不过一旦lambda 表达式捕获了一个变量就不能转换为函数指针了,导致编译器报错。
1 | using func_ptr = void(*)(int*); |
如果想让编译器成功编译,需要使用可调用对象包装器来处理声明的函数指针
1 | unique_ptr<int, function<void(int*)>> ptr1(new int(10), [&](int*p) {delete p; }); |
另外,在管理数组类型的地址时,能够自动释放。
1 | //独占智能指针可以管理数组类型的地址能够自动释放 |
弱引用智能指针weak_ptr
std::weak_ptr
更像是一个辅助,用来弥补智能指针的一些不足。弱引用指针只监测资源,不管理资源,它的构造不会增加引用计数,析构也不会减少引用计数。
weak_ptr的语法
- 初始化
weak_ptr
的初始化很简单朴素,依赖于shared_ptr
。
1 | shared_ptr<int> share(new int); |
use_count()
通过调用 std::weak_ptr 类提供的 use_count() 方法可以获得当前所观测资源的引用计数
expired()
判断观测的资源是否已经被释放
1 | share.reset(); |
lock()
获取管理所监测资源的 shared_ptr
对象
1 | shared_ptr<int> share1(new int(2)); |
运行结果
2
解决指向this的智能指针的双重析构
如果在类中用智能指针指向一个类中的this
指针时,当这个类生存周期结束进行析构时,会被析构两次。两次分别被类析构函数析构一次,智能指针删除器删除智能指针,再次调用类的析构函数对这个资源再次析构,从而导致程序崩溃。
1 | struct Test |
这个问题可以通过weak_ptr
解决,前文已经了解到,weak_ptr
只监测资源而不管理资源,如果用一个弱引用智能指针指向this
,析构两次的问题就得到解决了。C++11提供了std::enable_shared_from_this<T>
模板类,通过调用其中的shared_from_this()
方法,便可以得到一个检测this
的weak_ptr
。
解决循环引用问题
如果有两个不同类对象A和B,类对象A用智能指ap针指向了类对象B的智能指针bp,然后bp又指向了ap,造成了智能指针的循环引用问题。共享智能指针 ap、bp 对 A、B 实例对象的引用计数变为 2,在共享智能指针离开作用域之后引用计数只能减为1,这种情况下不会去删除智能指针管理的内存,导致类 TA、TB 的实例对象不能被析构,最终造成内存泄露。
这类问题的解决很容易,像解决死锁问题的思路一样,把循环打破就可以了。将其中的一个智能指针改成弱引用指针便可以解决问题。
1 | struct A; |
运行结果
class B is disstruct …
class A is disstruct …