C/C++ 指針詳解

指針詳解

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

學習完之后,回看找特定的知識點,善用目錄 —>

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

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.

指針的基本介紹

數據在內存中的存儲與訪問

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

在這里插入圖片描述

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

我們聲明兩個變量:

int a;
char c;

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

當我們為變量賦值時,如a = 5,程序就會先查到 a 的類型及其首地址,然后到這個地址把其中存放的值寫為 5。

指針概念

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

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

在這里插入圖片描述

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

指針的聲明和引用

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

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

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

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

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

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

指針代碼示例

指針的算術運算

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

示例程序

考慮以下程序:

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

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

輸出:

358010748
358010752

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

指針的加1

這是因為指針 p 是一個指向整型變量的指針,而一個整型變量在內存中占4個字節, 對 p 執行加1,應該得到的是下一個整型數據的地址,即在地址的數值上面應該加4。

相應地,如果是p+2的話,則打印出的地址的數值應該加8。

危險

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

比如,接著上面的例子,我們試圖打印 pp+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*等或者指向自定義結構體和類的指針。

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

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

示例程序

考慮一下程序:

#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;  //  強制類型轉換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

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

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

在這里插入圖片描述

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

void *

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

  1. void *時通用指針類型,它不針對某個特定的指針類型。在使用時將其賦值為指向某種特定的數據類型的指針時不需要做強制類型轉換。

  2. 由于不知道它指向的類型,因此不能直接對其進行解引用*p,也不能對其進行算數運算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;
}

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

在這里插入圖片描述

在程序中,x是整型變量,p指向xq指向pr指向q,這樣x, p, q, r的數據類型分別是:int, int*, int**, int***

  1. *p即對指針p的解引用,應該是 x 存儲的值,即6。
  2. *q是對指向指針的指針q的解引用,即其指向的地址 p 所存儲的值235。同時,這個值就是指針 p 的值,指向整型變量 x 的地址。
  3. **q是對*q的解引用,我們已經知道*q為235,則**q即地址為235的位置的值,是6。
  4. **r是對*r的解引用,而*r就是q,所以**r就是*q,235。
  5. ***r是對**r的解引用,同樣是235指向的值,6。

我們編譯運行該程序,得到的輸出是:

6
-1672706964
6
-1672706964
6

和我們分析的結果一致。

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

函數傳值 vs. 傳引用

在執行一個C語言程序時,此程序將擁有唯一的“內存四區”——棧區、堆區、全局區、代碼區.

具體過程為:操作系統把硬盤中的數據下載到內存,并將內存劃分成四個區域,由操作系統找到main入口開始執行程序。

內存四區

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

在這里插入圖片描述

函數調用

  1. 在程序未執行結束時,main()函數里分配的空間均可以被其他自定義函數訪問。

  2. 自定義函數若在堆區(malloc動態分配內存等)或全局區(常量等)分配的內存,即便此函數結束,這些內存空間也不會被系統回收,內存中的內容可以被其他自定義函數和main()函數使用。

函數傳值 call by value

假設新手程序員Albert剛剛學習了關于函數的用法,寫了這樣的程序:

#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()函數將a的值加1,然后打印出a = 11,但是,程序的實際運行結果卻是a = 10。問題出在哪里呢?

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

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

輸出:

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

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

在這里插入圖片描述

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

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

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

傳引用 call by reference

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

在這里插入圖片描述

實現如下:

#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

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

指針與數組

指針和數組常常一起出現,二者之間有著很強的聯系。

數組的聲明

當我們聲明一個整型數組int A[5]時,就會有五個整型變量:A[0] - A[4],被連續地存儲在內存空間中。

在這里插入圖片描述

數組與指針算術運算

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

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
// ...

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

數組名和指針

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

(這里還有一個要注意的小點:數組名不能直接自加,即不可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

數組/指針 取值/取地址

對于第 iii 個元素:

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

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

數組作為函數參數

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

例程1

我們現在定義一個SumOfElements()函數,用來計算傳入的數組的元素求和,該函數還需要傳入參數size作為數組的元素個數。在main()函數中新建一個數組,并通過sizeof來求得該數組的元素個數,調用該函數求和。

#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;
}

