零长度数组

news/2024/11/29 18:30:30/

什么是零长度数组

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

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

int  a[10];

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

int len;
int a[len];

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

int main(void)
{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("a array print:\n");for(int i=0;i<len;i++)printf("a[%d] = %d\n",i,a[i]);return 0;
}

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

input array len:3
a[0]= 6
a[1]= 7
a[2]= 8
a  array print:
a[0] = 6
a[1] = 7
a[2] = 8

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

int a[0];

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

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

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

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

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

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

零长度数组使用示例

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

struct buffer{int len;int a[0];
};
int main(void)
{struct buffer *buf;buf = (struct buffer *)malloc \(sizeof(struct buffer)+ 20);buf->len = 20;strcpy(buf->a, "hello wanglitao!\n");puts(buf->a);free(buf);  return 0;
}

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

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

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

零长度数组在内核中,一般以变长结构体的形式使用。今天我们就分析一下 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[0];
};

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

struct usb_iso_packet_descriptor iso_frame_desc[0];

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

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

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

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

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

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

运行结果分别为:

buffer1:4
buffer2:8

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

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

在这个程序中,我们分别定义一个普通数组、一个零长度数组和一个指针变量。其中这个指针变量 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 的汇编代码。

00021024 <array1>:21024:    00000001    andeq   r0, r0, r121028:    00000002    andeq   r0, r0, r22102c:    00000003    andeq   r0, r0, r321030:    00000004    andeq   r0, r0, r421034:    00000005    andeq   r0, r0, r521038:    00000006    andeq   r0, r0, r62103c:    00000007    andeq   r0, r0, r721040:    00000008    andeq   r0, r0, r821044:    00000009    andeq   r0, r0, r921048:    00000000    andeq   r0, r0, r0
0002104c <p>:2104c:    00021038    andeq   r1, r2, r8, lsr r0
Disassembly of section .bss:00021050 <__bss_start>:21050:    00000000    andeq   r0, r0, r0

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

$ readelf -s  a.out88: 00021024    40 OBJECT  GLOBAL DEFAULT   23 array189: 00021054     0 NOTYPE  GLOBAL DEFAULT   24 _bss_end__90: 00021050     0 NOTYPE  GLOBAL DEFAULT   23 _edata91: 0002104c     4 OBJECT  GLOBAL DEFAULT   23 p92: 00010480     0 FUNC    GLOBAL DEFAULT   14 _fini93: 00021054     0 NOTYPE  GLOBAL DEFAULT   24 __bss_end__94: 0002101c     0 NOTYPE  GLOBAL DEFAULT   23 __data_start_96: 00000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__97: 00021020     0 OBJECT  GLOBAL HIDDEN    23 __dso_handle98: 00010488     4 OBJECT  GLOBAL DEFAULT   15 _IO_stdin_used99: 0001041c    96 FUNC    GLOBAL DEFAULT   13 __libc_csu_init100: 00021054     0 OBJECT  GLOBAL DEFAULT   24 array2101: 00021054     0 NOTYPE  GLOBAL DEFAULT   24 _end102: 000102d8     0 FUNC    GLOBAL DEFAULT   13 _start103: 00021054     0 NOTYPE  GLOBAL DEFAULT   24 __end__104: 00021050     0 NOTYPE  GLOBAL DEFAULT   24 __bss_start105: 00010400    28 FUNC    GLOBAL DEFAULT   13 main107: 00021050     0 OBJECT  GLOBAL HIDDEN    23 __TMC_END__110: 00010294     0 FUNC    GLOBAL DEFAULT   11 _init

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

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


http://www.ppmy.cn/news/494603.html

相关文章

mysql smallint 长度_mysql中int、bigint、smallint 和 tinyint长度范围和区别

最近使用mySQL数据库的时候遇到了多种数字的类型,主要有int,bigint,smallint和tinyint。其中比较迷惑的是int和smallint的差别。今天就在网上仔细找了找,找到如下内容,留档做个总结: 使用整数数据的精确数字数据类型。 bigint 从 -2^63 (-9223372036854775808) 到 2^63-1 (…

Go 知识点(15)— 切片长度和容量

1. 切片声明时默认的长度和容量 1.1 切片声明时不指定容量 切片在使用 make 声明时&#xff0c;如果没有显式的说明切片的容量&#xff0c;那么默认容量和切片的长度保持一致。 func main() {s1 : make([]int, 3)fmt.Println("s1 length: ", len(s1)) // s1 len…

list的长度怎么获取python_python获取list的长度

python内嵌函数列表_IT/计算机_专业资料。内建函数 本节介绍的函数在解释... python基础教程08.1Lists_互联网_IT/计算机_专业资料。python基础教程 列表? 车万翔 ? ? 哈尔滨工业大学 ? 一个例子 ? v??读取三个数字,并计算平均数...... 请用Python 写一个获取用户输入数…

cad统计面积长度插件vlx_cad计算总长度插件

手机评站网今天精心准备的是《cad计算总长度插件》,下面是详解! 跪求一个CAD自动求和插件,将拾取的N条线段的长度... 已上传,亲测可用 启动CAD,点工具-加载应用程序 加载以后,输入命令n,框选所要统计的线条,总长度会在命令行显示 你好!我已经下载了CAD长度统计插件也…

oracle数据库的double类型长度,数据库double类型长度

DOUBLE(size,d) 带有浮动小数点的大数字。在括号中规定最大位数。在 d 参数中规定小数点右侧的 2页 如有你有帮助,请购买下载,谢谢! 最大位数。 DECIMAL(size,d) 作为字符串存储的 DOUBLE 类型,允许固定的小数点。 * 这些整数...... 耕地类型 耕地坡度级 土地总面积 (一二级地类…

gdb 打印字符串长度,gdb打印字符串长度设置

在使用gdb调试过程中,经常需要查看变量的值,最常用的方法: (gdb) print {变量名} 一般情况下,打印并不存在问题。但当一个字符串比较长时,打印出来的内容不完整,内容的最后为"..." 例如: (gdb) p (char*)0x23b744a98 $19 = 0x23b744a98 "obbs:S:1.3:{[{1…

Qt之 读取 /dev/input/event*设备 并解析

文章目录 项目背景输入设备应用编程什么是输入设备input子系统读取数据的流程应用程序如何解析数据struct input_event结构体事件之间的分隔、同步 Qt 读取gsensor数据实现 项目背景 某项目中&#xff0c;需要实现屏幕的自动翻转&#xff0c;即通过读取gsensor实时上报的坐标数…

10GHz带宽/USB3.1芯片,AW3410S高速切换开关系列方案及产品介绍

之前发过关于AW3410S这颗芯片的应用介绍&#xff0c;具体请参看这份帖子&#xff1a;AW3410S应用分享&#xff0c;10GHz / USB3.1模拟数据开关&#xff0c;多路复用/分离器 今天来详细介绍一下这颗10GHz带宽/USB3.1的芯片AW3410S目前已经产量的一系列方案及产品。 AW3410S系列…