數據結構和算法面試題系列—C指針、數組和結構體

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏html

0 概述

在用C語言實現一些常見的數據結構和算法時,C語言的基礎不能少,特別是指針和結構體等知識。node

1 關於ELF文件

linux中的C編譯獲得的目標文件和可執行文件都是ELF格式的,可執行文件中以segment來劃分,目標文件中,咱們是以section劃分。一個segment包含一個或多個section,經過readelf命令能夠看到完整的section和segment信息。看一個栗子:linux

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。git

更多ELF內容參見《程序猿的自我修養》一書。github

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'的流程是這樣的:ubuntu

  • 首先,數組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)的整數倍
  • 結構體大小爲最寬成員長度的整數倍
  • 結構體每一個成員相對結構體首地址的偏移量都是每一個成員自己大小(若是有pragma 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],含義相同。

注:在學習Python源碼過程當中,發現其柔性數組聲明並非用一個空數組或者char str[0],而是用的char str[1],即數組大小爲1。這是由於ISO C標準不容許聲明大小爲0的數組(gcc -pedanti參數能夠檢查是否符合ISO C標準),爲了可移植性,因此經常看到的是聲明數組大小爲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是不一樣的。
  • 注意包含指針的結構體的初始化和柔性數組的使用。

參考資料

相關文章
相關標籤/搜索