打印出的結果如我們所料,為15:

Sum of elements = 15

例程2

有人可能回想,既然我們已經將數組傳入函數了,能不能進行進一步的封裝,將數組元素個數的計算也放到調用函數內來進行呢?于是有了如下實現:

#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;
}

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

Sum of elements = 3

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

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

輸出結果:

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中傳入的數組A的大小僅有8字節,即一個指針的大小。

實際上,在編譯例程2時,編譯器會給我們一定的提示Warning:

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]);

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

分析

我們還是要畫出棧區來進行分析:

在這里插入圖片描述

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

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

指針與數組辨析

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

指針與字符數組

當我們在C語言中談論字符數組時,通常就是在談論字符串。

C語言中字符串的存儲

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

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

在這里插入圖片描述

在這個圖中,我們為了存儲字符串JOHN,我們使用了字符數組中的5個元素,其中最后一個字符 \0,用來標識字符串的結束。倘若沒有這個標識的話,程序就不知道這個字符串到哪里結束,就可能會訪問到5,6中一些未知的內容。

示例程序:

#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;
}

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

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

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

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

#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"; 		// 字符串就會儲存在分配給這個數組的內存空間中,這種情形下它會被分配在棧上

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

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

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

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

常量指針

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

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

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

指針與多維數組

指針與二維數組

二維數組概念

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

在這里插入圖片描述

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

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

例程

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

#include <stdio.h>int main(){int B[2][3] = {2, 3, 6, 4, 5, 8};printf("-----------------------\n");        // 指向一維數組的指針 	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");        // 指向一維數組的指針 	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;
}
  1. 第一組 (B&B[0]):數組名B是一個指針,其指向的元素是一個一維數組,即二維數組第一個元素(第一個一維數組)的首地址。而B[0]就是二維數組的第一個元素,即二維數組的第一個一維數組,對其進行取地址運算,故&B[0]就是第一個一維數組的地址,也即第一個指向第一個一位數組的指針。

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

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

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

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

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

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

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

小公式

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

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

指針與高維數組

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

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

三維數組概念

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

在這里插入圖片描述

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

小公式

同樣給出三維數組的小公式如下:

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

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

從右向左、從里向外看:

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

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

多位數組作為函數參數

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

二維數組做參數:

  1. void func(int (*A)[3]
  2. void func(int A[][3])

注意事項

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

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

    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); // 錯誤
    }
    

指針與動態內存

內存四區簡介

內存被分為四個區,分別是代碼區,靜態/全局區,棧區和堆區。

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

在這里插入圖片描述

在整個程序運行期間,代碼區,靜態/全局區,棧區的大小是不會增長的

有一個小點要說明一下:有堆、棧這兩種數據結構,也有堆、棧這兩個內存分區,內存中的棧基本是由數據結構中的棧實現的,而內存中的堆和數據結構中的堆毫無關系。堆可以簡單理解為一塊大的、可供自由分配釋放的內存空間。

之前我們已經介紹過在程序運行過程中,代碼區、靜態/全局區和棧區是怎樣運作的了,特別是函數調用時棧區的工作方式,我們特別進行了說明。

C/C++中的動態內存分配

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

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函數從堆上找到一塊給定大小的空閑的內存,并將指向起始地址的void *指針返回給程序,程序員應當根據需要做適當的指針數據類型的轉換。

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

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

free接收一個指向堆區某地址的指針作為參數,并將對應的堆區的內存空間釋放。

在這里插入圖片描述

new 和 delete

在C++中,程序員們通常使用newdelete操作符來進行動態內存的分配和釋放。以下是整型變量和整型數組的分配和釋放例程。

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

注意數組delete時要有[]

在C++中,不需要做指針數據類型的轉換,newdelete是類型安全的。它們是帶類型的,返回特定類型的指針。

malloc、calloc、realloc、free

malloc

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

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

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

  • 典型用法:

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

