一、什么是零长度数组

零长度数组就是长度为0的数组。

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

类型 数组名[数组元素个数];

int array[10];

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

int len;

scanf("%d", &len);

int array[len];

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

程序示例:

#include <stdio.h>

int main(void)

{

int len;

int i = 0;

printf("please input a length: ");

scanf("%d", &len);

int a[len];

for (i = 0; i < len; i++)

{

a[i] = i + 1;

}

for (i = 0; i < len; i++)

{

printf("a[%d] = %d\n", i, a[i]);

}

return 0;

}

执行结果:

deng@itcast:~/tmp$ gcc 7.c

deng@itcast:~/tmp$ ./a.out

please input a length: 10

a[0] = 1

a[1] = 2

a[2] = 3

a[3] = 4

a[4] = 5

a[5] = 6

a[6] = 7

a[7] = 8

a[8] = 9

a[9] = 10

————————————

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

我们在程序中定义一个零长度数组,你会发现除了 GCC 编译器,在其它编译环境下可能就编译通不过或者有警告信息。零长度数组的定义如下:

#include <stdio.h>

int main(void)

{

//定义长度为零的数组

int a[0];

return 0;

}

————————————

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

程序示例:

#include <stdio.h>

int main(void)

{

int a[0];

printf("sizeof(a): %lu\n", sizeof(a));

return 0;

}

执行结果:

deng@itcast:~/tmp$ gcc 7.c

deng@itcast:~/tmp$ ./a.out

sizeof(a): 0

————————————

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

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

程序示例:

#include <stdio.h>

struct student

{

int id;

char sex;

int a[0];

};

int main(void)

{

int a[0];

printf("sizeof(struct): %lu\n", sizeof(struct student));

return 0;

}

执行结果:

deng@itcast:~/tmp$ gcc 7.c

deng@itcast:~/tmp$ ./a.out

sizeof(struct): 8

————————————

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

二、零长度数组应用

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

程序示例:

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

struct student

{

int id;

char sex;

char a[0];

};

int main(void)

{

struct student *s = NULL;

s = malloc(sizeof(struct student) + 20);

if (NULL == s)

{

printf("malloc failed..\n");

return 1;

}

memset(s, 0, sizeof(struct student) + 20);

s->id = 1;

s->sex = 'M';

strcpy(s->a, "hello world");

printf("id: %d sex: %c a: %s\n", s->id, s->sex, s->a);

free(s);

return 0;

}

执行结果:

deng@itcast:~/tmp$ gcc 7.c

deng@itcast:~/tmp$ ./a.out

id: 1 sex: M a: hello world

————————————

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

通过这种灵活的动态内存申请方式,这个 student结构体表示的一片内存缓冲区,就可以随时调整,可大可小。

这个特性,在一些场合非常有用。比如,现在很多在线视频网站,都支持多种格式的视频播放:普清、高清、超清、1080P、蓝光甚至4K。

如果我们本地程序需要在内存中申请一个 buffer 用来缓存解码后的视频数据,那么,不同的播放格式,需要的 buffer 大小是不一样的。

如果我们按照 4K 的标准去申请内存,那么当播放普清视频时,就用不了这么大的缓冲区,白白浪费内存。

而使用变长结构体,我们就可以根据用户的播放格式设置,灵活地申请不同大小的 buffer,大大节省了内存空间。

三、指针可以代替零长度数组?

大家在各种场合,可能常常会看到这样的字眼:数组名在作为函数参数进行参数传递时,就相当于是一个指针。

在这里,我们千万别被这句话迷惑了:数组名在作为函数参数传递时,确实传递的是一个地址,但数组名绝不是指针,两者不是同一个东西。

数组名用来表征一块连续内存存储空间的地址,而指针是一个变量,编译器要给它单独再分配一个内存空间,用来存放它指向的变量的地址。

我们看下面这个程序。

程序示例:

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

struct s1

{

int len;

int a[0];

};

struct s2

{

int len;

int *a;

};

int main(void)

{

printf("sizeof(s1): %lu\n", sizeof(struct s1));

printf("sizeof(s2): %lu\n", sizeof(struct s2));

return 0;

}

执行结果:

deng@itcast:~/tmp$ ./a.out

sizeof(s1): 4

sizeof(s2): 16

————————————

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

程序示例:

#include <stdio.h>

#include <string.h>

#include <stdlib.h>

int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

int b[0];

int *p = &a[5];

int main(void)

{

return 0;

}

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

deng@itcast:~/tmp$ arm-linux-gcc 7.c -o a.out

deng@itcast:~/tmp$ arm-linux-objdump -D a.out > a.dis

deng@itcast:~/tmp$

————————————

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

00011024 <a>:

11024:  00000001    andeq  r0, r0, r1

