免费高清特黄a大片,九一h片在线免费看,a免费国产一级特黄aa大,国产精品国产主播在线观看,成人精品一区久久久久,一级特黄aa大片,俄罗斯无遮挡一级毛片

分享

C++:C++11新特性詳解(1)

 xiejblan 2019-02-25

前言:

雖然目前沒有編譯器能夠完全實(shí)現(xiàn)C++11,但這并不意味著我們不需要了解,學(xué)習(xí)它。深入學(xué)習(xí)C++11,你會發(fā)現(xiàn)這根本就是一門新的語言,它解決了c++98中許多遺留下來的問題。早晚會有一天,C++11便會普及大部分編譯器。因此,提早做些準(zhǔn)備也是應(yīng)該的。

在此我想做一個(gè)關(guān)于C++11的專題,將C++11的新特性進(jìn)行一一講解,以通俗易懂的語言及例子幫助讀者入門C++11。本文便是C++11新特性詳解系列文章的第一篇, 即C++:C++11新特性詳解(1)。

不過我要強(qiáng)調(diào)的是,這些文章主要是介紹C++11的新特性,有些在C++11不能編譯通過的語法在C++14甚至C++17中支持。所以,這種問題應(yīng)當(dāng)靈活處理。

不過還有一點(diǎn)要強(qiáng)調(diào),這些文章是我學(xué)習(xí)相關(guān)書籍以及博文而做的總結(jié),而且對于書中和博文中許多沒有解釋清楚的細(xì)節(jié)性問題我大都做了補(bǔ)充。因此這寫文章也算上是我個(gè)人的筆記,相信是一份不錯(cuò)的教程

C++11的簡要介紹
(1)出于保證穩(wěn)定性與兼容性增加了不少新特性,如long long整數(shù)類型、靜態(tài)斷言、外部模板等等 ;
(2)具有廣泛應(yīng)用性、能與其他已有的或者新增的特性結(jié)合起來使用的、具有普適性的一些新特性,如繼承構(gòu)造函數(shù),委派構(gòu)造函數(shù),列表初始化等等;
(3)對原有一些語言特性的改進(jìn),如auto類型推導(dǎo)、追蹤返回類型、基于范圍的for循環(huán),等等;
(4)在安全方面所做的改進(jìn),如枚舉類型安全和指針安全等方面的內(nèi)容;
(5)為了進(jìn)一步提升和挖掘C++程序性能和讓C++能更好地適應(yīng)各種新硬件,如多核,多線程,并行編程等等;
(6)顛覆C++一貫設(shè)計(jì)思想的新特性,如lambda表達(dá)式等;
(7)C++11為了解決C++編程中各種典型實(shí)際問題而做出的有效改進(jìn),如對Unicode的深入支持等。

下面是C++11的主要新特性:
C++11
C++11


// 2016.06.09 補(bǔ)充了nullptr部分

看完了前言,相信你對c++11的背景有了初步的了解。本篇文章主要詳解的C++特性如下:
(1)auto的類型推導(dǎo);
(2)decltype的類型推導(dǎo);
(3)auto 與 decltype 結(jié)合的追蹤返回類型;
(4)基于范圍的for循環(huán);
(5)nullptr;
(6)折疊規(guī)則;
(7)經(jīng)典面試題:int (*(*pf())()) () { return nullptr; }

1.auto類型推導(dǎo)

在早期版本中,關(guān)鍵字auto主要是用于聲明具有自動存儲期的局部變量。然而,它并沒有被經(jīng)常使用。原因是:除了static類型以外,其他變量(以“數(shù)據(jù)類型+變量名”的方式定義)都默認(rèn)為具有自動存儲期,所以auto關(guān)鍵字可有可無。

所以,在C++11的版本中,刪除了auto原本的功能,并進(jìn)行了重新定義了。即C++11中的auto具有類型推導(dǎo)的功能。在講解auto之前,我們先來了解什么是靜態(tài)類型,什么是動態(tài)類型。

(1)靜態(tài)類型,動態(tài)類型,類型推導(dǎo)

通俗的來講,所謂的靜態(tài)類型就是在使用變量前需要先定義變量的數(shù)據(jù)類型,而動態(tài)類型無需定義。

嚴(yán)格的來講,靜態(tài)類型是在編譯時(shí)進(jìn)行類型檢查,而動態(tài)類型是在運(yùn)行時(shí)進(jìn)行類型檢查。

如python:

a = "helloworld"; // 動態(tài)類型

而C++:

std::string a = "helloworld"; // 靜態(tài)類型

如今c++11中重新定義了auto的功能,這便使得靜態(tài)類型也能夠?qū)崿F(xiàn)類似于動態(tài)類型的類型推導(dǎo)功能,十分有趣~

下面是auto的基本用法:

double func();
auto a  = 1; // int, 盡管1時(shí)const int類型,但是auto會自動去const
auto b = func(); // double
auto c; // wrong, auto需要初始化的值進(jìn)行類型推導(dǎo),有點(diǎn)類似于引用

