精选文章

Android下使用TCPDUMP抓包Wireshark分析数据 如果想分析Android下某个APP的网络数据交互,需要在Android手机上抓包,最常用的抓包工具非tcpdump莫属,用tcpdump生成Wireshark识别的pcap文件,然后将pcap文件下载到电脑上,用电脑上的Wireshark加载pcap文件,通过Wireshark分析tcpdump抓取的数据。...

继续阅读

Mac下部署Android开发环境附加NDK 作为开发者,我们深有体会,不管是进行什么开发,为了部署开发环境,我们往往需要折腾很长时间、查阅很多资料才能完成,而且这次折腾完了,下次到了另一台新电脑上又得重新来过,整个部署过程记得还好,要是不记得又得重新开始,而且遇到Android这种GFW阻隔了开发资源下载链接的环境部署,又尤其浪费时间。所以这也是我写下这篇教程的初衷跟动力源泉,希望大家参考了这篇教程以后可以轻轻松松在Mac系统下将Android环境部署好。...

继续阅读

稍顯嚴肅的台中 坦白說,留在腦海中的台中影像並不多,來台灣之前在Booking上只訂到了台中的一家青旅,第一次住青旅有些不習慣,幹什麼都放不開。 同屋的一個男生是台灣人,不過一年中四分之三的時間在上海跟北京,這麼說來跟我還是比較有共同話題的。得之我準備花15天的時間環島,覺得太倉促了,他們大學時期花一個半月的時間也不見得能將台灣島給逛完。我只能無奈地表示,兩岸允許的簽證時間有限,自己的空閒時間更有限,只能用打卡式的旅行了,我深知正真地旅行應該慢下來,融入當地的環境,感受他們的風土人情,但第一次只能這樣作罷,以後換成民進黨上台,形勢會變成怎樣還不得而知,能否再過來還是個未知數。而我一向信奉的人生格言是秉燭夜遊,活在當下,所以,理解自己吧。...

继续阅读

為之留戀的新竹 來新竹之前本沒有對她有過高的期待,慢慢對她加分要從桃園火車站出發前往新竹開始。 在桃園火車站的候車月台上,有醒目的旅遊資料發放處,這上面的擺放的全是新竹的旅遊宣傳資料,關鍵的是資料做得非常簡潔易懂,而接下來一天的新竹之行就全部是依據這份寶典的指引來完成的。...

继续阅读

從桃園開始台灣之行 初到台灣恰逢華夏銀行系統升級,特意準備的華夏銀聯卡在桃園機場沒能派上用場,只好用建行在機場5000塊,算下來是很不划算的,但是沒辦法,誰叫我出機場就得花錢呢。 從機場打車到桃園的酒店,花了將近六百塊新台幣,到酒店時五點多,天已經漸亮了,洗漱完等到七點吃過早餐就開始補覺囉,一覺醒來已是中午,帶著換下來的衣服外出找自助洗衣店,順便覓食。...

继续阅读

  • Prev
  • Next

Linux下的编译器

6

文章分类 : C语言, 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++ 所遵行的部分后缀名命名规范如下表所示。

后缀:表示的文件类型
.c:C 语言源代码文件;
.a:静态库文件;
.cpp/.cxx/.cc/.C:C++ 源代码文件;
.h:头文件;
.i:预处理过的 C 源代码文件;
.ii:预处理过的 C++ 源代码文件;
.m:Objective-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
06 hello.c
trevor@trevor-PC:~/linux/linux100$ gcc hello.c
trevor@trevor-PC:~/linux/linux100$ ls
06 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
06 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[语言]:指定编译的语言(C、C++、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
06 hello.c
trevor@trevor-PC:~/linux/linux100$ gcc -c hello.c
trevor@trevor-PC:~/linux/linux100$ ls
06 hello.c hello.o
trevor@trevor-PC:~/linux/linux100$ gcc hello.o
trevor@trevor-PC:~/linux/linux100$ ls
06 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
06 hello.c
trevor@trevor-PC:~/linux/linux100$ gcc -c hello.c -o test1.o
trevor@trevor-PC:~/linux/linux100$ ls
06 hello.c test1.o
trevor@trevor-PC:~/linux/linux100$ gcc test1.o -o test1
trevor@trevor-PC:~/linux/linux100$ ls
06 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
06 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
09 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
09 a.out year.c
trevor@trevor-PC:~/linux/linux100$ ./a.out
段错误
trevor@trevor-PC:~/linux/linux100$ ls
09 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
09 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
5 + 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
5 + 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
2^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
5 + 6 = 11
5^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
99table.c: In function ‘main’:
99table.c:7: error: expected ‘;’ before ‘)’ token
99table.c:7: error: expected expression before ‘)’ token
99table.c:9: error: expected ‘;’ before ‘)’ token
99table.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$

除非注明,文章均为CppLive 编程在线原创,转载请注明出处,谢谢。

本文地址:https://www.cpplive.com/html/1758.html

评论 (6)

  • 大笨兔 says:

    现在还在Windows下折腾。
    好久没写博客了,最近把博客重新换了下域名。
    换成了muyu1993.com。希望在友链中更新一下。
    以后常来常往

  • 爱早起 says:

    我从google来的,学到了很多知识,呵呵

    • CppLive says:

      谢谢爱早起同学的留言,让我觉得这个博客还有那么一丝的生机 😛

      By the way,坚持写博客哦~

      • Adoo says:

        生机与否,就看你的心了。你要是无心写了,也就段了。再说毕竟是技术博客,如果不是一方大牛,大多时候其实也只能自慰而已。如果太在意访问神马的,坚持的又会多痛苦呢。

        • CppLive says:

          最近工作比平时忙,还有一些其他的事缠绕着,下班后虽然有时间,但心里总有一块石头悬着,找不回曾经的心态,都懒得花心思写博客了。

          Adoo兄说得极是,不能指望访问量这些浮云,毕竟建博客的初衷也是为了自我实现与自我总结,肯定会坚持写下去的。希望可以尽快走出这段生活低潮期,坚持把所想所得记录下来。

          ^ ^

  • 这里因为你的留言而存在!!!

    You must be logged in to post a comment.