C语言_自定义数据类型

目录

1.自定义数据类型_结构体

1.1 结构体类型的声明

 1.1.1 匿名结构体

1.2 结构体的自引用

1.2.1 Typedef结构体重命名: 

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

1.4 结构体内存对齐

1.4.1 结构体的对齐规则

1.4.2 为什么存在内存对齐?

1.4.3 设置默认对齐数

1.4.4 offsetof结构体偏移量计算函数

1.5 结构体传参

1.6 结构体实现位段(位段的填充&可移植性)

1.6.1 什么是位段?

1.6.2 位段的内存分配规则

1.6.3 位段存在的意义和具体数值的存放

2. 自定义数据类型_枚举

2.1 枚举类型的定义 

2.2 枚举的优点

2.3 枚举的使用

2.4 枚举的大小

3. 自定义数据类型_联合

3.1 联合类型的定义

3.2 联合的特点

3.3 联合大小的计算


         C语言内置类型(C语言自己的数据类型):char、short、int、long、float、double;

         复杂类型(自定义类型):结构体、枚举、联合体;

1.自定义数据类型_结构体

1.1 结构体类型的声明

        结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。比方说我们之前接触过的数组:数组找那个存放的必须是相同类型的变量,而结构体的强大也体现在其内部的成员变量可以是不同类型的变量;例如成员变量我可以设置为char[姓名]、int[年龄]等等;

        声明一个结构体类型:struct 声明结构体;stu---结构体名字;结构体的成员变量可以是不同的类型;name名字其实就是一个字符串,用char 定义;年龄是一个整型数字,用 int 修饰;

struct stu
{
	char name[20];//名字
	char tel[10];//电话
	char sex[10];//性别
	int age;//年龄
};
int main()
{
	return 0;
}
int main()
{
	struct stu S1;//用结构体类型创建结构体变量S1、S2;
	struct stu S2;
	return 0;
}

         这里需要注意:通过结构体创建的结构变量S1、S2、S3、S4、S5、S6是不同的;其中S1、S2为结构体局部变量,而S3、S4、S5、S6为结构体全局变量;

struct stu
{
	char name[20];
	char tel[10];
	char sex[10];
	int age;
}S4,S5,S6;
struct stu S3;
int main()
{
	struct stu S1;//用结构体类型创建结构体变量S1、S2;
	struct stu S2;
	return 0;
}

 1.1.1 匿名结构体

        匿名结构体类型:缺少结构体标签stu,必须在大括号外边定义结构体变量S1、S2、S3;否则缺少结构体名字,无法定义结构体变量;这种结构体称为匿名结构体;

struct 
{
	char name[20];
	char tel[10];
	char sex[10];
	int age;
}S4,S5,S6;

1.2 结构体的自引用

        如数据结构中的链表定义:1 2 3 4 5 ;我在内存中可以任意随机的存放;但是想要定义1之后,能找到2 ,接着能找到3,接着找到4,接着找到5;我需要定义一个数字1,同时在定义数字1的结构体中定义数字2的地址(用指针来指向数据2的地址);依次类推,在数据2中包含数字3的地址……其中:存放数据的地址叫数据域;存放地址的叫指针域

C语言_自定义数据类型_第1张图片

         结构体的自引用通过指针来指向下一个地址;

struct Node   //结构体自引用
{
	int data;
	struct Node * Next;
};

1.2.1 Typedef结构体重命名: 

typedef struct Node
{
	int data;
	struct Node * Next;
}Node;
int main()
{
	struct Node N1;//以下两种定义方式均可
	Node N2;
	return 0;
}

        给结构体struct重新命名为:Node;则主函数定义新变量:N1、N2既可以struct Node N1;也可以Node N2;在此建议:即使重命名,也不要把Node省略掉(typedef struct);

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

定义结构体变量:可以在结构体的大括号外边,分号前面定义结构体变量;也可以直接定义全局变量:struct stu S1;

结构体变量初始化:参照上述,定义一个结构体,定义结构体变量s,然后进行结构体变量初始化,结构体变量初始化需要用到大括号,结构体的成员变量是什么类型,初始化时就需要参照结构体成员变量的类型进行定义;结构体成员变量的访问:用结构体变量   +  .  进行访问;

struct S
{
	char c;
	int a;
	double d;
	char arr[20];
};
int main()
{
	struct S s = { 'c', 100, 3.14, "hello world" };
	printf("%c %d %.2lf %s\n", s.c, s.a, s.d, s.arr);
	return 0;
}//c 100 3.14 hello world

 结构体嵌套结构体类型访问:铭记:结构体初始化用到大括号;什么样的类型对应什么样的打印方式;

