【一起来学C】自定义类型——结构体,枚举,联合

自定义类型:结构体,枚举,联合

在C语言中,不但有着int,float,char,double,short这样的已经定义好的类型,还有着可以自己定义的类型,下面我们就一起来看一看吧。

结构体的声明

//struct是结构体关键字
struct tag//tag是结构体标签
{
	member-list;//是成员列表
}variable-list;//是结构体变量列表

具体的例子:

struct book
{
	char name[10];
	char tuthor[10];
}b1,b2;//创建了两个结构体类型的全局的b1,b2变量

struct book b3;//和上面创建的b1,b2没有任何区别,都是全局变量

int main()
{
	struct book b4;//创建了一个结构体类型的局部变量b4
    b4.name="1234";
	return 0;
}

使用typedef进行结构体声明

//struct是结构体关键字
struct tag//tag是结构体标签
{
	member-list;//是成员列表
}newname;//别名

具体的例子:

typedef struct number
{
	int x;
	int y;
}num; 
int main()
{
	num s1;//直接写num即可,不用写struct number
	s1.x = 9;
}

注意:

一旦使用了别名,就不可以再在声明的时候就创建变量了,因为别名占据了原来创建变量的地方

特殊的声明

有一种不带标签的结构体,但是必须在声明的时候定义

struct
{
	int a;
	char b;
	float c;
}x,y;//必须在声明的时候定义
int main()
{
    //这样也是可以使用的
	x.a = 1;
	x.b = 2;
	x.c = 3;
	y.a = 0;
}

除非使用typedef

typedef struct
{
	int a;
	char b;
	float c;
}s;
int main()
{
	s x;
	s y;
	x.a = 1;
	x.b = 2;
	x.c = 3;
	y.a = 0;
}

结构体的自定义

对于结构体来说,在结构体内部添加一个结构体是不是就是结构体的自定义了呢?

就像下面这样:

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

答案是不可以,因为结构体里面含有结构体,一层一层的嵌套,永远在套娃,所以这样不叫作结构体的自定义。

应该改成下面这种指针的模式

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

这样通过指针,我们也可以找到下一个结构体变量

结构体的初始化

struct Point
{
	int x;
	int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2
//初始化:定义变量的同时赋初值。
struct Point p3 = {x, y};
struct Stu //类型声明
{
	char name[15];//名字
	int age; //年龄
};
struct Stu s = {"zhangsan", 20};//初始化
struct Node
{
	int data;
	struct Point p;
	struct Node* next;
}n1 = {10, {4,5}, NULL}; //结构体嵌套初始化
struct Node n2 = {20, {5, 6}, NULL};//结构体嵌套初始化

结构体的内存对齐

计算结构体的大小的时候我们就要考虑结构体的内存对齐的因素了。

注意:这是一个很重要的考点

这时候我们就要知道结构体的内存对齐的规则

  1. 结构体的第一个成员永远放在结构体起始位置偏移量为0的位置上。(就是放在结构体的首位置)

  2. 结构体从第二个成员开始,总是放在偏移量为对齐数的整数倍处。

    对齐数=编译器的默认对齐数和变量自身大小的最小值

    Linux无默认对齐数

    VS的默认对齐数是8

  3. 结构体的总大小是每个成员变量的对齐数最大的整数倍

  4. 如果存在结构体的嵌套,那么嵌套的结构体对齐到自己的最大对齐数的整数倍处,整个结构体的大小就等于所有元素的最大对齐数的整数倍(包含嵌套结构体的最大对齐数)

那么,以下面得到几个为例,我们具体来分析以下:

struct S1
{
	char c1;
	int i;
	char c2;
};

在这里插入图片描述
答案是12,这就是根据上面的3条规则算出来的。

但是,同样的成员变量,改变了顺序,结构体的大小会不会有变化呢?就像下面这样:

struct S2
{
	char c1;
	char c2;
	int i;
};

我猜,很多人一开始肯定以为这个和上面的没什么区别,但是好好的套用上面的三条规则再看一下呢?

在这里插入图片描述
很没有想到,原来就是结构体改变了顺序,结构体的大小也会变化,这都是因为我们的结构体内存对齐的三条原则。

  1. 练习3

    //练习3
    struct S3
    {
    	double d;
    	char c;
    	int i;
    };
    

在这里插入图片描述
4. 练习4——结构体嵌套问题

//练习4-结构体嵌套问题
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

在这里插入图片描述

为什么存在结构体对齐:

