12.自定义类型

0.前言

内置类型:

char
short
int
long
long long
float
double

自定义类型

结构体
枚举
联合体
数组

1.结构体

结构体声明

struct tag//结构体标签
{
    member-list;//成员列表
}variable-list;//变量列表
struct Stu
{
    char name[20];
    int age;
    char sex[5];
    int hight;
}s2,s3,s4;//全局变量,分号不能丢

struct Stu s5;//全局变量

int main()
{
    struct Stu s1;//结构体变量,局部变量
    return 0;
}

特殊结构体声明

//匿名结构体类型
struct
{
    int a;
    char b;
    float c; 
}x;
struct
{
    int a;
    char b;
    float c; 
}a[20], *p;
//匿名结构体类型只能用一次,没有标签,无法再次定义了
//在上面代码的基础上,下面的代码合法吗?
p = &x;

警告:

编译器会把上面的两个声明当成完全不同的两个类型。

所以是非法的。

结构体自引用

数据结构:数据在内存中的存储结构
链表:数据域和指针域

struct Node
{
 int data;
 struct Node next;
};
//可行否?
如果可以,那sizeof(struct Node)是多少? 无限大了,显然不行
struct Node
{
 int data;
 struct Node* next;
};//这样才行,通过地址才能找到下一个元素,地址是用指针接收的
typedef struct
{
    int data;
    Node* next;
}Node;
//这样写代码,可行否?
//Node的定义先后顺序出现了问题
int main()
{
    Node n;//报错
    return 0;
}
//解决方案:
typedef struct Node
{
    int data;
    struct Node* next; 
}Node;//这个Node只是重命名,不是定义变量
int main()
{
    struct Node n2 = {0};//也对
    Node n = {0};//正确
    return 0;
}

定义和初始化

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};//结构体嵌套初始化

结构体内存对齐

offsetof

size_t offsetof( structName, memberName );

Return Value

offsetof returns the offset in bytes of the specified member from the beginning of its parent data structure. It is undefined for bit fields.

计算偏移量,计算结构体成员相较于起始位置的偏移量
#include<stdio.h>
#include<stddef.h>

struct S1
{
	char c1;//1
	int i;//4
	char c2;//1
};
int main()
{
	printf("%d\n", (int)sizeof(struct S1));//12
	printf("%d\n", (int)offsetof(struct S1, c1));//0
	printf("%d\n", (int)offsetof(struct S1, i));//4
	printf("%d\n", (int)offsetof(struct S1, c2));//8
	//(int)这样才不会产生警告,sizeof和offsetof返回的都是size_t
	return 0;
}

image-20220124154137178

c1和c2下面的3个字节干啥去了?

结构体对齐规则

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处

对齐数 = 编译器默认的一个对齐数与该成员大小的较小值VS中默认的值为8

  1. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

  2. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

struct S1
{
	char c1;//1
	int i;//4
	char c2;//1
};
//最大对齐数为4,当前已经占了9个字节,最近的4的倍数是12,因此结构体大小是12
注意:Linux环境没有默认对齐数,Linux环境下,对齐数就是成员自身大小
struct S2
{
    char c1;//1
    char c2;//1 
    int i;//4
};
printf("%d\n", sizeof(struct S2));//8
c2的对齐数1,任何数都是1的倍数,因此c2是紧靠着c1放的
i的对齐数是4,从4开始偏移4个字节大小,此时总大小是8刚好是4的倍速,因此结构体大小就是8
    
    仅仅是换了下顺序,结构体大小就变了
image-20220124160300095
struct S3
{
    double d;
    char c;
    int i;
};
printf("%d\n", sizeof(struct S3));//16

image-20220124160649448

struct S3
{
	double d;//8
	int i;//4
	char c;//1
};
printf("%d\n", sizeof(struct S3));//16
这样放结果依旧为16,结构体大小算的最大对齐数的整数倍,最大对齐数是8double int char 这样排完后,刚好占了13个字节空间,但13不是8的倍数
struct S4
{
    char c1;
    struct S3 s3;
    double d;
};
printf("%d\n", sizeof(struct S4));//32
image-20220124162107607

