程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++:C++11新特性詳解(1)

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

編輯:關於C++

前言:

雖然目前沒有編譯器能夠完全實現C++11,但這並不意味著我們不需要了解,學習它。深入學習C++11,你會發現這根本就是一門新的語言,它解決了c++98中許多遺留下來的問題。早晚會有一天,C++11便會普及大部分編譯器。因此,提早做些准備也是應該的。

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

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

不過還有一點要強調,這些文章是我學習相關書籍以及博文而做的總結,而且對於書中和博文中許多沒有解釋清楚的細節性問題我大都做了補充。因此這寫文章也算上是我個人的筆記,相信是一份不錯的教程

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

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


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

1.auto類型推導

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

所以,在C++11的版本中,刪除了auto原本的功能,並進行了重新定義了。即C++11中的auto具有類型推導的功能。在講解auto之前,我們先來了解什麼是靜態類型,什麼是動態類型

(1)靜態類型,動態類型,類型推導

通俗的來講,所謂的靜態類型就是在使用變量前需要先定義變量的數據類型,而動態類型無需定義。

嚴格的來講,靜態類型是在編譯時進行類型檢查,而動態類型是在運行時進行類型檢查。

如python:

a = "helloworld"; // 動態類型

而C++:

std::string a = "helloworld"; // 靜態類型

如今c++11中重新定義了auto的功能,這便使得靜態類型也能夠實現類似於動態類型的類型推導功能,十分有趣~

下面是auto的基本用法:

double func();
auto a  = 1; // int, 盡管1時const int類型,但是auto會自動去const
auto b = func(); // double
auto c; // wrong, auto需要初始化的值進行類型推導,有點類似於引用

注意: 其實auto就相當於一個類型聲明時的占位符,而不是一種“類型”的聲明,在編譯時期編譯器會將auto替代成變量的實際類型。

(2)auto的優勢

I. 擁有初始化表達式的復雜類型變量聲明時的簡化代碼。

也就是說,auto能夠節省代碼量,使代碼更加簡潔, 增強可讀性。

如:

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

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

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

如:

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;
}

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

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

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

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

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

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

再如這種“適應性”還能體現在模板的定義中:

template 
double func(const T1& a, const T2& b) {
    auto c = a + b;
    return c;
}
// 其實直接return a+b;也是可以的,這裡只是舉個例子,同時點出auto不能用於聲明函數形參這一易錯點

但是有一點要注意:不能將auto用於聲明函數形參,所以不能用auto替代T1,T2。

然而,因為func()只能返回double值,所以func()還可以進一步泛化,那就需要decltype的使用了,在後面會詳細講解。

現此處有一段有趣的宏定義:

# 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 使用細則

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, 指針不能指向臨時變量
auto &f = foo(); // wrong, 左值引用不能存儲右值
auto g = bar(); // int
auto &h = bar(); // int&

其實,對於指針而言, 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有規定,當定義多個變量時,所有變量的類型都要一致,且為第一個變量的類型,否則編譯出錯。

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

(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 v = {1}; // (4)wrong
}

I.auto不能作為函數參數,否則無法通過編譯;
II.auto不能推導非靜態成員變量的類型,因為auto是在編譯時期進行推導;
III.auto 不能用於聲明數組,否則無法通過編譯;
IV.auto不能作為模板參數(實例化時), 否則無法通過編譯。

2.decltype 類型推導

類型推導是隨著模板和泛型編程的廣泛使用而引入的。在非泛型編程中,類型是明確的,而在模板與泛型編程中,類型是不明確的,它取決於傳入的參數類型。

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

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

(1)typeid 與 decltype

對於C語言,是完全不支持動態類型的;
對於C++,與C不同的是,C++98標准中已經有部分支持動態類型了,便是運行時類型識別(RTTI)。

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

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

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

# include 
# include 
using namespace std;
class white {};
class Black {};
int main(void) {
    white a;
    Black b;
    // white 與 black 前的數字會因編譯器的不同而不同,如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無法滿足程序員的需求:因為RTTI在運行時才確定出類型,而更多的需求是在編譯時確定類型。並且,通常的程序是要使用推導出來的這種類型而不是單純地識別它

(2)decltypr的應用

I.decltype 與 using / typedef 連用
在頭文件,常常看到如下定義:

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

II.增加代碼的可讀性

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

III.重用匿名類型

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

注:對於強類型枚舉,也是C++11中的新特性,這個放到以後的文章裡講。

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

IV.decltype 可以適當擴大模板泛型編程的能力。

template 
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的返回值不會受到限制。如此顯然在一定程度上擴大了模板泛型編程的能力。

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

V. 實例化模板

int hash(char* );
map dict_key; // wrong, decltype需要利用函數返回值進行推導
map dict_key1;

(3)decltype 推導的四規則

在了解這四個規則之前,我們先來了解標記符表達式(id-expression)的概念。

標記符表達式(id-expression):所有除去關鍵字和字面量等編譯器需要使用的標記以外的程序員自定義的標記(token)都可以是標記符(identifier), 而單個標記符對應的表達式就是標記符表達式

int arr[4], int i, arr與i就是標記符表達式。對於前者,去除關鍵字int與字面量[4]後剩下的arr便是標記符表達式。

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

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

下面通過代碼分別對四規則進行舉例:

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

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

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

// 規則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*)&

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

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

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是標記符表達式,而(i)不是標記符表達式,所以它遵循的應當是規則3,decltype推導的類型為int&,即左值引用。i是左值,將i賦給val8是可以的,但是講1賦給val8卻是不可以的。1是右值編譯器不允許將一個右值賦給一個左值引用,所以編譯不通過,這點要注意!

補充一下,在C++11的標准庫中提供了is_lvalue_reference<>與is_rvalue_reference<>, 用於判斷方括號內的內容的類型是否為左值引用或右值引用。如果是,則返回1,如若不是,則返回0。所以可以利用他們來檢驗decltype推導出的類型是否為左值引用和右值引用。

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

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

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

舉例說明:

# include 
# include 
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::value << endl; // 1
    cout << is_volatile::value << endl; // 1
    cout << is_const::value << endl; // 1
    cout << is_volatile::value << endl; // 1
    cout << is_const::value << endl; // 0
    cout << is_volatilei)>::value << endl; // 0
    return 0;
}

