C++标准库 01 C++11语言新特性

发布于 2021-06-07  138 次阅读


一、两个细节

1.1 Template表达式中的空格

vector<vector<int> >;   //在所有cpp版本中可用
vector<vector<int>>;    //在cpp11标准中可用

1.2 nullpter与NULL

  nullptr为无类型的指针,NULL类型为int。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

void func(int);
void func(void*);

int main()
{  
    func(NULL); //calls func(int)
    func(nullptr);  //calls func(void*)
    return 0;
}

void func(int)
{
    cout << "calls func(int)" << endl;
}

void func(void*)
{
    cout << "calls func(void*)" << endl;
}

二、以auto完成类型自动推导

  在cpp11中,你可以使用“auto”关键字创建一个对象而不“直接指定”其类型。
  auto产生的对象根据其初始化值自动推导出该对象的类型,这种初始化可以是显示的,也可以是隐式的。
  下面通过一段代码来演示。

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

int main()
{  
    //基本用法:
    auto MyInt = 5;
    auto MyDouble(5.6);
    cout << "MyInt == " << MyInt << endl;
    cout << "MyDouble == " << MyDouble << endl;
    //auto a;   错误,auto应该在初始化时候为其指定类型

    //常见用法:auto代替复杂类型
    vector<int>MyVector;    //创建容器
    MyVector.push_back(0);

    vector<int>::iterator it = MyVector.begin();
    auto itr = MyVector.begin();
    if (it == itr)
    {
        cout << "it == itr" << endl;
    }
    //display: it == itr
    return 0;
}

三、初始化相关

3.1 一致性初始化(Uniform Initialization)

  在cpp11表中中,所有的初始化操作都被可以被大括号所代替。
  下面通过一段代码来演示。

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

int main()
{  
    int a(5);   //ok
    int b{ 5 }; //ok

    complex<int>c1{ 5,6 };  //ok
    complex<int>c2(5, 6);   //ok

    vector<int> v1{ 0, 1, 2 };  //ok 数组的初始化
    //vector<int> v2(0, 1, 2);  //wrong

    int c[]{ 1,2,3 };   //ok 数组的初始化
    //int d[](1, 2, 3); //wrong
    return 0;
}

3.2 初值列(Initializer List)

  初值列(用大括号进行初始化)会造成强迫初始化,如果变量属于某种基本类型,那么它将被初始化为0(或者nullptr——对指针而言)。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

int main()
{  
    int a(5);   //如果是:int a(); 则会报错:无法解析的外部命令
    int b{};
    cout << "a == " << a << endl;   //display 5
    cout << "b == " << b << endl;   //display 0
    return 0;
}
    初始列可以避免窄化(narrowing)
    下面通过一段代码来演示。

#include <iostream>
using namespace std;

int main()
{  
    int a(5.6);     //warning
    int b{ 5.6 };   //errow
    cout << "a == " << a << endl;
    cout << "b == " << b << endl;
    return 0;
}

  cpp11提供了自定义初值列来进行初始化。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

int main()
{  
    initializer_list<int>list{ 5,6,7 };
    for (auto it = list.begin(); it != list.end(); ++it)
    {
        cout << *it << ' ';
    }
    cout << endl;
    //display 5 6 7
    return 0;
}

  当初值列和指明参数个数的构造函数同时存在时,初值列构造函数将胜出。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

class Initializer
{
public:
    Initializer(int, int)
    {
        cout << "calls (int, int)" << endl;
    }

    Initializer(initializer_list<int>)
    {
        cout << "call list" << endl;
    }
};

int main()
{  
    //参数个数相同的时候:
    Initializer a(5, 6);        //显式小括号:calls (int, int)
    Initializer b{ 5,6 };       //显式大括号:call list
    Initializer e = { 1,2 };    //隐式:call list

    //参数个数不同的时候:
    Initializer c{ 1 };     //  call list
    Initializer d{ 1,2,3 };     //  call list
    return 0;
}

四、Range-Based for循环

  cpp11引入了新的循环形式,可以逐一迭代某个区间、集合、数组内的每一个元素。这种方式和Python中的for in range()类似。
  下面通过一段代码来演示。

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

int main()
{  
    vector<int>num{ 0,1,2 };
    for (const auto& it : num)
    {
        cout << it << " ";
    }
    //display 0 1 2
    return 0;
}

五、move语义

  注意:move语义是cpp11标准中最重要的一部分,建议联合其他资料一起阅读。
  cpp11提供了一种避免非必要拷贝(copy)和临时对象(temporary)的方法,这种方法被称为搬迁语义(move semantic)。
  请看下面一段代码。

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

class Base
{
private:
    int *element;
public:
    Base(const int& _el = 0)
    {
        element = new int(_el);
    }

    Base(const Base& _b):element{ _b.element }
    {
        cout << "call copy constructor" << endl;
    }

    Base(Base&& _b) :element{ _b.element }
    {
        _b.element = nullptr;
        cout << "call move constructor" << endl;
    }
};

Base func()
{
    Base b;
    return b;
}

int main()
{  
    Base Abase{ 5 };
    Base Bbase{ Abase };        //左值 调用copy构造函数
    Base Cbase{ func() };       //右值 调用move构造函数
    Base Dbase{ move(Abase) };  //move左值 调用move构造函数
    return 0;
}

  在上述结果中对于左值,采用复制构造函数;对于右值采用move构造函数。
  那么为什么move构造中要把原始指针置为null呢?因为如果不置为null,那么临时变量消失时,会析构,释放自己的指针,还资源给os,那么新对象中的指针也就随之会被delete掉。所以需要把原始指针置为null,这样临时变量在析构时就找不到给自己分配的那块内存了,那块内存也就可以被新对象正常使用了。
  注意:这里的重载规则要求如果要调用move构造,那么一定要传一个临时变量,并且一定要是一个可以修改的临时变量,如果不能修改,那么临对象内的指针就不能置为null了,如果一个函数返回的是const类型,那么就不能调用move构造了
  下面来看move函数。

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

int main()
{  
    vector<int> num{ 1,2,3,4,5,6 };
    move(num.begin() + 2, num.begin() + 3, num.begin());
    for (const auto& it : num)
    {
        cout << it << ' ';
    }
    //display 3 2 3 4 5 6等效于copy函数
    //范围[first, second)
    return 0;  
}

六、新式的字符串字面常量(String Literal)

  Row String Literal允许我们定义字符序列(character sequence)。这样可以节省掉不必要的特殊符号。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

int main()
{  
    string a = R"(\\n)";    //等价于 "\\\\n"
    cout << a << endl;      //display \\n

    string b = R"nc(a\
                    b\nc()"
                    )nc";
    cout << b << endl;      //等价于"a\\\n                   b\\nc()"\n";注意空格
    return 0;  
}

  Row string以R”(开头,以)”结尾,同时可以嵌套字母,例如:用R”abc(_yourchars)abc”来来书写。

七、Lambda表达式

7.1 Lambda语法

  所谓Lambda是一份功能定义式,可被用于语句(statement)或表达式(expression)内部。因此Lambda可以被用作内联函数来使用。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

int main()
{  
    auto lambda = []{
        cout << "call Lambda" << endl;
    };
    lambda();
    return 0;  
}
    同时,Lambda也可以接受参数,比如下面这样。

#include <iostream>
using namespace std;

int main()
{  
    string a = "call Lambda";
    auto lambda = [](const string& _l){
        cout << _l << endl;
    };
    lambda(a);
    return 0;  
}

  注意:Lambda不可以是template,必须指明所有类型。

7.2 Lambda的返回类型

  Lambda也可以像普通函数一样拥有返回值,这里你可以指定返回值的类型,也可以不指定。

#include <iostream>
using namespace std;

int main()
{  
    auto UnSpecifyType = []() {
        return 1;
    };

    auto SpecifyType = []()->int {
        return 2;
    };
    cout << UnSpecifyType() << endl;
    cout << SpecifyType() << endl;
    return 0;  
}

