5.1 什么是零长度数组

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

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

int  a[];

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

int len;
int a[len];

也就是说,数组的长度在编译时是未确定的,在程序运行的时候才确定,甚至可以由用户指定大小。比如,我们可以定义一个数组,然后在程序运行时才指定这个数组的大小,还可以通过输入数据来初始化数组,示例代码如下。

int main(void)
{
int len;

printf("input array len:");
scanf("%d",&len);
int a[len];

for(int i=;i<len;i++)
{
printf("a[%d]= ",i);
scanf("%d",&a[i]);
}

printf("a array print:\n");
for(int i=;i<len;i++)
printf("a[%d] = %d\n",i,a[i]);

return ;
}

在这个程序中,我们定义一个变量 len,作为数组的长度。程序运行后,我们可以通过输入指定数组的长度并初始化,最后再将数组的元素打印出来。程序的运行结果如下:

input array len:
a[]=
a[]=
a[]=
a array print:
a[] =
a[] =
a[] =

GNU C 可能觉得变长数组还不过瘾,再来一个实锤:支持零长度数组。这下没有其它编译器比我狠吧!是的,如果我们在程序中定义一个零长度数组,你会发现除了 GCC 编译器,在其它编译环境下可能就编译通不过或者有警告信息。零长度数组的定义如下:

int a[];

零长度数组有一个奇特的地方,就是它不占用内存存储空间。我们使用 sizeof 关键字来查看一下零长度数组在内存中所占存储空间的大小,代码如下。

int buffer[];
int main(void)
{
printf("%d\n", sizeof(buffer));
return ;
}

在这个程序中,我们定义一个零长度数组,使用 sizeof 查看其大小可以看到:零长度数组在内存中不占用空间,大小为0。

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

struct buffer{
int len;
int a[];
};
int main(void)
{
printf("%d\n",sizeof(struct buffer));
return ;
}

零长度数组在结构体中同样不占用存储空间,所以 buffer 结构体的大小为4。

5.2 零长度数组使用示例

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

struct buffer{
int len;
int a[];
};
int main(void)
{
struct buffer *buf;
buf = (struct buffer *)malloc \
(sizeof(struct buffer)+ );

buf->len = ;
strcpy(buf->a, "hello wanglitao!\n");
puts(buf->a);

free(buf);
return ;
}

在这个程序中,我们使用 malloc 申请一片内存,大小为 sizeof(buffer) + 20,即24个字节大小。其中4个字节用来存储结构体指针 buf 指向的结构体类型变量,另外20个字节空间,才是我们真正使用的内存空间。我们可以通过结构体成员 a,直接访问这片内存。

通过这种灵活的动态内存申请方式,这个 buffer 结构体表示的一片内存缓冲区,就可以随时调整,可大可小。这个特性,在一些场合非常有用。比如,现在很多在线视频网站,都支持多种格式的视频播放:普清、高清、超清、1080P、蓝光甚至4K。如果我们本地程序需要在内存中申请一个 buffer 用来缓存解码后的视频数据,那么,不同的播放格式,需要的 buffer 大小是不一样的。如果我们按照 4K 的标准去申请内存,那么当播放普清视频时,就用不了这么大的缓冲区,白白浪费内存。而使用变长结构体,我们就可以根据用户的播放格式设置,灵活地申请不同大小的 buffer,大大节省了内存空间。

5.3 零长度数组在内核中的使用

零长度数组在内核中,一般以变长结构体的形式使用。今天我们就分析一下 Linux 内核中的 USB 驱动。在网卡驱动中,大家可能都比较熟悉一个名字:套接字缓冲区,即 socket buffer,用来传输网络数据包。同样,在 USB 驱动中,也有一个类似的东西,叫 URB,其全名为 USB request block,即 USB 请求块,用来传输 USB 数据包。

