一、指針的本質:變量,指針變量就是指針變量
int *p:兩個變量,一個p(指針變量本身)是int *類型的
另一個是*p(指針指向的那個變量)是int類型的
注:指針說白了就是指針類型,前面定義的int類型是為了說明指針指向的那個數的類型,所以指針的解析方式都是按地址來解析的
(不管你是char *還是double *,解析方式都是地址)而指向的那個數的類型就要看你怎么定義的了
例如:
int *a
a是按照地址來解析的;*a則是按照int類型來解析的。
(1)所有的類型的數據存儲在內存中,都是按照二進制格式存儲的。所以內存中只知道有0和1,不知道是int的、還是float的還是其他類型
可查詢這些類型的存儲方式(數轉換成二進制往內存中放的方式)
int、char、short等的存儲類型相同只是內存格子大小不同(所以這幾種整形就彼此叫二進制兼容格式);
float和double的存儲方式彼此不同,和整形更不同。
例如:
1 int a = 5; 2 3 printf("a = %d.\n", a); // 5 4 printf("a = %f.\n", a); //亂碼 5 總結:怎么定義就要怎么取,int a = 5;則是分配4個單元格來裝0b1010,然后存入單元格里
例2:
int a = 20; char *p1 = &a; printf("*p1 = %d.\n", *p1); 注:char的范圍是-127~127,在這范圍內取出來則不會出錯,如果超出來則會出錯 所以用int取char不會出錯,但是char取int就有可能出錯也有可能不出錯
二、野指針(沒有定義具體地址的指針,它可以隨便指向任意的地址)
1.當一個指針為野指針時,不可亂去解引用,會產生很大的問題
2.野指針因為指向地址是不可預知的,所以有3種情況:
第一種是指向不可訪問(操作系統不允許訪問的敏感地址,譬如內核空間)的地址,結果是觸發段錯誤,這種算是最好的情況了;
第二種是指向一個可用的、而且沒什么特別意義的空間(譬如曾經使用過但是已經不用的棧空間或堆空間),這時候程序運行不會出錯,也不會對當前程序造成損害,這種情況下會掩蓋你的程序錯誤,讓你以為程序沒問題,其實是有問題的;
第三種情況就是指向了一個可用的空間,而且這個空間其實在程序中正在被使用(譬如說是程序的一個變量x),那么野指針的解引用就會剛好修改這個變量x的值,導致這個變量莫名其妙的被改變,程序出現離奇的錯誤。一般最終都會導致程序崩潰,或者數據被損害。這種危害是最大的。
3.指針變量如果是局部變量,則分配在棧上,本身遵從棧的規律(反復使用,使用完不擦除,所以是臟的,本次在棧上分配到的變量的默認值是上次這個棧空間被使用時余留下來的值),野指針也是有一定規律的,但沒意義
4.避免野指針方法:在指針的解引用之前,一定確保指針指向一個絕對可用的空間。
常規的做法是:
int *p = NULL; //第一步:定義指針時,同時初始化為NULL //一堆代碼 //第二步:這期間使用指針時,給它一個有意義的地址 //int a = 3; //p = &a; if ( NULL != P) //第三步:在指針解引用之前,先去判斷這個指針是不是NULL { *P = 5; } P = NULL; //第四步:指針使用完之后,將其賦值為NULL 注: 一般在判斷指針是否野指針時,都寫成 if (NULL != p) 而不是寫成 if (p != NULL) 原因是:如果NULL寫在后面,當中間是==號的時候,有時候容易忘記寫成了=,這時候其實程序已經錯誤,但是編譯器不會報錯。
這個錯誤(對新手)很難檢查出來;如果習慣了把NULL寫在前面,當錯誤的把==寫成了=時,編譯器會報錯。
5.什么是NULL
(1)NULL在C/C++中定義為:
1 #ifdef _cplusplus // 定義這個符號就表示當前是C++環境 2 #define NULL 0 // 在C++中NULL就是0 3 #else 4 #define NULL (void *)0 // 在C中NULL是強制類型轉換為void *的0 5 #endif
(2)在C語言中,int *p;你可以p = (int *)0;但是不可以p = 0;因為類型不相同。
(3)所以NULL的實質其實就是0,然后我們給指針賦初值為NULL,其實就是讓指針指向0地址處。第一,0地址處作為一個特殊地址(我們認為指針指向這里就表示指針沒有被初始化,就表示是野指針);第二,0地址在一般的操作系統中都是不可被訪問的,如果C語言程序員不按規矩(不檢查是否等於NULL就去解引用)寫代碼直接去解引用就會觸發段錯誤
三、數組
幾個關鍵符號(a a[0] &a &a[0])的理解(前提是 int a[10])
1.a就是數組名。a做左值時表示整個數組的所有空間(10×4=40字節),又因為C語言規定數組操作時要獨立單個操作,不能整體操作數組,所以a不能做左值;a做右值表示數組首元素(數組的第0個元素,也就是a[0])的首地址(首地址就是起始地址,就是4個字節中最開始第一個字節的地址)。a做右值等同於&a[0];
2.a[0]表示數組的首元素,也就是數組的第0個元素。做左值時表示數組第0個元素對應的內存空間(連續4字節);做右值時表示數組第0個元素的值(也就是數組第0個元素對應的內存空間中存儲的那個數)
3.&a就是數組名a取地址,字面意思來看就應該是數組的地址。&a不能做左值(&a實質是一個常量,不是變量因此不能賦值,所以自然不能做左值。);&a做右值時表示整個數組的首地址。
4.&a[0]字面意思就是數組第0個元素的首地址。做左值時表示數組首元素對應的內存空間,做右值時表示數組首元素的地址,做右值時&a[0]等同於a。
總結:
1:&a和a做右值時的區別:&a是整個數組的首地址,而a是數組首元素的首地址。這兩個在數字上是相等的,但是意義不相同。意義不相同會導致他們在參與運算的時候有不同的表現。
2:a和&a[0]做右值時都是表示首元素的地址,完全可以互相替代。
3:&a是常量,不能做左值。
4:a做左值代表整個數組所有空間,所以a不能做左值。
1、函數與指針
1.1.當變量作為形式參數時傳遞進去的是值(傳值調用)
eg.
1 void swap1(int a, int b) 2 { 3 int tmp; 4 tmp = a; 5 a = b; 6 b = tmp; 7 printf("in swap1, a = %d, b = %d.\n", a, b); 8 } 9 10 void swap2(int *a, int *b) 11 { 12 int tmp; 13 tmp = *a; 14 *a = *b; 15 *b = tmp; 16 printf("in swap1, *a = %d, *b = %d.\n", *a, *b); 17 } 18 19 int main(void) 20 { 21 int x = 3, y = 5; 22 swap1(x, y); 23 printf("x = %d, y = %d.\n", x, y); // x=3,y=5,交換失敗 24 25 int x2 = 3, y2 = 5; 26 swap2(&x2, &y2); 27 printf("x = %d, y = %d.\n", x2, y2); //x=5,y=3 交換成功 28 29 return 0; 30 } 31 注:第一個是傳值調用,第二個是傳址調用 32 實質:都是傳值調用,第一個傳的是變量的值,第二個傳的是指針變量的值
2、數組作為函數形參
2.1.組名作為形參,實際傳遞是不是整個數組,而是數組的首元素的首地址(首元素的指針)
2.2.在子函數內傳參得到的數組首元素首地址,和外面得到的數組首元素首地址的值是相同的。類似“傳址調用”
3、指針作為函數形參
和數組作為函數形參是一樣的
例子:
1 void func3(int *a) 2 { 3 printf("sizeof(a) = %d.\n", sizeof(a)); 4 printf("in func3, a = %p.\n", a); 5 } 6 7 void func2(int a[]) 8 { 9 printf("sizeof(a) = %d.\n", sizeof(a)); 10 printf("in func2, a = %p.\n", a); 11 } 12 13 int main(void) 14 { 15 int a[5]; 16 printf("a = %p.\n", a); 17 func2(a); /* 18 a = 0x7ffff2bdc8b0. 19 sizeof(a) = 8.(我裝的是64位的系統) 20 in func2, a = 0x7ffff2bdc8b0. 21 */ 22 23 int a3[5]; 24 printf("a = %p.\n", a3); 25 func3(a3); //同上 26 } 27 注:數組名直接打印和數組名作為形參,指針作為形參都是一樣的結果
難點:
void fun(int b[100])
{
sizeof(b);
}
注:
(1)函數傳參,形參是可以用數組的
(2)函數形參是數組時,實際傳遞不是整個數組,而是數組的首元素首地址。也就是說函數傳 參用數組來傳,實際相當於傳遞的是指針(指針指向數組的首元素首地址)。
// func完全等同於func1 void func(int a[]) { printf("數組大小 = %d.\n", sizeof(a)); } void func1(int *a) { printf("數組大小 = %d.\n", sizeof(a)); }
4、結構體作為形參(后續.....)
五、const關鍵字與指針
1、const修飾指針的4種形式
(1)const關鍵字,在C語言中用來修飾變量,表示這個變量是常量。(一個const關鍵字只能修飾一個變量)
(2)const修飾指針有4種形式:
const int *p1; // p本身不是cosnt的,而p指向的變量是const的(*p1不可改變)
int const *p2; // p本身不是cosnt的,而p指向的變量是const的(*p2不可改變)
int * const p3; // p本身是cosnt的,p指向的變量不是const的 (p3不可改變)
const int * const p4; // p本身是cosnt的,p指向的變量也是const的 (p4和*p4都不可改變)
若真的需要修改const上的值,則需要用強制類型轉換進行修改,騙過編譯器,和普通全局變量一樣將代碼存入data段,
這樣程序運行時,發現數據前沒有const這個關鍵字,就直接運行了(某些單片機無法修改)
2、函數傳參中使用const指針
例子:
1 char *p1; 2 const char *p2; 3 char *pstr = "hello"; 4 //char pstr[]= "hello"; 5 p1 = pstr; 6 p2 = pstr; 7 *p1 = a; 8 *p2 = a; 9 pritf("pstr = %c.\n",*p1); //若采用char *pstr 的話編譯器不會報錯誤,但在執行時會出現段錯誤 10 pritf("pstr = %c.\n",*p2); //若采用char *pstr 的話編譯器就會報錯誤,*p2為const類型,只讀 11 12 注:出現段錯誤的原因是,字符串存儲在代碼段,代碼段是個不可更改的區域,所有執行時會出現錯誤 13 若采用char pstr[]數組的話*p1即可更改,數組好像存放在數據區(不大確定)
補充:
#define dpChar char * typedef char *tpChar; //typedef用來重命名類型,或者說用來制造用戶自定義類型 dpChar p1, p2; // 展開:char *p1, p2; 相當於char *p1, char p2; tpChar p3, p4; // 等價於:char *p3, char *p4;
(參考朱有鵬老師)