`
izuoyan
  • 浏览: 8915594 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

(转)C语言家族扩展收藏 (转)C语言家族扩展

阅读更多

(转)C语言家族扩展

翻译:
5.1--5.6 林峰
5.7--5.20 董溥
5.21--5.26 王聪
5.27--5.34 刘洋
5.35--5.43 贾孟树

<!-- 换行,并从新行开始 -->

致谢:感谢陈老师指出其中的一些错误,已修订。

修订记录:
修订一些用词和标点符号。(董溥) 2007年1月12号
修订一些用词和错别字。(王聪) 2006年12月14号
修正一些错误的标签。(王聪,董溥) 2006年12月13号

GNU C提供了多种在ISO标准C中没有的特性。(‘-pedantic ’选项会使GCC打印一个警告信息,如果这些 特性被使用。)如果要在条件编译时测试这些特性是否可以使用,可以查预定义的宏__GNUC__,它总是在GCC中定义。

这些扩展在C和Objective C中适用。大部分特性也适用于C++。参见Extensions to the C++ Language 一部分,仅为C++提供的扩展。

这些特性在ISO C99中,但不是在C89或C++中,它们作为扩展被GCC以C89和C++接受。

5.1 表达式中的语句和声明

5.2 局部声明的标签

5.3 可赋值标签

5.4 嵌套函数

5.5 构建函数调用

5.6命名一个表达式的类型

5.7 使用typeof指代类型

5.8 左值概括

5.9 省略操作数的条件

5.10 双字节整型

5.11 复数

5.12 16进制浮点数

5.13 零长度数组

5.14 变长数组

5.15 可变参数的宏

5.16 为反向换行轻微放宽规则

5.17 包含换行的字符串常量

5.18 非左值数组可以具有下标

5.19 void与函数指针的运算

5.20 非常量初始化

5.21 复合文字

5.22 特定的初始化

5.23 case范围

5.24 向共同体类型转换

5.25 混合声明和代码

5.26 声明函数的属性

5.27 属性语法

5.28 原型和老式风格的函数定义

5.29 C++风格注释

5.30 标识符名称中的美元符

5.31 常量中的ESC字符

5.32 询问变量对齐方式

5.33说明变量属性

5.34 指定类型属性

5.35 内联函数像宏一样快

5.36 汇编指令和C表达式 操作数

5.37汇编代码中使用的控制名字

5.38 指定寄存器中的变量

5.39 备用关键字

5.40 不完整的枚举类型

5.41 用字符串命名函数

5.42 获取函数返回值或结构地址

5.43 GCC提供的其它内置函数

5.1 表达式中的语句和声明

GNU C把包含在括号中的复合语句看作是一个表达式。这样就允许你在表达式中使用循环、switch 语句以及局部变 量。

让我们回忆一下,复合语句是用大括号括起来的一组表达式,在这种构造之下,大括号外面要有一对圆括号。例如:

({ int y = foo (); int z;
if (y > 0) z = y;
else z = - y;
z; })

就是一个合法的求foo() 的绝对值的表达式(虽然比必要的复杂一点)。

复合语句的最后必须是一个以分号结尾的语句;而这个子语句的值将会被当成整个表达式的值。在这里,如果你使用了其它一些用大括号括起来的语句,由于 这个语句的返回值为空,所以实际上整个语句值为空。

这个特点在使宏定义变得“安全”这方面特别有用,因为这样只会对每个参数严格地只计算一次。比如说:在标准C中,求最大值的函数的宏,通常定义如 下:

#define foo(a)  ({int b = (a); b + 3; })

但是,这个宏定义对a和b都计算了两次,当参数带有副作用时,将会产生错误结果。在GNU C中,假设我们知道参数的类型,这儿我们假设是int型,那么你就可以象这样子让宏定义更安全:

#define maxint(a,b) \
({int _a = (a), _b = (b); _a > _b ? _a : _b; })

常量表达式中不允许使用语句,例如枚举类型的常量,位域的宽度或者是静态变量的初始值。

如果你不知道参数的类型,你仍然可以实现上述功能,不过你必须使用typeof(参见5.7节 用typeof引用一个类型)或者类型命名(参见5.6节 命名一个表达式的类型)。

C++对语句表达式并不是完全支持,他们的运行结果是不确定的。有可能在某些方面会他们完全支持语句表达式,或者他们会禁止,或者现在的漏洞会继续 不确定的存在着。目前,语句表达式还是没有默认参数理想。

需要说明的是,在C++中,语句表达式仍存在语义上的争议。如果你想在C++中使用语句表达式来代替内联函数,那么你可能会对对象析构的处理方式感 到惊讶。例如:

#define foo(a)  ({int b = (a); b + 3; })

这个宏和下面这个内联函数运行起来是不一样的:

inline int foo(int a) { int b = a; return b + 3; }

特殊地,如果foo的表达式中牵涉到临时对象的创建的话,那么这些临时对象的析构在宏中就会比在内联函数中更早执行。

这些因素都需要考虑,这就意味着在为C++的使用设计的头文件中使用这种形式的语句表达式并不是一个好方法。需要注意的是,有些版本的GNU C库中也包含了使用语句表达式的头文件,必然导致了也存在这个问题。

5.2 局部声明的标签

语句表达式在作用域内可以定义局部标签。简单的说,局部标签就是一个标识符。你可以用一个普通的goto语句跳到该标签处,不过只限于该语句表达式 所属的作用域。

一个局部标签的定义就像这样:

__label__ label
;

或者是:

__label__ label1
, label2
, ...

;

局部标签的声明必须处在语句表达式的开头,就是在“({”之后,在其它常规声明之前。

标签声明只是定义了标签的名字,并没有定义标签的内容。所以,在语句表达式的语句中,你必须用平常的方法给label1定义标签内容。

局部标签的特性相当有用,因为在宏中经常使用语句表达式。如果宏中含有嵌套循环,那么用goto语句来跳出循环就相当有效。但是,普通标签的作用域 只是整个函数,故在这儿无法使用:如果宏在一个函数中被多次展开,那么这个标签在函数内就会被重定义。而局部标签就可以解决这个问题。例如:

	#define SEARCH(array, target)                     \
({ \
__label__ found; \
typeof (target) _SEARCH_target = (target); \
typeof (*(array)) *_SEARCH_array = (array); \
int i, j; \
int value; \
for (i = 0; i < max; i++) \
for (j = 0; j < max; j++) \
if (_SEARCH_array[i][j] == _SEARCH_target) \
{ value = i; goto found; } \
value = -1; \
found: \
value; \
})

5.3 可赋值标签

你可以用一元运算符 "&&" 来获取当前函数中或者是包含函数中定义的一个标签的地址。它的返回值是空指针类型,该值是个常量,而且可以在任何合法使用该类型常量的地方使用。例如:

void *ptr;
...
ptr = &&foo;

要使用这些值,你必须设法跳到一个值处。这是通过计算过的goto 语句goto *exp 完 成的(2) 。例如:

goto *ptr;

允许使用任何关于空类型的表达式。

另一个使用这些常量的地方就是初始化一个可以用来当跳跃标签的静态数组:

static void *array[] = { &&foo, &&bar, &&hack };

你就可以象这样选择一个带下标的标签:

goto *array[i];

要注意到这样并未对下标是否越界进行检查,而C中的数组下标是从不会这样的。

数组标签值的这种用途与switch语句的用途很相似。所以,除非用switch语句来解决问题不理想时才使用这种数组。

标签值的另一种用法就是作为线程代码的解释器。解释器函数中的标签可以存储在线程代码中用来作更快的调度 。

你也许不会使用这种方法来跳至另一个函数的代码处。如果你这么用了的话,那么就会发生完全不可预期的事情。防止这种事情发生的最好办法就是,只将标 签值存到自变量中,而且永远不要将它作为一个参数进行传递。

上述例子的一个可替代的写法是:

static const int array[] = { &&foo - &&foo, &&bar - &&foo,
&&hack - &&foo };
goto *(&&foo + array[i]);

对代码来说,在共享库中更为友好。 因为它减少了必要的动态改变位置的次数,因此还实现了数据的只读。

5.4 嵌套函数

嵌套函数就是指在另一个函数中定义的函数(GNU C++中不支持嵌套函数)。嵌套函数名仅在它被定义的模块中有效。举个例子,我们这儿定义了一个名为square的嵌套函数,并且调用了它两次:

foo (double a, double b)
{
double square (double z) { return z * z; }

return square (a) + square (b);
}

这个嵌套函数可以处理在它定义的位置对它可见的包含函数的所有变量,这叫词法作用域。下面我们举个嵌套函数的例子,它使用了一个名为offset的 继承变量。

bar (int *array, int offset, int size)
{
int access (int *array, int index)
{ return array[index + offset]; }
int i;
...
for (i = 0; i < size; i++)
... access (array, i) ...
}

函数中可以定义变量的地方就可以定义嵌套函数,也就是说在任何模块的第一条语句之前。

通过储存嵌套函数的地址或者是将它的地址传给另一个函数,就可以在函数的作用域外调用该嵌套函数:

hack (int *array, int size)
{
void store (int index, int value)
{ array[index] = value; }

intermediate (store, size);
}

这里,intermediate 函数将store 函数的地址作为一个参数进行接收。如果,intermediate 函 数调用store ,传递给store 的参数就会被存进array 。这个 方法仅在未退出包含函数(这个例子中是hack)时有效。

当该包含函数已经退出,如果你通过嵌套函数的地址来调用它的话,后果将不可设想。如果当包含函数已经退出时你来调用它的话,而且它指向作用域中不再 存在的变量时,也许你会很幸运,但是,冒这个险是不可取的。但是,要是嵌套函数并没有指向任何作用域外的任何东西,那么你就是安全的。

GCC增加了一种获取嵌套函数地址的方法--trampolines 。要了解详细信息可以查阅:http://people.debian.org/~karlheg/Usenix88-lexic.pdf .

嵌套函数可以跳至一个从包含函数中继承的标签,条件是这个标签在包含函数中有明确的定义(参见5.2节 局部声明标签 )。这样的一个跳跃就回到了包含函数,同 时也退出了调用goto 语句的嵌套函数或者是其它的中间函数。这里有个例子:

bar (int *array, int offset, int size)
{
__label__ failure;
int access (int *array, int index)
{
if (index > size)
goto failure;
return array[index + offset];
}
int i;
...
for (i = 0; i < size; i++)
... access (array, i) ...
...
return 0;

/* Control comes here from access
if it detects an error. */
failure:
return -1;
}

一个嵌套函数一般都有它的内部连接。用extern 来声明一个嵌套函数是错误的,如果你需要在嵌套函数的定义之前声明 它,使用auto,否则该函数声明是没有意义的。

bar (int *array, int offset, int size)
{
__label__ failure;
auto int access (int *, int);
...
int access (int *array, int index)
{
if (index > size)
goto failure;
return array[index + offset];
}
...
}

通过使用下面所描述的内置函数,你就可以把一个函数所接收的参数记录下来,然后用同样的参数调用另一个函数而不管参数的数目和类型。

你同样也可以将函数调用的返回值记录下来,然后返回同样的值,而不管这个函数试图返回什么样的数据类型,只要你的调用函数需要这个值。

内置函数: void * __builtin_apply_args ()

这个内置函数返回一个指针,而这个指针指向的数据描述了如何用传给当前函数的参数来进行一个函数调用。

这个函数返回一个由参数指针寄存器,结构体值地址以及所有可能用来传递参数的寄存器组成的堆栈段的地址。

内置函数: void * __builtin_apply (void (*function)(), void *arguments, size_t size)
这个内置函数通过一份由argumentssize 指定的参数的拷贝来调用函 数。

arguments 的值必须是_builtin_apply_args 函数的返回值,而 size参数指定了堆栈参数数据的字节数。

这个内置函数返回一个指针,而这个指针指向的数据描述了如何返回function 函数返回的不管任何类型的返回值。这个 数据保存在堆栈段中。

有时候,计算确切的size的值并不容易。__builtin_apply 函数使用这个值来计算需要入栈的数据大小,以 及需要从数据来源拷贝多少数据。

内置函数: void __builtin_return (void *result)
这个函数返回了一个由包含函数中result 指定的值。你需要为result 函 数指定一个由__builtin_apply 函数返回的值。

5.6命名一个表达式的类型

在初始化语句中,你可以使用一个typedef 声明来给表达式类型取名。这里演示了如何给exp 类 型取个新类型名name:

typedef name
 = exp
;

把这个和语句内嵌表达式特性结合起来将非常有用。下面就是如何将二者结合起来来定义一个可对任何算术类型求最大值的宏:

#define max(a,b) \
({typedef _ta = (a), _tb = (b); \
_ta _a = (a); _tb _b = (b); \
_a > _b ? _a : _b; })

给局部变量起以下划线开头的名字的原因是为了防止与表达式中用来替代ab 的变量的变量名产 生冲突。最后,我们希望可以设计出一种新的声明语法,这种语法允许你声明作用域仅在他们的初始化语句之后的变量;用这种方法来防止这种冲突更加可靠。

5.7 使用typeof指代类型

另外一种提供表达式类型的方法是使用typeof 。使用这个关键词的语法与sizeof 相 似,但是建立的语义与typedef 定义一个类型相似。

有两种写typeof 参数的方法:跟一个表达或者一个类型。下面是一个跟表达式的例子:

typeof (x[0](1))

这假定x 是一个指向函数的指针数组;描述的类型是函数的值。

下面的例子使用类型名做为参数:

typeof (int *)

这儿的类型描述的是指向int 的指针。

如果你正在写一个头文件必须在包含ISO C程序时工作,使用__typeof__ 代替typeof 。 参考5.39节 备用关键字

typeof 可以在任何地方使用,typedef 名字可能会被用到。例如,你可以在申明的时 候使用它,在一个转换中或者在sizeoftypeof 中使用。

  • 这个声明y为x指向的数据类型:
    typeof (*x) y;
  • 这个声明y为一个数组具有这样的值:
    typeof (*x) y[4];
  • 这个声明y是一个指向字符的指针:
    typeof (typeof (char *)[4]) y;

    它与下面的传统C声明相同:

    char *y[4];

    为了看到使用typeof 定义的意义,为什么它可能是一种有用的写法,我们用宏来重新写它们:

    #define pointer(T)  typeof(T *)
    #define array(T, N) typeof(T [N])

    现在声明可以被改写为:

    array (pointer (char), 4) y;

    因此,array(pointer(char),4) 是含有4个指向char 指针的数组。

5.8 左值概括

复合表达式,条件表达式以及强制类型转换允许作为左值,条件是它们的操作数是左值。这意味着你可以取得它们的地址或者给它们赋值。

标准的C++允许复合表达式,条件表达式作为左值,允许强制类型转换引用类型,因此使用本扩展与C++的代码有冲突。

例如,一个复合表达式可以被赋值,条件是最后一个表达式是左值。下面两个表达式是相同的:

(a, b) += 5
a, (b += 5)

同样,复合表达式的地址也是可取的。下面两个表达式是相同的:

&(a, b)
a, &b

一个条件表达式,如果它的类型不是void 且真假分支都是有效左值,那它就是一个有效左值。例如,下面两个表达式是相同 的:

(a ? b : c) = 5
(a ? b = 5 : (c = 5))

如果强制类型转换的操作数是一个左值那么它也是左值。一个简单的赋值,它的左边是一个强制转换用来首先将右边转换为指定类型,然后再转换为表达式左 边的类型。当这存储完成后,值被转换回指定类型变成分配的值。因此,如果a是char* 类型,下面两个表达式是相同的:

(int)a = 5
(int)(a = (char *)(int)5)

算术赋值操作符,例如‘+=’,加到强制类型转换中,算术使用强制类型转换的后的类型,接下来与前面情况相同。因此,下面两个表达式是相同的:

(int)a += 5
(int)(a = (char *)(int) ((int)a + 5))

你不能取得强制类型转换的左值,因为使用它的地址将不会清楚的计算。试想假设允许&(int) ,f为float 类 型。下面的语句将试图在一个属于浮点数空间里存储一个整型:

*&(int)f = 1;

这与(int)f=1 (它可以把1转换为浮点类型并存储)做的事情大不相同。于其造成这种不一致,我们认为还是禁止对强 制类型转换使用'&'比较好。

如果你真的想要指向f 地址的int* 指针,你可以写为(int*)&f

5.9 省略操作数的条件

在条件表达式中的中间操作数可能会被忽略。这样,如果第一个操作数非零,那么它的值就是条件表达式的值。

因此,表达式

x ? : y

的值为x 的值,如果x 非零;否则为y 的值。

这个例子完全等价于

x ? x : y

在这个简单的例子中,忽略中间操作数的作用不是特别有用。第一个操作数,也许(如果它是一个宏参数),含有一个副作用时,它会变得有用。重复的中间 操作数可能会产生两次副作用。而忽略中间操作数使用已经计算的值可以避免烦人的重新计算。

5.10 双字节整型

ISO C99支持至少64位的整型数据类型,作为扩展,GCC以C89模式和C++中支持它。可以把带符号整型写为long long int ,无符号整型写为unsigned long long 。要创建一个long long int 型的整型,给整型添加'LL'后缀。创建无符号long long int 型整型,添加 'ULL'后缀。

你可以在运算中像其它整型一样使用它们。另外,这些类型的减法以及按位布尔操作对所有机器都是开码的。如果机器支持fullword-to- doubleword一个扩展乘法指令,那么乘法也是开码的。分割和移位只有在机器有特殊支持时才是开码的。不是开码的运算使用GCC特有的库例程。

这可能成为你使用long long 类型作为函数参数时的陷阱,除非你申明函数原型。如果一个函数期待一个int 型 参数,但你传递给它long long int 型参数,结果将变得十分混乱,因为调用者和子程序将会拒绝参数的位数。同样地, 如果函数期待一个long long int 型参数,而你传递给它int 型。避免这种问题最好的办 法是申明函数原型。

5.11 复数

ISO C99支持浮点复数数据类型,扩展GCC以C89模式和C++支持它并且支持整型复数,但ISO C99并不支持。你可以使用关键词_Complex 申 明复数类型。作为扩展,较老的GNU关键词_complex_ 也同样被支持。

例如,'_Complex double x; '申明x 为实部与虚部都为double 的 变量。'_Complex short int y; '申明y 为实部与虚部都为short int ;这看起来不是很有用,但它表明了建立复数类型是完整的。

要写一个复数类型的常量,使用’i ‘或’j ‘后缀(任意一个;它们是相同的)。例如,2.5fi_Complex float 类型,3i_Complex int 类型。这样的常量总是有一个纯虚 值,不过你可以添加一个真值来建立任意一个复数值。这是GNU扩展;如果你有一个ISO C99标准的C库(如GNU库),但你想建立浮点型的复数,你可以包含complex.h并使用宏I或者_Complex_I 代 替。

要获得复数表达式exp 的实部,写为_real_exp 。同样_imag_ 用 来获得虚部。这是一个GNU扩展;对于浮点类型,你需要使用ISO C99的函数crealf, creal, creall, cimagf, cimag and cimagl ,在complex.h中申明,GCC以内置函数提供。

使用复数类型的时候操作符’~‘代表共轭复数。这是GNU扩展;对于浮点类型,你可以使用ISO C99函数conjf, conj and conjl ,在complex.h中申明,同样GCC以内置函数支持。

GCC可以给复数以非邻接的方式分配自动变量;甚至当虚部在栈中时实部却可以在寄存器中(反之易然)。没有一个支持的调试信息格式有一种像这样代表 非邻接分配的方式,因此GCC把非邻接复数变量描述为好像有两个分离的非复数变量。如果变量的真实名字是foo ,两个虚构的变 量是foo$real,foo$imag 。你可以用你的调试器检查并设置两个虚构变量。

以后版本的GDB会知道如何识别这样的类型,并且把它们看作一个单一的复数类型。

5.12 十六进制浮点数

ISO C99不仅仅以普通的十进制方式支持浮点数,例如1.55e1 ,而且支持像0x1.fp3 这 样十六进制格式。作为GNU的扩展,GCC以C89(除非在某些十分严格的情况下)的方式和C++支持它。上面的格式中'0x ' 为十六进制开始,'p '或'P '为指数命令。指数是十进制数并以2为幂,它的有效部分会被乘。因此 '0x1.f '是1 15/16,'p3' 乘它得8,0x1.fp3 的 值与1.55e1 相同。

与十进制数浮点表示不同,十六进制中指数是必须有的。否则编译器不能解决具有二义性的表达式0x1.f 。这可能意味着1.0f 或 者1.9375 因为‘f ’同样是浮点常量类型的扩展。

5.13 零长度数组

GNU C中允许使用零长度的数组。它们作为结构体的最后一个元素十分有用,如果结构体确实是变长对象的首部:

struct line {
int length;
char contents[0];
};

struct line *thisline = (struct line *)
malloc (sizeof (struct line) + this_length);
thisline->length = this_length;

在ISO C89中,你需要给目录分配长度1,意味着要浪费空间或者使malloc的参数变得复杂。

在ISO C99中,你可以使用灵活的数组元素,只是在语法和主义上有微小的差异。

  • 灵活的数组元素写为contents[] 不带0
  • 灵活的数组元素具有不完全的类型,所以sizeof 操作也许不能被应用。作为零长度数组的原始实现的奇怪之处,sizeof 被 赋值为0。
  • 灵活的数组元素可以作为结构体的最后一个元素出现当其它元素非空时。GCC现在在任何地方允许零长度数组。定义仅含有一个零长度数组的结构体,无 论如何,你可能会遇到问题。这样的用法被反对,我们建议仅当灵活数组元素被允许的地方使用零长度数组。

GCC3.0以前的版本允许零长度数组被静态的初始化。除那些情况之外是有用的,它也允许在可能会破坏未来数据的情况下进行初始化。零长度数组的非 零初始化现在被反对。

作为替代GCC允许静态初始化灵活数组元素。这与定义一个新结构体包含原有的结构体紧跟着一个具有足够大小的数组来包含数据,也就是下面的情况,f1 被 构建为与f2 的申明类似。

struct f1 {
int x; int y[];
} f1 = { 1, { 2, 3, 4 } };

struct f2 {
struct f1 f1; int data[3];
} f2 = { { 1 }, { 2, 3, 4 } };

这个扩展的方便之处是f1 具有要求的类型,省去了查询f2.f1

这与普通静态数组对称,未知大小的数组同样写为[]。

当然,这个扩展仅当最高层目标的末尾有额外数据才有作用,否则我们将会覆盖接下来偏移的数据。为了避免初始化嵌入数组的复杂和混乱,我们可以拒绝任 何非空的初始化,除非结构体是最高层的目标。例如:

struct foo { int x; int y[]; };
struct bar { struct foo z; };

struct foo a = { 1, { 2, 3, 4 } }; // Legal.
struct bar b = { { 1, { 2, 3, 4 } } }; // Illegal.
struct bar c = { { 1, { } } }; // Legal.
struct foo d[1] = { { 1 { 2, 3, 4 } } }; // Illegal.

5.14 变长数组

ISO C99中允许变长自动数组,扩展GCC在C89模式和C++中接受了它们。(然而,GCC的变长数组在细节上仍然与ISO C99 标准有所不同。)这些数组的申明类似与其它自动数组,但是具有一个非常量表达式的长度。存储空间在定义的时候分配,当花括号结束时解除分配的空间。例如:

FILE *
concat_fopen (char *s1, char *s2, char *mode)
{
char str[strlen (s1) + strlen (s2) + 1];
strcpy (str, s1);
strcat (str, s2);
return fopen (str, mode);
}

转移或者中断超出数组名作用范围则会解除分配空间。进入作用域是不允许的;你会得到一个错误信息。

你可以使用alloca 函数来获得更像的变长数组。函数alloca 在大多数C中可以使用 (不是所有的)。另外,变长数组更好一些。

这两种方法还有其它不同之处。由alloca 分配的空间直到包含的函数返回时才回收。对于变长数组这个空间在数组作用范 围的最后被回收。(如果你在一个函数中同时使用变长数组和alloca ,重新分配的变长数组将会解除所有最近由alloca 分 配的空间。)

你同样可以在函数中使用变长数组作为参数:

struct entry
tester (int len, char data[len][len])
{
...
}

数组的长度在存储空间分配的时候被计算,它是为了数组作用范围而记忆,万一你使用sizeof 访问它。

如果你想首先传递数组然后是长度,你可以在参数列表中使用预先定义--另一个GNU扩展。

struct entry
tester (int len; char data[len][len], int len)
{
...
}

分号前的'int len '是预先定义的参数,它的目的是当申明的数据处理时知道len 的名 字。

你可以在预先定义中写任意数量的的参数。它们可以被逗号或者分号分开,但是最后一个必须以分号结束,它后面跟着“真正”的参数申明。每一个预先定义 必须在参数名和数据类型与一个“真实”定义相对应。ISO C99不支持参数的预先定义。

5.15 可变参数的宏

在1999年的ISO C标准中,宏可以被申明为可以接受变量以及与函数一样多的参数。定义宏的语法与函数的定义十分相似。下面是一个例子:

#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)

这儿的‘...’是可变参数。在调用中这样一个宏,它代表零或者多个标记直到括号结束这个调用,包含任何逗号。这种标记设置代替了宏结构中的标识符__VA_ARGS__ 当 它出现的时候。在CPP手册中获得更多信息。

GCC支持可变参数宏,使用一种不同的语法允许你给可变参数起一个名字,就像其它参数一样。下面是一个例子:

#define debug(format, args...) fprintf (stderr, format, args)

上面是全部与ISO C相同的方法,但按理说更易读和描述。

GNU CPP有两个可变参数宏扩展,允许他们使用上面任意一种定义方式。

在标准C中,不允许省略全部可变参数;但允许传递一个空参数。例如,这个调用在ISO C中是非法的,因为在字符串后没有逗号:

debug ("A message")

GNU CPP允许你完全省略可变参数。上面的例子,编译器会产生警告,即使宏扩展在格式字符串后仍然含有额外的逗号。

为了解决这个问题,CPP为可变参数指定特殊行为,使用连接操作符'##'。如果作为替代可以写为

#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)

如果可变参数为空或者被省略,'##'操作符使得预处理程序删除它前面的逗号。如果你在宏调用中使用了一些可变参数,GNU CPP不会警告连接操作符,并会替代逗号后面的可变参数。就像其它pasted宏一样,这些参数不是宏扩展。

5.16 为反向换行轻微放宽规则

最近,非传统预处理器放松了对反向换行的处理。先前,换行需要立即跟一个反斜杠。当前的执行允许由空格,水平和垂直制表符,和换页组成的空白空间在 反斜杠和接下来的换行中。预处理程序会生产一个警告,但会把它作为一个有效的反向换行对待并且连接两行组成一个新的逻辑行。这个工作包含注释和标记,包括 多行字符串,标记也一样。这个放松的目的是使注释不被当作空白空间,即使它们还没有被空格代替。

5.17 包含换行的字符串常量

作为扩展,GNU CPP允许字符串文字的通过多行不用反向换行。字符串文字每一个内嵌换行由'\n'字符代替,忽略了原始的换行。

CPP现在也允许这样的字符串指令(除了'#include'家族)。这个现在被反对,最终会被删除。

5.18 非左值数组可以具有下标

数组中允许下标,它们不是左值,即使‘&’操作不是。(在ISO C99中,两个都允许(即使数组可能不会在下一个顺序中被使用),但是ISO C99的特性还没有在GCC中完全支持。)例如,下面这个例子在GNU C中是正确的,但在C89中却不正确:

struct foo {int a[4];};

struct foo f();

bar (int index)
{
return f().a[index];
}

5.19 void与函数指针的运算

在GNU C中,void 与函数的指针支持加法与减法操作。这个是把void 与函数的大小看 为1。

由于是这样,使得sizeof 也允许在void 与函数上使用,它返回1。

如果这个扩展被使用,选项'-Wponter-arith '会请求一个警告。

5.20 非常量初始化

在标准C++和ISO C99中,一个聚合初始化程序的元素作为一个自动变量不需要常量表达式在GNU C中。下面是一个执行改变元素的初始化程序:

foo (float f, float g)
{
float beat_freqs[2] = { f-g, f+g };
...
}

5.21 复合文字

ISO C99支持复合文字。复合文字看起来就像是一个包含初始化的转换。它的值是转换中指定类型的对象,包含初始化中指定的元素。(GCC还没有实现全部ISO C99复合文字的语义。)作为一个扩展,GCC支持C89和C++中的复合文字。

通常,指定的类型是一个结构体。假设struct foostructure 声明如下:

struct foo {int a; char b[2];} structure;

这里有一个用复合文字构造一个struct foo 的例子:

structure = ((struct foo) {x + y, 'a', 0});

这等价于下面:

{
struct foo temp = {x + y, 'a', 0};
structure = temp;
}

你也可以构造一个数组。如果所有的复合文字元素都是常量表达式(组成),适合在初始化中使用,那么复合文字就是一个左值,而且能够强制转化为指向它 第一个元素的指针,就像这里所示:

char **foo = (char *[]) { "x", "y", "z" };

复合文字的元素不是简单的常量时,排列它们并不是很有用,因为复合文字不是一个左值;ISO C99指明了它,是一个和闭合控制块相关的,拥有自动存储空间和生存期的临时对象,但GCC没有实现这个功能。GCC中目前只有两种有效的方式去使用它: 用下标索引它,或者用它去初始化一个数组变量。前者可能比switch 语句慢,然而后者和一个普通的C初始化程序做的事情一 样。这里有一个用下标索引一个数组复合文字的例子:

output = ((int[]) { 2, x, 28 }) [input];

标量类型和共同体类型的复合文字也是允许的,但那时复合文字和一个强制转换等价。

5.22 特定的初始化

标准C89需要初始化语句的元素以固定的顺序出现,和被初始化的数组或结构体中的元素顺序一样。

在ISO C99中,你可以按任何顺序给出这些元素,指明它们对应的数组的下标或结构体的成员名,并且GNU C也把这作为C89模式下的一个扩展。这个扩展没有在GNU C++中实现。

为了指定一个数组下标,在元素值的前面写上“[index] = ”。比如:

int a[6] = { [4] = 29, [2] = 15 };

相当于:

int a[6] = { 0, 0, 15, 0, 29, 0 };

下标值必须是常量表达式,即使被初始化的数组是自动的。

一个可替代这的语法是在元素值前面写上“.[index] ”,没有“=”,但从GCC 2.5开始就不再被使用,但GCC仍然接受。为了把一系列的元素初始化为相同的值,写为“[first ... last] = value ”。 这是一个GNU扩展。比如:

int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };

如果其中的值有副作用,这个副作用将只发生一次,而不是范围内的每次初始化一次。

注意,数组的长度是指定的最大值加一。

在结构体的初始化语句中,在元素值的前面用“.fieldname = ”指定要初始化的成员名。例如,给定下面的结构体,

struct point { int x, y; };

和下面的初始化,

struct point p = { .y = yvalue, .x = xvalue };

等价于:

struct point p = { xvalue, yvalue };

另一有相同含义的语法是“.fieldname: ”,不过从GCC 2.5开始废除了,就像这里所示:

struct point p = { y: yvalue, x: xvalue };

[index] ”或“.fieldname ”就是指示符。在初始化共同体时,你也可以使用 一个指示符(或不再使用的冒号语法),来指定共同体的哪个元素应该使用。比如:

union foo { int i; double d; };

union foo f = { .d = 4 };

将会使用第二个元素把4转换成一个double 类型来在共同体存放。相反,把4转换成union foo 类 型将会把它作为整数i存入共同体,既然它是一个整数。(参考5.24节向共同体类型转换。)

你可以把这种命名元素的技术和连续元素的普通C初始化结合起来。每个没有指示符的初始化元素应用于数组或结构体中的下一个连续的元素。比如,

int a[6] = { [1] = v1, v2, [4] = v4 };

等价于

int a[6] = { 0, v1, v2, 0, v4, 0 };

当下标是字符或者属于enum 类型时,标识数组初始化语句的元素特别有用。例如:

int whitespace[256]
= { [' '] = 1, ['\t'] = 1, ['\h'] = 1,
['\f'] = 1, ['\n'] = 1, ['\r'] = 1 };

你也可以在“=”前面写上一系列的“.fieldname ”和“[index] ”指示符来指 定一个要初始化的嵌套的子对象;这个列表是相对于和最近的花括号对一致的子对象。比如,用上面的struct point 声 明:

struct point ptarray[10] = { [2].y = yv2, [2].x = xv2, [0].x = xv0 };

如果同一个成员被初始化多次,它将从最后一次初始化中取值。如果任何这样的覆盖初始化有副作用,副作用发生与否是非指定的。目前,gcc会舍弃它们 并产生一个警告。

5.23 case范围

你可以在单个case 标签中指定一系列连续的值,就像这样:

case low
 ... high
:

这和单独的case 标签的合适数字有同样的效果,每个对应包含在从low到high中的一个整数值。

这个特性对一系列的ASCII字符代码特别有用:

case 'A' ... 'Z':

当心:在...周围写上空格,否则当你把它和整数值一起使用时,它就会被解析出错。例如,这样写:

case 1 ... 5:

而不是:

case 1...5:

5.24 向共同体类型转换

向共同体类型转换和其它转换类似,除了指定的类型是一个共同体类型。你可以用union tag 或一个typedef 名 字来指定类型。向共同体转换实际上却是一个构造,而不是一个转换,因此不像普通转换那样产生一个左值。(参考5.21节复合文字 )

可以向共同体类型转换的类型是共同体中成员的类型。所以,给定下面的共同体和变量:

union foo { int i; double d; };
int x;
double y;

x和y都能够被转换成类型union foo

把这种转换作为给共同体变量赋值的右侧和在这个共同体的成员中存储是等价的:

union foo u;
...
u = (union foo) x == u.i = x
u = (union foo) y == u.d = y

你也可以使用共同体转换作为函数参数。

void hack (union foo);
...
hack ((union foo) x);

5.25 混合声明和代码

ISO C99和ISO C++允许声明和代码在复合语句中自由地混合。作为一个扩展,GCC在C89模式下也允许这样。比如,你可以:

int i;
...
i++;
int j = i + 2;

每个标识符从它被声明的地方到闭合控制块结束都是可见的。

5.26 声明函数的属性

在GNU C中,你可以声明关于在你程序中调用的函数的某些东西,来帮助编译器优化函数调用和更仔细地检查你的代码。

关键字__attribute__ 允许你在声明时指定特殊的属性。跟在这个关键字后面的是双重圆括号里面的属性说明。有 十四个属性noreturn, pure, const, format, format_arg, no_instrument_function, section, constructor, destructor, unused, weak, malloc, alias and no_check_memory_usage 是目前为函数定义的。在特别的目标系统上,也给函数定义 了一些其它属性。其它属性,包括section 都为变量声明(参考5.33节 指定变量属性 )和类型(参考5.34节 指定类型属性 )所支持。

你也可以把“__ ”放在每个关键字的前面和后面来指定属性。这允许你在头文件中使用它们,而不用关心一个可能有相同名字 的宏。比如,你可以使用__noreturn__ 而不是noreturn

参见5.27节 属性语法 来了解使用属性的精确语 法细节。

noreturn

一些标准库函数,就像abortexit ,不能返回。GCC会自动了解到这一点。一些程序 定义它们自己的从不返回的函数。你可以把它们声明为noreturn 来告诉编译器这个事实。比如,

void fatal () __attribute__ ((noreturn));

void
fatal (... )
{
... /* Print error message. */ ...
exit (1);
}

关键字noreturn 告诉编译器去假设fatal 不能返回。那它就能做优化,而不用理会如 果fatal 返回会发生什么。这会产生稍微好一点儿的代码。更重要的是,它有助于避免未初始化变量的伪造警告。

不要假设调用函数保存的寄存器在调用noreturn 函数之前被恢复。

对于一个noreturn 函数,有一个除void 之外的返回类型是毫无意义的。

在早于2.5版的GCC中没有实现noreturn 属性。声明不返回值的函数的一个可替代的方法,在当前版本和一些旧的 版本中都可以工作,如下:

typedef void voidfn ();

volatile voidfn fatal;

pure

很多函数除了返回值外没有作用,而且它们的返回值只取决于参数和/或全局变量。这样的一个函数可能依附于普通的子表达式的消除和循环的优化,就像一 个算术操作符那样。这些函数应该用属性pure 来声明。例如,

int square (int) __attribute__ ((pure));

说明假定的函数square 可以安全地比程序中说的少调用几次。

pure函数的一些常见例子是strlenmemcmp 。有趣的非pure 函 数是带无限循环,或者那些取决于易失性内存或其它系统资源的函数,它们可能在两次连续的调用中间改变(比如在多线程环境中的feof )。

pure 属性在GCC早于2.96的版本中没有实现。

const

很多函数不检查除它们的参数外的任何值,而且除返回值外没有任何作用。基本上,这比上面的pure 属性稍微更严格一些, 既然函数不允许去读全局内存。

注意,带指针参数,而且检查所指向数据的函数不能声明为const 。同样的,调用非const 函 数的函数通常也不能是const 。一个const 函数返回void 是没 任何意义的。

属性const 在GCC早于2.5的版本中没有实现。声明一个函数没有副作用的一个可替代的方式,能够在当前版本和一些 旧的版本中工作,如下:

typedef int intfn ();

extern const intfn square;

这种方法在2.6.0以后的GNU C++不起作用,既然语言指明const 必须依附于返回值。

format (archetype , string-index , first-to-check )

format 属性指明一个函数使用printf,scanf,strftimestrfmon 风 格的参数,应该通过格式化字符串进行类型检查。比如,声明:

extern int
my_printf (void *my_object, const char *my_format, ...)
__attribute__ ((format (printf, 2, 3)));

会促使编译器检查调用my_printf 中的参数和printf 风格的格式化字符串参数my_format 是 否一致。

参数archetype 决定格式化字符串是怎么被解释的,而且应当是printf,scanf,strftimestrfmon 。(你 也可以使用__printf__,__scanf__,__strftime__ 或者__strfmon__ 。) 参数string-index 指定哪个参数是格式化字符串参数(从1开始),而first-to-check 是 通过格式化字符串检查的第一个参数。对于参数不可用来检查的函数(比如vprintf ),指定第三个参数为0。在这种情况下, 编译器只检查格式化字符串的一致性。对于strftime 格式,第三个参数需要为0。

在上面的例子中,格式化字符串(my_format )是my_printf 函数的第二个参 数,而且要检查的函数从第三个参数开始,所以format 属性的正确参数是2和3。

format 属性允许你去识别你自己的把格式化字符串作为参数的函数,所以GCC可以检查对这些函数的调用错误。编译器 总是(除非使用了“-ffreestanding ”)为标准库函数printf,fprintf,sprintf,scanf,fscanf,sscanf,strftime,vprintf,vfprintfvsprintf 检 查格式,当请求这种警告时(使用“-Wformat ”),所以没有必要修改头文件stdio.h 。 在C99模式下,函数snprintf,vsnprintf,vscanf,vfscanfvsscanf 也 被检查。参考“控制C方言的选项”一节。

format_arg (string-index)

format_arg 属性指明一个函数使用printf,scanf,strftimestrfmon 风 格的参数,而且修改它(比如,把它翻译成其它语言),所以结果能够传递给一个printf,scanf,strftimestrfmon 风 格的函数(格式化函数的其余参数和它们在不修改字符串的函数中一样)。例如,声明:

extern char *
my_dgettext (char *my_domain, const char *my_format)
__attribute__ ((format_arg (2)));

促使编译器检查调用printf,scanf,strftimestrfmon 类型的函数 中的参数,其格式化字符串参数是函数my_dgettext 函数的调用,和格式化字符串参数my_format 是 否一致。如果format_arg 属性没有被指定,在对格式化函数的这种中,编译器所能告知的一切是格式化字符串参数不是常 量;当使用“-Wformat-nonliteral ”时,这会产生一个警告,但如果没有属性,调用将不会被检查。

参数string-index 指定哪个参数是格式化字符串(从1开始)。

format-arg 属性允许你去识别你自己的修改格式化字符串的函数,那样,GCC可以检查对printf,scanf,strftimestrfmon 类 型函数的调用,它们的操作数是对你自己的一个函数的调用。编译器总是以这种方式对待gettext,dgettextdcgettext , 除了当严格的ISO C支持通过“-ansi ”或者一个合适的“-std ”选项请求时,或者“-ffreestanding ” 使用时。参考“控制C方言的选项”一节。

no_instrument_function

如果给定“-finstrument-functions ”,在大多数用户编译的函数的入口和出口会生成对概要分析函数 的调用。有这个属性的函数将不会被测量。

section ("section-name")

通常,编译器会把它生成的代码放入text 部分。有时,然而,你需要额外的部分,或者你需要某些特别的函数出现在特别的 部分。section 属性指定一个函数放入一个特别的部分。比如,声明:

extern void foobar (void) __attribute__ ((section ("bar")));

把函数foobar 放进bar 部分。

一些文件格式不支持任意部分,所以section 属性并不是在所有平台上可用的。如果你需要把一个模块的全部内容映射到 一个特别的部分,考虑使用链接器的工具。

constructor

destructor

constructor 属性促使函数在执行main() 之前自动被调用。类似地,destructor 属 性促使函数在main() 函数完成或exit() 被调用完之后自动被调用。有这些属性的函数对初始 化在程序执行期间间接使用的数据很有用。

这些属性目前没有为Objective C所实现。

unused

这个属性,依附于一个函数,意味着这个函数将可能打算不被使用。GCC将不会为这个函数产生一个警告。GNU C++目前不支持这个属性,因为没有参数的定义在C++中是合法的。

weak

weak 属性促使声明被作为一个弱符号导出,而不是全局符号。这在定义库函数时非常有用,它们能够被用户代码覆盖,虽然 它也可以和非函数声明一起使用。弱符号被ELF 目标文件所支持,而且当使用GNU汇编器和链接器时也被a.out 目 标文件支持。

malloc

malloc 属性用来告诉编译器一个函数可以被当做malloc 函数那样。编译器假设对malloc 的 调用产生一个不能替换成其它东西的指针。

alias ("target")

alias 属性促使这个声明被作为另一个必须被指定的符号的别名导出。例如,

void __f () { /* do something */; }
void f () __attribute__ ((weak, alias ("__f")));
<
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics