`
javayestome
  • 浏览: 1042089 次
  • 性别: Icon_minigender_2
  • 来自: 北京
文章分类
社区版块
存档分类
最新评论

(转)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")));

声明“f”是“__f”的一个弱别名。在C++中,目标的重整名字

分享到:
评论

相关推荐

    C语言家族扩展

    GNC C 对ISO的所有扩展,主要在LINUX环境下使用,在linux内核中用的非常多

    C语言实现家族关系图好用

    在《C语言实现_家族关系查询系统.docx》文档中,可能会详细介绍如何定义数据结构、实现关系查询的函数,以及可能的优化和扩展。这包括如何存储和更新关系图,如何处理复杂的关系(如收养、再婚等),以及如何提高...

    C语言实现hash算法

    本项目是用C语言实现的哈希算法,包括SHA256、SHA384和SHA512三种不同的哈希函数,这些函数是SHA-2(Secure Hash Algorithm 2)家族的一部分。 SHA-2是由美国国家安全局(NSA)设计的,包含了多种不同长度的哈希...

    C语言库函数源码大全

    这些源代码可以帮助我们深入理解C语言底层的工作机制,了解函数内部实现细节,提高编程技能,并有助于进行性能优化和自定义扩展。 C语言库函数是C编程的基础,它们提供了丰富的功能,涵盖了输入输出、字符串处理、...

    Sha256算法c语言实现

    3. **消息扩展**:原始输入消息会被填充到512位的块大小,并通过一系列扩展步骤转化为64个32位的中间值。 4. **循环计算**:算法的核心是一个迭代过程,对每个消息块执行64次迭代。每次迭代涉及到四个主要步骤:...

    sha 算法c语言实现

    在标题“SHA算法C语言实现”中,我们关注的是如何使用C语言编写代码来实现SHA算法。C语言是一种底层、通用且高效的编程语言,适合编写这样的底层算法实现。 描述中提到“编译成功,并在nrf52832上验证”,nrf52832...

    C语言函数头文件大全

    函数头文件通常以`.h`为扩展名,用于在源代码中引入所需的函数、常量、数据类型等,确保编译器知道如何处理这些元素。本资源“C语言函数头文件大全”提供了一个全面的参考,包含了C语言常用的标准库函数和一些常见的...

    AVR系列单片机C语言编程与应用实例.pdf

    章节内容包括单片机简介、AT90LS8535单片机的基础知识、AVR单片机的C编程基础、C语言的数据结构、控制流、函数结构、指针类型、组合数据类型、8535的内部资源编程、人机接口技术、扩展编程以及通讯接口等。...

    C语言规范之C11标准

    学习C11标准对于C语言程序员来说至关重要,因为它不仅扩展了语言的能力,还提升了代码的安全性和可维护性。通过理解和应用这些新特性,开发者能够编写出更高效、更可靠和更适应现代编程环境的C程序。在实践中,掌握...

    c语言在虚拟机上的字库rar

    常见的编码有ASCII、ISO 8859-1、GB2312、GBK以及Unicode家族的UTF-8等。了解并正确处理字符编码是构建字库的基础。 2. 字库结构:字库是包含各种字符图形的数据集合,常以二进制形式存储。C语言可以通过数组、...

    c语言 日记管理系统

    为了便于管理和检索,我们还可以扩展其他字段,如天气、心情、标签等。这些信息可以被组织成一个结构体,通过结构体数组来存储多篇日记,这样既保证了数据的一致性,也方便了对日记的访问和操作。 其次,用户交互是...

    C语言设计模式.pdf

    设计模式本质上是一种解决软件设计问题的通用解决方案,它有助于提高软件的可重用性、可维护性和可扩展性。在C语言的设计实践中,虽然不像C++或者Java那样原生支持面向对象的特性,比如继承、封装和多态,但依然可以...

    51单片机C语言编程入门(详讲版).pdf

    MCS-51系列单片机是由Intel公司推出的8位微控制器家族。该系列单片机以其低成本、高性能和广泛的兼容性而闻名,在工业控制、家用电器等领域得到了广泛应用。MCS-51单片机具有以下特点: - **8位CPU**:处理速度适中...

    8051单片机C语言创新教程免费版 20181128

    8051家族还包括许多衍生型号,如8052、89C51等,它们在原基础上增加了更多功能。 二、C语言编程基础 1. 数据类型:C语言提供了各种基本数据类型,如char、int、float和double,用于存储不同类型的数值。在8051...

    vc++6.0c语言安装包

    1. **C++编程语言**:C++是C语言的扩展,它增加了类和对象的概念,引入了面向对象编程(OOP)的特性,如封装、继承和多态性,使得代码更易于管理和复用。 2. **Visual Studio家族**:VC++ 6.0是Visual Studio系列的...

    类C语言编程相关.zip

    在计算机科学领域,"类C语言编程相关"指的是使用与C语言家族相近的语言进行程序设计。这个家族包括了C语言本身、C++、C#以及Objective-C等,它们都继承了C语言的基本语法和特性,同时在不同方面进行了扩展和优化。...

    c语言标准库源码资料全集

    总之,这份"c语言标准库源码资料全集"是一个深入学习C语言的宝贵教材,对于提高编程能力、扩展知识视野、培养系统级思维都有极大的帮助。无论是初学者还是经验丰富的开发者,都能从中受益匪浅。

    C语言库函数(含有各个库函数用法实例)

    10. 非标准库函数:虽然C标准库中包含了很多函数,但还有一些常用的非标准库函数,如POSIX扩展中的`system`执行shell命令,`fork`和`exec`家族创建子进程。 通过学习这些库函数的用法实例,你可以更好地理解和应用...

    单片机应用技术项目教程(C语言版)

    8051单片机是单片机家族中的经典型号,由Intel公司推出,后来被许多厂商如Atmel、Philips等进行生产和改进。8051内含CPU、ROM、RAM、定时器/计数器、串行通信接口等多种功能模块,适用于各种嵌入式控制系统。在本...

Global site tag (gtag.js) - Google Analytics