一個printf(結(jié)構(gòu)體指針)引發(fā)的血案
編譯、測試,打印結(jié)果如下:
打印結(jié)果符合預(yù)期!也就是說分成兩條打印語句是可以正確讀取到目標(biāo)地址里的 int 型數(shù)據(jù)的,但是在一條語句里就不行!
其實此時,可以判斷出大概是 printf 語句的原因了。從現(xiàn)象上看,似乎是 printf 語句在執(zhí)行過程中打印第一個數(shù)字之后,影響到了指針 p 的值,但是具體是怎么影響的說不清楚,而且它是系統(tǒng)里的庫函數(shù),肯定不能改變 p 的值。
于是在 google 中搜索關(guān)鍵字:"glibc printf bug",你還別說,真的搜索到很多相關(guān)資料,但是瀏覽了一下,沒有與我們的測試代碼類似的情況,還得繼續(xù)思考。
3. 一步步分析問題本質(zhì)原因3.1 打印一個最簡單的字符串
既然是因為在 printf 語句中打印 2 個數(shù)據(jù)才出現(xiàn)問題,那么我就把問題簡化,用一個最簡單的字符串來測試,代碼如下:
char aa[] = "abcd";char *pc = aa;printf("%d, %d ", *pc, *pc);
編譯、執(zhí)行,打印結(jié)果為:"97, 97",非常正確!這就說明 printf 語句在執(zhí)行時沒有改變指針變量的指向地址。
3.2 打印一個結(jié)構(gòu)體變量
既然在字符串上測試沒有問題,那么問題就出在結(jié)構(gòu)體類型上了。那就繼續(xù)用結(jié)構(gòu)體變量來測試,因為上面的測試代碼是結(jié)構(gòu)體變量的數(shù)組,現(xiàn)在我們把數(shù)組的影響去掉,只對單獨的一個結(jié)構(gòu)體變量進行測試:
Student s = {1, "a"};
printf("%d ", s);
printf("%d, %d ", s, s);
注意:這里的 s 是一個變量,不是數(shù)組了,所以打印時就不需要用 * 操作符了。編譯、執(zhí)行,輸出結(jié)果:
輸出結(jié)果與之前的錯誤一樣,至此可以得出結(jié)論:問題的原因至少與數(shù)組是沒有關(guān)系的!
現(xiàn)在測試的結(jié)構(gòu)體中有 2 個變量:age 和 name,我們繼續(xù)簡化,只保留 int 型數(shù)據(jù),這樣更容易簡化問題。
3.3 測試更簡單的結(jié)構(gòu)體變量
測試代碼如下:
typedef struct _A{ int a; int b; int c;}A;
int main(){ A a = {10, 20, 30}; printf("%d %d %d ", a, a, a);}
編譯、執(zhí)行,打印結(jié)果為:10 20 30,把 3 個成員變量的值都打印出來了,太詭異了!好像是在內(nèi)存中,從第一個成員變量開始,自動遞增然后獲取 int 型數(shù)據(jù)。
于是我就把后面的兩個參數(shù) a 去掉,測試如下代碼:
A a = {10, 20, 30};printf("%d %d %d ", a);
編譯、執(zhí)行,打印結(jié)果仍然為:10 20 30!這個時候我快瘋掉了,主要是時間太晚了,我不太喜歡熬夜。
于是大腦開始偷懶,再次向 google 尋求幫助,還真的找到這個網(wǎng)頁:https://stackoverflow.com/questions/26525394/use-printfs-to-print-a-struct-the-structs-first-variable-type-is-char。感興趣的小伙伴可以打開瀏覽一下,其中有下面這兩段話說明了重點:
一句話總結(jié):用 printf 語句來打印結(jié)構(gòu)體類型的變量,結(jié)果是 undefined behavior!什么是未定義行為,就是說發(fā)生任何狀況都是可能的,這個就要看編譯器的實現(xiàn)方式了。
看來,我已經(jīng)找到問題的原因了:原來是因為我的知識不夠扎實,不知道打印結(jié)構(gòu)體變量是未定義行為。
補充一點心得:
我們在寫程序的時候,因為腦袋中掌握的大部分知識都是正確的,因此編寫的代碼大部分也都是與預(yù)期符合的,不可能故意去寫一些稀奇古怪的代碼。就比如打印結(jié)構(gòu)體信息,一般正常的思路都是把結(jié)構(gòu)體里面的成員變量,按照對應(yīng)的數(shù)據(jù)類型來打印輸出。但是偶爾也會犯低級錯誤,就像這次遇到的問題一樣:直接打印一個結(jié)構(gòu)體變量。因為發(fā)生錯誤了,所以才了解到原來直接打印結(jié)構(gòu)體變量,是一個未定義行為。當(dāng)然了,這也是一個獲取知識的途徑。
追查到這里,似乎可以結(jié)束了。但是我還是有點不死心,既然是未定義的行為,那么為什么每次打印輸出的結(jié)果都錯的這么一致呢?既然是由編譯器的實現(xiàn)決定的,那么我使用的這個 gcc 版本內(nèi)部是怎么來打印結(jié)構(gòu)體變量的呢?
于是我繼續(xù)往下查...
3.4 繼續(xù)打印結(jié)構(gòu)體變量
剛才的結(jié)構(gòu)體 A 中的成員都是 int 型,每個 int 數(shù)據(jù)在內(nèi)存中占據(jù) 4 個字節(jié),所以剛才打印出的數(shù)據(jù)恰好是跨過 4 個字節(jié)。如果改成字符串型,打印時是否也會跨過4個字節(jié),于是把測試代碼改成下面這樣:
typedef struct _B{ int a; char b[12];}B;
int main(){ B b = {10, "abcdefgh"}; printf("%d %c %c ", b);}
編譯、執(zhí)行,打印結(jié)果如下:
果然如此:字符 a 與數(shù)字 10 之間跨過 4 個直接,字符 e 與 a 之間也是跨過 4 個字節(jié)。那就說明 printf 語句在執(zhí)行時可能是按照 int 型的數(shù)據(jù)大。4個字節(jié))為單位,來跨越內(nèi)存空間,然后再按照百分號%后面的字符來讀取內(nèi)存地址里的數(shù)據(jù)。
那就來驗證這個想法是否正確,測試代碼如下:
Student s = {1, "aaa"};char *pTmp = &s;for (int i = 0;i < sizeof(Student); i++){ printf("%x ", *(pTmp + i));}
printf("");printf("%d, %x ", s);
編譯、執(zhí)行,打印結(jié)果為:
輸出結(jié)果確實如此:數(shù)字 1 之后的內(nèi)存中存放的是 3 個字符 'a',第二個打印數(shù)據(jù)格式是 %x,所以就按照整型數(shù)據(jù)來讀取,于是得到十六進制的616161。
至此,我們也知道了 gcc 這個版本中,是如何來操作這個 “undefined behavior” 的。但是事情好像還沒有結(jié)束,我們都知道:在調(diào)用系統(tǒng)中的 printf 語句時,傳入的參數(shù)個數(shù)和類型不是固定的,那么 printf 中是如何來動態(tài)偵測參數(shù)的個數(shù)和類型的呢?
四、C語言中的可變參數(shù)
在 C 語言中實現(xiàn)可變參數(shù)需要用到這下面這幾個數(shù)據(jù)類型和函數(shù)(其實是宏定義):
va_listva_startva_argva_end
處理動態(tài)參數(shù)的過程是下面這 4 個步驟:
定義一個變量 va_list arg;調(diào)用 va_start 來初始化 arg 變量,傳入的第二個參數(shù)是可變參數(shù)(三個點)前面的那個變量;使用 va_arg 函數(shù)提取可變參數(shù):循環(huán)從 arg 中提取每一個變量,最后一個參數(shù)用來指定提取的數(shù)據(jù)類型。比如:如果格式化字符串是 %d,那么就從可變參數(shù)中提取一個 int 型的數(shù)據(jù),如果格式化字符串是 %c,就從可變參數(shù)中提取一個 char 型數(shù)據(jù);數(shù)據(jù)處理結(jié)束后,使用 va_end 來釋放 arg 變量。
文字表達起來好像有點抽象、復(fù)雜,先看一下下面的 3 個示例,然后再回頭看一下上面這 4 個步驟,就容易理解了。
1. 利用可變參數(shù)的三個函數(shù)示例示例1:參數(shù)類型是 int,但是參數(shù)個數(shù)不固定#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <stdarg.h>
void my_printf_int(int num,...){ int i, val; va_list arg; va_start(arg, num); for(i = 0; i < num; i++) { val = va_arg(arg, int); printf("%d ", val); } va_end(arg); printf("");}
int main(){ int a = 1, b = 2, c = 3; my_printf_int(3, a, b, c);}
編譯、執(zhí)行,打印結(jié)果如下:
示例2:參數(shù)類型是 float,但是參數(shù)個數(shù)不固定#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <stdarg.h>
void my_printf_float (int n, ...){ int i; double val; va_list vl; va_start(vl,n); for (i = 0; i < n; i++) { val = va_arg(vl, double); printf ("%.2f ",val); } va_end(vl); printf ("");}
int main(){ float f1 = 3.14159, f2 = 2.71828, f3 = 1.41421; my_printf_float (3, f1, f2, f3);}

