三.指針與數組的“愛恨情仇”
本將中指針的算術運算本應放在第二講中,但考慮到它與數組關系密切故將其納入本講。
1.指針的算術運算
在上一講指針初始化的第4種方式中提到了可以將一個T類型數組的名字賦給一個相同類型的指針,這說明指針可以和數組發生聯系,在后面我們會看到這種聯系是十分密切的。當有語句char ary[100] = {'a', 'b', 'c', 'd','e', 'f'}; char *cp = ary; 后,cp就指向了數組array中的第一個元素。我們可以通過指針來訪問數組的元素:PRintf("%d", *cp); 此語句的作用是打印出cp所指向的元素的值,也就是數組的第一個元素?,F在通過cp = &array[3];使cp指向數組中的第4個元素,然后我們就可以對它進行各種操作了。
實際中經常會用指針來訪問數組元素,當兩個指針指向同一個數組時,會用到指針的算術運算:
<1>.指針+整數 或 指針-整數
指針與一個整數相加的結果是一個另一個指針。例如將上面的cp加1,運算后產生的指針將指向數組中的下一個字符。事實上當指針和一個整數相加減時,所做的就是指針加上或減去步長乘以那個整數的積。所謂步長就是指針所指向的類型的大小(即指針移動一個位置時要跳過幾個字節)。下面的例子會讓大家更加明了:
intia[100] = {0, 1, 2, 3, 4, 5}; double da[100] = {0.0, 1.0, 2.0, 3.0, 4.0, 5.0};
int *ip =ia; double *dp = da;
ip += 3; dp += 3;
ip加上3實際進行的操作是ip + 4 * 3,因為ip指向的元素的類型為int;而dp加3實際進行的操作是dp + 8 * 3,因為dp指向的元素的類型為double;這正是指針需要定義基類型的原因,因為編譯器要知道一個指針移動時的步長。
要注意的是指針的算術運算只有在原始指針和計算出來的新指針都指向同一個數組的元素或指向數組范圍的下一位置時才是合法的;另外,這種形式也適用于使用malloc動態分配獲得的內存。
下面這段代碼在大多數編譯器上都是可以運行的,但它卻是不安全的,因為b元素后面的內存區域所存儲的內容是不確定的,有可能是受系統保護的,如果又編寫了對p解引用的語句,那么很可能會造成運行時錯誤:
int b;
int *p = &b;
p += 2;
printf("%p/n", p);
<2>。指針間的減法
當兩個指針指向同一數組或有一個指針指向該數組末端的下一位置時,兩個指針還可以做減法運算。
intia[100] = {0, 1, 2, 3, 4, 5};
int *ip =ia;
int *ig =ia +3;
ptrdiff_t n = ig – ip;
n應該為3,表示這兩個指針所指向的元素的距離為3,ptrdiff_t是一個標準庫類型,它是一個無符號整數,可以為負數。注意指針進行減法得到的結果指示出兩指針所指向元素間的距離,即它們之間相隔幾個數組元素,與步長的概念無關。
另外,一個指針可以加減0,指針保持不變;如果一個指針具有0值(空指針),則在該指針上加0也是合法的,結果得到另一個值為0的指針;對兩個空指針做減法運算,得到的結果也是0。注意:ANSI C標準沒有定義兩個指針相加的運算,如果兩個指針相加,絕大多數編譯器會在編譯期報錯。
2.指針與數組的愛恨情仇
數組和指針有著千絲萬縷的聯系,它們之間的問題困惑著不少朋友,有的朋友對它們的概念不是很清楚,所以可能會導致誤用,從而出錯。下面就對數組名是什么,數組什么時候和指針相同等相關問題做出解釋。
<1>.數組名
聲明中:當我們聲明一個數組時,編譯器將根據聲明所指定的元素數量及類型為數組保留內存空間,然后再創建數組名,編譯器會產生一個符號表,用來記錄數組名和它的相關信息,這些信息中包含一個與數組名相關聯的值,這個值是剛剛分配的數組的第一個元素的首地址(一個元素可能會占據幾個地址,如整型占4個,此處是取起始地址)。現在聲明一個數組:int ia[100]; 編譯器此時為它分配空間,假設第一個數組元素的地址為0x22ff00;那么編譯器會進行類似#define ia 0x22ff00的操作,這里只是模擬,真實情況并非完全一樣,我們在編程時無需關注編譯器所做的事情,但要知道此時(聲明時)數組名只是一個符號,它與數組第一個元素的首地址相關聯。注意:數組的屬性和指針的屬性不相同,在聲明數組時,同時分配了用于容納數組元素的空間;而聲明一個指針時,只分配了用于容納指針本身的空間。
表達式中:當我們在表達式中使用數組名,如:ia[10] = 25;時,這個名字會被編譯器轉換為指向數組第一個元素的常量指針(指針本身的值不可變),它的值還是數組的第一個元素的首地址(一個指針常量),編譯器的動作類似于int *const ia = (void *)0x22ff00; 這里我們應重點關注的是:數組名是一個常量指針(常指針),即指針自身的值不能被改變。如果有類似ia++或ia+=3這類的語句是絕對不對的,會產生編譯錯誤。注意:當數組名作為sizeof操作符的操作數時,返回的是整個數組的長度,也就是數組元素的個數乘以數組元素類型的大??;另外,在對數組名實施&操作時,返回的是一個指向數組的指針,而非具有某個指針常量值的指針(這個問題在后面會詳細論述)。
通過數組名引用數組元素時:在前面講過的指針算術運算中指針加上一個整型數,結果仍然是指針,并且可以對這個指針直接解引用,不用先把它賦給一個新指針。如int last = *(ia + 99);此時的ia已經是一個常指針了,這個表達式計算出ia所指向元素后面的第99個元素的地址,然后對它解引用得到相應的值。這個表達式等價于int last = ia[99];事實上每當我們采用[ ]的方式引用數組元素時,如:ia[99],在編譯器中都會轉換成指針形式,也就是*(ia + 99) (這里ia + 99和&ia[99]的值都為數組最后一個元素的首地址,所以*(ia + 99)和*&ia[99]得到的結果是一樣的,較難理解的是*&ia[99],按照優先級和結合性規則,先對ia[99]取地址再解引用,有些編譯器見到這種表達式會直接優化成ia[99]。)現在可以看出來在表達式中,指針和數組名的使用可以互換,但唯一要注意的就是:數組名是常指針,不能對它的值進行修改。ia + 99是可以的,但ia++是不行的,它的意思是ia = ia +1;修改了ia的值。
作為函數參數:先來了解一下函數的實參與形參。實參(argument)是在實際調用時傳遞給函數的值;形參(parameter)是一個變量,在函數定義或者原型中聲明。C語言標準規定作為形參的數組聲明轉換為指針。在聲明函數形參的特定情況下,編譯器會把數組形式改寫成指向數組第一個元素的指針。所以不管下面哪種聲明方式,都會被轉換成指針:
void array_to_pointer(int *ia){……} //無需轉換
void array_to_pointer(int ia[ ]){……} //被轉換成*ia
void array_to_pointer(int ia[100 ]){……} //被轉換成*ia
那么如果有下面的操作
void array_test(int ia[100])
{
doubleda[10];
printf("%d", sizeof( ia ));
ia++;
//da++; //編譯錯誤,數組名是常指針
}
輸出的結果為4,此時的ia是作為函數形參而聲明的數組,已經被轉換為了一個不折不扣的指針(不再是常指針了),因此ia++;是合法的,不會引發編譯錯誤。為什么C語言要把數組形參當作指針呢?因為C語言中所有非數組形式的數據實參(包括指針)均以值傳遞形式調用(所謂值傳遞就是拷貝出一個實參的副本并把這個副本賦值給形參,從此實參與形參是各不相干的,形參值的變化不會影響實參)。如果要拷貝整個數組,在時間和空間上的開銷都很大,所以把作為形參的數組和指針等同起來是出于效率原因的考慮。我們可以把形參聲明為數組(我們打算傳遞給函數的東西)或者指針(函數實際接收到的東西),但在函數內部,編譯器始終把它當作一個指向數組第一個元素(數組長度未知)的指針。在函數內部,對數組參數的任何引用都將產生一個對指針的引用。我們沒有辦法傳遞一個數組本身,因為它總是被自動轉換為指向數組首元素的指針,而在函數內部使用指針時,能對數組進行的操作幾乎和傳遞數組沒有區別,唯一不同的是:使用sizeof(形參數組名)來獲得數組的長度時,得到的只是一個指針的大小,正如上面所述的ia。但要注意:以上討論的都是數組名作為函數形參的特殊情況,當我們在函數體內聲明一個數組時,它就是一個普通的數組,它的數組名仍是一個常指針,所以上面的da++;仍會引起編譯錯誤,請大家不要混淆。
還有一點,既然是值傳遞,那么理所當然地,在用數組名作為實參調用函數時,實參數組名同樣會被轉換為指向數組第一個元素的指針。
<2>.指向數組的指針
好了,關于數組名的討論可以告一段落了,現在來看指針與數組的另一種聯系。在前面說過,當對一個一維數組的數組名進行 &操作時,返回的是一個指向數組的指針。現在我們就來看看什么是指向數組的指針。在C語言中,所謂的多維數組實際上只是數組的數組,也就是說一個數組中的每個元素還是數組,由于二維數組較為常用,所以本文著重討論二維數組,更多維數組的原理與二維數組相同。所謂二維數組(數組的數組),就是每個元素都是一個一維數組的一維數組。另外,請大家先有一個感性的認識:指向數組的指針主要用來對二維數組進行操作,大家不理解沒有關系,我會在后面詳細說明。
通常我們聲明一個指向一維數組中的元素的指針是這樣做的:int ia[100], *ip = ia; ip指向這個數組的第一個元素,通過指針的算術運算,可以讓ip指向數組中的任一元素。對于二維數組,我們的目的同樣是讓一個指針指向它的每一個元素,只不過這次的元素類型是一個數組,所以在聲明這個指針時稍有不同,假設有二維數組int matrix[50][100], C語言采用如下的方式來聲明一個指向數組的指針。int (*p) [100];比普通聲明稍復雜一些,但并不難理解。由于括號的優先級是最高的,所以首先執行解引用,表明了p是一個指針,接下來是數組下標的引用,說明p指向的是某種類型的數組,前面的int表明p指向的這個數組的每個元素都是整數。對于這個聲明還可以換一個角度來理解:現在要聲明的是一個指針,因此在標識符p前面加上*。如果從內向外讀p的聲明,可以理解為*p是int[100]類型,即p是一個指向含有100個元素的數組的指針。
有些朋友可能對于一個用來操縱二維數組的指針只使用一個下標表示困惑,為什么聲明不是int (*p) [50][100]呢?現在來回顧一下操縱一維數組的指針聲明int *ip = ia;它表示ip指向了一個數組的第一個元素,通過對指針的算術運算可以使它指向數組中的任何一個元素,編譯器不需要知道指針ip指向的是一個多長的數組。對于二維數組道理相同,int (*p) [100] = matrix; matrix可以看成是一個長度為50的一維數組,每個元素都是一個int[100]型的數組,p同樣指向了matrix數組的第一個元素(第一個int[100]型的數組),通過對p的算術運算也可以使它指向matrix數組中的任意一個元素而不需要知道matrix是一個多長的數組,但一定需要知道matrix中每個數組元素的長度,所以就有了int (*p) [100]這種形式的聲明。由此可知,如果進行p + n (n為整數)這樣的運算,每次的步長就是n * 100 * sizof (int),相當于跳過了矩陣中的n行,因為每行都有100個元素并且元素為整型,所以跳過了n * 100 * sizof (int)個字節,指向這些字節之后的位置?,F在,對指向數組指針的聲明方式的疑惑我認為已經講清楚了。下面來看一個關于數組長度的問題。
在C語言中沒有一種內建的機制去檢查一個數組的邊界范圍,完全是由程序員自己去控制,這是C語言設計的一種哲學或者說一種理念:給程序員最大的自由度,程序員應該知道自己在做什么。凡事有利有弊,自由度大了,出錯的幾率就高了。很有朋友(包括我自己)在初用數組時應該會或多或少地遇到過數組越界的問題。在前面的論述中提到了通過對指針的算術運算可以使它指向數組中的任何一個元素包括超出數組范圍的第一個元素,這個超出范圍的第一個元素實際上是不存在的,這個“元素”的地址在數組所占的內存之后,它是數組的第一個出界點,這個地址可以賦給指向數組元素的指針,但ANSI C僅允許它進行賦值或比較運算,不能對保存這個地址的指針進行解引用或下標運算。
<3>.再回首——數組名
現在又要開始數組名的討論了,之所以再回首而沒有一氣呵成,是因為在一維數組名和二維數組名之間需要一個過渡知識,就是指向數組的指針。在表達式中一維數組名會轉換為指向數組第一個元素的指針,二維數組也是一樣的,請大家牢記在C語言中二維數組就是數組的數組,所以也會被轉換為指向第一個元素的指針,它的第一個元素是一個數組,所以最終的結果就是二維數組名被轉換成指向數組的指針。
來看int(*p) [100] = matrix;此時的matrix被轉換為一個指向數組的指針,對于matrix[n],是matrix數組的第n+1個元素的名字,也就是matrix數組中50個有著100個整型元素的數組之一,所以可以有p = &matrix[n]; 即p指向了一個數組元素,也就是矩陣中的某一行,matrix[n]本身是一個一維數組的數組名,它會被轉換為指向數組第一個元素的指針,因此可以有int *column_p = matrix[n];這個表達式是最常見的也最容易理解。如果對matrix[n]進行sizeof操作結果是100*sizeof(int);而sizeof(matrix)結果是50*100*sizeof (int)。
總結一下,p和matrix都是指向矩陣的一行(一個整型數組),p+ m 或者matrix + m都將使指針跳躍m行(m個整型數組),column_p和matrix[n]都指向某行(一個整型數組)的第一個元素,column_p + m和matrix[n] + m都將使指針跳躍m個整型元素。假若要訪問二維數組matrix中第1行第1列(注意數組下標從0開始)的元素可以有以下的幾種方式(i為int型變量):
通過數組名引用 通過指針p的引用 通過指針column_p的引用
i = matrix [0][0]; i = *(*(p+0)+0); column_p = matrix[0];
i = *(matrix [0]+0); i = *(p[0] + 0); i = *(column_p+0);
i = *(*(matrix+0)+0); i = (*(p + 0))[0]; i = column_p[0];
上面的各種表達式中的“+0”均可以省略掉,但如果數字不是0就不能省略了,由此在引用第1行第1列的元素時會產生一些簡化的表達式,如下:
通過數組名引用 通過指針p的引用 通過指針column_p的引用
i = matrix [0][0]; i = **p; column_p= matrix[0];
i = *matrix [0]; i =*p[0]; i= *column_p;
i = **matrix; i = (*p )[0]; i = column_p[0];
現在來看下面語句的輸出,它們可能會讓你感到困惑:
printf("%p/n", &matrix); 對應的指針操作:無
printf("%p/n", matrix); 對應的指針操作:printf("%p/n", p);
printf("%p/n",&matrix[0]); 對應的指針操作:printf("%p/n", p);
printf("%p/n",matrix[0]); 對應的指針操作:printf("%p/n", column_p);
printf("%p/n",&matrix[0][0]); 對應的指針操作:printf("%p/n", column_p);
在我機器上的輸出是:
0022B140
0022B140
0022B140
0022B140
0022B140
輸出的值雖然一樣,但這些參數的類型卻不完全相同。下面一一做出解釋:
&matrix: 對二維數組名取地址,返回一個指向二維數組的指針;
matrix: 二維數組名會被轉換為指向第一行(第一個數組)的指針,與&matrix[0]等價;
&matrix[0]:對第一個一維數組的數組名取地址,返回一個指向一維數組的指針;
matrix[0]: 二維數組中第一個一維數組的數組名,與&matrix[0][0]是等價的;
&matrix[0][0]:對第一行第一列元素取地址,返回一個指向整型元素的指針。
在ANSIC標準中沒有說明對一個數組名進行&操作是否合法,但現在的編譯器大都認為是合法的,并且會返回一個指向數組的指針。簡單地說就是:對一個n維數組的數組名取地址得到的是一個指向n維數組的指針。
另外,上例中相對應的指針表示方式我也寫了出來。對于&matrix沒有相對應的指針表示方式,因為我們沒有定義那種類型的指針,用p是表示不出來的,如果對p進行&的話,得到的是p這個指針的地址,而不是matrix的地址,兩者完全不同,值也不會相同的。
再次提醒大家:無論是matrix還是matrix[0],它們都是數組名,都會被轉化為一個常指針,不能修改它們自身的值。對于&matrix、&matrix[0]、&matrix[0][0],它們得到的都是常量(指針常量),表示的是物理內存的地址,同樣不能修改它們的值。本質上講就是你不能也不可能修改一個物理內存的地址。
<4>.指針數組
在聲明一個指向數組的指針時千萬不要丟到那個括號,如:int (*p) [100];如果丟掉了括號那就完全改變了意圖,從而意外地聲明了一個指針數組。指針數組要比指向數組的指針好理解,而且前面已經有了一些鋪墊,這個概念相信大家可以很輕松地搞定。
所謂指針數組就是一個數組它的所有元素都是指針,這與普通的數組沒什么區別,不過元素是指針罷了。下面來聲明一個指針數組char *cars[10];這種方式可能不太利于理解,如果寫成char* cars[10];的形式,可讀性就很強了,它明確表示了cars是一個具有10個元素的數組,每個元素的類型都是char*。下面舉一個完整的例子并用它來結束這段指針與數組的愛恨情仇。
#include<stdio.h>
voiddisplay_car_brands(const char *brand_table[], int size) // 有關參數和局部變量中的const解析請參見第6章Section 3.
{
const char **cbp;
for(cbp = brand_table; cbp < brand_table+ size; cbp++) {
printf("%s/n", *cbp);
printf("%c/n",**cbp);
}
}
int main( )
{
const char *cars[] = { "ASTONMARTIN",
"AUDI",
"BENZ",
"BENTLEY",
"BMW",
"BUGATTI",
"FERRARI",
"JAGUAR",
"LAMBORGHINI",
"MASERATI",
"MAYBACH",
"ROLLSROYCE"
};
int array_size = sizeof(cars)/sizeof(cars[0]);
display_car_brands(cars,array_size);
return 0;
}
首先我們定義了一個指針數組,每個元素都是一個指向char類型的指針,并將它初始化。初始化后的數組有12個指針元素,分別指向以上的各個字符串(即保存著每個字符串首字符的地址)。在display_car_brands()中定義了一個二級指針cbp,指向指針數組的第一個元素,通過自增cbp,遍歷每一個數組元素,正如上圖所示(這種數組就是所謂的鋸齒型數組,也叫交錯數組,即jagged array)。
對cbp解引用得到每個數組元素的值(即每一個字符串首字符的地址),然后通過printf("%s/n", *cbp);語句來輸出每個字符串,注意*cbp得到的是字符串首字符的地址,通過%s格式項接收一個地址以輸出整個字符串。接下來的printf("%c/n", **cbp);輸出每個串的第一個字母,**cbp首先得到每個字符串首字符的地址,再對該地址解引用得到相應的字符。因此程序的輸出為:
ASTON MARTIN
A
AUDI
A
BENZ
B
BENTLEY
B
BMW
B
BUGATTI
B
FERRARI
F
JAGUAR
J
LAMBORGHINI
L
MASERATI
M
MAYBACH
M
ROLLS ROYCE
R
新聞熱點
疑難解答