【C\C++语言入门篇】-- 结构体
前面两篇基本把指针给介绍完了,相信大家对指针已经不是那么陌生了。也不会因为指针和数组之间的关系而导致混淆了。大家可能也迫不及待想了解下后来的知识。今天我们就介绍下结构体。
对于结构体,既然叫结构体,形象上我们可以理解其就是一堆数据集合在一起形成一个结构。就比如一个学生的信息包括:学号、姓名、班级、年龄等等。这些信息都是属于这个学生的,因此我们就可以将这些信息统一绑定在一起。形成一个学生实体,这里有点C++的味道。我们学C也还是有必要这样思考。在我们周围几乎每一样东西都有它自己的信息或者组成。比如药品,它有什么功效,有什么成分等等都能统一绑定在一起形成一个实体,我们在程序中就能方便的访问这些实体的每一个信息或组成。因此,当我们在设计一个程序的时候,我们就能把一些具有共同特性或者组成元素集合到一起构成一个结构体。比如我们的学生就可以写成:
struct SStudent
{
char name[ 13 ]; //姓名
char className[ 16 ]; //班级名
char age; //年龄
....
};
这样一来,学生这个活生生的实体就把所有关于他的信息集中在一起了。这样就能集中管理了,里面的每一个信息就能通过结构体变量来访问。先看看怎么访问:
C语言:
struct SStudent student;
student.age = 22;
C++:
SStudent student;
student.age = 22;
从上面可以看出要访问一个结构体成员是很方便的,同时也体现了实体的概念。我们将学生实体的年龄信息取出来赋值为22岁。就好像在使用某个东西的某个功能一样。这也是众多面向对象语言的一种思想。就是将程序数据封装话、结构化,我们要操作一个数据就跟现实生活中的使用某个工具的某个功能一样。我们看到上面C和C++版本访问唯一不同的就是C++版本在声明结构体变量的时候不需要在前面加上struct关键字,个人觉得后来C++觉得struct没有必要再写了吧,麻烦!省略了不是更好!在语法和意义上两个版本是相同的。
结构体还可以不需要名字,比如:
struct
{
char age;
char name[ 16 ];
}
student, stu[ 10 ];
这里这个结构体就省略了名字,后面的student并不是名字,而是结构体变量。这种就是匿名结构体。跟普通的没有什么区别,后面的stu就是一个结构体数组,普通结构体定义也可以在声明结构体的时候紧跟着就声明变量的。只是这样你要定义其它变量就麻烦了,呵呵!这种一般用得比较固定或者就用这么一次就可以不要名字。
再来看看结构体别名。所谓别名就是可以使用另外一个名字。
typedef struct SStudent
{
char age;
char sex;
char class;
}STU,*PSTU;
这里的STU就是SStudent结构体的别名,就相当于是另外一个名字,使用的时候就可以不用加可恶的struct标识符了。
STU student;
PSTU pStuednt; //别名为指针类型
好了,结构体就这么简单,就是把不同类型或者同类型的一些数据集中到一起管理,构成一个实体。这个实体也可以理解为结构体。通常这样设计是为了程序的模块化结构化,这样理解起来更容易更接近于现实,计算机本来就是服务于现实的。再比如我们的链表(将一组数据串联成一个链,我们可以通过指针访问到这个链中的每一个结点,形象的叫着是一个链,本质其实就是一组数据通过指针链接在一起,通常存放在内存中是不连续的),举个简单的例子:
struct Node
{
Node* pNext;
char name[ 16 ];
};
这里也是一个结构体,里面包含一个指针和一个名字。假如我们这个名字就是某个学生的名字,这个结构体我们就形象看成是一个结点,什么是结点?结点你可以想象我有一条很漂亮的珍珠项链,项链上有很多颗珍珠串联在一起,那么每一颗珍珠就可以想象成是一个结点。项链就是由很多个结点串联在一起形成的。可能有的读者觉得这样比喻倒是很容易理解,但是联想到程序里面还是感觉有点抽象。其实也不能说是抽象,咱们就想成它就是这么回事。就好比我们要安装一个工具,注意到这句话里面出现了两个现实生活中的词:“安装”“工具”。在计算机里我们使用的所谓工具其实都是虚拟化的,这些名字只是为了形象一点,再说安装,也是如此,在现实生活中我们会在组装或者安装某个零件的时候才会使用这个词,在计算机里使用这个词也是为了大家能够更容易理解形象化罢了。所以我们不必太拘泥于叫法。
好,我们这里定义了一个结构体作为结点,我们的目的是想把全班所有学生的名字全部串联在一起,假如全班有50个人,那么就有50个结点。因此我们必须的有50个结构体结点来保存这50个学生的名字,而且我们这50个学生的名字还能够通过循环遍历能够找到其中任意一个。那么我们就得这样做:
struct Node root; //根结点(第一个学生结点)
struct Node secSt; //第2个学生结点
上面我们定义了2个学生结点,现在把这两个结点链接在一起。
strcpy( root.name,"masefee");
strcpy( secSt.name,"Tim");
root.pNext = &secSt;
secSt.pNext = NULL;
上面我们已经把这两个结点链接起来了。root结点的next指针指向的就是secSt,secSt的next指针这里赋值为NULL,如果还想指向下一个学生结点同理。再看看层级关系:
root---|----name ("masefee")
|----pNext---|----name ("Tim")
|----pNext---|----name ......
|----pNext ......
上面的层级关系很清晰的描述了这些结点的关系,这样就能够成一个链,我们可以通过遍历找到其中任何一个结点。我们也称这种存储在内存中为链式存储。其本质就是通过指针将一个一个数据块链接在一起。这里我只列举了两个结点。
问题一:我们怎么将50个结点链接在一起?(提示:每个结点可以malloc申请内存空间)
通过上面的描述,我们对结构体的用法和概念上有了初步的认识了。再来看看结构体指针(怎么总是离不开指针,呵呵,没办法指针在CC++里本来就是个永恒的主题)。
struct Node stuNode; // struct Node
struct Node* pNode = &stuNode;
strcpy( pNode->name,"masefee");
上面,我们定义了一个Node结构体指针,该指针指向了stuNode,最后我们将stuNode结构体的name拷贝成了“masefee”。同样我们可以使用库函数给申请空间,大小为Node结构的大小:
struct Node* pNode = ( struct Node*)malloc( sizeof( struct Node ));
这里我们使用malloc函数给申请了Node结构大小的一块内存,然后让pNode指针指向这块空间。因此我们就可以向这块内存中写入值了。
strcpy( pNode->name,"masefee");
pNode->pNext = NULL;
这里的pNext也可以指向下一块申请的内存空间(可以用来回答问题一),这里就不写了,大家要自己摸索才行。
说到这里,不得不说说结构体的对齐问题,什么是结构体对齐,为什么要对齐。我们都知道计算机的内存单位换算都是以2的多少次方来计算的,这样计算是有目的性的。当然是为了计算机的执行效率,大家可以想象一下,假如我们一个变量的类型占用3字节,一个5字节,一个1字节。计算机在寻址的时候对于这种参差不齐的内存会降低它的效率。所以通常默认情况下,结构体采用4字节对齐,意思就是说一些不足4字节的变量会可能被扩充到4字节大小或者与其它结构体成员变量进行合并成4字节。这样浪费小小的一点内存效率上会提高很多。这里说到4字节,当然就有8字节,16字节,1字节,2字节对齐了。我们这里就默认谈谈4字节对齐,其它都是同理的。先举个例子:
struct Align
{
char age;
int num;
};
sizeof( struct Align ) = ?
这里求sizeof的结果我们得到的确是8,而不是我们想要的5。这里是8的原因是默认为4字节对齐,这里char占用1字节,int占用4字节,首先编译器编译的时候遇到char会去寻找周围有没有更多的可以合并的字节,一共合并成4字节,或者合并一部分然后扩充一部分构成4字节,但是这里没有找到,那么age将被扩充到4字节,加上int的4字节,一共被扩充到了8字节。
struct Align align;
align.age = 0xff;
align.num = 0xeeeeeeee;
我们以为在内存中分布为:
age num ff ee ee ee ee
然而:
age num ff cc cc cc ee ee ee ee
age多出来了3个字节,这里未初始化时填充的是0xcc。假如我们定义成:
struct Align
{
char age;
char age1;
char age2;
int num;
};
那么age2将被扩充为2字节,age age1 age2合并成3字节再扩充一个字节就组成4字节了。这里sizeof还是为8字节。再比如:
struct Align
{
char age;
int num;
char age1;
};
这样sizeof结果出来将是12字节,原因也很简单,首先在编译age的时候,查找挨着没有能合并成4字节的成员,那么就会扩充成4字节,age1同理,假如age为0xff,num为0xeeeeeeee,age1为0xaa,内存分布就为:
ff cc cc cc ee ee ee ee aa cc cc cc
问题二:这里为什么不将age和age1分别扩充为2字节然后再合并成4字节,结构体一共8字节?
再举个例子。
struct Align
{
char age;
double num;
char age1;
};
这里的sizeof将是24字节,原因就是结构体对齐还是有标准的,假如默认是4字节对齐,常理这里完全可以将age和age1分别扩充成4字节,整个结构体16字节。但是编译器并没有这么做,而是都扩充成了8字节,这是因为结构体在处理对齐问题的时候,都是以最大的基本类型数据成员为标准进行对齐(注意这里是基本数据类型)。假如:
struct SStudent
{
int a[ 2 ];
}
struct Align
{
char age1;
struct SStudent stu;
};
这个Align结构体同样还是12字节,而不是16字节。
再比如:
struct SStudent
{
char a[ 13 ];
};
struct Align
{
char age1;
struct SStudent stu;
};
问题三:上面程序中Align结构体的大小是多少?为什么?
同样再来看看结构体指针和任意指针强制类型转换。
typedef unsigned char byte;
struct SStudent
{
byte age;
byte sex;
byte class;
};
byte array[ 99 ];
struct SStudent* pStu;
array[ 0 ] = 0xaa;
array[ 1 ] = 0xbb;
array[ 2 ] = 0xcc;
pStu = ( struct SStudent*)array;
上面这段程序,我们将array的前3个元素赋值为0xaa,0xbb,0xcc。这样做的目的是想看看我们强制类型转换过后,pStu结构体指针的pStu[ 0 ]三个成员是否就是array数组的前3个成员。答案是肯定的,大家可以自己调试监视看。这个array数组强制类型转换过去后,pStu[ 0 ], pStu[ 1 ],..., pStu[ 31 ], pStu[ 32 ]。一共就有33个结构体数据块。同样pStu++类似的加减及累加都会跳跃SStudent结构体大小个字节。跟前面一篇提到的原理一样。
在C++中结构体发生了翻天覆地的变化,跟C的结构体很大差别,这里暂时不说了。等我们说了函数的时候再谈C++的结构体。不过本文提到的结构体相关在C++中同样有效。在结构体中很多时候会用到位域,这里暂时不说,先留个思路在这里。等我们专门谈位运算的时候再来详细说明