栏目分类:
子分类:
返回
文库吧用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
文库吧 > IT > 软件开发 > 后端开发 > C/C++/C#

c语言入门---自定义类型:结构体,枚举,联合

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

c语言入门---自定义类型:结构体,枚举,联合

目录标题
  • 结构体
    • 一.为什么会有结构体
    • 二.如何来声明一个结构体
    • 三.结构体变量的创建和初始化
    • 四.结构体的特殊声明
    • 五.结构体内存对齐
    • 六.为什么会有内存对齐
    • 七.修改默认对齐数
    • 八.结构体传参
  • 位段
    • 一.什么是位段
    • 二.位段的内存分配
    • 三.位段的缺点
  • 枚举
    • 一.枚举类型的定义
    • 二.枚举的使用
    • 三.枚举的优点
  • 联合(共用体)
    • 一.联合类型的定义
    • 二.联合体的特点

结构体 一.为什么会有结构体

首先我们来想一个问题为什么会有结构体类型,在前面的c语言学习过程在外面学习了那么多的类型,比如说浮点型,各种长度的整型,等等,那么我们这里为什么还有结构体类型呢?因为在我们的生活中有那么一些事物用我们所学的类型是无法来描述的,比如说我们学生,大家觉得我们如何来描述一个学生呢?使用整型数组描述还是用浮点数来描述呢?大家发现都不大合适,因为我们一个学生既有名字也有性别,还有对应的爱好和身高等等,我们发现这种复杂的对象并不是我们所学的类型无法描述,而是我们得把多个类型组合在一起来描述,所以我们这里就有了结构体类型,这个结构体就是将多个相同的或者不同的类型组合在一起形成一个新的类型,那么我们就可以通过这个自定义的新类型来描述一个更加复杂的对象。

二.如何来声明一个结构体

我们首先来看看结构体声明的基本结构:

首先就是我们的结构体标识符struct这个是用来声明一个结构体的,而我们结构体后面的name就是我们声明的结构体类型的名字,而我们下面的花括号里面的内容就是对应的结构体成员,是由不同的类型来组成的,最后再由分号来结尾表示我们的结构体声明结束,那么我们可以看一个例子,就拿我们上面例子来演示-如何创建一个学生的结构体变量来描述一个学生呢?那么首先我们就得把大体的框架写出来:

#include
struct student
{
	
};
int mian()
{
	return 0;
}

那么我们这里就把结构体的声明放到main函数的前面,然后我们这里的struct就表示这是一个结构体的声明,后面的student就是这个结构体类型的名字,那么接下来就得往这个花括号里面添加具体的类型来描述我们学生这个对象,首先我们的学生得有姓名,所以我们首先能够想到的就是添加一个字符类型的数组用来描述我们学生的姓名,因为我们学生的姓名不会很长所以我们就把这个数组的大小设置为20,其次我们学生得有年龄,所以我们还可以创建一个整型的变量来描述我们的学生的年龄,那么我们这里就可以再往里面添加个short类型的变量进去,因为我们的学生还有性别所以我们这里还可以再添加一个字符类型的数组进去用来描述我们学生的性别,因为我们每个学生都有一个特定的学号,所以我们这里还得在这个结构体里面添加一个浮点型的数组来描述我们的学生的学号,那么想到这里我们基本上就描述完了一个学生,那么我们的代码的实现就如下:

#include
struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
};
int mian()
{
	return 0;
}

那么这里我们的结构体的声明就完成我们这里就创造了一个结构体类型出来,既然我们这里创建的是结构体的类型,那么我们就可以通过这个类型来创建一个结构体的变量出来,那么我们如何来创建一个结构体的变量呢?并且如何来对其进行初始化呢?我们接着往下看:

三.结构体变量的创建和初始化
#include
struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
};
int mian()
{
	return 0;
}

我们上面的代码创建了一个结构的类型这个类型是用来描述我们的学生的,那么既然是类型的话我们就可以通过这个类型来创建出一个变量来描述一个学生,那么这里我们就创建了三个变量分别来描述我们的学生一,学生二和学生三:

#include
struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
};
int mian()
{
	struct student stu1;
	struct student stu2;
	struct student stu3;
	return 0;
}

那么这里在创建结构体变量的时候大家得注意的一点就是:我们在创建变量的时候得把struct这个关键字加上,因为这个关键字表示的是这个是一个结构体变量,不能单独的写一个结构体类型名上去。那么我们变量创建好之后可以对其进行初始化,那么我们初始化的形式就是用一个大括号将不同的初始化内容包含在一起,然后用逗号将这些不同的内容进行隔开,但是有一点要注意的就是我们这里初始化的顺序得是按照我们结构体里面创建变量的顺序来进行初始化,比如说我们上面的结构体的顺序就是先名字再年龄再性别最后学号,那么我们在初始化的时候也得是这个顺序,不能将其顺序颠倒这样的话内容就会出错,更有可能会产生越界访问等错误,那么我们初始化的代码就如下:

#include
struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
};
int mian()
{
	struct student stu1 = {"zhangsan",18,"male","2021307446"};
	struct student stu2 = { "lisi",19,"male","2020305778" };
	struct student stu3 = {"jingxiang",20,"female","2019304556"};
	return 0;
}

