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

爲何要學結構體和指針

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

segment fault翻譯過來也就是段錯誤屬於Runtime Error 當你訪問未分配的內存時,會拋出該錯誤安全

C中關於內存的問題還有內存泄漏(memory leak), 這些問題最終都有可能拋出段錯誤,或者程序運行無響應,又或在運行結束後返回像這樣的一行語句bash

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. 結合指針來實現鏈表,這也許纔是結構體的最普遍的用途了吧,畢竟要用到類的話,爲何不選擇一門面向對象的語言呢, 所以本文主要強調鏈表

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

來看下面這兩個結構ui

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

它們的成員域徹底同樣,只是生命變量的順序不同,那麼它們的大小呢?spa

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, 2, 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, 2, 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中的指針,內容確實太多,在之後的學習中邊踩坑,邊總結,寫做本文的緣由也是在於將本身犯過的錯誤作一個記錄,在總結中積累經驗,不斷前行,總之,加油吧~

相關文章
相關標籤/搜索