GNU C扩展语法归纳详解(一)

文摘   2023-07-29 09:32   安徽  

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


前言

不同编译器,由于开发环境、硬件平台、性能优化的需要,除了支持C语言标准,还会自己做一些扩展。GCC编译器也对C语言标准做了很多扩展。

  • 零长度数组。

  • 语句表达式。

  • 内建函数。

  • __attribute__特殊属性声明。

  • 标号元素。

  • case范围。

  • ……

本系列文章,会归纳总结扩展语法,并逐一进行讲解。

指定初始化

在早期C语言标准不支持指定初始化时,GCC编译器就已经支持指定初始化了,因此这个特性也被看作GCC编译器的一个扩展特性。

指定初始化数组元素

在C语言标准中,当我们定义并初始化一个数组时,常用方法如下:

int a[] = {0,1,2,3,4,5,6,7,8};

依次给a[0] 和 a[8] 赋值。这种初始化的方式对于小长度数组来说比较方便。当数组比较大,并且数组里的非零元素并不连续时,再按照固定顺序初始化就比较麻烦了。

例如,对于数组 b[100],其中 b[10]、b[20]需要初始化为一个非零数值,如果按照固定顺序的方式,会在{}初始化数据中填充大量的 0 。

C99 标准改进了数组的初始化方式,支持指定元素初始化,不用按照固定的顺序初始化。

int b[100] = {[10] = 1, [30] = 2};

通过数组元素索引,可以直接给指定的数组元素赋值。这里有一个细节需要注意,各个赋值之间用逗号“,”隔开,而不是使用分号“;”

给数组中某一个索引范围的数组元素初始化,可以采用如下方式:

int b[100] = {[10 ... 30] = 1, [50 ... 60] = 2};

[10 ... 30] 表示一个索引范围,相当于给 b[10] 到 b[30] 之间的数组元素赋值为1。

这里有一个细节需要注意,就是 "..." 和其两端的数据范围2和8之间要有空格。

指定初始化结构体成员

和数组类似,在老的C语言标准中(C89/C90),初始化结构体变量也要按照固定的顺序,但在GNU C中(以及C99开始的C标准中)我们可以通过结构域来指定初始化某个成员。

如下代码:

struct student
{
    char name[20];
    int age;
}

int main(void)
{
    struct student stu =
    {
        .name = "yiqi";
        .age = 26;
    };

    printf("%s:%d\n", stu.name, stu.age);

    return 0;
}

我们定义一个结构体类型student,然后定义结构体变量stu。初始化stu时,我们采用“指名道姓”的初始化方式,通过结构域名.name和.age,就可以给结构体变量的某一个指定成员直接赋值。

当结构体的成员很多时,使用这种初始化方式会更加方便。

在Linux内核驱动中,大量使用GNU C的这种指定初始化方式,通过结构体成员来初始化结构体变量。

指定初始化的优势

指定初始化不仅使用灵活,而且使得代码易于维护。

尤其是在 Linux 内核这种大型项目中,如果采用C标准按照固定顺序赋值,当 file_operations 结构体类型发生变化时,如添加了一个成员、删除了一个成员、调整了成员顺序,那么使用该结构体类型定义变量的大量C文件都需要重新调整初始化顺序,牵一发而动全身。

通过指定初始化方式 ,就可以避免这个问 题 。无 论file_operations结构体类型如何变化,添加成员也好、删除成员也好、调整成员顺序也好,都不会影响其他文件的使用。

语句表达式

表达式和语句是C语言中的基础概念。表达式就是由一系列操作符和操作数构成的式子。

操作符可以是C语言标准规定的各种算术运算符、逻辑运算符、赋值运算符、比较运算符。操作数可以是一个常量,也可以是一个变量。

根据操作符的不同,表达式可以分为多种类型,具体如下:

  • 关系表达式

  • 逻辑表达式

  • 条件表达式

  • 赋值表达式

  • 算术表达式

表达式的后面加一个;就构成了一条基本的语句。编译器在编译程序、解析程序时,不是根据物理行,而是根据分号;来判断一条语句的结束标记的。

不同的语句,使用大括号{}括起来,就构成了一个代码块。C语言允许在代码块内定义一个变量,这个变量的作用域也仅限于这个代码块内 。

语句表达式

