柔性数组,这个名词对我来说算是比较新颖的,在学习跳跃表的实现时看到的。这么好听的名字,的背后到底是如何的优雅。


柔性数组,其名称的独特和迷惑之处在于“柔性”这个词。
在C/C++中定义数组,是一个定长的数据结构,最常用的定义如下

int arr[100];

上述代码的中arr数组的长度已知,我们把上面的语句称之为声明语句,因为在编译期数组的长度已经确定了,我暂且发明了一个词来称呼这类数组——“刚性”数组(声明,这个词是我臆想的,是不存在这种说法的)。

你可能会说:等等,C/C++不是有可以在运行期通过malloc调用来创建动态数组的做法吗?
没错,柔性数组正是需要malloc来实现的,其柔性也是在这个地方体现的。

套路先行

我们先来看一看柔性数组到底是用来干什么的吧?

柔性数组(flexible array member)也叫伸缩性数组成员,这种结构产生与对动态结构体的去求。在日常编程中,有时需要在结构体中存放一个长度是动态的字符串(也可能是其他数据类型),一般的做法,实在结构体中定义一个指针成员,这个指针成员指向该字符串所在的动态内存空间。

在通常情况下,如果想要高效的利用内存,那么在结构体内部定义静态的数组是非常浪费的行为。其实柔性数组的想法和动态数组的想法是一样的。

先修知识

  • 不完整类型

    在C/C++中对于不完整类型的定义是这样的:
    不完整类型是一种缺乏足够的信息去描述一个完整对象的类型
    还是以数组的定义/声明为例子。

// 一个为知长度的数组属于不完整类型
// 这个语句属于声明语句,不是定义语句
extern int a[]; // 这样的语句是错误的, extern关键字不能去掉
// int a[] // 不完整类型的数组需要补充完整才能使用
// 下面的语句是声明语句(定义+初始化)
int a[] = {10, 20};
  • 结构体

    看到这个标题的你可能会说,什么?结构体还用得着你来补充?
    如果各位看官对结构体和内存对其比较熟悉的话,可以跳过这部分,看总结本段的总结,对后面柔性数组的说明有点帮助。

对于内存对齐的部分已经超出了文章所要讨论的内容了。那我想讲的是什么东西,且看下面的代码

#include<stdio.h>

struct test{
int i;
char *p;
}; int main(void){
struct test t;
printf("t:\t%p\n", &t);
printf("t.i:\t%p\n", &(t.i));
printf("t.p:\t%p\n", &(t.p));
}

我们看到t.i的地址和t的地址是一样的。t.p的地址就是(&t + 0x8),0×8这个偏移地址就是成员p在编译时就被编译器给hard code了的地址。

总结:不管结构体的实例是什么,访问其成员就是实例的地址加上成员偏移量。这个偏移量是编译器hard code的,跟内存对齐等因素有关。

千呼万唤始出来

我们来回顾一下,柔性数组用来在结构体中存放一个长度动态的字符串。
其实不用柔性数组我们一样可以做到:在结构体中定义一个方法,在方法中动态地将指针指向动态数组

#include<cstring>
#include<cstdlib>
#include<cstdio> struct Test{
int a;
char *p; void set_str(const char *str){
int len = std::strlen(str);
if(len <=0)
return; p = (char*)std::malloc((len+1)*sizeof(char));
std::strcpy(p, str);
p[len] = '\0';
}
}; int main(){
const char copy_str[] = "Hello World"; Test t;
t.set_str(copy_str);
printf("Content:\n");
printf("t.p:\t%s\n", t.p); printf("Address:\n");
printf("t.p\t %p\n", t.p);
printf("&t.p\t %p\n", &(t.p));
}


我们看到,上面的代码的确是可以完成我们想要的结果。我们看了一下指针p和数组的起始地址。我们可以看到动态数组的内存块和字符串的内存是两块不一样的内存。
折磨程序员的来了,我们在析构对象时,需要显式地在析构函数里面对指针p引用的内存进行释放,不然会出现内存泄露的情况。

那么柔性数组是怎么做到的呢?
还是回到上述的结构体

struct Test{
int a;
char *p;
};

