前言
在學(xué)習(xí)C語言的時候掖看,我們經(jīng)常會遇到指針。也是在入門C語言的難點疏旨,不像Java無論怎么寫,頂多就是會報NullPointerException 空指針異常扎酷,也十分好對程序進行排查檐涝。
而在學(xué)習(xí)C語言的時候會經(jīng)常看到下面的代碼:
int *p;
// 或者
int **p;
// 甚至
int ***p;
變量存儲過程
如:
int a = 10;
printf("a = %d\n", a);
結(jié)果 a = 10
以上程序在計算機內(nèi)部做了些什么呢法挨?
(1)int a;
在棧中定義了一個變量a谁榜,這個a變量會在內(nèi)存中開辟一個int類型大小的空間,即4個字節(jié)凡纳,32位二進制位(但是會根據(jù)不同的系統(tǒng)會有一些差別)窃植。
(2)int a 先 &a拿到a的地址值(假如為:0x622fe09)
在a的自己的那片空間里存放數(shù)值10,由于計算機中所有的數(shù)據(jù)都是二進制存儲的惫企,所以是把10換算成二進制后撕瞧,再放到自己的空間中。
具體在內(nèi)存的存儲空間如下:
(3)變量名實際上是以一個名字代表存儲地址狞尔,在對程序編譯連接時由編譯系統(tǒng)給每一個變量名分配對應(yīng)的內(nèi)存地址丛版,所以從變量中取值,實際上是通過變量名找到對應(yīng)的內(nèi)存地址偏序,從該存儲單元中讀取數(shù)據(jù)页畦。
所以printf的時候就是通過變量名a獲取地址值,然后通過地址值獲取該存儲單元中的值研儒,并輸出豫缨。
指針類型
了解了變量的的存儲過程独令,再看指針變量的存儲過程。
指針本質(zhì)
上面知道變量向計算機申請一塊內(nèi)存在來存放變量的值好芭,然后我們可以通過&
符(即取地址值符)來獲取變量的實際地址燃箭,這個值就是變量所占內(nèi)存塊的起始地址(這個值是虛擬地址,并不是物理內(nèi)存上的地址舍败,只是起到通過這個值去內(nèi)存找到內(nèi)存的值招狸。這個和Java中的HashCode有點類似)
如需打印地址值的話:
int a = 10;
printf("%#X\n", &a);
一般都會得到類似這樣的一個值 0X62FE04
所以變量只是符號化,變量只是為了讓我們編程的時候更加方便邻薯,對人友好裙戏,可數(shù)計算機并不知道變量a,b什么之類的厕诡,計算機只認(rèn)識二進制(也就是010101之類的)累榜。這一點可以通過GCC編譯一個C源碼查看編譯后的代碼得到印證。
所以可以認(rèn)為C會維護一個映射灵嫌,將程序的變量轉(zhuǎn)換為地址壹罚,然后對這個地址進行讀寫。
規(guī)范
(1)和變量一樣醒第,一定是先定義后賦值
int a = 10;
int *p;
p = &a;
printf("%#X\n", p);
(2) 定義方式
int* p;
// 或者
int *p;
這兩種都是可以的渔嚷,都是指針類型,盡管第二種int *p
中的*p
是連著寫的稠曼,但是依然是變量名為p形病,變量類型為int類型的指針(int*)
但是不能像下面這樣定義
int a = 10;
int *p;
p = a;
因為不能把一個具體的值賦值個指針(類型不匹配)
指針存儲過程
如:
int a = 10;
int *p;
p = &b;
(1) 在棧中定義一個指針變量 p, 并在內(nèi)存中開辟和int一樣的內(nèi)存空間霞幅,指針變量也是變量漠吻。
(2) &a拿到a的起始地址值0x622fe09,然后把0x622fe09放到p自己的內(nèi)存空間中
指針的作用
剛開始我們可能會想司恳,既然有變量途乃,為什么還需要指針呢?直接用變量名不行嗎扔傅?
其實這個答案當(dāng)然是可以的耍共,在JavaScript和Java等語言中就是傳值的,如: 我們需要一個功能對一個數(shù)字 乘二我們一般會這樣做:
Java:
public static void main(String[] args) {
int a = 10;
a = doubleHandler(a);
System.out.println(a);
}
public static int doubleHandler(int a) {
return a * 2;
}
但這樣其實也是適用于C語言的(通過傳值的方式)猎塞。
int doubleHandler(int a) {
return a * 2;
}
int main() {
int a = 10;
a = doubleHandler(a);
printf("a的值是: %d\n", a);
return 0;
}
但是C語言卻可以通過傳址(變量地址值)的方式來改變變量试读。
int doubleHandler(int *pa) {
*pa = (*pa) * 2;
}
//
int main() {
int a = 10;
doubleHandler(&a);
printf("此時a的值是:%d\n", a);
return 0;
}
解引用
通過上面的:
*pa = (*pa) * 2;
- doubleHandler方法傳入的是一個地址值是怎樣拿到地址值對應(yīng)的值的?
pa中存儲的是a的地址值荠耽,然后通過運算法*
(即*pa) 即可拿到指針?biāo)傅牡刂穬?nèi)容了钩骇,所以(*pa)
就拿到了a的值。
- 為什么指針也需要類型?
因為指針變量存儲的是變量內(nèi)存的首地址倘屹,至于要從首地址去多少字節(jié)银亲,就需要用指針類型了。如果int類型的指針纽匙,就會從首地址開始提取4個字節(jié)务蝠,char類型的指針則會提取一個字節(jié)其余依次類推, 如下圖:
p指針也是一個變量哄辣,本身村粗也需要占據(jù)一塊內(nèi)存请梢,這塊內(nèi)存存儲的是a變量的首地址。
當(dāng)(*p)的時候力穗,就會從這個首地址連續(xù)去除4個byte,然后通過int類型的編碼方式讀取出來气嫁。
*p = *p 左右的區(qū)別
依然使用上面的例子:
*pa = (*pa) * 2;
可以看到賦值符號兩邊都有 *p
当窗,但是他們的區(qū)別是什么呢?
*pa出現(xiàn)在左邊即是左值寸宵,表示的pa指向int類型變量的內(nèi)存空間崖面,可以將賦值符號右邊的的值賦值給這一塊空間。
*pa出現(xiàn)在右邊即是右值梯影,表示的是pa指向int類型的變量的值巫员。
例子
int main() {
int a = 10;
int b;
int *p = &b;
*p = a;
int c = *p + 1;
printf("b的值是:%d\n", b);
printf("c的值是:%d\n", c);
return 0;
}
結(jié)果為:
b的值是:10
c的值是:11
示意圖如下:
傳值和傳址
傳值過程中,被調(diào)函數(shù)的形參作為被調(diào)函數(shù)的局部變量處理甲棍,即在內(nèi)存中堆棧中開辟空間以存放有主調(diào)函數(shù)放進來的實參的值简识,從而成為了實參的一個拷貝。傳值的特點就是對形參的任何操作不會影響主調(diào)函數(shù)實參的值感猛。
而在傳址的過程七扰,被調(diào)函數(shù)的形參雖然作為局部變量在堆棧中開辟了內(nèi)存空間,但是這時存放的是主調(diào)函數(shù)放進來的實參變量地址陪白。被調(diào)函數(shù)對形參的任何操作處理都會間接尋址颈走,即通過堆棧中的存放的地址值訪問主調(diào)函數(shù)的實參的值,進而相互影響咱士。
例子:
傳值:
#include <stdio.h>
void swap(int a, int b){
int temp;
temp = a;
a = b;
b = temp;
}
int main() {
int a = 10, b = 20;
swap(a, b);
printf("此時a的值是:%d, b的值是: %d\n", a, b);
return 0;
}
結(jié)果:
此時a的值是:10, b的值是: 20
傳址:
#include <stdio.h>
void swap(int *a, int *b){
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int a = 10, b = 20;
swap(&a, &b);
printf("此時a的值是:%d, b的值是: %d\n", a, b);
return 0;
}
結(jié)果:
此時a的值是:20, b的值是: 10
多級指針
多級指針一般會有二級指針(**p
)立由,三級指針(**P
),盡管還有 四五六但是很少見也很少用的到序厉。
例子:
int a = 10;
int *b = &a;
int **c = &b;
int ***d = &c;
上面的 *b
就是一級指針锐膜,**b
就是二級指針。
內(nèi)存示意:
數(shù)組指針
數(shù)組是C自帶的基本數(shù)據(jù)結(jié)構(gòu)脂矫,其實數(shù)組和指針也是有著十分緊密的聯(lián)系的枣耀。
例子
int arr[5] = {10, 20, 8, 7};
printf("%d\n", *arr); // 10
printf("%d\n", arr[0]); // 10
//
printf("%d\n", *(arr + 1)); // 20;
printf("%d\n", arr[1]); // 20
亦或者
int arr[5] = {10, 20, 8, 7};
int *pa = arr; //
printf("%d\n", *pa); // 10
printf("%d\n", pa[0]); // 10
第0個元素的地址稱為數(shù)組的首地址,數(shù)組名實際是指向數(shù)組的首地址的,當(dāng)我們通過下標(biāo)arr[0]
或者*(arr + 1)
去訪問數(shù)組的元素的時候捞奕。
實際上可以看做address[offset], address為首地址即地址起始值牺堰,offset為偏移量,這里的偏移量不是直接和address 相加颅围,而是乘以數(shù)組類型所在字節(jié)數(shù)
address + sizeof(int) * offset;
注意
盡管數(shù)組名有時可以用來當(dāng)做指針使用伟葫,但是數(shù)組名不是指針。
例子:
printf("%u\n", sizeof(arr));
printf("%u\n", sizeof(pa));
結(jié)果是:
20
8
第一個輸出20院促,是因為arr包含了5個int類型的元素筏养,(5 * 4);
第二個輸出8常拓,這個也是根據(jù)不同的系統(tǒng)而定的渐溶,在32位的機器上是4, 在64位的機器上位8弄抬,其實代表了系統(tǒng)的尋址能力茎辐,也就是指針長度
printf("%u\n", sizeof(pa));
// 等于下面的代碼
printf("%d\n", sizeof(int *));
二維數(shù)組
例子:
int arr[3][2] = {{10, 20}, {30, 40}, {50, 60}};
注意
二維數(shù)組和一維數(shù)組是一樣的,沒有本質(zhì)區(qū)別掂恕,都是按照線性排列的
10 20 30 40 50 60
并不是想象中的二維矩陣
10 20
30 40
50 60
當(dāng)我們向arr[1][1]
這樣去訪問的時候拖陆,編譯器是如何去計算他們的地址的呢。
如:
int arr[n][m]
那么訪問訪問arr[a][b]
元素地址的計算方式如下:
arr + (m * a + b);
*p++
一維數(shù)組
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int len = sizeof(arr) / sizeof(int);
int *p = arr;
int i;
for(; i < len; i++){
printf("arr[i] = %d\n", *p++);
}
return 0;
}
結(jié)果:
arr[i] = 10
arr[i] = 20
arr[i] = 30
arr[i] = 40
arr[i] = 50
亦或者
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int len = sizeof(arr) / sizeof(int);
int i;
for(; i < len; i++){
printf("arr[i] = %d\n", *(arr + i));
}
return 0;
}
二維數(shù)組
#include <stdio.h>
int main() {
int arr[3][2] = {{10, 20}, {30, 40}, {50, 60}};
int *p = arr;
int len = sizeof (arr) / sizeof (int);
int i;
for(; i < len; i++){
printf("%d ",*p++);
}
return 0;
}
結(jié)果
10 20 30 40 50 60
亦或者:
int main() {
int arr[3][2] = {{10, 20}, {30, 40}, {50, 60}};
int i, j;
for(i = 0; i < 3; i++){
for(j = 0; j < 2; j++){
printf("arr[%d][%d] = %d\n", i, j, *(*(arr + i) + j));
}
}
return 0;
}
結(jié)果:
arr[0][0] = 10
arr[0][1] = 20
arr[1][0] = 30
arr[1][1] = 40
arr[2][0] = 50
arr[2][1] = 60
函數(shù)指針
指針函數(shù)
#include <stdio.h>
int max(int a, int b) {
return a > b ? a : b;
}
int main() {
int a = 10, b = 20, maxVal;
int (*pmax)(int, int) = max;
maxVal = pmax(a, b);
printf("maxVal = %d\n", maxVal);
return 0;
}
解釋:
int (*pmax)(int, int) = max;
pmax是指針的名稱懊亡, int表示該函數(shù)值指針返回的是int類型數(shù)據(jù)依啰,(int,int) 表示該函數(shù)指針指向的函數(shù)形參是接收兩個int店枣。
回調(diào)函數(shù)
我們在JavaScript或者其他語言中會經(jīng)常用到回調(diào)函數(shù)速警。
通過指針函數(shù)我們也可以實現(xiàn)回調(diào)函數(shù)。
例子:
#include <stdio.h>
int callBack01() {
printf("callback01 handler\n");
return 0;
}
int callBack02() {
printf("callback02 handler\n");
return 0;
}
int callBack03() {
printf("callback03 handler\n");
return 0;
}
int runCallBack(int (*callback)()) {
printf("進入了 runCallBack function!\n");
callback();
printf("離開了 runCallBack function!\n\n");
}
int main() {
//
runCallBack(callBack01);
runCallBack(callBack02);
runCallBack(callBack03);
//
return 0;
}
結(jié)果:
進入了 runCallBack function!
callback01 handler
離開了 runCallBack function!
-
進入了 runCallBack function!
callback02 handler
離開了 runCallBack function!
-
進入了 runCallBack function!
callback03 handler
離開了 runCallBack function!
0地址
- 1.當(dāng)然你的內(nèi)存中有0地址, 但是0地址通常是個不能隨便碰的地址
- 2.所以你的指針不應(yīng)該具有0值
- 3.因此可以用0地址來表示特殊的事情
- 4.返回的指針是無效的
- 5.指針沒有被真正初始化(先初始化為0)
- NULL是一個預(yù)定定義的符號, 表示0地址
- 有的編譯器不愿意你用0來表示0地址.
void指針
對于void指針表示的是通用指針艰争,可以用來存放任何數(shù)據(jù)類型引用坏瞄。
void *ptr; // 定義一個void類型指針
void指針的用處就是在C語言中實現(xiàn)了泛型編程(或者動態(tài)內(nèi)存分配),因此任何指針都可以賦值給void指針甩卓,void指針也可以被轉(zhuǎn)換為原來的指針類型鸠匀,并且這個過程指針的實際所指向的地址不會發(fā)生變化。
例子1:
int num;
int *pi = #
printf("address of pi: %#X\n", pi);
void* pv = pi;
pi = (int *) pv;
printf("address of pi: %#X\n", pi);
結(jié)果:
address of pi: 0X62FE0C
address of pi: 0X62FE0C
例子2:
#include <stdio.h>
int main () {
int a = 3; // 定義a為整型變量
int *p1 = &a; // p1指向int型變量
char *p2; // p2指向char型變量
void *p3; // p3為無類型指針變量
p3 = (void *)p1; // 將p1的值轉(zhuǎn)換為 void*類型, 然后賦值給p3
p2 = (char *)p3;
printf("%d", *p2);
printf("%d", *p2);
// printf("%d", *p3); // 錯誤的
return 0;
}
結(jié)構(gòu)體指針
結(jié)構(gòu)體可以包含多個成員逾柿,這些成員也是和數(shù)組一樣的排列著的缀棍。
例子
struct person {
int age;
int height;
int weight;
};
struct person p1;
p1.age = 18;
p1.height = 180;
p1.weight = 60;
printf("%#X\n", &p1.age);
printf("%#X\n", &p1.height);
printf("%#X\n", &p1.weight);
printf("p的體積%d\n", sizeof (p1));
使用指針注意事項
(1) 用指針作為函數(shù)返回值需要注意, 函數(shù)運行結(jié)束后會銷毀內(nèi)部定義的所有局部數(shù)據(jù), 包括局部變量, 局部數(shù)據(jù)和形式參數(shù), 函數(shù)返回的指針不能指向這些數(shù)據(jù)。
(2) 函數(shù)運行結(jié)束后會銷毀該函數(shù)所有的局部數(shù)據(jù). 這里所謂的銷毀并不是將局部數(shù)據(jù)所占用的內(nèi)存全部清零, 而是程序放棄對它的使用權(quán)限, 后面的代碼可以使用這塊內(nèi)存.
(3) c語言不支持調(diào)用函數(shù)時返回局部變量的地址, 如果確實有這樣的需求, 需要定義局部變量未static變量.
所以返回本地變量地址是危險的机错,返回全局變量或者靜態(tài)變量的地址是安全的爬范,
返回在函數(shù)內(nèi)的 malloc 的內(nèi)存是安全的, 但是容易造成問題,
最好的做法是返回傳入的指針
Tips
- 1.不要使用全局變量來在函數(shù)間傳遞參數(shù)和結(jié)果
- 2.盡量避免使用全局變量
- 3.使用全局變量和靜態(tài)本地變量的函數(shù)是線程不安全的