首页 > 其他分享 >C语言0长度数组

C语言0长度数组

时间:2023-11-14 10:06:52浏览次数:34  
标签:struct buffer C语言 char LENGTH 数组 长度 data

一、零长度数组概念

众所周知, GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展.

多数情况下, 其应用在变长数组中, 其定义如下



struct Packet
{
    int state;
    int len;
    char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持
};



首先对 0长度数组, 也叫柔性数组 做一个解释 :
    用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体
    用法 : 在一个结构体的最后, 申明一个长度为0的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量

对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量!

二、0长度数组的用途

我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个len字段和data字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路

    定长数据缓冲区, 设置一个足够大小 MAX_LENGTH 的数据缓冲区
    设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间.

我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟, 释放和访问.

2.1 定长包(开辟空间, 释放, 访问)

比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度 MAX_LENGTH 为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费.

数据结构定义



//  定长缓冲区
struct max_buffer
{
    int     len;
    char    data[MAX_LENGTH];
};



数据结构大小

考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char) * MAX_LENGTH

由于考虑到数据的溢出, 变长数据包中的 data 数组长度一般会设置得足够长足以容纳最大的数据, 因此 max_buffer 中的 data 数组很多情况下都没有填满数据, 因此造成了浪费

数据包的构造

假如我们要发送 CURR_LENGTH = 1024 个字节, 我们如何构造这个数据包呢:

一般来说, 我们会返回一个指向缓冲区数据结构 max_buffer 的指针.



///  开辟
if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
{
   mbuffer->len = CURR_LENGTH;
   memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
   printf("%d, %s\n", mbuffer->len, mbuffer->data);
}



访问

这段内存要分两部分使用

前部分 4 个字节 p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包,那么这个包到底有多长呢,因此,我们就必须通过一个变量来表明这个数据包的长度,这就是len的作用),

而紧接其后的内存是真正的数据部分, 通过 p->data, 最后, 进行一个 memcpy() 内存拷贝, 把要发送的数据填入到这段内存当中

释放

那么当使用完毕释放数据的空间的时候, 直接释放就可以了



/// 销毁
free(mbuffer);
mbuffer = NULL;



小结

    使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间 MAX_LENGTH, 而实际使用过程中, 达到 MAX_LENGTH 长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的.

    但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作

2.2 指针数据包(开辟空间, 释放, 访问)

如果你将上面的长度为 MAX_LENGTH 的定长数组换为指针, 每次使用时动态的开辟 CURR_LENGTH 大小的空间, 那么就避免造成 MAX_LENGTH - CURR_LENGTH 空间的浪费, 只浪费了一个指针域的空间.

 数据包定义



struct point_buffer
{
    int     len;
    char    *data;
};



数据结构大小

考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char *)

空间分配

但是也造成了使用在分配内存时,需采用两步



// =====================
// 指针数组  占用-开辟-销毁
// =====================

///  占用
printf("the length of struct test3:%d\n",sizeof(struct point_buffer));

///  开辟
if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
{
    pbuffer->len = CURR_LENGTH;
    if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
    {
        memcpy(pbuffer->data, "Hello World", CURR_LENGTH);
        printf("%d, %s\n", pbuffer->len, pbuffer->data);
    }
}



    首先,需为结构体分配一块内存空间;
    其次,再为结构体中的成员变量分配内存空间.

这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它.

释放

相反, 释放时也是一样的.



/// 销毁
free(pbuffer->data);
free(pbuffer);
pbuffer = NULL;



小结
    使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH 长度的数组, 不会造成空间的大量浪费.
    但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向 struct point_buffer 的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏

2.3 变长数据缓冲区(开辟空间, 释放, 访问)

定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢?

GNU C 的0长度数组, 也叫变长数组, 柔性数组就是这样一个扩展. 对于0长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等:

数据结构定义



//  0长度数组
struct zero_buffer
{
    int     len;
    char    data[0];
};



数据结构大小

这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0]; 只是个数组名, 是不占用存储空间的,

即 sizeof(struct zero_buffer) = sizeof(int)

开辟空间

那么我们使用的时候, 只需要开辟一次空间即可



///  开辟
if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
{
    zbuffer->len = CURR_LENGTH;
    memcpy(zbuffer->data, "Hello World", CURR_LENGTH);
    printf("%d, %s\n", zbuffer->len, zbuffer->data);
}



 释放空间

释放空间也是一样的, 一次释放即可



///  销毁
free(zbuffer);
zbuffer = NULL;



 

对齐 对零字节数组的影响



struct zero_buffer
{
    int  xxx;   //4字节
    char yyy;   //1字节
    char data[0];   //零字节数组
}



零字节数组是不占struct空间的,它针向所有结构体内容之后,但是这个之后是哪里呢?答案是紧挨着char yyy 后面。

一般情况下char yyy就是struct的最后一项,但是当结构体没有被明确要求对齐的时候会出现填充的情况,即编译器为了对齐结构体往里面填无效的内容。
此时如下:



