C语言:自定义类型详解(结构体/枚举/联合)

1. 结构体

结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。(这里我们可以联想到数组,数组是一组相同类型元素的集合)
1.1 结构体的声明

struct   tag     //tag是结构体标签名,根据实际情况来定
{
  member-list;    //成员列表,里面每一个叫成员变量
}variable-list;  //变量列表,可以省略根据需要定义,分号一定要有

例如用结构体描述一个学生类型:

struct  Stu //Stu就是标签名
{
  char name[20]; //姓名
  int age; //年龄
  char id[20];  //学号
}s1,s2,s3; //在创建时定义三个结构体变量s1,s2,s3,注意分号不要丢

这里的是s1,s2,s3可以不直接创建,我们还可以在定义好结构体后再创建,如

struct Stu  s1,s2,s3;  //和上面效果一样

除此之外,结构体还有特殊的声明,那就是在声明结构体的时候,不完全声明,这种结构被称为匿名结构体类型。比如:

struct  //省略了标签名
{
  int a;
  char b;
}x;  //由于省略了标签名,这里只能直接在后面创建变量
 
struct  
{
  int a;
  char b;
}*p;  //定义一个该结构体的指针,可以存放该结构体变量的地址

问:上面两个结构体类型成员一样,可以*p=&x;吗?
答:不可以,编译器不会认为这两个结构体是一种类型,会把它们当成两种结构体类型,不建议使用这种类型,更好的方法是写上标签名
1.2 结构体内存对齐(结构体大小计算)

结构体内存对齐规则
1.结构体的第一个成员放在相较于结构体变量起始位置的偏移量为0的位置
2.第二个成员开始,后面的每个成员都要对齐到某个对齐数的整数倍处
对齐数:每个结构体成员自身大小和默认对齐数的最小值
在有些编译器上会有默认对齐数,例如VS上默认对齐数是8,而gcc编译器上没有默认对齐数,那么直接取成员对齐数的最小值
3.结构体的总大小:必须是最大对齐数的整数倍
最大对齐数:所有成员对齐数的最大值
4.如果结构体内嵌套了结构体的情况,嵌套的结构体对齐到自己最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

下面通过两个例子来理解:

计算该结构体类型大小
struct S1
{
  char c1;
  int i;
  char c2;
};
在这里插入图片描述
答:12字节,由图可以知道会有一部分浪费的空间(棕色部分)
printf(“%d”, sizeof(struct S1));  //我们也可以用这个语句打印出来该结构体的大小

第二个题:同样计算该结构体的大小

struct S2
{
  char c1;
  char c2;
  int i;
};
在这里插入图片描述
答:8字节

S1和S2类型的成员一模一样,但是他们所占的空间却不一样,所以我们在设计结构体的时候,既要满足对其,也要尽量节省空间,让占用空间小的成员尽量集中在一起,就会更加节省空间
另外,对于数组,例如int arr[3];我们只需要把它当成三个int型变量放进内存即可,计算方式一样。

若是对齐方式不合适时,我们也可以自己更改默认对齐数,使用如下语句即可

#pragma pack(1);  //设置默认对齐数为1,一般我们都设置成2n
#pragma pack();  //取消设置的默认对齐数,还原为编译器上的默认值

若默认对齐数为1,意味着什么?说明不需要再对齐,直接连续存放即可,可以当成没有对齐数了

为什么会有内存对齐?
查阅到相关资料,有两个原因
(1)平台原因:并不是所有的硬件平台都能够在任意地址上访问任意类型的数据。某些硬件平台对于数据的访问有特定的要求,即数据必须存储在某些特定的地址上,如果数据没有正确对齐,则可能会导致硬件异常或者降低性能。例如,一些处理器要求某些类型的数据必须对齐到特定的边界上(如2字节边界、4字节边界等),否则可能会触发错误或警告。
(2)性能原因:为了提高数据访问的速度,现代处理器通常支持在自然边界上对齐的数据的快速访问。如果数据不对齐,处理器可能需要多次内存访问才能完成一次数据读取或写入操作,这降低了处理速度。相反,如果数据在自然边界上对齐,处理器可以一次性完成数据的读取或写入,提高了访问效率。

