C语言结构体等自定义类型详解!看这一篇就够了!

前言

一.结构体

        1.结构体的定义与声明

        2.结构体的自引用

        3.结构体的初始化

         4.结构体的大小及内存对齐

         5.结构体的传参


前言

由于数组只能保存同种类型的数据,为了帮助我们保存不同类型的数据,将他们整合在一起,用来描述一个对象,就有了结构体这样的概念。


一、结构体

        1.结构体的声明

                结构体是不同类型的数据的集合,用来描述同一个对象。这些类型的数据叫做结构体的成员变量。他们声明示例如下:

struct Student
{
	char name[20];
	int age;
	char sex[5];
	float score;
} s1, s2, s3;//s1, s2, s3 是三个结构体变量 - 全局变量
int main()
{
	struct Student s4, s5, s6;//s4, s5, s6 是三个结构体变量 - 局部变量
	return 0;
}

        如图所示,其中的s1,s2,s3是全局变量,s4,s5,s6是局部变量。

        我们发现创建Student变量时,定义的代码过长 每次都要输入struct XXX 这样的代码,于是我们又发明了typedef关键字 避免代码的定义过于繁琐,实例如下:

struct Student
{
	char name[20];
	int age;
	char sex[5];
	float score;
} s1, s2, s3;//s1, s2, s3 是三个结构体变量 - 全局变量
typedef struct Student
{
	char name[20];
	int age;
	char sex[5];
	float score;
} Stu;
int main()
{
	struct Student s4, s5, s6;//s4, s5, s6 是三个结构体变量 - 局部变量
	Stu s7, s8;
	return 0;
}

        假如我们写代码的时候,不小心漏掉了Student结构体名称,结果发现仍然编译通过,创建的变量也可以正常使用,但是后来当我们再想在main函数里创建局部变量时发现无法创建,说明只能使用当时创建的全局变量,这也符合C语言的语法规则。实例如下:

struct 
{
	char name[20];
	char author[12];
	float price;
}b1, b2;

         2.结构体的自引用

        结构体中嵌套另一个结构体肯定可以,如果包含自己会发生什么情况?

struct Node
{
	int data;//数据
	struct Node n;//下一个节点
};
int main()
{
	printf("%zd\n", sizeof(struct Node));
}

        结果我们发现系统报错

其实不只是未定义的问题,如果我们仔细思考,一个结构体嵌套自己,那他的大小到底是多少?

这个问题没有答案,所以这样肯定是不行的,正确的方式应该是这样:

typedef struct Node
{
	int data;//存放数据-数据域
	struct Node* n;//存放下一个节点的地址-指针域
}Node;

int main()
{
	printf("%zd\n", sizeof(Node));

	return 0;
}

这也是顺序存储结构之一——链表的声明。

3.结构体变量的定义和初始化

        结构体定义方式有两种,可分别定义为全局变量和局部变量,而初始化也可分为局部初始化和全部初始化,实例如下:

struct Point
{
	int x;
	int y;
}p1 = {1,2};//定义时按照固定顺序直接赋值
struct Stu
{
	char name[15];//名字
	int age;
};

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

int main()
{
	int a = 10;
	int b = 20;
	struct Point p2 = {a, b};

	struct Stu s = { "zhangsan", 20 };//按照固定顺序全部初始化
	struct Stu s2 = { .age=18, .name="如花"};//乱序全部初始化
	struct Stu s3 = {"abc" };
	s3.age = 30;//定义时初始化一部分,后续乱序赋值

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

	struct Node n = { 100, {20, 21}, NULL };//按照固定顺序全部初始化
	printf("%d x=%d y=%d\n", n.data, n.p.x, n.p.y);

	return 0;
}

运行结果如下:

4.结构体的大小及内存对齐

        C语言中采用offsetof  自定义宏获取结构体成员变量偏移量的大小,先看以下代码:

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

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

int main()
{
	printf("%d\n", offsetof(struct S1, c1));
	printf("%d\n", offsetof(struct S1, i));
	printf("%d\n", offsetof(struct S1, c2));

	printf("%d\n", sizeof(struct S1));
	printf("%d\n", offsetof(struct S2, c1));
	printf("%d\n", offsetof(struct S2, c2));
	printf("%d\n", offsetof(struct S2, i));
	printf("%d\n", sizeof(struct S2));

	return 0;
}

        

        上图中两个结构体中成员变量看似相同,只是顺序不同,但他们得到的结构体大小和偏移量却是不同的,这也反映了结构体中有着特殊的内存开辟规则,也叫对齐规则。

        对齐规则如下:

        那我们知道了 这两个看似相同的结构体是这样分配内存的:

那为什么要有这种对齐方式呢?这样不是会浪费一部分的空间吗?

