牛骨文教育服务平台(让学习变的简单)
博文笔记

C99结构体指定初始化

创建时间:2008-07-11 投稿人: 浏览次数:4291
在阅读GNU/Linux内核代码时,我 们会遇到一种特殊的结构初始化方式。该方式是某些C教材(如谭二版、K&R二版)中没有介绍过的。这种方式称为指定初始化(designated initializer)。下面我们看一个例子,Linux-2.6.x/drivers/usb/storage/usb.c中有这样一个结构体初始化 项目: static struct usb_driver usb_storage_driver = {        .owner = THIS_MODULE,        .name = "usb-storage",        .probe = storage_probe,        .disconnect = storage_disconnect,        .id_table = storage_usb_ids, };     乍一看,这与我们之前学过的结构体初始化差距甚远。其实这就是前面所说的指定初始化在Linux设备驱动程序中的一个应用,它源自ISO C99标准。以下我摘录了C Primer Plus第五版中相关章节的内容,从而就可以很好的理解2.6版内核采用这种方式的优势就在于由此初始化不必严格按照定义时的顺序。这带来了极大的灵活 性,其更大的益处还有待大家在开发中结合自身的应用慢慢体会。     已知一个结构,定义如下 struct book {     char title[MAXTITL];     char author[MAXAUTL];     float value; };     C99支持结构的指定初始化项目,其语法与数组的指定初始化项目近似。只是,结构的指定初始化项目使用点运算符和成员名(而不是方括号和索引值)来标识具体的元素。例如,只初始化book结构的成员value,可以这样做:     struct book surprise = { .value = 10.99 };     可以按照任意的顺序使用指定初始化项目:     struct book gift = { .value = 25.99,                                     .author = "James Broadfool",                                     .title = "Rue for the Toad"};     正像数组一样,跟在一个指定初始化项目之后的常规初始化项目为跟在指定成员后的成员提供了初始值。另外,对特定成员的最后一次赋值是它实际获得的值。例如,考虑下列声明:     struct book gift = { .value = 18.90,                                     .author = "Philionna pestle",                                     0.25};     这将把值0.25赋给成员value,因为它在结构声明中紧跟在author成员之后。新的值0.25代替了早先的赋值18.90。     有关designated initializer的进一步信息可以参考c99标准的6.7.8节Ininialization。

特定的初始化

标准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来告诉编译器这个事实。比如,

 

            

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

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

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

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

 

            

pure

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

 

            

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

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

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

const

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

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

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

 

            

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

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

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

 

            

会促使编译器检查调用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风格的函数(格式化函数的其余参数和它们在不修改字符串的函数中一样)。例如,声明:

 

            

促使编译器检查调用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属性指定一个函数放入一个特别的部分。比如,声明:

 

            

把函数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属性促使这个声明被作为另一个必须被指定的符号的别名导出。例如,

 

            

声明“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, ABORTUNDEF

interrupt_handler

在H8/300,H8/300H和SH上使用这个选项表明指定的函数是一个中断处理程序。当这个属性存在时,编译器将会生成函数入口和出口工序,为适应在中断处理程序中的使用。

sp_switch

在SH上使用这个选项表明一个interrupt_handler函数应该切换到一个可替代的堆栈上。它期待一个字符串参数,用来命名一个存放替代堆栈地址的全局变量。

 

            

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__时,有两条原因不适合这么做。

  1. 不可能从宏中生成#pragma命令。

  2. 没有有效说明同样的#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)。

在跟在后边的圆括号中有属性的参数的字。这些参数有如下的格式:

标识符。比如,模式属性使用这个格式。 跟有逗号或非空的由逗号分隔的表达式表。比如,格式化属性使用这个格式。 可为空的由逗号分隔的表达式表。比如,格式化参数使用这个格式和一个单独整型常量表达式列表,并且别名属性使用这个格式和一个单独的字符串常量列表。

