文章
服务器与存储开发
2011 年 8 月
作者:Darryl Gove
“标准带来的最大好处是有大量标准可供选择。”
Computer Networks 第 2 版,第 254 页,Andrew S. Tannenbaum
系统头文件定义程序可使用的多个函数。可用的函数及其声明方式取决于编译时所采用的标准。如果源代码所依赖的函数在指定的规范中不存在,或使用了不同的声明方式,则可能发生编译时错误。本文讨论开发人员应如何编写和编译这样的程序,它们依赖 Oracle Solaris 支持的许多标准中定义的函数。
清单 1 所示的程序将使用 C 编译器而不是 C++ 编译器进行编译。
清单 1:示例程序
#include <sys/mman.h>
void func(void *addr, size_t len, int prot, int flags, int
fildes, off_t off)
{
mmap(addr,len,prot,flags,fildes,off);
}
|
C++ 编译器报告清单 2 所示的错误消息。
清单 2:C++ 错误消息CC -c test.c "test.c", line 6: Error: Formal argument 1 of type char* in call to mmap(char*, unsigned, int, int, int, long) is being passed void*. 1 Error(s) detected. |
错误消息显示,void* 指针被传递给一个需要 char* 参数的函数。在 C 中,void* 指针与 char* 指针兼容,所以 C 编译器在编译代码时不会报错。如果函数 func() 原型将变量 addr 指定为 int*,C 编译器也会发出警告。
手册页上的定义如清单 3 所示。这与清单 1 所示代码中该函数的使用方式一致。
清单 3:mmap 的手册页
$ man mmap
System Calls mmap(2)
NAME
mmap - map pages of memory
SYNOPSIS
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int
fildes, off_t off);
...
|
源代码与手册页一致,因此是正确的。所以问题不在这里。而在于头文件中 mmap() 的定义方式,如图 1 所示。

