C/C++返回內部靜態成員的陷阱

來源:互聯網
上載者:User
C/C++返回內部靜態成員的陷阱

陳皓

背景

在我們用 C/C++開發的過程中,總是有一個問題會給我們帶來苦惱。這個問題就是函數內和函數外代碼需要通過一塊記憶體來互動(比如,函數返回字串),這個問題困 擾和很多開發人員。如果你的記憶體是在函數內棧上分配的,那麼這個記憶體會隨著函數的返回而被彈棧釋放,所以,你一定要返回一塊函數外部還有效記憶體。

這是一個讓無數人困擾的問題。如果你一不小心,你就很有可能在這個上面犯錯誤。當然目前有很多解決方案,如果你熟悉一些標準庫的話,你可以看到許多各式各樣的解決方案。大體來說有下面幾種:

1) 在函數內部通過malloc或new在堆上分配記憶體,然後把這塊記憶體返回(因為在堆上分配的記憶體是全域可見的)。這樣帶來的問題就是潛在的記憶體問題。因 為,如果返回出去的記憶體不釋放,那麼就是memory Leak。或者是被多次釋放,從而造成程式的crash。這兩個問題都相當的嚴重,所以這種設計方法並不推薦。(在一些Windows API中,當你調用了一些API後,你必需也要調用他的某些API來釋放這塊記憶體)

2)讓使用者傳入一塊他自己的記憶體位址,而在函數中把要 返回的記憶體放到這塊記憶體中。這是一個目前普遍使用的方式。很多Windows API函數或是標準C函數都需要你傳入一個buffer和這個buffer的長度。這種方式對我們來說應該是屢見不鮮了。這種方式的好處就是由函數外部的 程式來維護這塊記憶體,比較簡顯直觀。但問題就是在使用上稍許有些麻煩。不過這種方式把犯錯誤的機率減到了最低。

3)第三種方式顯得比較另 類,他利用了static的特性,static的棧記憶體一旦分配,那這塊記憶體不會隨著函數的返回而釋放,而且,它是全域可見的(只要你有這塊記憶體的地 址)。所以,有一些函數使用了static的這個特性,即不用使用堆上的記憶體,也不需要使用者傳入一個buffer和其長度。從而,使用得自己的函數長得很 漂亮,也很容易使用。

這裡,我想對第三個方法進行一些討論。使用static記憶體這個方法看似不錯,但是它有讓你想象不到的陷阱。讓我們來用一個實際發生的案例來舉一個例子吧。

樣本

有過socket編程經驗的人一定知道一個函數叫:inet_ntoa,這個函數主要的功能是把一個數字型的IP地址轉成字串,這個函數的定義是這樣的(注意它的傳回值):

char *inet_ntoa(struct in_addr in);

顯然,這個函數不會分配堆上的記憶體,而他又沒有讓你傳一下字串的buffer進入,那 麼他一定使用“返回static char[]”這種方法。在我們繼續我們的討論之前,讓我們先瞭解一下IP地址相關的知識,下面是inet_ntoa這個函數需要傳入的參數:(也許你會 很奇怪,只有一個member的struct還要放在struct中幹什嗎?這應該是為了程式日後的擴充性的考慮)

        struct in_addr {
                unsigned long int s_addr;
        }

對 於IPV4來說,一個IP地址由四個8位的bit組成,其放在s_addr中,高位在後,這是為了方便網路傳輸。如果你得到的一個s_addr的整型值 是:3776385196。那麼,開啟你的Windows計算機吧,看看它的二進位是什嗎?讓我們從右至左,8位為一組(如下所示)。

11100001   00010111    00010000    10101100

再把每一組轉成十進位,於是我們就得到:225   23   16   172, 於是IP地址就是 172.16.23.225。

好了,言歸正傳。我們有這樣一個程式,想記錄網路包的源地址和目地地址,於是,我們有如下的代碼:

    struct in_addr src, des;
    ........
    ........
    fprintf(fp, "源IP地址<%s>"t  目的IP地址<%s>"n", inet_ntoa(src),   inet_ntoa(des));

會 發生什麼樣的結果呢?你會發現記錄到檔案中的源IP地址和目的IP地址完全一樣。這是什麼問題呢?於是你開始調試你的程式,你發現src.s_addr和 des.s_addr根本不一樣(如下所示)。可為什麼輸出到檔案的源和目的都是一樣的?難道說是inet_ntoa的bug?

    src.s_addr = 3776385196;    //對應於172.16.23.225
    des.s_addr = 1678184620;  //對應於172.16.7.100

原 因就是inet_ntoa()“自作聰明”地把內部的static char[]返回了,而我們的程式正是踩中了這個陷阱。讓我們來分析一下fprintf代碼。在我們fprintf時,編譯器先計算 inet_ntoa(des),於是其返回一個字串的地址,然後程式再去求inet_ntoa(src)運算式,又得到一個字串的地址。這兩個字串 的地址都是inet_ntoa()中那個static char[],顯然是同一個地址,而第二次求src的IP時,這個值的des的IP地址內容必將被src的IP覆蓋。所以,這兩個運算式的字串記憶體都是 一樣的了,此時,程式會調用fprintf把這兩個字串(其實是一個)輸出到檔案。所以,得到相同的結果也就不奇怪。

仔細看一下 inet_ntoa的man,我們可以看到這句話:The string is returned in a statically allocated buffer,  which  subsequent calls will overwrite. 證實了我們的分析。

小結

讓我們大家都捫心自問一下,我們在寫程式的過程當中是否使用了這種方法?這是一個比較危險,容易出錯的方法。這種陷阱讓人防不勝防。想想,如果你有這樣的程式:

    if ( strcmp( inet_ntoa(ip1), inet_ntoa(ip2) )==0 ) {
        .... ....
   }

本想判斷一下兩個IP地址是否一樣,卻不料掉入了那個陷阱——讓這個條件運算式永真。

這個事情告訴我們下面幾個道理:

1)慎用這種方式的設計。返回函數內部的static記憶體有很大的陷阱。
2)如果一定要使用這種方式的話。你就必須嚴肅地告訴所有使用這個函數的人,千萬不要在一個運算式中多次使用這個函數。而且,還要告訴他們,不copy函數返回的記憶體的內容,而只是儲存返回的記憶體位址或是引用是沒用的。不然的話,後果概不負責。
3)C/C++是很危險的世界,如果你不清楚他的話。還是回火星去吧。

附:看過Efftive C++的朋友一定知道其中有一個條款(item 23):不要試圖返回對象的引用。這個條款中也對是否返回函數內部的static變數進行了討論。結果也是持否定態度的。

(轉載時請註明作者和出處。未經許可,請勿用於商業用途)

更多文章請訪問我的Blog: http://blog.csdn.net/haoel

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.