结构体 AND 枚举 AND 联合

结构体

导语

 结构是一些值的集合,这些值称为成员变量。结构体的每个成员可以是不同类型的变量。

结构的声明

 在结构体的声明中,必须列出他所包含的所有成员,这个列表包含它所有成员的类型和名字。

struct tag  //结构体标签(一般不为空)
{
    member - list;    //成员变量:类型+变量名(不能为空)
}variable-list;     //变量列表(可省略)
//注意:结构体最后的那个;不能丢!!!

例如:用结构体来表述一个学生的信息

struct Student
{
    char id[20];//学号
    char name[32];//名字
    int age;//年龄
    char sex[5];//性别
};

这里没有创建变量,所有,不占用内存空间,只有创建时才开辟空间

特殊的声明

在声明结构的时候,可以不完全的声明。
例如:

//匿名结构体类型
struct
{
    int a;
    char b;
    float c;
}x;
//这个声明创建了一个变量x,它包含了三个成员,一个整数、一个字符、一个浮点数
struct
{
    int a;
    char b;
    float c;
}y[20], *z;
//这个声明创建了两个变量y和z。y是一个数组,它包含了20个结构。z是一个指针,它指向这个类型的结构

上面的两个结构体在声明的时候省略掉了结构体标签(tag)。

在上面代码的基础上,下面的代码合法吗?

z = &x;

 答:不合法,编译器会把上面的两个声明当成完全不同的两个类型,即使他们的成员列表完全相同。因此,变量y和z的类型和x的类型不同。所以,z=&x是不合法的。
警告
(1).编译器会把上面的两个声明当成完全不同的两个类型。
(2).匿名类型时,不可省略变量列表,否则会找不到创建的结构体。

结构成员

结构的成员可以是标量、数组、指针,甚至是其他结构体。

结构体成员的访问
struct Stu
{
    char name[20];
    int age;
};
1.结构成员的直接访问

 结构体变量访问成员是通过点操作符(.)访问的。点操作符接受两个操作数,左操作数就是结构变量的名字,右操作数就是需要访问成员的名字。
这里写图片描述

int main()
{
    struct Stu s;   //定义结构体变量,形如字符型变量、整型变量所示
    printf("%s\n", s.name); //由于没有初始化,所以是随机值
    printf("%d\n", s.age);

    strcpy(s.name, "zhangsan"); //使用.访问name成员
    s.age = 20; //使用.访问age成员

    printf("%s\n", s.name);
    printf("%d\n", s.age);

    system("pause");
    return 0;
}

这里写图片描述

2.结构成员的间接访问

根据上述的介绍,如果我们拥有一个结构体指针,应该如何访问结构体成员的。
我们如果通过上述的(.)操作符来操作符的话,会想到先对它解引用操作,然后通过(.)来操作,由于(.)操作符的优先级高于间接访问操作符,所以,又要需要加个()。
因为太麻烦了,所以,提供了一个更方便的操作符->(箭头操作符),箭头操作符有两个操作数,左操作数必须是一个指向结构的指针,右操作数指向结构变量的成员。举例如下图所示:

void print(struct Stu *ps)
{
    printf("%s  %d\n", (*ps).name, (*ps).age);
    printf("%s  %d\n", ps->name, ps->age);
}

这里写图片描述

结构的自引用

在结构中包含一个类型为该结构本身的成员是否可以?

struct Node
{
    int data;
    struct Node next;
};

答案:不可以,如果可以的话,sizeof(struct Node) 是多少呢?所以,这种自引用是非法的,成员next是一个完整的结构,里面包含了一个整型数字和又一个成员next,那么这个成员next又是一个完整的结构,又包含了一个整型数字和又一个成员next,如此循环没有止境,就像一个循环没有截止条件。

正确的自引用方式应该是:一个结构体包含指向该结构体本身的指针,其实它是指向同一种类型的不同结构体,这种说法有点像我们数据结构中的单链表,一个结构体里面包含了指向下一个结构体的指针。下面我们用代码和图理解一下一下:

struct Node
{
    int data;
    struct Node* next;  //将下一个节点的地址存放
};

这里写图片描述
那是不是只要我结构体中包含下一个结点的地址,或者说是结构体里面有一个结构体指针就可以自引用成功了吗?
看下面的代码

typedef struct Node //typedef是重命名
{       //就说将struct Node写为Node_t,就会少些一个struct 
    int data;
    Node_t* next;
}Node_t;

这样真的可以吗?
答案是:不可以,Node_t 直到最后才定义,在结构体声明内部,它并没有定义,所以是错误的。所以在结构体内部的必须用结构标签去声明next指针。