struct T
{
	double weight;
	short age;
};
struct S
{
	char c;
	struct T st;
	int a;
	double d;
	char arr[20];
};
int main()
{
	//struct S s = { 'c', 100, 3.14, "hello world" };
	struct S s = { 'c', { 55.6, 30 }, 100, 3.14, "hello world" };
	printf("%c %.1lf %d %d %.2lf %s\n", s.c,s.st.weight,s.st.age, s.a, s.d, s.arr);
	return 0;
}//c 55.6 30 100 3.14 hello world

1.4 结构体内存对齐

这一部分我们来计算结构体变量的所占的字节大小(sizeof(s1));计算结构体的大小涉及到结构体内存对齐的规则;

1.4.1 结构体的对齐规则

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

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

                对齐数= 编译器默认的对齐数与该成员大小的较小值VS中默认的值为8;(只是VS环境下设置的对齐数是8,gcc环境是没有设置默认对齐数的,没有对齐数就是较小值)

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

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

ag. 计算下述两个结构体的大小?输出答案:12   8

struct S1
{
	char c1;
	int a;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int a;
};
int main()
{
	struct S1 s1 = { 0 };
	printf("%d\n", sizeof(s1));
	struct S2 s2 = { 0 };
	printf("%d\n", sizeof(s2));
	return 0;
}//12  8 

   C语言_自定义数据类型_第2张图片C语言_自定义数据类型_第3张图片

 在此:读者可能有很多的疑问,为什么只是改变了结构体成员变量的顺序,结构体的大小就会发生如此大的改变?原因要追溯到结构体的对齐规则。(其中char占一个字节,int占4个字节,double占16个字节)

        首先:第一个成员要在与结构体变量偏移为0的地址处存放;计算struct S1时,第一个结构体变量为char c1,char占用一个字节,存放到左图所示第一个红色地址处;其他成员变量需要对齐到对齐数编译器默认的对齐数和成员大小的较小值)的整数倍处;VS默认的对齐数为8;存放第二个成员变量int a时,int占用4个字节;对齐数为  4和VS默认的对齐数8  的最小值,也就是4,所以第二个成员变量int 存放到4的整数倍地址处,取4,存放到左图所示紫色地址处,int占用四个字节,所以依次向下占用四个内存;然后存放第三个成员变量char c2,char 所占字节个数为1,对齐数为1和VS默认的对齐数8的最小值,也就是1;所以char c2存放到左图绿色地址处;至此,struct S1存放完毕,占用9个字节;但是结构体对齐规则定义:结构体的总大小为最大对齐数的整数倍;简单来说就是,S1中三个成员变量对应的对齐数分别为  1/8  4/8  1/8 中的最大值,也就是4,所以结构体的总大小为4的整数倍,但是至少也要保证存放9个字节,所以为3*4=12;也就是第一个输出结果12;

        仿照上述struct S1计算结构体大小的方式来计算struct S2的大小:首先第一个成员变量char s1存放到与结构体偏移量为0的地址处,存放到右图红色地址处;第二个成员变量char s2存放到对齐数1/8的最小值处,也就是1,即右图黑色地址处;第三个成员变量int 占用四个字节,需要存放到4/8的最小值处,也就是4,从偏移量为0的地址处向下数4的地址来存放int的四个字节,即右图绿色地址处;至此,struct S2存放完毕,占用8个字节;总的大小为最大对齐数的整数倍,最大对齐数为 1/8  1/8  4/8的最大值的整数倍,同时也要保证能够存放的下struct s2的8个字节,所以选取8;也就是第二个输出结果8;

ag. 嵌套结构体的计算规则:输出结果:12   8   16   32

struct S1
{
	char c1;
	int a;
	char c2;
};
struct S2
{
	char c1;
	char c2;
	int a;
};
struct S3
{
	double d;
	char c;
	int i;
};
struct S4
{
	char c1;
	struct S3 s3;
	double d;
};
int main()
{
	struct S1 s1 = { 0 };
	printf("%d\n", sizeof(s1));
	struct S2 s2 = { 0 };
	printf("%d\n", sizeof(s2));
	struct S3 s3 = { 0 };
	printf("%d\n", sizeof(s3));
	struct S4 s4 = { 0 };
	printf("%d\n", sizeof(s4));
	return 0;
}//12  8  16  32

         首先:struct S3的计算过程和上述s1、s2的计算过程相同,这里不作过多解释;S4中包含S3嵌套,如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有对齐数(含嵌套结构体的对齐数)的整数倍;struct S4中第一个成员变量char c1占一个字节,放在偏移量为0的地址处;嵌套结构S3放在对齐到自己最大对齐数的整数倍,s3中最大对齐数在double类型的8/8,对齐数为8,所以嵌套结构对齐到偏移量为8的地址处,本身嵌套结构占16个字节;char c1(1)+对齐的、保证偏移量为8(7)+嵌套结构本身占用的字节(16)+double占用的字节(8)=32;结构体整体的大小就是所有对齐数(含嵌套结构的对齐数)的整数倍;所有对齐数数中最大的就是8,恰巧32正好是8的整数倍;所以最终嵌套结构s4的结构体大小为32;

