GNU C 扩展语法:零初始化数组

简介: 零长度数组、变长度数组都是 GNU C 编译器支持的数组类型。今天我们来回顾一下零长度数组。

1. 什么是零长度数组?


零长度数组就是长度为0的数组。ANSI C 标准规定,数组的长度必须是一个常数,即数组的长度在编译时侯是确认的。


在 ANSI C 中定义一个数组方法如下:


int a[10];


C99 标准规定:可以定义一个变长数组。


int len;
int a[len];


也就是说数组在编译时是未确定的,在程序运行时才确定,甚至可以由用户指定大小。


#include <stdio.h>
int main()
{
    int len;
    printf("input array len:");
    scanf("%d", &len);
    int a[len];
    for (int i = 0; i < len; i++) {
        printf("a[%d] = ", i); 
        scanf("%d", &a[i]);
    }
    printf("array print:\n");
    for (int i = 0; i < len; i++) {
        printf("a[%d] = %d\n", i, a[i]);
    }
    return 0;
}


GNU C 可能觉得变长数组还不过瘾,又扩展支持【零长度数组】。


零长度数组定义如下:


int a[0];


零长度数组有一个奇特的地方,就是它不占用内存存储空间。我们使用 sizeof 关键字查看零长度数组的大小,会发现其长度为0。


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


#include <stdio.h>
struct buffer {
    int len;
    int a[0];
};
int main()
{
    printf("%d\n",sizeof(struct buffer));
    return 0;
}


零长度数组在结构体中同样不占用存储空间。


2. 使用示例


零长度数组经常以变长结构体的形式,在某些特殊的应用场合使用。在一个变长结构体中,零长度数组不占用结构体的存储空间,但是我们可以通过使用结构体的成员 a 去访问内存,非常方便。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct buffer {
    int len;
    int a[0];
};
int main()
{
    struct buffer *buf = NULL;
    buf = (struct buffer *)malloc(sizeof(struct buffer) + 20);
    if (!buf) {
    printf("ERROR(%s): malloc buf error!\n", __func__);
        return -1; 
    }
    printf("%d\n",sizeof(struct buffer));
    buf->len = 20; 
    strcpy(buf->a, "hello world!\n");
    puts(buf->a);
    free(buf);
    return 0;
}


在这个程序中,我们使用malloc 申请一片内存,大小为 sizeof(buffer) + 20, 即24 字节。其中4字节用来表示内存的长度,剩下的20字节才是我们真正可以使用的内存空间。我们可以通过结构体成员a 来直接访问这边内存。


3. 内核中的零长度数组


零长度数组在内核中一般以变长结构的形式出现。在 USB 驱动中,有一个东西叫做URB,其全称为 USB Request Block, 即 US 数据包B


struct urb {
 /* private: usb core and host controller only fields in the urb */
 struct kref kref;  /* reference count of the URB */
 void *hcpriv;   /* private data for host controller */
 atomic_t use_count;  /* concurrent submissions counter */
 atomic_t reject;  /* submissions will fail */
 int unlinked;   /* unlink error code */
 /* public: documented fields in the urb that can be used by drivers */
 struct list_head urb_list; /* list head for use by the urb's
      * current owner */
 struct list_head anchor_list; /* the URB may be anchored */
 struct usb_anchor *anchor;
 struct usb_device *dev;  /* (in) pointer to associated device */
 struct usb_host_endpoint *ep; /* (internal) pointer to endpoint */
 unsigned int pipe;  /* (in) pipe information */
 unsigned int stream_id;  /* (in) stream ID */
 int status;   /* (return) non-ISO status */
 unsigned int transfer_flags; /* (in) URB_SHORT_NOT_OK | ...*/
 void *transfer_buffer;  /* (in) associated data buffer */
 dma_addr_t transfer_dma; /* (in) dma addr for transfer_buffer */
 struct scatterlist *sg;  /* (in) scatter gather buffer list */
 int num_mapped_sgs;  /* (internal) mapped sg entries */
 int num_sgs;   /* (in) number of entries in the sg list */
 u32 transfer_buffer_length; /* (in) data buffer length */
 u32 actual_length;  /* (return) actual transfer length */
 unsigned char *setup_packet; /* (in) setup packet (control only) */
 dma_addr_t setup_dma;  /* (in) dma addr for setup_packet */
 int start_frame;  /* (modify) start frame (ISO) */
 int number_of_packets;  /* (in) number of ISO packets */
 int interval;   /* (modify) transfer interval
      * (INT/ISO) */
 int error_count;  /* (return) number of ISO errors */
 void *context;   /* (in) context for completion */
 usb_complete_t complete; /* (in) completion routine */
 struct usb_iso_packet_descriptor iso_frame_desc[0];/* (in) ISO ONLY */
};


我们看最后一个成员:


struct usb_iso_packet_descriptor iso_frame_desc[0];


