田維蓮 羊巍 六盤水師范學(xué)院
一、引言
C語言程序設(shè)計教學(xué)中,指針的教學(xué)是重難點;此時,學(xué)生已經(jīng)學(xué)了函數(shù)、數(shù)組及結(jié)構(gòu)體等內(nèi)容,而在指針與函數(shù)、指針與結(jié)構(gòu)體及文件的教學(xué)中,都需要學(xué)生對指針有著深刻的理解與掌握,同時,在其后續(xù)課程數(shù)據(jù)結(jié)構(gòu)中,對指針有很高要求,學(xué)生如果不掌握指針并靈活運用,很多算法的實現(xiàn)無從談起。另一方面,指針的教學(xué)比較難,指針涉及機器硬件,不好抽象,教學(xué)中,常常要圖示大量的內(nèi)存圖,學(xué)生往往會陷入內(nèi)存細(xì)節(jié),而忽視程序的邏輯與指針概念本身的清晰理解;筆者在多年的教學(xué)實踐中,認(rèn)為應(yīng)該對指針的教學(xué)幾個要點作一些梳理,通過示例強調(diào)在教學(xué)中幾個比較關(guān)鍵的知識點,同時,引入幾個新概念或新的理解方式,幫助學(xué)生理解、掌握運用指針。
二、指針的教學(xué)要點
(一)指針變量

