shell 编程调用c程序_UNIX系统上程序员需要掌握的C编程环境的基础知识

38e5e2c71fb6c969687253b31e21475b.png

这是一份非常简短的文档,可以帮助你熟悉UNIX系统上C编程环境的基础知识。它不是面面俱到或特别详细,只是给你足够的知识让你继续学习。

关于编程的几点一般建议:如果想成为一名专业程序员,需要掌握的不仅仅是语言的语法。具体来说,应该了解你的工具,了解你的库,并了解你的文档。与C编译相关的工具是gcc、gdb和ld。还有大量的库函数也可供你使用,但幸运的是libc包含了许多功能,默认情况下它与所有C程序相关联——需要做的就是包含正确的头文件。最后,了解如何找到所需的库函数(例如,学习查找和阅读手册页)是一项值得掌握的技能。我们稍后将更详细地讨论这些内容。

欢迎加入程序员读书会,每日分享IT好书及不定期免费赠书活动

就像生活中(几乎)所有值得做的事情,成为这些领域的专家需要时间——事先花时间了解有关工具和环境的更多信息,绝对值得付出努力。

E.1 一个简单的C程序

我们从一个简单的C程序开始,它保存在文件“hw.c”中。与Java不同,文件名和文件内容之间不一定有关系。因此,请以适当的方式,利用你的常识来命名文件。

第一行指定要包含的文件,在本例中为stdio.h,它包含许多常用输入/输出函数的“原型”。我们感兴趣的是printf()。当你使用#include指令时,就告诉C预处理器(cpp)查找特定文件(例如,stdio.h),并将其直接插入到#include的代码中。默认情况下,cpp将查看目录/usr/include/,尝试查找该文件。

下面一部分指定main()函数的签名,即它返回一个整数(int),并用两个参数来调用,一个整数argc,它是命令行上参数数量的计数。一个指向字符(argv)的指针数组,每个指针都包含命令行中的一个单词,最后一个单词为null。下面的指针和数组会更多。

/* header files go up here */

/* note that C comments are enclosed within a slash and a star, and

may wrap over lines */

// if you use gcc, two slashes will work too (and may be preferred)

#include

/* main returns an integer */

int main(int argc, char *argv[]) {

/* printf is our output function;

by default, writes to standard out */

/* printf returns an integer, but we ignore that */

printf("hello, world");

/* return 0 to indicate all went well */

return(0);

}

程序然后简单打印字符串“hello,world”,并将输出流换到下一行,这是由printf()调用结束时的“”实现的。然后,程序返回一个值并结束,该值被传递回执行程序的shell。终端上的脚本或用户可以检查此值(在csh和tcsh shell中,它存储在状态变量中),以查看程序是干净地退出还是出错。

E.2 编译和执行

我们现在将学习如何编译程序。请注意,我们将使用gcc作为示例,但在某些平台上,可以使用不同的(本机)编译器cc。

在shell提示符下,只需键入:

prompt> gcc hw.c