那么这里有小伙伴们就要说了如果遇到结构体的嵌套又该如何来做呢?那么这里我们还是照葫芦画瓢嘛,既然你嵌套了一个结构体进去,那么我们初始化的时候就在这个花括号里面再加一个花括号不就够了嘛,这个里面的花括号的内容就是初始化的内部嵌套的结构体的内容,比如说下面的这个例子:

#include
struct father
{
	char name[20];//描述学生爸爸的名字
	int age;//描述学生爸爸的年龄
};
struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
	struct father f;//嵌套一个结构体用来描述学生的爸爸
};
int mian()
{
	struct student stu1 = { "zhangsan",18,"male","2021307446",{"wangwu",48}};
	return 0;
}

那么这里我们就可以看到我们声明一个结构体的时候内部还嵌套了一个结构体,那么我们在对其初始化的时候就得在嵌套的结构体的位置上再加上一个花括号用来初始化内部嵌套的结构体的内容。那么我们的结构体的初始化就完成了剩下的就是我们如何来使用我们结构体里面的内容:那么这里就用到了我们的十分熟悉的两个操作符

我们首先来看看第一个,这个操作符就是一个点,我们首先声明一个结构体类型,那么这里我们就用这个结构体来描述一个学生,既然是学生的话,那么是不是就有该学生的性别,身高,年龄,名字该四个最基本的特征,那么我们就用这四个特征来创建出一个结构体:

#include
struct student
{
	char name[10];
	char sex[10];
	int age;
	int high;
};
int main()
{

	return 0;
}

我们把结构体的类型声明好了,那么接下来我们就要用这个类型来创建变量,并且将其初始化:

#include
struct student
{
	char name[10];
	char sex[10];
	int age;
	int high;
};
int main()
{
	struct student s = { "zhangsan","nan",18,180 };
	return 0;
}

这里我们创建了一个变量s,并且将其名字初始化为张三,性别为男,年龄为18岁,身高180cm,那么这里我们将这个变量初始化完了,那么我们要想将他的每个变量都打印出来那该怎么做呢?首先我们可以想到的是肯定是可以用到我们printf函数的,但是printf函数得需要一个具体的变量,比如说我创建了一个变量a,我要打印这个变量a的值的话我就得在printf函数中的最后把a加上,那么我们这里的变量s的类型是个结构体那该怎么办呢,我如果单单的把这个s写上去那肯定是不可以的,因为这个给s包含的变量有多个,所以我们这里就得用到我们这里说的操作符,我们将这个变量s后面加上一个点然后再加上你想要打印的变量就可以了:

#include
struct student
{
	char name[10];
	char sex[10];
	int age;
	int high;
};
int main()
{
	struct student s = { "zhangsan","nan",18,180 };
	printf("学生的姓名为:%sn",s.name );
	printf("学生的性别为:%sn",s.sex);
	printf("学生的年龄为:%dn",s.age );
	printf("学生的身高为:%dn",s.high);
	return 0;
}

但是我们这个操作符他只能针对我们的结构体,如果是结构体的指针的话就得用到这个操作符: ->
比如说我们这里要创建这个函数这个函数的功能是修改我们这个结构体变量的内容的,那么我们结构体和我们的变量有个同样的特征就是如果函数传值调用的话,系统是会自己创建一个空间,然后把传过来的内容进行复制一份,然后你要是在函数里面进行修改的话,改的是复制过来的内容,并不是实际的内容,所以我们这里要传址调用,那么我们函数接收也就得用struct student *进行接收:

#include
#include
struct student
{
	char name[10];
	char sex[10];
	int age;
	int high;
};
void change(struct student* ss)
{
	strcpy(ss->name, "lisi");
	strcpy(ss->sex, "nv");
	ss->age = 19;
	ss->high = 165;
}
int main()
{
	struct student s = { "zhangsan","nan",18,180 };
	printf("学生的姓名为:%sn",s.name );
	printf("学生的性别为:%sn",s.sex);
	printf("学生的年龄为:%dn",s.age );
	printf("学生的身高为:%dn",s.high);
	change(&s);
	printf("修改之后:n");
	printf("学生的姓名为:%sn", s.name);
	printf("学生的性别为:%sn", s.sex);
	printf("学生的年龄为:%dn", s.age);
	printf("学生的身高为:%dn", s.high);
	return 0;
}

我们来看一下运行之后的结果为:

那么这里总结一下如果遇到的是结构体的变量名那么就用这个点( . )如果是结构体的指针那么就用
-> 。

四.结构体的特殊声明

我们首先来看一段代码

#include
struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
}stu4,stu5,stu6;
int mian()
{
	struct student stu1 = {"zhangsan",18,"male","2021307446"};
	struct student stu2 = { "lisi",19,"male","2020305778" };
	struct student stu3 = {"jingxiang",20,"female","2019304556"};
	return 0;
}

我们来看这段代码与我们上面看到的代码有什么区别?是不是最大的感受就是我们这里的代码在声明结构体的时候在分号的前面多了stu4,stu5,stu6这么一句话,那这句话是什么意思呢?啊这里的意思就是在声明结构体的时候顺便创建了3个变量stu4,stu5,stu6,那么这里变量和我们main函数的里面创建的三个变量的区别就是我们这里在声明的时候创建的变量是全局变量,而我们在main函数里面创建的变量是局部变量,那么这里就是我们两个不同的地方创建的结构体变量的不同之处,那么有时候我们还会见到这样的结构体声明的形式:

struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
}* stu4,stu[20];

那么这里我首先问大家的就是我们这里的stu4是什么?很显然这里是一个由该结构体类型所创建出来的结构类型的指针,我们可以把这个代码简化成这样大家就很好的理解:struct student* stu4,那么后面的stu[20]是什么呢?也很明显这里表示的是一个数组,该数组有20个元素,并且每个元素的类型都是该结构体类型,该数组的名字是stu,那么这里都是我们在声明结构体的时候顺便创建的一些变量,那么有时候我们会觉得这些结构体很麻烦啊,每次创建一个结构体变量的时候都得加一个struct上去这就显得十分的麻烦那么我们能不能将其进行简化呢?答案是当然可以,那么我们这里就用到了之前学的typedef这个关键字来对其进行重命名,比如说上面的例子我们就觉得这个给struct student这个名字太长了,那么我们就可以在创建结构体变量的时候顺便对其进行重命名操作来简化这个名字,那么我们具体的操作如下:

#include
 typedef struct student
{
	char name[20];//描述学生的名字
	int age;//描述学生的年龄
	char sex[10];//描述学生的性别
	char id[20];//描述学生的学号
}stu;

int main()
{
	stu stu1 = {"zhangsan",18,"male","2021307446"};
	stu stu2 = { "lisi",19,"male","2020305778" };
	stu stu3 = {"jingxiang",20,"female","2019304556"};
	stu3 = stu2;
	printf("%s %d %s %s", stu3.name, stu3.age, stu3.sex, stu3.id);
	return 0;
}

那么我们将修改之后的名字就写在分号的前面这样,我们以后就可以用这个修改之后的名字来创建变量,那么这里大家得注意一个问题就是我们这里如果选着对其进行重命名的话,那么我们是不能在通过声明的时候顺便创建全局变量的,这里我刚刚尝试了一些虽然你写上去了,他是不会报错,但是你进行调试的话你你就会发现啊压根就没有创建这个变量希望大家能够注意一下,那么我们再来看下面一个代码:

#include
struct
{
	int a;
	int b;
	int c;
}x;
int main()
{
	return 0;
}

大家看看这个代码有什么特殊奇怪的地方,那么眼睛尖锐的小伙伴们一下字就发现了我们这里视乎没有没有结构体类型名啊,那么这里我们就将这种没有名字的结构体变量称为匿名结构体,这种结构体变量只能在结构体声明的时候顺便创建几个结构体变量,在其他的地方就无法再创建该类型的变量了,为什么呢?答案很简单因为我没名字啊!你咋找的到我呢?对吧。其实我们还有一种方法来验证我们匿名结构体的特殊之处我们可以创建两个匿名结构体,这两个匿名结构体的内部的变量都一模一样我们可以先通过第一个匿名结构体来创建一个变量,再通过第二个匿名结构体变量创建一个指针,再将第一个变量的地址取出来放到我们创建的指针里面,那么我们这里如果能够顺利的放进去的话就说明这两个匿名结构体的类型是一样的,如果报出了警告就说明就算我们这里是两个类型相同的匿名结构体,但是我们编译器依然会认为这两个结构体的类型不相同,那么我们的代码如下:

#include
struct
{
	int a;
	int b;
	int c;
}x;
struct
{
	int a;
	int b;
	int c;
}*p;
int main()
{
	p = &x;
	return 0;
}


我们可以看到编译器报出来警告说两个的类型不一样,那么这里就说明了一件事情就算我们这里是两个类型相同的匿名结构体,但是我们编译器依然会认为这两个结构体的类型不相同,那么大家以后写代码的时候还是得注意一下。

五.结构体内存对齐

我们说变量都是有大小的,我们创建的每一个变量都会在内存当中申请空间,比如说一个int类型的变量就会向空间申请4个字节,一个float类型的变量也会像空间申请4个字节,那么我们的一个结构体变量又会向空间申请多少个字节呢?很多小伙伴们就会说啊啊这肯定得取决于这个结构体里面的类容嘛,那么我们就看看下面这个结构体来算算它所创建的变量的大小是多少:

#include
struct s1
{
	char c1;
	int c2;
	char c3;
}tem1;
int main()
{
	printf("%d", (int)sizeof(tem1));
	return 0;
}

大家想想这个代码打印的结果是多少?有小伙伴看到这里就说啊这还不简单嘛一个结构体里面有两个char类型的变量和一个int类型的变量,一个char类型的变量占1个字节,一个int类型的变量占4个字节所以这里的结构体不就占了6个字节嘛,那么我们可以将这这个代码运行一下看看结果如何
嗯?是12并不是6,那么这是机灵的小伙伴就说啊,哦我知道了结构体的大小就是我们所有变量的大小的和乘以两倍嘛,6*2=12嘛,那么如果有小伙伴们这么想的话那么可以看看下面的这个代码:

#include
struct s1
{
	char c1;
	char c3;
	int c2;
}tem1;
int main()
{
	printf("%d", (int)sizeof(tem1));
	return 0;
}

大家可以看到我们这个代码最大的不同就是我们将两个char类型放到了前面然后将int类型放到了最后,那么这样的话大家觉得还会是12嘛,啊这时候就有小伙伴们说啊,啊这有什么区别啊肯定是12啊,那么我们将程序运行一下:

我们发现答案并不是12这是为什么呢?我们就稍微改变了结构体内部变量的位置就能够使得结构体的大小发生改变,那说明了什么?我们结构体的大小并不是所有变量的大小加在一起这么简单,而是有一套专门的规则来计算着这个大小,那么我们来看看这个规则是啥:

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

那么我们如何来理解这4句话呢?我们就直接来通过上面的例子来讲讲,首先来看看那个12是怎么算出来的,我们首先来看第一个规则:第一个成员在结构体变量偏移量为0的地址处,那么这个规则就是我们结构体中第一个变量所在的位置起始位置为0,那么我们这里的第一个变量的是char类型的变量所以我们这里0这个位置放的就是char这个变量

好char类型这个变量放进去之后剩下的就是我们的int类型变量,但是我们这个int类型的变量并不是直接放进去的,而是有一个规则来限定它放的一个位置,那么这个限定的规则就是:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数=编译器默认的一个对齐数与该成员大小的最小值,我们vs编译器的默认值是8,那么看到这个规则之后我们首先想到的一件事就是得把我们的对其数算出来,而我们的对齐数的算法就是变量的大小和8这两个数中取较小的那个,那么我们这里int类型的大小是4而默认对齐数是8所以我们这里的对齐数就是4,而我们把对齐数算出来之后我们就要把这个成员变量放到这个对齐数的整数倍的地址处,因为我们这个对齐数是4所以我们就要把这个变量放到地址为4的倍数的地方,所以我们这里就会把地址为1,2,3处的空间浪费掉从地址数为4的地方开始占用空间,所以我们接下来的图像就是这样:

那么最后就是将我们最后的char变量放进去因为我们char变量的大小为1,比我们的默认对齐数小,所以我们这里char变量的对齐数就是1,而我们要把char类型的变量放到1的任意整数倍的地址数上去,而我们这里的地址书都是整数,所以都是1的整数倍,所以我们这里就直接将char类型的变量放到我们地址数为8的位置上去,

那么这里我们所有的变量都放进去了,按道理来说我们的最终的大小应该是9,但是这里我们这里还有一个规则还得改变我们最终的大小就是:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。那么我们这里的三个变量的对齐数分别为1 ,4 ,1所以最大的对齐数就是4那么我们这里的结构体的总大小就得是这个最大对齐数的整数倍,也就是4的整数倍,那么我们刚刚算出来的大小是9不是4的整数倍,所以编译器就会对这个结构体的大小进行扩大再往下浪费3个字节的大小这样的话就变成了12就成为了我们4的整数倍,所以我们结构体的大小就是12,图如下所示:

那么这里想必大家应该能够明白如何来算一个结构体的大小了,那么我们将int类型放到最后的情况应该也能够很好的算出来,我们的图就长这样:

我们稍作修改我们的结构体的大小就重12变成了8,那么通过这个例子我们就发现了一个规律就是我们在创建一个结构体的时候尽量先将占内存小的变量放前面将占的内存大的变量放到结构体的最后这样可以在某些方面节省我们的内容,但是有小伙伴们就要说了我咋知道你说的是对的还是错的呢,那么这里我们就可以通过一个函数来验证我们的猜想我们c语言给了一个函数offsetof函数这个函数就是专门来求我们结构体中变量的偏移量的,那么我们就可以通过下面的代码将结构体中的每个偏移量全部打印出来:

#include
#include
struct s1
{
	char c1;
	int c2;
	char c3;
}tem1;
int main()
{
	printf("c1的偏移量为:%dn", (int)offsetof(struct s1,c1));
	printf("c2的偏移量为:%dn", (int)offsetof(struct s1,c2));
	printf("c3的偏移量为:%dn", (int)offsetof(struct s1,c3));
	printf("%d", (int)sizeof(tem1));
	return 0;
}

我们来看看这段代码运行的结果为:

在对比一下我们画的图就可以发现这个与我们讲的一模一样,那么这里我们还剩下最后一个问题没有解决就是我们结构体嵌套的大小又如何计算呢?那么按照上面的规律我们知道了一件事情就是我们要想求出结构体的大小首先要找对每个元素的对其数,那么我们的嵌套在里面的结构体的对齐数该如何来计算呢?那么我们这里的最后一条规则就告诉了我们答案他说:如果嵌套了结构体的情况,嵌套的结构体对其到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。那么这句话就告诉了我们嵌套的内部的结构体的最大对齐数就是自己内部的元素的最大对齐数,那么我们来看一个例子:

#include
struct s3
{
	double d;
	char c;
	int i;
};
struct s4
{
	char c1;
	struct s3 s3;
	double d;
};
int main()
{
	printf("%dn", (int)sizeof(struct s4));
	return 0;
}

我们来一步一步的分析这个这个打印出来的结果是多少首先是首先是将char变量放到偏移量为0的地址处:

然后就遇到了我们的嵌套的结构体变量,那我们在算这个之前我们就得先把这个结构体的变量对齐数算出来,我们内部嵌套的结构体变量的对齐数就等于内部元素的最大对齐数,那么这里就是我们内部的double元素他的对齐数就是8所以我们这里的内部结构体就会直接对其的地址为8的地方,再把我们的嵌套的结构体里面的元素放进去:

最后在把外部结构体剩下的内容再对其放进我们的内存当中,因为剩下的内容是一个double类型的变量所以这里的就会对其到地址为24的位置上去重24开始占8个位置到31结束:

这样我们的内容就排完了剩下的就是结构体整体的对齐,那么我们这里剩下的一步就是结构体整体的对齐,那么我们这里的最大对齐数就是我们这里的8,而我们的结构体的大小就得是这个8的整数倍,因为我们这里排完之后大小刚好是32为8的整数倍所以我们这里就不用再做更多的操作所以我们这里的大小就是32,我们来看看代码的运行的结果:

六.为什么会有内存对齐

大家看了上面的内容之后肯定会有点疑问在上面就是我们的结构体为什么要进行对齐呢?对齐的意义又是什么呢?那么这里z主要就是两个原因:
第一个:平台原因(移植原因)
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处获取某些特定类型的数据否则就会抛出硬件异常。那么这句话是什么意思呢?啊就是有些位置里面的数据在某些平台下我们是不能获取和更改的,那么为了能够使得这些代码具有移植性我们就可以可以通过对齐的方法来跳过这些无法读取的地址。
第二个:性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于为了访问未对齐的内存,处理器需要做两次内存访问,而对齐的内存访问仅需要一次访问。这样就提高了我们代码的性能,那么总的来说就是我们的结构体的内存对齐就是拿空间来换取时间的做法。比如说我们一个结构体里面装了两个变量第一个是char类型的变量,另外一个就是int类型的变量如果不对齐的话我们32位机器要访问第二个int类型的变量得访问几次啊?是不是两次啊,因为第一次char类型的变量存在所以第一次只访问到了3个字节,所以还得再来访问一次,如果对齐了呢?是不是一次就可以将我们的int类型的数据全部都访问到,那么是不是就增加了我们的读取效率,所以这也是我们的对齐的另外一个好处。

七.修改默认对齐数

既然有时候对齐数会使得我们的内存变大照成很多内存的浪费,那么我们有没有办法能够使得我们的对齐数变小呢?当然有,我们的vs编译器可以修改我们的默认对齐数,我们可以将对齐数修改成1 这样的话就不会出现对齐的事情,那么我们这里是通过#pragma pack( x)来修改我们的默认对齐数,修改完之后再通过#pragma pack()将对齐数再修改回去,那么我们可以通过下面的这一段代码来看看我们对齐数是否能够得到修改:

#include
#pragma pack(1)//将默认对齐数修改成1
struct num1
{
	char a;
	int b;
	char c;
};
#pragma pack()//将默认对齐数恢复到8
struct num2
{
	char a;
	int b;
	char c;
};
int main()
{
	printf("默认对齐数修改成1的大小为:%dn", (int)sizeof(struct num1));
	printf("默认对齐数修改成8的大小为:%d", (int)sizeof(struct num2));
	return 0;
}

我们来看看这段代码的运行结果,我们发现确实可以修改。

八.结构体传参

这里我们就直接上代码:

#include
struct s
{
	int data[1000];
	int num;
};
struct s s1 = { {1,2,3,4},1000 };
void print(struct s s1)
{
	printf("%dn", s1.num);
}
void print1(struct s* s1)
{
	printf("%dn", s1->num);
}
int main()
{
	print(s1);//传结构体
	print1(&s1);//传指针
	return 0;
}

我们来看看这段代码,这两个不同类型的打印有什么区别呢?我们发现这两个打印最大的不同就是我们一个传的是结构体的地址一个传的是结构体,那么这两个传法的根本不同在于,如果你传的是结构体的话他会再开辟一个空间将我们这个结构体的内容全部拷贝一份,而我们又知道函数在传参的时候是需要压栈的,就会有时间和空间上的系统开销,如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销就会比较大就会导致性能下降,而如果我们采取的是传地址的话我们这里就只需要将这个结构体的地址拷贝一份即可无需更多的性能上的开销,所以我们结构体在传递的过程中多半都采用传递地址的形式,但是传递结构体并不是一无是处,比如说我们为了防止程序员在传递地址时而一不小心改变了结构体的原本的内容就可以采用传递结构体的形式,但是这个功能我们传递指针的时候也可以做到我们只用在结构体指针的前面加上const就可以做到,那么这里我们就得出一个结论就是在结构体传参的时候最好传递结构体的地址,这样可以提高我们的性能。

位段 一.什么是位段

首先我们的位段的作用时为了节省空间的,位段的声明和结构时类似的,但是有两个不同的就是:

  1. 位段的成员必须时由int,unsigned int,signed int,char类型组成。
  2. 位段的成员后边必须有一个冒号和一个数字。

那么这里我们这里就可以来看一个例子:

#include
struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};
int main()
{
	printf("%d", sizeof(struct A));
	return 0;
}

