序言
這個部落格算是其他工具的使用總結吧,裡面包含gdp,動靜态庫的簡單認識...其中gdp是我們今天的額難點,需要我們好好的了解一下.
gcc & g++ 編譯器
這兩個工具比較的簡單,其中gcc是C語言的編譯器的,g++是C++的編譯器,我們這裡用gcc來示範,它們的選項都是一樣的.
程式編譯
我們先來看看如何在Linux下運作一個C語言的代碼.我們看到目前目錄下有一個.c檔案

我們要做的就是編譯這個代碼.
[bit@Qkj 08_07]$ gcc main.c
現在我們就可以看到了如何編譯一個代碼,可執行程式的檔案名預設是a.out,當然我們也可以修改.
[bit@Qkj 08_07]$ gcc main.c -o mybin
我們都知道,一個代碼要變成可執行程式要經過以下若幹步驟,我先列出來,在Linux的環境下給大家示範,有些指令看不太懂沒有關系,就看檔案的變化就可以了
- 預處理
- 編譯
- 彙編
- 連結
預處理(預編譯)
預處理大緻包含四個方面,就是去注釋,宏替換,頭檔案展開和條件編譯.這個過程我們在之前就談過.
#include <stdio.h>
#define M 100
int main()
{
// 列印一個宏
printf("宏 %d\n",M);
#ifdef A
printf("hello A\n");
#else
printf("對不起,你沒有定義 A\n");
#endif
return 0;
}
我們希望得到預處理後的結果的,下面的選項就可以.
[bit@Qkj 08_07]$ gcc -E main.c -o main.i
編譯
編譯器把代碼翻譯成彙編語言
[bit@Qkj 08_07]$ gcc -S main.i -o main.s
彙編
将彙編檔案翻譯成可重定位二進制檔案,在Windows下是.obj檔案.這時候我們這個檔案還不能運作.
[bit@Qkj 08_07]$ gcc -c main.s -o main.o
連結
這個才是我想和大家分享的,連結一直是我們比較難了解的.我們可以從這個角度了解,一旦我們調用了一個函數,如果這是别人實作的函數,例如printf函數,編譯器就會問,這個printf的函數在哪裡?我們使用的printf如何和C庫裡面的函數連結出來.
[bit@Qkj 08_07]$ ls /lib64/libc*
動靜态庫
前面我們說了庫這個詞,這是我第一次正式接觸庫的概念,我們下載下傳編譯器的時候,會自動下載下傳相應的庫,我們之前包含的标準庫的頭檔案,為何編譯器可以知道頭檔案所在的路徑,原因就是編譯器下載下傳的時候,.像庫這些檔案的路徑已經預設确定好了.頭檔案是包含函數聲明,庫是函數的實作.
庫分為動态庫和靜态庫,等會我們感性的認識一下它們兩個的差別.在Linux, .so字尾就是動态庫,Windows下是.dll.靜态庫在Linux是.a,Windows下是.lib.
- 動态庫 我們隻是包含頭檔案,遇到标準庫的函數,就去庫裡面找 動态連結
- 靜态庫 編譯器遇到了庫裡面的函數,編譯器自動把這個函數給複制到你的可執行程式中 靜态連結
注意,Linux預設是連接配接動态庫,我們也推薦的是動态庫.如果我們想要靜态庫連接配接,下面也行,加上一個選項就可以了
[bit@Qkj 08_08]$ gcc main.c -o s_a.out -static
上面的指令可能跑不過,原因就是Linux預設一般都是動态庫,我們需要自己下載下傳靜态庫,記住在root使用者下下載下傳,我們還沒有添加sudo這個東西,一個是C庫,一個是C++的
[root@Qkj ~]# yum install -y glibc-static
[root@Qkj ~]# yum install -y libstdc++-static
ldd 指令
ldd可以觀察可執行程式所依賴的庫,
[bit@Qkj 08_08]$ ldd a.out
gdb 調試器
gcc/g++都是編譯器,gdp才是我們的調試器關于這個調試器,大家知道它的基本的用法就可以了,我們先來用簡單的指令.
debug & release
在Linux下,預設是release模式,這個模式不可以調試,而且編譯器還會做一定程度的優化.gcc 的release 版本是傳遞給使用者使用,debug裡面包含了調試資訊 ,體積上一定大于release模式.
使用下面的選項就可以得到debug模式的版本
至于多的那一部分,就是調試資訊
調試
我們先把代碼給放出來,先稍微的看兩眼.
#include <stdio.h>
int add(int a)
{
int sum = 0;
int i = 0;
for(i=0;i<a;i++)
{
sum += i;
}
return sum;
}
int main()
{
int x = 10;
int total = add(x);
printf("%d\n",total);
return 0;
}
我們這裡開始,直接開始調試.這個還是比較簡單的,直接隻用這個格式的指令就可以了. gbd debug版本
[bit@Qkj 08_08]$ gdb mybin_g
如果你不想調試了,可以直接q,退出調試.
顯示代碼,直接 l
它預設不是從第零行顯示,如果我們想要從某一行顯示,可以l後面跟上數字,注意,一般gdp會自動記錄上一個指令,你可以直接按回車執行上一條指令.
打斷點 b 行号,打斷點很重要,我們在第十五行打斷點
檢視斷點,info b
跳到下一個斷點 c
取消斷點 d 斷點編号
程式跑起來,他會停留在第一個遇到的斷點, 直接 r
檢視變量,p
到下一行 n,可以了解成 逐過程,類似F10,也就是遇到函數不進入.
逐語句,s 進入函數 類似F11
常顯示display對于下面我們總不能每次都p sum的值吧,這裡有常顯示的指令
取消常顯示,undisplay,這裡面用的是編号
檢視堆棧 bt
until 10 跳到指定行
finish 跳出目前函數
make和Makefile
我們在window系統下學習C語言,一般會使用VSCode、VS這些軟體,它們有一個統稱叫做內建開發環境 ,在Vs2013中我們很容易寫出幾個 .c 檔案,隻需要編譯一下就可以運作起來,那麼在Linux環境下該怎麼做?這就是需要make和makefile,這裡需要先說明一下,在公司裡面makefile是可以自動生成的,不需要我們手寫,但是這裡需要我們了解原理.
我這裡就先說一下,不做詳細的解釋
- make 是一條指令
- Makefile 是一個檔案
環境準備
先建立幾個檔案,裡面的内容就随便編寫了.
[bit@Qkj 08_07]$ touch main.c mytest.c mytest.h
mytest.h
#ifndef __MYTEST_H__ //#ifndef __MYTEST_H__這個先不用管
#define __MYTEST_H__
#include <stdio.h>
void func();
#endif
編寫mytest.c
#include "mytest.h"
void func()
{
prinf("func()\n");
}
編寫main.c
#include "mytest.h"
int main()
{
show();
return 0;
}
Makefile/makefile
在談make之前,我們需要有談一下makefile,首先這是一個檔案,我們先建立一個這個檔案.注意Makefile和makefile都可以.
我們在本目錄下建立一個Makefile(或者makefile)檔案
[bit@Qkj 08_07]$ touch Makefile
那麼這裡就開始需要談談這個檔案裡面的内容了,我們想是不是可以吧自己想要的指令寫道這個檔案裡面,然後通過某種方法可以簡便的做法.那麼在裡面我們可以這麼做.
mybin: main.c mytest.c
gcc main.c mytest.c -o mybin
這裡是我們今天的重點,我會好好的解釋的
依賴關系&依賴方法
我們首先要知道一點:一個Makefile檔案存在下面的東西,缺一不可
- 依賴關系
- 依賴方法
那麼這倆個究竟是什麼東西,我們先來舉一個例子.假設在學校裡面,你缺生活費了,你給你把打了一個電話,說我是你孩子,是以需要給我打錢.這時候就出現了這兩個東西.親情就是依賴關系,打錢就是依賴方法.
這裡面我們還可以這麼做,我們編譯程式的過程也可以控制出來.
make
make是一個指令,我們就是簡單make一下就可以執行編譯程式的功能.
我們隻需要用一下make指令
清了解決方案
我們在VS裡面看到過清了解決方案這種情況,在Linux中中也就是删除掉我們編譯出來的程式,我們就是下面的做法.
這個地方還算是比較好的,如果我們生成的臨時檔案很多呢?總不能每一次都可以保證自己删除時完全的正确的吧,是以我們也把這個清理的程式放到makefile中.
這裡面還有一點沒有談到的,不過先不要着急,這裡面都會和大家講.
make clean
現在就存在一個問題,為何事make clean,要知道上面我們編譯程式可是一個make就可以了,難道clean有什麼是特殊的?準确來說,是第一個依賴關系和依賴方法比較特殊,誰在第一個,make的就是誰,其餘的前面都是make+目标檔案.
僞目标
在現實生活中,存在一些孤兒院的孩子,它們不存父母,計算機中也存在相似的東西,這中稱之為為目标,所謂僞目标就是沒有依賴檔案的目标檔案.這裡需要注意的,僞目标也是目标,不要把僞軍不當軍隊.
總是可執行
現在還存在最後一個大問題,.PHONY是什麼,它是必須的的嗎?大家可以把**.PHONY**看作makefile裡面的一個關鍵字.
一般情況下,我們用.PHONY修飾僞目标,被它總是修飾的總是被執行,那麼什麼是總是被執行呢?我們先來看看什麼是總是不被執行的.
注意,現在我們的.PHONY隻是修飾的clean目标檔案,我們先看看什麼是總是不被執行的.
我們呢觀察到,我們make了多次,編譯器會告訴我們目前程式已經是最新的了,不需要執行了.這就是總是不被執行的.反之,如果我用.PHONY修飾編譯程式的那部分目标檔案,他就可以總是被執行了.不過我們不建議這麼做,主要後面公司裡面一個程式很大,明明已經是最新了的,為何還要重新編譯,沒必要.
一般我們學到基礎那裡就可以了,要是還要深入的學習,這裡可以稍微了解一下,我們可以使用一些符号來表示目标檔案和依賴檔案
目标檔案 :依賴檔案
- 冒号左邊 目标檔案 也就是 $@
- 冒号左邊 依賴檔案 也就是 $^
要是我們想要簡省依賴方法就可以寫下面的指令
gcc $^ -o $@
要是我們還想進一步省略,就要寫下面的指令,不過這裡我們就不建議了,畢竟以後的makefile又不是我們來寫的.
解釋一下
- %.o 對應.c檔案生成的.o檔案
- %.c 本目錄下所有的.c檔案
- $< 所有的.c檔案一一展開在gcc下生成對應的.o檔案
stat 指令
這裡面我們需要在看看這個指令,我記得前面好像說過一嘴,沒說也沒有關系,這裡面在說說.statu可以看一下檔案的屬性,談這個關鍵字主要是想看看編譯器是如何知道你的代碼是最新的?這是一個重點.
[bit@Qkj 08_07]$ stat main.c
在Linux中,檔案的屬性裡面一般包含三個時間,我們都知道,檔案=内容+屬性
- Access: 進入或讀取檔案的最新時間
- Modify: 修改檔案内容的最新時間
- Change: 修改檔案權限的屬性的最新時間
也就是說,我們每執行一次操作,都會導緻這三個時間的變換.例如我們如果修改一下檔案的權限,這樣就會導緻Change時間被修改.
這裡我們需要注意的是,如果我們修改檔案的内容可能也導緻檔案的屬性被修改,主要檔案的大小也是屬性的一部分,這一點要記住了.還有就是Access: 時間,在比較新的Linux核心,我們如果讀取檔案的話可能不會修改Access: 這個時間,主要是這種操作太過頻繁,一般編譯器會在執行了十次左右樣的操作才會修改Access: 時間
現在我們就知道,總是不被執行的原理.在我們進行make的時候,編譯器會先比對它們的Modify:時間,看看源檔案時間是不是大于可執行程式的時間,大于就執行,否就不執行.如果别.PHONY修飾,那麼就會自動忽略這一過程.
進度條
今天我們實作一個進度條的小玩意兒,很簡單,我們先看看成果
緩沖區
在學習計算機時,你在很多地方有可能看到到緩沖區的字樣,那什麼是緩沖區啊?,我們先看一下現象
我是在Linux環境下示範,window環境下示範可能會沒有效果。
#include <stdio.h>
int main()
{
printf("你好,緩沖區");
sleep(3); //在Linux環境下sleep的機關 秒
return 0;
}
你會發現當我們執行程式時,會有大概幾秒的延遲,可我們不是先執行的printf函數嗎,後面才是sleep,實際上這也是事實,隻不過當我執行printf後,我們想要輸出的内容被放到一個緩沖區裡面了,後面程式停留了3秒,當程序快要結束的時候,後面才會重新整理緩沖區.下面我将仔細的說一下什麼緩沖區
本質上 緩沖區就是一塊記憶體,我們将想要輸出的資料放到緩沖區中,當我們重新整理緩沖區的時候會把他列印出來,我們可以把資料當成一個水池,緩沖區看作打水的的木桶,當水桶滿了的時候或者我們就是想要一半的水,就把水倒出來。
什麼情況下緩沖區會重新整理
- 全緩沖緩沖區滿了,會重新整理
- 行緩沖我們使用換行符,緩沖區也會重新整理
- 函數緩沖我們使用某個函數強制重新整理
- 程式退出,自動重新整理
下面我們一一示範
全緩沖
我們示範一下,大家就懂了
#include <stdio.h>
int main()
{
while(1)
{
printf("hello world");
sleep(3);
}
return 0;
}
由于時間有些長,我們就不等了,結果是當緩沖區滿了後,螢幕上會出現一螢幕的 hello world
行緩沖
我們這裡的換行是指的 \n,它可以是緩沖區重新整理
#include <stdio.h>
int main()
{
while(1)
{
printf("hello world\n");
sleep(3);
}
return 0;
}
函數緩沖
在C語言中提供了一個函數,它可以強制重新整理緩沖區,這就是 fflush(),它是stdio.h中的庫函數,在一個C程式中,編譯器會預設打開 3 個标準輸入輸出流,分别是下面的,這一點知道就可以了
- stdin
- stdout
- stderr
#include <stdio.h>
int main()
{
while(1)
{
printf("hello world");
fflush(stdout);
sleep(3);
}
return 0;
}
\n 和 \r
有很多人都搞不懂這兩個有什麼差別,我們今天重點說一下
- \n叫做 換行
- \r叫做 回車
我們舉一個寫文章的例子,當我們在“我叫張三,外号法外狂徒。”這句話再開一行時,看看他們之間的差別
有人可能會感到疑惑,這和我們用的不對啊,我們之前使用下面的代碼時,都是再下一行的開頭直接列印,實際上這是C語言的編譯器預設将換行回車這兩個濃縮到 \n了
printf("hello world\n");
printf("hello world\n");
示範效果 \r
我們先示範一下效果
#include "ProcBar.h"
int main()
{
int count = 10;
while(count--)
{
printf("%d\r",count);
sleep(1);
}
printf("hello word\n");
return 0;
}
注意看,我們沒有列印出9,8,7,6,5這樣的資料,原因是緩沖區的因素,我們使用函數重新整理一下緩沖區看看效果
#include "ProcBar.h"
int main()
{
int count = 10;
while(count--)
{
printf("%d\r",count);
fflush(stdout);
sleep(1);
}
printf("hello word\n");
return 0;
}
進度條實作
有了上面的知識點,我們就可以寫出自己的進度條了,由于多檔案情況下閱讀的體驗不太好,我把代碼寫在一個檔案中
- usleep() 和 sleep()的作用一樣,不過它的機關是 毫秒
- memset() 是 string.h庫的一個函數,它可以把數組内部都初始化指定的 資料
- %-100s 設定100的字段,-表示 左對齊
- %d%% 是為了了列印 百分比 %是轉換說明,是以用兩個%代替一個% %% -> %詳細的請:point_right:轉換說明
#ifndef __INCLUDE_H__ //防止頭檔案被重複引用
#define __INCLUDE_H__
#include <stdio.h>
#include <string.h>
#endif
void proBar()
{
const char* p = "|/-\\";
int i = 0;
char arr[101] = { 0 };
memset(arr, '\0', sizeof(arr));
while (i <= 100)
{
printf("[%-100s][%d%%][%c]\r", arr, i, p[i % 4]);
fflush(stdout);
arr[i++] = '#';
usleep(80000);
}
printf("\n");
}
int main()
{
proBar();
return 0;
}