在URB结构体最后定义了一个零长度数组,主要用于USB的同步传输。USB 摄像头对视频或图像的传输实时性要求比较高,对数据的丢帧不是很在意,所以USB 摄像头采用的是USB同步传输模式。


USB 摄像头一般会支持多种分辨率,不同的分辨率视频传输,一帧图像的数据大小是不一样的,对于USB 传输的数据包大小和个数需求也不一样。那么USB 到底该如何设计,才能在不影响 USB 其他传输模式的前提下,适配不同大小的数组传输需求呢?


答案就在这个结构体内的零长度数组上。当用户设置不同分辨率的视频格式时,USB 就使用不同大小和个数的数据包来传输一帧视频数据。通过零长度数组构成这个变长结构体可以满足这个需求。USB 驱动可以根据一帧图像数据大小,灵活的申请内存空间,以满足不同大小的数据传输。


而且这个零长度数组不占用结构体的存储空间。当USB 使用其他模式传输时,不受任何影响,完全可以当这个零长度数组不存在


4. 指针与零长度数组


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


在各种场合,我们可能经常会看到:数组名在作为函数参数进行传递时,就相当于一个指针。


注意,数组名作为参数传递时,传递的确实是一个地址,但是数组名绝不是指针。数组名用来表征一块连续内存空间的地址,而指针是一个变量,编译器要给它单独分配一个空间,用来存放它指向的变量的地址。


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));
}


输出结果:


buffer1: 4
buffer2: 8


我们在看另一个程序,定义一个普通数组、零长度数组和一个指针变量。


int array1[10] = {1,2,3,4,5,6,7,8,9};
int array2[0];
int *p = array1[5];
int main()
{
    return 0;
}


MIPS 下编译与反汇编:


mips-linux-gnu-gcc -m32 gnu2.c
mips-linux-gnu-objdump -D a.out > a.dump


从反汇编生成的汇编代码中,我们找到array1 和指针变量p的汇编代码:


004107c0 <array1>:                      
  4107c0:   00000001    movf    zero,zero,$fcc0
  4107c4:   00000002    srl zero,zero,0x0
  4107c8:   00000003    sra zero,zero,0x0
  4107cc:   00000004    sllv    zero,zero,zero
  4107d0:   00000005    lsa zero,zero,zero,0x1
  4107d4:   00000006    srlv    zero,zero,zero
  4107d8:   00000007    srav    zero,zero,zero
  4107dc:   00000008    jr  zero
  4107e0:   00000009    jalr    zero,zero
  4107e4:   00000000    nop 
004107e8 <p>:
  4107e8:   004107d4    0x4107d4
  4107ec:   00000000    nop 
Disassembly of section .rld_map:


其中,对于长度为10的数组array1[10],编译器给它分配了从0x4107c0~0x4107e4共40字节的存储空间,但是并没有给数组名array1单独分配存储空间,数组名array1 仅仅表示这40个连续空间的首地址,即array1[0]的地址。


对于指针变量,编译器给它分配了0x4107e8 这个存储空间,在这个存储空间上存储的是数组元素array1[5]的地址:0x4107d4。


对于array[2]这个零长度数组,编译器并没有给它分配存储空间,此时的array2仅仅是一个符号。


我们可以通过readelf(readelf -s) 查看a.out的符号表,来找到array2的地址。


image.png


从符号表可以看到,array2 的地址为0x0041081c,在 bss 段 后面。array2符号表示的默认地址是一片未使用的内存空间,仅此而已,编译器并不会单独为其分配空间。


至此大家应该知道了,数组名和指针并不是一回事儿,至于内核中为何不使用指针代替零长度数组,我想大家应该也明晰了。


5. 总结


本文主要介绍GNU C 扩展语法,零长度数组的使用,通过Linux 内核中的设计来体会零长度数组设计的妙处。


同时通过,反汇编来分析指针、零长度数组、以及数组名之间的关系。

相关文章
|
编译器 Linux 程序员
GNU C 扩展语法:关键字__attribute__ 使用
GNU C 扩展语法:关键字__attribute__ 使用
510 0
|
存储 安全 编译器
GNU C 扩展语法:指定初始化与语句表达式
GNU C 扩展语法:指定初始化与语句表达式
335 0
|
API C语言 开发者
|
7月前
|
编译器 Linux 开发工具
|
4月前
|
前端开发 C语言
gcc动态库升级
gcc动态库升级
|
2月前
|
编译器 Linux C语言
gcc的编译过程
GCC(GNU Compiler Collection)的编译过程主要包括四个阶段:预处理、编译、汇编和链接。预处理展开宏定义,编译将代码转换为汇编语言,汇编生成目标文件,链接将目标文件与库文件合并成可执行文件。
81 11
|
4月前
|
编译器 开发工具 C语言
Gcc 链接文件
Gcc 链接文件
41 4