天天看點

C語言隐式函數聲明帶來的錯誤執行個體(當隐式聲明遇到printf)1 原因分析2 終極解決方案

關于C語言隐式函數聲明的基本問題,請參見我的博文萬惡之源:C語言中的隐式函數聲明。 下面是最近遇到的一個執行個體之一。

client_sock = accept(server_sock, (struct sockaddr*)&client_name, &client_name_len);
        printf("from %s:%d\n", inet_ntoa(client_name.sin_addr), client_name.sin_port);
           

上述代碼段摘自一個網絡偵聽程式,功能就是列印出用戶端的IP位址和端口号。出現的症狀是一運作就報段錯誤(segment Fault)。

網上有很多文章提到了解決方式,但是卻沒有分析其問題産生的原因。這裡我們将徹底分析其原因,并給出解決此類問題的終極方法。

1 原因分析

inet_ntoa()這個函數的調用出了問題,是萬惡之源。在萬惡之源:C語言中的隐式函數聲明中我們說過,C語言編譯器對于沒有聲明原型的函數,通通作為傳回類型為整數的函數來處理,參數的類型則由調用的實參自動提升後确定。例如:

non_exist_function(, 'c');
           

在編譯時,這個沒有事先聲明的函數将被當作如下形式:

int non_exist_function(int,int);
           

注意,’c’(char)被提升為了int。

現在回到我們的代碼上來。

inet_ntoa(client_name.sin_addr), client_name.sin_port)
           

将會被當作:

int inet_ntoa(addr_in, unsigned short);
           

然而實際上真正的inet_ntoa的原型定義在了

extern char *inet_ntoa (struct in_addr __in);
           

這樣傳回值本來是指針類型,卻被截斷成了int類型。對于32位系統來說,由于指針類型和int類型的大小都是32位,恰好不會出現截斷的情況。這也是為什麼上述代碼在32位系統下編譯運作不會出現問題的原因。

然而到了64位系統下,char*是64位,int仍然是32位,就出現了截斷問題。

然而由于printf(char*, …)是個變參函數,是以調用它時,編譯器不會檢查可變參數的資料類型,而是按照實參類型進行準備參數入棧。相當于

printf("from %s:%d\n", , );
           

這樣訓示符%s對應的第一個參數的類型卻是int,進而導緻printf()内部在通過va_arg()提取參數時産生錯誤,最終導緻了段錯誤。

如果把上述代碼改寫為:

client_sock = accept(server_sock, (struct sockaddr*)&client_name, &client_name_len);
char* s = inet_ntoa(client_name.sin_addr);
printf("from %s:%d\n", s, client_name.sin_port);
           

編譯器就會給出警告資訊:

warning: initialization makes pointer from integer without a cast [enabled by default]
           

這樣程式員就容易發現存在的隐式函數聲明。

然而我們的實際代碼确實非常簡潔的一行代碼,導緻編譯器不會給出警告。

* 隐式函數聲明+printf()将會導緻非常隐蔽的錯誤!*

2 終極解決方案

GCC有個開關名為: -Wimplicit-function-declaration。隻要把這個開關打開就會對所有的隐式聲明函數的調用發出警告。

[[email protected] ~]$ gcc -Wimplicit-function-declaration  1.c
1.c: In function ‘main’:
.c::: warning: implicit declaration of function ‘inet_ntoa’ [-Wimplicit-function-declaration]
   printf("from %s:%d\n", inet_ntoa(client_name.sin_addr), client_name.sin_port);
           

這種警告比錯誤還嚴重!代碼一定要徹底清除這種警告。

知道了原因,解決方法異常簡單,隻要把包含函數原型聲明的頭檔案包含進來就可以了。