日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C/C++ 指针详解

發(fā)布時間:2025/3/8 c/c++ 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C/C++ 指针详解 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

指針詳解

參考視頻:https://www.bilibili.com/video/BV1bo4y1Z7xf/,感謝Bilibili@fengmuzi2003的搬運翻譯及后續(xù)勘誤,也感謝已故原作者Harsha Suryanarayana的講解,RIP。

學(xué)習(xí)完之后,回看找特定的知識點,善用目錄 —>

筆者親測實驗編譯器版本:

gcc版本

gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright ? 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

指針的基本介紹

數(shù)據(jù)在內(nèi)存中的存儲與訪問

在內(nèi)存中,每一字節(jié)(8位)有一個地址。假設(shè)圖中最下面的內(nèi)存地址位0,內(nèi)存地址向上生長,圖中標(biāo)識出的(下面)第一個字節(jié)的地址位201,地址向上生長一直到圖中最上面的地址208。

當(dāng)我們在程序中聲明一個變量時,如int a,系統(tǒng)會為這個變量分配一些內(nèi)存空間,具體分配多少空間則取決于該變量的數(shù)據(jù)類型和具體的編譯器。常見的有int類型4字節(jié),char類型1字節(jié),float類型4字節(jié)等。其他的內(nèi)建數(shù)據(jù)類型或用戶定義的結(jié)構(gòu)體和類的大小,可通過sizeof來查看。

我們聲明兩個變量:

int a; char c;

假如他們分別被分配到內(nèi)存的204-207字節(jié)和209字節(jié)。則在程序中會有一張查找表(圖中右側(cè)),表中記錄的各個條目是變量名,變量類型和變量的首地址。

當(dāng)我們?yōu)樽兞抠x值時,如a = 5,程序就會先查到 a 的類型及其首地址,然后到這個地址把其中存放的值寫為 5。

指針概念

我們能不能在程序中直接查看或者訪問內(nèi)存地址呢?當(dāng)然是可以的,這就用到我們今天的主角——指針。

指針是一個變量,它存放的是另一個變量的地址

  • 指針與它指向的變量 假設(shè)我們現(xiàn)在有一個整型變量a=4存放在內(nèi)存中的204地址處(實際上應(yīng)該是204-207四個字節(jié)中,這里我們用首地址204表示)。在內(nèi)存中另外的某個地址處,我們有另外一個變量 p,它的類型是“指向整型的指針”,它的值為204,即整型變量a的地址,這里的 p 就是指向整型變量 a 的指針。
  • 指針?biāo)嫉膬?nèi)存空間 指針作為一種變量也需要占據(jù)一定的內(nèi)存空間。由于指針的值是一個內(nèi)存地址,所以指針?biāo)紦?jù)的內(nèi)存空間的大小與其指向的數(shù)據(jù)類型無關(guān),而與當(dāng)前機器類型所能尋址的位數(shù)有關(guān)。具體來說,在32位的機器上,一個指針(指向任意類型)的大小為4個字節(jié),在64位的機器上則為8個字節(jié)。
  • 指針的修改 我們可以通過修改指針p的值,來使它指向其他的內(nèi)存地址。比如我們將 p 的值修改為 208,則可以使它指向存放在208地址處的另一個整型變量 b。
  • 指向用戶定義的數(shù)據(jù)類型 除了內(nèi)建的數(shù)據(jù)類型之外,指針也可以指向用戶定義的結(jié)構(gòu)體或者類。

指針的聲明和引用

  • 指針的聲明 在C語言中,我們通過 * 來聲明一個指向某種數(shù)據(jù)類型的指針:int *p。這個聲明的含義即:聲明一個指針變量 p,它指向一個整型變量。換句話說,p 是一個可以存放整型變量的地址的變量。

  • 取地址 如果我們想指定 p 指向某一個具體的整型變量 a,我們可以:p = &a。其中用到了取地址運算符 &,它得到的是一個變量的地址,我們把這個地址賦值給 p,即使得 p 指向該地址。

    這時,如果我們打印p, &a, &p的值會得到什么呢?不難理解,應(yīng)該分別是204,204,64。

  • 解引用 如果我們想得到一個指針變量所指向的地址存放的值,該怎么辦呢?還是用 *放在指針變量 p 前面,即 *p 注意這里的 * 就不再是聲明指針的意思了,而稱為 解引用,即把 p 所指向的對象的值讀出來。 所以如果我們打印 *p,則會得到其所指向的整型變量 a 的值:5。

    實際上,我們還可以通過解引用直接改變某個地址的值。比如 *p = 8,我們就將204地址處的整型變量的值賦為8。此時再打印*p或者a,則會得到8。

關(guān)于*,&兩個運算符的使用,可參考博客:指針(*)、取地址(&)、解引用(*)與引用(&)。

指針代碼示例

指針的算術(shù)運算

實際上,指針的唯一算術(shù)運算就是以整數(shù)值大小增加或減少指針值。如p+1、p-2等

示例程序

考慮以下程序:

#include <stdio.h>int main(){int a = 10;int* p;p = &a;printf("%d\n", p);printf("%d\n", p+1);return 0; }

初學(xué)者可能會好奇,指針p 不是一個常規(guī)意義上的數(shù)字,而是一個內(nèi)存地址,它能夠直接被加1嗎?答案是可以的,但是結(jié)果可能會和整數(shù)的加1結(jié)果不太一樣。

輸出:

358010748 358010752

可以看到p+1比p大了4,而不是我們加的1。

指針的加1

這是因為指針 p 是一個指向整型變量的指針,而一個整型變量在內(nèi)存中占4個字節(jié), 對 p 執(zhí)行加1,應(yīng)該得到的是下一個整型數(shù)據(jù)的地址,即在地址的數(shù)值上面應(yīng)該加4。

相應(yīng)地,如果是p+2的話,則打印出的地址的數(shù)值應(yīng)該加8。

危險

可能會造成危險的是,C/C++并不會為我們訪問的地址進行檢查,也就是說,我們可能通過指針訪問一塊未分配的內(nèi)存,但是沒有任何報錯。這可能會造成我們不知不覺地弄錯了一些數(shù)值。

比如,接著上面的例子,我們試圖打印 p 和 p+1 所指向的地址所存放的值:

#include <stdio.h>int main(){int a = 10;int* p;p = &a;printf("Addresses:\n");printf("%d\n", p);printf("%d\n", p+1);printf("Values:\n");printf("%d\n", *p);printf("%d\n", *(p+1));return 0; }

輸出:

Addresses: -428690420 -428690416 Values: 10 -428690420

可以看到,對指針進行加法,訪問 p+1 所指向的地址的值是沒有意義的,但是C/C++并不會禁止我們這么做,這可能會帶來一些難以察覺的錯誤。

指針的類型

明確指針的類型

首先要明確的是,指針是強類型的,即:我們需要特定類型的指針來指向特定類型的變量的存放地址。如int*、char*等或者指向自定義結(jié)構(gòu)體和類的指針。

指針不是只存放一個地址嗎?為什么指針必須要明確其指向的數(shù)據(jù)類型呢?為什么不能有一個通用類型的指針來指向任意數(shù)據(jù)類型呢?那樣不是很方便嗎?

原因是我們不僅僅是用指針來存儲內(nèi)存地址,同時也使用它來解引用這些內(nèi)存地址的內(nèi)容。而不同的數(shù)據(jù)類型在所占的內(nèi)存大小是不一樣的,更關(guān)鍵的是,除了大小之外,不同的數(shù)據(jù)類型在存儲信息的方式上也是不同的(如整型和浮點型)。

示例程序

考慮一下程序:

#include <stdio.h>int main(){int a = 1025;int *p;p = &a;printf("Size of integer is %d bytes\n", sizeof(int));printf("p\t Address = %d, Value=%d\n", p, *p);printf("p+1\t Address = %d, Value=%d\n", p+1, *(p+1));char*p0;p0 = (char*)p; // 強制類型轉(zhuǎn)換printf("Size of char is %d bytes\n", sizeof(char));printf("p0\t Address = %d, Value=%d\n", p0, *p0);printf("p0+1\t Address = %d, Value=%d\n", p0+1, *(p0+1));return 0;// 1025 == 0000 0000 0100 0001 }

輸出:

Size of integer is 4 bytes p Address = 1241147588, Value=1025 p+1 Address = 1241147592, Value=1241147588 Size of char is 1 bytes p0 Address = 1241147588, Value=1 p0+1 Address = 1241147589, Value=4

我們可以通過強制類型轉(zhuǎn)換,將指向整型的指針p 轉(zhuǎn)為指向字符型的p0。由于指向了字符型,p0在被解引用時只會找該地址一個字節(jié)的內(nèi)容,而整型1025的第一個字節(jié)的內(nèi)容為0001,第二個字節(jié)內(nèi)容為0100,所以會有上面程序的打印行為。

可以參考筆者畫的內(nèi)存示意圖來理解這段測試程序,其中v表示將該段內(nèi)存解釋為%d的值。

需要指出的是這里的指針的強制類型轉(zhuǎn)換,看似只會添亂,毫無用處,但是它實際上是有一些有用使用場景的,會在后面介紹。

void *

