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

發(fā)表評(píng)論
請(qǐng)輸入評(píng)論內(nèi)容...
請(qǐng)輸入評(píng)論/評(píng)論長度6~500個(gè)字
最新活動(dòng)更多
-
3月27日立即報(bào)名>> 【工程師系列】汽車電子技術(shù)在線大會(huì)
-
4月30日立即下載>> 【村田汽車】汽車E/E架構(gòu)革新中,新智能座艙挑戰(zhàn)的解決方案
-
5月15-17日立即預(yù)約>> 【線下巡回】2025年STM32峰會(huì)
-
即日-5.15立即報(bào)名>>> 【在線會(huì)議】安森美Hyperlux™ ID系列引領(lǐng)iToF技術(shù)革新
-
5月15日立即下載>> 【白皮書】精確和高效地表征3000V/20A功率器件應(yīng)用指南
-
5月16日立即參評(píng) >> 【評(píng)選啟動(dòng)】維科杯·OFweek 2025(第十屆)人工智能行業(yè)年度評(píng)選
推薦專題
- 1 UALink規(guī)范發(fā)布:挑戰(zhàn)英偉達(dá)AI統(tǒng)治的開始
- 2 北電數(shù)智主辦酒仙橋論壇,探索AI產(chǎn)業(yè)發(fā)展新路徑
- 3 “AI寒武紀(jì)”爆發(fā)至今,五類新物種登上歷史舞臺(tái)
- 4 降薪、加班、裁員三重暴擊,“AI四小龍”已折戟兩家
- 5 國產(chǎn)智駕迎戰(zhàn)特斯拉FSD,AI含量差幾何?
- 6 光計(jì)算迎來商業(yè)化突破,但落地仍需時(shí)間
- 7 東陽光:2024年扭虧、一季度凈利大增,液冷疊加具身智能打開成長空間
- 8 地平線自動(dòng)駕駛方案解讀
- 9 封殺AI“照騙”,“淘寶們”終于不忍了?
- 10 優(yōu)必選:營收大增主靠小件,虧損繼續(xù)又逢關(guān)稅,能否乘機(jī)器人東風(fēng)翻身?