@toc
兩個多月,二十多篇的的部落格,将近十w字的輸出,讓自己比開始之前對c語言程式設計有了不同的感悟,因為開學的關系之後應該會持續的更新一些新的内容,進入下一階段。計劃是在進入大二前持續刷題在課餘時間學習練習資料結構的知識,保持輸入和輸出的學習。
<code>在ansic的任何一種實作中,存在兩個不同的環境。</code> 第1種是翻譯環境,在這個環境中源代碼被轉換為可執行的機器指令。 第2種是執行環境,它用于實際執行代碼。
翻譯環境
1)組成一個程式的每個源檔案通過編譯過程分别轉換成目标代碼(object code)。 2)每個目标檔案由連結器(linker)捆綁在一起,形成一個單一而完整的可執行程式。 3)連結器同時也會引入标準c函數庫中任何被該程式所用到的函數,而且它可以搜尋程式員個人的程式庫,将其需要的函數也連結到程式中。
**幾個概念的了解:
<code>源檔案</code>:<code>.c為字尾的檔案</code>,比如test.c
<code>目标檔案</code>:.<code>obj為字尾的檔案</code>,由源檔案編譯後生成
<code>連結庫</code>:庫是寫好的現有的,成熟的,可以複用的代碼。
現實中每個程式都要依賴很多基礎的底層庫,不可能每個人的代碼都從零開始,是以庫的存在意義非同尋常。本質上來說庫是一種可執行代碼的二進制形式,可以被作業系統載入記憶體執行。 庫有兩種:靜态庫(.a、.lib)和動态庫(.so、.dll)。 windows上對應的是.lib.dll linux上對應的是.a.so
<code>靜态庫</code>:是因為在連結階段,會将彙編生成的目标檔案.o與引用到的庫一起連結打包到可執行檔案中。是以對應的連結方式稱為靜态連結。
比如:
**整個翻譯環境分為兩個大的部分:編譯 + 連結
編譯階段所需要的編譯器,我們可以找到它
連結階段所需要的連結器,我們也可以找到它
在<code>編譯階段</code>又可以分為以下3個步驟:
<code>預處理、編譯、彙編</code>
舉例 :
看代碼: sum.c
test.c
如何檢視編譯期間的每一步發生了什麼呢?
使用linux看詳細步驟:
預處理 選項 gcc -e test.c -o test.i 預處理完成之後就停下來,預處理之後産生的結果都放在test.i檔案中。
編譯 選項<code>gcc -s test.c</code>,編譯完成之後就停下來,結果儲存在test.s中。
彙編 <code>gcc -c test.c</code>,彙編完成之後就停下來,結果儲存在test.o中。
每個階段所做的事:
預處理階段:
①頭檔案的包含 ②#define定義的符号和宏的替換 ③注釋的删除(是以我們要大膽寫注釋!不會影響程式的運作和性能!) – - 這些都是文本操作
編譯階段:
把c語言代碼轉換為彙編代碼 文法分析、詞法分析、語義分析、符号彙總
彙編階段:
将彙編語言轉換為機器語言 生成符号表
連結階段:
把多個目标檔案(.obj(windows) / .o(linux))和連結庫進行連結 合并段表 符号表的合并和重定位
執行環境 / 運作環境:
程式執行的過程︰ 1.程式必須載入記憶體中。在有作業系統的環境中︰一般這個由作業系統完成。在獨立的環境中,程式的載入必須由手工安排,也可能是通過可執行代碼置入隻讀記憶體來完成。 ⒉程式的執行便開始。接着便調用main函數。 3.開始執行程式代碼。這個時候程式将使用一個運作時堆棧(stack ),存儲函數的局部變量和傳回位址。程式同時也可以使用靜态(static)記憶體,存儲于靜态記憶體中的變量在程式的整個執行過程一直保留他們的值。 4.終止程式。正常終止main函數; 也有可能是意外終止。
c語言允許在源程式中加入一些“預處理指令”(preprocessing directive), 以改程序式設計環境,提高程式設計效率。這些預處理指令是由c标準建議的, 但它不是c語言本身的組成部分, 不能用c編譯系統直接對它們進行編譯(因為編譯程式不能識别它們)。必須在對程式進行正式編譯(包括詞法和文法分析、代碼生成、優化等)之前, 先對程式中這些特殊的指令進行“預處理”(preprocess, 也稱“編譯預處理”或“預編譯”)。把預處理指令轉換成相應的程式段, 它們和程式中的其他部分組成真正的c語言程式, 對預處理指令進行的預處理工作, 是由稱為c預處理器(preprocessor)的程式負責處理的。
在預處理階段,預處理器把程式中的注釋全部删除; 對預處理指令進行處理, 如把#include指令指定的頭檔案(如stdio.h)的内容複制到#include指令處; 對#define指令,進行指定的字元替換(如将程式中的符号常量用指定的字元串代替), 同時删去預處理指令。
預定義符号
這些預定義符号都是語言内置的。
舉個栗子:
實際使用場景舉例:建立log日志
文法:
#define 機制包括了一個規定,允許把參數替換到文本中,這種實作通常稱為宏(macro)或定義宏(define macro)。
下面是宏的申明方式:
<code>#define name( parament-list ) stuff</code>其中的<code>parament-list</code>是一個由逗号隔開的符号表,它們可能出現在stuff中。
注意: <code>參數清單的左括号必須與name緊鄰</code>。 如果兩者之間有任何空白存在,參數清單就會被解釋為stuff的一部分。
如:
這個宏接收一個參數x . 如果在上述聲明之後,你把
置于程式中,預處理器就會用下面這個表達式替換上面的表達式:
警告: 這個宏存在一個問題: 觀察下面的代碼段:
乍一看,你可能覺得這段代碼将列印36這個值。 事實上,它将列印11. 為什麼?
替換文本時,參數x被替換成a + 1,是以這條語句實際上變成了: printf ("%d\n",a + 1 * a + 1 );
這樣就比較清晰了,由替換産生的表達式并沒有按照預想的次序進行求值。
在宏定義上加上兩個括号,這個問題便輕松的解決了:
這樣預處理之後就産生了預期的效
這裡還有一個宏定義:
定義中我們使用了括号,想避免之前的問題,但是這個宏可能會出現新的錯誤。
這将列印什麼值呢?
warning:看上去,好像列印100,但事實上列印的是55. 我們發現替換之後:
乘法運算先于宏定義的加法,是以出現了<code>55</code>
這個問題,的解決辦法是在宏定義表達式兩邊加上一對括号就可以了。
提示:
是以用于對數值表達式進行求值的宏定義都應該用這種方式加上括号,避免在使用宏時由于參數中的操作符或 鄰近操作符之間不可預料的互相作用。
#define替換規則
在程式中擴充#define定義符号和宏時,需要涉及幾個步驟。
在調用宏時,首先對參數進行檢查,看看是否包含任何由#define定義的符号。如果是,它們首先被替換。
替換文本随後被插入到程式中原來文本的位置。對于宏,參數名被他們的值替換。
最後,再次對結果檔案進行掃描,看看它是否包含任何由#define定義的符号。如果是,就重複上述處理過程。
注意:
宏參數和#define 定義中可以出現其他#define定義的變量。但是對于宏,不能出現遞歸。
當預處理器搜尋#define定義的符号的時候,字元串常量的内容并不被搜尋。
1. #的作用
使用 <code>#</code> ,把一個宏參數變成對應的字元串
<code>如何把參數插入到字元串中 ?</code>
2. ##的作用
<code>##</code> 可以把<code>位于它兩邊的符号合成一個符号</code>。 它允許宏定義從分離的文本片段建立辨別符。
注: 這樣的連接配接必須産生一個合法的辨別符。否則其結果就是未定義的。
當宏參數在宏的定義中出現超過一次的時候,如果參數帶有副作用,那麼你在使用這個宏的時候就可能出現危險,導緻不可預測的後果。副作用就是表達式求值的時候出現的永久性效果。
什麼是副作用:
比如
x+1 -->沒有副作用 ++x -->有副作用
max宏可以證明具有副作用的參數所引起的問題。
宏通常被應用于執行簡單的運算。
比如在兩個數中找出較大的一個。
使用宏比較好如果這個情況下
那為什麼不用函數來完成這個任務 ?
原因有二︰
用于調用函數和從函數傳回的代碼可能比實際執行這個小型計算工作所需要的時間更多,是以<code>宏比函數在程式的規模和速度方面更勝一籌</code>。 更為重要的是<code>函數的參數必須聲明為特定的類型</code>。是以函數隻能在類型合适的表達式上使用。反之這個宏怎可以适用于整型、長整型、浮點型等可以用于 > 來比較的類型。<code>宏是類型無關的</code>。
當然和宏相比函數也有劣勢的地方︰
每次使用宏的時候,一份宏定義的代碼将插入到程式中。除非宏比較短,否則可能大幅度增加程式的長度。 宏是沒法調試的。 宏由于類型無關,也就不夠嚴謹。 宏可能會帶來運算符優先級的問題,導緻程容易出現錯。 宏有時候可以做函數做不到的事情。比如︰宏的參數可以出現類型,但是函數做不到。
屬性
#define定義宏
函數
代碼長度
每次使用時,宏代碼都會被插入到程式中。除了非常小的宏之外,程式的長度會大幅度增長
函數代碼隻出現于一個地方;每次使用這個函數時,都調用那個地方的同一份代碼
執行速度
更快
存在函數的調用和傳回的額外開銷, 是以相對慢一些
操作符優先級
宏參數的求值是在所有周圍表達式的上下文環境裡,除非加上括号,否則鄰近操作符的優先級可能會産生不可預料的後果,是以建議宏在書寫的時候多些括号。
函數參數隻在函數調用的時候求值一次,它的結果值傳遞給函數。表達式的求值結果更容易預測。
帶有副作用的參數
參數可能被替換到宏體中的多個位置,是以帶有副作用的參數求值可能會産生不可預料的結果。
函數參數隻在傳參的時候求值一次, 結果更容易控制。
參數類型
宏的參數與類型無關,隻要對參數的操作是合法的,它就可以使用于任何參數類型。
函數的參數是與類型有關的,如果參數的類型不同,就需要不同的函數, 即使他們執行的任務是不同的。
調試
宏是不友善調試的
函數是可以逐語句調試的
遞歸
宏是不能遞歸的
函數是可以遞歸的
總結:我們在處理簡單、不易錯的問題時,使用宏可能比使用函數更有優勢,但是宏也有自身的缺點和不适用場景,需要謹慎使用。
命名約定
一般來講函數的宏的使用文法很相似。是以語言本身沒法幫我們區分二者。那我們平時的一個習慣是︰
<code>把宏名全部大寫,函數名不要全部大寫</code>
#undef
這條指令用于移除一個宏定義。
條件編譯
在編譯一個程式的時候我們如果要将一條語句(一組語句)編譯或者放棄是很友善的。因為我們有條件編譯指令。 比如說:
調試性的代碼,删除可惜,保留又礙事,是以我們可以<code>選擇性的編譯</code>。
<code>常見的條件編譯指令:</code>
1.單分支的條件編譯
2.多個分支的條件編譯
3.判斷是否被定義
4.嵌套指令
檔案包含
我們已經知道, <code>#include</code>指令可以使另外一個檔案被編譯。就像它實際出現于 #include 指令的地方一樣。
這種替換的方式很簡單: 預處理器先删除這條指令,并用包含檔案的内容替換。 這樣一個源檔案被包含10次,那就實際被編譯10次。
頭檔案被包含的方式:
本地檔案包含
查找政策:先在源檔案所在目錄下查找,如果該頭檔案未找到,編譯器就像查找庫函數頭檔案一樣在标準位置查找頭 檔案。 如果找不到就提示編譯錯誤。
linux環境的标準頭檔案的路徑 :
/ usr / include
vs環境的标準頭檔案的路徑∶
c : \program files(x86)\microsoft visua7 studio 9.0\vc\include
庫檔案包含
查找頭檔案直接去标準路徑下去查找,如果找不到就提示編譯錯誤。
這樣是不是可以說,對于庫檔案也可以使用" "的形式包含嗎?
答案是肯定的,可以。但是這樣做查找的效率就低些,當然這樣也不容易區分是庫檔案還是本地檔案了。
<> 和 “” 包含頭檔案的本質差別是:查找政策的不同
嵌套檔案包含
如果出現這樣的場景︰
comm.h和comm.c是公共子產品。 test1.h和test1.c使用了公共子產品。 test2.h和test2.c使用了公共子產品。 test.h和test.c使用了test1子產品和test2子產品。 這樣最終程式中就會出現兩份comm.h的内容。這樣就造成了檔案内容的重複。
如何解決這個問題 ?
條件編譯!! ! ! !
每個頭檔案的開頭寫:
或者:
<code>就可以避免頭檔案的重複引入。</code>
筆試題舉例:
頭檔案中的 ifndef/define/endif是幹什麼用的?
#include <filename.h> 和 #include "filename.h"有什麼差別?