结构体是统一在同一个名字之下的一组相关变量的集合。
结构体可以包含不同数据类型的变量——与只包含相同数据类型元素的数组相反。将结构体和指针联合使用,可以实现更复杂的数据结构,如链表、队列、堆栈和树。
结构体的定义
结构体属于派生数据类型,即它们是用其他数据类型的对象来构建的。
看下这个结构体的定义:
struct card{
char *face;
char *suit;
};
定义一个结构体的关键字是struct
,标识符card是对所定义结构体的命名,花括号内声明的变量称为结构体card的成员。
同一个结构体类型中的成员必须具有不同的名字,但不同的结构体类型中的同名成员不会引发冲突。每个结构体的定义都必须用一个分号来结束! 否则会产生语法错误。
自引用结构体
一个结构体不能包含它自身类型的实例,但是,指向同一个结构体的指针却可以出现在结构体的定义中。
struct A{
struct A a;
struct A* ptr;
};
上面这个struct A中,包含它自身的实例(a)。但是对于成员(ptr),由于它是一个指针,则允许出现在结构体定义中。
在这种结构体定义中出现自身结构体类型的指针成员的结构体,称为自引用结构体。
定义结构体类型的变量
结构体定义并不占用任何的空间,它只是创建了一种新的可用来定义变量的数据类型。定义结构体变量与定义其他类型的变量完全一样。
struct card aCard,deck[66],*cardPtr;
上面这条定义语句的含义分别是:声明一个类型为struct card的变量aCard、容量为66的数组deck、指针cardPtr。
声明具有某种结构体类型的变量,还可以通过在结构体定义的花括号和分号之间,加上用逗号分隔的变量名列表的方式来完成。例如:
#include<stdio.h>
struct card {
char *face;
char *suit;
}aCard,deck[66],*cardPtr;
int main(){
printf("%p\n%p\n%p\n",&aCard,&deck[0],&cardPtr);
}
可见,这些变量都是有地址的,系统已经为它们分配了空间,完成了变量的定义。
上面代码中结构体标记名card是可以省略的。但是省略标记名card后,那么改结构体类型变量的声明只能与结构体定义同时进行,不能单独声明。
结构体的初始化
与数组一样,可以采用初始化列表来初始化结构体。即在结构体变量定义语句中的变量名后边,加上赋值号和用一对花括号括起来的初始化列表,来初始化结构体变量。初始化列表中,不同的初始值用逗号隔开。例如下面这个声明:
card c1 = {"Three","Hearts"};
创建了一个card类型的变量c1,两个成员分别初始化为"Three","Hearts"
。
若列表中初始值的个数少于结构体中成员的个数,则剩余的没有初始值与之对应的成员将自动地初始化为0(成员为指针时初始化为NULL)。对于定义为全局变量的结构体变量,若没有显示的初始化,则将自动地初始化为0或NULL。
结构体变量的初始化还可以通过赋值语句来完成。例如可以将一个同类型的结构体变量赋值给另一个结构体变量,或者分别对结构体变量中的每个成员分别赋值。
对结构体的合法操作
-
将结构体变量赋值给其他具有相同类型的结构体变量,对指针成员,仅复制存储在指针的地址,不会多拷贝一份指针所指的内容;
#include<stdio.h>
#include<stdlib.h>
struct card{
char *face;
char *suit;
};
int main(){
card c1;
c1.face = (char*)malloc(10);
card c2 = c1;
printf("%p\n%p\n", c1.face, c2.face);
}
上面的程序可以看出,把一个struct card类型的c1赋值给c2,它们指针存储的地址是相同的。
所以在释放c1.face
指针所指向的内存时,用c2.face
访问该指针所存储的内存时会产生非法访问,导致程序崩溃。
在结构体内有指针类型的成员时,赋值时需要考虑浅拷贝和深拷贝的问题。
-
可以用运算符&取得结构体变量的地址;
使用示例:
#include<stdio.h>
struct card{
char *face;
char *suit;
};
int main(){
card c1;
card* p = &c1;
}
-
访问一个结构体变量中的成员;
有两种运算符可用来访问结构体的成员:一个是结构体成员运算符(.),也称为原点运算符; 另一个是结构体指针运算符(->),也称为箭头运算符。
使用示例:
#include<stdio.h>
#include<stdlib.h>
struct card{
char *face;
char *suit;
};
int main(){
card c1 = {"Three","Hearts"};
card* p = &c1;
printf("%s\n%s\n",c1.face,p->face);
printf("%s\n%s\n",c1.suit,p->suit);
}
-
用sizeof求结构体变量所占的字节数。
用sizeof求结构体变量所占的字节数,并不是简单的把各个成员的字节数相加。
在计算机存储系统中以字节为单位存储数据,不同的数据类型所占的空间不同,例如:整型数据(int)占4个字节、字符型数据(char)占1个字节、浮点型数据(float)占四个字节…
计算机为了快速的读写数据,默认情况下将数据存放在某个地址的起始位置,例如:整形数据(int)默认存储在地址能被4整除的起始位置、字符型数据(char)可以存放在任意的起始位置(被1整除)、浮点型数据(float)默认存储在地址能被4整除的起始位置。
这样的存储方式称为字节对齐。字节对齐有助于加快计算机的取数速度,减少花费的指令周期。
结构体的字节对齐的细节和具体编译器实现相关,但一般而言满足以下三个准则:
- 结构体变量的首地址能够被其最宽基本类型成员的大小所整除;
2)结构体每个成员相对于结构体首地址的偏移量都是当前成员大小的整数倍,如有需要编译器会在成员之间加上填充字节;
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节。
(注:基本类型是指char、short、int、float、double这样的内置数据类型)
下面用几个程序来举例说明:
#include<stdio.h>
#include<stdlib.h>
struct A{
char c;
int a;
};
int main(){
A a;
printf("A::%d\n",sizeof(a));
}
对于结构体A,其成员类型有char 和int两种,最宽的是int型,占四个字节。那么系统就会为A分配一个能被4整除的首地址。
分配成员地址时,A的第一个成员就是结构体A的首地址。
接下来分配int型成员时,为满足该成员的首地址能被4 整除,系统会填充三个字节来满足这个要求,使其相对A的首地址偏移量为4。
所以结构体A的字节数为 1(char) + 3(填充字节) + 4(int)= 8;内存分布如下:
1、数据成员顺序对求sizeof的影响
在给结构体数据成员分配空间时,是按前后顺序依次分配的。因为字节对齐的要求,数据成员的顺序也会对结构体求sizeof有影响。
#include<stdio.h>
struct A{
int a;
double b;
char c;
};
struct B{
double b;
int a;
char c;
};
int main(){
A a;
printf("A::%d\n",sizeof(a));
B b;
printf("B::%d\n",sizeof(b));
}
可以看到,调换成员顺序后,也会影响对结构体求sizeof的结果。
A的内存分布如上图,对最宽的double型8个字节对齐,先给int型的数据成员a分配四个字节,在给double分配时,为保证地址可以整除8,需要填充4个字节,在给b分配空间。
B的内存分布如上图,也是对最宽的double型8个字节对齐,先给double数据成员b分配8个字节,后给int型数据成员a和char型数据成员c分配时不需要填充字节依然满足内存字节的要求。
可见,数据成员的顺序会影响结构体成员的内存分布,合理的顺序可以节省所分配的空间。
2、结构体嵌套
#include<stdio.h>
struct B{
struct{
int a;
char c;
}b;
char c;
int a;
};
int main(){
B b;
printf("B::%d\n",sizeof(b));
}
B的内存分布如上图,在类内的结构体变量b分配完后,同样会遵循结构体大小是最长内存整数倍的要求,b分配完成员空间后会填充三个字节满足这个要求,然后再去分配B的其他成员。