一個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個字
最新活動更多
-
11月20日火熱報名中>> 2024 智能家居出海論壇
-
11月28日立即報名>>> 2024工程師系列—工業(yè)電子技術(shù)在線會議
-
12月19日立即報名>> 【線下會議】OFweek 2024(第九屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會
-
即日-12.26火熱報名中>> OFweek2024中國智造CIO在線峰會
-
即日-2025.8.1立即下載>> 《2024智能制造產(chǎn)業(yè)高端化、智能化、綠色化發(fā)展藍皮書》
-
精彩回顧立即查看>> 【在線會議】多物理場仿真助跑新能源汽車
推薦專題
- 1 腦機接口芯片,華為出了新專利!
- 2 今年諾獎對人工智能的重視,給我們的基礎(chǔ)教育提了個醒
- 3 銀行業(yè)AI大模型,從入局到求變
- 4 巨頭搶布局,VC狂撒錢,為了能讓「AI讀心」這些公司卷瘋了
- 5 阿斯麥ASML:“骨折級”洋相,又成AI第一殺手?
- 6 蘋果市值創(chuàng)新高,iPhone 16能否助力突破4萬億美元大關(guān)?
- 7 一場“載入史冊”的發(fā)布會,讓馬斯克失去了4700億
- 8 百度谷歌比較研究2024:中美“遠古AI龍頭”的現(xiàn)狀與趨勢
- 9 洞見AI風(fēng)潮 第二屆vivo藍河操作系統(tǒng)創(chuàng)新賽開啟招募
- 10 地平線開啟配售,阿里百度各砸5000萬美金,市值最高超500億
- 高級軟件工程師 廣東省/深圳市
- 自動化高級工程師 廣東省/深圳市
- 光器件研發(fā)工程師 福建省/福州市
- 銷售總監(jiān)(光器件) 北京市/海淀區(qū)
- 激光器高級銷售經(jīng)理 上海市/虹口區(qū)
- 光器件物理工程師 北京市/海淀區(qū)
- 激光研發(fā)工程師 北京市/昌平區(qū)
- 技術(shù)專家 廣東省/江門市
- 封裝工程師 北京市/海淀區(qū)
- 結(jié)構(gòu)工程師 廣東省/深圳市