目录

21世纪C语言 第1章 便利编译配置

使用包管理器

必须获取的包:

  • 编译器。必须安装gccclang可能也有用。
  • gdb,调试器
  • Valgrind,测试C内存使用错误。
  • gprof,一个分析工具
  • make,你永远不需要直接调用编译器
  • pkg-config,查找库
  • Doxygen,文档生成
  • 文本编辑器。Emacsvim
  • 自动工具:AutoconfAutomakelibtool
  • Git
  • Shell替换品,比如Z shell。

一些省去重复造轮子的C库:

  • libcURL
  • libGLib
  • libGSL
  • libSQLite3
  • libXML2

通向库的路径

1
2
3
4
5
6
7
#include <math.h>  //erf, sqrt
#include <stdio.h> //printf

int main(){
    printf("The integral of a Normal(0, 1) distribution "
           "between -1.96 and 1.96 is: %g\n", erf(1.96*sqrt(1/2.)));
}

编译器会将math.h和stdio.h文件内容粘贴进代码文件。math.h中的声明并没有说明erf函数做了什么。链接器负责找到erf,你需要告诉链接器-lm找到math库。-l指示一个库需要链接进来。你可以免费使用printf,因为链接器会用隐式的-lc将标准库libc链接进来。

如果使用gcc编译器,完整的命令包括一些额外的标志就像这样:

1
gcc erf.c -o erf -lm -g -Wall -O3 -std=gnu11

一些出名的标志

推荐使用这些编译标志:

  • -g,为调试添加符号。
  • -std=gnu11,clang-和gcc-特定,指示编译器允许遵守C11和POSIX标准的代码。POSIX标准指定系统中要有c99程序。
  • -o3,指示优化等级3,会尝试任何手段编译更快的代码。如果不需要太多优化,也可以使用**-o0**。
  • -Wall,添加编译器警告。也可以使用**-w1**,只显示编译器警告,没有附注。-Werror,编译器将会把警告视为错误。

路径

在一个典型配置中,库至少会安装在三个地方:

  • 操作系统供应商可能会定义1到2个标准目录来安装库。
  • 本地系统管理员可能有一个目录安装不想被供应商覆盖的包。
  • 用户在自己主目录可能有库目录。

假设你有一个叫libuseful的库安装在/usr/local目录。你已经把#include <useful.h>写进代码,现在你需要使用下面的命令编译代码:

1
gcc -I/usr/local/include use_useful.c -o use_useful -L/usr/local/lib -luseful
  • -I将指定路径添加进头文件搜索路径,编译器在搜索路径搜索你包含的头文件。
  • -L添加库搜索路径。
  • 链接的顺序有关系。如果你有一个名字为specific.o的文件,依赖libbroad库,且libbroad库依赖libgeneral库,那么你需要,gcc specific.o -lbroad -lgeneral。任何其它的顺序都可能会失败。

pkg-config返回已安装库的维护信息。

1
2
3
4
5
pkg-config --libs gsl libxml-2.0
pkg-config --cflags gsl libxml-2.0

-lgsl -lgslcblas -lm -lxml2
-I/usr/include/libxml2

回到前面那个命令,当你使用反撇号包含一个命令时,shell会使用其输出替换该命令。

1
gcc `pkg-config --cflags --libs gsl libxml-2.0` -o specific specific.c

等价于:

1
gcc -I/usr/include/libxml2 -lgsl -lgslcblas -lm -lxml2 -o specific specific.c

运行时链接

静态库由编译器通过拷贝库内容链接进可执行程序。共享库在运行时链接进程序,意味着和编译时一样存在库的查找问题。如果是一个在常见位置的库,运行时系统将没有查找库的问题。如果库不在标准路径,则你需要找到一种修改运行时路径查找的方法。

  • 如果使用Autotools打包程序,libtool知道如何添加正确的标志,你不需要担心它。
  • 当使用gcc,clang或icc基于libpath的库编译程序时,添加: LDADD=-Llibpath -Wl, -Rlibpath到makefile里面。-L标志告诉编译器去哪里查找库以确定符号;-Wl标志传递标志给链接器,链接器将指定-R标志的库嵌入运行时库的查找路径。pkg-config通常不知道运行时路径,因此需要手动输入。
  • 运行时,链接器将使用另一个路径查找不在常见位置也没有-Wl,R…指定的库。这个路径在shell的启动脚本里面设置:
1
2
export LD_LIBRARY_PATH=libpath:$LD_LIBRARY_PATH     #Linux, Cygwin
export DYLD_LIBRARY_PATH=libpath:$DYLD_LIBRARY_PATH    #OS X

