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 表达式中的语句和声明
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 |
一个嵌套函数一般都有它的内部连接。用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]; } ... } |
通过使用下面所描述的内置函数,你就可以把一个函数所接收的参数记录下来,然后用同样的参数调用另一个函数而不管参数的数目和类型。
你同样也可以将函数调用的返回值记录下来,然后返回同样的值,而不管这个函数试图返回什么样的数据类型,只要你的调用函数需要这个值。
-
这个内置函数返回一个指针,而这个指针指向的数据描述了如何用传给当前函数的参数来进行一个函数调用。
这个函数返回一个由参数指针寄存器,结构体值地址以及所有可能用来传递参数的寄存器组成的堆栈段的地址。
内置函数:
-
这个内置函数通过一份由
arguments
和size
指定的参数的拷贝来调用函数。arguments
的值必须是_builtin_apply_args
函数的返回值,而size参数指定了堆栈参数数据的字节数。这个内置函数返回一个指针,而这个指针指向的数据描述了如何返回
function
函数返回的不管任何类型的返回值。这个数据保存在堆栈段中。有时候,计算确切的size的值并不容易。
__builtin_apply
函数使用这个值来计算需要入栈的数据大小,以及需要从数据来源拷贝多少数据。
内置函数:void * __builtin_apply_args ()
void * __builtin_apply (void (*function)(), void *arguments, size_t size)
内置函数:
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; }) |
给局部变量起以下划线开头的名字的原因是为了防止与表达式中用来替代a
和b
的变量的变量名产生冲突。最后,我们希望可以设计出一种新的声明语法,这种语法允许你声明作用域仅在他们的初始化语句之后的变量;用这种方法来防止这种冲突更加可靠。
5.7 使用typeof指代类型
另外一种提供表达式类型的方法是使用typeof
。使用这个关键词的语法与sizeof
相似,但是建立的语义与typedef
定义一个类型相似。
有两种写typeof
参数的方法:跟一个表达或者一个类型。下面是一个跟表达式的例子:
typeof (x[0](1)) |
这假定x
是一个指向函数的指针数组;描述的类型是函数的值。
下面的例子使用类型名做为参数:
typeof (int *) |
这儿的类型描述的是指向int
的指针。
如果你正在写一个头文件必须在包含ISO C程序时工作,使用__typeof__
代替typeof
。参考5.39节 备用关键字
typeof
可以在任何地方使用,typedef
名字可能会被用到。例如,你可以在申明的时候使用它,在一个转换中或者在sizeof
,typeof
中使用。
- 这个声明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 foo
和structure
声明如下:
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
一些标准库函数,就像abort
和exit
,不能返回。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函数的一些常见例子是strlen
和memcmp
。有趣的非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,strftime
或strfmon
风格的参数,应该通过格式化字符串进行类型检查。比如,声明:
extern int my_printf (void *my_object, const char *my_format, ...) __attribute__ ((format (printf, 2, 3))); |
会促使编译器检查调用my_printf
中的参数和printf
风格的格式化字符串参数my_format
是否一致。
参数archetype
决定格式化字符串是怎么被解释的,而且应当是printf,scanf,strftime
或strfmon
。(你也可以使用__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,vfprintf
和vsprintf
检查格式,当请求这种警告时(使用“-Wformat
”),所以没有必要修改头文件stdio.h
。在C99模式下,函数snprintf,vsnprintf,vscanf,vfscanf
和vsscanf
也被检查。参考“控制C方言的选项”一节。。
format_arg (string-index)
format_arg
属性指明一个函数使用printf,scanf,strftime
或strfmon
风格的参数,而且修改它(比如,把它翻译成其它语言),所以结果能够传递给一个printf,scanf,strftime
或strfmon
风格的函数(格式化函数的其余参数和它们在不修改字符串的函数中一样)。例如,声明:
extern char * my_dgettext (char *my_domain, const char *my_format) __attribute__ ((format_arg (2))); |
促使编译器检查调用printf,scanf,strftime
或strfmon
类型的函数中的参数,其格式化字符串参数是函数my_dgettext
函数的调用,和格式化字符串参数my_format
是否一致。如果format_arg
属性没有被指定,在对格式化函数的这种中,编译器所能告知的一切是格式化字符串参数不是常量;当使用“-Wformat-nonliteral
”时,这会产生一个警告,但如果没有属性,调用将不会被检查。
参数string-index
指定哪个参数是格式化字符串(从1开始)。
format-arg
属性允许你去识别你自己的修改格式化字符串的函数,那样,GCC可以检查对printf,scanf,strftime
或strfmon
类型函数的调用,它们的操作数是对你自己的一个函数的调用。编译器总是以这种方式对待gettext,dgettext
和 dcgettext
,除了当严格的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"))); |
声明“f
”是“__f
”的一个弱别名。在C++中,目标的重整名字必须被使用。
并不是所有的目标机器支持这个属性。
no_check_memory_usage
no_check_memory_usage
属性促使GCC忽略内存引用的检查,当它为函数生成代码时。通常如果你指定“-fcheck-memory-usage
”(参考“3.18 代码生成转换选项”一节),GCC在大多数内存访问之前生成调用支持的例程,以便允许支持代码记录用法和探测未初始化或未分配存储空间的使用。既然GCC不能恰当处理asm
语句,它们不允许出现在这样的函数中。如果你用这个属性声明了一个函数 ,GCC将会为那个函数生成内存检查代码,允许使用asm
语句,而不必用不同选项去编译那个函数。这允许你编写自己的支持例程如果你愿意,而不会导致无限递归,如果它们用“-fcheck-memory-usage
”编译的话。
regparm (number)
在Intel 386上,regparm
属性促使编译器用寄存器EAX,EDX
,和ECX
,而不是堆栈,来传递最多number
个参数。带可变参数数目的函数将会继续在堆栈上传递它们的参数。
stdcall
在Intel 386上,stdcall
属性促使编译器假定被调用的函数将会弹出用来传递参数的堆栈空间,除非它适用可变数目的参数。
cdecl
在Intel 386上,cdecl
属性促使编译器假定调用函数将会弹出用来传递参数的堆栈空间。这对覆盖“-mrtd
”开关的作用很有帮助。
PowerPC上Windows NT的编译器目前忽略cdecl
属性。
longcall
在RS/6000和PowerPC上,longcall
属性促使编译器总是通过指针来调用函数,所以在距当前位置超过64MB(67108864字节)的函数也能够被调用。
long_call/short_call
这个属性允许指定如果在ARM上调用一个特别的函数。两个属性都覆盖“-mlong-calls
”命令行开关和#pragma long_calls
设置。long_call
属性促使编译器总是通过先装入它的地址到一个寄存器再使用那个寄存器的内容来调用这个函数。short_call
属性总是直接把从调用现场到函数的偏移量放进‘BL’指令中。
dllimport
在运行Windows NT的PowerPC上,dllimport
属性促使编译器通过一个全局指针去调用函数,这个指针指向由Windows NT的dll库安装的函数指针。指针名是通过组合__imp__
和函数名来形成的。
dllexport
在运行Windows NT的PowerPC上,dllexport
属性促使编译器提供一个指向函数指针的全局指针,那样它就能用dllimport
属性调用。指针名是通过组合__imp__
和函数名来形成的。
exception (except-func [, except-arg])
在运行Windows NT的PowerPC上,exception
属性促使编译器修改为声明函数导出的结构化异常表的表项。字符串或标识符except-func
被放在结构化异常表的第三项中。它代表一个函数,当异常发生时被异常处理机制调用。如果它被指定,字符串或标识符except-arg
被放在结构化异常表的第四项中。
function_vector
在H8/300和H8/300H上使用这个选项用表明指定的函数应该通过函数向量来调用。通过函数向量调用函数将会减少代码尺寸,然而,函数向量有受限的大小(在H8/300上最多128项,H8/300H上64项),而且和中断向量共享空间。
为此选项,你必须使用2.7版或以后的GNU binutils中的GAS和GLD才能正确工作。
interrupt
在H8/300,H8/300H和SH上使用这个选项表明指定的函数是一个中断处理程序。当这个属性存在时,编译器将会生成函数入口和出口工序,为适应在中断处理程序中的使用。
注意,H8/300,H8/300H和SH处理器的中断处理程序可以通过interrupt_handler
属性来指定。
注意,在AVR上,中断将会在函数里面被启用。
注意,在ARM上,你可以通过给中断属性添加一个可选的参数指定要处理的中断类型,就像这样:
void f () __attribute__ ((interrupt ("IRQ")));
这个参数的允许值是:IRQ, FIQ, SWI, ABORT
和UNDEF
。
interrupt_handler
在H8/300,H8/300H和SH上使用这个选项表明指定的函数是一个中断处理程序。当这个属性存在时,编译器将会生成函数入口和出口工序,为适应在中断处理程序中的使用。
sp_switch
在SH上使用这个选项表明一个interrupt_handler
函数应该切换到一个可替代的堆栈上。它期待一个字符串参数,用来命名一个存放替代堆栈地址的全局变量。
void *alt_stack; void f () __attribute__ ((interrupt_handler, sp_switch ("alt_stack"))); |
trap_exit
在SH上为interrupt_handle
使用此选项来使用trapa
而不是rte
来返回。这个属性期待一个整数参数来指定要使用的陷阱号。
eightbit_data
在H8/300和H8/300H上使用此选项来表明指定的变量应该放到8比特数据区。编译器将会为在8比特数据区上的操作生成更高效的代码。注意8比特数据区的大小限制在256字节。
tiny_data
在H8/300H上使用此选项来表明指定的变量应该放到微数据区。编译器将会为在微数据区中存取数据生成更高效的代码。注意微数据区限制在稍微低于32K字节。
signal
在AVR上使用此选项来表明指定的函数是一个信号处理程序。当这个属性存在时,编译器将会生成函数入口和出口工序,为适应在信号处理程序中的使用。在函数内部,中断将会被屏蔽。
naked
在ARM或AVR移植上使用此选项来表明指定的函数不需要由编译器来生成开场白/收场白工序。由程序员来提供这些工序。
model (model-name)
在M32R/D上使用这个属性来设置对象和函数生成代码的可寻址性。标识符model-name是small,medium或large其中之一,各代表一种编码模型。
small模型对象驻留在内存的低16MB中(所以它们的地址可以用ld24指令来加载),可用bl指令调用。
medium模型对象可能驻留在32位地址空间的任何地方(编译器将会生成seth/add3指令来加载它们的地址),可用bl指令调用。
large模型对象可能驻留在32位地址空间的任何地方(编译器将会生成seth/add3指令来加载它们的地址),而且可能使用bl指令够不到(编译器将会生成慢得多的seth/add3/jl指令序列)。
你可以在一个声明中指定多重属性,通过在双圆括号中用逗号来把它们分割开,或者在一个属性声明后紧跟另一个属性声明。
一些人反对__attribute__
特性,建议使用ISO C的#pragma
来替代。在设计__attribute__
时,有两条原因不适合这么做。
- 不可能从宏中生成
#pragma
命令。 - 没有有效说明同样的
#pragma
在另一个编译器中可能意味着什么。
这两条原因适用于几乎任何提议#pragma
的应用程序。为任何东西使用#pragma
基本上都是一个错误。
ISO C99标准包括_Pragma
,它现在允许从宏中生成pragma
。另外,#pragma
GCC名字空间现在为GCC特定的pragma
使用。然而,人们已经发现使用__attribute__
来实现到相关声明的自然的附件属性很方便,而为构造而使用的#pragma
GCC没有自然地形成语法的一部分。查看“C预处理器”中的“多种预处理命令”一部分。
5.27 属性语法
这一段说明了在C语言中,使用到__attribute__
的语法和属性说明符绑定的概念。一些细节也许对C++和Objective C有所不同。由于对属性不合适的语法,这里描述的一些格式可能不能在所有情况下成功解析。
看5.26节,声明函数的属性,了解施加于函数的属性语义的细节。看5.33节,说明变量属性,了解施加于变量的属性语义的细节。看5.34节指定类型属性,了解施加与结构体,共用体,和枚举类型的属性语义的细节。
属性说明符是的格式是:__attribute__
((属性列表))。属性列表是一个可为空的由逗号分隔的属性序列,其中的属性类型如下:
- 空。空属性会被忽略。
- 字(可能是一个标识符如unused,或者一个保留字如const)。
- 在跟在后边的圆括号中有属性的参数的字。这些参数有如下的格式:
- 标识符。比如,模式属性使用这个格式。
- 跟有逗号或非空的由逗号分隔的表达式表。比如,格式化属性使用这个格式。
- 可为空的由逗号分隔的表达式表。比如,格式化参数使用这个格式和一个单独整型常量表达式列表,并且别名属性使用这个格式和一个单独的字符串常量列表。
属性说明符列表是一个由一个或多个属性说明符组成的序列,不被任何其它标志分开。
属性说明符列表可能跟在一个标签后的冒号出现,除了
case
或default
中的标签。唯一的属性使用在一个未使用的标签后是合理的。这个特征被设计成代码被包含而未使用的标签但是在编译是使用了"-Wall
"参数。它也许不常用与人工编写的代码中,虽然它应该在代码需要跳到一个包含在一段有#ifdef
说明的条件编译的程序段中很有用。属性说明符列表可以作为结构体、共用体或枚举说明符的一部分出现。它既可以直接跟在结构体、共用体或枚举说明符后,也可以紧贴大括号之后。如果结构体、共用体或枚举类型没有在使用属性说明符列表中用到的说明符定义,也就是说在如下这样的用法中
struct __attribute__((foo))
没有跟空的大括号,它会被忽略。在用到属性说明符的地方,跟它靠近的大括号,它们会被认为和结构体、共用体或被定义的枚举类型有联系,不同任何出现在包含声明的类型说明符,并且类型直到属性说明符之后才结束。否则,属性说明符作为声明的一部分出现,计算未命名参数和类型明声明,并且和这个声明相关(有可能内嵌在另外一个声明中,例如在参数声明的时候)。将来属性说明符在一些地方也任何用于特殊声明符不超出代替声明;这些情况将在一下提到。在属性说明符被用于在函数或数组中说明参数的地方,它也许用于函数或数组而不是指向一个会被隐含转换的参数的指针,但这不仅是正确的工具。
任何在可能包含属性说明符的声明开始处的说明符与修饰符列表,抑或没有这样一个列表也许在上下文包含存储类说明符。(一些属性尽管本质自然是类型说明符,并且仅在需要使用存储类说明符的地方是合理的,例如section。)对这个语法有一个必要的限制:第一,在函数定义中的老式风格的参数声明无法有属性说明符开头,这种情况尚不提供)。在一些其它情况下,属性说明符被允许使用这种语法但不被编译器所支持。所有的属性说明符在这里被当做正割声明的一部分。在逐步被废弃的在int类型默认省略了类型说明符的用法的地方,这样的说明符和修饰符列表可能是没有任何其它说明符或修饰符的属性说明符列表。
在不止一个标识符使用单独的说明符或修饰符的声明中的由逗号分隔的说明符列表中,属性说明符列表可能直接在一个说明符之前出现(第一个除外)。目前,这样的属性说明符不仅适用于被出现在自己的声明中的标识符,也可以用于在此声明明中此后声明的所有标识符,但是将来它们可能只能用于那样的单独的标识符。例如:
__attribute__((noreturn)) void d0 (void), __attribute__((format(printf, 1, 2))) d1 (const char *, ...), d2 (void)
无返回值属性用于所有的函数声明中;格式化属性会只用于d1,但是目前也用于d2(并且由此造成错误)。
属性说明符列表可能直接出现在逗号、等号或分号之前,终止标识符的说明除过函数定义。目前,这样的属性说明符用于已声明的对象或函数,但是将来它们将附属与相邻的最远的说明符。在简单情况下没有区别,但是例如在
void (****f)(void) __attribute__((noreturn))
这样的声明中,当前无返回值的属性用于f,这就会造成从f起不是一个函数的警告,但是将来它也许用于函数****f。在这种情况中的属性的明确的语言符号将不用于定义。在对象或函数的汇编名称处被说明(看5.37节控制用于汇编代码的名称),当前,属性必须跟随与asm说明之后;将来,在asm说明之前的属性可能用于邻近的声明符,并且它那些在它之的被用于已声明的对象或函数。将来,属性说明符可能被允许在函数定义的声明符后出现(在任意老式风格参数声明或函数题之前)。
属性说明符列表可能出现在内嵌声明符的开始。目前,这种用法有一些限制:属性用于在这个声明中声明过的标识符和所有之后的声明过的标识符(如果它包括一个逗号分隔的声明符列表),而不仅是用于特定的声明符。当属性说明符跟在指针声明符“*”之后时,它们应该出现在任意一种修饰符序列之后,并且不能和它们混在一起。接下来的陈述预期将来的语言符号仅使这种语法更有用。如果你对标准ISO C的声明符的说明格式熟悉的话,它将更好理解。
考虑
T D1
这样的声明(像C99标准6.7.5第四段中的子句),T包含声明说明符的地方说明一个Type类型(比如int
)并且D1
是一个包含标识符的标志的声明符。类型位标志被说明用来导出类型不包括在属性说明符中的声明符就像标准ISO C那样。如果
D1
有(属性说明符列表D
)这样的格式,并且T D
这样的声明为标志说明了“导出声明符类型列表 类型”的类型,这样T D1
为标志说明了“导出声明符类型列表 属性说明符列表 类型”的类型。如果
D1
有 * 类型修饰符和属性说明符列表 D这样的格式,并且T D
这样的声明为标志说明“导出声明符类型列表 类型”的类型,则T D1为标志说明“导出声明符列表 类型修饰符和属性说明符列表 类型”的类型。例如,
void (__attribute__((noreturn)) ****f)()
;说明“指向返回空的无返回值函数的指针的指针的指针”的类型。作为另外的例子,char *__attribute__((aligned(8))) *f
;说明“指向8位宽度的指向char
型数据的指针的指针”的类型。再次注意这个陈述是被期待将来的语法符号,不是当前实现的。5.28 原型和老式风格的函数定义
GNU C对ISO C到允许函数原型忽略一个新的老式风格的无原型定义。考虑一下的例子:
/* 除非老式的编译器才使用原型*/
/* Use prototypes unless the compiler is old-fashioned. */ #ifdef __STDC__ #define P(x) x #else #define P(x) () #endif /* 原型函数声明. */ int isroot P((uid_t)); /* 老式风格的函数定义. */ int isroot (x) /* ??? lossage here ??? */ uid_t x; { return x == 0; }
设想类型
uid_t恰好是短整型。ISO C是决不允许这个例子的,因为在老式风格中的参数子字被提升了。因此在这个例子中,函数定义的参数确实是个和原型参数的短整型类型不匹配的整型。
ISO C的这个限制是它难以写出可以移植到传统C编译器上的代码,因为程序员不知道
uit_t类型是短整型、整型还是长整型。因此,像GNU C允许在这些情况下原型忽略新的老式风格的定义。更严谨的是在GNU C中,函数原型参数类型如果一个钱类型想后来的类型在提升以前一样,则忽略更新的老式风格定义说明的参数。因此在GNU C中上面这些个例子等价与下面的例子:
int isroot (uid_t); int isroot (uid_t x) { return x == 0; }
GNU C++ 不支持老式风格函数定义,故这个扩展和它是不相关的。
5.29 C++风格注释
在GNU C当中,你可以使用C++风格的注释,就是一"//"开头并且一直到本行末。许多其它的C实现方案允许这样的注释,并且它们可能成为将来的C标准。但是,C++风格注释在你说明了"
-ansi
",或"-std
"选项来声明使用ISO C在C99之前的版本时,将不会被识别,或者"-traditional
"选项,因为它们和传统的被这样的//*注释*/分隔符分隔的结构不相容。5.30 标识符名称中的美元符
在GNU C当中,你可以一般的在标识符名称中使用美元符。这是因为许多传统的C实现方案允许这样的标识符。但是,标识符中的美元符在少量目标机器不被支持,典型原因是目标汇编器不允许它们。
5.31 常量中的ESC字符
你可以在一个字符串或字符常量中使用序列'
/e'
来表示ASCII字符ESC
。5.32 询问变量对齐方式
关键字
__alignof__
允许你询问一个对象如何对齐,或者一个类型的需要的最小对齐。它的语法很像sizeof
。例如,不过目标机器需要一个双精度值来使一个8位的边界对齐,这样
__alignof__(double)
就是8.在许多RISC机器上就是这样的。在很多传统的机器设计,__alignof__(double)
是4或者甚至是2.一些机器实际上从不需要对齐;它们允许参考任意数据类型甚至在一个奇数地址上。对这些机器,
__alignof__
报告类型的建议对齐。当
__alignof__
的操作数是一个左值而不是一个类型时,这个值是这个左值已知有的最大对齐。它可能由于它的数据类有而有这个对齐,或者因为它是结构体的一部分并且从那个结构体继承了对齐。例如在这个声明之后:struct foo { int x; char y; } foo1;
__alignof__(foo1.y)
的值可能是2或4,同__alignof__(int)
相同,即使foo1.y
的数据类型自己不需要任何对齐。这是要求一个不完全的类型的对齐的错误。
一个使你说明一个对象对齐的关联特征是
__attribute__ ((aligned (alignment)))
;请看下节。5.33说明变量属性
关键字
__attribute__
允许你说明变量或结构体域的特殊属性。这个关键字是跟有括在一对圆括号中的属性说明。现在给变量定义了八个属性:aligned, mode, nocommon, packed, section, transparent_union, unused
,和weak
。在特定的目标机器上定义了为变量定义了一些其它属性。其它属性可以用于函数(见5.26节 声明函数属性)和类型(见5.34节指定类型属性)。其它q前端和末端可能定义更多的属性(见C++语言的扩展章节)。你可能也说明属性有‘
__
’开头并且跟在每一个关键字后边。这允许你在头文件中使用它们而不必担心可能有同名的宏。例如,你可以使用__aligned__
来代替aligned
。见5.27节属性语法,了解正确使用属性语法的细节。
int x __attribute__ ((aligned (16))) = 0;
move16
指令。
struct foo { int x[2] __attribute__ ((aligned (8))); };
typedef
定义的名称的对齐因为这个名字仅仅是个别名,而不是特定的类型。
short array[3] __attribute__ ((aligned));
packed
属性。见下边。
__attribute__
中说明
aligned(16)
仍然值提供给你8字节对齐。从你的连接器文档中可以获得更多的信息。
mode (模式)
mode
模式。这有效地使你获取一个整型或浮点型的符合宽度。
byte
'或'
__byte__
'模式来表示模式同一字节的整数一致,'
word
'或'
__word
'来表示一个字的整数的模式,并且'
pointer
'或'
__pointer__
'来表示指针的模式。
nocommon
common
"而是直接给它分配空间。如果你说明'
-fno-common
'标志,GCC将对所有变量这样做。给变量说明
nocommon
属性则提供初值为零。变量可能仅在一个源文件中初始化。
packed
packed
属性说明一个变量或结构体域应该有尽可能小的对齐──一个变量一字节或一个域一字节,除非你和对齐属性一起说明了一个更大的值。
x
被
packed
属性说明的结构体,所以它直接跟在
a
之后:
struct foo { char a; int x[2] __attribute__ ((packed)); }; |
section("段名")
data
和
bss
。但是有时候你学要附加段,或者你需要一定特定的变量去出现在特殊的段中,例如去映射特殊的硬件。
section
属性说明一个变量(或函数)存在一个特定的段中。例如,这个小程序使用一些说明段名:
struct duart a __attribute__ ((section ("DUART_A"))) = { 0 }; struct duart b __attribute__ ((section ("DUART_B"))) = { 0 }; char stack[10000] __attribute__ ((section ("STACK"))) = { 0 }; int init_data __attribute__ ((section ("INITDATA"))) = 0; main() { /* Initialize stack pointer */ init_sp (stack + sizeof (stack)); /* Initialize initialized data */ memcpy (&init_data, &data, &edata - &data); /* Turn on the serial ports */ init_duart (&a); init_duart (&b); } |
section
属性,就像例子中那样。GCC给出一个警告或者在未初始的变量声明中忽略
section
属性。
section
属性。连接器要求每个对象被定义一次,例外的是未初始化变量假定竟如
common
(或
bss
)段而且可以多重“定义”。你可以强制一个变量带'
-fno-common
'标志初始化或
nocommon
属性。
section
属性不全适用于所有的平台。如果你需要映射一个完全满足的模块到一个特定的段,慎重考虑使用连接器的设备来代替。
int foo __attribute__((section ("shared"), shared)) = 0; int main() { /* Read and write foo. All running copies see the same value. */ return 0; } |
section
属性完全初始化全局定义是使用
shared
属性,因为连接器的工作方式。看
section
属性来获得更多的信息。
shared
属性仅适用于Windows NT。
unused
weak
model(模型名)
seth/add3
指令来装入它们的地址)。
说明多个属性用逗号吧它们分隔开写在一对圆括号中:例如,'__attribute__ ((aligned (16), packed))'
。
5.34 指定类型属性
当你定义结构体和共用体类型时,关键字attribute
允许你为这些类型指定特殊的属性。这个关键字后面跟随着包含双parentheses
的指定类型。四中属性常被定义为:对齐(aligned),封装(packed)型,透明共用体型(transparent-union)和未使用。另外的属性则被定义为函数(看5.26段函数属性的声明)和变量(看5.33段指定变量属性)。
你可以指定这些属性在关键字之前或后面。这就使你能在头文件应用这种属性而不必声明 可能有同样名字的宏 例如:你能用_aligned__ instead of aligned
.
你可以在括号中放入枚举的定义或声明, 结构或共用类型的定义和集装属性,括号后指定其属性。
你也能在枚举,结构或共用间指定属性的tag
和名字而不是在)后。
看5.27 属性语法,对于准确使用语法属性
aligned
struct S { short f[3]; } __attribute__ ((aligned (8))); typedef int more_aligned_int __attribute__ ((aligned (8))); |
S
变量到另外一个时。拥有所有的结构体S 对齐8位的变量使编译器能使用
ldd
和
std
,因此可以提高运行效率。
aligen
对于结构体和共用体队列成员的属性。但是以上例子中插入的注释更加明显,直观和可读对于编译者来校正一个完全的结构体或共用体类型组合。
封装(packed)
transparent_union
wait
函数要适应4.1BSD接口。记入
wait
函数的参数是空型,
wait
函数将接受各种型的参数,但是它也能接受另外的指针型和这个将能使参数类型的检测降低效用。而为"sys/wait.h"定义的接口如下:
typedef union { int *__ip; union wait *__up; } wait_status_ptr_t __attribute__ ((__transparent_union__)); pid_t wait (wait_status_ptr_t); |
int w1 () { int w; return wait (&w); } int w2 () { union wait w; return wait (&w); } |
wait
的执行将是这样:
pid_t wait (wait_status_ptr_t p) { return waitpid (-1, p.__ip, 0); } |
未用(unused)
5.35 内联函数像宏一样快
通过声明一个内联函数,你就可以直接用GCC把函数源代码和调用它的函数源代码合成起来。这样,通过消除高层的函数调用使得函数执行更快;另外,如果任何的实参是常数,它们的已知值可能允许简化从而不使所有的内联函数代码在编译时被包含进来。这对代码大小的影响几乎是不可预知的;和内联函数相比,结果代码的大或小取决于具体情况。函数内联是一种优化,而且只在优化编译上起作用。如果你没有用'-0',那就没有函数是真正内联的。
在C99标准里包含内联函数,但是,当前,GCC的实现和C99的标准要求确实存在差异。
像这样,在函数声明里用内联的关键字可以声明一个函数内联:
inline int inc (int *a) { (*a)++; } |
(如果你正在写一个要被包含在一个标准C程序的头文件,请用 __inline__
代替 inline
.参见5.39节 备用关键字。)你同样可以通过加选项`-finline-functions
'使得所有足够简单的程序内联。
注意,在函数定义中的固定用法可以使函数不适合做内联。这些用法有:varargs
函数的使用 ,alloca
函数的使用, 可变大小数据类型的使用(参见5.14节 变长数组),goto
计算的使用 (参见 5.3节 可赋值标签), 非局部goto
语句的使用, 以及嵌套函数的使用(参见 5.4节嵌套函数 ).用`-Winline
'可以在一个函数被标记为内联而不能被取代时出现警告,并给出错误原因。
注意在C和Objective C中, 不象C++那样, 内联关键字不会影响函数的联接。
GCC会自动将定义在C++程序内class body
中的元函数内联即使它们没有被明确的声明为内联。(你可以用`-fno-default-inline
'忽略它;参见选项控制C++语法。)Options Controlling C++ Dialect
当一个函数同时是静态和内联时,如果所有对这个函数的调用都被综合在调用者中,而且函数地址从没有被使用过,函数所有的汇编代码都没有被引用过。在这种情况下,GCC事实上不会为这个函数输出汇编代码,除非你指定选项`-fkeep-inline-functions
'。由于各种原因一些函数调用不会被综合(特殊的,调用在函数定义之前和在函数中的递归调用是不能被综合的)。如果有一个非综合调用,函数会被像平常一样编译出汇编代码。如果程序引用了它的地址,这个函数也必须像平常一样被编译,因为地址是不能被内联的。
当一个函数不是静态时,编译器会假定在其它源文件中可能存在调用;由于在任何程序中全局符号(全局变量)只能被定义一次,函数一定不能在其它源文件中被定义,所以在那里的调用是不能被综合的。因此,通常一个非内联函数总是被独立的编译。
如果你在函数定义中同时指定内联和外部援引,那么这个定义只会被用作内联。即使没有明确的引用它的地址,函数也决不会被独立编译。这些地址变成了外部援引,就好像你只是声明了函数,没有定义它一样。
这种内联和外部援引的结合和宏定义的效果差不多。这种结合的用法是把函数定义和这些关键字放在一个头文件中,把另外一份定义(缺少内联和外部援引)的拷贝放在库文件中。头文件中的定义会使对这个函数的大多数调用成为内联。如果还有其它函数要用,它们将会查阅库文件中专门的拷贝文件。
为了将来当 GCC 实现 C99 标准语法中的内联函数时有兼容性,仅使用静态内联是最好的。(当`-std=gnu89
'被指明时,当前语法可以保留可用部分,但最后的默认将会是`-std=gnu99
',并且它将会实现C99语法,尽管现在他并没有被这样做。)
GCC没有优化是没有内联任何函数。内联好还是不好并不明确,既然这样,但是我们发现没有优化时正确执行是很困难的。所以我们做简单的事,避开它。
5.36 汇编指令和C表达式 操作数
在汇编指令中用汇编语言,你可以指定用C语言中的表达式的操作数。这就意味着你不需要猜测哪个寄存器或存储单元中包含你想要用的数据。
你必须指定一个尽可能像机器描述中的汇编指令模块,为每个操作数加上一个被约束排成一列的操作数。
这是怎样使用68881的 fsinx
指令的例子:
asm ("fsinx %1,%0" : "=f" (result) : "f" (angle));
这里angle是一个用来输入操作数的C表达式,result
是输出操作数。每个都有`"f
"'作为它的操作数约束,说明需要一个浮点寄存器。`=f' 中的`='指明了这个操作数是一个输出;所有输出操作数的被约束使用`='。这种约束在同语言中被用于机器描述(参见20.7节 操作数约束)。
每个操作数在插入语中被一个后面跟着C表达式的约束操作数字符串描述。一个冒号隔开了汇编程序模块和第一个输出操作数,另一个隔开了最后一个输出操作数和第一个输入操作数,即便要的话。逗号在每个群中隔开了不同的操作数。操作数的总数被限制在10或者被限制在操作数的最大数字,在任何机器描述中的任何指令模型,无论哪一个都较大。
如果只有输入操作数而没有输出操作数,你应该在输出操作数在的位置的两头放两个连续的冒号。
输出操作数表达式必须是一个左值;编译器可以检测这点。输入操作数不需要是左值。编译器不能检测操作数的数据类型对指令执行来说是否合理。它不能解析汇编指令模块,它也不知道汇编指令的意思。甚至不能判断它是否是一个有效的汇编输入。扩展汇编的特征是多数情况下用于机器指令,而编译器本身却不知道它的存在。如果输出表达式不可能是直接地址(比如说,它是一个位域),你的约束必须允许一个寄存器。那样的话,GCC将会把寄存器当作汇编程序的输出,接下来存储寄存器内容用作输出。
普通的输出操作数必须是只读的;GCC 会假定这些在指令之前操作数中的左值是死的,并且不需要产生。扩展汇编支持输入-输出或读-写操作数。用字符`+'可以指出这种操作数并且在输出操作数中列出。
当对一个读写操作数(或是操作数中只有几位可以被改变)的约束允许一个而中选一的寄存器,你可以从逻辑上把它的作用分成两个操作数,一个是输出操作数和一个只写输出操作数。他们之间的关系是在指令执行时,被约束表示出他们需要在同一个位置。你可以对两个操作数用同样的C语言表示或不同的表示。这里我们写了一个结合指令(假想的)用后备地址寄存器当作它的只读资源操作数,把foo
作为它的读写目的地。
asm ("combine %2,%0" : "=r" (foo) : "0" (foo), "g" (bar));
对操作数1来说,`"0"'约束是指它必须占据相同的位置相操作数0一样。在约束中一个阿拉伯数字只被允许出现在输入操作数中,而且,它必须提及到一个输出操作数。
在约束中只有一个阿拉伯数字可以保证一个操作数会和其它操作数一样出现在同一个地方。起码的事实,foo是两个操作数的确切值并不足以保证在产生的汇编代码中它们会出现在相同的位置。下面的代码是不可靠的:
asm ("combine %2,%0" : "=r" (foo) : "r" (foo), "g" (bar));
各种优化或重新装载可以使操作数0和1在不同的寄存器中;GCC 知道没有理由不这样做。举例来说,编译器可能会找到一个寄存器中foo值得拷贝并用它作为操作数1,但是产生的输出操作数0却在另外一个寄存器中(后来拷贝到foo自己的地址里)。当然,由于用于操作数1的寄存器在汇编码中甚至没有被提及,就不会产生结果。但GCC却不能指出来。
一些频繁使用的指令指定了硬设备寄存器。为了描述这些,在输入操作数之后写上第三个冒号,后面紧跟着频繁使用的硬设备寄存器的名字(以串的形式给出)。这又一个现实的例子:
asm volatile ("movc3 %0,%1,%2" : /* no outputs */ : "g" (from), "g" (to), "g" (count) : "r0", "r1", "r2", "r3", "r4", "r5"); |
你不可能通过用一个输入操作数或一个输出操作数交迭的方式来描述一个频繁使用的硬设备寄存器。举个例子,如果你在快表中提到一个寄存器,你就不可能用一个操作数描述一个有一个成员的寄存器组。你没有办法去指定一个输入操作数没有同时指定为输出操作数时被修正。注意如果你指定所有输出操作数都出于这个目的(而且因此没有被使用),你就需要去指定可变的汇编代码构造,像下面说得那样,去阻止GCC删除那些没有被用到的汇编代码段。
如果你从汇编代码中找到一个特殊的寄存器,你大概不得不在第三个冒号列出之后这个寄存器来告诉编译器寄存器的值是修正值。在一些汇编程序中,寄存器的名字以`%'开始;要在汇编代码中生成一个‘%’,你必须在输入时这样写:`%%'。
如果你的汇编指令可以改变条件码寄存器,在频繁使用的寄存器列表中加上`cc
'。GCC在一些机器上表现条件码像制定硬设备寄存器一样;`cc
'可以去命名这种寄存器。在其它机器上。条件码被给于不同的操作,而且指定`cc'没有效果。但是它在任何机器上总是有效的。
如果你的汇编指令通过不可预知的方式修正存储器,在频繁使用的寄存器列表中加上`memory
'。这会使GCC通过汇编指令不把存储器的值存入寄存器。如果存储器没有受影响,你也可以加上可变的没有被列在汇编程序输入输出表上的关键字,就像`memory
'的频繁使用没有被计算反而成为汇编程序的副作用一样。
你可以在一个汇编模块中把多个汇编指令放在一起,用系统中通常用的字符将它们隔开。在大多数地方一个联合是新的一行打断原来的行,加上一个空格键移动到指令区域(象这样写 /n/t)。如果汇编程序允许将逗号作为断行字符的话,逗号是可以使用的。注意,一些汇编程序语法将逗号作为注释的开始。输入操作数被保证不被用于任何频繁使用的寄存器和输出操作数的地址,所以你可以随意读写频繁使用的寄存器。以下是一个模块中多重指令的例子;它假定子程序 _foo
从寄存器9和10上接受参数:
asm ("movl %0,r9/n/tmovl %1,r10/n/tcall _foo" : /* no outputs */ : "g" (from), "g" (to) : "r9", "r10"); |
除非一个输出操作数有`&'修正限制,否则GCC会把它当作一个不相干的操作数而分配给它一个相同的寄存器,而这是建立在输出产生之前输入被销毁的假定之上。当汇编代码多于一条指令时,这个假定就可能有错。这种情况下,对每个输出操作数用`&'可能不会和一个输入交迭。参见20.7.4修正限制字符。
如果你想测试一下由汇编指令产生的代码情况,你必须包含一个象下面这样的分支和标志和汇编构造:
asm ("clr %0/n/tfrob %1/n/tbeq 0f/n/tmov #1,%0/n0:" : "g" (result) : "g" (input)); |
这个要假定你的汇编程序支持局部标签,象GNU汇编器和大多数UNIX汇编器做的那样。
谈到标签,不允许从一个汇编程序跳到另一个,编译器优化者不知道这样的跳转,所以当它们决定怎样去优化时不会考虑这些。
用汇编指令通常最方便的方法是把指令压缩在一个象宏定义的函数里。举个例子:
#define sin(x) / ({ double __value, __arg = (x); / asm ("fsinx %1,%0": "=f" (__value): "f" (__arg)); / __value; }) |
这里用可变的__arg
来保证指令在合适的double
型特征值上运行,而且仅仅接受这些能自动转换为double
的参数。
另外一个可以确保指令在正确的数据类型上运行的方法是在汇编程序中用一张表。这和用可变__arg
指出在于它可以转化为更多的类型。举例来说:你想得到的类型是整型,整形参数会接受一个指针而不会出错,当你把这个参数传给一个名字为__arg
的整型变量时,就会出现使用指针的警告,除非你再调用函数中明确转化它。
如果一个汇编程序已经有了输出操作数,为了优化GCC会假定指令对改变输出操作数没有副作用。这并不意味着有副作用的指令不会被用到,但你必须要小心,编译器会略去没有被使用的输出操作数或使他们移出循环,如果他们组成一个表达式时,会用一个代替两个。同时当你的指令在一个看上去不会改变的变量上没有副作用,如果它被在一个寄存器中找到的话,旧的值在不久之后会被再次使用。
通过在汇编程序后写上可变关键字,你可以阻止一条汇编指令被删除,永久被移走,或被结合起来。例:
#define get_and_set_priority(new) / ({ int __old; / asm volatile ("get_and_set_priority %0, %1" / : "=g" (__old) : "g" (new)); / __old; }) |
如果你写的汇编指令没有输出,GCC会知道这条指令有副作用而不会删除它或者把它移到循环外。
可变关键字表示指令有很大的副作用。GCC不会删除一段汇编程序如果它是可达的话。(指令仍会被删除如果GCC不能证明控制流会到达指令。)GCC对一些可变的汇编指令不会重新排序。例:
*(volatile int *)addr = foo; asm volatile ("eieio" : : ); |
假定addr包含一个映射到设备寄存器的内存地址。个人微机的eieio(强迫输入输出执行顺序)指令会告诉CPU要保证在其它输入输出之前要执行设备寄存器中所保存的指令。
注意对编译器来说即使是一个看上去无关紧要的可变汇编指令也能被移动,比如通过jump指令。你不要期盼一个可变汇编指令的顺序会保持非常的连贯。如果你需要连贯的输出,就用单一的汇编程序吧。GCC还会通过一条可变汇编指令来进行一些优化;GCC不会像其它编译器一样在遇到可变汇编指令时忘记每一件事。
没有任何操作数的汇编指令会像一个可变汇编指令来同等对待。
准许访问汇编程序指令条件码是很自然的想法。然而,但我们试图去实现这个时,却没有可靠的办法。问题在于输出操作数可能会被再次装载,这可能会导致额外增加的储存指令。在大多数机器上,这些指令会在测试时间改变条件码。这些问题不会出现在普通的测试和比较指令上。因为他们没有任何输出操作数。
由于和上面提到的相似的原因,让一个汇编指令有权访问先前指令留下的条件码是不可能的。如果你要写一个被包含在标准C程序中的头文件时,用__asm__
代替asm
。参见5.39 备用关键字
5.36.1 i386浮点数汇编操作数
-
在汇编操作数使用规则中有很多是站寄存器的用法。这些规则只对栈寄存器中的操作数起作用:
在汇编程序中给一组死的操作数,就需要知道那些在汇编时被暗自取出,而在GCC中必须是被明确弹出的。一个在汇编时会被暗自弹出的输入寄存器必须被明确被标志为频繁使用的,除非你强制它去匹配一个输出操作数。
-
对任何在汇编时会被暗自弹出的输入寄存器,就需要知道怎样去调解一个堆栈进行弹出补偿。如果栈里任何没有弹出的输入比任何弹出寄存器更靠近栈顶,就很难知道栈是什么样子,剩下的栈的去向也就很难知道了。任何会被弹出的操作数都要比不被弹出的操作数更靠近栈顶。如果一个输入死在
an insn
,对输出重新装载可能会用到输入寄存器。看看下面这个例子:asm ("foo" : "=t" (a) : "f" (b));
这行汇编程序的意思是输入的B不会在汇编时被弹出,汇编器会把结果压入栈寄存器,栈会比压入之前更深一层。但是,如果B死在这insn里,重新装载对输入和输出用同一个寄存器是有可能的。
如果任何输入操作数用到了
f
约束, 所有的输出寄存器限制必须用到& earlyclobber
。上面的汇编程序可以写成这样:asm ("foo" : "=&t" (a) : "f" (b));
-
一些操作数需要放在栈的特殊位置。
由于所有的387操作码用到了读写操作数,在汇编操作数之前的输出操作数都是死的,而且会被汇编操作数压栈。除了栈顶之外压在任何地方是没有意义的。
-
输出操作数必须从栈顶开始:而且不可能是一个寄存器跳跃。
一些汇编程序段可能需要额外的栈空间用于内部计算。这可以用式输入输出与栈寄存器不相干的方法来保证。这里有几个要去写的汇编代码。这段汇编接收一个可以在内部弹出的输入,并产生两个输出。
asm ("fsincos" : "=t" (cos), "=u" (sin) : "0" (inp));
asm ("fyl2xp1" : "=t" (result) : "0" (x), "u" (y) : "st(1)");
这段汇编接收两个通过操作码
fyl2xp1
弹出的输入,并产生一个输出代替它们。用户必须给出用于栈寄存器频繁使用的st(1)
的C代码来了解fyl2xp1
弹出两个输入。
5.37汇编代码中使用的控制名字
在进行声明之后你可以为C函数或函数变元的汇编代码中写入asm (or __asm__)来指定你要在汇编代码中使用的名字,象下面这样:
int foo asm ("myfoo") = 2;
这种用于在汇编代码中用于变量foo
指定应该是myfoo
' 而不是通常的 `_foo
'.
系统中的C函数或变量通常是以下划线开始的,这种特征就不允许你以下划线开始为一个连接命名。
非静态局部变量对这种特征是没有意义的,因为这种变量不需要由汇编程序名。如果你试着把一个变量放在一个指定的寄存器中,看看5.38节 指定寄存器中的变量。GCC目前把这种代码当作一种警告, 但是在不久后它就可能变成一个错误,而不仅仅是警告了。
你不能在函数定义时这样用汇编;但是你可以像这样,在定义前为函数写一个声明并且把汇编代码放在这里:
extern func () asm ("FUNC");
func (x, y)
int x, y;
...
|
确保汇编程序的名字不会与其它任何汇编标记发生冲突。而且,不能使用寄存器名;那会产生完全无效的汇编代码。GCC目前还没有能力在寄存器中存储这些静态变量,以后或许会被架上。
5.38 指定寄存器中的变量
GNU C允许你把商量的全局变量放在指定的硬设备寄存器中。你还可以在为普通变量分配的寄存器中指定这样的寄存器。全局寄存器变量通过程序保留寄存器。这在编程中非常有用,比如有多个会被频繁访问全局变量的编程语言解释程序。指定寄存器中的局部寄存器变量不需要保有寄存器。编译器的数据流分析有能力决定那里指定的寄存器包含活的值,这些变量在哪里会被其它代码用到。在数据流分析中表现为死的寄存器变量可能会被删除。提到寄存器变量可能会被删除、移动或是简化。
通过扩展汇编的特征局部变量有时使用起来很方便(参见5.36 汇编指令和C表达式 操作数),如果你要用汇编指令直接在指定寄存器中写入输出值的话。(倘若你指定的寄存器适合汇编里操作数的指定限制的话)。
5.38.1 定义全局寄存器变量
GCC中你可以这样定义全局寄存器变量:
register int *foo asm ("a5");
这里的a5是要使用的寄存器名。在你的机器上选一个通常被保留或是被函数调用释放的寄存器,以使库函数不能频繁使用它。
通常来说寄存器名是由CPU决定的,所以你可能需要根据CPU类型调整你的程序。在68000上寄存器a5是一个好的选择。在有寄存器窗口的机器上,一定要选择一个不会受函数调用机制影响的全局变量寄存器。
另外,运行在同种CPU上的操作系统可能会在寄存器命名上有区别;这样你就需要更多的条件。举例来说,一些68000操作系统把这个寄存器叫做%a5。
最终有了一个可以让编译器自动选择寄存器的方法,但首先我们要决定编译器怎样去选并让你去引导这个选择。显然没有这种办法。
在特定寄存器上定义一个全局寄存器变量,要保证寄存器完全用作这个用途,至少在当前时这样实现的。这些寄存器不会被函数存储起来。存储这些寄存器永远不会被删除即使它表现为死,但是提到寄存器变量可能会被删除、移动或是简化。从信号或多个控制线程操作这些全局变量是不安全的,因为系统库函数可能会为其它事用到这些寄存器(除非你为了手头的工作特别重新编译它们)。
一个函数通过第三个不知道这个全局变量的函数(也就是说,在不同的源文件中这个变量没有被声明),用这个全局变量去调用另一个foo
这样的函数是不安全的。这是由于损失可能保有的这个寄存器并把其它变量放在那里。举例来说,你不能指望一个你要传递给qsort
的全局变量在比较函数中被用到,因为qsort
可能会把其它的什么东西放在那个寄存器中。(如果你用相同的全局变量重新编译qsort
,问题就会得到解决)
如果你要重新编译qsort
或是世上没有用到你的全局寄存器变量的源文件,已达到它们不使用那个寄存器的目的,接下来它就有能力指定编译器选项`-ffixed-reg
'。事实上你不需要为那些源文件增加一个全局变量寄存器声明。
编译时没有这个全局寄存器变量的函数去调用一个可以改变全局寄存器变量的函数是安全的,因为它可以经常使调用者期待在那里找到用来返回的值。因此,用到全局变量的程序的入口函数必须被明确保存,并且要恢复属于调用者的值。
在大多数机器上,在设置跳跃的时候,远跳跃会恢复每一个全局寄存器变量的值。而在另一些机器上,远跳跃不会改变这些值。考虑到移植性,调用设置跳跃指令的函数必须用其它参数储存这些全局寄存器变量的值,并在远跳跃时恢复他们。这样,无论远跳跃做什么都有同样的效果。
所有的全局寄存器变量声明必须在函数定义之前。否则,那些寄存器可能在先前的函数中被使用,而声名阻止这样却晚了。
全局寄存器变量不应该有初始值,因为一个可执行文件并不意味着会为寄存器提供初始值。
在sun工作站上,g3 ... g7被认为是合适的寄存器,但是某几个库函数,像getwd
,用于分割和求余的子程序,修正了g3 和 g4,g1 和 g2 是局部临时存储器。
在68000上,a2 ... a5 d2 ... d7应该是合适的,当然,用很多这样的寄存器就不合适了。
5.38.2 用于局部变量的指定寄存器
你可以用一个制定的寄存器定义一个局部寄存器变量:
register int *foo asm ("a5");
这里的a5是要使用的寄存器名。注意这和定义全局寄存器变量的语法是相同的,只是局部寄存器变量要出现在一个函数中。
通常来说寄存器名是由CPU决定的,但这并不是问题,因为指定寄存器大多用在外在的汇编指令上(参见5.36 汇编指令和C表达式 操作数)。所有这些通常都需要你根据CPU的类型给你的程序加上条件。
另外,运行在同种CPU上的操作系统可能会在寄存器命名上有区别;这样你就需要更多的条件。举例来说,一些68000操作系统把这个寄存器叫做%a5。
定义这样一个局部寄存器变量,不需要保有这个寄存器;它留下的其它代码可用的部分放在控制流可以决定变量的值不是活着的地方。然而,这些寄存器在重新装载途径是不可用的;过多地使用这些特征会使编译器在编译特定程序时没有可用的寄存器。
不是任何时候这些选项都能保证GCC能够产生寄存器中包含这种变量的代码。在一个汇编声明中你不必要为这个寄存器写一段外在的说明并假定它总是会提及这个变量。当变量的值在数据流分析中表现为死的时候,存储在变量中的值可能会被删除。提到寄存器变量可能会被删除、移动或是简化。
5.39 备用关键字
用选项`-traditional
'可以使一些关键字失去作用;`-ansi
' 和各种 `-std
'选项是另外一些失效。在一个被所有程序(指包含标C和传统C)时都要使用的多种功能的头文件中,当你要用GNU扩展C或标准C时,就会产生麻烦。在编译程序是加上选项`-ansi
'时,关键字asm, typeof
和 inline
会失效(尽管inline
可以和`-std=c99
'用在程序编译中),同时在编译程序是加上选项`-traditional
'时,关键字const, volatile, signed, typeof
和 inline
会失效。标准C99里限制的关键字只有当`-std=gnu99
'(被最终默认)或者是`-std=c99
'(等价于 `-std=iso9899:1999
') 被使用时才会有效。
解决这个问题的方法是在每个有争议的关键字的首部和尾部加上`__'。例如:用 __asm__
代替 asm, __const__
代替 const
, __inline__
代替 inline
.
其它编译器不会接受这种有选择性的关键字;
如果你要有其它的编译器编译,你可以通过宏用习惯的关键字去替代备用的关键字。像这样:
#ifndef __GNUC__ #define __asm__ asm #endif |
`-pedantic
'和其它的一些选项对许多GNU C扩展会产生警告。你可以通过在一个表达式前写上__extension__
来阻止这种警告。这样用__extension__
不会有副作用。
5.40 不完整的枚举类型
你可以不用指定它的可能值定义一个枚举标记。这样会产生一个不完整的类型,很像你写了一个结构体foo而没有描述它的元素所得到的东西。一个稍后的声明可以指定可能的值来完善这个类型。
你不能给不完善的类型分配存储空间,或使它成为变量。然而,你可以用指针指向它。
这个扩展或许不是非常有用,但它是枚举类型的操作和对结构体和共用体的操作更加一致。 GNU C++不支持这个扩展。
5.41 用字符串命名函数
GCC预先假定两个魔术标识符用来保存当前函数名。当函数名出现在源文件中时,函数名会被保存在标识符__FUNCTION__
中。标识符__PRETTY_FUNCTION__
保存着在一个语言的特殊模式中打印出来很漂亮的函数名字。
extern "C" { extern int printf (char *, ...); } class a { public: sub (int i) { printf ("__FUNCTION__ = %s/n", __FUNCTION__); printf ("__PRETTY_FUNCTION__ = %s/n", __PRETTY_FUNCTION__); } }; int main (void) { a ax; ax.sub (0); return 0; } |
输出如下:
__FUNCTION__ = sub __PRETTY_FUNCTION__ = int a::sub (int) |
编译器会自动用文字传中含有合适名字的字符串替代标志服。这样,他们就不会预处理宏和变量,如__FILE__
和 __LINE__
。这就意味着它们和其它的字符串是由连接关系的,而且它们可以用来初始化字符数组。比如:
char here[] = "Function " __FUNCTION__ " in " __FILE__; |
另一方面,`#ifdef __FUNCTION__
' 在函数中没有任何特殊的意义,因为预编译程序不会对标识符__FUNCTION__
做任何专门的事。
GCC仍然支持C99标准里定义的魔术字__func__
。
标志符被编译者含蓄的声明了就好像发表声明,立即执行每个函数定义的下一个操作句柄,函数名是装入词汇表函数。这个名字是原始的函数名。
static const char __func__[] = "function-name";
这样定义时,__func__
是一个变量而不是一个字符串。特别指出,__func__
不能连接其它字符串。
C++中,和声明__func__
一样,__func__
和__PRETTY_func__
都是变量。
5.42 获取函数返回值或结构地址
这些函数可以被用来获取函数中调用者的信息。
内置函数: void * __builtin_return_address (unsigned int level)
这个函数返回了当前函数的返回地址,或是它的主调函数地址。这里的等级参数是一个用于扫描调用栈的结构数字。如果这个值是0的话,返回的是当前函数返回地址;如果是1的话,返回的是当前函数的主调函数的返回地址,等等。等级参数必须是一个整型常量。在一些机器上,不可能决定除了当前函数外其它函数的返回地址。在这种情况下,或者是没有到达栈顶,这个函数会返回0。
这个函数只能和非零参数一起用作调试。
内置函数: void * __builtin_frame_address (unsigned int level)
这个函数和__builtin_return_address很相似,但他返回的是函数结构而不是函数地址。用0值调用这个函数时返回当前函数的结构地址,用1值调用这个函数时返回当前函数的主调函数的结构地址,等等。
这里的结构是堆栈中保存局部变量和寄存器的一块区域。结构地址通常是第一个被这个函数压入堆栈的第一个字的地址。然而,精确的定义取决于处理器和调用协定。如果处理器有一个专门的结构指针寄存器,并且函数有一个这样的结构,函数会返回这个结构指针寄存器的值。用于函数__builtin_return_address
中的警告同样适用于这个函数。
5.43 GCC提供的其它内置函数
GCC提供了大量的内置函数。其中一些只在处理例外或变量长度参数表时内部使用。之所以没有列出,是由于它们可能随时间变化;一般情况下,我们不推荐使用这些函数。
保留的函数是为优化而提供的。
GCC包含了许多标准C库函数的内置版本。加了前缀的版本会被当作C库函数即使你加了`-fno-builtin
'选项。(参见3.4 C语法控制选项)。许多这样的函数只有在特定情况下才会被优化;如果在某种特例下没有被优化,这个函数就会去调用库函数。
这些函数会被识别并假定没有返回abort, exit, _Exit
和 _exit
,但里一方面却不是内置的。在严格的国际标准C模式下(使用了`-ansi
', `-std=c89
' 或 `-std=c99
')没有_exit
。在严格的C89模式下(使用了`-ansi
' 或 `-std=c89
')没有_Exit
。
在严格的ISO C模式外,函数alloca, bcmp, bzero, index, rindex
和 ffs
被当作内置函数处理。相应的版本__builtin_alloca, __builtin_bcmp, __builtin_bzero, __builtin_index, __builtin_rindex
和 __builtin_ffs
会在严格的国际标准C模式下被识别。
ISO C99函数conj, conjf, conjl, creal, crealf, creall, cimag, cimagf, cimagl, llabs
和 imaxabs
除了在严格的C89模式下都被当作内置函数。在国际标准C99模式下的函数cosf, cosl, fabsf, fabsl, sinf, sinl, sqrtf
, 和 sqrtl
的内置版本在任何模式都可被识别,这是由于为了在标准C99下提出使用它们,C89保留了这些函数名字。所有这些函数的相应版本前都加了__builtin_
前缀。
除非指定选项`-fno-builtin
',否则这些C89中的函数会被当作内置函数加以识别:abs, cos, fabs, fprintf, fputs, labs, memcmp, memcpy, memset, printf, sin, sqrt, strcat, strchr, strcmp, strcpy, strcspn, strlen, strncat, strncmp, strncpy, strpbrk, strrchr, strspn,
和 strstr
。所有这些函数的相应版本前都加了__builtin_
前缀。
GCC提供了国际C99标准的浮点数比较宏的版本(可以避免提高无序操作数的例外):__builtin_isgreater, __builtin_isgreaterequal, __builtin_isless, __builtin_islessequal, __builtin_islessgreater,
和 __builtin_isunordered
。
内置函数:int __builtin_constant_p (exp)
你可以用内置函数__builtin_constant_p
来决定在编译时是否一个值是常数,以此GCC可以执行含有那个常数的常量叠加表达式。这个函数的参数是要测试的值。如果那个值在编译时是常量,函数返回整数1,否则返回0。返回0并不表示那个值不是常数,不过通过给定`-O
'选项,GCC不能证明它是一个常量。
这个函数的一个有代表性的用途是内存成为临界资源时的深入应用。假设你有许多复杂的运算,如果这里面包含常数的话,你可能需要它被打包。没有的话,就需要去调用一个函数。例如:
#define Scale_Value(X) / (__builtin_constant_p (X) / ? ((X) * SCALE + OFFSET) : Scale (X)) |
你可以在一个宏或是内联函数中使用这个内置函数。然而,如果你在一个内联函数中用到它并把这个函数的参数传给内置函数,当你通过一个字符常量或复合字(参见5.21 复合文字)调用这个内联函数时,GCC不会返回1;除非你指定选项`-O' ,否则当你给内联函数传递一个数字常量时,GCC不会返回1。
你还可以用__builtin_constant_p
初始化静态数据。例如:
static const int table[] = { __builtin_constant_p (EXPRESSION) ? (EXPRESSION) : -1, /* ... */ }; |
即使EXPRESSION不是一个常量表达式,这样的初始化也是可以接受的。在这种情况下,GCC应该在评估内置函数时更加保守,因为这没有机会去优化。
先前的GCC版本不接受这样的数据初始化内置函数。最早的完全可以支持的版本是3.0.1。
内置函数:long __builtin_expect (long exp, long c)
使用__builtin_expect
是为编译器提供分支预测信息。总的来说,你应该更喜欢真实地反馈(通过`-fprofile-arcs
'),因为程序员在预言他们的程序执行时声名不怎么好。然而,在有些应用程序中收集这样的信息是很难的。
返回值是exp
的值,exp
应该是一个整型表达式。c
的值在编译时必须是一个常数。内置函数的语法期望exp == c
.例如:
if (__builtin_expect (x, 0)) foo (); |
必须指出,我们并不指望调用foo
,因为我们希望x的值是0。限于exp
必须是整形表达式,当测试指针或浮点时应使用这样的结构:
if (__builtin_expect (ptr != NULL, 1)) error (); |