C++ 11新特性

constexpr關鍵字

常量表達式給編譯器提供了優化的機會, 編譯器計算出他們的值並把結果硬編碼到程序中. 例如,定義一個數組需要常量表達式(來指定數組大小), 枚舉值必須是常量表達式. 然而,常量表達式中從來都不允許調用函數或創建對象. 所以,像下面這樣的簡單代碼卻是非法的:

int get_five() {return 5;} 
int some_value[get_five() + 7]; // 創建一個包含12個整數的數組. 這種形式在C++中是非法的.

因爲get_five() + 7不是常量表達式. C++98的編譯器在編譯期沒辦法知道get_five()是常量.

C++11引入了constexpr關鍵字,允許將變量聲明爲constexpr類型以便由編譯器來驗證變量的值是否是一個常量表達式。聲明爲constexpr的變量一定是一個常量,而且必須用常量表達式初始化,否則編譯器將報錯。上面的例子可以寫成下面這樣:

constexpr int get_five() {return 5;} 
int some_value[get_five() + 7]; // 創建一個包含12個整數的數組. 這種形式在C++11中是合法的.

這樣可以讓編譯器理解並驗證get_five()是一個編譯期常量!

常量表達式的值需要在編譯時就得到計算,因此對聲明constexpr時用到的類型必須有所限制。因爲這些類型一般比較簡單,值也顯而易見、容易得到,就把它們稱爲“字面值類型”。

1.變量限制

目前接觸過的類型中,算數類型、引用和指針和某些類都屬於字面值類型。自定義類、IO庫、string類型則不屬於字面值類型,也就不能被定義成constexpr。儘管指針和引用都能定義成constexpr,但是它們的初始值卻受到嚴格限制。一個constexpr指針必須是nullptr或0,或者是存儲於某個固定地址中的對象。

//constexpr聲明中如果定義了一個指針,限定符constexpr僅僅對指針有效,與指針所指的對象無關。constexpr把它所定義的對象置爲了頂層const。
const int *p=nullptr;              //p是指針常量
int *const r=nullptr;              //r是常量指針
constexpr int *q=nullptr;          //q是常量指針

constexpr int i=10;                //10是常量表達式
constexpr int j=i+10;              //i+10是常量表達式
constexpr int k=size();            //當size是個constexpr函數時,纔是正確的聲明語句

2.函數限制

作用在函數上的constexpr關鍵字對函數的行爲施加了一些限制,如get_five()函數。

1.這個函數的返回值類型不能是void;

2.在函數體中不能聲明變量或新類型;

3.函數體內只能包含聲明語句,空語句和單個return語句且,return語句中的表達式也必須是常量表達式;

3.constexpr和const

const並不能代表“常量”,它僅僅是對變量的一個修飾,告訴編譯器這個變量只能被初始化,且不能被直接修改(實際上可以通過堆棧溢出等方式修改)。而這個變量的值,可以在運行時也可以在編譯時指定。

constexpr可以用來修飾變量、函數、構造函數。一旦以上任何元素被constexpr修飾,那麼等於說是告訴編譯器 “請大膽地將我看成編譯時就能得出常量值的表達式去優化我”。

初始化列表

int a[]={1,3,5};//C++98通過,C++11通過
int b[]{2,4,6};//C++98失敗,C++11通過
vector<int> c{1,3,5};//C++98失敗,C++11通過
map<int,float> d = {{1,1.0f},{2,2.0f},{5,3.2f}};//C++98失敗,C++11通過

 統一的初始化

C++11提供了一個完全統一的可以用在任何類型的對象的初始化語法. 它擴展了初始化列表語法:

struct BasicStruct 
{
   int x;
   double y;
};

struct AltStruct 
{
   AltStruct(int x, double y) : x_{x}, y_{y} {}

private:
   int x_;
   double y_;
};

BasicStruct var1{5, 3.2};
AltStruct var2{2, 4.3};

C++98中,僅允許使用initializer-list初始化數組,C++11擴展了initializer-list的概念,使得普通類型也可以使用initializer-list初始化(不要把它和類的成員初始化搞混,它們的確都叫initializer-list,要區分時,可以將類的成員初始化叫做member initializer list):

#include <initializer_list>

