Python源碼剖析筆記0——C語言基礎回顧

要分析python源碼扮休,C語言的基礎不能少呈宇,特別是指針和結(jié)構(gòu)體等知識党饮。這篇文章先回顧C語言基礎述雾,方便后續(xù)代碼的閱讀胆建。

1 關于ELF文件

linux中的C編譯得到的目標文件和可執(zhí)行文件都是ELF格式的烤低,可執(zhí)行文件中以segment來劃分,目標文件中笆载,我們是以section劃分拂玻。一個segment包含一個或多個section,通過readelf命令可以看到完整的section和segment信息宰译¢苎粒看一個栗子:

char pear[40];
static double peach;
int mango = 13;
char *str = "hello";

static long melon = 2001;

int main()
{
        int i = 3, j;
        pear[5] = i;
        peach = 2.0 * mango;
        return 0;
}

這是個簡單的C語言代碼,現(xiàn)在分析下各個變量存儲的位置沿侈。其中mango闯第,melon屬于data section,pear和peach屬于common section中缀拭,而且peach和melon加了static咳短,說明只能本文件使用。而str對應的字符串"helloworld"存儲在rodata section中蛛淋。main函數(shù)歸屬于text section咙好,函數(shù)中的局部變量i,j在運行時在棧中分配空間。注意到前面說的全局未初始化變量peach和pear是在common section中褐荷,這是為了強弱符號而設置的勾效。那其實最終鏈接成為可執(zhí)行文件后,會歸于BSS segment。同樣的层宫,text section和rodata section在可執(zhí)行文件中都屬于同一個segment杨伙。

更多ELF內(nèi)容參見《程序猿的自我修養(yǎng)》一書。

2 關于指針

想當年學習C語言最怕的就是指針了萌腿,當然《c與指針》和《c專家編程》以及《高質(zhì)量C編程》里面對指針都有很好的講解限匣,系統(tǒng)回顧還是看書吧,這里我總結(jié)了一些基礎和易錯的點毁菱。環(huán)境是ubuntu14.10的32位系統(tǒng)米死,編譯工具GCC。

2.1 指針易錯點

/***
指針易錯示例1 demo1.c
***/

int main()
{
        char *str = "helloworld"; //[1]
        str[1] = 'M'; //[2] 會報錯
        char arr[] = "hello"; //[3]
        arr[1] = 'M';
        return 0;
}

demo1.c中贮庞,我們定義了一個指針和數(shù)組分別指向了一個字符串哲身,然后修改字符串中某個字符的值。編譯后運行會發(fā)現(xiàn)[2]處會報錯贸伐,這是為什么呢?用命令gcc -S demo1.c 生成匯編代碼就會發(fā)現(xiàn)[1]處的helloworld是存儲在rodata section的怔揩,是只讀的捉邢,而[3]處的是存儲在棧中的。所以[2]報錯而[3]正常商膊。在C中伏伐,用[1]中的方式創(chuàng)建字符串常量并賦值給指針,則字符串常量存儲在rodata section晕拆。而如果是賦值給數(shù)組藐翎,則存儲在棧中或者data section中(如[3]就是存儲在棧中)。示例2給出了更多容易出錯的點实幕,可以看看吝镣。

/***
指針易錯示例2 demo2.c
***/
char *GetMemory(int num) {
        char *p = (char *)malloc(sizeof(char) * num);
        return p;
}

char *GetMemory2(char *p) {
        p = (char *)malloc(sizeof(char) * 100);
}

char *GetString(){
        char *string = "helloworld";
        return string;
}

char *GetString2(){
        char string[] = "helloworld";
        return string;
}

void ParamArray(char a[])
{
        printf("sizeof(a)=%d\n", sizeof(a)); // sizeof(a)=4,參數(shù)以指針方式傳遞
}

int main()
{
        int a[] = {1, 2, 3, 4};
        int *b = a + 1;
        printf("delta=%d\n", b-a); // delta=4昆庇,注意int數(shù)組步長為4
        printf("sizeof(a)=%d, sizeof(b)=%d\n", sizeof(a), sizeof(b)); //sizeof(a)=16, sizeof(b)=4
        ParamArray(a); 
        
        
        //引用了不屬于程序地址空間的地址末贾,導致段錯誤
        /*
        int *p = 0;
        *p = 17;         
        */
        
        char *str = NULL;
        str = GetMemory(100);
        strcpy(str, "hello");
        free(str); //釋放內(nèi)存
        str = NULL; //避免野指針

        //錯誤版本,這是因為函數(shù)參數(shù)傳遞的是副本整吆。
        /*
        char *str2 = NULL;
        GetMemory2(str2);
        strcpy(str2, "hello");
        */

        char *str3 = GetString();
        printf("%s\n", str3);

        //錯誤版本拱撵,返回了棧指針,編譯器會有警告表蝙。
        /*
        char *str4 = GetString2();
        */
        return 0;
}

