1. 程式人生 > 程式設計 >談談我對指標和結構體的理解

談談我對指標和結構體的理解

為什麼要學結構體和指標

最近重學C語言版的資料結構,在動手實現前,覺得好像和Java也差不了太多,直接上手寫了一個最基本的順序儲存的線性表,嗯,有幾個語法錯誤,在編譯器的提示下,修正並執行起來,棘手的問題才剛剛開始,segment fault,出現了。

segment fault翻譯過來也就是段錯誤屬於Runtime Error 當你訪問未分配的記憶體時,會丟擲該錯誤

C中關於記憶體的問題還有記憶體洩漏(memory leak),這些問題最終都有可能丟擲段錯誤,或者程式執行無響應,又或在執行結束後返回像這樣的一行語句

Process exited after 5.252 seconds with return value 3221225477

而這些記憶體問題的根源往往是對指標的使用不夠恰當,忽略了指標的初始化,或者弄不清楚指標的指向。由於大一學習C語言時,不夠用功,對指標與結構體的基礎有相當的缺失,為了彌補這些缺失,同時方便後續資料機構的C實現,我決定重新探究一下C和指標。

在學習的過程中也很感謝C和指標這本書,我重點閱讀了6,10,11,12四個章節的內容,pdf版我也會放在文末。

結構體的定義和使用

struct ListNode {
    int a;
    float b;
    char* c;
};
//未define type前生命結構體變數必須跟上struct關鍵字
struct ListNode ln = {0};  //初始化,a=0,b=0.000000,c = NULL/0
struct ListNode* p; //typedef可以省去struct,直接用ListNode宣告 typedef struct ListNode ListNode; ListNode ln; //valid typedef struct ListNode* PtrToListNode; PtrToListNode p; //valid 複製程式碼

結構體(struct)的用途

  1. 類似面向物件中類的功能,方便訪問結構化的資料
  2. 結合指標來實現連結串列,這也許才是結構體的最廣泛的用途了吧,畢竟要用到類的話,為什麼不選擇一門面向物件的語言呢, 因此本文主要強調連結串列

結構的儲存分配,有趣的問題

來看下面這兩個結構

struct s1
{
	char a;
	int b;
	char c;
};
struct s2
{
	int b;
	char a;
	char c;
};
複製程式碼

它們的成員域完全一樣,只是生命變數的順序不一樣,那麼它們的大小呢?

printf("s1 size:%d\n",sizeof(struct s1));
printf("s2 size:%d\n",sizeof(struct s2));
複製程式碼

輸出結果:

s1 size:12
s2 size:8

這就是這兩個結構體儲存結構的差異導致的,我們都知道

1個int = 4個位元組

1個字元 = 1個位元組

  • s1: 先拿1個位元組存放a,而a後面的3個位元組空閒,接下來4個位元組的b,最後1個位元組的c空閒3個位元組,以存放下一個結構體,一共12個位元組

  • s2: b佔用4個位元組,a,c 佔用2個連續的位元組, 最後空餘2個位元組,以存放下一個結構體,一共8個位元組

指標

1.指標的基本概念

  • 指標也是一個變數
  • 指標的宣告並不會自動分配任何記憶體,指標必須初始化,未清楚指向的先初始化為NULL
  • 指標指向另外一個變數(也可以是另一個指標)的記憶體地址
  • 指標的內容是它所指向變數的值
  • 指標的值是一個整形資料
  • 指標的大小是一個常數(通常是4個位元組,64位作業系統是8個位元組,但編譯器也許統統預設為32位)
  • 指標的型別(void*/int*/flot*等)決定了間接引用時對值的解析(值在記憶體中都是由一串2進位制的位表示,顯然值的型別並非值本身固有得特性,而是取決於它的使用方式)

例:一個32位bit(4個位元組byte)值:01100111011011000110111101100010

型別
1個32位整數 1735159650
2個16位整數 26476和28514
4個字元 glob
浮點數 1.116533 * 10^24

2.指標的使用

指標的使用場景:

  1. 建立連結串列
  2. 作為函式引數傳遞
  3. 作為函式返回值
  4. 普通陣列一樣使用指標
  5. 建立變長陣列

1.單連結串列的建立

struct ListNode {
    int val;
    struct ListNode *next; //注意這裡的*,想想沒有*的話該結構體的定義合法嗎?
};
複製程式碼

連結串列是C語言,資料結構的難點,關於連結串列的詳細問題,我會在下一篇部落格中詳細解釋。

2.什麼時候要用指標作函式引數?

Ans:

  1. 要通過函式改變一個函式外傳來的引數的值,只能用址傳遞,即用指標作為引數傳進函式。
  2. 即使不要求對變數修改,也最好用指標作引數。