GNU C 对 C 语言标准作了扩展,允许在一个表达式里内嵌语句,允许在表达式内部使用局部变量、for循环和goto跳转语句。这种类型的表达式,称为语句表达式。

语句表达式的格式如下:

({表达式1; 表达式2; 表达式3;})

语句表达式最外面使用小括号 () 括起来,里边一对大括号 {} 包起来的是代码块,代码块里允许内嵌各种语句。语句的格式可以是一般表达式,也可以是循环、跳转语句。

和一般表达式一样,语句表达式也有自己的值。语句表达式的值为内嵌语句中最后一个表达式的值。例如:

sum = 
({
    int s = 0;
    for(int i = 0; i < 10; i++)
        s = s + i;
    s;
})

在上面的程序中,通过语句表达式实现了从1到10的累加求和,因为语句表达式的值等于最后一个表达式的值,所以在for循环的后面,我们要添加一个s;语句表示整个语句表达式的值。

我们在语句表达式内定义了局部变量,使用了for循环语句。

宏定义中使用语句表达式

语句表达式的主要用途在于定义功能复杂的宏。使用语句表达式来定义宏,不仅可以实现复杂的功能,还能避免宏定义带来的歧义和漏洞。

举个例子,定义一个宏,求两个数的最小值。

#define MIN(x,y) {
      typeof(x) _x = (x);   \
      typeof(y) _y = (y);   \
      (void)(&_x = &_y);   \
      _x < _y ? _x : _y;     \
            })

对于 GNU C 来说,因为它扩展了一个关键字 typeof,可以获取参数的数据类型。对于其他版本的 C 语言,需要查看手册进行考证,在此不展开介绍。

其中 (void)(&x==&y)  是用于检查 x 和 y 的类型是否相同。它有两个作用:

  • 一是用来给用户提示一个警告。对于不同类型的指针比较,编译器会发出一个警告,提示两种数据的类型不同。

  • 二是两个数进行比较运算,运算的结果却没有用到,有些编译器可能会给出一个 warning,加一个(void)后,就可以消除这个警告。

关于这段代码的详细解释,可以参考  完美实现C语言比较两个数大小的宏定义

typeof

GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型。

使用 typeof 可以获取一个变量或表达式的类型。typeof 的参数有两种形式:表达式或类型。

使用方法如下代码:

int main
{
    int i = 2;
    typeof(i) k = 6;

    int *p = &k;
    typeof(p) q = &i;

    printf("k = %d\n", k);
    printf("*p = %d\n", *p);
    printf("i = %d\n", i);
    printf("*q = %d\n", *q);

    return 0;
}

通过 typeof 获取一个变量的类型 int 后,可以使用该类型再定义一个变量。这和我们直接使用int定义一个变量的效果是一样的。

container_of宏

有了上面语句表达式和typeof的基础知识,我们就可以分析 Linux 内核第一宏:container_of 了。

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

#define container_of(ptr, type, member) ({   \
        const typeof(((type *)0)->member) * __mptr = (ptr); \
        (type *)((char *)__mptr - offsetof(type, member)); })

这个宏定义包含了 GNU C 编译器扩展特性的综合运用,宏中有宏。

那么这个宏到底是干什么的呢?它的主要作用就是,根据结构体某一成员的地址,获取这个结构体的首地址。

根据宏定义,我们可以看到,这个宏有三个参数:type为结构体类型,member为结构体内的成员,ptr为结构体内成员member的地址。

如果我们知道了一个结构体的类型和结构体内某一成员的地址,就可以获得这个结构体的首地址。container_of 宏返回的就是这个结构体的首地址。

container_of 宏的实现主要用到了我们前两节所学的语句表达式和 typeof,再加上结构体存储的基础知识。

从语法角度来看,container_of宏的实现由一个语句表达式构成。语句表达式的值即最后一个表达式的值

(type *)((char *)__mptr - offsetof(type, member));

详细解释,可以参考 对linux内核中container_of()宏的理解

这个宏在内核中非常重要,Linux内核中有大量的结构体类型数据,为了抽象,对结构体进行了多次封装,往往在一个结构体里嵌套多层结构体。

好了,今天先到这,感谢阅读,加油~


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


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


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

裸机思维
傻孩子图书工作室。探讨嵌入式系统开发的相关思维、方法、技巧。
 最新文章