前些天發表了一篇博文關於c語言內存地址對齊的一點思考,引發了你們比較熱烈的討論,的確在這篇文章中示例的選擇不是很恰當,示例有不少不嚴謹的地方,博客的評論中獲得了不少同窗的指點。也有不少同窗指出這種作法技巧性太強,不適合在項目開發中使用,的確是這樣,沒有深厚的功底尤爲是對gcc編譯器的深度理解,是比較容易出錯。node
不過寫做該篇文章是爲了向你們介紹利用內存對齊的特性來存儲一些信息,若是你們從此在別人的代碼中看到這種用法,不至於一頭霧水,至少記得在osc之前某個時刻某人曾經介紹過這種用法,達到這個目的足以。另外須要注意,思考系列博文都是基於GNU gcc,其餘編譯器可能會出現不一致的狀況。linux
今天咱們要探討的是c語言中的等於運算符(==),不少同窗可能會以爲很是奇怪,這個等於運算符有什麼好探討的呢?無非就是比較兩個操做數是否相等,若是相等返回1,不然返回0,如1 == 1表達式返回1,1 == 2表達式返回0,如此簡單,有什麼能夠探討的呢。c++
事實真的如此嗎?讓咱們先從一個簡單的示例開始吧。架構
咱們的探討依然是從一個簡單的示例開始,以下:函數
int a = 10; long b = 12; (void)(a == b); (void)(&a == &b);
代碼很簡單,定義兩個變量a和b,分別爲int類型和long類型,第04行代碼將表達式(a == b)的值強制轉換爲void類型,該行代碼沒有任何做用,加上void的目的在於該表達式不能用於右值,第05行的代碼同第04行,區別在於等於運算符的兩個操做數由(int,long)變爲(int *, long*)。ui
接下來咱們打開編譯器全部告警,用gcc -Wall編譯一下源程序,看看會出現什麼狀況。spa
咱們看到編譯的結果有點奇怪,第05行代碼報告警信息,信息內容以下:.net
warning: comparison of distinct pointer types
告警信息的意思是不一樣指針類型的比較。此時,咱們感到很是奇怪,第04行和第05行代碼沒有本質的區別,都是等於運算符表達式,惟一的區別在於操做數的類型不一樣,04行比較的是(int,long),05行比較的是(int *, long*),因此若是要告警的話,應該這兩行代碼都會告警纔對,由於04行比較的數據類型也不一致。結果爲何只有05行告警,而04行不告警。指針
當咱們遇到這樣的一個問題的時候,咱們應該如何去分析去探索呢?在此分享一下鄙人的一點淺薄見解。code
當遇到這樣的問題的時候,毋庸置疑,源碼是最好的去處同時也是信息最全的,最不會騙人的地方。咱們知道gcc是一個相對比較龐大的項目,要求咱們對編譯原理有比較紮實的基礎。這個時候咱們會體會到科班出身和非科班出身的不一樣之處,也從側面給目前還正在大學就讀的同窗一點警示,大學期間不要爲了一點小小小的項目經驗,就荒廢了本身的計算機專業基礎,那是很是很是不值得的。
接下來就簡單的談一下分析思路,從抽象到具體,是我一向的思路。先總體的瞭解gcc的架構,而後再定位到咱們要解決的具體的問題。其中最難的是對問題源碼的定位階段,這裏面有兩種狀況:
1)若是對總體的架構和流程很是熟悉,則能夠一步步的分析流程,跟蹤到該問題具體的源碼;
2)當咱們只瞭解總體的架構,那麼咱們不可能一下就定位到問題源碼,可是咱們能夠定位到該問題的一個超集,對於本文示例來講,剛開始的時候,不知道問題源碼在哪兒,但至少我能夠肯定該問題確定是在AST構造階段。當咱們知道這一點後,接下來利用一些技巧,分析該階段,這樣就縮小了咱們的問題規模。
這裏面介紹一個小技巧,當出現問題的時候,咱們如何快速的定位。技巧很簡單,就是在gcc源碼下直接grep 'warning: comparison of distinct pointer types' -r *,此時可能會有多個地方出現該字符串,再利用咱們以前分析獲得的問題發生階段一步步的排除,縮小範圍,最終可以定位到咱們的源碼。
針對本文,定位的源碼信息以下(如下只列出與本文分析相關源碼,其餘省略):
/*** * Notes: * file location: gcc/c/c-typeck.c * function: tree build_binary_op (location_t location, enum tree_code code, tree orig_op0, tree orig_op1, int convert_p) * */ case EQ_EXPR: case NE_EXPR: ... if ((code0 == INTEGER_TYPE || code0 == REAL_TYPE || code0 == FIXED_POINT_TYPE || code0 == COMPLEX_TYPE) && (code1 == INTEGER_TYPE || code1 == REAL_TYPE || code1 == FIXED_POINT_TYPE || code1 == COMPLEX_TYPE)) short_compare = 1; ... else if (code0 == POINTER_TYPE && code1 == POINTER_TYPE) { tree tt0 = TREE_TYPE (type0); tree tt1 = TREE_TYPE (type1); addr_space_t as0 = TYPE_ADDR_SPACE (tt0); addr_space_t as1 = TYPE_ADDR_SPACE (tt1); addr_space_t as_common = ADDR_SPACE_GENERIC; /* Anything compares with void *. void * compares with anything. Otherwise, the targets must be compatible and both must be object or both incomplete. */ if (comp_target_types (location, type0, type1)) result_type = common_pointer_type (type0, type1); else if (!addr_space_superset (as0, as1, &as_common)) { error_at (location, "comparison of pointers to " "disjoint address spaces"); return error_mark_node; } else if (VOID_TYPE_P (tt0)) { if (pedantic && TREE_CODE (tt1) == FUNCTION_TYPE) pedwarn (location, OPT_Wpedantic, "ISO C forbids " "comparison of %<void *%> with function pointer"); } else if (VOID_TYPE_P (tt1)) { if (pedantic && TREE_CODE (tt0) == FUNCTION_TYPE) pedwarn (location, OPT_Wpedantic, "ISO C forbids " "comparison of %<void *%> with function pointer"); } else /* Avoid warning about the volatile ObjC EH puts on decls. */ if (!objc_ok) pedwarn (location, 0, "comparison of distinct pointer types lacks a cast"); if (result_type == NULL_TREE) { int qual = ENCODE_QUAL_ADDR_SPACE (as_common); result_type = build_pointer_type (build_qualified_type (void_type_node, qual)); } }
從上面的源碼,咱們能夠看到,當兩個操做數都爲INTEGER_TYPE的時候(long爲long int),只作了一行代碼處理:
short_compare = 1;
而當兩個操做數都爲POINTER_TYPE的時候,則作了不少判斷:
comp_target_types (location, type0, type1)
addr_space_superset (as0, as1, &as_common)
VOID_TYPE_P (tt0)
VOID_TYPE_P (tt1)
從上面咱們能夠看到,第一個comp_target_types函數比較了兩個操做數指針指向的的數據類型是否一致,當都不知足上述這些條件的時候,編譯器就會打印一條告警信息:
pedwarn (location, 0, "comparison of distinct pointer types lacks a cast");
從上面咱們能夠看到gcc在處理等於運算符EQ_EXPR的時候,不一樣的操做數類型其處理邏輯是不同的。上述源碼很好的解釋了咱們示例中提出的問題。
注1:int和long對於咱們來講可能認爲是兩種數據類型,但對於gcc來講,都是INTEGER_TYPE;
注2:後續有時間再詳細的和你們一塊兒探索下gcc的架構;
從以上分析,咱們知道二元運算符等於運算符,當兩個操做數都爲指針類型的時候,編譯器會先進行指針所指向的數據類型的檢查等諸多操做以後再比較操做數的值。
在瞭解了gcc編譯器的這個特性以後,咱們天然會思考,這個特性有什麼用途呢?接下來咱們就看一下關於該特性的一個簡單應用。
#define max(x,y) ({ \ typeof(x) _x = (x);›\ typeof(y) _y = (y);›\ (void) (&_x == &_y);› \ _x > _y ? _x : _y; })
這是一個計算兩個數最大值的宏,先得到x,y變量值,而後就使用了咱們的等於運算符的特性。咱們知道宏和函數最大的區別在於宏不能作靜態數據類型檢查而僅僅是簡單的替換,因此一旦出現錯誤,就很難定位。
第04行的代碼的做用就是比較x和y的數據類型是否一致,當類型不一致的時候,就會編譯器告警,這樣作就避免了計算不一樣數據類型的最大值。經過這種方式,使得咱們在使用宏的同時,可讓咱們的代碼更加健壯。
從上面的應用咱們知道,當咱們須要檢查兩個變量的數據類型是否一致的時候,就能夠利用==運算符在處理兩個操做數都是指針類型的狀況,會對指針指向的數據類型進行比較。
本文先從一個簡單的示例引出了gcc在計算等於運算符表達式,當兩個操做數都爲指針類型的時候的不一樣處理方式。再從gcc源碼的角度分析了,爲何會出現這種狀況。接着介紹了該特性的一個簡單應用。從以上的分析,咱們知道,當咱們愈來愈瞭解咱們的編譯器的時候,咱們就能夠編寫更加高效,更加健壯的代碼。
這裏再吐槽一下,與其花時間去作一些沒有太大意義的事情如研究c++強大的功能和語法等,不如花更多的時間去了解咱們所使用的系統(這裏系統包括編譯器,os,處理器等),編寫高效的代碼,由於咱們實在是太不瞭解咱們的系統,有太多的東西值得咱們去思考去探索。
【1】http://en.wikibooks.org/wiki/GNU_C_Compiler_Internals/GNU_C_Compiler_Architecture
【2】http://www.airs.com/dnovillo/200711-GCC-Internals/200711-GCC-Internals-1-condensed.pdf