Linux 下 C 語言編程
<from : http://www.xxlinux.com/linux/article/development/soft/20060611/1247_4.html>
Linux的發行版中包含了很多軟件開發工具。 它們中的很多是用於 C 和 C++應用程序開發的。 本文介紹了在 Linux 下能用於 C 應用程序開發和調試的工具。 本文的主旨是介紹如何在 Linux 下使用 C 編譯器和其他 C 編程工具, 而非 C 語言編程的教程。 在本文中你將學到以下知識:
· 什麼是 C
· GNU C 編譯器
· 用 gdb 來調試GCC應用程序
你也能看到隨 Linux 發行的其他有用的 C 編程工具。 這些工具包括源程序美化程序(pretty print programs), 附加的調試工具, 函數原型自動生成工具(automatic function prototypers)。
注意: 源程序美化程序(pretty print programs)自動幫你格式化源代碼產生始終如一的縮進格式。
什麼是 C?
C 是一種在 UNIX 操作系統的早期就被廣泛使用的通用編程語言。 它最早是由貝爾實驗室的 Dennis Ritchie 爲了 UNIX 的輔助開發而寫的, 開始時 UNIX 是用彙編語言和一種叫 B 的語言編寫的。 從那時候起, C 就成爲世界上使用最廣泛計算機語言。
C 能在編程領域裏得到如此廣泛支持的原因有以下一些:
· 它是一種非常通用的語言。 幾乎你所能想到的任何一種計算機上都有至少一種能用的 C 編譯器。 並且它的語法和函數庫在不同的平臺上都是統一的, 這個特性對開發者來說很有吸引力。
· 用 C 寫的程序執行速度很快。
· C 是所有版本的UNIX上的系統語言。
C 在過去的二十年中有了很大的發展。 在80年代末期美國國家標準協會(American National Standards Institute)發佈了一個被稱爲 ANSI C 的 C 語言標準。這更加保證了將來在不同平臺上的 C 的一致性。 在80年代還出現了一種 C 的面向對象的擴展稱爲 C++。 C++ 將在另一篇文章 "C++ 編程"中描述。
Linux 上可用的 C 編譯器是 GNU C 編譯器, 它建立在自由軟件基金會的編程許可證的基礎上, 因此可以自由發佈。 你能在 Linux 的發行光盤上找到它。
GNU C 編譯器
隨 Slackware Linux 發行的 GNU C 編譯器(GCC)是一個全功能的 ANSI C 兼容編譯器。 如果你熟悉其他操作系統或硬件平臺上的一種 C 編譯器, 你將能很快地掌握 GCC。 本節將介紹如何使用 GCC 和一些 GCC 編譯器最常用的選項。
使用 GCC
通常後跟一些選項和文件名來使用 GCC 編譯器。 gcc 命令的基本用法如下:
gcc [options] [filenames]
命令行選項指定的操作將在命令行上每個給出的文件上執行。 下一小節將敘述一些你會最常用到的選項。
GCC 選項
GCC 有超過100個的編譯選項可用。 這些選項中的許多你可能永遠都不會用到, 但一些主要的選項將會頻繁用到。 很多的 GCC 選項包括一個以上的字符。 因此你必須爲每個選項指定各自的連字符, 並且就象大多數 Linux 命令一樣你不能在一個單獨的連字符後跟一組選項。 例如, 下面的兩個命令是不同的:
gcc -p -g test.c
gcc -pg test.c
第一條命令告訴 GCC 編譯 test.c 時爲 prof 命令建立剖析(profile)信息並且把調試信息加入到可執行的文件裏。 第二條命令只告訴
GCC 爲 gprof 命令建立剖析信息。
當你不用任何選項編譯一個程序時, GCC 將會建立(假定編譯成功)一個名爲 a.out 的可執行文件。 例如, 下面的命令將在當前目錄下產生一個叫 a.out 的文件:
gcc test.c
你能用 -o 編譯選項來爲將產生的可執行文件指定一個文件名來代替 a.out。 例如, 將一個叫 count.c 的 C 程序編譯爲名叫 count 的可執行文件, 你將輸入下面的命令:
gcc -o count count.c
注意: 當你使用 -o 選項時, -o 後面必須跟一個文件名。
GCC 同樣有指定編譯器處理多少的編譯選項。 -c 選項告訴 GCC 僅把源代碼編譯爲目標代碼而跳過彙編和連接的步驟。 這個選項使用的非常頻繁因爲它使得編譯多個 C 程序時速度更快並且更易於管理。 缺省時 GCC 建立的目標代碼文件有一個 .o 的擴展名。
-S 編譯選項告訴 GCC 在爲 C 代碼產生了彙編語言文件後停止編譯。 GCC 產生的彙編語言文件的缺省擴展名是 .s 。 -E 選項指示編譯器僅對輸入文件進行預處理。 當這個選項被使用時, 預處理器的輸出被送到標準輸出而不是儲存在文件裏。
優化選項
當你用 GCC 編譯 C 代碼時, 它會試着用最少的時間完成編譯並且使編譯後的代碼易於調試。 易於調試意味着編譯後的代碼與源代碼有同樣的執行次序, 編譯後的代碼沒有經過優化。 有很多選項可用於告訴 GCC 在耗費更多編譯時間和犧牲易調試性的基礎上產生更小更快的可執行文件。 這些選項中最典型的是-O 和 -O2 選項。
-O 選項告訴 GCC 對源代碼進行基本優化。 這些優化在大多數情況下都會使程序執行的更快。 -O2 選項告訴 GCC 產生儘可能小和儘可能快的代碼。 -O2 選項將使編譯的速度比使用 -O 時慢。 但通常產生的代碼執行速度會更快。
除了 -O 和 -O2 優化選項外, 還有一些低級選項用於產生更快的代碼。 這些選項非常的特殊, 而且最好只有當你完全理解這些選項將會對編譯後的代碼產生什麼樣的效果時再去使用。 這些選項的詳細描述, 請參考 GCC 的指南頁, 在命令行上鍵入 man gcc 。
調試和剖析選項
GCC 支持數種調試和剖析選項。 在這些選項裏你會最常用到的是 -g 和 -pg 選項。
-g 選項告訴 GCC 產生能被 GNU 調試器使用的調試信息以便調試你的程序。 GCC 提供了一個很多其他 C 編譯器裏沒有的特性, 在 GCC 裏你能使 -g 和 -O (產生優化代碼)聯用。 這一點非常有用因爲你能在與最終產品儘可能相近的情況下調試你的代碼。 在你同時使用這兩個選項時你必須清楚你所寫的某些代碼已經在優化時被 GCC 作了改動。 關於調試 C 程序的更多信息請看下一節"用 gdb 調試 C 程序" 。
-pg 選項告訴 GCC 在你的程序里加入額外的代碼, 執行時, 產生 gprof 用的剖析信息以顯示你的程序的耗時情況。 關於 gprof 的更多信息請參考 "gprof" 一節。
用 gdb 調試 GCC 程序
Linux 包含了一個叫 gdb 的 GNU 調試程序。 gdb 是一個用來調試 C 和 C++ 程序的強力調試器。 它使你能在程序運行時觀察程序的內部結構和內存的使用情況。 以下是 gdb 所提供的一些功能:
· 它使你能監視你程序中變量的值。
· 它使你能設置斷點以使程序在指定的代碼行上停止執行。
· 它使你能一行行的執行你的代碼。
在命令行上鍵入 gdb 並按回車鍵就可以運行 gdb 了, 如果一切正常的話, gdb 將被啓動並且你將在屏幕上看到類似的內容:
GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.14 (i486-slakware-linux), Copyright 1995 Free Software Foundation, Inc. (gdb) |
當你啓動 gdb 後, 你能在命令行上指定很多的選項。 你也可以以下面的方式來運行 gdb :
gdb
當你用這種方式運行 gdb , 你能直接指定想要調試的程序。 這將告訴gdb 裝入名爲 fname 的可執行文件。 你也可以用 gdb 去檢查一個因程序異常終止而產生的 core 文件, 或者與一個正在運行的程序相連。 你可以參考 gdb 指南頁或在命令行上鍵入 gdb -h 得到一個有關這些選項的說明的簡單列表。
爲調試編譯代碼(Compiling Code for Debugging)
爲了使 gdb 正常工作, 你必須使你的程序在編譯時包含調試信息。 調試信息包含你程序裏的每個變量的類型和在可執行文件裏的地址映射以及源代碼的行號。 gdb 利用這些信息使源代碼和機器碼相關聯。
在編譯時用 -g 選項打開調試選項。
gdb 基本命令
gdb 支持很多的命令使你能實現不同的功能。 這些命令從簡單的文件裝入到允許你檢查所調用的堆棧內容的複雜命令, 表27.1列出了你在用 gdb 調試時會用到的一些命令。 想了解 gdb 的詳細使用請參考 gdb 的指南頁。
命令描述 | |
file | 裝入想要調試的可執行文件。 |
kill | 終止正在調試的程序。 |
list | 執行一行源代碼但不進入函數內部。 |
next | 執行一行源代碼但不進入函數內部。 |
step | 執行一行源代碼而且進入函數內部。 |
run | 執行當前被調試的程序 |
quit | 終止 gdb |
watch | 使你能監視一個變量的值而不管它何時被改變。 |
break | 在代碼裏設置斷點, 這將使程序執行到這裏時被掛起。 |
make | 使你能不退出 gdb 就可以重新產生可執行文件。 |
shell | 使你能不離開 gdb 就執行 UNIX shell 命令。 |
gdb 支持很多與 UNIX shell 程序一樣的命令編輯特徵。 你能象在 bash 或 tcsh裏那樣按 Tab 鍵讓 gdb 幫你補齊一個唯一的命令, 如果不唯一的話 gdb 會列出所有匹配的命令。 你也能用光標鍵上下翻動歷史命令。
gdb 應用舉例
本節用一個實例教你一步步的用 gdb 調試程序。 被調試的程序相當的簡單, 但它展示了 gdb 的典型應用。
下面列出了將被調試的程序。 這個程序被稱爲 greeting , 它顯示一個簡單的問候, 再用反序將它列出。
#include main () { char my_string[] = "hello there"; my_print (my_string); my_print2 (my_string); } void my_print (char *string) { printf ("The string is %sn", string); } void my_print2 (char *string) { char *string2; int size, i; size = strlen (string); string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size - i] = string[i]; string2[size+1] = `%content%'; printf ("The string printed backward is %sn", string2); } |
用下面的命令編譯它:
gcc -o test test.c
這個程序執行時顯示如下結果:
The string is hello there
The string printed backward is
輸出的第一行是正確的, 但第二行打印出的東西並不是我們所期望的。 我們所設想的輸出應該是:
The string printed backward is ereht olleh
由於某些原因, my_print2 函數沒有正常工作。 讓我們用 gdb 看看問題究竟出在哪兒, 先鍵入如下命令:
gdb greeting
注意: 記得在編譯 greeting 程序時把調試選項打開。
如果你在輸入命令時忘了把要調試的程序作爲參數傳給 gdb , 你可以在 gdb 提示符下用 file 命令來載入它:
(gdb) file greeting
這個命令將載入 greeting 可執行文件就象你在 gdb 命令行裏裝入它一樣。
這時你能用 gdb 的 run 命令來運行 greeting 了。 當它在 gdb 裏被運行後結果大約會象這樣:
(gdb) run Starting program: /root/greeting The string is hello there The string printed backward is Program exited with code 041 |
這個輸出和在 gdb 外面運行的結果一樣。 問題是, 爲什麼反序打印沒有工作? 爲了找出癥結所在, 我們可以在 my_print2 函數的 for 語句後設一個斷點, 具體的做法是在 gdb 提示符下鍵入 list 命令三次, 列出源代碼:
(gdb) list
(gdb) list
(gdb) list
技巧: 在 gdb 提示符下按回車健將重複上一個命令。
第一次鍵入 list 命令的輸出如下:
1 #include 2 3 main () 4 { 5 char my_string[] = "hello there"; 6 7 my_print (my_string); 8 my_print2 (my_string); 9 } 10 |
如果按下回車, gdb 將再執行一次 list 命令, 給出下列輸出:
11 my_print (char *string) 12 { 13 printf ("The string is %sn", string); 14 } 15 16 my_print2 (char *string) 17 { 18 char *string2; 19 int size, i; 20 |
再按一次回車將列出 greeting 程序的剩餘部分:
21 size = strlen (string); 22 string2 = (char *) malloc (size + 1); 23 for (i = 0; i < size; i++) 24 string2[size - i] = string[i]; 25 string2[size+1] = `%content%'; 26 printf ("The string printed backward is %sn", string2); 27 } |
根據列出的源程序, 你能看到要設斷點的地方在第24行, 在 gdb 命令行提示符下鍵入如下命令設置斷點:
(gdb) break 24
gdb 將作出如下的響應:
Breakpoint 1 at 0x139: file greeting.c, line 24
(gdb)
現在再鍵入 run 命令, 將產生如下的輸出:
Starting program: /root/greeting The string is hello there Breakpoint 1, my_print2 (string = 0xbfffdc4 "hello there") at greeting. |
c :24
24 string2[size-i]=string[i]
你能通過設置一個觀察 string2[size - i] 變量的值的觀察點來看出錯誤是怎樣產生的, 做法是鍵入:
(gdb) watch string2[size - i]
gdb 將作出如下回應:
Watchpoint 2: string2[size - i]
現在可以用 next 命令來一步步的執行 for 循環了:
(gdb) next
經過第一次循環後, gdb 告訴我們 string2[size - i] 的值是 `h`。 gdb 用如下的顯示來告訴你這個信息:
Watchpoint 2, string2[size - i] Old value = 0 `%content%00' New value = 104 `h' my_print2(string = 0xbfffdc4 "hello there") at greeting.c:23 23 for (i=0; i |
這個值正是期望的。 後來的數次循環的結果都是正確的。 當 i=10 時, 表達式 string2[size - i] 的值等於 `e`, size - i 的值等於 1, 最後一個字符已經拷到新串裏了。
如果你再把循環執行下去, 你會看到已經沒有值分配給 string2[0] 了, 而它是新串的第一個字符, 因爲 malloc 函數在分配內存時把它們初始化爲空(null)字符。 所以 string2 的第一個字符是空字符。 這解釋了爲什麼在打印 string2 時沒有任何輸出了。
現在找出了問題出在哪裏, 修正這個錯誤是很容易的。 你得把代碼裏寫入 string2 的第一個字符的的偏移量改爲 size - 1 而不是 size。 這是因爲 string2 的大小爲 12, 但起始偏移量是 0, 串內的字符從偏移量 0 到 偏移量 10, 偏移量 11 爲空字符保留。
爲了使代碼正常工作有很多種修改辦法。 一種是另設一個比串的實際大小小 1 的變量。 這是這種解決辦法的代碼:
#include main () { char my_string[] = "hello there"; my_print (my_string); my_print2 (my_string); } my_print (char *string) { printf ("The string is %sn", string); } my_print2 (char *string) { char *string2; int size, size2, i; size = strlen (string); size2 = size -1; string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size2 - i] = string[i]; string2[size] = `%content%'; printf ("The string printed backward is %sn", string2); } |
另外的 C 編程工具
Slackware Linux 的發行版中還包括一些我們尚未提到的 C 開發工具。 本節將介紹這些工具和它們的典型用法。
xxgdb
xxgdb 是 gdb 的一個基於 X Window 系統的圖形界面。 xxgdb 包括了命令行版的 gdb 上的所有特性。 xxgdb 使你能通過按按鈕來執行常用的命令。 設置了斷點的地方也用圖形來顯示。
你能在一個 Xterm 窗口裏鍵入下面的命令來運行它:
xxgdb
你能用 gdb 裏任何有效的命令行選項來初始化 xxgdb 。 此外 xxgdb 也有一些特有的命令行選項, 表 27.2 列出了這些選項。
表 27.2. xxgdb 命令行選項. | |
選 項 | 描 述 |
db_name | 指定所用調試器的名字, 缺省是 gdb。 |
db_prompt | 指定調試器提示符, 缺省爲 gdb。 |
gdbinit | 指定初始化 gdb 的命令文件的文件名, 缺省爲 .gdbinit。 |
nx | 告訴 xxgdb 不執行 .gdbinit 文件。 |
bigicon | 使用大圖標。 |
calls
你可以在 sunsite.unc.edu FTP 站點用下面的路徑:
/pub/Linux/devel/lang/c/calls.tar.Z
來取得 calls , 一些舊版本的 Linux CD-ROM 發行版裏也附帶有。 因爲它是一個有用的工具, 我們在這裏也介紹一下。 如果你覺得有用的話, 從 BBS, FTP, 或另一張CD-ROM 上弄一個拷貝。 calls 調用 GCC 的預處理器來處理給出的源程序文件, 然後輸出這些文件的裏的函數調用樹圖。
注意: 在你的系統上安裝 calls , 以超級用戶身份登錄後執行下面的步驟: 1. 解壓和 untar 文件。 2.cd 進入 calls untar 後建立的子目錄。 3.把名叫 calls 的文件移動到 /usr/bin 目錄。 4.把名叫 calls.1 的文件移動到目錄 /usr/man/man1 。 5.刪除 /tmp/calls 目錄。 這些步驟將把 calls 程序和它的指南頁安裝載你的系統上。
當 calls 打印出調用跟蹤結果時, 它在函數後面用中括號給出了函數所在文件的文件名:
main [test.c]
如果函數並不是向 calls 給出的文件裏的, calls 不知道所調用的函數來自哪裏, 則只顯示函數的名字:
printf
calls 不對遞歸和靜態函數輸出。 遞歸函數顯示成下面的樣子:
fact <<< recursive in factorial.c >>>
靜態函數象這樣顯示:
total [static in calculate.c]
作爲一個例子, 假設用 calls 處理下面的程序:
#include main () { char my_string[] = "hello there"; my_print (my_string); my_print2(my_string); } my_print (char *string) { printf ("The string is %sn", string); } my_print2 (char *string) { char *string2; int size, size2, i; size = strlen (string); size2 = size -1; string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size2 - i] = string[i]; string2[size] = `%content%'; printf ("The string printed backward is %sn", string2); } |
將產生如下的輸出:
1 main [test.c] 2 my_print [test.c] 3 printf 4 my_print2 [test.c] 5 strlen 6 malloc 7 printf |
calls 有很多命令行選項來設置不同的輸出格式, 有關這些選項的更多信息請參考 calls 的指南頁。 方法是在命令行上鍵入 calls -h 。
cproto
cproto 讀入 C 源程序文件並自動爲每個函數產生原型申明。 用 cproto 可以在寫程序時爲你節省大量用來定義函數原型的時間。
如果你讓 cproto 處理下面的代碼:
#include main () { char my_string[] = "hello there"; my_print (my_string); my_print2(my_string); } my_print (char *string) { printf ("The string is %sn", *string); } my_print2 (char *string) { char *string2; int size, size2, i; size = strlen (string); size2 = size -1; string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size2 - i] = string[i]; string2[size] = `%content%'; printf ("The string printed backward is %sn", string2); } |
你將得到下面的輸出:
/* test.c */ int main(void); int my_print(char *string); int my_print2(char *string); |
這個輸出可以重定向到一個定義函數原型的包含文件裏。
indent
indent 實用程序是 Linux 裏包含的另一個編程實用工具。 這個工具簡單的說就爲你的代碼產生美觀的縮進的格式。 indent 也有很多選項來指定如何格式化你的源代碼。這些選項的更多信息請看indent 的指南頁, 在命令行上鍵入 indent -h 。
下面的例子是 indent 的缺省輸出:
運行 indent 以前的 C 代碼:
#include main () { char my_string[] = "hello there"; my_print (my_string); my_print2(my_string); } my_print (char *string) { printf ("The string is %sn", *string); } my_print2 (char *string) { char *string2; int size, size2, i; size = strlen (string); size2 = size -1; string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size2 - i] = string[i]; string2[size] = `%content%'; printf ("The string printed backward is %sn", string2); } |
運行 indent 後的 C 代碼:
#include main () { char my_string[] = "hello there"; my_print (my_string); my_print2 (my_string); } my_print (char *string) { printf ("The string is %sn", *string); my_print2 (char *string) { char *string2; int size, size2, i; size = strlen (string); size2 = size -1; string2 = (char *) malloc (size + 1); for (i = 0; i < size; i++) string2[size2 - i] = string[i]; string2[size] = `%content%'; printf ("The string printed backward is %sn", string2); } |
indent 並不改變代碼的實質內容, 而只是改變代碼的外觀。 使它變得更可讀, 這永遠是一件好事。
gprof
gprof 是安裝在你的 Linux 系統的 /usr/bin 目錄下的一個程序。 它使你能剖析你的程序從而知道程序的哪一個部分在執行時最費時間。
gprof 將告訴你程序裏每個函數被調用的次數和每個函數執行時所佔時間的百分比。 你如果想提高你的程序性能的話這些信息非常有用。
爲了在你的程序上使用 gprof, 你必須在編譯程序時加上 -pg 選項。 這將使程序在每次執行時產生一個叫 gmon.out 的文件。 gprof 用這個文件產生剖析信息。
在你運行了你的程序併產生了 gmon.out 文件後你能用下面的命令獲得剖析信息:
gprof
參數 program_name 是產生 gmon.out 文件的程序的名字。
技巧: gprof 產生的剖析數據很大, 如果你想檢查這些數據的話最好把輸出重定向到一個文件裏。
f2c 和 p2c
f2c 和 p2c 是兩個源代碼轉換程序。 f2c 把 FORTRAN 代碼轉換爲 C 代碼, p2c 把 Pascal 代碼轉換爲 C 代碼。 當你安裝 GCC 時這兩個程序都會被安裝上去。
如果你有一些用 FORTRAN 或 Pascal 寫的代碼要用 C 重寫的話, f2c 和 p2c 對你非常有用。 這兩個程序產生的 C 代碼一般不用修改就直接能被 GCC 編譯。
如果要轉換的 FORTRAN 或 Pascal 程序比較小的話可以直接使用 f2c 或 p2c 不用加任何選項。 如果要轉換的程序比較龐大, 包含很多文件的話你可能要用到一些命令行選項。
在
一個 FORTRAN 程序上使用 f2c , 輸入下面的命令:
f2c my_fortranprog.f
注意: f2c 要求被轉換的程序的擴展名爲 .f 或 a .F 。
要把一個Pascal 程序裝換爲 C 程序, 輸入下面的命令:
p2c my_pascalprogram.pas
這兩個程序產生的 C 源代碼的文件名都和原來的文件名相同, 但擴展名由 .f 或 .pas 變爲 .c 。