struct zero_buffer
{
    int  xxx;    //4字节
    char yyy;    //1字节
    char 填充;    //char data[0];//零字节数组
    char 填充;
    char 填充;
}



很遗憾现在data[0]指向的是填充部位。而我们期望的是它要指向zero_buffer之后。

这样会造成如下错误:
通过sizeof()



zero_buffer* p = new char[sizeof(zero_buffer)+3];//申请三个字节紧跟着zero_buffer结构体
memset((char*)p,0,sizeof(zero_buffer)+3);//在结构体里填零,方便对比
p->xxx =1;
p->yyy =2;
p->data[0]=1;
p->data[1]=2;
p->data[2]=3;



不好意思,你的空间是申请到了,但你赋值都赋到了对齐的废字节上了,内存结构:



1    //xxx
2    //yyy
1    //实际的data[0]
2    //实际的data[1]
3    //实际的data[2]
0    //你以为的data[0]
0    //你以为的data[1]
0    //你以为的data[2]



在实际工作中你会发现你的数据结构后面始终有几个字节是0,而你通过sizeof(结构体)的偏移去读你的char[0]会因为偏移错误而读到错误的数据,而这件事情发生与否取决于你的结构体是否变动:幽灵BUG。

解决之道
非常简单:对有零字节数组参与的结构体进行1字节对齐:



#pragma pack(1)
struct zero_buffer
{
    int  xxx;//4字节
    char yyy;//1字节
    char data[0];//零字节数组
}
#pragma pack()



再执行上面的操作:



zero_buffer* p = new char[sizeof(zero_buffer)+3];//申请三个字节紧跟着zero_buffer结构体
memset((char*)p,0,sizeof(zero_buffer)+3);//在结构体里填零,方便对比
p->xxx =1;
p->yyy =2;
p->data[0]=1;
p->data[1]=2;
p->data[2]=3;



内存结果:



1    //xxx
2    //yyy
1    //实际的data[0]=你认为的data[0]
2    //实际的data[1]=你认为的data[1]
3    //实际的data[2]=你认为的data[2]



 

四、总结



// zero_length_array.c
#include <stdio.h>
#include <stdlib.h>
#define MAX_LENGTH      1024
#define CURR_LENGTH      512

//  0长度数组
struct zero_buffer
{
    int     len;
    char    data[0];
}__attribute((packed));

//  定长数组
struct max_buffer
{
    int     len;
    char    data[MAX_LENGTH];
}__attribute((packed));

//  指针数组
struct point_buffer
{
    int     len;
    char    *data;
}__attribute((packed));

int main(void)
{
    struct zero_buffer  *zbuffer = NULL;
    struct max_buffer   *mbuffer = NULL;
    struct point_buffer *pbuffer = NULL;
    
    // =====================
    // 0长度数组  占用-开辟-销毁
    // =====================
    ///  占用
    printf("the length of struct test1:%d\n",sizeof(struct zero_buffer));
    ///  开辟
    if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
    {
        zbuffer->len = CURR_LENGTH;
        memcpy(zbuffer->data, "Hello World", CURR_LENGTH);
        printf("%d, %s\n", zbuffer->len, zbuffer->data);
    }
    ///  销毁
    free(zbuffer);
    zbuffer = NULL;
    
    
    // =====================
    // 定长数组  占用-开辟-销毁
    // =====================
    ///  占用
    printf("the length of struct test2:%d\n",sizeof(struct max_buffer));
    ///  开辟
    if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
    {
        mbuffer->len = CURR_LENGTH;
        memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
        printf("%d, %s\n", mbuffer->len, mbuffer->data);
    }
    /// 销毁
    free(mbuffer);
    mbuffer = NULL;
    
    
    // =====================
    // 指针数组  占用-开辟-销毁
    // =====================
    ///  占用
    printf("the length of struct test3:%d\n",sizeof(struct point_buffer));
    ///  开辟
    if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
    {
        pbuffer->len = CURR_LENGTH;
        if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
        {
            memcpy(pbuffer->data, "Hello World", CURR_LENGTH);
            printf("%d, %s\n", pbuffer->len, pbuffer->data);
        }
    }
    /// 销毁
    free(pbuffer->data);
    free(pbuffer);
    pbuffer = NULL;
    return EXIT_SUCCESS;
}



 



  • 长度为0的数组并不占有内存空间, 而指针方式需要占用内存空间.
  • 对于长度为0数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 才申请空间时需分别进行, 释放时也需分别释放.
  • 对于长度为0数组的访问可采用数组方式进行
  • 0长度数组其实就是灵活的运用的数组指向的是其后面的连续的内存空间
  • 必须将data[0]定义在struct的末尾,结构体的末尾, 就是指向了其后面的内存数据。 

GNU手册还提供了另外两个结构体来说明,更容易看懂意思:



struct f1 {
    int x;
    int y[];
} f1 = { 1, { 2, 3, 4 } };
struct f2 {
    struct f1 f1;
    int data[3];
} f2 = { { 1 }, { 5, 6, 7 } };



我把f2里面的2,3,4改成了5,6,7以示区分。如果你把数据打出来。即如下的信息:



f1.x = 1
f1.y[0] = 2
f1.y[1] = 3
f1.y[2] = 4



也就是f1.y指向的是{2,3,4}这块内存中的数据。所以我们就可以轻易的得到,f2.f1.y指向的数据也就是正好f2.data的内容了。打印出来的数据:



f2.f1.x = 1
f2.f1.y[0] = 5
f2.f1.y[1] = 6
f2.f1.y[2] = 7

标签:struct,buffer,C语言,char,LENGTH,数组,长度,data
From: https://blog.51cto.com/lang13002/8361137

相关文章

  • 11月13数组以及数组常用发法
    目录1.数组2.数据的常用方法1.length方法2.push方法3.pop方法4.unshift方法5.shift方法6.slice方法7.reverse方法8.join方法9.concat方法10.sort方法特殊情况解决特殊情况的方法11.forEach方法12.splice方法null13.map方法还有用for循环取值1.数组数组的作用:使用单独的变量名来......
  • 数组直接通过索引修改属性值不能触发watch
    下面说法错误的是()Awatch监听对象必须设置deep:trueB数组直接通过索引修改属性值,能触发watch方法Cwatch内部可以写异步方法Dimmediate:true可以开启首次赋值监听正确答案:B因为没有getter和setter方法,所以数组直接通过索引修改属性值不能触发watchvue无法监听数组......
  • concat()返回一个新的数组,还需要用新数组替换原数组才能实现视图的更新。
    在Vue中,下列哪个选项对数组的操作不会触发视图的更新()Apush()Bshift()Cconcat()Dreverse()正确答案:Cconcat()返回一个新的数组,还需要用新数组替换原数组才能实现视图的更新。七个改变原数组且会让vue监听到的方法。push()在末尾添加一个pop()在末尾删除一个shift(......
  • C语言程序设计教程3
    1强制类型转换当类型不同时可能导致数据丢失所以需要强制类型转换所以需要强制类型转换,()中间放需要转变的类型2关系操作符>,<,=;>=(大于等于);<=(小于等于);!=(相当于数学里面的“不等于”用于测试不相等);==(用于测试相等),一个=叫做赋值操作符3逻辑操作符&&(逻辑与,”并且“,全真则真,有一......
  • 【pwn】[HGAME 2023 week1]choose_the_seat --数组越界,劫持got表
    查一下程序保护情况发现是partialrelro,说明got表是可以修改的,下一步看代码逻辑看到这一段puts(&seats[16*v0]);存在数组越界的漏洞,因为上面的代码没有对v0进行负数的限制,v0可以是负数,我们来看一下seat的数据可以发现seat上面的数据就是got表,seat到exit的距离只需要传入......
  • Object.defineProperty(obj,key,val)不可以监听数组变化,需要做特殊处理,所以Vue3.0使用
    关于Vue双向数据绑定说法错误的是()AVue实现双向数据绑定是采用数据劫持和发布者-订阅者模式BObject.defineProperty(obj,key,val)可以监听数组变化,不需要做特殊处理CVue2.0数据劫持是利用ES5的Object.defineProperty(obj,key,val)方法来劫持每个属性的getter和setterD......
  • 代码随想训练营第三十四天(Python)| 1005.K次取反后最大化的数组和、134. 加油站、135.
    1005.K次取反后最大化的数组和classSolution:deflargestSumAfterKNegations(self,nums:List[int],k:int)->int:nums.sort(key=lambdax:abs(x),reverse=True)foriinrange(len(nums)):ifnums[i]<0andk>0:......
  • 实验4 C语言数组应用编程
    一、实验目的能正确使用c语法规则定义、初始化、访问、输入/输出一维/二维数值型数组能正确使用c语法规则定义、初始化、访问、输入/输出一维/二维字符数组能正确使用数组作为函数参数能熟练使用常用的字符串处理函数针对具体问题场景,能灵活用数组组织数据,应用、设计算法编......
  • C语言——共用体union存储结构与大小端存储模式
    1、大小端存储模式大端:低位数据字节存储在高地址小端:低位数据字节存储在低地址注释:例如int的权重低的8bit,低位指的是00000001000000000000000000000000000000012、共用体union存储结构共用体内每段数据元素从低地址开始存储。注释:数组的每个元素的地址随着索引的增大......
  • 删除arr数组中的第i个元素的最好做法是?
    删除arr数组中的第i个元素的最好做法是?①arr.splice(i-1,1)②arr.slice(0,n).concat(arr.slice(n+1,arr.length));③Array.prototype.remove=function(dx){if(isNaN(dx)||dx>this.length){returnfalse;}for(vari=0,......