C語言指針——從入門到精通-創(chuàng)新互聯(lián)

〇、前言

本文是本人在學(xué)習(xí) C語言的過程中所積累的對 C語言指針的感悟,可能會有些地方描述不準確,還請指出。本文遵循一般文章結(jié)構(gòu),從簡單到難,從基本概念到抽象總結(jié)。適合任何任何學(xué)習(xí) C語言的人群。

我們提供的服務(wù)有:成都網(wǎng)站設(shè)計、做網(wǎng)站、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認證、來賓ssl等。為超過千家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的來賓網(wǎng)站制作公司一、指針的概念

指針的值就是某一個變量的內(nèi)存地址,指針變量就是用來存放某個變量的內(nèi)存地址的變量,和廣義的變量沒有什么區(qū)別。

在同一CPU構(gòu)架下,不同類型的指針變量所占用的存儲單元長度是相同的。這是因為操作系統(tǒng)的位數(shù)與其所能支持的大內(nèi)存有直接的關(guān)系。由于計算機是按照字節(jié)尋址的,如在 32 位操作系統(tǒng)下,32位比特位一共能描述 2^32 個狀態(tài),一個狀態(tài)標記大小為 1B(一般定義8位(bit,比特)為一字節(jié)),所以一共有 2^32*1B = 4GB。因此 32 位系統(tǒng)所能支持的大內(nèi)存為 4GB。而對于 64 位操作系統(tǒng)(目前主流操作系統(tǒng)),所能支持的大內(nèi)存為 2^64*1B = 17179869184GB,這是一個很大的數(shù),基本上可以支持任何現(xiàn)實中任意存在的內(nèi)存。

而指針最小就是以字節(jié)為單位進行指引,因此對于 32 位操作系統(tǒng),它也要是 32 位的,即 4 字節(jié)大小;而對于 64 位操作系統(tǒng),它就得是 64 位 即 8 字節(jié)大小。

我們可以寫一個程序驗證下:

#includeint main() {int a = 4;
    int* p = &a;
    printf("%zu\n", sizeof(p));
    // 指針變量占據(jù) 8 個字節(jié)
    printf("%p\n", p);
    // 變量 a 存放在以內(nèi)存地址為0x16f85f71c開頭的內(nèi)存單元中
}

輸出結(jié)果為:

8
0x16f85f71c

在 C/C++語言中,指針一般被認為是指針變量,指針變量的內(nèi)容存儲的是其指向的對象的首地址,指向的對象可以是變量(指針變量也是變量),數(shù)組,函數(shù)等占據(jù)存儲空間的實體。

二、指針詳解

以下的例子由易到難,每次增長一顆星。

(一)★
#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;
    printf("%p\n%p\n", p1, p2);
    return 0;
}

輸出為:

0x16bacb718
0x16bacb714

這是一個很簡單的例子,程序中創(chuàng)建了兩個局部變量,然后利用指針輸出它們的地址。其中&是取地址符,取出來以后賦給兩個指針變量p1、p2,并將其打印出來。
細心的話可以發(fā)現(xiàn)這兩個地址由大到小,相差為 4。這說明棧中的這兩個變量恰好相鄰,且一個int類型的變量占用 4B大小空間。而且還說明??臻g的增長方向是由大到小的,當然我們的主題是指針,這里不再贅述。

(二)★★
#includeint main() {int a = 0;
    int* p = &a;
    printf("通過指針修改前 a 的值為:%d\n", a);
    *p = 10;
    printf("通過指針修改后 a 的值為:%d\n", a);
    return 0;
}

輸出為:

通過指針修改前 a 的值為:0
通過指針修改后 a 的值為:10

這里就比上面稍微多了一點東西,就是可以通過指針去修改被該指針所指的變量的值。換句話說,就是間接修改變量的值。其中*p的含義為解引用,指代的就是變量a。

