Compilation options
Setting search paths
默认情况下,gcc 从
/usr/local/include/
和 /usr/include/
搜索头文件,从 /usr/local/lib/
和 /usr/lib/
搜索库文件。
The list of directories for header files is often referred to as the include path, and the list of directories for libraries as the library search path or link path.
这些目录按顺序搜索,默认搜索路径还可能包括其他系统相关或 site-specific 站点特定目录,以及 GCC 安装本身中的目录。例如,在 64 位平台上,默认情况下还可能搜索其他 lib64
目录。
当其他目录中安装了别的库时,需要扩展搜索路径,以便找到库。编译器选项 -I
和 -L
分别将新目录添加到 include path
和 library search path
的开头。
假设 dbmain.c 依赖的头文件和库都不在默认搜索路径下,那么需要手动指定 gcc -Wall -I/opt/gdbm-1.8.3/include -L/opt/gdbm-1.8.3/lib dbmain.c -lgdbm
。-l
告诉 gcc 链接哪个库,否则 gcc 不知道需要的函数在哪,-L
告诉 gcc 去哪找。
请注意,永远不应将头文件的绝对路径放在源代码中的 #include
语句中,因为这将阻止程序在其他系统上进行编译。应始终使用 -I
选项或下面描述的 INCLUDE_PATH
变量来设置头文件的包含路径。
Environment variables
头文件和库的搜索路径也可以通过 shell 中的环境变量来控制,或者放到 startup file 中,比如 .bash_profile
。
可以使用环境变量 C_INCLUDE_PATH
(对于 C 头文件)或 CPLUS_INCLUDE_PATH
(对于 C++ 头文件)将附加目录添加到 include path
。比如 export C_INCLUDE_PATH=/opt/gdbm-1.8.3/include
。
通过这种方式指定的目录会在命令行 -I
指定的目录之后在系统标准默认目录 /usr/local/include
和 /usr/include
之前搜索。
相似的,额外的目录可以通过环境变量 LIBRARY_PATH
加到 link path
中,比如 export LIBRARY_PATH=/opt/gdbm-1.8.3/lib
。
通过这种方式指定的目录会在命令行 -L
指定的目录之后在系统标准默认目录 /usr/local/lib
和 /usr/lib
之前搜索。
通过环境变量配置后,上面的编译命令可以简化为 gcc -Wall dbmain.c -lgdbm
。
命令行 –> 环境变量 –> 默认。
Extended search paths
按照 Unix 标准的搜索路径约定,可以在环境变量中以冒号分隔的列表形式一起指定多个目录,DIR1:DIR2:DIR3
这些目录按从左往右的顺序搜索,.
代表当前目录。The current directory can also be specified using an empty path element. For example, :DIR1:DIR2
is equivalent to .:DIR1:DIR2
.
1 | C_INCLUDE_PATH=.:/opt/gdbm-1.8.3/include:/net/include |
如果需要在命令行中指定多个目录,-I
和 -L
可以重复使用。gcc -I. -I/opt/gdbm-1.8.3/include -I/net/include -L. -L/opt/gdbm-1.8.3/lib -L/net/lib
当环境变量和命令行选项一起使用时,编译器按以下顺序搜索目录:
- command-line options
-I
and-L
, from left to right - directories specified by environment variables, such as
C_INCLUDE_PATH
andLIBRARY_PATH
- default system directories
在日常使用中,通常使用选项 -I
和 -L
将目录添加到搜索路径中。
Shard libraries and static libraries
如果直接执行上面编译后的文件会报错:
1 | ./a.out: error while loading shared libraries: |
这是因为 GDBM 包提供了一个 shard library。这种类型的库需要特别的处理,它必须首先从磁盘上加载。
External libraries are usually provided in two forms: static libraries and shared libraries.
静态库 static library 通常是 .a
结尾的文件,当一个程序链接了静态库,该程序所使用的任何外部函数的目标文件中的机器代码都会从库中复制到最终的可执行文件中。
共享库采用更高级的链接形式处理,可使可执行文件更小。它们使用扩展名 .so
,代表共享对象。
An executable file linked against a shared library contains only a small table of the functions it requires, instead of the complete machine code from the object files for the external functions.
Before the executable file starts running, the machine code for the external functions is copied into memory from the shared library file on disk by the operating system—a process referred to as dynamic linking.
动态链接使得可执行程序文件更小,节省磁盘空间,因为一个共享库能被多个程序使用。大多数的操作系统提供了虚拟内存机制,允许物理内存中的一个共享库被所有正在运行的程序使用,节省了内存和磁盘。
此外,共享库的升级迭代不需要重新编译使用它的程序,只要共享库的接口没变。
基于这些优点,在大多数的系统上,如果可以使用共享库, gcc 默认使用共享库来编译程序,每当使用静态库 libNAME.a
与选项 -lNAME
进行链接时,编译器首先检查具有相同名称和 .so
扩展名的替代共享库。
在这种情况下,当编译器在 link path 中搜索 libgdbm
库时,它会在 /opt/gdbm-1.8.3/lib
目录下发现 libgdbm.a
和 libgdbm.so
,于是共享库被优先使用了。
但是,当可执行文件启动时,它的加载函数 loader function 必须找到共享库才能将其加载到内存中。默认情况下,加载器仅在预定义的一组系统目录中搜索共享库,例如 /usr/local/lib
和 /usr/lib
,如果库不在这些目录之一中,则必须将其添加到加载路径中 load path。
注意,包含共享库的目录原则上可以使用链接器选项 -rpath
存储(硬编码
hard-code
)在可执行文件本身中,但通常不会这样做,因为如果移动库或将可执行文件复制到另一个系统,就会产生问题。
设置加载路径最简单的方法是通过环境变量 LD_LIBRARY_PATH,可以用冒号分隔多个目录。
系统级别的共享库路径配置可以在 loader conf file /etc/ld.so.conf
中定义。
当然如果不想使用共享库只想用静态库,可以通过 -static
来实现,gcc -Wall -static -I/opt/gdbm-1.8.3/include/ -L/opt/gdbm-1.8.3/lib/ dbmain.c -lgdbm
, -static
避免使用共享库。
同样可以直接指定具体的静态库文件,而不使用 -l
链接:
gcc -Wall -I/opt/gdbm-1.8.3/include dbmain.c /opt/gdbm-1.8.3/lib/libgdbm.a
link with static libgcc -Wall -I/opt/gdbm-1.8.3/include dbmain.c /opt/gdbm-1.8.3/lib/libgdbm.so
link with shared lib,此时仍然需要设置LD_LIBRARY_PATH
共享库是被 loader function 加载的。
C language standards
默认情况下,gcc
使用 C 语言的 GNU
方言(称为 GNU C
)编译程序。这种方言将 C 语言的官方 ANSI/ISO
标准与几个有用的 GNU 扩展(如嵌套函数 nested functions 和可变大小数组 variable-size arrays)结合起来。大多数 ANSI/ISO
程序都可以在 GNU C
下编译而无需更改。
有多个选项可控制 gcc
使用的 C 语言方言 dialect。最常用的选项是 -ansi
和 -pedantic
(严格按照标准发出警告)。还可以使用 -std
选项选择每个标准的特定 C 语言方言。
ANSI/ISO
有时有效的 ANSI/ISO
程序可能与 GNU C 中的扩展不兼容。为了处理这种情况,编译器选项 -ansi
会禁用那些与 ANSI/ISO
标准冲突的 GNU 扩展。在使用 GNU C 库 (glibc) 的系统上,它还会禁用 C 标准库的扩展。这样,就可以编译为 ANSI/ISO
C 编写的程序,而不会受到 GNU 扩展的任何预期之外的影响。
比如这是一个合法的 ANSI/ISO C 程序,使用了 asm 作为变量名。
1 | // test.c |
变量 asm 在 ANSI/ISO
中是合法的,但是在 GNU C 下 asm 是个关键字扩展,它允许在 C 函数中直接使用汇编。
1 | # 直接编译会报错,因为 asm 在 GNU C 下是个关键字 |
下一个示例显示了 -ansi
选项在使用 GNU C 库的系统(例如 GNU/Linux
系统)上的效果。下面的程序从头文件 math.h
中的预处理器定义 M_PI 打印出 pi 的值,π = 3.14159…
1 |
|
常量 M_PI 不是 ANSI/ISO
C 标准库的一部分(它来自 Unix 的 BSD 版本)。在这种情况下,程序使用 -ansi
选项进行编译会报错。
也可以使用 ANSI/ISO
C 编译程序,只需启用 GNU C 库本身的扩展即可。这可以通过定义特殊宏来实现,例如 _GNU_SOURCE
,它可以启用 GNU C 库中的扩展。
gcc -Wall -ansi -D_GNU_SOURCE pi.c
通过启用 _GNU_SOURCE
宏完成编译。 -D
用来定义 macros。
GNU C 库提供了许多这样的宏(称为功能测试宏 feature test macros),用于控制对 POSIX 扩展(_POSIX_C_SOURCE
)、BSD 扩展(_BSD_SOURCE
)、SVID 扩展(_SVID_SOURCE
)、XOPEN 扩展(_XOPEN_SOURCE
)和 GNU 扩展(_GNU_SOURCE
)的支持。
_GNU_SOURCE
宏同时启用所有扩展,当 POSIX 扩展与其它扩展发生冲突时,POSIX 扩展优先于其它扩展。
Strict ANSI/ISO
命令行选项 -pedantic
与 -ansi
组合将导致 gcc 拒绝所有 GNU C 扩展,而不仅仅是那些与 ANSI/ISO
标准不兼容的扩展。这有助于编写遵循 ANSI/ISO
标准的可移植程序。
这是一个使用可变大小数组 variable-size arrays(GNU C 扩展)的程序。数组 x[n] 声明为整数变量 n 指定的长度。该程序将使用‘-ansi’进行编译,因为对可变长度数组的支持不会干扰有效的 ANSI/ISO 程序的编译——它是一个向后兼容的扩展:
1 | int main(int argc, char *argv[]) |
gcc -Wall -ansi gnuarray.c
可以编译,但是 gcc -Wall -ansi -pedantic gnuarray.c
则会报错。
注意,-ansi -pedantic
没有警告并不保证程序严格符合 ANSI/ISO
标准。标准本身仅指定了一组应生成诊断的有限情况,而这些情况正是 -ansi -pedantic
报告的内容。
Selecting specific standards
GCC 使用的具体语言标准可以通过 -std
选项控制。支持以下 C 语言标准:
-std=c89
or-std=iso9899:1990
-std=iso9899:199409
-std=c99
or-std=iso9899:1999
-std=gnu89
or-std=gnu99
Warning options in -Wall
如前所述,警告选项 -Wall
可针对许多常见错误启用警告,应始终使用。它包括了大量其他更具体的警告选项,也可以单独选择。以下是这些选项的摘要:
-Wcomment
included in-Wall
此选项对嵌套注释发出警告。嵌套注释通常出现在一段包含注释的代码后来被注释掉时:更好的处理方式是 条件编译,**#if 0… #endif**。1
2
3/* commented out
double x = 1.23; /* x-position */
*/-Wformat
included in-Wall
该选项对 printf 和 scanf 等函数中格式字符串的错误使用发出警告,其中格式说明符与相应函数参数的类型不一致。-Wunused
included in-Wall
This option warns about unused variables.-Wimplicit
included in-Wall
This option warns about any functions that are used without be- ing declared. The most common reason for a function to be used without being declared is forgetting to include a header file.-Wreturn-type
included in-Wall
此选项会警告那些定义时没有指定返回类型但未声明为void
的函数。它还会捕获未声明为void
的函数中的空返回语句。
可以在 GCC 参考手册中找到 -Wall
中包含的完整警告选项集。-Wall
中包含的选项具有共同的特征,即它们报告始终错误的构造,或者可以以明确正确的方式轻松重写。这就是它们如此有用的原因——-Wall
产生的任何警告都可以被视为潜在严重问题的迹象。
Additional warning options
GCC 提供了许多其他未包含在 -Wall
中但通常很有用的警告选项。通常,这些选项会针对可能在技术上有效(technically valid)但很可能引起问题的源代码产生警告。这些选项的标准基于常见错误的经验 — 它们未包含在 -Wall
中,因为它们仅指示可能有问题或可疑的代码。
由于这些警告可以针对有效代码发出,因此没有必要一直使用它们进行编译。更合适的做法是定期使用它们并查看结果,检查是否有任何意外情况,或者对某些程序或文件启用它们。
-W
这是一个类似于 -Wall
的通用选项,它警告一些常见的编程错误,例如函数可能返回无值(也称为超出函数体末尾),以及有符号值和无符号值之间的比较。例如,以下函数测试无符号整数是否为负数(当然这是不可能的):
1 | int foo (unsigned int x) |
使用 -Wall
参数编译这个代码不会产生任何警告信息,但是使用 -W
就会产生警告,gcc -W -c w.c
。
在实践中,选项 -W
和 -Wall
通常一起使用。
-Wconversion
此选项会警告可能导致意外结果的隐式类型转换。例如,将负值赋给无符号变量,如以下代码所示, unsigned int x = -1;
在技术上是 ANSI/ISO
C 标准允许的(根据机器表示,将负整数转换为正整数),但可能是一个简单的编程错误。如果需要执行这样的转换,可以使用显式转换,例如 ((unsignedint)-1),以避免此选项的任何警告。在二进制补码机器上,转换的结果给出无符号整数可以表示的最大数字。
-Wshadow
此选项会警告在已声明变量名的作用域中重新声明该变量名。这称为变量阴影 variable shadowing,会导致混淆变量的哪个出现对应于哪个值。
以下函数声明了一个局部变量 y,该变量遮盖了函数主体中的声明:
1 | double test(double x) |
这是有效的 ANSI/ISO C,其中返回值为 1。当查看行 y = x 时(尤其是在大型复杂的函数中),变量 y 的阴影可能会使其(错误地)看起来返回值为 x。
Shadowing can also occur for function names. 例如,下面的程序尝试定义一个变量 sin
,它将遮蔽标准函数 sin(x)
。
1 | double sin_series (double x) |
-Wcast-qual
此选项对强制转换以删除类型限定符的指针发出警告,例如 const。例如,以下函数从其输入参数中丢弃 const 限定符,从而允许它被覆盖:
1 |
|
不加 -Wcast-qual
,complie cleanly,但是运行时段错误。修改 str 的原始内容违反了其 const 属性。此选项将警告变量 str 的不正确强制转换,从而允许修改字符串。
-Wwrite-strings
此选项隐式地为程序中定义的所有字符串常量赋予 const 限定符,如果尝试覆盖它们,则会导致编译时警告。修改字符串常量的结果未由 ANSI/ISO
标准定义,并且 GCC 中不推荐使用可写字符串常量。
-Wtraditional
该选项会警告某些部分的代码会被 ANSI/ISO
编译器和传统的 ANSI
前编译器以不同的方式解释。在维护旧版软件时,可能需要调查原始代码中此选项生成的警告是打算采用传统解释还是 ANSI/ISO
解释。
上述选项会产生诊断警告消息,但允许编译继续并生成目标文件或可执行文件。对于大型程序,最好通过在生成警告时停止编译来捕获所有警告。-Werror
选项通过将警告转换为错误来更改默认行为,即在出现警告时停止编译。
Using the preprocessor
GNU C preprocessor 工具 cpp,是组成 GCC 工具包的一部分。预处理器在编译源文件之前会展开其中的宏。每当 GCC 处理 C 或 C++ 程序时,都会自动调用它。在 GCC 的新版本中,预处理器被集成到编译器中,尽管这样也提供了单独的 cpp 命令。
定义宏
以下程序演示了 C 预处理器的最常见用法。它使用预处理器条件 #ifdef
来检查宏是否已定义。当相关宏定义了,预处理器将包含相应的代码直到结束 #endif
命令。
1 |
|
gcc 选项 -DNAME
从命令行定义预处理器宏 NAME
。 gcc -Wall -DTEST dtest.c
,这样编译,则会认为 TEST 已经定义了。
宏通常是未定义的,除非在命令行中使用选项 -D
指定,或在源文件(或库头文件)中使用 #define
指定。一些宏由编译器自动定义 - 这些宏通常使用以双下划线前缀 __
开头的保留命名空间。
可以通过在空文件上运行带有选项 -dM
的 GNU 预处理器 cpp
来列出完整的预定义宏集: cpp -dM /dev/null
。
注意,此列表包含少量由 gcc 定义的系统特定宏,这些宏不使用双下划线前缀。这些非标准宏可以用 gcc 的 -ansi
选项禁用。
带值的宏
In addition to being defined, a macro can also be given a concrete value. This value is inserted into the source code at each point where the macro occurs.
1 |
|
请注意,宏不会在字符串内部扩展——预处理器只会替换字符串外部的 NUM。
要定义具有值的宏,可以使用 -D
命令行选项,形式为 -DNAME=VALUE
。gcc -Wall -DNUM=100 dtestval.c
此示例使用数字,但宏可以采用任何形式的值。无论宏的值是什么,它都会直接插入到源代码中出现宏名的位置。 gcc -Wall -DNUM="2+2" dtestval.c
请注意,当宏成为表达式的一部分时,最好用括号将宏括起来。
1 | printf ("Ten times NUM is %d\n", 10 * (NUM)); |
When a macro is defined with -D
alone, gcc uses a default value of 1.
A macro can be defined to a empty value using quotes on the command line, -DNAME=""
. Such a macro is still treated as defined by conditionals such as #ifdef
, but expands to nothing.
可以使用 shell 转义的引号字符来定义包含引号的宏。例如,命令行选项 -DMESSAGE="\"Hello, World!\""
定义了一个宏 MESSAGE,该宏扩展为字符序列"Hello, World!"
。
Preprocessing source files
可以使用 gcc 的 -E
选项直接查看预处理器对源文件的影响。-E
选项使 gcc 运行预处理器 cpp,显示宏扩展后的输出,然后退出而不编译生成的源代码。
预处理器还会插入记录源文件和行号的行,格式为 #line-number "source-file"
,以帮助调试并允许编译器发出与此信息相关的错误消息。这些行不会影响程序本身。
查看预处理源文件的能力对于检查系统头文件的效果以及查找系统函数的声明非常有用。以下程序包含头文件 stdio.h
以获取函数 printf 的声明:
1 |
|
通过使用 gcc -E
预处理文件,可以看到所包含头文件中的声明。预处理的系统头文件通常会生成大量输出。这些输出可以重定向到文件,或者使用 gcc 的 -save-temps
选项更方便地保存:gcc -c -save-temps hello.c
。
运行此命令后,预处理的输出将在文件 hello.i
中可用。除了预处理的 .i
文件外,-save-temps
选项还会保存 .s
汇编文件和 .o
目标文件。-E
也可以使用 cpp
工具。