struct urb {
struct kref kref;
void *hcpriv;
atomic_t use_count;
atomic_t reject;
int unlinked;

struct list_head urb_list;
struct list_head anchor_list;
struct usb_anchor *anchor;
struct usb_device *dev;
struct usb_host_endpoint *ep;
unsigned int pipe;
unsigned int stream_id;
int status;
unsigned int transfer_flags;
void *transfer_buffer;
dma_addr_t transfer_dma;
struct scatterlist *sg;
int num_mapped_sgs;
int num_sgs;
u32 transfer_buffer_length;
u32 actual_length;
unsigned char *setup_packet;
dma_addr_t setup_dma;
int start_frame;
int number_of_packets;
int interval;

int error_count;
void *context;
usb_complete_t complete;
struct usb_iso_packet_descriptor iso_frame_desc[];
};

在这个结构体内定义了 USB 数据包的传输方向、传输地址、传输大小、传输模式等。这些细节我们不深究,我们只看最后一个成员:

struct usb_iso_packet_descriptor iso_frame_desc[];

在 URB 结构体的最后,定义一个零长度数组,主要用于 USB 的同步传输。USB 有4种传输模式:中断传输、控制传输、批量传输和同步传输。不同的 USB 设备对传输速度、传输数据安全性的要求不同,所采用的传输模式是不同的。USB 摄像头对视频或图像的传输实时性要求较高,对数据的丢帧不是很在意,丢一帧无所谓 ,接着往下传。所以 USB 摄像头采用的是 USB 同步传输模式。

现在淘宝上的 USB 摄像头,打开它的说明书,一般会支持多种分辨率:从16*16到高清720P多种格式。不同分辨率的视频传输,对于一帧图像数据,对 USB 的传输数据包的大小和个数需求是不一样的。那USB到底该如何设计,去适配这种不同大小的数据传输要求,但又不影响 USB 的其它传输模式呢?答案就在结构体内的这个零长度数组上。

当用户设置不同的分辨率传输视频,USB 就需要使用不同大小和个数的数据包来传输一帧视频数据。通过零长度数组构成的这个变长结构体就可以满足这个要求。可以根据一帧图像数据的大小,灵活地去申请内存空间,满足不同大小的数据传输。但这个零长度数组又不占用结构体的存储空间,当 USB 使用其它模式传输时,不受任何影响,完全可以当这个零长度数组不存在。所以,不得不说,这样的设计真是妙!

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

大家在各种场合,可能常常会看到这样的字眼:数组名在作为函数参数进行参数传递时,就相当于是一个指针。在这里,我们千万别被这句话迷惑了:数组名在作为函数参数传递时,确实传递的是一个地址,但数组名绝不是指针,两者不是同一个东西。数组名用来表征一块连续内存存储空间的地址,而指针是一个变量,编译器要给它单独再分配一个内存空间,用来存放它指向的变量的地址。我们看下面这个程序。

struct buffer1{
int len;
int a[];
};
struct buffer2{
int len;
int *a;
};
int main(void)
{
printf("buffer1: %d\n", sizeof(struct buffer1));
printf("buffer2: %d\n", sizeof(struct buffer2));
return ;
}

运行结果分别为:

buffer1:4
buffer2:8

对于一个指针变量,编译器要为这个指针变量单独分配一个存储空间,然后在这个存储空间上存放另一个变量的地址,我们就说这个指针指向这个变量。而数组名,编译器不会再给其分配一个存储空间的,它仅仅是一个符号,跟函数名一样,用来表示一个地址。我们接下来看另一个程序。

//hello.c
int array1[] ={,,,,,,,,};
int array2[];
int *p = &array1[];
int main(void)
{
return ;
}

在这个程序中,我们分别定义一个普通数组、一个零长度数组和一个指针变量。其中这个指针变量 p 的值为 array1[5] 这个数组元素的地址,也就是说指针 p 指向 arraay1[5]。我们接着对这个程序使用 arm 交叉编译器进行编译,并进行反汇编。

$ arm-linux-gnueabi-gcc hello.c -o a.out
$ arm-linux-gnueabi-objdump -D a.out

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

 <array1>:
: andeq r0, r0, r1
: andeq r0, r0, r2
2102c: andeq r0, r0, r3
: andeq r0, r0, r4
: andeq r0, r0, r5
: andeq r0, r0, r6
2103c: andeq r0, r0, r7
: andeq r0, r0, r8
: andeq r0, r0, r9
: andeq r0, r0, r0
0002104c <p>:
2104c: andeq r1, r2, r8, lsr r0
Disassembly of section .bss:

