C/C++/Linux

C++11中主要新特性

2019-10-29  本文已影响0人  ninedreams

lamda表达式

Lambda表达式来源于函数式编程,说白就了就是在使用的地方定义函数,有的语言叫“闭包。如果lambda函数没有传回值(例如void),其回返类型可被完全忽略。 定义在与 lambda 函数相同作用域的变量参考也可以被使用。这种的变量集合一般被称作closure(闭包)。C++引入Lambda的最主要原因就是:

  1. 可以定义匿名函数
  2. 编译器会把其转成函数对象

Lambda表达式完整的声明格式如下:

[capture list] (params list) mutable exception-> return type { function body }

各项具体含义如下

  1. capture list:捕获外部变量列表
  2. params list:形参列表
  3. mutable指示符:用来说用是否可以修改捕获的变量
  4. exception:异常设定
  5. return type:返回类型
  6. function body:函数体

此外,我们还可以省略其中的某些成分来声明“不完整”的Lambda表达式,常见的有以下几种:

[capture list] (params list) -> return type {function body}
[capture list] (params list) {function body}
[capture list] {function body}

简单的例子:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool cmp(int a, int b) {
    return  a < b;
}

int main(int argc, char** argv) {
    vector<int> myvec{ 3, 2, 5, 7, 3, 2 };
    vector<int> lbvec(myvec);

    sort(myvec.begin(), myvec.end(), cmp);   // 旧式做法
    cout << "predicate function:" << endl;
    for (int it : myvec)
        cout << it << ' ';
    cout << endl;

    sort(lbvec.begin(), lbvec.end(), [](int a, int b) -> bool { return a < b; });   //Lambda表达式
    cout << "lambda expression:" << endl;
    for (auto it : lbvec)
        cout << it << ' ';
}

自动类型推导auto

auto并没有让C++成为弱类型语言,也没有弱化变量,只是使用auto的时候,编译器根据上下文情况,确定auto变量的真正类型。auto可以用来定义变量,也可以作为函数的返回值。

auto addTest(int a, int b)  {
    return a + b;
}

int main(int argc, char** argv) {
    auto index = 10;
    auto ret = addTest(1,2);
    std::cout << "index: " << index << std::endl;
    std::cout << "res: " << ret << std::endl;
}

如上定义函数时可以使用auto作为返回值。但是auto只能用于定义函数,不能用于声明函数。如果声明在头文件中,定义在代码文件中,那么编译将无法通过,但是可以把定义和声明都在头文件中,那么就可以了。所以一般不要这么使用auto,在定义一些变量的时候使用就可。
而我一般不使用auto,因为代码习惯我想知道每个变量是什么类型,特别是一些涉及到隐式转换时,会相当的不妙。
另外auto与for循环也是相当的简洁。

int main(int argc, char** argv) {
    int numbers[] = { 1,2,3,4,5 };
    for (auto n : numbers) {
        std::cout << n << std::endl;
    }
}

以上用法不仅仅局限于数据,STL容器都同样适用。

自动化推导decltype

decltype是类型的推导,颇有一点跟auto是相反的意思。decltype的类型推导并不是像auto一样是从变量声明的初始化表达式获得变量的类型,而是总是以一个普通表达式作为参数,返回该表达式的类型,而且decltype并不会对表达式进行求值。
如下:

    int i = 4;
    decltype(i) a; //推导结果为int。a的类型为int。

using/typedef合用,用于定义类型:

    using size_t = decltype(sizeof(0)); //sizeof(a)的返回值为size_t类型
    using ptrdiff_t = decltype((int*)0 - (int*)0);
    using nullptr_t = decltype(nullptr);
    vector<int >vec;
    typedef decltype(vec.begin()) vectype;
    for (vectype i = vec.begin(); i != vec.end(); i++) {
        //...
    }

重用匿名类型,在C++中,我们有时候会遇上一些匿名类型,而借助decltype,我们可以重新使用这个匿名的结构体:

struct {
    int d ;
    doubel b;
} anon_s;
decltype(anon_s) as; //定义了一个上面匿名的结构体

最好的用处,泛型编程中结合auto,用于追踪函数的返回值类型:

template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty) {
    return x * y;
}

decltype推导四规则

  1. 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。
  2. 假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
  3. 假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。
  4. 假设e的类型是T,则decltype(e)为T。

nullptr

C/C++的NULL宏是个有很多潜在BUG的宏。因为有的库把其定义成整数0,有的定义成 (void*)0。在C的时代还好,但是在C++的时代,这就会引发很多问题。例如test(int a)这个函数,传入了a=0,这个时候你就不知道是传入的NULL还是整数0。

delete和default函数

我们知道C++的编译器在你没有定义某些成员函数的时候会给你的类自动生成这些函数,比如,构造函数,拷贝构造,析构函数,赋值函数。有些时候,我们不想要这些函数,比如,构造函数,因为我们想做实现单例模式。传统的做法是将其声明成private类型。
在新的C++11中引入了两个指示符,delete意为告诉编译器不自动产生这个函数,default告诉编译器产生一个默认的。例下:

struct A {
    A() = default; //C++11
    virtual ~A()=default; //C++11
};
struct NoCopy {
    NoCopy & operator =( const NoCopy & ) = delete;
    NoCopy ( const NoCopy & ) = delete;
};
NoCopy a;
NoCopy b(a); //compilation error, copy ctor is deleted

问题出现了,我们为什么需要加default,难道本身不就是default吗?不全然是,比如构造函数,因为只要你定义了一个构造函数,编译器就不会给你生成一个默认的。所以,为了要让默认的和自定义的共存,才引入这个参数。如下:

struct SomeType {
     SomeType() = default; // 使用编译器生成的默认构造函数
     SomeType(OtherType value);
};

关于delete还有两个有用的地方:

  1. 让你的对象只能生成在栈内存上
struct NonNewable {
    void *operator new(std::size_t) = delete;
};
  1. 阻止函数的其形参的类型调用
    void f(int i);
    void f(double) = delete;

若尝试以 double 的形参调用 f(),将会引发编译期错误, 编译器不会自动将 double 形参转型为 int 再调用f(),如果传入的参数是double,则会出现编译错误。

线程库

std::thread为C++11的线程类,使用方法和boost接口一样,非常方便。同时,C++11的std::thread解决了boost::thread中构成参数限制的问题,我想着都是得益于C++11的可变参数的设计风格。

#include <thread>
void work_thread1() {
    std::cout << "work_thread1 - 1\r\n" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "work_thread1 - 2" << std::endl;
}

void work_thread2(int iParam, std::string sParam) {
    std::cout << "work_thread2 - 1" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(5));
    std::cout << "work_thread2 - 2" << std::endl;
}

int main(int argc, char** argv) {
    std::thread t1(work_thread1);
    std::thread t2(work_thread2, 10, "abc");
    t1.join();
    std::cout << "join" << std::endl;
    t2.detach();
    std::cout << "detach" << std::endl;
}

新型智能指针

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

理解智能指针需要从下面三个层次:

包含在头文件<memory>中,shared_ptrunique_ptrweak_ptr

shared_ptr

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

例:

#include <iostream>
#include <memory>

int main() {
        int a = 10;
        std::shared_ptr<int> ptra = std::make_shared<int>(a);
        std::shared_ptr<int> ptra2(ptra); //copy
        std::cout << ptra.use_count() << std::endl;

        int b = 20;
        int *pb = &a;
        //std::shared_ptr<int> ptrb = pb;  //error
        std::shared_ptr<int> ptrb = std::make_shared<int>(b);
        ptra2 = ptrb; //assign
        pb = ptrb.get(); //获取原始指针

        std::cout << ptra.use_count() << std::endl;
        std::cout << ptrb.use_count() << std::endl;
}

运行结果:


unique_ptr

unique_ptr唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
例:

#include <iostream>
#include <memory>

int main(int argc, char** argv) {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能赋值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷贝
        std::unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
        uptr2.release(); //释放所有权
    }
    //超过uptr的作用域,內存释放
}

weak_ptr

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
例:

#include <iostream>
#include <memory>

int main(int argc, char** argv) {
    {
        std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
        std::cout << sh_ptr.use_count() << std::endl;

        std::weak_ptr<int> wp(sh_ptr);
        std::cout << wp.use_count() << std::endl;

        if(!wp.expired()) {
            std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
            *sh_ptr = 100;
            std::cout << wp.use_count() << std::endl;
        }
    }
    //delete memory
}

右值引用和move语义

在老版的C++中,临时性变量(称为右值”R-values”,位于赋值操作符之右)经常用作交换两个变量。比如下面的示例中的tmp变量。示例中的那个函数需要传递两个string的引用,但是在交换的过程中产生了对象的构造,内存的分配还有对象的拷贝构造等等动作,成本比较高。

void naiveswap_(string &a, string &b) {
 string temp = a;
 a=b;
 b=temp;
}

C++ 11增加一个新的引用(reference)类型称作右值引用(R-value reference),标记为typename &&。他们能够以non-const值的方式传入,允许对象去改动他们。这项修正允许特定对象创造出move语义。

举例而言,上面那个例子中,string类中保存了一个动态内存分存的char指针,如果一个string对象发生拷贝构造(如:函数返回),string类里的char内存只能通过创建一个新的临时对象,并把函数内的对象的内存copy到这个新的对象中,然后销毁临时对象及其内存,这是原来C++性能上重点被批评的事。

能过右值引用,string的构造函数需要改成“move构造函数”,如下所示。这样一来,使得对某个stirng的右值引用可以单纯地从右值复制其内部C-style的指针到新的string,然后留下空的右值。这个操作不需要内存数组的复制,而且空的暂时对象的析构也不会释放内存,其更有效率。

class string {
    string (string&&); //move constructor
    string&& operator=(string&&); //move assignment operator
};

The C++11 STL中广泛地使用了右值引用和move语议。因此,很多算法和容器的性能都被优化了。

上一篇 下一篇

猜你喜欢

热点阅读