为什么存在内存对齐?

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

image-20220124162849012

32位机器(32根地址线)一次访问4个字节,如果不对齐,为了得到i,访问了2次,效率降低

总结:

结构体对齐就是拿空间换时间

那在设计结构体的时候,既要满足对齐,又要节省空间:让占用空间小的成员尽量集中在一起(前后没关系)

修改默认对齐数

#include<stdio.h>
#include<stddef.h>
#pragma pack(1)//设置默认对齐数为1
struct S1
{
	char c1;//1 - 1 - 1
	int i;//4 - 1 - 1
	char c2;//1 - 1 - 1
};
struct S3
{
	double d;//1
	int i;//1
	char c;//1

};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
	printf("%d\n", (int)sizeof(struct S1));//6
	printf("%d\n", (int)sizeof(struct S3));//13
	//(int)这样才不会产生警告,sizeof和offsetof返回的都是size_t
	return 0;
}
默认对齐数改成1了,#pragma pack()是成对使用的

结构体传参

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

//如果不想传地址后,结构体内容被改变,那么就用const修饰
void print2(const struct S* ps)

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

2.位段

#include<stdio.h>
struct A 
{
	int _a : 2;//a成员只需要2个bit位
	int _b : 5;//5个bit位
	int _c : 10;//10个bit位
	int _d : 30;//30个bit位
};
int main()
{
	printf("%d\n", sizeof(struct A));//8
	return 0;
}

位段内存分配

  1. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型

  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。

  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

struct A 
{
    //先开4个byte 32bit
	int _a : 2;//a成员只需要2个bit位
	int _b : 5;//5个bit位
	int _c : 10;//10个bit位,剩余15bit
    //再开辟4byte
	int _d : 30;//30个bit位
};
//位段在不同编译器实现方式不一样
#include<stdio.h>
struct S 
{
	//1byte 8bit
	char a : 3;//剩余5
	char b : 4;//剩余1
	//再开1byte
	char c : 5;//剩余3
	//再开1byte
	char d : 4;//剩余1
};
int main()
{
	printf("%d\n", sizeof(struct S));//3
	return 0;
}
//如果每一个bit都不浪费,那么开辟2byte完全够用,但最终是3byte,说明是有浪费的,C语言标准也没有规定是否该浪费,完全取决于编译器

位段在一定程度节省空间,虽然也会有一点浪费,且位段可移植性不好

int _a : 2;
_a只需要2个bit就能完成任务,大大节省空间,4个状态 00 01 10 11
    
    假设表示年龄
strcut S
{
    int age:10;//10个比特位完全够了
    0111111111  --> 1023
}
    
struct S 
{
    char a:3;
    char b:4;
    char c:5;
    char d:4;
};
struct S s = {0};
s.a = 10; //1010,a是3bit只能放进010
s.b = 12; //1100
s.c = 3; //00011
s.d = 4;//0100
但放在一个byte内部的高地址还是低地址处C标准也没规定,全靠编译器决定
00000000 00000000 00000000
01100010 00000011 00000100
62		 03		  04
    通过F10内存窗口可以验证,说明位段一个字节内部从低位开始使用的
    这与栈区开辟局部变量不同,栈区中是字节之间的关系,是从高地址到低地址的

image-20220124203144887

位段跨平台问题

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

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

  3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。

  4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的。

位段应用

网络上传输的数据包如果过大,网络就会堵塞

image-20220124203855854

3.枚举

把可能的取值一一列举出来

枚举定义

