總是有人認為數組就是指針,指針就是數組,兩者好像完全是一樣的東西。之前的我也曾幼稚的這樣認為過。其實,事
實並非這樣,指針就是指針,數組就是數組,兩者是完全不同的東西。我們之所以會認為數組就是指針,指針就是數
組,無非就是因為他們都可以“以指針的形式”和”以數組的形式“進行訪問。下邊我們分別來講解數組和指針。
(一)數組:
int a[5]; 我相信所有人都知道這是一個數組,裡邊包含了5個元素。
數組的定義及初始化:
int n = 4; int arr[n] = {1,2,3,4};以上數組的定義方法是初學者最容易犯的錯誤。因為n是一個變量,而數組的元素個數必須是一個常量。
那我們如果用const修飾的量,會不會出錯??在.c文件中,const修飾的量是只讀變量,注意仍然是變量,而在.cpp
文件中,const修飾的就是常量,我們可以用來定義數組個數。
但是,如果你這樣子做就是可以的,如下:
#define N 10 int arr[N] = {0};因為在預編譯階段標識符N就會被替換成10.
編譯器通常不會為普通const只讀變量分配內存,而是將他們保存在符號表(關於符號表,之後的博客中我將詳
細解析)中,const只讀變量在程序運行過程中只有一份拷貝,而宏在預編譯階段會進行替換。
注意:1.數組一旦給定,a就與這塊內存匹配不可改變,也就是a++,--a,這些都是不對的。
在博客strlen和sizeof中,有提過求數組大小,這裡再說一次。
sizeof(a)的值在32位系統下是20.
sizeof(a[0])在32位系統下是4.
sizeof(a[5])在32位系統下是4.並沒有出錯。一定要記住,sizeof是關鍵字而不是函數,函數求值是在運行的時
候,關鍵字求值是在編譯的時候,雖然不存在a[5]這個元素,但是這裡並沒有真正去訪問它。
2.只有在兩種場合下,數組名並不發生降級:當數組名作為sizeof的操作數或是單目操作符&的操作數時。
二維數組:二維數組其實就是一維數組。int b[2][3];這裡b可以看做是包含2個元素的數組,每個元素又是一個包含
3個元素的數組,在內存中的存儲方式如圖。
3.數組名能否作為左值和右值呢??數組名不能作為左值。當數組名是左值時,編譯器會認為數組名是數組首
元素的地址,而這個地址開始的一塊內存是一個整體,我們只能訪問數組中的某個元素,而不能把數組當成一個
整體進行訪問。數組名可以作為右值,作為右值時,表示的數組首元素的地址。
4.a和&a的區別:在strlen和sizeof那篇博客中,關於這個我們已經做過整理和區分,這裡給出例題來理解一下。
int main() { int a[5] = { 1,2,3,4,5 }; int *ptr = (int *)(&a + 1); printf("%d %d", *(a + 1), *(ptr - 1)); return 0; }我們可以仔細分析一下這道題目,看看最後輸出的結果到底是什麼呢??
答案是2 5. &a+1是指向數組的指針,指向數組元素5後邊的那塊內存,強制類型轉換,轉換成int *,所以結果是5.
如果沒有進行強制類型轉換,第二個結果還是1.
(二)指針
指針常量:請看下邊一段代碼。
int main() { int a = 10; *0x0018ff44 = 20; return 0; }注:vc++6.0下第一個變量的地址就是0x0018ff44.
上邊這段代碼到底能不能將a的值變為20呢??答案是否定的,因為編譯器認為0x0018ff44是一個十六進制數字,是
無法進行進行解引用。所以,使用指針常量的時候一定要進行強制類型轉換。
*(int *)0x0018ff44 = 20;這樣就可以將a的值改為20.
接著看下邊的例子:
int main() { int *p = (int *)0x0018ff44; *p = NULL; p = NULL; return 0; }
我們繼續在vc++6.0下調試這段代碼,發現當執行到*p = NULL;時,p = 0x00000000.這到底是為什麼呢??因為p
的地址是0x0018ff44 ,p裡邊保存的地址也是0x0018ff44 (自身的地址),所以*p就是p(在這段程序中是這樣),
所以改變*p也就改變了p。
在《c語言深度解剖》這本書中對這個問題作出了特別詳細的解釋,有興趣的讀者可以自行閱讀。
指針變量是存放變量的地址的變量。
一說到指針變量,我們的腦海裡應該會閃現出在三個和它密切相關的“人物”------指針變量的內容、指針變量的指
向、指針變量的地址。
char ch = 'a'; char *cp = &ch;我們來通過左值和右值來理解指針變量。
&ch 作為右值,&ch是ch的地址,也就同指針變量cp中存儲的變量一樣。作為左值是非法。
cp作為右值,表示cp的值,作為左值表示cp所代表的那塊內存。
&cp作為右值,表示指針變量cp的地址,作為左值非法。
*cp作為右值,表示cp所指向的空間的內容,作為左值表示cp所指向的那段空間。
*cp+1作為右值,表示的是字符‘b’,作為左值非法。
*(cp+1)作為右值,cp的下一段空間的內容,作為左值,表示cp的下一段內存。
++cp作為右值,表示cp的下一段內存的內容,作為左值非法。
cp++作為右值,表示cp的內容,作為左值非法。
*++cp作為右值,表示ch後邊的內存地址的值,作為左值表示ch後邊那塊內存位置。
*cp++作為右值ch的內容,作為左值,表示ch這段空間。
注:++的優先級大於*。
++*cp作為右值,表示cp指向的內存的內容加1,作為左值,非法。
(*cp)++作為右值,表示cp指向的內存的值,作為左值,非法。
注:(*cp)++與*cp++不一樣。
++*++cp作為右值,表示cp下一段內存指向的內存的內容加1,作為左值非法。
++*cp++作為右值,表示cp指向的內存空間的值加1,作為左值非法。
指針之間的運算:
指針-指針:是兩指針之間的元素的個數,單位是個數,不是字節。指針-指針的前提是兩指針指向同一塊內存;指針
類型相同。看下邊一個例子:
int main() { int arr[10] = {0}; int n = &arr[10] - &arr[0]; printf("%d",n); return 0; }注:以上代碼可以正確求出數組的大小。&arr[10],僅取地址,不會產生越界,如果改變arr[10],就會出錯。
說到這裡,肯定會有人說,那可不可以用數組的最後一個元素的地址減去數組第一個元素之前的那個地址來求數組大
小呢?答案是不可以。因為數組的前邊是不會預留空間,而數組末尾會預留空間。所以,不要讓指針指向-1號下標。
指針和數組講到這裡,想必大家也應該理解了吧。接下來,我將會對比著指針和數組,再來分析一些重要內容。
(三)指針,數組對比分析:
看以下的例子:
char arr [10] = "abcdef"; char *ap = arr+2;注意:圖中是ap不是cp
ap就是&arr[2];
*ap就是arr[2];
ap[0]就是arr[2];注意:c的下標引用和間接訪問表達式是一樣的。
ap+6就是&arr[8];
*ap+6為arr[2]+6;注意,間接訪問的優先級高於加法運算符。
*(ap+6)表示的是arr[8];
ap[6]也是arr[8];
&ap是表示存放ap的內存地址。
ap[-1]就是arr[1].
ap[9]實則是表達式arr[11],但是arr數組總共只有10個元素,ap[9]這個表達式越過了數組的右界,but編
譯器一般檢測不出來,所以如果在程序中寫出這樣的表達式,會出現意想不到的結果。
看下邊的代碼:
int main() { int i,arr[10]; for(i = 0;i<=12;i++) { printf("love you"); arr[i] = 0; } return 0; }
這個程序中數組發生了越界,導致程序死循環。當然這只是在vs編譯環境下。我們通過下邊的圖片看看為什麼死循環
注意:棧與其他數據結構不同,棧是先入棧的是在高地址。
如果在vs下,剛好越過了預留空間,改變了i的值,導致死循環。
如果是在vc++6.0下,arr[9]與i是挨著的。只要數組一越界。就會出問題。
如果在linux下的gcc編譯器中運行,貌似arr[9]與i也是挨著。(我將循環判斷條件改成i<=10,依然死循環)
指針數組與數組指針:
首先,我們必須明確指針數組是一個數組,每個元素都是一個指針。數組指針是一個指針,指向的是一個數組。
對於指針數組,我們並不陌生,main函數裡的一個參數就是指針數組。比如給定指針數組int *arr[5];
而數組指針呢?
注意注意:對於以上的指針數組,他的類型到底是什麼??應該是int(*)[5],指針變量是p,知道這一點很重要,
強制類型轉換時會用到。
千萬不要認為二維數組就是一個二級指針。二維數組其實就是一個一維數組,每個元素又是一個一維數組。
下邊一個關於二維數組傳參的例子:
void fun(int arr[2][3]) { ; } int main() { int arr[2][3]; fun(arr); return 0; }實參裡的arr,是數組首元素的地址,是一個指向大小為3的數組的指針。所以,在形參裡的那個第二維大小是不可省
略的。,除了代碼中給出的形參外,形參還可以是int[][3];或者是int (*arr)[3].牢記int arr[][].和int**arr這兩個都是不可
以的。
關於聲明和定義:
定義:就是創建了一個對象,並為這個對象分配了一塊內存,並取了名字。
聲明:(1)告訴編譯器,這個名字已經匹配到一塊內存了,下邊的代碼就不要用這個名字了。
(2)告訴編譯器,這個名字已經預定,別的地方不要使用它了。
從上邊的學習中,我們已經知道,數組和指針是不一樣的東西,所以定義為數組就要聲明為數組,定義為指針就要聲
明為指針,其他的都是不對的,下邊我們來一一分析一下。
定義為數組,聲明為指針:
比如:
int arr[] = "abcdef";在另一個文件中有如下聲明:
extern char *arr;如果我們在聲明的這個文件中輸出arr,將會出現什麼結果呢??程序崩潰,圖解如下:
而聲明的指針只占4個字節,他看到的就是0x61 0x52,0x63,0x64,他可能不是一個有效的地址。所以這樣不對。
定義為指針,聲明為數組:
定義:char* p = "abcdef";
聲明:extern char str[];
所以在聲明的那個文件中我們看到的str[0]就是0x00,str[1]就是0x12......是不會得到正確結果的。但是如果我們想要
輸出abcdef該怎樣輸出呢??
printf("%s",*(char **)str);
或者:printf("%s",(char *)*(int *)str);將str轉為int*,解引用,再強轉。
分析到這裡,所以指針就是指針,數組就是數組,不要亂用。
下邊整理出幾道比較重要的例題:
例1:
int a[5][5]; int (*p)[4] = NULL; p = (int (*)[4])a; printf("%d %p",&p[4][2]-&a[4][2],&p[4][2]-&a[4][2]);分析一下這個程序輸出的結果是什麼。。
當以%d格式輸出時,結果是4,這個就利用了指針相減那個知識點;當以%p輸出時,會輸出什麼呢??它會將-4變成
無符號數輸出,因為地址並沒有負數。
-4原碼:10000000 00000000 00000000 00000100
反碼: 11111111 11111111 11111111 11111011
補碼:11111111 11111111 11111111 11111100
所以%p格式輸出 ff ff ff fc
例2:
int a[4] = {1,2,3,4}; int *ptr1 = (int *)(&a +1); int *ptr2 =((int)a+1); printf("%x %x",ptr1[1],*ptr2);程序輸出結果會是什麼??下邊圖解:
所以程序輸出結果:4 20 00 00 00(小端模式)
其實,(int)a+1是將a轉換成整形再加1.