C++ 11
引入了大量非常有用的特性勇垛,使代碼更直觀扎运、安全瑟曲、簡潔、方便豪治。
此處列舉的僅是一部分較常用的特性洞拨,完整的列表還需參考官方文檔或者微軟的文檔:Support For C++11/14/17 Features (Modern C++)
初始化列表 Initializer List
所有STL容器都支持初始化列表,如下:
std::vector<int> v = { 1, 2, 3 };
std::list<int> l = { 1, 2, 3 };
std::set<int> s = { 1, 2, 3 };
std::map<int, std::string> m = { {1, "a"}, {2, "b"} };
在自定義class
上支持初始化列表
#include <initializer_list>
class A {
public:
B(const std::initializer_list<int>& items)
: m_items(items)
{}
private:
std::vector<int> m_items;
};
A a1 = { 1, 2, 3 };
// 或者
A a2{ 1, 2, 3 };
統(tǒng)一的初始化方法 Uniform Initialization
可以統(tǒng)一使用大括號(hào){}
進(jìn)行初始化负拟。對構(gòu)造函數(shù)的選擇的優(yōu)先級如下:
class A {
public:
// first choice
A(const std::initializer_list<int>& v) : age(*v.begin())
{}
// second choice
A(int age) : age(age)
{}
// third choice
int age;
};
A a{ 5 };
上面的調(diào)用會(huì)選擇帶初始化列表的構(gòu)造函數(shù)烦衣。
class A {
public:
A() {}
int age;
};
A a{ 5 };
這個(gè)代碼會(huì)編譯出錯(cuò)。因?yàn)榇嬖跇?gòu)造函數(shù)掩浙,但函數(shù)簽名又不匹配花吟。換言之,只要存在自定義的構(gòu)造函數(shù)厨姚,就要求至少有一個(gè)構(gòu)造函數(shù)的參數(shù)列表與大括號(hào)中的參數(shù)完全匹配衅澈,才能使用這種方式初始化。
類型推導(dǎo) Auto Type
過去的這種冗長的類型聲明
std::map<int, std::string>::const_iterator itr = m.find(1);
現(xiàn)在可以寫成這樣了
auto itr = m.find(1);
編譯器會(huì)自動(dòng)推導(dǎo)出正確的類型谬墙。字面量也可以:
auto i = 1; // int
auto d = 1.1; // double
auto s = "hi"; // const char*
auto a = { 1, 2 }; // std:: initializer_list<int>
如果是用Visual Studio矾麻,把鼠標(biāo)懸停在變量名上方纱耻,可以看到推導(dǎo)后的類型名稱。類型推導(dǎo)對于泛型編程非常方便险耀,比如:
template<typename T, typename K>
auto add(T a, K b) {
return a + b;
}
auto a = add(1, 2); // int add(int, int)
auto b = add(1, 2.2); // double add(int, double)
留意第二個(gè)調(diào)用弄喘,返回值被正確地推斷為double
類型。
遍歷 foreach
以前遍歷vector
一般是這么寫的
for (std::vector<int>::const_iterator itr = v.begin(); itr != v.end(); ++itr) {
std::cout << *itr << std::endl;
}
這樣寫有兩個(gè)缺點(diǎn):
- 迭代器聲明很冗長 (用
auto
可以部分解決) - 循環(huán)內(nèi)部必須對迭代器解引用(主要是難看)
可以使用的新的遍歷方式:
for (int i : v) {
std::cout << i << std::endl;
}
代碼立馬簡潔了許多甩牺。但是要注意蘑志,這里每次循環(huán),會(huì)對i
進(jìn)行一次拷貝贬派。此處i
是一個(gè)int
值急但,拷貝不會(huì)造成問題,但是如果是一個(gè)class
搞乏,我們就更希望用引用的方式進(jìn)行遍歷波桩,一般寫成:
std::vector<string> v = { "a", "b" };
for (auto& s : v) {
std::cout << s << std::endl;
}
用auto&
即可以變成引用方式遍歷,甚至還能在循環(huán)中改變它的值请敦。也可以使用const auto&
镐躲,只是一般沒有必要。
空指針 nullptr
以往我們使用NULL
表示空指針侍筛。它實(shí)際上是個(gè)為0的int
值萤皂。下面的代碼會(huì)產(chǎn)生岐義:
void f(int i) {} // chose this one
void f(const char* s) {}
f(NULL);
為此C++ 11
新增類型nullptr_t
,它只有一個(gè)值nullptr
匣椰。上面的調(diào)用代碼可以寫成:
void f(int i) {}
void f(const char* s) {} // chose this one
f(nullptr);
強(qiáng)類型枚舉 enum class
原來的enum
有兩個(gè)缺點(diǎn):
- 容易命名沖突
- 類型不嚴(yán)格
如下代碼:
enum Direction {
Left, Right
};
enum Answer {
Right, Wrong
};
此代碼編譯報(bào)錯(cuò):Right
重定義裆熙。這里使用了單個(gè)單詞作為名稱,很容易出現(xiàn)沖突禽笑。所以我們一般加個(gè)前綴入录,變成:
enum Direction {
Direction_Left, Direction_Right
};
enum Answer {
Answer_Right, Answer_Wrong
};
這樣寫很難看,而且如果這兩個(gè)枚舉是分別從兩個(gè)第三方庫引入的佳镜,那就無法自己改名字了纷跛。而且改成這樣依然有個(gè)問題:
auto a = Direction_Left;
auto b = Answer_Right;
if (a == b)
std::cout << "a == b" << std::endl;
else
std::cout << "a != b" << std::endl;
這個(gè)代碼將輸出a == b
,因?yàn)檫@兩上值都為0邀杏。然而允許兩個(gè)不同類型的值作比較贫奠,就是不合理的,容易隱藏一些bug望蜡。
C++ 11
引入了enum class
:
enum class Direction {
Left, Right
};
enum class Answer {
Right, Wrong
};
auto a = Direction::Left;
auto b = Answer::Right;
if (a == b)
std::cout << "a == b" << std::endl;
else
std::cout << "a != b" << std::endl;
- 引用時(shí)必須加上枚舉名稱(
Direction_Left
變成Direction::Left
)唤崭,似乎寫法上差不多,但是這樣類型更加嚴(yán)格脖律。下面的a == b
編譯將會(huì)報(bào)錯(cuò)谢肾,因?yàn)樗鼈兪遣煌念愋汀?/li> - 枚舉值不再是全局的,而是限定在當(dāng)前枚舉類型的域內(nèi)小泉。所以使用單個(gè)單詞作為值的名稱也不會(huì)出現(xiàn)沖突芦疏。
靜態(tài)斷言 static assert
static_assert
可在編譯時(shí)作判斷冕杠。
static_assert( size_of(int) == 4 );
構(gòu)造函數(shù)的相互調(diào)用 delegating constructor
同一個(gè)class
的多個(gè)構(gòu)造函數(shù)的內(nèi)部實(shí)現(xiàn)通常非常相似,比如:
class A {
public:
A(int x, int y, const std::string& name) : x(x), y(y), name(name) {
if (x < 0 || y < 0)
throw std::runtime_error("invalid coordination");
// other stuff
}
A(int x, int y) : x(x), y(y), name("A") {
if (x < 0 || y < 0)
throw std::runtime_error("invalid coordination");
// other stuff
}
A() : x(0), y(0), name("A") {
// other stuff
}
private:
int x;
int y;
std::string name;
};
為了避免重復(fù)代碼酸茴,通常會(huì)把共同的代碼挪到一個(gè)init
成員函數(shù)里:
class A {
public:
A(int x, int y, const std::string& name) {
init(x, y, name);
}
A(int x, int y) {
init(x, y, "A");
}
A() {
init(0, 0, "A");
}
private:
void init(int x, int y, const std::string& name) {
if (x < 0 || y < 0)
throw std::runtime_error("invalid coordination");
this->x = x;
this->y = y;
if (name.empty())
throw std::runtime_error("empty name");
this->name = name;
// other stuff
}
private:
int x;
int y;
std::string name;
};
這樣寫有三個(gè)問題:
- 二次賦值分预。執(zhí)行到
init
函數(shù)時(shí),數(shù)據(jù)成員實(shí)際已經(jīng)初始化了薪捍。比如name
成員笼痹,此時(shí)已經(jīng)初始化為一個(gè)空字符串了。這里實(shí)際上是又調(diào)用了一次“=
”操作符酪穿。對于初始化成本比較高的類型凳干,這樣做就有可能影響性能了。 - 只能調(diào)用成員的無參構(gòu)造函數(shù)被济。只有構(gòu)造函數(shù)的初始化列表才能調(diào)用成員的帶參數(shù)構(gòu)造函數(shù)救赐。
- 無法保證
init
只被調(diào)用一次。有些初始化步驟必須保證只被執(zhí)行一次只磷,這一點(diǎn)只有構(gòu)造函數(shù)可以保證经磅。
C++ 11
允許構(gòu)造函數(shù)之間相互調(diào)用了:
class A {
public:
A(int x, int y, const std::string& name) : x(x), y(y), name(name) {
if (x < 0 || y < 0)
throw std::runtime_error("invalid coordination");
if (name.empty())
throw std::runtime_error("empty name");
// other stuff
}
A(int x, int y) : A(x, y, "A")
{}
A() : A(0, 0)
{}
private:
int x;
int y;
std::string name;
};
除了優(yōu)雅地解決了上述三個(gè)問題之外,代碼也簡潔了許多喳瓣,連name
成員的默認(rèn)值"A"
也只需要寫一次。
禁止重寫 final
- 禁止虛函數(shù)被重寫
class A {
public:
virtual void f1() final {}
};
class B : public A {
virtual void f1() {}
};
此代碼編譯報(bào)錯(cuò)赞别,提示不能重寫f1
畏陕。雖然f1
是虛函數(shù),但是因?yàn)橛?code>final關(guān)鍵字仿滔,保證它不會(huì)被重寫惠毁。你可能會(huì)說,那不聲明virtual
不就完了崎页。但是如果A
本身也有基類鞠绰,f1
是繼承下來的,那virtual
就是隱含的了飒焦。
- 禁止類被繼承
class A final {
};
class B : public A {
};
此代碼編譯報(bào)錯(cuò)蜈膨,提示不能繼承A
。
顯式聲明重寫 override
class A {
public:
virtual void f1() const {}
};
class B : public A {
virtual void f1() {}
};
上面的代碼在重寫函數(shù)f1
時(shí)不小心漏了const
牺荠,但是編譯器不會(huì)報(bào)錯(cuò)翁巍。因?yàn)樗恢滥闶且貙?code>f1,而認(rèn)為你是定義了一個(gè)新的函數(shù)休雌。這樣的情況也發(fā)生在基類的函數(shù)簽名變化時(shí)灶壶,子類如果沒有全部統(tǒng)一改過來,編譯器也不能發(fā)現(xiàn)問題杈曲。
C++ 11
引入了override
聲明驰凛,使重寫更安全胸懈。
class B : public A {
virtual void f1() override {}
};
此時(shí)編譯報(bào)錯(cuò),提示找不到重寫的函數(shù)恰响。
定義成員初始值
當(dāng)我們?yōu)橐粋€(gè)class
增加成員變量時(shí)趣钱,要注意在所有構(gòu)造函數(shù)中都對它進(jìn)行初始化(除非這個(gè)成員的默認(rèn)構(gòu)造函數(shù)就滿足我們的要求)。雖然C++ 11
允許構(gòu)造函數(shù)相互調(diào)用渔隶,但至少該成員變量的聲明和初始化是分開寫的羔挡,導(dǎo)致后者經(jīng)常被遺忘。現(xiàn)在C++ 11
可以在聲明成員變量的時(shí)直接賦初始值间唉。
class A {
public:
int m = 1;
};
這個(gè)初始化的動(dòng)作會(huì)在所有構(gòu)造函數(shù)之前執(zhí)行绞灼,可以理解為這些初始值會(huì)被自動(dòng)放到初始化列表。如果初始化列表也有個(gè)初始化呈野,則選用初始化列表的值低矮。
class A {
public:
A() : m(2)
{}
int m = 1; // 這個(gè)1被忽略
};
那實(shí)際上m
會(huì)不會(huì)是先被初始化為1,再被改為2呢(二次賦值)被冒?我們用一個(gè)自定義的類作為成員變量:
class M {
public:
M(int i) : i(i) {
std::cout << "M(" << i << ")" << std::endl;
}
M(const M& other) : i(other.i) {
std::cout << "copy M(" << i << ")" << std::endl;
}
M& operator = (const M& other) {
i = other.i;
std::cout << "= M(" << i << ")" << std::endl;
return *this;
}
private:
int i;
};
class A {
public:
A() : m(1)
{}
private:
M m = M(2);
};
A a;
我們?yōu)?code>M實(shí)現(xiàn)了三件套(構(gòu)造函數(shù)军掂,復(fù)制構(gòu)造函數(shù),賦值操作符)昨悼,并打印出信息蝗锥,這樣我們可以知道具體發(fā)生了什么。運(yùn)行結(jié)果:
M(1)
說明下面的M(2)
直接被忽略了率触。
默認(rèn)構(gòu)造函數(shù) default
當(dāng)一個(gè)class
有自定義構(gòu)造函數(shù)時(shí)终议,編譯器就不會(huì)自動(dòng)生成一個(gè)無參構(gòu)造函數(shù)。現(xiàn)在可以通過default
關(guān)鍵字強(qiáng)制要求生成這個(gè)構(gòu)造函數(shù)葱蝗。
class A {
public:
A(int i) {}
A() = default;
};
當(dāng)然穴张,你也可以直接寫成
A() {}
但用default
意圖更加明確,編譯器也可以相應(yīng)地做優(yōu)化两曼。
刪除構(gòu)造函數(shù) delete
以往皂甘,當(dāng)我們需要隱藏構(gòu)造函數(shù)時(shí),可以把它聲明為private
成員
class A {
private:
A();
};
現(xiàn)在可以使用delete
關(guān)鍵字
class A {
public:
A() = delete;
};
常量表達(dá)式 constexpr
int size() { return 3; }
int a[size()];
上面的代碼編譯失敗悼凑,因?yàn)殪o態(tài)數(shù)組的大小必須在編譯期確定偿枕。改成:
constexpr int size() { return 3; }
int a[size()];
加上了constexpr
,函數(shù)size
變成在編譯期計(jì)算户辫,返回值被看成一個(gè)常量益老。
字符串字面量
const char* a = "string a";
const char* b = u8"string b"; // UTF-8
const char16_t* c = u"string c"; // UTF-16
const char32_t* d = U"string d"; // UTF-32
const char* e = R"(string e1 "\\
stirng e2)"; // raw string
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << c << std::endl;
std::cout << d << std::endl;
std::cout << e << std::endl;
輸出結(jié)果:
第1、2行沒問題寸莫;第3捺萌、4行實(shí)際是打印出了內(nèi)存地址,因?yàn)?code>std::cout不支持這兩種類型。
第5種比較有意思桃纯,它是忽略了轉(zhuǎn)義符的字符串酷誓。從這個(gè)例子可以看到:
- 它的格式是
R"(...)"
,中間的...
是內(nèi)容态坦。 - 內(nèi)容可以出現(xiàn)
"
符號(hào)而不會(huì)截?cái)嘧址?/li> - 轉(zhuǎn)義符
\
被當(dāng)成一個(gè)字符 - 換行也被當(dāng)成字符串的內(nèi)容(如果要忽略換行符盐数,則在換行前使用
\
連接符)。 - 縮進(jìn)也被當(dāng)成內(nèi)容伞梯。
利用這個(gè)特性玫氢,這樣的代碼:
auto xml = "<root>\n"
"\t<item value=\"1\">\n"
"\t<item value=\"2\">\n"
"</root>";
就可以直接寫成:
auto xml = R"(<root>
<item value="1">
<item value="2">
</root>)";
不足之處就是會(huì)破壞代碼的縮進(jìn),因?yàn)榭s進(jìn)也被看成是字符串的內(nèi)容谜诫。
Lambda函數(shù)
這是個(gè)非常強(qiáng)大的重量級功能漾峡。簡單地講,就是可以用它定義一個(gè)臨時(shí)的函數(shù)對象喻旷,它像其它對象一樣可以傳遞和保存生逸。更為強(qiáng)大的是,它甚至可以訪問當(dāng)前函數(shù)的上下文且预。
特性
- 調(diào)用
auto add = [](int a, int b) { return a + b; };
std::cout << add(1, 2) << std::endl;
-
=
后面的部分就是Lambda
函數(shù)槽袄。先忽略前面的[]
。()
里面的是參數(shù)列表锋谐,{}
里面的是實(shí)現(xiàn)遍尺。跟普通的函數(shù)基本一樣。 - 這里沒有聲明返回值類型涮拗,編譯器會(huì)根據(jù)
return
語句推導(dǎo)乾戏。如果有多個(gè)return
語句,而且類型不一樣多搀,則會(huì)報(bào)錯(cuò)歧蕉。 - 使用方式與普通函數(shù)一樣灾部。
- 傳遞
template<typename filter_func>
void print(const std::vector<int>& v, filter_func filter) {
for (auto i : v) {
if (filter(i))
std::cout << i << std::endl;
}
}
bool isGreaterThanTen(int i) {
return i > 10;
}
class GreaterThanTenFilter {
public:
bool operator()(int i) {
return i > 10;
}
};
std::vector<int> v = { 5, 10, 15, 20 };
print(v, isGreaterThanTen); // 輸出 15 20
print(v, GreaterThanTenFilter()); // 輸出 15 20
以上代碼分別使用了函數(shù)指針和函數(shù)對象來指定過濾條件康铭。這兩種方式存在以下缺點(diǎn):
- 代碼冗余。需要單獨(dú)定義一個(gè)函數(shù)或
class
才能實(shí)現(xiàn)赌髓。 -
filter_func
的類型不明確从藤。此處filter_func
是一個(gè)參數(shù)為一個(gè)int
,返回值為bool
型的函數(shù)锁蠕。但是這一點(diǎn)無法從函數(shù)聲明看出來夷野。并且函數(shù)對象使用()
操作符語義也不明確。 -
print
函數(shù)必須使用模板荣倾。雖然print
內(nèi)部并沒有使用泛型的必要悯搔,但是考慮到兼容函數(shù)指針和函數(shù)對象的用法,也只能使用模板實(shí)現(xiàn)舌仍。 - 不靈活妒貌。如果這個(gè)
10
是一個(gè)運(yùn)行時(shí)才確定的數(shù)字n
通危,就需要修改函數(shù)對象才能實(shí)現(xiàn)。(函數(shù)指針無法實(shí)現(xiàn))
使用Lambda
:
#include <functional>
void print(const std::vector<int>& data, std::function<bool(int)> filter) {
for (auto i : data) {
if (filter(i))
std::cout << i << std::endl;
}
}
std::vector<int> v = { 5, 10, 15, 20 };
print(v, [](int i) { return i > 10; }); // 輸出 15 20
解決了上面提到的幾個(gè)問題:
- 代碼簡潔灌曙。無需另外定義函數(shù)或
class
即可實(shí)現(xiàn)菊碟。整體代碼縮小了不少。 - 類型明確在刺。新增的
std::function
是一個(gè)通用的函數(shù)對象逆害,可以使用Lambda
初始化。最大的優(yōu)點(diǎn)是參數(shù)和返回值都是明確的蚣驼,可以從聲明看出來魄幕。 - 無須使用模板。
- 更靈活隙姿。這一點(diǎn)接下來講梅垄。
- 可以訪問當(dāng)前函數(shù)的上下文
上面的例子如果把硬編碼的10
改成變量n
,只需要改調(diào)用的地方:
int n = 10;
print(v, [=](int i) { return i > n; });
可以看到前面的[]
改成了[=]
输玷,這表示Lambda
使用值傳遞的方式捕獲外部變量队丝。
[]
表示捕獲列表,用來描述Lambda
訪問外部變量的方式欲鹏。如下:
捕獲列表 | 作用 |
---|---|
[a] |
a 為值傳遞 |
[a, &b] |
a 為值傳遞机久,b 為引用傳遞 |
[&] |
所有變量都用引用傳遞。當(dāng)前對象(即this 指針)也用引用傳遞赔嚎。 |
[=] |
所有變量都用值傳遞膘盖。當(dāng)前對象用引用傳遞。 |
注意事項(xiàng)
- 捕獲時(shí)機(jī)
int i = 1;
auto f = [=]() { std::cout << i << std::endl; };
i = 2;
f(); // 輸出 1
可以看出尤误,在定義Lambda
的地方就已經(jīng)捕獲到i
的值侠畔。后面修改i
也不影響f
的輸出。
如果把[=]
改成[&]
损晤,則會(huì)輸出2软棺。因?yàn)?code>Lambda實(shí)際上只捕獲到i
的引用。
- 局部變量的生命周期
std::function<void()> GetLambda() {
int i = 1;
return [&]() { std::cout << i << std::endl; };
}
auto f = GetLambda();
f(); // 輸出 -858993460 之類的亂碼
使用引用的方式訪問局部變量時(shí)尤勋,要注意Lambda
的生命周期不能超過該局部變量的生命周期喘落。
內(nèi)部實(shí)現(xiàn)
(待續(xù)……)
參考資料:
Learn C++ 11 in 20 Minutes - Part I
Learn C++ 11 in 20 Minutes - Part II
Support For C++11/14/17 Features (Modern C++)
Lambda 表達(dá)式