C语言 自定义类型

在C语言当中,除了我们常用的几个基本的数据类型之外,还有一种类型叫自定义类型。比如:我们要描述一个学生。这个学生有姓名,性别,年龄,身高等。单独用基本的数据类型是不能完全描述的。这个时候就要使用我们自定义的类型来进行描述。自定义的类型有结构体,枚举和联合体。

一、结构体

结构体时一些值得集合,这些值被称为成员变量,每一个成员变量可以有不同的类型。

1,声明

struct tag//这里的tag表示标签,不是变量名

{

        member-list;//成员变量

}variable-list;//变量列表,用来定义变量。注意这里的分号

比如,描述一个人:

struct person
{
	char name[20];
	char sex[5];
	int age;
	char nation[20];
};

这里的变量列表可以没有。在变量列表里面定义的变量是全局变量。

2,定义且初始化

要定义,可以直接在变量列表里面,也可以在main函数里面定义。

struct person
{
	char name[20];
	char sex[5];
	int age;
	char nation[20];
}person1;//结构体变量person1

struct person person2;//结构体变量person2

int main(void)
{
	struct person person3;//结构体变量person3
    struct person person4[2];//结构体数组变量person4,有两个元素,都是结构体类型
    return 0;
}

要初始化也很简单。

#include<stdio.h>

struct person
{
	char name[20];
	int age;
}person1 = {"zhangsan", 18};//结构体变量person1

struct person person2 = {"lisi", 18};//结构体变量person2

int main(void)
{
	struct person person3 = {"wangwu", 18};//结构体变量person3
	struct person person4[2] = 
	{"abc", 18, {"def", 18}};//加不加{}都可以

	return 0;
}

有的时候,结构体还会出现嵌套的情况:

struct person
{
	char name[20];
	int age;
}person1 = {"zhangsan", 18};//结构体变量person1

struct people
{
	struct person person;
	char nation[20];
};

 对其进行初始化:

struct people people = { {"zhangsan",18}, "XXX" };//嵌套结构体的初始化

我们在声明结构体的时候,也可以不完全声明,这个时候省略掉标签。

struct
{
 int a;
 char b;
 float c;
}a;

有一种情况是不可以省略标签的,即使用typedef关键字的时候。typedef关键字能够为一种数据类型定义一个新名字。如果省略,就会报错。

typedef struct S
{
	int data[1000];
	int num;
}s;
	s s1 = { {1,2,3,4}, 1000 };

3,结构体成员的访问

结构体成员的访问需要通过点(.)操作符来进行访问的。下面我们来打印这几个初始化的变量。

#include<stdio.h>

struct person
{
	char name[20];
	int age;
}person1 = {"zhangsan", 18};//结构体变量person1

struct people
{
	struct person person;
	char nation[20];
};

struct person person2 = {"lisi", 18};//结构体变量person2

int main(void)
{
	struct person person3 = { "wangwu", 18 };//结构体变量person3
	struct person person4[2] = 
	{"abc", 18, {"def", 18}};//加不加{}都可以

	struct people people = { {"zhangsan",18}, "XXX" };//嵌套结构体的初始化

	printf("%s\n", person1.name);
	printf("%d\n", person1.age);
	printf("--------------\n");
	printf("%s\n", person2.name);
	printf("%d\n", person2.age);
	printf("--------------\n");
	printf("%s\n", person3.name);
	printf("%d\n", person3.age);
	printf("--------------\n");
	printf("%s\n", person3.name);
	printf("%d\n", person3.age);
	printf("--------------\n");
	printf("%s\n", person4[1].name);
	printf("%d\n", person4[1].age);
	printf("--------------\n");
	printf("%s\n", people.person.name);
	printf("%s\n", people.nation);

	return 0;
}

 现在,我们来看看结构体指针访问指向的变量的成员。有以下代码:

struct Stu
{
	char name[20];
	int age;
};

void print(struct Stu* ps)
{
    printf("name = %s   age = %d\n", (*ps).name, (*ps).age);
    //使用结构体指针访问指向对象的成员,为了简化,使用->操作符来替代(*).操作。
    printf("name = %s   age = %d\n", ps->name, ps->age);
}
int main()
{
    struct Stu s = { "zhangsan", 20 };
    print(&s);//结构体地址传参
    return 0;
}

#include<stdio.h>

struct person
{
	char name[20];
	int age;
};

struct people
{
	struct person* person;
	char nation[20];
};

int main(void)
{
	struct person person = { "zhangsan", 18 };
	struct people people = { &person ,"XXX"};

	printf("%s\n", people.person->name);
	printf("%d\n", people.person->age);
	printf("%s\n", people.nation);

	return 0;
}

 4,结构体传参

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

在传参的时候,参数需压栈。传递一个结构体对象的时候,由于结构体过大,参数在压栈的时候的系统开销比较大,导致性能的下降。另外,在计算结构体的大小的时候,如果是传的结构体的值,会导致结构体的大小无限的增大。所以,结构体在传参要传地址。

5,结构体内存对齐

现在,我们来讨论结构体的内存大小。

5.1,来看下面这段代码,计算它的大小(*):

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

int main(void)
{
	printf("%d\n", sizeof(struct S1));
	return 0;
}

 我们发现,这个结构体的大小并不是我们认为的6个字节,而是12个字节。

结构体内存的计算如下:

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

2,其他的成员变量要对齐到对齐树的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8,有的默认为4。

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

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

简单来说,就是:在内存里面,第一个变量放在开始的0地址处,其他的要放在什么地址处,要看对齐数。对齐数是默认对齐数和成员大小的最小值。按对齐数放了之后,这个结构体的大小是最大的对齐数的整数倍。

计算下面嵌套结构体的大小: 

struct S1
{
	char c1;//1
	int i;//4
};//大小为8

