侯捷C++-C++11新特性

右值引用

左值:具有可寻址的存储单元,并且能由用户改变其值的量
右值:即将被销毁的临时变量,如字面值常量“hello”(将亡值),返回非引用类型的表达式int func()(纯右值)等;

为什么要有右值引用?

避免不必要的拷贝,如果没有右值引用,每一次的拷贝都需要开辟一片新的空间存储被拷贝的值,这样就使拷贝不会影响原来的值,这样当然没有错,但是有些情况下被拷贝的值再完成拷贝之后就被销毁了(如各种临时变量),那么也就没有必要再考虑会不会影响原来的值了,可以直接把原来的值 过来,也就是直接将原来的存储空间拿给拷贝后的变量,不需要开辟新空间;

因此,右值引用就被提出了,对于右值引用,我们可以使用 移动构造函数 而不是 拷贝构造函数 从而避免开辟新空间,大大提高效率;

右值引用的用法

  1. 对于临时变量,编译器会自动将其看做一个右值引用;
  2. 对于左值,可以使用 std::move(lvalue) 移动语义将其变为一个右值引用;
1
2
3
class M;
M c1(c); // 会调用拷贝构造函数
M c2(std::move(c1)); // 会调用移动构造函数

因此,对于自定义的带有指针的类,需要定义移动构造函数;在标准库中,大多数函数也都有针对右值引用的版本;

拷贝构造和移动构造的区别:

在写移动构造或者移动赋值时要注意,只要让原来的指针断开(指向NULL)即可,不要销毁,销毁是析构函数要做的事;

std::forward

C++11还实现了 完美转发,完美转发的含义是在参数传递的过程中保持其左值或右值的特性;
没有完美转发的案例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
void print(int& t){
cout<<"左值"<<endl;
}
void print(int&& t){
cout<<"右值"<<endl;
}
void testForward(int&& t){
print(t);
}
int main(){
testForward(1); // 输出:左值,因为在testForward中,对于print来说,t是一个有名字的,可以改变的值,是一个左值;
return 0;
}

使用 std::forward 可以实现完美转发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void print(int& t){
cout<<"左值"<<endl;
}
void print(int&& t){
cout<<"右值"<<endl;
}
void testForward(int&& t){
print(t);
print(std::forward<int>(t));
}
int main(){
testForward(1); // 输出:右值,std::forward 保留了参数的左右值信息
return 0;
}

Lambdas

lambda表达式可以理解为一个匿名的内联函数;可以被当作一个参数或者局部对象;常用于标准库函数中的函数对象参数,如定义比较大小的函数对象参数

lambda表达式的完全形式如上,**[]中表示传入的外界变量()中表示函数参数**,后三个分别表示 **mutable(可改变传入值),throwSpec(允许报错),retType(返回类型)**;

1
2
3
4
5
6
7
8
9
10
11
12
13
[]{
std::cout<<"Hello Lambda"<<std::endl;
}
// 直接使用
[]{
std::cout<<"Hello Lambda"<<std::endl;
}()
// 常用方法
auto L=[]{
std::cout<<"Hello Lambda"<<std::endl;
};

L(); // prints"Hello Lambda"


可以看出,编译器会把Lambda表达式看成一个**Functor(仿函数/函数对象)**;


lambda表达式是一个匿名的内联函数,在编译阶段就会被展开填补在调用处,因此如果是 传值 如左上图中案例;如果传引用则不会有这种问题,如中上图案例;如果没写mutable却要更改传入值,则会编译报错;

如下图所示,是lambda表达式的一种常见用法,先定义出lambda表达式,再用 decltype 得到其类型作为模板参数:

1
2
3
4
5
6
// 定义比较大小的lambda表达式
auto cmp = [](const pair<int, int>& lhs, const pair<int, int>& rhs){
return lhs.second>rhs.second;
};
// 使用decltype得到cmp的类型作为模板参数定义优先级队列
priority_queue<pair<int, int>, vector<pair<int, int>>, deeltype(cmp)> pri_que;

还有一种用法是直接在参数处定义一个lambda表达式作为局部函数对象,不需要定义一个完整的仿函数:

1
2
3
4
vector<int> vi{5,28,50,83,70,590,245};
int x=30;
int y=100;
vi.erase(remove_if(vi.begin(), vi.end(), [x,y](int n){return x<n&&n<y}))

智能指针

C++ 智能指针最佳实践&源码分析

unique_ptr

unique_ptr的核心特点就如它的名字一样,它拥有对持有对象的唯一所有权。即两个unique_ptr不能同时指向同一个对象。

  1. unique_ptr 不能被复制到另一个unique_ptr
  2. unique_ptr 只能通过转移语义将所有权转移到另一个unique_ptr
