本節書摘來自異步社群出版社《c++面向對象高效程式設計(第2版)》一書中的第3章,第3.12節,作者: 【美】kayshav dattatri,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。
c++面向對象高效程式設計(第2版)
在設計類的接口時,要聲明類的成員函數,并指定它們的參數。類的客戶調用成員函數時,提供實參(如果有的話)。
每種方法都應清楚地指明參數的傳遞模式,參數可以按值、按引用或按指針傳遞。與const聯合使用,參數會更加安全可靠。函數的原型用于向客戶傳達這些資訊。
每個參數的傳遞模式都給客戶傳達特定的含義。再者,有時還需遵循一些經長時間驗證确實行之有效的規則。是以,為參數選擇合适的類型非常重要。在接下來的内容中,我們将介紹參數傳遞的不同樣式和它們的含義。
注意:
在接下來的示例中,術語主調函數(caller)指的是g()函數(或者main程式),它調用另一個函數f()。在這種情況下,f()就是被調函數(calle e),即g()所調用的函數。換言之,主調函數是發起轉移控制權的函數,被調函數是接受控制權的函數。
在以下所列例子中,将使用t類和x類,以及x類的成員函數f()。無需考慮t類和x類中所具體包含的内容。
<code>(1)void x::f(t arg) // 第一例,按值傳遞(pass by value)。</code>
被調函數可以對arg(原始對象的副本)進行讀取和寫入。在f()内改動arg不會影響f()的主調函數,因為主調函數已提供原始對象的副本。這也許是參數傳遞最佳和最安全的模式,主調函數和被調函數互相保護。但是,這種模式也存在缺點:要調用複制構造函數複制原始對象,再将原始對象的副本傳遞給f(),而且在退出f()時,通常還必須通過arg調用析構函數。必須記住,每次調用構造函數後,遲早都要調用析構函數銷毀對象。構造函數和析構函數的開銷很大。再者,有時複制對象操作僅限于特權客戶或被完全禁止。在這種情況下,就不能使用按值傳遞,用按引用傳遞參數會更好。另外,複制大型對象非常耗時,此時,按值傳遞參數通常不是首選的方案。f()不應該在對象(該對象調用f())的資料成員中儲存arg的位址(使用this指針),因為一旦退出函數,arg即被銷毀。
<code>(2)void x::f(const t arg) // 第二例,按值傳遞。</code>
該例和上例非常相似,仍然是按值傳遞,且它有普通按值傳遞所有的優缺點。但是,在該例中,被調函數隻能對arg進行讀取,不能寫入,因為arg被聲明為const對象。通常,主調函數對這樣的參數傳遞樣式都視而不見。實際上,主調函數并不關心被調函數如何操作它的副本。因為那隻是個副本,并不是真正的對象。const僅是被調函數對原始對象副本施加的額外限制。
<code>(3)void x::f(t& arg) // 第一例,按引用傳遞(pass by reference)。</code>
除非有其他的說明,否則,該例意味着被調函數可對arg進行讀出和寫入。換言之,arg是一個輸入輸出形參(in-out parameter)。被調函數可以修改真正的對象,也就是說,f()可以在需要時從arg中讀取輸入形參,然後再将結果寫回arg中。如果确實打算這樣操作,要在注釋中清楚地說明。注意,arg屬于主調函數,f()不會銷毀它。
另一方面,我們可能打算把arg作為隻輸出形參(out-only parameter)使用(即被調函數可将結果值寫入arg中,但不能從中讀取值)。編譯器無法強制執行這個規則。通常,在這種情況下,arg是一個未初始化的對象,僅用于傳回值(隻是一個輸出形參)。主調函數建立一個空對象(可能使用預設構造函數),并将其傳遞給f(),f()把傳回值寫入arg中。需要更詳細的文檔才能清楚地說明該意圖。如果打算把arg作為輸出形參使用,那麼,最好讓這樣的函數都遵循一種不同的命名約定(如函數名字首copy)。在主調函數已標明儲存格式,被調函數隻負責填充原始對象的情況下,使用引用作為輸出形參是不錯的方案。通常這種情況要用到繼承層次,我們将在後續章節中介紹。記住,arg屬于主調函數。按引用傳遞參數保證了參數是活的對象,它不像空指針那樣。這也保證了引用的對象在f()調用的生存期内一直存在。不要在真正需要使用對象的地方,使用指向t的指針。
警告:
通常認為,無論何時傳遞引用參數,被調函數都不應該儲存arg的位址。因為在退出函數後,無法保證arg還存在。(3)說明,f()應假設arg的生存期受限于f()的作用域内。想象一下,如果f()将arg的位址儲存在它的對象(該對象調用f())的一個資料成員中,稍後試圖通過已儲存的位址使用arg。在此之前,arg可能已經在主調函數的作用域内被銷毀了。主調函數不會保證arg的生存期!進行任何類似的操作一定會導緻程式崩潰(或引起無法預料的行為和難以追蹤的潛在程式錯誤)。以上的分析并不是說f()不應該擷取arg的位址,擷取位址沒錯,我們在指派操作符中就要擷取位址。但是,不要在任何資料成員中儲存該位址。
線程安全
在多線程環境中,主調函數必須保證arg在f()函數的整個生存期内都存在。如果某線程調用一個帶引用形參的函數,在f()調用完成之前,如果其他的線程被排程執行,且該程序銷毀了傳遞給f()的對象,情況會變得非常糟糕。調試這樣的代碼簡直是一場噩夢。
hand 絕不要對隻輸入形參(in-only parameter)使用按引用傳遞(無const限定符)模式。
<code>(4)void x::f(const t& arg) // 第二例,按引用傳遞。</code>
此例優于(3),被調函數對arg隻能讀取不能寫入。因為arg是對const對象的引用,它是一個隻輸入形參(in-only parameter)。在傳遞大型對象時,此傳遞樣式為高效之道,強烈推薦使用。在按值傳遞不可用時,盡可能地使用該樣式。和(3)一樣,該例也意味着被調函數不能儲存arg的位址,因為無法保證arg在函數傳回後仍存在。在(3)和(4)中,f()函數可能也想制作一份arg的副本,但主調函數不允許這樣做。要在文檔中清楚地說明主調函數的意圖。
<code>(5)void x::f(t* argp) // 第一例,按指針傳遞。</code>
c程式員鐘愛指針。大多數時候,他們在c中使用指針合情合理,因為c并沒有引用的概念。然而,在c++中,如果不能清楚地了解指針的意圖,會讓情況變得很糟。無論何時,使用指針的好處是:可以用一個特别的值—— 0(也稱為null),差別合法指針和非法指針。引用無此特點,無法差別合法引用和非法引用。實際上,正确使用引用時,絕不會出現對不存在對象的引用。
這種情況有些含糊不清,而且有潛在的不安全隐患。如果被調函數僅對argp所指向的對象寫入,那麼,它隻是一個輸出形參,argp的名稱前應綴有out。甚至,如果此函數也能遵循不同的命名約定會更好(例如,函數名字首copy)。主調函數必須把可修改對象的位址傳遞給f()。傳遞null指針非常容易(有意或無意地)。這意味着,被調函數不能假定argp所指向對象存在。指針argp本身按值傳遞,這表明f()既不能建立新對象也不能儲存argp中的位址,這樣,主調函數才能檢索到該位址。如果确實希望讓f()改變argp所持的位址,應傳遞對argp指針的引用1。argp也可以被當做輸入輸出形參,在這種情況下,被調函數能讀取argp所指向的對象,還能為其填入傳回值。如果傳入的是一個空指針就不能這樣做。
如果确實想使用這種模式,最好是讓argp帶預設值0,如果将接口改變為:
<code>void x::f(t* argp = 0)</code>
就相當于明确地告知客戶,即使将零指針(zero point)傳遞給這個函數,也不會引起任何無法預料的行為。
1該聲明應該是f(<code>t* & argp</code>)。如果你對此感興趣,确定自己能了解這樣的文法和如此複雜聲明的含義。
本文僅用于學習和交流目的,不代表異步社群觀點。非商業轉載請注明作譯者、出處,并保留本文的原始連結。