C++基础知识(12)

78、auto关键字

(1) auto是通过右边的类型推导出左边变量的类型

auto a = 10; // a的类型推导出为int

int i = 10;
auto b = i; // b的类型推导出为int

(2)auto的限制情况

  • auto的使用必须马上初始化,否则无法推导出类型
  • auto在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败
  • auto不能用作函数参数
  • 在类中auto不能用作非静态成员变量
  • auto不能定义数组,可以定义指针
  • auto无法推导出模板参数

关于auto与decltype关键字的详细讲解可看这篇博客:C++11新特性之auto和decltype


79、decltype关键字

(1)decltype是用来推导表达式类型的。编译器分析表达式的类型,表达式实际不会进行运算。

int fun {return 0;}
decltype(fun()) i; i为int类型,因为fun的返回值是int

int x = 0;
decltype(x)y; // y是int类型
decltype(x+y)z; // z是int类型

(2)decltype不会像auto一样忽略引用和cv属性,decltype会保留表达式的引用和cv属性

(3)auto和decltype的配合使用,auto和decltype一般配合使用在推导函数返回值的类型问题上。


80、lambda表达式

lambda表达式可以说是c++11最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

auto func = [capture] (params) opt -> ret { func_body; };

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable之类), ret是返回值类型,func_body是函数体。

一个完整的lambda表达式:

auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;

如上代码,很多时候lambda表达式返回值是很明显的,c++11允许省略表达式的返回值定义。

lambda表达式允许捕获一定范围内的变量:

  • []不捕获任何变量

  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用

  • [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用

  • [=, &a]值捕获外部作用域所有变量,按引用捕获a变量

  • [a]只值捕获a变量,不捕获其它变量

  • [this]捕获当前类中的this指针

lambda表达式示例代码:

int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;

auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };

代码中的f2是编译不过的,因为我们修改了按值捕获的外部变量,其实lambda表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。

还可以使用lambda表达式自定义stl的规则,例如自定义sort排序规则:

struct A {
    int a;
    int b;
};

int main() {
    vector<A> vec;
    std::sort(vec.begin(), vec.end(), [](const A &left, const A &right) { return left.a < right.a; });
}

81、基于范围的for循环

vector<int> vec;

for (auto iter = vec.begin(); iter != vec.end(); iter++) { // before c++11
    cout << *iter << endl;
}

for (int i : vec) { // c++11基于范围的for循环
    cout << "i" << endl;
}

82、委托构造函数与继承构造函数

(1)委托构造函数

委托构造函数允许在同一个类中一个构造函数调用另外一个构造函数,可以在变量初始化时简化操作

// 不使用委托构造函数的情况
class A
{
    A(int a){ m_a = a; }
    A(int a, int b){m_a = a; m_b = b;}
    A(int a, int b, int c){
        m_a = a; 
        m_b = b;
        m_c = c;
    }
    
    int m_a;
    int m_b;
    int m_c;
};

// 使用委托构造函数的情况
class A
{
    A(int a){ m_a = a; }
    A(int a, int b):A(a){ m_b = b; }
    A(int a, int b, int c):A(a,b){ m_c = c; }
    
    int m_a;
    int m_b;
    int m_c;
};

(2)继承构造函数

继承构造函数可以让派生类直接使用基类的构造函数,如果有一个派生类,我们希望派生类采用和基类一样的构造方式,可以直接使用基类的构造函数,而不是再重新写一遍构造函数。

class Base
{
    Base(int a){ m_a = a; }
    Base(int a, int b):Base(a){ m_b = b; }
    Base(int a, int b, int c):Base(a,b){ m_c = c; }
    
    int m_a;
    int m_b;
    int m_c;   
};

// 不使用继承构造函数的情况
class Derived1: Base
{
    Derived1(int a):Base(a){}
    Derived1(int a, int b):Base(a,b){}
    Derived1(int a, int b, int c):Base(a,b,c){}
};

// 使用继承构造函数的情况
class Derived2: Base
{
    using Base::Base;
};

83、列表初始化

(1)列表初始化的规则

  在C++11中可以直接在变量名后面加上初始化列表来进行对象的初始化。

(2)聚合类型可以进行直接列表初始化,那么什么是聚合类呢?

 (a) 类型是一个普通数组,如int[5],char[],double[]等

 (b) 类型是一个类,且满足以下条件:

  • 没有用户声明的构造函数
  • 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
  • 没有私有或保护的非静态数据成员
  • 没有基类
  • 没有虚函数
  • 没有{}和=直接初始化的非静态数据成员
  • 没有默认成员初始化器

(3)列表初始化的优点

  • 1 高效,减少调用构造函数的次数
  • 2 防止类型窄化,避免精度丢失的隐式类型转化
  • 3 可以使用初始化列表接受任意长度的参数

84、移动语义和完美转发

(1)移动语义

在了解移动语义之前先来回顾一下深拷贝和浅拷贝:

  • 深拷贝:指拷贝对象的时候,假如被拷贝对象还有指针引用指向其他资源,需要重新开辟一块新内存来存放这部分资源,而不是简单的拷贝

  • 浅拷贝:指拷贝对象的时候,仅仅拷贝对象本身,而忽略指针指向的内存

浅拷贝会造成两个指向指向一块内存,析构的时候会造成隐患,而深拷贝虽然安全但是效率低。

移动语义:类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担。通过移动构造函数可以实现移动语义。

#include<iostream>
using namespace std;

class Test
{
public:
    Test(){}
    Test(int size):m_size(size){
        m_data = new int[size];
    }
    Test(const Test& a){
        m_size = a.m_size;
        m_data = new int[m_size];
        cout<<"copy constructor"<<endl;
    }
    Test(Test&& a){
        m_size = a.m_size;
        m_data = a.m_data;
        a.m_data = nullptr;
        cout<<"move constructor"<<endl;
    }

    ~Test(){
        if(m_data)
        {
            delete[] m_data;
        }
    }

    int* m_data;
    int m_size;
};

int main()
{
    Test a(10);
    Test b = a; // 调用拷贝构造函数
    Test c = std::move(a); // 调用移动构造函数
    return 0;
}

如果不使用std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能!

注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。

(2)完美转发

完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。

C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。

在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可:

template <typename T>
void function(T&& t) {
    otherfunc(std::forward<T>(t));
}

测试例子如下:

#include<iostream>
using namespace std;

template<typename T>
void print(T& t){
    cout << "lvalue" << endl;
}
template<typename T>
void print(T&& t){
    cout << "rvalue" << endl;
}

template<typename T>
void TestForward(T && v){
    print(v);
    print(std::forward<T>(v));
    print(std::move(v));
}

int main(){
    TestForward(1); // lvalue,rvalue,rvalue
    int x = 1;
    TestForward(x); // lvalue,lvalue,rvalue
    TestForward(std::forward<int>(x)); // lvalue,rvalue,rvalue
    return 0;
}
  • 对于TestForward采用右值引用T &&,主要是提供一个万能接口,传参为左值和右值均可以接收!但当实参传到形参的时候,形参v俨然是一个左值,所以print(v)所打印出来的永远是lvalue,而与之相对的print(std::move(v))永远会打印rvalue。

  • print(std::forward(v))则是实现了完美转发,传进来的是左值就打印lvalue,右值就打印rvalue。具体的底层代码解释可以看这篇博客:C++11 新特性 forward 完美转发 释疑

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

czy1219

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值