文章目錄
- 數組名的理解
- 使用指針訪問數組
- 一維數組傳參的本質
- 冒泡排序
- 二級指針
- 指針數組
- 指針數組模擬二維數組
數組名的理解
之前我們在使用指針訪問數組內容時,有這樣的代碼:
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int* p=&arr[0];
這里我們使用&arr[0]
的方式拿到了數組第一個元素的地址,但是其實數組名本來就是地址,而且是數組首元素的地址。
我們來做個測試,看數組名到底是不是首元素的地址:
從打印出來的值來看,它們確實是一樣的,所以數組名確實是首元素的地址。
那么數組名是首元素地址的話,我們去求地址長度,在32位平臺下應該為4個字節(地址的長度與多少位的平臺有關)
我們用sizeof
計算下地址長度是多少:
這是為什么呢?
數組名就是數組首元素(第一個元素)的地址,但是有兩個例外:
sizeof(數組名)
,sizeof中單獨放數組名時,這里的數組名表示整個數組,不是首元素的地址,此時計算的是這整個數組的大小,單位是字節。&數組名
,這里的數組名表示整個數組,取出的是整個數組的地址(整個數組的地址和數組首元素的地址是有區別的)
從值的角度來看的話,取整個數組的地址與取首元素的地址打印出來的值一模一樣。
這是因為無論是取首元素地址,還是取整個數組的地址,它們的值都是從首元素的地址開始的。
但是我們平時寫代碼的時候要知道,數組名前+取地址符號是取整個數組的地址。
如果我們想看看它們的區別還是可以看到的,我們給首元素的地址、整個數組分別都+1:
我們發現,前兩個地址+1都只跳過了一個整形的大小,而第三個地址+1卻跳過了一個數組的大小(40個字節)
所以我們將三個地址打印出來只能看到打印出來的值是一樣的,但是類型絕對是不同的,因為int*
類型+1跳過的應該是一個整形,而不是40個字節。
除此之外,任何地方使用數組名,數組名都表示首元素的地址。
使用指針訪問數組
有了前面知識的支持,再結合數組的特點,我們就可以很方便的使用指針訪問數組了。
為什么在訪問數組的時候可以使用指針呢?
1.因為數組在內存中是連續存放的,只要是連續存放的,只要找到第一個,就可以順藤摸瓜找到其他的了。
2.指針±整數的運算,方便我們獲得每一個元素的地址
接下來我們寫個代碼,實現用指針訪問數組,給數組的每個元素輸入值,再輸出值。
注意:
數組就是數組,是一塊連續的空間(數組的大小與元素個數和元素類型都有關系)
而指針(變量)就是指針(變量),是一個變量(通常是4/8個字節)
那么它倆之間的聯系是什么呢?
數組名是地址,是首元素的地址,可以使用指針來訪問數組
拓展:
我們知道arr[i]
與*(arr+i)
這兩種寫法是完全等價的,并且*(arr+i)
里面的式子是加法,加法是支持交換律的。所以arr[i]==*(arr+i)==*(i+arr)
我們代入代碼中看會不會錯:
結果也是正確的。
既然這三個等價:arr[i]==*(arr+i)==*(i+arr)
*(arr+i)
可以寫成arr[i]
,那么*(i+arr)
可不可以寫成i[arr]
呢?
我們帶入代碼試試:
我們發現即使是這樣程序也沒有問題。那么這說明了什么呢?
我們知道[]
是個下標引用操作符,既然加號操作符+
可以實現:1+2
可以寫成2+1
;那么,arr[i]
也就可以寫成i[arr]
了
提示:這種方式雖然可行,但是不推薦。
因為這樣寫不利于閱讀代碼。
一維數組傳參的本質
數組我們學過,數組是可以傳遞給函數的,現在我們討論一下數組傳參的本質。
首先從第一個問題開始:
寫個函數實現數組元素的打印,我們之前都是在函數外部計算數組元素的個數:
那我們可以只把數組傳給函數,少傳一個參數。然后在函數內部求數組的元素個數再打印數組嘛?
此時我們這樣寫卻出現錯誤,數組里的元素只打印了一個,這是為什么呢?我們進行調試:
- 按F11開始調試,創建數組執行完后打開監視,查看創建的數組有沒有問題:
- 發現創建數組沒有問題后按F11進入函數內,在監視里輸入
arr,10
查看數組傳參有沒有問題:
此時數組的傳參也沒有問題,我們繼續調試。 - 當我們繼續往下執行的時候卻發現原本期望的值是10,這里
sz
卻顯示的是1
如果sz
是1的話,在下面循環中只能循環一次,所以也就只能打印第一個元素的。
此時就找到了問題所在,是sz
出問題了。
所以這為什么會算錯呢?
我們之前講過,數組傳參有兩個例外:sizeof(數組名)
, &數組名
。
只有這兩種例外表示的是整個數組,而現在這段代碼中的Print(arr)
并不算這兩種情況,所以此時傳的是首元素的地址。
那我們將首元素的地址傳過去,Print()
函數接收不應該用指針嘛?
這個函數接收的正確寫法就應該是int* p
這樣的指針來接收。
那為什么我們之前用int arr[10]
這樣數組的形式去接收呢?
因為數組傳參的時候,形參的部分是可以寫成數組的形式的。對于我們之前初學來說,傳的是數組,就用數組來接收,這樣講法對于我們初學接受度很好。
形參的部分雖然可以寫成數組的形式,但是本質上還是指針變量(地址)。就相當于這個地方是int* arr
既然Print()
不屬于那兩種例外,所以傳的就是個地址,形參也就是個指針變量。
那么下面的sizeof(arr)
就不是求一個數組的大小了,而是求一個指針變量的大小:
在x86
的環境下,無論什么類型的指針,都是4個字節。
所以數組傳參的時候,形參是可以寫成數組的形式的,但是本質上還是一個指針變量(地址),下面要求大小的時候
sizeof(arr)
求的就不是一整個數組的大小,而是首元素地址的大小。
所以int sz = sizeof(arr) / sizeof(arr[0])
是得不到元素個數的
還有一點要說明:
我們說數組傳參的時候傳的是首元素的地址(假設是0x0012ff40
),所以傳給函數的地址也是0x0012ff40
。
所以我們在這個函數里使用的都是0x0012ff40
這個地址
既然實參使用的是0x0012ff40
,形參也是使用0x0012ff40
,那么實參跟形參使用的數組不就是一樣的嘛?
所以我們得到以下結論:
- 數組傳參的本質是傳遞了數組首元素的地址,所以形參使用的數組跟實參使用的數組一定是同一個數組。
- 既然形參使用的數組跟實參使用的數組是同一個數組,所以形參的數組是不會單獨再創建數組空間的,形參的數組是可以省略掉數組大小的:
注意:無論形參部分寫不寫10,都不影響下面int sz = sizeof(arr) / sizeof(arr[0])
這個表達式(正確寫法時這個表達式一定要放在函數外面!)
接下來我們將這段錯誤代碼改過來:
1.既然數組傳的是地址,函數的形參就寫成指針
2.求數組元素的表達式放在函數外面
正確代碼:
冒泡排序
冒泡排序解決的是排序的問題。
冒泡排序的核心思想就是:兩兩相鄰的元素進行比較。
假設我們這里有一串降序的數字:9 8 7 6 5 4 3 2 1 0
我們要排成升序:0 1 2 3 4 5 6 7 8 9
,此時該怎么用冒泡排序排成升序呢?
就這樣一對一對的比較下去,最后9會到最后:
因為9是最大的一個元素,所以它無論與誰進行比較,都來到最后一位。
這樣將9放在最后,我們叫做一趟冒泡排序
所以剩下該解決9前面的數字
所以,一趟冒泡排序解決一個數字。第一趟解決了最大的,第二趟解決了次大的…
那么這個地方有10個元素,要有幾趟冒泡排序呢?
9趟。因為前9個數字在進行冒泡排序后,最后一個數字應該已經在它們應該在的位置上了。
所以是n
個元素的時候,我們需要進行n-1
趟冒泡排序。
我們開始寫代碼進行實現:
-
先將元素和排序的函數創建出來
-
當我們把排序函數名字寫好之后就是傳參了,因為我們要排的是這個數組,所以數組需要傳進函數。
并且冒泡排序的趟數得依據元素的個數,而函數內部不能求元素個數,所以得在主函數里面求元素的個數 -
接下里寫冒泡排序里的內容
-
寫形參:指針接收數組(用數組形式也行),
int sz
接收元素個數。
因為這個函數只用排好序就行了,所以返回類型寫個void
就可以了。 -
因為冒泡排序是一趟解決一個元素,所以首先要考慮趟數
上面我們說過,當有n
個元素的時候,我們需要進行n-1
趟冒泡排序,所以i<sz-1
-
趟數確定后就思考一趟的排序過程:
一趟排序的過程就是兩個相鄰元素之間相比較,我們創建一個變量j
視為元素下標,使得相鄰兩元素之間相比較。 -
接下來我們分析:一趟冒泡排序要進行多少對比較。
以上面的例子來說,當我們進行第一趟冒泡排序的時候,待排序的元素有10個,需要進行9對比較
所以代碼循環次數可以寫成這樣,控制9次相鄰元素進行比較
但是我們發現:當我們進行第二趟冒泡排序的時候,待排序的元素只有9個,需要進行的是8對比較
所以比較次數也與趟數有關:
當第一趟冒泡排序時有10個待排元素,我們要進行9對比較;
當第二趟冒泡排序時有9個待排元素,我們要進行8對比較;
即第三趟冒泡排序時有8個待排元素,我們要進行7對比較…
所以我們將循環次數改為:j<sz-1-i
(因為i
控制了趟數)
此時當第一趟冒泡排序的時候,就可以進行9對比較;第二趟冒泡排序的時候,就可以進行8對比較了…
-
-
在冒泡排序函數執行完之后,我們寫個函數將這個被排完序的數組打印出來:
完整的代碼為:
void bubble_sort(int arr[], int sz)
{//趟數int i = 0;for (i = 0; i < sz - 1; i++){//一趟排序的過程int j = 0;for (j = 0; j <sz-1-i ; j++){if (arr[j] > arr[j + 1]){int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}}
}void print_arr(int arr[], int sz)
{int i = 0;for (i = 0; i < sz; i++){printf("%d ",arr[i]);}
}int main()
{int arr[] = {9,8,7,6,5,4,3,2,1,0};//排序int sz = sizeof(arr) / sizeof(arr[0]);//元素個數bubble_sort(arr,sz);//打印print_arr(arr,sz);return 0;
}
打印出的結果:
注意:這段代碼是可以優化的。
當需要排序的數組本身是接近有序的情況時:9 0 1 2 3 4 5 6 7 8
,此時只需要進行一趟冒泡排序就可以變成:0 1 2 3 4 5 6 7 8 9
那我們該怎么進行優化呢?
-
我們定義一個變量
flag
,在進行每趟冒泡排序之前都假設這趟元素已經有序: -
如果這趟數組不是有序的,將
flag
改為0:
例如9 0 1 2 3 4 5 6 7 8
這個數組第一趟冒泡排序時9與0交換了,所以這趟并不是有序的,就將flag
改為0,進行下一趟的比較。
-
下一趟全部比較完后發現并沒有進入到
if
語句中(0 1 2 3 4 5 6 7 8 9
8次比較全部不符合if中的表達式,沒進入if語句中),所以flag
還是為1,此時就可以跳出循環,不再進行第3趟、第4趟的比較排序了。
注意:這個break
跳的是這個循環,break
是在這個循環里的
所以,當任何一對元素交換了,就說明這一趟的元素不是有序的,就得進行下一趟的元素比較、交換。
若是下次沒有元素的交換,就說明這一趟的元素就是有序的,不用再進行下一趟了,所以直接跳出循環。
二級指針
我們寫段常見的代碼:
這就是我們之前用的一級指針。
那么什么是二級指針呢?
我們根據上面這段代碼畫出圖:
既然p
也有地址,那么我們就可以通過&p
拿到p
的地址,然后放進變量pp
里。此時pp
就存放著一個一級指針變量的地址,我們叫pp
為二級指針。
那么pp
的類型該怎么寫呢?
pp
的類型為int**
,完整寫法:int** pp=&p;
對于這個類型int**
我們該怎么去理解呢?
首先我們要知道,int**
是二級指針的類型。
所以一級指針與二級指針的類型理解思路是一樣的,分兩部分理解:
- 最后一個
*
說明該變量是指針 - 前面一部分說明該變量指向的對象類型
那么,此時p+1
與pp+1
各自跳過幾個字節呢?
我們知道,指針±整數與指針的類型有關。因為p
是整形指針,指向的對象為整形,所以p+1
跳過4個字節。
那么pp+1
呢?
因為pp
指向的是int*
類型的變量,也就是指針變量。
所以我們就要看指針變量的大小為多少,就跳過幾個字節。有可能是4個字節,也有可能是8個字節。
如果我們想取出pp
的地址可以嗎?
當然是可以的,此時得寫成:int*** ppp=&pp;
同樣的:int**
說明ppp
指向的對象(pp
)是int**
類型的,最后一個*
說明ppp
是指針變量。此時ppp
就是一個三級指針。
可以一直往下推,但是不建議。
那么二級指針到底是怎么樣用的呢?
如果我們要通過pp
找到p
有什么辦法呢?
解引用pp
將p
的地址打印出來:
*pp
——訪問pp
里存的地址,找到這個地址后拿該地址里的數據(對pp里的地址進行解引用)
我們也將a
的地址打印出來,看看*pp
的值是不是a
的地址
發現值確實一樣。
我們通過對二級指針解引用,可以找到一級指針內存的數據,再進行解引用,就可以找到10了.
*pp==p==&a;
//通過對pp里的地址解引用找到p(a的地址)
//對*pp再次解引用:
**pp==*p=10
所以,對二級指針變量兩次解引用可以間接的找到10.
指針數組
指針數組是指針還是數組呢?
答案是數組。
我們類比一下:整型數組,是存放整型的數組;字符數組,是存放字符的數組。
所以指針數組,是存放指針的數組。
指針數組的每個元素都是地址,可以指向?塊區域
那么指針數組有什么用呢?
我們可以用指針數組模擬二維數組。
指針數組模擬二維數組
舉例:
當我們寫下這樣的代碼的時候,我們知道,這三個數組各自是一塊內存空間,在內存里也并不是連續存放的,可能離得很遠。
如果我們想把這三塊空間弄成二維數組:將這三個數組分別當成二維數組的第一行、第二行、第三行。
我們該怎么辦呢?
因為數組名是首元素的地址,我們將這三個數組首元素的地址放入到一個指針數組arr
中,通過訪問這個指針數組,再訪問到這三個數組。
這樣就可以模擬出一個二維數組了:
當我們寫上arr[0]
的時候,就是訪問arr
數組中arr1
這個元素,而這個元素是arr1
數組首元素的地址,所以我們就可以在這個地址的基礎上進行+整數,進行arr1
數組元素的遍歷。
同理,當我們寫上arr[1]
的時候,就是訪問arr
數組中arr2
這個元素,而這個元素是arr2
數組首元素的地址,所以我們也就可以找到arr2
數組中的每個元素了…
//可以看作訪問第一行所有元素
arr[0][j]; j:0~4//訪問第二行所有元素:
arr[1][j]; j:0~4//訪問第三行所有元素:
arr[2][j]; j:0~4
這樣就實現了指針數組模擬二維數組
因為arr1
共有5個元素,首元素地址+0為元素1的地址,首元素+1為元素2的地址…首元素+4就為元素5的地址了,所以整數j
的取值范圍就為0~4
我們繼續寫代碼:
實現了每個元素的打印。
注意:這種寫法并不是真的二維數組,真的二維數組在內存中是連續存放的,而這種寫法只是通過地址將三個分散的數組看為三行,再進行打印,這里的arr
并不是真的二維數組。
那么既然arr
并不是真的二維數組,那么代碼中的打印為什么要寫成二維數組的形式呢?
因為: