c++11 右值引用 异常

c++11

列表初始化

struct Point
{
    int _x;
    int _y;
};
int main()
{
    int array1[] = { 1, 2, 3, 4, 5 };
    int array2[5] = { 0 };
    Point p = { 1, 2 };

    // C++11中列表初始化也可以适用于new表达式中
    int* pa = new int[4]{ 0 };

    return 0;
}
calss Date{};

int main()
{
    Date d1(2022, 1, 1); // old style
    // C++11支持的列表初始化,这里会调用构造函数初始化
    Date d2{ 2022, 1, 2 };
    Date d3 = { 2022, 1, 3 };
    return 0;
}

std::initializer_list使用场景:
std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加
std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=
的参数,这样就可以用大括号进行赋值。

变量类型的推导

1.auto

C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初

int main()
{
    int i = 10;
    auto p = &i;
    auto pf = strcpy;
    cout << typeid(p).name() << endl;
    cout << typeid(pf).name() << endl;
    map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
    //map<string, string>::iterator it = dict.begin();
    auto it = dict.begin();
    return 0;
}

始化值的类型。

2.decltype

关键字decltype将变量的类型声明为表达式指定的类型。

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
    decltype(t1 * t2) ret;
    cout << typeid(ret).name() << endl;
}
int main()
{
    const int x = 1;
    double y = 2.2;
    decltype(x * y) ret; // ret的类型是double
    decltype(&x) p; // p的类型是int*
    cout << typeid(ret).name()<<endl;
    
    F(1, 'a');
    return 0;
}
3.nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示
整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

范围for

final和 override

final关键字修饰的函数不可以被继承。

override关键字可以检查虚函数是否被重新

右值引用

1.概念

左值:可以取地址的值

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边
右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

右值:不可以取地址的值

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)
等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。

无论是右值引用还是左值引用都是给对象取别名。

int main()
{
    double x = 1.1, y = 2.2;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下几个都是对右值的右值引用
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
        // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    10 = 1;
    x + y = 1;
    fmin(x, y) = 1;
    return 0;
}

右值不能取地址,但是对右值引用的变量可以取地址,因为右值引用后会存到特点的位置。

int main()
{
    double x = 1.1, y = 2.2;
    int&& rr1 = 10;
    const double&& rr2 = x + y;
    rr1 = 20;
    rr2 = 5.5; // 报错
    return 0;
}
2.左值引用和右值引用的比较

2.1 左值引用可以引用左值,不能引用右值,但是const 左值引用可以引用右值

int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a; // ra为a的别名
    //int& ra2 = 10; // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

2.2 右值引用只能引用右值,不能引用左值

      但是右值引用可以引用move后的左值

int main()
{
    // 右值引用只能右值,不能引用左值。
    int&& r1 = 10;
    // error C2440: “初始化”: 无法从“int”转换为“int &&”
    // message : 无法将左值绑定到右值引用
    int a = 10;
    int&& r2 = a;
    // 右值引用可以引用move以后的左值
    int&& r3 = std::move(a);
    return 0;
}
3.右值引用应用场景

移动构造,移动赋值

calss string
{
public:
    // 移动构造
    string(string&& s)
    :_str(nullptr)
    ,_size(0)
    ,_capacity(0)
    {
        cout << "string(string&& s) -- 移动语义" << endl;
        swap(s);
    }
    // 移动赋值
    string& operator=(string&& s)
    {
        cout << "string& operator=(string&& s) -- 移动语义" << endl;
        swap(s);
        return *this;
    }
private:
    char* _str;
    size_t _size;
    size_t _capacity; // 不包含最后做标识的\0
};

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回
。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,
传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

右值引用和移动语义解决上述问题:
在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用
了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

不仅仅有移动构造,还有移动赋值:
在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值
string& operator=(string&& s)
{
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}
int main()
{
    bit::string ret1;
    ret1 = bit::to_string(1234);
    return 0;
}
 // 运行结果:
 // string(string&& s) -- 移动语义
 // string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象
