一個printf(結(jié)構(gòu)體指針)引發(fā)的血案
編譯、測試,打印結(jié)果如下:
打印結(jié)果符合預(yù)期!也就是說分成兩條打印語句是可以正確讀取到目標(biāo)地址里的 int 型數(shù)據(jù)的,但是在一條語句里就不行!
其實(shí)此時,可以判斷出大概是 printf 語句的原因了。從現(xiàn)象上看,似乎是 printf 語句在執(zhí)行過程中打印第一個數(shù)字之后,影響到了指針 p 的值,但是具體是怎么影響的說不清楚,而且它是系統(tǒng)里的庫函數(shù),肯定不能改變 p 的值。
于是在 google 中搜索關(guān)鍵字:"glibc printf bug",你還別說,真的搜索到很多相關(guān)資料,但是瀏覽了一下,沒有與我們的測試代碼類似的情況,還得繼續(xù)思考。
3. 一步步分析問題本質(zhì)原因3.1 打印一個最簡單的字符串
既然是因?yàn)樵?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)體變量來測試,因?yàn)樯厦娴臏y試代碼是結(jié)構(gòu)體變量的數(shù)組,現(xiàn)在我們把數(shù)組的影響去掉,只對單獨(dú)的一個結(jié)構(gòu)體變量進(jìn)行測試:
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。感興趣的小伙伴可以打開瀏覽一下,其中有下面這兩段話說明了重點(diǎn):
一句話總結(jié):用 printf 語句來打印結(jié)構(gòu)體類型的變量,結(jié)果是 undefined behavior!什么是未定義行為,就是說發(fā)生任何狀況都是可能的,這個就要看編譯器的實(shí)現(xiàn)方式了。
看來,我已經(jīng)找到問題的原因了:原來是因?yàn)槲业闹R不夠扎實(shí),不知道打印結(jié)構(gòu)體變量是未定義行為。
補(bǔ)充一點(diǎn)心得:
我們在寫程序的時候,因?yàn)槟X袋中掌握的大部分知識都是正確的,因此編寫的代碼大部分也都是與預(yù)期符合的,不可能故意去寫一些稀奇古怪的代碼。就比如打印結(jié)構(gòu)體信息,一般正常的思路都是把結(jié)構(gòu)體里面的成員變量,按照對應(yīng)的數(shù)據(jù)類型來打印輸出。但是偶爾也會犯低級錯誤,就像這次遇到的問題一樣:直接打印一個結(jié)構(gòu)體變量。因?yàn)榘l(fā)生錯誤了,所以才了解到原來直接打印結(jié)構(gòu)體變量,是一個未定義行為。當(dāng)然了,這也是一個獲取知識的途徑。
追查到這里,似乎可以結(jié)束了。但是我還是有點(diǎn)不死心,既然是未定義的行為,那么為什么每次打印輸出的結(jié)果都錯的這么一致呢?既然是由編譯器的實(shí)現(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ù)。
那就來驗(yàn)證這個想法是否正確,測試代碼如下:
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í)如此:數(shù)字 1 之后的內(nèi)存中存放的是 3 個字符 'a',第二個打印數(shù)據(jù)格式是 %x,所以就按照整型數(shù)據(jù)來讀取,于是得到十六進(jìn)制的616161。
至此,我們也知道了 gcc 這個版本中,是如何來操作這個 “undefined behavior” 的。但是事情好像還沒有結(jié)束,我們都知道:在調(diào)用系統(tǒng)中的 printf 語句時,傳入的參數(shù)個數(shù)和類型不是固定的,那么 printf 中是如何來動態(tài)偵測參數(shù)的個數(shù)和類型的呢?
四、C語言中的可變參數(shù)
在 C 語言中實(shí)現(xiàn)可變參數(shù)需要用到這下面這幾個數(shù)據(jù)類型和函數(shù)(其實(shí)是宏定義):
va_listva_startva_argva_end
處理動態(tài)參數(shù)的過程是下面這 4 個步驟:
定義一個變量 va_list arg;調(diào)用 va_start 來初始化 arg 變量,傳入的第二個參數(shù)是可變參數(shù)(三個點(diǎn))前面的那個變量;使用 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 變量。
文字表達(dá)起來好像有點(diǎn)抽象、復(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個字
最新活動更多
-
6月20日立即下載>> 【白皮書】精準(zhǔn)測量 安全高效——福祿克光伏行業(yè)解決方案
-
7月3日立即報名>> 【在線會議】英飛凌新一代智能照明方案賦能綠色建筑與工業(yè)互聯(lián)
-
7月22-29日立即報名>> 【線下論壇】第三屆安富利汽車生態(tài)圈峰會
-
7.30-8.1火熱報名中>> 全數(shù)會2025(第六屆)機(jī)器人及智能工廠展
-
7月31日免費(fèi)預(yù)約>> OFweek 2025具身機(jī)器人動力電池技術(shù)應(yīng)用大會
-
免費(fèi)參會立即報名>> 7月30日- 8月1日 2025全數(shù)會工業(yè)芯片與傳感儀表展
推薦專題
- 1 AI 眼鏡讓百萬 APP「集體失業(yè)」?
- 2 大廠紛紛入局,百度、阿里、字節(jié)搶奪Agent話語權(quán)
- 3 深度報告|中國AI產(chǎn)業(yè)正在崛起成全球力量,市場潛力和關(guān)鍵挑戰(zhàn)有哪些?
- 4 上海跑出80億超級獨(dú)角獸:獲上市公司戰(zhàn)投,干人形機(jī)器人
- 5 國家數(shù)據(jù)局局長劉烈宏調(diào)研格創(chuàng)東智
- 6 一文看懂視覺語言動作模型(VLA)及其應(yīng)用
- 7 下一代入口之戰(zhàn):大廠為何紛紛押注智能體?
- 8 百億AI芯片訂單,瘋狂傾銷中東?
- 9 Robotaxi新消息密集釋放,量產(chǎn)元年誰在領(lǐng)跑?
- 10 格斗大賽出圈!人形機(jī)器人致命短板曝光:頭腦過于簡單