C 扩展

在阅读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
一些标准库函数,就像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”(参考“t/newbie/gcc.htm#3.18">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节属性语法,了解正确使用属性语法的细节。
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属性说明一个变量或结构体域应该有尽可能小的对齐──一个变量一字节或一个域一字节,除非你和对齐属性一起说明了一个更大的值。
这里是一个域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属性不全适用于所有的平台。如果你需要映射一个完全满足的模块到一个特定的段,慎重考虑使用连接器的设备来代替。
在Windows NT上, 在命名的段附加放置变量定义,这段也可以个在所有运行的可执行文件或DLL文件之间共享。例如,这个小程序通过将其放入命名的段并将该段标记位共享而定义 了共享数据。   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。
这 个属性附属与一个共用体型的函数参数,意味着参数可能具有与共用体成员一致的任何类型,但是参数被当做共用的第一个成 员的类型传送。看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
这种属性指定一个最小的队列(按位算)为变量指定类型。例如,如下的声明:
  struct S { short f[3]; } __attribute__ ((aligned (8))); typedef int more_aligned_int __attribute__ ((aligned (8))); 

强制使编译器确定每个类型为S的结构体变量或者更多的组合整型,将被分配和匹配为至少8位。在可精简效能结构中,当复制一个结构体S变量到另外一个时。拥有所有的结构体S 对齐8位的变量使编译器能使用ldd和std,因此可以提高运行效率。
{注 意,任何给定的结构体或共同体类型的对齐是ISO C标准所需的,至少是正在谈论的结构体或共同体类型所有成员对齐的最小公倍数的一个完全的倍数。这就意为着你能有效的教正附属于aligen对于结构体和 共用体队列成员的属性。但是以上例子中插入的注释更加明显,直观和可读对于编译者来校正一个完全的结构体或共用体类型组合。
封装(packed)
这种属性接近于枚举,结构或者共用类型的定义,指定一个所需最小的内存用来代办这种类型。
为结构体和共用体指定这种属性等效于为他们的每个成员指定集装的的属性。指定“短-枚举”标志等同于指定封装的属性在所有的枚举定义。
你也可以仅仅在括号后面指定这种属性,而不是为他定义类型的声明,除非声明也包括枚举的定义。
transparent_union
这 种属性基于共用体的定义,表明一些函数的参数使用共用类型会被看作调用函数的一种特殊途径.首先,参数与同透明共用体类型保持一致,能成为任何类型的共用 体;不需要转换. 加入共用体包含指针类型,一致的参数能成为一个空指针常量或空指针表达式;加入共用体包含空指针,一致参数能成为指针表达式。加入共用体成员类型是之至, 类型修饰符就像指定,就如一般的指针的转换一样。
第二,参数被传给函数用到的调用转换(透明共用体的第一个成员,不能调用转换共用体本身。所有的成员必须拥有相同的机器代理;这就使参数传输能进行了。
透 明共用体拥有多种接口库函数来处理兼容性问题。例如,假定等待函数必须要接受一种整型来遵从Posix,或者一个共用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)
当应用一种类型(包括共用体和结构体),这个属性意为着这种变量类型可能出席那不能使用的,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删除那些没有被用到的汇编代码段。
如果你从汇编代码中找到一个特殊的寄存器,你大概不得不在第三个冒号列出之后这个寄存器来告诉编译器寄存器的值是修正值。在一些汇编程序中,寄存器的名字以`%'开始;要在汇编代码中生成一个‘%’,你必须在输入时这样写:`%%'。
如果你的汇编指令可以改变条件码寄存器,在频繁使用的寄存器列表中加上`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; } 
输出如下:
  __FUN CTION__ = 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 (); 
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_10.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_9.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_8.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_7.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_6.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_5.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_4.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_3.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_2.html
文章出处: http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042.html
 

 

 原文地址 http://www.diybl.com/course/3_program/c++/cppsl/2008713/133042_2.html

你可能感兴趣的:(C 扩展)