注意: 其實(shí)auto就相當(dāng)于一個(gè)類型聲明時(shí)的占位符,而不是一種“類型”的聲明,在編譯時(shí)期編譯器會將auto替代成變量的實(shí)際類型。

(2)auto的優(yōu)勢

I. 擁有初始化表達(dá)式的復(fù)雜類型變量聲明時(shí)的簡化代碼。

也就是說,auto能夠節(jié)省代碼量,使代碼更加簡潔, 增強(qiáng)可讀性。

如:

std::vector<std::string> array;
std::vector<std::string>::iterator it = array.begin();
// auto
auto it = array.begin();

auto在STL中應(yīng)用非常廣泛,如果在代碼中需要多次使用迭代器,用auto便大大減少了代碼量,使得代碼更加簡潔,增強(qiáng)了可讀性

II.免除程序員在一些類型聲明時(shí)的麻煩,或者避免一些在類型聲明時(shí)的錯(cuò)誤。

如:

class PI {
public:
    double operator *(float v) {
        return (double)val*v;
    }
    const float val = 3.1415927f;
};
int main(void) {
    float radius = 5.23f;
    PI pi;
    auto circumference = 2*( pi*radius);
    return 0;
}

設(shè)計(jì)PI類的作者將PI的*運(yùn)算符進(jìn)行了重載,使兩個(gè)float類型的數(shù)相乘返回double類型。這樣做的原因便是避免數(shù)據(jù)上溢以及精度降低。假如用戶將circumference定義為float類,就白白浪費(fèi)了PI類作者的一番好意,用auto便不會出現(xiàn)這樣的問題。

但是auto并不能解決所有的精度問題,如:

unsigned int a = 4294967295; // unsigned int 能夠存儲的最大數(shù)據(jù)
unsigned int b = 1;
auto c = a+b;
cout << c << endl; // 輸出c為0

a+b顯然超出了unsigned int 能夠存儲的數(shù)據(jù)范圍,但是auto不能自動將其匹配為能存儲這一數(shù)據(jù)而不造成溢出的類型如unsigned long類型。所以在精度問題上自己還是要多留一點(diǎn)心,分析數(shù)據(jù)是否會溢出

III.“自適應(yīng)”性能在一定程度上支持泛型編程。

如上面提到PI類,假如原作者要修改重載*返回的數(shù)據(jù)類型,即將double換成其他類型如long double,則它可以直接修改而無需修改main函數(shù)中的值。

再如這種“適應(yīng)性”還能體現(xiàn)在模板的定義中:

template <typename T1, typename T2>
double func(const T1& a, const T2& b) {
    auto c = a + b;
    return c;
}
// 其實(shí)直接return a+b;也是可以的,這里只是舉個(gè)例子,同時(shí)點(diǎn)出auto不能用于聲明函數(shù)形參這一易錯(cuò)點(diǎn)

但是有一點(diǎn)要注意:不能將auto用于聲明函數(shù)形參,所以不能用auto替代T1,T2。

然而,因?yàn)閒unc()只能返回double值,所以func()還可以進(jìn)一步泛化,那就需要decltype的使用了,在后面會詳細(xì)講解。

現(xiàn)此處有一段有趣的宏定義:

# define Max1(a, b) ((a) > (b)) ? (a) : (b)
# define Max2(a, b) ({             auto _a = (a);
            auto _b = (b);
            (_a > _b) ? _a : _b;})

用Max2的宏定義效率更高。

(3)auto 使用細(xì)則

int x;
int* y = &x;
double foo();
int& bar();
auto* a = &x; // a:int*
auto& b = x; // b:int&
auto c = y; // c:int*
auto* d = y; // d:int*
auto* e = &foo(); // wrong, 指針不能指向臨時(shí)變量
auto &f = foo(); // wrong, 左值引用不能存儲右值
auto g = bar(); // int
auto &h = bar(); // int&

其實(shí),對于指針而言, auto* a = &x <=> auto a = &x
但是對于引用而言,上面的情況就不遵循了,如果是引用, 要在auto后加&。

double foo();
float* bar();
const auto a = foo(); // a:const double
const auto &b = foo(); // b:const double&
volatile auto* c = bar(); // c:volatile float*

auto d = a; // d:double
auto &e = a; // e:const double
auto f = c; // f:float*
volatile auto& g = c; // g:volatile float*&

auto 會自動刪除const(常量性),volatile(易失性)

對于引用和指針,即auto*, auto&仍會保持const與volatile。

auto x = 1, y = 2; // (1) correct
const auto* m = &x, n = 1; // (2)correct
auto i = 1, j = 3.14f; // (3) wrong
auto o = 1, &p = 0, *q = &p; // (4)correct

auto有規(guī)定,當(dāng)定義多個(gè)變量時(shí),所有變量的類型都要一致,且為第一個(gè)變量的類型,否則編譯出錯(cuò)。