calloc

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

  • 典型用法:

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

realloc

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

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

  • 以下這種情況使用realloc相當于free

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

    以下這種情況使用realloc相當于malloc

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

free

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

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

使用場景

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

#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;
}

作者將這樣在運行時才知道數組的大小是不行的。但是筆者實驗過發現是可以的,這應該是C99支持的特性變長數組。

但是這并不妨礙我們試著練習用動態分配內存的方式來新建一個數組,我們可以這樣做:

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

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

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

別忘了手動釋放堆區內存。

free(A);

注意

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

內存泄漏

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

#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;
}   

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

allocate_stack

在這里插入圖片描述

對于調用allocate_stack的程序,在allocate_stack函數調用時,每次將數組創建在棧區,然后再函數返回時,程序自動將其棧幀釋放,數組也被釋放掉,不會占用內存。

allocate_heap

在這里插入圖片描述

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

函數返回指針

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

示例程序

考慮這樣一個程序:

#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);
}

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

并且這種函數傳參的方式我們之前已經講過,成為值傳遞。

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

#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);
}

函數返回指針

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

#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);
}

這個版本在作者測試時是正常的,但是如果在打印結果之前再多調用一個函數printHello,則會導致輸出錯誤。這究竟是怎么回事呢?我們還是要借助棧區內存分析。

在這里插入圖片描述

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

我們看到,在調用Add是,有Add自己的棧幀,其中存放兩個傳入的指向整型的指針ab,指向main函數棧幀中的我們想要加和的兩個整型變量xyAdd的棧幀中還有一個整型變量c,是我們的計算結果,按照上面的程序寫法,Add函數返回一個整型指針指向變量c,即main中的ptr

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

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

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

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

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

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

使用場景

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

我們嘗試在堆區分配內存,將上面的程序中的Add函數改為:

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

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

在這里插入圖片描述

這樣,Add返回的指針所指向的地址存放在堆區,不像棧區一樣在函數返回之后被程序自動釋放,可以在main函數中正常地進行訪問。堆區內存使用結束之后不要忘記釋放。

函數指針

代碼段

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

在這里插入圖片描述

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

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

在這里插入圖片描述

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

函數指針的定義和使用

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

#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);
}
  1. 聲明函數指針的語法是:int (*p)(int, int),這條語句聲明了一個接收兩個整型變量作為參數,并且返回一個整型變量的函數指針。注意函數指針可以指向一類函數,即可以說,指針p指向的類型是輸入兩整型,輸出一整型的這一類函數,即所有滿足這個簽名的函數,都可以賦值給p這個函數指針。

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

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

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

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

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

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

    void (*p)(int, int);
    int (*p)(int);
    int (*p)(int, char);
    

函數指針的使用案例(回調函數)

回調函數的概念

這里使用函數指針的案例都圍繞著這么一個概念:函數指針可以用作函數參數,而接收函數指針作為參數的這個函數,可以回調函數指針所指向的那個函數

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

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

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

回調函數的實際使用場景

#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");
}

輸出排序結果:

-5 -4 -1 2 3 7 9

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

即實現如下:

#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");
}

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

輸出:

-1 2 3 -4 -5 7 9

另外回調函數還有更多有趣的應用,比如事件回調函數等。

Ref:

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/532760.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/532760.shtml
英文地址,請注明出處:http://en.pswp.cn/news/532760.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

android雙聯動列表,Android Fragment實現列表和內容聯動

在平板上經常能看到這種的情況&#xff1a;左邊是一個列表&#xff0c;右邊是列表項對應的內容&#xff0c;當點擊某一個列表時&#xff0c;右邊內容區也會隨之改變。下面使用fragment簡單的demo&#xff1a;思路&#xff1a;在mainactivity定義一個回調接口&#xff0c;并在列…

linux /proc 詳解

linux /proc 詳解 本文整理了一下 linux /proc下的幾個常用的目錄和文件&#xff0c;可供查閱&#xff0c;之后在學習工作中有別的用到的話會再補充。 /proc 簡介 Linux系統上的/proc目錄是一種文件系統&#xff0c;即proc文件系統。與其它常見的文件系統不同的是&#xff0…