enum Day//星期
{
    //枚举的可能取值
    Mon,
    Tues,
    Wed,
    Thur,
    Fri,
    Sat,
    Sun
};
enum Sex//性别
{
    MALE,
    FEMALE,
    SECRET
}enum Color//颜色
{
    RED,
    GREEN,
    BLUE
};
int main()
{
    enum Day d = Sun;
    enum Sex s = SECRET;
    printf("%d\n", MALE);//0
    printf("%d\n", FEMALE);//1
    printf("%d\n", SECRET);//2
    return 0;
}
//默认值从0开始,逐渐递增1
//可以随意初始化
enum Color//颜色
{
 RED=1,
 GREEN=2,
 BLUE=4
};
//其实还是常量,之后不能修改的
typedef enum Sex//性别
{
    MALE=4,
    FEMALE,
    SECRET
}Sex;
int main()
{
    enum Day d = Sun;
    Sex s = MALE;
    printf("%d\n", MALE);//4
    printf("%d\n", FEMALE);//5
    printf("%d\n", SECRET);//6
    printf("%d\n", sizeof(s));//4,其实就是整型大小
    return 0;
}
Sex s = 0;
如果这样胡乱赋值,在c里面是能通过编译,但在cpp里面就不行了,cpp检测更加严格

枚举优点

  1. 增加代码的可读性和可维护性

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

  3. 防止了命名污染(封装)

  4. 便于调试(#define定义的不能调试,#define是替换)

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

image-20220124205909291

//使用枚举配合菜单使用,不用再写孤零零的不清楚具体意思的123456
enum Option
{
	EXIT,//0
	ADD,//1
	DEL,//2
	SEARCH,//3
	MODIFY,//4
	SORT,//5
	SHOW//6
};
void menu()
{
	printf("***************************************\n");
	printf("********     1.add      2.del     *****\n");
	printf("********     3.search   4.modify  *****\n");
	printf("********     5.sort     6.show    *****\n");
	printf("********     0.exit               *****\n");
	printf("***************************************\n");
}

switch (input)
		{
		case ADD:
			AddContact(&con);
			break;
		case DEL:
			DeleteContact(&con);
			break;
		case SEARCH:
			break;
		case MODIFY:
			break;
		case SORT:
			break;
		case SHOW:
			ShowContact(&con);
			break;
		case 0:
			break;
			printf("退出通讯录\n");
		default:
			printf("选择错误\n");
			break;
		}

4.联合(共用体)

定义

#include<stdio.h>
union Un
{
	char c;
	int i;
};
int main()
{
	union Un u;
	printf("%d\n", sizeof(u));//4
	printf("%p\n", &u);//00AFF9D8
	printf("%p\n", &(u.c));//00AFF9D8
	printf("%p\n", &(u.i));//00AFF9D8
	return 0;
}
//一个联合变量的大小,至少是最大成员的大小
//i和c共用了第一个字节
//i的值改了,c的值也会变

image-20220124211412278

利用好联合体可以一定程度上节省空间

判断当前机器大小端

#include<stdio.h>
int check_sys()
{
	int a = 1;
	return *(char*)&a;
}
int main()
{
	int ret = check_sys();
	if (1 == ret)
	{
		printf("小端\n");//结果为1 证明是小端存储
	}
	else
	{
		printf("大端\n");
	}
	return 0;
}
有时候使用4个字节,有时候使用1个字节,可以用联合体写
int check_sys()
{
	union//使用匿名联合体,只用一次,避免之后的命名冲突
	{
		char c;
		int i;
	}u;
	u.i = 1;
	return u.c;
}

联合体大小

  1. 联合的大小至少是最大成员的大小。
  2. 当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
#include<stdio.h>
union Un1
{
	char c[5];//1 - 8 ->1 VS默认对齐数8
	int i;//4 - 8 ->4
};
union Un2
{
	short c[7];//2
	int i;//4
};
int main()
{
	printf("%d\n", sizeof(union Un1));//8,最大对齐数是4,
	//最大成员大小是5,不是4整数倍
	printf("%d\n", sizeof(union Un2));//16
	//最大成员大小是14,也不是4的整数倍
	return 0;
}

练习

通讯录:

  1. 存放1000个人的信息
  2. 人的相关信息,名字,年龄,电话,住址,性别
  3. 增删查改
  4. 排序

image-20220124170941665

test.c

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值