存儲和處理是程序設計的基本矛盾。存儲中也有處理,是基本處理,例如,機器指令中的操作碼,C語言內置類型中的運算符。隨著處理越來越復雜,程序設計的基本矛盾不斷向前發展,從而推動了程序語言的發展。指針(在機器語言中是地址)是存儲和處理的“媒介”、“中介”,是語言的要素,它隨著處理越來越復雜也在同時向前發展。
1函數參數與指針
C語言程序是由函數構成的,函數表示處理,實參表示存儲,函數的指針參量表示存儲和處理的中介,實參初始化形參,函數通過指針處理存儲中的數據。以表1為例。

在下面的函數原型中,形參pa的聲明是等價的,都表示指針,都是存儲與處理的中介:
int Sum(int *pa,int n);
int Sum(int pa[6],int n);
int Sum(int pa[],int n);
2模塊化設計與指針
一組存儲中的數據通過傳址在函數之間傳遞。如果這組數據是“只讀”的,那么如何保證它不被改寫?在模塊化程序設計中,程序按模塊編譯,如果在模塊單獨編譯階段就對“只讀”數據的安全性進行控制,即保證“只讀”數據把地址傳給的是“只讀”函數,就會減少連接調試階段的工作負擔。const限定修飾符便是這種控制的工具。
const限定符既可以限定存儲中的“只讀”數據,也可以限定“只讀”函數。被const修飾的數據稱為const常量,它必須初始化;被const修飾的函數具有被const修飾的指針參量,這個指針稱為指向const常量的指針,表示函數對該指針指向的數據是“只讀”的。const常量的聲明格式為:
const 類型標識符 變量標識符=初始化數據;
或
類型標識符 const 變量標識符=初始化數據;
指向const常量的指針,其聲明格式為:
const 類型標識符 *指針變量標識符;
或
類型標識符 const *指針變量標識符;
應用舉例:
void Display(const int *pa,int n);//終端顯示。“只讀”函數。
void Selection(int *pa,int n);
//選擇排序。非“只讀”函數。
const int a[5]= {1,3,2,5,4};
//const常量數組。
int b[5]= {1,3,2,5,4};
//非const常量數組。
Selection(a,5);//非法!
Display(a,5);//合法。
Selection(b,5);//合法。
Display(b,5);//合法。
指針是復合類型,它有兩個值,一個是指針自身的數據(無符號整型值),表示地址,另一個是它指向的數據(指針基類型值),是指針間接引用的對象。const修飾的部分不同,意義不同。
如果const修飾的是指針指向的數據,那么它是在修飾在修飾函數,表示以該指針為參量的函數對該指針指向的數據是“只讀”的,該指針就是指向const常量的指針。對這樣的指針,有下面幾點需要認識:
① 數據無論是不是const常量型,都可以傳址給指向const常量的指針。例如上面的調用語句Display(b,5),其中數組b并不是const常量型的。用實參和形參的關系來表示便是
const int *pa=b;
但是const常量型數據只能傳址給代表函數“只讀”性質的指向const常量的指針。可以把帶有指向const常量指針參量的函數比作一個認真辦事的人,什么樣的事情交給他,他都認真處理,而一件需要認真處理的事情一定要交給他。
② 因為指向const常量的指針表示的是函數的“只讀”性質,而不是指針本身的數據的只讀性質,所以與const常量不同,這種指針不必初始化。例如:
const int *pa=b;
可以分解為
const int *pa;
pa=b;
而且對它本身的數據可以改變,例如:
const int *pa;
pa=b;//指向數組b
pa=a;//又指向數組a
③ 傳遞性。指向const常量的指針表示的是函數的“只讀”性質,任何數據傳址給這樣的指針,不僅具有這種指針參量的函數對該數據是“只讀”的,而且該函數調用的其它函數對該數據也是“只讀”的,這就是說,指向const常量的指針只能傳值給同類指針。仿佛一個認真辦事的人,什么事情交給他,他都認真處理,不僅如此,他所尋求的合作伙伴,也一定是認真辦事的人。例如:
void Display(const int*pa,int n);//輸出函數。
int Sum(const int* ps,int n)//求和函數。
{
Display(ps,n);// const int* pa=ps;
……
}
④ 引入const修飾符之后,任何函數,如果對某一指針參量指向的數據是“只讀”的,都必須把該指針參量限定為指向const常量的指針,表明該函數的“只讀”性質,以保證const常量型數據通過傳址調用該函數,被編譯器檢錯。
如果const修飾的是指針本身,那么它是在修飾數據,表示指針本身的值const常量型的,這樣的指針稱為const常量指針。與const常量型一樣,const常量指針必須初始化,而且其值不能改變。聲明格式為:
類型標識符 *const指針標識符=初始化數據;
例如:
const int a[5]= {1,3,2,5,4};//const常量數組。
int b[5]= {1,3,2,5,4};//非const常量數組。
int c[5]= {1,3,2,5,4};//非const常量數組。
int *const pc=b;//const常量指針必須初始化。
pc=c;//非法!const常量指針的值不能改變。
因為const常量指針不是限定函數,對它指向的數據可以修改,所以不能把const常量型數據的地址賦給const常量指針。例如:
pc[0]=10;//合法。
int *const ps=a;//非法。
3運算符函數與指針
3.1運算符函數
運算符處理的對象如果是語言內置基本類型(整型、浮點型、字符型等),它的意義是內定的。如果是用戶定義的結構,意義就是待定的。以結構數組的查找Find為例:
struct Student//用戶結構
{
long ID;double g;//ID表示學號,g表示成績。
};
typedef Student Type;//形式數據類型Type。
int Find(const Type *pa,int n,Type item)//查找。
{
for(int i=0;i if(pa[i]==item)//待定。 return(i); return(-1); } 陰影部分中的關系運算對象是結構,系統無法確定是比較學號還是比較成績。我們可以進入函數體直接改造: if(pa[i].g==item.g) 不過這是權宜之計。結構各式各樣,數組的處理程序數不勝數,都一一改造嗎?這顯然不符合代碼的復用性要求。解決這個問題的方法是運算符重載。 運算符重載的思路是,首先把以內置類型為處理對象的運算符從觀念上看作函數,然后通過對該函數重載,擴大運算符的操作對象。這樣的函數稱為運算符函數,運算符函數名為operator @,@代表某一種運算符。運算符重載就是運算符函數重載。 以比較運算符“==”為例,首先把該運算符從觀念上看作一個函數: int operator==(int,int); 于是兩個整數的比較運算表達式 x==y 被看作運算符函數的調用 Operator== (x,y 然后反過來,重載運算符函數operator==: bool operator== (Student a,Student b)//重載運算符函數的定義 { return(a.g==b.g); //比較成績 } 重載之后,運算符“==”的處理對象就增加了結構Student。具體的執行過程是,編譯器如果發現內部無法解釋的運算符處理,就會去尋找重載的運算符函數,找到之后,調用這個函數。例如,函數Find中的表達式 pa[i]==item 被編譯器替換成 operator==(pa[i],item) 運算符重載是函數的一種調用形式。對用戶自定義類型重載的運算符運算,可以等價地表示為運算符函數的調用,但是內部基本類型的運算符運算是內定的,不能實際的替換成運算符函數的調用形式,例如,不能把表達式5==6替換為operator==(5,6)。 3.2引用 運算符重載函數的參量不能全部是語言內置基本類型,至少要有一個是用戶定義類型,以免和內置基本類型的運算符沖突。舉例說明,如果我們想把雙浮點型擴展為求余運算%的對象,那么下面的運算符重載是不行的: double operator%(double a,double b)//非法!參量缺少用戶類型 { return((long)a%(long)b); } 因為這樣的運算符函數與浮點型基本運算沖突,使編譯器失去了檢錯能力。 一個可行的方法是,首先創建一個用戶結構類型表示雙浮點型: struct DOUBLE//創建一個用戶結構類型表示雙浮點型 { double f; }; 然后運算符重載如下: double operator%(DOUBLE A,double b)//參量A是用戶類型 { return((long)A.f%(long)b); } 應用舉例: DOUBLE x={13.1}; double y=4.5; cout<<(x%y);//結果是1 可是,新的問題出現了。運算符函數是值調用,值調用的實質是參數復制,即實參復制給形參,而運算符函數的參數主要是結構,結構可以很大,參數復制既占空間,又費時間,加之,運算符的使用頻率高,綜合起來考慮,為運算符函數的值調用而需要付出的時空代價是令人難以承受的。解決這個問題的方法自然想到地址調用,因為不論參數多大,其地址需要的單元只是2個字節或4個字節(因系統而定),效率有了保證。可是地址調用的參量都是指針,而指針是語言內置類型,在上一節最后我們已經指出,運算符函數的參量至少要有一個是用戶類型,因此下面的運算符重載是非法的。 bool operator== (const Student*a,const Student*b)//非法! { return(a->g==b->g);//比較成績 } 我們可以做如下改進,使某一個參量不是內置類型: bool operator== (Student a,const Student*b) { return(a.g==b->g);//比較成績 } 于是有: if(pa[i]==item)// if(operator==(pa[i],iem)) return(i); 但是,表達式“pa[i]==item”把運算符的簡潔形式“pa[i]==item”破壞了,而且第1個參量仍然是值傳遞。 運算符重載給我們提出了一個難題:運算符函數既要具備地址調用的效率,又要保留值調用的簡潔自然的形式。解決這個難題的方法就是引用型。引用的聲明格式為: 類型標識符 引用=被引用的變量; 舉例說明: int x=5; int y=x;//定義一個引用,引用必須初始化 稱y是x的引用,或x是被y引用的變量。 引用的實質是指針。在內部,引用是指針,而且它必須初始化,取得被引用變量的地址,初始化值不能改變。語句int y=x在內部相當于int* y=x。 在外部,對用戶來說,聲明之后的引用名稱不再表示指針,而是表示指針指向的變量,相當于前面有一個隱藏的運算符“*”。例如: y=6;//內部相當于*y=6; 因此,人們從形式上把引用y看作是被引用變量x的別名或同義詞,也就是說y就是x。如圖1所示。 在內部,引用相當于const常量指針。在外部,引用與const常量指針不同,對它本身既不能取址也不能取值,因為它是被引用的變量的別名,例如,y表示的是x的地址,而不是y指針的地址;y的值是x的值,而不是y指針的值即x的地址。 可以用下面一個簡單方法來驗證“引用的實質是指針”。我們知道,一個函數的自動局部變量地址不能是函數返回值,因為函數調用之后,其自動局部變量的生命周期結束,空間被撤消,返回它的地址是沒有意義的。例如: int* Func2(void) { int x=5; return(x);//int*temp=x;錯誤!不能返回自動局部變量地址 } 編譯器錯誤提示為:returning address of local variable or temporary(返回值是一個局部變量或臨時變量的地址)。當我們返回一個自變量的引用時,編譯器的錯誤提示是相同的: int Func2(void) { int x=5; return(x); //int temp=x; 錯誤!不能返回自動局部變量地址 } 把運算符函數的參量設為引用型,問題就得到了解決: bool operator== (const Student a,const Student b) { return(a.g==b.g); //比較成績 } typedef Student Type; int Find(const Type *pa,int n,Type item)//查找。 { for(int i=0;i if(pa[i]==item)//if(operator==(pa[i], item)) return(i); return(-1); } 引用型參量a和b的實質是指針,相當于const常量指針,而運算符函數operator==是“只讀”的,它的指針參量應該是指向const常量的指針,所以a和b的實質是指向const常量的const常量指針,而它們的名稱是const常量型引用。 3.3基本類型運算符中的引用 地址是處理和數據之間的“媒介”、“中介”,它是程序語言的要素,一開始就包含在機器指令這個程序語言的細胞中,例如,機器指令的操作數一般是數據的地址。進入到C語言,地址發展為指針,它就應該包含在基本類型的運算符表達式中。以下面的賦值表達式為例: (x=y)=z 執行過程是,y的值給x,z的值給x,結果是x和z的值相等。 從概念上用復合運算符函數表示為: operator=(operator=(x,y),z) 這不僅要求運算符函數operator=的第1個參量是引用,而且返回值也是第1個參量的引用。為了理解,我們以用戶定義的結構Student為例,重載賦值運算符: Student operator= (Student a,const Student b) { a.ID=b.ID; a.g=b.g; return(a);//Student _temp=a; } 由此說明,引用是指針發展的一種較高級的形式。運算符重載是引用產生的必要性,而基本數據類型運算符包含著它產生的可能性。 有人可能要問,在基本類型的賦值表達式中,操作數可以是字面值常量,例如: (x=3)=4 那么既然形參是引用,而且引用的實質是指針,那么實參就必須傳址,可是字面值常量3和4是不能尋址的。問題是能夠這樣解決的:如果實參是字面值常量,系統就開辟一個臨時的const常量型空間來存儲實參,然后將const常量型空間的地址傳遞為形參[1]。 4通用算法與指針 C++標準模板庫STL的主要組件是容器類、通用算法和迭代器。容器類和通用算法在更高級上分別代表著存儲和處理,迭代器是它們的中介,迭代器是指針的更高級形式,是一種smart pointers。 “STL的中心思想在于:將數據容器(containers)和算法(algorithms)分開,彼此對立設計,最后再以一帖膠著劑將它們撮合在一起。容器和算法的泛型化,從技術角度來看并不困難,C++的class templates和function templates可分別達成目標。但是如何設計出兩者之間的良好膠著劑,才是大難題”。[2] 有關具體內容將在后期引入C++后進一步討論。 5小結 存儲和處理是程序設計的基本矛盾,處理的不斷復雜,推動了這個矛盾的不斷發展,進而也推動了程序語言的不斷發展。地址、指針、指向const常量的指針、引用和迭代器是處理和存儲的“媒介”在程序語言發展中的一系列進化。 參考文獻 [1] 王立柱.C/C++與數據結構(第3版上)[M]. 北京: 清華大學出版社,2008. 215. [2] 侯捷.STL源碼剖析[M]. 武昌: 華中科技大學出版社, 2002. 79.