C語言__指標總結__
1.什麼是指標
指標是一種資料型別,使用它定義的變數叫指標變數,其值為另一個變數的地址,即記憶體位置的直接地址(記憶體地址的整數)
2.為什麼要使用指標
A.解決函式之間無法通過傳參來共享變數:
函式的形參變數屬於被呼叫者,實參屬於呼叫者,函式之間的變數名是可以互相重名的,因為他們的存放空間是各自獨立的,互不相干,他們之間的資料傳遞都是值傳遞(記憶體拷貝、賦值)
B.優化函式之間的傳參效率:
假設你有一個很大的陣列需要傳,如果只是單純的用賦值、記憶體拷貝,效率是很低的,但如果你是用指標,傳的是一個數組的首地址,接下來被呼叫者就會根據你給的記憶體地址去接收資料,因為只是傳地址,所以效率會很高
C.使用堆記憶體:
因為C語言中沒有提供管理記憶體的語句,只能依靠標準庫中提供的函式來對堆進行申請和釋放,而且堆記憶體無法與識別符號建立聯絡,只能配合指標使用
3.如何使用指標
定義:型別* 變數名p
1.指標變數一般以p結尾,為了區別與普通變數
2.* 表示此變數是指標變數,一個* 只能定義一個指標變數,不能連續定義,如:
int* p1,p2,p3; //p1是指標變數,p2,p3是int變數
int *p1,*p2,*p3; //三個指標變數
3.型別表示的是儲存的是什麼型別變數的地址,它決定了當通過地址訪問這塊記憶體時要訪問的位元組數
4.指標變數的預設值是不確定的,一般初始化為NULL(空指標)
5.賦值:因為指標變數裡面存放的是記憶體的地址,所以給指標變數也要用地址給他賦值,格式為:指標變數 = 地址,如:
棧地址賦值:
int num = 0;
int* p = NULL;
p = #
堆地址賦值:
int* p = NULL;
p = malloc(4); // 向堆記憶體申請4個位元組的空間
6.解引用(根據地址訪問記憶體):指標只是一個地址,傳來傳去,但要想得到裡面的值,這就需要一把"鑰匙",這個"鑰匙"就是* ,指標和變數的關係就像是這樣:*指標變數名 <=> 變數,他倆是等價的。
根據指標變數中儲存的記憶體編號去訪問記憶體中多少位元組,這就是由指標變數的型別所決定了;
如果指標變數中存放的地址出錯,此時訪問可能會發生段錯誤(賦值產生的錯誤)。
4.使用指標要注意的問題
指標的使用,主要會發生下面2個問題 。
1.空指標:
指標變數的為NULL(大多數為0,也有特殊情況為1),這種指標變數叫空指標,空指標是不能進行解引用的,因為NULL被作業系統當作復位地址(裡面儲存了系統重啟所需要的資料),當作業系統察覺到程式試圖訪問NULL位置的資料時,系統就會向程式傳送段錯誤的訊號,程式就會死亡,說白了,你(程式)要想在我(作業系統)的地盤上執行,你就得遵守我的規矩,禁區(NULL)看看就行,知道有這個地方(可以初始化為NULL),但不要亂進(解引用)。
空指標還可被當作錯誤標誌,如果一個函式返回值型別時指標型別時,但它實際上返回的值NULL,則說明函式執行失敗或者出錯。
在C語言中,應該杜絕對空指標進行解引用,當使用來歷不明的指標(呼叫者提供的)時,應首先判斷該指標是否為NULL,如:
if(NULL == p)
{
}
2.野指標:
指標變數的值是不確定的,或者都是無效的,這種指標叫野指標。使用野指標不一定會產生問題,可能產生的後果如下:
1.一切正常(運氣好)
2.段錯誤
3.髒資料
上面說野指標不一定會產生問題,那是不是說明野指標會比空指標好一點,因為一旦對空指標進行解引用,就必然會出現段錯誤,而野指標運氣好就不會出現,其實不然,野指標其實比空指標危害更大,因為野指標是無法判斷出來的、也無法測試出來,也就意味著一旦產生,無法杜絕。
那有什麼辦法來解決野指標呢,雖然野指標無法判斷也無法測試出來,但是所有的野指標都是人為製造出來的,最好的辦法就是防範於未然,不產生野指標,方法有:
1.定義指標變數時,要對其進行初始化
2.不返回區域性變數的地址
3.資源釋放後,指向它的指標要及時置空
5.指標與陣列的關係
陣列名就是指標(常指標),陣列名與陣列首地址是對映關係(即陣列名與陣列首地址對應),而指標是指向關係
由於陣列名就是指標,所以陣列名可以使用指標的解引用運算子,如:
#include <stdio.h>
int main()
{
char str[] = "hello";
printf("%c",*str);
}
因為str是str這個陣列的首地址,所以解引用出來的就是該陣列首個字元
輸出結果為:h
而指標也可以使用陣列的[ ]運算子,如
#include <stdio.h>
int main()
{
char str1[] = "hello";
char* p = str1;
printf("%c",p[0]);
}
輸出結果:h
需要注意的是,當使用陣列當函式的引數時,陣列會蛻變成指標,長度也就丟失了,因此需要額外再新增一個引數用來傳遞陣列的長度:
#include <stdio.h>
#include <string.h>
void func(char a[])
{
printf("%d",sizeof(a));
}
int main()
{
char a[] = "hello world";
printf("%d",sizeof(a));
puts("");
func(a);
return 0;
}
輸出:12
4
第一行輸出12沒問題,
第二行輸出4是因為將陣列作為引數傳過去是,傳的是陣列的首地址,沒有把長度也傳過去
6.指標的運算
指標的本質就是個整數,因此從語法上來說整數能使用的運算子它都能使用,但也不是所有的運算子對指標運算都是有意義的
指標 + 整數 <=> 指標 + 寬度 * 整數,向右移動,如
#include <stdio.h>
#include <string.h>
int main()
{
int num = 5;
int* p = #
printf("%x,%x",p,p+1);
return 0;
}
輸出結果:bfba6ff8,bfba6ffc
因為記憶體地址是隨機分配的,所以每次執行的地址都有可能是不同的,但他們兩個地址的差都是4(一個int型別的大小)
指標 + 整數 <=> 指標 - 寬度 * 整數,向左移動,例子跟上面的差不多,可自行實驗
指標 - 指標 <=> 指標 - 指標/寬度 計算出兩個指標之間相隔多少個元素。如:
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "hello";
char str2[] = "world";
char* p1 = str1;
char* p2 = str2;
printf("%x,%x",p1,p2);
puts("");
printf("%d",p2 - p1);
return 0;
}
輸出結果:bfd90e90,bfd90e96
6
因為char型別是1位元組,所以中間隔了6個元素
7.指標與const配合
在瞭解配合之前,先講一下我的理解,因為p是一個指標變數,相當於一個寶箱,沒有 * 這個鑰匙,就打不開寶箱,所以const加在 * 前還是 * 後,就能決定保護的是地址裡面的值,還是保護地址的值
const int * p:保護指標指向的資料,不能通過指標解引用修改記憶體的值。
int const * p:保護指標指向的資料,不能通過指標解引用修改記憶體的值。(因為const加在 * 前,說明這個寶箱已經被開啟,所以保護的是裡面的寶物)
int * const p:保護指標變數,指標變數初始化之後不能再顯式的賦值。(因為const加在 * 後,說明這個寶箱還沒被開啟,所以保護的是寶箱)
const int const * p:既不能修改指標的值,也不能修改記憶體的值。
int const * const p:既不能修改指標的值,也不能修改記憶體的值。(因為const既加在 * 前和 * 後,說明及保護寶箱也保護寶物)
8.什麼是二級指標、什麼情況下使用
說到一級指標,就會有二級指標,一級指標指向的是存放變數的記憶體地址,而二級指標則指向的是存放一級指標變數的記憶體地址,即指向指標的指標。
就好像挖寶,你挖到第一個寶箱,開啟一看,裡面放的是另一張藏寶圖,然後你根據藏寶圖找到了第二個寶箱,裡面放的就是你要的寶藏,那麼第一個寶箱就像是二級指標,第二個寶箱就像是一級指標,而你想要的寶藏,就像是普通變數。
那麼要在什麼情況下使用呢,一級指標是為了再呼叫函式的,能共享變數,以及優化傳參效率。那麼二級指標就可知道了,那就是為了呼叫函式時,能共享一級指標變數。也就是說,當你呼叫的函式要對一級指標進行修改時,傳參就需要傳遞二級指標。或者當你要傳遞一級指標陣列時(數組裡面存放的是指標變數),傳參時也需要二級指標(跟一級指標傳陣列是一樣的)
總的來說,二級指標和一級指標是差不多一樣的,只不過一級指標指向的是普通變數,二級指標指向的是指標變數,理論上來講,也有三級指標,四級指標,但是基本用不到,估計也就學術上,考試上用到三級指標。
9.函式指標
函式指標本身就是指標變數,所以在使用上跟指向變數的指標差不多,那麼函式指標要怎麼樣指向這個函式呢
C語言在編譯時,每個函式都有一個入口地址,那麼函式指標就是指向這個入口地址的指標。
那麼函式指標的用途是什麼,主要有兩點,一個是呼叫函式,另一個時作為函式的引數
函式指標可以體現在回撥函式這一塊內容。
10.陣列指標
陣列指標,就是指向陣列的指標,指的是陣列的首地址。
定義方法:型別 (*p)[陣列大小]
下面舉例說明:
int (*p)[10]; // 因為()優先順序較高,所以p是一個指標,指向一個整形的一維陣列,10就是這個p的跨度(就像int的跨度為4,char為1)
由上面的例子得到二位陣列的定義
定義方法:型別 (*p)[陣列大小]
下面定義一個a[5][10]來舉例說明:
int (*p)[10] // 同一維陣列定義一樣,只不過可以對p進行加減
p++;
printf("%d",p[5]) // p[5] <=>a[1][5];因為p指向的是首地址,當加一後,加的是這個陣列一行的長度,所以p就指向了第二行,
所以p[5]等價於啊a[1][5]
11.指標陣列
指標陣列,就是儲存指標的陣列,這個數組裡面存放的都是指標變數。
定義方法:型別 *p[n]
因為[]優先順序高,先與p結合成為一個數組,再由int*說明這是一個整型指標陣列,它有n個指標型別的陣列元素。
就是一個數組裡面,存的都是指標變數。
那麼如何要將二維陣列賦給一指標陣列呢:
就是該指標陣列的第一個元素,指向二維陣列的的一行,第二個元素指向第二行,以此類推
int *p[3];
int a[3][4];
p++; //該語句表示p陣列指向下一個陣列元素。注:此陣列每一個元素都是一個指標
for(i=0;i<3;i++)
p[i]=a[i]
這裡int *p[3] 表示一個一維陣列記憶體放著三個指標變數,分別是p[0]、p[1]、p[2]
所以要分別賦值。
這樣指標陣列和陣列指標的區別就好理解了,陣列指標只是一個指標變數,指向的是一個數組。而指標陣列是一個數組裡面存放個指標變數。
主要怎麼區別指標陣列和陣列指標,需要看它是怎麼定義的,就需要了解一些符號的優先順序:()>[]>*
12.結構體指標
結構體指標也是一種指標,用法在我感覺上,跟二維陣列指標有點像,如:
#include <stdio.h>
typedef struct Student
{
char name[20];
int age;
}Stu;
int main()
{
Stu stu[3] = {{"Dragon",21},{"Lin",21},{"Shan",21}};
Stu *p = &stu[0];
for(int i = 0;i < 3;i++,p++)
{
printf("姓名:%s,年齡:%d\n",p->name,p->age);
}
}
輸出:姓名:Dragon,年齡:21
姓名:Lin,年齡:21
姓名:Shan,年齡:21
13.結構體成員指標
當結構體中,有一個成員是指標時,指向的就是該成員所佔記憶體段的起始地址,如果2個結構體成員指標共同指向一個地方的,當其中一個結構體指標內容跟改變,另一個也會相應改變,如
#include <stdio.h>
#include <string.h>
typedef struct Student
{
char name[20];
int* age;
}Stu;
int main()
{
Stu stu_a;
Stu stu_b;
int stu_age = 20;
strcpy(stu_a.name,"Dragon");
stu_a.age = &stu_age;
printf("姓名:%s,年齡:%d\n",stu_a.name,*stu_a.age);
stu_b = stu_a;
*stu_b.age = 25;
printf("姓名:%s,年齡:%d\n",stu_a.name,*stu_a.age);
}
輸出結果:姓名:Dragon,年齡:20
姓名:Dragon,年齡:25
原因:因為有賦值語句在,所以stu_a裡面的成員一個個拷貝到stu_b中,所以這兩個結構體裡的int* age指向了同一塊地方,所以當對其中任
何一個結構體修改年齡時,另一個的年齡也會相應改變
14.指標與堆記憶體配合
指標與堆記憶體的關係還是很密切的,因為C語言中沒有提供管理記憶體的語句,只能依靠標準庫中提供的函式來對堆進行申請和釋放,而且堆記憶體也無法對識別符號建立聯絡,所以想要使用堆記憶體,還得靠使用指標,只能靠指標來配合使用堆記憶體。
那麼要如何使用呢,首先我們要先向堆記憶體申請一塊空間
語法:
型別* p = NULL;
p = malloc(你想申請的大小);
接下來如果申請成功,指標變數p就指向你想使用的那塊堆記憶體,你就可以根據p來對來進行操作。
需要注意的是,如果對堆記憶體使用不當,會產生下列2個問題:
A.記憶體洩漏:
在程式執行期間由於管理失誤導致記憶體無法被釋放,這種情況叫作記憶體洩漏(當一個程式結束後,屬於它的所有資源都會釋放,包括洩漏的記憶體,但有些程式不能停止 7 * 24(一天執行24小時,一週執行7天))。
如休防止記憶體洩漏:
1、指標不輕易改變指向的位置(型別 * const p)。
2、寫完申請記憶體的語句,要立即寫釋放記憶體的語句(誰申請,誰釋放)。
3、要保證記憶體釋放語句初始執行。
如果出現記憶體洩漏怎麼辦:
GDB除錯、打斷點,監控程式的行為。
B.記憶體碎片
記憶體碎片:已經被釋放了,但無法繼續使用的記憶體叫記憶體碎片(在程式執行過程中由於頻繁的申請、釋放小塊的記憶體,導致一些記憶體雖然被釋放但不能形成連續的大塊記憶體而導致無法使用)。
記憶體碎片歸根結底是由於釋放時間與申請時間不協調造成的,因此不可能完全消除(只能儘量減少),有時候也會自動修復。
1、不要頻繁的申請和釋放記憶體。
2、儘量申請大塊的連線的記憶體。
3、優先選擇棧記憶體儲存資料。
15.總結
雖然有很多種指標的型別,但是歸根結底,他們的用法都是相同的,無非是對地址內的變數進行操作,只要知道這一點,任它千變萬化,我自巋然不動。