android模擬器太卡,安卓模擬器安裝之后太卡怎么解決

用安卓模擬器玩游戲原理就是在電腦上安裝了一部手機&#xff0c;如果你的電腦配置不是非常高&#xff0c;能不卡頓嗎?遇到卡頓怎么解決?1、安裝最新版本的顯卡驅動。逍遙模擬器對于顯卡的性能要求很高&#xff0c;因此升級至最新版本的顯卡驅動&#xff0c;是確保逍遙模擬器流…

編程環境中Runtime(運行時)的三個含義

編程環境中Runtime&#xff08;運行時&#xff09;的三個含義 轉自&#xff1a;https://www.zhihu.com/question/20607178 知乎答主doodlewind 三個含義 實際上編程語境中的 runtime 至少有三個含義&#xff0c;分別是&#xff1a; 指「程序運行的時候」&#xff0c;即程序…

非常不錯的一款html5【404頁面】,不含js腳本可以左右擺動,原生JavaScript實現日歷功能代碼實例(無引用Jq)...

這篇文章主要介紹了原生JavaScript實現日歷功能代碼實例(無引用Jq),文中通過示例代碼介紹的非常詳細&#xff0c;對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下成品顯示&#xff0c;可左右切換月份html 代碼移動端日歷日一二三四五六css代碼*{margin: 0;pa…

12 [虛擬化] 進程抽象;fork,execve,exit

12 [虛擬化] 進程抽象&#xff1b;fork&#xff0c;execve&#xff0c;exit 南京大學操作系統課蔣炎巖老師網絡課程筆記。 視頻&#xff1a;https://www.bilibili.com/video/BV1N741177F5?p12 講義&#xff1a;http://jyywiki.cn/OS/2021/slides/8.slides#/ 本講概述 回到“…

計算機應用與基礎實踐怎么考,自考計算機基礎應用科目筆試和實踐性考試怎么考...

自考計算機基礎應用科目筆試和實踐性考試怎么考&#xff1f; 報考自考的考生有些專業的考生會在自己的課程科目中發現計算機基礎應用不僅有理論知識考試還有實踐性考試&#xff0c;那么自考計算機基礎應用科目的筆試和實踐性考試怎么考&#xff1f;自考計算機基礎應用科目筆試怎…

14 [虛擬化] 虛存抽象;Linux進程的地址空間

14 [虛擬化] 虛存抽象&#xff1b;Linux進程的地址空間 南京大學操作系統課蔣炎巖老師網絡課程筆記。 視頻&#xff1a;https://www.bilibili.com/video/BV1N741177F5?p14 講義&#xff1a;http://jyywiki.cn/OS/2021/slides/10.slides#/ 本講概述 程序 狀態機&#xff1b;…

瀏覽器是指在用戶計算機上,自考《網頁設計與制作》測試題及答案

自考《網頁設計與制作》測試題及答案學習是一個不斷積累的過程&#xff0c;為幫助考生們更好地復習《與制作》科目知識點&#xff0c;以下是搜索整理的一份自考《網頁設計與制作》測試題及答案&#xff0c;供參考練習&#xff0c;希望對大家有所幫助!想了解更多相關信息請持續關…

Ubuntu 18.04 安裝OpenCV C++

Ubuntu 18.04 安裝OpenCV C 構建并安裝 僅構建核心模塊 # 更新并安裝依賴 # 更新并安裝依賴 sudo apt update && sudo apt install -y cmake g wget unzip# 下載并解壓包 wget -O opencv.zip https://github.com/opencv/opencv/archive/master.zip unzip opencv.zip…

html計算x的y,HTML5畫布:旋轉時計算x,y點

我開發了一個HTML5 Canvas應用程序&#xff0c;它涉及到讀取一個xml文件&#xff0c;該文件描述了需要在畫布上繪制的箭頭&#xff0c;直形和其他形狀的位置。的XML布局的HTML5畫布&#xff1a;旋轉時計算x&#xff0c;y點實施例&#xff1a;如果對象被旋轉它涉及計算一個點的位…

