Key Takeaways:
-
C++ 依然非常重要,而且將會(huì)永遠(yuǎn)如此。
-
有許多資源可以幫助我們學(xué)習(xí)現(xiàn)代 C++,包括 Godbolt 的編譯器探索器、ISOCpp 和 CppReference。
-
C++ 可以比以前更加簡(jiǎn)單。除了便利性相關(guān)的增強(qiáng)外,潛在的性能改善也是 C++11 和后續(xù)標(biāo)準(zhǔn)的驅(qū)動(dòng)力之一。
-
我們通過(guò)填充一個(gè) vector 并輸出其內(nèi)容進(jìn)行練習(xí)。我們通過(guò)在 vector 上使用算法、range 和 lambda,以便于尋找具有特定屬性的元素,實(shí)現(xiàn)進(jìn)一步的練習(xí)。
C++ 是一門(mén)古老但不斷演進(jìn)的語(yǔ)言。你幾乎可以使用它來(lái)做任何事情,而且可以在很多地方找到它的身影。實(shí)際上,C++ 的發(fā)明者 Bjarne Stroustrup 將其描述為一切事物的隱形基礎(chǔ)。有時(shí),它可以深入到另外一門(mén)語(yǔ)言的庫(kù)中,因?yàn)?C++ 可以用于性能關(guān)鍵的路徑中。它可以在小型的嵌入式系統(tǒng)中運(yùn)行,也可以為視頻游戲提供動(dòng)力。你的瀏覽器可能正在使用它。C++ 幾乎無(wú)處不在!
C++ 為何如此重要
迄今為止,C++ 已經(jīng)存在了很長(zhǎng)的時(shí)間,但是其變化也是非常大的,尤其是 2011 年之后。當(dāng)時(shí),推出了一個(gè)名為 C++11 的新標(biāo)準(zhǔn),標(biāo)志著一個(gè)頻繁更新的時(shí)代正式開(kāi)啟。如果你從 C++11 就沒(méi)有使用過(guò) C++,那么你有很多東西需要補(bǔ)習(xí),這要從哪里開(kāi)始呢?
該語(yǔ)言是需要編譯的,面向特定的架構(gòu),如 PC、大型機(jī)、嵌入式設(shè)備、定制硬件,或者你想到的其他東西。如果你需要代碼在不同類(lèi)型的機(jī)器上運(yùn)行,那需要重新編譯它。這有缺點(diǎn)也有優(yōu)點(diǎn)。不同的配置會(huì)帶來(lái)更多的維護(hù)工作,但編譯到特定架構(gòu)能夠讓你“因地制宜(down to the metal)”,從而獲得速度方面的優(yōu)勢(shì)。
不管你的目標(biāo)是哪種平臺(tái),均需要一個(gè)編譯器。你還需要一個(gè)編輯器或集成開(kāi)發(fā)環(huán)境(IDE)來(lái)編寫(xiě) C++ 代碼。ISOCpp 給出了一個(gè)資源清單,包括 C++ 編譯器。Gnu 編譯器集(Gnu compiler collection,gcc)、Clang 和 Visual Studio 均有免費(fèi)版本。你甚至可以使用 Matt Godbolt 的編譯器探索器,在瀏覽器上嘗試基于各種編譯器的代碼。編譯器可能支持不同版本的 C++,所以必須在編譯器標(biāo)記中說(shuō)明你所需要的版本,例如 g++ 的 -std=c++23 或 Visual Studio 的 /std:c++latest。ISOCpp 網(wǎng)站上有一個(gè) FAQ 區(qū)域,概述了最近的一些變化,包括 C++11 和 C++14,以及整體的概覽。另外,還有多本關(guān)于 C++ 最近版本的圖書(shū)。
使用 Vector 快速了解 C++11
如果你已經(jīng)被落下了,那么大量的資源可能會(huì)讓你不知所措。但是,我們可以通過(guò)一個(gè)小例子來(lái)理解一些基礎(chǔ)知識(shí)。停下來(lái),親自動(dòng)手試一試往往是最好的學(xué)習(xí)方法。因此,我們從簡(jiǎn)單基礎(chǔ)的東西開(kāi)始吧!
一個(gè)很有用(且簡(jiǎn)單)的起點(diǎn)是不太起眼的 vector,它位于 std 命名空間的 vector 頭文件中。CppReference 提供了一個(gè)概述,告訴我們 vector 是一個(gè)序列容器,封裝了動(dòng)態(tài)大小的數(shù)組。因此,vector 包含了一個(gè)連續(xù)的元素序列,我們可以根據(jù)需要調(diào)整 vector 的大小。vector 本身是一個(gè)類(lèi)模板,因此它需要一個(gè)類(lèi)型,例如 std::vector。我們可以使用 push_back 將一個(gè)條目添加到 vector 的尾部。C++11 引入了一個(gè)名為 emplace_back 的新方法,該方法取值來(lái)構(gòu)造一個(gè)新的條目。對(duì)于 int,代碼看上去是一樣的:
std::vector<int> numbers;
numbers.push_back(1);
numbers.emplace_back(1);
如果我們有比 int 更復(fù)雜的東西,那么就可能在 emplace 版本中獲得性能方面的收益,因?yàn)?emplace 版本可以就地構(gòu)造條目,從而避免對(duì)其進(jìn)行復(fù)制。
C++11 引入了 r-value 引用和移動(dòng)語(yǔ)義(move semantics)來(lái)避免不必要的復(fù)制。潛在的性能改善是 C++11 的驅(qū)動(dòng)力之一,后續(xù)的版本都是在此基礎(chǔ)上進(jìn)行的。為了解釋什么是 r-value 引用,我們可以考慮前面樣例中的 push_back 方法。它有兩個(gè)重載形式,其中一個(gè)會(huì)接受一個(gè)常量引用,即 const T& 值,另外一個(gè)接受一個(gè) r-value 引用,即 T&& 值。第二個(gè)版本會(huì)將元素移動(dòng)到 vector 中,這可以避免復(fù)制臨時(shí)對(duì)象。與之類(lèi)似,emplace_back 的簽名通過(guò) r-value 引用來(lái)獲取參數(shù),Args&&…,同樣允許移動(dòng)參數(shù)而無(wú)需復(fù)制。移動(dòng)語(yǔ)義是一個(gè)很大的話題,我們只是接觸到了它的皮毛。如果你想了解更多詳情的話,Thomas Becker 在 2013 年撰寫(xiě)了一篇很好的文章,介紹了它的細(xì)節(jié)。
我們創(chuàng)建一個(gè) vector 并在其中放置幾個(gè)條目,然后使用來(lái)自 iostream 頭文件的 std::cout 展示其內(nèi)容。我們使用流插入操作符<<來(lái)顯示這些元素。我們基于 vector 的 size 編寫(xiě)一個(gè) for 循環(huán),并使用操作符 [] 來(lái)訪問(wèn)每個(gè)元素:
void warm_up()
{
std::vector<int> numbers;
numbers.push_back(1);
numbers.emplace_back(1);
for(int i=0; i
{
std::cout << numbers[i] << ' ';
}
std::cout << '
';
}
int main()
{
warm_up();
}
該代碼會(huì)顯示兩個(gè) 1。這段代碼可以在編譯器探索器上找到。
類(lèi)模板參數(shù)推斷
讓我們做一些更有意思的事情,并學(xué)習(xí)一下現(xiàn)代的 C++。我們構(gòu)建幾個(gè)數(shù)字三角,會(huì)發(fā)現(xiàn)它們之間存在一個(gè)模式。數(shù)字三角的值是 1,3,6,10……它們分別由 1,1+2,1+2+3,1+2+3+4,……相加而成。如果我們這些斯諾克球架起來(lái),就可以組成一個(gè)三角形,它也因此得名:
如果再增加一排,我們就會(huì)再增加六個(gè)斯諾克球。再加一排就會(huì)增加七個(gè),以此類(lèi)推。
為了得到數(shù)字 1,2,3 等,我們可以構(gòu)建一個(gè)充滿 1 的 vector,然后將這些數(shù)字相加。我們可以直接創(chuàng)建一個(gè) vector,比如 18 個(gè) 1,而不必再增加另一個(gè)循環(huán)。我們說(shuō)明想要多少個(gè)元素,然后再指明它的值:
std::vector numbers(18, 1);
注意我們不需要再聲明了。因?yàn)閺?C++17 開(kāi)始,類(lèi)模板參數(shù)推斷(CTAD)就已經(jīng)實(shí)現(xiàn)了。編譯器可以推斷出我們指的是 int,因?yàn)槲覀円蟮闹凳?1,這是一個(gè) int。如果我們需要顯示 vector,那么可以使用基于 range 的 for 循環(huán)。此時(shí),我們不必使用基于 vector 索引的傳統(tǒng) for 循環(huán),而是聲明一個(gè)類(lèi)型,甚至可以使用新的關(guān)鍵字 auto,告訴編譯器判斷類(lèi)型,然后是冒號(hào)和容器:
for (auto i : numbers)
{
std::cout << i << ' ';
}
std::cout << '
';
CTAD 和基于 range 的 for 循環(huán)是 C++11以來(lái)引入的一些便利特性。
Range
有了由“1”組成的 vector,我們就可以包含numeric頭文件,并使用部分的和來(lái)填充一個(gè)新的vector,如 1,1+1,1+1+1……,這樣就有了 1,2,3……我們需要聲明新vector的類(lèi)型,因?yàn)檫@里要從一個(gè)空的vector開(kāi)始,如果沒(méi)有任何值可供使用,那么編譯器將無(wú)法推斷其類(lèi)型。partial_sum需要開(kāi)頭和結(jié)尾的數(shù)字,最后我們需要使用back_inserter,這樣目標(biāo) vector 會(huì)根據(jù)需要增長(zhǎng):
…
std::vector numbers(18, 1);
std::vector<int> sums;
std::partial_sum(numbers.begin(), numbers.end(),
std::back_inserter(sums));
這樣我們就得到了 1 到 18 的數(shù)字,均包含邊界值。我們已經(jīng)完成了數(shù)字三角的部分工作,但是 C++ 現(xiàn)在可以讓我們的代碼更加簡(jiǎn)潔。C++11 引入了?iota?函數(shù),也位于numeric頭文件中,它能夠用不斷增加的值填充一個(gè)容器:
std::vector<int> sums(18);
std::iota(sums.begin(), sums.end(), 1);
實(shí)際上,C++23引入了一個(gè)range版本,它會(huì)為我們找到對(duì)應(yīng)的begin和end:
std::iota(sums, 1);
C++23 還沒(méi)有得到廣泛的支持,所以可能需要等到你的編譯器提供 range 版本。numeric 和 algorithm 頭文件中的很多算法都有兩個(gè)版本,其中一個(gè)需要一對(duì)輸入迭代器(即 first and last),另一個(gè)則是 range 版本,只需要接受容器即可。ranges 重載正在逐漸添加到標(biāo)準(zhǔn) C++ 中。ranges 提供的功能遠(yuǎn)遠(yuǎn)超過(guò)我們這里避免聲明兩個(gè)迭代器的場(chǎng)景。我們可以過(guò)濾和轉(zhuǎn)換輸出,將這些東西連接在一起,并使用視圖來(lái)避免復(fù)制數(shù)據(jù)。ranges 支持惰性計(jì)算,所以視圖的內(nèi)容會(huì)在需要的時(shí)候才評(píng)估計(jì)算出來(lái)。Ivan ?uki?的 Functional Programming in C++ 一書(shū)在這方面提供了更多的細(xì)節(jié)(書(shū)中還包含更多的內(nèi)容)。
我們需要做的最后一件事就是形成數(shù)字三角。查看 vector 的部分和:
std::partial_sum(sums.begin(), sums.end(), sums.begin());
我們已經(jīng)得到了想要的數(shù)字三角,即 1,3,6,10,15……171。
我們注意到,有些算法有 ranges 版本,那我們可以嘗試一個(gè)。前兩個(gè)三角數(shù)字是 1 和 3 是奇數(shù),然后是兩個(gè)偶數(shù) 6 和 10。這個(gè)模式是不是可持續(xù)的呢?如果我們對(duì) vector 進(jìn)行轉(zhuǎn)換,用點(diǎn)號(hào)“.”來(lái)標(biāo)記奇數(shù),用星號(hào)“*”來(lái)標(biāo)記偶數(shù),就能看出最終結(jié)果。我們可以聲明一個(gè)新的 vector 來(lái)存放轉(zhuǎn)換結(jié)果。對(duì)于每個(gè)數(shù)字,僅需要一個(gè)字符,所以我們需要一個(gè) char 類(lèi)型的 vector:
std::vector<char> odd_or_even.
我們可以編寫(xiě)一個(gè)簡(jiǎn)短的函數(shù),它會(huì)獲取一個(gè) int 并返回對(duì)應(yīng)的字符:
char flag_odd_or_even(int i)
{
return i % 2 ? '.' : '*';
}
如果 i % 2 的值不為零,這就是一個(gè)奇數(shù),所以我們返回.,否則,返回 *。我們可以在來(lái)自 algorithm 頭文件的 transform 函數(shù)中使用這個(gè)自己的函數(shù)。最初的版本需要一對(duì)輸入迭代器(first 和 last)、一個(gè)輸出迭代器和一個(gè)一元函數(shù)(unary function),該函數(shù)會(huì)接受一個(gè)輸入,就像我們的 flag_odd_or_even 函數(shù)這樣。C++20 引入了一個(gè) ranges 版本,它能夠接受一個(gè)輸入源,而不是一對(duì)迭代器,另外還需要一個(gè)輸出迭代器和一元函數(shù)。這意味著我們可以通過(guò)如下方式來(lái)轉(zhuǎn)換先前生成的和:
std::vector<char> odd_or_even;
std::transform(sums,
std::back_inserter(odd_or_even),
flag_odd_or_even);
輸出將會(huì)如下所示:
. . * * . . * * . . * * . . * * . .
看上去,我們確實(shí)是不斷地得到兩個(gè)奇數(shù),然后是兩個(gè)偶數(shù)。Stack Exchange 的數(shù)學(xué)網(wǎng)站闡述了出現(xiàn)這種現(xiàn)象的原因。
Lambdas
我們使用另一個(gè)新的 C++ 特性對(duì)我們的代碼做最后的改進(jìn)。如果我們想要看一下實(shí)際的轉(zhuǎn)換代碼的話,那需要要轉(zhuǎn)移到另外一個(gè)地方才能看到這個(gè)一元函數(shù)都做了些什么。
C++11 引入了匿名函數(shù)或 lambda 表達(dá)式的特性。它們看起來(lái)與有名稱(chēng)的函數(shù)類(lèi)似,將參數(shù)放在括號(hào)中,將函數(shù)主體放到花括號(hào)中,但是它們沒(méi)有名字,不需要返回類(lèi)型,并且有一個(gè)用 [] 表示的捕獲組:
[](int i) { return i%2? '.':'*'; }
如果與有名稱(chēng)的函數(shù)進(jìn)行對(duì)比,會(huì)看到兩者的相似性:
char flag_odd_or_even(int i){ return i % 2 ? '.' : '*'; }
我們可以在捕獲組中聲明變量,這會(huì)給我們一個(gè)閉包。這些內(nèi)容超出了本文的范圍,但是在函數(shù)式編程中它們是非常強(qiáng)大和常見(jiàn)的。
如果我們將一個(gè) lambda 分配給一個(gè)變量,
auto lambda = [](int i) { return i % 2 ? '.' : '*'; };
那么,我們就可以像調(diào)用有名稱(chēng)的函數(shù)那樣調(diào)用它:
lambda(7);
這個(gè)特性允許我們使用 lambda 重寫(xiě)轉(zhuǎn)換調(diào)用:
std::transform(sums,
std::back_inserter(odd_or_even),
[](int i) { return i%2? '.':'*'; });
這樣的話,我們就可以在一個(gè)地方看到轉(zhuǎn)換函數(shù),而不必再去查看其他的地方了。
總 結(jié)
將所有的內(nèi)容組合在一起,就形成了如下的代碼:
int main()
{
std::vector<int> sums(18);
std::iota(sums.begin(), sums.end(), 1);
std::partial_sum(sums.begin(), sums.end(), sums.begin());
std::vector<char> odd_or_even;
std::transform(sums,
std::back_inserter(odd_or_even),
[](int i) { return i%2? '.':'*'; });
for (auto c : odd_or_even)
{
std::cout << c << ' ';
}
std::cout << '
';
}
我們使用了 ranges、lambda 和基于 range 的 for 循環(huán),瀏覽了移動(dòng)語(yǔ)義,并練習(xí)了對(duì) vector 的使用。對(duì)于首次重回 C++ 的人來(lái)說(shuō),這是一個(gè)不錯(cuò)的起點(diǎn)!
你可以在編譯器探索器中嘗試上述的代碼。
關(guān)于作者Frances Buontempo,Frances Buontempo 有多年的 C++ 經(jīng)驗(yàn),還有過(guò)使用 Python 和其他各種語(yǔ)言的經(jīng)驗(yàn)。她曾發(fā)表過(guò)關(guān)于 C++ 的演講,并且是 ACCU 的 Overload 雜志的編輯。她有數(shù)學(xué)背景,為 PragProg 寫(xiě)了一本關(guān)于遺傳算法和機(jī)器學(xué)習(xí)的書(shū),并且正在為 Manning 寫(xiě)一本名為 C++ Bookcamp 的 C++ 書(shū),以幫助那些被現(xiàn)代 C++ 落下的人迎頭趕上。
-
C++
+關(guān)注
關(guān)注
22文章
2110瀏覽量
73685 -
編輯器
+關(guān)注
關(guān)注
1文章
806瀏覽量
31190 -
Vector
+關(guān)注
關(guān)注
3文章
60瀏覽量
8623
原文標(biāo)題:C++ 變化很大!得重學(xué)這門(mén)語(yǔ)言了
文章出處:【微信號(hào):CPP開(kāi)發(fā)者,微信公眾號(hào):CPP開(kāi)發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論