【C语言】自定义类型——结构体、位段、联合体、枚举

目录

1. 结构体

1.1 结构体的声明

1.2 匿名结构体

1.3 结构体的自引用

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

1.5 结构体内存对齐

1.5.1 结构体内存对齐的规则

1.5.2 结构体内存对齐的示例

1.5.3 为什么存在结构体内存对齐

1.5.4 修改默认对齐数

1.6 结构体成员的访问

1.7 结构体作为函数参数

2. 位段

2.1 位段的声明

2.2 位段的内存分配

3. 联合体

3.1 联合体的声明

3.2 联合体变量的定义和初始化

3.2 用联合体判断大小端

3.3 联合体大小的计算

4. 枚举

4.1 枚举的声明

4.2 枚举的使用


1. 结构体

1.1 结构体的声明

struct tag
{
    member-list;
}variable-list;
  • 仅声明结构体类型,不定义结构体变量
struct Person
{
	char name[20];
	char tele[20];
	char sex[3];
	int age;
};
  • 声明结构体类型的同时,定义结构体变量
struct Book
{
	char bookName[20];
	char authorName[20];
	int publishYear;
}b1, b2;

1.2 匿名结构体

在声明结构体的时候,可以不完全的声明。比如省略结构体标签(tag),就称为匿名结构体。匿名结构体变量只能在声明结构体类型的同时被定义。

struct
{
	struct Person p; // 其他结构体作为该结构体的成员
	char id[20];     // 学号
	double score;    // 成绩
}x; // 声明匿名结构体的同时定义匿名结构体变量x

struct
{
	struct Person p;
	char id[20];
	double score;
}*p; // 声明匿名结构体的同时定义匿名结构体指针p

在上面代码的基础上,下面的代码是非法的。编译器会把上面的两个声明当成完全不同的两个类型。

p = &x; // err

1.3 结构体的自引用

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

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

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

// ok
typedef struct Node
{
    int data;
    struct Node* next;
}Node; // 把struct Node类型重命名为Node

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

  • 定义
struct Person
{
	char name[20];
	char tele[20];
	char sex[3];
	int age;
}p1, p2; // 声明结构体类型的同时定义结构体变量p1、p2

struct Peo p3; // 定义结构体变量p3

struct Student
{
	struct Person p;
	char id[20];
	double score;
}s1; // 声明结构体类型的同时定义结构体变量s1
  • 初始化
struct Person
{
	char name[20];
	char tele[20];
	char sex[3];
	int age;
}p1, p2;
// 顺序初始化
struct Person p1 = { "黄蓉","123456","女",35 };
// 指定初始化(C99)
struct Person p2 = { .sex = "女",.tele = "666",.age = 20,.name = "赵敏" };

struct Student
{
	struct Person p;
	char id[20];
	double score;
};
// 顺序初始化
struct Student s1 = { { "乔峰","987654321","男",30 },"101",99.999 };
// 指定初始化(C99)
struct Student s2 = { .id = "777",.score = 60.0,.p = { "慕容复","415784814","男",30 } };

1.5 结构体内存对齐

1.5.1 结构体内存对齐的规则

  • 第一个成员在与结构体变量偏移量为0的地址处。
  • 其他成员变量要对齐到对齐数的整数倍的地址处。对齐数是编译器默认的一个对齐数与该成员大小的较小值。VS中默认的值为8。
  • 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
  • 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

1.5.2 结构体内存对齐的示例

#include <stddef.h>
size_t offsetof(type, member)
// offsetof是一个宏,用于求结构体中一个成员在该结构体中的偏移量

示例1

#include <stdio.h>
#include <stddef.h>

struct S1
{
	char c1; // 对齐数=min{8,1}=1
	int i;   // 对齐数=min{8,4}=4
	char c2; // 对齐数=min{8,1}=1
}; // 最大对齐数=max{1,4,1}=4

int main()
{
	printf("c1的偏移量:%d\n", offsetof(struct S1, c1));
	printf("i 的偏移量:%d\n", offsetof(struct S1, i));
	printf("c2的偏移量:%d\n", offsetof(struct S1, c2));
	printf("结构体的大小:%d\n", sizeof(struct S1));
	return 0;
}

示例2

#include <stdio.h>
#include <stddef.h>

struct S2
{
	char c1; // 对齐数=min{8,1}=1
	char c2; // 对齐数=min{8,1}=1
	int i;   // 对齐数=min{8,4}=4
}; // 最大对齐数=max{1,1,4}=4