(三)★★★
#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;

    printf("a在%p\n", p1);
    printf("b在%p\n", p2);

    int **pp1 = &p1, **pp2 = &p2;

    printf("a的指針p1在%p\n", pp1);
    printf("a的指針p1在%p\n", pp2);
    
    return 0;
}

輸出為:

a在0x16da73718
b在0x16da73714
a的指針p1在0x16da73708
a的指針p1在0x16da73700

可以看到,指針變量也有一個內(nèi)存地址,它指向的是一個指針變量的內(nèi)存地址。那么,我們同樣可以通過二級指針對一級指針作出修改:

#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;

    printf("a在%p\n", p1);
    printf("b在%p\n", p2);

    int **pp1 = &p1, **pp2 = &p2;

    printf("a的指針p1在%p\n", pp1);
    printf("b的指針p2在%p\n", pp2);

    // 交換兩個二級指針
    int** temp = pp1;
    pp1 = pp2;
    pp2 = temp;
    printf("a的指針p1在%p\n", pp1);
    printf("b的指針p2在%p\n", pp2);
    
    return 0;
}

輸出為:

a在0x16fa0b718
b在0x16fa0b714
a的指針p1在0x16fa0b708
b的指針p2在0x16fa0b700
a的指針p1在0x16fa0b700
b的指針p2在0x16fa0b708

這里需要提一點,就是當一個變量(前提是一個變量)為表達式左值時,它代表一個變量,當其為右值時,它代表變量的值。關(guān)于左值右值的概念,不再贅述。

交換前的指針關(guān)系:
在這里插入圖片描述
交換后的指針關(guān)系:
在這里插入圖片描述
我們還可以嘗試通過pp2來直接修改變量a的值:

#includeint main() {int a = 0, b = 1;
    int *p1 = &a, *p2 = &b;

    printf("a在%p\n", p1);
    printf("b在%p\n", p2);

    int **pp1 = &p1, **pp2 = &p2;

    printf("a的指針p1在%p\n", pp1);
    printf("a的指針p1在%p\n", pp2);

    // 交換兩個二級指針
    int** temp = pp1;
    pp1 = pp2;
    pp2 = temp;
    printf("a的指針p1在%p\n", pp1);
    printf("a的指針p1在%p\n", pp2);
    
    **pp2 = 10;
    printf("修改后a的值為:%d\n", a);
    return 0;
}

輸出為:

a在0x16b1ff718
b在0x16b1ff714
a的指針p1在0x16b1ff708
a的指針p1在0x16b1ff700
a的指針p1在0x16b1ff700
a的指針p1在0x16b1ff708
修改后a的值為:10

其中二級指針pp2解引用了兩次,才與a等價。這里的例子大量使用了二級指針,即int**類型的變量。具體來說,指針類型有多種,比如int* p,int** p,int*** p等等。

(四)★★★★

前面幾節(jié)都是很簡單的概念,接下來幾節(jié)將會引入數(shù)組、字符串等來對指針的使用進行更深的闡述。

(1)關(guān)于數(shù)組名
#includeint main() {int a[3] = {1, 2, 3};
    printf("%p\n", a);
    printf("%zu\n", sizeof(a));
}

輸出為:

0x16fc9b708
12

可以看到,數(shù)組名 a 是一個指針類型的值,它實際上指向的是大小為 12B 的連續(xù)空間的首地址。數(shù)組名其實是一個常量,因此它不能當做左值。當我們修改它的值時,它會提示array type 'int [3]' is not assignable,意思是這個變量不可被修改。另外,a 的大小為 12B 這表明和普通的指針并不一樣,或者a并不是簡單的指針。因此,很多參考書上說“數(shù)組名本質(zhì)上就是一個指針”的說法是完全錯誤的。

(2)利用數(shù)組名來訪問某個內(nèi)存單元

我們剛提到,數(shù)組名是一個常量,因此我們可以利用一個指針變量來訪問內(nèi)存單元:

#includeint main() {int a[3] = {1, 2, 3};
    int* p = a;
    for (int i = 0; i< 3; i++) {printf("%d ", *(p + i));
    }
    return 0;
}

輸出為:

1 2 3

其中p+i的含義是,指針指向了p所指的內(nèi)存再向右偏移i個類型為int的內(nèi)存單元。之所以是int,是因為我們聲明的是int類型的指針。比如:

#includeint main() {int a[3] = {1, 2, 3};
    
    int* p = a;
    printf("%p\n", p);
    printf("%p\n", p + 1);
    printf("%p\n", p + 2);

    long *q = a;
    printf("%p\n", q);
    printf("%p\n", q + 1);

    return 0;
}

輸出為:

0x16f1c3708
0x16f1c370c
0x16f1c3710
0x16f1c3708
0x16f1c3710

可以看到,因為long類型占 8 個字節(jié)(long的定義為不少于int類型的大小,有些計算機系統(tǒng)longint等價,但大部分long類型都占 8 字節(jié)),int占 4 個字節(jié),所以long類型的指針偏移的長度為int類型的兩倍。同樣,我們可以搞點稍微復(fù)雜的事情:

#includeint main() {int a[3] = {1, 2, 3};

    printf("數(shù)組的首地址為:%p\n", a);

    int* p = (int*)(&a + 1);
    int* q = a + 3;
    printf("此時 p 指向:%p\n", p);
    printf("此時 q 指向:%p\n", q);
    return 0;
}

輸出為:

數(shù)組的首地址為:0x16b25f708
此時 p 指向:0x16b25f714
此時 q 指向:0x16b25f714

這說明,&a + 1的偏移量為3個 int 類型大小。這里就需要格外注意,指針偏移一個單位是參考哪一種類型的變量指針的。這里參考的是int a[3],也就是說,a的數(shù)據(jù)類型為int[3],偏移一次當然偏移 12B了,因為int[3]的大小就是 12B!

我們就可以很簡單地預(yù)見以下的程序輸出:

#includeint main() {int a[3] = {1, 2, 3};

    int* p = (int*)(&a + 1);
    printf("%d\n", *(p - 1));
    return 0;
}

輸出為:

3

因為p指向的是數(shù)組最后一個元素的下一個內(nèi)存單元,又因為pint類型的指針,因此一個偏移量大小為int,所以p-1之后,就指向了數(shù)組的最后一個元素。

(3)字符串

字符串是一個重點,也是一個難點。但是只要掌握指針基本概念,把握字符數(shù)組和字符串的區(qū)別和聯(lián)系,也是送分題。

首先得了解下字符串輸出的基本原理:

#includeint main() {char c[3] = {'a', 'b', 'c'};
    printf("%s\n", c);
    return 0;
}

輸出為:

abc*/

為什么會有這么奇怪的輸出呢?這是因為printf("%s\n", c)輸出字符串時,只要輸入一個指針(任何類型的指針都可以),就會一直打印,直到遇到'\0'為止。比如下面這個程序:

#includeint main() {int a[3] = {100, 101, 102};
    printf("%s\n", a);
    return 0;
}

輸出結(jié)果為:

d

這是因為數(shù)據(jù)存放為大端方式,100存在到第一個高地址字節(jié)空間后,后面三個存放的都是'\0',所以就停止打印了。我們可以印證一下:
假設(shè)我們想要打印出"abc",這就要保證內(nèi)存單元里面放的是97\98\99\00。即0x61626300。大端方式存入內(nèi)存單元就為:0x00636261,這個數(shù)字 10 進制大小為6513249,因此:

#includeint main() {int a[3] = {6513249,100,200};
    printf("%s\n", a);
    return 0;
}

程序輸出為:

abc

果然輸出了"abc",這無疑是一件令人激動的事情!當然,這里的重點是字符串輸出,就不再贅述其它了。

經(jīng)過上面的例子,可以充分地說明輸出函數(shù)的特性,即只要是一個指針,丟給printf("%s\n", a)后,就會打印出結(jié)果。

