GCC 编译器对 C 语言标准进行了一些列扩展,接下来会逐个介绍GNU C 的扩展语法,可能有很多我们习以为常的用法,亦或是大家不常用的操作。
本文介绍以下两个扩展语法:
指定初始化
语句表达式的应用
1. 指定初始化
在 C 语言标准中,当我们定义并初始化一个数组时,常用方法如下:
int a[10] = {0,1,2,3,4,5,6,7,8};
按照这种固定的顺序,我们可以依次的对a[0] 到 a[8] 赋值。a[9] 没有赋值,编译器会自动设置为0.
当数组毕竟小时,使用这种初始化方式会比较方便,但是当数组比较大,并且数组里的非零元素不连续,在按照固定顺序赋值就很麻烦了。
C99 标准改进了数组初始化方式,支持指定元素初始化,不在按照固定的顺序初始化。
int b[100] = {[10] = 1, [30] = 2};
通过数组元素索引,我们可以直接给指定的数组元素赋值,除了数组,一个结构体变量的初始化,也可以通过指定某个结构体成员直接赋值。
在早期 C 语言标准不支持指定初始化时,GCC编译器就已经支持指定初始化了,因此这个特性也被看做GCC的一个扩展特性。
1.1 指定初始化数组元素
在 GNU C 中,通过数组元素索引,我们可以直接给指定的几个元素赋值。这里注意,各个赋值之间用逗号隔开,而非分号
int b[100] = {[10] = 1, [30] = 2};
如果想给数组中一个索引范围的元素初始化,可以采用 ...
#includeint main(void) { int b[100] = { [10 ... 30] = 1, [50 ... 60] = 2 }; for (int i = 0; i < 100; i++) { if (i % 10 == 0) { printf(" "); } printf("%d ", b[i]); } printf(" "); return 0; }
GNU C 支持 使用 ... 表示范围扩展,在这里使用[10 ... 30] 表示一个范围,相当于给b[10] 到 b[30] 之间的20个数赋值。
... 不仅可以用在数组初始化中,也可以用在switch-case 语句中。
int main(void) { int i = 4; switch(i) { case 1: printf("1 " ); break; case 2 ... 8: printf("%d " , i); break; default: printf("default "); break; } return 0; }
这里需要注意 ... 两边的数据之间要有空格,否则,会报编译错误。
1.2 指定初始化结构体成员
和数组类似,在 C 语言标准中,初始化结构体变量也要按照固定顺序,但是在 GNU C 中我们可以结构体域来指定初始化某个成员。
struct student { char name[20]; int age; }; int main(void) { struct student stu1 = {"s1", 20}; printf("%s:%d ", stu1.name, stu1.age); struct student stu2 = { .name = "s2"; .age = 28; } printf("%s:%d ", stu2.name, stu2.age); return 0; }
1.3 Linux 内核中的指定初始化
在Linux 驱动中,大量使用 GNU C的这种指定初始化方式,通过结构体成员来初始化结构体变量:
static const struct file_operations ci_port_test_fops = { .open = ci_port_test_open, .write = ci_port_test_write, .read = seq_read, .llseek = seq_lseek, .release = single_release, };
在驱动程序中,我们经常使用file_operations 这个结构体来注册我们开发的驱动,然后系统会以回调的方式。
结构体file_operations 里定义了很多结构体成员,而在这个驱动中,我们只是初始化了部分成员变量。通过访问结构体的各个成员域来指定初始化,当结构体成员很多时优势就体现出来了,初始化会更加方便。
1.4 指定初始化的好处
指定初始化不仅使用灵活,还有一个好处就是代码易于维护。特别是在Linux 内核这种大型项目中,有几万个文件,大量使用了这种指定初始化。
如果采用标准C语言按照固定顺序初始化赋值,一旦增加、删除一个成员,大量的文件都有重新调整初始化顺序,牵一发而动全身。
2. 语句表达式
2.1 语句表达式
GNU C 对 C 语言标准作了扩展,允许在一个表达式里内嵌语句,允许在表达式内部使用局部变量、for 循环 和 goto 跳转语句。这种类型的表达式,我们称之为语句表达式:
( { 表达式1; 表达式2; 表达式3; } )
语句表达式最外面使用 () 括起来,里面使用 {} 包起来的是代码块,代码块里允许内嵌各种语句。
语句的格式可以是一般表达式,也可以是循环和跳转语句。
和一般表达式一样,语句表达式也有自己的值。语句表达式的值为内嵌语句中最后一个表达式的值。
int main (void) { int sum = 0; sum = ( { int s = 0, i = 0; for (i = 0; i < 10; i++) s = s + i; s; }); printf("sum = %d ", sum); return 0; }
编译:
gcc -std=gnu89 gnu1.c / gcc -std=gnu99 gnu1.c
在上面的程序中,通过语句表达式计算1到10的累加,因为语句表达式的值等于最后一个表达式的值,所以在 for 循环后面要添加一个s。如果你将这个值改成 s=100,会发现sum结果变成100了。
在语句表达式中使用跳转。
int main (void) { int sum = 0; sum = ( { int s = 0, i = 0; for (i = 0; i < 10; i++) s = s + i; goto here; s; }); printf("sum = %d ", sum); here: printf("here: "); printf("sum = %d ", sum); return 0; }
2.2 在宏定义中使用语句表达式
语句表达式的主要用途在于定义功能复杂的宏。使用语句表达式来定义宏,不仅可以实现复杂的功能,还能避免宏定义带来的歧义和漏洞。
下面就以一个例子,让我们领略宏定义的杀伤力。
题目:定义一个宏,求两个数的最大值。
合格:
对于学过C语言的同学,写出这个宏基本上不是什么难事,使用条件运算符即可完成。
#include#define MAX(x, y) x > y ? x :y int main() { printf("max = %d ", MAX(1,2)); printf("max = %d ", MAX(2,1)); printf("max = %d ", MAX(2,2)); printf("max = %d ", MAX(1!=1,1!=2)); return 0; }
运行结果如下,发现最后一个结果与预期不符合
max = 2 max = 2 max = 2 max = 0
我们使用预处理命令展开宏
gcc -E gnu1.c -o gnu1.i
因为 > 号的优先级(6)大于 != 号的优先级,所以展开后,结果就和预期不一样了。
为了避免这种错误,我们可以给宏参数加一个小括号,防止展开后的运算符发生变化。
#define MAX(x, y) (x) > (y) ? (x) :(y)
中等:
上面的宏只能算合格,还是存在漏洞:
#include#define MAX(x, y) (x) > (y) ? (x) :(y) int main() { printf("max = %d ", 3 + MAX(1,2)); return 0; }
预期结果应该是5,结果是1.
预处理展开如下:
优先级顺序:+ 大于 > 号 所以表达式变成了
4 > 2 ? 1:2
故对此宏进行改进:
#define MAX(x, y) ( (x) > (y) ? (x) : (y) )
使用小括号括起来,就避免了当一个表达式同时含有宏定义和其他高优先级运算符时破坏整个表达式的运算顺序。
良好:
上面的宏,虽然解决了运算符优先级问题,然任然存在一些漏洞。
定义两个变量i和j,然后比较两个变量的大小,并做自增运算。实际运行结果发现max=7
#include#define MAX(x, y) ( (x) > (y) ? (x) :(y) ) int main() { int i = 2; int j = 6; printf("max = %d ", MAX(i++,j++)); return 0; }
预处理展开后表达式如下:
i 和 j 在展开后做了两次自增运算,导致打印 max的值为7。
当然,在C语言编程规范里,使用宏时一般是不允许参数变化的。但是万一碰到这种情况,又该如何处理呢?
这个时候,语句表达式就需要上场了,在语句表达式中定义两个临时变量,分别来暂时存储 i 和 j 的值,然后用临时变量进行比较。
#include#define MAX(x, y) ({ int _x = x; int _y = y; _x > _y ? _x : _y; }) int main() { int i = 2; int j = 6; printf("max = %d ", MAX(i++,j++)); return 0; }
预处理展开:
优秀:
在上面定义的宏中,我们定义了两个int型变量,只能比较整型数据。如果希望比较其他数据类型呢?
#include#define MAX(type, x, y) ({ type _x = x; type _y = y; _x > _y ? _x : _y; }) int main() { int i = 2; int j = 6; printf("max = %d ", MAX(int, i++,j++)); printf("max = %f ", MAX(float, 3.14,3.15)); return 0; }
很容易想到通过一个参数,将数据类型传进去。
进一步修改:
我们只想保留两个参数。
#include#define MAX(x, y) ({ typeof(x) _x = (x); typeof(y) _y = (y); (void) (&_x == &_y); _x > _y ? _x : _y; }) int main() { int i = 2; int j = 6; printf("max = %d ", MAX(i++,j++)); printf("max = %f ", MAX(3.14,3.15)); return 0; }
GNU C 使用关键字 typeof 来获取宏参数的数据类型。比较难以理解的就是第三句:(void) (&_x == &_y);
这句话看起来多余,实际上有两个作用:
对于不同类型的指针比较,编译器会发生一个警告,提示两个数据类似不同;
当比较结果没有用到时,有些编译器可能会给一个警告,加上(void) 后 可以消除警告。
3. 总结
本文主要介绍了GNU C 的扩展:指定初始化和语句表达式的使用,重点介绍了语句表达式在宏中的使用。
事实上 Linux 内核大量使用了GNU C 的扩展语法,特别是语语句表达式在宏中的使用,了解GNU的扩展,有助于我们对C语言的认识更加清晰。
编辑:黄飞
评论
查看更多