對于(1): x, y都是int類型,符合auto定義多變量的機(jī)制, 編譯通過;
對于(2):我們發(fā)現(xiàn),m、n的類型不同,那為什么不報(bào)錯(cuò)?變量類型一致是指auto一致。m為const int*, 則auto匹配的是int,而n恰好為int類型,所以編譯通過;
對于(3): i的類型是int, j的類型是float,類型不相同,編譯出錯(cuò);
對于(4): o的類型是int, p前有&,其實(shí)就是auto&, 即p為int&,而q前有,相當(dāng)于auto,即q為int*,不難發(fā)現(xiàn)o, p, q三者auto匹配都為int,所以符合auto定義多變量的機(jī)制,編譯通過。

(4)局限性

void func(auto x = 1) {} // (1)wrong
struct Node {
    auto value = 10; // (2)wrong
};
int main(void) {
    char x[3];
    auto y = x;
    auto z[3] = x; // (3)wrong
    vector<auto> v = {1}; // (4)wrong
}

I.auto不能作為函數(shù)參數(shù),否則無法通過編譯;
II.auto不能推導(dǎo)非靜態(tài)成員變量的類型,因?yàn)閍uto是在編譯時(shí)期進(jìn)行推導(dǎo);
III.auto 不能用于聲明數(shù)組,否則無法通過編譯;
IV.auto不能作為模板參數(shù)(實(shí)例化時(shí)), 否則無法通過編譯。

2.decltype 類型推導(dǎo)

類型推導(dǎo)是隨著模板和泛型編程的廣泛使用而引入的。在非泛型編程中,類型是明確的,而在模板與泛型編程中,類型是不明確的,它取決于傳入的參數(shù)類型。

decltype與我前面講到的auto還是有一些共同點(diǎn)的,如二者都是通過推導(dǎo)獲得的類型來定義另外一個(gè)變量,再如二者都是在編譯時(shí)進(jìn)行類型推導(dǎo)。不過他們類型推導(dǎo)的方式有所不同,auto是通過初始化表達(dá)式推導(dǎo)出類型,而decltype是通過普通表達(dá)式的返回值推導(dǎo)出類型。

不過在講解decltype之前,我們先來了解一下typeid。

(1)typeid 與 decltype

對于C語言,是完全不支持動態(tài)類型的;
對于C++,與C不同的是,C++98標(biāo)準(zhǔn)中已經(jīng)有部分支持動態(tài)類型了,便是運(yùn)行時(shí)類型識別(RTTI)。

RTTI機(jī)制:為每個(gè)類型產(chǎn)生一個(gè)type_info類型數(shù)據(jù),程序員可以在程序中使用typeid隨時(shí)查詢一個(gè)變量的類型,typeid就會返回變量相應(yīng)的type_info數(shù)據(jù),type_info的name成員可以返回類型的名字。在C++11中,增加了hash_code這個(gè)成員函數(shù),返回該類型唯一的哈希值以供程序員對變量類型隨時(shí)進(jìn)行比較。

也許你會有這樣一個(gè)想法:我直接對type_info.name進(jìn)行字符串比較不就可以了么,為什么還要給每個(gè)類型一個(gè)哈希值?我認(rèn)為,字符串比較的開銷也是比較大的,如果用每個(gè)類型來對于一個(gè)哈希值,通過比較哈希值確定類型是否相同的方法,會比使用字符串比較的效率要高得多。

下面一段代碼是對typeid()type_info.name(), type_info.hash_code的應(yīng)用:

# include <iostream>
# include <typeinfo>
using namespace std;
class white {};
class Black {};
int main(void) {
    white a;
    Black b;
    // white 與 black 前的數(shù)字會因編譯器的不同而不同,如g++打印的是5
    cout << typeid(a).name() << endl; // 5 white
    cout << typeid(b).name() << endl; // 5 Black
    white c;
    bool a_b_sametype = (typeid(a).hash_code() == typeid(b).hash_code());
    bool a_c_sametype = (typeid(a).hash_code() == typeid(c).hash_code());
    cout << "Same type?" << endl;
    cout << "A and B" << (int)a_b_sametype << endl; // 0
    cout << "A and C" << (int)a_c_sametype << endl; // 1
    return 0;
}

然而,RTTI無法滿足程序員的需求:因?yàn)?strong>RTTI在運(yùn)行時(shí)才確定出類型,而更多的需求是在編譯時(shí)確定類型。并且,通常的程序是要使用推導(dǎo)出來的這種類型而不是單純地識別它。

(2)decltypr的應(yīng)用

I.decltype 與 using / typedef 連用
在頭文件,常??吹饺缦露x:

using size_t = decltype(sizeof(0));
using ptrdiff_t = decltype((int *)0-(int*)0);
using nullptr_t = decltype(nullptr);

II.增加代碼的可讀性

std::vector<std::string> vec;
typedef decltype(vec.begin()) iterator; // (1)
decltype(vec)::iterator it; // (2)

III.重用匿名類型

enum class {K1, K2, K3} anon_e;  // 匿名的強(qiáng)類型枚舉
union {
    decltype (anon_e) key;
    char* name;
} anon_u; // 匿名的union聯(lián)合體
struct {
    int d;
    decltype(anon_u) id; 
} anon_s[100]; // 匿名的struct數(shù)組
int main(void) {
    decltype(anon_s) as;
    as[0].id.key = decltype(anon_e)::K1; // 引用匿名強(qiáng)類型枚舉中的值
    return 0;
}

