最近两天复习c++的链表的时候发现了一个问题值得深思. 首先从一个现象上引出问题:
在我写线性表的链式存储的时候定义了几个结构体:(全部代码在这里)
linklist.h
typedef void LinkList;
typedef struct _tag_LinkListNode
{
struct _tag_LinkListNode * next;
}LinkListNode;
linklist.c:
typedef struct _tag_LinkList
{
LinkListNode header;
int length;
}TLinkList;
main.c:
typedef struct Teacher
{
LinkListNode node;
char name[64];
int age;
}Teacher;
要想说明问题我再列举插入的方法:
.h
int LinkList_Insert(LinkList * list, LinkListNode * node, int pos);
main.c
LinkList_Insert(list, (LinkListNode *)&t3, LinkList_Length(list));
//遍历
for (i=0; i<LinkList_Length(list); i++)
{
Teacher *tmp = (Teacher *)LinkList_Get(list, i);
if (tmp != NULL)
{
printf("age:%d ", tmp->age);
}
}
好了,我们可以开始进行分析. 大伙是不是觉得这个定义有点混乱,那么我给上个图先缕清关系:
有的人肯定会说为什么一个节点结构体只是一个next指针而没有其他数据,的确,一个节点除了next域还有data域. 但是为了让这个节点结构体更通用(因为头节点是没有data域,只有next域). 这也就引出我的讨论点. 使用结构体的强制类型转换方式,正如上面代码一样.
我们先讨论一下结构体的内存分配和强制类型转换再回来分析:
我们先来看一个例子:
struct str1
{
char a;
int b;
float c;
double d;
};
str1这个结构体占用的内存是多少呢?如果用变量类型直接想加,得到的结果是17,但显然不是这样的。这个程序运行的正确结果是24.为什么呢?
因为为了CPU能够快速访问,提高访问效率,变量的起始地址应该具有某些特性,这就是所谓的“对齐”。比如4字节的int型变量,那它的起始地址就应该在4字节的边界上,即起始地址可以被4整除。
内存对齐的规则很简单:
1. 起始地址为该变量类型所占内存的整数倍,若不足则不足部分用数据填充至所占内存的整数倍。
2. 该结构体所占总内存为结构体成员变量中最大数据类型的整数倍。
接下来我们分析上面的例子:
char型变量占一个字节,所以它的起始地址为0,而int类型占4个字节,它的起始地址应该是4(的整数倍),那么内存地址1、2、3就需要被填充。同样,float占用4个字节,而结构体中a,b两个成员变量占了0~7内存地址,c的地址从8开始,符合规则一,占用内存地址为8~11。double类型占8个字节,所以d的起始地址就应该从16开始,那么12、13、14、15内存地址就需要被填充。d从16地址开始,占用8个字节。整个结构体占用字节数为24,符合规则二。内存分配如图:红色区域为填充部分
下面再举一个例子,进一步说明:
struct str2
{
double a;
int b;
char c;
double d;
};
str2这个结构体占用的内存空间是多少呢?是24!怎么分析呢?
首先double类型的a占用内存地址为0~7,int类型的b起始地址为8,符合规则一,占用地址为8~11,char类型的c占一个字节,地址为12.那么double类型的d,起始地址为13吗?显然不是,满足规则一的地址是16,所以d起始地址为16,占用16~23。结构体总共24个字节,满足规则二。如果这个结构体最后再加一个成员变量 char e,那这个结构体占用的内存是多少?char类型的e起始地址为24,占用地址为24,但是结构体一种有25个字节,就不满足规则二了,怎么办呢?为了满足规则二,我们将25~31进行填充,因此整个结构体占用32个字节。
接下来我们在讨论一下结构体的强制类型抓换问题:
/* 双向链表 (类似于父类)*/
typedef struct hLinks{
struct hLinks *bwLink;
struct hLinks *fwLink;
} hLinks;
/*
* 一个使用双向链表的结构
* (类似于子类)
*/
typedef struct hEnt{
hLinks links;
int hData;
char key[10];
} hEnt;
首先,我们要搞清楚的一点是:C语言中的结构体并不能直接进行强制类型转换,只有结构体的指针可以进行强制类型转换.
PrintLink( hLinks *h )
{
hEnt *p ;
for( p = ( hEnt* ) h->fwLink;
p != ( hEnt* ) h;
p = ( hEnt* )( (hLinks*)p )->fwLink )
{
printf("hData=[%d], key=[%s]/n", p->hData, p->key);
}
}
话说回来,结构体指针的强制类型转换问题在这里面始终存在。PrintLink中就出现了这样的情况,那么在将hLinks指针转换为hEnt类型指针时有两个问题:
1. 结构体中的成员情况是怎么的?
2. 结构体中的成员的值的情况是怎么样的?
首先,结构体是储存在一块连续内存中的,计算机只关心的是结构体的大小和操作方式,结构体大小是定义的时候决定的(要进行对齐),而结构体的操作确实和结构体中的成员类型有关的。指针表示的是内存地址,那么在强制类型转换之后,计算机便以转换后的结构体来看待这个地址内存中的内容。比如两个结构体的内存结构如下:
可以看出,在前两个内存单元中两个结构体存储的内容是相同的,当然不管相不相同计算机是不管的,当hLinks类型转换成hEnt类型时,计算机就将原结构体看做是hEnt类型的。转换后的hEnt类型结构体的前面两个内存单元的内容就是hLinks中的前两个单元内容,而hEnt的后两个内存单元中的内容取得是hLinks后的两个单元(这两个单元不是hLinks类型的成员,而是别的内容,所有如果转换后的hEnt要访问hData和key的话是不安全的!)。
接下来我们回归我们的线性表的链式存储问题上:
我们会很神奇的发现, 当有一个新的老师节点加入的时候,只需要将teacher结构体强制类型转换成LinkListNode结构体,这样因为它们结构体内存的首元素地址是重叠的所以可以进行了直接的拷贝. 置于剩下的teacher中的name, age这些元素其实并没有拷贝到LinkListNode结构体中,因为转换后是延用LinkListNode的内存分配. 但是通过这种方式将teacher结构体串联了起来,就像图上画的一样. 当我们需要某个结点的信息时,只需要将LinkListNode再强转成teacher结构体,这样它们的首地址又是重叠的,结构体中的内存地址分配是连续的所以根据teacher中LinkListNode对象的地址就找到了相对应的teacher结构体中其他元素的信息.
这种方法是很常见的,在Unix网络编程,高级编程中等好多都是用到这样的思想. 充分利用结构体内存对齐的思维.
联系方式: reyren179@gmail.com