class Magic {
public:
    Magic(std::initializer_list<int> list) {}
};

Magic magic = {1,2,3,4,5};
std::vector<int> v = {1, 2, 3, 4};

 

型推導 

在C++98中,auto其實是用於聲明一個變量具有“自動存儲期”,但實際上除了static以外的變量都是默認具有自動存儲期的,因此過去的auto幾乎沒有人使用。

C++11中,auto被賦予了新的含義,以前的auto含義被取消了。auto成爲了新的類型指示符,auto聲明的變量的類型必須在編譯期由編譯器推導出來。

定義有顯式初始化的變量可以用auto關鍵字來自動確定變量類型,這將用初始化表達式的類型來創建變量:

auto some_strange_callable_type = boost::bind(&some_function, _2, _1, some_object);
auto other_variable = 5;

//程序員不用像下面這樣寫代碼
for (std::vector::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

//而可以用更簡短的形式:
for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)

另外,關鍵字decltype可以用來在編譯期確定表達式的類型.例如: 

int some_int;
decltype(some_int) other_integer_variable = 5;

decltype 和 auto 一起使用會更爲有用,因爲 auto 參數的類型只有編譯器知道.然而 decltype對於那些大量運用運算符重載和類型特化來編碼的表達式非常有用。auto對減少代碼冗餘也很有用.比如說:

auto x = 1;
auto y = 2;
decltype(x+y) z;

追蹤返回類型

//因爲在函數中 x和y的類型未聲明,所以引入追蹤返回類型來聲明返回值類型
template<typename T, typename U>
auto add(T x, U y) -> decltype(x+y) 
{
    return x+y;
}

decltype在推導類型時,如果表達式是一個簡單的名字,它會推導出名字的類型,但如果表達式不只是一個名字,比如decltype((x)),那麼即使x只是一個int,該decltype也會推導出引用類型:int&。

int a = 0;
delctype(a)    //推導出int類型
delctype((a))  //推導出int&類型

for循環 

int my_array[5] = {1, 2, 3, 4, 5};

for (int &x : my_array) 
{
    std::cout<<x;
}

 Lambda表達式

Lambda 表達式,實際上就是提供了一個類似匿名函數的特性,而匿名函數則是在需要一個函數,但是又不想費力去命名一個函數的情況下去使用的。Lambda 表達式的基本語法如下:

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

1) []不捕獲任何變量。
2) [&]捕獲外部作用域中所有變量,並作爲引用在函數體中使用(按引用捕獲)。
3) [=]捕獲外部作用域中所有變量,並作爲副本在函數體中使用(按值捕獲)。注意值捕獲的前提是變量可以拷貝,且被捕獲的變量在 lambda 表達式被創建時拷貝,而非調用時才拷貝。如果希望lambda表達式在調用時能即時訪問外部變量,我們應當使用引用方式捕獲。
4) [=,&foo]按值捕獲外部作用域中所有變量,並按引用捕獲foo變量。
5) [bar]按值捕獲bar變量,同時不捕獲其他變量。
6) [this]捕獲當前類中的this指針,讓lambda表達式擁有和當前類成員函數同樣的訪問權限。如果已經使用了&或者=,就默認添加此選項。捕獲this的目的是可以在lamda中使用當前類的成員函數和成員變量。

stable_sort(words.begin(),words.end(),[](const string &a,const string &b){return a.size()<b.size();});

override、final、default、delete

override 確保該函數爲虛函數並重寫來自基類的虛函數。

final 阻止了子類重寫這個函數。

default 聲明他是自動生成的。

delete 刪除函數(用於放拷貝)。

struct Base1
{
    virtual void f() override;
};

struct Derived1 : Base1
{
    virtual void f() override; //子類必須重寫
};


struct Base
{
    virtual void f() final;
};

struct Derived : Base
{
    virtual void f(); //錯誤,因爲虛函數Base::f 被標記爲final了.
};

struct Base3
{
    Base3() = default;    //聲明自動生成的構造函數
}

struct Base4
{
    Base4(const Base4&) = delete;  //聲明拷貝構造函數爲deleted函數
    Base4& operator = (const Base4 &) = delete; // 聲明拷貝賦值操作符爲deleted函數
}

