在讨论自定义数据类型之前,我们不妨先回忆一下C语言的内置类型。例如字符型的char,整型中的int short long 以及浮点型的 float double,这些都会C语言本身提供的数据类型,但仅仅有这些,是不足以满足我们的开发的。那么也就意味着需要一些复杂类型来帮助我们实现对复杂对象的操作,例如结构体,枚举,联合体等。
结构体
本章主要讨论结构体。将由以下几个部分组成
结构体类型的声明
结构的基础
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。(那么看到了集合我们又会想到数组,回忆一下数组,数组指的是相同类型的一个集合。这是他们的区别。)
结构的声明
struct tag{
member-list;//成员列表
}variable-list;//变量列表
例如描述一个学生
生命一个结构体类型同时给出学生的属性(成员变量)
struct Student {
char name[20];//姓名
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
};//注意分号不能少
可以由以下几个成员组成,char类型数组描述姓名,int类型变量描述年龄,char类型变量描述性别,以及最后的char类型表示学号,当然还可以自己添加更多的信息比如成绩 手机号家庭地址等等。
如何使用呢?用的时候我们需要创建结构体变量s1,s2来使用
int main(){
//创建结构体变量
struct Student s1;
struct Student s2;
}
除此之外还有匿名结构体类型
struct {
int a;
char b;
float c;
}x;
我们可以看到 struct后面是没有名字的,但是在大阔号的后面有个x我们创建的时候就要用到这个x来创建变量。后面这个x就是 变量列表。我们在是用的时候直接使用x.成员变量即可。
同时我们也可以用一个指针变量来代表这个结构体。
struct {
int a;
char b;
float c;
}*xa;
当然这里需要注意的是,x和*xa都是结构体,同时里面的成员变量 也相同,那我们可不可以将x的地址给xa也就是xa这个指针指向x得地址呢?
xa=&x;
这样是会出警告的,这就是需要注意的地方,即便是同样的结构体成员变量也相同,但是在编译的时候,系统会认为这两个结构体是两个不一样的类型。这是一种非法操作。
结构体重命名
这里要用到typedef这个关键字
typedef struct Student{//这里的Student是不能省略的
char name[20];//姓名
int age; //年龄
char sex[5]; //性别
char id[20]; //学号
}Student;
int main(){
//我们在声明变量得时候可以这样
struct Student s1;
//或者直接使用重命名后的Student进行定义
Student s2;
}
这里需要注意 使用typedef的时候 后面的Student是不能省略的。
结构的自引用
在结构体中包含一个类型为该结构本身的成员是否可以呢?
这里我们参考数据结构中的链表,定义一个结构体类型Node,思考一下这样定义可以吗?
struct Node{
int data;
struct Node n;
};
如果可以的话,那么这个结构体Node他占多少个字节能算出来吗?
其实这里是会报错的。原因就是结构体不能自己里面包含自己这个变量,你是没办法计算出来这个结构体到底有多大的。所以这种写法是错误的。
那么到底应该怎么写?
struct Node{
int data;//数据域
struct Node* next;//指针域
};
应该放的是下一个结构体的指针,这样的话每个结构体的大小是能够算出来的。整型的四个字节以及地址的4/8给字节。
结构体变量的定义和初始化
有了结构体类型,那如何定义变量呢?其实也很简单。
struct AA{
char c;
int a;
double d;
char arr[20];
};
int main(){
struct AA a={'c',100,3.14,"hello 51cto"};
printf("%c,%d,%lf,%s",a.c,a.a,a.d,a.arr);
return 0;
}
如果结构体中嵌套了个结构体的话那么初始化应该怎么初始化呢?
struct T{
double weight;
short age;
};
struct S{
char c;
struct T st;
int a;
double d;
char arr[20];
};
int main(){
// struct AA a={'c',100,3.14,"hello 51cto"};
// printf("%c,%d,%lf,%s",a.c,a.a,a.d,a.arr);
struct S s={'c',{56.5,19},100,3.14,"hello 51cto"};
printf("%lf\n",s.st.weight);
return 0;
}
可以看到结构体S中嵌套了一个结构体T,在对S进行初始化的时候我们需要对T也进行初始化,其实只需要在其中加入大阔号{}然后在里面使用同样的初始化方式初始化即可。
然后输出的时候找到声明的变量 s.st.weight 或者 s.st.age。
结构体内存对齐
内存对齐我们先看一下如何计算结构体所占的字节数,也就是说我们创建了一个结构体他是在内存中占据了多大的空间,这个怎么计算。
struct S1{
char c1;
int a;
char c2;
};
struct S2{
char c1;
char c2;
int a;
};
int main(){
struct S1 s1={0};
printf("%d\n",sizeof (s1));
struct S2 s2={0};
printf("%d\n",sizeof (s2));
return 0;
}
大家可以自己算一下这两个结构体大小是否一致,以及大小为多少?
这个代码输出结果是:
s1 的内存空间
s2的内存空间
为什么会出现这种情况呢?
答案是因为有结构体的内存对齐。首先我们先讨论结构体的对齐规则:
1.第一个成员在结构体变量的偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数=编译器默认的一个对齐数与该成员大小的较小值。vs中的默认值为8 ,gcc没有默认对齐数(成员大小就是对齐数)当然我们可以自己修改默认对齐数语法为:
#pragma pack(4)
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有的最大对齐数(含嵌套结构体的对齐数)的整数倍。
大家可以认真思考一下上面的四个规则。然后我们分析一下为什么是这样。给出内存示意图
首先对于s1:
根据上面的规则 结构体的首地址开始计算,
第一个类型因为第一个类型为char 占据1个字节 相对于8来说1比较小所以直接找1的倍数存即可那么就从第一个位置存c1。第一个绿色格子
第二个成员变量类型为int 占据4个字节。相对于8来说4比较小,那么就往下找这片空间4的倍数,往下数三个格。拿出4个格子橙色部分来存储
然后第三个成员变量char类型 占据1个字节,相对于8来说1比较小,所以找1的整数倍直接在橙色下面找第一个格存即可。第二个绿色格子
到此为止我们已经找到了内存空间的分布与存储了,但是有个问题,这里加起来也就才占据9个字节。怎么算出来是12呢?
那么这就要用到第三个规则了。结构体的总大小也要对齐最大对齐数的整数倍,s1的最大对齐数是4,所以最后也要是4的整数倍,距离9最近的整数倍是12,所以最后存储就是12。
那也许有人会问绿色和橙色部分中间那些空间怎么办呢?那些空间依照规则实际上就浪费了。
那依照这种方式大家可以自行分析第二个为什么是8了。
接下来为了让大家理解最后一个规则,我在给出一个例子,也就是结构体嵌套结构体。
struct S3{
double d;
char c;
int i;
};
struct S4{
char c1;
struct S3 s3;
double d;
};
int main(){
struct S3 s3={0};
printf("%d\n",sizeof (s3));//16
struct S4 s4={0};
printf("%d\n",sizeof (s4));//32
return 0;
}
最后的结果是
我们先算s3, 首先double类型占据8个字节 它又是第一个成员变量那么从第一个地址处开始存储。占据了8个字节。第二个成员变量为char 1个字节,那就继续往后存储,现在总共9个字节了。但是第三个成员变量int类型 占据4个字节,它需要找到4的倍数,离9最近的4的倍数是12所以从12这个位置开始存,占据4个字节,所以总共是16个字节,又因为16是8的整数所以这个s3就占据16个字节。
然后再来计算s4,char 类型占据一个字节从第一个位置开始存储,紧接着要存储s3,根据规则4,s3自己的最大对齐数为double类型的8个字节。所以这个s3的最对齐数为8所以我们要往下数7个字节,然后再存储s3 存16个字节那目前就占据了内存的24个字节了中间有7个字节是浪费的。最后存储double类型,8个字节 ,24也是8的倍数所以从24往后存即可,存到32个字节。根据规则3,结构体的大小要是最大对齐数的倍数,32也是8的倍数,所以不用往后再加字节数了。所以最终的大小就是32。
计算偏移量的宏offsetof
struct S3{
double d;
char c;
int i;
};
struct S4{
char c1;
struct S3 s3;
double d;
};
int main(){
struct S3 s3={0};
printf("%d\n",offsetof(struct S4,c1)) ;//0
printf("%d\n",offsetof(struct S4,s3)) ;//8
printf("%d\n",offsetof(struct S4,d)) ;//24
}
这里有个宏是可以直接算出来结构体中成员变量的偏移量的,这个大家要会用。
但是我们这里会发现有了这个内存对齐后,会有空间的浪费,那么为什么还需要内存对齐呢?
关于这个问题,官方是没有给出具体的原因的。但是根据大部分资料的原因有两个。
1.平台原因:不是所有硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取的某些特定类型的数据,否则抛出硬件异常。
2.性能原因:数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
总的来说内存对齐,算是拿空间换取时间的做法。
结构体传参
我们之前使用结构体都是直接在main函数中对齐进行赋值然后输出,例如
struct S{
int a;
char c;
double d;
};
int main(){
struct S s;
s.a=100;
s.c='b';
s.d=35,6;
printf("%d",s.a);
return 0;
}
算是一种使用,但实际上结构体并不是这样用的。那我们假设写一个初始化结构体的函数如下init
struct S{
int a;
char c;
double d;
};
void init(struct S tmp){
tmp.a=102;
tmp.d=3.14;
tmp.c='aa';
}
int main(){
struct S s={0};
init(s);
// s.a=100;
// s.c='b';
// s.d=35,6;
printf("%d",s.a);
return 0;
}
能不能实现对结构体的赋值呢?最后的输出结果是否是102呢?
答案是
之所以这样的原因是结构体tmp是另外一个新生成的结构体,我们虽然修改了tmp但是并没有影响原本的结构体s所以这里需要传递地址。怎么修改呢?看下面
struct S{
int a;
char c;
double d;
};
void init(struct S* tmp){
tmp->a=102;
tmp->d=3.14;
tmp->c='aa';
}
int main(){
struct S s={0};
init(&s);
// s.a=100;
// s.c='b';
// s.d=35,6;
printf("%d",s.a);
return 0;
}
输出结果:
这样就实现了初始化原本的那个结构体s,所以结构体传参数,要传递其地址。
除了这个原因以外,结构体所占据的空间比较大,如果不传递地址,在时间和空间的开销比较大,所以结构传递参数尽可能的传递地址。