<__bss_start>:
: andeq r0, r0, r0

从汇编代码中,可以看到,对于长度为10的数组 array1[10],编译器给它分配了从 0x21024--0x21048 一共40个字节的存储空间,但并没有给数组名 array1 单独分配存储空间,数组名 array1 仅仅表示这40个连续存储空间的首地址,即数组元素 array1[0] 的地址。而对于 array2[0] 这个零长度数组,编译器并没有给它分配存储空间,此时的 array2 仅仅是一个符号,用来表示内存中的某个地址,我们可以通过查看可执行文件 a.out 的符号表来找到这个地址值。

$ readelf -s  a.out
: OBJECT GLOBAL DEFAULT array1
: NOTYPE GLOBAL DEFAULT _bss_end__
: NOTYPE GLOBAL DEFAULT _edata
: 0002104c OBJECT GLOBAL DEFAULT p
: FUNC GLOBAL DEFAULT _fini
: NOTYPE GLOBAL DEFAULT __bss_end__
: 0002101c NOTYPE GLOBAL DEFAULT __data_start_
: NOTYPE WEAK DEFAULT UND __gmon_start__
: OBJECT GLOBAL HIDDEN __dso_handle
: OBJECT GLOBAL DEFAULT _IO_stdin_used
: 0001041c FUNC GLOBAL DEFAULT __libc_csu_init
: OBJECT GLOBAL DEFAULT array2
: NOTYPE GLOBAL DEFAULT _end
: 000102d8 FUNC GLOBAL DEFAULT _start
: NOTYPE GLOBAL DEFAULT __end__
: NOTYPE GLOBAL DEFAULT __bss_start
: FUNC GLOBAL DEFAULT main
: OBJECT GLOBAL HIDDEN __TMC_END__
: FUNC GLOBAL DEFAULT _init

从符号表里可以看到,array2 的地址为 0x21054,在程序 bss 段的后面。array2 符号表示的默认地址是一片未使用的内存空间,仅此而已,编译器绝不会单独再给其分配一个内存空间来存储数组名。看到这里,也许你就明白了:数组名和指针并不是一回事,数组名虽然在作为函数参数时,可以当一个地址使用,但是两者不能划等号。菜刀有时候可以当武器用,但是你不能说菜刀就是武器。

至于为什么不用指针,很简单。使用指针的话,指针本身也会占用存储空间不说,根据上面的 USB 驱动的案例分析,你会发现,它远远没有零长度数组用得巧妙——不会对结构体定义造成冗余,而且使用起来也很方便。

微信公众号:宅学部落(armlinuxfun)

嵌入式C语言自我修养 05:零长度数组的更多相关文章

  1. 嵌入式C语言自我修养 06:U-boot镜像自拷贝分析:section属性

    6.1 GNU C 的扩展关键字:attribute GNU C 增加一个 __atttribute__ 关键字用来声明一个函数.变量或类型的特殊属性.声明这个特殊属性有什么用呢?主要用途就是指导编译 ...

  2. 嵌入式C语言自我修养 01:Linux 内核中的GNU C语言语法扩展

    1.1 Linux 内核驱动中的奇怪语法 大家在看一些 GNU 开源软件,或者阅读 Linux 内核.驱动源码时会发现,在 Linux 内核源码中,有大量的 C 程序看起来“怪怪的”.说它是C语言吧, ...

  3. 嵌入式C语言自我修养 13:C语言习题测试

    13.1 总结 前面12节的课程,主要针对 Linux 内核中 GNU C 扩展的一些常用 C 语言语法进行了分析.GNU C 的这些扩展语法,主要用来完善 C 语言标准和编译优化.而通过 C 标准的 ...

  4. 嵌入式C语言自我修养 04:Linux 内核第一宏:container_of

    4.1 typeof 关键字 ANSI C 定义了 sizeof 关键字,用来获取一个变量或数据类型在内存中所占的存储字节数.GNU C 扩展了一个关键字 typeof,用来获取一个变量或表达式的类型 ...

  5. 嵌入式C语言自我修养 02:Linux 内核驱动中的指定初始化

    2.1 什么是指定初始化 在标准 C 中,当我们定义并初始化一个数组时,常用方法如下: ] = {,,,,,,,,}; 按照这种固定的顺序,我们可以依次给 a[0] 和 a[8] 赋值.因为没有对 a ...

  6. 嵌入式C语言自我修养 03:宏构造利器:语句表达式

    3.1 基础复习:表达式.语句和代码块 表达式 表达式和语句是 C 语言中的基础概念.什么是表达式呢?表达式就是由一系列操作符和操作数构成的式子.操作符可以是 C 语言标准规定的各种算术运算符.逻辑运 ...

  7. 嵌入式C语言自我修养 12:有一种宏,叫可变参数宏

    12.1 什么是可变参数宏 在上面的教程中,我们学会了变参函数的定义和使用,基本套路就是使用 va_list.va_start.va_end 等宏,去解析那些可变参数列表我们找到这些参数的存储地址后, ...

  8. 嵌入式C语言自我修养 11:有一种函数,叫内建函数

    11.1 什么是内建函数 内建函数,顾名思义,就是编译器内部实现的函数.这些函数跟关键字一样,可以直接使用,无须像标准库函数那样,要 #include 对应的头文件才能使用. 内建函数的函数命名,通常 ...

  9. 嵌入式C语言自我修养 10:内联函数探究

    10.1 属性声明:noinline & always_inline 这一节,接着讲 __atttribute__ 属性声明,__atttribute__ 可以说是 GNU C 最大的特色.我 ...

