GNU C扩展语法归纳详解_gnu c 扩展,2024年最新十位互联网一线高工手写Golang高级知识

已剪辑自: https://mp.weixin.qq.com/s?__biz=Mzg3ODU3Nzk3MQ==&mid=2247502432&idx=1&sn=0708407176e93ecb4fb71f4c5e73cc39&chksm=cf131a3cf864932a32766d201d28316f52edc38bbd9182722e0c967457fe1ca25ee274aefe44&token=1046321147&lang=zh_CN&scene=21#wechat_redirect

前言

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

在51单片机上用C语言开发程序,我们经常使用Keil for C51集成开发环境。你会发现Keil for C51或者其他IDE里的C编译器会对C语言做很多扩展,如增加了各种关键字。

  • data:RAM的低128B空间,单周期直接寻址。
  • code:表示程序存储区。
  • bit:位变量,常用来定义51单片机的P0~P3管脚。
  • sbit:特殊功能位变量。
  • sfr:特殊功能寄存器。
  • reentrant:重入函数声明。

如果你在程序中使用以上这些关键字,那么你的程序只能使用51编译器来编译运行;如果你使用其他编译器,如VC++6.0,则编译是通不过的。

同样的道理,GCC编译器也对C语言标准做了很多扩展。

  • 零长度数组。
  • 语句表达式。
  • 内建函数。
  • __attribute__特殊属性声明。
  • 标号元素。
  • case范围。
  • ……

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

指定初始化

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

指定初始化数组元素

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