11028:  00000002    andeq  r0, r0, r2

1102c:  00000003    andeq  r0, r0, r3

11030:  00000004    andeq  r0, r0, r4

11034:  00000005    andeq  r0, r0, r5

11038:  00000006    andeq  r0, r0, r6

1103c:  00000007    andeq  r0, r0, r7

11040:  00000008    andeq  r0, r0, r8

11044:  00000009    andeq  r0, r0, r9

11048:  00000000    andeq  r0, r0, r0

0001104c <p>:

1104c:  00011038    andeq  r1, r1, r8, lsr r0

Disassembly of section .bss:

————————————

从汇编代码中,可以看到,对于长度为10的数组 a[10],编译器给它分配了从 0x11024–0x11048 一共40个字节的存储空间,但并没有给数组名 a单独分配存储空间,数组名 a仅仅表示这40个连续存储空间的首地址,即数组元素 a[0] 的地址。

而对于 a[0] 这个零长度数组,编译器并没有给它分配存储空间,此时的 a仅仅是一个符号,用来表示内存中的某个地址,我们可以通过查看可执行文件 a.out 的符号表来找到这个地址值。

78: 000082d4 0 FUNC GLOBAL DEFAULT 12 _start

79: 000082bc    0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@@GLIBC_

80: 00000000    0 NOTYPE  WEAK  DEFAULT  UND __gmon_start__

81: 00000000    0 NOTYPE  WEAK  DEFAULT  UND _Jv_RegisterClasses

82: 00008408    0 FUNC    GLOBAL DEFAULT  13 _fini

83: 0001104c    4 OBJECT  GLOBAL DEFAULT  22 p

84: 00008410    4 OBJECT  GLOBAL DEFAULT  14 _IO_stdin_used

85: 0001101c    0 NOTYPE  GLOBAL DEFAULT  22 __data_start

86: 00011050    0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start__

87: 0000841c    0 NOTYPE  GLOBAL DEFAULT  ABS __exidx_end

88: 00011024    40 OBJECT  GLOBAL DEFAULT  22 a

89: 00011020    0 OBJECT  GLOBAL HIDDEN    22 __dso_handle

90: 00011054    0 NOTYPE  GLOBAL DEFAULT  ABS __end__

91: 0000839c  104 FUNC    GLOBAL DEFAULT  12 __libc_csu_init

92: 00011054    0 NOTYPE  GLOBAL DEFAULT  ABS __bss_end__

93: 00011050    0 NOTYPE  GLOBAL DEFAULT  ABS __bss_start

94: 00011054    0 NOTYPE  GLOBAL DEFAULT  ABS _bss_end__

95: 00011054    0 OBJECT  GLOBAL DEFAULT  23 b

96: 00011054    0 NOTYPE  GLOBAL DEFAULT  ABS _end

97: 00011050    0 NOTYPE  GLOBAL DEFAULT  ABS _edata

98: 00008414    0 NOTYPE  GLOBAL DEFAULT  ABS __exidx_start

99: 00008380    28 FUNC    GLOBAL DEFAULT  12 main

100: 00008290    0 FUNC    GLOBAL DEFAULT  10 _init

————————————

从符号表里可以看到,b 的地址为 0x11054,在程序 bss 段的后面。b符号表示的默认地址是一片未使用的内存空间,仅此而已,编译器绝不会单独再给其分配一个内存空间来存储数组名。

看到这里,也许你就明白了:数组名和指针并不是一回事,数组名虽然在作为函数参数时,可以当一个地址使用,但是两者不能划等号。菜刀有时候可以当武器用,但是你不能说菜刀就是武器。

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

—— END ——

看到这里你是不是对C语言又有了一点新的认知呢~

如果你喜欢这篇文章的话,动动小指,点个赞再走~

如果你想学编程,小编推荐一个C语言/C++编程学习基地【点击进入】!

 

一个活跃、高逼格、高层次的编程学习殿堂;编程入门只是顺带,思维的提高才有价值!

涉及:编程入门、游戏编程、网络编程、Windows编程、Linux编程、Qt界面开发、黑客等等....

