C++11的一些常见特性

因为面试被问到了,C++的新特性,但从未归纳过,故将整理c++11,c++17,c++20的常见特性,并用例子实现一遍。加油!!!

1.nullptr

C++用nullptr代替NULL,原因NULL在C++中会被定义为0或(void*)0,取决于编译器。

C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。

从而会引发重载的一些问题,例如

void foo(char *a);
void foo(int a);

为了避免这块从而引入nullptr,nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。

const class nullptr_t
{
public:
    template<class T>
    inline operator T*() const
        { return 0; }

    template<class C, class T>
    inline operator T C::*() const
        { return 0; }
 
private:
    void operator&() const;
} nullptr = {};

2.类型推导

C++11 引入了 auto 和 decltype 这两个关键字实现了类型推导,让编译器来操心变量的类型。

auto

auto 在很早以前就已经进入了 C++,但是他始终作为一个存储类型的指示符存在,与 register 并存。在传统 C++ 中,如果一个变量没有声明为 register 变量,将自动被视为一个 auto 变量。而随着 register 被弃用,对 auto 的语义变更也就非常自然了。(register以前就是将变量放到寄存器中,不能直接用地址取,对register使用取地址操作会让register失效,反之默认是auto,现已废弃)

注意:auto只能推导出数据的不加引用(&)的数据类型

但是实测可以推导出const char *和char *,推导不出const char和char,说明顶层const的const会被忽略

此外,auto 还不能用于推导数组类型

auto 的推导规则

  1. 在不声明为引用或指针时,auto会忽略等号右边的引用类型和const、volatile限定
  2. 在声明为引用或者指针时,auto会保留等号右边的引用和const、volatile属性
auto i; // error: declaration of variable 't' with deduced type 'auto' requires an initializer
//因此我们在使用auto时,必须对该变量进行初始化。

auto i= 0; //0为int类型,auto自动推导出int类型
auto j = 2.0; //auto 自动推导出类型为float

int a = 0;
auto b = a; //a 为int类型
auto &c = a; //c为a的引用
auto *d = &a; //d为a的指针
auto i = 1, b = "hello World"; //error: 'auto' deduced as 'int' in declaration of 'i' and deduced as 'const char *' in declaration of 'b'

/* auto 作为成员变量的使用*/
class test_A
{
public:
    test_A() {}
    auto a = 0; //error: 'auto' not allowed in non-static class member
    static auto b = 0; //error: non-const static data member must be initialized out of line
    static const auto c = 0;
};

/*c11 中的使用*/
auto func = [&] {
    cout << "xxx";
}; 
// 不关心lambda表达式究竟是什么类型
auto asyncfunc = std::async(std::launch::async, func);

 

decltype

 decltype用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算

注意:decltype不会像auto一样忽略引用和const、volatile属性,decltype会保留表达式的引用和const、volatile属性

decltype 的推导规则

对于decltype(exp)有:

  1. exp是表达式,decltype(exp)和exp类型相同
  2. exp是函数调用,decltype(exp)和函数返回值类型相同
  3. 其它情况,若exp是左值,decltype(exp)是exp类型的左值引用
int a = 0, b = 0;
decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值

d = 20;
cout << "c " << c << endl; // 输出c 20

auto 与 decltype 配合

decltype(auto)是C++14新增的类型指示符,可以用来声明变量以及指示函数返回类型。在使用时,会将
“=”号左边的表达式替换掉auto,再根据decltype的语法规则来确定类型。举个例子:
int e = 4;
const int* f = &e; // f是底层const
decltype(auto) j = f;//j的类型是const int* 并且指向的是e

基于范围的 for 循环

C++11 引入了基于范围的迭代写法,我们拥有了能够写出像 Python 一样简洁的循环语句。

// & 启用了引用
for(auto &i : arr) {    
    std::cout << i << std::endl;
}

初始化列表

C++11 提供了统一的语法来初始化任意的对象,例如:

struct A {
    int a;
    float b;
};
struct B {

    B(int _a, float _b): a(_a), b(_b) {}
private:
    int a;
    float b;
};

