如何使用gcc編譯器

来源:http://www.cnblogs.com/dengxiangliu/archive/2017/02/03/6363296.html
-Advertisement-
Play Games

要想讀懂本文,你需要對C語言有基本的瞭解,本文將介紹如何使用gcc編譯器。 首先,我們介紹如何在命令行的方式下使用編譯器編譯簡單的C源代碼。 然後,我們簡要介紹一下編譯器究竟作了哪些工作,以及如何控制編譯的過程。 我們也簡要介紹了調試器的使用方法。 gcc介紹 你能想象使用封閉源代碼的私有編譯器編譯 ...


  要想讀懂本文,你需要對C語言有基本的瞭解,本文將介紹如何使用gcc編譯器。 首先,我們介紹如何在命令行的方式下使用編譯器編譯簡單的C源代碼。 然後,我們簡要介紹一下編譯器究竟作了哪些工作,以及如何控制編譯的過程。 我們也簡要介紹了調試器的使用方法。

gcc介紹

  你能想象使用封閉源代碼的私有編譯器編譯自由軟體嗎?你怎麼知道編譯器在你的可執行文件中加入了什麼?可能會加入各種後門和木馬。Ken Thompson是一個著名的黑客,他編寫了一個編譯器,當編譯器編譯自己時,就在'login'程式中留下後門和永久的木馬。請到這裡閱讀他對這個傑作的描述。幸運的是,我們有了gcc。當你進行configure; make; make install 時,gcc在幕後做了很多繁重的工作。如何才能讓gcc為我們工作呢?我們將開始編寫一個紙牌游戲, 不過我們只是為了演示編譯器的功能,所以儘可能地精簡了代碼。 我們將從頭開始一步一步地做,以便理解編譯過程,瞭解為了製作可執行文件需要做些什麼,按什麼順序做。我們將看看如何編譯C程式,以及如何使用編譯選項讓gcc按照我們的要求工作。步驟(以及所用工具)如下: 預編譯 (gcc -E), 編譯 (gcc), 彙編 (as),和 連接 (ld)。

1、開始

  首先,我們應該知道如何調用編譯器。實際上,這很簡單。我們將從那個著名的第一個C程式開始。

#include <stdio.h>
int main(int argc,char **argv)
{
  printf("Hello World!\n");
}

 

把這個文件保存為 game.c,你可以在命令行下編譯它:

gcc game.c

在預設情況下,C編譯器將生成一個名為 a.out的可執行文件。 你可以鍵入如下命令運行它:

a.out
 
Hello World

  每一次編譯程式時,新的 a.out將覆蓋原來的程式。你無法知道是哪個程式創建了a.out。我們可以通過使用 -o 編譯選項,告訴 gcc我們想把可執行文件叫什麼名字。我們將把這個程式叫做game,我們可以使用任何名字,因為C沒有Java那樣的命名限制。

gcc -o game game.c
然後:
Game
輸出:
Hello World
 

  到現在為止,我們離一個有用的程式還差得很遠。如果你覺得沮喪,你可以想一想我們已經編譯並運行了一個程式。因為我們將一點一點為這個程式添加功能,所以我們必須保證讓它能夠運行。似乎每個剛開始學編程的程式員都想一下子編一個1000行的程式, 然後一次修改所有的錯誤。沒有人,我是說沒有人,能做到這個。你應該先編一個可以運行的小程式,修改它,然後再次讓它運行。這可以限制你一次修改的錯誤數量。另外,你知道剛纔做了哪些修改使程式無法運行,因此你知道應該把註意力放在哪裡。這可以防止這樣的情況出現:你認為你編寫的東西應該能夠工作,它也能通過編譯,但它就是不能運行。請切記,能夠通過編譯的程式並不意味著它是正確的。

下一步為我們的游戲編寫一個頭文件。頭文件把數據類型和函數聲明集中到了一處。這可以保證數據結構定義的一致性,以便程式的每一部分都能以同樣的方式看待一切事情。

#ifndef DECK_H
#define DECK_H
 
#define DECKSIZE 52
typedef struct deck_t
{
  int card[DECKSIZE];
  /* number of cards used */
  int dealt;
}deck_t;
 
#endif /* DECK_H */

 

  把這個文件保存為 deck.h,只能編譯 .c 文件,所以我們必須修改 game.c。在game.c的第2行,寫上 #include "deck.h"。 在第5行寫上 deck_t deck;為了保證我們沒有搞錯,把它重新編譯一次。

gcc -o game game.c

如果沒有錯誤,就沒有問題。如果編譯不能通過,那麼就修改它直到能通過為止。

2、預編譯

  編譯器是怎麼知道 deck_t 類型是什麼的呢?因為在預編譯期間, 它實際上把"deck.h"文件複製到了"game.c"文件中。源代碼中的預編譯指示以"#"為首碼。 你可以通過在gcc後加上 -E 選項來調用預編譯器。

 
gcc -E -o game_precompile.txt game.c
wc -l game_precompile.txt
  3199 game_precompile.txt

  幾乎有3200行的輸出!其中大多數來自 stdio.h 包含文件,但是如果你查看這個文件的話,我們的聲明也在那裡。如果你不用 -o 選項指定輸出文件名的話,它就輸出到控制台。預編譯過程通過完成三個主要任務給了代碼很大的靈活性。

  1. 把"include"的文件拷貝到要編譯的源文件中。
  2. 用實際值替代"define"的文本。
  3. 在調用巨集的地方進行巨集替換。

  這就使你能夠在整個源文件中使用符號常量(即用DECKSIZE表示一付牌中的紙牌數量), 而符號常量是在一個地方定義的,如果它的值發生了變化,所有使用符號常量的地方都能自動更新。在實踐中,你幾乎不需要單獨使用 -E 選項,而是讓它把輸出傳送給編譯器。

3、編譯

  作為一個中間步驟,gcc把你的代碼翻譯成彙編語言。它一定要這樣做,它必須通過分析你的代碼搞清楚你究竟想要做什麼。如果你犯了語法錯誤,它就會告訴你,這樣編譯就失敗了。 人們有時會把這一步誤解為整個過程。但是,實際上還有許多工作要gcc去做呢。

4、彙編

  as 把彙編語言代碼轉換為目標代碼。事實上目標代碼並不能在CPU上運行,但它離完成已經很近了。編譯器選項 -c 把 .c 文件轉換為以 .o 為擴展名的目標文件。如果我們運行

gcc -c game.c

我們就自動創建了一個名為game.o的文件。這裡我們碰到了一個重要的問題。我們可以用 任意一個 .c 文件創建一個目標文件。正如我們在下麵所看到的,在連接步驟中我們可以把這些目標文件組合成可執行文件。讓我們繼續介紹我們的例子。因為我們正在編寫一個紙牌游戲,我們已經把一付牌定義為 deck_t,我們將編寫一個洗牌函數。 這個函數接受一個指向deck類型的指針,並把一付隨機的牌裝入deck類型。它使用'drawn' 數組跟蹤記錄那些牌已經用過了。這個具有DECKSIZE個元素的數組可以防止我們重覆使用一張牌。

#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include "deck.h"
 
static time_t seed = 0;
 
void shuffle(deck_t *pdeck)
{
  /* Keeps track of what numbers have been used */
  int drawn[DECKSIZE] = {0};
  int i;
 
  /* One time initialization of rand */
  if(0 == seed)
  {
    seed = time(NULL);
    srand(seed);
  }
  for(i = 0; i < DECKSIZE; i++)
  {
    int value = -1;
    do
    {
      value = rand() % DECKSIZE;
    }
    while(drawn[value] != 0);
 
    /* mark value as used */
    drawn[value] = 1;
 
    /* debug statement */
    printf("%i\n", value);
    pdeck->card[i] = value;
  }
  pdeck->dealt = 0;
  return;
}

 

把這個文件保存為 shuffle.c。我們在這個代碼中加入了一條調試語句,以便運行時,能輸出所產生的牌號。這並沒有為我們的程式添加功能,但是現在到了關鍵時刻,我們看看究竟發生了什麼。因為我們的游戲還在初級階段,我們沒有別的辦法確定我們的函數是否實現了我們要求的功能。使用那條printf語句,我們就能準確地知道現在究竟發生了什麼,以便在開始下一階段之前我們知道牌已經洗好了。在我們對它的工作感到滿意之後,我們可以把那一行語句從代碼中刪掉。這種調試程式的技術看起來很粗糙,但它使用最少的語句完成了調試任務。以後我們再介紹更複雜的調試器。

請註意兩個問題

  1. 我們用傳址方式傳遞參數,你可以從'&'(取地址)操作符看出來。這把變數的機器地址傳遞給了函數,因此函數自己就能改變變數的值。也可以使用全局變數編寫程式,但是應該儘量少使用全局變數。指針是C的一個重要組成部分,你應該充分地理解它。
  2. 我們在一個新的 .c 文件中使用函數調用。操作系統總是尋找名為'main'的函數,並從那裡開始執行。 shuffle.c中沒有'main'函數,因此不能編譯為獨立的可執行文件。我們必須把它與另一個具有'main'函數並調用'shuffle'的程式組合起來。