所以字符串是使用空字符'\0'結(jié)尾的一組數(shù)據(jù),就這么簡單。我們可以很容易地構(gòu)造出一些字符串,比如我們上面通過一些手段構(gòu)造的"abc",以及下面用字符數(shù)組構(gòu)造的字符串(常用手段):

#includeint main() {char string[12] = {'H','e','l','l','o','w','o','r','l','d','!','\0'};
    printf("%s\n",string);

    char *string1 = "Helloworld!";
    printf("%s\n",string1);

    return 0;
}

在用字符數(shù)組構(gòu)造字符串時,一定要注意在最后一個內(nèi)存單元加上’\0’,因為我們不能保證字符串結(jié)束后的下一個內(nèi)存單元放的是不是’\0’,因此字符串可能不會正常終止。

還可以使用字面值常量來創(chuàng)建字符串,比如char *string1 = "Helloworld!"。這種不需要在在最后一個內(nèi)存單元加上'\0',編譯系統(tǒng)會自動加。

(4)字符串賦值

為了明白字符串賦值的本質(zhì),這里不會使用庫函數(shù)提供的各種函數(shù),比如strcpy(),strcat()等。

#includeint main() {char string[12] = {'H', 'e', 'l', 'l', 'o', 'w',
                       'o', 'r', 'l', 'd', '!', '\0'};
    char string1[12] = {0};  // 初始化與否都可以,因為緊接著我們就要對其賦值
    for (int i = 0; i< 12; i++) {string1[i] = string[i];
    }
    printf("%s\n", string1);

    return 0;
}

本例采用了逐個字符賦值的方法來完成對字符串的賦值。我們的主題是指針,那么可不可以像下面那樣賦值:

#includeint main() {char string[12] = {'H', 'e', 'l', 'l', 'o', 'w',
                       'o', 'r', 'l', 'd', '!', '\0'};
    char string1[12] = {0};  // 初始化與否都可以,因為緊接著我們就要對其賦值
    string1 = string;
    printf("%s\n", string1);

    return 0;
}

當然不可以,前文已經(jīng)提到,數(shù)組名只一個地址常量,既然是常量當然就不可以被修改,自然就不能當做左值。事實上,上文還提到了通過字面值常量來創(chuàng)造字符串,既然是常量,那么自然也就不能修改,比如:

#includeint main() {char* p = "Helloworld!";
    *(p + 1) = 'E';
    printf("%s\n", p);
    return 0;
}

這個程序的目的是把"Helloworld"改為"HElloworld",那么目的能達到嗎?自然不能,編譯器會報錯bus error。我們不去管這個錯誤的具體含義,只需要知道常量字符串是無法修改的。也就是說,我們的指針并不是指在哪里就改哪里,那計算機系統(tǒng)就亂套了!

如果我們迫切的想要修改,可以這樣做:

#include#include#includeint main() {char* p = "Helloworld!";
    int len = strlen(p);
    char* newString = (char*)malloc(sizeof(char) * (len + 1));
    for (int i = 0; i< len; i++) {newString[i] = p[i];
    }
    newString[len] = '\0';
    newString[1] = 'E';
    printf("%s\n", newString);

    return 0;
}

輸出結(jié)果為:

HElloworld!

過程也很簡單,就是申請一塊內(nèi)存空間,先復(fù)制過來,然后再修改成想要的樣子。

(五)★★★★★

開始之前,我們先來談?wù)勈裁词悄涿麛?shù)組。顧名思義,匿名就是藏起來名字的意思。比如:

#includeint main() {int* p = (int[2]){19, 20};
    return 0;
}

我們定義了一個匿名的數(shù)組,并賦給一個int*類型的變量 p。匿名類在 Java 中 的作用之一是起到很好的封裝性,同樣在 C語言中也有這樣的作用。這個數(shù)組,只能通過 p 訪問。你可能注意到了,我并沒有用int類型的指針去描述 p,而是用int*類型來描述 p。