int main()
{
	printf("c1的偏移量:%d\n", offsetof(struct S2, c1));
	printf("c2的偏移量:%d\n", offsetof(struct S2, c2));
	printf("i 的偏移量:%d\n", offsetof(struct S2, i));
	printf("结构体的大小:%d\n", sizeof(struct S2));
	return 0;
}

 

示例3

#include <stdio.h>
#include <stddef.h>

struct S3
{
	double d; // 对齐数=min{8,8}=8
	char c;   // 对齐数=min{8,1}=1
	int i;    // 对齐数=min{8,4}=4
}; // 最大对齐数=max{8,1,4}=8

int main()
{
	printf("d的偏移量:%d\n", offsetof(struct S3, d));
	printf("c的偏移量:%d\n", offsetof(struct S3, c));
	printf("i的偏移量:%d\n", offsetof(struct S3, i));
	printf("结构体的大小:%d\n", sizeof(struct S3));
	return 0;
}

示例4

#include <stdio.h>
#include <stddef.h>

struct S3
{
	double d; // 对齐数=min{8,8}=8
	char c;   // 对齐数=min{8,1}=1
	int i;    // 对齐数=min{8,4}=4
}; // 最大对齐数=max{8,1,4}=8

struct S4
{
	char c;       // 对齐数=min{8,1}=1
	struct S3 s3; // struct S3的最大对齐数=8
	double d;     // 对齐数=min{8,8}=8
}; // 最大对齐数=max{1,8,8}=8

int main()
{
	printf("c 的偏移量:%d\n", offsetof(struct S4, c));
	printf("s3的偏移量:%d\n", offsetof(struct S4, s3));
	printf("d 的偏移量:%d\n", offsetof(struct S4, d));
	printf("结构体的大小:%d\n", sizeof(struct S4));
	return 0;
}

1.5.3 为什么存在结构体内存对齐

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

结构体内存对齐是用空间换时间的做法。

在设计结构体的时候,既要满足对齐,又要节省空间,就要让占用空间小的成员尽量集中在一起。

struct S1 
{
    char c1; // 对齐数=min{8,1}=1
    int i;   // 对齐数=min{8,4}=4
    char c2; // 对齐数=min{8,1}=1
}; // 最大对齐数=max{1,4,1}=4
// sizeof(struct S1)=12

struct S2
{
    char c1; // 对齐数=min{8,1}=1
    char c2; // 对齐数=min{8,1}=1
    int i;   // 对齐数=min{8,4}=4
}; // 最大对齐数=max{1,1,4}=4
// sizeof(struct S2)=8

struct S1和struct S2类型的成员一模一样,但是所占空间的大小不同。

1.5.4 修改默认对齐数

#pragma pack(1) // 设置默认对齐数为1

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

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

结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。

1.6 结构体成员的访问

  • 结构体变量.成员变量
  • 结构体指针->成员变量
#include <stdio.h>

struct Book
{
	char bookName[20];
	char authorName[20];
	int publishYear;
};

struct Novel
{
	struct Book b;
	char type[10];
	char length[10];
};

int main()
{
	struct Novel n = { { "射雕英雄传","金庸",1957 },"武侠","长篇" };
	struct Novel* p = &n;
	printf("%s %s %d %s %s\n",    n.b.bookName,    n.b.authorName,    n.b.publishYear,    n.type,    n.length);
	printf("%s %s %d %s %s\n", (*p).b.bookName, (*p).b.authorName, (*p).b.publishYear, (*p).type, (*p).length);
	printf("%s %s %d %s %s\n",   p->b.bookName,   p->b.authorName,   p->b.publishYear,   p->type,   p->length);
	return 0;
}

1.7 结构体作为函数参数

#include <stdio.h>

struct Book
{
	char bookName[20];
	char authorName[20];
	int publishYear;
};

struct Novel
{
	struct Book b;
	char type[10];
	char length[10];
};

void print1(struct Novel n)
{
	printf("%s %s %d %s %s\n", n.b.bookName, n.b.authorName, n.b.publishYear, n.type, n.length);
}

void print2(struct Novel* p)
{
	printf("%s %s %d %s %s\n", p->b.bookName, p->b.authorName, p->b.publishYear, p->type, p->length);
}

int main()
{
	struct Novel n = { { "射雕英雄传","金庸",1957 },"武侠","长篇" };
	print1(n);  // 传结构体变量
	print2(&n); // 传结构体指针
	return 0;
}

结构体传参最好传结构体的地址(结构体指针)。因为函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。

2. 位段

位段又称位域,C语言允许在一个结构体中以位为单位来指定其成员所占内存长度。

2.1 位段的声明