2.2 指針和數(shù)組

在2.1中也提到了部分指針和數(shù)組內(nèi)容拴测,在C中指針和數(shù)組在某些情況下可以相互轉(zhuǎn)換來使用,比如char *str="helloworld"可以通過str[1]來訪問第二個字符府蛇,也可以通過*(str+1)來訪問集索。
此外,在函數(shù)參數(shù)中,使用數(shù)組和指針也是等同的抄谐。但是指針和數(shù)組在有些地方并不等同渺鹦,需要特別注意。

比如我定義一個數(shù)組char a[9] = "abcdefgh";(注意字符串后面自動補\0),那么用a[1]讀取字符'b'的流程是這樣的:

  • 首先蛹含,數(shù)組a有個地址毅厚,我們假設是9980。
  • 然后取偏移值浦箱,偏移值為索引值*元素大小吸耿,這里索引是1,char大小也為1酷窥,因此加上9980為9981咽安,得到數(shù)組a第1個元素的地址。(如果是int類型數(shù)組蓬推,那么這里偏移就是1 * 4 = 4)
  • 取地址9981處的值妆棒,就是'b'。

那如果定義一個指針char *a = "abcdefgh";沸伏,我們通過a[1]來取第一個元素的值糕珊。跟數(shù)組流程不同的是:

  • 首先,指針a自己有個地址毅糟,假設是4541.
  • 然后红选,從4541取a的值,也就是字符串“abcdefgh”的地址姆另,假定是5081喇肋。
  • 接著就是跟之前一樣的步驟了,5081加上偏移1迹辐,取5082地址處的值蝶防,這里就是'b'了。

通過上面的說明可以發(fā)現(xiàn)明吩,指針比數(shù)組多了一個步驟慧脱,雖然看起來結(jié)果是一致的。因此贺喝,下面這個錯誤就比較好理解了菱鸥。在demo3.c中定義了一個數(shù)組,然后在demo4.c中通過指針來聲明并引用它躏鱼,顯然是會報錯的氮采。如果改成extern char p[];就正確了(當然聲明你也可以寫成extern char p[3],聲明里面的數(shù)組大小跟實際大小不一致是沒有關系的),一定要保證定義和聲明匹配染苛。

/***
demo3.c
***/
char p[] = "helloworld";

/***
demo4.c
***/
extern char *p;
int main()
{
        printf("%c\n", p[1]);
        return 0;
}

3 關于typedef和#define

typedef和#define都是經(jīng)常用的鹊漠,但是它們是不一樣的主到。一個typedef可以塞入多個聲明器,而#define一般只能有一個定義躯概。在連續(xù)聲明中登钥,typedef定義的類型可以保證聲明的變量都是同一種類型,而#define不行娶靡。此外牧牢,typedef是一種徹底的封裝類型,在聲明之后不能再添加其他的類型姿锭。如代碼中所示塔鳍。

#define int_ptr int *
int_ptr i, j; //i是int *類型,而j是int類型呻此。

typedef char * char_ptr;
char_ptr c1, c2; //c1, c2都是char *類型轮纫。

#define peach int
unsigned peach i; //正確

typdef int banana;
unsigned banana j; //錯誤,typedef聲明的類型不能擴展其他類型焚鲜。

另外掌唾,typedef在結(jié)構(gòu)體定義中也很常見,比如下面代碼中的定義忿磅。需要注意的是糯彬,[1]和[2]是很不同的。當你如[1]中那樣用typedef定義了struct foo贝乎,那么其實除了本身的foo結(jié)構(gòu)標簽,你還定義了foo這種結(jié)構(gòu)類型叽粹,所以可以直接用foo來聲明變量览效。而如[2]中的定義是不能用bar來聲明變量的,因為它只是一個結(jié)構(gòu)變量虫几,并不是結(jié)構(gòu)類型锤灿。

還有一點需要說明的是,結(jié)構(gòu)體是有自己名字空間的辆脸,所以結(jié)構(gòu)體中的字段可以跟結(jié)構(gòu)體名字相同但校,比如[3]中那樣也是合法的,當然盡量不要這樣用啡氢。后面一節(jié)還會更詳細探討結(jié)構(gòu)體状囱,因為在Python源碼中也有用到很多結(jié)構(gòu)體。

typedef struct foo {int i;} foo; //[1]
struct bar {int i;} bar; //[2]

struct foo f; //正確倘是,使用結(jié)構(gòu)標簽foo
foo f; //正確亭枷,使用結(jié)構(gòu)類型foo

struct bar b; //正確,使用結(jié)構(gòu)標簽bar
bar b; // 錯誤搀崭,使用了結(jié)構(gòu)變量bar叨粘,bar已經(jīng)是個結(jié)構(gòu)體變量了,可以直接初始化,比如bar.i = 4;