int a[10] = {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语言标准中,初始化结构体变量也要按照固定的顺序,但在GNU 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时,我们采用GNU C的初始化方式,通过结构域名.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§ 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内核中有大量的结构体类型数据,为了抽象,对结构体进行了多次封装,往往在一个结构体里嵌套多层结构体。

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

已剪辑自: https://mp.weixin.qq.com/s?__biz=Mzg3ODU3Nzk3MQ==&mid=2247502489&idx=1&sn=871f346af2de65ef9847f5223654e9f9&chksm=cf131ac5f86493d3c6d69fd4c17d60904c371c3b1c119c825635bb2cd7a672b463db71cce20d&token=1329370240&lang=zh_CN&scene=21#wechat_redirect

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

本系列文章,归纳总结 GCC 编译器对C语言标准的扩展语法,并逐一进行讲解。

前期文章:

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

接下来继续讲解其他扩展语法内容。

上一篇文章,在讲解指定初始化数组元素的时候,介绍过用 ... 实现索引范围。

GNU C支持使用…表示范围扩展,这个特性不仅可以使用在数组初始化中,也可以使用在switch-case语句中,如下面的程序 :

#include <stdio.h>

int main(void)
{
int i = 4;
switch(i)
{
case 1:
printf(“1\n”);
break;
case 2 … 8:
printf(“%d\n”, i);
break;
case 9:
printf(“9\n”);
break;
}
return 0;
}

在这个程序中,如果当case值为2~8时,都执行相同的case分支,我们就可以通过 case 2...8: 的形式来简化代码。

需要注意的是,... 和两端的数据范围之间要有空格,否则会编译报错。

零长度数组,顾名思义,零长度数组就是长度为0的数组。

ANSI C标准规定:定义一个数组时,数组的长度必须是一个常数,即数组的长度在编译的时候是确定的。

C99标准规定:可以定义一个变长数组。也就是说,数组的长度在编译时是未确定的,在程序运行的时候才确定,甚至可以由用户指定大小。

int len;
int a[len];

除此之外,GNU C 还支持零长度数组。其定义如下:

int a[0];

零长度数组有一个奇特的地方,就是它不占用内存存储空间。当使用sizeof查看其大小,通过运行结果可以看到,零长度数组在内存中不占用存储空间,长度大小为0。

零长度数组一般单独使用的机会很少,它常常作为结构体的一个成员,构成一个变长结构体。

在一个变长结构体中,零长度数组不占用结构体的存储空间,但是我们可以通过使用结构体的成员 a 去访问内存,非常方便。变长结构体的使用示例如下:

struct buffer
{
int len;
int a[0];
}

int main(void)
{
struct buffer *buf;
buf = (struct buffer *)malloc(sizeof(struct buffer)+20);
buf->len = 20;
strcpy(buf->a, “hello yiqixue!\n”);
puts(buf->a);

free(buf);

return 0;
}

在这个程序中 , 我们使用 malloc 申请一片内存 ,大小为 sizeof(buffer)+20,即 24 字节,其中 4 字节为结构体buffer的大小。

成员变量 len 用来表示内存的长度 20,20字节空间是我们使用的内存空间。我们可以通过结构体成员a直接访问这片内存。

关于零长度数组的应用可以参考:C语言中长度为零的数组详解

指针与零长度数组

思考一个问题:为什么不使用指针来代替零长度数组?

大家可能常常听到:数组名在作为函数参数进行参数传递时,就相当于一个指针。注意,数组名在作为参数传递时,传递的确实是一个地址,但数组名绝不是指针,两者不是同一个东西。

数组名用来表征一块连续内存空间的地址,而指针是一个变量,编译器要给它单独分配一个内存空间,用来存放它指向的变量的地址。

看一段示例程序:

struct buffer1
{
int len;
int a[0];
};

struct buffer2
{
int len;
int *a;
};

int main(void)
{
printf(“buffer1:%d\n”, sizeof(struct buffer1));
printf(“buffer2:%d\n”, sizeof(struct buffer2));

return 0;
}

在32位CPU平台上,这段程序的运行结果为:

buffer1: 4
buffer2: 8

对于一个指针变量,编译器要为这个指针变量单独分配一个存储空间,然后在这个存储空间上存放另一个变量的地址。

而对于数组名,编译器不会再给它分配一个单独的存储空间,它仅仅是一个符号,和函数名一样,用来表示一个地址 。

那么为什么不用指针,而用零长数组呢?如果使用指针,指针本身占用存储空间不说,它远远没有零长度数组用得巧妙,零长度数组不会对结构体定义造成冗余,而且使用起来很方便。

扩展关键字 attribute

GNU C 增加了一个 __attribute__ 关键字用来声明一个函数、变量或类型的特殊属性。

声明这个特殊属性的主要用途就是,指导编译器在编译程序时进行特定方面的优化或代码检查。例如,我们可以通过属性声明来指定某个变量的数据对齐方式。

__attribute__ 的用法非常简单,当我们定义一个函数、变量或类型时,直接在它们名字旁边添加下面的属性声明即可:

attribute((ATTRIBUTE))

需要注意的是,__attribute__ 后面是两对小括号,不能只写一对,否则编译就会报错。括号里面的 ATTRIBUTE 表示要声明的属性。

目前 __attribute__ 支持十几种属性声明:

  • section.
  • aligned.
  • packed.
  • format.
  • weak.
  • alias.
  • noinline.
  • always_inline.
  • ……

在这些属性中,aligned 和 packed 用来显式指定一个变量的存储对齐方式。在正常情况下,当我们定义一个变量时,编译器会根据变量类型给这个变量分配合适大小的存储空间,按照默认的边界对齐方式分配一个地址。

而使用 __atttribute__ 这个属性声明,就相当于告诉编译器:按照我们指定的边界对齐方式去给这个变量分配存储空间 。

char c2 attribute((aligned(8))) = 4;
int val attribute((section(“.data”)));

有些属性可能还有自己的参数。如 aligned(8) 表示这个变量按 8 字节地址对齐,属性的参数也要使用小括号括起来。

如果属性的参数是一个字符串,则小括号里的参数还要用双引号引起来。

当然,我们也可以对一个变量同时添加多个属性说明。在定义变量时,各个属性之间用逗号隔开:

char c2 attribute((packed,aligned(4)));
char c2 attribute((packed,aligned(4))) = 4;
attribute((packed,aligned(4))) char c2 = 4;

以上示例中,我们对一个变量添加两个属性声明,这两个属性都放在 __attribute__(()) 的两对小括号里面,属性之间用逗号隔开。

注意,属性声明要紧挨着变量,上面的三种声明方式都是没有问题的。

面的声明方式在编译的时候可能就通不过:

char c2 = 4 attribute((packed,aligned(4)));

接下来,我们讲解几个特殊属性。

section 属性的主要作用是:在程序编译时,将一个函数或变量放到指定的段,即放到指定的section中。

一个可执行文件主要由代码段、数据段、BSS段构成。代码段主要存放编译生成的可执行指令代码,数据段 和 BSS 段用来存放全局变量、未初始化的全局变量。代码段、数据段和 BSS 段构成了一个可执行文件的主要部分。

除了这三个段,可执行文件中还包含其他一些段。用编译器的专业术语讲,还包含其他一些 section,如只读数据段、符号表等 。

参考 深入理解 C 语言的 hello world

在 Linux 环境下,使用 GCC 编译生成一个可执行文件 a.out,使用 readelf 命令,就可以查看这个可执行文件中各个 section 的基本信息,如大小、起始地址等。

在这些 section 中,.text section 就是我们常说的代码段;.data section 是数据段;.bss section 是 BSS 段。

在 GNU C 中,我们可以通过 __attribute__ 的 section 属性,显式指定一个函数或变量,在编译时放到指定的 section里面。

我们知道,未初始化的全局变量默认是放在 .bss section 中的,即默认放在 BSS 段中。现在我们就可以通过 section 属性声明,把这个未初始化的全局变量放到数据段 .data 中:

int val = 8;
int val_n attribute((section(“.data”)));

int main(void)
{
return 0;
}

GNU C通过 attribute 来声明 aligned 和 packed 属性,指定一个变量或类型的对齐方式。

这两个属性用来告诉编译器:在给变量分配存储空间时,要按指定的地址对齐方式给变量分配地址。

如果你想定义一个变量,在内存中以8字节地址对齐,就可以这样定义:

int a attribute((aligned(8));

通过aligned属性,我们可以显式地指定变量a在内存中的地址对齐方式。

注意,aligned 有一个参数,表示要按几字节对齐,地址对齐的字节数必须是 2 的幂次方,否则编译就会出错。

通过 aligned 属性声明,虽然可以显式地指定变量的地址对齐方式,但是也会因边界对齐造成一定的内存空洞,浪费内存资源。

按照地址对齐的一个原因为:这种对齐设置可以简化 CPU 和内存 RAM 之间的接口和硬件设计。

对于 32 位的计算机系统来说,在CPU读取内存时,硬件设计上可能只支持 4 字节或 4 字节倍数对齐的地址访问,CPU每次向内存 RAM 读写数据时,一个周期可以读写 4 字节。

结构体对齐

编译器在给一个结构体变量分配存储空间时,不仅要考虑结构体内各个基本成员的地址对齐,还要考虑结构体整体的对齐。为了结构体内各个成员地址对齐,编译器可能会在结构体内填充一些空间;为了结构体整体对齐,编译器可能会在结构体的末尾填充一些空间 。

详细可参考:结构体大小 你算对了吗

根据结构体的对齐规则,结构体的整体对齐要按结构体所有成员中最大对齐字节数或其整数倍对齐,或者说结构体的整体长度要为其最大成员字节数的整数倍,如果不是整数倍则要补齐 。

指定结构体某个成员变量的地址对齐方式,成员变量 b 按照 4 字节对齐:

struct data
{
char a;
short b attribute((aligned(4)));
int c;
};

该结构体大小为 12 字节。这是因为,我们显式指定 short 变量以 4 字节地址对齐,导致变量 a 的后面填充了 3 字节空间。int型变量 c 也要4字节对齐,所以变量b的后面也填充了2字节,导致整个结构体的大小为12字节。

显式指定整个结构体的对齐方式:

struct data
{
char a;
short b;
int c;
} attribute((aligned(16)));

这个结构体成员共占 8 个字节。正常来说,整个结构体的对齐只要按最大成员的对齐字节数(4字节)对齐即可。

但是,经过显式指定结构体整体以 16 字节对齐后,编译器就会在这个结构体的末尾填充 8 字节以满足 16 字节对齐的要求,最终导致结构体的总长度变为16字节。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-EgwVnPDa-1712963717677)]
[外链图片转存中…(img-aRtwIi3c-1712963717678)]
[外链图片转存中…(img-BxDOmK2Y-1712963717679)]
[外链图片转存中…(img-lGcP8OfG-1712963717679)]
[外链图片转存中…(img-S8OKa4JU-1712963717680)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-hDHQLEB5-1712963717680)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 13
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值