struct S2
{
	char c1;//1
	struct S1 s1;//8
	int d;//4
};


int main(void)
{
	printf("%d\n", sizeof(struct S2));
	return 0;
}

 在计算的时候,不可以直接把成员变量的大小直接加起来,在取对齐数的最大值倍数处。比如第一个计算(*)处:加起来是6,在取对齐数(4)的整数倍,结果是8,这显然和计算的结果不符合。

在来看最后一道:

struct S
{
	short s1;//2
	char c1;//1
	int s2;//4
};


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

 如果做的熟练了,也不必要画出图来,直接进行计算就可以了。有的时候会遇到short和char在一起的情况,这个时候,可以直接看成4个大小。

5.2,存在内存对齐的原因:

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

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

总体来说: 结构体的内存对齐是拿空间来换取时间的做法。这种做法是很常见的。

我们在设计结构体的时候,既要满足对齐,又要节省空间,那么就让占用空间小的成员尽量集中在一起。如果我们将(*)处的顺序变一下,比如两个char型放在一起,这个结构体的大小就会发生变化。

5.3,修改默认对齐数

#pragma pack(1)//设置默认对齐数为1
struct S2
{
	char c3;
	int i2;
	char c4;
};
int main()
{
	printf("%d\n", sizeof(struct S2));//6
	return 0;
}

还原为默认:

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

二、位段

1,定义和声明

结构体可以实现位段。位段的声明和结构体类似。位段是以位为单位来定义结构体(或联合体)中的成员变量所占的空间。采用位段结构既能够节省空间,又方便于操作。 比如位段A:

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

位段的成员名后边有一个冒号和一个数字。后面数字的单位是比特。位段的成员必须是整型家族。

2,位段的内存分配

位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。前面的位段A种,_a是int类型,是以4个字节开辟的。那么A的大小是多少(32位)?

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

int main()
{
	//输出的结果是什么?
	printf("%d\n", sizeof(struct A));
	return 0;
}

 这是如何计算的?

在这里,_a开辟了4个字节的大小,但是_a只需要两个比特就够了。在32位下,还有30个比特。_b需要5个比特,还剩下25比特,_c需要10个比特,这样就剩下15个比特,_d需要30个比特,剩下的15个比特不够,就在开辟4个字节大小的空间来存放_b。这里的图假设是从左开始放。

我们来看这一道题(在VS上是从右开始放):

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

int main(void)
{
	struct S s = { 0 };
	s.a = 10;
	s.b = 12;
	s.c = 3;
	s.d = 4;//在内存中是什么样的
    return 0;
}

3,位段的跨平台问题

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

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

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

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

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

三、枚举

枚举,就是一一列举。将可能的值进行一一列举。

1,定义:

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur,
 Fri,
 Sat,
 Sun
};
enum Day//星期
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

int main(void)
{
	printf("%d\n", Mon);
	printf("%d\n", Tues);
	printf("%d\n", Wed);
	printf("%d\n", Thur);
	printf("%d\n", Fri);
	printf("%d\n", Sat);
	printf("%d\n", Sun);

	return 0;
}

 以上定义的 enum Day是枚举类型。{}中的内容是枚举类型的可能取值,也叫枚举常量 。这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。

enum Day//星期
{
 Mon,
 Tues,
 Wed,
 Thur = 6,
 Fri,
 Sat,
 Sun
};

2,优点

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

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

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

4. 便于调试

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

3,使用

在switc语句当中,如果我们的分支太多,case后面的标签又是数字,这个时候,要知道这个数字代表什么,就需要去找。但是如果我们使用enum,就可以把数字换成让人一眼便知的常量名,大大提高代码的可读性。

enum Game
{
	EXIT,
	GAME_BEGIN,
	GAME_DISCONTINUE
};

int main(void)
{
	switch (1)
	{
	case GAME_BEGIN:
		printf("你开始了游戏!\n");
	case GAME_DISCONTINUE:
		printf("你中止游戏!\n");
	case EXIT:
		printf("你退出了游戏!\n");
	}

	return 0;
}

 四、联合体(共用体)

1,定义:

联合体也是一种特殊的自定义类型,这种类型定义的变量包含一系列的成员。这些成员是公用一块空间,所以也叫共用体。联合体的定义和结构体类似。

//联合类型的声明
union Un
{
 char c;
 int i;
};
//联合变量的定义
union Un un;

2,特点:

联合体是共用一块空间的,这样,这个联合变量的大小,至少是最大成员的大小,才能存放最大的那个成员。

#include<stdio.h>

union Un
{
	int i;
	char c;
};
union Un un;

int main(void)
{
	// 下面输出的结果是一样的吗?
	printf("%p\n", &(un.i));
	printf("%p\n", &(un.c));

    //下面输出的结果是什么?
    un.i = 0x44;
    un.c = 0x55;
    printf("%x\n", un.i);

	return 0;
}

 

从这里可以看出联合体的特点:共用一块空间。正因为如此,所以在打印un.i的时候,会发现结果是0x55。这里的0x44被覆盖了。由联合体的特点,可以用来判断当前计算机的大小端存储。

union Un
{
	int i;
	char ch;
};

int main(void)
{
	union Un un;
	un.i = 1;
	printf("%d\n", un.ch);//1,是小端
	
	return 0;
}

3,联合体大小的计算

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

union Un1
{
	char c[5];//共5个元素,每个占1个字节,总的大小为5
	int i;//4
};

int main(void)
{
	printf("%d\n", sizeof(union Un1));//8,是最大对齐数4的倍数。注意不是5,这里的5是数组总的大小
	return 0;
}
union Un2
{
	short c[7];//2*7
	int i;//4
};

int main(void)
{
	printf("%d\n", sizeof(union Un2));//16
	return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值