請輸入評論內(nèi)容...
請輸入評論/評論長度6~500個字
最新活動更多
-
即日-9.1立即下載>> 【限時下載】ADI中國三十周年感恩回饋助力企業(yè)升級!
-
即日-9.16點擊進入 >> 【限時福利】TE 2025國際物聯(lián)網(wǎng)展·深圳站
-
10月23日立即報名>> Works With 開發(fā)者大會深圳站
-
10月24日立即參評>> 【評選】維科杯·OFweek 2025(第十屆)物聯(lián)網(wǎng)行業(yè)年度評選
-
11月27日立即報名>> 【工程師系列】汽車電子技術(shù)在線大會
-
12月18日立即報名>> 【線下會議】OFweek 2025(第十屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會
推薦專題
- 1 阿里首位程序員,“掃地僧”多隆已離職
- 2 先進算力新選擇 | 2025華為算力場景發(fā)布會暨北京xPN伙伴大會成功舉辦
- 3 宇樹機器人撞人事件的深度剖析:六維力傳感器如何成為人機安全的關(guān)鍵屏障
- 4 清華跑出具身智能獨角獸:給機器人安上眼睛和大腦,融資近20億
- 5 特朗普要求英特爾首位華人 CEO 辭職
- 6 踢館大廠和微軟,剖析WPS靈犀的AI實用主義
- 7 騰訊 Q2 財報亮眼:AI 已成第二增長曲線
- 8 谷歌吹響AI沖鋒號,AI還有哪些機會
- 9 蘋果把身家押在Siri上:一場輸不起的自我革命
- 10 共探合作新機遇!江門市新會區(qū)(深圳)“AI + 機器人” 產(chǎn)業(yè)對接會成功舉辦