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

在咱們用C/C++開發的過程當中,老是有一個問題會給咱們帶來苦惱。這個問題就是函數內和函數外代碼須要經過一塊內存來交互(好比,函數返回字符串),這個問題困擾和不少開發人員。若是你的內存是在函數內棧上分配的,那麼這個內存會隨着函數的返回而被彈棧釋放,因此,你必定要返回一塊函數外部還有效的內存。shell

這是一個讓無數人困擾的問題。若是你一不當心,你就頗有可能在這個上面犯錯誤。固然目前有不少解決方法,若是你熟悉一些標準庫的話,你能夠看到許多各式各樣的解決方法。大致來講有下面幾種:編程

1)在函數內部經過malloc或new在堆上分配內存,而後把這塊內存返回(由於在堆上分配的內存是全局可見的)。這樣帶來的問題就是潛在的內存問題。由於,若是返回出去的內存不釋放,那麼就是memory Leak。或者是被屢次釋放,從而形成程序的crash。這兩個問題都至關的嚴重,因此這種設計方法並不推薦。(在一些Windows API中,當你調用了一些API後,你必需也要調用他的某些API來釋放這塊內存)網絡

2)讓用戶傳入一塊他本身的內存地址,而在函數中把要返回的內存放到這塊內存中。這是一個目前廣泛使用的方式。不少Windows API函數或是標準C函數都須要你傳入一個buffer和這個buffer的長度。這種方式對咱們來講應該是家常便飯了。這種方式的好處就是由函數外部的程序來維護這塊內存,比較簡顯直觀。但問題就是在使用上稍許有些麻煩。不過這種方式把犯錯誤的機率減到了最低。socket

3)第三種方式顯得比較另類,他利用了static的特性,static的棧內存一旦分配,那這塊內存不會隨着函數的返回而釋放,並且,它是全局可見的(只要你有這塊內存的地址)。因此,有一些函數使用了static的這個特性,即不用使用堆上的內存,也不須要用戶傳入一個buffer和其長度。從而,使用得本身的函數長得很漂亮,也很容易使用。函數

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

 

示例

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

char *inet_ntoa(struct in_addr in);調試

顯然,這個函數不會分配堆上的內存,而他又沒有讓你傳一下字符串的buffer進入,那麼他必定使用「返回static char[]」這種方法。在咱們繼續咱們的討論以前,讓咱們先了解一下IP地址相關的知識,下面是inet_ntoa這個函數須要傳入的參數:(也許你會很奇怪,只有一個member的struct還要放在struct中幹什麼?這應該是爲了程序往後的擴展性的考慮)code

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變量進行了討論。結果也是持否認態度的。

相關文章
相關標籤/搜索