使用Makefile

makefile提供了所有这些无止境的调整的一种解决方案。它基本上是组织的一组变量和单行shell脚本的序列。POSIX标准的make程序读取makefile里面的指令和变量,并将长且单调的命令行组合给我们。

1
2
3
4
5
6
P=program_name
OBJECTS=
CFLAGS = -g -Wall -O3
LDLIBS=
CC=c99
$(P): $(OBJECTS)

用法:

  • 一次就好:将这几行保存在.c文件同一个目录,并命名为Makefile(GNU Make)。在第一行设置你的程序名,没有.c后缀。
  • 每次需要重新编译:输入make

设置变量

shellmake使用**$指示变量的值。shell使用$var**,而make需要任何变量名长度大于1个字符的变量包含在括号中:$(var)

有几种方法告诉make变量:

  • 调用make之前设置变量并export这个变量。POSIX标准命令行设置CFLAGS变量:export CFLAGS=’-g -Wall -O3’
  • 你可以将这些export命令放进shell启动脚本,比如.bashrc或.zshrc。
  • 你可以在命令之前赋值设置一个变量。PANTS=kakhi env | grep PANTS。等号两边不能有空格,因为空格是用来区分命令和赋值的。
  • 早期的makefile可以在文件头设置变量。在makefile文件里面,等号两边可以有空格。
  • make允许在命令行设置变量,独立于shell
1
2
make CFLAGS="-g -Wall"  # Set a makefile variable.
CFLAGS="-g -Wall" make  # Set an environment variable visible to make and its children.

C语言中的环境变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <stdlib.h> //getenv, atoi
#include <stdio.h> //printf

int main(){
    char *repstext = getenv("reps");
    int reps = repstext ? atoi(repstext) : 10;

    char *msg = getenv("msg");
    if (!msg) msg = "Hello.";

    for (int i = 0; i < reps; ++i)
        printf("%s\n", msg);
}

reps=10 msg="Ha" ./getenv
msg="Ha" ./getenv
reps=20 msg=" " ./getenv

make也提供一些内置变量:

  • $@ 所有的目标文件。目标文件是源文件编译完生成的中间文件(.o文件)。
  • $* 去掉后缀的目标文件。如果目标文件是prog.o,则**$是prog,且$**.c就是prog.c
  • $< 引起目标被触发并编译的文件名。如果我们编译prog.o,可能因为prog.c最近被修改了,所以**$<**就是prog.c。

规则

除了设置变量,makefile的片段具有以下形式:

1
2
target: dependencies
        script

如果通过命令make target目标被调用,则依赖被检查。如果目标是一个文件,依赖也全部是文件,且目标比依赖新,则文件是最新的,没有什么事要做。否则,目标的处理被暂停,依赖被运行或生成,可能通过另一个目标,当依赖的脚本都完成了,目标的脚本开始运行。

1
2
3
4
5
6
7
8
all: html doc publish
doc:
    pdflatex $(f).tex
html:
    latex -interaction batchmode $(f)
    latex2html $(f).tex
publish:
    scp $(f).pdf $(Blogserver)

在前面简单的makefile里面,我们只有一个目标/依赖/脚本组合。比如:

1
2
3
P=domath
OBJECTS=addition.o subtraction.o
$(P): $(OBJECTS)

P=domath是需要编译的程序,它依赖对象文件addition.o和substration.o。因为addition.o没有作为目标列出来,make使用隐式规则将.c文件编译成.o文件。同样的操作处理substraction.o和domath.o(GNU make隐式假定domath依赖domath.o)。当所有对象被编译时,我们没有脚本规则建立$(P)目标,GNU make填写默认脚本,链接.o文件成可执行程序。

POSIX标准make将.c文件编译成.o文件的默认规则:

1
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $*.c

$(CC)变量代表你的C编译器;POSIX标准指定默认CC=c99。$(CFLAGS)设置为之前的标志。$(LDFLAGS)没有设置因此为空。

GNU make将目标文件编译成可执行程序的默认规则:

1
$(CC) $(LDFLAGS) $@ $(LDLIBS)

回忆一下链接的顺序很重要,因此我们需要两个链接器变量。

1
2
cc specific.o -lbroad -lgeneral
LDLIBS=-lbroad -lgeneral

如果想要看完整的make默认规则和内置变量,试一试:make -p > default_rules

这就是游戏规则:查找正确的变量并在makefile里面设置。

  • CFLAGS变量是一个根深蒂固的习俗,但是为链接器设置的变量在每个系统都不一样。甚至LDLIBS也不是POSIX标准,它只是GNU make使用。
  • CFLAGSLDLIBS变量是我们指定所有编译器标志并查找和指定库。如果你有pkg-config,使用反撇号调用。