属性说明符列表是一个由一个或多个属性说明符组成的序列,不被任何其它标志分开。

属性说明符列表可能跟在一个标签后的冒号出现,除了casedefault中的标签。唯一的属性使用在一个未使用的标签后是合理的。这个特征被设计成代码被包含而未使用的标签但是在编译是使用了"-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节属性语法,了解正确使用属性语法的细节。

aligned (对齐) 这个属性以字节为单位说明一个变量或结构体域的最小对齐。例如,这个声明: int x __attribute__ ((aligned (16))) = 0; 造成编译器在一个16字节的边界上分配全局变量x。在68040上,这可以用在和一个汇编表达式连接去访问需要16字节对齐对象的move16指令。 你也可以说明结构体域的对齐。例如,创建一个双字对齐的整型对,你可以写: struct foo { int x[2] __attribute__ ((aligned (8))); }; 这是创建一个有两个成员的共用体强制共用体双字对齐的一个替换用法。 它可能不能说明函数的;函数的对齐是被机器的需求所决定且不能被改变的。你不能说明一个typedef定义的名称的对齐因为这个名字仅仅是个别名,而不是特定的类型。 在前边的例子,你可以明确说明你希望编译器以给定的变量或结构体域对齐(以字节位单位)。另外一种方案,你可以省略对齐因子并且进让编译器以你为之编译的目标机器的最大有用对齐来对齐变量或域。例如,你可以写: short array[3] __attribute__ ((aligned)); 无论何时你在一个要对齐的属性说明中省略对齐因子,编译器都会自动为声明的变量或域设置对齐到你为之编译的目标机器上对 曾经对任意数据类型用过的最大对齐。这样做可以经常可以是复制动作更有效,因为编译器可以使用任何指令复制最大的内存块当执行复制到你要求这样对齐的变量 或域中或从这从中复制时。 对齐属性可以仅仅增加对齐;但是你可以通过也说明packed属性。见下边。 注意这对齐属性的可行性可能被你的连接器的固有限制所限制。在许多系统上,连接器仅仅可以把变量整理和对齐到一个特定的最大对齐。(对一些连接器,所支持的最大对齐可能非常非常小。)如果你的连接器几近可以对齐变量最大8字节,那么在__attribute__中说明aligned(16)仍然值提供给你8字节对齐。从你的连接器文档中可以获得更多的信息。 mode (模式) 这个属性位声明说明数据类型──任何类型都符合mode模式。这有效地使你获取一个整型或浮点型的符合宽度。 你可能也说明"byte"或"__byte__"模式来表示模式同一字节的整数一致,"word"或"__word"来表示一个字的整数的模式,并且"pointer"或"__pointer__"来表示指针的模式。

nocommon

这个属性说明要求GCC不要把放置一个变量为 "common"而是直接给它分配空间。如果你说明"-fno-common"标志,GCC将对所有变量这样做。 给变量说明nocommon属性则提供初值为零。变量可能仅在一个源文件中初始化。 packed packed属性说明一个变量或结构体域应该有尽可能小的对齐──一个变量一字节或一个域一字节,除非你和对齐属性一起说明了一个更大的值。 这里是一个域xpacked属性说明的结构体,所以它直接跟在a之后:
 