位段的声明与结构体类似,不同之处在于:

  • 位段成员名后加一个冒号和一个整数,整数规定了成员所占用的位数,且不能超过成员名类型的大小。
  • C99之前,位段的成员必须是(signed/unsigned) int、(signed/unsigned) char,C99支持bool类型作为位段的成员类型。
struct S1 // 结构体
{
	int a;
	int b;
	int c;
	int d;
};

struct S2 // 位段
{
	int a : 2;  // a占2bit
	int b : 5;  // b占5bit
	int c : 10; // c占10bit
	int d : 30; // d占30bit
};

2.2 位段的内存分配

  • 位段的空间上是按照需要以4个字节(int)或者1个字节(char/bool)的方式来开辟的。
  • 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

示例1

#include <stdio.h>

struct S
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

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

以上代码,先开辟4个字节(32b),存放a(2b),还剩30b,存放b(5b),还剩25b,存放c(10b),还剩15b,不够存放d(30b),所以再开辟4个字节(32b),足够存放d(30b)。一共开辟了8个字节,struct S类型的大小为8个字节。

示例2

struct S
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};

int main()
{
	struct S s = { 0 };
	s.a = 10; // 1010
	s.b = 12; // 1100
	s.c = 3;  // 11
	s.d = 4;  // 100
	return 0;
}

以上代码,空间是如何开辟的?

小端字节序:

3. 联合体

联合体又称共用体,与结构体不同的是,联合体成员变量相互覆盖,共同占用同一块内存空间。

3.1 联合体的声明

联合体的声明与结构体类似。

union tag
{
    member-list;
}variable-list;

3.2 联合体变量的定义和初始化

联合体变量的定义和初始化方式也与结构体类似,但结构体能初始化所有成员,联合体只能初始化一个成员。

C99之前,只有联合体的第一个成员可以获得初始值;C99支持指定初始化,只能初始化一个成员,但不一定是第一个。

union Un
{
	char c;
	int i;
	double d;
};
union Un u1 = { 1 };
union Un u2 = { .i = 9 };

3.2 用联合体判断大小端

联合体的重要性质:成员变量共同占用同一块内存空间。

#include <stdio.h>

int check_sys()
{
	union Un
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}

/*
int check_sys()
{
	int a = 1;
	return *(char*)&a;
}
*/

int main()
{
	int ret = check_sys();
	if (ret == 1)
		printf("小端\n");
	else
		printf("大端\n");
	return 0;
}

3.3 联合体大小的计算

  • 联合体的大小至少是最大成员的大小。
  • 联合体的大小为最大对齐数的整数倍。
union Un1
{
	char arr[5]; // 对齐数=min{8,1}=1
	int i;       // 对齐数=min{8,4}=4
}; // 最大对齐数=max{1,4}=4
// union Un1的大小至少是5×1=5个字节,还必须是4的整数倍
// sizeof(union Un1)=8

union Un2
{
	short s[7]; // 对齐数=min{8,2}=2
	int i;      // 对齐数=min{8,4}=4
}; // 最大对齐数=max{2,4}=4
// union Un2的大小至少是7×2=14个字节,还必须是4的整数倍
// sizeof(union Un1)=16

4. 枚举

4.1 枚举的声明

虽然枚举和结构体、联合体没有什么共同的地方,但是它们的声明方法很类似。与结构体或联合体的成员不同,枚举常量的名字必须不同于作用域范围内声明的其他标识符。

enum 枚举名
{
	枚举常量 [= 整型常量],
	枚举常量 [= 整型常量],
	...
	枚举常量 [= 整型常量]
}枚举变量;

如果枚举常量都没有被显式初始化,则从第一个枚举常量开始依次默认为0,1,2……

如果某个枚举常量被显式初始化,且其后的一些成员没有被显式初始化,则其后的成员按依次加1的规则确定其值。

enum Season
{
	SPR, // 0
	SUM, // 1
	FAL, // 2
    WIN  // 3
};

enum Day // 星期
{
	Mon = 1, // 1
	Tues,    // 2
	Wed,     // 3
	Thur,    // 4
	Fri,     // 5
	Sat,     // 6
	Sun      // 7
};

enum Color
{
	RED,       // 0
	GREEN = 5, // 5
	BLUE       // 6
};

4.2 枚举的使用

#include <stdio.h>

enum Color
{
	RED = 1,
	GREEN = 2,
	BLUE = 4
}clr1;

int main()
{
	enum Color clr1 = RED; // ok
	printf("clr1 = %d\n", clr1);

	int clr2 = GREEN;	   // ok
	printf("clr2 = %d\n", clr2);

	enum Color clr3 = 10;  // C语言ok,C++err
	printf("clr3 = %d\n", clr3);

	return 0;
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值