对于性能原因
假设有char型变量c和int型变量i在内存中如图所示
对于32位机器,字长就是32位,字长决定了一次读写数据是32bit,也就是4个字节(一字节等于8bit)
在这里插入图片描述

1.3 结构体的自引用
如何在结构体中包含一个类型为该结构体本身的成员?

方法一:
struct Node
{
  int data;
  struct Node next;
};
方法二:
struct Node
{
  int data;
  struct Node*next;
};

问:上面哪种方法可行?若可行,该结构体类型大小如何如何计算?
答:方法二可行,对于方法一,无法算出第二个成员next的大小,next是一个struct Node类型,一个next里面包含data和next,而所包含的这个next又包含一个data和next,由此下去无穷无尽,无法计算出next成员的大小,而方法二中,next变量是一个指向struct Node变量的指针,我们知道指针的大小为4或8个字节,是可以计算的。
总结:结构体类型里想包含同类型结构体变量,应该用同类型的结构体指针,指针指向需要找到的下一个结构体变量。

有时我们觉得结构体类型名太复杂,用起来不方便,可以使用关键字typedef来重定义类型名

typedef  struct S1
{
  char c1;
  int i;
  char c2;
}S1;   //当有了typedef后,这里的S1就不是定义的结构体变量,而是重新定义的结构体类型名
S1 stu1,stu2,stu3;  //重定义后便可以直接使用S1来定义变量

下面这个代码正确吗?

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

答:错误,不能提前使用Node,只能在定义好之后使用

1.4 结构体的定义和初始化

struct SN
{
  char c;
  int i;
}sn1={‘q’,100}, sn2={.i=200,.c=‘w’};  //全局变量
//sn1是直接按照结构体成员的顺序初始化,而sn2是不按照顺序,两种方法都可以
int main()
{
  struct SN sn3,sn4;  //局部变量,sn3,sn4没有初始化
  printf(“%c %d”, sn2.c, sn2.i);  //打印格式
return 0;
}

结构体嵌套的初始化

struct SN
{
  char c;
  int i;
}sn1={‘q’,100}, sn2={.i=200,.c=‘w’};
struct S
{
  double d;
  strucr SN sn;  //一个结构体类型里包含另一个结构体类型成员
  int arr[3];
int main()
{
  struct S s={ 3.14, {‘a’,99}, {1,2,3} };
  printf(“%lf %c %d\n”, s.d, s.sn.c, s.sn.i);
  int i=0;
  for(i=0;i<3;i++)
  {
   printf("%d ",s.arr[i]);
  }
return 0;
}
在这里插入图片描述

1.5 结构体传参
我们知道向函数传参有值传递和地址传递,若想把结构体传递给函数我们选择哪一种方式呢?是直接传递结构体变量还是传结构体变量的地址?

struct S
{
  int num;
  char c;
}s={5, ‘a’};
void print1(struct S s)  //结构体变量传参,接收结构体变量,这里的s是形参
{
  printf(“%d\n”.s.num);
}
void print2(struct S* p)  //结构体地址传参,这里的p是结构体指针变量
{
  printf(“%d\n”,p->num);
}
int main()
{
  print1(s);  //传结构体变量
  print2(&s);  //传结构体变量的地址
  return 0;
}

上面两种传参方式都可以,但是第二种方式更好。若直接传变量,函数会使用同类型的形参来接收,而形参是实参的临时拷贝,也就是说函数会在内存上开辟一块空间来拷贝s变量,这个过程既需要时间也需要空间,若结构体过大,系统的开销也会更大,可能会导致性能的下降。若传地址,就不需要开辟大量的空间,直接根据地址去找到原结构体的位置进行操作。
总结:结构体传参的时候,要传结构体的地址

以上就是结构体的全部内容,还有枚举和联合的相关内容,未完待续…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值