pwd
返回了根目錄
這時候看到系統返回了一個 /,這個 / 被我們稱為系統的 根目錄(root
),這個位置也就是我們現在在系統中的位置。
但是,我們要開展工作的位置的路徑為:
/home/user/project
所以,我們要學會如何切換我們所在的位置:
輸入 cd 命令,并在 cd 命令后加上空格,在此后輸入開展工作的位置的路徑:
cd /home/user/project
這就是一個層級化路徑,其實其可以通過
cd home
cd user
cd project
逐次到達。
下載輸入 ls -l。
這里就有個old目錄。
mv main.c old //mv [選項] 源文件或目錄 目標文件或目錄
再使用移動操作:
再創建新文件,通過命令
touch main.c
創建新文件
這時候可以看見:
去old
目錄下把key
刪除
更多相關命令的可附加參數及使用意義,可以通過 man [空格] [命令名]
的方式進行進行進一步的查詢(查詢后退出只需敲擊鍵盤上的 q 即可)。
多模塊程序
之前的課中,所有文件操作都是單文件進行。對于一個只完成一個特定的任務,只包含十幾個函數的程序來說,單文件的組織方式還算可以接受,但是當程序越來越長,程序實現的功能越來越多,將他們全部都組織在一個文件里就會變得不那么容易讓人接受了。
因此,我們需要學習如何在 C 語言中將不同功能在多個代碼文件中分別實現,然后將它們看作多個模塊組織在一起為同一個程序服務。
關于gcc命令
原地址:gcc命令中參數c和o混合使用的詳解、弄清gcc test.c 與 gcc -c test.c 的差別
gcc命令使用GNU推出的基于C/C++的編譯器,是開放源代碼領域應用最廣泛的編譯器,具有功能強大,編譯代碼支持性能優化等特點。現在很多程序員都應用GCC,怎樣才能更好的應用GCC。目前,GCC可以用來編譯C/C++、FORTRAN、JAVA、OBJC、ADA等語言的程序,可根據需要選擇安裝支持的語言。
語法
gcc(選項)(參數)
選項
-o:指定生成的輸出文件;
-E:僅執行編譯預處理;
-S:將C代碼轉換為匯編代碼;
-wall:顯示警告信息;
-c:僅執行編譯操作,不進行連接操作。
參數
C源文件:指定C語言源代碼文件。
實例
常用編譯命令選項
假設源程序文件名為test.c
無選項編譯鏈接
gcc test.c
將test.c
預處理、匯編、編譯并鏈接形成可執行文件。
這里未指定輸出文件,默認輸出為a.out
。
選項 -o
gcc test.c -o test
將test.c
預處理、匯編、編譯并鏈接形成可執行文件test
。
-o
選項用來指定輸出文件的文件名。
選項 -E
gcc -E test.c -o test.i
將test.c預處理輸出test.i文件。
選項 -S
gcc -S test.i
將預處理輸出文件test.i匯編成test.s文件。
選項 -c
gcc -c test.s
將匯編輸出文件test.s編譯輸出test.o文件。
無選項鏈接
gcc test.o -o test
將編譯輸出文件test.o鏈接成最終可執行文件test。
選項 -O
gcc -O1 test.c -o test
使用編譯優化級別1編譯程序。級別為1~3,級別越大優化效果越好,但編譯時間越長。
多源文件的編譯方法
如果有多個源文件,基本上有兩種編譯方法:
假設有兩個源文件為test.c
和testfun.c
- 多個文件一起編譯
gcc testfun.c test.c -o test
將testfun.c
和test.c
分別編譯后鏈接成test可執行文件。
- 分別編譯各個源文件,之后對編譯后輸出的目標文件鏈接。
gcc -c testfun.c
將testfun.c編譯成testfun.o
gcc -c test.c
將test.c編譯成test.o
gcc -o testfun.o test.o -o test
將testfun.o和test.o鏈接成test
以上兩種方法相比較,第一中方法編譯時需要所有文件重新編譯,而第二種方法可以只重新編譯修改的文件,未修改的文件不用重新編譯。
再來復習一下:
gcc -c a.c 編譯成目標文件a.o
gcc -o a a.o 生成執行文件a.exe
gcc a.c 生成執行文件a.exe
gcc -o a -c a.c 編譯成目標文件a
gcc -o a a.c 生成執行文件a.exe
在a.c中引用test.c中的一個函數后:
gcc -c test.c 編譯成目標文件test.o
gcc -c a.c 編譯成目標文件a.o
gcc -o a test.o a.o 生成執行文件a.exe
gcc -o a test.o a.c 生成執行文件a.exe
gcc -o a test.c a.c 生成執行文件a.exe
gcc -o a test.o a.c 生成執行文件a.exe
總結:只要參數中有-c,總是生成目標文件;只要參數中無-c而只有-o,則總是生成執行文件。
在剛開始學習 C 語言的時候,我們曾經學習過,當我們的程序只有一個main.c
文件時,我們可以在命令行中通過
gcc -o program main.c
對單個代碼文件進行編譯,生成可執行文件program
,并且通過./program
運行編譯生成的程序。在我們之前的課程中,計蒜客的學習系統也幫你進行了這樣的操作。
相比于單個文件、單一功能的程序,當程序有多個模塊時,問題就開始變得復雜了。我們對每一個模塊會首先編譯出每個模塊對應的*.o目標代碼文件(relocatable object file),例如:
gcc -c -o set.o set.c
會將我們的一個set.c
文件編譯成一個set.o
的目標代碼文件。請注意,這里的-c表示生成目標代碼文件。-o與之前單文件的時候一樣,在它之后我們會寫明被生成的文件的名稱。
當我們完成了每一個獨立模塊的編譯并獲得它們的目標代碼文件后,我們可以將我們的主程序的目標代碼文件與他們鏈接在一起。例如:
gcc -o program main.o set.o others.o
將目標代碼文件set.o
和others.o
與main.o
在鏈接在一起,并且輸出了 可執行文件(excutable file)program。
我們依然可以通過./program
運行編譯生成的程序。
當我們將一個程序寫在多個文件中時,每一個文件中的變量和函數默認都是只有文件內的部分才可以訪問的。但是有一些特殊的全局變量、類型定義、函數可能會需要在多個文件中被使用。
這時候,我們可以將這類的內容單獨寫成一個 頭文件(header file),并且將全局變量、類型定義、函數聲明寫到頭文件中。
對于一個文件set.c,習慣上它的頭文件會被命名為set.h。在所有需要用set.h中全局變量、類型定義、聲明的函數的文件中,用
#include "set.h"
將對應的頭文件引入。在這里的引入頭文件方式和引入系統庫頭文件的方式很類似,只不過這里用的是引號""
而不是尖括號<>
。
由于頭文件里也可以引入頭文件,因此我們可能事實上多次引入同一個文件,比如我們引1.h
和2.h
,且1.h
也引入2.h
,這時因為2.h
被引入了兩次,就有可能出現重復的聲明。為了解決這個問題,我們2.h
中定義一個宏,在2.h的最開始判斷這個宏是否被定義過,如果被定義過,就跳過2.h整個文件的內容。
這里我們將會用到兩個新的預處理指令#ifndef xxx
和#endif
,它們成對出現且#ifndef
在前,作用是如果這時并未已定義xxx
宏,則這對#ifndef xxx
, #endif
之間的內容有效。(其中xxx
可以替換為任意宏名)
這樣```2.h```可以寫為類似于如下的內容:
#ifndef xxx
#define xxx
typedef enum Status { Success, Fail };
typedef struct {
char *name;
int age;
} People;
Status go_to_Jisuanke(People);
#endif
細心的同學已經發現,如果在程序中尚未引入2.h
的位置定義了xxx
宏,則#include "2.h"
中的聲明并不會被引入,因此我們不應該在此使用xxx
這種平凡的名字。實際上,我們一般會采用一個與頭文件名相關的名字來代替xxx
,比如一個常用的代碼風格里,這個宏的名字形式為工程名_路徑名_文件名_H_
。
總結的幾點
- 某一代碼中定義的函數如果需要被其他代碼文件所使用,應該將函數的聲明放入頭文件,并在其他代碼文件中引入這一頭文件。
- 并不需要把每個函數單獨寫成一個模塊,還是應該根據功能的劃分和實現去決定怎么抽出模塊。
- 可以只有多個
.c
的文件,也并不一定非要都拆出.h
文件。 -
#include
可以被用于引入系統庫頭文件也可以被用于引入自己實現的頭文件。 - 只不過在引入系統庫頭文件時,我們往往會使用尖括號
<>
,而在引入自己實現的頭文件時一般用引號""
。 - 用
gcc
時,-o
之后寫的是生成可執行文件的名稱。-c
的參數的使用會幫我們得到一個對象文件。
//-c和-o都是gcc編譯器的可選參數
//-c表示只編譯(compile)源文件但不鏈接,會把.c或.cc的c源程序編譯成目標文件,一般是.o文件。
//-o用于指定輸出(out)文件名。不用-o的話,一般會在當前文件夾下生成默認的a.out文件作為可執行程序。
//例如
gcc -c test.c //將生成test.o的目標文件
gcc -o app test.c //將生成可執行程序app
gcc -c a.c -o a.o //表示把源文件a.c編譯成指定文件名a.o的中間目標文件(其實在這里,你把-o a.o省掉,效果是一樣的,因為中間文件默認與源文件同名,只是后綴變化)。
Makefile
在前面學習多模塊程序的時候,我們需要先把每個模塊的代碼都生成為目標代碼文件,然后再將目標代碼文件聯編成一個可執行文件。如果每一次編譯都要輸入這么多命令,是不是很復雜呢?如果每次修改一點點內容就需要重新編譯整個工程,是不是很浪費時間呢?
為了解決所遇到的問題,方便開發,我們使用一個叫做make
的命令,它可以讀取Makefile
文件,并且根據Makefile
中的規則描述把源文件生成為可執行的程序文件。
最基本的Makefile中包含了一系列形式如下的規則。請注意,每一條規則的命令前,必須要有一個制表符\t。
目標: 依賴1 依賴2 ...
命令
例如,可以寫一條規則:
array.o: array.c array.h
gcc -c -o array.o array.c
表示生成的文件是目標代碼文件array.o
,它依賴于array.c
和array.h
。
當我們在命令行中執行make array.o
時,根據這一規則,如果array.o
不存在或者array.c
與array.h
至少之一比array.o
更新,就會執行gcc -c -o array.o array.c
。
我們把上述代碼保存為Makefile
,與array.c
和array.h
放在同一目錄,在那個目錄里執行make array.o
就能看到效果。
注意:Makefile
里的除當前目錄隱藏文件外的第一個目標會成為運行make
不指定目標時的默認目標。
再看:
main: array.o main.o
gcc -o main array.o main.o
main.o: main.c array.h
gcc -c -o main.o main.c
array.o: array.c array.h
gcc -c -o array.o array.c
在Makefile
有多條規則時,如果我們希望只生成其中一個,我們可以在make命令后加上需要生成的目標的名稱。例如,在這里我們可以執行make main.o
、make array.o
或make main
。當我們執行make main
時,make
命令發現array.o
和main.o
不存在,就會根據以它們為目標的規則先生成它們。
很多時候,會需要將.o為后綴的目標代碼文件和可執行的程序文件刪除,完全從頭進行編譯。那么我們可以寫一條clean規則,例如:
clean:
rm -f array.o main.o main
rm
命令表示刪除文件,-f
表示強制,因此rm -f array.o main.o main
按照預期,當我們執行make clean
就可以刪除array.o
、main.o
和main
了。事實真的這樣嗎?
因為畢竟這時如果已經存在clean
文件,rm
命令就不會執行了。為了解決這個問題,我們通過一個特殊的方法告訴make
這個名為clean
的規則在clean存在的時候仍然有效。
.PHONY: clean
clean:
rm -f array.o main.o main
.PHONY
用于聲明一些偽目標,偽目標與普通的目標的主要區別是偽目標不會被檢查是否存在于文件系統中而默認不存在且不會應用默認規則生成它。
在Makefile中我們還可以使用它的變量和注釋。
# 井號開頭的行是一個注釋
# 設置 C 語言的編譯器
CC = gcc
# -g 增加調試信息
# -Wall 打開大部分警告信息
CFLAGS = -g -Wall
# 整理一下 main 依賴哪些目標文件
MAINOBJS = main.o array.o
.PHONY: clean
main: $(MAINOBJS)
$(CC) $(CFLAGS) -o main $(MAINOBJS)
array.o: array.c array.h
$(CC) $(CFLAGS) -c -o array.o array.c
main.o: main.c array.h
$(CC) $(CFLAGS) -c -o main.o main.c
clean:
rm -f $(MAINOBJS) main
上面這個例子已經是一個較為完整的Makefile
了。以#開頭的是我們的注釋,我們在這里用注釋說明了我們定義的Makefile
變量的用途。CC
變量定義了編譯器,CFLAGS
變量標記了編譯參數,MAINOBJS
變量記錄了main
依賴的目標文件。定義的變量可以直接通過$(變量名)
進行使用。
總結
- 一個 Makefile 可以包含多個規則,我們既可以每次在make后說明執行哪個功能,也可以通過定義的all來執行一系列的規則。
- 在用gcc編譯時加上-Wall會顯示錯誤信息,Wall是用于顯示大部分警告信息的,編譯錯誤信息默認就會顯示。
- Makefile其實描述了一系列轉為對象文件、聯編的過程,不使用make也是可以完成的。
- Makefile中的變量是用$()的方式來用噠。
Makefile體驗
→ ~/project ls -l
total 16
-rw-r--r-- 1 user user 304 Sep 15 16:46 array.c
-rw-r--r-- 1 user user 87 Sep 15 16:46 array.h
-rw-r--r-- 1 user user 297 Sep 15 16:46 main.c
-rw-r--r-- 1 user user 0 Sep 15 16:46 main.h
-rw-r--r-- 1 user user 419 Sep 15 16:46 Makefile
→ ~/project
→ ~/project cat Makefile
# 設置 C 語言的編譯器
CC = gcc
# -g 增加調試信息
# -Wall 打開大部分警告信息
CFLAGS = -g -Wall
# 整理一下 main 依賴哪些目標文件
MAINOBJS = main.o array.o
.PHONY: clean
main: $(MAINOBJS)
$(CC) $(CFLAGS) -o main $(MAINOBJS)
array.o: array.c array.h
$(CC) $(CFLAGS) -c -o array.o array.c
main.o: main.c array.h
$(CC) $(CFLAGS) -c -o main.o main.c
clean:
rm -f $(MAINOBJS) main
→ ~/project
→ ~/project make
gcc -g -Wall -c -o main.o main.c
gcc -g -Wall -c -o array.o array.c
gcc -g -Wall -o main main.o array.o
→ ~/project
→ ~/project ./main
1 2 3 4 5 6 7 8 9 0
數組元素和為: 45
數組元素平均值為: 4.5
→ ~/project
→ ~/project make clean
rm -f main.o array.o main
→ ~/project
→ ~/project ls -l
total 16
-rw-r--r-- 1 user user 304 Sep 15 16:46 array.c
命令行參數
之前,main
函數一般都沒參數,對應在運行時,一般就直接輸入可執行的程序文件名(例如./main
)。
但實際上main函數可以有參數。我們可以將任何過去無參數的main函數替換成下面這種有參數的main函數(不過考慮到我們并沒有利用,不寫是很正常的)。
int main(int argc, char **argv) {
// ...
}
在這里,main
函數有兩個參數,第一個參數是整數型,會傳入命令行參數的個數,程序運行時就可以接收到。第二個參數是char **
,其中儲存了用戶從命令行傳遞進來的參數。
如果我們的程序可執行文件名為main
,則在命令行中輸入./main hello world
我們會得到argc
為3
,argv[0]
為./main
,argv[1]
為hello
,argv[2]
為world
。如果有更多參數也可以以此類推。
命令行參數默認都是空格分隔,但是如果我們希望包含空格的一個字符串作為參數,我們則需要在輸入參數時用引號將其包裹起來。
如果我們的程序可執行文件名為main
,則在命令行中輸入./main "hello world" is my greet
我們會得到argc
為5
,argv[0]
為./main
,argv[1]
為hello world
,argv[2]
為is
,argv[3]
為my
,argv[4]
為greet
。
任何被接收到的argv
參數都可以被當做正常的字符串在代碼里使用。在很多程序的設計中,我們會需要根據接收到的參數來決定程序的執行方式,這時候,學會使用argc
和argv
就顯得很重要了。在之后的課程中,你也會需要運用這一塊的知識,一定要學明白喔。
一些總結
- 命令行讀入的參數是從命令行鍵入的可執行程序路徑開始計算。
- 在main函數中用于接收命令行參數的函數參數中,第一個是命令行參數的個數。
- 在int main(int argc, char **argv)中,argc就固定為 2 了,它取到的應該是命令行中鍵入的參數個數。
命令行參數
命令行參數是怎么獲取和使用的?
請先輸入 cat main.c 看一下我們當前所在目錄下的 main.c 文件。
看到,在這個 main.c 的文件中,我們的 main 函數獲取了命令行參數的個數 argc(整數型)和一系列參數(字符指針的指針,可以用訪問數組的形式訪問)。
這個程序將預期輸出命令行參數的數量,并且將每一個參數逐一列出。
接下來讓我們 make 一下,完成對這個程序的編譯。
完成了 make
,就讓我們把它運行起來吧。請輸入 ./main
并運行起來這個程序,并在之后隨意輸上一些空格分隔開的字符串,例如:
./main I feel better after this
我們程序中的argc
接受到的參數一共是幾個,它們分別對應了我們在終端中輸入的哪一部分的內容呢。
文件操作
之前課程中,我們學習、設計的所有程序都是從標準輸入進行讀取、向標準輸出進行寫出的,操作系統為我們準備好了標準輸入、標準輸出的界面。在這節課中,我們將要學習如何從文件中進行讀取、如何向文件進行寫入。
在讀文件的時候我們需要先有一個可以讓我們訪問到文件的 文件指針(file pointer),它是一個FILE
類型的指針。
我們可以通過下面的方式聲明一個文件指針。
FILE *fp;
這時候,如果我們希望對一個文件進行操作,我們需要先使用
fp = fopen(文件路徑, 訪問模式);
將文件指針和文件關聯起來,其中第一個參數是一個字符串,對應了我們希望訪問的文件路徑。第二個參數是訪問模式,它可以是表示只讀模式的"r"
,也可以是表示只寫模式的"w"
,還可以是在文件末尾追加的"a"
。
當我們將文件指針和文件關聯起來后,我們就可以通過fgetc(fp)
;獲得當前指針之后位置的一個字符了,每獲得一個字符,指針會向后移動一個字符(如果到達文件尾部則會返回EOF
)。
我們這時通過fputc('c', fp);
的方式將字符'c'
寫入到fp
關聯的文件內了。
了解到這些信息后,我們就可以實現將一個文件復制到另一個文件內的函數了,例如:
void filecopy(FILE *in_fp, FILE *out_fp) {
char ch;
while ((ch = fgetc(in_fp)) != EOF) {
fputc(ch, out_fp);
}
}
這個函數接收的兩個參數都是文件指針。這個函數會通過一個可讀模式的文件指針逐字符地讀取,并且通過一個可寫模式的文件指針逐字符地將所有字符寫出,從而起到復制文件內容的作用。
你需要注意,在給文件指針進行命名的時候,要避開 stdin、stdout 和 stderr 這三個名稱。因為這三個名稱其實已經用于標準輸入、標準輸出、標準錯誤的文件指針。
你可能會問了,那我們看到的 stdin
、stdout
和 stderr
的這三個文件指針可以直接使用嗎?回答是肯定的。
我們是通過 fgetc(stdin);
獲得來自標準輸入的字符,也可以通過fputc(ch, stdout);
或 fputc(ch, stderr);
將變量 ch
中的字符輸出到標準輸出或標準錯誤中的。
除了fgetc和fputc之外,我們還可以使用fscanf和fprintf函數。這兩個函數都很像我們已經很熟悉的scanf和printf函數,只是不過,scanf和printf 可以被看作 fscanf和fprintf 的特例。
我們使用 fscanf 從文件指針in_fp進行讀取時,可以寫成:
fscanf(in_fp, "%c", &a);
而如果我們寫
fscanf(stdin, "%c", &a);
這將完全與下面直接使用 scanf 的方式等價。
scanf("%c", &a);
類似地,我們使用fprintf向文件指針out_fp進行寫出時,可以寫成:
fprintf(out_fp, "%c", a);
而如果我們寫
fprintf(stdout, "%c", a);
這將完全與下面直接使用 printf
的方式等價。
printf("%c", a);
在使用文件并且確定不再繼續使用后,我們要通過下面所示的方式將文件指針fp與文件的關聯斷開。你可以將它視為和fopen
相反的一個操作。
fclose(fp);
如果你不在程序中使用fclose
,程序正常結束時,程序會為所有打開的文件調用fclose
。
stdin
、stdout
其實也是被打開的文件指針,如果你覺得用不到的話,其實也是可以使用fclose
將他們關閉掉的。你可以自己試一試,關閉 stdin
、stdout
會對我們以前寫過的程序帶來什么樣的影響呢?