注:對于強(qiáng)類型枚舉,也是C++11中的新特性,這個(gè)放到以后的文章里講。

一般來說,使用匿名便是表明只想使用一次,不想被重用,此處只是表明decltype能實(shí)現(xiàn)這種功能。

IV.decltype 可以適當(dāng)擴(kuò)大模板泛型編程的能力。

template <typename T1, typename T2>
void Sum(T1& t1, T2 & t2, decltype(t1+t2)& s) {
    s = t1+t2
}
int main(void) {
    int a = 3;
    long b = 5;
    float c = 1.0f, f = 2.3f;
    long e;
    float f;
    Sum(a, b, e);
    Sum(c, d, f);
    return 0;
}

通過將返回類型改為void,并將原來的返回值s定義于形參之中,利用decltype(t1+t2)& 對其聲明,使t1 + t2的返回值不會受到限制。如此顯然在一定程度上擴(kuò)大了模板泛型編程的能力。

但是此處還是有個(gè)缺陷,程序員仍然需要提前設(shè)定返回值的類型,如變量e與變量f,還是不能實(shí)現(xiàn)真正意義上的模板泛型編程。為使其更加泛化,通常采用追蹤返回類型來實(shí)現(xiàn),這個(gè)在后邊會講到。

V. 實(shí)例化模板

int hash(char* );
map<char*, decltype(hash)> dict_key; // wrong, decltype需要利用函數(shù)返回值進(jìn)行推導(dǎo)
map<char*, decltype(hash(nullptr))> dict_key1;

(3)decltype 推導(dǎo)的四規(guī)則

在了解這四個(gè)規(guī)則之前,我們先來了解標(biāo)記符表達(dá)式(id-expression)的概念。

標(biāo)記符表達(dá)式(id-expression):所有除去關(guān)鍵字和字面量等編譯器需要使用的標(biāo)記以外的程序員自定義的標(biāo)記(token)都可以是標(biāo)記符(identifier), 而單個(gè)標(biāo)記符對應(yīng)的表達(dá)式就是標(biāo)記符表達(dá)式。

int arr[4], int i, arr與i就是標(biāo)記符表達(dá)式。對于前者,去除關(guān)鍵字int與字面量[4]后剩下的arr便是標(biāo)記符表達(dá)式。

還有一點(diǎn),C++11中對值的類型分類與C++98有所不同。在C++98中,值可分左值與右值。通俗地來講, 所謂的左值便是含有變量名的數(shù)值,所謂的右值就是沒有變量名的數(shù)值,即為臨時(shí)變量, 以及包含右值引用。而在C++11中,就將右值更進(jìn)一層地分類:分為純右值與將亡值,純右值即為沒有變量名的數(shù)值,將亡值即為右值引用,且左值與將亡值合稱為泛左值。

decltype推導(dǎo)的四規(guī)則如下:
(1)如果e是一個(gè)沒有帶括號的標(biāo)記符表達(dá)式或者類成員訪問表達(dá)式,那么decltype(e)就是e所命名的實(shí)體的類型。此外,如果e是一個(gè)被重載的函數(shù),可能會導(dǎo)致編譯錯(cuò)誤;
(2)否則,假設(shè)e的類型是T,如果e是一個(gè)將亡值(xvalue), 那么decltype(e)為T&&;
(3)否則,假設(shè)e的類型是T,如果e是一個(gè)左值,則decltype(e)為T&;
(4)否則,假設(shè)e的類型是個(gè)T, 則decltype(e)為T。

下面通過代碼分別對四規(guī)則進(jìn)行舉例:

int arr[5] = {0};
int *ptr = arr;
struct S {
    double d;
} s;
void overload(int);
void overload(char);
int&& RvalRef();
const bool Func(int);

// 規(guī)則1
decltype(arr) var1; // int [5]
decltype(ptr) var2; // int*
decltype(s.d) var4; // double
decltype(Overloaded) var5; // wrong

// 規(guī)則2
decltype(RvalRef()) val6 = 1; // int&&

// 規(guī)則3
decltype(true? i : i) var7 = i; // int&
decltype((i)) var8 = i; // int&
decltype(++i) var9 = i; // int&
decltype(arr[3]) var10 = i; // int&
decltype(*ptr) var11 = i; //int&
decltype("lval") var2 = "lval"; // (const char*)&

// 規(guī)則4
decltype(1) var13; // int
decltype(i++) var14; // int
decltype(Func(1)) var15; // const bool

上面的代碼中,需要重點(diǎn)注意的是:
(1)++i 與 i++: ++i返回的是左值引用,i++返回的是右值。
(2)字符串的字面常量為左值,其它字符串字面量為右值。
(3)對于 decltype((i)) val8 = idecltype((i)) val8 = 1;前者能通過編譯,后者不可以,提示的錯(cuò)誤信息如下:

aaa.cpp: In function ‘int main()’:
aaa.cpp:6:26: error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
     decltype((i)) val8 = 1;

