前言
我们已经学习了编译器自带的类型:int 、 char 、long 、float 、double… 。除了这些类型,编译器还允许我们自己设计类型。
自定义类型包括:结构体 、 枚举 和 联合。
一、结构体
1.什么是结构
结构体是一些值的集合,被称为结构体成员。
数组:相同类型变量的集合。
结构:可以是不同类型变量的值。
2.结构体的声明
结构体声明的必要条件是关键字:struct
和结构体成员
.
为什么说结构体标签不是必要的呢?看3.匿名结构体。
这里我们用书来举一个例子:
struct Book
{
char name[20]; //名字
float price; //价格
int page; //页码
};
int main()
{
struct Book b = { "追风筝的人" , 39.9f , 299 };
return 0;
}
3. 特殊结构体的声明
结构体中有一个特殊的结构体:匿名结构体。
匿名结构体的定义省略了结构体标签。
struct
{
int a;
char c;
}Y;
注意:若要使用匿名结构体则必须在其后面定义结构体变量,否则就没机会使用了。
4. 结构体的自引用
结构体成员能否有自身类型的变量??
答案是不可以。当计算下面第一个结构体的大小的时候,struct Node
里还包含着一个struct Node
,显然是错误的。
struct Node
{
int date[10];
struct Node next;
};
所以结构体内部只能有自己类型的指针变量。
struct Node
{
int date[10];
struct Node* next;
};
typedef能够将类型名起别名,那么typedef能够给匿名结构体起别名并且在结构体内定义一个自己类型的指针变量呢??
这里typedef
将匿名结构体起别名为Node
,但是结构体成员有一个Node*
的结构体成员,这里就像是没有先定义一个变量而先使用这个变量,所以显然是错误的。
//error
typedef struct
{
int a;
Node* next;
}Node;
那么我们应该怎么正确的起别名并在结构体内定义一个自己类型的指针变量呢??看下面一个代码。
typedef struct Node
{
int a;
struct Node* next;
}Node;
5. 结构体变量的定义和初始化
5.1 结构体变量的定义
结构体变量的定义有两种方式:
struct Node
{
int a;
char c
}s3,s4; //(1) //全局变量
struct Node s5; //(2) //全集变量
struct Node s6;
int main()
{
struct Node s1 ; //(2) //局部变量
struct Node s2 ;
return 0;
}
5.2 结构体变量的初始化
结构体变量的初始化也有两种方式:
(1)按顺序初始化
(2)按自己想要的顺序(按成员变量初始化)
注意:结构体的初始化与数组的初始化相同都需要{}
struct Node
{
int a;
char c;
};
struct A
{
char m; //结构体的嵌套
struct Node s1;
}
int main()
{
struct Node s1 = {200 , 'v'}; //(1)
struct Node s2 = { .c = 'v' ,.a = 100 }; //(2)
struct A a = {'y' , {1000 , 'x'}}; //结构体的嵌套初始化
return 0;
}
6. 内存对齐
6.1 内存对齐的四大规则:
(1)结构体的第一个成员始终放在偏移量为0的地方。
(2)结构体的第二个成员及以后,每个成员对齐到对齐数要与偏移量成整数倍的地方。
对齐数:成员变量与默认对齐数较小的那个值。
(3)结构体的大小要是结构体中所有成员中的最大对齐数的整数倍,否则浪费空间补齐。
(4)如果嵌套了结构体,那么结构体要对齐到自己最大对齐数与偏移量成整数倍的位置。整个结构体的大小要是最大对齐数的整数倍,最大对齐数包括嵌套结构体内成员的对齐数。
6.2 例子:
struct s1
{
char a;
int b;
char c;
};
struct s2
{
char a;
char c;
int b;
};
struct s3
{
double a;
char c;
int b;
};
struct s3
{
double a;
char c;
int b;
};
struct s4
{
char y;
struct s3 s;
int x;
};
6.3 内存对齐总结
(1)内存对齐就是用空间换取时间
(2)为了在对齐的情况下并使用更小的内存,应将占用空间小的成员集中在一起(不强制要求顺序的前提下)
7. 修改对齐数
#pragma pack(4)//设置默认对齐数为4
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
8. 结构体传参
函数传参的时候需要压栈,需要消耗时间和空间。
关于下面这两个函数 Print1
与 Print2
,前者是传值调用,后者是传址调用。
当将整个结构体传入函数的时候,若结构体过大,压栈的消耗会更多,性能会下降。而将结构体指针传入函数时,无论结构体多大,一个指针也就 4 / 8
个字节 ,压栈的时候不会消耗太多。
总结 : 结构体传参使用结构体指针
#include <stdio.h>
struct Node
{
int date;
char name[100];
};
Print1(struct Node n) //传值调用
{
printf("%d\n", n.date);
}
Print2(struct Node* pn) //传址调用
{
printf("%d\n", pn->date);
}
int main()
{
struct Node n = {100 , "chineseperson04"};
Print1(n);
Print2(&n);
return 0;
}
二、位段
1. 什么是位段
位段与结构体相似,但不同的是:
(1)位段的成员必须有整数家族的。
(2)位段的成员后面要加一个冒号和数字。
注意:(数字的单位是比特)
struct M
{
int _a : 2;
int _b : 4;
int _c : 10;
int _d : 30;
};
2. 位段的内存分配
若成员类型为 int
则先开辟 4 个字节,若成员类型为 char
则先开辟 1 个字节,不够再开辟空间。
由于位段不跨平台,并且目前用的少,这里就不深入讲解了。
三、枚举
1. 枚举的定义
enum Sex //性别
{
MALE,
FAMALE,
SECRAT
};
枚举中定义的成员叫做枚举常量。
枚举常量默认从0
开始,一次加一。(在不初始化的前提下)
枚举成员可以从第一个成员开始初始化也可以在后面初始化。
2. 使用枚举的优点
(1)与#define
相比,枚举有类型为 enum
,更严谨。
(2)增加代码的可维护性和可读性。
(3)便于调试,调试阶段可以看到枚举变量的值是多少,而 #define
在编译的时候就将它定义的宏变为了值。
(4)与 #define
相比枚举可以一次定义多个变量。
四、 联合(共用体)
1. 联合类型的定义、声明
union un //声明
{
char a;
int b;
};
union un u; //定义
2.联合的特点
联合内的成员公用一个空间。
#include <stdio.h>
union un
{
char a;
int b;
};
union un u;
int main()
{
printf("%p\n", &u);
printf("%p\n", &(u.a));
printf("%p\n", &(u.b));
return 0;
}
3. 联合大小的计算
(1)联合内成员共用一个空间,所以联合的大小至少是联合成员中大小最大的一个。
(2)联合与结构体一样需要内存对齐。所以联合的大小应该是联合成员中最大对齐数的整数倍,否则浪费空间补齐。
#include <stdio.h>
union un1
{
char arr1[5];
int b;
};
union un2
{
char arr1[7];
int b;
};
int main()
{
printf("%d\n", sizeof(union un1));
printf("%d\n", sizeof(union un2));
return 0;
}
结尾
如果有什么建议和疑问,或是有什么错误,希望大家能够提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,希望能给我一个小小的赞!