1
2
3
4
std::unique_ptr<A> a1(new A());
std::unique_ptr<A> a2(a1); // 编译报错,已删除拷贝构造函数
std::unique_ptr<A> a2 = a1; // 编译报错,已删除赋值构造函数
std::unique_ptr<A> a3 = std::move(a1);

实现

删除拷贝构造函数和赋值构造函数,在析构函数中delete raw_ptr

shared_ptr

与unique_ptr的唯一所有权所不同的是,shared_ptr强调的是共享所有权。也就是说多个shared_ptr可以拥有同一个原生指针的所有权。

shared_ptr 是通过引用计数的方式管理指针,当引用计数为 0 时会销毁拥有的原生对象。

1
2
3
4
std::shared_ptr<A> a1(new A());
std::shared_ptr<A> a2(a1);
std::shared_ptr<A> a2 = a1;
std::shared_ptr<A> a3 = std::move(a1);

实现

  1. 构造函数:指针指向该对象,引用计数置为一
  2. 拷贝构造函数:指针指向该对象,引用计数加一
  3. 赋值构造函数:=左边引用计数减一,右边引用计数加一,如果左边引用计数降为0,要销毁指针指向的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
template<typename T>
class sharedPtr {
private:
T* _ptr;
int* _count;
public:
// 构造函数
sharedPtr(T* p):_ptr(p) {
if (p != nullptr) {
_count = new int(1);
}
else {
_count = new int(0);
}
}
// 拷贝构造函数
sharedPtr(const sharedPtr& other) {
if (&other != this) {
_ptr = other._ptr;
_count = other._count;
(*_count)++;
}
else {
_ptr = nullptr;
_count = new int(0);
}
}
// 赋值构造函数
sharedPtr& operator=(const sharedPtr& rhs) {
if (rhs == this) { // 判断自我赋值
return *this;
}
if (_ptr) { // 等号左边计数减一并判断是否销毁
(*_count)--;
if ((*_count) == 0) {
delete _ptr;
delete _count;
}
}
// 等号右边计数加一,赋值
(* rhs._count)++;
_ptr = rhs._ptr;
_count = rhs._count;
return *this;
}
// 返回raw_ptr
T* operator->() {
if (_ptr) {
return _ptr;
}
}
// 返回对象引用
T& operator*() {
if (_ptr) {
return *_ptr;
}
}
// 析构函数
~sharedPtr() {
(* _count)--;
if ((*_count) == 0) {
delete _ptr;
delete _count;
}
}
void getRefCount() {
cout << "Reference Count:" << (*_count) << endl;
}
};

shared_ptr指向的对象不是线程安全的;

weak_ptr

weak_ptr 比较特殊,它主要是为了配合shared_ptr而存在的。就像它的名字一样,它本身是一个弱指针,因为它本身是不能直接调用原生指针的方法的。如果想要使用原生指针的方法,需要将其先转换为一个shared_ptr。那weak_ptr存在的意义到底是什么呢?

解决shared_ptr的循环引用问题,weak_ptr不增加引用计数

如图,栈上创建的共享指针person指向一个新建的Person对象,引用计数为1,car指向一个新建的Car对象,引用计数为1;
之后让person的成员变量m_car指向car表示这个人的汽车,car对象引用计数为2,让car的成员变量m_person指向person表示这辆车的主人,person对象引用计数为2。
此时如果程序结束,栈上的person被销毁,堆中的person对象引用计数减1,变为1,栈上的car被销毁,推中的car对象引用计数减1,变为1;都没有变成0,都不会被销毁,造成内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::shared_ptr<A> a1(new A());
std::weak_ptr<A> weak_a1 = a1; // 不增加引用计数
if(weak_a1.expired())
{
//如果为true,weak_a1对应的原生指针已经被释放了
}

long a1_use_count = weak_a1.use_count();//引用计数数量

if(std::shared_ptr<A> shared_a = weak_a1.lock())
{
//此时可以通过shared_a进行原生指针的方法调用
}

weak_a1.reset();//将weak_a1置空

另外,一切应该不具有对象所有权,又想安全访问对象的情况。

如:一个公司类可以拥有员工,那么这些员工就使用std::shared_ptr维护。另外有时候我们希望员工也能找到他的公司,所以也是用std::shared_ptr维护,这个时候问题就出来了。但是实际情况是,员工并不拥有公司,所以应该用std::weak_ptr来维护对公司的指针。


侯捷C++-C++11新特性
https://kenny-hoho.github.io/2024/03/01/侯捷C++-C++11新特性/
作者
Kenny-hoho
发布于
2024年3月1日
许可协议