要分析python源碼,C語言的基礎不能少,特別是指針和結構體等知識。這篇文章先回顧C語言基礎,方便后續代碼的閱讀。
1 關于ELF文件
linux中的C編譯得到的目標文件和可執行文件都是ELF格式的,可執行文件中以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語言代碼,現在分析下各個變量存儲的位置。其中mango,melon屬于data section,pear和peach屬于common section中,而且peach和melon加了static,說明只能本文件使用。而str對應的字符串"helloworld"存儲在rodata section中。main函數歸屬于text section,函數中的局部變量i,j在運行時在棧中分配空間。注意到前面說的全局未初始化變量peach和pear是在common section中,這是為了強弱符號而設置的。那其實最終鏈接成為可執行文件后,會歸于BSS segment。同樣的,text section和rodata section在可執行文件中都屬于同一個segment。
更多ELF內容參見《程序猿的自我修養》一書。
2 關于指針
想當年學習C語言最怕的就是指針了,當然《c與指針》和《c專家編程》以及《高質量C編程》里面對指針都有很好的講解,系統回顧還是看書吧,這里我總結了一些基礎和易錯的點。環境是ubuntu14.10的32位系統,編譯工具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中,我們定義了一個指針和數組分別指向了一個字符串,然后修改字符串中某個字符的值。編譯后運行會發現[2]處會報錯,這是為什么呢?用命令gcc -S demo1.c
生成匯編代碼就會發現[1]處的helloworld是存儲在rodata section的,是只讀的,而[3]處的是存儲在棧中的。所以[2]報錯而[3]正常。在C中,用[1]中的方式創建字符串常量并賦值給指針,則字符串常量存儲在rodata section。而如果是賦值給數組,則存儲在棧中或者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,參數以指針方式傳遞
}
int main()
{
int a[] = {1, 2, 3, 4};
int *b = a + 1;
printf("delta=%d\n", b-a); // delta=4,注意int數組步長為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); //釋放內存
str = NULL; //避免野指針
//錯誤版本,這是因為函數參數傳遞的是副本。
/*
char *str2 = NULL;
GetMemory2(str2);
strcpy(str2, "hello");
*/
char *str3 = GetString();
printf("%s\n", str3);
//錯誤版本,返回了棧指針,編譯器會有警告。
/*
char *str4 = GetString2();
*/
return 0;
}
2.2 指針和數組
在2.1中也提到了部分指針和數組內容,在C中指針和數組在某些情況下可以相互轉換來使用,比如char *str="helloworld"
可以通過str[1]
來訪問第二個字符,也可以通過*(str+1)
來訪問。
此外,在函數參數中,使用數組和指針也是等同的。但是指針和數組在有些地方并不等同,需要特別注意。
比如我定義一個數組char a[9] = "abcdefgh";
(注意字符串后面自動補\0),那么用a[1]讀取字符'b'的流程是這樣的:
- 首先,數組a有個地址,我們假設是9980。
- 然后取偏移值,偏移值為索引值*元素大小,這里索引是1,char大小也為1,因此加上9980為9981,得到數組a第1個元素的地址。(如果是int類型數組,那么這里偏移就是1 * 4 = 4)
- 取地址9981處的值,就是'b'。
那如果定義一個指針char *a = "abcdefgh";
,我們通過a[1]來取第一個元素的值。跟數組流程不同的是:
- 首先,指針a自己有個地址,假設是4541.
- 然后,從4541取a的值,也就是字符串“abcdefgh”的地址,假定是5081。
- 接著就是跟之前一樣的步驟了,5081加上偏移1,取5082地址處的值,這里就是'b'了。
通過上面的說明可以發現,指針比數組多了一個步驟,雖然看起來結果是一致的。因此,下面這個錯誤就比較好理解了。在demo3.c中定義了一個數組,然后在demo4.c中通過指針來聲明并引用它,顯然是會報錯的。如果改成extern char p[];
就正確了(當然聲明你也可以寫成extern char p[3],聲明里面的數組大小跟實際大小不一致是沒有關系的),一定要保證定義和聲明匹配。
/***
demo3.c
***/
char p[] = "helloworld";
/***
demo4.c
***/
extern char *p;
int main()
{
printf("%c\n", p[1]);
return 0;
}
3 關于typedef和#define
typedef和#define都是經常用的,但是它們是不一樣的。一個typedef可以塞入多個聲明器,而#define一般只能有一個定義。在連續聲明中,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在結構體定義中也很常見,比如下面代碼中的定義。需要注意的是,[1]和[2]是很不同的。當你如[1]中那樣用typedef定義了struct foo,那么其實除了本身的foo結構標簽,你還定義了foo這種結構類型,所以可以直接用foo來聲明變量。而如[2]中的定義是不能用bar來聲明變量的,因為它只是一個結構變量,并不是結構類型。
還有一點需要說明的是,結構體是有自己名字空間的,所以結構體中的字段可以跟結構體名字相同,比如[3]中那樣也是合法的,當然盡量不要這樣用。后面一節還會更詳細探討結構體,因為在Python源碼中也有用到很多結構體。
typedef struct foo {int i;} foo; //[1]
struct bar {int i;} bar; //[2]
struct foo f; //正確,使用結構標簽foo
foo f; //正確,使用結構類型foo
struct bar b; //正確,使用結構標簽bar
bar b; // 錯誤,使用了結構變量bar,bar已經是個結構體變量了,可以直接初始化,比如bar.i = 4;
struct foobar {int foorbar;}; //[3]合法的定義
4 關于結構體
在學習數據結構的時候,定義鏈表和樹結構會經常用到結構體。比如下面這個:
struct node {
int data;
struct node* next;
};
在定義鏈表的時候可能就有點奇怪了,為什么可以這樣定義,貌似這個時候struct node還沒有定義好為什么就可以用next指針指向用這個結構體定義了呢?
4.1 不完全類型
這里要說下C語言里面的不完全類型。C語言可以分為函數類型,對象類型以及不完全類型。而對象類型還可以分為標量類型和非標量類型。算術類型(如int,float,char等)和指針類型屬于標量類型,而定義完整的結構體,聯合體,數組等都是非標量類型。而不完全類型是指沒有定義完整的類型,比如下面這樣的
struct s;
union u;
char str[];
具有不完全類型的變量可以通過多次聲明組合成一個完全類型。比如下面2詞聲明str數組是合法的:
char str[];
char str[10];
此外,如果兩個源文件定義了同一個變量,只要它們不全部是強類型的,那么也是可以編譯通過的。比如下面這樣是合法的,但是如果將file1.c中的int i;
改成強定義如int i = 5;
那么就會出錯了。
//file1.c
int i;
//file2.c
int i = 4;
4.2 不完全類型結構體
不完全類型的結構體十分重要,比如我們最開始提到的struct node的定義,編譯器從前往后處理,發現struct node *next
時,認為struct node是一個不完全類型,next是一個指向不完全類型的指針,盡管如此,指針本身是完全類型,因為不管什么指針在32位系統都是占用4個字節。而到后面定義結束,struct node成了一個完全類型,從而next就是一個指向完全類型的指針了。
4.3 結構體初始化和大小
結構體初始化比較簡單,需要注意的是結構體中包含有指針的時候,如果要進行字符串拷貝之類的操作,對指針需要額外分配內存空間。如下面定義了一個結構體student的變量stu和指向結構體的指針pstu,雖然stu定義的時候已經隱式分配了結構體內存,但是你要拷貝字符串到它指向的內存的話,需要顯示分配內存。
struct student {
char *name;
int age;
} stu, *pstu;
int main()
{
stu.age = 13; //正確
// strcpy(stu.name,"hello"); //錯誤,name還沒有分配內存空間
stu.name = (char *)malloc(6);
strcpy(stu.name, "hello"); //正確
return 0;
}
結構體大小涉及一個對齊的問題,對齊規則為:
- 結構體變量首地址為最寬成員長度(如果有
#pragma pack(n)
,則取最寬成員長度和n的較小值,默認pragma的n=8)的整數倍 - 結構體大小為最寬成員長度的整數倍
- 結構體每個成員相對結構體首地址的偏移量都是每個成員本身大?。ㄈ绻衟ragma pack(n),則是n與成員大小的較小值)的整數倍
因此,下面結構體S1和S2雖然內容一樣,但是字段順序不同,大小也不同,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 柔性數組
柔性數組是指結構體的最后面一個成員可以是一個大小未知的數組,這樣可以在結構體中存放變長的字符串。如代碼中所示。注意,柔性數組必須是結構體最后一個成員,柔性數組不占用結構體大小.當然,你也可以將數組寫成char str[0]
,含義相同。
updated: 查看Python源碼過程中,發現其柔性數組聲明并不是用一個空數組或者char str[0]
,而是用的char str[1]
,即數組大小為1。這是因為ISO C標準不允許聲明大小為0的數組(gcc -pedanti
參數可以檢查是否符合ISO C標準),為了可移植性,所以常??吹降氖锹暶鲾到M大小為1。當然,很多編譯器比如GCC等把數組大小為0作為了一個非標準的擴展,所以聲明空的或者大小為0的柔性數組在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 總結
- 關于const,c語言中的const不是常量,所以不能用const變量來定義數組,如
const int N = 3; int a[N];
這是錯誤的。 - 注意內存分配和釋放,杜絕野指針。
- C語言中弱符號和強符號一起鏈接是合法的。
- 注意指針和數組的區別。
- typedef和#define是不同的。
- 注意包含指針的結構體的初始化和柔性數組的使用。