Linux 編譯器

簡單的說,編譯器就是一個可執行程序,它專門用於將程序員易於編寫的高級語言 (如 C 語言) 翻譯爲機器可以識別的低級語言。編譯器將源代碼編譯爲可執行程序的大致工作流程爲如下:源代碼 (source code) → 預處理 (preprocessor) → 編譯器 (compiler) → 彙編 (assembler) → 目標代碼 (object code) → 鏈接 (linker) → 可執行程序 (executables) 。Linux 下可用的編譯器有 GCC、EGCS 和 PGCC,其中最常用最受歡迎的編譯器便是 GCC,所以這裏以 GCC 爲代表來學習和使用一下 Linux 下的編譯器。

一、編譯器:GCC

1、GCC概述

GCC 起初是 GNU 推出的 C語言編譯器,用於類 Unix 系統下的編程,所以名爲 GNU C Compiler 。隨着衆多自由開發者的加入,GCC 發展迅速,如今已成爲一個支持衆多語言的編譯器了,其中包括 C、C++、Ada、Object C 和 Java 等,以至於 GCC 開始被擴展爲 GNU Compiler Collection ,也就是“GNU 編譯器集合”的意思。

GCC 通常用來編譯 C 程序和 C++ 程序,編譯 C 程序一般用 gcc,編譯 C++ 程序則用 g++,由於 C++ 兼容 C 語言,g++ 也可以編譯 C 程序。我們知道,Linux 系統不以後綴名來區分文件類型,但是 gcc 或 g++ 則需要根據後綴名來區分程序文件的類型,如果後綴名不符合規範,則會提示文件類型無法識別,gcc 或 g++ 所遵行的部分後綴名命名規範如下表所示。

後綴:表示的文件類型
.cC 語言源代碼文件;
.a:靜態庫文件;
.cpp/.cxx/.cc/.CC++ 源代碼文件;
.h:頭文件;
.i:預處理過的 C 源代碼文件;
.ii:預處理過的 C++ 源代碼文件;
.mObjective-C 源代碼文件;
.o:編譯後的目標文件;
.s:彙編語言源代碼文件;
.S:還需要預編譯的彙編語言源代碼文件。

2、GCC初識

對 GCC 有了一個概要的瞭解以後,我們不妨編寫一個簡單的 C 語言程序,然後使用 gcc 來編譯試試看。下面就以我們最爲熟悉的 Hello World 爲例,簡單演示一下 gcc 的使用方法。

(1) 編寫源程序

使用 Vim 新建一個名爲 hello.c 的 C 語言源文件,編輯如下圖中代碼所示。

#include <stdio.h>

int main(void)
{
    printf("Hello World !\n")
    return 0;
}

之所以加上頭文件 stdio.h ,是因爲函數 printf 定義在其中,否則將提示 printf 函數未定義的錯誤。

(2) 編譯源程序

使用 gcc 命令跟源代碼文件執行編譯工作,如下圖所示。

trevor@trevor-PC:~/linux/linux100$ gcc hello.c
hello.c: In function main’:
hello.c:6: error: expected ‘;’ before return
trevor@trevor-PC:~/linux/linux100$

出錯了,提示 hello.c 第 6 行 return 前面缺少分號“;”,我們觀察到,實際上是第 5 行那條打印語句後面少了一個分號“;”,這證明對於基本的語法錯誤,gcc 可以輕鬆檢測出來,只要我們讀懂了錯誤原因,問題就很容易解決。

(3) 對症下藥

使用 Vim 編輯原文件,加上分號後保存,重新執行 gcc 命令編譯 hello.c,這次通過了。如下圖所示,而且發現原目錄底下多了一個名爲 a.out 的可執行文件,執行它,輸出“Hello World !”並換行,跟程序預期的結果一樣。在不指定目標文件存儲路徑跟名字的情況下,gcc 默認在當前目錄下生成一個名爲 a.out 的可執行程序。如果想要自定義該文件名,加上“-o”參數並跟上指定的路徑或文件名即可。如下圖所示,將目標文件指定爲 hello ,編譯成功以後,當前目錄下將生成一個名爲 hello 的程序,執行它,跟 a.out 的結果一樣,只是程序名不同而已。

trevor@trevor-PC:~/linux/linux100$ vim hello.c
trevor@trevor-PC:~/linux/linux100$ ls
 hello.c