【C语言高级编程】你见过长度为0的数组吗?管你信不信,看就完了!的更多相关文章

  1. (转)C语言中长度为0的数组

    前面在看Xen的源码时,遇到了一段代码,如下所示: 注意上面最后一行的代码,这里定义了一个长度为的数组,这种用法可以吗?为什么可以使用长度为0 的数组?长度为的数组到底怎么使用?……这篇文章主要针对该 ...

  2. GNU C的定义长度为0的数组

    在标准C和C++中,长度为0的数组是被禁止使用的.不过在GNU C中,存在一个非常奇怪的用法,那就是长度为0的数组,比如Array[0];很多人可能觉得不可思议,长度为0的数组是没有什么意义的,不过在 ...

  3. C/C++ 中长度为0的数组

    参考文献:http://blog.csdn.net/zhaqiwen/article/details/7904515 近日在看项目中的框架代码时,发现了了一个奇特的语法:长度为0的数组例如 uint8 ...

  4. Java中长度为0的数组与null的区别

    有如下两个变量定义,这两种定义有什么区别呢? 1. int[] zero = new int[0]; 2. int[] nil = null; zero是一个长度为0的数组,我们称之为“空数组”,空数 ...

  5. <转>浅析长度为0的数组

    前面在看Xen的源码时,遇到了一段代码,如下所示: 注意上面最后一行的代码,这里定义了一个长度为的数组,这种用法可以吗?为什么可以使用长度为0 的数组?长度为的数组到底怎么使用?……这篇文章主要针对该 ...

  6. c++ new长度为0的数组

    在程序中发现一下代码: int CHmcVideoMgt ::OnGetDiskRunningInfo( SOCKETPARAM *pSocketInfo ,Json:: Value Param ) ...

  7. C++ new 长度为0的数组

    在C++中可以new一个长度为0的数组,通过下面的语句: char* p = new char[0]; 指针p中保存一个非NULL的地址,但是你不能够对p指向的内存进行写入,因为p的内存长度为0, 该 ...

  8. struct中长度为0的数组用途与原理

    前言 在标准C和C++中,长度为0的数组是被禁止使用的.不过在GNUC中,存在一个非常奇怪的用法,那就是长度为0的数组,比如Array[0]; 很多人可能觉得不可思议,长度为0的数组是没有什么意义的, ...

  9. 【嵌入式】C语言高级编程▁▁▁嵌入式C语言入门编程学习!

    ✍  1.C 语言标准 什么是 C 语言标准呢? 我们生活的现实世界,就是由各种标准构成的,正是这些标准,我们的社会才会有条不紊的运行. 比如我们过马路,遵循的交通规则就是一个标准:红灯停,绿灯行,黄 ...

随机推荐

  1. 营销经验总结:如何才能提升h5游戏代入感?

    HTML5游戏拥有即点即玩,无需下载,并具备传播性广的特点,这就使得商家看到了无限商机,如何让产品更加深入人心,是游戏推广最为重要的环节.优秀的代入感才是游戏产品宣传的关键,那么有哪些要素的支撑才能确 ...

  2. python基础 画图

    python 画图 matplotlib 库只保存图片,不显示图片? 在导入库时,添加如下代码 import matplotlib matplotlib.use('Agg')  各种 symbol ? ...

  3. 洛谷 P4072 [SDOI2016]征途 斜率优化DP

    洛谷 P4072 [SDOI2016]征途 斜率优化DP 题目描述 \(Pine\) 开始了从 \(S\) 地到 \(T\) 地的征途. 从\(S\)地到\(T\)地的路可以划分成 \(n\) 段,相 ...

  4. 高可用服务之Keepalived利用脚本实现服务的可用性检测

    上一篇博客主要聊到了keepalived高可用LVS集群的相关配置,回顾请参考https://www.cnblogs.com/qiuhom-1874/p/13659428.html:keepalive ...

  5. 从头看看Tomcat启动Spring容器的原理

    通过带注解Spring Boot可以启动一个web容器,并初始化bean容器.那么Tomcat启动并初始化spring容器的原理是怎样的? Tomcat启动web程序时会创建一对父子容器(图1): 有 ...

  6. [HDU2553]N皇后问题(DFS)

    题目链接 http://acm.hdu.edu.cn/showproblem.php?pid=2553 题意 n<=10,输出N皇后问题的方法数. 题解 可以使用各种方法.这里使用DFS. 使用 ...

  7. [程序员代码面试指南]递归和动态规划-换钱的方法数(DP,完全背包)

    题目描述 给定arr,arr中所有的值都为正数且不重复.每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim,求组成aim的方法数. 解题思路 完全背包 和"求换钱的 ...

  8. 在Linux系统下搭建和配置一个minio文件服务器(二)

    上一篇主要讲述了在linux系统中搭建一个minio文件服务器,那么这一篇则用来整合java代码中使用,我之前自己已经搭建好了一个springboot项目,那么这一篇将详细讲述如何把minio整合进s ...

  9. springboot项目整合rabbitMq涉及消息的发送确认,消息的消费确认机制,延时队列的实现

    1.引入maven依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactI ...

  10. Python其他数据结构collection模块-namtuple defaultdict deque Queue Counter OrderDict arrary

    nametuple 是tuple扩展子类,命名元组,其实本质上简单类对象 from collections import namedtuple info = namedtuple("Info ...