结构体,枚举,联合体,内存对齐

本文深入探讨C语言中的自定义类型——结构体、枚举和联合体,详细阐述它们的声明、初始化、内存对齐规则以及位段的应用。同时,文章强调了内存对齐对性能的影响和如何避免空间浪费。枚举作为节省空间和提高可读性的工具,被推荐在定义全局变量时使用。
摘要由CSDN通过智能技术生成

前言

本篇介绍自定义类型:结构体,枚举,联合体。

对于结构体:重点掌握结构体,联合体的内存对齐,和结构体的位段

自定义类型

int ,int** ,float,double*等,这些都是C语言中内嵌的标准类型–直接用的。但是对于C语言没有内嵌的类型,需要我们通过某些关键字主动的定义,因此出现了 结构体,枚举,联合体。

结构体

什么是结构体

我们知道一种类型只能对应一种数据,依int为例,其对应整形。那么对于像人这种数据集合的,有没有对应的数据类型呢?—结构体

C语言规定了结构体,通过关键字 struct 的自定义类型。

结构体的声明

  • 我们称结构体中的类型为成员

  • struct 只是声明了一种数据类型,对于数据我们仍是存放在结构体变量中。同时struct只是一个关键字,不是真的的结构体。

  • 无论何时定义结构体变量或者指针 struct +tag都要有

  • 定义时结尾的不能丢。

struct tag
{
member-list;
}variable-list;
//tag--结构体类型标签
//member-list 结构体中的成员
// variable-list 结构体变量名

正常声明

//形式一
//声明结构体类型struct Stu,含有char,int类型的成员
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}//分号不能丢!!!!!!!!!!!!!
  
 //形式二
 //声明结构体变量S.
 struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}S;//分号不能丢 
     
  //形式三
      struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}//分号不能丢 
 int main()
      {
         struct Stu S;//声明一个结构体变量。
         struct Stu S[3];//声明一个结构体数组,数组中每个元素都是结构体类型
      }

特殊声明

在声明结构时,可以不完全声明

//无tag的匿名结构体,可以这样定义,非常不好,不建议怎么用
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], *p;




弊端:

  • p=&x;
    
*在编译器看来,上面无tag的2种声明是2种类型。因此p=&x;是非法的。*

+ *另外非常不利于再次定义结构体变量,可以看成是一次性用品*

结构体的自引用

//错误案例
struct Node
{
int data;
struct Node next;
}
//正确案例
struct Node
{
int data;
struct Node *next;
}
  • 这种在声明过程中加入一个本类型的成员,编译器会认为其是非法的,但是如果加入本类型的指针是没有问题的。
  • 博主在这里认为是因为:因为程序是一行一行读的,Node还在定义中(成员信息不全),自引用是非法的。但是对于结构体指针,只是一个指向结构体的指针,编译器不会报错的。

image-20211029181119852

结构体变量的成员初始化

结构体通过{}来依次对结构体成员赋值。

非嵌套结构体成员的初始化

//方式一:全局结构体变量的初始化
struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
struct Point p3 = { 1, 2 };//初始化



//局部变量的初始化
struct Stu //类型声明
{
	char name[15];//名字
    //注意字符数组初始化时要用“”
	int age; //年龄
};
int main()
{

	struct  Stu  S = { "zhangsan", 20 };//定义变量并初始化。
	return 0;
}


嵌套结构体成员的初始化

struct Point
{
	int x;
	int y;
};
struct Node
{
	int data;
	struct Point p;
	//注意这里可以加结构体变量的原因是:前面已经完整声明结构体类型
	struct Node* next;
};

struct Node n2 = { 20, {5, 6}, NULL };
//通过{} 来 初始化嵌套的结构体

结构体数组的定义与初始化

  • 数组可以存放很多种数据类型,自定义的结构体也是一种数据类型,因此有了结构体数组,数组中的每个元素都是结构体。
  • 数组所有知识,结构体数组全部适用
struct Stu //类型声明
{
	char name[15];//名字
	//注意字符数组初始化格式;""
	int age; //年龄
};
int main()
{
	struct Stu S[3] = { {"李四",23},{"张三",24},{"王二",25} };//定义结构体数组S,并初始化
	return 0;
}

结构体成员访问

  • 对于结构体变量,访问成员使用C语言关键字:“.”.

image-20211029184815735

  • 对于结构体指针,访问指向结构体的成员时有2种形式—更推荐第一种,方便,已理解。
    image-20211029184940974

注意

  • 结构体访问中:对于数组的访问得到的是数组首元素地址,但是对于数组中其它元素位置的访问,可以通过像下面的方式进行访问

image-20211029185638394

  • 对于指针成员的访问:得到的指针的值。

image-20211029190151931

结构体内存对齐!!!!!

内存对齐的规则

  • C语言内嵌的标准类型,在内存中存储时都有其字节大小,如整形-4字节,double-8字节,但是对于结构体这种自定义类型,其占据内存大小不是简单的内部成员大小相加的和
  • 对于数组成员,对齐数看的是一个元素的字节大小,不是整体
  • 对于结构体,C语言有规定:结构体的大小是内存对齐后的内存大小。

结构体内存对齐的规则:

image-20211029211745846

用例

