Sun Studio 12:C 用户指南

第 6 章 转换为 ISO C

本章提供的信息可以帮助您移植 K&R 风格 C 语言应用程序,以符合 9899:1990 ISO/IEC C 标准。由于您不想符合更新的 9899:1999 ISO/IEC C 标准,因此本章提供的信息假定您使用 -xc99=none。C 编译器缺省为 -xc99=all,支持 9899:1999 ISO/IEC C 标准。

6.1 基本模式

ISO C 编译器允许同时使用旧式和新式 C 代码。如果您使用下列 -X(注意大小写)选项并且 -xc99=none,则编译器提供不同的 ISO C 标准一致性级别。-Xa 为缺省模式。请注意,编译器的缺省模式为 -xc99=all,因此在设置每个 -X 选项的情况下编译器的行为取决于 -xc99 的设置。

6.1.1 -Xc

c = 一致性)在没有 K&R C 兼容性扩展的情况下,在最大程度上与 ISO C 一致。编译器对使用 ISO C 构造的程序发出错误和警告。

6.1.2 -Xa

ISO C 以及 K&R C 兼容性扩展,具有 ISO C 要求的语义更改。如果 K&R C 和 ISO C 为相同构造指定不同语义,则编译器发出关于冲突的警告并使用 ISO C 解释。这是缺省模式。

6.1.3 -Xt

t = 转换)ISO C 以及 K&R C 兼容性扩展,没有 ISO C 要求的语义更改。如果 K&R C 和 ISO C 为相同构造指定不同语义,则编译器发出关于冲突的警告并使用 K&R C 解释。

6.1.4 -Xs

(s = K&R C) 编译的语言包括与 ISO K&R C 兼容的所有功能。编译器对在 ISO C 和 K&R C 之间具有不同行为的所有语言构造发出警告。

6.2 旧式和新式函数的混合

1990 ISO C 标准在语言方面的最大变化是借鉴了 C++ 语言的函数原型。通过为每个函数指定其参数的数目和类型,不仅使各常规编译获得对每个函数调用的形参 (argument) 和实参 (parameter) 检查(类似于 lint 的参数检查)的益处,而且参数自动转换(与赋值情形相同)为函数预期的类型。由于存在许许多多可以而且应该转换为使用原型的现有 C 代码行,因此 1990 ISO C 标准包括了控制旧式和新式函数声明混合的规则。

6.2.1 编写新代码

编写全新的程序时,在头文件中使用新式函数声明(函数原型),在其他 C 源文件中使用新式函数声明和定义。但是,如果将来可能将代码移植到使用传统的(即采纳 ISO 标准之前)C 编译器的计算机,我们建议您在头文件和源文件中使用宏 __STDC__(仅为 ISO C 编译系统定义)。有关示例,请参阅6.2.3 混合注意事项

只要同一对象或函数的两个不兼容声明处于同一作用域中,符合 ISO C 的编译器就必须发出诊断。如果使用原型来声明和定义所有函数,并且相应的头文件包含在正确的源文件中,则所有调用应与函数的定义一致。此协议消除一种最常见的 C 编程错误。

6.2.2 更新现有代码

如果您有现有的应用程序并且要获取函数原型的益处,则可以进行很多更新,取决于您要更改的代码的数目:

  1. 重新编译而不进行任何更改。

    如果使用 – v 选项进行调用,即使不更改代码,编译器也会对参数类型和数目的不匹配发出警告。

  2. 仅在头文件中增加函数原型。

    包括所有全局函数调用。

  3. 在头文件中增加函数原型,并使每个源文件以其局部(静态)函数的函数原型开头。

    包括所有函数调用,但是这样做需要在源文件中为每个局部函数键入两次接口。

  4. 更改所有函数声明和定义以使用函数原型。

对于大多数程序员,第 2 种选择和第 3 种选择可能最具成本效益。遗憾的是,这些选项要求程序员详细了解混合新旧风格的规则。

6.2.3 混合注意事项

为了使函数原型声明适用于旧式函数定义,两者都必须使用 ISO C 术语指定功能相同的接口或必须具有兼容类型

对于具有可变参数的函数,不能混合 ISO C 的省略号和旧式 varargs() 函数定义。对于具有固定数目参数的函数,情况相当简单:只需指定在先前实现中传递的参数的类型。

在 K&R C 中,根据缺省参数提升,就在将每个参数传递到被调用函数之前对其进行转换。这些提升规定,所有比 int 短的整数类型均要提升为 int 长度,并且任何 float 参数均要提升为 double,从而简化了编译器和库。函数原型更具有表现力-指定的参数类型即为传递给函数的类型。

因此,如果为现有的(旧式)函数定义编写函数原型,则在具有以下任一类型的函数原型中不应有参数:

char

signed char

unsigned char

float

short

signed short

unsigned short

 

在编写原型方面仍存在两个复杂因素:typedef 名称以及短的无符号类型的提升规则。

如果旧式函数中的参数是使用 typedef 名称(如 off_tino_t)声明的,则知道 typedef 名称指定的类型是否受到缺省参数提升的影响是至关重要的。对于这两个名称,off_tlong 类型的,因此它适合于在函数原型中使用;ino_t 过去是 unsigned short 类型的,因此如果在原型中使用它,则编译器将发出诊断,因为旧式定义和原型指定的接口不同且不兼容。

考虑究竟应该使用什么来代替 unsigned short 致使问题最终复杂化。K&R C 和 1990 ISO C 编译器之间一个最大的不兼容性是用于将 unsigned charunsigned short 展宽为 int 值的提升规则。(请参见6.4 提升:无符号保留与值保留。)与这样的旧式参数匹配的参数类型取决于编译时使用的编译模式:

最佳方法是将旧式定义更改为指定 intunsigned int 并使用函数原型中的匹配类型。如有必要,在输入函数后,您可以始终将其值赋给具有更窄类型的局部变量。

请注意原型中 ID 的使用,它可能受预处理的影响。请看以下示例:


#define status 23
void my_exit(int status);   /* Normally, scope begins */
                            /* and ends with prototype */

不要将函数原型与包含窄类型的旧式函数声明混合在一起。


void foo(unsigned char, unsigned short);
void foo(i, j) unsigned char i; unsigned short j; {...}

