訂閱
糾錯
加入自媒體

一個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);}

<上一頁  1  2  3  下一頁>  
聲明: 本文由入駐維科號的作者撰寫,觀點僅代表作者本人,不代表OFweek立場。如有侵權(quán)或其他問題,請聯(lián)系舉報。

發(fā)表評論

0條評論,0人參與

請輸入評論內(nèi)容...

請輸入評論/評論長度6~500個字

您提交的評論過于頻繁,請輸入驗證碼繼續(xù)

暫無評論

暫無評論

人工智能 獵頭職位 更多
掃碼關(guān)注公眾號
OFweek人工智能網(wǎng)
獲取更多精彩內(nèi)容
文章糾錯
x
*文字標(biāo)題:
*糾錯內(nèi)容:
聯(lián)系郵箱:
*驗 證 碼:

粵公網(wǎng)安備 44030502002758號