1.4.2 为什么存在内存对齐?

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

        2.性能原因:数据结构(栈)应该尽可能的在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅仅需要一次即可;可大大节省时间;

        总体来说,就是拿空间去换时间

        所以在设计结构体的时候,我们既要满足对齐,又要节省空间;根据上述的例子:struct s1的大小为12,struct s2的大小为8,所以为了最大程度的节省空间,在设计结构体时尽可能的将占空间小的成员放在一起,防止浪费空间;

1.4.3 设置默认对齐数

        C语言中可以通过  #pragma pack()进行主动修改对齐数;一般设置为2的次方数(2  4  8  16 ……);

        以下程序,如果不使用#pragma pack(),则输出的结果是16;如果加上#pragma pack(),结果会发生改变;在程序之前加上#pragma pack(1)(程序之前的括号里加上x 就表示修改对齐数为x)表示修改默认的对齐数为1;在结构体之后加上#pragma pack()表示恢复修改的对齐数;

#pragma pack(1)
struct S
{
	char c1;
	double d;
};
#pragma pack(0)
int main()
{
	struct S s;
	printf("%d\n", sizeof(s));
	return 0;
}

1.4.4 offsetof结构体偏移量计算函数

        函数使用定义:size_t offsetof(structName,memberName)-----------(结构体名称,结构体成员名称);该函数需要引用头文件:#include    切记:函数第一个参数为struct S;

struct S
{
	char c1;
	double d;
	int a;
};
int main()
{
	printf("%d\n", offsetof(struct S,c1));
	printf("%d\n", offsetof(struct S, d));
	printf("%d\n", offsetof(struct S, a));
	return 0;
}// 0  8  16 

1.5 结构体传参

        定义:Init初始化函数;切记:结构体初始化需要传参为  &结构体变量  ; 用指针来接收;因为定义的是结构体,所以接收的参数为

struct S* pc;指针访问为pc->结构体变量

struct S
{
	char c1;
	double d;
	int a;
};
Init(struct S* pc)
{
	pc->c1 = 'w';
	pc->d = 3.14;
	pc->a = 30;
}
int main()
{
	struct S s1 = { 0 };
	Init(&s1);
	printf("%c\n", s1.c1);
	printf("%.2lf\n", s1.d);
	printf("%d\n", s1.a);
	return 0;
}

1.6 结构体实现位段(位段的填充&可移植性)

1.6.1 什么是位段?

        位段的声明和结构是类似的。位段也属于结构体的一种类型;位段的位表示的二进制数;也就是我们往位段里面放数,5=101;10=1010,放进位段的是二进制数;

        1. 位段的成员必须是int 、unsigned int、signed int、short int (必须是是int 类型);

        2. 位段的成员名后面有一个冒号和一个数字;

ag. A就是一个位段类型;其中_A  _B  _C  _D均为位段的成员;位段成员冒号后面的数字表示位段成员所占的比特位:单位bit;1个字节等于8个bit;所以经过我们的计算,该位段总共47个比特位,是不是就是说该位段的大小(sizeof)就是6个字节,答案显然不是;位段也有自身的内存分配规则;

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

1.6.2 位段的内存分配规则

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

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

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

ag. 以该例题具体讲解位段是如何为结构体变量开辟内存的,也就是位段的大小,也可以说打印sizeof(位段);

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

        首先假设我们定义stuct A a ,结构体变量为a,位段为结构体变量开辟内存时,会根据位段的类型进行开辟,供所有位段成员使用,题中成员类型为int 型,我开辟一个整型供所有成员使用,int 占4个字节,32个比特位,a 占2个,b 占5个,c 占10个,32-17=15;是不够d 所使用的,位段规则定义15个bit舍弃,重新开辟一个int 型,32个比特位,供d 使用,舍弃2个bit位,所以该位段总共占8个字节;同时,位段设置时如果所占比特位大于该类型的字节大小,就会报错;也就是 int _e:33;系统就会报错,int型是无法承载33个比特位的;

        如果定义的char类型,则占一个字节,8个比特位;