正确使用 __STDC__ 可生成一个可用于新旧编译器的头文件:


header.h:
    struct s { /* .  .  .  */ };
    #ifdef __STDC__
       void errmsg(int, ...);
       struct s *f(const char *);
       int g(void);
    #else
      void errmsg();
      struct s *f();
      int g();
    #endif

以下函数使用原型,但仍可在较旧的系统中编译:


struct s *
#ifdef __STDC__
    f(const char *p)
#else
    f(p) char *p;
#endif
{
    /* .  .  .  */
}

下面是更新的源文件(与上述选项 3 相同)。局部函数仍使用旧式定义,但包括了原型供较新的编译器使用:


source.c:
   #include “header.h”
      typedef /* .  .  .  */ MyType;
   #ifdef __STDC__
      static void del(MyType *);
      /* .  .  .  */
      static void
      del(p)
      MyType *p;
      {
      /* .  .  .  */
      }
      /* .  .  .  */

6.3 带有可变参数的函数

在先前实现中,不能指定函数预期的参数类型,但 ISO C 鼓励您使用原型执行该操作。为支持诸如 printf() 之类的函数,原型语法包括特殊的省略号() 终结符。由于一个实现可能需要执行一些特殊操作来处理可变数目的参数,因此 ISO C 要求此类函数的所有声明和定义均包含省略号终结符。

由于参数的 "" 部分没有名称,因此 stdarg.h 中包含的一组特殊宏为函数提供对这些参数的访问权。此类函数的早期版本必须使用 varargs.h 中包含的类似的宏。

我们假定要编写的函数是一个称为 errmsg() 的错误处理程序,它返回 void,并且函数的唯一固定参数是指定关于错误消息的详细信息的 int。此参数后面可以跟一个文件名和/或一个行号,在它们之后是格式和参数,与指定错误消息文本的 printf() 类似。

为使示例可以使用较早的编译器进行编译,我们广泛使用了仅针对 ISO C 编译系统定义的宏 __STDC__。因此,该函数在相应头文件中的声明为:


#ifdef __STDC__
    void errmsg(int code, ...);
#else
    void errmsg();
#endif

在包含 errmsg() 定义的文件中,新旧风格变得复杂。首先,要包含的头文件取决于编译系统:


#ifdef __STDC__
#include <stdarg.h>
#else
#include <varargs.h>
#endif
#include <stdio.h>

包含 stdio.h 是因为我们稍后调用 fprintf()vfprintf()

其次是函数的定义。标识符 va_alistva_dcl 是旧式 varargs.h 接口的一部分。


void
#ifdef __STDC__
errmsg(int code, ...)
#else
errmsg(va_alist) va_dcl /* Note: no semicolon! */
#endif
{
   /* more detail below */
}

由于旧式可变参数机制不允许指定任何固定参数,因此必须安排在可变部分之前访问它们。此外,由于参数的 "" 部分缺少名称,新的 va_start() 宏具有第二个参数-"" 终结符前面的参数的名称。

作为一种扩展,Sun ISO C 允许在没有固定参数的情况下声明和定义函数,如下所示:

int f(...);

对于此类函数,应在第二个参数为空的情况下调用 va_start(),如下所示:

va_start(ap,)

以下是函数的主体:


{
    va_list ap;
    char *fmt;
#ifdef __STDC__
    va_start(ap, code);
#else
    int code;
    va_start(ap);
    /* extract the fixed argument */
    code = va_arg(ap, int);
#endif
    if (code & FILENAME)
        (void)fprintf(stderr, "\"%s\": ", va_arg(ap, char *));
    if (code & LINENUMBER)
        (void)fprintf(stderr, "%d: ", va_arg(ap, int));
    if (code & WARNING)
        (void)fputs("warning: ", stderr);
    fmt = va_arg(ap, char *);
    (void)vfprintf(stderr, fmt, ap);
    va_end(ap);
}

va_arg()va_end() 宏对旧式版本和 ISO C 版本的处理方式相同。由于 va_arg() 更改 ap 的值,因此对 vfprintf() 的调用不能为:


(void)vfprintf(stderr, va_arg(ap, char *), ap);

FILENAMELINENUMBERWARNING 宏的定义可能包含在与 errmsg() 的声明相同的头文件中。

errmsg() 的调用的样例为:


errmsg(FILENAME, "<command line>", "cannot open: %s\n",
argv[optind]);
 

6.4 提升:无符号保留与值保留

1990 ISO C 标准的补充材料 "Rationale" 部分出现以下信息:"QUIET CHANGE"。依赖于无符号保留算术转换的程序表现各异,可能没有错误消息。这被认为是委员会对普遍的当前实践的最重大的更改。

本节研究此更改如何影响代码。

6.4.1 背景

根据 K&R 的《The C Programming Language》(第一版),unsigned 准确地指定一种类型;不存在 unsigned charunsigned shortunsigned long,但是在此之后不久,大多数 C 编译器增加了这些类型。有些编译器未实现 unsigned long,但是包含了其他两种类型。自然地,当这些新类型与在表达式中其他类型混合时,实现为类型提升选择不同的规则。

在大多数 C 编译器中,使用比较简单的规则“无符号保留”:当无符号类型需要展宽时,将被展宽为无符号类型;当无符号类型与带符号类型混合时,结果为无符号类型。

ISO C 指定的另一个规则称为“值保留”,其中结果类型取决于操作数类型的相对长度。当展宽 unsigned charunsigned short 类型时,如果 int 的长度足以表示较短类型的所有值,则结果类型为 int。否则,结果类型为 unsigned int。对于大多数表达式,值保留规则产生最常见类型的算术结果。

6.4.2 编译行为

只有在转换模式或 ISO 模式(-Xt-Xs)下,ISO C 编译器才使用无符号保留提升;在其他两种模式下,即符合标准模式 (–Xc) 和 ISO 模式 (–Xa),使用值保留提升规则。

6.4.3 第一个示例:强制类型转换的使用

在以下代码中,假定 unsigned charint 窄。


int f(void)
{
    int i = -2;
    unsigned char uc = 1;

   return (i + uc) < 17;
}

以上代码导致编译器在您使用 -xtransition 选项时发出以下警告:

line 6: warning: semantics of "<" change in ISO C; use explicit cast

加法运算结果的类型为 int(值保留)或 unsigned int(无符号保留),但二者之间的位模式不会更改。在二进制补码机器上,我们获得:


    i:       111...110 (-2)
+   uc:      000...001 ( 1)
===================
        111...111 (-1 or UINT_MAX)

这种位表示对应于 -1(对于 int)或 UINT_MAX(对于 unsigned int)。因此,如果结果类型为 int,则使用带符号比较且小于测试为真;如果结果类型为 unsigned int,则使用无符号比较且小于测试为假。

强制类型转换的加法用来指定这两种行为之中所期望的行为:


value preserving:
    (i + (int)uc) < 17
unsigned preserving:
    (i + (unsigned int)uc) < 17

由于不同的编译器对相同的代码选择的不同的含义,因此该表达式存在歧义。强制类型转换的加法帮助阅读器并消除警告消息。

6.4.4 位字段

相同的位字段值的提升存在同样的情况。在 ISO C 中,如果 intunsigned int 位字段中的位数小于 int 中的位数,则所提升的类型为 int;否则,所提升的类型为 unsigned int。在大多数较旧的 C 编译器中,对于显式无符号位字段,所提升的类型为 unsigned int,在其他情况下为 int

强制类型转换的类似使用可以消除存在歧义的情况。

6.4.5 第二个示例:相同的结果

在以下代码中,假定 unsigned shortunsigned char 均比 int 短。


int f(void)
{
    unsigned short us;
    unsigned char uc;
    return uc < us;
}

在此示例中,两个自动变量会同时提升为 intunsigned int,因此比较有时无符号,有时带符号。然而,由于两种选择的结果相同,因此 C 编译器并不向您发出警告。

6.4.6 整型常量

与表达式一样,有些整型常量的类型规则已更改。在 K&R C 中,只有在无后缀十进制常量的值用 int 足以表示时,其类型才为 int;只有在无后缀八进制或十六进制常量的值用 unsigned int 足以表示时,其类型才为 int。否则,整型常量的类型为 long。有时,值用结果类型不足以表示。在 1990 ISO/IEC C 标准中,常量类型是以下列表中与值对应的第一个类型:

当您使用 -xtransition 选项时,对于其行为可能会根据所涉及常量的类型处理规则而更改的任何表达式,ISO C 编译器会向您发出警告。旧整型常量类型处理规则仅在转换模式下使用;ISO 模式和符合标准模式使用新规则。


注 –

无后缀十进制常量的类型处理规则已按照 1999 ISO C 标准更改。请参见2.1.1 整型常量


6.4.7 第三个示例:整型常量

在以下代码中,假定 int 为 16 位。


int f(void)
{
    int i = 0;

    return i > 0xffff;
}

由于十六进制常量的类型为 int(在二进制补码机器上,值为 – 1)或 unsigned int(值为 65535),因此在 – Xs-Xt 模式下比较为真,在 – Xa– Xc 模式下比较为假。

同样,相应的强制类型转换澄清代码并禁止警告:


-Xt, -Xs modes:
    i > (int)0xffff

-Xa, -Xc modes:
    i > (unsigned int)0xffff
       or
    i > 0xffffU

U 后缀字符是 ISO C 的新增功能,对于较旧的编译器它可能会产生错误消息。

6.5 标记化和预处理

这可能是以前的 C 版本中最少涉及的部分,涉及将每个源文件从一串字符转换为标记序列(即可进行语法分析)的操作。这些操作包括白空间(包括注释)的识别、将连续指令捆绑为标记、处理预处理指令行以及宏替换。然而,从未对其各自顺序提供保证。

6.5.1 ISO C 转换阶段

这些转换阶段的顺序由 ISO C 指定。

替换源文件中的每个三字符序列。ISO C 正好有九个为弥补不完善的字符集而单独构造的三字符序列,它们是命名 ISO 646-1983 字符集之外的字符的三字符序列:

表 6–1 三字符序列

三字符序列 

转换为 

??=

#

??-

~

??(

[

??)

]

??!

|

??<

{

??>

}

??/

\

??’

^

ISO C 编译器必定理解这些序列,但我们建议不要使用它们。在您使用 -xtransition 选项时,只要 ISO C 编译器在转换 (–Xt) 模式下替换三字母(甚至是在注释中),它就会向您发出警告。例如,考虑以下情形:


/* comment *??/
/* still comment? */

??/ 变为反斜杠。该字符和后面的换行符被删除。结果字符为:


/* comment */* still comment? */

第二行的第一个 / 是注释的结尾。下一个标记是 *

  1. 删除每个反斜杠/换行符对。

  2. 源文件转换为预处理标记和白空间序列。每个注释有效地替换为一个空格字符。

  3. 处理各个预处理指令并替换所有宏调用。每个 #included 源文件在其内容替换指令行之前运行较早的阶段。

  4. 解释各个换码序列(形式为字符常量和字符串文字)。

  5. 并置相邻字符串文字。

  6. 各个预处理标记转换为常规标记,编译器正确分析这些标记并生成代码。

  7. 解析所有外部对象和函数引用,形成最终程序。

6.5.2 旧 C 转换阶段

以前的 C 编译器不执行如此简单的阶段序列,也不保证何时应用这些步骤。独立预处理程序识别标记和空白的时间基本上与它替换宏和处理指令行的时间相同。然后输出由适当的编译器完全重新标记化,接着编辑器分析语言并生成代码。

由于预处理程序中标记化进程的操作时间很短,且宏替换是作为基于字符(而不是基于标记)的操作执行的,因此在预处理过程中标记和空白可能会发生很大的变化。

这两种方法存在很多差异。本节其余部分讨论代码行为如何因宏替换过程中发生的行拼接、宏替换、字符串化以及标记粘贴而更改。

6.5.3 逻辑源代码行

在 K&R C 中,仅允许将反斜杠/换行符对作为一种将指令、字符串文字或字符常量延续到下一行的方法。ISO C 扩展了该概念以便反斜杠/换行符对可以将任何内容延续到下一行。结果为逻辑源代码行。因此,依赖于反斜杠/换行符对任一侧上单独标记识别的任何代码的行为不像预期的那样。