原因是:i是標(biāo)記符表達(dá)式,而(i)不是標(biāo)記符表達(dá)式,所以它遵循的應(yīng)當(dāng)是規(guī)則3,decltype推導(dǎo)的類型為int&,即左值引用。i是左值,將i賦給val8是可以的,但是講1賦給val8卻是不可以的。1是右值編譯器不允許將一個(gè)右值賦給一個(gè)左值引用,所以編譯不通過,這點(diǎn)要注意!

補(bǔ)充一下,在C++11的標(biāo)準(zhǔn)庫中提供了is_lvalue_reference<>與is_rvalue_reference<>, 用于判斷方括號內(nèi)的內(nèi)容的類型是否為左值引用或右值引用。如果是,則返回1,如若不是,則返回0。所以可以利用他們來檢驗(yàn)decltype推導(dǎo)出的類型是否為左值引用和右值引用。

(4)cv限制符的繼承與冗余的符號

所謂的cv限制符是指:const和volatile。

與auto不同,decltype能夠“帶走”表達(dá)式的cv限制符。不過,如果對象的定義中有cv限制符時(shí),其成員不會繼承const或volatile限制符。

舉例說明:

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

const int ic = 0;
volatile int iv;
struct S {
    int i;
};
const S a = {0};
volatile S b;
volatile S* p = &b;

int main(void) {
    cout << is_const<decltype(ic)>::value << endl; // 1
    cout << is_volatile<decltype(iv)>::value << endl; // 1
    cout << is_const<decltype(a)>::value << endl; // 1
    cout << is_volatile<decltype(b)>::value << endl; // 1
    cout << is_const<decltype(a.i)>::value << endl; // 0
    cout << is_volatile<decltype(p->i)>::value << endl; // 0
    return 0;
}

還有,使用decltype從表達(dá)式推導(dǎo)出類型后進(jìn)行類型定義時(shí),可能會出現(xiàn)一些冗余的符號:cv限制符,符號引用&。如果推導(dǎo)出的類型已經(jīng)有了這些屬性,冗余的符號將會被忽略,這種規(guī)則叫做折疊規(guī)則。

下面用表格來概括一下折疊規(guī)則

typedef T& TR;  // T&的位置是對于TR的類型定義
TR v;  // TR的位置是聲明變量v的類型

TR的類型定義 聲明變量v的類型 v的實(shí)際類型
T& TR T&
T& TR& T&
T& TR&& T&
T&& TR T&&
T&& TR& T&
T&& TR&& T&&

規(guī)律:
當(dāng)TR為T&時(shí),無論定義類型v時(shí)有多少個(gè)&,最終v的類型都是T&;
當(dāng)TR為T&&時(shí),則v最終的類型與定義類型v時(shí)&的數(shù)量有關(guān):
(1)如果&的總數(shù)量為奇數(shù),則v的最終類型為T&;
(2)如果&的總數(shù)量為偶數(shù),則v的最終類型為T&&。

上面主要是對引用符號&的冗余處理,那么對于指針符號* decltype該如何處理呢?

# include <iostream>
# include <type_traits>
using namespace std;
int i = 1;
int &j = i;
int *p = &i;
const int k = 1;
int main(void) {
    decltype(i)& val1 = i;
    decltype(i)& val2 = i;
    cout << is_lvalue_reference<decltype(val1)>::value << endl; // 1
    cout << is_rvalue_reference<decltype(val2)>::value << endl; // 0
    cout << is_lvalue_reference<decltype(val2)>::value << endl; // 1
    decltype(p)* val3 = &i; // 編譯失敗,val3為int**類型,等號右側(cè)為int*類型
    decltype(p)* val4 = &p; // int**
    auto* val5 = p; // int*
    v3 = &i;
    const decltype(k) var4 = 1; // 冗余的const
    return 0;
}

由上面的代碼可知,auto對于*的冗余編譯器采取忽略的措施,而decltype對于*的冗余編譯器采取不忽略的措施。

(5)追蹤返回類型

在C++98中,如果一個(gè)函數(shù)模板的返回類型依賴于實(shí)際的入口參數(shù)類型,那么該返回類型在模板實(shí)例化之前可能都無法確定,這樣的話我們在定義該函數(shù)模板時(shí)就會遇到麻煩。

我們很快就會想到可以用decltype來定義:

// 注:本段代碼不能通過編譯,只是幫助讀者了解追蹤返回類型的由來
template<typename T1, typename T2>
decltype(t1+t2) Sum(T1& t1, T2& t2) {
    return t1+t2;
}

不過這樣有個(gè)問題。因?yàn)榫幾g器是從左到右讀取的,此時(shí)decltype內(nèi)的t1, t2都未聲明,而按照 C/C++ 編譯器的規(guī)則,變量在使用前必須聲明。

因此, 為了解決這一問題,C++11引進(jìn)了新語法——追蹤返回類型,來聲明和定義這樣的函數(shù):

template<typename T1, typename T2>
auto Sum(T1& t1, T2& t2)->decltype(t1+t2) {
    return t1+t2;
}

