Aptitude Command Line Interface

不過對於習慣在文字介面下過活的人,上述方法未免太痛苦了:D,aptitude 提供的強大 command line,可以讓你更快速達到目的,而且和 apt 系列指令基本上相容,具官方說法,aptitude 和 apt-get 管理不同資料庫來維護相依性,實際上使用,apt-get 和 aptitude 可以看到彼此安裝的套件。

aptitude 的命令形式為 aptitude action [argument],如 aptitude update,使用 update 這個 action 指示更新最近套件列表,可用的 action 為:

update 從來源處更新套件列表
upgrade 更新所有資安更新
dist-upgrade 更新所有版本更新

search pattern1 [pattern2...] 搜尋滿足樣式的套件
show package1 [package2...] 顯示套件資訊
changelog 顯示套件更改紀錄

install package1 [package2...] 安裝套件
reinstall package1 [package2...] 再次安裝套件
remove package1 [package2...] 移除套件
purge package1 [package2...] 清除套件

download 下載套件的 .deb 檔案
clean 刪除已下載的套件檔案
autoclean 刪除舊的已下載的套件檔案

hold 將套件標示為保持狀態
unhold 取消對一套件的保持命令

markauto 將套件標記為自動安裝
unmarkauto 將套件標記為手動安裝

forbid-version 禁止升級特定的套件版本
forget-new 將新套件辨識為已知套件

可用的 argument 請自行參閱 aptitude -h 所顯示的內容,可最為命令時的輔助用。

另外在搜尋中有些判斷語句可以作為輔助
~ahold 代表標示為保持現狀的套件
~b 為損壞套件
~g 無用的套件
~c 未清除的套件
~n{text}名稱中含有{text}的套件
~d{text} 描述中含有{text}的套件
~m{maintainer} 由指定維護者維護的套件
~V{version} 版本號{version}的套件

這在清潔系統時非常有用,例如,可以下

# aptitude purge ~g

這樣的命令,就會清除系統內所有無用的套件,這個功能在 deborphan 套件內也有提供且更為強大,但 aptitude 這樣的整合提供了很高的便利性。

文字介面的彈性不只於此,在互動式介面中可以用的標記這裡全都可以用,例如,可以下這樣的命令

# aptitude install A+ B- C_ D=

這樣便代表了安裝A,移除B,清除C,保持D,僅用一行便可以完成。

Tag Definition
無論利用 aptitude 瀏覽套件庫或是使用文字介面查詢,可以看到每個套件前面標示有一些 tag,標示套件目前的狀況或種類:
p 已清除或未安裝的
v 虛擬套件
B 損壞
u 已由 dpkg 解開
C 配置到一半的的
H 安裝到一半的的
c 移除 (remove) 但尚未清除 (purge) 組態的
i 已經安裝的
E 內部錯誤的

在 Linux 使用 USB Webcam

實作環境:

Fedora Core 4 with GNOME Environment (kernel: 2.6.13)
gnomemeeting (yum install gnomemeeting)
usbutils (yum install usbutils)
spca5xx (wget http://mxhaard.free.fr/spca50x/Download/spca5xx-20051001.tar.gz)
實測硬體: (VendorID:ProductID)

Z-Star Vimicro QCam VC0305 ( 0ac8:305b )
Logitech Quickcam Express Elch2 ( 046d:0928 )
實作步驟:

接上 USB Webcam
執行 lsusb (需 usbutils 套件) 查看 VendorID 與 ProductID
查看 spca5xx 是否支援該 Webcam (VendorID 與 ProductID 需同時吻合), 若 spca5xx 不支援, 到: Linux-USB device overview 看看; 若還是沒有, 再用 Google 找找看. 再沒有就換個 Webcam 吧!
下載並安裝 spca5xx. 在 kernel 2.6 安裝 spca5xx 很簡單, 解包 spca5xx 後, 於該目錄內 make && make install 即可
執行 modprobe spca5xx 載入 spca5xx 模組
在 GNOME 桌面中, 點選應用程式→網際網路→視訊會議 (gnomemeeting), 開啟視訊功能看到 webcam 視訊表示一切 ok!
spca5xx 載入時可使用的參數: (資料來源: modinfo -p spca5xx)

autoadjust: CCD dynamically changes exposure (spca501x only !! )
autoexpo: Enable/Disable hardware auto exposure / whiteness (default: enabled) (PC-CAM 600 only !!)
debug: Debug level: 0=none, 1=init/detection, 2=warning, 3=config/control, 4=function call, 5=max
snapshot: Enable snapshot mode
force_rgb: Read RGB instead of BGR
gamma: gamma setting range 0 to 7 3-> gamma=1
OffRed: OffRed setting range -128 to 128
OffBlue: OffBlue setting range -128 to 128
OffGreen: OffGreen setting range -128 to 128
GRed: Gain Red setting range 0 to 512 /256
GBlue: Gain Blue setting range 0 to 512 /256
GGreen: Gain Green setting range 0 to 512 /256
compress: Turn on/off compression (not functional yet)
bright: Initial brightness factor (0-255) not know by all webcams !!
contrast: Initial contrast factor (0-255) not know by all webcams !!
ccd: If zero, default to the internal CCD, otherwise use the external video input
min_bpp: The minimal color depth that may be set (default 0)
lum_level: Luminance level for brightness autoadjustment (default 32)
usbgrabber: Is a usb grabber 0x0733:0x0430 ? (default 1)
使用範例: modprobe spca5xx gamma=3 bright=170

解壓縮軟體unrar, unzip

在linux上有裝X系統的話,一定會去下載什麼作業啦或是什麼檔的,很多都常是.zip or .rar這樣我們就沒有辦法用tar解開它,所以我們需要裝unrar和unzip。

安裝unrar和unzip,因為unrar是在non-free套件內,所以我們要先改/etc/apt/sources.list
#vim /etc/apt/sources.list
deb http://ftp.tw.debian.org/debian/ lenny main non-free
deb-src http://ftp.tw.debian.org/debian/ lenny main non-free

#debfoster unrar unzip
如此這樣就裝好了,應該不困難,接下來我們要看看它基本的用法。

unrar的用法,解.rar的壓縮檔
#unrar x data.rar
unzip的用法,解.zip的壓縮檔
#unzip data.zip
今天把系統裝好,一樣是最簡安裝,結果我發現,奇怪為什麼沒有聲音,看來是我的軀動卡沒有正常運做吧!我的環境是xorg+wmii什麼都沒有,所以只好自己裝。

# debfoster alsa alsa-utils
# alsaconf

之後會問你音效介面卡,如果選擇正確的話,它會成功的完成配置。
ALSA聲音系統的設備文件位於/dev/snd的目錄下。

在X環境一些好用的軟體

推薦一些我覺得好用的軟體
audacious (音樂播放器)
gpicview (顯示圖片)
filezilla (多功能的檔案傳輸軟體)
filezilla-locales (filezilla中文語系)
dclock (數位電子時鐘)
gkrellm (系統監控軟體)
pidgin (即時通訊軟體具多種協定)
iceweasel (firefox)詳見blog文章
iceweasel-l10n-zh-tw (iceweasel中文語系)
leafpad (很單純的notepad)
pcmanfm (輕量級的檔案總管)詳見blog文章
vim (一定要裝的)
ssh (遠端ssh登入)
ntpdata (網路校時軟體)詳見blog文章
screen (一定要裝的)
unrar (解.rar壓縮程式)
unzip (解.zip壓縮程式)

開發套件
gcc-3.4
libc6-dev
manpages-dev

設定檔下載
screen http://moon.cse.yzu.edu.tw/~s932361/linux/screenrc.txt
vim http://moon.cse.yzu.edu.tw/~s932361/linux/vimrc.txt
本來以為在debian上的browser都叫firefox,但是因為某些原因,在debian上firefox它套件叫做iceweasel,功能就是firefox,所以今天我們要來安裝它,而且要讓它支援flash plugin,這就可以連上youtube看影片了。

#debfoster iceweasel
沒什麼技術,就直接裝了就對了,好了之後現在要讓iceweasel支援flash plugin,所以我們手動下載官方的flash_playter_9.tar.gz,因為官網太慢了,我自己mirror一份,有興趣的自己抓
http://moon.cse.yzu.edu.tw/~s932361/linux/flash_player.tgz

$wget http://moon.cse.yzu.edu.tw/~s932361/linux/flash_player.tgz (使用者自己抓就行了)
$tar zxvf flash_player.tgz
$cd install_flash_player_9_linux
$./flashplayer-installer (會問你安裝的問題,看的懂的就看,看不懂就是一直按y.....之後離開按q)

以上安裝完成之後,就有iceweasel+flash_plugin了,之後直接啟動iceweasel就可以開心的逛狂網頁了。
如果你的debian系統是和我一樣只安裝基系統,而X環境的部份是手動裝的話(xorg+wmii)那你一定沒有安裝檔案總管方面的軟體,在linux上有非常多的file manager,但是目前很輕量級功能又非常強的,看來是非pcmanfm莫屬了,所以就我的環境來安裝pcmanfm

#debfoster pcmanfm
裝好之後直接啟動pcmanfm但是會出現找不到icons的主題,所以雖然是可以使用,但是都看不到資料夾也看不到檔案,只有名字而已,因此要來解決這個問題,我們要安裝icons themes,就目前我了解的pcmanfm只支援gnome的icons themes,所以來安裝gnome-icon-theme

#debfoster gnome-icon-theme
裝好之後,我們可以到/usr/share/icons/下面看到gnome的icons了,接下來就是要設定讓pcmanfm可以讀取到此預設的icons theme,所以在家目錄修改.gtkrc-2.0,若沒有這個檔請自己建立
#vim .gtkrc-2.0
gtk-icon-theme-name="gnome"

設定完成之後即可直接啟動pcmanfm,如此在X環境裡面就有很好用的檔案總管軟體了
linux的輸入法有非常多種,但是gcin和scim比較多人使用,而我比較喜歡使用scim當我的輸入法,而且它支援無蝦米輸入法的tables,這篇就來談談如何安裝scim+無蝦米輸入法。

首先需要先安裝幾個套件scim scim-tables-zh im-switch
無蝦米輸入法下載點 http://moon.cse.yzu.edu.tw/~s932361/linux/liu_scim.tgz

#debfoster scim (scim輸入法主程式)
#debfoster scim-tables-zh (scim中文各種輸入法)
#debfoster im-switch (各種輸入法的選擇程式,自動啟動輸入法引擎)
im-switch可參考http://www.debian.org.hk/story/im-switch

以上都安裝好之後,接下來要安裝無蝦米輸入法了
#wget http://moon.cse.yzu.edu.tw/~s932361/linux/liu_scim.tgz
#tar zxvf liu_scim.tgz (解開之後裡面有兩個檔案,一個是noseeing.bin and liu5.png)
#cd liu_scim
#cp liu5.png /usr/share/scim/icons/
#cp noseeing.bin /usr/share/scim/tables/liu5.bin (我喜歡自己更名啦,其實換不換都可以)
以上工作完成之後就裝好無蝦米輸入法了

這時候重新啟動X,進入之後開始對scim做細部設定,所以執行scim-setup會有視窗跳出來勾一勾就設定好了,裡面可以設定輸入法快速鍵,我是習慣用ctrl+alt+方向鍵右,來選擇入法,如此就可以在X環境中快樂的使用各種輸入法了。
通常系統灌好的時候,需要網路校時,所以使用ntpdate是很方便的

#debfoster ntpdate
#ntpdate time.stdtime.gov.tw

透過和stdtime.gov.tw即可完成網路校時
對於X一般內件的xterm中文支援度比較差,所以選用mlterm是很不錯的選擇,這篇教學如何更改X內件的xterm為mlterm

首先當然要先裝軟體要裝兩個mlterm mlterm-tools

#debfoster mlterm (安裝mlterm主程式)
#debfoster mlterm-tools (安裝mlterm的設定程式,可以調字體字型大小等等功能)
安裝之後要將內件的xterm取代為mlterm
#update-alternative --config x-terminal-emulator (可以讓你選擇目前有的虛擬終端機,這個指令會更改 /etc/alternatives 裡面指向的指令)

完成之後就已完成修改了,重新啟動X就行了
$startx

如果你要使用設定工具的話,啟動mlterm之後在mlterm上按下ctrl+滑鼠右鍵,即可叫出設定視窗,可以設定任何你想設定的功能。
首先要先安裝一些不同的字體,像是windows用的新細明體,或是Mac OS用的HiLei Pro字型都可以自行安裝

假設今天取得兩種字型一個是Mac_Fonts.tgz和windws_font.tgz需要的可以提供下載點

http://moon.cse.yzu.edu.tw/~s932361/linux/mac_font.tgz
http://moon.cse.yzu.edu.tw/~s932361/linux/windows_font.tgz

下載取得之後先解開會得到兩個資料夾,之後搬到/usr/share/fonts/truetype下面
#cp -R Mac_Fonts windows_font /usr/share/fonts/truetype
$fc-cache -fv (一般使用者做就行了,讓系統更新目前的字型cache)
$fc-list :lang=zh-tw (看看有沒有出現在list之中)

那我的wmii怎麼選擇我的字型呢?這就要修改,這就要編輯自己的設定檔了,在家目錄下面
自己建立一個.fonts.conf做自己的設定,這樣比較好,這裡有參考fontconfig的文章
http://fractal.csie.org/~eric/wiki/Fontconfig

$vim .fonts.conf
<?xml version="1.0"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
<alias>
<family>serif</family>
<prefer>
<family>LiHei Pro</family>
<family>MingLiU</family>
<family>PMingLiU</family>
</prefer>
</alias>
<alias>
<family>sans-serif</family>
<prefer>
<family>LiHei Pro</family>
<family>MingLiU</family>
<family>PMingLiU</family>
</prefer>
</alias>
<alias>
<family>monospace</family>
<prefer>
<family>LiHei Pro</family>
<family>MingLiU</family>
<family>PMingLiU</family>
</prefer>
</alias>
</fontconfig>

看想讓用那個字型把它排列在最前面就可以了,這樣重新登入X的時候它就會變換你預設的字型了,非常的方便。
裝好基系統之後想要建構一般常使用的X window的環境的話,一般人是用gnome or kde但是我覺得那都不適合我,一方面太肥了,另一方面裝了一票用不到的東西,我希望我要什麼去裝什麼,東西夠用就好,所以我選擇了wmii一套windows manager而且是tiling window可切換,真的非常的適合我

#debfoster xorg
#debfoster wmii
安裝完之後,直接startx若是解析度太低的話,要自己手動調整
#vim /etc/X11/xorg.conf
Section "Monitor"
Identifier "Configured Monitor"
HorizSync 30-95
VertRefresh 60-75
EndSection

Section "Screen"
Identifier "Default Screen"
Monitor "Configured Monitor"
DefaultDepth 24
SubSection "Display"
Depth 24
Modes "1280x1024"
EndSubSection
EndSection
設定好之後存檔,重新啟動startx應該就可以了因為新版的xorg會自動偵測,但是好像都不成功的樣子,所以還是動手設定比較好,如此X環境應該是設定完成了。
當基系統裝完第一件事要做的就是更新系統和設定使用者所需要的語系和環境的設定檔

#cd /etc/apt
#vim sources.list (編輯套件來源,看想要加什麼或是移除什麼)
#aptitude update
#aptitude safe-upgrade (軟體升級)
#aptitude dist-upgrade (版次升級)
以上已經完成更新最新系統了

設定使用者語系,先看看有沒有安裝locales,如果有的話直接以下操做,若是沒有的話,請安裝
#dpkg-reconfigure locales
我勾選的語系是
[*] en_US ISO-8859-1
[*] zh_TW BIG5
[*] zh_TW.UTF-8 UTF-8
而預設的語系我是選BIG5因為和windows比較相容,如此語系已經設定好了
在root的權限我比較喜歡用英文的,所以我設定了
#cd /root
#vim .profile
LANG=en_US
LANGUAGE=en_US

接來下先處理套件安裝的問題,請看「必要套件管理軟體」
當debian 基系統裝好之後,先安裝套件管理工具,可以讓你的基系統安裝軟體時必免多裝不需要的軟體,和完整的移除軟體和相依套件

#aptitude
Option->Preferences->Install recommended packages automatically 取消勾選,必免裝了不必要的recommand package套件

#aptitude install debfoster
#cd /etc
#vim debfoster.conf (修改debfoster套件管理的設定檔,讓本來由apt-get處理套件改為aptitude)
InstallCmd = aptitude install
RemoveCmd = aptitude purge
UseRecommends = no (使用debfoster安裝套件時,不要自動安裝recommand套件)
KeeperFile = /etc/software (個人喜歡放在/etc下面有個software來記錄)

使用debfoster
#debfoster -q (讓debfoster記錄目前所有安裝的套件)
#vim /etc/software 來看看裝了什麼套件和移除套件了,若要移除套件,直接把名單中的名字移除存檔離開之後執行#debfoster -f讓debfoster自動移除套件和相依,建議安裝套件也透過debfoster記錄套件安裝
EX:
#debfoster xxxxxx (由debfoster來安裝套件)
編輯debfoster名單之後用
#debfoster -f (由debfoster自動解決要移除套件和相依)

安裝deborphan和localepurge
#debfoster deborphan localepurge
deborphan用來移除孤兒套件包
localepurge用來移除不必要的語系檔

使用deborphan
#aptitude purge `deborphan`

重新設定localepurge
#dpkg-reconfigure localepurge

Linux 常用指令參考

在Linux中最常使用到的指令,和最常用到的參數。

※ ls 列出目錄

* -a 連隱藏檔都列出
* -l 列出詳細資訊
* -d 只顯示目錄訊息而非目錄下的檔案
* -R 遞迴列出檔案及子目錄其下的所有子目錄和檔案

※ pwd 顯示使用者目前的目錄

* -p 則將結徑目錄顯示出來 (專門用在連結目錄)

※ mkdir 建立目錄

* -m 直接設定目錄屬性 (mkdir -m 700 test)
* -p 建立目錄中的子目錄 (mkdir -p test1/test2)

※ mv 移動檔案或改檔名

* -f 強制移動
* -i 已存在目的檔,會詢問是否over wirte

※ cp 檔案複製

* -i 若已存在則會詢問要否over write
* -f 強制複製或取代
* -a 完全複裂含使用人,屬性一樣的複制過來 (用在root)
* -r 用於目錄copy (重要)
* -d 若來源檔為連結檔的屬性,則複製連結檔而非檔案本身
* -s 複製成符號連結檔
* -l 複製成硬式連結檔

※ rm 移除檔案

* -f 強制移除
* -r 用於移除目錄
* -i 會詢問使用者是否真的要移除

※ cat 看檔

* -n 印出行號
* -A 可列出一些特殊字元

※ more 分頁顯示檔案內容

* enter 下翻一行
* space 下翻一頁
* :f 顯示目前行數和檔名
* q 離開
* / 尋找字串
* n 符合字串下一筆
* N 反向尋找符合字串下一筆

※ less 分頁顯示檔案內容 (可上翻)

* enter 下翻一行
* space 下翻一頁
* page up 上翻一頁
* page down 下翻一頁
* q 離開
* / 尋找字串
* n 符合字串下一筆
* N 反向尋找符合字串下一筆

※ chmod, chown 改變檔案屬性, 改變檔案所有人

* -R 連同子目錄都更新為同屬性

※ file 顯示某個檔案的基本資料

※ which 尋找某指令在那裡 (依所脫定的環境path去找)

※ whereis 尋找某指令

* -b 只找binary檔
* -m 只找man檔
* -s 只找source檔
* -u 找沒有說明的文件

※ locate 尋找檔案 (找資料庫)

* 使用前先updatedb

※ find 尋找檔案 (找檔案系統)

* -name 尋找檔名
* ex: find / -name test1.c

※ df 檢查磁碟使用量

* -a 列出所有使用量
* -h 容量以k, m, g顯示
* -T 連fs name都顯示出來
* -i 使用掉的i-node數量

※ du 檢查資料夾用量

* -a 列出目錄下所有子目錄檔案的所有用量
* -h 容量以k, m, g顯示
* -s 只顯示目錄總量 (和-a不能共用)

※ ln 製做符號連結和硬式連結

* -s 符號連結
* -f 目標檔有在的話移除再建立
* 不加參數則是建立硬式連結
* ex: ln -s test s_test

※ gzip 建立gun zip壓縮檔 (只能對單一檔案)

* -c 壓縮後輸出到銀目,配合資料流重導向
* -d 解壓縮
* -t 檢查有沒有錯誤
* -1~9 壓縮比
* ex:(壓) gzip test
* ex:(解) gzip -d test

※ tar 打包加壓縮 (重要)

* -j 使用bzip壓縮
* -z 使用gzip壓縮
* -c 建立打包
* -x 解開打
* -v 看檔案打包過程
* -f 輸出檔案的檔名
* -p 包留原來屬性
* ex:(包) tar -zcvf test.tar.gz test/
* ex:(解) tar -zxvf test.tar.gz
在此是針對只使用command line的朋友們,我想大家在使用putty(pietty)登入的時候一定會只使用一個終端機介面,而常常使用vim編完又要編譯程式的時候,就需要離開vim再編譯程式,編好有錯誤,又需要進入vim修改你的程式,這樣的動做實在非常的辛苦,大部份的時間都浪費在切換的時間中。

這邊我想提供大家一個使用終端機遠端連線的時候一定要用的一個套件就叫screen,這個東西有多便利呢?當你在putty下連線的時候,進入 screen這隻程式的時候,你已經具有多工作畫面了,你一定會覺得很奇怪在putty下面怎麼多畫面處理多樣事情對吧!但是只要透過screen的快速按鍵就行了,輕鬆切換(類似使用windows的alt+tab的方法)。這邊我提供了從裝好到使用,大家可以跟著試試看。

※ step1. 開始使用screen

$screen (開始執行這個程式)
因為screen所以它也有一些參數

-ls 列出目前有的screen
-r pid 回復工作環境

其它還有別的參數,但是上面兩個最重要囉!!

當你執行screen -ls的時候,可以列出你已建立的screen,但是目前不是在使用中,而你想要使用的話就執行screen -r pid(在-ls可以看到的行程代碼)這樣的話就可以重新的接上你之前建立的screen了(看不懂沒關系,等一下實際操做一下)而如果你的screen -ls之後只有單一個screen的話直接使用screen -r就可以回到本來的screen的工作環境了。

※ step2. 與法教學與使用

* 註:ctrl+a, c等的意思為「按下ctrl不放後按下a,之後全放在按下c」
* ctrl+a 這個是screen一開始全都要按的動做,之後就是組合鍵了
* ctrl+a , c 生成一個新的screen
* ctrl+a, p (previous) 切換到上一個screen
* ctrl+a, n (next) 切換到下一個screen
* ctrl+a, 0~9 切換到第0到第9個screen,如果你有開的話
* ctrl+a, w 列出目前已開啟的所有視窗
* ctrl+a, k (kill) 關閉目前正在使用的這個screen,但不是全部關閉所有的screen
* ctrl+a, i (information) 顯示目前視窗資訊
* ctrl+a, l 目前的screen重繪
* ctrl+a, d 暫時離開screen (超重要)
* ctrl+a, \ 砍掉所有的screen視窗,且離開screen (超重要)

※ EX1. 編寫程式使用

今天你想使用vim編輯你的程式,又希望編輯完可以馬上編譯,通常你都會多了進進出來vim的動做,今天就教你徹底的快速切換。

$screen
$vim test.c (進到vim寫東西)
在vim中進到指令模式(就是按下esc)後的command line mode,按下上面的快速鍵ctrl+a, c建立一個新的screen這時候,你就會看到我的視窗怎麼出來一個新的了,表示建立成功囉,那之前的那個呢?別急,按下ctrl+a, p就可以回到本來的vim中了,而要回到新的screen視窗的時候,按下ctrl+a , n 就可以回來囉。
(假設目前正在新的screen)
$gcc -o test test.c(編譯程式)
這樣出現錯誤的資訊馬上就可以使用ctrl+a, p切回去改,改完存檔後又可以ctrl+a, n切回來編,實在有夠方便的啦。

※ EX2. 不小心斷線

當如果你使用putty連線的時候,因為不小心與伺服器斷線或是因為某種原因沒有存檔導致編輯的檔案全都空了,這是件很幹的事情,但是當你使用了screen的話就放心啦。

執行了screen但是不小心因為系統不穩斷線的話,只需要重新登入主機重新執行screen -r就可以回復了,而如果你有眾多個screen被你建立的話,你需要screen -ls就可以看到總共你有多少個screen,而使用screen -r pid就可以回到你工作的狀態了。

※ EX3. 把工作環境帶到任何地方

執行了screen後不論你開了幾個screen (按下ctrl+a, c建立)做了多少的事情,如果有急事發生,或是下班了要回家了,那你可以很容易就可以把你的工作環境帶回家,你開了多少檔,有存沒有存都沒有關係,只需要按下ctrl+a, d就可以「暫時」離開screen這隻程式,但其實你還在背景執行著,等你回家之後下達 screen -r重新執行screen就可以自動連上你暫時離開的工作環境,但是如果你建立了眾多的screen,請先screen -ls看看你要連結你那一個工作環境。
使用putty(pietty)登入你的主機的時候是否老是看到黑黑的一片呢?而且你的命令提示列也是黑黑的,當你cat一堆資料的時候,完全不知道從那開始的,所以這邊主要教各位幾樣東西,學會ansi color,修改你的登入畫面(motd)以及修改命令提示列,讓你可以做一個適合你的命令提示列。

※ 先來介紹一下什麼叫ansi color

你一定有注意到,使用「ls」指令的時候,列出來的目錄和檔案會有上色,這些都是使用ansi color上色的,方便你辨示檔案或是目錄或是執行檔等等的,所以我們也可以拿來加以應用,但是要先學會下面的規則。

兩種上色方式

1. \033[(色碼)m (文字) \033[m (用於寫在shell中或是程式中表示)
2. ctrl+v, ctrl+[, [ (色碼) m (文字) ctrl+v, ctrl+[, [m (用於文字檔)

我想大家都可以看到,有兩個部份就是起始碼和結束碼但是1, 2方式都不同,其中第一種的表示方法大多都是用在你寫的shell中或是程式中,而第二種是寫在單一的純文字檔,兩種方法都可以使用,第二種方式我要講清楚一點,免得按的時候會出錯。

2的按法是
按著ctrl後不放按下v之後全放
再按下ctrl後不放按下[之後全放
打上一個[
打上色碼(下面講)
補上結尾碼m
開始輸入你要上色的文字
重複前面的按法

※ 色碼表示(不用背,要用來這裡查就好了)

色碼分成前景色(文字色)背景色(背景色),又分了一個「暗/亮」的開關,這個也是有個規則的。

0/1;前景色;背景色

順序你不能換自乩換喔!要依照這個規則來打,中間的分號是必要的,別自己就省掉了,接下來是色碼。

(顏色) 黑 紅 綠 黃 藍 紫 靛 白
前景 30 31 32 33 34 35 36 37
背景 40 41 42 43 44 45 46 47

以上就是色碼的調配,可以調出很多種變化,給幾個例子好了

* 0;33;42 (文字深黃色,背景綠色)
* 1;35;41 (文字淺紫色,背景紅色)

※ 將你的命令提示列上色(bash)

要將命令提示列上色,本來要懂一下變數和bashrc的知識,不過這裡先教你改一下,這樣可以先讓你玩到結果,請先用vim開啟你的.bashrc

vim .bashrc
在檔案後面貼上

PS1='${debian_chroot:+($debian_chroot)}\[\033[1;33m\][\u@\h]\[\033[m\]:\[\033[1;34m\]<\w>\[\033[m\]\$ '

存檔後離開
source .bashrc重新載入

上面照著做應該就可以完成了,現在我們來解釋一下,剛剛那是什麼情況,我先把沒有加入ansi color的本來的資料寫出來「PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '」這邊看到的全是「關鍵字」\u代表username而\h是代表hostname,而\w是目前的目錄,當然你不想要顯示出來也可以拿掉,之後就請在之中穿插著上面的ansi color code就行了。
在你寫程式或是編輯任何的資料的時候,我想多多少少一定會用到vim,如果你是桌面模式的話我想,操作方面應該是比較簡單的,但是因為我習慣於 command line,所以一開始多多少少使用vim會卡卡的,現在就教你怎麼快速上手vim,vim的使用設定檔和文件會在下面可下載,但是比較常用的指令現在來告訴大家,多練練幾次很容易就會上手的。

如果對於vim完全不會使用的人請先讀讀這個文章大家來學vim了解如何使用vim之後才進入我們真正的主題「VIM常用的指令」。

※ 指令模式下

* :w 存檔
* :q 離開
* :wq 存檔加離開
* :q! 離開不存檔

※ 一般模式下 (你按下esc之後就是了)

* i 游標前插入
* a 游標後插入
* o 插入新的一行
* 0 移動到此行的最前面
* $ 移動到此行的最後面
* gg 移動到檔案的第一行
* G 移動到檔案的最後一行
* nG (n是第幾行) 移動到那行
* ctrl+u (up) 上捲半頁
* ctrl+d (down) 下捲半頁
* 資料尋找"/"接字串,如/test要找下一個的話按n要找上一個的話按N
* yy 複制一行 (nyy,3yy表複制3行)
* dd 刪除一行 (3dd,表刪除3行)
* cc 修改整行
* p 貼上(貼在下一行)
* P 貼上(貼在上一行)
* x 刪除所在的該字元
* u (undo) 回復 (超重要)
* ctrl+r (redo) 重做 (超重要)
* r 取代一字元
* R 進入取代模式,打下去的都會replace直按到esc回到一般模式
* v 反白模式,配合d(刪除)y(複製)

※ vim開多視窗

* ctrl+w, n 開一新視窗,之後請選擇你要編輯的檔(:e test把test叫進來編輯)
* ctrl+w, j 移動編輯指標到下面的視窗
* ctrl+w, k 移動編輯指標到上面的視窗
* ctrl+w, q 關閉這個視窗

※ 檔案內容尋找並取代

在改一個文件之後的某個字要一次修正全文的話,使用以下的方法會很快速,不然用手一行一行改會改到天亮的。

:%s/被換的字/要換的字/[c, g, i, e](參數)
%是說現在的目標是整個檔案(當然也可以1,10表示只有1到10行間做尋找且取代
c 詢問是否替換,以免你後悔又要按u了
g 整行中有出現目標的全都要換掉(否則只會換最前面的字而已)
i 不分大小寫
e 不顯示錯誤

ex1: :%s/int/Int/cgi
ex2: :1,10%s/int/double/cg
對於有自己架設mysql-server的人如果裡面沒有資料,那毀掉沒有關係,但是像本人我因為有架設Blog的關係,所以裡面的資料都是我的心血,所以少了一樣我都會瘋掉,因此資料庫備份是我幾乎寫一次新文章就備份一次,所以順便就把資料庫備份的方式和救援的方式分享出來。

※ 使用mysqldump備份資料

如果你是自己架設的mysql會有這個指令,用which找看看,就知道放在那裡了,應該是在/usr/bin裡面,如果需要很詳細說明的話可以自己用man來看看有什麼參數可以設定,以下我只提供最簡單備份的方法。

mysqldump -uUSER -p - -skip-extended-insert DATABASE > db.back

USER 資料庫的使用者
DATABASE 是你要備份的資料庫名字(這個使用者必須有對此資料庫有權限)
db.back 備份的檔名

參數說明:

* -u 登入資料庫的使用者
* -p 登入者密碼(執行後才要輸入)
* - -skip-extended-insert 使用比較適合人閱讀的格式備份

執行後會要求輸入此資料庫使用者的密碼,確認後便會開始執行備份動作!

※ 使用mysql回復資料

有備份出來的資料,就用一樣的方法把資料回復回去,在此是指無條件的蓋寫回去,資料表不存在的話會自動建立,資料不存在會自動建立,就像備份之前完全一樣。

mysql -uUSER -p DATABASE < db.back

USER 資料庫的使用者
DATABASE 是你要回復的資料庫名字(這個使用者必須有對此資料庫有權限)
db.back 備份的檔名

執行後會要求輸入此資料庫使用者的密碼,確認後便會開始執行回復動作!

GDB常用指令

在Linux中寫了一個程式,但往往執行的結果都是出乎你的意料,因為可能是某個部份邏輯錯誤,所以可能會造成程式當掉了,因此我們使用GNU gdb這個除錯程式來除bug,而常使用的基本指令和基本用法,介紹給大家。

※ 編譯時加入-g的參數

為了讓gdb可以debug你的程式,所以在你編譯的期間,必須加入-g的參數,如此才可以讓gdb來除錯,所以編譯時的語法。

gcc ex.c -g -o ex

基本上編不過就是你程式語法或是語義的錯誤,而除錯主要是用在,程式可以正確的執行,但是執行出來的結果卻不是你要的,如果才需要除蟲。開始執行gdb除錯程式。

gdb ./your_programe

※ gdb的指令

進入gdb之後會類似像shell一樣有個提示字元,所以也和shell一樣要輸入你要執行的指令。

1. 環境的設定

* file ./your_programe 開始別的程式除錯
* set args xxx xxx 將xxx xxx當成參數執行你的程式
* set listsize n 設定使用list指令會一次顯示n行

2. 中斷點的設定

* break(b) n | function_name 設定中斷點在行或函式
* break(b) filename:n 如果是多個c檔案時指定filename和第n行
* clear n 清除第n行的中斷點
* delete index 清除第index號的中斷點
* disable index 暫時使第index號中斷無作用
* enable index 使第index號中斷再作用

3. 開始執行debug

* run 當都設定好你要給此程式的參數後開始執行
* quit 離開gdb除錯程式

4. 開始追蹤程式

* continue(c) 繼續執行直到下一個中斷點或結束
* next(n) 執行一行程式碼,不會跳進函式去執行
* step(s) 執行一行程式碼,如果碰到函式會跳進函式內部去執行
* until(u) 跳離一個while for迴圈

5. 監看你想知道的變數值

* print(p) 印出某個變數的資料
* display 會每次step, next時都會印出值來,print只印一次
* list(l) 向下列出程式碼,和listsize行數有關
* list(l) - 向上列出程式碼,和listsize行數有關
* list(l) n 列出第n行的程式碼

6. 其它觀看設定

* info (i) break (b) 看目前有建立的中斷點列表
* show args 顯示目前給此程式的參數
* show listsize 顯示目前使用list指令會一次顯示多少行
gcc是在unix like system上最常用的編譯程式,而它的參數實在非常的多,功能非常的強大,不過常常會用到的也只有那幾個而已,列出幾個代表性的編譯選項。

※ 使用方式

gcc [option] filename

※ 選項

* -c : 只做編譯(不做連結)
* -S : 輸出組譯碼
* -E : 將預處理結果顯示
* -o filename : 指定輸出檔名
* -ansi : 程式要求依據ansi c標準
* -Dmacro : 使定義巨集(marco)為有效
* -Dmarco=defn : 使定義巨集(marco)為defn
* -Wa,option : 將選項(option)傳給組譯器
* -wl,option : 將選項(option)傳給連結器
* -I : 追加include檔案的搜尋路徑
* -L : 追加library檔案的搜尋路徑
* -l : 指定連結的函式庫
* -Wall : 顯示所有的警告訊息
* -g : 編入除錯資訊(要使用GDB除錯一定要加)
* -O2 : 做最佳化

※ 使用範例

Example:

gcc -o file a.c b.c c.c
gcc -Wall -g -o test test.c
gcc -Iinclude -Llibrary -lmy_lib -o test1 test1.c
gcc -DDEBUG_ON -o test2 test2.c
gcc -c -o test3 test.c
這裡我們討論當你在Unix like的系統之中如何透過GNU tool幫你建立你整個專案,也許你的程式只有幾個檔案,慢慢用手編是簡單也沒問題,但是如果是大型的專案,超過幾十個檔案而且需要連結不少的函式庫,那你應該怎麼做呢?最方便的就是學習makefile和make指令,使用這個工具幫你做編譯和連結的動作。

※ 使用make好處

* 透過你所設定的條件幫你編譯好
* 方便專案管理
* 會透過檔案比對,依照相依性來編譯,不會全都編浪費時間
* 可以同時編譯函式庫或是檔案

※ make常用指令

* make -k: 會讓make在遇到錯誤的時候仍然運行,而不會停在第一個問題
* make -n: 只印出將會進行的工作,而不會真的去執行
* make -f makefile_name: 告訴make需要用那個makefile檔案。當你的make檔不是叫makefile的時候,需要自行透過-f加上你檔案名字,make才找的到你的makefile

※ make指令格式

make [option] [target] option就是上面的設定項目,而target等等會講到,就是我們將要產生出來的目標,可以接很多個目標,如果目標不寫的話預設是all。

Example:

make -n all clean
make install
make
make -f makefile2 install

※ 撰寫makefile檔案

makefile是由一堆「目標」和其「相依性檔案」還有「法則」所組成的,而法則在寫的時候前面不可以使用空格,只能使用Tab鍵,而且同一法則要換行的話需要使用'\'字元,而要加入註解的話要用'#'為開頭字元。

* [target] 目標 - 產生出來的東西或某個項目
* [dependency] 相依性項目 - 目標受相依檔案改變需要重新產生目標
* [rule] 法則 - 如何讓相依性項目編譯和連結來產生目標

Example:

#這是makefile的格式(註解)
[target]: [dependency] [dependency]
[TAB][rule]
[TAB][rule]
[target]: [dependency]
[TAB][rule]

整個makefile就是利用上面的格式,目標和相依性項目還有法則,組合之後就可以編出一個執行檔了,我們下面就來舉個最容易的範例。

Example:

all: myapp app.doc
myapp: main.o a.o b.o
[tab]gcc main.o a.o b.o -o myapp
main.o: main.c a.h
[tab]gcc -c main.c
a.o: a.c a.h
[tab]gcc -c a.c
b.o: b.c b.h
[tab]gcc -c b.c

make (呼叫make執行)

這是個最簡單的範例,我們來看一下它怎麼運做的,首先執行make的時候都會執行all這個目標,如果沒有設定all這個目標,它就會拿第一個目標來產生,在這邊看到要產生all的話,需要兩個檔案myapp和app.doc(主程式和說明檔),make開始會去找尋如何產生myapp和 app.doc的方法,所以myapp會成為下一個要產生出來的目標,所以要產生出myapp的話需要有main.o, a.o, b.o這三個檔連結在一起才可以產生myapp執行檔,那該怎麼產生呢?我們的法則有寫要使用gcc main.o a.o b.o -o myapp來產生,但是我們連main.o, a.o, b.o都沒有了,怎麼可能可以產生myapp呢?所以現在要先產生出這些目的檔,以main.o來看,它就成為下一個目標,它需要main.c和a.h才可以產生出來,而產生的法則是gcc -c main.c就可以產生出main.o檔了而a.o, b.o也是同樣的方式產生,一旦main.o a.o b.o都產生了,那麼目標myapp就可以透過法則來產生出來了。

所以寫makefile最重要的事情就是先搞清楚你最後產生什麼東西,該怎麼產生,那它需要什麼檔,再依它需要的檔怎麼產生,寫它的法則,一直追到最後的結果就是怎麼產生.c檔,就是本來的source了,那就要自己寫了^^。

※ 多重target(目標)

目標就是我們所定最後要產生的結果,所以我們可以定一些目標,讓shell script幫我們做很多事情,而常用makefile裡面會寫的目標如。

* install 產生完可執行檔之後怎麼將程式安裝到指定位置
* clean 只想產生執行檔,剩下的目的檔不需要可以刪除
* uninstall 從系統之中整個移除你安裝的所有檔案

Example:

all: myapp app.doc
myapp: main.o a.o b.o
[tab]gcc main.o a.o b.o -o myapp
main.o: main.c a.h
[tab]gcc -c main.c
a.o: a.c a.h
[tab]gcc -c a.c
b.o: b.c b.h
[tab]gcc -c b.c
#install 安裝套件
install: myapp app.doc
[tab]cp myapp app.doc /usr/local/myapp/
#clean 刪除產生出來的目的檔
clean:
[tab]rm -f *.o

make install (呼叫make執行install target)

在範例中我們增加了install和clean兩個目標,install主要就是把產生出來的檔案複製到我們想要安裝的地方,而clean的話則是把目標下面有的.o檔都移除或是要重新建立專案的時候,先把所有的檔都移除。

※ make的巨集(macro)

想想如果一個專案檔案好幾十個的話,那你不就目標相依性還有法則一行一行打到死嗎?所以make可以使用巨集讓你很方便的更改你的資料,或是編譯器或是參數等等的使用方法。

Example:

CC = gcc 指定
$(CC) 叫用
CFLAGS = -ansi -Wall -g 指定
$(CFLAGS) 叫用

還可以使用一種將副檔名改成別的副檔名的巨集使用方式,可以讓你輕鬆的將本來的副檔名改成另一個副檔名,且指定給某巨集。

Example:

SRC = a.c b.c
OBJ = $(SRC:.c=.o)

有幾個特別的內部巨集,讓makeifle 更加簡明,每個巨集都是在使用之前才被展開,所以巨集的意義隨makefile的處理而有所不同。

* $? 代表需要重建的相依性項目
* $@ 目前的目標項目名稱
* $< 代表第一個相依性項目
* $* 代表第一個相依性項目,不過不含副檔名

還有兩個有用的特別字元,可以加在要執行的命令之前。

* - make會忽略命令的錯誤。
* @ make不會在標準輸出上,顯示要執行的命令。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#object
OBJS = main.o a.o b.o
#install path
INSTALL_PATH = /usr/local/myapp/

all: myapp app.doc
myapp: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c a.h
[tab]$(CC) $(CFLAGS) -c -o $@ $<
a.o: a.c a.h
[tab]$(CC) $(CFLAGS) -c -o $@ $<
b.o: b.c b.h
[tab]$(CC) $(CFLAGS) -c -o $@ $<
#install 安裝套件
install: myapp app.doc
[tab]cp myapp app.doc $(INSTALL_PATH)
#clean 刪除產生出來的目的檔
clean:
[tab]rm -f *.o

make install (呼叫make執行install target)

這樣是不是方便多了,使用了$@ $<可以讓你少打很多字,而且把目的檔使用一個巨集表示,可以讓你方便管理,不會忘了打某個目的檔讓你一直編不出來而且在編譯的時候在CFLAGS可以設定編出來的模式為那一種,方便加入編譯的參數。

※ make內建的法則

到目前為止我們都很成功的寫出makefile可以幫我們編譯和連結出執行檔,但是其實我們還是重覆打了很多很類似的指令,浪費了不少的時間,然而make提供我們很多內建的法則。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#object
OBJS = main.o a.o b.o

all: myapp app.doc
myapp: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c a.h
a.o: a.c a.h
b.o: b.c b.h

而make已經幫我們做了gcc -Wall -ansi -g -c -o main.o main.c(a.o b.o也一樣)的工作了為了什麼自動加入-Wall -ansi -g呢?因為我們巨集有設定CFLAGS這是make內件就有的巨集名稱,所以一旦設好了,使用內建法則的時候,就會自動的加入你要的編譯參數。

※ make檔尾的法則

使用檔尾的延伸檔名估為一個法則,所以當一個檔案有吻合檔尾法則的時候,make就知道要使用什麼法則去產生目標。

Example:

格式 .[old_suffix].[new_suffix]:
.c.o:
[tab]$(CC) $(CFLAGS) -c -o $@ $<
.cpp.o:
[tab]g++ -c $<

如此就會把這目錄下面所有的.c檔變成.o檔,而法則就是去編譯它,而如果你想更懶一點的話還可以完全不寫,直接使用內建的法則,這樣也可以直接把目錄下面的所有檔都編好,為什麼呢?因為你要編出myapp的時候需要使用到$(OBJS)所以,就算你不寫.c.o或是任何的法則,make預設都會自己產生.o檔讓你可以連結出主程式。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -s
#object
OBJS = main.o a.o b.o

all: myapp app.doc
myapp: $(OBJS)
[tab]$(CC) $(OBJS) -o $@

那幹麻還要寫什麼檔尾法則呢?其實使用預設的法則有一些缺點,就是資料夾或是檔案相依性的問題,這樣預設法則根本就會有問題,因為你要 include的資料根本不在本目錄下面,所以這時候檔尾法則就很好用了,因為可以告訴make,針對所有的.c要變成.o都要以下面的rule來做。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -s
#object
OBJS = main.o a.o b.o
#include path
INCLUDE_PATH = include

all: myapp.exe app.doc
myapp.exe: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
.c.o:
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<

上例使用檔尾法則告訴make我在編譯的時候,要用到的.h檔位置在include裡面,要去裡面找,這樣程式就不會說找不到標頭檔的問題了,但是如果你使用內件的法則,它只會選擇目前目錄下面的檔案而已。

此外在更新的版本裡還有另一種語法,就是利用萬用字元的語法,它不限用在檔尾,還可以用在全部檔名的比對上。上範例可以使用萬用字元改寫。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -s
#object
OBJS = main.o a.o b.o
#include path
INCLUDE_PATH = include

all: myapp.exe app.doc
myapp.exe: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
%.o: %.c
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<

其實感覺沒有什麼差別對吧!但是後面討論專案的時候,就會感覺到差別了,其實萬用字元法則比較適合用於編譯一個大型的函式庫,而檔尾法則適合編譯一個目錄下面所有的檔案。

※ 專案討論1

專案目錄配置如下:

* + include (目錄)
- a.h
- b.h
- c.h
- d.h
* - main.c
* - a.c
* - b.c
* Makefile

檔案相依如下:

* main.c include a.h d.h
* a.c include a.h
* b.c include b.h

建立要求如下:

* 建立二元檔main
* clean target移除全部目的檔

決定好上面的目錄結構之後還有檔案相依之後我們就開始來寫我們的makefile,但是有幾個巨集是非常建議,先寫出來的,而且請多多利用註解,可以幫助未來你看檔的時候不會不知道你在寫什麼東西。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
H_PATH = include
#source
SRCS = main.c a.c b.c
#object
OBJS = $(SRCS:.c=.o)

all: main
main: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c $(H_PATH)/a.h $(H_PATH)/d.h
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
a.o: a.c $(H_PATH)/a.h
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
b.o: b.c $(H_PATH)/b.h
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
clean:
[tab]-rm -f *.o

雖然是寫好了,但怎麼覺得多寫好多東西,所以呢?就要善用隱含法則幫我做最多事情,所以將上面改寫如下。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
H_PATH = include
#source
SRCS = main.c a.c b.c
#object
OBJS = $(SRCS:.c=.o)

all: main
main: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
main.o: main.c $(H_PATH)/a.h $(H_PATH)/d.h
a.o: a.c $(H_PATH)/a.h
b.o: b.c $(H_PATH)/b.h
clean:
[tab]-rm -f *.o

什麼??編不出來,它說我的標頭檔都找不到,哇列,該怎辦呢?因為我們使用隱含法則幫我們做最簡單的產生.o檔的方式,它只用在同一目錄下面的檔案,但是本目錄以外,就會找不到了,所以我們就使用檔尾法則來告訴make如果要產生.o檔的話,需要的標頭檔,應該在$(H_PATH),請去那裡找。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
H_PATH = include
#source
SRCS = main.c a.c b.c
#object
OBJS = $(SRCS:.c=.o)

all: main
main: $(OBJS)
[tab]$(CC) $(OBJS) -o $@
.c.o:
[tab]$(CC) $(CFLAGS) -I$(H_PATH) -c -o $@ $<
main.o: main.c $(H_PATH)/a.h $(H_PATH)/d.h
a.o: a.c $(H_PATH)/a.h
b.o: b.c $(H_PATH)/b.h
clean:
[tab]-rm -f *.o

如果就可以成功的依照makefile所寫的內容一步步的把程式產生出來了,當然你也可以用喜歡萬用字元來做把.c.o改成%.o: %.c就可以了。

※ 專案討論2

這邊我們要編譯不同目錄裡面的檔案,來建立我們自己的函式庫,而且將它移動到某個目錄之中,這是有點困難的,我們來看看以下的一些設定。

專案目錄配置如下:

* + include (目錄)
- gear_calendar.h
- gear_io.h
- gear_color.h
- gear_string.h
- trax_file.h
- trax_name.h
* + source (實作檔)
- gear_calendar.c
- gear_io.c
- gear_string.c
- trax_file.c
- trax_name.c
* + library (放函式庫的目錄)
* Makefile

檔案相依如下:

* gear_calendar.c include gear_calendar.h
* gear_io.c include gear_io.h
* gear_string.c include gear_string.h
* trax_file.c include trax_file.h
* trax_name.c include trax_name.h

建立要求如下:

* 建立libtrax.a, libgear.a兩個函式庫
* clean target移除全部目的檔
* install target移動函式庫到library目錄下

因為目前要編的是函式庫,而且還不單只有一個函式庫,所以我們把標頭檔全都放在同一個資料夾裡面,而且屬於那個函式庫的檔案就以函式庫名稱為檔名的第一部份命名,如此我們方便分類我們的檔案,而實作檔就放在另一個目錄裡面,而且以其對應的標頭檔來命名實作檔,目錄檔案分配好之後我們就可以開始設計 makefile了。

Example:

#compiler
CC = gcc
#option
CFLAGS = -Wall -ansi -s
#library path
LIBRARY_PATH = library
#include path
INCLUDE_PATH = include
#source path
SOURCE_PATH = source
#library name
ALL_LIB = libgear.a libtrax.a
#gear library need objects
GEAR_OBJS = gear_calendar.o gear_io.o gear_string.o
TRAX_OBJS = trax_name.o trax_file.o

all: $(ALL_LIB)
libgear.a: $(GEAR_OBJS)
[tab]ar rcs $@ $(GEAR_OBJS)
libtrax.a: $(TRAX_OBJS)
[tab]ar rcs $@ $(TRAX_OBJS)
gear_calendar.o: $(SOURCE_PATH)/gear_calendar.c $(INCLUDE_PATH)/gear_calendar.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
gear_string.o: $(SOURCE_PATH)/gear_string.c $(INCLUDE_PATH)/gear_string.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
gear_io.o: $(SOURCE_PATH)/gear_io.c $(INCLUDE_PATH)/gear_io.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
trax_name.o: $(SOURCE_PATH)/trax_name.c $(INCLUDE_PATH)/trax_name.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
trax_file.o: $(SOURCE_PATH)/trax_file.c $(INCLUDE_PATH)/trax_file.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
install: $(ALL_LIB)
[tab]-cp $(ALL_LIB) $(LIBRARY_PATH)
clean:
[tab]-rm -f $(GEAR_OBJS) $(ALL_LIB)

經過一番的辛苦,終於寫完了,從頭到尾一直在剪剪貼貼,我們可不可以用別的方法呢?用隱含法則好了,之先前的專案有說過了,隱含法則沒有 include path會造成編譯錯誤,那可以用.c.o的檔尾法則阿,但是又想到一個問題,檔尾法則只可以用在本目錄也,但是我們實作標(.c)和介面檔(.h)都放在不同的目錄也,我們該怎麼辦呢?那就用萬用字元法則吧!

Example:

#compiler
CC = gcc
#option
CFLAGS = -Wall -ansi -s
#library path
LIBRARY_PATH = library
#include path
INCLUDE_PATH = include
#source path
SOURCE_PATH = source
#library name
ALL_LIB = libgear.a libtrax.a
#gear library need objects
GEAR_OBJS = gear_calendar.o gear_io.o gear_string.o
TRAX_OBJS = trax_name.o trax_file.o

all: $(ALL_LIB)
libgear.a: $(GEAR_OBJS)
[tab]ar rcs $@ $(GEAR_OBJS)
libtrax.a: $(TRAX_OBJS)
[tab]ar rcs $@ $(TRAX_OBJS)
%.o: $(SOURCE_PATH)/%.c $(INCLUDE_PATH)/%.h
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
install: $(ALL_LIB)
[tab]-cp $(ALL_LIB) $(LIBRARY_PATH)
clean:
[tab]-rm -f $(GEAR_OBJS) $(ALL_LIB)

我們先來看看%.o: $(SOURCE_PATH)/%.c $(INCLUDE_PATH)/%.h以這個相依來說,make會先看看本目錄有沒有gear_calendar.o檔,如果沒有的話,會相依於我們同檔名在source/和include/的實作檔和介面檔,所以make可以找到實作檔且將它編譯出來。而%代表所有目錄下的檔名,在這裡有的檔名是 gear_calendar.o gear_io.o gear_string.o trax_name.o trax_file.o這些包在$(GEAR_OBJS)和$(TRAX_OBJS)兩個巨集裡面。

最後在透過ar指示將產生出來的.o檔依造我們設定的$(GEAR_OBJS)和$(TRAX_OBJS)兩個巨集,包在不同的static library之中,如果有函式庫不會包的話,可以參考本部落格的文章(Make Library for Linux),裡面會教三種函式庫(static library, shared library, dynamic library)。

因為只是產生兩個函式庫而已,但是我們還沒有真的安裝到library裡面可以讓別人使用,所以使用make install來安裝,請注意一下install: $(ALL_LIB)這個相依性是libgear.a libtrax.a要有存在,因為需要存在,所以它需要先去編出這兩個函式庫才可以安裝,所以不論你直接make install 或是make,它都會去產生兩個函式庫,而make install只是把函式庫產生出來,並且安裝到指定的目錄而已,其實你可以改成如下。

Example:

install: all
[tab]cp $(ALL_LIB) $(LIBRARY_PATH)

效果是一樣的,所以寫makefile最重要的是對此專案要很了解需要編什麼檔,什麼函式庫等等,而拿到別人的source通常也是透過看makefile可以了解它整個專案是怎麼產生出來,什麼介面和實作有相依性。

※ 專案討論3

這邊我們寫了一個檔案來測試我們先前建立的函式庫,使用我們自己的介面,所以討論2的部份需要真的完全了解。

專案目錄配置如下:

* + libgear (函式庫)
+ include (介面標頭檔)
+ source (介面實作檔)
+ library (函式庫使用檔)
* space.c
* Makefile

檔案相依如下:

* space.c include gear_io.h gear_color.h gear_string.h

建立要求如下:

* 建立space執行檔
* clean target移除全部目的檔
* 如果函式庫還沒有建立的話,要自動建立函式庫

這邊的要求還滿難的,因為如果函式庫libgear.a libtrax.a沒有的話根本不能編space.c所以產生函式庫是很重要的,所以我們要想個辦法讓它先去確認函式庫有沒有存在,有的話才可以直接連結使用,但是不在的話,也沒有關係,我們可以讓剛剛討論2的makefile幫我們來產生函式庫。

Example:

#compiler
CC = gcc
#cflags
CFLAGS = -Wall -ansi -g
#include path
INCLUDE_PATH = libgear/include
#library path
LIBRARY_PATH = libgear/library
#use library
USE_LIB = $(LIBRARY_PATH)/libgear.a $(LIBRARY_PATH)/libtrax.a
#objects
OBJECTS = space.o

all: space
space: $(OBJECTS) $(USE_LIB)
[tab]$(CC) $(OBJECTS) $(USE_LIB) -o $@
$(USE_LIB):
[tab]cd libgear; make install
space.o: space.c
[tab]$(CC) -I$(INCLUDE_PATH) $(CFLAGS) -c -o $@ $<
clean:
[tab]rm -f $(OBJECTS)

這邊最重要的就是確認有沒有libgear.a libtrax.a這兩個函式庫,沒有的話一切都不用搞了,所以設了一個巨集$(USE_LIB)因為路徑不一樣,所以要自己設定好,而在連結的時候需要$(USE_LIB)如果它不存在的話,就成為一個目標了,而這個目標就是叫libgear目錄下面的makefile去做,把libgear.a libtrax.a產生出來,而其它的部份怎麼編譯space.o我相信大一定已經非常熟悉了,所以這邊就不講下去了。
使用函式庫(API)讓我們開發程式速度增加,而且小細節的部份不用去特別注意,因為函式庫已經幫你解決掉了,自己辛苦寫了那麼多的程式,一定也會想有自己的函式庫,方便自己以後開發程式使用,所以這邊講解在Linux上所用到的函式庫有那些,以及如何使用。

※ 預先了解觀念

* 內外部連結(external linkage, internal linkage)
* 多檔案程式製成
* 基本gcc的使用
* Linux目錄樹

※ 將學會的觀念

* static library (.a library)
* shared library (.so library)
* dynamic loader (DL library)

※ 練習檔

Example:

test1.c for share and static library

#include
#include
#include "adio.h"
#include "adstring.h"
int main()
{
char input[21];
char buffer[11];
char buf[11];
const char* str = "hello world!!";
adstring_strcpy(buffer, str, 11);
puts(buffer);
adio_fgets(input, 21, stdin);
puts(input);
adstring_strcpy(buffer, input, 5);
puts(buffer);
int a = 12345;
puts(adstring_itoa(buf, a, 11));
puts(buf);
return 0;
}

test2.c for dynamic loader

#include
#include
#include
#include
int main()
{
void* handle;
char* (*so_strcpy)(char*, const char*, int);
char* (*so_gets)(char*, int);
char* (*so_itoa)(char*, int, int);
char* error;
handle = dlopen("./libadlib.so", RTLD_LAZY);
if (!handle)
{
puts (dlerror());
exit(1);
}
so_strcpy = dlsym(handle, "adstring_strcpy");
so_gets = dlsym(handle, "adio_fgets");
so_itoa = dlsym(handle, "adstring_itoa");
char input[21];
char buffer[11];
char buf[11];
const char* str = "hello world!!";
so_strcpy(buffer, str, 11);
puts(buffer);
so_gets(input, 21);
puts(input);
so_strcpy(buffer, input, 5);
puts(buffer);
int a = 12345;
so_itoa(buf, a, 11);
puts(buf);
dlclose(handle);
return 0;
}

adio.h for static, shared include

#ifndef ADVENCE_IO_H
#define ADVENCE_IO_H
#include
char* adio_fgets(char* buf, int num, FILE* fp);
void adio_stdinclean(void);
#endif

adstring.h for static, shared include

#ifndef ADVENCE_STRING_H
#define ADVENCE_STRING_H
char* adstring_strcpy(char* to, const char* from, int num);
char* adstring_itoa(char* to, int from, int num);
#endif

adio.c for make lib

#include
#include
char* adio_gets(char* buf, int num, FILE* fp)
{
char* find = 0;
fgets(buf, num, stdin);
if ((find = strrchr(buf, '\n')))
{
*find = '\0';
}
else
{
while (fgetc(fp) != '\n');
}
return buf;
}
void adio_stdinclean()
{
while (getchar() != '\n');
}

adstring.c for make lib

#include
#include
char* adstring_strcpy(char* to, const char* from, int num)
{
int size = num-1;
strncpy(to, from, size);
if (strlen(from) >= size)
{
to[size] = '\0';
}
return to;
}
char* adstring_itoa(char* to, int from, int num)
{
char tmp[11];
sprintf(tmp, "%d", from);
adstring_strcpy(to, tmp, num);
return to;
}

※ 靜態函式庫(static library)

所謂的靜態函式庫,簡單講是把一堆object檔用ar(archiver)包裝集合起來,檔名以「.a」結尾。優點是執行效能通常會比後兩者快,而且因為是靜態連結,所以不易發生執行時找不到library或版本錯置而無法執行的問題。缺點則是檔案較大,維護度較低;例如library如果發現bug需要更新,那麼就必須重新連結執行檔。

* 連結時期在程式還沒有開始執行就已連結
* 執行檔因為包含了函式庫,所以檔案較大
* 執行檔因為不用去尋找函式庫,所以執行較快
* 函式庫如果更新的話,使用到此函式庫所有的檔案要全部重新連結

Example:
Make static library

gcc adio.c adstring.c -Wall -c (編出adio.o adstring.o)
ar rcs libadlib.a adio.o adstring.o (包成libadlib.a檔)

如此我們可以建立由adio.o adstring.o所產生的static library(.a)其中檔名libadlib.a前面的「lib」和後面的「.a」是不能少的,因此你的靜態函式庫命名要以lib(xxxxx).a 才行,再使用ar指令將.o檔包在一個函式庫裡面,接著我們來看使用方法。

Example:
Use static library

gcc test.c -I. -L. -ladlib -o test1

其中-I是代表目前的標頭檔尋找位子,-L是代表目前的函式庫尋找位子,-ladlib是使用函式庫libadlib.a所以剛剛說的lib(xxxx).a現在看的出來只取出xxxx前後都不見了,如此可以成功使用static library了。

一般來說static library使用的目的是不想給別人你的source所以你都包好成為.a檔,之後只需要傳給別人你的標頭檔,和你的靜態函式庫.a別人就可以直接使用了。

※ 共享函式庫(shared library)

Shared library會在程式執行起始時才被自動載入。因為程式庫與執行檔是分離的,所以維護彈性較好。有兩點要注意,shared library是在程式起始時就要被載入,而不是執行中用到才載入,而且在連結階段需要有該程式庫才能進行連結。首先有一些名詞要弄懂,soname、 realname與linkername。

soname用來表示是一個特定library 的名稱,像是libmylib.so.1 。前面以「lib」開頭,接著是該library 的名稱,然後是「.so」接著是「版號」,用來表名他的介面;如果介面改變時,就會增加版號來維護相容度。

realname 是實際放有library程式的檔案名稱,後面會再加上minor 版號與release 版號,像是libmylib.so.1.0.0 。一般來說,最尾碼的release版號用於程式內容的修正,介面完全沒有改變。中間的minor用於有新增加介面,但相舊介面沒改變,所以與舊版本相容。最前面的version版號用於原介面有移除或改變,與舊版不相容時。

linkername是用於連結時的名稱,是不含版號的soname ,如: libmylib.so。通常linker name與real name是用ln 指到對應的real name ,用來提供彈性與維護性。

* 連結時期在程式開始時連結載入
* 執行檔因為不含有函式庫所以比較小
* 執行檔需要尋找所使用的.so檔所以比較慢
* 函式庫若有更改,重新建立函式庫即可,程式方面不用重編譯
* 可具有函式庫版本管理

Example:
Make shared library

gcc adio.c adstring.c -c -Wall -fPIC (編出adio.o adstring.o)
gcc adio.o adstring.o -shared -Wl,-soname,libadlib.so.1 -o libadlib.so.1.0.1 (編出libadlib.so.1.0.1共享函式庫的檔)
ln -s libadlib.so.1 libadlib.so (連結時的linker name)
ldconfig -n . (產生libadlib.so.1 -> libadlib.so.1.0.1的s-link)

編譯時要加上-fPIC用來產生position-independent code。也可以用-fpic參數。(不太清楚差異,只知道-fPIC 較通用於不同平台,但產生的code較大,而且編譯速度較慢)。而-shared 表示要編譯成shared library,-Wl 用於參遞參數給linker,因此-soname與libmylib.so.1會被傳給linker處理,-soname用來指名soname 為libadlib.so.1,library會被輸出成libadlib.so.1.0.1 (也就是real name),若不指定soname 的話,在編譯結連後的執行檔會以連時的library檔名為soname,並載入他。否則是載入soname指定的library檔案,所以我們產生以下各檔。

* soname: libadlib.so.1 (由ldconfig -n . 產生)
* realname: libadlib.so.1.0.1 (由gcc -shared -Wl .....產生)
* linkername: libadlib.so (自己用ln -s所建立的)

其中我們要將上面三個soname, realname, linkername用符號連結串起來libadlib.so -> libadlib.so.1 -> libadlib.so.1.0.1,為什麼呢?因為當我們連結使用的時候會連結libadlib.so而在程式執行的時候,會因為soname的關系 ld(連結器)會去尋找libadlib.so.1,而透過符號連結,可以由libadlib.so.1找到libadlib.so.1.0.1我們真正的共享函式庫檔,這樣做有一些好處。

* 連結的時候連結libadlib.so沒有版次的問題,因為會自動連到最新的版次,如果那天有更新的soname的版次出來的話只需要更改libadlib.so -> libadlib.so.2新的版次即可
* 如果共享函式庫做一點點小修正,而後只要增加共享函式庫的編號即可,如libadlib.so.1.0.2表示修正過,因此目錄下會有 libadlib.so.1.0.1(舊版本)libadlib.s0.1.0.2(新版本),但是透過「ldconfig -n .」會自動產生libadlib.so.1 -> libadlib.so.1.0.2的符號連結,不過先決條件是你此版的soname還是libadlib.so.1,而使用了ldconfig才會自動產生這樣的符號連結
* ldconfig是專門用在自動產生soname對映到最近期產生,或是編號最新,共享函式庫的符號連結檔

產生出來了libadlib.so(s-link), libadlib.so.1(s-link), libadlib.so.1.0.1(share library)我們來看看應該如何使用這些檔案。

Example:
Use shared library

gcc test.c -I. -L. -ladlib -o test2 (和static library沒有兩樣)

雖然建立好像和static library差別不大,如果目錄下同時有static與shared library的話,會以shared為主,使用-static參數可以避免使用shared連結,但是由上例編完後開始執行的時候應該會告訴你,找不到你的共享函式庫,嘿嘿!!因為路徑的問題,預設下只會找尋/usr/lib或是/usr/local/lib所以當然啦。你都放在自己的目錄當然找不到,我們可以用ldd來觀查看看。

Example:
查看test2中使用那些shared library

ldd ./test2 (會告訴你libadlib.so.1找不到啦)
(libadlib.so.1 => not found)

因此我們有幾個解決的辦法,來解決找不到你的shared library的方法。

1. 把libadlib.so(linkername), libadlib.so.1(soname), libadlib.so.1.0.1(realname)全都複製一份到/usr/lib中
2. /etc/ld.so.conf,加入一個新的library搜尋目錄,並執行ldconfig更新快取
3. 設定LD_LIBRARY_PATH環境變數來搜尋library,如:export LD_LIBRARY_PATH=.

以上的三種方式都可以讓你的shared library被正確的找到,但是我們通常使用第一種方法,因為這樣你的函式庫就可以被使用此系統的所有人共享了,而第三種方法通常都是用在暫時搬到某台機器上測式時暫時讓目前的目錄會被搜尋到。

※ 動態載入函式庫 (dynamic loading library)

其實動態載入函式庫不是一個檔案或是程式,而是一個技術,只有當你的函式庫為共享函式庫的時候,才可以使用dynamic loading這樣的技術,而此技術是不需要在連結的時候告訴ld說我們要用那個函式庫,而是在程式執行期間,執行某一行,真的需要用到那個函式庫裡面的某個函式的時候,才去和此函式庫連結,而且只單取用其中的某個函式,如此對系統需求更是少,但是同樣的因為載入的關係,會造成程式執行速度比較慢,此技術類似windows上的DLL動態連結檔。

這個技術比較有一點難度的就是,它透過一些函式來達到這樣的效果,動態載入是透過dl function的一套函式來實作,以下列出這些常使用的dl函式,而它的標頭檔為dlfcn.h。

void* dlopen(const char* filename, int flag);
(開啟載入filename指定的library載入執行程式中)

* filename 要載入的.so檔名(含路徑)
* flag 載入的方式(RTLD_LAZY)
* 回傳函式庫的記憶體位址

void* dlsym(void* handle, const char* symbol);
(取得symbol指定的函式名稱,所指向的函式記憶體位址)

* handle 由dlopen所取得的函式庫記憶體位址
* symbol 要使用此函式庫的某個函式名稱
* 回傳函式的記憶體位址

int dlclose(void *handle);
(關閉dlopen開啟的handle)

char *dlerror(void);
(傳回最近所發生的錯誤訊息)

以上的test2.c是由test1.c改變而來的,結果都是完全一樣的,但是test2.c是使用dynamic loading的技術完成的,此範例可以看到如果使用上面說的四個函式,程式會寫了之後,還需要怎麼連結才行,因此來我們連結看看吧!假設上面的 shared library所建立的三個檔libadlib.so, libadlib.so.1, libadlib.so.1.0.1都在目前的目錄下面。

Example:
dynamic loading shared library

gcc test2.c -ldl -o test3 (dl是dlfcn.h的函式庫)

對於動態載入沒有路徑上的問題,只要你在dlopen裡面的filename路徑設定是正確的,那就可以開啟使用你的shared library達到動態載入的能力,它和之前使用函式庫的方法有一些不同。

* 在test2.c中並不用含入adio.h adstring.h標頭檔
* 使用函都是透過dlsym回傳的函式指標來使用函式庫中的函式
* 在編譯的時候非常簡單,只要-ldl即可
* 對於函式的介面要非常清楚(不然怎麼寫函式指標)

※ 總結

使用static library, shared library, dynamic loading library都各有好處,通常如果小程式的話製成static library就很好用了,編譯的時候-L. -ladlib就連好了不用考慮會找不到函式庫的問題,但是如果你的函式庫常常要改動的話,使用shared library比較好,因為透過ldconfig可以為你的soname自動建立符號連結,指向你的realname的函式庫檔,所以只要編好新的 shared library就可以換掉舊的,而且程式不用重編譯,但是麻煩在於如果要移到別台機器上,移動過去配置就要花上不少時間,有點麻煩。最後使用 dynamic loading這是個技術,只能用在shared library,讓你單獨用到某個函式庫中的幾個函式,但是麻煩的地方是,載入你全都要自已來控制,自己撰寫程式碼,透過dlopen, dlsym, dlclose等等來控制。
在C語言中看到最多的東西就是「變數」了,因為對於變數的觀念就變的很重要了,需要了解變數如何宣告,變數的範疇和儲存週期以及關鍵字static,這邊來先來談談C語言中,變數範疇和儲存週期有那些要注意的地方。

※ 一個變數的生命是由

1. 儲存期而定 (storage duration)
2. 範疇 (scope)

※ 其中儲存期可分成

1. 自動存在期 (automatic duration)
2. 靜態存在期 (static duration)
3. 動態存在期 (dynamic duration)

而所謂的範疇則是變數中有效的範圍部份,在block內的變數不可被外部參考,在未看到宣告前不能被用到。

1. 自動存在期 (自動變數)

自動變數是在程式執行時在區塊開始時候才產生出來,當區塊結束後,變數就被「收回去了」,等下次又執行到此區段才會又建立此變數。

* 一般是我們放在main() function()幫忙運算,或是for(int i = 0)所用到的變數。
* 有效範圍是從被宣告開始,到區塊的結束。
* 一開始不設定值的話,內容值會是堆疊中沒有用的資料,為一無用的值。
* 每次區塊重新執行會重新建立此變數,若有初值,每次建立會重新指定初值。
* 此資料存在堆疊之中,非資料段,故使用完,超過block會將裡面的資料清除。

2. 靜態儲存在期 (全域變數、靜態自動變數、字串常數)

全域變數、靜態自動變數、字串常數屬於靜態變數,意思是當程式在linking time時已配置空間了,故這此變數是存在資料區段,而這此變數會和程式同生共死,程式開始執行前已存在了。

a.全域變數 (global variable)

* 宣告在所有的function外面,當宣告後其scope是後面的函式都可使用。
* 變數和程式共存亡。
* 若沒對此變數初值的話,會自動初值為0。

b.靜態自動變數 (static automatic variable)

* 宣告為static的自動變數,會和程式共存亡。
* 只俱有區塊的scope。
* 同時俱有全域變數般的生命週期,又俱有區塊般的可見性。
* 若沒對此變數初值的話,會自動初值為0。
* 當希望某函式內的某個變數,下次呼叫時仍然保持它上次呼叫的值的時候,使用static來宣告成靜態自動變數。

c.字串常數 (array of n const char)

* 是一種特例,在程式執行前已建立此空間,所以不論在程式中以指標指向那個字串常數,都可取,但不可存,因為是const且在記憶體中是連續的記憶體空間。
* 類似靜態自動變數,但是只要有指標指向,就可以取得內容。
* 字串常數和程式共存亡。
* 不可以反參照修改字串,因為是const。
* char str1[] = "this is string"; 自動存在期,反參照可修改字串內容。
* const char* str2 = "this is string"; 靜態存在期,反參照不可修改字串內容。

3. 動態存在期 (以malloc所配置出來的空間)

又稱為動態記憶體配置,使用malloc來生出空間,free來還回空間,以此方式產生的空間是不受系統控制的,因為自己必須自行管理是否刪除。

* 生命週期為建立 (malloc) 後一直到 (free) 才會還回此空間,因此不管你是使用main()中來生出此空間,還是在function()中來產生空間,只要用指標就可以參考到。
* 「有借就有還」,要了多少空間就要還回去多少空間,否則會造成memory leak
* 最好統一在main() init()這樣的函式中產生,不要在任意的function()中產生,記憶體比較好管理。
* 每個程式都有一定的配置空間,如果使用後不還回去的話,空間會被吃光光,可能程式執行到一半,就會出現無法配置的錯誤,因為之前借了都沒有還回去。
* 不受區塊或是scope控制,只要指向的指標沒有丟掉就可以了。
* 指向此空間開頭的指標不能丟失,否則的話,沒有辦法用free()還回去,一定會造成memory leak。

變數週期表

變數範疇圖

變數週期圖

指標觀念

我想對於C語言指標的部份一定很多的初學者一直跨不過這個阻礙,因為我也是過來人,我想對於某些部份大家可能存在著同樣的問題,由網路上和自己測式研究發現了一些知識,而且可以以理論來解釋,所以大家不妨看看,我所介紹指標的部份,指標對於C/C++真的很重要,過你越過這個關卡,我想你才算是真的懂寫C語言吧!

※ 兩個運算子

「&」

* 取址運算子 ( 取得變數的記憶體位址 )
* 參考運算子 ( 參考傳遞使用 )

「*」

* 一般運算符號
* 取值運算子 ( 將指標變數中的內容位址取出真正的內容值 )

Example:

int x = 20; (將實值20存入x這個變數空間)
int* ptr; (宣告一個指標變數(ptr is point to int))
ptr = &x; (將x的記憶體位址放入ptr指標變數中)
printf("%d %p", *ptr, ptr); (印出ptr所指向的值和ptr指標內容值)

圖解:

※ int* ptr 宣告的意義

* 中文:ptr是一個指向整數型別的指標變數
* 英文:ptr is a pointer, point to a int variable

※ 解參考寫入資料 (deference)

Example:

int y; (建立一般變數y)
int* y_ptr; (建立一個int型別的指標變數)
y_ptr = &y; (讓y_ptr指向變數y的位址)
*y_ptr = 50; (透過解參考寫入資料50)

圖解:

※ 兩個重點

* y_ptr = &y 指標一定要指定某變數的位址否則就要指定為0(null),當此指標中的變數位址是有效的時候才可以解參考。
* *y_ptr = 50 利用「*」取值反向參考變數y來指定y的內容為50

※ 指標初始嚴重錯誤

int* ptr = 100 指標內容不可指定「常數值」必須為一變數的地址,如int* ptr = &a,所以大部份指標宣告的時候都會直接指定初值,以免解參考的時候出現錯誤,因為有可能會把指標變數當成一般變數來使用,指標變數和一般變數是完全不同的變數型別,所以指標變數是指標變數,而一般存值的變數是一般變數,千萬別混在一起了。

※ 指標遞增的意義

Example:

int a;
int* ptr_int = &a; (本來的ptr_int內容值是3310)
ptr_int++; (會變成3314)
char ch;
char* ptr_char = &ch; (ptr_char內容值是3121)
ptr_char++; (會變成3122)

因為指標型態不同,因此指標+1相當於記憶體位址+1(char), +4(int), +8(double)等等,依類型的不同有所不同。

※ 指標變數的加減比較

1. 指標內容可做加減法如:int* p = &a; p = p+1 (代表記憶體位址+4,因為型別為int)
2. 兩指標內容不可相加如:int* p, * q; p = p+q (不合法記憶體位址不能相加)
3. 兩指標內容可以比大小如:if (p > q) ... (得知那個指標變數內的記憶體先後)
4. 兩指標相減稱為差值運算,用來計算陣列中有幾個元素

※ 陣列指標間的關係

Example:

int array[] = {0, 1, 2};

1. array (陣列名稱) 等同於 &array[0] (陣列第一元素位址)
2. array[] 在compiler中會視為 int* const array (為一個無法指向別的空間的常數指標,一輩子只能指向array陣列開頭)
3. array[0] 等同於 *(array+0) , array[1] 等同於 *(array+1) ..... array[n] 等同於 *(array+n)
4. array++ (嚴重錯誤) 會是個error因為 array 陣列名稱是個「常數指標」是不可以改變內容值的,只可以唯讀 *(array+0) 以此類推。

※ 指標的權限

1. 指標可讀可寫,資料可讀可寫 int* ptr
2. 指標可讀可寫,資料唯讀 const int* ptr (無法透過反參考修改指向的變數內容值)
3. 指標唯讀,資料可讀可寫 int* const ptr = &x (指標建立時一定要給初始值,具只能建立一次,未來不可以修改此指標內容指向別的變數)
4. 指標唯讀,資料唯讀 const int* const ptr (同時具有2, 3點的功能)

※ 變數型態表示讀法(重要)

Example:

const int* const array[10];

先看 const array[10] 表示有一個 10 個元素的陣列,每個元素為const,再看const int* 表這10個元素每個都是const的int指標,表示所指向的資料不能修改。

圖解:
寫過C的人一定知道陣列和指標的關係密不可分,而對於初學者的話,這個部份常常是非常容易搞混的,因為指標的觀念不熟,或是陣列的觀念不熟,所以在這個部份,對於指標陣列分析一下,讓初學者可以清楚的明白,到底是存在什麼樣的關係。

※ 陣列的意義

array是同一型態的資料集合,記憶體內容是連續的

同樣的也可以是指向int型別的指標array

※ 二維陣列

深入的array就是二維的array了,其實在線性記憶體中並沒有類似向距陣那樣的儲存結構,故我們須要以一維的array來模擬二維陣列。

※ 幾個重點

* 從上圖中我們可以發現x[0] = &x[0][0]的位址,而x[1] = &x[1][0]的位址。
* 在二維中單取一列的話x[0],會取得第1列的起始位址,x[1]會取得第2列的起始位址。
* 當傳遞二維陣列的時候須要傳遞下標,除了第一個維度外,這是因為函式只知道線性記憶體,因此須要告訴函式,多少個col。

Example:

void print(int data[][3]); (3個col分成一個row)
void print(int (*data)[3]); (和上面完全相同)

* 一維陣列使用指標記憶體位址,即可指向、操做、解參考(dereference),那二維陣列該如何使用指標來指向呢?

Example:
一維做法

int x[10];
int* p = x; (指向)
p[2] = 100; (解參考)
++p; (操做)

二維做法

int x[3][2];
int (*p)[2] = x; (p是個指標,指向有2個int元素一維的陣列)
p[1][1] = 10;
p[2][0] = 100;

* int (*p)[2]其中的括號是必須要存在的,否則int* p[2]意思可是p是個陣列有兩個元素,每個元素都是一個指向int型別的指標。
* 只有使用二維的指標在函式中才知道線性記憶體是分成幾組,才可以讓你方便的使用x[1][1]來存取。

※ 使用指標來分析

若將int x[3][2]使用指標的方式來看的話,以上的圖我們可以很清楚的了解到x和x[0]和x[0][0]的關系,所以由以下的等式,更可以了解二維陣列到底意義在那

* x這個二維陣列名稱,表示著這是個位址,而且等於是x[0]的位址,也是x[0][0]的位址。
* x = &x[0] = &x[0][0]
* x[0]的內容值是個指標,所存放的是x[0][0]的位址。
* x[0] = &x[0][0]
* &x = x = &x[0] = x[0] = &x[0][0] 印出來的址相同,都是代表同一個位址
* *vip 上式不同的地方,在於型別
* &x 型別為 int (*)[3][2]
* x 型別為 int (*)[2]
* &x[0] 型別為 int (*)[2]
* x[0] 型別為 int*
* &x[0][0] 型別為 int*
* x[0][0] 型別為 int

Example:
你可以做個實驗,印出下面的值,看看位址多少

int x[3][2];
printf("%p\n", &x);
printf("%p\n", x);
printf("%p\n", x[0]);
printf("%p\n", &x[0]);
printf("%p\n", &x[0][0]);

* 由上面輸出的資料我們可以了解x[0]與&x[0]輸出是一樣的,就是x[0]的位址和它的內容值都是同一個位址。
* &x[0] = x[0] 差別在於「型別不同」(非常重要) 。
* 反正&x[0] = &*(x + 0) = x + 0 = x所以&x[0] = x就簡寫成x就好。
* 將一個指標或是記憶體位址加1,所增加的數值將會和所指向的物件型別有關。

Example:

x + 1 = &x[1] ; (row move列移動)
x[0] + 1 = &x[0][1]; (col move行移動)

「將x[0], x[1], x[2]當成一維陣列的起始位址」,則x[0][0], x[0][1]就是此一維陣列的第一個第二個元素

* **x = *(*(x + 0) + 0) = x[0][0] 第一列第一個元素資料
* 現在要取第二列第二個元素資料,有以下幾種取法

Example:

x[1][1]; (使用下標運算子給索引直接取值)
*(*(x + 1) + 1); (使用指標方式利用指標的offset取值)
*(x[1] + 1); (合併使用)

* 為什麼x + 1和x[0] + 1所指的結果是不同的呢?

Example:

x + 1 = &x[1]
x[0] + 1 = &x[0][1]

先來看看我們宣告是怎麼宣告的int x[3][2]英文解釋為x is an array of 3 arrays of 2 ints,拆解後x有3個arrays,因此將x + 1會指到第二個arrays,而x[0]是第一個arrays,而x[0] + 1則表示指向第二個元素。

※ 自我功力測驗

看了這麼久的指標和陣列,有一維的有二維的,來試試以下幾個題目,看是否都能以定義和觀念完全解意義所在

Example:

int n = 5;
double x;
int* pi;
double* pd;
pi = &n;
pd = &x;
pd = pi; (是錯誤的喔!因為型別不同不能相互指定)int* pi;
int (*pa)[3];
int ar1[2][3];
int ar2[3][2];
int **p2;
pi = &ar1[0][0]; (ok 解析:*(ar1 + 0) + 0)
pi = ar1[0]; (ok 解析:*(ar1 + 0))
pi = ar1; (是錯誤的喔!ar1是指向一個以3-int型態資料為單位的陣列)
pa = ar1; (ok)
pa = ar2; (是錯誤的喔!ar2是指向一個以2-int型態資料為單位的陣列)
p2 = π (ok 解析:指向指標的指標)

※ 拆解二維陣列以一維陣列仿二維

當我們傳遞一個二維陣列到一個函數的時候,我們都是傳一個地址,但是在函式那邊它看到的是什麼呢?就單單一個地址的話,那麼我們該如何存取第二個維度呢?所以傳遞陣列的時候,不止是傳遞一個位址,還需要傳遞它的「結構型別」和大小,才可以讓我們的函式來操做這個二維陣列。

今天我們宣告了int x[2][3],當以指標傳遞給函式的時候,有二種方法可以參考到正確資料。

Example:
方法1

print(x); (call)
void print(int (*p)[3]); (get)

方法2

print(x[0]); (call)
void printf(int* p); (get)
(兩者一樣)
print(&x[0][0]); (call)
void print(int* p); (get)

但是兩者看到的結構完全不同

故資料在存取的時候在(法1)中可像在main()中使用二維的下標運算子來存取資料,如要取得資料直接用p[0][1]列可存取第一排的第二個元素,但使用在(法2)可是完全錯誤的。

在(法2)中就像存取一維陣列一樣,要取得資料的話,直接*(p+0) 或是p[0],於於要印出資料,線性的找尋資料非常方便。

在呼叫print(x)和print(&x[0][0])或print(x[0]),意思果然不同,x表示此陣列結構和位址,而&x[0][0],只取出第二排第一個元素,純址,沒有結構。
若能善用指標的話,我想程式設計起來一定簡潔又明白,但是對於初學者而言,指標又偏偏的那個不好學,而且又常看別人程式出現了*ptr++等奇怪的東西,所以就來徹底的分析一下指標的遞增遞減,在看這篇前,請先對「指標的觀念」有明確的認知。

指標的遞增遞減(++, - -)其實也沒有什麼好怕的,就只是讓指標內容值,增加一單位的型別大小,如int* ptr;那ptr++的話會增加4bytes而同樣的,但是透過指標和陣列的組合,因為陣列資料是連續的,而就那麼剛剛好把指標遞增的話,剛剛好可以移動到下一個元素,所以指標才會那麼多人愛用,因為只要簡單的(++)就行了,但是問題來了,當你(++)過了頭,出了陣列的邊界的話,雖然不會怎麼樣,但是只要你有解參考(dereference)的話,嘿嘿你就等著「程式區段錯誤」吧!

※ 指標遞增與解參考

* *ptr++

1. *ptr 先解參考(dereference)
2. ptr+=1 指標指向下一個元素

Example:

int a[3] = {1, 2, 3};
int* ptr = a;
printf("%d\n", *ptr++); (output 1)
printf("%d\n", *ptr); (output 2)

* *++ptr

1. ptr+=1 指標指向下一個元素
2. *ptr 後解參考(dereference)

Example:

int a[3] = {1, 2, 3};
int* ptr = a;
printf("%d\n", *++ptr); (output 2)
printf("%d\n", *ptr); (output 2)

* (*ptr)++

1. *ptr 先解參考(dereference)
2. data+=1 遞增運算對指向的變數遞增,就是把ptr所指向的那個變數加1

Example:

int a[3] = {1, 2, 3};
int* ptr = a + 2;
(*ptr)++;
printf("%d\n", *ptr); (output 4)

* ++(*ptr)

和(*ptr)++效果一樣,但是最大的不同就是寫在輸出值的時候

Example:

printf("%d\n", (*ptr)++); (輸出未遞增的變數內容,後效用)
printf("%d\n", ++(*ptr)); (輸出已遞增的變數內容,立刻效用)

※ 範例參考

Example:

int data[2] = {100, 200};
int moredata[2] = {300, 400};
int* ptr1;
int* ptr2;
int* ptr3;ptr1 = ptr2 = data;
ptr3 = moredata;
printf("*ptr1=%d, *ptr2=%d, *ptr3=%d", *ptr1, *ptr2, *ptr3);
printf("*ptr1++=%d, *++ptr2=%d, (*ptr3)++=%d", *ptr1++, *++ptr2, (*ptr3)++);
printf("*ptr1=%d, *ptr2 = %d, *ptr3=%d", *ptr1, *ptr2, *ptr3);結果是:*ptr1=100, *ptr2=100, *ptr3=300
*ptr1++=100, *++ptr2=200, (*ptr3)++=300
*ptr1=200, *ptr2=200, *ptr3=301
在C語言中,寫一個大型的程式一定不可能只單單的使用一個main()的函式就把整個程式寫完,最重要的就是將大型的程式切成一個模組一個模組,在將各個小問題的模組一個一個解決,這樣的程式看起來又有架構,可讀性又高,但是通常初學者最常問的問題就是陣列傳到副程式,或是指標傳到副程式等等的問題,如此就來分析一下,所有可以傳遞到副程式的寫法和觀念。

※ 函式的基本概念

* 當寫一個函式的時候都需要一個「宣告」(prototype)
* 函式分成三個部份「函式名稱」「傳入值」「回傳值」

* 函式傳入值有「傳址」(pass an address by value)「傳值」(pass by value)
* 呼叫函式方傳入值叫「實際參數」,而副函式接受值叫「形式參數」
* (重要) 傳入的變數是什麼型別,副函式就要以什麼型別去接受
* (超重要) 不可回傳函式中建立的指標,這邊講清楚,傳「指標」代表傳這個指標的內容值,就是所謂的「記憶體位址」

以上是最基本函式的觀念,以上這些名詞請一定要記熟,而且最重要的就是「呼叫怎麼傳入,函式怎麼接受」,而不可以回傳在函式中所建立的指標,若回傳malloc得到的位址的話例外。

※ 記憶體配置邏輯圖

* 一個程式運作的時候有三個記憶體儲存區分別為Global、Stack、Heap
* Global 是拿來存放「與程式共存亡」的變數,如全域變數、字串常數、static的變數等
* Stack 是拿來存放「離開block後會消失」的變數,如在函式中(包含main())的變數
* Heap 是一塊空間,讓你在程式執行時,建立變數(malloc)所存放的資料空間
* 如果不了解的話,請參考本Blog中的「變數生命週期」
* 而我們把變數傳給函式的時候,都是把這些資料存放在Stack中,Stack中的變數有個特性,當你離開這個block的時候或是離開函式的時候,Stack中的值,就會變的無效值 (非常重要)
* 所以函式可以回傳值,但是不可以回傳指向這個Stack某塊記憶體的位址,因為解參考(dereference)的時候會錯誤,因為資料早就不存在了

※ 解析型別

有了以上的概念之後,開始進入我們如何傳遞一個變數或是陣列或是指標給函式,但是還要做一些「型別」認知的訓練。

Example:

int x; //x is int
(x型別為「int」)
int* x; //x is pointer, point to int
(x型別為指向int型態的指標「int*」)
int x[10]; //x is an array of 10 ints
(x型別為指向int型態的常數指標「int* const」)
int x[10][10]; //x is an array of 10 arrays of 10 ints
(x型別為指向具有十個元素,每個元素為int,的指標「int (*)[10]」)
int x[10][10][10]; //x is an array of 10 arrays of 10 arrays of 10 ints
(x型別為指向具有十個元素,每個元素為十個元素,每個元素都為int,的指標「int (*)[10][10]」)
char buf[10]; //buf is an array of 10 chars
(buf型別為指向char型態的指標「char*」)
const char* buf = "test"; //buf is an const pointer
(buf型別為指向char型態的常數指標「const char*」)

注意上面所有解析都是只單對這個變數代表的意義所解析的型別,如int x[10]雖然是個陣列,但是x代表是這個陣列的起始位址,所以這個位址的型別是int*依此類推。

※ pass by value V.S. pass an address by value

Example:

void write(int* tmp) (傳址呼叫函式)
{
*tmp = 20;
}
void show(int tmp) (傳值呼叫函式)
{
printf("%d\n", tmp);
}
int main()
{
int a = 10;
write(&a);
show(3);
return 0;
}

當主程式中呼叫副程式的時候會依順序執行下列動做:

1. 呼叫函式時把變數內容或是常數或是地址(實際參數)複製到Stack中
2. 主控權移交到函式手上
3. 函式的傳入值(形式參數)對映Stack中變數內容或是常數或是地址
4. 型別的契合(重超要), 可能會有隱含的資料轉型,就是因為型別的不契合才會自動轉型,或是記憶體存取錯誤就是因為指標的型別不契合,所以型別要正確
5. 執行遇到return時主控權移交回它的呼叫者手上
6. 函式執行結束後Stack內容變成無效值,剛剛對映Stack中的形式參數已變成無效用,但不一定會清除

重點整理

* 其實傳值呼叫和傳址呼叫都是把資料考到Stack中,只是一個是「值」一個是「址」
* 為什麼傳陣列的時候要傳「址」,為的是不浪費Stack空間且浪費複製時間
* 什麼時候用到傳值,用到傳值只為的是單一運算結果,如a+b+c回傳的只有一個答案
* 什麼時候用到傳址,用到傳值是了要修改不止一個運算結果,如將陣列每一個元素都+1,用傳值是沒有辦法做的
* 因為函式結束後Stack內容會無效,所以不可回傳Stack中某個變數的指標給呼叫者解參考(超重要)
* 在函式中使用malloc要到一塊記憶體,是從Heap中配置的空間,因此可以回傳此指標,因為和Stack沒有關係
* 使用malloc在函式中配置的記憶體空間,若沒有free回去的話,那一定要回傳此指標,讓呼叫者對此空間控制,否則遺失指標會造成memory leak

※ pass by value 的運作

由此圖中我們可以了解pass by value是如何運作的,也可以了解到return值對pass by value是多重要,若是沒有return tmp的話,那這函式做的一切都是徒然,做了白工,而且只能回傳單一值。

※ pass an address by value 的運作

由此圖中我們可以看到使用pass an address by value可以一次更改兩個變數的內容值,而且不需要return任何資料,所以需要更改一個值以上的資料我們必需要使用到傳遞指標讓函式來間接的幫我們修改資料內容。

※ 各式各樣的函式傳遞

* int (pass by value)(單一數值)

Example:

int add(int a)
{
return a+1;
}
int main()
{
int b = 1;
int data = add(b);
}

* int* (pass an address by value)(單一指標)

Example:

void add(int* a)
{
*a += 1;
}
int main()
{
int b = 1;
add(&b);
}

* int* (pass an address by value)(一維陣列)

Example:

void init(int* ptr, int size)
{
int i;
for (i = 0; i < size; ++i)
{
ptr[i] = 0;
}
}
int main()
{
int x[10];
init(x, 10);
}

* int (*)[10] (pass an address by value)(二維陣列)

Example:

void init(int (*ptr)[10], int size)
{
int i, j;
for (i = 0; i < size; ++i)
{
for (j = 0; j < 10; ++j)
{
ptr[i][j] = 0;
}
}
}
int main()
{
int x[10][10];
init(x, 10);
}

* char* (pass an address by value)(常數字串)

Example:

char getlastch(const char* p)
{
while (*p)
{
++p;
}
return *--p;
}
int main()
{
char ch = getlastch("address"); (輸出s)
}

* int** (pass an address by value)(雙指標)

Example:

void init(int** ptr, int size)
{
int i;
for (i = 0; i < size; ++i)
{
ptr[i] = (int*)malloc(sizeof(int) * 10);
(又將每個int*配出十個大小的int陣列)
}
}
int main()
{
int** x = (int**)malloc(sizeof(int*) * 10);
(配出int* x[10]的空間)
init(x, 10);
}

* char** (pass an address by value)(字串陣列)

Example:

void show(char** char_ary, int size);
int main()
{
char* ary[MAX] = {
"one",
"two",
"three"
};
show(ary, MAX);
return 0;
}
void show(char** char_ary, int size)
{
int i;
for (i = 0; i < size; ++i)
printf("%s\n", char_ary[i]);
}

在函式的傳遞中,對於型別的判定是非常重要的,傳遞實際參數不論是「值」或是「址」最重要的是,要非常了解這個「址」的型別,是二維的還是一維的,這都非常的重要,因為C語言在函式傳遞的時候,若發現你的型別不符合的話,會想辦法幫你自隱藏的型別轉換,所以要特別注意,傳遞時的型別,否則到時候指標加加減減解參考到錯誤的空間那你程式就準備當掉了,所以指標的快速好用,也帶給程式設計者一項很重要的任務,就是參考到的記憶體位址必須要完全正確才行。
通常傳遞一個陣列的時候,因為需要告訴函式,需要多少個col分成一組,也就是說要告訴它,除了第一個維度以外,所有維度的下標,但是C99支援了一種可傳遞任何陣列大小的方法,稱為VLAs(變動長度陣列)。

※ 適用Compiler

* gcc 3.4.2 (或更高)
* Dev-C++
* Mingw

※ 一般用法

Example:

#define COL 4
void show(int (*ar)[COL], int size) (size是指row大小)
{
int i, j;
for (i = 0; i < size; ++i)
{
for (j = 0; j < COL; ++j)
{
printf("%d", ar[i][j]);
}
}
}
int main()
{
show(array1, 5);
show(array2, 100);
return 0;
}

以上是我們最常見傳遞二維陣列的用法,此在函式傳遞有深入的介紹,看不懂的可以去看看那篇文章,這只提一下而已。

※ 使用VLAs

Example:

void show(int row, int col, int ar[row][col]); (VLAs的宣告)
int main()
{
show(3, 2, array1);//3 x 2維度
show(100, 5, array2);//100 x 5任意維度
return 0;
}
void show(int row, int col, int ar[row][col])
{
int i, j;
for (i = 0; i < row; ++i)
{
for (j = 0; j < col; ++j)
{
printf("%d", ar[i][j]);
}
}
}

由此可以看見VLAs的函式宣告也和一般不同,但是因為可以傳遞任何的維度,真的是非常好用,但是也有一些限制。

* VLAs必須是自動儲存類別,也就是說,它必須在「函式」裡面宣告,或是作為函式的「形式參數」
* 不可以在宣告時,對它進行初始化
* 一個陣列若宣告在函式外面,則為static且和程式共存亡
* 一個陣列有初值的話,也是為static且和程式共存亡
* VLAs宣告的順序必需要先將大小的變數宣告在前,而陣列宣告在後,如int a, int b為長和寬,而int ar[a][b]則為這個陣列的長寬設定。
* VLAs的宣告並不會實際產生一個陣列,而只是一個「指標」而已

※ 執行期配空間

Example:

void init(int a, int b, int ar[a][b]);
void show(int a, int b, int ar[a][b]);
int main()
{
int n1;
int n2;
scanf("%d%d", &n1, &n2);
int ar[n1][n2]; (執行的時候才決定二維大小)
init(n1, n2, ar);
show(n1, n2, ar);
return 0;
}
void init(int a, int b, int ar[a][b])
{
int i, j;
for (i = 0; i < a; ++i)
{
for (j = 0; j < b; ++j)
{
scanf("%d", &ar[i][j]);
}
}
}
void show(int a, int b, int ar[a][b])
{
int i, j;
for (i = 0; i < a; ++i)
{
for (j = 0; j < b; ++j)
{
printf("%d ", ar[i][j]);
}
printf("\n");
}
}

VLAs可以當成像是malloc動態配置空間使用,但是需要符合以上的求要,雖然看起來好像和malloc一樣,都可以配置動態的記憶體空間,但是兩個儲存的空間卻是完全不同的,傳統的陣列都是存在Global的空間,而VLAs存在Stack中,而malloc則是存在Heap中,所以生命的週期自然就不相同。

字串觀念

我想寫過其它語言如C#, Java這樣語言的人都用過String這個dataType但是在C語言這種比較低階一點的語言是沒有所謂「字串」這種東西的,而它在C語言裡面又是非常重要的一環,因此許多初學者在C中使用字串雖然是會用,但是不了解背後真正C字串的意義,因此想分享一些我對C style String的見解。

※ 字串在C中的意義

在C語言之中,只有字元,並沒有所謂的字串,故字串稱為字元陣列,只要是陣列中一排英文字的後面有個'\0'的字元就稱為「字串」

Example:

char str[] = {'a', 'b', 'c', '\0'}; (str為一字串,其字串為abc)

在C中字串可分為兩種型式(非常重要)

Example:

const char* str = "test1"; (指標字串)
char str[] = "test2"; (陣列字串)

※ 字串常數

所謂的字串常數是在你程式中只要有寫到"abc"等等的字串表示如此稱為字串常數。

Example:

#define MAX "max";
const char* str = "str";
printf("hello world!!");

只要用""裝起來的就是字串常數

但是其中有一點是例外的,就是程式中用字串常數初始一個字元陣列的時候。

Example:

char str[] = "hello"; (使用字串常數初始一個字串陣列)
char str[] = {'h', 'e', 'l', 'l', 'o','\0'};

以上兩種表示方式意思是完全相同的,只是寫"hello world"會比較方便,會在使用compiler幫你編譯的時候自動轉換。而在字串常數中有幾個重點。

* 字串常數一旦建立,在程式執行中不可被修改
* 不可以透過指標解參考(dereference)修改某個字元
* 生命週期和程式共存亡
* 指向字串常數的指標,如果被指向另一個字串常數或是字串陣列,或是任何的記憶體位址後,就永遠沒有辦法在指向本來的字串常數,因為已丟失字串常數的位址

※ 字串陣列

只要是使用陣列來存字元,而且後面加了個'\0'的字元的,在C中就認定是字串。

Example:

char str[] = {'a','b','c','\0'};
char str[] = "abc";

字串陣列就如同一般陣列一樣可以修改其內容值,可透過指標解參考(dereference)修改或是透過下標運算子([])來修改其中的某個字元。而在字串陣列中有幾個重點。

* 看過陣列篇的都知道陣列名字是代表陣列的起始位址,而且是不可以被修改的,因此想要對str做遞增、遞減運算都不行,因為它不是一個指標
* 可以透過指標或是下標運算來修改其中某個字元,但是不可以把空字元'\0'丟失,否則字串陣列就成為單單只是存字元的陣列而已,不具有字串功能
* 生命週期和宣告在那裡有關

※ 字串常數和字串陣列的不同之處(非常重要)

Example:

char str[] = "test1";
char* str = "test1";

以上兩行程式,是否可以分辨出「字串常數」和「字串陣列」,如果分不出來,可以到前面重看了,如果分的出來,那麼他們到底有什麼不同呢?

儲存的空間不同

char str[] = "test1";就像陣列一樣,陣列宣告在那裡,就有那個地方的儲存性質,寫在全域,就是一個靜態陣列,會和程式共存亡,但是如果寫在main()裡面的話就是自動變數,所以當程式離開main()的時候,這個字串陣列就會消失,完全是看你在什麼地方宣告它而定,就如同使用一般陣列一樣。

char* str = "test1"; 在前面有提過test1稱為字串常數,它是在compiler的階段會在Global區段就建立好一個存放test1這個資料的空間,所以程式執行時,不論你在任何地方取得這個字串都是有效的,因為它會和程式共存亡,所以有一個很重要的一點就是「只可以讀,不可以修改」字串內容。

型別的不同

char str[] = "test1";解析str表示陣列的起始位址,那str的型別為char*,看過函式傳遞那篇的人應該很清楚。char*表示著,指向的指標,可讀可寫,可移動。

char* str = "test1";解析str表示恉向Global中test1字串的資料區段起始位址,但是由於我們前面說過,它是「只可以讀,不可以修改」,因此我們要修正這個指標為const char*,如此才完全正確。const char*表示著,指向的指標,只可以讀,不可以寫,指標可移動。

※ 字串常數和字串陣列的相同之處(非常重要)

* 兩者都可以透過*(string+i)或是string[i]來取得字串中的某個字元
* 不論是字串常數或是字串陣列,最後一個字元一定是要空字元'\0'

※ 使用sizeof運算子計算大小時的錯誤

初學者一定想過使用sizeof來計算字串大小,因為這樣可以把連'\0'這個空字元都可以算進去,如此就可以知道字串共用了多少個字元了。

Example:

char str[] = "hello world";
printf("%d\n", sizeof(str));

如此可以達成你的目的,可以知道str這個字串包含空字元一共有多少個字元,但是如下範例。

Example:

const char* str = "hello world";
printf("%d\n", sizeof(str));

算出來的結果為什麼等於4呢?而且不管怎麼算就是4,其實因為你是算了它的指標大小,而不是真的算到共用了多少的空間,指標在win32系統上都是 4btye所以不管是什麼指標使用sizeof()運算出來都是4,因此想要知道字串常數共有多個字元的話,請使用strlen(str)+1加1是為了空字元,同樣的也適用於字元陣列。

※ 字串陣列溢出

Example:

char str[3] = "abc";
char buffer_str[5];
scanf("%s", buffer_str); (輸入hello world)

由此範立一看就知道是錯的,為什麼呢?因為我們的字串超過字串陣列,可以容納的值,你和記憶體要的空間不夠放你的資料會造成「改到其它的資料」,C 是個很低階的語言,在程式中要了3個char的空間,但是你卻放了abc還有空字元,所以那個空字元會覆寫記憶體中其它變數的區段,造成程式不穩定或是當掉,而buffer的情況也是一樣buffer不夠使用者的輸入,會當成記憶體被新輸入的資料覆寫,所以我們的scanf()需要看buffer的大小來定訂可輸入的資料大小,要改寫成。

scanf("%4s", buffer_str);

為是什麼是4不是5呢?原來輸入的字串需要含有'\0'這個空字元的空間,所以字串的邊界條件是很重要的,如此別人資料輸入在多也沒有用,因為只會讀入4個字元,和自動加上去的空字元,剛剛好符合我們訂的大小。

當一個C語言的程式設計師,要非常的細心,因為指標之所以不穩定,就是因為邊界計算錯誤,會存取或是覆寫到別的記憶體空間,但是如果可以了解觀念後善用的話,C語言是個非常快速和低階的語言,因為可以直接存取記憶體,因此在C中所有的邊界判斷都要自己來,這是辛苦的地方。

※ 字串陣列和字串常數的使用時機

字串陣列

* 當成用scanf讀入字串的buffer來使用,通常buffer我們會設大一點,必避資料輸入超過會造成程式當掉

字串常數

* 當成輸出文字來使用,比如說要出印一個選單,就使用字串常數

※ 字串的輸出

Example:

const char* str1 = "test"; (字串常數)
char str2[] = "test"; (字串陣列)
printf("%s\n", str1);
printf("%s\n", str2);

這樣就可以輸出字串了,但是有沒有想過一個問題就是,明明str1是個指標,而且str2也是一個常數指標,那為什麼本來輸出應該是一個地址才對阿,為什麼是輸出一個字串呢?原因有兩點,一點是「%s」另一點是「'\0'」,在printf()中%s它就會把這個地址視為要印出字串,所以他就會不斷的使用*str++把資料印出來,直遇到\0為止,因此我們觀念可以連貫起來,為什麼字串最重要的就是要有'\0'這個空字元的關係了,所以空字元是整個字串的命脈。

※ 字串的輸入

Example:

char buffer1[];
char* buffer2;
scanf("%s%s", buffer1, buffer2);

有人一定會這樣寫,為的是希望讓程式輸入資料的時候,看資料大小來設定陣大小,或是建立字串常數,這想個想法雖然是很好,但是程式有分成編譯階段和執行階段,現在的情況是在執行階段,但是空間配置是在編譯階段,而我們在編譯階段並沒有和系統要多大的空間來暫存,而在執行期的時候,又要把資料放到「不存在」的資料空間,這樣程式一跑起來就可能會當掉,因為根本沒有配置空間,如果想要在執行期配置空間的話,需要用動態記憶體配置malloc來實作。

因此最重要的部份就是要讓一個buffer1可以存資料的話,一定要預先配好足夠的大小,而buffer2只是個指標,是個完全的錯誤,在此就不討論了,因此範例應該改成。

Example:

char buffer1[80];
scanf("%79s", buffer1);

配置了80個字元的空間,還需要使用%79s才算完全正確,為了怕使用者輸入超過80個字元造成記憶體可能會被覆寫,所以要限定使用者輸入後可取入的大小。

※ 字串的陣列

這裡談到的是字串「的」陣列,和字串陣列(用字元陣列接成的字串)意思不同,請別搞混,所謂的字串的陣列,是一個陣列中裝了很多常數字串。

常數字串的陣列

Example:

const char* str_array[] = {
"today",
"yesterday",
"tomorrow"
};

str_array陣列之中放著3個字串常數

字串陣列的陣列

Example:

char str_array[][20] = {
"today",
"yesterday",
"tomorrow"
};

str_array陣列之中放著3個字串陣列

對於字串的陣列部份,就是字串+陣列,所以如果了解陣列的話,那兩者配合起來沒有什麼問題,兩三分鐘就可以完全了解了,但是如果看不太懂以上的寫法的話,那陣列的部份可能就要仔細的去看看觀念了。
一般來說寫到目前的程式好像都只有單一個檔案,而使用到基本的stdio.h這個標頭檔,但是往往很多人都不知道標頭檔真的意義是要幹麻的,而C程式也不是說寫一個大型的專案用一個檔案就寫的完的,所以在這邊會講解使用多個檔案組合成一個大型的程式,而檔案和檔案之間是如何共用或是傳遞變數,以及如何使用標頭檔。

要完全了解如何將檔案和檔案之間的變數傳遞或是共用變數的話,那要先了解何為「內部連結」和「外部連結」,其實這屬於變數的儲存週期。而談到為什麼需要使用標頭檔的話,那就先必須了解「宣告」和「定義」有何不同,最後才真的講到主要講的東西,就是如何使用標頭檔和程式檔,串成一個大型程式的基本架構。

※ 連結性(linkage)

一般在C中的變數都會有自動儲存期或是靜態儲存期或是由malloc配出來的動態儲存期,基本上連結性只和全域變數有關係,我們知道全域變數是靜態儲存期,這沒有問題,但是怎麼知道全域變數是什麼連結性的呢?

* 靜態儲存期外部連結性
* 靜態儲存期內部連結性
* 靜態儲存期沒有連結或

※ 靜態儲存期外部連結性

Example:

int data; (全域變數,且具有外部連結性)
int main()
{
....
}

最簡單的最普通的全域變數就具有外部連結性,那何為外部連結性呢?就是說這個變數可以跨檔案之間共享,也就是你在這邊的變數data可以被別的檔案使用。其實透過我們的編譯,會將上面的程式碼翻成。

Example:

extern int data = 0; (定義一個外部連結變數data為0)
int main()
{
....
}

其實int data;在編譯器眼中只是個「暫時定義」的宣告而已,而int data = 0;才是真的定義,但是一旦在編譯時期沒有定義,只有暫時的定義的話,那麼所有的暫時定義都會被合併成為一個定義,且設定初值為0,所以說「暫時定義」可以有很多個,只要型別和識別字一樣的話。所有的暫時定義宣告,如果沒加上static(內部連結性)那麼就是外部連結性,在編譯期間,都會翻成如下範例。

Example:

int data; (暫時定義宣告)
int main()
{
....
}

翻譯成為

extern int data = 0;
int main()
{
....
}

※ 靜態儲存期內部連結性

Example:

static int data; (全域變數,且具有內部連結性)
int main()
{
....
}

要讓一個變數只具有內部連結性的話,在全域變數加上一個static的修飾字就成為內部連結性了,那何為內部連結性呢?就是說這個全域變數只可以被這個檔案使用,不具有在幾個檔案之間共同分享的功能,因此算是一個檔案私有的全域變數。

※ 靜態儲存期沒有連結性

怎麼還有那種叫沒有連結性的阿,其實這就是它不是一個全域變數,因為只有全域變數才會有內外部的連結性,那這東西是什麼,就是你在函式中用到的靜態儲存期變數。

Example:

int a()
{
static int data;
}
int main()
{
int a();
}

所以只要記住不是全域變數以外的變數只要有靜態存在期的那種就是沒有連結性,沒有連結性代表這個變數沒有辦法被別的檔案所共享,而且此變數也不是在本檔案中任何地方都可以存取的到,因為會受到範疇的影響。

※ 透過外部連結性讓別的檔案參用到同一變數

Example:

file a.c

int a; (暫時定義變數具外部連結性)
static int b; (暫時定義變數具內部連結性)
const int c = 100; (定義常數變數具有外部連結性)

file main.c

extern int a; (參用宣告)
extern const int c; (參用宣告)
int main()
{
printf("%d%d", a, c);
}

在程式中,比較不懂的應該是extern到底是在幹麻的,extern這關鍵字,其實表示「外部連結性」的意思(其實可加可不加,因為如果是暫時定義宣告,最都會被翻成extern),而代表後面跟著的宣告其實是參用某個檔的外部連結性的變數,而不是真的在此宣告一個變數a或是cosnt c,而是告訴連結器說,曾經有個int a和const int c在程式中某個檔案有定義,請去參用它。

其實在只要是外部連結的變數,不管你在那裡宣告,對於所有的檔案此變數都可以共用,那幹麻在每個檔案裡面都要多加個宣告,阿不是在a.c檔中已宣告了,其它的檔都可以用,那幹麻其它的檔還要在宣告呢?主要就是編譯的時候需要讓compiler知道有這樣的一個變數,而變數共享的話(外部連結)是 linker要做的事情,所以下面會提到為什麼宣告可以很多次,但是定義只可以有一次,只是宣告很多次只是讓compiler認識這一個變數而已,共享完全是要看你變數是不是外部連結性。

※ 全域變數定義(definition)和暫時定義(tentative definition)的不同

常常有人把全域變數的定義和暫時定義完全的搞在一起,其實暫時定義和定義是完全不同的,暫時定義是不會配置記憶體空間的,但是定義會配置空間。

Example:

int i; (暫時定義宣告)
int i = 0; (定義)

雖然看上去沒有什麼不同,但是暫時定義和定義有以下幾點不同:

* 在同一編譯單元內,可為同一識別字作數個外部連結性的宣告,但是型別和連結性要一致。
* 如果全域變數帶有初值的話就是個定義了(extern int a = 0)。
* 如果全域變數沒有extern修飾字或是初值的話,就是一個「暫時定義宣告」(int a)。
* 可以很多個暫時定義,但是只能有一個定義(int a = 0)。
* 如果存在一個定義的話,那所有的暫時定義都會當做是多餘的宣告。
* 假設在編譯單位中沒有定義的話,所有的暫時定義都會變成設定初值為0的定義,且併合在一起,成為單一的定義。

所以流程如下:

1. 尋找是否有外部連結的變數已定義了。
2. 如果有定義:所有的暫時定義宣告被乎略。
3. 如果沒定義:所有的暫時定義合併成為一個定義,且被翻成初值為0的定義。

所以如果你寫成這樣,暫時定義很多次,只定義一次的話,編譯是不會出錯的。

Example:

int i; (多餘宣告)
int i; (多餘宣告)
int i = 0;

如此是可以的喔!因為可以暫時定義很多次但只定義一次。

在多檔案之中定義和宣告就非常有關係了,在某個檔案中定義,或是在某個檔案中處理這變數的值,但是又可以在另一個檔案中參用讀取修改這個變數值,所以「定義」一次,在別檔案「參用」。

Example:

extern int a; (參用宣告)
extern const int c; (參用宣告)

※ 推廣外部連結性和內部連結性

既然連全域變數都有內外部的連結性,那函數呢?這當然是沒有問題的,因為函式什麼都不修飾的情況下,就具有外部連結性了,和全域變數什麼都不修飾的情況下一樣。那是不是加上static就成了內部連結性了呢?這是完全正確的。

Example:

void show() (函式具有外部連結性)
{
....
}
static void print() (函式具有內部連結性)
{
....
}
int main()
{
show();
print();
}

那一樣的,我如果想讓本檔案去參考到另一個檔案我所寫的函式的話該怎麼做呢?

Example:

file a.c

void show() (具有外部連結性的函數)
{
....
}
static void print() (具有內部連結性的函式)
{
....
}

file main.c

void show(); (函式的參用宣告)
int main()
{
show();
}

對於函式的參用宣告可以加上修飾用extern也可以不加,因為外部連結的函式宣告本來就具有跨檔案的能力,所以如果加了extern可以讓你更清楚你函式是在別的地方定義的,而你這邊使用只是「參用函式」而已,如此就可以讓本檔案的程式去參用另一個檔案中的函式,但是一樣套上面的話,如果寫一個多檔案組成的程式,如果函式不是讓整個程式都可以使用到的話,請宣告和定義成static讓範疇只具有檔案範圍,避免手忙腳乩之中,在此檔案定義了一個 a()函式,在另一個檔案中又定義一次a()函式,這樣連結器就會傻掉,不知道要去跑那個函式了。

※ 標頭檔

幾乎程式的第一行我們寫的都是#include 應該會有人不知道為什麼要這樣寫,其實.h(標頭檔)主要的目的就是提供一個「介面」,所謂的介面意思為裡面宣告了滿滿的函式宣告以及一堆的常數,讓你在程式設計的過程式,不用去寫一些很麻煩的底層的程式,像是printf()就有宣告在stdio.h裡面,所以我們只要拿來使用就行了,而不用自己寫,在編譯的過程之中,編譯器會只會去看你函式使用的格式對不對,或是有沒有語義上的錯誤,而真的把函式庫和我們寫程式串起來是連結器的工作,總之一個大型的程式可以分成。

* 介面(.h的標頭檔)
* 介面實作(.h的實作檔)
* 主程式

簡單用個小小的程式把上面所講的「介面」、「介面實作」、「主程式」串起來。

Example:

file interface.h

extern int a;
extern const int b;
void show();
void print();

file interface.c

#include
#include "interface.h"
int a;
const int b = 100;
static int c;
static int get()
{
....
}
void show()
{
get();
....
}
void print()
{
....
}

file main.c

#include
#include "interface.h"
int main()
{
printf("%d%d", a, b);
show();
print();
}

由此我們可看見我們把個一個程式拆成了三個部份了,而主程式看起來非常的簡單,只需要呼叫而已,其它的工作都是在模組裡面都完成,而且 interface.c裡面含有隱藏的內部函式是私用的,在main.c裡面完全不知道有這個函式,只需要知道看「介面」檔裡面怎麼宣告,就怎麼使用,根本連管它怎麼做的都不知道,就好比我們使用printf()的時候,只要記得加入stdio.h標頭檔和如何使用printf()這個函式就夠了,剩下的事情實作檔會幫你做好的。而且具有重覆使用性,如果那天要在開發另一個程式的時候,將自己最常使用的函式做成「介面」「介面實作」,那以後程式開發,也可以如同使用stdio.h一樣方便的應用以前寫過的函式。
一個程式中字串處理是最重要的,往往因為字串處理的關係造成程式當掉,或是用指標取得的資料不是你所要的,又或是邊界條件的關係,讓你Debug搞了好久,只是錯在一點點小地方,後面我會加上自己修正stdio版本的函式,讓使用更方便。

※ scanf從鍵盤輸入中格式讀取資料

我想scanf()應該大家最常用到的輸入資料的函數了,所以這邊就不多講了,但是它卻具有很特別的妙用,scanf是格式化輸入,所以可以你訂出來的格式,別人套用你的格式來輸入資料。

scanf中空格的意義

使用%s%d%f等等的格式輸入,在讀到目標之前有遇到空白或是TAB或是換行字元,都會自動略過,直遇到整數或是字串或是浮點數,但是如果是用%c的話,那是讀取stdin中第一個字元,那如果是空白列,那可就讀到空白了,因此可改成scanf(" %c")表示要讀取一個字元之前,自動略過前面有空白或是TAB或是換行的字元。

scanf中的略過「*」和集合「[]」

* %*s (跳過讀入一個字串)
* %*d (跳過讀入一個數值)
* %[] (讀取字串,但只讀取包含於集合中的字元)
* %[^] (讀取字串,直讀到包含於集合中出現的字元)
* []的正規表示式,可用「-」來表示從那裡到那裡
* %[a-z]讀取字串中的字元只含a-z的字元,若讀到別的字元或是數字就停止
* %[^0-3]讀取一字串,直讀到遇到0123字元的出現就停止讀取。
* 程式回傳讀入成功的數目

Example:
輸入2007/6/6要取得年月日的話

if (scanf("%d%*[ \n\t/]%d%*[ \n\t/]%d", &a, &b, &c) == 3)
{
printf("year: %d, month: %d, day %d", a, b, c);
}

or

scanf("%d %*c%d %*c%d", &a, &b, &c);

其中%*[ \n\t/]指跳過「空白」「Tab」「換行」「/」這些字元,或是利用scanf中的空白字元,讓讀入自動會跳過空白或TAB或是換行。

由上程式我們可以由自訂的格式輸入來把year, month, day全都讀入,而別人如果沒有造你的格式輸入的話,資料回傳就不會剛剛好等於3,那就是代表資料輸入有錯誤。

Example:
輸入(13, 22, 41)要把數字取出來的話

scanf("%*[ \t\n(]%d%*[ \t\n,]%d%*[ \t\n,]%d%*[ \t\n)]", &a, &b, &c);

仔細的分解一下,就漸漸可以了解這種格式輸入要怎麼活用,如果可以活用的話,那取資料的時候就可以大大減少自己在那邊拆來拆去,拿出自己要的東西了。

※ fgets從鍵盤一次讀入一行

本來應該使用gets的,但是因為gets沒有長度限制,所以你的string buffer要是不夠大的話,輸入只要超過你的buffer那資料就會覆寫到別得資料空間,所以說是非常可怕的,因此使用fgets,可以限定輸入的資料不可以超過你的buffer大小,fgets還有些要注意的。

* 只會讀到n-1個字元,或是讀到'\n'(換行字元)就停了
* 輸入超過buffer大小時,結尾不含'\n'
* 輸入不到buffer大小時,結尾自動會補上'\n'

Example:
修正fgets讓不管是輸入超過或是輸入不到,都不會有換行字元

char* fix_gets(char* buf, int num, FILE* fp)
{
char* find = 0;
fgets(buf, num, fp);
if (find = strrchr(buf, '\n'))
{
*find = '\0';
}
else
{
while (fgetc(fp) != '\n');
}
return buf;
}

如此我從fix_gets讀入到buffer中的資料不會再有的時候有'\n',又有的時候沒有'\n'的問題了,而且也不用管它怎麼做,只要把 buffer的大小告訴它,就自動幫我做的好好的了。而當stream中資料實在太多超過buf的時候,會自動把buf不要取得的資料刪掉。

Example:
呼叫使用修正過的fix_gets函數

char buffer[21];
fix_gets(buffer, 21, stdin);

※ sscanf從陣列中格式化讀取資料

和scanf並沒有兩樣,只是讀取資料是從陣列中取出而已,通常用在我們有一些固定格式的資料,放在這個陣列裡面,而我們要抽離出裡面某些值的時候,可以使用sscanf,而它也具有scanf格式化的特性,如此就不用自己寫一堆判斷式了。

Example:
從buffer格式化讀出需要的資料

char str[] = "2007/3/25";
if (sscanf(str, "%d%*c%d%*c%d", &a, &b, &c) == 3)
{
printf("%d %d %d\n", a, b, c);
}

※ printf格式化資料輸出

我想printf應該是寫C第一個用到的函式吧!除了該有的格式以外,它格式化輸出,有兩種修飾字「旗標」和「數字」還有「.數字」,以下就來解釋一下這幾個東西分別的意義。

旗標

* - 靠左對齊 ex: %-20s
* + 若是正數就加正號,負數就加負號 ex: %+6.2f
* # 輸出8或16進位時,會加上0(八進制)或是0x(十六進制) ex: %#x
* 0 實際數值前的位置全部放0,而非空白字元 ex: %010d

數字

* 設定最少使用長度,如果顯示的數字或是字串超過的話,會使用更多的位數 ex: %4d

. 數字

* 對%f, %e,它代表小數點右邊有幾個數字 ex: %.2f (小數點後2位)
* 對%s,它代表最多的輸出字元數 ex: %.5s (只輸出字串前5個字)
* 對%d,它代表最少要出現幾個數字,不夠的自動補0 ex: %.10d (輸出數字不滿10位,則前面自動補0)

Example:
各種printf的格式化輸出

printf("%10d", 123); output: _______123
printf("%-10d", 123); output: 123_______
printf("%10.3f", 3345.67); output: __3345.670
printf("%010.2f", 3345.67); output: 0003345.67
printf("%5.3d", 8); output: __008
printf("%10.5s", "hello world"); output: _____hello
printf("%-10.5s", "hello world"); output: hello_____

※ puts將字串輸出且自動加上換行符號

使用puts(data);相同於使用printf("%s\n", data);但是使用puts比較簡短方便,而且會自動加上換行字元,真的是非常的棒,所以單一要輸出一個字串不混合輸出的時候,使用puts比較方便。

※ sprintf格式化的把資料寫入buffer中

sprintf是非常好用的函式,最常拿來使用型別轉換了,因為要把數字或是浮點數轉換成字串,在標準函式庫裡面是沒有的,因此使用sprintf最好用了,只要指一下格式,就可以把資料寫到我們的buffer中了。

Example:
將100數值轉成字串

char* itoa(char* to, int from, int num)
{
char tmp[11]; (int最大數表示為2147483648剛好10位要加上'\0')
sprintf(tmp, "%d", from);
fix_strcpy(to, tmp, size); (修正過指定大小的strcpy下面會說)
return to;
}

※ strcmp比較兩字串是否相同

比較兩字串是否相同,如果相同的話回傳0,不同的話可能是正值也可能是負值,不過最常使用的就是看有沒有回傳0了,因此程式可以寫成這樣。

Example:
比較兩字串是否相等

const char* str1 = "hello world";
const char* str2 = "helloworld";
if (!strcmp(str1, str2))
{
do something...
}

※ strncpy複製字串到buffer中

本來應該使用strcpy的但是因為strcpy沒有限字元數,因此如果buffer不夠大的話,複製過去多的字元可能會覆寫到其它的記憶體,造成程式當掉,因此轉而使用strncpy比較安全。

* 會複製n個字元,或是讀到'\0'(空字元)為止
* 複製來源少於目標時,連'\0'都會複製
* 複製來源多於目標時,'\0'要自行加上去

Example:
修正strncpy讓複製n-1字元過去,而且都會自動加上'\0'

char* fix_strcpy(char* to, const char* from, int num)
{
int size = num-1;
strncpy(to, from, size);
if (strlen(from) >= size)
{
to[size] = '\0';
}
return to;
}

如此我們在使用上,就不用管它怎麼做的了,只需要將我們的buffer的大小直接傳給它,就會自動幫我做的好好的了。

Example:
使用修正版的strcpy對字串做複製

char buffer[11];
const char* str = "hello world!!";
fix_strcpy(buffer, str, 11);

※ strlen計算一字串的元字數

strlen計算字元數可以用於指標或是陣列,只要它最後面有'\0'空字元者都可以使用,而回傳的是不含空字元的數量。
常常我們從stdin讀取資料或是從檔案讀資料,最重要的就是先判斷是不是我們要的資料才來做處理,最常用在給使用者輸入一個資料,我們先來做認可,之後在來處理,但是認可他的資料其實是一個很大的工夫,這裡我們來討論||(or)和&&(and)的妙用之處。

※ &&運算子

&&運算是從左往右運算的,如果兩個運算元不是0的話會得到結果為1,並且當第一個運算元為0的時候,就不會計算第二個運算元的值了。

Example:

if (a == 0 && b == 0)
{
....
}

當a不是0的時候後可的b==0根本不會做,因為a!=0已讓&&得到一個false的結果了,所以通常使用&&的話都是讓每一個運算式層層「把關」,要每關都有通過才會執行裡面的工作。

Example:

if ((關卡1) && (關卡2) && (關卡3) && (關卡4))
{
處理正確資料程序
}

如以上,在每個關卡如果錯誤的話(算出來為0)那麼後面的關卡就測不到,且不會執行if中的程式了。 歸納以下重點。

* 每一樣測試條件都要成立才會進入,且通過測試。
* 某一樣測試條件沒有過的話,後面的測試將會全都跳過。
* 專門用在開始處理資料前的判斷資料輸入符不符合。
* (VIP) 全部為真進入,單一為假離開。

Example:

while (scanf("%d", &n) == 1 && n > 1 && n < 5)
{
printf("%d\n", n);
}

※ ||運算子

||運算是從左往右算,如果兩個運算元中有一個不是0時就是得到1,並且當第一個運算元是1時,就不會計算第二個運算元的值了。

Example:

if (a != 0 || b != 0)
{
....
}

當a不是0的話後面的b!=0不會做,因為a!=0已造成true了,所以通常用||來「排除」每一個錯誤,只要一有錯誤的成立,立刻為1,那麼後面的運算式就都不會做了。

Example:

if ((錯誤1) || (錯誤2) || (錯誤3) || (錯誤4))
{
錯誤處理程序
}

如以上,每個錯誤如果一成立,那就得到為1,就可以立刻進入,錯誤處理工作,而後面的錯誤測試就全都跳過了。歸納以下重點。

* 其中任一個測試條件只要一成立,立刻進入,且通過測試。
* 某個測式條件只要一過,後面就不做了。
* 專門用在排除錯誤輸入,要求重新輸入資料。
* (VIP) 單一為真進入,全部為假離開。

Example:

while (scanf("%d", &n) != 1 || n > 5 || n < 1)
{
printf("error!!");
}

※ 較複雜的錯誤判斷

如果一群資料中我們只要3 5 7這三個值才做正確處理的話,那其它的都是錯誤資料,我們可以分別判斷兩類資料一種是正確的使用&&另一種是錯誤的使用||。

Example:

while (scanf("%d", &n) == 1 && (n == 3 || n == 5 || n == 7))
{
處理正確資料
}
while (scanf("%d", &n) != 1 || (n != 3 && n != 5 && n != 7))
{
處理錯誤程序
}

在正確資料處理判斷的部份,設定了兩個關卡,第一關是讀到的資料必須一筆資料,第二關是讀到的這個資料,必須是3 5 7 其中的一個數字,所以說資料可以是3「或」5「或」7這三個數字。

在錯誤資料處理判斷的部份,有兩個錯誤只要任一個成立,那就立刻進入錯誤處理,第一個錯誤成立條件是讀不到資料,第二個錯誤成立條件是3 5 7 以外的任何數字,所以說資料必須不是3「且」不是5「且」不是7。

※ 總結

要處理正確資料,使用&&且設定每關的關卡標準,通通都通過的話才進行正確的運算工作,而要處理錯誤資料的話使用||且設定錯誤成立的條件,當有條件成立的話,表示出現錯誤了,立刻進行處理。

將每一個「關卡」和「條件」以集合的概念想清楚要如何達成,如所有數字中只要1~4,其它不要,那處理正確資料的關卡就是(n >= 1 && n <= 4) 必須大於1且小於4,那處理錯誤資料的條件為(n <> 4)必須小於1或是大於4。

如果是再更複雜的判斷的話,就使用函式專門來判斷輸入資料是否正確,只需要回傳是true 或是false就可以了,而在函式中可以大大的發揮演算的能力。
常常資料要輸出輸入的,如果對於函式不熟的話又要常常去翻書查,這邊介紹最常用的檔案處理函式和基本輸出入函式,還有函式的使用範例。

※ 檔案輸出入函式

* FILE* fopen(const char* filename, const char* mode) 開啟檔案
* mode: r, w, a, r+, w+, a+, rb, wb, ab
* 成功回傳檔案指標FILE* stream
* 失敗回傳NULL

Example:

FILE* fp;
if (!(fp = fopen("data.txt", "r")))
{
puts("file open error");
exit(1);
}

* int fclose(FILE* stream) 關閉檔案
* 成功回傳0
* 失敗回傳EOF

Example:

if (fclose(fp))
{
puts("close file error");
exit(1);
}

* int fgetc(FILE* stream) 從檔案中取得一個字元
* 成功回傳字元
* 失敗回傳EOF

Example:

char ch;
while ((ch = fgetc(fp)) != EOF)
{
putchar(toupper(ch));
}

* int fputc(int ch, FILE* stream) 輸出一個字元到檔案中

Example:

char ch;
while ((ch = getchar()) != EOF)
{
fputc(ch, fp);
}

* int fscanf(FILE* stream, const char* format, ...) 格式化從檔案讀入
* 成功回傳讀到的個數
* 失敗回傳EOF

Example:

while (fscanf(fp, "%d%s%d", &id, name, &score) == 3)
{
printf("id: %d name: %s score: %d\n", id, name, score);
}

* int ungetc(int ch, FILE* stream) 把已取出來的字元塞回去stream中

Example:

char ch;
while ((ch = getchar()) != EOF)
{
if (ch == 'b' || ch == 'd')
{
ungetc(ch);
}
}

* int fprintf(FILE* stream, const char* format, ...) 格式化輸出到檔案

Example:

const char* str = "test1";
fprintf(fp, "%s\n", str);

* char* fgets(char* str, int num, FILE* stream) 從檔案中一次讀取一行
* 成功回傳str
* 失敗回傳NULL

Example:

char buf[80];
while (fgets(buf, sizeof(buf), fp))
{
puts(buf);
}

* int fputs(const char* str, FILE* stream) 一次寫入一行到檔案之中
* 不會加上換行符號

Example:

const char* str = "test";
fputs(str, fp);

* int fread(void* buffer, size_t size, size_t num, FILE* stream) 二元讀檔
* 成功回傳讀取到的個數
* 失敗回傳0 (讀取0個)

Example:

int count;
double buffer[10];
while ((count = fread(buffer, sizeof(*buffer), 10, fp)))
{
printf("file readed count: %d\n", count);
}

* int fwrite(void* buffer, size_t size, size_t num, FILE* stream) 二元寫檔
* 成功回傳寫入的個數
* 失敗回傳0 (寫入0個)

Example:

int buffer[80];
int success;
if ((success = fwrite(buffer, sizeof(*buffer), 80, fp)))
{
printf("write success count: %d\n", success);
}

* int fseek(FILE* stream, long offset, int flag) 移動檔案指標
* flag: SEEK_SET, SEEK_CUR, SEEK_END
* void rewind(FILE* stream) 移動檔案指標到檔案起始處
* long ftell(FILE* stream) 回傳目前檔案位子(可計算檔案大小)

Example:

long file_size;
fseek(fp, 0, SEEK_END);
file_size = ftell(fp);
rewind(fp);

* int fflush(FILE* stream) 清除緩衝區將緩衝區寫入檔案

Example:

fflush(stdout);
fflush(fp);

* void perror(const char* str) 印出開讀檔案時的錯誤資訊

Example:

FILE* fp;
if (!(fp = fopen("data.txt", "r")))
{
const char* str = "Error message with data.txt";
perror(str);
exit(1);
}

※ 標準輸出入函式

* int getchar(void) 從stdin讀取一個字元
* 成功回傳字元
* 失敗回傳EOF

Example:

char ch;
while ((ch = getchar()) != EOF)
{
putchar(ch);
}

* int putchar(int ch) 輸出一個字元到stdout

Example:

char ch = 'a';
putchar(ch);

* int scanf(const char* format, ...) 格式化從stdin讀取資料
* int printf(const char* format, ...) 輸出格式資料到stdout中

※ 字元字陣輸出入函式

* int sscanf(const char* buffer, const char* format, ...) 從buffer中格式化讀取資

Example:

char buffer[80]
while (fgets(buffer, sizeof(buffer), stdin))
{
int id;
char name[20];
sscanf(buffer, "%d%s", &id, name);
}

* int sprintf(char* buffer, const char* format, ...) 將格式化的資料寫入buffer中

Example:

char buffer[80];
sprintf(buffer, "%.2f", 449.382);
puts(buffer);
在寫某一個具有多個功能的程式,我想一個良好的使用者介面會讓使用者使用上更加的方便,使用者喜歡用選項來選擇他想要達到的目的,因此介紹一個程式小小的技巧來解決這樣的問題。

※ 單一字元讀取問題

為了一次取得使用者輸入的一個字我們需要使用到getchar()但是這個函式往往讓初學者一直遇到相同的問題,就是它會將'\n'的換行字元留在stdin之中,讓你下一次取得字元的時候自動讀入很麻煩,所以我們要想一個辦法清空stdin讓換行字元被刪除。

Example:

void clearstdin()
{
while (getchar() != '\n');
}

它會讓stdin中所有的字元不斷的被getchar()出來,直遇到'\n'換行字元,而且會連換行字元都被抓出來,因此我們就達到我們的目的了。

※ 顯示選單函式

選單函式主要的功能就是要求使用者輸入的資料是否正確,這樣在主程式判斷的時候就不用還要做錯誤處理的功能,因為選單函式已經做掉了。

Example:

char menu()
{
puts("1) go case 1");
puts("2) go case 2");
puts("3) go case 3");
puts("4) go case 4");
puts("q) go exit");
printf("Choice: ");
char ans = tolower(getchar());
clearstdin();
while (!strchr("1234q", ans)) (1234q以外的字元排除)
{
printf("error option, please enter again: ");
ans = tolower(getchar());
clearstdin();
}
return ans;
}

這函式主要做到幾項功能:

* 顯示選單
* 取得使用者輸入字元
* 排除輸入錯誤字元
* 回傳正確字元

※ 選擇程式

要怎麼樣依使用者輸入的資料做選擇,那就是使用switch了,這邊我們要用到剛剛的顯示選單的函式,它會回傳一個正確的使用者輸入的字元,所以這邊我們就不用管對錯了,因為一定保證是對的,剛剛的函式就已經錯誤處理掉了。

Example:

char ch;
bool execute = true;
while (execute && (ch = menu()))
{
switch (ch)
{
case '1':
puts("case 1");
break;
case '2':
puts("case 2");
break;
case '3':
puts("case 3");
break;
case '4':
puts("case 4");
break;
case 'q':
puts("exit programe");
execute = false; (q的話讓執行中止)
break;
default:
puts("error");
break;
}
}

看上去非常的簡單,只是重點在於execute這個開關,正常的情況下它是true就是會一直執行,但是當你輸入q的時候,就會把開關關起來了,這樣下一次執行while的時候就會離開了,如果你要問為什麼execute要放在(ch = menu())前面的話,那你可能要先了解while在判段式的時候是由左向右,而且當一遇到0的時候後面完全不看,所以execute放在前面,一直行到馬上為false那就立刻離開了。

※ 完整範例

Example:

#include
#include
#include
#include

char menu(void);
void clearstdin(void);
int main()
{
char ch;
bool execute = true;
while (execute && (ch = menu()))
{
switch (ch)
{
case '1':
puts("case 1");
break;
case '2':
puts("case 2");
break;
case '3':
puts("case 3");
break;
case '4':
puts("case 4");
break;
case 'q':
puts("exit programe");
execute = false;
break;
default:
puts("error");
break;
}
}
return 0;
}
char menu()
{
puts("1) go case 1");
puts("2) go case 2");
puts("3) go case 3");
puts("4) go case 4");
puts("q) go exit");
printf("Choice: ");
char ans = tolower(getchar());
clearstdin();
while (!strchr("1234q", ans))
{
printf("error option, please enter again: ");
ans = tolower(getchar());
clearstdin();
}
return ans;
}
void clearstdin()
{
while (getchar() != '\n');
}
兩個最重要的資料結構就是「搜尋」和「排序」了,所以看過資料結構的人,想必知道快速排序算是在排序中最好用的,而二元搜尋也是一樣,但是實做的過程往往很複雜,但是std的標準函式庫已有提供給我們使用了,以下就來討論如何使用qsort和bsearch。

※ qsort函式

* void qsort( void *buf, size_t num, size_t size, int (*compare)(const void *, const void *));
* buf 是指向要排序陣列開頭的指標
* num 是資料的數量
* size 是每個元素的大小
* int (*compare)(const void*, const void*) 是指向如何比較的函式指標

※ qsort使用(數字排序)

Example:

#define size_of_ary(ary) sizeof(ary)/sizeof(*ary)

int main()
{
int ints[] = {12,46,4,8,413,86,15,2,48,0,54,6};
qsort(ints, size_of_ary(ints), sizeof(int), int_comp);
return 0;
}
int int_comp(const void* a, const void* b)
{
int* temp_a = (int*)a;
int* temp_b = (int*)b;
if (*temp_a < *temp_b)
return -1;
else if (*temp_a == *temp_b)
return 0;
else
return 1;
}

使用size_of_ary這個巨集可以算出陣列有多少個元素,對於compare這個函式指標,主要目的就是要告訴函式要怎麼去做比較,那個小那個大或是相等,才可以準確的去幫你排序出來,在compare這函式指標原型說要使用const void*泛指標,主要目的是為了要讓所有的型別都可以支援,所以你可以自由的轉成int*或是double*所以實做compare函主第一個目的就是先轉型成你真正的型別。

※ qsort使用(字串排序)

Example:

#define size_of_ary(ary) sizeof(ary)/sizeof(*ary)

int main()
{
const char* color[] = {
"Red", "Blue", "Yellow", "Black", "Green", "Axz"
};
qsort(color, size_of_ary(color), sizeof(char*), my_compare);
return 0;
}
int my_compare(const void* a, const void* b)
{
int ans;
const char** temp_a = (const char**)a;
const char** temp_b = (const char**)b;
ans = strcmp(*temp_a, *temp_b);
return ans;
}

這邊我想最大的問題,一定是看不懂const char** temp_a = (const char**)a這是雙指標,但是為什麼要用雙指標呢?你可以改成單指標,但是我保證一定不能跑,因為其實你的color[]的每個元素都是一個 char*的指標,而對於const void* a它的意思是指向color[]的每一個陣列元素,那意思不是為「const void* a 指向char*指向字串」嗎?所以const void* a應該要轉型成const char** a才正確。

※ bsearch 函式

* void *bsearch(const void *key,const void *buf, size_t num, size_t size, int (*compare)(const void *, const void *));
* key 要尋找的資料的指標
* buf 是指向要搜尋陣列開頭的指標
* num 元素的數量
* size 每個元素的大小
* int (*compare)(const void*, const void*)指向如何比較的函式指標
* 若找不到回傳NULL,否則回傳指向資料的指標

※ bsearch 使用(數字搜尋)

Example:

#define size_of_ary(ary) sizeof(ary)/sizeof(*ary)

int main()
{
int ints[] = {12,46,4,8,413,86,15,2,48,0,54,6};
int key = 8;
int* gets;
if ((gets = (int*)bsearch(&key, ints, size_of_ary(ints), sizeof(int), int_comp)))
{
printf("find int %d\n", *gets);
}
return 0;
}
int int_comp(const void* a, const void* b)
{
int* temp_a = (int*)a;
int* temp_b = (int*)b;
if (*temp_a < *temp_b)
return -1;
else if (*temp_a == *temp_b)
return 0;
else
return 1;
}

和qsort不同的地方在於他需要傳入一個比較鍵值key的指標,而會回傳找到資料的指標,或是找不到回傳NULL所以可以透過if來判斷有沒有找到,以免對NULL做解參考(dereference)的動做。

※ bsearch 使用(字串搜尋)

Example:

#define size_of_ary(ary) sizeof(ary)/sizeof(*ary)

int main()
{
const char* color[] = {
"Red", "Blue", "Yellow", "Black", "Green", "Axz"
};
char* key = "Yellow";
char** find;
if ((find = (char**)bsearch(&key, color, size_of_ary(color), sizeof(char*), my_compare)))
{
printf("i find %s\n", *find);
}
return 0;
}
int my_compare(const void* a, const void* b)
{
int ans;
const char** temp_a = (const char**)a;
const char** temp_b = (const char**)b;
ans = strcmp(*temp_a, *temp_b);
return ans;
}