(1)數(shù)據(jù)類型

C語言中的數(shù)據(jù)類型有哪些?一般來說,有四大類型,分別是基本類型,構(gòu)造類型,空類型以及我們講的指針類型。
在這里插入圖片描述

指針類型是花樣最多的一種類型,它通常包括 5 類:int*,char*,int**,int(*)[]int*[]。
在這里插入圖片描述

換句話說,從語法的角度講,把指針聲明語句里的指針名字去掉,剩下的部分就是這個指針的類型。比如,給出一個例子:

#includeint main() {int a[3] = {1, 2, 3};

    int* p;
    char* s;
    int** q;
    int(*pt)[3];
    int*[3];

    return 0;
}

在這個例子中,指針的類型分別為:int*,char*,int**,int(*)[3],int*[3]。我們經(jīng)常用到數(shù)組指針以及指針數(shù)組,因此重點理解它們就是關(guān)鍵。

(2)指針數(shù)組

對于指針數(shù)組,描述它的語法為int*p[3]。首先,它是一個數(shù)組,所以得是個p[3];其次要是一個指針類型的也就是int*。所以就產(chǎn)生了int*p[3]。數(shù)組 p 中每一個元素都是一個指針,指向一個一維數(shù)組。比如:

#includeint main() {int a[3] = {1,2,3};
    int b[3] = {4,5,6};
    int c[5] = {7,8,9,10,11};

    int *p[3] = {a,b,c};

    return 0;
}

為了更明顯,不如把它寫成int*(p[3]),但是由于[3]的結(jié)合度非常高,因此這個括號可以去掉。

(3)數(shù)組指針

顧名思義,數(shù)組指針就是一個指向數(shù)組的指針。我們這樣定義:·int(*p)[3]·。首先得是一個指針,因此得是(int*p);然后得是一個數(shù)組類型的,因此就是(int*p)[3]。但這樣明顯歧義了,因此就成了int(*p)[3]。它指向一個類型為int[3]的變量。比如:

#includeint main() {int a[3] = {1,2,3};
    int (*p)[3] = &a;
}

a 的數(shù)據(jù)類型為int[3],p 的類型為int(*)[3]。要想讓 p指向 a,我們只需要把變量 a 的地址賦值給 p,即int (*p)[3] = &a。為了更明顯,我們可以這樣修改一下程序:

#includeint main() {int a[3] = {1,2,3};
    int b[4] = {1,2,3,4};
    int (*p)[3];
    p = &a;
    p = &b;
}

可以看到這樣的報錯:incompatible pointer types assigning to 'int (*)[3]' from 'int (*)[4]' [-Wincompatible-pointer-types]。意思是從“int (*)[4]”[-Wincompatible-pointer-types] 分配給“int (*)[3]”的不兼容指針類型。這也從側(cè)面反映了int(*)[3]是一種確定的指針類類型。

能看到這里,想必你已經(jīng)功力深厚,返璞歸真了,哈哈~

三、總結(jié)

這里只總結(jié)難點:

  • 對于聲明一個指針數(shù)組int*p[3],這里的重點是 p 的數(shù)據(jù)類型為int*[3],數(shù)組每一個元素都是一個指針,指向int 類型的數(shù)組
  • 對于聲明一個數(shù)組指針int(*p)[3],這里的重點是 p 的數(shù)據(jù)類型為int(*)[3],p 是一個指針,指向類型為 int[3]的變量。

全文完,感謝你的閱讀。

你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧

網(wǎng)站欄目:C語言指針——從入門到精通-創(chuàng)新互聯(lián)
當前鏈接:http://muchs.cn/article38/dieopp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供企業(yè)建站小程序開發(fā)、動態(tài)網(wǎng)站、全網(wǎng)營銷推廣、微信公眾號、網(wǎng)站維護

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都做網(wǎng)站