周立功:開發人員該如何跨越自然語言和編程語言之間的鴻溝?
經周立功教授授權,特對本書內容進行連載。第一章為程序設計基礎,本文為1.1.3節語言的鴻溝。
周立功
開發人員對問題域的認識是一種思維活動,而人類的任何思維活動都是藉助於他們熟悉的某種自然語言進行的。而軟體系統的最終實現必須用一種計算機能夠閱讀和理解的語言描述系統,這種語言就是編程語言。
人們所習慣使用的自然語言和計算機能夠理解及執行的編程語言之間存在很大的差距,這種差距被稱為「語言的鴻溝」,實際上也是認識和描述之間的鴻溝。也就意味著,一方面人們藉助自然語言對問題域所產生的認識遠遠不能被機器理解和執行,另一方面機器能夠理解的編程語言又很不符合人們的思維方式。因此開發人員需要跨越兩種語言之間的這條鴻溝,即從思維語言過度到描述語言。
之所以學習和開發的成本很高,主要是理解困難所造成的,所以必須建立一種用於溝通的通用語言,因為構建通用語言的過程就是自我思維訓練和建立邏輯推理的過程。
1. 變數和指針
2016年初,幾個要好的朋友要我幫忙教他們的孩子編程,四個已經畢業的學生,經過測試考試成績為0。首先從變數的三要素開始學起,大家一起建立一套通用語言。比如:
int iNum = 0x64;
最初的辭彙有變數的類型int、變數名iNum和變數的值0x64,接著編寫第一個只有幾條語句的簡單程序,詳見程序清單 1.1。
程序清單 1.1輸出變數的地址和變數的值
1 #include
2 int main(int argc, char *argv[])
3 {
4 intiNum = 0x64;
5
6 printf("%x,%x
", &iNum, iNum);
7 return0;
8 }
通過運行結果可以清楚地看到,0x64存儲在0x22FF74內存單元中。雖然使用&運算符可以獲取變數iNum在內存中的地址,但&iNum是一個孤立的概念。
當將地址形象化地稱為「指針」時,即可通過該指針找到以它為地址的內存單元。於是辭彙表中又多了一個新的成員——指針,同時還多了一條新的語句——&iNum是指向int變數iNum的指針,該語句標識了「指針與變數」之間的關聯關係。理解指針的最好方法之一是繪製圖表,變數與指針的關係詳見圖 1.1。
雖然這幾位學生的基礎很差,但畢竟在校還是認真聽了課的。因此只要稍微提醒一下,依然還能記得一些概念。既然通過&iNum就能找到變數iNum的值0x64,那麼如何存放&iNum呢?定義一個存放指針&iNum的(指針)變數。比如:
int iNum = 0x64;
int *ptr = &iNum;
現在辭彙表中有多了一個新的成員——指針變數,即ptr是指向int的指針(變數),「int *」類型名是指向int的指針類型,詳見圖1.2。雖然有時也將指針變數泛化為指針,但要根據當前所處的環境而定。
此時,大家不約而同地說出了——&ptr是指向指針變數ptr的指針,那麼誰指向&ptr呢?指針的指針,或雙重指針,於是辭彙表中又多了一條語句。如果有以下定義:
int iNum = 0x64;
int *ptr = &iNum;
如果在「int *ptr」定義前添加typedef:
typedef int *ptr;
此時,ptr等同於int *。為了便於理解,將類型名ptr替換為PTR_INT。比如:
typedef int *PTR_INT;
有了PTR_INT類型,即可構造指向PTR_INT類型的指針變數pPtr。即:
PTR_INT*pPtr = &ptr;
其中, pPtr是指向PTR_INT *的指針變數,PTR_INT的類型為int *,pPtr是指向int **的指針變數,那麼pPtr就成了保存int型ptr地址的雙重指針。其定義方式簡寫如下:
int **pPtr= &ptr;
以下關係恆成立:
*pPtr ==*(&ptr) == ptr
**pPtr== *ptr == *(&iNum) == iNum
當你讀到這裡時,我相信你對指針已經沒有那麼恐懼感了。
2. 數組
如果有以下聲明:
int a[2];
你是否想過在聲明「int a[2];」時,a、&a[0]和&a是什麼類型?大多數人可能認為,a不是指向該數組首元素的指針嗎?可以說「是」,也可以說「不是」。為何一個看起來似乎很簡單的問題,卻會讓人深深地感到迷茫呢?
由於數組類型屬於構造類型,因此通過邏輯推理可以搞清楚其中的來龍去脈,基於此,我們需要以全新理念看待數組,從變數的類型、變數的地址和變數的值這三個不同的視角出發進行分析。
從概念層次上來看,數組名是概念也是符號,由於其在聲明時、在表達式中、作為&和sizeof的操作數時具有一定的差異,因此需要區別對待。
按照變數的聲明規則:a是由2個int值組成的數組,取出標識符a,剩下的int [2]就是a的類型。這是在開發編譯器時約定的聲明規則,不需要特別做出解釋。通常將int [2]解讀為由2個int值組成的數組類型,簡稱數組類型。而大多數C教材幾乎都不提及,從而為構造二維數組帶來了很大的障礙。事實上,C語言中並不存在二維數組,且C語法也不支持二維數組,僅支持由一維數組構造的數組的數組。
從規約層次上來看,除了在聲明中或數組名當作sizeof或&的操作數之外,表達式中的數組(變數)名a被解釋為指向該數組首元素a[0]的指針,因而可將這個原則標識為「a==&a[0]」或等價於「*a==*(&a[0])==a[0]」。當a[0]作為&的操作數時,&a[0]是指向a[0]的指針,a[0]的類型為int,&a[0]的類型為int *const,即指向常量的指針,簡稱常量指針,其指向的值不可修改。當a作為&的操作數時,則&a是指向a的指針。
使用指針運算規則,當將一個整型變數i和一個數組名相加時,將得到指向第i個元素的指針,即「a+i==&a[i]」或「*(a+i)==*(&a[i])==a[i]」,因為a在編譯期和運行時的語義完全不同。
由於a的類型為int [2],因此&a是指向「int [2]數組類型」變數a的指針,簡稱數組指針。其類型為int (*)[2],即指向int [2]的指針類型。為何要用「()」將「*」括起來?如果不用括弧將星號括起來,那麼「int (*)[2]」就變成了「int*[2]」,而int *[2]類型名為指向int的指針的數組(元素個數2)類型,這是設計編譯器時約定的語法規則。
問題又來了,C語言的發明者K&R編寫的C教材是這樣解釋「(*)」的:因為「*」是前置運算符,它的優先順序低於「()」。為了讓連接正確地運行,有必要加上括弧。其實這樣解讀未免有些牽強附會了,因為聲明中的「*、()、[]」都不是運算符,運算符的優先順序在語法規則中是在表達式中定義的。回歸本質「int *[2]」中的「*」不加括弧,那就是指針類型數組。如果事先不是這樣約定,則無法開發編譯器。
從實現層次上來看,最重要的不是就事論事地實現,而是要從特殊到一般地尋找問題的通解——泛化。比如,在一個數組中尋找一個特定的值,常見的函數原型如下:
int*findValue(int *arrayHead, size_t arraySize, int value);
顯然,findValue()函數不僅暴露了數組的實現細節,比如,arraySize,而且太過於依賴特定的數據結構。那麼,如何設計一個演算法,使它適用於大多數數據結構呢?或如何在即將處理的未知的數據結構上,正確地實現所有操作呢?實際上,一個序列有開始和結尾,即可使用++得到下一個元素,也可以使用*得到當前元素的值。
為了讓findValue()函數適用於所有類型的數據結構,其操作應該更抽象化,讓findValue()函數接受兩個指針作為參數,表示一個操作範圍,詳見程序清單1.2。
程序清單 1.2 findValue()查找函數
1 int *findValue(int *begin, int *end, int value)
2 {
3 while(begin != end && *begin != value)
4 ++begin;
5 return begin;
6 }
顯而易見,這樣的循環結構重複做某件事就是一種迭代操作,在每次迭代操作中,對迭代器begin的修改等價於修改循環控制標誌或計數器。當將begin的作用抽象化和通用化後,begin和end就成為了迭代器。其基本思想是迭代器變數存儲了數組的某個元素的位置,因此能夠遍歷該位置的元素。通過迭代器提供的方法,可以持續遍曆數組的下一個元素。
++將迭代器移動到下一個數據項,其意圖是利用遞增操作完成賦值。*檢索迭代器的當前元素,其意圖是利用遞增操作檢索元素。為了檢測輸入迭代器元素的結尾,通常將迭代器與另一個恰好位於輸入區間末尾之後的已知迭代器end進行比較,測試迭代器是否相等。
這個函數在「前閉後開」範圍[begin, end)內(不含end,end指向array最後元素的下一個位置)查找value,並返回一個指針,指向它所找到的第一個符合條件的元素,如果沒有找到就返回end。這裡之所以用「!=」,而不是用「
由此可見,findValue()函數中並無任何操作是針對特定的整數array的,即只要將操作對象的類型加以抽象化,且將操作對象的表示法和範圍目標的移動行為抽象化,則整個演算法就可以工作在同一個抽象層面上了。通常將整個演算法的過程稱為演算法的泛型化。泛化的目的旨在使用同一個findValue()函數處理各種數據結構,通過抽象創建可重用代碼。
3. 二維數組
假設有以下聲明:
int data0[2] = ;
int data1[2] = ;
int data2[2] = ;
當去掉聲明中的數組名時,則data0、data1和data2類型都是「int [2]」。實際上,數組本身也是一種數據類型,當數組的元素為一維數組時,則該數組為數組的數組,因此可以通過「int [2]」數組類型構造數組的數組。即:
typedef int data0[2];
顯然,只要將data0改為T,則T就成為了int [2]一樣的數組類型,即可用T再定義一個一維數組:
typedef int T[2];
T data[3];
其等價於:
int data[3][2];
由於T的類型為int [2],即data[0]、data[1]和data[2]的類型均為int [2],佔用一個int大小。而表達式中的data可以被解釋為指針,其類型為int (*)[2],佔用2個int大小其中。顯然,data是由data[0]、data[1]和data[2]這3個元素組成的一維數組,而data[0]、data[1]和data[2]本身又是一個由2個int值組成的一維數組。因此可以用下標區分data[0]、data[1]和data[2]一維數組,其分別對應於data[0][0]和data[0][1]、data[1][0]和data[1][1]、data[2][0]和data[2][1]。
由於表達式中的數組名data可以被解釋為指針,即data是指向data[0]的指針,因此data的值和&data[0]的值相等。則以下關係恆成立:
data == &data[0]
*data ==*(&data[0]) == data[0]
由於data[0]本身是一個由2個int值組成的數組,即表達式中的data[0]是指向data[0][0]的指針,因此data[0]的值和它的首元素的地址&data[0][0]的值相等。
則以下關係恆成立:
data[0] ==&data[0][0]
*(data[0]) ==*(&data[0][0]) == data[0][0]
*data == &data[0][0]
顯而易見,data是指針的指針,必須解引用兩次才能獲得原值,則以下關係恆成立:
data == &data[0]== &(&data[0][0])
**data == data[0][0]
雖然data[0]指向的對象佔用一個int大小,而data指向的對象佔用2個int大小,但&data[0]和&data[0][0]都開始於同一個地址,因此data的值和data[0]的值相等。則以下關係恆成立:
data == data[0] == &data[0]== &data[0][0]
當將數組的數組作為函數參數時,數組名同樣視為地址,因此相應的形參如同一維數組一樣也是一個指針,比較困難的是如何正確地聲明一個指針變數pData指向一個數組的數組data? 如果將pData聲明為指向int類型是不夠的,因為指向int類型的指針變數只能與data[0]的類型匹配。假設有以下代碼:
int data[3][2] = {, , };
int total = sum(data, 3);
那麼sum()函數的原型是什麼?
由於表達式中的數組名data可以被解釋為指針,即data的類型為指向int[2]的指針類型int (*)[2],因此必須將pData聲明為與之匹配的類型,data才能作為實參傳遞給sum()。其函數原型如下:
int sum(int (*pDdata)[2],int size);
由於data[0][0]是一個int值,因此&data[0][0]的類型為int *const。即可用以下方式指向data的第1個元素,增加指針的值使它指向下一個元素。即:
int *ptr = &data[0][0];
int *ptr = data[0];
如果將某人一年之中的工作時間,使用下面這個「數組的數組」表示:
int working_time[12][31];
在這裡,如果開發一個根據一個月的工作時間計算工資的函數,可以象下面這樣將某月的工作時間傳遞給這個函數:
calc_salary(working_time[month]);
其相應的函數原型如下:
int calc_salary(int*working_time);
4.指針數組與函數指針
雖然數組和指針數組存儲的都是數據,但數組存儲的是相同類型的字元或數字,而指針
數組存儲的是相同類型的指針。比如:
int data0, data1, data2;
int *ptr[3] = {&data0, &data1,&data2};
其中,ptr是指向由int *[3]類型的指針組成的數組,ptr[0]指向&data0,ptr[1]指向&data1,ptr[2]指向&data2。因此只要初始化一個指針數組變數存儲各個字元串的首字元的地址,即可引用多個字元串。比如:
char * keyWord[5] = {"eagle","cat", "and", "dog", "ball"};
其中,keyWord[0]的類型是char*,&keyWord[0]的類型是char**。雖然這些字元串看起來好像存儲在keyWord指針數組變數中,但指針數組變數中實際上只存儲了指針,每一個指針都指向其對應字元串的第一個字元。
函數指針數組也是指針數組,該數組的每個元素是一個函數的地址。如果有以下聲明:
typedef int (*PF)(int, int);
其中,PF是一個指向返回值為int的函數的指針類型,該函數有兩個int類型參數。假設需要聲明一個包含4個元素的數組變數oper_func,用於存儲4個函數的地址。即可使用PF定義一個存儲函數指針的數組:
PF oper_func[4];
其中,oper_func為指向函數的指針的數組,上述聲明與以下聲明:
int (*oper_func[4])(int, int);
雖然形式不一樣,但其意義完全相同。
顯然,也可以使用PF定義相應的函數指針變數。比如:
PF pf1, pf2;
它的另一種聲明形式如下:
int (*pf1)(int, int);
int (*pf2)(int, int);
在指針函數中,還有一類這樣的函數,其返回值為指向函數的指針。比如:
PF ff(int);
它的另一種聲明形式如下:
int (* ff(int))(int, int); //ff指針指向的是一個函數
顯然,有了基本數據類型和變數,即可構造各種類型的數據結構。
5.C的局限性
(1)符號重載
符號重載是一種強大的機制,它允許修改一個操作符的含義,當你看到加號時,有時它代表加法運算,有時它代表將字元串連接起來其它的含義,因此其具體的語義與使用場合有很大的關係。比如:
Matrix a, b, c;
c= a + b;
這裡的加號表示矩陣加法,而不是整數或浮點數的加法。
(2)模板
我們經常會遇到類似這樣的問題,比如,比較兩個整數的大小,其函數原型如下:
intmax(int a, int b);
如果還要比較兩個double雙精度數中較大的一個,還得再寫一個新的函數。比如:
intmax(double a, double b);
如果又需要比較兩個字元串中較大的一個呢?則需要編寫第三個函數。也許,我們會想到使用typedef。比如:
1 typedef intitem;
2 item max(item a, item b)
3 {
4 if(a > b)
5 return a;
6 else
7 return b;
8 }
如果需要使用多個不同版本的max函數,則使用typedef無法滿足要求,因為程序只能為item定義一種數據類型。
解決以上問題的一種更靈活的機制是使用模板函數,在函數實現的任何地方,item都不會受到受限於任何特定的類型。當使用模板函數時,編譯器在檢查參數的類型時,自動確定item的數據類型。比如:
1 template ;
2 item max(Item a, Item b)
3 {
4 if(a > b)
5 return a;
6 else
7 return b;
8 }
表達式template稱為模板前綴,Item稱為模板參數。模板前綴總是在模板函數定義的前面,它提醒編譯器:下面的定義將使用名為Item的未確定的數據類型。事實上,模板前綴表明——Item是一種將來要填充的數據類型,現在不用擔心它,只在函數內部使用。
由此可見,C語言既有它的優勢,但也有它的不足之處,而使用C++具有更大的威力。事實上,嵌入式系統軟體的開發不僅可以使用C語言,同樣可以使用C++。
小編註:好消息是《程序設計與數據結構》紙質版已正式開售,可在周立功淘寶官方店購買:https://s.click.taobao.com/wEWbTkw
同時小編獲得周立功教授授權將會在本公眾號連載。
同時歡迎大家留言談談你對軟體工程的見解,精彩論點,小編會自掏腰包送周立功教授的《程序設計與數據結構》紙質版一本哦。
關注電子工程師時間公眾號,獲取有料的乾貨
TAG:電子工程師時間 |
※語言的奧秘:語言是動物跟人類之間無法逾越的鴻溝
※喝酒五語言:豪言壯語,花言巧語,胡言亂語、不言不語,自言自語
※當代漢語詩歌的語言進程
※諸玄識再爆猛料:漢語是伊甸園神性語言,歐洲語言是被上帝變亂的語言
※英語,法語,德語,漢語,這些語言竟然都有個共同的祖先?
※改變你的世界,從語言開始!
※語言的魔方:語言塑造文化
※改變你的世界,從語言開始
※大自然的語言,時間和夢
※周麗艷:語言與意義的悖論——《詞語的孩子》中的語言主題
※情感中語言的魔力,為什麼要用肯定的語言彼此讚賞?
※親,發音、語言和言語你真的分得清嗎
※中國古代各民族之間語言不通,是如何交流的呢?
※國產編程語言《易語言》是怎麼一步一步變遊戲外掛「代言人」的?
※韓國、越南以前說漢語,它們如何在幾十年內改變文字和語言的?
※【裴恩說】語言無力時,讓行動發聲!
※漢字、漢語,作為我們傳統的文化,為什麼會在他國語言中出現?
※裁判和球員用什麼語言溝通?世界盃謎題已被解開
※論廣告語言的修辭之美!
※關於語言發育障礙和語言康復,家長的閱讀指南