随机推荐

  1. 宽字符、多字节、unicode、utf-8、gbk编码转化

    今天遇到一个编码的问题,困惑了我很长时间,所以就简要的的了解了一下常用的编码类型. 我们最常见的是assic编码,它是一种单字节编码,对多容纳256个字符. 我们在编程的时候经常遇到unicode,u ...

  2. jave 逻辑运算 vs 位运算 + Python 逻辑运算 vs 位运算

    JAVA中&&和&.||和|(短路与和逻辑与.短路或和逻辑或)的区别 博客分类: 面试题目 Java.netBlog  转自 :http://blog.csdn.net/web ...

  3. linq join一些忘记的操作

  4. 从Objective-C到Swift,你必须会的(三)init的顺序

    Objective-C的构造函数吧,就最后return一个self.里头你要初始化了什么都可以.在Swift的init函数里把super.init放在前面,然后再初始化你代码里的东西就会报错了. 所以 ...

  5. .NET基础 (16)事件

    事件1 请解释事件的基本使用方法2 事件和委托有何联系3 如何设计一个带有很多事件的类型4 用代码表示如下情景:猫叫.老鼠逃跑.主人惊醒 事件1 请解释事件的基本使用方法 事件时一种使对象或类能够提供 ...

  6. 23 DesignPatterns学习笔记:C++语言实现 --- 1.1 Factory

    23 DesignPatterns学习笔记:C++语言实现 --- 1.1 Factory 2016-07-18 13:03:43 模式理解

  7. Modelsim10.2c使用教程(一个完整工程的仿真)

    之前玩过Altera的板子,不不, 现在应该叫intel PSG.在QuartusII13.0上老喜欢用modelsim_ae做仿真,小工程用起来也方便,但是我做IIC配置摄像头的时序仿真时,就显得有 ...

  8. ASP.NET MVC Core的TagHelper (高级特性)

    这篇博文ASP.NET MVC Core的TagHelper(基础篇)介绍了TagHelper的基本概念和创建自定义TagHelper的方式,接着继续介绍一些新的看起来比较高级的特性.(示例代码紧接着 ...

  9. C#基础入门 五

    C#基础入门 五 递归 递归调用:一个方法直接或间接地调用了它本身,就称为方法的递归调用. 递归方法:在方法体内调用该方法本身. 递归示例 public long Fib(int n) { if(n= ...

  10. task4: 结对编程-词频统计[修改版]

    问题描述: 读取一个文件,统计其中单词出现次数,并按从高到低的顺序显示,相同顺序的字典序排列. 思路: 基于上次的程序用正则提取出文本里的单词,然后利用字典计数(先get,为null则置1,不为nul ...