C/C++使用0元素数组实现可变结构体

文章目录

    • 一、结构体在内存中的布局
    • 二、0元素数组
    • 三、使用0元素数组实现可变结构体

一、结构体在内存中的布局

结构体只是一个数据的集合,结构体地址也只是第一个元素的地址,如果各个元素没有内存对齐的限制,它们的则会依次挨着存放在内存中,也就不会存在填充字节。

为什么要内存对齐?

处理器的核心是一个字长的存储设备或寄存器,称为程序计数器,在任何时刻,程序计数器都指向主存中的某条机器语言指令。总线被设计成传送定长的字节块,即字(word),字的字长由机器的位数决定,4字节(32位)还是8字节(64位)。地址总线或者数据总线传送值到程序计数器时,一次都是一个字长,内存对齐就是为了避免传送一个字长就能解决的事却传了两次。假设现有环境是一个32位的机器,则字长为4个字节,内存结构如下图所示:
C/C++使用0元素数组实现可变结构体_第1张图片
有一个结构体A:

struct A {
	short a;
	bool b;
	int c;
};

先不考虑内存对齐,将a、b、c存放到内存中,则a占用{0,1},b占用{2},c占用{3,4,5,6},从图可以看出,3、4是属于跨的内存(我乱叫的,这么理解就行),所以想要存取c中的内容,就需要cpu与内存总线交互两次,导致效率低下,所以就有了内存对齐这一约束。

那如何约束呢?让元素a的起始地址为2的倍数,b为1的倍数,c为4的倍数,这样就可以避免上面存在问题。a占用{0,1},0是1的倍数;b占用{2},2是2的倍数;到3开始存放c,但是3不是4的倍数,咋办?那就把它空下(空下的这玩意叫内存的内部碎片),从4开始存放c,所以c占用{4,5,6,7}。

内存对齐包括了结构体内存对齐和基本类型内存对齐,目的在于用空间换取存取效率,方式是约束每个变量存放的起始地址。可以好好推敲一下这么做的巧妙之处和存在意义,结构体内存对齐详细的讲述可以参考下面两篇文章:

  • struct 字节对齐详解
  • struct结构体字节对齐

二、0元素数组

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

struct RecordType {
	int fieldCount;
	RecordFieldType fieldArray[0];
};

有的编译器可能会报错,可以成成下面这种形式,但是效果一样:

struct RecordType {
	int fieldCount;
	RecordFieldType fieldArray[];
};

fieldArray数组长度为零,且放在在结构体最后,从结构体内存布局就可以知道,这个必须得放在最后。sizeof(RecordType)=4可以看出来,结构体RecordType大小只有fieldCount元素的四个字节,fieldArray只是一个数组名,代表了一个偏移量,一个不可修改的常量地址(并非指针),0个元素或者空元素数组编译时不占用空间。如果对于数组名和指针的概念比较模糊,可以看看这篇文章,数组名和指针的区别。

char buffer[0];
cout << sizeof(buffer) << endl; //输出:0

参考文章:

  • 妙用0元素数组实现大小可变结构体
  • C语言0长度数组(可变数组/柔性数组)详解

三、使用0元素数组实现可变结构体

这也是之前做一个符合类型的codeGen时用到了这玩意。假设你现在要实现一种语言的编译器,在codeGen阶段,有一个符合类型叫做record类型,它很像C语言的结构体,里面可以包含很多中数据类型,包括基本类型、数组、以及record自身等,这个时候,你需要设计一种好的结构,让每一次不同的record能有一个固定的表示方式,好编写运行时代码。设计思路大概如下,但远不止这些。

#include 
#include 
#include 
#include 

struct RecordFieldType {
	/**
	 * 这个结构体表示每个record元素的类型
	 * 所以至少应该由要给void *typePtr,但是为了方便就没写
	 */
	char type;
	int value;
};

struct RecordType {
	int fieldCount;
	/**
	 * 结构体最后,数组长度为零,且sizeof(RecordType) = 4
	 */
	RecordFieldType fieldArray[0];
};

RecordType* createRecordType(int count);
void initRecordType(RecordType *recordTypePtr);
void freeRecordType(RecordType *recordTypePtr);
void testFun();

RecordType* createRecordType(int count) {
	RecordType *recordTypePtr = (RecordType*) malloc(
			sizeof(RecordType) + count * sizeof(RecordFieldType));
	recordTypePtr->fieldCount = count;
	return recordTypePtr;
}

void initRecordType(RecordType *recordTypePtr) {
	int count = recordTypePtr->fieldCount;
	RecordFieldType *fieldPtr = recordTypePtr->fieldArray;
	for (int i = 0; i < count; i++) {
		fieldPtr[i].type = 'a';
		fieldPtr[i].value = i;
	}
}

void printRecordType(RecordType *recordTypePtr) {
	int count = recordTypePtr->fieldCount;
	RecordFieldType *fieldPtr = recordTypePtr->fieldArray;
	for (int i = 0; i < count; i++) {
		std::cout << "type:" << fieldPtr[i].type << " value:"
				<< fieldPtr[i].value << std::endl;
	}
}

void freeRecordType(RecordType *recordTypePtr) {
	free(recordTypePtr);
	recordTypePtr = NULL;
}
void testFun() {
	int count = 3;
	RecordType *recordTypePtr = createRecordType(count);
	initRecordType(recordTypePtr);
	printRecordType(recordTypePtr);
	freeRecordType(recordTypePtr);
}

int main() {
	testFun();
	return 0;
}

你可能感兴趣的:(C/C++)