目录
自定义类型包括三种:结构体,联合体,枚举;下面我们对结构体进行详解;
结构体与数组的区别主要在与:结构体可以存放不同类型的变量;
1>.结构体的使用方法
struct STU
{
char name[10];
int age;
char sex[10];
}s1 = { "翠花",20,"female" } ;
int main()
{
struct STU s2 = { "熊大",20,"male" };
printf("%s %d %s", s2.name, s2.age, s2.sex);
return 0;
}
结构体包含:结构体名称(可以没有),内容,变量(可以没有);
其中的s1和s2的区别就是s1是全局变量,而s2是局部变量;在给结构体初始化的时候,如果不想写结构体内元素的名称,就需要按照结构体内容的顺序来初始化,如果不按照顺序就需要,写出其元素的名称,比如:{ .age=20, .name="熊大" , .sex="male" };
在打印的时候用:结构体名称 . 结构体内容;
2>.结构体的不完全声明
//结构体的不完全声明
struct
{
char name[10];
int age;
}s1 = {"cai",18};
int main()
{
return 0;
}
当结构体不完全声明的时候,因为它没有名字,所以结构体的变量只能创建一次,且为全局变量;而且不能用指针和地址ps=&s1来让两个结构体相同,因为其没有名字,编译器会认为他们是不同类型的结构体;
3>.结构体的自引用
数据结构
数据结构是数据在内存中的组织结构,其中包含线性数据结构,线性数据结构包含两种顺序表和链表,是数据储存的两种不同的方式;
结构体的存放方式也有这两种,所以如果我们想通过一个结构体找到下一个结构体我们应该通过指针的方式,找到下一个结构体的地址。
结构体的自引用
自引用包含数据域和指针域,指针域是用来寻找下一个结构体的;
struct stu
{
int num;
struct stu *next;
};
int main()
{
return 0;
}
注意:应该使用地址,而不是结构体,因为这样会导致无法计算结构体的大小的;
用typedef重命名结构体
//typedef重命名结构体
typedef struct STU
{
int age;
}Node; //将中间的一个整体命名为Node
int main()
{
Node s1 = { 20 };
return 0;
}
typedef struct STU
{
int age;
Node* next; //错误的使用方法,
//因为还没来得及重命名就是用了Node
}Node; //将中间的一个整体命名为Node
int main()
{
Node s1 = { 20 };
return 0;
}
4>.结构体的内存对齐
结构体的内存对齐是用来计算结构体的大小的;
可以看到两个结构体的内容是相同的,但是两个结构体的大小却不相同,这是因为结构体的成员在内存中存在对齐现象;
offsetof宏
offsetof宏是用来计算结构体成员相较于结构体变量起始位置的偏移量;头文件<stddef.h>,返回值类型是size_t;
可以看到,c1和c2都只需要占用一个字节的空间,但是a相较于起始位置是4,中间空了一个字节没有使用,还是被其他数据占用了,接下来我们对内存对齐规则进行讲解;
内存对齐规则
1)结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址;
2)其他成员要对其到对齐数的整数倍的地址处;
对齐数=min{编译器默认的对齐数,该成员变量的大小};vs默认对齐数是8,linxu中gcc编译器默认是0;
3)结构体总大小是最大对齐数(结构体中每个变量都有自己的一个对齐数,不包含编译器默认的对对齐数)的整数倍;
4)如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整数大小就是所有最大对对齐数(含嵌套结构中成员的对齐数)的整数倍。
第一种情况刚好8比特位,是最大对齐数的整数倍,所以其大小是8;第二种情况是9个比特位,不是4的整数倍,扩大到4的整数倍是12。
嵌套结构体,嵌套的成员看的是其内部的最大对其数,而不是把嵌套结构的总大小当作最大对齐数;
内存对齐的原因
1)平台的原因(移植原因)
不是所有的硬件平台都能够访问任何地址的数据,有些平台只能在特定的位置访问特定类型的数据,而如果此时数据是按照顺序紧挨着放的,就会出现访问错误,抛出硬件异常的问题;
2)性能原因
数据结构(尤其是栈)应该尽可能地在自然边界上对齐;原因在于,为了访问未对齐的内存,处理器可能需要做两次访问,而对于对齐的内存访问,只需要一次访问;该原因在位段中会详细讲解;
总的来说:结构体的内存对齐规则就是为了用空间来换取时间;
如果想要让结构体占据的空间更小,可以让所需空间小的成员集中放置(就是在写结构体的时候,把空间小的成员写在一起);
5>.修改默认对齐数
使用# pragma pack(a)来修改编译器的默认对齐数,其中修改默认对齐数为a;用# pragma pack()来恢复默认对齐数,()中什么都不写;
6>.结构体传参
结构体传参也包含两种:传址调用,传值调用;
传址调用
与变量相同传址调用就是将地址传过去;
//结构体的传址调用
struct s
{
int arr[5];
char a;
};
void print(struct s s2)
{
for (int i = 0; i < 5; i++)
printf("%d ", s2.arr[i]);
printf("%c", s2.a);
}
int main()
{
struct s s1 = { {1,2,3,4,5},'c' };
print(s1);
return 0;
}
传址调用
struct s
{
int arr[5];
char a;
};
void print(struct s* s2)
{
for (int i = 0; i < 5; i++)
printf("%d ", s2->arr[i]);
printf("%c", s2->a);
}
int main()
{
struct s s1 = { {1,2,3,4,5},'c' };
print(&s1);
return 0;
}
注意:结构体在传参的时候,尽量传地址;因为传参的时候,参数要进行压栈,会有时间和空间上的开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销较大,所以会导致性能下降。
7>.结构体实现位段
因为结构体需要占用更多的空间,所以我们创建位段来降低其空间占有;
位段的声明和结构体是相似的,有两种不同:
1)位段的成员必须是:int,unsigned int,signed int或char,在C99中位段成员的类型也可以选择其他类型;
2)位段的成员后面有一个冒号和数字;
//位段
struct S
{
char c;
int a;
int d;
int e;
};
struct S2
{
char c : 1;
int a : 2;
int d : 5;
int e : 10;
};
位段中的是“位”是二进制位;其冒号后面的数字表示给他分配的空间,占用几个比特位,也就是二进制位。
位段的内存分配
位段的空间按照需要以4个字节(int)或一个字节(char)的方式来将开辟的;
以VS为例:VS使用空间的时候是从右向左使用的;当剩下空间不足以存放下一个成员的时候,VS会浪费掉这一部分空间,开辟新的空间;
struct S
{
char a : 2;
char b : 5;
char c : 5;
char d : 4;
};
int main()
{
struct S s1 = { 10,12,3,4 };
return 0;
}
注意:以上都是在VS集成开发环境下的情况,在其他编译器下可能不太一样,这也是位段的弊端。
位段使用的不确定性
位段的跨平台问题
1)int类型是看成有符号整形还是无符号整形是不确定的(看编译器);
2)位段中最大位的数目是不确定的(16位机器最大时16,32位机器最大时32,在早期的16位机器上,sizeof(int)=2);
3)位段中的成员在内存中是从左向右分配还是从右向左分配时不确定的,标准尚未定义;
4)当一个结构包含两个位段的时候,第二个位段成员比较大,无法容纳第一位段剩余的位时,时舍弃剩余的位还是利用,时不确定的;即当开辟的1个字节(或4个字节)不够容纳下一个成员的时候,是紧挨着放,还是再开辟1个字节(或4个字节)从头放 是不确定的;
总结:跟结构体相比,位段可以达到结构体的效果,并且更好的节省空间,但是位段存在跨平台问题;
位段使用的注意事项
位段在使用的时候,可能出现一个字节被多个变量使用,有些成员的起始位置不是某个字节的起始位置,而内存只给每个字节分配空间,不给bit位分配空间,所以不能对位段成员只用&,也不能用scanf来输入位段成员的值;