| 3.13. Hello STL 列表篇 vector是一種容器。我們還知道,vector其實(shí)是一個(gè)“類(lèi)模板”,具體使用前,必須通過(guò)以下語(yǔ)法指定將要存儲的元素類(lèi)型: vector<元素類(lèi)型>
這就是決定了vector是一個(gè)“通用”的容器,如果元素類(lèi)型是“Beauty”,那么它就是一個(gè)裝美女的容器,如果元素類(lèi)型是“Money”,那么它就是裝錢(qián)的容器。 “列表/list” 也是一種容器,同樣,list也是一個(gè)“類(lèi)模板”,具體使用前,必須通過(guò)以下語(yǔ)法指定將要存儲的元素類(lèi)型: list <元素類(lèi)型>
看來(lái)list也是一種通用容器——或許你想問(wèn):既生vector,何生list呢?
3.13.1. vector VS. list 最簡(jiǎn)單的回答是:list和vector的結構不同,因此決定了它們在能力分工上的不同。且先說(shuō)說(shuō)生活中的容器。桶是圓柱體,柜子是立方體,這就叫“結構不同的容器”。在設計上,圓柱體的桶,有利于用最少的材質(zhì),制造出最大容量的桶,而柜子正好相反,如果家里所有衣柜、書(shū)柜等等全部是圓柱體,那么它們將憑空浪費掉屋子的很多空間。 因此,雖然容器的功能粗一看就是“存放元素”,但是,由于要存放的元素自身結構不同,或者外部對容器的遍歷方式不同等原因,我們不得不需要有多種“功能傾向”明顯不同的容器。比如vector和list。 vector最大的結構特點(diǎn)是:它開(kāi)辟連續的內存空間用以存儲元素。而list正好相反,它允許使用不連續內存空間。 假設你想在熱鬧的市區蓋5間房子,有兩種方案,第一種方案把5間房連續蓋在一起,這會(huì )讓你生活很方便,但你需要一大塊地皮,可能市政府不會(huì )滿(mǎn)足你,為了你這一塊地,可能要拆牽別人許多房;第二種方案你同意把五間房分開(kāi)蓋,這會(huì )給生活帶來(lái)一定不便,但現在你只需在市區見(jiàn)縫插針地找到5塊小地皮,就可以動(dòng)工了。 第一種方案正是vector分配內存的策略,第二種方案則是list分配內存的策略。由此,我們可以得出第一個(gè)“猜想”:如果我們有一大堆對象,并且這些對象個(gè)頭都很大,那么我們就必須考慮是否放棄采用vector直接存儲? 作為代替方案,有兩種,其一是仍然采用vector存儲,但存放的元素改成是“堆對象”,也就是說(shuō)將每個(gè)對象創(chuàng )建在“堆內存區”中,而容器中僅僅保存對象在“堆內存”中的地址。這個(gè)方案不足之處是:由于“堆對象”我們必須手工“殺死它們”,所以會(huì )帶來(lái)額外的內存管理工作。其二,我們可以采用list容器進(jìn)行管理。list允許我們仍然將數據保存在“棧內存區”中,不過(guò)必須將每個(gè)元素分開(kāi)保存,從而有利于分配出足夠的內存。 當然,“內存分配”并不是我們考慮是否采用list的唯一因素——如果非要找一個(gè)vector的代替方案,STL中的deque容器可能更適合——更為重要的考慮對元素的訪(fǎng)問(wèn),包括讀取元素、添加元素、插入元素、刪除元素的方式,及其代價(jià)。 比如“插入元素”:在vector模式下:你有三間緊密蓋在一起的房子,現在你想在第1、2間房子中插入一間新房子——老天,這在現實(shí)中幾乎難以實(shí)現。聽(tīng)說(shuō)有一種“樓房平移”技術(shù),允許在地基下安裝輪子,然后以極慢的,比如一天1厘米的速度,將樓房不知不覺(jué)地挪開(kāi)數米…… vector采用連續內存保存元素,因此當需要在中間插入元素時(shí),同樣需要將插入點(diǎn)之后的所有元素往后“平移”,有時(shí)甚至需要將整個(gè)內存摧毀重建。類(lèi)似的,在vector中刪除元素的代價(jià)也非常之大。由此我們得出第二個(gè)“猜想”:如果你在存儲對象的同時(shí),還需要經(jīng)常插入、刪除位于中部的元素,那么采用vector也是不合適的。這時(shí)候又可以考慮list,因為list中的元素,東一個(gè)西一個(gè),元素之間存在不是“物理”的,而是“邏輯”上的次序關(guān)系。要在所謂“中間位置”插入一個(gè)元素,易如反掌。 “猜想”到此結束,還是直接了解一下list的結構吧。
3.13.2. 基礎 (圖 39 list 內存結構示意)
現在,假設B左邊的那位“釘子戶(hù)”搬走,而我們正好在此時(shí)在A(yíng)和B之間插入A’元素,這引起的變化是什么呢? (圖 40 A與B之間插入A''元素)
list可以實(shí)現高速地插入、刪除元素,這一點(diǎn)讓vector望塵莫及。vector僅當在尾部“追加”元素時(shí),才有可能因為不必遷移前面元素而獲得較高速度。然而,list卻沒(méi)有vector的“隨機訪(fǎng)問(wèn)”能力。我們無(wú)法采用“[N]”來(lái)直接訪(fǎng)問(wèn)list中第N個(gè)元素,相反,我們只能直接訪(fǎng)問(wèn)到第一個(gè),或最后一個(gè)元素,然后依據“后一元素地址”,不斷往后, 或者依據“前一元素地址”不斷往前,從而遍歷每個(gè)元素。 如果你不太理解,那么想象一隊士兵,我們被規定只允許“接觸”正副班長(cháng),但卻要將球傳給中間的某個(gè)士兵時(shí)該如何辦?(提示:一隊士兵中,通常班長(cháng)排頭,副班長(cháng)壓尾)。 3.13.3. 迭代器/iterator概念 由于在訪(fǎng)問(wèn)某一元素時(shí),必須同時(shí)得到“前一元素地址”和“后一元素地址”等相關(guān)信息,因此,當我們在訪(fǎng)問(wèn)list時(shí),并不是直接得到當初存入list的裸數據(如上圖中的A、B、C),而是得到另外一種類(lèi)型的數據,稱(chēng)為“迭代器/Iterator”。這不僅僅是list的策略,而是STL中常規容器共有的特點(diǎn)。對于list,“迭代器”包裝了裸數據,同時(shí)又新增了用于前往“前一元素”和“后一元素”的信息。 有關(guān)迭代器的實(shí)現,需要觸及許多們尚未學(xué)習的C++知識。在今天,你暫時(shí)可以把迭代器當作是對“裸數據”的一層封裝,比如一個(gè)list<Beauty>的迭代器,暫時(shí)可以理解為這樣一個(gè)結構: struct iterator { Beauty *ptr; //指向我們保存的“美女”元素 iterator* next; //后一個(gè)美女的位置 iterator* prior; //前一個(gè)美女的位置 }; 依據這個(gè)簡(jiǎn)單的迭代器結構,假設我們現在擁有一個(gè)iterator的對象,名為 cur。那么,我們可以這樣的操作: 1.訪(fǎng)問(wèn)到當前“美女”的名字: cur.ptr->GetName(); 2. 前進(jìn)到后一個(gè)“美女”元素: cur = cur.next; 3. 后退到前一個(gè)“美女”元素: cur = cur.prior;
真實(shí)的STL迭代器內部實(shí)現要比上面的例子,復雜得多——作為回報——它的對外的接口更加直觀(guān)了: 1. 訪(fǎng)問(wèn)到當前“美女”的名字: cur->GetName(); 2. 前進(jìn)到后一個(gè)“美女”元素: ++cur; //或者:cur++; 3. 后退到前一個(gè)“美女”元素: --cur; //或者:cur--;
和vector提供的“[]”一樣,“++、- -、->、*”也是操作符,同樣可以通過(guò)“操作符重載”技術(shù),使其與“函數”類(lèi)型的方式,提供特定的功能。 普通迭代器的類(lèi)型名稱(chēng)為:iterator。不過(guò)它總是定義在具體的容器類(lèi)中。對于list<Beauty>。我們可以認為存在這樣一個(gè)類(lèi)型: struct list< Beauty > { //… }; 而它的迭代器類(lèi)型的定義,總是嵌套其中: struct list< Beauty > { struct iteraotr { //… }; //… }; 回憶一下,我們經(jīng)??梢园岩粋€(gè)類(lèi)也當作一個(gè)名字空間(namespace),所以,list<int>容器類(lèi)型的迭代器的類(lèi)型完整名稱(chēng)即是:list< Beauty >::iterator——還記得“德國小蠊::小強”嗎? 定義一個(gè)list<Beauty>的迭代器的變量,代碼為: list<Beauty>::iterator iter; 由于list的迭代器既可以前進(jìn)(++操作),又可以后退(--操作),因此,我們稱(chēng)之為“雙向迭代器”。 list的不少常用函數,都會(huì )涉及到迭代器,比如返回值是一個(gè)迭代器,或者參數是一個(gè)迭代器。我們先了解和迭代器無(wú)關(guān)的常用函數。
3.13.4. 常用函數(一) 請新建一個(gè)控制臺應用項目,命名為“HelloSTLVector”。打開(kāi)項目唯一的源文件:main.cpp。如果你覺(jué)得必要,請確保修改文件編碼為“系統默認”。 在002行加入包含<list>頭文件的代碼: 001 #include <iostream> 002 #include <list> 在main函數最前面加入一行定義,以產(chǎn)生一個(gè)“專(zhuān)門(mén)用于存儲整數的list”的變量,變量名為“l(fā)st”。(第1個(gè)字符是字母“l(fā)”還是數字“1”?你猜得出的,不是嗎?) 006 int main() { 008 list<int> lst; ...... 請在當前代碼的基礎上,請一邊閱讀以下課程,一邊在項目中完成相應的代碼。
在list頭部插入一個(gè)元素。 lst.push_front(10); lst.push_front(20); 現在,lst中的元素是:20、10。
在list尾部插入一個(gè)元素。 lst.push_back(8); lst.push_back(9); 現在,list中的元素是:20、10、8、9。
刪除將list第一個(gè)元素。 lst.pop_front(); 現在,lst中的元素是:10、8、9。沒(méi)錯,第一個(gè)元素被丟棄了。 pop_front()僅僅是“丟”掉第一個(gè)元素,并不將第一個(gè)元素返回給函數的調用者。
刪除list最后一個(gè)元素。 lst.pop_back(); 現在,lst中的元素是:10、8。
![]() 以下代碼片段錯在哪里: list<int> lst; lst.push_back(1); lst.push_back(2); int a = lst.pop_front(); int b = lst.pop_back();
請對比隨后的front()與back()函數,二者允許我們得到(但不丟棄)第一個(gè)元素或最后一個(gè)元素。
刪除容器中所有元素。 lst.clear(); 現在lst中一個(gè)元素也沒(méi)有。
返回第一個(gè)元素(“裸元素”,而非迭代器)。 lst.push_back(1); lst.push_back(2); int a = lst.front(); cout << a << endl; a的值是1。同時(shí)lst中的元素現在是:1、2。 front()并不影響容器內部的任何數據,所以它是一個(gè)“常量成員函數”。
返回最后一個(gè)元素(“裸元素”,而非迭代器)。 int b = lst.back(); cout << b << endl; b的值是2。 和front()一樣,back()同樣直接返回容器中的元素,而不是迭代器。本例與前例中,lst的類(lèi)型是:list<int>,所以a和b都被定義成int類(lèi)型。
返回容器中元素的個(gè)數。 int count = lst.size(); cout << count << endl; count值為2。
判斷容器是否為空(元素個(gè)數為0)。 如果僅關(guān)心容器是否為空,請調用此函數以獲得更好性能,而不要通過(guò):“size() == 0” 判斷。 cout << lst.empty() << endl; lst.clear(); cout << lst.empty() << endl; 第一行輸出0;第二行輸出1。
默認狀態(tài)下,cout將bool值視為整數處理,值false對應0,值true對應1。
3.13.5. 常用函數(二)
begin()函數返回list第一個(gè)元素的迭代器。 需要特別注意的是:end() 返回的是最后一個(gè)元素的“下一個(gè)”迭代器。假設list有3個(gè)元素,那么end()返回了第4個(gè)元素的迭代器,第4個(gè)元素并不真實(shí)存在,它是個(gè)“虛”的元素(請參看“l(fā)ist 內存結構示意圖”)。 得到lst的第一個(gè)迭代器代碼如下: list<int>::iterator iter = lst.begin(); 有了一個(gè)迭代器,如果通過(guò)它得到對應的元素呢?方法很簡(jiǎn)單——操作一個(gè)“迭代器”和操作堆對象非常類(lèi)似——采用“*”操作符。 int a = *iter; 我們也可以通過(guò)迭代器來(lái)修改對應的元素: *iter = 1000; 我們來(lái)一個(gè)相對完整的代碼片段,演示如何通過(guò)一個(gè)迭代器讀取與修改對應的元素。 list <int> lst; lst.push_back(1); lst.push_back(2); list<int>::iterator iter = lst.begin(); int a = *iter; cout << a << endl; //輸出 1 cout << *iter << endl; //同樣輸出1 *iter = 1000; cout << *iter << endl; //輸出1000 cout << a << endl; //輸出1; int b = *iter; cout << b << endl; //輸出1000; 光說(shuō)begin()函數了,end()函數是不是也可以有相同操作呢? lst<int>::iterator iter2 = lst.end(); int a = *iter2; //災難發(fā)生 *iter2 = 100; //災難發(fā)生 因為end()返回的迭代器綁定到一個(gè)“虛擬”的元素。元素總是保存在內存中,所謂“虛擬”的元素,可能是一塊“并不真實(shí)存在的內塊”,也可能是指一塊“并不屬于你的內存”——還不理解嗎?來(lái)聽(tīng)一個(gè)故事吧。
![]() 有一次,丁小明興沖沖地給我一張1000萬(wàn)元的存款單,我拿了直奔銀行要求取現,結果那天我鼻青臉腫地回來(lái)了。 后來(lái)我去丁家,正遇上他家老婆蹲在地上,面前一張板凳,板凳上一個(gè)詭異紙盒,紙盒內一撂單據。他家老婆神情興奮地數著(zhù):“開(kāi)始!50元、120元、75元5角、60元2角7分……1000萬(wàn)?結束?!蔽也粣u地罵了一聲:“虛偽!”。丁家老婆靦腆地沖我家一笑,蓋上了紙盒。我才發(fā)現紙盒上寫(xiě)了幾行字: 產(chǎn)地:C++標準委員會(huì ) 結構:std::list 類(lèi)型: std::list<存款單> 我惡狠狠地搶過(guò)這個(gè)紙盒,翻成底朝天——果然,這個(gè)盒子兩邊都有蓋!我惱怒地打開(kāi)后面的蓋,天!那張千萬(wàn)元的單據,居然自動(dòng)跑到另一邊去了!
rbegin() 是list中最后一個(gè)元素的迭代器。而rend()同樣需要引起你的注意:它是第一個(gè)元素之前的那個(gè)虛擬元素的迭代器。 rbegin()、rend()返回的迭代器,和begin()/end()返回類(lèi)型并不相同,前者稱(chēng)為“逆向迭代器”,具體類(lèi)型是:list<元素類(lèi)型>:: reverse_iterator。
在指定位置插入一個(gè)新元素。pos并不是一個(gè)用來(lái)表示元素位置的整數,而是一個(gè)iterator(并且不能是reverse_iterator),表示在指定迭代器的前面,插入新元素。 lst.clear(); lst.push_back(10); lst.push_back(100); list<int>::iterator iter = lst.begin(); ++iter; //iter前進(jìn)1步,指向第二個(gè)元素 lst.insert(iter, 1); //在第二個(gè)元素的位置上,插入新元素 現在,lst中的元素是:10、1、100。
從容器中刪除pos位置對應的元素。pos同樣是一個(gè)迭代器(但不能是reverse_iterator)。 lst.clear(); lst.push_back(10); lst.push_back(100); list<int>::iterator iter = lst.begin(); ++iter; lst.erase(iter); 現在lst中只有一個(gè)元素:10。
3.13.6. 常量迭代器 迭代器可以“訪(fǎng)問(wèn)”到容器中的每個(gè)元素,“訪(fǎng)問(wèn)”方法即可以是“讀取”,也可以“修改”。和“常量成員函數”的思路一樣,為了更好的“封裝”效果,STL提供了一種“只讀迭代器”,類(lèi)名為“const_iterator”及反向版“const_ reverse_iterator”。對應的,“begin()/end()、rbegin()/rend()”也分別有一個(gè)“常量版”。 list<int>::const_iterator c_iter = lst.begin(); //此時(shí)調用的是常量版 int a = *c_iter; //正確 *c_iter = 1000; //錯誤!c_iter是只讀版迭代器,不允許修改它綁定的元素
![]() 請將“常用函數(一)”“常用函數(二)”、“常量迭代器”三小節中的所有示例代碼,合并到工程的main.cpp源文件中,并確保沒(méi)有編譯錯誤。
3.13.7. 遍歷list容器 list容器不允許“隨機”訪(fǎng)問(wèn),不過(guò),我們可以得到第一個(gè)元素的 “迭代器”,然后通過(guò)“迭代器”不斷前進(jìn),從而訪(fǎng)問(wèn)到每一個(gè)元素。 下面的代碼,實(shí)現將lst中的每一個(gè)整數,輸出到屏幕上。 001 for (list<int>::const_iterator c_iter = lst.begin(); 002 c_iter != lst.end(); 003 ++c_iter) { 005 cout << *c_iter << endl; } for循環(huán)頭被我故意折成三行,因為它看起來(lái)有點(diǎn)長(cháng),但其實(shí)仍然是這樣三部分: 001行是“初始化語(yǔ)句”。定義了一個(gè)“只讀迭代器”的變量:c_iter,它指向lst的第一個(gè)元素。 002行是“循環(huán)條件判斷”語(yǔ)句?!?=”是C++中用以實(shí)現“不等”判斷的操作符。本循環(huán)不斷進(jìn)行的條件就是:c_iter不等于lst的“結束迭代器”。 003行,c_iter通過(guò)++操作,實(shí)現前進(jìn)到下一元素的位置上。 如果你對這三行感覺(jué)得有些不太理解,那么你需復習一下3.12.3 小節有關(guān)如何遍歷vector的內容。 005行,輸出c_iter當前指向的元素的值。 由于list擁有“雙向迭代器”,所以這個(gè)次序也可以倒過(guò)來(lái),改成從最后一個(gè)元素開(kāi)始,“倒著(zhù)”走向第一個(gè)元素。強調一點(diǎn),對于一個(gè)逆向迭代器/ reverse_iterator,對它進(jìn)行“++”操作,就是促使它從容器的尾部往頭部“前進(jìn)”一個(gè)位置,而對其進(jìn)行“--”操作,則是促使它從容器的頭部往“后退”一個(gè)位置。 001 for (list<int>::const_reverse_iterator c_iter = lst.rbegin(); 002 c_iter != (list<int>::const_reverse_iterator) lst.rend(); ++c_iter) { cout << *c_iter << endl; }
![]() 事實(shí)上,002行中的加粗部分,本不必要。然而gcc 4.0(含4.0)以下版本存在錯誤,我們不得不加上該段代碼,用于強制告知編譯器我們要的是一個(gè)“const_reverse_iterator”,而非“reverse_iterator”。 4.1以上版本的gcc不存在該問(wèn)題,然而Code::Blocks自帶的gcc是mingw-gcc3.4.5。
變化發(fā)生在001行和002行。你喜歡玩“找碴”游戲嗎?仔細找找兩段代碼之間的不同。 ![]() 請在上次課堂作業(yè)的基礎上,加入本小節兩種方向遍歷list的練習代碼。
3.13.8. 實(shí)例:成績(jì)管理系統1.0 ![]() 夜。 風(fēng)雨夜。 白發(fā)蒼蒼的老者,敲開(kāi)丁家大門(mén)。 來(lái)者姓李,20年前丁小明的小學(xué)老師。 他帶著(zhù)厚厚的一摞試卷…… 他想做什么?
自打小丁完成了“美女大賽管理系統”之后,他就一直在想寫(xiě)一個(gè)更有意義的軟件。 現在需求來(lái)了:老師希望能夠按照試卷的次序,錄入學(xué)生成績(jì),然后依據學(xué)號的次序,輸出學(xué)生成績(jì)。小丁簡(jiǎn)單地翻了一下試卷,發(fā)現一個(gè)事實(shí):試卷的次序和學(xué)號無(wú)關(guān)。 李老師說(shuō),錄入時(shí),需要輸入學(xué)號與成績(jì);輸出時(shí),最好能同時(shí)顯示學(xué)號與姓名。 小丁的思路是:分成兩個(gè)struct,其一包含學(xué)號、姓名;其二包含學(xué)號、成績(jì)。二者之間通過(guò)“學(xué)號”構成邏輯關(guān)聯(lián): //學(xué)生 struct Student { unsigned int number; //學(xué)號 string name; }; //成績(jì) struct Score { unsigned int number; //學(xué)號 float mark; //分數 }; 學(xué)號采用“無(wú)符號整數”(0和正整數)類(lèi)型,因為學(xué)號不允許為負數,為0則有特殊用處。分數采用“float”類(lèi)型,因為存在像86.5分這樣帶小數的成績(jì)。
![]() 小丁的思路是正確的。表面看,“學(xué)生”可以擁有一或多個(gè)“成績(jì)”,但“擁有”并不一定適合在類(lèi)中加入一個(gè)相關(guān)的成員數據?!俺煽?jì)”在很多時(shí)候,可以暫時(shí)脫離“學(xué)生”而獨立進(jìn)行運算,比如本例的“錄入成績(jì)”。這種情況下,相關(guān)信息獨立定義,通過(guò)一個(gè)“關(guān)鍵值”來(lái)維系兩者的關(guān)系是個(gè)好主意。本例中,這個(gè)關(guān)鍵值就是“學(xué)號”。 再考慮一個(gè)更為極端的例子:“學(xué)生”有“家長(cháng)”,“家長(cháng)”有“工作單位”、“工作單位”有“法人代表”。顯然,在“學(xué)生”結構中含有“法人代表”很不合理,這就稱(chēng)為信息之間的過(guò)度偶合,在編程設計中,應該避免。
接著(zhù),小丁決定使用vector來(lái)保存班級里的學(xué)生。因為學(xué)生數據相對穩定。成績(jì)數據則使用list保存,每次錄入成績(jì)時(shí),根據錄入的學(xué)號,找到合適的位置插入,比如已經(jīng)錄入10、11、14、16號成績(jì),當錄入12號時(shí),自動(dòng)找到11與14之間插入。既然成績(jì)要不斷地插入,那么采用list確實(shí)很合理。
新建一個(gè)控制臺應用項目,命名為“HelloSTL_ScoreManage_Ver1”。打開(kāi)該項目下的main.cpp文件,再通過(guò)菜單:“編輯->文件編碼”設置為“系統默認”。 首先加入<list>、<vector>、<string>等頭文件的包含,以及前述兩個(gè)類(lèi)型的定義。代碼如下: #include <iostream> #include <list> #include <vector> #include <string> using namespace std; //學(xué)生 struct Student { unsigned int number; //學(xué)號 string name; }; //成績(jì) struct Score { unsigned int number; //學(xué)號 float mark; //分數 }; //此處是 main() 函數的默認實(shí)現,略…… 接著(zhù),需要一個(gè)“學(xué)生成績(jì)管理/StudentScoreManager”類(lèi)型,該類(lèi)型當前提供以下三樣功能: 第一、批量錄入學(xué)生基本信息(學(xué)號、姓名) 第二、批量錄入考試成績(jì)。 第三、以學(xué)號為次序,輸出考試成績(jì)。 考試時(shí),誰(shuí)先做完,誰(shuí)就先交卷,所以交完以后的卷子,可以看成是無(wú)序的。那么第三步如何實(shí)現呢?我們現在想到的辦法是:拿到一個(gè)學(xué)號時(shí),就在無(wú)序的list中查找這個(gè)學(xué)號的成績(jì)。 請在main()函數之前,Score類(lèi)定義之后,插入以下代碼: //學(xué)生成績(jì)管理類(lèi) class StudentScoreManager { public: void InputStudents(); void InputScores(); void OutputScores() const; private: vector<Student> students; list<Score> scores; }; OutputScores函數被聲明為“const”,因為通常一個(gè)用來(lái)“顯示”成績(jì)的功能,不應該修改類(lèi)型的任何成員數據。想想,你的老師交給你一份學(xué)生成績(jì)表,讓你將它打印,并張貼出來(lái)——在這個(gè)過(guò)程中,你的成績(jì)由30分變成80分?有這可能嗎?
InputStudents()函數將用來(lái)實(shí)現錄入學(xué)生基本信息。由于通常是對著(zhù)“花名冊”錄入,所以實(shí)現為只能按照學(xué)號次序錄入——這樣做也有一個(gè)好處:可以不用手工輸入學(xué)號了。 void StudentScoreManager::InputStudents() { 037 int number = 1; //學(xué)號從1開(kāi)始 while(true) { 041 cout << "請輸入" << number << "號學(xué)生姓名(輸入x表示結束):"; string name; getline(cin, name); 047 if (name == "x") { break; } 052 Student student; 053 student.number = number; 054 student.name = name; 056 students.push_back(student); 058 ++number; } } 這又是一個(gè)“死循環(huán)”,不過(guò)這次打破循環(huán)的方法比較有趣,041行提示用戶(hù)輸入小寫(xiě)字母‘x’表示退出。具體實(shí)現是在047選擇的if條件判斷。 程序被設計成以1號、2號、3號順序輸入學(xué)生姓名,學(xué)號從1開(kāi)始,并且在每次錄完一個(gè)學(xué)生之后,學(xué)號就自動(dòng)加1。這是037行與058兩行所完成的重要任務(wù)。 每次循環(huán),都會(huì )在052行新定義了一個(gè)Student的“棧對象”,并且在隨后053、054兩行取得必要的值,然后在056行處,被加入到vector中?!皸ο蟆眘tudent會(huì )在每次循環(huán)結束自動(dòng)消亡,然而,由于push_back()函數是先復制一份,再把復制品扔入vector,所以不必擔心每次錄入的學(xué)生對象會(huì )人間蒸發(fā)。
接下來(lái)是錄入成績(jì)的成員函數:InputScores()。 void StudentScoreManager::InputScores() { while(true) { unsigned int number; cout << "請輸入學(xué)號(輸入0表示結束):"; cin >> number; if (number == 0) { break; } //簡(jiǎn)單判斷學(xué)號是否正確: 078 if (number > students.size()) { cout << "錯誤:學(xué)號必須位于: 1 ~ " << students.size() << " 之間。" << endl; 081 continue; } float mark; cout << "請輸入該學(xué)員成績(jì):"; cin >> mark; Score score; score.number = number; score.mark = mark; 092 scores.push_back(score); //直接加在尾部 } } 錄入成績(jì)的代碼,并不比錄入學(xué)生信息復雜多少。我們耍了同樣的花招來(lái)結束一個(gè)循環(huán),只不過(guò)這一次用到學(xué)號上面:輸入0,表示結束錄入。 078行我們對用戶(hù)輸入的學(xué)號做一個(gè)簡(jiǎn)單的合法判斷。學(xué)員的總數已知是students.size(),如果用戶(hù)輸入大于學(xué)員總數的學(xué)號,被認為不合法。這里用到了continue。 092行通過(guò)調用push_back,直接將新錄入的成績(jì)添加在list的尾部,所以list聽(tīng)成績(jì)存儲并沒(méi)有按照學(xué)號排序。
我們將遍歷students中的每一個(gè)學(xué)生,輸出他的學(xué)號和姓名。然后再次通過(guò)循環(huán),在scores中找到指定學(xué)號的成績(jì),如果沒(méi)有找到,提輸出:“查無(wú)成績(jì)”。 void StudentScoreManager::OutputScores() const { 120 for (unsigned int i=0; i<students.size(); ++i) { 122 unsigned int number = students[i].number; //學(xué)號 cout << "學(xué)號:" << number << endl; cout << "姓名:" << students[i].name << endl; //查找成績(jì): 128 bool found = false; 130 for (list<Score>::const_iterator iter = scores.begin(); iter != scores.end(); ++iter) { if (iter->number == number) { found = true; //找到了 cout << "成績(jì):" << iter->mark << endl; break; } } 144 if (found ==false) //沒(méi)找到?? { cout << "成績(jì):" << "查無(wú)成績(jì)。" << endl; } } } 這段代碼有兩層for循環(huán)。 120行的for循環(huán),遍歷了每個(gè)學(xué)生。122行定義number記住當前學(xué)員的學(xué)號。隨后兩行輸出學(xué)號和姓名。 128行我們又定義了一個(gè)bool變量:found。通過(guò)130行的for循環(huán),遍歷每個(gè)成績(jì),找到指寫(xiě)學(xué)號的分數,然后輸出。如果沒(méi)有找到,則直至循環(huán)結束,found也不會(huì )被置為真。 隨后144行的判斷條件成立,屏幕輸出相應提示。
![]() 為了簡(jiǎn)化代碼,前述兩InputXXX函數對用戶(hù)非法輸入的情況僅做最簡(jiǎn)單的判斷。事實(shí)上,在要求輸入學(xué)號或者分數時(shí),如果用戶(hù)不小心輸入一些字母,比如:abc,程序就會(huì )陷入真正的死循環(huán),此時(shí)必須使用Ctrl+C來(lái)強行中斷。如果你想預防此類(lèi)錯誤,可以每次接受輸入之后,立即判斷cin.fail()是否為真,具體請參看3.12.4節的實(shí)例。
|
聯(lián)系客服