C++11新特性
前言
今天这篇文章学习一下C++11的一些新特性,毕竟这也是面试过程中经常会问的,开搞!
类型推导
顾名思义,C++11引入了类型推导关键字,分别为auto和decltype,这些关键字可以在代码编译阶段自动推导出变量或者表达式的类型
auto
使用auto可以让编译器自动去推导变量的类型,例如:
1 | auto a=10; //自动推导出a是int类型 |
当然,auto的使用也有一定的规则:
- auto使用必须初始化,(auto it;) 这样是错误的
- (auto a=1,b=1.0;)这样也是错误的,因为编译器有二义性,所以没法推导具体是哪个类型
- auto不能作为函数形参,(void Func(auto it))这样也是错误的
- 在类中,auto不能用作非静态成员变量
- auto不能用来定义数组,(auto nums[10])这样也是不允许的
- auto没办法推导出模板参数,例如(vector
nums)
还有一些auto的一些特性:
- 如果auto不被申明为指针或者引用的时候,编译器会自动忽略等号右边的引用类型和CV属性
- 在声明为引用或者指针时,auto会保留等号右边的引用和cv属性
CV:const和volatile
1 | int i = 0; |
decltype
decltype与auto不同在于它是用来推导表达式类型的,例如:
1 | const int i=10; |
decltype类型只会进行表达式的类型推导,并不会对表达式进行运算
decltype(temp)的使用规则:
- temp如果是表达式,则decltype(temp)和temp的类型相同
- temp如果是函数调用,则decltype(temp)和函数的返回值相同
- 如果temp为一个左值,则decltype(temp)为左值引用
1
2
3int a=10,b=10;
decltype(a+b) i=10; //返回int,因为a+b是一个右值
decltype(a+=b) c=10; //返回int&,因为a+=b是一个左值与auto不同,decltype会保留表达式的引用和CV属性
1
2const int &i=10;
decltype(i) a=20; //a的类型为const int&
auto和decltype的配合使用
1 | template(typename U,typename V) |
左值和右值
我们先看一下下面这行代码
1 | int a=10; |
这是一个在普通不过的定义变量的代码,但是在C++11中,这里面就多了两个叫做左值和右值的东西
左值
顾名思义,能够放在等号左边的值就是左值,可以取地址并且有名字的东西就是左值
右值
顾名思义,能够放在等号右边的值就是右值,不能取地址的没有名字的东西就是右值
1 | int a=b+c; //a就是左值,b和c就是右值 |
左值和右值的区别
左值是有名字,有地址的,可以放在等号左边的,而右值没有名字且不能取地址,而且也不能放到等号左边
1 | int a=b+c; |
左值一般有:
- 函数名和变量名
- 返回左值引用的函数调用
- 前置的自增自减表达式,如:++i,–i
后置的自增自减表达式为右值
- 由赋值表达式或赋值运算符连接的表达式(a=b, a += b等)
- 解引用表达式 *p
纯右值和将亡值
纯右值和将亡值都属于右值
运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值,例如:
- 除字符串字面值外的字面值
- 返回非引用类型的函数调用
- 后置自增自减表达式i++、i–
- 算术表达式(a+b, a*b, a&&b, a==b等)
- 取地址表达式等(&a)
将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务
左值引用和右值引用
左值引用就是对左值的引用,右值引用就是对右值的引用,都是引用,所以它们都只是变量的一个别名,并不拥有所绑定的对象的堆存,所以必须立马初始化
左值引用
1 | int a=10; //a是左值 |
如上可知:
- 左值引用必须满足等号右边的变量可以取地址
- 如果为常量引用,是可以的,但是这样做的话就无法对常量引用进行修改操作,因为已经定义为const了,例如上面代码中如果对d进行修改(d=20;)就会报错
右值引用
1 | 右值引用的一般为: |
绑定右值的右值引用,其变量本身是个左值
1 | void Func(int& a) |
为什么要有右值引用?
首先,我们都知道,函数传参的时候可以通过指针或者引用进行值传递,而相对于用指针传递,引用传递可以减少一次拷贝,可以直接对引用的对象进行操作,在这种情况下我们使用的都是左值引用,一般分为两种:
- int Func(int& a);
- int& Func();
但是第二种会出现一个问题,那就是如果返回的是函数体内的一个临时变量怎么办?而解决这种问题,我们就能通过右值引用,当函数的返回值为一个右值引用的话,会将返回的临时变量中的内存占为己有,仍然保持了有效性并且避免了拷贝1
2
3
4
5
6int& Func()
{
int a=10;
return a;
}
这里就会出现问题了,因为a是在函数体内被临时创建的,会在栈上创建,等到函数结束,就会销毁a,那样返回对a的引用的话就会出现问题,因为内存中已经没有a地址了
那么,如何返回一个右值引用呢?这里就涉及到了右值引用的一些应用
右值引用的应用
移动语义
首先说一下移动和拷贝的区别:
- 拷贝分为两种,浅拷贝和深拷贝,差别在于对于是否对新创建的对象有一块新的地址内存
- 深拷贝会涉及到数据从一块内存被复制到一块新的内存,浅拷贝又会有问题
- 移动简单来说就是将数据从旧的内存移动到新的内存,原本旧的内存就不会有数据了,使用移动而不使用拷贝的是因为可以避免频繁拷贝而造成的开销
- 例如当我们使用vector的push_back()的时候,如果对同一个对象多次push_back(),我们只是希望能够将参数传进去就可以,但是编译器是会拷贝参数对象,然后传入,这样内存中就会有两份一样的参数,而如果使用移动语义的话,就可以避免拷贝的开销,直接将数据进行移动
- 在C++11之前,当进行值传递时,编译器会隐式调用拷贝构造函数;自C++11起,通过右值引用来避免由于拷贝调用而导致的性能损失
std::move()
是可以实现移动的一个方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15std::move()的源码
template <typename _Tp>
constexpr typename std::remove_reference<_Tp>:: type&& move(_Tp&& __t) noexcept
{
return static_cast<typename std::remove_reference<_Tp>::type&&>(__t);
}
/*
本质还是做了一个类型转换:
如果传递的是左值,则推导为左值引用,然后由static_cast转换为右值引用
如果传递的是右值,则推导为右值引用,然后static_cast转换为右值引用
*/
如果使用了move,就意味着:
1. 原来的对象将不再被使用,如果对其使用就会造成不可预计的错误
2. 所有权转移,资源的所有权被转移给新的对象移动构造函数和移动赋值操作符
移动构造函数和拷贝构造函数一样,将对象的实例作为其参数,并从原始对象创建一个新的实例。但是,
移动构造函数可以避免内存重新分配
,这是因为移动构造函数的参数是一个右值引用,也可以说是一个临时对象,而临时对象在调用之后就被销毁不再被使用,因此,在移动构造函数中对参数进行移动而不是拷贝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
70
71class BigObj {
public:
//构造函数
explicit BigObj(size_t length)
: length_(length), data_(new int[length]) {
}
// 析构函数
~BigObj() {
if (data_ != NULL) {
delete[] data_;
length_ = 0;
}
}
// 拷贝构造函数
BigObj(const BigObj& other)
: length_(other.length_), data(new int[other.length_]) {
std::copy(other.mData, other.mData + mLength, mData);
}
// 赋值运算符
BigObj& operator=(const BigObj& other) {
if (this != &other;) {
delete[] data_;
length_ = other.length_;
data_ = new int[length_];
std::copy(other.data_, other.data_ + length_, data_);
}
return *this;
}
// 移动构造函数
BigObj(BigObj&& other) : data_(nullptr), length_(0) {
data_ = other.data_;
length_ = other.length_;
other.data_ = nullptr;
other.length_ = 0;
}
//这里可以看到,代码中并没有分配任何新的资源,也不会复制其他的资源,因为采用右值引用作为形参,所以我们可以理解为代码中直接让data_等于一个已经存在于内存中的值,而这个值就是other.data_,这样就不需要像拷贝构造函数一样,需要对other.data_进行一个复制然后在传给data_
// 移动赋值运算符
BigObj& operator=(BigObj&& other) {
if (this != &other;) {
delete[] data_;
data_ = other.data_;
length_ = other.length_;
other.data_ = NULL;
other.length_ = 0;
}
return *this;
}
private:
size_t length_;
int* data_;
};
int main() {
std::vector<BigObj> v;
v.push_back(BigObj(25)); //调用的是vector中push_back(&&)
v.push_back(BigObj(75));
BigObj boj(20);
v.push_back(boj); //调用的是push_back(&),因为obj是一个左值
v.push_back(std::move(boj)); //调用的是push_back(&&),因为通过move将左值转化为一个右值引用,前提是BigObj类中定义了移动语义
v.insert(v.begin() + 1, BigObj(50));
return 0;
}
完美转发
看不懂这个
- 标题: C++11新特性
- 作者: 这题超纲了
- 创建于: 2023-09-18 15:16:54
- 更新于: 2023-09-23 15:33:30
- 链接: https://qx-gg.github.io/2023/09/18/blog20/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。