7.3 Capture用以访问外部作用域

  在Lambda introducer(每个Lambda开始的方括号)内,你可以指明一个capture来处理作用域内未被传递为实参的数据。

  • [=]意味着以by value的方式传入(传值,不可修改)。因此当这个Lambda被定义时,你可以读取所有外部数据,但是不能更改他们。
  • [&]意味着以by reference的方式传入(引用,可以修改)。因此当这个Lambda被定义时,你对所有数据的修改都是合法的,前提是拥有修改的权限。

  你还可以用mutable的方式传入(传值,可以修改)。这种方式虽然已by value的方式传入,但是你却可以修改其中的所有数据。
  下面通过一段代码来演示。

#include <iostream>
using namespace std;

int main()
{  
    int x = 0, y = 0;

    auto ByValue = [x, y]()
    {
        //++x;  error:必须是可以修改的左值
        //++y;
    };

    auto ByReference = [&x, &y]()
    {
        ++x;    //ok
        ++y;
    };

    auto Mutable = [x, y]()mutable
    {
        ++x;    //ok
        ++y;
    };
    return 0;  
}

  值得注意的是,想要在Lambda中使用外部参数,必须要将其放入introducer中。

7.4 Lambda的类型

  Lambda的类型是个不具名function object(或称functor)。每个Lambda表达式的类型是独一无二的。如果想根据类型声明对象,可借助于template或auto。如果实在需要写下类型,可使用decltype(),例如把一个Lambda作为hash fucntion或是ordering准则或是sorting准则传递给关联式(associative)容器或者不定序(unordered)容器。
  或者使用标准库提供的std::functioon<>class template,指明一个一般化类型给functional programing使用。这个class template提供了“明确指出函数的返回类型为Lambda的唯一方法”。
  下面通过一段代码来演示。

#include <iostream>
#include <functional>
using namespace std;

function<int(int, int)> ReturnLambda()
{
    return [](int x, int y)
    {
        return x + y;
    };
}

int main()
{  
    auto l = ReturnLambda();
    cout << l(5, 6) <<  endl;
    //返回类型为lambda,不能够直接cout,这里用一个中间变量代替函数本身
    return 0;  
}

八、虽旧犹新的语言特性

8.1 非模板类型参数(Nontype Template Parameter)

  在类型参数(Type parameter)之外,我们也可以为template使用非类型参数(Nontype parameter)。这样的参数亦被视为template的一部分。例如对于标准的class bitset<>,你可以传递bit个数作为template实参。下面的语句定义了两个bitfield:一个带有62bit,另外一个带有64bit。

#include <iostream>
#include <bitset>
using namespace std;

int main()
{  
    bitset<32>flags32 = 5;
    cout << flags32 << endl;
    //display:00000000000000000000000000000101
    bitset<64>flags64 = 5;
    cout << flags64 << endl;
    //display:0000000000000000000000000000000000000000000000000000000000000101
    return 0;  
}

  因为使用了不同的实参,使用这些bitset的类型是不相同的。因此不可以对上述二者使用运算符,除非进行类型转换。

8.2 基础类型的明确化初始值

  如果你使用“一个明确的构造函数调用,但不给实参”这样的语法,基础类型会被设定初值0。
  下面通过一段代码来说明。

#include <iostream>
using namespace std;

int main()
{  
    int a;          //undefined value
    int b = int();  //initialized with zero
    int c();        //error
    int d{};        //initialized with zero(since C++11)
    cout << a << ' ' << b << ' ' << c << ' ' << d << endl;
    return 0;  
}

8.3 main()定义式

  main函数只有两种定义式具备可移植性,那就是:

int main()
{  

}
//和
int main(int argc, char** argv)
{  

}

  注意其返回类型必须是int。
  你可以(但不必要)以一个return结束main()。不同于c,c++定义了一个隐晦的return 0;于main()的终点。这意味着在程序离开main()的时候不用写return语句。0意外的任何值都代表着某种失败。
  如果不想以“main()返回”方式结束c++程序,通常应该调用exit()、quick_exit()(c++11)或terminate()。