c提供了两种聚合类型,分别为数组和结构,数组是相同类型元素的集合,其每个元素是通过下标引用或指针间接访问来选择的,
结构也是某些值的集合,这些值成为结构的成员,而各个成员的类型可能不同,因此每个成员的大小很可能不同,因此,不能通过下标引用来访问成员,一般是通过成员的名字来访问成员。和数组不同,结构变量属于标量类型,可以其他标量能进行的操作,结构变量也可以进行,而在使用时结构变量并不被替换程指针。
上面叙述了结构与数组的区别,现在来探讨关于结构的话题。
首先是结构的声明,我们来看
struct
{
int a;
char b;
float c;
}x;
这样就声明了一个匿名结构,并定义了一个结构变量x,结构变量x的成员有a,b,c,三个成员的类型都不同。我们继续看下面
struct
{
int a;
char b;
float c;
}x2,y[5],*z;
这里声明了一个匿名结构就,并创建了x2,y[5],*z,显然,这里的结构成员表和上面的相同,但是编译器在编译时这两种声明会被完全当成两种不同的类型,因此,这里的x2与上面的x是完全不同的,而语句z=&x也是非法的,可见,同一个程序中声明的两个结构无论如何都是不同的。
在上面的匿名声明中,结构的创建只能在声明结构时创建,这样做的缺点是每创建结构变量都需要在声明的地方创建,不够灵活,我们现在用一种运用标签的方法来声明结构
struct TAG
{
int a;
char b;
float c;
};
struct TAG x;
struct TAG y[5];
struct TAG *z;
这里声明了一个结构TAG,TAG是一个标签,它标识了一种模式,在后三行代码中,struct TAG就像是一种类型一样,如同 int i 定义一个整型变量一般,struct TAG x; 定义了一个结构变量,struct TAG y[5]; 定义了一个结构数组,struct TAG *z; 定义了一个结构指针。在其他需要定义相同类型的结构变量的地方,只需要用struct TAG 就可以了。
我们来看一种更好的声明技巧
typedef struct
{
int a;
char b;
float c;
}Type;
Type x;
Type y[5];
Type *z;
这种声明效果与上面的声明效果完全相同,而这里创建新的结构变量时用Type,在上面代码中,标签TAG标识了一种模式,而这里的 Type 这是作为一种类型。
我们再来看一段代码来探讨一下结构的自引用
typedef struct
{
int a;
Type b;
}Type;
你能看出这段代码有哪些错误吗?首先,结构的第二个成员b是一个完整的结构变量,其内又包含了另一个结构,这样层层包含下去是无限的,就像一个没有终止条件的递归函数一般,因此这样的写法是非法的。然后,我们再来看,类型名直到声明的末尾才定义,而在前面的结构成员表中就已经用于创建结构变量b,显然这种先用后声明的做法是极不正确的,我们再来看
typedef struct type
{
int a;
struct type *b;
}Type;
这里struct type *b 创建了一个结构指针变量,由于存在标签 type,且指针变量b的大小已知,显然这里的自引用是合法的。
上面我们探讨了结构的声明,现在我们来看看结构的成员可以有哪些呢。虽然在前面的的示例中我们只是简单的用了三种类型,但其实任何一个可以在结构外面声明的变量都可以作为结构的成员,比如,结构的成员可以是标量、数组、指针甚至其他的结构。比如
typedef struct
{
int a;
char b[5];
float *c;
Type x;
}Def;
在这个声明中,有整型变量a,字符型数组b,指向单精度浮点型数据的指针变量c,还有结构变量x。
我们前面了解了结构的成员组成,现在来探讨一下对结构成员进行初始化。与数组一样,结构成员的初始化在一个花括号中,不同成员用逗号隔开,而其内部的其他集合成员也用花括号,用逗号与其他成员数据分开。比如
struct EX
{
int a;
short b[5];
}x={1,{0,2,4,5,6}};
这样就对一个结构进行了初始化。
内存对齐
现在我们来了解一下结构的储存分配,我们先写下一个结构
typedef struct
{
char a;
int b;
char c[5];
}Type;
这里的类型 Type 有多大呢?按照数组的内存分配,我们试着猜想结构的内存大小就是结构的各个成员的大小相加,那么 Type 的大小就是三个成员的大小相加,则值为10个字节。真的是这样吗?我们来验证一下我们的猜想,在VS编译器下执行语句printf("%d\n",sizeof(Type)); 输出结果是否是10呢?我们来看一下
结果为16!这与我们的猜测相差6个字节。在我们的猜测中,每个成员在内存中都是紧挨在一起的,因此猜测中的大小为10个字节,而真实情况是此结构在内存中占用16个字节,所以可以肯定在结构所占用的内存中,有的存储位置上面没有存放数据。我们使用宏offsetof来得到结构中的变量a、b和c在内存中的存储位置相对于结构的起始位置偏移了多少个字节,我们在VS编译器中执行语句
printf("%d\n",offsetof(Type,a));
printf("%d\n",offsetof(Type,b));
printf("%d\n",offsetof(Type,c));
来看一下变量a、b和c的位置的偏移量
运行结果分别为0、4、8。可见,三个结构体成员在内存中并不是紧挨着的,而是像下面这样分配的
图中的6个白色框就是被浪费的内存。可见,结构的内存分配是按照某种机制来实现的,而这种内存分配方式叫做内存对齐。内存对齐的规则有
(1) 结构的第一个成员永远都放在结构的0偏移处。
(2) 从第2个成员开始,都是对齐到某个对齐数的整数倍处。(对齐数:结构成员自身大小和默认对齐数的较小值。默认对齐数在VS环境下为8个字节,在32位linux环境下为4个字节,64位linux中为8个字节,可通过#pragma pack()调节)
(3) 结构的总大小必须是最大对齐数的整数倍。
(4)如A结构体中嵌套有B结构体对象,那么这个B结构体对象的对齐数为B结构体内部成员的最大对齐数。
知道了内存对齐规则就不难理解为什么会浪费6个字节的空间了。a的位置对齐到0偏移处,b的对齐数为4,因此对齐到4偏移处,而数组c每个元素为字符型变量,对齐数为1,可在对齐在任意位置。现在我们可以调整结构成员的位置来节省某些浪费的空间
typedef struct
{
int b;
char a;
char c[5];
}Type;
这样将a和b的位置交换,然后在VS编译器中执行语句
printf("%d\n",sizeof(Type));
printf("%d\n",offsetof(Type,b));
printf("%d\n",offsetof(Type,a));
printf("%d\n",offsetof(Type,c));
我们来看一下运行结果
可见,现在结构减小了4个字节。而三个成员在内存中的位置是这样的
可见,这里只浪费了2个字节的空间,这样就可以减小结构内存空间的浪费。
为什么要结构体对齐?
1、平台原因:某些硬件平台只能在某些特定的地址去读取特定类型的数据,否则就会抛出异常。
2、性能原因:在我们看来,内存空间的排放就是一个个小的地址空间线性排放的,然而在CPU看来,内存空间就是一个一个的“块”,这些块可以是2、 4、 8、 16字节的,当CPU访问内存时,实际上就是每次访问一个“块”,因此,如果进行了内存对齐,当CPU拿到这一个块之后就可以直接进行读取了,而如果不进行内存对齐,那么当CPU拿到一个块后,还有继续对块内进行二次读取。因此进行内存对齐后,其读取速度会大大的提高。
讨论完结构的内存分配后,我们来探讨一下结构成员的访问。由于不能用下标访问的方式访问结构成员,因此我们要用别的方式来访问结构成员,分为直接访问和间接访问。
首先是直接访问,直接访问要用到点操作符“.”,在上面的结构基础上,我们创建一个结构变量T,然后运用点操作符“.”,表达 T.a 就访问了结构的成员a,访问其他成员也可用类似的方法。
然后是间接访问。在前面的结构的基础上,我们定义一个结构指针 Type *cp,此时cp就有能力指向Type型的结构变量,执行语句 cp=&T; 此时指针就指向了结构变量T,我们运用箭头操作符“->”,就可以访问变量T的成员了,比如访问成员a,我们可以用语句 cp -> a; 这样就访问了结构变量T的成员a。