在刚开始学习c语言时,我们就了解了整形、浮点型等常用的数据类型,把他们组合起来,就是我们今天学习的自定义类型。自定义类型包括结构体、枚举、联合。在编程的过程中,它们有不同的应用场景,下面我们一起来学习。
结构体的声明
结构体是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。
结构体的声明
struct tag
{
member-list;
}variable-list;
举例
描述一个学生:
struct student
{
char name[20];//姓名
short age;//年龄
char sex[5];//性别
}s1,s2;//s1,s2是全局变量;分号不能丢
特殊的声明(匿名结构体)
在声明结构的时候,可以不完全的声明
举例
struct
{
char name[20];
short age;
char sex[5];
}s1;//全局变量
上面的结构在声明的时候省略掉了结构体标签(tag)。
匿名结构体易错点
struct
{
char name[20];
short age;
char sex[5];
}s1;//全局变量
struct student
{
char name[20];
short age;
char sex[5];
}*p;
上面两个结构体定义的变量全都相同,那么我们可以写成p = &s1吗?
通过验证,这样写是不可以的,因为编译器会把上面的两个声明当成完全不同的两个类型,我们在写程序时要注意这一点。
思考一个问题:在结构中包含一个类型为该结构本身的成员是否可以呢?
我们先来看这段代码:
struct Node
{
int data;
struct Node next;
}
在这个结构体中包含了类型为该结构体本身的变量,这个代码会进入死递归,因为没有结束的条件,struct Node next找不到出口。
正确的自引用方式可以用指针来构造:
struct Node
{
int data;
struct Node* next;
};
自引用易错点
给匿名结构体重命名,在自引用时,不能使用它的重命名
typedef struct
{
int data;
Node* next;
}Node;
这样写代码的方式是错误的,因为编译器到Node* next时,Node还没有创建,编译器不知道Node*是什么,就会出现错误,这也是需要注意的点。
解决方法
typedef struct Node
{
int data;
struct Node* next;
}Node;
举例
struct Point
{
int x;
int y;
};
struct Node
{
struct Point p;
struct Node *next;
};
int main()
{
struct Node s1 = { { 3, 5 }, NULL };
printf("%d %d\n", s1.p.x, s1.p.y);
struct Point a = { 2, 3 };
printf("%d %d\n", a.x, a.y);
return 0;
}
计算下面结构体大小:
//练习一
struct s1
{
char c1;
int i;
char c2;
};
先来介绍一个函数offsetof: 计算结构体变量相对首地址的偏移量
size_t offsetof(structName, memberName);
头文件 #include
int main()
{
printf("%d\n", offsetof(struct s1, c1));
printf("%d\n", offsetof(struct s1, i));
printf("%d\n", offsetof(struct s1, c2));
return 0;
}
以编译结果来看,结构体里面的变量不是连续存储,不能直接用变量大小相加。实际上,计算结构体大小需要内存对齐。
结构体的对齐规则
以上面题为例:c1在偏移量为0的地址处,int i的大小为4字节,vs编译器默认值为8(选较小值),int i的对齐数为4,占偏移量为4~7的内存大小(对齐数的整数倍),char c2的大小为1字节,char c2的对齐数为1,占偏移量为8的内存大小,现在结构体总大小为9,最大对齐数为4,又因为结构体总大小为最大对齐数的整数倍,所以偏移量为10~11浪费,结构体总大小为12字节
例题
//练习二
struct S2
{
char c1;//成员大小1 默认对齐数8 对齐数1 0地址处
char c2;//成员大小1 默认对齐数8 对齐数1 1地址处
int i;//成员大小4 默认对齐数8 对齐数4 4-7地址处
//总大小是8字节 是最大对齐数的整数倍
};
//练习三
struct S3
{
double d;//成员大小8 默认对齐数8 对齐数8 0-7地址处
char c;//成员大小1 默认对齐数8 对齐数1 8地址处
int i;//成员大小4 默认对齐数8 对齐数4 12-15地址处
//最大对齐数8 总大小为16
};
//练习4-结构体嵌套问题
struct S4
{
char c1;//成员大小1 默认对齐数8 对齐数1 0地址处
struct S3 s3;//成员大小16 自己最大对齐数8 8-23地址处
double d; //成员大小8 默认对齐数8 对齐数8 24-31地址处
//总大小为32 是最大对齐数的整数倍
};
为什么存在内存对齐?
总体来说:
例如:上面的s1和s2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
使用#pragma预处理指令,可以改变默认对齐数
举例
#pragma pack(1)//设置默认对齐数为1
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
//修改的默认对齐数,只能是2^n(n=0,1,2,3……)
struct S
{
int data[1000];
int num;
};
struct S s = {{1,2,3,4}, 1000};
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
上面的 print1 和 print2 函数哪个好些?
首选print2函数。
函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。
如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能 的下降
总结: 结构体传参的时候,要传结构体的地址。
位段的声明和结构是类似的,有两个不同:
1.位段的成员必须是 int、unsigned int 或signed int 、char。
2.位段的成员名后边有一个冒号和一个数字。
//冒号后面的数字,指的是这个变量需要几个比特位来存储
struct A
{
int _a:2;//4字节 32位 用2位 剩30位
int _b:5;//用5位 剩25位
int _c:10;//用10位 剩15位
int _d:30;//不够用,重新开4字节
//共用8字节
//对于vs编译器,当比特位不够用时,需要重新开辟
}
位段的内存分配
位段的跨平台问题
总结: 跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。
定义:
enum Day//星期
{ Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
};
以上定义的 enum Day , enum Sex 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫枚举常量 。
这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。 例如:
enum Color//颜色
{
RED=1,
GREEN=2,
BLUE=4
};
枚举的优点
枚举的使用
enum Color
{
RED,
GREEN,
BLUE
};
int main()
{
enum Color c = RED;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
return 0;
}
联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员共用同一块空间(所以联合也叫共用体)。
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//例一
union Un
{
char c[5];//最少为5字节 要对齐到最大对齐数的整数倍
int i;//最大对齐数为4 总大小为8字节
};
//例二
union U
{
short s[7];//最少为14字节
int i;//最大对齐数4,总大小16
};
面试题
判断当前计算机的大小端存储
int a=0x11223344;
将a以大端字节序存储和小端字节序存储分别存储
思路:利用共用体的知识解此题 。因为共用体的特点是成员共用同一块空间 ,构造有int型和char类型成员变量的共用体,因为用同一块空间,所以访问char,就是访问int类型的第一个字节,再进行判断
union un
{
int i;
char c;
};
int is_check()
{
union un u;
u.i = 1;//00 00 00 01 判断大小端只需要判断第一个字节处是1还是0
return u.c;
}
int main()
{
int ret=is_check();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
总结:
我们要学会并掌握结构体内存对齐,因为在笔试题中这也是一个考点,我们要重视起来。在使用枚举的时候,尽可能列举出枚举变量的所有可能,这样在后面写代码的过程中,会方便一些。