6.5.4 宏替换

在 ISO C 之前,从未详细描述宏替换过程。这种不明确性产生很多有分歧的实现。依赖于比明显常量替换和简单类函数宏更奇特的事情的任何代码可能并不真正可移植。本手册无法指出旧 C 宏替换实现与 ISO C 版本之间的所有差异。除标记粘贴和字符串化之外的几乎所有宏替换的使用产生的标记系列均与以前完全相同。此外,ISO C 宏替换算法可以完成在旧 C 版本中无法完成的工作。例如,


#define name (*name)

使 name 的任何使用均替换为通过 name 进行的间接引用。旧 C 预处理程序会产生大量圆括号和星号,并最终产生关于宏递归的错误。

ISO C 对宏替换方法的主要更改是:要求在替换标记列表中进行替换之前针对宏参数(而不是那些本身是宏替换操作符 ### 的操作数)进行递归扩展。然而,这种更改很少在结果标记中产生实际差异。

6.5.5 使用字符串


注 –

在 ISO C 中,如果您使用 -xtransition 选项,则以下带有 ? 标记的示例将生成警告。仅在转换模式(–Xt-Xs)下,结果才与以前版本的 C 相同。


在 K&R C 中,以下代码生成字符串文字 "x y!"


#define str(a) "a!"   ?
str(x y)

因此,预处理程序在字符串文字和字符常量中查找看起来类似宏参数的字符。ISO C 认识到此功能的重要性,但不允许对部分标记的操作。在 ISO C 中,以上宏的所有调用均生成字符串文字 "a!"。为在 ISO C 中实现旧效果,我们使用 # 宏替换操作符和字符串文字并置。


#define str(a) #a "!"
str(x y)

以上代码生成两个字符串文字 "x y""!",它们在并置后生成相同的 "x y!"

不直接替换字符常量的类似操作。此功能的主要用法与以下类似:


#define CNTL(ch) (037 & ’ch’)    ?
CNTL(L)

它生成


(037 & ’L’)

求值为 ASCII control-L 字符。我们知道的最佳解决办法是将此宏的用法更改为:


#define CNTL(ch) (037 & (ch))
CNTL(’L’)

此代码的可读性和实用性更强,因为它还可以应用于表达式。

6.5.6 标记粘贴

在 K&R C 中,将两个标记组合在一起至少有两种方法。以下代码中的两个调用均使用 x1 两个标记生成单个标识符 x1


#define self(a) a
#define glue(a,b) a/**/b ?
self(x)1
glue(x,1)

同样,ISO C 不认可这两种方法。在 ISO C 中,以上两个调用均生成两个独立标记 x1。可以通过使用 ## 宏替换操作符针对 ISO C 重新编写以上第二种方法:


#define glue(a,b) a ## b
glue(x, 1)

只有在定义了 __STDC__ 时,才应将 # 和 ## 用作宏替换操作符。由于 ## 是实际操作符,因此对于定义和调用中的空白,调用更加自由。

没有什么直接方式可用来实现两种旧式粘贴方案中第一种方案,但是由于它在调用时引入了粘贴的任务,因此使用它的频率比使用其他形式要低。

6.6 constvolatile

关键字 const 是 ISO C 所采用的 C++ 功能之一。而类似关键字 volatile 由 ISO C 委员会构造,从而创建了“类型限定符”类别。

6.6.1 类型(仅适用于 lvalue

constvolatile 属于标识符的类型,而不属于标识符的存储类。然而,当从表达式求值中获取对象的值时,确切地说是当 lvalue 变为 rvalue 时,经常会将这些类型从类型的最顶端删除。这些术语起源于原型赋值 "L=R",其中左侧必须仍直接引用对象(一个 lvalue),右侧只需为一个值(一个 rvalue)。因此,只有本身是 lvalues 的表达式才可以由 const 和/或 volatile 限定。

6.6.2 派生类型中的类型限定符

类型限定符可修改类型名称和派生类型。派生类型是 C 声明的那些可反复应用而生成越来越复杂的类型的部分:指针、数组、函数、结构和联合。除函数之外,可使用一个或两个类型限定符更改派生类型的行为。

例如,


const int five = 5;

声明并初始化类型为 const int 并且其值未被相应的程序更改的对象。关键字的顺序对于 C 并不重要。例如,声明:


int const five = 5;


const five = 5;

与以上声明在效果上相同。

声明


const int *pci = &five;

声明一个类型为指向 const int 的指针的对象,该对象最初指向以前声明的对象。指针本身没有限定类型-它指向限定类型,在程序执行过程中几乎可以更改为指向任何 int。除非使用强制类型转换,否则不能使用 pci 修改它所指向的对象,如下所示:


*(int *)pci = 17;

如果 pci 实际上指向 const 对象,则此代码的行为不确定。

声明


extern int *const cpi;

表明程序中某个位置存在类型为指向 intconst 指针的全局对象的定义。在此情况下,cpi 的值将不会被相应的程序更改,但是可用来修改它指向的对象。请注意,在以上声明中,const 位于 * 之后。以下一对声明产生的效果相同:


typedef int *INT_PTR;
extern const INT_PTR cpi;

这些声明可以合并为以下声明,其中对象的类型声明为指向 const intconst 指针:


const int *const cpci;
  

6.6.3 const 意味着 readonly

根据经验,对于关键字,readonly 优于 const。如果以此方式读取 const,则如下声明:


char *strcpy(char *, const char *);

很容易理解,即第二个参数仅用于读取字符值,而第一个参数覆写它指向的字符。此外,尽管在以上示例中,cpi 的类型是指向 const int 的指针,但您仍可以通过其他某些方法更改它指向的对象的值,除非它确实指向被声明为 const int 类型的对象。

6.6.4 const 用法示例

const 的两种主要用法是将在编译时初始化的大型信息表声明为无变化,以及指定该指针参数不修改它们所指向的对象。

第一种用法潜在允许某个程序的部分数据被同一程序的其他并行调用共享。它可能导致尝试将此不变数据修改为通过某种内存保护故障立即检测,因为该数据驻留在内存的只读部分中。

第二种用法有助于在演示期间生成内存故障之前查找潜在错误。例如,如果将指针传递给无法进行如此修改的字符串,则临时将空字符置入字符串中间的函数会在编译时被检测到

6.6.5 volatile 意味着精确语义

到目前为止,示例全都使用 const,因为它在概念上更简单。但是,volatile 到底意味着什么?对于编译器编写者,它只有一种含义:访问此类对象时不采用代码生成快捷方式。在 ISO C 中,声明具有相应属性及 volatile 限定类型的各个对象是程序员的责任。

6.6.6 volatile 用法示例

volatile 对象的四个常见示例为:

前三个示例是具有特殊行为的对象的所有实例:在程序执行期间的任何点均可修改其值。因此,表面上的死循环:


flag = 1;
while (flag);

实际上有效,只要 flag 具有 volatile 限定类型。某些异步事件将来可能将 flag 设置为零。否则,由于 flag 的值在循环主体中保持不变,编译系统会将以上循环更改为完全忽略 flag 值的真正死循环。

第四个示例涉及调用 setjmp 的函数的局部变量,因此进一步加以讨论。关于 setjmplongjmp 行为的细小字体注释表明对于符合第四种情形的对象的值没有任何保证。对于大多数所期望的行为,有必要让 longjmp 检查调用 setjmp 的函数与调用 longjmp 的函数之间的各个栈帧是否有保存的寄存器值。由于存在异步创建栈帧的可能性,使该作业更加困难。

在将自动对象声明为 volatile 限定类型时,编译系统知道生成的代码必须与程序员编写的代码完全匹配。因此,此类自动对象的最新值始终保存在内存中,而不是仅保存在寄存器中,且调用 longjmp 时保证它是最新的。

6.7 多字节字符和宽字符

最初,ISO C 的国际化仅影响库函数。但是,国际化的最终阶段(多字节字符和宽字符)还影响语言。

6.7.1 亚洲语言需要多字节字符

亚洲语言计算机环境的主要难点在于,I/O 需要大量的表意字符。为了适用于通常的计算机体系结构约束,这些表意字符被编码为字节序列。相关的操作系统、应用程序和终端将这些字节序列理解为单个表意字符。此外,所有这些编码都允许将常规单字节字符与表意字符字节序列混杂在一起。识别不同表意字符的难度取决于使用的编码方案。

无论使用什么编码方案,ISO C 均将术语“多字节字符”定义为表示为表意字符编码的字节序列。所有多字节字符都是“扩展字符集”的成员。常规的单字节字符仅仅是多字节字符的特殊情形。对编码的唯一要求是多字节字符不能将空字符用作它的编码的一部分。

ISO C 指定程序注释、字符串文字、字符常量和头文件名均为多字节字符序列。

6.7.2 编码变种

编码方案分为两种。第一种方案是,每个多字节字符都是自标识的,即,可以在任何多字节字符对之间插入任何多字节字符。

第二种方案是,特殊的移位字节的存在会更改后续字节的解释。一个示例是,某些字符终端进入和退出行绘制模式所用的方法。对于使用与移位状态相关的编码以多字节字符编写的程序,ISO C 要求每个注释、字符串文字、字符常量和头文件名称都必须以未移位状态开始和结束。

6.7.3 宽字符

如果所有字符的字节数或位数都相同,则消除了处理多字节字符的一些不便之处。由于在这样的字符集中可能存在成千上万的表意字符,因此应使用 16 位或 32 位大小的整数值容纳所有成员。(整个中文字母表包含的表意字符超过 65,000 个!)ISO C 包括 typedef 名称 wchar_t,将其作为大得足以容纳扩展字符集的所有成员的实现定义整数类型。

对于每个宽字符,都存在对应的多字节字符,反之亦然;必须具有对应于常规单字节字符的宽字符,才能具有与其单字节值相同的值,包括空字符。但是,并不保证宏 EOF 的值可以存储在 wchar_t 中,因为 EOF 可能无法表示为 char

6.7.4 转换函数

1990 ISO/IEC C 标准提供了五个管理多字节字符和宽字符的库函数,1999 ISO/IEC C 标准提供了更多此类函数。

6.7.5 C 语言特征

为了给亚洲语言环境中的程序员带来更大的灵活性,ISO C 提供了宽字符常量和宽字符串文字。它们具有与其非宽版本相同的形式,但位置是紧邻字母 L 之后:

在常规版本和宽版本中,多字节字符均有效。生成表意字符 ¥ 所必需的字节序列与编码有关,但是如果它由多个字节组成,则字符常量 '¥' 的值是实现定义的,正如 'ab' 的值是实现定义的一样。除了换码序列之外,常规字符串文字包含引号之间指定的字节,包括每个指定的多字节字符的字节。

当编译系统遇到宽字符常量或宽字符串文字时,每个多字节字符都将转换为宽字符,如同调用了 mbtowc() 函数一样。因此,L'¥' 的类型为 wchar_tabc¥xyz 的类型为八位数组 wchar_t。正如常规字符串文字那样,每个宽字符串文字都附加有额外的零值元素,但是在这些情况下,它是值为零的 wchar_t

正如常规字符串文字可用作字符数组初始化的快捷方法,宽字符串文字可用于初始化 wchar_t 数组:


wchar_t *wp = L"a¥z";
wchar_t x[] = L"a¥z";
wchar_t y[] = {L’a’, L’¥’, L’z’, 0};
wchar_t z[] = {’a’, L’¥’, ’z’, ’\0’};

在以上示例中,xyz 这三个数组以及 wp 指向的数组具有相同长度。所有数组均使用相同的值进行初始化。

最后,正如常规字符串文字一样,并置相邻宽字符串文字。但是,对于 1990 ISO/IEC C 标准,相邻常规字符串文字和宽字符串文字会产生不确定的行为。而且,1990 ISO/IEC C 标准指定,如果编译器不接受这样的并置,则不需要使用编译器来产生错误。

6.8 标准头文件和保留名称

在标准化过程早期,ISO 标准委员会选择包含库函数、宏和头文件作为 ISO C 的一部分。

本节介绍各种保留名称及有关其保留的某些基本原理。最后是一系列规则,遵循这些规则可使程序扫清任何保留名称。

6.8.1 标准头文件

标准头文件如下:

表 6–2 标准头文件

assert.h

locale.h

stddef.h

ctype.h

math.h

stdio.h

errno.h

setjmp.h

stdlib.h

float.h

signal.h

string.h

limits.h

stdarg.h

time.h

大多数实现提供更多头文件,但是严格符合 1990 ISO/IEC 标准的 C 程序只能使用这些头文件。

关于其中某些头文件的内容,其他标准稍有不同。例如,POSIX (IEEE 1003.1) 指定 fdopenstdio.h 中声明。为了允许这两种标准共存,POSIX 要求在包含任何头文件之前对宏 _POSIX_SOURCE 进行 #defined,以保证这些附加名称存在。在其可移植性指南中,X/Open 对其扩展也使用这种宏方案。X/Open 的宏是 _XOPEN_SOURCE

ISO C 要求标准头文件同时是自给自足和幂等的。标准头文件之前或之后不需要任何其他头文件进行 #included,并且每个标准头文件可多次进行 #included 而不会导致问题。该标准还要求它的头文件只能在安全上下文中进行 #included,以便保证头文件中使用的名称保持不变。

6.8.2 保留供实现使用的名称

标准进一步限制与其库相关的实现。以前,大多数程序员认为不应该在 UNIX 系统上对他们自己的函数使用 readwrite 等名称。ISO C 要求实现中的引用仅引入该标准保留的名称。

因此,该标准保留所有可能名称的子集供实现使用。此类名称由标识符组成,标识符以下划线开头,后面是其他下划线或大写字母。此类名称包含与以下常规表达式匹配的所有名称:


_[_A-Z][0-9_a-zA-Z]*

严格地说,如果程序使用此标识符,其行为不确定。因此,使用 _POSIX_SOURCE(或 _XOPEN_SOURCE)的程序具有不确定的行为。

但是,不确定的行为具有不同的程度。如果在符合 POSIX 标准的实现中使用 _POSIX_SOURCE,则您知道程序的不确定行为包括某些头文件中的某些附加名称,并且该程序仍符合公认的标准。ISO C 标准中的预留漏洞允许实现符合表面上不兼容的规范。另一方面,当遇到 _POSIX_SOURCE 等名称时,不符合 POSIX 标准的实现按任意方式执行。

该标准还保留以下划线开头的所有其他名称,以用于作为常规文件作用域标识符以及作为结构和联合的标记的头文件,但不用于局部作用域。允许以下公共实践:让命名为 _filbuf_doprnt 的函数实现库的隐藏部分。

6.8.3 保留供扩展使用的名称

除了显式保留的所有名称之外,1990 ISO/IEC C 标准还保留(供实现和将来标准使用)与某些模式匹配的名称:

表 6–3 保留供扩展使用的名称

文件 

保留名称模式 

errno.h

E[0-9A-Z].*

ctype.h

(to|is)[a-z].*

locale.h

LC_[A-Z].*

math.h

当前函数名称 [fl]

signal.h

(SIG|SIG_)[A-Z].*

stdlib.h

str[a-z].*

string.h

(str|mem|wcs)[a-z].*

在以上列表中,只有在包含相关头文件时,以大写字母开头的名称才是宏并被保留。其余名称可指定函数,不能用于为任何全局对象或函数命名。

6.8.4 可安全使用的名称

可以遵循以下四个简单规则以避免与任何 ISO C 保留名称冲突:

这些规则仅仅是要遵循的一般准则,缺省情况下大多数实现将继续向标准头文件增加名称。

6.9 国际化

6.7 多字节字符和宽字符介绍了标准库的国际化。本节讨论受影响的库函数,并提供一些关于应如何编写程序以便利用这些功能的提示。本节只讨论关于 1990 ISO/IEC C 标准的国际化。1999 ISO/IEC C 标准并未进行重大扩展以支持高于此处讨论的国际化。

6.9.1 语言环境

任何时候,C 程序都有当前语言环境-描述适合于某个民族、文化和语言的惯例的信息集合。语言环境具有字符串名称。唯一的两个标准化语言环境名称为 "C"""。每个程序在 "C" 语言环境中启动,这导致所有库函数像过去一样工作。"" 语言环境是实现在选择适合程序的调用的正确惯例集时的首选。"C""" 可导致相同的行为。实现可能提供其他语言环境。

为了实用和方便,语言环境被划分为一系列种类。程序可更改整个语言环境,或者只更改一个或多个种类。通常,每个种类影响与受其他种类影响的函数分开的函数集,因此可以临时更改一个种类。

6.9.2 setlocale() 函数

setlocale() 函数是指向程序语言环境的接口。通常,对于那些采用调用国家(地区)惯例的程序,在程序执行路径的开头部分都应发出一个调用,如:


#include <locale.h>
/*...*/
setlocale(LC_ALL, "");

。该调用导致程序的当前语言环境更改为相应的本地版本,因为 LC_ALL 是指定整个语言环境而不是某个种类的宏。以下是标准种类:

LC_COLLATE

排序信息 

LC_CTYPE

字符分类信息 

LC_MONETARY

货币打印信息 

LC_NUMERIC

数值打印信息 

LC_TIME

日期和时间打印信息 

这些宏中的任何宏均可作为 setlocale() 的第一个参数传递以指定该种类。

setlocale() 函数返回给定种类的当前语言环境的名称(或 LC_ALL),当其第二个参数为空指针时,它仅用于查询。因此,如下代码可用于在有限持续时间内更改语言环境或其中一部分:


#include <locale.h>
/*...*/
char *oloc;
/*...*/
oloc = setlocale(LC_category, NULL);
if (setlocale(LC_category, "new") != 0)
{
        /* use temporarily changed locale */
    (void)setlocale(LC_category, oloc);
}

大多数程序不需要此功能。

6.9.3 更改的函数

只要可能并且适当,就会将现有库函数扩展为包括与语言环境相关的行为。这些函数分为两组:

对于附加字符,如果当前语言环境的 LC_CTYPE 种类不是 "C",则除 isdigit()isxdigit() 之外的所有 ctype.h 判定函数都可返回非零(真)值。在西班牙语言环境中,isalpha(’ñ’) 应为真。同样,字符转换函数 tolower()toupper() 应相应地处理 isalpha() 函数标识的任何额外字母字符。ctype.h 函数通常是使用由字符参数索引的查表而实现的宏。通过将表重新设置为新语言环境的值可更改这些函数的行为,因此没有性能影响。

当前语言环境的 LC_NUMERIC 种类不是 "C" 时,写入或解释可打印浮点值的那些函数可以更改为使用非句点 (.) 的小数点字符。不存在将任何数值转换为包含千位分隔符类型字符的可打印形式的规定。从可打印形式转换为内部形式时,允许实现接受此类附加形式,同样是在非 "C" 语言环境中。使用小数点字符的函数是 printf()scanf() 系列、atof() 以及 strtod()。允许实现定义的扩展的函数是 atof()atoi()atol()strtod()strtol()strtoul()scanf() 系列。

6.9.4 新函数

某些语言环境相关的功能是以新标准函数的形式添加的。除了 setlocale()(它允许控制语言环境本身)之外,该标准还包括以下新函数:

localeconv()

数值/货币转换 

strcoll()

两个字符串的整理顺序 

strxfrm()

转换字符串以便整理 

strxfrm()

转换字符串以便整理 

此外,还有多字节函数 mblen()mbtowc()mbstowcs()wctomb()wcstombs()

localeconv() 函数返回一个指针,该指针指向包含对设置数值格式有用的信息以及适合当前语言环境的 LC_NUMERICLC_MONETARY 种类的货币信息的结构。这是唯一的一个其行为依赖于多个种类的函数。对于数值,结构描述小数点字符、千位分隔符和分隔符应在的位置。有十五个描述如何格式化货币值的其他结构成员。

strcoll() 函数类似于 strcmp() 函数,只是它根据当前语言环境的 LC_COLLATE 种类比较两个字符串。strxfrm() 函数也可用于将一个字符串转换为另一个字符串, 以便任何两个此类转换后字符串均可以传递到 strcmp(),并且可获得与 strcoll() 传递两个预转换字符串时返回的排序类似的排序。

strftime() 函数提供与 sprintf()struct tm 中的值使用的格式设置类似的格式设置,并提供依赖当前语言环境的 LC_TIME 种类的某些日期和时间表示。此函数基于 ascftime() 函数,后者作为 UNIX System V 发行版 3.2 的一部分发行

6.10 表达式中的分组和求值

Dennis Ritchie 在设计 C 时所作的选择之一是为编译器提供一个许可证,以便重新整理包含算术上可交换并且关联的相邻操作符(甚至出现圆括号)的表达式。这在 Kernighan 和 Ritchie 合著的《The C Programming Language》的附录中已明确指出。但是,ISO C 没有给编译器同样的自由。

本节通过考虑以下代码片段中的表达式语句,讨论这两个 C 定义之间的差异,并阐明表达式的副作用、分组以及求值之间的差别。


int i, *p, f(void), g(void);
/*...*/
i = *++p + f() + g();

6.10.1 定义

表达式的副作用是修改内存并访问 volatile 限定对象。以上表达式的副作用是更新 ip 以及函数 f()g() 内包含的任何副作用。

表达式的分组是值与其他值和运算符相结合的一种方式。以上表达式的分组主要是加法的执行顺序。

表达式的求值包括生成结果值所必需的所有运算。要对表达式求值,所有指定的副作用必须在上下两个序列点之间发生,并且使用特定的分组执行指定的操作。对于以上表达式,必须在前一个语句之后和该表达式语句的 ; 之前更新 ip;函数调用可以在前一个语句之后使用它们的返回值之前的任何时候,按任何顺序发生。特别地,在使用操作的值之前,导致内存更新的操作符不需要分配新值。

6.10.2 K&R C 重新整理许可证

由于加法在算术上可交换并且关联,因此 K&R C 重新整理许可证适用于以上表达式。为了区别常规圆括号和表达式的实际分组,左、右花括号指定分组。表达式的三种可能的分组为:


i = { {*++p + f()} + g() };
i = { *++p + {f() + g()} };
i = { {*++p + g()} + f() };

给定 K&R C 规则,所有这些分组均有效。此外,即使表达式是按以下任意方式编写的,所有这些分组仍有效:


i = *++p + (f() + g());
i = (g() + *++p) + f();

如果在溢出导致异常的体系结构上对该表达式求值,或者加法和减法在溢出时不是互逆运算,则当一个加法运算溢出时,这三种分组表现不同。

对于这些体系结构上的此类表达式,K&R C 中唯一可用的求助措施是分割表达式以强制进行特定的分组。以下是分别强制执行以上三种分组的可能重写:


i = *++p; i += f(); i += g()
i = f(); i += g(); i += *++p;
i = *++p; i += g(); i += f();

6.10.3 ISO C 规则

对于算术上可交换并且关联但实际上在目标体系结构上并非如此的操作,ISO C 不允许进行重新整理。因此,ISO C 语法的优先级和关联性完整描述了所有表达式的分组;所有表达式在进行语法分析时必须进行分组。所考虑的表达式按以下方式分组:


i = { {*++p + f()} + g() };

此代码仍不表示必须在 g() 之前调用 f(),也不表示必须在调用 g() 之前增加 p

在 ISO C 中,不需要为了避免意外的溢出而分割表达式。

6.10.4 圆括号

由于不完全的理解或不准确的表示,ISO C 经常被错误地描述为支持圆括号或根据圆括号求值。

由于 ISO C 表达式仅仅具有语法分析指定的分组,因此圆括号仍然仅用作控制表达式语法分析方式的方法;表达式的自然优先级和关联性与圆括号同等重要。

以上表达式可写为:


i = (((*(++p)) + f()) + g());

对其分组或求值没有不同影响。

6.10.5 As If 规则

使用 K&R C 重新整理规则的几个理由:

ISO C 委员会最终承认:重新整理规则应用于所描述的目标体系结构时,本来是要作为 as if 规则的一个实例。ISO C 的 as if 规则是通用许可证,它允许实现任意偏离抽象机器描述,只要偏离不更改有效 C 程序的行为。

因此,由于无法通知此类重新分组,因此允许在任何机器上重新整理所有二元按位运算符(移位除外)。在溢出回绕的典型二进制补码机器上,可以由于相同原因重新整理涉及乘法或加法的整型表达式。

因此,C 中的此更改对大多数 C 程序员没有重大影响

6.11 不完全类型

ISO C 标准引入术语“不完全类型”使 C 的基本(但容易造成误解)部分形式化,这种类型的开头具有某种暗示。本节描述不完全类型、其允许位置以及它们有用的原因。

6.11.1 类型

ISO 将 C 的类型分为三个不同的集合:函数、对象和不完全。函数类型很明显;对象类型包含其他一切,除非不知道对象的大小。该标准使用术语“对象类型”指定指派的对象必须具有已知大小,但是除 void 之外的不完全类型也称为对象,知道这一点很重要。

不完全类型有三种不同形式:void、未指定长度的数组以及具有非指定内容的结构和联合。void 类型与其他两种类型不同,因为它是无法完成的不完全类型,并且它用作特殊函数返回和参数类型。

6.11.2 完成不完全类型

通过在表示相同对象的相同作用域中的后面声明中指定数组大小,可完成数组类型。当声明并在相同声明中初始化不具有大小的数组时,仅在其声明符的末尾与其初始化函数的末尾之间,数组才具有不完全类型。

通过在具有相同标记的相同作用域中的后面声明中指定内容,可完成不完全结构或联合类型。

6.11.3 声明

某些声明可使用不完全类型,但是其他声明需要完全对象类型。需要对象类型的声明是数组元素、结构或联合的成员以及函数的局部对象。所有其他声明允许不完全类型。特别地,允许下列构造:

函数返回和参数类型特殊。除 void 之外,在定义或调用函数之前,必须完成以这种方式使用的不完全类型。返回类型的 void 指定不返回值的函数,单个参数类型的 void 指定不接受参数的函数。

由于数组和函数的参数类型重写为指针类型,因此表面上不完全的数组参数类型实际上并非不完全。mainargv 的典型声明(即 char *argv[],一个未指定长度的字符指针数组)重写为指向字符指针的指针。

6.11.4 表达式

大多数表达式运算符需要完全对象类型。仅有的三个例外是一元运算符 &、逗号运算符的第一个操作数以及 ?: 运算符的第二个和第三个操作数。除非需要指针运算,否则接受指针操作数的大多数运算符也允许指向不完全类型的指针。该列表包含一元运算符 *。例如,给定:


void *p

&*p 是使用该声明的有效子表达式。

6.11.5 正当理由

为什么不完全类型是必要的?在忽略 void 的情况下,只有一个由不完全类型提供的功能是 C 无法以其他方式处理的,而必须利用对结构和联合的正向引用。如果两个结构需要相互指向的指针,则唯一的方法是使用不完全类型:


struct a { struct b *bp; };
struct b { struct a *ap; };

具有某种形式的指针以及异构数据类型的所有强类型编程语言提供处理这种情形的某些方法。

6.11.6 示例

为不完全结构和联合类型定义 typedef 名称通常很有用。如果您有一系列包含许多相互指向的指针的复杂数据结构,结构前面有一个 typedef 列表(可能在中央头文件中),则可以简化声明。


typedef struct item_tag Item;
typedef union note_tag Note;
typedef struct list_tag List;
.  .  .
struct item_tag { .  .  .  };
.  .  .
struct list_tag {
    struct list_tag {
};

此外,对于其内容不应该用于程序其余部分的结构和联合,头文件可以声明不带该内容的标记。程序的其他部分可以使用指向不完全结构或联合的指针而不会出现任何问题,除非它们尝试使用它的任何成员。

频繁使用的不完全类型是未指定长度的外部数组。通常,没有必要知道使用其内容的数组的范围

6.12 兼容类型和复合类型

对于 K&R C,引用同一实体的两个声明可能是不同的;对于 ISO C 更是这样。ISO C 中使用的术语“兼容类型”表示“足够接近”的类型。本节描述兼容类型和“复合类型”(合并两种兼容类型而产生的结果)。

6.12.1 多个声明

如果只允许 C 程序声明每个对象或函数一次,则不需要兼容类型。链接(允许两个或更多声明引用相同实体)、函数原型和分别编译全部需要此功能。独立转换单元(源文件)具有与单个转换单元不同的类型兼容性规则。

6.12.2 分别编译兼容性

由于每个编译可能查看不同的源文件,因此独立编译中的大多数兼容类型规则实质上是结构化的:

6.12.3 单编译兼容性

当相同作用域内的两个声明描述相同的对象或函数时,这两个声明必须指定兼容类型。然后这两种类型合并为与这两种类型兼容的单个复合类型。后面将详细讨论复合类型。

兼容类型是递归定义的。底部为类型说明符关键字。规则规定,unsigned shortunsigned short int 相同,不带类型说明符的类型与带有 int 的类型相同。所有其他类型仅当派生它们的类型兼容时才为兼容类型。例如,如果限定符 const volatile 是相同的,且未限定基类型是兼容的,则两个限定类型是兼容的。

6.12.4 兼容指针类型

要使两种指针类型兼容,它们指向的类型必须兼容,并且必须对这两个指针进行相同的限定。考虑到指针的限定符在 * 之后指定,因此以下两个声明


int *const cpi;
int *volatile vpi;

声明指向相同类型 int 的两个以不同方式限定的指针。

6.12.5 兼容数组类型

要使两个数组类型兼容,它们的元素类型必须兼容。如果两个数组类型具有指定的大小,则它们必须匹配,即,不完全数组类型(请参见6.11 不完全类型)同时与另一不完全数组类型和一个具有指定大小的数组类型兼容。

6.12.6 兼容函数类型

要使函数兼容,请遵守以下规则:

6.12.7 特殊情况

signed int 的行为与 int 相同,不同之处可能在于位字段,其中无格式 int 可能表示无符号的数量。

另一点值得注意的是,每个枚举类型必须与某些整数类型兼容。对于可移植的程序,这意味着枚举类型是独立类型。通常,ISO C 标准将枚举类型视为独立类型。

6.12.8 复合类型

由两个兼容类型构成的复合类型也是递归定义的。兼容类型可能彼此不同的原因在于不完全数组或旧式函数类型。因此,复合类型最简单的描述是,它是与两个原始类型均兼容的类型,包括原始类型中的各个可用数组大小和各个可用参数列表