(2021) 20 [虛擬化] 進程調度

(2021) 20 [虛擬化] 進程調度 南京大學操作系統課蔣炎巖老師網絡課程筆記。 視頻&#xff1a;https://www.bilibili.com/video/BV1HN41197Ko?p20 講義&#xff1a;http://jyywiki.cn/OS/2021/slides/11.slides#/ 背景 — 機制與策略分離 機制&#xff1a;一個通用的、可定制…

計算機組裝過程英文版,計算機組裝與維護試題及答案(國外英文資料).doc

計算機組裝與維護試題及答案(國外英文資料)計算機組裝與維護試題及答案(國外英文資料)(1) choiceIn the following equipment, the input device is (b)A. b. b. c. c. c. d. d.In Windows 98, the combination of CTRL Alt Del is (c)A. cold start b. heat start c. interr…

make命令及makefile

make命令及makefile 轉自&#xff1a;https://www.ruanyifeng.com/blog/2015/02/make.html Make 命令教程 作者&#xff1a; 阮一峰 日期&#xff1a; 2015年2月20日 代碼變成可執行文件&#xff0c;叫做編譯&#xff08;compile&#xff09;&#xff1b;先編譯這個&#…

局域網中計算機網絡密碼查看,Win10怎么查看電腦上已知的wifi網絡密碼

方法一&#xff1a;網絡和共享中心查詢1、在Windows 10桌面最左下角的【Windwos開始圖標上右鍵】&#xff0c;在彈出的菜單中點擊打開【網絡連接】&#xff0c;如下圖所示。2、在打開的網絡連接設置中&#xff0c;雙擊已經連接的【無線網絡名稱】&#xff0c;在彈出的【WLAN狀態…

(2021) 22 [持久化] 1-Bit的存儲

(2021) 22 [持久化] 1-Bit的存儲 南京大學操作系統課蔣炎巖老師網絡課程筆記。 視頻&#xff1a;https://www.bilibili.com/video/BV1HN41197Ko?p22 講義&#xff1a;http://jyywiki.cn/OS/2021/slides/12.slides#/ 背景 回顧 操作系統是什么&#xff1f;一組對象 一組API…

計算機一級試題論述,計算機一級考試理論題及答案要點

計算機一級考試IT1必做題[1]. 著名的計算機科學家尼.沃思提出了________。A&#xff0e;數據結構&#xff0b;算法程序B&#xff0e;存儲控制結構C&#xff0e;信息熵D&#xff0e;控制論[2]. 下面有關掃描儀的敘述中&#xff0c;錯誤的是________。A&#xff0e;分辨率是掃描儀…

(2021) 23 [持久化] I/O設備與驅動

(2021) 23 [持久化] I/O設備與驅動 南京大學操作系統課蔣炎巖老師網絡課程筆記。 視頻&#xff1a;https://www.bilibili.com/video/BV1HN41197Ko?p23 講義&#xff1a;http://jyywiki.cn/OS/2021/slides/13.slides#/ 背景 很多人 (你們的同學們、家長們) 都有一個認識&…

計算機考研計劃時間,2019計算機考研時間安排:復習時間規劃

隨著考研競爭越來越激烈&#xff0c;考研復習一定要做好規劃&#xff0c;每天的時間要做好管理&#xff0c;分清輕重緩急&#xff0c;這樣才能高效率復習。管理的5個原則&#xff0c;大家抓緊調整個人復習。小編還為大家精心準備了計算機考研復習資料還有計算機考研報考指導助力…

(2021) 24 [持久化] 文件系統API

(2021) 24 [持久化] 文件系統API 南京大學操作系統課蔣炎巖老師網絡課程筆記。 視頻&#xff1a;https://www.bilibili.com/video/BV1HN41197Ko?p24 講義&#xff1a;http://jyywiki.cn/OS/2021/slides/14.slides#/ 背景 回顧 硬件視角&#xff1a;持久化的“層層抽象” 物…