C++11新特性

C++11新特性

这题超纲了 柳筋

前言

今天这篇文章学习一下C++11的一些新特性,毕竟这也是面试过程中经常会问的,开搞!

类型推导

顾名思义,C++11引入了类型推导关键字,分别为autodecltype,这些关键字可以在代码编译阶段自动推导出变量或者表达式的类型

auto

使用auto可以让编译器自动去推导变量的类型,例如:

1
2
3
4
5
auto a=10;  //自动推导出a是int类型
auto b=3.14159; //自动推导出b是double类型
auto c=hashmap.find(target); //自动推导出c是哈希表容器中的指向target的迭代器
int i=100;
auto &d=i; //自动推导出d是i的引用

当然,auto的使用也有一定的规则:

  1. auto使用必须初始化,(auto it;) 这样是错误的
  2. (auto a=1,b=1.0;)这样也是错误的,因为编译器有二义性,所以没法推导具体是哪个类型
  3. auto不能作为函数形参,(void Func(auto it))这样也是错误的
  4. 在类中,auto不能用作非静态成员变量
  5. auto不能用来定义数组,(auto nums[10])这样也是不允许的
  6. auto没办法推导出模板参数,例如(vector nums)

还有一些auto的一些特性:

  1. 如果auto不被申明为指针或者引用的时候,编译器会自动忽略等号右边的引用类型和CV属性
  2. 在声明为引用或者指针时,auto会保留等号右边的引用和cv属性
    CV:const和volatile
1
2
3
4
5
6
7
8
9
10
int i = 0;
auto *a = &i; // a是int*
auto &b = i; // b是int&
auto c = b; // c是int,忽略了引用

const auto d = i; // d是const int
auto e = d; // e是int

const auto& f = e; // f是const int&
auto &g = f; // g是const int&
decltype

decltype与auto不同在于它是用来推导表达式类型的,例如:

1
2
3
4
5
6
7
const int i=10;
int a=2;
decltype(i) b=100; //b是const i类型
decltype(a) c=20; //c是int类型
int Func(){return 0};
decltype((Func)) d; //i是int类型
decltype(a+c) e; //e是int类型

decltype类型只会进行表达式的类型推导,并不会对表达式进行运算

decltype(temp)的使用规则:

  1. temp如果是表达式,则decltype(temp)和temp的类型相同
  2. temp如果是函数调用,则decltype(temp)和函数的返回值相同
  3. 如果temp为一个左值,则decltype(temp)为左值引用
    1
    2
    3
    int a=10,b=10;
    decltype(a+b) i=10; //返回int,因为a+b是一个右值
    decltype(a+=b) c=10; //返回int&,因为a+=b是一个左值
    与auto不同,decltype会保留表达式的引用和CV属性
    1
    2
    const int &i=10;
    decltype(i) a=20; //a的类型为const int&
auto和decltype的配合使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template(typename U,typename V)
return_value Func(U u,V v)
{
return u+v;
}
在上面的函数模板里面,我们不知道u和v的类型,所以没办法推导出u+v的类型,也就没办法写出return_value的类型

template(typename U,typename V)
decltype(u+v) Func(U u,V v)
{
return u+v;
}
这样写也是不对的,因为U和V并没有定义,所以没法这样使用decltype

auto Fund(U u,V v)->decltype(u+v){
return u+v;
}
这样是可以的,这也是C++11里面的返回值后置语法,就是为了解决函数返回值依赖于参数但却难以确定返回值类型的问题

左值和右值

我们先看一下下面这行代码

1
int a=10;

这是一个在普通不过的定义变量的代码,但是在C++11中,这里面就多了两个叫做左值右值的东西

左值

顾名思义,能够放在等号左边的值就是左值,可以取地址并且有名字的东西就是左值

右值

顾名思义,能够放在等号右边的值就是右值,不能取地址的没有名字的东西就是右值

1
2
int a=b+c;  //a就是左值,b和c就是右值
int a=10; //a就是左值,10就是右值

左值和右值的区别

左值是有名字有地址的,可以放在等号左边的,而右值没有名字且不能取地址,而且也不能放到等号左边

1
2
int a=b+c;
&(b+c) //错误,因为(b+c)表达式返回的是一个右值,没有地址也没有名字

