变长数组(非const变量来定义数组的长度)是每个C++开发人员梦寐以求的东西。通常我们实现C++变长数组时,主要通过new(或malloc)实现。如下面这段代码。

int   inamelen = 100;
char *pNameStr = new char [inamelen ];

但这种实现有两个显著的缺点:

  • pNameStr 指针无法记录自己的长度,它的长度必须另行存储,而且你还需明确知道记录pNameStr长度的变量是inamelen 。假设记错了,那你的程序就会存在崩溃的风险。
  • pNameStr在使用完后,必须还给内存。否则就会存在内存泄露的风险。因为pNameStr存储区在堆上。

我们继续看这样的结构体,这样结构体可以记录数组的长度。我们首先看一下这样实现形式:

struct NameStr
{
    int  namelen;  // 名称字符串长度
    char *psznamestr;//名称字符串指针
}
// 创建一个可变数组,数组长度为n。
Struct NameStr  MyName;
MyName.namelen = n;
MyName.psznamestr = malloc(n*sizeof(char ))

这种结构体的数组大小是动态分配的,但是这样分配的地址不连续的(即psznamestr指向的数组不是接在MyName.namelen的后面,这样有时候会不方便,比如说用memcpy去从MyName首地址开始拷贝,只能拷贝namelen 和psznamestr的值(即数组的首地址),如果想要得到数组里的数据就还要用memcpy从psznamestr的值再来拷贝一次,这样就很麻烦,特别是在socket编程的时候,每次传出的数据都是“大小+数据”的模式,如果是这样写的结构体,就得要调用2次传输函数,但是下面这种写法可以解决这个麻烦。

在C89中,有一种称为“struct hack”的方法来得到可变数组。我们看下面这个可变数组实现:

struct NameStr
{
    int  namelen;  // 名称字符串长度
    char namestr [1];//名称字符串数组地址
}
// 创建一个可变数组,数组长度为n。
struct NameStr*pNameStr = malloc(sizeof(struct NameStr) + (n-1)*sizeof(char ));

动态数组pNameStr分配于堆上 。在堆内存上的布局如图2-4所示。创建时通过申请内存生成,使用完毕是必须释放申请的内存。虽然这种实现同样必须申请和释放内存,但是他有一个明确的优点是这种实现可以记录动态数组的长度。

可变长度数组 java 可变长数组怎么实现_可变长度数组 java


图2-4 struct hack动态数组内存布局

小心struct hack陷阱

  • struct hack动态数组必须分配于堆上。它通过struct数据结构实现,动态数组成员(如namestr)必须是struct的最后一个数据成员。
  • struct hack可记录动态数组的长度。struct hack同样存在一个缺点即需要编程人员手工分配和释放。

Struct hack方式虽然解决了动态数组的问题,但是struct hack得到的数组长度n,让人感觉有些邪恶的(在内存申请时只申请了n-1个,而实际上分配的数组长度却为n)。所以为了解决这种问题C99提供了类似的,但合法的“struct”机制。采用这样机制,上述可变数组实现可改写成下述代码。

struct NameStr
{
    int  namelen;  // 名称字符串长度
    char namestr [];//名称字符串数组地址
}
// 创建一个可变数组,数组长度为n。
struct NameStr*pNameStr = malloc(sizeof(struct NameStr) + (n)*sizeof(char ));

C99的这种“struct”机制,让struct hack动态数组实现更符合编程人员的视觉需要,也更容易理解。但是他依然没解决编程人员手动内存分配,而引入的内存泄露风险。

最后,我们看一种真正的动态数组实现方式,这种实现方式:一可解决编程人员手工内存操作的内存泄露风险,二可具有普通数组一切特性,但是这种数组的长度可以使一个非const变量。这就是C99新引入的变长数组功能。

新的C99标准新增了变长数组的支持,你可以像以前定义数组一样,定义一个变长数组。数组长度可以是一个非const变量。可变数组的空间大小直到程序运行时才能确定,因此只有程序在运行时才能为数组分配空间。在GCC编译器会在程序运行时根据实际指定的大小(变量当前的值)调节ESP的值,为数组在栈上分配适当大小的空间。由于要在运行时才能为数组分配空间,在开始分配空间之前空间的大小是不确定的,因此分配空间的起始地址也是不确定的(例如要在栈上分配两个可变长数组的情况下)。

为了在以后的代码中对可变长数组的内容进行引用操作,程序必须通过某种方式获取可变长数组的地址。在GCC编译器中会在相对于EBP固定的偏移量的栈上分配的一个固定大小的区域(称为内情向量)来记录可变长数组的信息,如数组的开始地址等。后续代码通过内情向量中的起始地址访问可变长数组。因为数组依靠在程序运行时动态的调整EBP来分配空间,所以这种类型的数组只能够定义在栈内,不能够定义在数据段(全局数组,静态数组)上。

下面这段代码是采用动态数组的编程实现。

#include<stdio.h>
#include<stdlib.h>

int main()
{
    unsigned int uiArrySize = 0;     // 变长数组非const常量长度存储单元
    fscanf(stdin, “%d”, &uiArraySize);
    int  aiArray[uiArrySize];        // 定义变长数组
    if (0 == uiArrySize)             // 判断长度是否为0。若非0,则打印数组数据。
    {
        printf(“aiArray array is a empty array!\n”);
        return 0;
	}
	else 
	{
        /*错误: loop 初始化声明仅仅适用于C99模式下。
          Note:需应用-std=c99或-std=gnu99编译选项*/
        for(int i = 0; i < uiArrySize; i++)
        {
            printf("%d\t", aiArra[i]);  
        }
    }
}

小心地雷:

  • 虽然大部分编译器已支持C99标准,但是至今依然有很多编译器不支持可变数组。他们遵从的依然是C89标准。
  • 可变数组必须分配到栈上,不能分配到数据段上。如全局数组,静态数组就不能定义为可变数组。

请谨记