auto**占位符**與->return_type是構(gòu)成追蹤返回類型函數(shù)的兩個(gè)基本元素。

I.引進(jìn)返回類型函數(shù)后返回類型無需寫明作用域:
如:

class ABCDEFGH {
    struct B {};
};
// 原來版本
ABCDEFGH::B ABCDEFGH::func() {}
// c++11
auto ABCDEFGH::func()->B {}

這樣再一定程度上能簡化代碼,增強(qiáng)可讀性;

II.使模板更加泛化(如Sum函數(shù))
III.簡化函數(shù)的定義,提高代碼的可讀性。

在這里舉一個(gè)很經(jīng)典的例子:

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

int (*(*pf())()) () { return nullptr; }

// auto(*)()->int(*) ()  返回int(*)()類型的函數(shù)p
// auto pf1()->auto(*)()->int(*)() // 返回函數(shù)p的函數(shù)
auto pf1()->auto(*)()->int(*)() { return nullptr; }

int main(void) {
    cout << is_same<decltype(pf), decltype(pf1)>::value << endl; // 1
    return 0;
}

首先說明一下main函數(shù):我認(rèn)為應(yīng)該是is_same<decltype(pf()), decltype(pf1())>::value, 即p1、p2后面要加上括號,但是不知道為什么兩種方式都能通過編譯,此處求指點(diǎn)~

我先來分析一下那個(gè)很復(fù)雜很復(fù)雜的函數(shù):int (*(*pf())()) () { return nullptr; }
先介紹一下函數(shù)指針返回函數(shù)指針的函數(shù)的語法:

// function ptr
return_type(*func_pointer)(parameter_list)
// A function return func_pointer
return_type(*function(func_parameter_list))(parameter_list) {}

函數(shù)指針的變量名為func_pointer,指向的函數(shù)返回類型為return_type參數(shù)列表為parameter_list;
返回函數(shù)指針的函數(shù)名稱為function,參數(shù)列表為func_parameter_list,返回類型為return_type(*)(parameter_list)。

對于int (*(*pf())()) () { return nullptr; }
(1)該函數(shù)的返回類型為int(*)(), 是一個(gè)指向返回類型為int,參數(shù)列表為空的函數(shù)的指針;
(2)該函數(shù)的參數(shù)列表為空;
(3)該函數(shù)的名稱為*pf();
(4)說明pf()返回的也是一個(gè)函數(shù)指針,且這個(gè)函數(shù)指針指向該函數(shù)。

這種函數(shù)的定義方式使得代碼的可讀性大大降低,C++11中的追蹤返回類型能大大改善這種情況:
auto pf1()->auto(*)()->int(*)() { return nullptr; }
即pf1這個(gè)函數(shù)先返回auto(*)()->int(*)()的函數(shù)指針, 而這個(gè)函數(shù)指針auto(*)()指向的函數(shù)的返回類型為int(*)()的函數(shù)指針。如此一來,果真大大提高了代碼的可讀性。

V.廣泛應(yīng)用于轉(zhuǎn)發(fā)函數(shù)

先了解一下轉(zhuǎn)發(fā)函數(shù)的概念。

何為完美轉(zhuǎn)發(fā)?是指在函數(shù)模板中,完全依照模板的參數(shù)類型,將參數(shù)傳遞給函數(shù)模板中調(diào)用另外一個(gè)函數(shù)

完美轉(zhuǎn)發(fā)那么肯定也有不完美轉(zhuǎn)發(fā)。如果在參數(shù)傳遞的過程中產(chǎn)生了額外的臨時(shí)對象拷貝,那么其轉(zhuǎn)發(fā)也就算不上完美轉(zhuǎn)發(fā)。為了避免起不完美,我們要借助于引用以防止其進(jìn)行臨時(shí)對象的拷貝。

舉例:

# include <iostream>
using namespace std;

double foo(int a) {
    return (double)a + 0.1;
}
int foo(double b) {
    return (int)b;
}
template<class T>
auto Forward(T t)->decltype(foo(t)) {
    return foo(t);
}

int main(void) {
    cout << Forward(2) << endl; // 2.1
    cout << Forward(0.5) << endl; // 0
    return 0;
}

VI.也可以應(yīng)用于函數(shù)指針及函數(shù)引用

// 函數(shù)指針
int (*fp)();
<=>
auto (*fp)()->int;
// 函數(shù)引用
int (&fr)();
<=>
auto (&fr)()->int;

不僅如此,追蹤返回類型還能應(yīng)用于結(jié)構(gòu)或類的成員函數(shù),類模板的成員函數(shù),此處就不再舉例了。

特殊:沒有返回值的函數(shù)也可以被聲明為追蹤返回類型,只需將返回類型聲明為void即可。

3.基于范圍的for循環(huán)

基于范圍的for循環(huán),結(jié)合auto的關(guān)鍵字,程序員只需要知道“我在迭代地訪問每一個(gè)元素”即可,而再也不必關(guān)心范圍,如何迭代訪問等細(xì)節(jié)。

