gcc语法

ads

扫描关注一起学嵌入式,一起学习,一起成长


不同编译器,由于开发环境、硬件平台、性能优化的需要,除了支持 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语言的语法归纳讲解完毕。

感谢阅读,加油~


扫码,拉你进高质量嵌入式交流群


关注我【一起学嵌入式】,一起学习,一起成长。


觉得文章不错,点击“分享”、“”、“在看” 呗!

最后编辑于:2024/1/18 拔丝英语网

admin-avatar

英语作文代写、国外视频下载

高质量学习资料分享

admin@buzzrecipe.com