運行命令

gcc -c shuffle.c

並確定它創建了一個名為shuffle.o 的新文件。編輯game.c文件,在第7行,在 deck_t類型的變數 deck 聲明之後,加上下麵這一行:

shuffle(&deck);

現在,如果我們還象以前一樣創建可執行文件,我們就會得到一個錯誤

gcc -o game game.c
/tmp/ccmiHnJX.o: In function `main':
/tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle'
collect2: ld returned 1 exit status

編譯成功了,因為我們的語法是正確的。但是連接步驟卻失敗了,因為 我們沒有告訴編譯器'shuffle'函數在哪裡。 那麼,到底什麼是連接?我們怎樣告訴編譯器到哪裡尋找這個函數呢?

5、連接

連接器ld,使用下麵的命令,接受前面由 as 創建的目標文件並把它轉換為可執行文件

gcc -o game game.o shuffle.o

這將把兩個目標文件組合起來並創建可執行文件 game

  連接器從shuffle.o目標文件中找到 shuffle 函數,並把它包括進可執行文件。 目標文件的真正好處在於,如果我們想再次使用那個函數,我們所要做的就是包含"deck.h" 文件並把 shuffle.o 目標文件連接到新的可執行文件中。

  像這樣的代碼重用是經常發生的。雖然我們並沒有編寫前面作為調試語句調用的 printf 函數,連接器卻能從我們用 #include <stdlib.h> 語句包含的文件中找到它的聲明,並把存儲在C庫(/lib/libc.so.6)中的目標代碼連接進來。這種方式使我們可以使用已能正確工作的其他人的函數,只關心我們所要解決的問題。 這就是為什麼頭文件中一般只含有數據和函數聲明,而沒有函數體。一般,你可以為連接器創建目標文件或函數庫,以便連接進可執行文件。我們的代碼可能產生問題,因為在頭文件中我們沒有放入任何函數聲明。為了確保一切順利,我們還能做什麼呢?

6、另外兩個重要選項

-Wall 選項可以打開所有類型的語法警告,以便幫助我們確定代碼是正確的, 並且儘可能實現可移植性。當我們使用這個選項編譯我們的代碼時,我們將看到下述警告:

game.c:9: warning: implicit declaration of function `shuffle'

  這讓我們知道還有一些工作要做。我們需要在頭文件中加入一行代碼,以便告訴編譯器有關 shuffle 函數的一切,讓它可以做必要的檢查。聽起來象是一種狡辯,但這樣做可以把函數的定義與實現分離開來,使我們能在任何地方使用我們的函數,只要包含新的頭文件並把它連接到我們的目標文件中就可以了。下麵我們就把這一行加入deck.h中。

void shuffle(deck_t *pdeck);

這就可以消除那個警告信息了。

  另一個常用編譯器選項是優化選項 -O# (即 -O2)。 這是告訴編譯器你需要什麼級別的優化。編譯器具有一整套技巧可以使你的代碼運行得更快一點。 對於象我們這種小程式,你可能註意不到差別,但對於大型程式來說,它可以大幅度提高運行速度。 你會經常碰到它,所以你應該知道它的意思。

7、調試

  我們都知道,代碼通過了編譯並不意味著它按我們得要求工作了。你可以使用下麵的命令驗證是否所有的號碼都被使用了

game | sort - n | less

並且檢查有沒有遺漏。如果有問題我們該怎麼辦?我們如何才能深入底層查找錯誤呢?

  你可以使用調試器檢查你的代碼。大多數發行版都提供著名的調試器:gdb。如果那些眾多的命令行選項讓你感到無所適從,那麼你可以使用KDE提供的一個很好的前端工具 KDbg。 還有一些其它的前端工具,它們都很相似。要開始調試,你可以選擇 File->Executable 然後找到你的 game 程式。 當你按下F5鍵或選擇 Execution->從菜單運行時,你可以在另一個視窗中看到輸出。 怎麼回事?在那個視窗中我們什麼也看不到。不要擔心,KDbg沒有出問題。問題在於我們在可執行文件中沒有加入任何調試信息,所以KDbg不能告訴我們內部發生了什麼。編譯器選項 -g 可以把必要的調試信息加入目標文件。你必須用這個選項編譯目標文件 (擴展名為.o),所以命令行成了:

gcc -g -c shuffle.c game.c
gcc -g -o game game.o shuffle.o

  這就把鉤子放入了可執行文件,使gdb和KDbg能指出運行情況。調試是一種很重要的技術,很值得你花時間學習如何使用。調試器幫助程式員的方法是它能在源代碼中設置“斷點”。現在你可以用右鍵單擊調用 shuffle 函數的那行代碼,試著設置斷點。那一行邊上會出現一個紅色的小圓圈。現在當你按下F5鍵時,程式就會在那一行停止執行。按F8可以跳入shuffle函數。我們現在可以看到 shuffle.c 中的代碼了!我們可以控製程序一步一步地執行, 並看到究竟發生了什麼事。如果你把游標暫停在局部變數上,你將能看到變數的內容。 太好了。這比那條 printf 語句好多了,是不是?

8、小結

  本文大體介紹了編譯和調試C程式的方法。我們討論了編譯器走過的步驟,以及為了讓編譯器做這些工作應該給gcc傳遞哪些選項。我們簡述了有關連接共用函數庫的問題, 最後介紹了調試器。真正瞭解你所從事的工作還需要付出許多努力,但我希望本文能讓你正確地起步。你可以在 gccasldmaninfo page中找到更多的信息。

自己編寫代碼可以讓你學到更多的東西。作為練習你可以以本文的紙牌游戲為基礎,編寫 一個21點游戲。那時你可以學學如何使用調試器。使用GUI的KDbg開始可以更容易一些。 如果你每次只加入一點點功能,那麼很快就能完成。切記,一定要保持程式一直能運行!

要想編寫一個完整的游戲,你需要下麵這些內容:

  • 一個紙牌玩家的定義(即,你可以把deck_t定義為player_t)。
  • 一個給指定玩家發一定數量牌的函數。記住在紙牌中要增加“已發牌”的數量,以便 能知道還有那些牌可發。還要記住玩家手中還有多少牌。
  • 一些與用戶的交互,問問玩家是否還要另一張牌。
  • 一個能列印玩家手中的牌的函數。 card 等於value % 13 (得數為0到12),suit 等於 value / 13 (得數為0到3)。

一個能確定玩家手中的value的函數。Ace的value為零並且可以等於1或11。King的value為12並且可以等於10。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • lambda 傳遞ref參數有個語法bug,必須要顯式書寫參數類型。 //如 delegate bool FuncType(ref int num); FuncType func1; func1 = num => true; //錯 func1 = (ref num) => true;//錯 fun... ...
  • 上一篇在C++中反射調用.NET(一)中,我們簡單的介紹瞭如何使用C++/CLI並且初步使用了反射調用.NET程式集的簡單方法,今天我們看看如何在C++與.NET程式集之間傳遞複雜對象。 ...
  • python爆破定長密碼的腳本 ...
  • 指針與地址的加減 摘自 "http://http://www.cnblogs.com/haore147/p/3647231.html" 如上,a是一個一維數組,數組中有5個元素,所以 a的類型是數組指針;ptr是一個int 型的指針,ptr的類型是整型指針 。 1. &a + 1:取數組a 的首地址 ...
  • 四、填色 color 函數有三個參數。第一個參數指定有多少紅色,第二個指定有多少綠色,第三個指定有多少藍色。比如,要得到車子的亮紅色,我們用 color(1,0,0),也就是讓海龜用百分之百的紅色畫筆。 這種紅色、綠色、藍色的混搭叫做RGB(Red,Green,Blue)。因為紅綠藍是色光上的三原色 ...
  • 開發Python的環境有很多,原來已經在vs2013上面搭建好python的開發環境了,但是vs2013每次啟動都占太多記憶體(太強大了吧),這下出了vs code,既輕量又酷炫,正好拿來試一試開發python,點擊visual studio code1.9安裝教程 下麵直接上搭建Python環境步驟 ...
  • 1. 什麼是緩存? 資料庫的緩存指的是應用程式和物理數據源之間的數據。即把物理數據源的數據複製到緩存。有了緩存,可以降低應用程式對物理數據源的訪問頻率,從而提高效率。緩存的介質一般是記憶體,也可以是硬碟。 Hibernate的緩存有三種類型:一級緩存、二級緩存和查詢緩存。 2. 一級緩存 一級緩存即S ...
  • 在Python里,海龜不僅可以畫簡單的黑線,還可以用它畫更複雜的幾何圖形,用不同的顏色,甚至還可以給形狀填色。 一、從基本的正方形開始 引入turtle模塊並創建Pen對象: 前面我們用來創建正方形的代碼如下: 此段代碼太長,我們可以用for迴圈進行優化: 效果如下: 二、畫星星 我們只需把for循 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...