  1. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址
    处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器
    需要作两次内存访问;而对齐的内存访问仅需要一次访问。

所以,这是一种以空间换时间的方法,用以提高时间效率:
在这里插入图片描述

那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

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

修改默认对齐数

之前我们见过了 #pragma 这个预处理指令,这里我们再次使用,可以改变我们的默认对齐数。

下面是例子:

#include <stdio.h>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
	char c1;
	int i;
	char c2;
}
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为8
struct S2
{
	char c1;
	int i;
	char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
	printf("%d\n", sizeof(struct S1));
	printf("%d\n", sizeof(struct S2));
	return 0;
}

第一个答案是12,第二个答案是6

offsetof 宏的实现

百度有这样一道面试题:

写一个宏,计算结构体中某变量相对于首地址的偏移,并给出说明

这个时候,我们就可以使用offsetof函数来实现

函数原型:

size_t offserof(struct s1,struct s1.n);

第一个参数是结构体变量的类型名,第二个参数是成员名

下面是具体实现的例子:

#include<stddef.h>
struct s1
{
	char c;
	int i;
	char d;
};
int main()
{
	printf("%u\n", offsetof(struct s1,c));
	printf("%u\n", offsetof(struct s1, i));
	printf("%u\n", offsetof(struct s1, d));
	return 0;
}
//答案是0 4 8

结构体传参

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;
}

从上面的传参可以看出,有两种形式:结构体传参,结构体地址传参

那个更加高效呢:

答案是结构体地址传参,因为传值的时候会进行实参的拷贝,拷贝到形参,在压栈的过程中,很有占用过多内存,可能发生栈溢出。

所以,在都可以达到目标效果的前提下,当然选择更加高效的传地址的方法。

位段

位段也是结构体中的一种表现形式

什么是位段

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

1.位段的成员必须是 int、unsigned int 或signed int 。
2.位段的成员名后边有一个冒号和一个数字。

比如:

struct A
{
	int _a:2;
	int _b:5;
	int _c:10;
	int _d:30;
};

位段成员变量中冒号后面是要开辟的空间

同样的,我们也会考虑位段的大小问题

这个就要考虑在位段中内存是如何分配的了:

位段在内存中的分配:

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

下面以一个例子来介绍:

//一个例子
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;

它们在内存中的分布是这样的。

对于这个结构体s来说,它是一个字节,一个字节开辟的。

  1. 首先,先开辟一个字节,先存放a占据3个bit
  2. 再存放b,占据4个bit
  3. 接着要去存放c,但是内存已经不太够用,所以需要再开辟一个新的字节
  4. 将占用5个bit的c放入新开辟的字节中
  5. 接下剩下的bit还是也不够用,再次开辟新的字节,将d放入。

在这里插入图片描述
接着将a,b,c,d的值按照存入的bit位放入就可以,最后在内存中显示的大小是0x620304

未开始赋值的时候是根据位段开辟了3个字节
在这里插入图片描述
都赋值后,
在这里插入图片描述

位段的跨平台问题

  1. int 位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题。
  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。
  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位
    还是利用,这是不确定的。

总结:

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

位段的应用

在这里插入图片描述
在网络传输的过程中,对于一个数据,我们往往要附加上一系列其他的IP地址,协议等。把他们打包成一个整体。

这时候把他们改变成位段就可以很好的解决问题了,这样就可以减少空间的浪费,挺高网络传输的效率。

枚举

枚举类型是一种特殊的结构体类型。

那么什么类型的变量适合使用枚举呢?那就是个数是有限的元素集适合使用枚举。枚举枚举,就是要把所有的变量都列举出来,所以枚举变量的个数必须是有限的。

枚举类型的定义:

和结构体类似

注意:

  1. 最开头写上enum,并且还要写上枚举常量的标签
  2. 成员常量之间用逗号分隔开,最后一个常量不做任何处理

具体的例子:

enum Day//星期
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
enum Sex//性别
{
	MALE,
	FEMALE,
	SECRET
}enum Color//颜色
{
	RED,
	GREEN,
	BLUE
};

以上定义的 enum Dayenum Sexenum Color 都是枚举类型。 {}中的内容是枚举类型的可能取值,也叫 枚举常
量 。

枚举的性质

  1. 枚举常量自身具有值,默认是从0开始,如果改变的话,依次加1

    enum Color//颜色
    {
    	RED = 1,
    	GREEN = 7,
    	BLUE
    };
    int main()
    {
    	printf("%d\n", RED);//1
    	printf("%d\n", GREEN);//7
    	printf("%d\n", BLUE);//8
    	return 0;
    }
    
  2. 枚举常量最开始可以赋值

    enum Color//颜色
    {
    	RED=1,
    	GREEN=2,
    	BLUE=4
    };
    
  3. 枚举常量是常量,不可以被修改