图 1:mmap() 定义
在头文件中,mmap() 的定义受两个 #define 语句的保护。如果定义了 _XPG4_2 或 _POSIX_C_SOURCE > 2 为 true,则该定义与手册页一致。但默认情况下无法获得一致的定义。
标准 (5) 中记录了 Oracle Solaris 所支持的标准。表 1 中列出了支持的标准以及对应的“功能测试宏”。
表 1:Oracle Solaris 支持的标准| 规范 | 编译器/标志 | 功能测试宏 |
|---|---|---|
| 1989 ANSI C 和 1990 ISO C | c89 | 无 |
| 1999 ISO C | c99 | 无 |
| 1989 SVID3 | cc -Xt -xc99=none | 无 |
| POSIX.1-1990 | c89 | _POSIX_SOURCE |
| POSIX.1-1990 和 POSIX.2-1992 C 语言绑定选项 | c89 | _POSIX_SOURCE 和 POSIX_C_SOURCE=2 |
| POSIX.1b-1993 | c89 | _POSIX_C_SOURCE=199309L |
| POSIX.1c-1996 | c89 | _POSIX_C_SOURCE=199506L |
| POSIX.1-2001 | c99 | _POSIX_C_SOURCE=200112L |
| 1990 CAE XPG3 | cc -Xa -xc99=none | _XOPEN_SOURCE |
| 1992 CAE XPG4 | c89 | _XOPEN_SOURCE 和 _XOPEN_VERSION=4 |
| 1994 SUS (CAE XPG4v2)(包括 XNS4) | c89 | _XOPEN_SOURCE 和 _XOPEN_SOURCE_EXTENDED=1 |
| 1997 SUSv2(包括 XNS5) | c89 | _XOPEN_SOURCE=500 |
| 2001 SUSv3 | c99 | XOPEN_SOURCE=600 |
开发人员应定义功能测试宏,以便指明源代码所遵循的标准。POSIX 和 X/OPEN 标准要求开发人员指定源代码所遵循的标准。所以,在 C++ 中编译清单 1 所示代码时会发出错误消息。Oracle Solaris 10 的默认标准为系统 V 接口定义 v3 (SVID3),所以在头文件中,mmap() 的定义使用 caddr_t 而不是 void*。
POSIX 标准规定,在一个严格遵循标准的应用程序中,“对于 C 语言,应在包括任何头文件之前将 _POSIX_C_SOURCE 定义为 200112L。”
我们还发现,所有标准都指定了 C 编译器而不是 C++ 编译器。POSIX 标准中未提及 C++ 编译器,C++ 标准中也未提及 POSIX 标准。使用 C++ 标准库对某个特定的 POSIX 标准进行编译时情况会变得比较复杂。这可能导致应用程序要求与标准库要求之间的冲突。不过,这些冲突往往可通过函数原型和特定函数的可用性来解决。
表 1 显示了为符合 Oracle Solaris 支持的各种标准中所定义的接口,需要定义的功能测试宏。选择特定的标准将导致头文件仅 提供选定标准中定义的接口。而这可能导致问题。例如,清单 4 中的程序依赖于函数 gethrtime() 的可用性。
#include <sys/time.h>
double now()
{
return (double)gethrtime();
}
|
无论定义了 POSIX 还是 X/Open 功能测试宏,该代码都将编译失败。因为这两个标准中均未定义 gethrtime() 函数,因此该函数被视为标准的扩展。编译错误如清单 5 所示,使用 _POSIX_C_SOURCE 对代码进行编译,指明代码遵循 POSIX.1-1990 标准。
gethrtime() 不可用 $ cc -c -D_POSIX_SOURCE test2.c "test2.c", line 5: warning: implicit function declaration: gethrtime |
对于使用标准扩展的代码,编译时还需要定义 __EXTENSIONS__,如清单 6 所示。注意,__EXTENSIONS__ 的定义中包括所有在标准中未定义的扩展的原型。
$ cc -c -D_POSIX_SOURCE -D__EXTENSIONS__ test2.c |
一个有用的编译器选项是标志 -H。它要求编译器报告编译时包括的头文件。清单 7 显示了一个示例。
-H 输出包括的头文件列表
$ cc -c -H -D_POSIX_SOURCE -D__EXTENSIONS__ test2.c
/usr/include/sys/time.h
/usr/include/sys/feature_tests.h
/usr/include/sys/ccompile.h
/usr/include/sys/isa_defs.h
/usr/include/sys/types.h
/usr/include/sys/machtypes.h
/usr/include/sys/int_types.h
/usr/include/sys/select.h
/usr/include/sys/time_impl.h
/usr/include/sys/time.h
/usr/include/time.h
/usr/include/iso/time_iso.h
|
C++ 标准定义了大量与 C 头文件等效的头文件。例如,C++ 头文件 <cmath> 等同于 C 头文件 <math.h>。两者的主要区别在于,C++ 标准规定在头文件中声明的函数应放在 std 命名空间中。清单 8 显示了一个调用 sin() 和 cos() 函数的示例程序。
#include <math.h>
float func(float d)
{
return sin(d)+cos(d);
}
|
清单 8 所示代码既可用 C 编译器也可用 C++ 编译器进行编译。不过,如果使用 C++ 标准头文件 <cmath> 代替 C 头文件 <math.h>,则无法通过 Oracle Solaris Studio C++ 编译器进行编译,如清单 9 所示。
<cmath> 的示例
#include <cmath>
using namespace std;
float func(float d)
{
return sin(d)+cos(d);
}
$ CC -c m.c
"m.c", line 5: Error: The function "sin" must have a prototype.
"m.c", line 5: Error: The function "cos" must have a prototype.
2 Error(s) detected.
|
出现错误的原因是 sin() 和 cos() 函数位于 std 命名空间中。这意味着,如果没有额外的声明,将无法访问这两个函数。解决该问题的办法有三种:
std::,指明可在 std 命名空间中找到这两个函数。例如,将 sin() 变成 std::sin()。显式限定符 std:: 确保在引用时不会使用其他名为 sin 的函数。using std::sin;,可将函数 sin() 显式放入全局命名空间中。该方法可以使标准函数对普通名称查找可见。using namespace std; 指令,告诉编译器在 std 命名空间内搜索名称。在实际应用程序中,这并不是 一个好办法,因为它将 std 命名空间内的所有声明都放在全局命名空间内,这可能会引发定义冲突。注意,使用其他编译器编译原始代码可能不会出错。这是因为并非所有编译器都遵循 C++ 标准中的规则:<cyyy> 头文件将 <yyy.h> 中的名称仅放在 std 命名空间内。
Oracle Solaris 支持各种标准。如果应用程序需要 POSIX 标准的特定实现,就需要在包括头文件之前定义相应的功能测试宏以表明这一要求。如果缺少这些定义,编译器会假设代码遵循 SVID3 中定义的接口。
如果编写应用程序时遵循特定的标准,但还使用了该标准的扩展作为接口,则还需要定义功能测试宏 __EXTENSIONS__。
感谢 Steve Clamage、Alan Coopersmith 和 Lee Damico 的建议和指正。
| 修订版 1,2011 年 8 月 3 日 |