左值一般有:

  1. 函数名和变量名
  2. 返回左值引用的函数调用
  3. 前置的自增自减表达式,如:++i,–i
    后置的自增自减表达式为右值
  4. 赋值表达式赋值运算符连接的表达式(a=b, a += b等)
  5. 解引用表达式 *p

纯右值和将亡值

纯右值和将亡值都属于右值

运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值,例如:

  1. 除字符串字面值外的字面值
  2. 返回非引用类型的函数调用
  3. 后置自增自减表达式i++、i–
  4. 算术表达式(a+b, a*b, a&&b, a==b等)
  5. 取地址表达式等(&a)

将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务

左值引用和右值引用

左值引用就是对左值的引用,右值引用就是对右值的引用,都是引用,所以它们都只是变量的一个别名,并不拥有所绑定的对象的堆存,所以必须立马初始化

左值引用
1
2
3
4
5
int a=10;   //a是左值
int &b=a; //b就是左值引用
int &c=10; //错误,因为10为右值,无法取地址,所以无法进行引用
int &c; //错误,引用必须初始化
const int &d=10; //成功,因为是常引用,引用常量数字,这个常量数字会存储在内存中,所以可以去地址

如上可知:

  1. 左值引用必须满足等号右边的变量可以取地址
  2. 如果为常量引用,是可以的,但是这样做的话就无法对常量引用进行修改操作,因为已经定义为const了,例如上面代码中如果对d进行修改(d=20;)就会报错
右值引用
1
2
3
4
5
6
7
8
右值引用的一般为:
type &&name=exp;

int a=10; //a是左值
int &&b=a; //错误,因为a是左值
int &&b=10; //正确
int &&b=std::move(a); //正确,通过移动语义,可以实现右值引用左值
int &&c; //错误,右值引用也是引用,必须进行初始化

绑定右值的右值引用,其变量本身是个左值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Func(int& a)
{
cout<<"左值"<<endl;
}

void Func(int&& a)
{
cout<<"右值"<<endl;
}

int main()
{
int&& a=10;
Func(a);
}

//输出:左值
为什么要有右值引用?

首先,我们都知道,函数传参的时候可以通过指针或者引用进行值传递,而相对于用指针传递,引用传递可以减少一次拷贝,可以直接对引用的对象进行操作,在这种情况下我们使用的都是左值引用,一般分为两种:

  1. int Func(int& a);
  2. int& Func();
    但是第二种会出现一个问题,那就是如果返回的是函数体内的一个临时变量怎么办?
    1
    2
    3
    4
    5
    6
    int& Func()
    {
    int a=10;
    return a;
    }
    这里就会出现问题了,因为a是在函数体内被临时创建的,会在栈上创建,等到函数结束,就会销毁a,那样返回对a的引用的话就会出现问题,因为内存中已经没有a地址了
    而解决这种问题,我们就能通过右值引用,当函数的返回值为一个右值引用的话,会将返回的临时变量中的内存占为己有,仍然保持了有效性并且避免了拷贝

那么,如何返回一个右值引用呢?这里就涉及到了右值引用的一些应用

右值引用的应用

移动语义

首先说一下移动拷贝的区别:

  1. 拷贝分为两种,浅拷贝深拷贝,差别在于对于是否对新创建的对象有一块新的地址内存
  2. 深拷贝会涉及到数据从一块内存被复制到一块新的内存,浅拷贝又会有问题
  3. 移动简单来说就是将数据从旧的内存移动到新的内存,原本旧的内存就不会有数据了,使用移动而不使用拷贝的是因为可以避免频繁拷贝而造成的开销
  4. 例如当我们使用vector的push_back()的时候,如果对同一个对象多次push_back(),我们只是希望能够将参数传进去就可以,但是编译器是会拷贝参数对象,然后传入,这样内存中就会有两份一样的参数,而如果使用移动语义的话,就可以避免拷贝的开销,直接将数据进行移动
  5. 在C++11之前,当进行值传递时,编译器会隐式调用拷贝构造函数;自C++11起,通过右值引用来避免由于拷贝调用而导致的性能损失
    std::move()是可以实现移动的一个方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    std::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
    71
    class 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 进行许可。
推荐阅读
Linux下的“库”和“链接” Linux下的“库”和“链接” 浙江宇视科技一面 浙江宇视科技一面 C++的继承、封装和多态 C++的继承、封装和多态
 评论