我們這里首先對通用指針類型void *的一些基本特性做出說明,后面會介紹一些具體的使用場景。

  • void *時通用指針類型,它不針對某個特定的指針類型。在使用時將其賦值為指向某種特定的數(shù)據(jù)類型的指針時不需要做強制類型轉(zhuǎn)換。

  • 由于不知道它指向的類型,因此不能直接對其進行解引用*p,也不能對其進行算數(shù)運算p+1。

  • 指向指針的指針

    我們之所以能夠把整型變量 x 的地址存入 p 是因為 p 是一個指向整型變量的指針int*。那如果想要把指針的地址也存儲到一個變量中,這個變量就是一個指向指針的指針,即int**。

    這個邏輯說起來時挺清楚的,在實際程序中,則有可能會暈掉。我們來看一個示例程序,開始套娃:

    #include <stdio.h>int main(){int x;int* p = &x;*p = 6;int** q = &p;int*** r = &q;printf("%d\n", *p);printf("%d\n", *q);printf("%d\n", **q);printf("%d\n", **r);printf("%d\n", ***r);return 0; }

    在這里我們不按編譯器實際輸出的地址值來進行分析,因為這個地址值是不固定的且通常較大。筆者在這里畫了一小段內(nèi)存,我們按圖中的地址值來分析打印輸出的內(nèi)容。在圖中,紅色字體是地址值,青色塊是該變量占據(jù)的地址空間,其中的黑色字體是該變量的值。假設(shè)我們在32位機中,即一個指針占4個字節(jié)。

    在程序中,x是整型變量,p指向x,q指向p,r指向q,這樣x, p, q, r的數(shù)據(jù)類型分別是:int, int*, int**, int***。

  • *p即對指針p的解引用,應(yīng)該是 x 存儲的值,即6。
  • *q是對指向指針的指針q的解引用,即其指向的地址 p 所存儲的值235。同時,這個值就是指針 p 的值,指向整型變量 x 的地址。
  • **q是對*q的解引用,我們已經(jīng)知道*q為235,則**q即地址為235的位置的值,是6。
  • **r是對*r的解引用,而*r就是q,所以**r就是*q,235。
  • ***r是對**r的解引用,同樣是235指向的值,6。
  • 我們編譯運行該程序,得到的輸出是:

    6 -1672706964 6 -1672706964 6

    和我們分析的結(jié)果一致。

    大家可以自己設(shè)計一些這種小示例程序,試著分析一下,再來查看程序運行的結(jié)果是否與預(yù)期一致。

    函數(shù)傳值 vs. 傳引用

    在執(zhí)行一個C語言程序時,此程序?qū)碛形ㄒ坏摹皟?nèi)存四區(qū)”——棧區(qū)、堆區(qū)、全局區(qū)、代碼區(qū).

    具體過程為:操作系統(tǒng)把硬盤中的數(shù)據(jù)下載到內(nèi)存,并將內(nèi)存劃分成四個區(qū)域,由操作系統(tǒng)找到main入口開始執(zhí)行程序。

    內(nèi)存四區(qū)

    • 堆區(qū)(heap):一般由程序員手動分配釋放(動態(tài)內(nèi)存申請與釋放),若程序員不釋放,程序結(jié)束時可能由操作系統(tǒng)回收。
    • 棧區(qū)(stack):由編譯器自動分配釋放,存放函數(shù)的形參、局部變量等。當(dāng)函數(shù)執(zhí)行完畢時自動釋放。
    • 全局區(qū)(global / stack):用于存放全局變量和靜態(tài)變量, 里面細分有一個常量區(qū),一些常量存放在此。該區(qū)域是在程序結(jié)束后由操作系統(tǒng)釋放。
    • 代碼區(qū)(code / text):用于存放程序代碼,字符串常量也存放于此。

    函數(shù)調(diào)用

  • 在程序未執(zhí)行結(jié)束時,main()函數(shù)里分配的空間均可以被其他自定義函數(shù)訪問。

  • 自定義函數(shù)若在堆區(qū)(malloc動態(tài)分配內(nèi)存等)或全局區(qū)(常量等)分配的內(nèi)存,即便此函數(shù)結(jié)束,這些內(nèi)存空間也不會被系統(tǒng)回收,內(nèi)存中的內(nèi)容可以被其他自定義函數(shù)和main()函數(shù)使用。

  • 函數(shù)傳值 call by value

    假設(shè)新手程序員Albert剛剛學(xué)習(xí)了關(guān)于函數(shù)的用法,寫了這樣的程序:

    #include <stdio.h>void Incremnet(int a){a = a + 1; }int main(){int a;a = 10;Incremnet(a);printf("a = %d\n", a);return 0; }

    在該程序中,Albert期望通過Increment()函數(shù)將a的值加1,然后打印出a = 11,但是,程序的實際運行結(jié)果卻是a = 10。問題出在哪里呢?

    實際上,這種函數(shù)調(diào)用的方式稱為值傳遞call by value,這樣在Increment()函數(shù)中,臨時變量local variable a,會在該函數(shù)結(jié)束后立刻釋放掉。也就是說Increment()函數(shù)中的a ,和main() 函數(shù)中的 a 并不是同一個變量。我們可以分別在Increment()和main()兩個函數(shù)內(nèi)打印變量a的地址:

    printf("Address of a in Increment: %d", &a); printf("Address of a in main: %d", &a); // 將這兩句分別放在Increment函數(shù)和main函數(shù)中

    輸出:

    Address of a in Increment: 2063177884 Address of a in main: 2063177908

    這里兩個地址的具體值不重要,重要的是他們是不一樣的,也就是說我們在兩個函數(shù)中操作的a變量并不是同一個,所以程序輸出的是沒有加1過的a的值。

    筆者這里還是根據(jù)原視頻作者的講解,通過畫出內(nèi)存的形式來分析值傳遞。

    程序會為每個函數(shù)創(chuàng)造屬于這個函數(shù)的棧幀,我們首先調(diào)用main()函數(shù),其中的變量a一直存儲在main()函數(shù)自己的棧幀中。在我們調(diào)用Increment()函數(shù)的時候,會單獨為其創(chuàng)造一個屬于它的棧幀,然后main()函數(shù)將實參a=10傳給Increment()作為形參,a會在其中加1,但是并沒有被返回。在Increment()函數(shù)調(diào)用結(jié)束后,它的棧幀被釋放掉,main()函數(shù)并不知道它做了什么,main()自己的變量值一直是10,然后調(diào)用printf()函數(shù),將該值打印出來。

    可以看到,局部變量的值的生命周期隨著被調(diào)用函數(shù)Increment()的結(jié)束而結(jié)束了,而由于main()中的a和Incremet()中的a并不是同一個變量(剛才已經(jīng)看到,二者并不在同一地址),因此最終打印出的值還是10。

    傳引用 call by reference

    那怎樣才能實現(xiàn)Albert的預(yù)期呢?我們剛才已經(jīng)看到,之所以最終在main()中打印的值沒有加1,就是因為加1的變量和最終打印的變量不是同一個變量。那我們只要使得最終打印的變量就是在Increment()中加過1的變量就可以了。這要怎么實現(xiàn)呢?我們剛剛學(xué)過,通過指針可以指向某個特定的變量,并可以通過解引用的方式對該變量再進行賦值,而又由于在程序未執(zhí)行結(jié)束時,main()函數(shù)里分配的空間均可以被其他自定義函數(shù)訪問。因此我們可以將main()中的變量地址傳給Increment(),在其中對該地址的值進行加一,這樣最終打印的變量就會是加過1的了。

    實現(xiàn)如下:

    #include <stdio.h>void Incremnet(int* p){*p = *p + 1; }int main(){int a;a = 10;Incremnet(&a);printf("a = %d\n", a);return 0; }

    這種傳地址的方式我們稱之為call by reference

    它可以在原地直接修改傳入的參數(shù)值。另外,由于傳的參數(shù)是一個指針,無論被指向的對象有多么大,這個指針也只占4個字節(jié)(32位機),因此,這種方式也可以大大提高傳參的效率。

    指針與數(shù)組

    指針和數(shù)組常常一起出現(xiàn),二者之間有著很強的聯(lián)系。

    數(shù)組的聲明

    當(dāng)我們聲明一個整型數(shù)組int A[5]時,就會有五個整型變量:A[0] - A[4],被連續(xù)地存儲在內(nèi)存空間中。

    數(shù)組與指針?biāo)阈g(shù)運算

    還記得我們在前面介紹過指針的算術(shù)運算時,提到過指針的算術(shù)運算可能會導(dǎo)致訪問到未知的內(nèi)存,因為我們定義一個指針時,它指向的位置的鄰居通常是未知的。而在數(shù)組中,我們沒有這個問題,因為數(shù)組是一整塊連續(xù)的內(nèi)存空間,我們確定旁邊也存放著一些相同類型的變量。

    int A[5]; int* p; p = A;printf("%d\n", p); // 200 printf("%d\n", *p); // 2 printf("%d\n", p+1); // 204 printf("%d\n", *(p+1)); // 4 // ...

    在數(shù)組中,指針的算術(shù)運算就很有意義了。因為相鄰位置的變量都是已知的,我們可以通過這種偏移量的方式去訪問它們。

    數(shù)組名和指針

    數(shù)組與指針的另一個聯(lián)系是:數(shù)組名就是指向數(shù)組首元素的指針。數(shù)組的首元素的地址,也被稱為數(shù)組的基地址。

    (這里還有一個要注意的小點:數(shù)組名不能直接自加,即不可A++,但是可以將其賦值給一個指針,指針可以自加:p++)

    比如上面例程中我們寫的p = A。這樣,考慮以下例程的打印輸出:

    printf("%d\n", A); // 200 printf("%d\n", *A); // 2 printf("%d\n", A+3); // 212 printf("%d\n", *(A+3)); // 8

    數(shù)組/指針 取值/取地址

    對于第 iii 個元素:

    • 取地址:&A[i] or (A+i)
    • 取值: A[i] or *(A+i)

    關(guān)于C/C++中指針與數(shù)組的關(guān)系可參考博客:C++中數(shù)組和指針的關(guān)系(區(qū)別)詳解,筆者已將全文重要的一些知識點都總結(jié)好,放在文章開頭。

    數(shù)組作為函數(shù)參數(shù)

    注意我們可以通過sizeof函數(shù)獲取到數(shù)組的元素個數(shù):sizeof(A) / sizeof(A[0]),即用整個數(shù)組的大小除以首元素的大小,由于我們的數(shù)組中存儲的元素都是相同的數(shù)據(jù)類型,因此可以通過此法獲得數(shù)組的元素個數(shù)。

    例程1

    我們現(xiàn)在定義一個SumOfElements()函數(shù),用來計算傳入的數(shù)組的元素求和,該函數(shù)還需要傳入?yún)?shù)size作為數(shù)組的元素個數(shù)。在main()函數(shù)中新建一個數(shù)組,并通過sizeof來求得該數(shù)組的元素個數(shù),調(diào)用該函數(shù)求和。

    #include <stdio.h>int SumOfElements(int A[], int size){int i, sum = 0;for (i=0; i<size; i++){sum += A[i];}return sum; }int main(){int A[] = {1, 2, 3, 4, 5};int size = sizeof(A) / sizeof(A[0]);int total = SumOfElements(A, size);printf("Sum of elements = %d\n", total);return 0; }

    打印出的結(jié)果如我們所料,為15:

    Sum of elements = 15

    例程2

    有人可能回想,既然我們已經(jīng)將數(shù)組傳入函數(shù)了,能不能進行進一步的封裝,將數(shù)組元素個數(shù)的計算也放到調(diào)用函數(shù)內(nèi)來進行呢?于是有了如下實現(xiàn):

    #include <stdio.h>int SumOfElements(int A[]){int i, sum = 0;int size = sizeof(A) / sizeof(A[0]);for (i=0; i<size; i++){sum += A[i];}return sum; }int main(){int A[] = {1, 2, 3, 4, 5};int total = SumOfElements(A);printf("Sum of elements = %d\n", total);return 0; }

    結(jié)果好像除了億點點問題(筆者注:這里筆者的測試結(jié)果與原視頻作者不同(原結(jié)果1),是由于筆者是在64位機上進行的測試,一個指針大小為8字節(jié),而原作者使用的是32位機,指針占4字節(jié),這在接下來的測試程序中也有體現(xiàn)):

    Sum of elements = 3

    為了測試問題出在哪里,讓我們在main()和SumOfElements()函數(shù)中打印如下信息:

    printf("Main - Size of A = %d, Size of A[0] = %d\n", sizeof(A), sizeof(A[0])); printf("SOE - Size of A = %d, Size of A[0] = %d\n", sizeof(A), sizeof(A[0])); // 將這兩行分別添加到main和SumOfElements

    輸出結(jié)果:

    SOE - Size of A = 8, Size of A[0] = 4 Main - Size of A = 20, Size of A[0] = 4 Sum of elements = 3

    果然,在SOE中傳入的數(shù)組A的大小僅有8字節(jié),即一個指針的大小。

    實際上,在編譯例程2時,編譯器會給我們一定的提示W(wǎng)arning:

    pointer.c: In function ‘SumOfElements’: pointer.c:5:22: warning: ‘sizeof’ on array function parameter ‘A’ will return size of ‘int *’ [-Wsizeof-array-argument]int size = sizeof(A) / sizeof(A[0]);

    可以看到,還是比較準(zhǔn)確地指出了可能存在的問題,在被調(diào)函數(shù)中直接對數(shù)組名使用sizeof,會返回指針的大小。

    分析

    我們還是要畫出棧區(qū)來進行分析:

    我們期望的是向左邊那樣,在main()函數(shù)將數(shù)組A作為參數(shù)傳給SOE()之后,會在SOE()的棧幀上拷貝一份完全相同的20字節(jié)的數(shù)組。但是在實際上,編譯器卻并不是這么做的,而是只把數(shù)組A的首地址賦值給一個指針,作為SOE()的形參。也就是說,SOE()的函數(shù)簽名SumOfElements(int A[]) 其實是相當(dāng)于SumOfElements(int* A)。這也就解釋了為什么我們在其內(nèi)部計算A的大小時,得到的會是一個指針的大小。結(jié)合我們之前介紹過的值傳遞和地址傳遞的知識。可以這樣講:數(shù)組作為函數(shù)參數(shù)時是傳引用(call by reference),而非我們預(yù)期的值傳遞。

    需要指出的是,編譯器的這種做法其實是合理的。因為通常來講,數(shù)組會是一個很長,占內(nèi)存空間很大的變量,如果每次傳參都按照值傳遞完整地拷貝一份的話,效率極其低下。而如果采用傳引用的方式,需要傳遞的只有一個指針的大小。

    指針與數(shù)組辨析

    這里視頻原作者做了許多解釋,筆者認(rèn)為有一句話可以概括二者關(guān)系的本質(zhì):數(shù)組名稱和指針變量的唯一區(qū)別是,不能改變數(shù)組名稱指向的地址,即數(shù)組名稱可視為一個指向數(shù)組首元素地址的指針常量。也就是說數(shù)組名指針是定死在數(shù)組首元素地址的,其指向不能被改變。比如數(shù)組名不允許自加A++,因為這會它是一個不可改變的指針常量,而一般指針允許自加p++;還有常量不能被賦值,即若有數(shù)組名 A,指針 p,則A = p是非法的。詳見博客:C++中數(shù)組和指針的關(guān)系(區(qū)別)詳解。

    指針與字符數(shù)組

    當(dāng)我們在C語言中談?wù)撟址麛?shù)組時,通常就是在談?wù)撟址?/p>

    C語言中字符串的存儲

    在C語言中,我們通常以字符數(shù)組的形式來存儲字符串。對于一個有 nnn 個字符組成的字符串,我們需要一個長度至少為 n+1n+1n+1 的字符數(shù)組。例如要存儲字符串JOHN,我們需要一個長度至少為 5 的字符數(shù)組。

    之所以字符數(shù)組的長度要比字符串中字符的個數(shù)至少多一個,是因為我們需要符號來標(biāo)志字符串的結(jié)束。在C語言中,我們通過在字符數(shù)組的最后添加一個 \0 來標(biāo)志字符串的結(jié)束。如下圖。

    在這個圖中,我們?yōu)榱舜鎯ψ址甁OHN,我們使用了字符數(shù)組中的5個元素,其中最后一個字符 \0,用來標(biāo)識字符串的結(jié)束。倘若沒有這個標(biāo)識的話,程序就不知道這個字符串到哪里結(jié)束,就可能會訪問到5,6中一些未知的內(nèi)容。

    示例程序:

    #include <stdio.h>int main(){char C[4];C[0] = 'J';C[1] = 'O';C[2] = 'H';C[3] = 'N';printf("%s", C);return 0; }

    這里原作者給出了這樣一個示例程序,并且測試得到的輸出結(jié)果是JOHN+幾個亂碼,這是合理的,因為如前所述,沒有設(shè)置 \0 來標(biāo)識字符串的結(jié)束。

    但是筆者在自己的機器上親測(編譯器為gcc 7.5.0)的時候打印輸出是正常的JOHN字符串,這是由于有些編譯器會自動的為你補全\0。筆者也嘗試了通過調(diào)整gcc的-O參數(shù)嘗試了各種優(yōu)化等級,都可以正常打印字符串。

    而通過 char C[20] = "JOHN" 這種方式定義的字符串,編譯器一定會在其末尾添加一個 \0。 原作者強調(diào)這里編譯器會強制要求聲明的數(shù)組長度大于等于5,也就是說char C[4] = "JOHN"是無法通過編譯的。但在筆者測試時,這也是可行的,但是 3 就肯定不行了哈。

    通過引入頭文件string.h,可以使用strlen()函數(shù)獲取到字符串的長度,無論我們聲明的字符數(shù)組有多長(比如上面這個20),該函數(shù)會找到第一個 \0,并返回之前的元素個數(shù),也就是我們實際的字符串長度。有以下例程:

    #include <stdio.h> #include <string.h>int main(){char C[20] = "JOHN";int len = strlen(C);printf("%d", len);return 0; }

    輸出會是 4,我們實際的字符串JOHN的長度。

    字符串常量與常量指針

    char[] 和 char*

    char C[20] = "JOHN"; // 字符串就會儲存在分配給這個數(shù)組的內(nèi)存空間中,這種情形下它會被分配在棧上

    當(dāng)向上面一樣使用字符數(shù)組進行初始化時,字符串就會儲存在分配給這個數(shù)組的內(nèi)存空間中,這種情形下它會被分配在棧上。

    而當(dāng)使用 char* 的形式聲明一個字符串時(如下),它會是一個字符串常量,通常會被存放在代碼區(qū)

    char* C = "JOHN"; // 如此聲明則為字符串常量,存放在代碼區(qū),其值不能被修改

    既然叫做常量,那它的值肯定是不能更改的了,即*C[0]='A' 是非法的操作。

    常量指針

    還記得我們之前提到過,即數(shù)組名稱可視為一個指向數(shù)組首元素地址的指針常量指針常量的含義是指針的指向不能被修改,如數(shù)組名看作指針時不能自加,因為這會修改它的指向。

    而本小節(jié)提到的常量指針則是指指針指向的值不能被修改。常量指針通常用在引用傳參時,如果某個函數(shù)要進行一些只讀的操作(如打印),為了避免在函數(shù)體內(nèi)部對數(shù)據(jù)進行了寫操作,而又因為是傳引用,則會破壞原數(shù)據(jù)。如以下打印字符串的函數(shù),由于打印字符串不需要改動原來的數(shù)據(jù),故可以在函數(shù)簽名中加上const關(guān)鍵字,來使得 C 是一個常量指針,保證其指向的值不會被誤操作修改。注意此處的 C 是常量指針,而非指針常量,即其指向可以改變,因此函數(shù)體中的C++是合法的操作。

    void printString(const char* C){while (*C != '\0'){printf("%c", *C);C++;}printf("\n"); }

    指針與多維數(shù)組

    指針與二維數(shù)組

    二維數(shù)組概念

    我們可以聲明一個二維數(shù)組:int B[2][3],實際上,這相當(dāng)于聲明了一個數(shù)組的數(shù)組。如此例中,B[0], B[1] 都是包含3個整型數(shù)據(jù)的一維數(shù)組。

    如前所述,數(shù)組名相當(dāng)于是指向數(shù)組首元素地址的指針常量。在這里,首元素不在是一個整型變量,而是一個包含3個整型變量的一維數(shù)組。這時int* p = B就是非法的了,因為數(shù)組名B是一個指向一維數(shù)組的指針,而非一個指向整型變量的指針。正確的寫法應(yīng)該是:int (*p)[3] = B。

    而B[0]就相當(dāng)于是一個一維數(shù)組名(就像前幾章的A),也相當(dāng)于一個指向整型的指針常量。

    例程

    我們通過一個例程來幫助自己分析理解二維數(shù)組和指針,與上面的元素設(shè)定一致,也假設(shè)地址就按上方藍色字體,每一組有虛線分隔開來,每一組之內(nèi)的含義是一樣的。大家可以先不看注釋中的解釋與答案,自己試著分析一下每一組是什么含義。后面會給出筆者的分析。

    #include <stdio.h>int main(){int B[2][3] = {2, 3, 6, 4, 5, 8};printf("-----------------------\n"); // 指向一維數(shù)組的指針 400printf("%d\n", B);printf("%d\n", &B[0]);printf("-----------------------\n"); // 指向整型的指針 400printf("%d\n", *B);printf("%d\n", B[0]);printf("%d\n", &B[0][0]);printf("-----------------------\n"); // 指向一維數(shù)組的指針 412printf("%d\n", B+1); printf("%d\n", &B[1]);printf("-----------------------\n"); // 指向整型的指針 412printf("%d\n", *(B+1));printf("%d\n", B[1]);printf("%d\n", &B[1][0]);printf("-----------------------\n"); // 指向整型的指針 420 printf("%d\n", *(B+1)+2);printf("%d\n", B[1]+2); printf("%d\n", &B[1][2]); printf("-----------------------\n"); // 整型 3printf("%d\n", *(*B+1)); printf("-----------------------\n"); return 0; }
  • 第一組 (B,&B[0]):數(shù)組名B是一個指針,其指向的元素是一個一維數(shù)組,即二維數(shù)組第一個元素(第一個一維數(shù)組)的首地址。而B[0]就是二維數(shù)組的第一個元素,即二維數(shù)組的第一個一維數(shù)組,對其進行取地址運算,故&B[0]就是第一個一維數(shù)組的地址,也即第一個指向第一個一位數(shù)組的指針。

    所以說第一組是指向一位數(shù)組的指針,其值為 400。

  • 第二組:(*B,B[0],&B[0][0]):對數(shù)組名B進行解引用,得到的是其第一個元素(第一個一維數(shù)組)的值,也就是一個一維數(shù)組名B[0](相當(dāng)于前面幾章的一維數(shù)組名A),這個一維數(shù)組名就相當(dāng)于是一個指向整型數(shù)據(jù)的指針常量。而B[0][0]是一個整型數(shù)據(jù)2,對其進行取地址運算,得到的是一個指向整型變量的指針。

    所以說第二組是指向整型變量的指針,其值也為400,但與第一組指向的元素不同,注意體會。

  • 第三、四組與第一、二組類似,關(guān)鍵區(qū)別在于加入了指針運算。這里需要注意的是對什么類型的指針進行運算,是對指向一維數(shù)組的指針(+12),還是對指向整型的指針(+4)。在這兩組中都是對指向一維數(shù)組的指針(如二維數(shù)組名B)進行運算,所以地址要偏移12個字節(jié)。

  • 第五組中開始有了對不同的指針類型進行指針運算的情況。在這一組中的,+1都是對指向一維數(shù)組的指針進行運算,要+(1*12),而+2都是對指向整型變量的指針進行運算,要+(2*4),故最終結(jié)果是420。

  • 最后一組只有一個值。但需要一步一步仔細分析。首先*B是對二位數(shù)組名進行解引用,得到的是一個一位數(shù)組名,也就是一個指向整型的指針常量。對其加1,需要偏移4個字節(jié),即(*B+1)是一個指向地址404處的整型變量的指針,對其進行解引用,直接拿出404地址處的值,得到3。

    大家可以考慮一下,如果加一個括號*(*(B+1))的值會是多少呢?

  • 小公式

    對于二位數(shù)組和指針、指針運算的關(guān)系,原作者給出了這樣一個公式,筆者同樣寫在下面供大家參考。希望大家不要死記硬背,而是試著去理解它。

    B[i][j] == *(B[i]+j) == *(*(B+i)+j)

    指針與高維數(shù)組

    前面我們已經(jīng)看到多維數(shù)組的本質(zhì)其實就是數(shù)組的數(shù)組。如果你已經(jīng)對上一小節(jié)例程中的幾組值得含義都已經(jīng)完全搞清楚了,那么理解高維數(shù)組也不難了。

    以下我們以三維數(shù)組為例進行介紹,開始套娃。

    三維數(shù)組概念

    我們可以這樣聲明一個三維數(shù)組:int C[3][2][2]。三維數(shù)組中的每個元素都是二維數(shù)組, 具體來說,它是由三個二維數(shù)組組成的,每個二維數(shù)組是由兩個一維數(shù)組組成的,每個一維數(shù)組含有兩個整型變量。圖示如下:

    類似地,如果我們想將三維數(shù)組名C賦值給一個指針的話,應(yīng)該這樣聲明:int (*p)[2][2] = C。

    小公式

    同樣給出三維數(shù)組的小公式如下:

    C[i][j][k] == *(C[i][j]+k) == *(*(C[i]+j)+k) == *(*(*(C+i)+j)+k)

    這里筆者只簡單分析一下。首先,要明確,在本例中,一個整型變量占4個字節(jié),一個一維數(shù)組占2*4=8個字節(jié),一個二維數(shù)組占2*2*4=16個字節(jié),而整個三維數(shù)組占3*2*2*4=48個字節(jié)。

    從右向左、從里向外看:

    C是三維數(shù)組名,其值是三維數(shù)組中的第一個元素(即第一個二維數(shù)組)的起始地址,800,相當(dāng)于指向二維數(shù)組的指針常量,C+i是對指向二維數(shù)組的指針進行運算,因此應(yīng)該偏移+i*16個字節(jié),而對其進行解引用*(C+i),得到的就是起始地址為800+i*16處的那個二維數(shù)組,其名為C[i](相當(dāng)于B);
    而二維數(shù)組名是一個指向一位數(shù)組的指針常量,然后C[i]+j是對指向一維數(shù)組的指針進行運算,偏移+j*8個字節(jié),而對其進行解引用*(C[i]+j),得到的是起始地址為800+i*16+j*8處的一維數(shù)組,其名為C[i][j](相當(dāng)于A);
    而一維數(shù)組名是一個指向整型變量的指針常量,C[i][j]+k是對指向整型變量的指針進行運算,應(yīng)該偏移+k*4個字節(jié),而對其進行解引用*(C[i][j]+k),得到的是起始地址為800+i*16+j*8+k*4處的那個整型變量的值,即C[i][j][k]。

    大家可以試著分析一下*(C[1]+1)和*(C[0][1]+1)分別是多少,這時作者給出的兩個小測試題,答案是824和9。

    多位數(shù)組作為函數(shù)參數(shù)

    一維數(shù)組作為參數(shù)需要注意是傳引用,另外在函數(shù)體內(nèi)不修改數(shù)據(jù)時,注意在函數(shù)簽名中將數(shù)組名指針聲明為常量指針

    二維數(shù)組做參數(shù):

  • void func(int (*A)[3]
  • void func(int A[][3])
  • 注意事項

    • 注意:多維數(shù)組做函數(shù)參數(shù)時,數(shù)組的第一個維度可以省略,但是其他維度必須指定。所以說,對一個需要接收二維數(shù)組的參數(shù),將函數(shù)簽名聲明為void func(int **A) 是不可行的,因為這樣沒有指定任何數(shù)組維度。

    • 注意:在調(diào)用時要正確地傳遞參數(shù)數(shù)組的類型。比如下面這樣就是不可行的:

      void func1(int Arr[][3]){} void func2(int Arr[][2][2]){}int main(){int A[2][2];int B[2][3];int C[3][2][2];int D[3][2][3];func1(A); // 錯誤func1(B); // 正確func2(C); // 正確func2(D); // 錯誤 }

    指針與動態(tài)內(nèi)存

    內(nèi)存四區(qū)簡介

    內(nèi)存被分為四個區(qū),分別是代碼區(qū),靜態(tài)/全局區(qū),棧區(qū)和堆區(qū)。

    • 代碼區(qū):存放指令。
    • 靜態(tài)區(qū) / 全局區(qū):存放靜態(tài)或全局變量,也就是不再函數(shù)中聲明的變量,它們的生命周期貫穿整個應(yīng)用程序。
    • 棧區(qū):用來存放函數(shù)調(diào)用的所有信息,和所有局部變量。
    • 堆區(qū):大小不固定,可以由程序員自由地分配和釋放(動態(tài)內(nèi)存申請與釋放)。

    在整個程序運行期間,代碼區(qū),靜態(tài)/全局區(qū),棧區(qū)的大小是不會增長的

    有一個小點要說明一下:有堆、棧這兩種數(shù)據(jù)結(jié)構(gòu),也有堆、棧這兩個內(nèi)存分區(qū),內(nèi)存中的棧基本是由數(shù)據(jù)結(jié)構(gòu)中的棧實現(xiàn)的,而內(nèi)存中的堆和數(shù)據(jù)結(jié)構(gòu)中的堆毫無關(guān)系。堆可以簡單理解為一塊大的、可供自由分配釋放的內(nèi)存空間。

    之前我們已經(jīng)介紹過在程序運行過程中,代碼區(qū)、靜態(tài)/全局區(qū)和棧區(qū)是怎樣運作的了,特別是函數(shù)調(diào)用時棧區(qū)的工作方式,我們特別進行了說明。

    C/C++中的動態(tài)內(nèi)存分配

    • 在C中,我們需要使用四個函數(shù)進行動態(tài)內(nèi)存分配:malloc(),calloc(),realloc(),free()。
    • 在C++中,我們需要使用兩個操作符:new,delete。另外,由于C++是C的超集,兼容C,故也可以用以上4個函數(shù)來進行動態(tài)內(nèi)存分配。

    malloc 和 free

    #include <stdio.h> #include <stdlib.h>int main(){int a;int* p;p = (int*)malloc(sizeof(int));*p = 10;free(p);return 0; }

    malloc函數(shù)從堆上找到一塊給定大小的空閑的內(nèi)存,并將指向起始地址的void *指針返回給程序,程序員應(yīng)當(dāng)根據(jù)需要做適當(dāng)?shù)闹羔様?shù)據(jù)類型的轉(zhuǎn)換。

    向堆上寫值的唯一方法就是使用解引用,因為malloc返回的總是一個指針。如果malloc無法在堆區(qū)上找到足夠大小的空閑內(nèi)存,則會返回NULL。

    程序員用malloc在堆上申請的內(nèi)存空間不會被程序自動釋放,因此程序員在堆上申請內(nèi)存后,一定要記得自己手動free釋放。

    free接收一個指向堆區(qū)某地址的指針作為參數(shù),并將對應(yīng)的堆區(qū)的內(nèi)存空間釋放。

    new 和 delete

    在C++中,程序員們通常使用new,delete操作符來進行動態(tài)內(nèi)存的分配和釋放。以下是整型變量和整型數(shù)組的分配和釋放例程。

    p = new int; *p = 10; delete p;p = new int[20] delete[] p;

    注意數(shù)組delete時要有[]。

    在C++中,不需要做指針數(shù)據(jù)類型的轉(zhuǎn)換,new和delete是類型安全的。它們是帶類型的,返回特定類型的指針。

    malloc、calloc、realloc、free

    malloc

    • 函數(shù)簽名:void* malloc(size_t size)。函數(shù)接收一個參數(shù)size,返回的void*指針指向了分配給我們的內(nèi)存塊中的第一個字節(jié)的地址。

    • void*類型的指針只能指明地址值,但是無法用于解引用,所以通常我們需要對返回的指針做強制類型轉(zhuǎn)換,轉(zhuǎn)換成我們需要的指針類型。

    • 通常我們不顯式地給出參數(shù)size的值,而是通過sizeof,再乘上我們需要的元素個數(shù),計算出我們需要的內(nèi)存空間的大小。

    • 典型用法:

      int* p = (int*)malloc(3 * sizeof(int)); *p = 10; *(p+1) = 3; p[2] = 2; // 之前學(xué)過的數(shù)組的形式

    calloc

    • 函數(shù)簽名:void* calloc(size_t num, size_t size)。函數(shù)接收兩個參數(shù)num,size,分別表示特定類型的元素的數(shù)量,和類型的大小。同樣返回一個void*類型的指針。

    • 典型用法:

      int *p = (int*)calloc(3, sizeof(int));
    • calloc與malloc的另一個區(qū)別是:malloc分配完內(nèi)存后不會對其進行初始化,calloc分配完內(nèi)存后會將值初始化位0。

    realloc

    • 函數(shù)簽名:void* realloc(void* ptr, size_t size)。函數(shù)接收兩個參數(shù),第一個是指向已經(jīng)分配的內(nèi)存的起始地址的指針,第二個是要新分配的內(nèi)存大小。返回void*指針。可能擴展原來的內(nèi)存塊,也可能另找一塊大內(nèi)存拷貝過去,如果是縮小的話,就會是原地縮小。

    • 如果縮小,或者拷貝到新的內(nèi)存地址,總之只要是由原來分配的內(nèi)存地址不會再被用到,realloc函數(shù)自己會將這些不被用到的地址釋放掉。

    • 以下這種情況使用realloc相當(dāng)于free:

      int* B = (int*)realloc(A, 0);

      以下這種情況使用realloc相當(dāng)于malloc:

      int* B = (int*)realloc(NULL, sizeof(int));

    free

    在堆區(qū)動態(tài)分配的內(nèi)存會一直占據(jù)著內(nèi)存空間,如果程序員不將其顯式地釋放,程序是不會自動將其釋放的,直到整個程序結(jié)束。 已經(jīng)沒有用的堆區(qū)內(nèi)存如果不進行手動釋放會造成內(nèi)存泄漏,因此,使用上面三個函數(shù)在動態(tài)分配的堆區(qū)內(nèi)存的使命結(jié)束后,程序員有責(zé)任記得將它們釋放。

    在C中,我們使用free函數(shù)來進行堆區(qū)內(nèi)存的釋放。只需將要釋放的內(nèi)存的其實地址傳入即可:free(p)。

    使用場景

    當(dāng)我們想要根據(jù)用戶的輸入來分配一個合適大小的數(shù)組,如果寫成如下這樣:

    #include <stdio.h> #include <stdlib.h>int main(){int n;printf("Please Enter the Size of Array You Want:\n");scanf("%d", &n);int* A[n];return 0; }

    作者將這樣在運行時才知道數(shù)組的大小是不行的。但是筆者實驗過發(fā)現(xiàn)是可以的,這應(yīng)該是C99支持的特性變長數(shù)組。

    但是這并不妨礙我們試著練習(xí)用動態(tài)分配內(nèi)存的方式來新建一個數(shù)組,我們可以這樣做:

    int* A = (int*)malloc(n * sizeof(int));

    或者用calloc,會自動將初始值賦為0:

    int* A = (int*)calloc(n, sizeof(int));

    別忘了手動釋放堆區(qū)內(nèi)存。

    free(A);

    注意

    在C程序中,只要我們知道某個內(nèi)存的地址,我們就能訪問它,C語言并不禁止我們的這種行為。但我們應(yīng)當(dāng)注意,不要去試圖讀寫未知的內(nèi)存,因為這將使我們的程序的行為不可預(yù)測,可能某個存在非法讀寫的程序在某個機器上運行正常,但是到了另一個環(huán)境、另一個機器上就會崩潰。最好的方法是:只去讀寫為我們分配的內(nèi)存,而不要試圖訪問未知的內(nèi)存

    內(nèi)存泄漏

    動態(tài)分配內(nèi)存使用結(jié)束后不進行釋放的行為可能會造成內(nèi)存泄漏。乍看之下,好像不進行內(nèi)存釋放”只是“多占了一些內(nèi)存空間而已,為什么會被稱為內(nèi)存泄漏呢?而又為什么只有堆區(qū)的動態(tài)內(nèi)存未被釋放會造成內(nèi)存泄漏呢?本小節(jié)將介紹相關(guān)內(nèi)容。

    #include <stdio.h> #include <stdlib.h>void allocate_stack(){int Arr[10000]; }void allocate_heap(){int* Arr = (int*)malloc(10000 * sizeof(int)); }int main(){int c;while (1) {printf("'0' to break, '1' to continue\n");scanf("%d", &c);if (!c) break;else {int i = 0;for (i=0; i<100; i++){allocate_heap();allocate_stack();}}}return 0; }

    我們有在主函數(shù)上調(diào)用allocate_stack或者allocate_heap,兩者的區(qū)別是一個在棧上開辟一個數(shù)組并直接返回,另一個在堆區(qū)開辟一個數(shù)組并且不釋放返回。在主函數(shù)中死循環(huán)詢問是否繼續(xù)開辟數(shù)組,得到繼續(xù)開辟數(shù)組的命令后開辟100個數(shù)組。我們可以通過top命令清晰地看到allocate_stack的內(nèi)存占用在每次開辟數(shù)組后驟增,然后掉下去,而allocate_heap的內(nèi)存占用每次驟增后也不掉下去,直到內(nèi)存占用過大被操作系統(tǒng)kill掉。

    allocate_stack

    對于調(diào)用allocate_stack的程序,在allocate_stack函數(shù)調(diào)用時,每次將數(shù)組創(chuàng)建在棧區(qū),然后再函數(shù)返回時,程序自動將其棧幀釋放,數(shù)組也被釋放掉,不會占用內(nèi)存。

    allocate_heap

    對于調(diào)用allocate_heap的程序:每次調(diào)用allocate_heap在堆區(qū)開辟一個數(shù)組Arr,在棧上只創(chuàng)建了一個指針p來指向這個堆區(qū)數(shù)組,但是堆區(qū)數(shù)組沒有釋放,這樣在allocate_heap函數(shù)返回之后,函數(shù)棧幀上的p也被程序釋放掉,就再也沒有辦法去釋放堆區(qū)的數(shù)組Arr。這樣隨著函數(shù)調(diào)用次數(shù)越來越多,這些堆區(qū)的數(shù)組都處于已分配但無法引用也無法使用的狀態(tài)。而堆區(qū)大小又是不固定的,可以一直向操作系統(tǒng)申請,終有一天,會超過內(nèi)存上限,被系統(tǒng)這就是內(nèi)存泄漏。

    函數(shù)返回指針

    指針本質(zhì)上也是一種數(shù)據(jù)類型(就像int、char),其中存儲了另外一個數(shù)據(jù)的地址,因此將一個指針作為返回類型是完全可行的。但是,需要考慮的是,在什么情況下,我們會需要函數(shù)返回一個指針類型呢?

    示例程序

    考慮這樣一個程序:

    #include <stdio.h> #include <stdlib.h>int Add(int a, int b){int c = a + b;return c; }int main(){int x = 2, y = 4;int z = Add(x, y);printf("Sum = %d\n", z); }

    我們定義了一個加和函數(shù)Add,它從main函數(shù)中接收兩個參數(shù),并將二者求和的值返回給main。需要注意的是,就像我們之前提到的那樣,這里的x,y,z都是棧區(qū)main函數(shù)棧幀里的局部變量,而a, b 則都是棧區(qū)上Add函數(shù)棧幀中的局部變量。

    并且這種函數(shù)傳參的方式我們之前已經(jīng)講過,成為值傳遞。

    要將函數(shù)的傳參方式改為地址傳遞,只需改為以下程序:

    #include <stdio.h> #include <stdlib.h>int Add(int* a, int* b){int c = (*a) + (*b);return c; }int main(){int x = 2, y = 4;int z = Add(&x, &y);printf("Sum = %d\n", z); }

    函數(shù)返回指針

    上面關(guān)于傳值和傳引用的做法我們已經(jīng)在前面介紹過了,我們這一小節(jié)的重點是看看怎樣讓函數(shù)返回一個指針,我們的第一個版本可能會是這樣的:

    #include <stdio.h> #include <stdlib.h>int* Add(int* a, int* b){int c = (*a) + (*b);return &c; }void printHello(){printf("Hello\n"); }int main(){int x = 2, y = 4;int* ptr = Add(&x, &y);// printHello();printf("Sum = %d\n", *ptr); }

    這個版本在作者測試時是正常的,但是如果在打印結(jié)果之前再多調(diào)用一個函數(shù)printHello,則會導(dǎo)致輸出錯誤。這究竟是怎么回事呢?我們還是要借助棧區(qū)內(nèi)存分析。

    我們再次劃出這個程序運行時的內(nèi)存,這里沒有用到堆區(qū),就暫時先不畫出來了。

    我們看到,在調(diào)用Add是,有Add自己的棧幀,其中存放兩個傳入的指向整型的指針a,b,指向main函數(shù)棧幀中的我們想要加和的兩個整型變量x,y,Add的棧幀中還有一個整型變量c,是我們的計算結(jié)果,按照上面的程序?qū)懛?#xff0c;Add函數(shù)返回一個整型指針指向變量c,即main中的ptr。

    問題來了,在Add函數(shù)返回之后,它在棧區(qū)上的棧幀也被程序自動釋放了,這個時候,原來存放整型變量c的150這個內(nèi)存地址中的值就已經(jīng)是未知的了,我們之前說過,訪問未知的內(nèi)存是極其危險的。

    如果在Add函數(shù)返回之后,沒有調(diào)用其他任何函數(shù),直接對150解引用,有可能能夠打印出正確的結(jié)果,如果編譯器沒有對釋放的棧幀進行其他處理。但是如果調(diào)用了其他函數(shù),如printHello,即使該函數(shù)中沒有任何我們看得到的參數(shù),但也需要保存一些返回地址、寄存器現(xiàn)場等參數(shù),因此也會在棧區(qū)占用一塊作為自己的棧幀。這時,內(nèi)存位置150幾乎是肯定被重寫了,這時無法得到預(yù)期的結(jié)果。無論如何,訪問未知的內(nèi)存地址是一定要杜絕的,不能寄希望于偶然的正確結(jié)果。

    另外,需要指出的是,main函數(shù)也通過傳引用的形式將地址傳遞給了Add函數(shù),但這是沒問題的,因為Add函數(shù)調(diào)用時,main函數(shù)的棧幀還是在原來的內(nèi)存位置,這是已知的,我們可以進行訪問。即棧底向上傳參數(shù)是可以的,從棧底向上傳一個局部變量或者一個局部變量的地址是可以的。但是棧頂向下傳參數(shù)是不可以的,從棧頂向下傳一個局部變量或者一個局部變量的地址是不可以的。可想而知,C/C++中的main函數(shù)是可以自由地向被調(diào)函數(shù)傳引用的。

    筆者自己在親測這個程序,編譯時會報警告:

    pointer.c: In function ‘Add’: pointer.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]return &c;^~

    而運行時則會直接Core Dumped。應(yīng)該是新版本的編譯器直接禁止了這種返回已釋放的棧區(qū)指針的行為。

    使用場景

    可以見到,返回被調(diào)函數(shù)在棧區(qū)的局部變量的指針是危險的。通常,我們可以安全地返回堆區(qū)或者全局區(qū)的內(nèi)存指針,因為它們不會被程序自動地釋放。

    我們嘗試在堆區(qū)分配內(nèi)存,將上面的程序中的Add函數(shù)改為:

    int* Add(int* a, int* b){int* c = (int*)malloc(sizeof(int));*c = (*a) + (*b);return c; }

    這樣,程序就可以正常地工作了。

    這樣,Add返回的指針?biāo)赶虻牡刂反娣旁诙褏^(qū),不像棧區(qū)一樣在函數(shù)返回之后被程序自動釋放,可以在main函數(shù)中正常地進行訪問。堆區(qū)內(nèi)存使用結(jié)束之后不要忘記釋放。

    函數(shù)指針

    代碼段

    函數(shù)指針,就像名字所描述的那樣,是用來存儲函數(shù)的地址的。之前我們介紹的指針都是指向數(shù)據(jù)的,本章我們將討論指向函數(shù)的指針。我們可以使用函數(shù)指針來解引用和執(zhí)行函數(shù)。

    我們已經(jīng)不止一次提過內(nèi)存四區(qū)或者內(nèi)存四區(qū)的某一部分了。但是有一部分我們在之前的講述中一直沒有提到過,那就是代碼區(qū)(Code)。我們知道,雖然我們編寫程序源代碼時使用的大多是C、C++等高級語言,但是機器要真正的運行程序,必須是運行二進制的機器代碼。從C到機器代碼的這個過程(包括預(yù)處理、編譯、匯編、鏈接)是由編譯器替我們完成的,得到的二進制代碼將被存放在可執(zhí)行文件中。

    應(yīng)用程序的代碼段,就是用來存放從可執(zhí)行文件拷貝過來(到內(nèi)存代碼段)的機器碼或指令的。下面我們來仔細討論一下代碼區(qū)。

    如上圖所示,假設(shè)一條指令占4個字節(jié),在內(nèi)存中,一個函數(shù)就是一塊連續(xù)的內(nèi)存(其中存放的不是數(shù)據(jù),而是指令)。指令通常都是順序執(zhí)行的,直到發(fā)生跳轉(zhuǎn)(如函數(shù)調(diào)用,函數(shù)返回),會根據(jù)指令調(diào)到指定的地址執(zhí)行。假設(shè)圖中藍色區(qū)域(指令02 - 指令 05,地址208 - 220)是一個函數(shù),指令00是一條跳轉(zhuǎn)指令,調(diào)用了藍色區(qū)域的函數(shù),程序就會從200跳轉(zhuǎn)到208執(zhí)行。函數(shù)的起始地址(比如208),被稱為函數(shù)的入口點,它是函數(shù)的第一條指令的地址。

    函數(shù)指針的定義和使用

    下面這個程序定義和使用了一個函數(shù)指針:

    #include <stdio.h>int Add(int a, int b){return a + b; }int main(){int c;int (*p)(int, int);p = &Add;c = (*p)(2, 3);printf("%d\n", c); }
  • 聲明函數(shù)指針的語法是:int (*p)(int, int),這條語句聲明了一個接收兩個整型變量作為參數(shù),并且返回一個整型變量的函數(shù)指針。注意函數(shù)指針可以指向一類函數(shù),即可以說,指針p指向的類型是輸入兩整型,輸出一整型的這一類函數(shù),即所有滿足這個簽名的函數(shù),都可以賦值給p這個函數(shù)指針。

    另外,要注意指針要加括號。否則int *p(int, int),是聲明一個函數(shù)名為p,接收兩個整型,并返回一個整型指針的函數(shù)。

  • 函數(shù)指針賦值:p = &Add,將函數(shù)名為Add的函數(shù)指針賦值給p。同樣注意只要滿足p聲明時的函數(shù)簽名的函數(shù)名都可以賦值給p。

  • 函數(shù)指針的使用:int c = (*p)(2, 3),先對p解引用得到函數(shù)Add,然后正常傳參和返回即可。

  • 還有一點,在為函數(shù)指針賦值時,可以不用取地址符號&,僅用函數(shù)名同樣會返回正確的函數(shù)地址。與之匹配的,在調(diào)用函數(shù)的時候也不需要再解引用。這種用法更加常見。

    int (*p)(int, int); p = Add; c = p(2, 3);
  • 再強調(diào)一下:注意函數(shù)指針可以指向一類函數(shù),即可以說,指針p指向的類型是輸入兩整型,輸出一整型的這一類函數(shù),即所有滿足這個簽名的函數(shù),都可以賦值給p這個函數(shù)指針。用不同的函數(shù)簽名來聲明的函數(shù)指針不能指向這個函數(shù)。

    如以下這些函數(shù)指針的聲明都是不能指向Add函數(shù)的:

    void (*p)(int, int); int (*p)(int); int (*p)(int, char);
  • 函數(shù)指針的使用案例(回調(diào)函數(shù))

    回調(diào)函數(shù)的概念

    這里使用函數(shù)指針的案例都圍繞著這么一個概念:函數(shù)指針可以用作函數(shù)參數(shù),而接收函數(shù)指針作為參數(shù)的這個函數(shù),可以回調(diào)函數(shù)指針?biāo)赶虻哪莻€函數(shù)

    #include <stdio.h>void A(){printf("Hello !\n"); }void B(void (*ptr)()){ptr(); }int main(){void (*p)() = A;B(p);B(A); return 0; }

    或者我們可以直接在主函數(shù)中B(A),而不需要上面那寫兩句先復(fù)制給p,再調(diào)用p。

    在上面的例程中,將函數(shù)A()的函數(shù)指針傳給B(),B()在函數(shù)體內(nèi)直接通過傳入的函數(shù)指針調(diào)用函數(shù)A(),這個過程成為回調(diào)。這里函數(shù)指針被傳入另一個函數(shù),再被用函數(shù)指針進行回調(diào)的函數(shù)A()成為回調(diào)函數(shù)

    回調(diào)函數(shù)的實際使用場景

    #include <stdio.h> #include <math.h>void BubbleSort(int A[], int size){int i, j, temp;for (i=0; i<size; i++){for (j=0; j<size-1; j++){if (A[j] > A[j+1]){temp = A[j];A[j] = A[j+1];A[j+1] = temp;}}} }int main(){int A[] = {2, -4, -1, 3, 9, -5, 7};int size = sizeof(A) / sizeof(A[0]);// BubbleSort(A, size, greater);BubbleSort(A, size, abs_greater);int i = 0;for (i=0; i<size; i++){printf("%d ", A[i]);}printf("\n"); }

    輸出排序結(jié)果:

    -5 -4 -1 2 3 7 9

    對于這個排序函數(shù),我們可能有時需要升序排序有時需要降序排序,即我們可能會根據(jù)具體使用場景有不同的排序規(guī)則。而由于實現(xiàn)不同的排序函數(shù)時,整個算法的邏輯是不變的,只有排序的規(guī)則會不同,總不至于為了不同的排序規(guī)則都單獨寫一個函數(shù),這時我們就可以借助函數(shù)指針作為參數(shù)來實現(xiàn)不同的排序規(guī)則的切換。

    即實現(xiàn)如下:

    #include <stdio.h> #include <math.h>void BubbleSort(int A[], int size, int (*compare)(int, int)){int i, j, temp;for (i=0; i<size; i++){for (j=0; j<size-1; j++){if (compare(A[j], A[j+1]) > 0){temp = A[j];A[j] = A[j+1];A[j+1] = temp;}}} }int greater(int a, int b){if (a > b) return 1;else return -1; }int abs_greater(int a, int b){if (abs(a) > abs(b)) return 1;else return -1; }int main(){int A[] = {2, -4, -1, 3, 9, -5, 7};int size = sizeof(A) / sizeof(A[0]);BubbleSort(A, size);int i = 0;for (i=0; i<size; i++){printf("%d ", A[i]);}printf("\n"); }

    我們在排序函數(shù)中接收一個函數(shù)指針Compare作為參數(shù),用整個參數(shù)來指示排序的規(guī)則。這樣我們就利用回調(diào)函數(shù)實現(xiàn)了這一想法。我們可以寫不同的排序規(guī)則作為回調(diào)函數(shù),比如筆者這里又寫了一個按照絕對值比較大小的回調(diào)函數(shù)abs_greater,

    輸出:

    -1 2 3 -4 -5 7 9

    另外回調(diào)函數(shù)還有更多有趣的應(yīng)用,比如事件回調(diào)函數(shù)等。

    Ref:

    https://blog.csdn.net/helloyurenjie/article/details/79795059

    創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎

    總結(jié)

    以上是生活随笔為你收集整理的C/C++ 指针详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。