C  :C  11新特性詳解(1)
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

前言:

雖然目前沒有編譯器能夠完全實現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


// 2016.06.09 補充了nullptr部分

看完了前言,相信你對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<std::string> array;
std::vector<std::string>::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 <typename T1, typename T2>
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<auto> 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 <iostream>
# include <typeinfo>
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<std::string> 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 <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的返回值不會受到限制。如此顯然在一定程度上擴大了模板泛型程式設計的能力。

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

V. 例項化模板

int hash(char* );
map<char*, decltype(hash)> dict_key; // wrong, decltype需要利用函式返回值進行推導
map<char*, decltype(hash(nullptr))> 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 <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從表示式推匯出型別後進行型別定義時,可能會出現一些冗餘的符號:cv限制符,符號引用&。如果推匯出的型別已經有了這些屬性,冗餘的符號將會被忽略,這種規則叫做摺疊規則

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

typedef T& TR;  // T&的位置是對於TR的型別定義
TR v;  // TR的位置是宣告變數v的型別

TR的型別定義宣告變數v的型別v的實際型別
T&TRT&
T&TR&T&
T&TR&&T&
T&&TRT&&
T&&TR&T&
T&&TR&&T&&

規律:
當TR為T&時,無論定義型別v時有多少個&,最終v的型別都是T&;
當TR為T&&時,則v最終的型別與定義型別v時&的數量有關:
(1)如果&的總數量為奇數,則v的最終型別為T&;
(2)如果&的總數量為偶數,則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**型別,等號右側為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<typename T1, typename T2>
decltype(t1 t2) Sum(T1& t1, T2& t2) {
return t1 t2;
}

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

因此, 為了解決這一問題,C 11引進了新語法——追蹤返回型別,來宣告和定義這樣的函式:

template<typename T1, typename T2>
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 <iostream>
# include <type_traits>
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<decltype(pf), decltype(pf1)>::value << endl; // 1
return 0;
}

首先說明一下main函式:我認為應該是is_same<decltype(pf()), decltype(pf1())>::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 <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.也可以應用於函式指標及函式引用

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

不僅如此,追蹤返回型別還能應用於結構或類的成員函式,類别範本的成員函式,此處就不再舉例了。

特殊:沒有返回值的函式也可以被宣告為追蹤返回型別,只需將返回型別宣告為void即可。

3.基於範圍的for迴圈

基於範圍的for迴圈,結合auto的關鍵字,程式設計師只需要知道“我在迭代地訪問每一個元素”即可,而再也不必關心範圍,如何迭代訪問等細節。

// 通過指標p來遍歷陣列
# 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的模板函式,其內含指標的自增。

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

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

c 11的基於範圍的for迴圈,則無需告訴迴圈體其界限範圍。

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

注意: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 <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()只是單純傳入一個指標,並不能確定陣列的大小,所以不能使用基於範圍的for迴圈。

4.nullptr

在良好的C 程式設計習慣中,宣告一個變數的同時,總是需要記得在合適的程式碼位置將其初始化。對於指標型別的變數,這一點尤其應當注意。未初始化的懸掛指標通常會是一些難於除錯的使用者程式的錯誤根源

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

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

NULL其實是巨集定義,在傳統C標頭檔案(stddef.h)中的定義如下:

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

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

# 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既可以被替換成整型,也可以被替換成指標,因此在函式呼叫時就會出現問題。因此,在早期版本的C 中,為了解決這種問題,只能進行顯示型別轉換

所以在C 11中,為了完善這一問題,引入了nullptr的指標空值型別的常量。為什麼不重用NULL?原因是重用NULL會使已有很多C 程式不能通過C 11編譯器的編譯。為保證最大的相容性且避免衝突,引入新的關鍵字是最好的選擇。

而且,出於相容性的考慮,C 11中並沒有消除NULL的二義性

那麼,nullptr有沒有資料型別呢?標頭檔案對其型別的定義如下:

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

即nullptr_t為nullptr的型別, 稱為指標空值型別。指標空值型別的使用有以下幾個規則:
1.所有定義為nullptr_t型別的資料都是等價的,行為也是完全一致的。
也就是說,nullptr_t的物件都是等價,都是表示指標的空值,即滿足“==”。
2.nullptr_t型別的資料可以隱式轉換成任意一個指標型別。
3.nullptr_t型別資料不能轉換成非指標型別,即使用reinterpret_cast()的方式也不可以實現轉化;
4.nullptr_t型別的物件不適用於算術運算的表示式;
5.nullptr_t型別資料可以用於關係運算表示式,但僅能與nullptr_t型別資料或者是指標型別資料進行比較,當且僅當關係運算子為-=, <=, >=, 等時返回true。

# include <iostream>
# include <typeinfo>
using namespace std;
int main(void) {
// nullptr 隱式轉換成char*
char* cp = nullptr;
// 不可轉換成整型,而任何型別也不可能轉化成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;
// 不能轉化成整型或bool型別,以下程式碼不能通過編譯
if (0 == nullptr);
if (nullptr);
// 不可以進行算術運算,以下程式碼不能通過編譯
// nullptr  = 1
// nullptr * 5
// 以下操作均可以正常進行
// 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 看起來像個指標型別,用起來更像。但是在把nullptr_t應用於模板的時候,我們會發現模板只能把它作為一個普通的型別進行推導,並不會將其視為T*指標。

# include <iostream>
using namespace std;
template<typename T>
void g(T* t) {}
template<typename T>
void h(T t) {}
int main(void) {
// nullptr 並不會被編譯器“智慧”地推導成某種基本型別的指標或者void*指標。
// 為了讓編譯器推匯出來,應當進行顯示型別轉換
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是編譯時期的常量,它的名字是一個編譯時期的關鍵字,能夠為編譯器所識別,而(void*)0只是一個強制型別轉化的表示式,其返回值也是一個void*的指標型別。
2.nullptr 能夠隱式轉化成指標,而(void*)0只能進行顯示轉化才能變成指標型別(c 11)。雖然在c 標準中(void*)型別的指標可以實現隱式轉化。

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

補充: 除了nullptr, nullptr_t的物件的地址都可以被使用者使用 nullptr是右值,取其地址沒有意義,編譯器也是不允許的。如果一定要取其地址,也不是沒有辦法。可以定義一個nullptr的右值引用,然後對該引用進行取地址。

const nullptr_t&& p = nullptr;

以上內容皆為本人觀點,歡迎大家提出批評和指導,我們一起探討!


相關文章

程式語言 最新文章