trevor@trevor-PC:~/linux/linux100$ gcc hello.c
trevor@trevor-PC:~/linux/linux100$ ls
 a.out hello.c
trevor@trevor-PC:~/linux/linux100$ ./a.out
Hello World !
trevor@trevor-PC:~/linux/linux100$ gcc hello.c -o hello
trevor@trevor-PC:~/linux/linux100$ ls
 a.out hello hello.c
trevor@trevor-PC:~/linux/linux100$ ./hello
Hello World !
trevor@trevor-PC:~/linux/linux100$

3、剖析GCC

前面我們使用 gcc 命名編譯了一個經典的 HelloWorld 程序,從而對 GCC 有了一個感性認識,但我們看到的只是 GCC 的編譯結果(包括出錯的情況),對 GCC 的具體編譯流程並不瞭解,下面就一起來了解一下。

雖然 gcc 被稱爲 C 語言的編譯器,但使用 gcc 將 C 語言源代碼文件生成可執行文件的過程不僅僅是編譯的過程,而是要經歷如下圖所示的相互關聯的幾個步驟:

GCC編譯流程圖解

GCC編譯流程圖解

源代碼(.c) -【預處理】→ 預處理文件(.i) -【編譯】→ 彙編源代碼(.s) -【彙編】→ 動態加載函數庫文件(.o) -【鏈接】→ 二進可執行文件(通常無後綴)。

如上圖所示,命令 gcc 在編譯 C 語言程序爲可執行程序的過程中,其實內部調用了不同的可執行程序,這些可執行程序隸屬於 gcc 命令,或者說這些命令統稱爲 gcc 。

1、gcc 調用 cpp 對源代碼進行預處理,主要完成對源代碼文件中包含(include)的頭文件、預編譯語句(如宏定義define等)的處理,例如對函數內部用到的宏變量進行替換等等。

2、gcc 調用 cc1 編譯預處理文件,將預處理文件內的 C 語言翻譯成彙編代碼,然後將翻譯過來的彙編源代碼保存在以“.s”爲後綴的文件中。

3、gcc 調用 as 對彙編源代碼進行彙編處理,生成以“.o”爲後綴的動態加載函數庫文件。

4、gcc 調用 ld 完成對一個或多個動態加載函數庫文件的鏈接工作,創建一個可執行文件,將所有的動態加載函數安排到可執行程序的恰當位置。

二、GCC用法

1、GCC基本用法及其選項

我們在使用 GCC 編譯源代碼文件的時候,必須指定一系列的參數跟文件名稱,gcc 或 g++ 的調用參數很多,但是基本的編譯過程中常用到的參數屈指可數,下面就讓我們一起來了解一下 GCC 的基本用法及其選項。

gcc 或 g++ 的用法跟參數含義幾乎一樣,他們最基本的用法是:

gcc/g++ [參數] [文件名]

其中參數常用到下列值:

-x[語言]:指定編譯的語言(CC++、Object C等)
-c:只編譯生成以“.o”爲後綴名的動態加載函數庫文件,而不鏈接成爲可執行文件;
-S:只編譯生成以“.s”爲後綴名的彙編源代碼,而不彙編跟鏈接成爲可執行文件;
-E:只預處理生成預處理代碼輸出到標準輸出,通常我們使用“-o”參數將其輸出內容存儲到以“.i”爲後綴名的文件中;
-o:用於指定生成文件的名字,而不採用默認名;
-g:生成可執行文件的時候加上調試工具(GNU  gdb)所必需的符號信息,當需要執行 gdb 調試時使用;
-O(或-O1):編譯、鏈接過程執行優化處理,用於提高生的成可執行文件的執行效率;
-O2:相比 -O 更高的優化級別;
-O3:相比 -O2 更高的優化級別;
-D:用於指向想要定義的宏;
-w:禁止提示警告信息;
-Wall:開啓所有警告信息,用於嚴格編譯;
-l:用於指定需要用到的函數庫名字或者頭文件存儲的目錄;
-L:用於指定函數庫的存儲目錄;
-v:顯示編譯器版本。

2、只編譯子程序(-c)

當我們爲 GCC 加上 -c 選項以後,就只會生成以“.o”爲後綴名的動態加載函數庫文件,而不會鏈接成爲可執行文件。在編譯大型工程的時候,源代碼文件很多,編譯的時間也會隨着文件的增多、代碼量的增加而增長,這時候使用 -c 選項就變得很有必要了。

