C語言指針詳解

前言

在學(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)存的存儲空間如下:

內(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é)其余依次類推, 如下圖:

image-20210405115229648

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 = &num;
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));
結(jié)構(gòu)體存儲

使用指針注意事項

  • (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ù)是線程不安全的
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市弱匪,隨后出現(xiàn)的幾起案子青瀑,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件斥难,死亡現(xiàn)場離奇詭異枝嘶,居然都是意外死亡,警方通過查閱死者的電腦和手機哑诊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門群扶,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人镀裤,你說我怎么就攤上這事竞阐。” “怎么了暑劝?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵骆莹,是天一觀的道長。 經(jīng)常有香客問我担猛,道長汪疮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任毁习,我火速辦了婚禮,結(jié)果婚禮上卖丸,老公的妹妹穿的比我還像新娘纺且。我一直安慰自己,他們只是感情好稍浆,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布载碌。 她就那樣靜靜地躺著,像睡著了一般衅枫。 火紅的嫁衣襯著肌膚如雪嫁艇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天弦撩,我揣著相機與錄音步咪,去河邊找鬼。 笑死益楼,一個胖子當(dāng)著我的面吹牛猾漫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播感凤,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼悯周,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了陪竿?” 一聲冷哼從身側(cè)響起禽翼,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后闰挡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锐墙,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年解总,在試婚紗的時候發(fā)現(xiàn)自己被綠了贮匕。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡花枫,死狀恐怖刻盐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情劳翰,我是刑警寧澤敦锌,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站佳簸,受9級特大地震影響乙墙,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜生均,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一听想、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧马胧,春花似錦汉买、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至威彰,卻和暖如春出牧,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背歇盼。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工舔痕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人旺遮。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓赵讯,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耿眉。 傳聞我的和親對象是個殘疾皇子边翼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容