上述程序片段中,聲明了4個變量,在教學(xué)中應(yīng)該明確有關(guān)的3個問題:
1.變量與對象的關(guān)聯(lián)方式
變量i保存對象2,而類型為int *的變量p保存值為i的內(nèi)存地址的對象&i,我們可能更關(guān)心的是變量p與對象2的關(guān)聯(lián),應(yīng)該明確,變量i與對象2是直接關(guān)聯(lián),變量p與對象2是間接關(guān)聯(lián),變量p通過保存變量i的地址而與對象2關(guān)聯(lián),可以通過間接尋址運算符*取得變量i的值(即對象2),但此時應(yīng)注意,語句int * p = &i 中的符號“*”不是間接尋址運算符,其作用是編譯時,告訴編譯器,變量p是一個類型為int *的指針(引用)變量,變量p可以和類型為int的變量通過賦值的形式發(fā)生關(guān)聯(lián),即p = &i。
2.C語言中量(常量與變量)的分類
可以明確,C語言中,從上述變量與對象的關(guān)聯(lián)方式來說,變量分為兩類,一類為值類型變量,其保存的是非地址值的值對象(如變量i),另一類為引用(指針)類型變量(如變量p),其保存的值是內(nèi)存地址,但要注意,*p是變量i 的別名(也就是說,在變量i的作用域范圍內(nèi),凡是出現(xiàn)變量i的地方,均可用*p替換。),*p是左值,通過*p可以對變量i 的值進行讀寫,對*p進行賦值操作,等同于對變量i進行賦值操作。
對于C語言中的值常量而言,也可以分為兩類,一類是地址值,我們稱其為引用(指針)類型,另一類常量稱為值類型。
3.指針變量的使用
C語言中,要使用指針變量,要有一個前提,指針變量要么和一個變量發(fā)生關(guān)聯(lián)(如語句:int *p = &i;使得指針變量p和變量i發(fā)生了關(guān)聯(lián)),要么指針變量有一個非空的合法地址值(如語句:int*q = (int *)malloc(sizeof(int) * 2;使得變量q得到了一個類型為int *的,由函數(shù)malloc分配的連續(xù)內(nèi)存地址的首地址值)。
指針變量t被初始化為NULL,稱t為空指針,t的值NULL(宏NULL在stdio.h和stdlib.h等頭文件中均有定義)是一個區(qū)別于任何有效指針值的特殊值,這表明t“不指向任何地方”;若指針變量t沒有初始化,則這個指針變量就成懸空指針,這表明t是一個“不知道指向哪里”的指針,若是通過空指針或懸空指針訪問內(nèi)存,程序要么崩潰,要么出現(xiàn)難以預(yù)料的結(jié)果,為此,應(yīng)該強調(diào),一個良好的編程習(xí)慣是,將暫時不用的指針變量在聲明時立即初始化為NULL,這樣可以清出程序中的懸空指針,同時在使用指針變量應(yīng)檢測其值是否為空,如上述的代碼片段中,對指針變量q的檢測(if(q))。
(二)指針與數(shù)組

上述代碼片段中,意在展示通對指針處理數(shù)組,學(xué)生習(xí)慣使用的arr[i],實際上,最后arr[i]被編譯成為*(arr+i),也就是說arr[i]等價于*(arr+i),同理*(p+i)等價于p[i];同時,在教學(xué)中,應(yīng)該強調(diào),p、*p以及*arr是左值,而arr不是,數(shù)組名arr是一個類型為int *const的指針常量,其值為數(shù)組arr中首元素(arr[0])的地址值,另一個問題是,arr[5]非法,而&arr[5]卻合法,雖然,arr[5]不存在,但是,對arr[5]取地址是可以的,實際上,&arr[5]等價于&*(arr + 5),而&*(arr + 5)就是(arr + 5),(arr + 5)是一個類型為int *的地址值。
(三)指針、函數(shù)及結(jié)構(gòu)體

上述示例代碼片段中,函數(shù)total_score的作用:將結(jié)構(gòu)體c的成員scores中前size-1項數(shù)組元素進行加總并返回。將類型為class*的指針作為函數(shù)的參數(shù)是一種高效的作法,這樣作避免了實參向形參傳值時,數(shù)組scores被復(fù)制,學(xué)生可能會疑問:設(shè)計一個函數(shù)原型為int total_score(class cl)的函數(shù)來做同樣的事也可以高效完成,因為,形參cl的成員scores是一個數(shù)組名,當(dāng)調(diào)用發(fā)生時,復(fù)制給形參cl的成員scores的值不就是個地址嗎,實參的數(shù)組沒有被復(fù)制,實參和形參共享同一個數(shù)組?應(yīng)該強調(diào)的是,在C語言中,對結(jié)構(gòu)體進行復(fù)制時,結(jié)構(gòu)體中的數(shù)組成員也被復(fù)制,而非兩個結(jié)構(gòu)體共享同一個數(shù)組,因此,將類型為class結(jié)構(gòu)體作為函數(shù)的參數(shù)的做法低效。
函數(shù)no_pass將結(jié)構(gòu)體c數(shù)組成員中,值小于60的數(shù)組元素下標(biāo)標(biāo)記于另一個結(jié)構(gòu)體nopass中,并返回nopass。此處,學(xué)生可能會有疑問:應(yīng)該返回一個指向nopass的指針,而非結(jié)構(gòu)體nopass,這樣可以避免調(diào)用no_pass函數(shù),取返回值時,未發(fā)生的結(jié)構(gòu)體數(shù)組成員被復(fù)制,因而高效。在這個地方,可以強調(diào),C語言中,返回一個指向局部自動變量的指針是致命的,一旦no_pass函數(shù)執(zhí)行完畢,其所占據(jù)的內(nèi)存空間隨即被回收,結(jié)構(gòu)體nopass不存在了,指向nopass的指針變成了指向一個不存在的對象的指針,在no_pass函數(shù)外部使用該指針,程序可能會崩潰或出現(xiàn)無法預(yù)知結(jié)果。因此,no_pass函數(shù)的處理方式是正確而又明智的,這種處理方式避免了“返回一個指向局部自動變量的指針”的錯誤。
(四)指針與字符串

上述的示例代碼中,學(xué)生可能會有疑問:程序為什么報錯?筆者認(rèn)為,要從幾個方面說起,一方面,”Hello”到底是什么?如果回答,”Hello”是字符串。那么這個回答是含混不清的。首先,”Hello”是字符串字面量(一些教材又稱為字符串常量,并解譯:常量就是不能被改變的量,于是修改字符串常量程序會報錯,這樣的解譯實際上沒有把問題說清楚。),字符串字面量就是字符串的在源程序中的字面表達;其次,”Hello”被保存于一片連續(xù)的內(nèi)存空間中,字符’o’的后面被追加代表字符串結(jié)束的字符’