What You See Is What You Get Element

使用标准头文件编写可移植的 C/C++ 应用程序

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 所示。

mmap 定义

图 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() 的可用性。

清单 4:依赖于标准扩展的应用程序
#include <sys/time.h>

double now()
{
  return (double)gethrtime();
} 

无论定义了 POSIX 还是 X/Open 功能测试宏,该代码都将编译失败。因为这两个标准中均未定义 gethrtime() 函数,因此该函数被视为标准的扩展。编译错误如清单 5 所示,使用 _POSIX_C_SOURCE 对代码进行编译,指明代码遵循 POSIX.1-1990 标准。

清单 5:依据 POSIX.1-1990 标准进行编译时,gethrtime() 不可用
$ cc -c -D_POSIX_SOURCE test2.c
"test2.c", line 5: warning: implicit function declaration: gethrtime

对于使用标准扩展的代码,编译时还需要定义 __EXTENSIONS__,如清单 6 所示。注意,__EXTENSIONS__ 的定义中包括所有在标准中未定义的扩展的原型。

清单 6:编译时启用扩展
$ cc -c -D_POSIX_SOURCE -D__EXTENSIONS__ test2.c

一个有用的编译器选项是标志 -H。它要求编译器报告编译时包括的头文件。清单 7 显示了一个示例。

清单 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++ 标准定义了大量与 C 头文件等效的头文件。例如,C++ 头文件 <cmath> 等同于 C 头文件 <math.h>。两者的主要区别在于,C++ 标准规定在头文件中声明的函数应放在 std 命名空间中。清单 8 显示了一个调用 sin()cos() 函数的示例程序。

清单 8:调用三角函数的函数示例
#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 所示。

清单 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 声明 可逐个将各个名称引入命名空间中。例如,在包括头文件之后声明 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 日