// 通過指針p來遍歷數(shù)組
# include <iostream>
using namespace std;
int main(void) {
    int arr[5] = {1, 2, 3, 4 , 5};
    int *p;
    for (p = arr; p < arr+sizeof(arr)/sizeof(arr[0]); ++p) {
            *p *= 2;
    }
    for (p = arr; p < arr+sizeof(arr)/sizeof(arr[0]); ++p) {
            cout << *p << "\t";
    }
    return 0;
}

而如今在C++模板庫中,有形如for_each的模板函數(shù),其內(nèi)含指針的自增。

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

int action1(int &e) { e*=2; }
int action2(int &e) { cout << e << "\t"; }
int main(void) {
    int arr[5] = {1, 2, 3, 4, 5};
    for_each(arr, arr+sizeof(arr)/sizeof(a[0]), action1);
    for_each(arr, arr+sizeof(arr)/sizeof(a[0]), action2);
    return 0;
}

以上兩種循環(huán)都需要告訴循環(huán)體其界限范圍,即arr到arr+sizeof(arr)/sizeof(a[0]),才能按元素執(zhí)行操作。

c++11的基于范圍的for循環(huán),則無需告訴循環(huán)體其界限范圍。

# include <iostream>
using namespace std;
int main(void) {
    int a[5] = {1, 2, 3, 4, 5};
    for (int& e: arr) e *= 2;
    for (int& e: arr) cout << e << "\t";
    // or(1)
    for (int e: arr) cout << e << "\t";
    // or(2)
    for (auto e:arr) cout << e << "\t";
    return 0;
}

基于范圍的for循環(huán)后的括號由冒號“:”分為兩部分,第一部分是范圍內(nèi)用于迭代的變量,第二部分則表示被迭代的范圍。

注意:auto不會自動推導(dǎo)出引用類型,如需引用要加上&
auto& :修改
auto:不修改, 拷貝對象
基于范圍的循環(huán)在標(biāo)準(zhǔn)庫容器中時(shí),如果使用auto來聲明迭代的對象的話,那么該對象不會是迭代器對象,而是解引用后的對象。

continuebreak的作用與原來的for循環(huán)是一致的。

使用條件:
(1)for循環(huán)迭代的范圍是可確定的:對于類,需要有begin()與end()函數(shù);對于數(shù)組,需要確定第一個(gè)元素到最后一個(gè)元素的范圍;
(2)迭代器要重載++;
(3)迭代器要重載*, 即*iterator;
(4)迭代器要重載== / !=。

對于標(biāo)準(zhǔn)庫中的容器,如string, array, vector, deque, list, queue, map, set,等使用基于范圍的for循環(huán)沒有問題,因?yàn)闃?biāo)準(zhǔn)庫總是保持其容器定義了相關(guān)操作。

注意:如果數(shù)組大小不能確定的話,是不能使用基于范圍的for 循環(huán)的。

// 無法通過編譯
# include <iostream>
using namespace std;
int func(int a[]) {
    for (auto e: a) cout << e << "\t";
}
int main(void) {
    int arr[] = {1, 2, 3, 4, 5};
    func(arr);
    return 0;
}

這段代碼無法通過編譯,原因是func()只是單純傳入一個(gè)指針,并不能確定數(shù)組的大小,所以不能使用基于范圍的for循環(huán)。

4.nullptr

在良好的C++編程習(xí)慣中,聲明一個(gè)變量的同時(shí),總是需要記得在合適的代碼位置將其初始化。對于指針類型的變量,這一點(diǎn)尤其應(yīng)當(dāng)注意。未初始化的懸掛指針通常會是一些難于調(diào)試的用戶程序的錯(cuò)誤根源。

而典型的初始化指針通常有兩種:0與NULL, 意在表明指針指向一個(gè)空的位置

int *p = 0;
int *q = NULL;

NULL其實(shí)是宏定義,在傳統(tǒng)C頭文件(stddef.h)中的定義如下:

// stddef.h
# undef NULL
# if define(_cplusplus)
# define NULL 0
# else
# define NULL ((void*)0)
# endif

從上面的定義中我們可以看到,NULL既可被替換成整型0,也可以被替換成指針(void*)0。這樣就可能會引發(fā)一些問題,如二義性

# include <iostream>
using namespace std;
void f(int* ptr) {}
void f(int num) {}
int main(void) {
    f(0);
    f((int*)0);
    f(NULL);   // 編譯不通過
    return 0;
}

NULL既可以被替換成整型,也可以被替換成指針,因此在函數(shù)調(diào)用時(shí)就會出現(xiàn)問題。因此,在早期版本的C++中,為了解決這種問題,只能進(jìn)行顯示類型轉(zhuǎn)換。

所以在C++11中,為了完善這一問題,引入了nullptr的指針空值類型的常量。為什么不重用NULL?原因是重用NULL會使已有很多C++程序不能通過C++11編譯器的編譯。為保證最大的兼容性且避免沖突,引入新的關(guān)鍵字是最好的選擇。

而且,出于兼容性的考慮,C++11中并沒有消除NULL的二義性

那么,nullptr有沒有數(shù)據(jù)類型呢?頭文件對其類型的定義如下:

// <cstddef>
typedef decltype(nullptr) nullptr_t;

即nullptr_t為nullptr的類型, 稱為指針空值類型。指針空值類型的使用有以下幾個(gè)規(guī)則:
1.所有定義為nullptr_t類型的數(shù)據(jù)都是等價(jià)的,行為也是完全一致的。
也就是說,nullptr_t的對象都是等價(jià),都是表示指針的空值,即滿足“==”。
2.nullptr_t類型的數(shù)據(jù)可以隱式轉(zhuǎn)換成任意一個(gè)指針類型。
3.nullptr_t類型數(shù)據(jù)不能轉(zhuǎn)換成非指針類型,即使用reinterpret_cast()的方式也不可以實(shí)現(xiàn)轉(zhuǎn)化;
4.nullptr_t類型的對象不適用于算術(shù)運(yùn)算的表達(dá)式;
5.nullptr_t類型數(shù)據(jù)可以用于關(guān)系運(yùn)算表達(dá)式,但僅能與nullptr_t類型數(shù)據(jù)或者是指針類型數(shù)據(jù)進(jìn)行比較,當(dāng)且僅當(dāng)關(guān)系運(yùn)算符為-=, <=, >=, 等時(shí)返回true。

# include <iostream>
# include <typeinfo>
using namespace std;
int main(void) {
    // nullptr 隱式轉(zhuǎn)換成char*
    char* cp = nullptr;

    // 不可轉(zhuǎn)換成整型,而任何類型也不可能轉(zhuǎn)化成nullptr_t
    int n1 = nullptr;  // 編譯不通過
    int n2 = reinterpret_cast<int>(nullptr);  // 編譯不通過

    // nullptr 與 nullptr_t 類型變量可以作比較
    nullptr_t nptr;
    if (nptr == nullptr)
        cout << "nullptr_t nptr == nullptr" << endl;
    else 
        cout << "nullptr_t nptr != nullptr" << endl;
    if (nptr < nullptr)
        cout << "nullptr_t nptr < nullptr" << endl;
    else
            cout << "nullpte_t nptr !< nullptr" << endl;

    // 不能轉(zhuǎn)化成整型或bool類型,以下代碼不能通過編譯
    if (0 == nullptr);
    if (nullptr);
    // 不可以進(jìn)行算術(shù)運(yùn)算,以下代碼不能通過編譯
    // nullptr += 1
    // nullptr * 5

    // 以下操作均可以正常進(jìn)行
    // sizeof(nullptr) == sizeof(void*)
    sizeof(nullptr);
    typeid(nullptr);
    throw(nullptr);
    return 0;
}

輸出:
nullptr_t nptr == nullptr
nullptr_t nptr !< nullptr
terminate called after throwing an instance of "decltype(nullptr)" Aborted

nullptr_t 看起來像個(gè)指針類型,用起來更像。但是在把nullptr_t應(yīng)用于模板的時(shí)候,我們會發(fā)現(xiàn)模板只能把它作為一個(gè)普通的類型進(jìn)行推導(dǎo),并不會將其視為T*指針。

# include <iostream>
using namespace std;
template<typename T>
void g(T* t) {}
template<typename T>
void h(T t) {}
int main(void) {
    // nullptr 并不會被編譯器“智能”地推導(dǎo)成某種基本類型的指針或者void*指針。
    // 為了讓編譯器推導(dǎo)出來,應(yīng)當(dāng)進(jìn)行顯示類型轉(zhuǎn)換
    g(nullptr); // 編譯失敗,nullptr的類型是nullptr_t,而不是指針
    g((float*)nullptr); // T* 為 float*類型
    h(0);  // T 為 整型
    h(nullptr);  // T 為 nullptr_t類型
    h((float*)nullptr); // T 為 float*類型
    return 0;
}

null與(void*)0的:
1.nullptr是編譯時(shí)期的常量,它的名字是一個(gè)編譯時(shí)期的關(guān)鍵字,能夠?yàn)榫幾g器所識別,而(void*)0只是一個(gè)強(qiáng)制類型轉(zhuǎn)化的表達(dá)式,其返回值也是一個(gè)void*的指針類型。
2.nullptr 能夠隱式轉(zhuǎn)化成指針,而(void*)0只能進(jìn)行顯示轉(zhuǎn)化才能變成指針類型(c++11)。雖然在c++標(biāo)準(zhǔn)中(void*)類型的指針可以實(shí)現(xiàn)隱式轉(zhuǎn)化。

int *p1 = (void*)0;  // 編譯不通過
int *p2 = nullptr;

補(bǔ)充: 除了nullptr, nullptr_t的對象的地址都可以被用戶使用 nullptr是右值,取其地址沒有意義,編譯器也是不允許的。如果一定要取其地址,也不是沒有辦法??梢远x一個(gè)nullptr的右值引用,然后對該引用進(jìn)行取地址。

const nullptr_t&& p = nullptr;

以上內(nèi)容皆為本人觀點(diǎn),歡迎大家提出批評和指導(dǎo),我們一起探討!


    本站是提供個(gè)人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多