Base4 b1;
Base4 b1(b2);            //錯誤 拷貝構造函數被刪除,無法調用
Base4 b3 = b1;           //錯誤 賦值操作符被刪除,無法調用

關鍵字nullptr

nullptr比NULL更安全。當需要使用NULL時,應使用nullptr代替。

 強類型枚舉

使用 enum class(也可以用同義詞enum struct)來聲明:

enum class Enumeration {
    Val1,
    Val2,
    Val3 = 100,
    Val4 // = 101
};

這種枚舉是類型安全的;枚舉值不能隱式地轉換成整數,所以也不可以和整數做比較.表達式 Enumeration::Val4 == 101會報一個編譯錯誤.

右尖括號

std::vector<std::vector<int>> wow;

這在傳統C++編譯器下是不能夠被編譯的,而 C++11 開始,連續的右尖括號將變得合法,並且能夠順利通過編譯。

模板的別名 

在傳統 C++中,typedef 可以爲類型定義一個新的名稱,但是卻沒有辦法爲模板定義一個新的名稱。因爲,模板不是類型。C++11 使用 using 引入了下面這種形式的寫法,並且同時支持對傳統 typedef 相同的功能:

template <typename T>
using NewType = SuckType<int, T, 1>;    // 合法

typedef void (*FunctionType)(double);       // 老式語法
using FunctionType = void (*)(double);      // 新式語法

默認模板參數 

template<typename T = int,typename U = int>
auto(T x,U y)->decltype(x+y)
{
    return x+y;
}

 

無限制的union

在傳統 C++中,例如,union不能包含定義了非平凡構造函數的對象.C++11廢除了其中的一些限制: 現在,聯合可以包含定義了非平凡構造函數的對象;如果包含了,那麼聯合就必須要顯式定義一個構造函數.

struct Point 
{
    Point() {}
    Point(int x, int y): x_(x), y_(y) {}
    int x_, y_;
};

union U 
{
    int z;
    double w;
    Point p; // 非法的C++98; 合法的C++11.
    U() {new(&p) Point();} // 由於Point的原因, 必須定義構造函數.
};

long long int類型

C++11增加了一個新的整數類型long long int

允許sizeof運算符作用在類型的數據成員上,無須明確的對象

struct SomeType { OtherType member; };

sizeof(SomeType::member); // C++03 不行. C++11 可以.
    //這會返回OtherType的大小.C++03不允許這樣做,會報一個編譯錯誤.C++11允許這樣做.

 正則表達式

新的標準庫定義了一個新的頭文件,由一些新的類組成:

正則表達式由模板類std::regex的實例來表示;

模式匹配由的結果模板類std::match_results的實例來表示;

智能指針

智能指針

右值引用 

右值引用 

元組 

增加元組std::tuple

 委託構造

 構造函數可以在同一個類中一個構造函數調用另一個構造函數。

class Base
{
public:
    Base(){value1 = 1;}
    Base(int value):Base()    //委託Base()構造函數
    {
        value2 = 2;
    }

    int value1;
    int value2;
}

  繼承構造

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
};
struct B:A
{
  using A::A;  //關於基類各構造函數的繼承一句話搞定
};

通過using A::A的聲明。將基類中的構造函數全繼承到派生類中,更巧妙的是,這是隱式聲明繼承的。即假設一個繼承構造函數不被相關的代碼使用,編譯器不會爲之產生真正的函數代碼,這樣比透傳基類各種構造函數更加節省目標代碼空間。

但此時另一個問題:當使用using語句繼承基類構造函數時。派生類無法對類自身定義的新的類成員進行初始化,我們可使用類成員的初始化表達式,爲派生類成員設定一個默認初始值。

struct A
{
  A(int i) {}
  A(double d,int i){}
  A(float f,int i,const char* c){}
};

struct B:A
{
  using A::A;
  int d{0};
};

注意:

1.基類的構造函數被聲明爲私有構造函數或者派生類是從基類虛繼承的,那麼就不能在派生類中聲明繼承構造函數。 

2.一旦使用了繼承構造函數,編譯器就不會爲派生類生成默認構造函數。

3.對於繼承構造函數來說,參數的默認值是不會被繼承的。

對POD定義的修正