接收,编译器就没办法优化了。bit::to_string函数中会先用str生成构造生成一个临时对象,但是
我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时
对象做为bit::to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。
STL中.

4.完美转发

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发


template<typename T>
void PerfectForward(T&& t)
{
    Fun(t);
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

std::forward 完美转发在传参的过程中保留对象原生类型属性

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
    Fun(std::forward<T>(t));
}
int main()
{
    PerfectForward(10); // 右值
    int a;
    PerfectForward(a); // 左值
    PerfectForward(std::move(a)); // 右值
    const int b = 8;
    PerfectForward(b); // const 左值
    PerfectForward(std::move(b)); // const 右值
    return 0;
}

可变参数模板

// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

lambda表达式(实际上可以看成一个对象)

1.语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement
}

[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用但是只能捕捉同一作用域的或者全局变量。
(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。

int main()
{
    // 最简单的lambda表达式, 该lambda表达式没有任何意义
    []{};
    // 省略参数列表和返回值类型,返回值类型由编译器推导为int
    int a = 3, b = 4;
    [=]{return a + 3; };
    // 省略了返回值类型,无返回值类型
    auto fun1 = [&](int c){b = a + c; };
    fun1(10)
    cout<<a<<" "<<b<<endl;
    // 各部分都很完善的lambda函数
    auto fun2 = [=, &b](int c)->int{return b += a+ c; };
    cout<<fun2(10)<<endl;
    // 复制捕捉x
    int x = 10;
    auto add_x = [x](int a) mutable { x *= 2; return a + x; };
    cout << add_x(10) << endl;
    return 0;
}

通过上述例子可以看出,lambda表达式实际上可以理解为无名函数该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。

2.捕捉列表说明

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
[var]:表示值传递方式捕捉变量var
[=]:表示值传递方式捕获所有父作用域中的变量(包括this)
[&var]:表示引用传递捕捉变量var
[&]:表示引用传递捕捉所有父作用域中的变量(包括this)
[this]:表示值传递方式捕捉当前的this指针
注意:

a. 父作用域指包含lambda函数的语句块
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
           [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者
非局部变量都会导致编译报错。
f. lambda表达式之间不能相互赋值,即使看起来类型相同

void (*PF)();
int main()
{
    auto f1 = []{cout << "hello world" << endl; };
    auto f2 = []{cout << "hello world" << endl; };
    // 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了
    //f1 = f2; // 编译失败--->提示找不到operator=()
    // 允许使用一个lambda表达式拷贝构造一个新的副本
    auto f3(f2);
    f3();
    // 可以将lambda表达式赋值给相同类型的函数指针
    PF = f2;
    PF();
    return 0;
}

3.函数对象与lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。

class Rate
{
public:
    Rate(double rate): _rate(rate)
    {}
    double operator()(double money, int year)
    { return money * _rate * year;}
private:
    double _rate;
};
int main()
{
    // 函数对象
    double rate = 0.49;
    Rate r1(rate);
    r1(10000, 2);
    // lamber
    auto r2 = [=](double monty, int year)->double{return monty*rate*year;};
    r2(10000, 2);
    return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样
函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可
以直接将该变量捕获到。

包装器

function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

有了包装器,可以解决模板的效率低下,实例化多份的问题!

#include <functional>
template<class F, class T>
T useF(F f, T x)
{
    static int count = 0;
    cout << "count:" << ++count << endl;
    cout << "count:" << &count << endl;
    return f(x);
}
    double f(double i)
    {
        return i / 2;
    }
    struct Functor
    {
        double operator()(double d)
        {
            return d / 3;
        }
    };
int main()
{
    // 函数名
    std::function<double(double)> func1 = f;
    cout << useF(func1, 11.11) << endl;
    // 函数对象
    std::function<double(double)> func2 = Functor();
    cout << useF(func2, 11.11) << endl;
    // lamber表达式
    std::function<double(double)> func3 = [](double d)->double{ return d /4; };
    cout << useF(func3, 11.11) << endl;
    return 0;
}

另外玩法

// 使用包装器以后的玩法
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
    stack<int> st;
    map<string, function<int(int, int)>> opFuncMap =
    {
        { "+", [](int i, int j){return i + j; } },
        { "-", [](int i, int j){return i - j; } },
        { "*", [](int i, int j){return i * j; } },
        { "/", [](int i, int j){return i / j; } }
    };
    for(auto& str : tokens)
    {
        if(opFuncMap.find(str) != opFuncMap.end())
        {
            int right = st.top();
            st.pop();
            int left = st.top();
            st.pop();
            st.push(opFuncMap[str](left, right));
        }
        else
            st.push_back(stoi(str));
    }
    return st.top();
}
};
bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可
调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而
言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M
可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺
序调整等操作。

// 使用举例
#include <functional>
int Plus(int a, int b)
{
    return a + b;
}
class Sub
{
public:
    int sub(int a, int b)
    {
        return a - b;
    }
};
int main()
{
    //表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
    std::function<int(int, int)> func1 = std::bind(Plus, placeholders::_1,
    placeholders::_2);
    //auto func1 = std::bind(Plus, placeholders::_1, placeholders::_2);
    //func2的类型为 function<void(int, int, int)> 与func1类型一样
    //表示绑定函数 plus 的第一,二为: 1, 2
    auto func2 = std::bind(Plus, 1, 2);
    cout << func1(1, 2) << endl;
    cout << func2() << endl;
    Sub s;
    // 绑定成员函数
    std::function<int(int, int)> func3 = std::bind(&Sub::sub, s,
    placeholders::_1, placeholders::_2);
    // 参数调换顺序
    std::function<int(int, int)> func4 = std::bind(&Sub::sub, s,
    placeholders::_2, placeholders::_1);
    cout << func3(1, 2) << endl;
    cout << func4(1, 2) << endl;
    return 0;
}

异常

C++异常概念

异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。

throw:当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。

catch:在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异常,可以有多个catch进行捕获。

try:try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。
使用 try/catch 语句的语法如下所示:

try
{
    // 保护的标识代码
}catch( ExceptionName e1 )
{
    // catch 块
}catch( ExceptionName e2 )
{
    // catch 块
}catch( ExceptionName eN )
{
    // catch 块
}

异常的使用

异常的抛出和匹配原则

1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,
所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似
于函数的传值返回)
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,
使用基类捕获
,这个在实际中非常实用。

在函数调用链中异常栈展开匹配原则

1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则
调到catch的地方进行处理。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的
catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异
常,否则当有异常没捕获,程序就会直接终止。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "Division by zero condition!";
    else
        return ((double)a / (double)b);
}
void Func()
{
    int len, time;
    cin >> len >> time;
    cout << Division(len, time) << endl;
}
int main()
{
    try 
    {
        Func();
    }
    catch (const char* errmsg) {
        cout << errmsg << endl;
    }
    catch(...){
    cout<<"unkown exception"<<endl;
    }
    return 0;
}
异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用
链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
    {
        throw "Division by zero condition!";
    }
    return (double)a / (double)b;
}
void Func()
{
    // 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
    // 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
    // 重新抛出去。
    int* array = new int[10];
    try 
    {
        int len, time;
        cin >> len >> time;
        cout << Division(len, time) << endl;
    }
    catch (...)
    {
        cout << "delete []" << array << endl;
        delete[] array;
        throw;
    }
    // ...
    cout << "delete []" << array << endl;
    delete[] array;
}
int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
    cout << errmsg << endl;
    }
    return 0;
}

异常安全

构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不
完整或没有完全初始化。

析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内
存泄漏、句柄未关闭等)
C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄
漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII
我们智能指针这节进行讲解。

异常规范

1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的
后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

2. 函数的后面接throw(),表示函数不抛异常
3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值