struct foo
{
  char a;
  int x[2] __attribute__ ((packed));
};
section("段名") 通常,编译器将把对象放置在生成它的段中想databss。但是有时候你学要附加段,或者你需要一定特定的变量去出现在特殊的段中,例如去映射特殊的硬件。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属性不全适用于所有的平台。如果你需要映射一个完全满足的模块到一个特定的段,慎重考虑使用连接器的设备来代替。 在Windows NT上, 在命名的段附加放置变量定义,这段也可以个在所有运行的可执行文件或DLL文件之间共享。例如,这个小程序通过将其放入命名的段并将该段标记位共享而定义了共享数据。
 

            
你仅可以在section属性完全初始化全局定义是使用shared属性,因为连接器的工作方式。看section属性来获得更多的信息。 shared属性仅适用于Windows NT。 这个属性附属与一个共用体型的函数参数,意味着参数可能具有与共用体成员一致的任何类型,但是参数被当做共用的第一个成 员的类型传送。看5.34节说明类型属性了解更多细节。你也能吧这个属性用在对共用体数据类型适用typedef是;这样它就可以被用在所有这个类型的函 数参数上。 unused 变量有这个属性,意味着这个变量可能没有被使用。GCC将不对这个变量产生警告。 weak weak属性在5.26节声明函数属性已经被陈述过。 model(模型名) 在M32R/D上使用这个属性去设置对象的编址能力。这个标识符模型名是small,medium或large中的一个,代表每一个代码模型。 小模型对象存在与低16MB内存中(所以它们的地址可以和一个ld24指令一起装入)。 中和大模型对象可能存在任何一个32位的地址空间中(编译器将形成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 这种属性指定一个最小的队列(按位算)为变量指定类型。例如,如下的声明:

 

            
强制使编译器确定每个类型为S的结构体变量或者更多的组合整型,将被分配和匹配为至少8位。在可精简效能结构中,当复制一个结构体S变量到另外一个时。拥有所有的结构体S 对齐8位的变量使编译器能使用lddstd,因此可以提高运行效率。 {注意,任何给定的结构体或共同体类型的对齐是ISO C标准所需的,至少是正在谈论的结构体或共同体类型所有成员对齐的最小公倍数的一个完全的倍数。这就意为着你能有效的教正附属于aligen对于结构体和共用体队列成员的属性。但是以上例子中插入的注释更加明显,直观和可读对于编译者来校正一个完全的结构体或共用体类型组合。 封装(packed) 这种属性接近于枚举,结构或者共用类型的定义,指定一个所需最小的内存用来代办这种类型。 为结构体和共用体指定这种属性等效于为他们的每个成员指定集装的的属性。指定“短-枚举”标志等同于指定封装的属性在所有的枚举定义。 你也可以仅仅在括号后面指定这种属性,而不是为他定义类型的声明,除非声明也包括枚举的定义。 transparent_union 这种属性基于共用体的定义,表明一些函数的参数使用共用类型会被看作调用函数的一种特殊途径。 首先,参数与同透明共用体类型保持一致,能成为任何类型的共用体;不需要转换. 加入共用体包含指针类型,一致的参数能成为一个空指针常量或空指针表达式;加入共用体包含空指针,一致参数能成为指针表达式。加入共用体成员类型是之至, 类型修饰符就像指定,就如一般的指针的转换一样。 第二,参数被传给函数用到的调用转换(透明共用体的第一个成员,不能调用转换共用体本身。所有的成员必须拥有相同的机器代理;这就使参数传输能进行了。 透明共用体拥有多种接口库函数来处理兼容性问题。例如,假定等待函数必须要接受一种整型来遵从Posix,或者一个共用wait函数要适应4.1BSD接口。记入wait函数的参数是空型,wait函数将接受各种型的参数,但是它也能接受另外的指针型和这个将能使参数类型的检测降低效用。而为"sys/wait.h"定义的接口如下:

 

            
这种接口允许整形或共用体等待参数的传输,用整型的调用转换,这个程序能调用参数和类型:

 
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) 当应用一种类型(包括共用体和结构体),这个属性意为着这种变量类型可能出席那不能使用的,GCC讲不能产生警告对这些类型的变量,几十变量什么作用都没,这还经常能导致锁或线程的种类(通常被定义但不引用,但是包含构建与消毁都存在非平凡簿记功能.)

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删除那些没有被用到的汇编代码段。

如果你从汇编代码中找到一个特殊的寄存器,你大概不得不在第三个冒号列出之后这个寄存器来告诉编译器寄存器的

声明:该文观点仅代表作者本人,牛骨文系教育信息发布平台,牛骨文仅提供信息存储空间服务。