A a {1, 1.1};    // 统一的初始化语法
B b {2, 2.2};
对于在函数体中初始化,是在所有的数据成员被分配内存空间后才进行的。
列表初始化是给数据成员分配内存空间时就进行初始化,就是说分配一个数据成员只要冒号后有此数据成
员的赋值表达式(此表达式必须是括号赋值表达式),那么分配了内存空间后在进入函数体之前给数据成员
赋值,就是说初始化这个数据成员此时函数体还未执行。

默认模板参数

//这里用到了auto和decltype的结合推导
template<typename T = int, typename U = int> auto add(T x, U y) -> decltype(x+y) { return x+y; }

Lambda 表达式

Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

Lambda 表达式的基本语法如下:

[ caputrue ] ( params ) opt -> ret { body; };

1) capture是捕获列表;
2) params是参数表;(选填)
3) opt是函数选项;可以填mutable,exception,attribute(选填)
mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
exception说明lambda表达式是否抛出异常以及何种异常。
attribute用来声明属性。
4) ret是返回值类型(拖尾返回类型)。(选填)
5) body是函数体。

捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

1) []不捕获任何变量。
2) [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。
3) [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的变量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当使用引用方式捕获。

int a = 0;
auto f = [=] { return a; };

a+=1;

cout << f() << endl;       //输出0

int a = 0;
auto f = [&a] { return a; };

a+=1;

cout << f() <<endl;       //输出1

4) [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。
5) [bar]按值捕获bar变量,同时不捕获其他变量。
6) [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

class A
{
 public:
     int i_ = 0;

     void func(int x,int y){
         auto x1 = [] { return i_; };                   //error,没有捕获外部变量
         auto x2 = [=] { return i_ + x + y; };          //OK
         auto x3 = [&] { return i_ + x + y; };        //OK
         auto x4 = [this] { return i_; };               //OK
         auto x5 = [this] { return i_ + x + y; };       //error,没有捕获x,y
         auto x6 = [this, x, y] { return i_ + x + y; };     //OK
         auto x7 = [this] { return i_++; };             //OK
};

int a=0 , b=1;
auto f1 = [] { return a; };                         //error,没有捕获外部变量    
auto f2 = [&] { return a++ };                      //OK
auto f3 = [=] { return a; };                        //OK
auto f4 = [=] {return a++; };                       //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; };                      //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); };                //OK
auto f7 = [=, &b] { return a + (b++); };                //OK

注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。

原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。

lambda表达式的大致原理:每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。

lambda表达式是不能被赋值的:

auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b;   // 非法,lambda无法赋值
auto c = a;   // 合法,生成一个副本

lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。

最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过lambda表达式给出条件,传递给count_if函数:

int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });

再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:

vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(), [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

当需要遍历容器并对每个元素进行操作时:

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val){
    if(!(val & 1)){
        ++ even_count;
    }
});
std::cout << "The number of even is " << even_count << std::endl;

新增容器

std::array栈上的数组

std::forward_list 单向链表(不提供size()函数)

std::unordered_map/std::unordered_multimap 和 std::unordered_set/std::unordered_multiset。

无序容器中的元素是不进行排序的,内部通过 Hash 表实现,插入和搜索元素的平均复杂度为 O(constant)。

std::tuple元组,不可变的字典

右值引用和move语义

为什么要有右值引用。

1.效率性,如果一个变量不用了,我们用移动构造可以复用之前的内存,从而只需要实现指针的转移,而不是重新去申请一块内存进行赋值,可能会有阻塞(即使很小),效率慢。

2.安全性,当我们用左值可能调用类的成员函数,这会导致不可预知的行为,右值却是非常安全的,因为复制构造函数之后,我们不能再使用这个临时对象了,因为这个转移后的临时对象会在下一行之前销毁掉。

td::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。

当然,如果你在使用了mova(a)之后,还继续使用a,那无疑是搬起石头砸自己的脚,还是会导致严重的运行错误。

总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。

 

 

posted @ 2021-09-08 22:25  Ldler  Views(284)  Comments(0Edit  收藏  举报