扫描关注一起学嵌入式,一起学习,一起成长
不同编译器,由于开发环境、硬件平台、性能优化的需要,除了支持 C 语言标准,还会自己做一些扩展。
本系列文章,归纳总结 GCC 编译器对C语言标准的扩展语法,并逐一进行讲解。
前期文章:
GNU C扩展语法归纳详解(一)
GCC对C语言扩展语法归纳详解(二)
接下来继续讲解其他扩展语法内容。
属性声明
GNU C 增加了一个 __attribute__
关键字用来声明一个函数、变量或类型的特殊属性。
声明这个特殊属性的主要用途就是,指导编译器在编译程序时进行特定方面的优化或代码检查。例如,我们可以通过属性声明来指定某个变量的数据对齐方式。
__attribute__
的用法非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可:
__attribute__((ATTRIBUTE))
下面几个小节,继续讲解一些特殊属性声明。
属性声明 format
GNU通过 __attribute__
扩展的 format 属性,来指定变参函数的参数格式检查。
一些自定义的打印函数往往是变参函数,用户在调用这些接口函数时参数往往不固定,那么编译器在编译程序时,怎么知道我们的参数格式对不对呢?如何对我们传进去的实参做格式检查呢?
__attribute__
的 format 属性这时候就派上用场了。它的使用方法如下:
__attribute__((format(archetype, string-index, first-to-check)))
void LOG(const char *fmt, ...) __attribute__((format(printf, 1, 2)))
定义一个变参函数 LOG,属性 format(printf,1,2) 有3个参数,第1个参数 printf 是告诉编译器,按照 printf() 函数的标准来检查;第 2 个参数表示在 LOG() 函数所有的参数列表中格式字符串的位置索引;第3个参数是告诉编译器要检查的参数的起始位置。
使用示例
LOG("yi qi xue qian ru shi, %dn", 100);
在这个 LOG() 函数中有 2 个参数,第 1 个参数是格式字符串,第 2 个参数是要打印的一个常量值 10,用来匹配格式字符串中的占位符。
通过 format(printf,1,2) 属性声明,告诉编译器:LOG() 函数的参数,其格式字符串的位置在所有参数列表中的索引是 1,即第一个参数;要编译器帮忙检查的参数,在所有的参数列表里索引是2。
如果一个字符串中含有格式匹配符(例如 %d),也称为占位符,那么这个字符串就是格式字符串。
知道了 LOG() 参数列表中格式字符串的位置和要检查的参数位置,编译器就会按照检查 printf 的格式打印一样,对 LOG() 函数进行参数检查了。
如果 LOG 定义如下:
void LOG(int num, const char *fmt, ...) __attribute__((format(printf, 2, 3)))
这个函数定义多了一个参数 num,格式字符串在参数列表中的位置发生了变化(在所有的参数列表中,索引由1变成了2),要检查的第一个变参的位置也发生了变化(索引从原来的2变成了3)。
补充说明,实现自己的打印函数有一些优势:
-
实现自己需要的打印格式
-
实现打印开关控制和优先级控制
-
根据需要不断添加功能
属性声明 weak
GNU C通过 weak 属性声明,可以将一个强符号转换为弱符号。使用方法如下。
void __attribute__((weak)) func(void);
int num __attribute__((weak));
先来解释一下强符号和弱符号相关的知识。
在一个程序中,无论是变量名,还是函数名,在编译器的眼里,就是一个符号而已。符号可以分为强符号和弱符号:
-
强符号:函数名,初始化的全局变量名
-
弱符号:未初始化的全局变量名。
强符号和弱符号主要用来解决在程序链接过程中,多个同名全局变量、同名函数的冲突问题。编译器对于这种同名符号冲突,在做符号决议时,一般会选用强符号,丢掉弱符号。
注意,一个项目中,不能同时存在两个强符号。如果在一个多文件的工程中定义两个同名的函数或全局变量,那么链接器在链接时就会报重定义错误。
允许强符号和弱符号同时存在;若同时定义一个初始化的全局变量和一个未初始化的全局变量,这种写法在编译时是可以编译通过的。
当同名的符号都是弱符号时,会选择在内存中的存储空间大的那一个。
举例如下,使用GNU C扩展的weak属性,将一个强符号转换为弱符号 :
// func.c 文件中的内容
int a __attribbute__((weak)) = 1;
void func(void)
{
printf("func: a = %dn", a);
}
//main.c 文件中的内容
int a = 4;
void func(void);
int main(void)
{
printf("main: a = %dn", a);
func();
return 0;
}
$ gcc -o test main.c func.c
$ ./test
main: a = 4
func: a = 4
我们通过 weak 属性声明,将 func.c 中的全局变量 a 转化为一个弱符号,然后在 main.c 中同样定义一个全局变量 a,并初始化 a 为 4。链接器在链接时会选择 main.c 中的这个强符号,所以在两个文件中,变量a的打印值都是4。
函数的强符号和弱符号
链接器对于同名函数冲突,同样遵循相同的规则。函数名本身就是一个强符号,在一个工程中定义两个同名的函数,编译时肯定会报重定义错误。
我们可以通过 weak 属性声明,将其中一个函数名转换为弱符号。
// func.c 文件中的内容
int a __attribbute__((weak)) = 1;
void func(void)
{
printf("func: a = %dn", a);
printf("func.c: strong symboln");
}
//main.c 文件中的内容
int a = 4;
void __attribbute__((weak)) func(void)
{
printf("a = %dn", a);
printf("main.c: weak symboln");
}
int main(void)
{
func();
return 0;
}
编译后执行结果如下:
a = 4
func.c: strong symbol
在 main.c 中定义了一个同名的 func() 函数,然后通过weak属性声明将其转换为一个弱符号。
链接器在链接时会选择func.c中的强符号,当我们在 main() 函数中调用 func() 函数时,实际上调用的是func.c 文件里的 func() 函数。
全局变量 a 在 func.c 中定义的是一个弱符号,所以在 func() 函数中打印的是 main.c 中的全局变量a的值。
弱符号的用途
在编译阶段,当编译器只看到一个源文件中引用一个变量或函数的声明,而没有看到其定义时,编译器一般编译不会报错,编译器会认为这个符号可能在其他文件中定义。
在链接阶段,链接器会到其他文件中找这些符号的定义,若未找到,则报未定义错误。
如果一个函数被声明为弱符号时,链接器找不到这个函数的定义时,也不会报错。编译器会将这个函数名,设置为0或一个特殊的值。
当程序运行调用这个函数时,跳转到零地址或一个特殊的地址会报错,产生一个内存错误(段错误)。
也就是说,编译、链接不会有问题,到运行阶段就会出错。
void __attribute__((weak)) func();
int main(void)
{
func();
return 0;
}
弱符号的这个特性,在库函数中应用得很广泛。如你在开发一个库时,基础功能已经实现,有些高级功能还没实现,那么你可以将这 些函数通过 weak 属性声明转换为一个弱符号。
通过这样设置,即使还没有定义函数,我们在应用程序中只要在调用之前做一个非零的判断就可以了,并不影响程序的正常运行。等以后发布新的库版本,实现了这些高级功能,应用程序也不需要进行任何修改,直接运行就可以调用这些高级功能。
属性声明:alias
GNU C 扩展了一个 alias 属性,这个属性很简单,主要用来给函数定义一个别名。
void __f(void)
{
printf("__fn");
}
void f() __attribbute__((alias("__f")));
int main(void)
{
f();
return 0;
}
通过alias属性声明,我们可以给 __f()
函数定义一个别名 f()
,以后如果想调用 __f()
函数,则直接通过f()调用即可。
在Linux内核中,你会发现 alias 有时会和 weak 属性一起使用。如有些函数随着内核版本升级,函数接口发生了变化,我们可以通过 alias 属性对这个旧的接口名字进行封装,重新起一个接口名字。
// f.c 文件
void __f(void)
{
printf("__fn");
}
void f() __attribute__((weak, alias("__f")));
//main.c 文件
void __attribute__((weak)) f(void);
void f(void)
{
printf("f()n");
}
int main(void)
{
f();
return 0;
}
上面的程序例子,在 main.c 中新定义了 f() 函数,当 main() 函数调用 f()
函数时,会直接调用 main.c 中新定义的函数;当 f()
函数没有被定义时,则调用 __f()
函数。
内联函数
与内联函数相关的两个属性:noinline 和 always_inline。
这两个属性的用途是告诉编译器,在编译时,对我们指定的函数内联展开或不展开。其使用方法如下
static inline __attribute__((noline)) int func();
static inline __attribute__((always_inline)) int func();
使用 inline
声明的函数被称为内联函数。使用 inline 声明一个内联函数,只是建议编译器在编译时内联展开。
在使用 noinline 和 always_inline 对一个内联函数作显式属性声明后,编译器的编译行为就变得确定了:使用noinline声明,就是告诉编译器不要展开;使用always_inline属性声明,就是告诉编译器要内联展开。
调用一个函数的执行过程为:保存当前函数现场、跳到调用函数执行、恢复当前函数现场、继续执行当前函数 。
有些函数短小精悍,而且调用频繁,调用开销比较大,这时候我们就可以将这个函数声明为内联函数。
编译器在编译过程中遇到内联函数,像宏一样,将内联函数直接在调用处展开,这样做就减少了函数调用的开销,直接执行内联函数展开的代码,不用再保存现场和恢复现场。
内联函数与宏
内联函数与宏定义相比,有以下优势:
-
参数类型检查。内联函数虽然具有宏的展开特性,但其本质仍是函数,在编译过程中,编译器仍可以对其进行参数检查,而宏不具备这个功能。
-
便于调试。函数支持的调试功能有断点、单步等,内联函数同样支持。
-
返回值。内联函数有返回值,返回一个结果给调用者。这个优势是相对于ANSI C说的,因为现在宏也可以有返回值和类型了,如前面使用语句表达式定义的宏。
-
接口封装。有些内联函数可以用来封装一个接口,而宏不具备这个特性。
内联函数也有缺点:
-
内联函数会增大程序的体积 。
-
降低了函数的复用性。
当我们确定使用内联展开或者不展开时,就可以用 noinline 或者 always_inline对函数进行属性声明。
编译器对内联函数做展开处理时,会直接在调用处展开内联函数代码,不会再单独生成内联函数的汇编代码。
内联函数一般定义在头文件中,任何想使用这个内联函数的源文件,都不必亲自再去定义一遍,直接包含这个头文件,即可像宏一样使用。
使用 inline 定义的内联函数,编译器不一定会内联展开,那么当一个工程中多个文件都包含这个内联函数的定义时,编译时就有可能报重定义错误。
使用 static 关键字修饰,则可以将这个函数的作用域限制在各自的文件内,避免重定义错误的发生。
至此,关于GCC扩展C语言的语法归纳讲解完毕。
感谢阅读,加油~
觉得文章不错,点击“分享”、“赞”、“在看” 呗!
发表评论