官方给出的解释是这样的:

        那我们就明白了,为了写出来的代码大部分平台都能够适用,并且提高代码的访问速度(虽然提高的不多),所以C语言采用了这样的结构对齐方式。

        我们声明时尽量让小一点的单元放在一起,尽可能减少开辟的空间~~

           下面再举两个小例子~~

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

struct S4
{
	char c1;
	struct S3 s3;
	double d;
};

        答案分别是16    32 

                

        由于适应不同场景下的需要,编译器也提供了我们自己修改默认对齐数的方式~~采用的是#pragma pack()

#pragma pack(1)
struct S2
{
	char c1;//1 1 1
	int i;//4   1 1
	char c2;//1 1 1
};
#pragma pack()
int main()
{
	//printf("%d\n", sizeof(struct S1));//8
	printf("%d\n", sizeof(struct S2));//12
	//printf("%d\n", sizeof(struct S3));//
	//printf("%d\n", sizeof(struct S4));

	return 0;
}

        修改后,s2的各个成员变量的默认对齐数就成了1 S2的最大对齐数也成了1 所以S2 结构体大小就成了6~ 

        注意!!!更改完默认最大对齐数的值后记得恢复!!!

      5.结构体的传参

        结构体的传参一共有两种方式:传值调用和传值调用 请看代码:

struct S
{
	int data[1000];
	int num;
};
void print1(struct S t)
{
	printf("%d %d %d %d\n", t.data[0], t.data[1], t.data[2], t.num);
}

void print2(const struct S * ps)
{
	printf("%d %d %d %d\n", ps->data[0], ps->data[1], ps->data[2], ps->num);
}

int main() 
{
	struct S s = { {1,2,3}, 100 };
	print1(s);//传值调用
	print2(&s);//传址调用

	return 0;
}

 结果如下:虽然两种方法都能够成功调用 并满足我们的需求,但是还是推荐传地址调用的方式~

        


二、位段

1.位段的定义

       由于结构体的内存对齐的方式浪费了大量空间,所以人们又发明了用结构体实现的位段 位段的出现就是用来节省空间的。

        而位段的成员变量只能是int或char类型,并采用int/char XXX: n(n指的是n个比特位)这样的方式来声明位段!这也方便了内存空间开辟和访问~

        

      2.位段的内存对齐

代码如下:

struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};
int main()
{
    struct S s = { 0 };
    s.a = 10;
    s.b = 12;
    s.c = 3;
    s.d = 4;
    printf("%d\n", sizeof(struct S));
    return 0;
}我们不得不去猜测 内存到底是如何存放的?又是如何减少开辟的空间的?

  在VS环境中,位段是这样存放的~

由此可见,VS编译器是从右向左存放数据,一旦遇到较大的不够的内存就会舍弃,并且由于4个二进制位一个16进制位以及小端存储的规则,就产生了上图的内存存放~

但是我们想想,其他编译器是如何存放位段的?从右向左还是从左向右,小端还是大端?会不会舍弃不够用的空间?我们使用前并不知道,这就要说到位段的缺点~

3.位段的缺陷

        所以如果代码要跨平台使用,不要使用位段,为了节省空间还是可以使用的~

 

4.位段的应用


三、枚举类型

        1.枚举定义

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,当然在定义的时候也可以赋初值。例如:

​
enum Sex
{
	//枚举的可能取值
	MALE=3,//枚举常量
	FEMALE,
	SECRET
};
int main()
{
	printf("%d\n", MALE);
	printf("%d\n", FEMALE);
	printf("%d\n", SECRET);
	enum Sex sex = SECRET;
	printf("%zd\n", sizeof(sex));
	return 0;
}
enum Color//颜色 
{
RED=1, 
GREEN=2, 
BLUE=4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。 
clr = 5;//不可以这样直接赋值!!
​

2.枚举常量的优点

        

所以如果项目比较大,常量之间关系复杂,不妨试试使用枚举常量~


四、联合体类型

        1.联合体类型的定义

        联合也是一种特殊的自定义类型,这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体),示例如下:

        

        由此可见,联合体的各个成员变量都是公用同一块空间的,所以联合体也在很大程度上省掉了空间,弥补了结构体的不足~

         由于使用的是同一块空间,所以联合体的成员变量不会同时使用~

        2.联合体的应用

                判断当前计算机的大小端存储,代码如下:

int check_sys()
{
	union
	{
		char c;
		int i;
	}u;

	u.i = 1;
	return u.c;//返回1表示小端,返回0表示大端
}
int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");

	return 0;
}

               

在内存中是这样分布的~ 

        3.联合体的大小计算、

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

 所以联合体还是比较严谨的~           


总结

例如:以上就是今天要讲的内容,本文仅仅简单介绍了C语言常见自定义类型的使用,如果问题欢迎评论区讨论留言~~

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值