GNU C 一些有趣的扩展语法
复合语句作为表达式
在使用 Rust 的过程中,常常会觉得 Rust 任何一个 block 都能返回值的特性太赞了,这可以极大地限制一些临时变量的作用域,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn main() {
let value: Option<u8> = {
let string = "123".to_owned();
if let Ok(temp) = string.parse::<i32>() {
if temp > 255 {
None
} else {
Some(temp as u8)
}
} else {
None
}
};
println!("{}", value == Some(123));
}
事实上,在 GNU C 扩展语法中,可以享受到类似的特性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
int main()
{
unsigned char value = ({
const char *string = "123";
int temp = atoi(string);
if (temp > 255)
temp = 0;
temp;
});
printf("%d\n", value);
}
该语法是在 C 语言的复合语句(也就是被花括号包裹的语句)外侧再围绕一层小括号,该复合语句最后一句的返回值将被视为整个复合语句的返回值。
不过这种语法远没有 Rust 那样的灵活,因为 C 语言的 if
/ for
/ while
等代码块是不返回值的,也就是说我们不能把上面的代码改成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
int main()
{
unsigned char value = ({
const char *string = "123";
int temp = atoi(string);
if (temp > 255)
0;
else
temp;
});
printf("%d\n", value);
}
但是写成三元运算符是可以的:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
int main()
{
unsigned char value = ({
const char *string = "123";
int temp = atoi(string);
temp > 255 ? 0 : temp;
});
printf("%d\n", value);
}
在 GNU C 中这种写法常见于宏函数,比如 kernel 中的 max
宏:
1
2
3
4
5
6
7
8
9
10
#define __max(t1, t2, max1, max2, x, y) ({ \
t1 max1 = (x); \
t2 max2 = (y); \
(void) (&max1 == &max2); \
max1 > max2 ? max1 : max2; })
#define max(x, y) \
__max(typeof(x), typeof(y), \
__UNIQUE_ID(max1_), __UNIQUE_ID(max2_), \
x, y)
范围匹配
同样是一个在 Rust 中很香的语法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn print_score(score: u32) {
match score {
0..=59 => println!("D"),
60..=69 => println!("C"),
70..=79 => println!("B"),
80..=89 => println!("B+"),
90..=94 => println!("A"),
95..=100 => println!("A+"),
_ => println!("?"),
}
}
fn main() {
print_score(75);
}
当然 Rust 的范围匹配远不止这么简单,不过受限于 C 语言的 switch
只能匹配整数类型,因此只有这种情况可以在 GNU C 里实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
void print_score(unsigned int score)
{
switch (score)
{
case 0 ... 59:
printf("D\n");
break;
case 60 ... 69:
printf("C\n");
break;
case 70 ... 79:
printf("B\n");
break;
case 80 ... 89:
printf("B+\n");
break;
case 90 ... 94:
printf("A\n");
break;
case 95 ... 100:
printf("A+\n");
break;
}
}
int main()
{
print_score(75);
return 0;
}
三元表达式的省略
三元表达式即 a ? b : c
,通常在 a
, b
, c
三者都比较短的时候,我们会使用这个语法以避开使用 if
。
GNU C 对该语法有另一种支持,即 a ? : b
,当 a
是立即数时,该语法等效于 a ? a : b
,但是如果 a
不是立即数,而是包含函数调用的表达式时,该语法可以避免调用两次该表达式造成的意料之外的结果。
无对齐
事实上,这是 GNU C 的 __attribute__
语法的一种,但是我并不想介绍整个体系,在此我们只关心 __attribute__((packed))
。
在 C 中,通常会对结构体成员进行对齐优化,让结构体成员的地址处于更好计算的位置。举个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
struct TestAlign {
char a; // 1 byte
int b; // 4 bytes
char c[2]; // 2 bytes
double d; // 8 bytes
};
int main()
{
// print 24 bytes
printf("size of TestAlign is : %ld bytes\n", sizeof(struct TestAlign));
}
看起来我们定义的结构体 TestAlign
是 1+4+2+8
一共 15 字节,然而 sizeof
告诉我们它是 24 字节。具体来说,在一字节的 a
后增加了三个字节,以和四字节的 b
对齐,在二字节的 c
后增加了六个字节,以和八字节的 d
对齐。
在大部分情况下,我们不会感受到这种对齐带来的不便。但是在有一种场合下,这种对齐就会带来麻烦,那就是对数据流的解析。如果没有对齐,我们可以定义一个结构体,将数据流中的所有数据定义为结构体的字段,然后再将该数据流指针强转为该结构体的指针,就可以使用成员字段来访问数据流内的数据。
在 GNU C 中,__attribute__((packed))
是一个将结构体声明为无对齐的语法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
struct TestAlign {
char a; // 1 byte
int b; // 4 bytes
char c[2]; // 2 bytes
double d; // 8 bytes
} __attribute__((packed));
int main()
{
// print 15 bytes
printf("size of TestAlign is : %ld bytes\n", sizeof(struct TestAlign));
}
再次运行此代码,我们会发现 TestAlign
结构体的大小变成了 15 字节,不再有多余的对齐字节。
在 MSVC 中也存在类似的语法 #pragma pack(1)
:
1
2
3
4
5
6
7
#pragma pack(1)
struct TestAlign {
char a; // 1 byte
int b; // 4 bytes
char c[2]; // 2 bytes
double d; // 8 bytes
};
零长度数组
当你把一个数组声明为 a[0]
或者 a[]
时,GNU C 将其视为一个零长度数组。
当然,零长度数组并不意味着它真的一个元素都没有,这个语法的实际含义是任意大的数组。也正是因为它是任意大的,因此通常只能在结构体的最后一个字段定义;另一方面,GNU C 禁止在栈上初始化该类结构体的对象,只能在静态域中初始化。
1
2
3
4
5
6
7
8
struct TestZeroLengthArray
{
int x;
int y[];
};
// Can only initialize a zero-length array at static field
struct TestZeroLengthArray arr = {1, {2, 3, 4}};
零长度数组也经常和无对齐语法组合,用来表示数据流末尾的不定长数据,这便是较为经典的标头+数据的结构,很多格式的文件都可以以这种形式进行解析。
动态长度数组
在绝大多数语言包括 C++ 中,想要获得一个动态长度的数组,都需要向堆申请内存。当然,在 C 中我们也可以通过 malloc
函数申请堆内存来完成动态长度数组的申请。然而受限于 C 本身语法的简洁性,没有一个安全的手段可以确保该堆内存在使用完毕之后可以被正确释放。
GNU C 看向了另一个函数 alloca
,该函数不是标准的 C 库函数,甚至不包含在 POSIX.1 中,它的作用是在栈上申请一定大小的空间,并且在发生 longjmp
或 siglongjmp
时自动释放申请的空间。通常来说,不建议直接使用该函数,因为操作栈内存是十分危险且很难控制的。不过 GNU C 将其包装为了动态长度数组语法,让我们可以忽视其中的细节放心地使用它。
该语法并没有什么难理解的地方,和正常使用数组唯一的区别,就是可以使用编译时不可知的变量或表达式作为数组的大小:
1
2
3
4
5
void testVariableArray(unsigned int length)
{
int array[length];
// operate array
}
二进制字面量
C++14 正式引入了使用 0b0101
的形式来书写二进制字面量的语法。而早在 GCC 4.3 版本(2009 年释放),这一特性就已经被引入了 GNU C 扩展语法中,因此我们可以在 GNU C 中书写二进制字面量。