struct S1
{
	char c1;
	int i;
	char c2;
};
int main()
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DPweTKN-1635521268675)(自定义类型.assets/image-20211029204836061-163551171727410.png)]

struct S2
{
	char c1;
	char c2;
	int i;
};
int main()
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

image-20211029204935016

struct S3
{
	int i;
	char c;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S3));
	return 0;
}

image-20211029205911151

struct S3
{
	int i;
	char c;
	double d;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S4));
	return 0;
}

image-20211029212329910

为什么存在内存对齐

  • 平台原因(移植原因):
    不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常.

  • 性能原因:
    数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问**

image-20211029213515543

  • 总体来说:

内存对齐是拿空间换时间。----注意现在的内存非常大,这些浪费的可以不考虑。

怎么尽量避免因内存对齐,导致的空间浪费

  • 在设计结构体成员时:

让占用空间小的成员尽量集中在一起 。

struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
  • S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别

修改编译器的默认对齐数

  • 如果觉得默认对齐数不合适时,可以更改默认对数

#pragma 这个预处理指令,可以改变我们的默认对齐数

#pragma pack(4)//设置默认对齐数为4
struct S3
{

	int i;
	double d;

};
#pragma pack()//取消设置的对齐数,还原为默认对齐数8

struct S4
{
   int i;
	double d;
};

int main()
{
	printf("%d\n", sizeof(struct S3));
	printf("%d\n", sizeof(struct S4));


	return 0;
}

image-20211029215334475

结构体传参

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函数。
  • 原因:
    函数传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
  • 结论:
    结构体传参的时候,要传结构体的地址

位段

什么是位段

  • 不会解释,上代码。哈哈~ ~ ~
struct A
{
int a:2;
int b:5;
int c:10;
int d:30;
};//声明A是一个位段类型
  • a本来是4字节存入内存,经过位段后,a存入内存是2bite-----注意是bite,不是字节哦!。

  • 因此,位段的本质就是数据存储的大小进行截断改变。

这可以看出,位段可能会导致数据的流失,

  • 位段的声明和结构是类似的,有两个不同

  • 位段的成员必须是 整形家族(char。int);

  • 位段的成员名后边有一个冒号和一个数字

位段的内存分配

  • 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟存入数据的。
  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
struct S
{
char a:3;
char b:4;
char c:5;
char d:4;
};
struct S s = {0};
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;
//空间是如何开辟的?

image-20211029222954867

位段的跨平台问题

  • int 位段被当成有符号数还是无符号数是不确定的。

  • 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。

  • 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
    当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

  • 总结:跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在

位段的应用

  • 我们知道在网络上传输数据时,如果传输结构类型的数据,因为空间的浪费,会非常大,影响传输速度,但是如果使用位段就可以极大提高传输速度.

枚举

什么是枚举

  • 枚举顾名思义就是一一列举。把可能的取值一一列举。
    比如我们现实生活中:

  • 一周的星期一到星期日是有限的7天,可以一一列举。
    性别有:男、女、保密,也可以一一列举。
    月份有12个月,也可以一一列举

  • 这里就可以使用枚举了

枚举的定义

enum Day//星期
{
Mon,
Tues,
Wed,
Thur,
Fri,
Sat,
Sun
};
enum Sex//性别
{
MALE,
FEMALE,
SECRET
}enum Color//颜色
{
RED,
GREEN,
BLUE
};
  • 以上定义的 enum Day , enum Sex , enum Color 都是枚举类型。

  • 枚举{}里面的,称谓枚举整形常量—常量,一旦定义了就不可修改了,枚举常量具有全局性。

  • 注意最后一个枚举常量,后面没有“,”;

  • 这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值 ,不过也仍保留依次递增的特点

image-20211029224601659

枚举的优点

  • 枚举常量具有全局性且不可修改,因此可以代替#define声明的宏,原因:

  • 增加代码的可读性和可维护性

  • #define定义的标识符比较枚举有类型检查,更加严谨。

    image-20211029225314970

  • 防止了命名污染(封装)

  • 便于调试

    image-20211029225456903

  • 使用方便,一次可以定义多个常 量

  • 鉴于怎么多优点,多多使用哈哈~~

联合体

联合类型的定义

  • 联合体也是一种特殊的自定义类型
    这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;

  • 联合体变量的定义位置和结构体一样,全局或者局部都行,尽量别无名定义

联合的特点

  • 联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为
    联合至少得有能力保存最大的那个成员)
union Un
{
int i;
char c;
};
union Un un;
// 下面输出的结果是一样的吗?
printf("%d\n", &(un.i));
printf("%d\n", &(un.c));
//下面输出的结果是什么?
un.i = 0x11223344;
un.c = 0x55;
printf("%x\n", un.i);

image-20211029230300214

  • 由此可见不能随便使用联合体

联合体大小

  • 联合的大小至少是最大成员的大小。

  • 看对齐数,数组仍是数组元素,但是对于成员大小看的的数组整体大小。

  • 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍 .

union Un1
{
char c[5];
int i;
};
//5不是最大
union Un2
{
short c[7];
int i;
};
int main(){
printf("%d\n", sizeof(union Un1));
printf("%d\n", sizeof(union Un2));
}

image-20211029231504936

小结

  • 结构体的内存对齐是常考点,必须要掌握。
  • 如果我们定义全局变量时,可以多多使用枚举,优点多多。
  • 23
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值