GNU 扩展的数组语法
字数: 0 字 阅读时间: 0 分钟
零长数组
在标准 C 中,定义数组必须使用正整数,不允许使用 0。但是 GNU 允许我们定义零长数组。那么长度为 0 的数组有什么用呢?答案是可变长的对象。一般情况下零长数组搭配结构体组成变长结构体来使用,我们来看一个代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
int len;
char str[0];
} string_t;
string_t *new_string(int len) {
string_t *s = malloc(sizeof(string_t) + (len * sizeof(char)));
s->len = len;
return s;
}
void delete_string(string_t *s) {
free(s);
}
void print_string(string_t *s) {
puts(s->str);
}
void set_string(string_t *s, char *str) {
strcpy(s->str, str);
}
int main(void) {
string_t *str = new_string(12);
set_string(str, "Hello World");
print_string(str);
delete_string(str);
str = new_string(9);
set_string(str, "So cool!");
print_string(str);
delete_string(str);
return 0;
}
运行结果:
ubuntu@hi3798mv100:~/C-Learn$ gcc main.c -o main && ./main
Hello World
So cool!
ubuntu@hi3798mv100:~/C-Learn$
是不是有点 C++ 的 new
的那味了?当然这不是我们的正题。我们使用了一个叫做 string
的结构体,然后我们用 malloc
动态的调整其中的 str
成员大小,就像一个 String
类一样,可以构造,销毁。
这里注意用 malloc
分配的空间是结构体的大小 + 缓冲区的大小。
那你可能会说,这个结构体跟我用下面的结构体有什么区别吗?
struct foo{
int len;
char* str;
};
用起来确实没有什么区别,但是这个结构体有两个成员, char*
在 32 位机上的占用空间是 4 字节,也就是说这个结构体总共占用 8 字节空间,而如果用零长数组,不仅可以少 4 字节空间,最重要的是你可以少一次分配内存。如果这里的结构体用的也是动态创建的方式,那么就需要分配两次内存:结构体一次,结构体里的 str
又是一次。这样无形中会增加内存碎片,如果疏忽,少释放了一次内存,会造成内存泄漏。 new
和 delete
函数就会像下面那样:
typedef struct {
int len;
char *str;
} string_t;
string_t *new_string(int len) {
string_t *s = malloc(sizeof(string_t));
s->str = malloc(sizeof(char) * len);
s->len = len;
return s;
}
void delete_string(string_t *s) {
/* 少一个 free 你试试 */
free(s->str);
free(s);
}
这里用一张简单的图来表示这两者的区别:
那么零长数组占用空间真的是 0 吗?我们用代码来看一下:
#include <stdio.h>
int array[0];
typedef struct {
int len;
char str[0];
} string_t;
int main(void) {
printf("size of array: %zu\n", sizeof(array));
printf("length of array: %zu\n", sizeof(array) / sizeof(array[0]));
printf("size of string_t: %zu\n", sizeof(string_t));
return 0;
}
运行结果:
ubuntu@hi3798mv100:~/C-Learn$ gcc main.c -o main && ./main
size of array: 0
length of array: 0
size of string_t: 4
ubuntu@hi3798mv100:~/C-Learn$
可以看到,如果我们单独定义个零长数组,那么编译器给他分配的空间是 0,也就是不分配空间。所以相较于指针,会更省空间,使用也会更方便。
需要注意,零长数组成员必须放在结构体的最后一个成员,因为如果按照上面的方法分配,最后一个成员就相当于是数组的首地址,它后面紧挨着的就是数组,如果不放到最后,会造成数组的内容写到结构体其他成员的位置,造成不可预料的后果。
至于有什么用,上面的参考链接已经给了,我想我解释的也算清楚。这里就不多赘述。
变长数组
定义数组长度必须使用常量或者常量表达式,但是 GNU 允许我们使用变量来定义数组长度,这种数组就叫做变长数组 (Variable-length array, VLA)。
其实从 C99 就引入了 VLA,允许我们使用变量定义数组长度,但是这容易造成栈溢出,因此不是所有编译器都支持 VLA。万一定义了一个超长数组,如果没有错误处理,就会栈溢出。
#include <stdio.h>
int main(void) {
int length;
puts("input length: ");
scanf("%d", &length);
int array[length];
printf("length of array: %zu\n", sizeof(array) / sizeof(array[0]));
return 0;
}
Ubuntu 20.04 下 GCC 9.4.0 编译运行结果:
ubuntu@hi3798mv100:~/C-Learn$ gcc main.c -o main
ubuntu@hi3798mv100:~/C-Learn$ ./main
input length:
10
length of array: 10
ubuntu@hi3798mv100:~/C-Learn$
Ubuntu 20.04 下 GCC 9.4.0 使用 C89/C99 标准编译,不使用 GNU 扩展:
ubuntu@hi3798mv100:~/C-Learn$ gcc -std=c89 --pedantic main.c -o main
main.c: In function ‘main’:
main.c:7:5: warning: ISO C90 forbids variable length array ‘array’ [-Wvla]
7 | int array[length];
| ^~~
main.c:7:5: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement]
main.c:8:12: warning: ISO C90 does not support the ‘z’ gnu_printf length modifier [-Wformat=]
8 | printf("length of array: %zu\n", sizeof(array) / sizeof(array[0]));
| ^~~~~~~~~~~~~~~~~~~~~~~~
ubuntu@hi3798mv100:~/C-Learn$ gcc -std=c99 --pedantic main.c -o main
ubuntu@hi3798mv100:~/C-Learn$
我们看到,使用 C89 编译抛出了三个警告,第一个警告说 ISO C90 不允许使用变量定义数组长度;第二个警告说 ISO C90 不允许混合代码与声明,也就是说必须将变量声明放在语句前面;第三个是不支持 %z
这种 printf
格式。而使用 C99 编译,没有任何警告,这是因为 C99 已经支持了上面所说的三点。
Windows 下 Clang 18.1.6 编译结果:
Deadline039@LAPTOP-83FQRJF MINGW64 /e/C-Learn
$ clang main.c -o main.exe -D_CRT_SECURE_NO_WARNINGS
Deadline039@LAPTOP-83FQRJF MINGW64 /e/C-Learn
$ ./main.exe
input length:
10
length of array: 10
Deadline039@LAPTOP-83FQRJF MINGW64 /e/C-Learn
$
Windows 下 MSVC 编译结果:
Rebuild started at 5:18 PM...
1>------ Rebuild All started: Project: Project1, Configuration: Debug x64 ------
1>Source1.c
1>E:\Users\Deadline039\Desktop\VS_C\Source1.c(7,15): error C2057: expected constant expression
1>E:\Users\Deadline039\Desktop\VS_C\Source1.c(7,15): error C2466: cannot allocate an array of constant size 0
1>E:\Users\Deadline039\Desktop\VS_C\Source1.c(7,9): error C2133: 'array': unknown size
1>E:\Users\Deadline039\Desktop\VS_C\Source1.c(8,38): warning C4034: sizeof returns 0
1>Done building project "Project1.vcxproj" -- FAILED.
========== Rebuild All: 0 succeeded, 1 failed, 0 skipped ==========
========== Rebuild completed at 5:18 PM and took 01.145 seconds ==========
我们看到,不论是 GCC 还是 Clang,都允许我们使用变量来定义数组长度。但是 MSVC 不允许我们使用变量定义数组长度。
那么这个变长数组定义的位置在哪里呢?用下面的代码测试一下:
#include <stdio.h>
int main(void) {
int length;
puts("input length: ");
scanf("%d", &length);
int array[length];
printf("length of array: %zu\n", sizeof(array) / sizeof(array[0]));
printf("Address of length: %p, array: %p\n", &length, array);
return 0;
}
运行结果
ubuntu@hi3798mv100:~/C-Learn$ gcc main.c -o main && ./main
input length:
2
length of array: 2
Address of length: 0xbea6d4f0, array: 0xbea6d4e8
ubuntu@hi3798mv100:~/C-Learn$
我们查看一下内存空间:
0xbe6d4e4 | |
+--------------+
0xbe6d4e8 | array[0] |
+--------------+
0xbe6d4ec | array[1] |
+--------------+
0xbe6d4f0 | length |
+--------------+
0xbe6d4f4 | |
从上图可以看出,数组是分配在栈上的,而不是使用堆里。因此变长数组可以由编译器来进行分配与释放,相比与 malloc
也会较为安全,不会有内存泄漏的问题。
顺带一提,全局数组不可以用变量来定义长度,哪怕是用 const
修饰的变量也不可以:
#include <stdio.h>
const int length = 10;
int array[length];
int main(void) {
printf("length of array: %zu\n", sizeof(array) / sizeof(array[0]));
return 0;
}
编译结果:
ubuntu@hi3798mv100:~/C-Learn$ gcc main.c -o main && ./main
main.c:4:5: error: variably modified ‘array’ at file scope
4 | int array[length];
| ^~~~~
ubuntu@hi3798mv100:~/C-Learn$
虽然 VLA 用起来比 malloc
会更加方便,但是 Linus 有云:
AND USING VLA'S IS ACTIVELY STUPID! It generates much more code, and much slower code (and more fragile code), than just using a fixed key size would have done.
Linus
数组初始化
GNU 允许我们给一定范围的成员给初值:
#include <stdio.h>
int array[] = {[0 ... 2] = 1, [4 ... 6] = 2};
int main(void) {
printf("length of array: %d\n", sizeof(array) / sizeof(array[0]));
for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) {
printf("array[%d] = %d\t", i, array[i]);
}
printf("\n");
return 0;
}
运行结果:
length of array: 7
array[0] = 1 array[1] = 1 array[2] = 1 array[3] = 0 array[4] = 2 array[5] = 2 array[6] = 2
需要注意,三个点前后必须有空格,否则会导致编译错误。