1
2
CFLAGS=`pkg-config --cflags apophenia glib-2.0` -g -Wall -std=gnu11 -O3
LDLIBS=`pkg-config --libs apophenia glib-2.0`

或者手动指定**-I**,-L和**-l**标志:

1
2
CFLAGS=-I/home/b/root/include -g -Wall -O3
LDLIBS=-L/home/b/root/lib -lweirdlib
  • 在你将一个库和其路径添加进LDLIBSCFLAGS后,没有理由再去除它。你不会在意最终的可执行程序可能大一点。而且这样也可以makefile在各个工程里面不太需要修改。
  • 如果你的程序需要更多C文件,在makefile中添加name.o到OBJECTS。
  • 如果你的程序只有一个.c文件,你可能根本不需要makefile。你可以使用下面的方法使用make:
1
2
3
export CFLAGS='-g -Wall -O3 -std=gnu11'
export LDLIBS='-lm'
make erf

从源文件使用库

可以通过编译源代码来安装库。下面用GSL(GNU Scientific Library)库作为例子。假设你有root权限:

1
2
3
4
5
6
wget ftp://ftp.gnu.org/gnu/gsl/gsl-2.1.tar.gz
tar xvzf gsl-2.1.tar.gz
cd gsl-2.1
./configure
make
sudo make install

如果没有出错的话,GSL就已经安装好了。下面是一个简单的使用gsl库的程序:

1
2
3
4
5
6
7
#include <gsl/gsl_cdf.h>
#include <stdio.h>

int main(){
    double bottom_tail = gsl_cdf_gaussian_P(-1.96, 1);
    printf("Area between [-1.96, 1.96]: %g\n", 1-2*bottom_tail);
}

要使用你安装的库,你需要修改你的makefile,取决与你是否有pkg-config,你可以使用其中一个:

1
LDLIBS=`pkg-config --libs gsl`

或:

1
LDLIBS=-lgsl -lgslcblas -lm

如果你没有安装在标准位置且没有pkg-config,你需要添加路径:

1
2
CFLAGS=-I/usr/local/include
LDLIBS=-L/usr/local/lib -Wl,-R/usr/local/lib

从源文件使用库(即使你的系统管理员不允许)

首先创建一个目录,比如:

1
mkdir ~/root

接着添加路径:

1
PATH=~/root/bin:$PATH

在makefile中添加新路径:

1
2
LDLIBS=-L$(HOME)/root/lib (plus the other flags, like -lgsl -lm ...)
CFLAGS=-I$(HOME)/root/include (plus -g -Wall -O3 ...)

将所要的库安装到指定路径:

1
./configure --prefix=$HOME/root && make && make install

通过嵌入文档编译C程序

你已经看过编译的模式很多次了:

  1. 设置一个变量代表编译的标志
  2. 设置一个变量代表链接的标志,每一个你使用的库包括一个-l标志
  3. 使用make或IDE将变量转换为完整的编译和链接命令。

从命令行包含头文件

gccclang有一个方便的标志包含头文件,比如:

1
gcc -include stdio.h

和这句一样:

1
#include <stdio.h>

-include是编译器特定的。

统一的头文件

为了有用,头文件必须包含typedefs,宏定义和函数声明,且不应该包含没有不会使用的定义或声明。而现在的趋势是节省用户时间,将多个头文件包含进一个头文件。

嵌入文档

嵌入文档是一个POSIX标准shell的特性,你可以用在C,Python,Perl或其他。

1
2
3
4
python - <<"XXXX"
lines=2
print "\nThis script is %i lines long.\n" %(lines,)
XXXX
  • 嵌入文档是shell的标准特性,因此它应该能在任何POSIX系统上工作。
  • “XXXX"是任意你喜欢的字符串;“EOF"很流行,”—–“看起来不错只要顶部和底部的破折号数量相同既可以。当shell看到你选择的字符串为独立的一行,它将停止发送脚本到程序stdin。
  • 有一个变体以«-开始,它会删除没一行开头的所有tab字符。
  • 作为另一个变体,«XXXX和«“XXXX"不同。前面那个可以插入$shell_variable。

从标准输入编译

1
2
3
4
5
6
7
go_libs="-lm"
go_flags="-g -Wall -include allheads.h -O3"
alias go_c="c99 -xc - $go_libs $go_flags"
go_c << '---'
int main(){printf("Hello from the command line.\n");}
---
./a.out