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