typedef struct Node
{
    int data;
    struct Node* next;   //结构体内部必须带结构体标签
}Node_t;
结构体的不完整声明

如果有可能定义出下面的结构:

struct A
{
    int a;
    struct B *p;
};
struct B
{
    int b;
    struct A *p;
};
  • 这样可以吗?
    答案是:不可以,向这种结构体,一个结构是在另一个结构体的基础上声明的,但是每一个结构体都引用了了其他结构体的标签,这样应该如何声明呢?
    解决方案是:不完整声明,声明一个作为结构体标签的标识符。就如同分苹果,今天这里有三个人分苹果,其中两个人来了,一个人迟到了,但是迟到的同学说他会过来,那大家就会给他分出属于他的苹果等着他来认领。代码表现为:
struct B;
struct A
{
    int a;
    struct B *p;
};
struct B
{
    int b;
    struct A *p;
};
  • 在A的成员列表中需要B的不完整声明,一旦A被声明之后,B的成员列表也就可以被声明了。
结构的定义和初始化
结构体的定义

有了结构体类型,那如何定义变量,其实很简单。
结构体的定义有两种类型,一种是就如同整型、浮点型、字符型数字定义一样。另一种在生命类型的同时定义变量。

struct Stu
{
    char name[20];
    int age;
}stu1;  //声明类型的同时定义变量stu1
//注意这里没有typedef,所以大家不要人为stu1是重命名
struct Stu stu2;    //定义结构体变量stu2
结构体变量的初始化

结构体的初始化和数组初始化类似。一个位于一对{}内部、由,分隔开的初始值用来初始化结构各个成员的初始化。可在定义变量的同时进行初始化赋值,结构中如果包含数组或结构体,就使用结构体嵌套初始化,类似与多位数组的初始化,再用一个{}内部进行数组或结构体的初始化。

struct Stu stu3 = {"zhangsan", 20}; //定义变量的同时初始化
struct Node
{
    int data;
    struct Stu;
    struct Node* next;
}n1 = { 10, {"lisi", 20}, NULL };   //结构体嵌套初始化
struct Node n2 = { 20, { "wangwu", 23 }, NULL };    //结构体嵌套初始化
结构体内存对齐<重点>

首先我们要掌握结构体内存对齐规则:
1. 第一个成员与结构体变量偏移量为0的地址处。
2.其它成员变量要在对齐的某个数字(对齐数)的整数倍的地址处。对齐数 = 编辑器默认的一个对齐数 与 该成员大小的较小值。vs中默认的值为8 Linux默认的对齐数为4
3.结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己最大的对齐数的整数倍处,结构体整体的大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
让我们做几道小练习来练练手吧!

//练习一:
struct student
{

};
printf("%d\n", sizeof(struct student));  //1

这个虽然是一个结构体,但是结构体内部没有成员,大家是不是就认为它是0个字节,但是我在Dev便一起上测试了一番,它的结果是1。如果我们吧编译器当做一个模具,模具的容积能为0吗?
具体详解:请看《C语言深度解剖》这本书

//练习二:
struct S1
{
    char c1;
    int i;
    char c2;
};
printf("%d\n", sizeof(struct S1));  //12

分析过程如下图所示:
这里写图片描述

//练习三:
struct S2
{
    char c1;
    char c2;
    int i;
};
printf("%d\n", sizeof(struct S2));  //8

分析结果如下图所示:
这里写图片描述

//练习四
struct S3
{
    double d;
    char c;
    int i;
};
printf("%d\n", sizeof(struct S3));   //16

分析过程如下图所示:
这里写图片描述
总结:通过练习二—四,我们可以知道在设计结构体对时,既要满足对齐,又要节省空间,应做到让占用空间小的成员尽量集中在一起

//练习五(结构体嵌套问题):
struct S4
{
    char c1;
    struct S3 s3;  //为联系三的结构体
    double d;
};
printf("%d\n", sizeof(struct S4));  //32

分析过程如下图所示:
这里写图片描述
为什么存在着内存对齐呀?
1.平台原因(移植原因)
 不是所有的硬件平台都能访问任意地址上的任意数据的:某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2.性能原因
 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问。
总体来说:
结构体的内存对齐就是,浪费空间,换取时间
如何修改系统的默认对齐数呀?

#pragma pack(4)  //()里面的数字一般不为奇数,一般为2的次方数且>2
                 //()里面的数字为所设置的默认对齐数
//结构体声明

#pragma pack()  //取消设置的默认对齐数
                //取消时()里面的要不不写,要不就写1