尤其是當傳入引數過大時,例如一個成員眾多的結構體,最好用指向該結構體的指標來代替,一個指標最大也就8個位元組,不僅如此,C語言傳值呼叫方式要求將引數的一份拷貝傳遞給函式。

因此,值傳遞對空間和時間都是一個極大的浪費,以後可以看到將指標作為引數的例子將會很常見,址傳遞唯一的缺陷在於存在函式修改該變數,可以將其設為常指標的方式避免這種情況的發生。

void print_large_struct(large_struct const * st){}

這行語句的作用是,告訴編譯器我的st這個指標是一個常指標,它指向的內容不能被改變,如果我在函式不小心改變了它的內容,請報錯給我。

只能用指標的例子--呼叫函式改變a的值

//改變引數的值的兩種方式
void changeByValue(int a){
	a = 666;
	printf("in the func:%d\n",a);
}
void changeByAddr(int* a){
	*a = 666;
}
int main()
{
	int a = 0;
	
	changeByValue(a);  //a直接作引數
	printf("out the func:%d\n",a);

	changeByAddr(&a); //取a的地址作引數
	printf("after changeByAddr:%d\n",a);

	return 0;
}
複製程式碼

輸出結果:

in the func:666 out the func:0
after changeByAddr:666

3.為什麼用指標來作為返回值?

Ans:
我的意思是,你有時可以這麼做

4.指標與普通陣列

  • 指標指向陣列
int a[3] = {1,2,3};
int* pa = a;
for (int i = 0; i < 3; ++i)
	printf("%d\n",*pa++);
複製程式碼

*pa++實際上是先對pa間接引用,即*pa,再執行pa = pa + 1,注意這裡的1不是指標運算上移動一個位元組,編譯器會根據指標的型別進行移動,例如這裡型別,是整型實際上移動4個位元組,一個int的長度。

  • int
for (int i = 0; i < 3; ++i)
	printf("%d\n",pa++);
複製程式碼

輸出結果:

6487600
6487604
6487608

  • double
double b[3] = {1,3};
double *pb = b;
for (int i = 0; i < 3; ++i)
  printf("%d\n",pb++);
複製程式碼

輸出結果:

6487552
6487560
6487568

與此同時,陣列變數本身就是指向陣列第一個元素也就是a[0]的指標,它包含了第一個元素的地址,因此也完全可以把a當成一個指標來用,以下的引用都是合法的。

p = a;
p = &a[0]; //與上面相同都是將p指向a陣列

//a++不合法,陣列名不能作左值進行自增運算,採用*(a+i)的方式推進
for (int i = 0; i < 3; ++i)
	printf("%d\n",*(a+i));
複製程式碼

5.操作指標來自定義一個變長陣列?

寫下這一點的我又看了翁愷老師的mooc(c語言進階),其中的4.1非常的經典,基本是線性表的雛形了。

  • 變長陣列
//Q1
typedef struct Array{
	int * array;
	int size;
}Array;
//Q2
Array arrary_create(int init_size){
	Array a;
	a.size = init_size;
	a.array = (int*)malloc(init_size * sizeof(int));
	return a;
}

void array_free(Array* a){
	free(a->array);
	a->array = NULL;
	a->size = 0;
}
//Q3
void array_inflate(Array* a,int more_size){
	int* p = (int*)malloc((a->size + more_size) * sizeof(int));
	for (int i = 0; i < a->size; ++i)
		p[i] = a->array[i];
	free(a->array);
	a->array = p;
	a->size = a->size + more_size;
}
//Q4
int array_at(Array const * a,int index){
	return a->array[index];
}
複製程式碼

在以上程式碼,我分別作了4個標記,它們對應著4個問題。

Q1:Why Array not Array* ?

  • typedef struct Array{...}* Array

這麼做?我將無法得到一個結構體的本地變數,我只能操作指向這個結構體的指標,卻無法生成一個結構體,這是一個可笑的問題,我的指標該指向誰呢? 同時,看到Array a;你能想到a它是一個指標嗎?

Q2:Again ?Why Array not Array* ?

Array* array_create(int init_size){
	Array a;
	a.size = init_size;
	a.array = (int*)malloc(init_size * sizeof(int));
	return &a;
}
複製程式碼

這樣做?注意到這個a是在array_create函式裡面定義的區域性變數哦。讓我們來看一下C的回收機制,你就會明白,為什麼這樣做行不通。

  1. 如果是在函式內定義的,稱為 區域性 變數,儲存在棧空間內。它的空間會在函式呼叫結束後自行釋放。
  2. 如果是全域性變數,儲存在DATA段或者BSS段,它的空間是始終存在的,直至程式結束執行。
  3. 如果是new或者malloc得到的空間,它儲存在HEAP(堆)中,除非手動delete或free,否則空間會一直佔用直至程式結束。

函式的確返回了一個指標,但在函式返回的同時,a就會被回收,那麼你返回的a的地址就是一個指向未知位置的指標,是一個意義不明確的值,不再是你所認為的指向那個你當初在函式裡創造的結構體了哦。

另一種做法?

Array* array_create(Array* a,int init_size){
	a->size = init_size;
	a->array = (int*)malloc(init_size * sizeof(int));
	return a;
}
複製程式碼

這麼做不是不可以,但它有兩個潛在的風險

  1. 如果a == NULL ,那麼這必然引發記憶體訪問錯誤;
  2. a已經指向了某個已經存在的結構體,那你在新建的是不是要對a->array進行free呢?

與其這樣複雜,我們不妨採用更為簡單的辦法,返回一個結構體本身。

Q3: 每次inflate都要將原來array裡的元素複製到新申請的空間裡面太複雜?

當然,你也可以這樣做:

void array_inflate(Array* a,int more_size){
	a->array = (int*)realloc(a->array,(a->size + more_size) * sizeof(int));
	a->size = a->size + more_size;
}
複製程式碼

那麼既然都已經接觸到malloc,realloc了,不妨在此總結以下這幾個函式吧!

malloc calloc realloc 和 free

它們都是從堆上獲取可用的(連續?至少邏輯上是連續的,物理上根據作業系統, 很可能不是連續的)的記憶體塊的首地址,返回的都是void*型別的指標,都需要強制型別轉換。
它們申請的記憶體有可能比你的請求略多一點,記憶體庫為空時返回NULL指標。
現實是存在這個可能的!因此用到動態記憶體分配時一定要檢查返回是不是NULL啊

  • realloc與malloc不同的在於,realloc需要一個原記憶體的地址,和一個擴大後的size,如果原記憶體後面接著有可用的記憶體塊,就將這一部分也分給原地址,否則尋找一個足夠大的記憶體,返回新的地址並且自動將資料複製到新的記憶體
  • calloc第一個引數為申請的個數,第二個引數為每個單元的大小,例如calloc(100,sizeof(int))申請100個int大小的記憶體。注意,calloc最大的不同在於它會自動為這些記憶體初始化,指標初始化為NULL,很大程度上避免了一些未初始化的錯誤。
  • free接受一個指標型別的引數,這個引數要麼是NULL,free(NULL)是安全的。 要麼就只能是上面三兄弟從堆裡分配來的記憶體了。

Q4: Why const* ?

這算是指標作為函式引數傳入的例子了吧,const是因為我不希望我訪問a中元素時,a被修改掉了,所以告訴編譯器幫我盯著一下。事實上我們看到函式裡面很安全,並沒有對*a進行修改,函式足夠簡單時,我們完全可以去掉const

3. 指標的運算

需要注意的是,當指標指向的並不是一個陣列時,指標的運算是無意義的

//指標是整型的資料,它們之間當然可以運算,但下面是無意義的
int a = 3;
int b = 1;
int* pa = &a;
int* pb = &b;
printf("%d\n",pb - pa);
複製程式碼

但當指標指向一個陣列時,減法運算的意義就是兩個指標的距離,這個距離也是一個邏輯上的距離

int a[5];
int *pa,*pb;
pa = &a[0],pb = &a[3];
int distance = pb - pa;
複製程式碼

得到的distance是16/4(1個int4個位元組)為4; 再看一個有趣的例子,指標的關係運算

//讓a中元素全部變成5
int a[3] = {1,3};
int* p;
for(p = &a[0]; p < &a[3]; *p ++ = 5); //長得有點奇怪卻合法的for迴圈
複製程式碼

a++ 和 ++a的相同點都是給a+1,不同點是a++是先參加程式的執行再+1,而++a則是先+1再參加程式的執行。

在這裡我們訪問了陣列最後一個元素後面那個地址,並與之作比較來決定推進的邊界, 這居然是合法的,事實上在最後一次比較時我們的p已經指向了那個位置,但我們沒有對其進行間接訪問,因此這組迴圈是完全合法的。 再看下面這個例子:

//將a中元素全部變成0
for(p = &a[2]; p >= &a[0]; p--)
  *p = 0;
複製程式碼

在最後一次比較時,p已經從a[0]的位置自減了1,也就是說它移到了陣列之外,與上一個例子不一樣的是,他將與a[0]的地址進行比較,這是無意義的,其中涉及到的標準如下:

標準允許指向陣列元素的指標與陣列最後一個元素後面的那個記憶體位置的指標進行比較,但不允許與指向陣列第一個元素之前的那個記憶體位置的指標進行比較

關於C中的指標,內容確實太多,在以後的學習中邊踩坑,邊總結,寫作本文的原因也是在於將自己犯過的錯誤做一個記錄,在總結中積累經驗,不斷前行,總之,加油吧~