enum Color//颜色
{
	RED = 1,
	GREEN = 7,
	BLUE
};
int main()
{
	GREEN = 6;//这样编译器会报错
	return 0;
}
  1. 具体的使用

    只能将枚举常量赋予枚举变量。

    enum Color//颜色
    {
    	RED = 1,
    	GREEN = 7,
    	BLUE
    };
    int main()
    {
    	enum Color c = GREEN;//enum Color代表一个单独的变量,不是结构体
    	printf("%d", c);//7
    	return 0;
    }
    

枚举的优点:

我们可以使用 #define 定义常量,为什么非要使用枚举? 枚举的优点:

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

例子:

enum Option
{
	EXIT,//0
	ADD,//1
	SUB,//2
	MUL,//3
	DIV//4
};

void menu()
{
	printf("******************************\n");
	printf("**** 1. add     2. sub    ****\n");
	printf("**** 3. mul     4. div    ****\n");
	printf("**** 0. exit              ****\n");
	printf("******************************\n");
}

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case ADD:
			//加法
			break;
		case SUB:

			break;
		case MUL:

			break;
		case DIV:

			break;
		case EXIT:

			break;
		}
	} while ();
	return 0;
}

这样的使用枚举类型,使程序更加具有可读性,更加容易理解。

联合

联合体也叫共用体。这个联合体很不简单因为它所有的成员公用同一个内存

联合体的声明:

union un
{
	int i;
	char c;
};
int main()
{
	union un u;//创建一个联合体变量
	printf("%p\n", &u);
	printf("%p\n", &u.c);
	printf("%p\n", &u.i);
	return 0;
}

上面打印的三个答案是相同的。因为i和c共同使用同一块空间

判断计算机大小端

原来算法:

请大家判断以下这样是正确的吗?

int main()
{
	int x = 0x11223344;
	char y = (char)x;
	printf("%x", y);
	return 0;
}

使用强制转换类型无论如何都是取到x的第一个字节,而不是内存中第一个字节,所以这样的作法是绝对错误的。

正确的做法:(直接从给出内存中的地址来进行取字节访问)

int main()
{
	int x = 0x11223344;
	char* p = (char*)&x;
	printf("%x\n", *p);
	if (*p == 0x44)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

使用联合体:

union un
{
	int i;
	char c;
};
int main()
{
	union un u;
	u.i = 0x11223344;
	printf("%x\n", u.c);
	if (u.c == 0x44)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

联合体的大小计算

计算的原则:

联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍

union Un1
{
	char c[5];
	int i;
};
union Un2
{
	short c[7];
	int i;
};
//下面输出的结果是什么?
int main()
{
	printf("%d\n", sizeof(union Un1));//8,最大对齐数为4
	printf("%d\n", sizeof(union Un2));//16,最大对齐数为4
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言中,结构体是一种自定义数据类型,可以将不同类型的变量组合在一起,形成一个新的数据类型结构体定义的基本形式如下: ```c struct 结构体名 { 数据类型 成员名1; 数据类型 成员名2; // ... }; ``` 其中,结构体名是用户自定义的名称,成员名是结构体中每个成员的名称,数据类型可以是任意C语言的数据类型,包括基本数据类型自定义数据类型结构体变量的定义方式如下: ```c struct 结构体名 变量名; ``` 读取结构体中的成员变量可以通过“.”运算符来实现,例如: ```c #include <stdio.h> struct Person { char name[20]; int age; }; int main() { struct Person p; printf("请输入姓名:"); scanf("%s", p.name); printf("请输入年龄:"); scanf("%d", &p.age); printf("姓名:%s,年龄:%d\n", p.name, p.age); return 0; } ``` 枚举是一种特殊的数据类型,用于定义一组常量。枚举的定义方式如下: ```c enum 枚举名 { 常量名1, 常量名2, // ... }; ``` 其中,枚举名是用户自定义的名称,常量名是枚举中每个常量的名称。枚举常量的值默认是从0开始自动递增的,也可以手动指定值。例如: ```c #include <stdio.h> enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }; int main() { enum Weekday today = Tuesday; printf("今天是星期%d\n", today + 1); return 0; } ``` 联合是一种特殊的数据类型,它的成员变量共享同一块内存空间。联合的定义方式如下: ```c union 联合名 { 数据类型 成员名1; 数据类型 成员名2; // ... }; ``` 其中,联合名是用户自定义的名称,成员名是联合中每个成员的名称,数据类型可以是任意C语言的数据类型,但所有成员的大小不能超过联合的大小。例如: ```c #include <stdio.h> union Number { int i; float f; }; int main() { union Number n; n.i = 123; printf("int: %d, float: %.2f\n", n.i, n.f); n.f = 3.14; printf("int: %d, float: %.2f\n", n.i, n.f); return 0; } ``` 以上就是C语言自定义数据类型中的结构体枚举联合的基本用法和注意事项。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值