1.6.3 位段存在的意义和具体数值的存放

        定义位段int _a:2;如果不使用位段,则_a 直接就会占用4个字节,32个比特位,而使用位段,只会占用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 = 20;
	s.c = 3;
	s.d = 4;

	return 0;
}

         初始化位段具体存放时:char-1个字节-8个比特位;第一个字节可以存放a b 占7个比特位,舍弃1个比特位;第二个字节放c,舍弃3个比特位;第三个字节放d,舍弃4个比特位;

        a=10,二进制1010;但a只占3个比特位,所以存放的是010;b=20,二进制10100,b占4个比特位,所以存放的是0100c=3,二进制为011,c占5个比特位,需要前面补0;所以放入地址的为00011d=4,二进制为100,d占4个比特位,前面补0,所以放入地址的为0100

        所以该位段占(大小)3个字节; 0010 0010 0000 0011 0000 0100 转换16进制数为:2 2 0 3 0 4;总结:位段先使用低位,在使用高位,多余的舍弃,不够的从新开辟新的地址,开辟字节的大小根据存放的类型进行选择;

2. 自定义数据类型_枚举

2.1 枚举类型的定义 

枚举顾名思义就是列举;枚举等同于结构体;枚举内部存放的是可能的情况(枚举常量);但需要区别于结构体;不是分号断开;是逗号断开,最后没有符号;

enum Sex
{
	MALE,
	FEMALE,
	SECURE
};

 枚举enum:打印结果为0  1  2 ;默认枚举常量是有值存在的;

enum Sex
{
	MALE,
	FEMALE,
	SECURE
};
enum Color
{
	RED,
	GREEN,
	BLUE
};
int main()
{
	enum Sex s = MALE;
	enum Color c = RED;
    printf("%d %d %d\n", RED, GREEN, BLUE);
	return 0;
}// 0 1 2

 枚举初始化;初始化以后就不能在更改了;

enum Sex
{
	MALE=2,
	FEMALE=3,
	SECURE=8
};

2.2 枚举的优点

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

2. 和#define定义的标识符比较枚举有类型检查,更加严谨;简单来说就是#define LED  P2^0  ,当程序使用到P2^0时,LED只是当做一个符号,没有具体类型;而枚举是有类型的;

3. 防止了命名污染(封装);枚举在大括号里,可以区别,防止重复;

4. 便于调试;

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

2.3 枚举的使用

enum Color
{
	RED=1,
	GREEN=2,
	BLUE=4
};
int main()
{
	enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异;
	clr = 5;
	printf("%d\n", clr);
	return 0;
}

2.4 枚举的大小

因为枚举只能定义一个枚举成员变量,枚举成员变量初始化默认是0 1  2  ;所以枚举的大小为一个整型的大小,也就是4个字节 ;

enum S
{
	RED,
	BLUE,
	GREEN
};
int main()
{
	enum S s = BLUE;
	printf("%d\n", sizeof(s));
	return 0;
}//4

3. 自定义数据类型_联合

3.1 联合类型的定义

联合也称作联合体,也称作共用体;联合定义的变量包含一系列的成员,这些成员共用同一块空间,即他们的地址是相同的。联合体的格式同结构体;        

union un
{
	char c;
	int i;
};

3.2 联合的特点

联合体的成员是共用同一块内存空间的,一个联合体变量的大小至少是最大成员的大小(简单来说就是本身需要有能力去保存最大成员的大小);

变相的来说就是定义的联合变量不能同时使用,因为在改变 c 的同时,会一并改变 i 的大小;

union un
{
	char c;
	int i;
};
int main()
{
	union un u;
	printf("%d\n", &(u.c));
	printf("%d\n", &(u.i));
	printf("%d\n", &u);
	return 0;
}//7337912
   7337912
   7337912

3.3 联合大小的计算

大小端存储方式:低字节放在高地址上,叫做大端存储模式;高字节放在低地址上,叫做小端存储模式;

ag. 代码实现判断大小端存储模式?

int main()
{
	int a = 1;// 00 00 00 01
	if (*(char*)&a == 1)
	{
		printf("小端\n");
	}
	else
		printf("大端\n");
	return 0;
}

把 a 的地址取出来,强制类型转换为char类型;解引用如果是1,则表示低端是1,低地址放在低端就是小端存储模式

联合的大小:联合大小至少是最大成员的大小;

                     当最大成员的大小不是最大对齐数的整数倍时,就要对齐到最大对齐数的整数倍;联合体的大小是最终对齐数的整数倍

union un
{
	int a;
	char arr[5];
};
int main()
{
	union un u;
	printf("%d\n", sizeof(u));
	return 0;
}// 8

int占用4个字节,对齐数是8;则 int 的对齐数是最小对齐数,也就是4;char占用 1 个字节,对齐数是8,则char 的对齐数是最小对齐数,也就是1;所以最大对齐数是4,联合体的大小是最大对齐数的整数倍;同时联合体的大小至少是最大成员的大小;所以最终联合体的大小是8;

你可能感兴趣的:(c语言)