那么我们这里的A就是一个位段类型,那么我们的位段A的大小是多少呢?有同学就说啊这里有4个int所以这里的位段的大小就是16个字节,那么如果这里是16个字节的话,那我就觉得位段就可有可无了,因为我们的位段本来就是用来节省空间的,所以我们现在所知道的事情肯定就是比16个字节药效的那么到底是多少呢?我们可以先运行一下来看看:

我们发现答案竟然是8,那么这个8是怎么来的呢?那么我们接着往下看。

二.位段的内存分配

首先我们要知道的一点就是我们的位段在空间上是按照需要以4个字节(int)或者一个字节(char)的方式来开辟,那么我们上面的代码因为元素都是整型所以我们这里是每四个字节四个字节的开辟,所以我们这里的一开始就会向内存申请4个字节,那么我们这里冒号表示的是比特位,那么一个变量a所占的内存就是2个比特位,一个变量b占的就是5个比特位,一个变量c占的就是10个比特位,那么此时就已经用掉了17个比特位了,那么还剩下15个比特位,那么这个15个比特位是无法放下我们的变量d的,因为我们的变量需要30个比特位,所以我们这里就会再申请4个字节的内存,来装下我们的d这样的话就是我们这个位段所占的空间就是8个字节,那么我们这里的8就是这么来的,但是这里就有了这么一个问题,我们这里的d它有没有用第一次剩下的那15个比特的空间呢?那么带着这样的疑问我们再来看看下面这个例子:

#include
struct s
{
	char a : 3;
	char b : 4;
	char c : 5;
	char d : 4;
};
int main()
{
	struct s s1 = { 0 };
	s1.a = 10;
	s1.b = 12;
	s1.c = 3;
	s1.d = 4;
	return 0;![请添加图片描述](https://img-blog.csdnimg.cn/ff4db525cb7d491a907fb0e059b659c8.png)

}

那么我们来一步一步的分析这个代码首先我们创建一个位段s1,然后将这个位段的全部内容都改成0,然后再来一步一步的修改这个位段的内容,首先我们将s1中的a改成10,而我们知道10的二进制是1010,但是我们这里的a只有3个比特位所以我们这里就只能将010装进去,而且我们这里假设内存的使用是从右往左使用的,因为这里的元素都是char所以我们这里申请字节就是一个字节一个字节的申请,那么我们第一步的操作结果就如图所示:

然后接下来的操作就是将这个位段中的b改成12,12对应的二进制的码是1100,而我们的b分配得到的也是4个字节所以我们这里就可以刚好转下,因为我们申请的空间还剩下5个字节可以装下我们的b所以我们这里就不会申请空间,所以我们这里的操作就如下:

接下来的操作就是把位段中的c改成3,而我们的位段中的c占的内存的是5个比特位所以我们这里的剩下的空间就不够了所以我们就会再向操作系统申请1个字节来装下我们的这个c并且将他的值改成3也就是00011
那么最后就是将位段中的d改成4,我们的d需要4个比特位,那么我们剩下的位置不够所以我们这里又会向操作系统申请一个字节的空间,那么这里的4对应的就是0100,那么我们的图就变成了这样
那么这里根据我们的假设在内存中的二进制应该就是这样的,那么我们这里是二进制,但是我们内存中显示的是16进制,所以我们就得将其转化为16进制再来与内存中的内容进行对比,那么我们这里转换的结果就是:620304,那么我们再去内存中看看:
我们可以看到我们的位段在内存中的存储也是620304那么这就说明我们的猜想和vs中的逻辑是一样的,我们每次都申请一个字节,如果内存不够的话就会再申请一个字节,并且之前留有的内存便不会再使用,而且内存的使用是从右向左使用的,那么看到这里我们就得来讲讲位段的缺点了。

三.位段的缺点

我们的位段涉及很多不确定的因素,位段是不跨平台的,注意可移植的程序应该避免使用位段,这是为什么呢?那么我们这里就来讲讲位段的跨平台问题:

  1. int位段被当成有符号数还是无符号数是不确定的。
  2. 位段中最大位的数目是不能确定的。在16位机器当中最大时16,32位机器最大是32,写成27在16位机器上就会出现问题。这里我要提醒大家一点的就是我们写位段的时候冒号后面的数字不能超过前面的那个变量的大小,比如说int在我们32位平台下的int就是4个字节也就是32个比特位,那么你冒号后面的数字就不能超过32,所以有时候我们在32位平台下写的代码在16位平台下跑就会出现问题,所以这里大家要注意一下。
  3. 位段中的成员在内存中是从左向右分配的还是从右向左分配的在标准当中是未定义的,我们这里vs他是从右向左分配的,但是在其他平台下就不一定是的,所以这也是一个问题所在。
  4. 当一个结构包含两个不同位段是,第二个位段成员比较大,无法容纳于第一个位段剩下的位时,是舍舍弃剩余的位还是利用这个是不确定的在有些编译器下是使用但有些是直接舍弃。

那么我们这里总结一下,我们的位段和结构相比,位段可以达到同样的效果,而且还可以节省空间,但是我们的位段存在跨平台的问题这里大家要注意一下。

枚举

在我们的生活中有那么一些事物他是可以一一列举的比如说我们生活中的星期这个就是可以一一列举出来,星期一,星期二一直到星期天,还有我们生活当中的三原色:红绿蓝,以及我们生活中的月份这种都是可以一一列举出来的东西我们都可以使用枚举将他们列举出来那么接下来我们就来讲讲枚举是如何来进行使用的。

一.枚举类型的定义

我们来看看一段代码:

enum day
{
	Mon,
	Tues,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};
enum color
{
	RED,
	GREEN,
	BLUE
};

这里就是我们定义的两个枚举类型那么我们这里的枚举的关键字就是我们这里的enum这个就表示的是枚举,那么我们的大括号里面的内容就是我们的枚举常量,这些常量用逗号将其进行隔开,最后再用分号结尾表示我们的枚举类型的定义结束,那么这里我们的枚举是有默认的值的,比如说我们这里的星期里面的7个常量这些常量都是有默认值比如说这里的Mon的默认值就是0,剩下的就是以此类推加1,Tues就是1 ,Wed就是2这样依次往后的类推,所以我们这里的枚举常量的的取值都是默认从0开始的一次递增1,当然在我们在定义的时候也是可以对其进行赋值的,比如说我们这里想将Mon的值赋值为1,将Tues的值赋值为5那么我们就可以这么做:

enum day
{
	Mon=1,
	Tues=5,
	Wed,
	Thur,
	Fri,
	Sat,
	Sun
};

那么这么做的话我们的Mon的值就变成了就变成了1 ,我们Tues的值就变成了5,但是我们下面的值也会依次发生改变下面的Wed就随着上面的值依次加一Wed就变成了6,Thur就变成了7,那么如果我们是从后面的地方开始更改默认值的话我们的前面的值是不会发生改变的比如说下面的代码:

#include
enum day
{
	Mon,
	Tues,
	Wed=100,
	Thur,
	Fri,
	Sat,
	Sun
};
int main()
{
	printf("%dn", Mon);
	printf("%dn", Tues);
	printf("%dn", Wed);
	printf("%dn", Thur);
	printf("%dn", Fri);
	return 0;
}

我们是从Wed开始更改的默认值,那么我们Wed前面的值就不会发生更改还是重0开始往后的值依次加一直到你修改的地方开始就发生更改,然后后面的值就还是随着这个更改的值依次加一,比如说这里Mon的值就是默认为0,Tues的值就是Mon的值加1,因为我们这里的Wed我们认为使其发生了修改所以他就变成了100,那么这个值往后的值就会随着这个100值往上加一,那么我们这里打印出来的结果就是那么这里大家不知道发现了一个问题没有就是我们这里的值默认是从0开始往后依次加一,那么我们将原来本应该为3的位置改成0,会发生什么情况呢?就想这样

#include
enum day
{
	Mon,
	Tues,
	Wed,
	Thur=0,
	Fri,
	Sat,
	Sun
};
int main()
{
	printf("%dn", Mon);
	printf("%dn", Tues);
	printf("%dn", Wed);
	printf("%dn", Thur);
	printf("%dn", Fri);
	return 0;
}

我们来直接看看打印出来的结果:

我们发现不仅仅是原来为3的地方的值变成了0而且后面的位置相继发生了改变,而且这样的改变使得与前面的默认值相同了,这样的话就会对我们后面的代码照成一定的困扰,如果你以后为了防止相同的话那么我还是建议你在修改默认值的时候尽量往大的值进行修改。

二.枚举的使用

那么我们上面是对枚举的类型进行了一个定义,也就是说创建出来了一个大致的框架,那么我们这里就可以通过这个框架来创建出我们的变量,比如说下面的代码就是我们通过这个框架创建出来的变量:

#include
enum day
{
	Mon,
	Tues,
	Wed,
	Thur ,
	Fri,
	Sat,
	Sun
};
int main()
{
	enum day day1 = Mon;
	enum day day2 = Tues;
	enum day day3 = Wed;
	return 0;
}

那么这里就有同学说啊我们这里的枚举他是有默认值的,那么我们这里能不能通过这样的形式来修改我们这个变量的值呢?就比如说这样:day1=5;答案是不行的啊,因为我们这里的day1他是一个变量,这个变量的类型是一个枚举的类型,那么我们将一天整型的值赋值给一个枚举类型的变量那么是不是会报错啊,哎有些小伙伴们就跑去试了一下发现没报错啊,那这是因为前面c语言情况下的编译器检查的不是很严格如果你将它改成c++的背景下的话你就会发现他报了一个这样的错误:

这就说明我们这里的枚举变量的类型是枚举类型的,并不是默认的整型,那么看到这里相比小伙伴都知道了枚举的使用了,但是相比很多小伙伴们都有那么点疑问就是我们这个枚举有什么用呢?那么接下来我们就来讲讲我们枚举的优点。

三.枚举的优点

这里我们就来讲讲枚举的优点:
第一点:
就是我们的枚举能够增加我们代码的可读性性和可维护性。那么我们这个怎么来理解呢?我们首先可以回忆一下我们之前在写小游戏的时候写的那个菜单,上面写着0就是退出游戏,1就是开始游戏,2就是调整难度等等等,再到我们马上就要写的那个通讯录,0就是退出,1就是增加联系人,2就是删除联系人,3就是显示联系人这一系列的操作,那么我们是将这些操作放到我们的switch语句里面来进行的选着,那么我们这里就有这么一个问题我们写case后面的是1是2是3,那我在写的时候我怎么会记得这些1 2 3 4 对应的是什么呢?难道我还得一个一个的往菜单里面看去找吗?那么这里显然就很麻烦,所以我们就可以将这些操作放到我们枚举常量里面去,用这些枚举常量来代替我们的我们这里的1 2 3 4,这样我们在写代码在维护代码的时候就能够很大程度上的提高我们的代码的可读性。
第二点:
有同学肯定有这么个疑问就是为什么我们不用#define来代替我们的枚举呢?这不是同样的作用嘛,那么这里我们要知道的一点就是我们这里的#define他定义的标识符他是直接替换的,他没有对应的类型,但是我们的枚举是具有类型的,所以我们在使用枚举的时候他是他会有对应的类型的的检查这样的话我们的写的代码就更加的严谨
第三点:
防止命名污染,我们这里将有关的变量全部都放到了一起去了这样的话我们就不用这里创建一个变量,那里创建一个变量的,这样的话就容易照成命名污染降低代码的阅读性
第四点:
便于调试我们这里的枚举变量他与我们的#define定义的标识符还有一个非常大的区别就是我们#define的标识符他在运行的时候是直接进行替换的,而我们枚举是一个变量,这样的话我们的变量在调试的时候是能够看到变换的过程的,而我们的#define定义的标识符那就看不到,无法对这个变量进行调试。

联合(共用体)

如果说我们的结构体是一个居民楼的话,每个家庭都有一个单独房子,而且每个家庭都得为工商面积多出一点钱的话那么我们的联合就是多个家庭住一个房子。简称胶囊房。

一.联合类型的定义

我们首先来看看下面的代码,我们这里创建了一个联合体,这个联合体里面装有两个变量:

#include
union un
{
	char a;
	int i;
};
int main()
{
	union un c = { 0 };
	printf("%pn", &c);
	printf("%pn", &(c.a));
	printf("%pn", &(c.i));
	return 0;
}

那么这里的union就是这里的联合的标识符,那么我们这里的union un表示的就是这个整体是一个联合的类型,那么我们下面就通过这个联合的类型创建的一个变量,我们再将这个变量的整体的地址取出来,再将这个变量的里面的联合的成员的a的地址取出来,再将这个联合里面的成员i取出来的地址取出来再打印出来看看我们就会发现一个现象就是:

我们这里的三个地址一模一样那么就说明了一件事情就是我们这里的联合确实用的同一块地址,那么我们这里联合大小是多少呢?首先我们肯定知道一点就是我们这里联合的大小至少得是成员中最大的成员的大小,那么我们这里可以用代码运行出来看看:

#include
union un
{
	char a;
	int i;
};
int main()
{
	union un c = { 0 };
	printf("%dn", (int)sizeof(c));
	return 0;
}

我们可以打印出来看看:

我们发现大小确实是4,但是我们这里就不要简单的认为我们的这里的联合的大小就是最大成员的大小哦,那是因为我们这里不需要对齐,我们可以看看这个例子:

#include
union un
{
	short c[7];
	int i;
};
int main()
{
	union un c = { 0 };
	printf("%dn", (int)sizeof(c));
	return 0;
}

我们来看看这个大小是多少,因为我们这里是short类型的数组,所以我们这里的大小至少是14,但是我们这里的最大对齐数是4(我们这里虽然是数组但是他的对齐数还是得看他的内部元素的类型)那么这里的类型是short所以对齐数是2,又因为这里还有个int所以我们这里的最大的对齐数就是4,所以我们这里联合的大小就得是我们这里的最大对齐数的整数倍,那么我们这里的联合的大小就会扩大一点变成16,所以我们这里用的术语是至少,那么知道了如何创建一个联合体以及如何计算一个联合体的大小,那么我们接下来来看看联合体有什么特点。

二.联合体的特点

我们联合体就和我们的共享单车很想,你用的时候我就不能用,我在用的时候你就不能用,因为我们这里的联合的成员是公用一块内存的空间,所以我们在通过里面的成员的来改变内存的大小的时候很可能会影响另一个成员的值,比如说下面的代码:

#include
union un
{
	int i;
	char c;
};
int main()
{
	union un u1;
	u1.i = 0x11223344;
	u1.c = 0x55;
	printf("%x", u1.i);
	return 0;
}

我们一开始将这个联合的内容初始化为11223344,然后再通过u1.c来修改这个内容,那么我们这里打印出来的时候就会发现我们这里内容就发生了修改:

但是修改也只能修改它本身的位置,不能将每个位置都修改,但是也会对整体的值照成影响,那么我们这里就知道了一件事就是我们这里的联合只能供一个成员使用,当一个成员使用的时候另外一个成员的值就可能会发生一定的修改,那么这里我们本章的内容就结束了,感谢大家阅读。
点击此处获得代码

转载请注明:文章转载自 www.wk8.com.cn
本文地址:https://www.wk8.com.cn/it/1037094.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 wk8.com.cn

ICP备案号:晋ICP备2021003244-6号