C++11將POD的概念拆分成了兩個獨立的概念:平凡的(trivial)標準佈局(standard-layout), 放寬了幾條POD規則. 一個平凡的(trivial)類型可以靜態初始化. 這意味着可以通過memcpy來拷貝數據,而不必通過拷貝構造函數. 平凡類型的變量的生命期是從分配存儲空間開始的,而不是從構造完成開始. 平凡的類和結構體定義如下:

①有一個平凡的默認構造函數,默認的意思就是由編譯器爲我們自動生成的,不許是我們自己定義的,這可以使用默認構造函數語法, 例如SomeConstructor() = default;

②有平凡的拷貝和移動構造函數, 可以使用默認語法.

③有平凡的拷貝和移動賦值運算符, 可以使用轉變語法.

④有一個平凡的析構函數, 必須是非虛函數.

C++11中,我們使用模版類std::is_trivial<T>::value來判斷數據類型是否爲平凡類型。

#include "stdafx.h"
#include <iostream>

using namespace std;

class A { A(){} };
class B { B(B&){} };
class C { C(C&&){} };
class D { D operator=(D&){} };
class E { E operator=(E&&){} };
class F { ~F(){} };
class G { virtual void foo() = 0; };
class H : G {};
class I {};

int _tmain(int argc, _TCHAR* argv[])
{
    std::cout << std::is_trivial<A>::value << std::endl;  //有不平凡的構造函數
    std::cout << std::is_trivial<B>::value << std::endl;  //有不平凡的拷貝構造函數
    std::cout << std::is_trivial<C>::value << std::endl;  //有不平凡的拷貝賦值運算符
    std::cout << std::is_trivial<D>::value << std::endl;  //有不平凡的拷貝賦值運算符
    std::cout << std::is_trivial<E>::value << std::endl;  //有不平凡的移動賦值運算符
    std::cout << std::is_trivial<F>::value << std::endl;  //有不平凡的析構函數
    std::cout << std::is_trivial<G>::value << std::endl;  //有虛函數
    std::cout << std::is_trivial<H>::value << std::endl;  //有虛基類
    std::cout << std::is_trivial<I>::value << std::endl;  //平凡的類return 0;
}

一個類型是標準佈局的,就意味着它將以與C兼容的方式來排列和打包它的成員.標準佈局的類和結構體定義如下:

①所有的非靜態數據成員都有相同的訪問控制 (public, private, protected)

②在類和結構體繼承時需要滿足以下兩個情況之一:

派生類中有非靜態類,那麼這個派生類只能有且只有一個僅包含了靜態成員的基類。

基類有非靜態成員,那麼派生類中不允許有非靜態成員。

③類中第一個非靜態類型與基類不是同一個類型

④沒有虛類和虛基類(與trival中重複)

⑤所有非靜態數據成員都符合標準佈局的要求

C++11中,我們使用模版類std::is_standard_layout<A>::value來判斷類型是否是一個標準佈局類型。

#include <iostream>

using namespace std;

class A {
private:
    int a;
public:
    int b;
};

class B1 {
    static int x1;
};

class B2 {
    int x2;
};

class B : B1, B2 {
    int x;
};

class C1 {};
class C : C1 {
    C1 c;
};

class D { virtual void foo() = 0; };
class E : D {};
class F { A x; };

int main()
{
    std::cout << std::is_standard_layout<A>::value << std::endl;  //違反定義1,成員a和b具有不同的訪問權限
    std::cout << std::is_standard_layout<B>::value << std::endl;  //違反定義2,繼承樹有兩個(含)以上的類有非靜態成員
    std::cout << std::is_standard_layout<C>::value << std::endl;  //違反定義3,第一個非靜態成員是基類類型
    std::cout << std::is_standard_layout<D>::value << std::endl;  //違反定義4,有虛函數
    std::cout << std::is_standard_layout<E>::value << std::endl;  //違反定義4,有虛基類
    std::cout << std::is_standard_layout<F>::value << std::endl;  //違反定義5,非靜態成員x不符合標準佈局類型
    return 0;
}

如果一個類型是平凡的(trivial),是標準佈局的, 並且所有的非靜態數據成員和基類都是POD類型的, 那麼這個類型就是POD類型.

 

大家覺得OK,請點個贊👍👍👍,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章