gcc不是真正的编译器,而是所谓的“编译器驱动程序”,因此它协调了编译的许多步骤。通常有4~5个步骤。首先,gcc将执行cpp(C预处理器)来处理某些指令(例如#define和#include。程序cpp只是一个源到源的转换器,所以它的最终产品仍然只是源代码( 即一个C文件)。然后真正的编译将开始,通常是一个名为cc1的命令。这会将源代码级别的C代码转换为特定主机的低级汇编代码。然后执行汇编程序as,生成目标代码(机器可以真正理解的数据位和代码位),最后链接编辑器(或链接器)ld将它们组合成最终的可执行程序。幸运的是(!),在大多数情况下,你可以不明白gcc如何工作,只需愉快地使用正确的标志。

上面编译的结果是一个可执行文件,命名为(默认情况下)a.out。然后运行该程序,只需键入:

prompt> ./a.out

运行该程序时,操作系统将正确设置argc和argv,以便程序可以根据需要处理命令行参数。具体来说,argc将等于1,argv [0]将是字符串“./a.out”,而argv[1]将是null,表示数组的结束。

E.3 有用的标志

在继续使用C语言之前,我们首先指出一些有用的gcc编译标志。

prompt> gcc -o hw hw.c # -o: to specify the executable name

prompt> gcc -Wall hw.c # -Wall: gives much better warnings

prompt> gcc -g hw.c # -g: to enable debugging with gdb

prompt> gcc -O hw.c # -O: to turn on optimization

当然,你可以根据需要组合这些标志(例如gcc -o hw -g -Wall hw.c)。在这些标志中,你应该总是使用-Wall,这会提供很多关于可能出错的额外警告。不要忽视警告!相反,要修复它们,让它们幸福地消失。

E.4 与库链接

有时,你可能想在程序中使用库函数。因为C库中有很多函数(可以自动链接到每个程序),所以通常要做的就是找到正确的#include文件。最好的方法是通过手册页(manual page),通常称为man page。

例如,假设你要使用fork()系统调用[1]。在shell提示符下输入man fork,你将获得fork()如何工作的文本描述。最顶部的是一个简短的代码片段,它告诉你在程序中需要#include哪些文件才能让它通过编译。对于fork(),需要#include sys/types.h和unistd.h,按如下方式完成:

#include

#include

但是,某些库函数不在C库中,因此你必须做更多的工作。例如,数学库有许多有用的函数(正弦、余弦、正切等)。如果要在代码中包含函数tan(),应该先查手册页。在tan的Linux手册页的顶部,你会看到以下两行代码:

#include

...

Link with -lm.

你已经应该理解了第一行——你需要#include数学库,它位于文件系统的标准位置(即/usr/include/math.h)。但是,下一行告诉你如何将程序与数学库“链接”。有许多有用的库可以链接。其中许多都放在/usr/lib中,数学库也确实在这里。

有两种类型的库:静态链接库(以.a结尾)和动态链接库(以.so结尾)。静态链接库直接组合到可执行文件中。也就是说,链接器将库的低级代码插入到可执行文件中,从而产生更大的二进制对象。动态链接通过在程序可执行文件中包含对库的引用来改进这一点。程序运行时,操作系统加载程序动态链接库。这种方法优于静态方法,因为它节省了磁盘空间(没有不必要的大型可执行文件),并允许应用程序在内存中共享库代码和静态数据。对于数学库,静态和动态版本都可用,静态版本是/usr/lib/libm.a,动态版本是/usr/lib/libm.so。

在任何情况下,要与数学库链接,都需要向链接编辑器指定库。这可以通过使用正确的标志调用gcc来实现。

prompt> gcc -o hw hw.c -Wall -lm

-l×××标志告诉链接器查找lib×××.so或lib×××.a,可能按此顺序。如果出于某种原因,你坚持使用动态库而不是静态库,那么可以使用另一个标志——看看你是否能找到它是什么。人们有时更喜欢库的静态版本,因为使用动态库有一点点性能开销。

最后要注意:如果你希望编译器在不同于常用位置的路径中搜索头文件,或者希望它与你指定的库链接,可以使用编译器标志-I /foo/bar,来查找目录/foo/ bar中的头文件,使用-L /foo/bar标志来查找/foo/bar目录中的库。以这种方式指定的一个常用目录是“.”(称为“点”),它是UNIX中当前目录的简写。

请注意,-I标志应该针对编译,而-L标志针对链接。

E.5 分别编译

一旦程序开始变得足够大,你可能希望将其拆分为单独的文件,分别编译每个文件,然后将它们链接在一起。例如,假设你有两个文件,hw.c和helper.c,希望单独编译它们,然后将它们链接在一起。

# we are using -Wall for warnings, -O for optimization

prompt> gcc -Wall -O -c hw.c

prompt> gcc -Wall -O -c helper.c

prompt> gcc -o hw hw.o helper.o -lm

-c标志告诉编译器只是生成一个目标文件——在本例中是名为hw.o和helper.o的文件。这些文件不是可执行文件,而只是每个源文件中代码的机器代码表示。要将目标文件组合成可执行文件,必须将它们“链接”在一起。这是通过第三行gcc -o hw hw.o helper.o)完成的。在这种情况下,gcc看到指定的输入文件不是源文件(.c),而是目标文件(.o),因此直接跳到最后一步,调用链接编辑器ld将它们链接到一起,得到单个可执行文件。由于它的功能,这行通常被称为“链接行”,并且可以指定特定的链接命令,例如-lm。类似地,仅在编译阶段需要的标志,诸如-Wall和-O,就不需要包含在链接行上,只是包含在编译行上。

当然,你可以在一行中为gcc指定所有C源文件(gcc -Wall -O -o hw hw.c helper.c),但这需要系统重新编译每个源代码文件,这个过程可能很耗时。通过单独编译每个源文件,你只需重新编译编辑修改过的文件,从而节省时间,提高工作效率。这个过程最好由另一个程序make来管理,我们接下来介绍它。

E.6 Makefile文件

程序make让你自动化大部分构建过程,因此对于任何认真的程序(和程序员)来说,都是一个至关重要的工具。来看一个简单的例子,它保存在名为Makefile的文件。

要构建程序,只需输入:

prompt> make

这会(默认)查找Makefile或makefile,将其作为输入(你可以用标志指定不同的makefile,阅读手册页,找出是哪个标志)。gmake是make的gnu版本,比传统的make功能更多,所以我们将在下面的部分中重点介绍它(尽管我们互换使用这两个词)。这些讨论大多数都基于gmake的info页面,要了解如何查找这些页面,请参阅“E.8文档”部分。另外请注意:在Linux系统上,gmake和make是一样的。

hw: hw.o helper.o

gcc -o hw hw.o helper.o -lm

hw.o: hw.c

gcc -O -Wall -c hw.c

helper.o: helper.c

gcc -O -Wall -c helper.c

clean:

rm -f hw.o helper.o hw

Makefile基于规则,这些规则决定需要发生的事情。规则的一般形式是:

target: prerequisite1 prerequisite2 ...

command1

command2

...

target(目标)通常是程序生成的文件的名称。目标的例子是可执行文件或目标文件。目标也可以是要执行的操作的名称,例如在我们的示例中为“clean”。

prerequisite(先决条件)是用于生成目标的输入文件。目标通常依赖于几个文件。例如,要构建可执行文件hw,需要首先构建两个目标文件:hw.o和helper.o。

最后,command(命令)是一个执行的动作。一条规则可能有多个命令,每个命令都在自己的行上。重要提示:必须在每个命令行的开头放一个制表符!如果你只放空格,make会打印出一些含糊的错误信息并退出。

通常,命令在具有先决条件的规则中,如果任何先决条件发生更改,就要重新创建目标文件。但是,为目标指定命令的规则不需要先决条件。例如,包含delete命令、与目标“clean”相关的规则中,没有先决条件。

回到我们的例子,在执行make时,大致工作如下:首先,看到目标hw,并且意识到要构建它,它必须具备两个先决条件,hw.o和helper.o。因此,hw依赖于这两个目标文件。然后,Make将检查每个目标。在检查hw.o时,看到它取决于hw.c。这是关键:如果hw.c最近被修改,但hw.o没有被创建,make会知道hw.o已经过时,应该重新生成。在这种情况下,会执行命令行gcc -O -Wall -c hw.c,生成hw.o。因此,如果你正在编译大型程序,make会知道哪些目标文件需要根据其依赖项重新生成,并且只会执行必要的工作来重新创建可执行文件。另外请注意,如果hw.o根本不存在,也会被创建。

继续,基于上面定义的相同标准,helper.o也会重新生成或创建。当两个目标文件都已创建时,make现在可以执行命令来创建最终的可执行文件,然后返回并执行以下操作:gcc -o hw hw.o helper.o -lm。

到目前为止,我们一直没提makefile中的clean目标。

要使用它,必须明确提出要求,键入以下代码:

prompt> make clean

这会在命令行上执行该命令。因为clean目标没有先决条件,所以输入make clean将导致命令被执行。在这种情况下,clean目标用于删除目标文件和可执行文件,如果你希望从头开始重建整个程序,就非常方便。

现在你可能会想,“好吧,这似乎没问题,但这些makefile确实很麻烦!”你说得对——如果它们总是这样写的话。幸运的是,有很多快捷方式,让使用更容易。例如,这个makefile具有相同的功能,但用起来更好:

# specify all source files here

SRCS = hw.c helper.c

# specify target here (name of executable)

TARG = hw

# specify compiler, compile flags, and needed libs

CC = gcc

OPTS = -Wall -O

LIBS = -lm

# this translates .c files in src list to .o’s

OBJS = $(SRCS:.c=.o)

# all is not really needed, but is used to generate the target

all: $(TARG)

# this generates the target executable

$(TARG): $(OBJS)

$(CC) -o $(TARG) $(OBJS) $(LIBS)

# this is a generic rule for .o files

%.o: %.c

$(CC) $(OPTS) -c $< -o $@

# and finally, a clean line

clean:

rm -f $(OBJS) $(TARG)

虽然我们不会详细介绍make语法,但如你所见,这个makefile可以让生活更轻松一些。例如,它允许你轻松地将新的源文件添加到构建中,只需将它们加入makefile顶部的SRCS变量即可。你还可以通过更改TARG行轻松更改可执行文件的名称,并且可以轻松修改指定编译器,标志和库。

关于make的最后一句话:找出目标的先决条件并非总是很容易,特别是在大型复杂程序中。毫不奇怪,有另一种工具可以帮助解决这个问题,称为makedepend。自己阅读它,看看是否可以将它合并到一个makefile中。

E.7 调试

最后,在创建了良好的构建环境和正确编译的程序之后,你可能会发现程序有问题。解决问题的一种方法是认真思考——这种方法有时会成功,但往往不会。问题是缺乏信息。你只是不知道程序中到底发生了什么,因此无法弄清楚为什么它没有按预期运行。幸运的是,有某种帮助工具:gdb,GNU调试器。

将以下错误代码保存在buggy.c文件中,然后编译成可执行文件。

#include

struct Data {

int x;

};

int

main(int argc, char *argv[])

{

struct Data *p = NULL;

printf("%d

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值