試想一下,倘若一個大型項目的源代碼有數以萬計的源文件,爲了完善某些功能,我們修改了其中的個別文件,是不是意味着要將其他未修改的文件也重新編譯一遍呢?這樣的做法顯然是既浪費時間也浪費資源的,不可取,但如果我們在編譯這個龐大的項目時,使用了編譯器的 -c 選項,那麼有多少個源文件(頭文件除外)就將生成多少個與之對應的“.o”文件,最後只需要將所有已生成的“.o”文件鏈接一下生成我們想要的可執行文件即可,倘若其中某個文件被修改,只需要重新編譯被修改文件,再將所有的“.o”文件鏈接成可執行文件便是,不用編譯其他未修改的文件,是不是節省了很多時間呢?

下面我們用前面的 HelloWorld 源代碼來演示一下 -c 選項的使用,如下圖所示。

trevor@trevor-PC:~/linux/linux100$ ls
 hello.c
trevor@trevor-PC:~/linux/linux100$ gcc -c hello.c
trevor@trevor-PC:~/linux/linux100$ ls
 hello.c hello.o
trevor@trevor-PC:~/linux/linux100$ gcc hello.o
trevor@trevor-PC:~/linux/linux100$ ls
 a.out hello.c hello.o
trevor@trevor-PC:~/linux/linux100$ ./a.out
Hello World !
trevor@trevor-PC:~/linux/linux100$

我們發現,使用 -c 選項編譯出來了一個同名但後綴不同的 hello.o 文件,再對 hello.o 執行 gcc 命令,生成了名爲 a.out 的可執行程序,執行它以後得到了我們想要的結果——輸出“Hello World!”。

3、產生目標文件(-o)

上一步中,編譯生成動態加載函數庫文件跟可執行文件的名字都是編譯器爲我們默認指定的,如果加上 -c 選項以後,我們就可以讓編譯器生成我們想要的文件名了。

下面依然用 HelloWorld 源代碼來演示一下 -o 選項的使用,如下圖所示。

trevor@trevor-PC:~/linux/linux100$ ls
 hello.c
trevor@trevor-PC:~/linux/linux100$ gcc -c hello.c -o test1.o
trevor@trevor-PC:~/linux/linux100$ ls
 hello.c test1.o
trevor@trevor-PC:~/linux/linux100$ gcc test1.o -o test1
trevor@trevor-PC:~/linux/linux100$ ls
 hello.c test1 test1.o
trevor@trevor-PC:~/linux/linux100$ ./test1
Hello World !
trevor@trevor-PC:~/linux/linux100$ gcc hello.c -o test2
trevor@trevor-PC:~/linux/linux100$ ls
 hello.c test1 test1.o test2
trevor@trevor-PC:~/linux/linux100$ ./test2
Hello World !
trevor@trevor-PC:~/linux/linux100$

使用 -o 選項,我們可以個性化命名生成的文件,就像上圖所示的那樣,test1 跟 test2 以及前面演示中生成的 a.out 功能一樣,內容也是一樣的,唯獨名字不同罷了。

4、附加調試信息(-g)

現代的開發系統都具有強大的調試工具,它們成爲程序開發者跟蹤程序執行過程、解決程序潛在問題的利器,使用 GCC 開發程序也不例外,與之配套的調試工具便是 gdb ,簡稱至 GUN Debugger,我們常用它來調試 GCC 編譯生成的可執行文件,關於 gdb 的詳細內容將在《下一章》講到,這裏引入 gdb 是爲了簡單介紹一下 GCC 的 -g 選項。

默認編譯生成的可執行文件是無法使用 gdb 來跟蹤或調試的,因爲可執行程序中沒有可供 gdb 調試使用的特殊信息,爲了將必要的調試信息整合到可執行文件中,我們便需要用到 -g 選項,這樣生成的可執行程序,倘若出現問題,便可以使用 gdb 找出問題具體出現的位置,便於問題的解決。

下面我們就來製造一個“問題”程序,演示一下 -g 選項的使用,同時也體驗一下 gdb 的調試功能。

#include <stdio.h>

int main(void)
{
    int num = 365;
    printf("%s days a year\n", num);
    return 0;
}

仔細觀察以後發現,上面代碼中的“問題”出在 printf 函數上,int 類型的整數應該使用 %d 打印,使用 %s 的話,就變成打印地址爲 365 的內存區域了。如下圖所示,在編譯該程序時,警告提示 year.c 第 6 行個格式不匹配,我們忽略它,是爲了故意製造一個“問題”程序。執行該“問題”程序時,出現段錯誤,爲了記錄錯誤出現的具體位置,我們需要對 core 文件進行相關配置。

那麼什麼是 core 文件呢?其實,當 Shell 中運行的程序因爲錯誤而崩潰時,系統會自動生成一個文件用於記錄崩潰時刻的系統信息,包括內存和寄存器信息,可供程序開發者日後排查問題時使用,這個文件就是 core 文件。一般而言,core 文件存放在當前目錄,不論崩潰的程序編譯時是否加了 -g 選項,都可以使用“gdb 程序名 core文件”命令來查看程序崩潰時的相關信息,只是編譯時加了 -g 選項的程序崩潰後可以使用 gdb 通過 core 文件跟蹤到程序崩潰的具體文件、函數以及行數,而未加 -g 選項的程序崩潰後則只能通過 core 文件跟蹤到崩潰的具體函數而已。

進入 Shell 以後,core 文件的大小默認設置爲 0,這樣程序在崩潰以後系統就不會幫我們記錄 core 文件了,爲了能夠調試,我們使用命令“ulimit -c unlimited”將 core 文件大小設置爲 unlimited (無限大),當然也可以使用數字來代替 unlimited,對 core 文件的上限大小做更精確的設定。

trevor@trevor-PC:~/linux/linux100$ ls
 year.c
trevor@trevor-PC:~/linux/linux100$ gcc -g year.c
year.c: In function main’:
year.c:6: warning: format ‘%s expects type char *’, but argument 2 has type int
trevor@trevor-PC:~/linux/linux100$ ls
 a.out year.c
trevor@trevor-PC:~/linux/linux100$ ./a.out
段錯誤
trevor@trevor-PC:~/linux/linux100$ ls
 a.out year.c
trevor@trevor-PC:~/linux/linux100$ ulimit -c unlimited
trevor@trevor-PC:~/linux/linux100$ ./a.out
段錯誤 (核心已轉儲)
trevor@trevor-PC:~/linux/linux100$ ls
 a.out core year.c
trevor@trevor-PC:~/linux/linux100$ gdb a.out core
GNU gdb (GDB) 7.2-ubuntu
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/trevor/linux/linux100/a.out...done.
[New Thread 6006]

warning: Can't read pathname for load map: 輸入/輸出錯誤.
Reading symbols from /lib/libc.so.6...(no debugging symbols found)...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/ld-linux.so.2...(no debugging symbols found)...done.
Loaded symbols for /lib/ld-linux.so.2
Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
#0 0x00a53b33 in vfprintf () from /lib/libc.so.6
(gdb) bt
#0 0x00a53b33 in vfprintf () from /lib/libc.so.6
#1 0x00a5aad0 in printf () from /lib/libc.so.6
#2 0x080483ea in main () at year.c:6
(gdb) quit
trevor@trevor-PC:~/linux/linux100$

如上圖所示,執行了“ulimit -c unlimited”命令以後,程序再次崩潰時,提示“核心已轉儲”,即生成了 core 文件,執行“gdb 程序名 core文件”命令來查看程序崩潰時的相關信息,這時的 gdb 告訴我們程序最終崩潰在 vfprintf 函數,在 gdb 內再執行 bt 命令,用於查看堆棧信息,這下就一目瞭然了,分析得知程序由 year.c 的第 6 行進入 printf 函數,最終崩潰在 vfprintf 函數,這時我們便可以回到源代碼 year.c 的第 6 行分析 printf 函數是否存在問題,這樣就能很容易地發現 printf 函數中參數格式不匹配的問題。

5、多文件編譯

我們在前面編譯的都是單個文件,然而實際應用中的項目通常包含多個文件,下面就來演示一下多文件編譯。

(1)編寫 main.c 跟 add.c 兩個文件,main.c 文件內調用 add.c 文件中的 add 函數。

/* main.c */
#include <stdio.h>

int main(void)
{
    int a = 5;
    int b = 6;
    int c = 0;

    c = add(a, b);
    printf("%d + %d = %d \n", a, b, c);

    return 0;
}

/* add.c */
int add(int a, int b)
{
    return a + b;
}

(2)編譯多文件

編譯多文件主要有兩種方法,一種是將多個文件分別編譯成動態加載函數庫文件,然後再將所有的動態加載函數庫文件鏈接成一個可執行文件;一種是將多個文件直接編譯生成一個可執行程序。如下圖所示。

trevor@trevor-PC:~/linux/linux100$ ls
add.c main.c
trevor@trevor-PC:~/linux/linux100$ gcc -c add.c main.c
trevor@trevor-PC:~/linux/linux100$ ls
add.c add.o main.c main.o
trevor@trevor-PC:~/linux/linux100$ gcc add.o main.o -o add_1
trevor@trevor-PC:~/linux/linux100$ ls
add_1 add.c add.o main.c main.o
trevor@trevor-PC:~/linux/linux100$ ./add_1
 + 6 = 11
trevor@trevor-PC:~/linux/linux100$ gcc add.c main.c -o add_2
trevor@trevor-PC:~/linux/linux100$ ls
add_1 add_2 add.c add.o main.c main.o
trevor@trevor-PC:~/linux/linux100$ ./add_2
 + 6 = 11
trevor@trevor-PC:~/linux/linux100$

6、連接庫文件

當我們需要提供一些函數接口給第三方時,出於隱藏函數實現代碼或升級、集成方便的考慮,通常將這些函數接口編譯成動態庫(.so文件)或者靜態庫(.a文件),第三方如果要使用這些函數庫內的函數,則需要連接庫文件。

那麼怎樣連接庫文件呢?方法很簡單,首先將動態庫或靜態庫拷貝到系統庫所在路徑下(/usr/lib/ 或 /lib/ ),如果我們使用的是系統集成的庫文件(如 libm.so、libpthread.so 等)或者想將庫文件放在其他路徑(編譯時指定庫文件查找路徑),則不必進行這一歩;然後,在用到庫文件中函數的代碼內加載相應的頭文件(如 math.h、pthread.h);最後,編譯代碼時指定需要連接的庫名,如果庫文件不在系統庫路徑下,還需要指定庫的路徑。

下面,我們就來編寫一個調用系統數學庫函數的程序來演示一下 GCC 對庫文件的連接。

/* pow.c */
#include <stdio.h>
#include <math.h>

int main(void)
{
    float a = 2;
    float b = 10;

    printf(" %.f^%.f = %.f \n", a, b, pow(a, b));

    return 0;
}

pow 函數來自於系統數學庫 libm.so,用於計算指數,去掉前綴“lib”跟後綴“.so”,剩下的 m 即爲該庫文件的名字。如下圖所示,在編譯 pow.c 時,提示 pow 函數未定義,我們加上 -l 選項執行庫文件名爲 m 後,pow 函數被找到,編譯成功了。

trevor@trevor-PC:~/linux/linux100$ ls
pow.c
trevor@trevor-PC:~/linux/linux100$ gcc pow.c
/tmp/ccsVBC0p.o: In function `main':
pow.c:(.text+0x2d): undefined reference to `pow'
collect2: ld returned 1 exit status
trevor@trevor-PC:~/linux/linux100$ gcc pow.c -l m
trevor@trevor-PC:~/linux/linux100$ ls
a.out pow.c
trevor@trevor-PC:~/linux/linux100$ ./a.out
^10 = 1024
trevor@trevor-PC:~/linux/linux100$

7、綜合示例

前面都是比較單一的例子,下面編寫幾個稍微全面些的文件將前面介紹的主要 GCC 選項一起來演示一下。

/* add.c */
int add(int a, int b)
{
    return a + b;
}

/* head.h */
#ifndef _HEAD_H
#define _HEAD_H

#include <stdio.h>
#include <math.h>

int add(int a, int b);

#endif

/* main.c */
#include "head.h"

int main(void)
{
    int a = 5;
    int b = 6;
    int c = 0;

    c = add(a, b);
    printf(" %d + %d = %d \n", a, b, c);
    c = pow(a, b);
    printf(" %d^%d = %d \n", a, b, c);

    return 0;
}

編譯如上源代碼,附帶調試信息,然後運行程序,如下圖所示。

trevor@trevor-PC:~/linux/linux100$ ls
add.c head.h main.c
trevor@trevor-PC:~/linux/linux100$ gcc -c -g add.c main.c
trevor@trevor-PC:~/linux/linux100$ ls
add.c add.o head.h main.c main.o
trevor@trevor-PC:~/linux/linux100$ gcc add.o main.o -o math -lm
trevor@trevor-PC:~/linux/linux100$ ls
add.c add.o head.h main.c main.o math
trevor@trevor-PC:~/linux/linux100$ ./math
 + 6 = 11
^6 = 15625
trevor@trevor-PC:~/linux/linux100$

三、GCC延續

到此,我們對 GCC 已經有了一個比較全面的認識,下面就來介紹一下 GCC 編譯失敗的幾種錯誤類型及其相應的對策。

GCC 在編譯程序的過程中,一旦發現程序有錯誤,就停止編譯,放棄生成最終的可執行文件。爲了便於程序開發者修改錯誤, GCC 在遇到錯誤時將給出相關的錯誤信息,我們通過對這些錯誤信息的分析、處理,便可以更加快捷地找到錯誤原因,一步一步地修正錯誤,最終編譯出我們想要的可執行文件。GCC 給出的錯誤信息主要分爲四大類,分別是語法錯誤、缺少頭文件、缺少庫文件以及變量未定義。

1、語法,懂的,卻錯了

因爲沒有遵循語法而造成的錯誤很常見,很多時候不是我們不懂語法,而是粗心大意地將某些語法給用錯了,如下面這段代碼,用來輸出九九乘法表,大略一看,貌似沒啥錯誤,那我們就來編譯試試看。

/* 99table.c */
#include <stdio.h>

int main(void)
{
    int i, j; /*定義兩個循環變量*/

    for(i = 1, i <= 9, i++) /*控制行變量*/
    {
        for(j = 1, j <= i, j++) /*控制列變量*/
        {
            printf("%dx%d=%-2d ", i, j, i*j); /*打出數字*/
            if(i == j) /*控制換行條件,就是當i=j的時候換行*/
            {
                printf("\n"); /*換行表達式*/
            }
        }
    }

    return 0;
}

如下圖所示,當我們使用 gcc 編譯 99table.c 時,提示第 7 行和第 9 行有錯誤,具體原因是“括號前面期望分號”。回到源代碼中,第 7 行和第 9 行是 for 語句,回顧 C 語言的語法,for 語句中的三部分必須用分號分隔,而代碼中用的是逗號,因而是“括號前面期望分號”,與 gcc 提示的原因一致。

trevor@trevor-PC:~/linux/linux100$ gcc 99table.c
table.c: In function main’:
table.c:7: error: expected ‘;’ before ‘)’ token
table.c:7: error: expected expression before ‘)’ token
table.c:9: error: expected ‘;’ before ‘)’ token
table.c:9: error: expected expression before ‘)’ token
trevor@trevor-PC:~/linux/linux100$

有時候類似的一個語法錯誤,因爲連鎖反應的緣故,可能導致 GCC 報告一堆錯誤,這時候,我們就需要保持清醒的頭腦,不要被表象嚇倒,按照提示一個一個地將問題解決,必要時再參考一下相關語法教程。

2、一個都不能少的頭文件

編寫大型程序的時候,用到的函數相當之多,我們不可能也沒必要記住所有函數的頭文件,使用或編譯的時候查詢一下函數手冊,將頭文件加上即可。所以,編譯程序時,缺少頭文件的錯誤也很常見。如下面這段代碼,故意將 time_t 結構體所依賴的頭文件 time.h 註釋掉,編譯看看提示什麼錯誤。

#include <stdio.h>
//#include <time.h>

int main(void)
{
    time_t now;

    now = time(NULL);
    printf("The time now is %s", ctime(&now));

    return 0;
}

編譯如上代碼,gcc 顯示如下圖所示錯誤,提示 time_t 未定義,因爲 time_t 包含在 time.h 中,而 time.h 又被註釋掉了。