還有,使用decltype從表達式推導出類型後進行類型定義時,可能會出現一些冗余的符號:cv限制符,符號引用&。如果推導出的類型已經有了這些屬性,冗余的符號將會被忽略,這種規則叫做折疊規則

下面用表格來概括一下折疊規則

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

TR的類型定義 聲明變量v的類型 v的實際類型 T& TR T& T& TR& T& T& TR&& T& T&& TR T&& T&& TR& T& T&& TR&& T&&

規律:
當TR為T&時,無論定義類型v時有多少個&,最終v的類型都是T&;
當TR為T&&時,則v最終的類型與定義類型v時&的數量有關:
(1)如果&的總數量為奇數,則v的最終類型為T&;
(2)如果&的總數量為偶數,則v的最終類型為T&&。

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

# include 
# include 
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::value << endl; // 1
    cout << is_rvalue_reference::value << endl; // 0
    cout << is_lvalue_reference::value << endl; // 1
    decltype(p)* val3 = &i; // 編譯失敗,val3為int**類型,等號右側為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中,如果一個函數模板的返回類型依賴於實際的入口參數類型,那麼該返回類型在模板實例化之前可能都無法確定,這樣的話我們在定義該函數模板時就會遇到麻煩。

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

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

不過這樣有個問題。因為編譯器是從左到右讀取的,此時decltype內的t1, t2都未聲明,而按照 C/C++ 編譯器的規則,變量在使用前必須聲明。

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

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

auto**占位符**與->return_type是構成追蹤返回類型函數的兩個基本元素。

I.引進返回類型函數後返回類型無需寫明作用域:
如:

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

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

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

在這裡舉一個很經典的例子:

# include 
# include 
using namespace std;

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

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

int main(void) {
    cout << is_same::value << endl; // 1
    return 0;
}

首先說明一下main函數:我認為應該是is_same::value, 即p1、p2後面要加上括號,但是不知道為什麼兩種方式都能通過編譯,此處求指點~

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

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

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

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

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

V.廣泛應用於轉發函數

先了解一下轉發函數的概念。

何為完美轉發?是指在函數模板中,完全依照模板的參數類型,將參數傳遞給函數模板中調用另外一個函數

完美轉發那麼肯定也有不完美轉發。如果在參數傳遞的過程中產生了額外的臨時對象拷貝,那麼其轉發也就算不上完美轉發。為了避免起不完美,我們要借助於引用以防止其進行臨時對象的拷貝。

舉例:

# include 
using namespace std;

double foo(int a) {
    return (double)a + 0.1;
}
int foo(double b) {
    return (int)b;
}
template
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.也可以應用於函數指針及函數引用

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

不僅如此,追蹤返回類型還能應用於結構或類的成員函數,類模板的成員函數,此處就不再舉例了。

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

3.基於范圍的for循環

基於范圍的for循環,結合auto的關鍵字,程序員只需要知道“我在迭代地訪問每一個元素”即可,而再也不必關心范圍,如何迭代訪問等細節。

// 通過指針p來遍歷數組
# include 
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的模板函數,其內含指針的自增。

# include 
# include 
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;
}

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

c++11的基於范圍的for循環,則無需告訴循環體其界限范圍。

# include 
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循環後的括號由冒號“:”分為兩部分,第一部分是范圍內用於迭代的變量,第二部分則表示被迭代的范圍。

注意:auto不會自動推導出引用類型,如需引用要加上&
auto& :修改
auto:不修改, 拷貝對象
基於范圍的循環在標准庫容器中時,如果使用auto來聲明迭代的對象的話,那麼該對象不會是迭代器對象,而是解引用後的對象

continuebreak的作用與原來的for循環是一致的。

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

對於標准庫中的容器,如string, array, vector, deque, list, queue, map, set,等使用基於范圍的for循環沒有問題,因為標准庫總是保持其容器定義了相關操作

注意:如果數組大小不能確定的話,是不能使用基於范圍的for 循環的。

// 無法通過編譯
# include 
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()只是單純傳入一個指針,並不能確定數組的大小,所以不能使用基於范圍的for循環。

4.nullptr

nullptr是為了解決原來C++中NULL的二義性問題而引進的一種新的類型,因為NULL實際上代表的是0。

 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved