Effective STL條款16

條款16: 如何將vector和string的數據傳給傳統的API

因爲 C++語言已經於1998年被標準化,C++的中堅分子在努力推動程序員從數組轉到vector時就沒什麼顧慮了。同樣顯然的情況也發生於嘗試使開發者從char*指針遷移到string對象的過程中。有很好的理由來做這些轉變,包括可以消除常見的編程錯誤(參見條款13),而且有機會獲得STL算法的全部強大能力(比如參見條款31)。

但是,障礙還是有的,最常見的一個就是已經存在的傳統C風格的API接受的是數組和char*指針,而不是vector和string對象。這樣的API函數還將會存在很長時間,如果我們要高效使用STL的話,就必須和它們和平共處。

幸運的是,這很容易。如果你有一個vector對象v,而你需要得到一個指向v中數據的指針,以使得它可以被當作一個數組,只要使用&v[0]就可以了。對於string對象s,相應的咒語是簡單的s.c_str()。但是是隻讀的。如廣告中難懂的條文時常指出的,必然會有幾個限制。

給定一個


表達式v[0]生產一個指向vector中第一個元素的引用,所以,&v[0]是指向那個首元素的指針。vector中的元素被C++標準限定爲存儲在連續內存中,就像是一個數組,所以,如果我們想要傳遞v給這樣的C風格的API:


我們可以這麼做:


也許吧。可能吧。唯一的問題就是,如果v是空的。如果這樣的話,v.size()是0,而&v[0]試圖產生一個指向根本就不存在的東西的指針。這不是件好事。其結果未定義。一個較安全的方法是這樣:


如果走錯路了,你可能會碰到一些半吊子的人物,他們會告訴你說可以用v.begin()代替&v[0],因爲(這些討厭的傢伙將會告訴你)begin返回指向vector內部的迭代器,而對於vector,其迭代器實際上是指針。那經常是正確的,但正如條款50所說,並不總是如此,你不該依賴於此。begin的返回類型是iterator,而不是一個指針,當你需要一個指向vector內部數據的指針時絕不該使用begin。如果你基於某些原因決定鍵入v.begin(),就應該鍵入&*v.begin(),因爲這將會產生和&v[0]相同的指針,這樣可以讓你有更多的打字機會而且讓其他要弄懂你代碼得人感覺到更晦澀。坦白地說,如果你正在和告訴你使用v.begin()代替&v[0]的人打交道的話,你該重新考慮一下你的社交圈了。(譯註:在VC6中,如果用v.begin()代替&v[0],編譯器不會說什麼,但在VC7和GCC中這麼做的話,就會引發一個編譯錯誤)

類似從vector上獲取指向內部數據的指針的方法,對string不是可靠的,因爲(1)string中的數據並沒有承諾被存儲在連續內存中,(2)string的內部表示形式並沒承諾以一個null字符結束。這解釋了string的成員函數c_str存在的原因,它返回一個按C風格設計指針,指向string的值。因此我們可以這樣傳遞一個string對象s給這個函數,


象這樣:


即使是字符串的長度爲0,它都能工作。在那種情況下,c_str將返回一個指向null字符的指針。即使字符串內部自己內含null時,它同樣能工作。但是,如果真的這樣,doSomething很可能將第一個內含的null解釋爲字符串結束。string對象不在意是否容納了結束符,但基於char*的C風格API在意。

再看一下doSomething的聲明:


在兩種形式下,指針都被傳遞爲指向const的指針。vector和string的數據只能傳給只讀取而不修改它的API。這到目前爲止都是最安全的事情。對於string,這也是唯一可做的,因爲沒有承諾說c_str產生的指針指在string數據的內部表示形式上;它可以返回一個指針指向數據的一個不可修改的拷貝,這個拷貝滿足C風格API對格式的要求。(如果這個恐嚇令你毛骨悚然的話,還請放心吧,因爲它也許不成立。我沒聽說目前哪個庫的實現使用了這個自由權的。)

對於vector,有更多一點點靈活性。如果你將v傳給一個修改其元素的C風格API的話,典型情況都是沒問題,但被調用的函數絕不能試圖改變vector中元素的個數。比如,它絕不能試圖在vector還未使用的容量上“創建”新的元素。如果這麼幹了,v將會變得內部狀態不一致,因爲它再也不知道自己的正確大小了。v.size()將會得到一個不正確的結果。並且,如果被調用的函數試圖在一個大小和容量(參見條款14)相等的vector上追加數據的話,真的會發生災難性事件。我甚至根本就不願去想象它。實在太可怕了。

你注意到我在前面的“典型情況都是沒問題”那句話用的是“典型地”一詞嗎?你當然注意到了。有些vector對其數據有些額外的限制,而如果你把一個vector傳遞給需要修改vector數據的API,你一定要確保這些額外限制繼續被滿足。舉個例子,條款23解釋了排序的vector往往是一種可行的選擇來實現關聯容器,但對這些vector而言,保持順序非常重要。如果你將一個排序的vector傳給一個可能修改其數據的API函數,你需要重視vector在調用返回後不再保持順序的情況。

如果你想用C風格API返回的元素初始化一個vector,你可以利用vector和數組潛在的內存分佈兼容性將存儲vecotr的元素的空間傳給API函數:


這個技巧只能工作於vector,因爲只有vector承諾了與數組具有相同的潛在內存分佈。但是,如果你想用來自C風格API的數據初始化string對象,也很簡單。只要讓API將數據放入一個vector<char>,然後從vector中將數據拷到string:


事實上,讓C風格API將數據放入一個vector,然後拷到你實際想要的STL容器中的主意總是有效的:


此外,這也提示了vector和string以外的STL容器如何將它們的數據傳給C風格API。只要將容器的每個數據拷到vector,然後將它們傳給API:


你也可以將數據拷進一個數組,然後將數組傳給C風格的API,但你爲什麼想這樣做?除非你在編譯期就知道容器的大小,否則你不得不分配動態數組,而條款13解釋了爲什麼你應該總是使用vector來取代動態分配的數組。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章