trevor@trevor-PC:~/linux/linux100$ gcc timenow.c
timenow.c: In function main’:
timenow.c:6: error: time_t undeclared (first use in this function)
timenow.c:6: error: (Each undeclared identifier is reported only once
timenow.c:6: error: for each function it appears in.)
timenow.c:6: error: expected ‘;’ before now
timenow.c:8: error: now undeclared (first use in this function)
trevor@trevor-PC:~/linux/linux100$ trevor@trevor-PC:~/linux/linux100$ ls
pthread.c
trevor@trevor-PC:~/linux/linux100$ gcc pthread.c
/tmp/cc9Ar5ti.o: In function `main':
pthread.c:(.text+0x42): undefined reference to `pthread_create'
collect2: ld returned 1 exit status
trevor@trevor-PC:~/linux/linux100$ gcc pthread.c -lpthread
trevor@trevor-PC:~/linux/linux100$ ls
a.out pthread.c
trevor@trevor-PC:~/linux/linux100$ ./a.out
this is in the old thread!
this is in the new thread!
trevor@trevor-PC:~/linux/linux100$

3、站在巨人的肩上,卻忘了巨人的存在

這裏的巨人是指函數庫,我們往往在使用了函數庫內的函數,在編譯的時候卻忘了指定要鏈接的函數庫,這是我們在用到標準庫以外的其他函數庫時常常遇到的錯誤情況。下面就一起來再現一下遺忘巨人的過程。

如下代碼在主線程中新建一個子線程,用到了 pthread 函數庫中的 pthread_create 函數。

/* pthread.c */
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void *newThread(void *argv)
{
    printf("this is in the new thread!\n");
    return NULL;
}

int main(void)
{
    pthread_t threadID;

    pthread_create(&threadID, NULL, newThread, NULL);
    printf("this is in the old thread!\n");
    sleep(1);

    return 0;
}

如下圖所示,第一次編譯的時候未用 -l 選項指定要鏈接的函數庫,導致 gcc 找不到 pthread_create 函數而提示錯誤;第二次編譯的時候雖然加上了 -l 選項,但因爲函數庫名錯誤而提示找不到函數庫;第三次編譯的時候指定 pthread 函數庫後終於編譯通過了。

trevor@trevor-PC:~/linux/linux100$ ls
pthread.c
trevor@trevor-PC:~/linux/linux100$ gcc pthread.c
/tmp/ccNtQ0RK.o: In function `main':
pthread.c:(.text+0x42): undefined reference to `pthread_create'
collect2: ld returned 1 exit status
trevor@trevor-PC:~/linux/linux100$ gcc pthread.c -lthread
/usr/bin/ld: cannot find -lthread
collect2: ld returned 1 exit status
trevor@trevor-PC:~/linux/linux100$ gcc pthread.c -lpthread
trevor@trevor-PC:~/linux/linux100$ ls
a.out pthread.c
trevor@trevor-PC:~/linux/linux100$ ./a.out
this is in the old thread!
this is in the new thread!
trevor@trevor-PC:~/linux/linux100$

4、變量未定義而使用

這裏的變量可分爲局部變量、全局變量、宏變量、函數指針等,變量未定義而使用的情況很常見,具體可以分爲如下幾種情況:

(1)局部變量未定義

局部變量因爲使用範圍有限,未定義的情況很少見,多數情況下是由變量名被寫錯造成的;

(2)全局變量未定義

被多個文件使用的全局變量,在一個文件中定義,在其他文件中使用時需要 extern 它,全局變量未定義的情況通常是忘記 extern 這個變量造成的。

(3)宏變量未定義

宏變量通常被定義在頭文件中,當我們在其他文件中使用該宏變量時,因爲沒有 include 它所在的頭文件而造成變量未定義的錯誤。

(4)函數未定義

當我們使用另外一個文件中的某個函數,在編譯的時候未將該文件包含進來,或者使用某個函數庫中的函數,編譯時卻未鏈接該函數庫,就會因爲找不到該函數的定義而出錯。

下面我們就來演示一下全局變量未定義的情況,編寫如下兩個源代碼文件,特意註釋掉 extern.c 文件中的 extern 語句。

/* define.c */
int num = 1024;

/* extern.c */
#include <stdio.h>

//extern int num;
int main(void)
{
    printf("num = %d \n", num);
    return 0;
}

編譯這兩個文件時,出現如下圖所示錯誤,提示 num 變量未定義。倘若我們去掉 extern.c 文件中 extern 語句前面的註釋,問題就可以迎刃而解了。

trevor@trevor-PC:~/linux/linux100$ gcc extern.c define.c
extern.c: In function main’:
extern.c:6: error: num undeclared (first use in this function)
extern.c:6: error: (Each undeclared identifier is reported only once
extern.c:6: error: for each function it appears in.)
trevor@trevor-PC:~/linux/linux100$
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章