//例题:
#pragma pack(4)
struct S5   //这个是上联练习五,我在vs2013上面,系统默认对齐数为8,我现在修改为4
{             //系统默认 成员大小 对齐数 所占内存
    double d; //4       8       4     0-7
    char c;   //4       1       1     8
    //浪费9-11
    int i;    //4       4       4     12-15
}; //总大小计算为16,最大对齐数为4,16为4 的倍数,所以结构体占16个字节
#pragma pack()
结构体传参
struct S
{
    int datd[100];
    int num;
};
//结构体传参
void print1(struct S s)
{
    printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* s)
{
    printf("%d\n", s->num);
}
int main()
{
    struct S s = { { 1, 2, 3, 4 }, 1000 };
    print1(s);//传结构体
    print2(&s);//传地址

    system("pause");
    return 0;
}

上面的print1 和 print2函数哪个好一些?
答案肯定是:print2函数。
原因:
 在学习函数栈帧时,函数传参时,参数需要压栈。如果传递一个结构体时,即结构体过大,参数压栈的系统开销比较大,所以会导致性能下降。
结论:结构体传参的时候,要传地址


位段

位段的声明和结构是类似的,有两个不同:
- 位段的成员必须声明为int、unsigned int 和 signed int
- 位段的成员名后边有一个冒号和 一个整数。(这个整数指定了改为短所占位的数目)

struct A
{
    int a : 2;
    int b : 5;
    int c : 10;
    int d : 30;
};
//A是一个位段类型
printf("%d\n", sizeof(struct A));  //???

这里写图片描述
如果按照存放一来存放的话,位段A就占6个字节;按照存放二来存放的话,位段A就占8个字节。还有就是大小端存储的问题。

位段的内存分配
1.位段的成员可以是int、 unsigned int、 signed int 或者是char (属于整形家族)类型。
2.位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的。
3.位段设计很多的不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

位段的跨平台问题
1.int位段被认为是无符号数还是有符号数——不确定。
2.位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器中是会出现问题的)
3.位段中的成员在内存中从左向右分配还是从右向左分配——不确定
4.当一个结构体包含两个位段,第二个位段成员比较大,取法容乃于第一个位段剩余的位是,是舍弃剩余位还是利用——不确定。
总结
 跟结构体相比。位段可以达到同样的效果,虽然可以很好的节省空间,但是有跨平台的问题存在。


枚举

例如:
    1.一周的星期一到星期天是有限的七天,可以一一列举
    2.性别有男、女、保密三种情况,也可以一一列举
    3.月份有十二个月,可以一一列举

枚举解释就是将一个事物的所有可能取值一一列举。

枚举的定义
enum Day//星期
{
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
    Sun
};
enum Sex//性别
{
    MALE,
    FEMALE,
    SECRET
};

以上定义的enum Day 和 enum Sex都是枚举类型。
{}中的内容是枚举类型的所有可能取值,称为枚举常量。这些可能取值都是有值的。默认从0开始,依次递增1,当然在定义的时候也可以直接赋初值。

enum Color
{
    RED = 1,
    GREEN = 2,
    BLUE = 4

};
枚举的优点

我们有#define 定义常量,为什么要使用枚举?

  1. 增加代码的可读性和可维护性
  2. 和#define定义的标识符比较,枚举有类型检查,更加严谨
  3. 防止了命名污染
  4. 便于调试
  5. 使用方便,一次可以定义多个常量
枚举的使用
enum Color
{
    RED = 1,
    GREEN = 2,
    BLUE = 4

};
enum Color clr = GREEN;

只能用枚举常量给枚举变量赋值,才不会出现类型差异


联合(共用体)

联合类型的定义

联合也是一种自定义类型,这种类型定义的变量也包括一系列成员,特征是:这些成员公用同一块空间(所以联合也叫共用体)。
联合的定义和声明与结构体相同。

//联合类型的声明
union Un
{
    char c;
    int i;
};
//联合变量的定义
union Un un;
//计算联合变量的大小
printf("%d\n", sizeof(un)); //4个字节

这里写图片描述

联合的特点

联合的成员是共用同一块空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少的有能力保存最大的那个成员)。

union Un
{
    char c;
    int i;
};
union Un un;
//下面输出的结构是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//答案是:一样的

//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);
//11223355

这里写图片描述
面试题
判断当前计算机的大小端存储

联合大小的计算
  • 联合的大小至少是最大成员的大小。
  • 当最大成员不是最大对齐数的整数倍的时候,就要对其道最大对齐数的整数倍。
union Un1
{               
    char c[5];  //1
    int i;      //4
};              //最大对齐数为4
union Un2
{
    short c[7]; //2
    int i;      //4
};              //最大对齐数为4
//下面输出什么结果呀?
printf("%d\n", sizeof(union Un1));//8
printf("%d\n", sizeof(union Un2));//16
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值