因為聲明、定義、賦值和初始化都和 編譯原理 有關,所以答案就在 編譯器的輸出 上。
一、未聲明
1.c:
int
main
()
{
a
=
1
;
}
1.c:2:5: error: use of undeclared identifier 'a'
a = 1;
^
1 error generated.
請逐字閱讀編譯器的輸出。說兩遍,請逐字閱讀這個輸出。
註意這裏有個詞語叫 undeclared
- use of: 使用。誰使用?你,ahh,【你在使用一個】變量。
- undeclared:declare 是【對外宣告】,undeclared形容詞 --【沒有對外宣告過的】,文縐縐的說法,叫【未聲明】
- identifier: identify是【身份證】,也叫ID。*ier是什麽人,identifier就是【有身份的人】,文縐縐的說法,叫【識別元】
合起來是,【你在使用一個】【沒有對外宣告過的】【身份證】'a'。
這句話是說,a 識別元沒有聲明,不知道它是個什麽東西。
這就叫未聲明 undeclared
二、未定義
1.c:
extern
int
a
;
int
main
()
{
a
=
1
;
}
/tmp/ccxhuV7j.o: In function `main':
1.c:(.text+0x6): undefined reference to `a'
collect2: error: ld returned 1 exit status
請逐字閱讀編譯器的輸出。註意這裏有個詞語叫 undefined
- undefined: 【沒有定義過的】意思
- reference:【介紹信】的意思,這裏有個文縐縐的說法,叫【參照】
- to 'a': 對於 a
合起來是,對於 a 的【介紹信】,是【沒有定義過的】
這句話是說,a 是個名稱,它參照的記憶體實體是不存在的。a 為什麽一定要參照一個記憶體實體?哈哈,你一下子就摸到的電腦語言的靈魂,為了行文的需要,這個疑問不能在這裏展開,因為答案太長了,本文倒數的4節是這個問題的答案。
這就叫未定義 undefined
三、不能賦值
1.c:
int
main
()
{
main
=
123
;
}
1.c:2:8: error: non-object type 'int ()' is not assignable
main = 123;
~~~~ ^
1 error generated.
請逐字閱讀編譯器的輸出。註意這裏有個詞語叫 assignable
這句話是說
合起來就是,非物件類別的函數類別不可以被賦值
main 是返回值為int的函數類別,它為什麽不能被賦值呢?
要從物件說起,物件是一塊可以操作的記憶體塊。
言外之意,記憶體中還存在不能被操作的記憶體?哈哈,你的認知決定了你的高度,是的,記憶體有
- 向量區 禁止入內
- 文本區 禁止亂塗亂畫
- 數據區 自己的可以隨便玩,不是自己的禁止拍照
- IO區 只開放給專家學者
物件!一個多麽熟悉多麽豐富的詞語。。。
本例中,main 是一塊文本區的記憶體,不是可操作的記憶體,所以不能被賦值。
這就叫不能賦值 not assignable
四、不能初始化
1.c:
int
a
=
"foo"
;
int
main
()
{}
1.c:1:9: error: initializer element is not computable at load time
int a = "foo";
^
請逐字閱讀編譯器的輸出。註意這裏有個詞語叫 initializer
element:是常量"foo",是個字串地址
is not computable:不是算數
at load time:在程式執行的時候
合起來就是,字串地址在執行時不能被計算。額,編譯器,你管的可真多。
a 這個位置是 int ,字串就是一個地址,也是 int 。因此,從原理上來說上面程式沒有問題。事實上,在老 c 語言中,上述程式正常。
但是,後來語法變了。為什麽?為了更加規範和安全,這種行為被禁止了。編譯器給出的理由是,初始化的元素在執行時是算不了的。其實,這是一個善意的謊言,指標當然可以計算。但是為了規範有人阻止了你,阻止你的人正是 初始化。初始化是編譯器這個大程式的一個子程式。
程式分編譯時,執行時。目前看來,編譯時越來越龐大,越來越聰明。各種思想方法論被發明出來,典型如c++。
事實上,編譯器初始化的一些小聰明展現了他的可憐父母心,巴不得把所有後事都料理完,臟活累活全都不讓孩子幹。
比如下面的程式碼
1.c:
char
*
s
=
"bar"
;
int
days
[]
=
{
31
,
28
,
31
,
30
,
31
,
30
,
31
,
31
,
30
,
31
,
30
,
31
};
long
hour
=
60
*
60
*
1000
;
int
main
()
{}
編譯器為"bar"分配字串儲存,把days變成陣列,hour算成600000,而不是在執行時再算。
這就是初始化器 initializer,一個編譯邏輯
五、九年前的一個心結
不知道三年後,這個答案你是否還看。
但是,起碼,這個答案了結了我九年前的一個心結。
在2009年左右,我愛去 chinaunix (當時叫CU)上閑逛,這個論壇以脾氣大,炮火猛著稱。
有次我調侃了某位仁兄,沒想到他立馬查閱了我以往所有的提問和回答,抓到了我一個小辮子。
這個小辮子是,在某個提問中,我提到對初始化的機制不太清楚。
他當即罵我,連初始化都不懂,還在這裏討論個XX,裝什麽裝,滾!!!
一時間,我百感交集,
一方面,我被他迅雷般的回復、雷霆般的怒火驚呆,這是一次絕妙的反擊,他用我最近在另一篇貼文中的提問來充當證據。
另一方面,我為自己不知道初始化的底層細節而感到臉紅和委屈。
初始化到底是怎麽回事,為什麽我一直不能調查清楚呢。
這個問題就像哥德巴哈猜想一樣,看著簡單,實際卻無法證明。
真是啞巴吃黃蓮,有苦說不出。我當時只是感覺這個問題很難,但是不敢肯定。
我一時間很受傷,慢慢遠離了 CU,也慢慢遠離了 C 語言,而我對這個問題依然不甚清楚。
很多年過去了,在一次不成功的專案中,無心插柳柳成蔭,明白了初始化的來龍去脈。
六、程式
在討論聲明、定義、賦值和初始化這些概念之前,必須要對程式的歷史做一些回顧。
目的是要建立一個信念,程式最好是這個樣子(就是現在我們看到這樣),或者幹脆說必須是這個樣子,要不然就沒法跟機器打交道。
在二戰期間雷達的使用,隨後電腦的發明,IBM大型電腦的研發,以及人類登月,「編程活動」在實踐中不斷的湧現出種種方法論,編程這門技術日趨成熟。
小型機出現了,它培養並湧現出一批黑客。
後來,桌面機的出現,更是讓這門技術開始流向民間,後面的故事大家都熟悉了,比爾蓋茨和喬布斯把握住了機會。
回到 1970 年代,即使在 C 語言被發明之前,人們已經意識到了很多寶貴的經驗、教條和技巧,C 語言和 UNIX 系統是站在這些巨人的肩膀上建立起來的。
這裏只列舉幾條和本話題相關的幾個「肩膀」:
1. File 檔
比如書籍,人類天生喜歡把資訊記錄在一個個檔中,這是個看似再自然不過的特征。
而為什麽是檔,而不是其他形式,比如巖畫、結繩記事,或者更猛的諸如爪子抓痕、尿液標記。
答案你懂的,因為檔資訊量大,傳播方便。結繩記事成本高,時間長就繩子不夠了。巖畫能被外借閱讀嗎,不能。所以,當 1956 年 IBM 發明硬碟後,就有人琢磨著把檔在硬碟中表示出來。
大約在 1961 年,方案終於出來了,這個方案叫「檔案系統」,每個檔有個名字,可以被看到,它會給你一種假象,仿佛在桌子上看到一個個的紙質檔。
你可以開啟它,修改它,關閉它。就像我們寫一封信一樣,先開啟信封,寫信,再把它裝回信封。
因為程式設計師選擇了用檔去編寫程式,並且一直沿用至今,所以檔對程式的影響可以說是方方面面。
2. Virtual Memory 虛擬記憶體
虛擬記憶體這項電腦的特征,很大程式上影響了程式(就是供機器執行的那個東西)的組織結構。
它極其難以理解,並且追蹤起來非常不便。但是為什麽非要采用它呢?
在1940、1950年代,人們編寫大型程式會被記憶體管理的問題困擾,比如記憶體覆蓋這種相互幹擾。
我相信,以當時的條件,偵錯起來簡直是手足無措。
最早提出虛擬記憶體的是 1956 年一位德國物理學家。
虛擬記憶體方便了多工、多道程式處理,但是這不是最重要的。
最重要的是,因為加強了系統對程式的控制,所以方便了人的編程活動。
它在系統和程式間架起一道硬件防火墻,使得程式崩潰不會殃及到系統。
虛擬記憶體如此重要,在高級程式語言發明之前它就成為了電腦重要的特征。因此,它影響了所有語言的設計。
3. Subroutine 子過程
1968 年有人提出goto有害論,隨後,結構化編程成為主流。
我們今天看到的函數是子過程的一種。
函數在進入子過程前有一個備份操作,結束子過程前有一個恢復操作,並且函數和棧有密切關系,它有一個呼叫鏈。
4. Pointer 指標
1964 年被發明。
在 pdp11 小型機中,有指標和二級指標的尋址模式,直接用指令實作的。
指標形如 (r),二級指標形如 *(r)
我覺得這種寫法比 *p, **p 要好點,不過挺遺憾,可能是不能表達三級指標,沒有被 C 采用
還有一種尋址是 (r)+ 和 -(r),因為這兩種操作用的頻率很高,所以沒有實作 +(r) 和 (r)-。
(r)+ 和 -(r) 可以望文生義,是個很好的設計。
如果 C 語言采用這個設計,就不會後來對 ++i,i++ 的詬病了吧。
5. Stack 棧
這裏說的棧並不是數據結構中前進後出那個棧,而是系統中為棧設計的一整套機制,包括兩個通用寄存器和一些指令。
棧具體是什麽時候被發明的我不清楚,也許是種學術的結晶吧。
6. text&data 程式和數據
程式應該邏輯和數據分離。事實上數據被進一步細分。
在 C 種程式中,這種劃分是
a) 正文區
函數的定義、組譯子過程、中斷和陷入向量
在unix系統中,正文區占內核 57.45%
而
函數的定義占正文段 91.49%
子過程占正文段 7.62%
中斷和陷入向量占正文段 0.86%
b) 初始化數據區
下列兩種情況數據會放入初始化數據區
c程式碼中明確初始化的全域變量
組譯程式碼中初始化的數據
在unix系統中,初始化數據區占內核 3.51%
c) bss數據區
c程式碼中的未初始化變量
組譯程式碼中的未初始化變量
c程式碼中的靜態局部變量
在unix系統中,bss數據區占內核 39.03%
bss 意思是 block started by symbol
這句話揭示了一定真相,編譯器只是把它們當作符號來看待,為其安排地址。
在程式檔中它們不必存在 - 實際是就是不存在,當然這是前輩故意這麽設計的。
當程式執行的時候,bss 段才在記憶體中被安排並被清空,這件事情是作業系統或者說載入器做的。
所以,bss實體是載入器實作的,而初始化數據實體是編譯器實作的。bss實體只在記憶體中,初始化數據實體在程式檔中。
七、聲明和定義
前文提到,程式設計師選擇用檔去編寫程式,選擇檔的原因也分析了。
人和檔要想到達機器那邊,需要經過編譯器這座橋梁。
一個大型程式會被組織成多個檔,這就給編譯帶來了難題。
這些檔最終是要被轉譯成程式的,可是它們的數量卻是變化的。
較小的程式可能有2個檔,較大的程式可能有幾百萬個檔,甚至只要我想,我可以制造一個10億檔的程式碼包。
並且一個檔就像一個畫板一樣,它終究是有一個大小的。就算一個檔很大很大,它仍然有個大小,你不能制造一個無窮大的檔。
編譯器在這個時候,接受了人類喜歡檔這一事實(這麽說是想把編譯器擬人成一頭牛 )。
- 程式碼是組織在多個檔中的,編譯器為了解決這個問題,很自然的就提出了聲明和定義這兩個概念
聲明 Declarations
定義 Definitions
一個變量或函數在記憶體中只能存在一份,所以在程式碼中它只能在一個地方登場(登場這個詞是兩年後想出的),這就是定義。
而這個變量或函數可能被多個檔使用,在每個檔中它都要登場,也就是每個檔中都定義,怎麽解決這個矛盾呢?-- 用聲明。
如果程式不是存放在多個檔中,那麽根本就不需要聲明,直接使用,直接登場,根本不用關心聲明定義。
程式雖然放在多個檔中,如果它們能相互參照(考,那和一個檔有什麽分別),那麽也不需要聲明。
可是,你知道這些假設都是不可能的,因為人類是用一個一個的檔去表達的。所以,是人類喜歡檔這一特征,造成了編譯器必須去這麽設計,必須有聲明和定義這種語法。
八、賦值和初始化
現在的編譯器已經「聰明」到超出你的想象。
即使去看幾十年前的老 c 編譯器,它的聰明程度也會令你驚嘆。
初始化就是這樣的一個「聰明」的行為。
可是偏偏初始化使用了和賦值一樣的語法,形如
int foo = 123;
結果,導致了這個編譯時行為有點耍「小聰明」的味道。
如果我告訴你,在c語言本來的設計中,初始化和賦值是兩種截然不同的語法。
你就會恍然大悟了。
int foo 123; /* 初始化,只能用在全域變量 */
int foo = 123; /* 賦值,只能用在局部變量 */
這兩種語法出現的場景、作用的物件和含義都不相同,很好區分。
初始化完全是編譯器的行為,賦值則是執行時的行為。
標準 C 後來統一了初始化這個概念,全域變量的初始化和自動變量的預設值賦值都叫初始化。
這確實更「高級」了,但是其實這兩個初始化差別卻存在,全域變量的初始化值只能是常量。
所以這也是 C 的一個遺憾。
八、參考
K&R 1.10節 2.4節 4.3節 4.4節 4.9節 是這個話題是官方的答案。
K&R 這本書我十年後再次重讀,讓我感到我越讀不懂它,這是一本電腦界的奇幻之書。
它把大量的優美的程式碼、演算法隱藏在例子中,
晦澀的機器原理埋藏在它的括弧,甚至是語氣之內,
艱深的編譯原理清晰的列在附錄之後,
對歷史的講述、對標準微妙的嘲諷和無奈編撰在序言前言的字裏行間,
對軟件工程的爭論用務實的程式碼去化解。
兩位作者的友情和自謙以及對程式設計師的諄諄教導貫穿這本不厚也不薄的書。
這是本適合重讀,不適合學習的書。
這是本不能用讀書計劃解決的書。
這是本拿來驗證自己的書。