我们想把字符串和结构体连在一起的话,释放的内存时候就能够顺便把字符串的内存给释放掉了,看一看下面的代码

// 使用上面的结构体Test
const str copy_str[] = "Hello World";
int len = std::strlen(copy_str);
// 申请连续的空间
Test *p_test = (Test*)std::malloc(sizeof(Test)+(len+1)*sizeof(char));
// 复制数组
std::strcpy(p_test+1, copy_str);
((char*)(p_test+1))[len] = '\0';

起始这么依赖,会发现char *p就成了多余的东西了,我们完全可以使用语句(char*)(p_test+1)来获取字符串的地址了。

聪明的程序员不想被这么丑陋的代码给糊弄,他们想如果能够找到一种方法既能直接引用字符串,又不占用结构体的空间就很棒了。符合这个条件的应该是一个非对象的符号地址
回忆一下上文所说的不完整类型,起始就是一个符号地址。在结构体的尾部放一个长度为0的方案似乎不错,但是C/C++标准规定是不能定义长度为0的数组。标准不允许?编译器厂商就自行开发呗,有些编译器把0长度的数组作为自己的非标准扩展。

struct flexible_t{
int a;
double b;
char c[0];
};

c就叫柔性数组成员(flexible array member).我觉得翻译成灵活数组成语也是可以的。此时p_test->c就是数组的首地址,不再需要原来那么丑陋的代码了。

这种代码结构这么常用,标准马上就支持了。在C99标准中便包含了柔性成员数组。
记得上文所说的不完整类型吗,C99便是使用不完整类型实现柔性数组成员的。为什么使用不完整类型呢,说说我的理解。

int a[] = {10, 20}; 

看到这个声明语句,我们发现a[]其实就是个数组记号,不完整类型,由于赋值语句,所以在编译时便确定了数组的大小,是一个完整的数组类型。
在结构体中便利用不完整类型在运行对动态的数组进行指明。

C99标准的定义如下

struct flexible_t{
int a;
double b;
char c[]; // 不只是char类型,其他类型同样也是可以
}

由于声明内存连续性的关系,柔性数组成员必须定义在结构体的最后一个,并且不能是唯一的成员。
我们再来看一看整个结构体(包含数组内存的分布情况)

#include<cstring>
#include<cstdlib>
#include<cstdio> # define new_instance(n) (Felexible*) std::malloc(sizeof(Flexible) + (n+1)*sizeof(char)) struct Flexible{
int a;
char p[0];
}; int main(){
const char copy_str[] = "Hello World";
// 我们使用宏来把创建对象的代码简化
Flexible *flexible_p = new_instance(std::strlen(copy_str));
std::strcpy(flexible_p->p, copy_str); printf("Content:\n");
printf("%s\n", flexible_p->p);
printf("Address:\n");
printf("t.p:\t %p\n", flexible_p->p);
printf("&t.p:\t %p\n", &(flexible_p->p)); free(flexible_p);
}

由运行结果就可以看出,整个结构体是连续的,并且释放结构体的方式也非常简单直接对结构体指针进行释放。

warning C4200: 使用了非标准扩展: 结构/联合中的零大小数组

由于这个是C99的标准,在ISO C和C++的规格说明书中是不允许的。在vs下使用0长度的数组可能会得到一个警告。
然而gcc, clang++预先支持了C99的玩法,所以在Linux下编译无警告

总结

我们学习了柔性数组成员的来源及一些用法,
其实柔性数组成员在实现跳跃表时有它特别的用法,在Redis的SDS数据结构中和跳跃表的实现上,也使用柔性数组成员。

 
 

flexible array柔性数组、不定长的数据结构Struct详解的更多相关文章

  1. C/C++数组名与指针的区别详解

    1.数组名不是指针我们看下面的示例: #include <iostream> int main() { ]; char *pStr = str; cout << sizeof( ...

  2. 常见数据结构图文详解-C++版

    目录 简介 一.数组 1. 静态数组 array 2. 动态数组 2.1. vector 2.2. priority_queue 2.3. deque 2.4. stack 2.5. queue二.单 ...

  3. java中Array/List/Map/Object与Json互相转换详解

    http://blog.csdn.net/xiaomu709421487/article/details/51456705 JSON(JavaScript Object Notation): 是一种轻 ...

  4. SE6 不定参数和默认参数详解和使用细节

    在SE5以前我们通常通过arguments类数组对象来引用不定形参,SE6则使用了一种叫做不定参数的写法,比起隐式的arguments要直观的多. 不定参数使用...参数名来指定一个不定参数,参数名指 ...

  5. OpenCV学习笔记(四十)——再谈OpenCV数据结构Mat详解

    原文:http://blog.csdn.net/yang_xian521/article/details/7107786 我记得开始接触OpenCV就是因为一个算法里面需要2维动态数组,那时候看cor ...

  6. JavaScript系列--JavaScript数组高阶函数reduce()方法详解及奇淫技巧

    一.前言 reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值. reduce() 可以作为一个高阶函数,用于函数的 compose. reduce()方 ...

  7. LeetCode.516 最长回文子序列 详解

    题目详情 给定一个字符串s,找到其中最长的回文子序列.可以假设s的最大长度为1000. 示例 1: 输入: "bbbab" 输出: 4 一个可能的最长回文子序列为 "bb ...

  8. LeetCode 646 最长数对链详解

    题目描述 给出 n 个数对. 在每一个数对中,第一个数字总是比第二个数字小. 现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面.我们用这种形 ...

  9. java中Array/List/Map/Object与Json互相转换详解(转载)

    JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式.易于人阅读和编写.同时也易于机器解析和生成.它基于JavaScript Programming Langu ...

随机推荐

  1. 斯坦福大学公开课机器学习:监督学习在行人检测的应用(supervised learning for pedestrian detection)

    对于下图,左边是行人,作为阳性例子,赋值y=1,右边是景物,作为阴性例子,赋值y=0; 步长概念: 如下图所示,步长表示绿色框框移动的距离,有时候也称为滑动参数stride,如果一次移动一个像素,则称 ...

  2. solr6.4.2之webservice兼容升级

    摘要:这次solr底层升级是一次比较大的升级.从底层搜索引擎 solr4.8 升级到 solr6.4.2,由于solr底层从6.x开始以来的jdk必须指定为1.8,而且很多内部实现类都已经废弃或者干脆 ...

  3. Kafka 0.10问题点滴

    15.如何消费内部topic: __consumer_offsets 主要是要让它来格式化:GroupMetadataManager.OffsetsMessageFormatter 最后用看了它的源码 ...

  4. Spark记录-大数据简介

    什么是大数据 大数据(big data),指无法在一定时间范围内用常规软件工具进行捕捉.管理和处理的数据集合,是需要新处理模式才能具有更强的决策力.洞察发现力和流程优化能力的海量.高增长率和多样化的信 ...

  5. 机器学习课程-第8周-聚类(Clustering)—K-Mean算法

    1. 聚类(Clustering) 1.1 无监督学习: 简介 在一个典型的监督学习中,我们有一个有标签的训练集,我们的目标是找到能够区分正样本和负样本的决策边界,在这里的监督学习中,我们有一系列标签 ...

  6. centos redis集群搭建

    说明: 10.0.0.111部署6500,6501,6502三个主节点 10.0.0.222部署6500,6501,6502三个备份节点 1.安装redis:略 2.配置内核参数 # 配置 vm.ov ...

  7. webkitAnimationEnd事件与webkitTransitionEnd事件

    写一个焦点图demo,css3动画完成以后要把它隐藏掉,这里会用到css3的事件,以前没有接触过,结果查了一下发现这是一片新天地啊,而且里面还有好多坑,比如重复动画多次触发什么的.anyway,我还是 ...

  8. Vue.js入门系列教程(二)

    过滤器:filter 全局过滤器 <!DOCTYPE html> <html lang="en"> <head> <meta charse ...

  9. Oracle——存储过程简单入门实例

    1.连接plsql developer,打开一个SQL Window 2.SQL Window中创建表user_info -- Create table create table USER_INFO ...

  10. C# print2flash3文件转化

    1.下载print2flash3 并且安装print2flash3 2.转换工具类 (1)需要导入using Print2Flash3; 这个程序集 using System; using Syste ...