struct foobar {int foorbar;}; //[3]合法的定義

4 關于結(jié)構(gòu)體

在學習數(shù)據(jù)結(jié)構(gòu)的時候升敲,定義鏈表和樹結(jié)構(gòu)會經(jīng)常用到結(jié)構(gòu)體答倡。比如下面這個:

struct node {
    int data;
    struct node* next;
};

在定義鏈表的時候可能就有點奇怪了,為什么可以這樣定義驴党,貌似這個時候struct node還沒有定義好為什么就可以用next指針指向用這個結(jié)構(gòu)體定義了呢瘪撇?

4.1 不完全類型

這里要說下C語言里面的不完全類型。C語言可以分為函數(shù)類型鼻弧,對象類型以及不完全類型设江。而對象類型還可以分為標量類型和非標量類型。算術類型(如int攘轩,float叉存,char等)和指針類型屬于標量類型,而定義完整的結(jié)構(gòu)體度帮,聯(lián)合體歼捏,數(shù)組等都是非標量類型。而不完全類型是指沒有定義完整的類型笨篷,比如下面這樣的

struct s;
union u;
char str[];

具有不完全類型的變量可以通過多次聲明組合成一個完全類型瞳秽。比如下面2詞聲明str數(shù)組是合法的:

char str[];
char str[10];

此外,如果兩個源文件定義了同一個變量率翅,只要它們不全部是強類型的练俐,那么也是可以編譯通過的。比如下面這樣是合法的冕臭,但是如果將file1.c中的int i;改成強定義如int i = 5;那么就會出錯了腺晾。

//file1.c
int i;

//file2.c
int i = 4;

4.2 不完全類型結(jié)構(gòu)體

不完全類型的結(jié)構(gòu)體十分重要,比如我們最開始提到的struct node的定義辜贵,編譯器從前往后處理悯蝉,發(fā)現(xiàn)struct node *next時,認為struct node是一個不完全類型托慨,next是一個指向不完全類型的指針鼻由,盡管如此,指針本身是完全類型厚棵,因為不管什么指針在32位系統(tǒng)都是占用4個字節(jié)蕉世。而到后面定義結(jié)束,struct node成了一個完全類型婆硬,從而next就是一個指向完全類型的指針了讨彼。

4.3 結(jié)構(gòu)體初始化和大小

結(jié)構(gòu)體初始化比較簡單,需要注意的是結(jié)構(gòu)體中包含有指針的時候柿祈,如果要進行字符串拷貝之類的操作哈误,對指針需要額外分配內(nèi)存空間哩至。如下面定義了一個結(jié)構(gòu)體student的變量stu和指向結(jié)構(gòu)體的指針pstu,雖然stu定義的時候已經(jīng)隱式分配了結(jié)構(gòu)體內(nèi)存蜜自,但是你要拷貝字符串到它指向的內(nèi)存的話菩貌,需要顯示分配內(nèi)存。

struct student {
    char *name;
    int age;
} stu, *pstu;

int main()
{
    stu.age = 13; //正確
    // strcpy(stu.name,"hello"); //錯誤重荠,name還沒有分配內(nèi)存空間
        
    stu.name = (char *)malloc(6);
    strcpy(stu.name, "hello"); //正確
        
    return 0;
}

結(jié)構(gòu)體大小涉及一個對齊的問題箭阶,對齊規(guī)則為:

  • 結(jié)構(gòu)體變量首地址為最寬成員長度(如果有#pragma pack(n),則取最寬成員長度和n的較小值戈鲁,默認pragma的n=8)的整數(shù)倍
  • 結(jié)構(gòu)體大小為最寬成員長度的整數(shù)倍
  • 結(jié)構(gòu)體每個成員相對結(jié)構(gòu)體首地址的偏移量都是每個成員本身大谐鸩巍(如果有pragma pack(n),則是n與成員大小的較小值)的整數(shù)倍
    因此,下面結(jié)構(gòu)體S1和S2雖然內(nèi)容一樣婆殿,但是字段順序不同诈乒,大小也不同,sizeof(S1) = 8, 而sizeof(S2) = 12. 如果定義了#pragma pack(2)婆芦,則sizeof(S1)=8怕磨;sizeof(S2)=8
typedef struct node1
{
    int a;
    char b;
    short c;
}S1;

typedef struct node2
{
    char b;
    int a;
    short c;
}S2;

4.4 柔性數(shù)組

柔性數(shù)組是指結(jié)構(gòu)體的最后面一個成員可以是一個大小未知的數(shù)組,這樣可以在結(jié)構(gòu)體中存放變長的字符串消约。如代碼中所示肠鲫。注意,柔性數(shù)組必須是結(jié)構(gòu)體最后一個成員,柔性數(shù)組不占用結(jié)構(gòu)體大小.當然或粮,你也可以將數(shù)組寫成char str[0],含義相同导饲。

updated: 查看Python源碼過程中,發(fā)現(xiàn)其柔性數(shù)組聲明并不是用一個空數(shù)組或者char str[0]氯材,而是用的char str[1]渣锦,即數(shù)組大小為1。這是因為ISO C標準不允許聲明大小為0的數(shù)組(gcc -pedanti參數(shù)可以檢查是否符合ISO C標準)浓体,為了可移植性泡挺,所以常潮惭龋看到的是聲明數(shù)組大小為1命浴。當然,很多編譯器比如GCC等把數(shù)組大小為0作為了一個非標準的擴展贱除,所以聲明空的或者大小為0的柔性數(shù)組在GCC中是可以正常編譯的生闲。

struct flexarray {
        int len;
        char str[];
} *pfarr;

int main()
{
        char s1[] = "hello, world";
        pfarr = malloc(sizeof(struct flexarray) + strlen(s1) + 1);
        pfarr->len = strlen(s1);
        strcpy(pfarr->str, s1);
        printf("%d\n", sizeof(struct flexarray)); // 4
        printf("%d\n", pfarr->len); // 12
        printf("%s\n", pfarr->str); // hello, world
        return 0;
}

5 總結(jié)

  • 關于const,c語言中的const不是常量月幌,所以不能用const變量來定義數(shù)組碍讯,如const int N = 3; int a[N];這是錯誤的。
  • 注意內(nèi)存分配和釋放扯躺,杜絕野指針捉兴。
  • C語言中弱符號和強符號一起鏈接是合法的蝎困。
  • 注意指針和數(shù)組的區(qū)別。
  • typedef和#define是不同的倍啥。
  • 注意包含指針的結(jié)構(gòu)體的初始化和柔性數(shù)組的使用禾乘。

6 參考資料

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市虽缕,隨后出現(xiàn)的幾起案子始藕,更是在濱河造成了極大的恐慌,老刑警劉巖氮趋,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伍派,死亡現(xiàn)場離奇詭異,居然都是意外死亡剩胁,警方通過查閱死者的電腦和手機诉植,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摧冀,“玉大人倍踪,你說我怎么就攤上這事∷靼海” “怎么了建车?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長椒惨。 經(jīng)常有香客問我缤至,道長,這世上最難降的妖魔是什么康谆? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任领斥,我火速辦了婚禮,結(jié)果婚禮上沃暗,老公的妹妹穿的比我還像新娘月洛。我一直安慰自己,他們只是感情好孽锥,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布嚼黔。 她就那樣靜靜地躺著,像睡著了一般惜辑。 火紅的嫁衣襯著肌膚如雪唬涧。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天盛撑,我揣著相機與錄音碎节,去河邊找鬼。 笑死抵卫,一個胖子當著我的面吹牛狮荔,可吹牛的內(nèi)容都是我干的胎撇。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼殖氏,長吁一口氣:“原來是場噩夢啊……” “哼创坞!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起受葛,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤题涨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后总滩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纲堵,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年闰渔,在試婚紗的時候發(fā)現(xiàn)自己被綠了席函。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡冈涧,死狀恐怖茂附,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情督弓,我是刑警寧澤营曼,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站愚隧,受9級特大地震影響蒂阱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狂塘,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一录煤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧荞胡,春花似錦妈踊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至窖梁,卻和暖如春赘风,著一層夾襖步出監(jiān)牢的瞬間夹囚,已是汗流浹背纵刘。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留荸哟,地道東北人假哎。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓瞬捕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舵抹。 傳聞我的和親對象是個殘疾皇子肪虎,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,517評論 1 51
  • 指針是C語言中廣泛使用的一種數(shù)據(jù)類型。 運用指針編程是C語言最主要的風格之一惧蛹。利用指針變量可以表示各種數(shù)據(jù)結(jié)構(gòu)扇救; ...
    朱森閱讀 3,444評論 3 44
  • 版權聲明:本文為 gfson 原創(chuàng)文章,轉(zhuǎn)載請注明出處香嗓。注:作者水平有限迅腔,文中如有不恰當之處,請予以指正靠娱,萬分感謝...
    gfson閱讀 2,983評論 0 6
  • 最近迷上笑來老師沧烈,訂了專欄,報了新生大學像云,還持續(xù)不斷地追著他聽講座锌雀。昨天晚上是他關于《七年一輩子》這本書和他十年的...
    自我冒險家閱讀 218評論 1 0
  • 那天你走了 拋下一切踏上了行程 那是人一輩子最終的站點 如果人能活一百歲 那么死亡是百年一遇的事 而你是幸還是不幸...
    雨初33閱讀 316評論 0 0