进阶C语言 第四章-------《自定义类型》 (结构体、枚举、联合)知识点+完整思维导图+深入细节+通俗易懂+基本练习题+建议收藏


绪论

        书接上回,通过上章的一些函数,我们可以让我们对于一些数值的调整有很大的帮助,本章自定义类型在C语言中同样也有着非常重要的地位,相信只要认真的阅读了本章,一定会对你有很大的帮助。

所以安全带系好,发车啦(建议电脑观看)。

        附:红色,部分为重点部分;蓝颜色为需要记忆的部分(不是死记硬背哈,多敲);黑色加粗或者其余颜色为次重点;黑色为描述需要

思维导图:

要XMind思维导图的话可以私信哈

目录

1.结构体

1.1结构体的声明

1.2结构体变量的初始化

1.3结构体内存对齐

1.4修改默认对齐数

1.5结构体传参

2.位段

3.枚举(enum)

3.1枚举的定义

4.联合体(共用体union)


1.结构体

结构指一些值的集合,这些值被称为成员变量,而结构体成员可以是不同的类型(类似数组,数组也是一些值的集合,不过这些数组的值的类型是统一的)。

而结构体就像一张张体检表,上面有着一个人的许多信息,或者某样物的许多信息的集合

1.1结构体的声明

知识点:

语法:

struct name

{

        member;

        .....;

};

对于里面的成员至少是1个;

下面直接通过代码的形式来展示如何声明(让这个结构体合法)一个结构体

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
};

        这样就声明了一个结构体,其实结构体的声明也可以看成创建了一个新的类型,里面的成员有着不同的类型来修饰这个新的类型,这样就能对于一些复杂对象进行描述如上的学生

细节(注意点):

一种特别的声明匿名结构体类型(不完全声明):

struct
{
	int a;
};

此时的声明没有名字,只能跟在该声明后创建变量名/用typedef的实现

而对于结构体变量的定义有两种方法:

  1. 直接跟在结构体后面,这种是全局的
  2. 在函数内以结构体类型 + 变量名 的形式来创建一个变量(结构体类型是一种自定义类型),并且是局部的

具体如下:

//方法1
struct
{
	int a;
}s1,s2;//可以同时创建一个或多个结构体变量(s1,s2)

struct Stu student = { "张三",20,"男"};//(全局的)

//方法2
struct Stu
{
	char name[20];
	int age;
	char sex[5];
};

int main()
{
	struct Stu student = { "张三",20,"男"};//(结构体变量student)

	return 0;
}

所以对于匿名结构体来说他是没有结构体的名字的所以就会导致无法进行第二种,只能第一种直接在结构体后面创建变量,来使用和初始化

用typedef来对结构体来进行简化重命名

typedef struct Stu
{
    int age;
    char name[20];
    char sex[5];
}Stu;
当typedef重命名匿名结构体时让匿名结构体就可以让其在其余地方用
typedef struct
{
	int a;
}a;


int main()
{
	a b = { 20 };
	printf("%d", b.a);	 	
	return 0;
}

  但是要注意当嵌套时不能省略里面
typedef struct Stu
{
    int name;
    struct Stu * Node;//不能写成Stu* Node因为此时还不行因为重命名还在后面才执行
}Stu;

1.2结构体变量的初始化

知识点:

对于结构体变量的初始化的方法同样分为了两种方法,对应着创建结构体变量的两种方法:

  1. 在全局的结构体变量后直接初始化
    struct Stu
    {
    
    	char name[20];
    	int age;
    	char sex[5];
    
    }s1 = { "张三",20,"男"};
  2. 在局部的结构体变量后再初始化
    struct Stu
    {
    	char name[20];
    	int age;
    	char sex[5];
    	struct Stu* a;
    };
    
    int main()
    {
    	struct Stu student1;
    	struct Stu student = { "张三",18,"男",&student1 };
    
    	
    	printf("%s %d %s %p", student.name, student.age, student.sex,student.a);
    	return 0;
    }
  3. 单独逐一初始化
    struct Stu
    {
    	char name[20];
    	int age;
    	char sex[5];
    }student;
    int main()
    {
    
    	student.age = 25;
    //字符串数组的话,你要想赋值,你就必须要通过拷贝
        strcpy (student.name ,"LiSi");
        strcpy (student.sex ,"男"); 
    	printf("%s %d %s",student.name,student.age,student.sex);	
    	return 0;
    }

细节:

  1. 不能在自己的结构体成员中有和自己一样的结构体类型这样的话这个类型的大小将无限大,(要创建只能是别的结构体类型)
    struct Stu
    {
    	int age;
    };
    
    
    struct Stu
    {
    
    	char name[20];
    	int age;	
    
    	//struct Stu b;error
    
    	struct Stu p;  
        char sex[5];
    }s1 = { "张三",20,{0},"男"};
    
    int main()
    {
    	printf("%s %d %s", s1.name, s1.age, s1.p.age , s1.sex);
    	
    	return 0;
    }
  2. 当创建了结构体变量后,对于结构体变量初始化,只能单独改变了,不能同时改变了(只能在struct Stu student = {....}这种形式时改)
    struct Stu
    {
    	char name[20];
    	int age;
    	char sex[5];
    };
    
    int main()
    {
    	struct Stu student1 = { "张三",20,"男" };
    
    	student = { "lisi",18,"nan" };error
    	student.age = 25;
    	printf("%s %d %s", student.name, student.age, student.sex);
    	
    	return 0;
    }
  3. 乱序初始化:
    struct Stu
    {
    	char name[20];
    	int age;
    	char sex[5];
    	struct Stu* a;
    };
    
    int main()
    {
    	struct Stu student1;
    	struct Stu student = { .a = &student1 , .age = 20 , .name= "lisi",.sex= "nan"};
    
    	
    	printf("%s %d %s %p", student.name, student.age, student.sex,student.a);
    	return 0;
    }

1.3结构体内存对齐

知识点:

是当我们要求一个结构体大小时就需要用到结构体内存对齐 

对齐规则:

  1. 第一个成员变量直接对齐到偏移量为0的地址处
  2. 从第二个成员变量开始要对齐到偏移量为自身对齐数的整数倍处                                                  对齐数 = 判断自身大小和默认对齐数并取较小值(在vs环境下默认的一个对齐数值是8
  3. 结构体的总大小最终要为所有成员变量中的取过的最大对齐数的整数倍
  4. 如果有镶嵌结构体,那这个最大对齐数的判断也要包括所镶嵌的结构体 内的 成员的对齐数 ,并且这个镶嵌结构体也要对齐到自身的最大的对齐数上(在外部的结构体内)

附:在Linux gcc 环境下没有默认对齐数 对齐数就是其本身大小

下面通过例子就可以更好的去理解和记忆

细节:

计算偏移量的宏offsetof

两个参数:offsetof(结构体类型成员名

头文件:#include<stddef.h>

为什么要有内存对齐

  1. 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。(如int 4 double 8)
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。(例:在32位机器上一次读取数据是4byte 假如结构体存中存着char 和 int  其中char 要占1byte int 占 4byte 这样总共就是5byte  那当我们要读取int 时机器就会先读取前面的4byte 那int还剩1byte要读所以就会再读一次,可若当我们进行了内存对齐就只需要一次就能将4byte的int 读完)
  3. 总体来说:结构体的内存对齐是拿空间来换取时间的做法。

所以因为存在内存对齐:所以一个成员相同的结构体,其大小都有可能不同:

struct s
{
    char c1;
    int i;
    char c2;  
};// 1 -> 4 + 4 -> 8 + 1 -> 12(要是最大对齐数的整数倍)

struct c
{
    char c1;
    char c2;
    int i;
};// 1 -> 1 + 1 -> 4 + 4 -> 8 

所以我们要有内存对齐的概念并且通过改变写法来节约空间

所以我们在写结构体时应该经量将小的成员放在一起

练习:

例1:

#include<stdio.h>
#include<stddef.h>
struct S1
{
    char c1;
    int i;
    char c2;
};
int main()
{
    printf("%d\n", sizeof(struct S1));   
    printf("%d\n", offsetof(struct S1,c1));
    printf("%d\n", offsetof(struct S1,i));
    printf("%d\n", offsetof(struct S1, c2)); 
}

分析:

  1. 首先将c1对齐到偏移量为0处                                                                                                              
  2. 其次看其余的成员变量:

    i --> 自身大小为4byte 、 默认对齐数大小为8byte 、取较小值4为最终的对齐数(取较小值)

    所以i要对齐到偏移量为4的倍数的地址去,0处往后就有偏移量为4处的地址可以 存i的4byte

    c2 --> 自身大小为1byte、默认为8、取1为对齐数(取较小值)

    c2要对齐到偏移量为1的倍数中去,4偏移处存了i的4byte后 就到了8偏移处 往后的任意位置都是1倍数,所以在8偏移处存c2                                                                                                    

  3. 最后:从0 ~ 8 结构体的总大小变成了9并不是移处不是最大对齐数(1 4 1)4的倍数,所以最终要放到偏移量为11处,所以最终大小为12byte(0 ~ 11)

偏移量:可以看成距离起始位置(箭头指向的)的距离(看线)

附加一种我自己的判断方法(不用画图直接口算):

第一个成员直接放(毋庸置疑),从第二个成员开始就要满足从对齐数的整数倍地址放,这样举个例子来解释:如当一个int 类型大小为4 所以就要从偏移量(而这个偏移量就直接找倍数即可不用想的太多)为 4处开始 ,那么就有:

从某一开始的偏移量(满足对齐数的倍数) +  自身元素大小 = 总大小  /  所到的偏移量处(继续往后开始的偏移量)

这个公式是我自己总结的可以细品,多来两道体就可以很快的算出结构体大小

具体如下:

struct name
{
    int a;
    char b;
    int c;
    int d;

};

直接口算:

先放了int 这样就是 0 + 4 = 4 此时还没放完  所以是位移到了4偏移量处 (继续往后开始的偏移量

再放char 此处因为char的对齐数肯定是1 所以可以放到任何位置  4 + 1 = 5 

再放int 应该找到8偏移处开始, 所以就是 8 + 4 = 12 移动到12偏移处(继续往加开始的偏移量,直接找倍数"8"

再放int 直接从12开始 ,12 + 4 = 16 就为最终的总大小

再快一点就可以直接 :4 -> 4 + 1 -> 8(直接找倍数"8")+ 4 - > 12(已经是4的倍数) + 4 = 16

例2:

struct S3
{
	double d;// 对齐数8
	char c;// 1
	int i;//4
};//总大小为 8 -> 8 + 1 -> 12 + 4 -> 16

struct S4
{
	char c1;// 1
	struct S3 s3;// 16
	double d;// 8
};
//此处double类型的大小是8 默认也是8 所以最终对齐数就是8 ,其次注意嵌套结构体 放在外部的结构体时要对
		//齐到嵌套结构体内部最大对齐数的整数倍即8的倍数
int main()
{
	printf("%d\n", sizeof(struct S4));//总大小为:1 - > 8(直接找倍数) + 16 - > 24 + 8 -> 32
	return 0;
}

附:对于结构体成员是数组是如 char arr[5];这是其实可以直接把他们看成5和char类型的成员,char c1 ;char c2 ;char c3 ;char c4 ;char c5;在用对齐知识进行正常对齐   

1.4修改默认对齐数

知识点:

如标题所示:默认对齐数是可以修改的方法如下:

#pragma pack( )在括号内填所要修改成的值:如#pragma pack( 4 ) 

如果#pragma pack( )单独出现时表示回复默认对齐数

细节:

不能随意的修改默认对齐数应该根据所需,如当修改成了#pragma pack( 1 )这样其实也就不存在内存对齐了

就会导致这时的对齐数一定都是1

所以当对齐方式不合适时,我们就可以自行修改默认对齐数

1.5结构体传参

知识点:

当我们把结构体变量放在函数里时,我们同样可以分成值传递、和址传递。

具体如下:

struct s
{
    int a;
    char b;
}s1;


void print1(struct s s1)
{
    printf("%d\n",s1.a);
    printf("%c\n",s1.b);
}

void print2(struct s * s1)
{

    printf("%d\n",(*s1).a);
    printf("%c\n",(*s1).b);

    printf("%d\n",s1->a);
    printf("%c\n",s1->b);
}



int main()
{
    print(s1);
    print(&s1);

    return 0;
}

当然,print2传地址肯定比print1传值是要好的(函数传参要压栈,如果结构体过大对于时间和空间上都会有损失),所以最好直接统一直接传结构体的地址

练习:通讯录(学生信息管理系统)​​​​​

2.位段

2.1位段的基本知识

知识点:

位段是一种附于结构体之上的一种改变结构体成员所占内存的功能(类似结构体的一种新的结构体),位(比特位);

位段的用法是在结构体成员后加上冒号和数字位段的成员一般只是int / unsigned int / signed int 、char(整形家族)

位段的意义对于数值来说有时候一些数值并不一定需要一个整形的大小(32bit)才能放的下,他可能只需几个bit即可放的下,所以通过位段的形式来对所开辟的空间进行限制来节约空间

细节:

  1. 一般来说位段里的成员类型都是相同的或者是其有无符号位的类型
  2. 通过其结构体成员的类型来依次开辟空间4byte(int)、1byte(char),只有当不够的时候才会再次开辟
  3. 位段的使用,对于后面的数字来说不能超过其自身类型大小int (32bit)char(8bit)
  4. 因为位段涉及这许多的不确定因素,所以位段是不跨平台的,如果是可移植程序中要慎用(避免)

通过代码进行具体讲解:

 #define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct A
{
	int a : 2;
	int b : 5;
	int c : 10;
	int d : 30;
};

int main()
{
	
	printf("%d", sizeof(struct A));//最终打印出8,而不是16
    
	return 0;
}

对于上面的代码为什么最终大小是8:

首先其全部是int类型,所以先开辟32bit,a的所需大小是2bit,所以还剩30,b:5,所以还剩25、c:10 ,还剩15,当到d时,所剩的空间已不够,所以就需要才开辟32bit,所以最终开辟64bit(8byte)

对于第一次开辟的空间最后还剩下的15bit 是否使用 这C语言中并没有严格的规定 所以就导致位段有不确定因素

对于位段的跨平台问题

1. int 位段被当成有符号数还是无符号数是不确定的。
2. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题,此时整形的最大bit是16)。
3. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义(vs2019右到左)
4. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的(vs舍弃)。
 

练习:

  1. 当开辟了一个32bit的空间第一个 成员变量的值 从左边开始放还是从右边开始放也就是对于位段在内存中是如何存成员的数据的?
  2. 以及当空间不够时,到底是利用剩余的空间再加上新开辟的空间还是直接只用新开辟的空间?                                                                                                                                        下面通过调试的方法来看

通过调试的方法我们可以得到在vs2019环境下:

(小端存储):实际应该是 04 03 62 此时位段是将前名的成员从右往左开始放的,并对于前面剩下的空间也不会利用(最后的03 04 就能看出:04(d)并没有用到前面剩下的空间0)

数据的存储问题

  1. 对于开辟好的空间存放成员的数据时他会从右往左一次放进对应的bit
  2. 如果之前开辟的空间不够后他不会再利用那些不够的空间,而是直接将数据放在新空间内

 

 对于b.d 我就不写了方法是一样的,先将要存的数字转化成二进制,在通过所分配的bit 将这个二进制放到内存中,如果空间不够就开辟新空间(新空间开辟的地址大于老空间)

   最终大小就是3byte


3.枚举(enum)

对也就是一一列举,对一些值进行列举:如一周的星期,一年的月份

将这些周进行列举,创建一个枚举常量来确定这个实际的事

3.1枚举的定义

知识点:

语法如下

enum name

{

        e1,

        e2,e3

};

对于枚举名(name)一般起的有意义点,枚举常量(e1,e2,e3)间用逗号隔开最后一个不需要加,而枚举常量是枚举变量的可能取值(enum name s = e1

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

细节:

每个枚举常量都有对应可代替的数值,当你不初始化时他默认从0开始递增

这样你就可以通过Mod来代替0;

当然你还有自定义开始的值,从后开始跟着递增,如:

 所以你在整月份时就可以将 Jan = 1 然后以此递增

不能在枚举外的其余地方进行枚举常量的修改

对于枚举定义常量相较于#define定义常量的区别:

  1. 增加了可读性
    1. 有了类型检查                                                                                                                
  2. 将常量进行了封装让其不能被轻易更改
  3. 可以调试
  4. 方便使用,一次定义多个常量

对于枚举的大小,因为其内容可以代表整形所以其大小也和整型一样(4byte)


4.联合体(共用体union)

知识点:

联合体同样也是一种自定义类型,这种类型定义的变量也包含一系列的成员,而这些成员不同于结构体他们是共用同一块空间的(他就想一个自习室,大家都可以用,不过大家的用法不同,你可能要用到电脑、而别人只看书空间共享,当使用方法可能不同,并且假如上一个人在使用完后没有拿自己的电脑,就会导致下一个人来对时候电脑还在这个空间内)。

联合体的大小

  1. 联合体这块空间的大小是至少等于一系列成员中最大的成员的类型大小
  2. 并且这块空间的大小也要对齐其中最大对齐数的整数倍

细节:

 #define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
union A
{
	char arr[6];
	int a;
};
int main()
{
	printf("%d", sizeof(union A));//8
	return 0;
}

附:对于联合体所共用的空间,当前一个成员用过后,如果另一个成员使用时并不覆盖的话仍然会存留之前的数据,并不会自动销毁

练习:

写一个函数来实现判断计算机是大端还是小端

union A
{
	char arr;
	int i;
};

int Judge_system(union A* a)
{
	a->i = 1;
	return a->arr;
}

int main()
{
	union A a = {0};
	int ret = Judge_system(&a);
	if (ret == 0)
	{
		printf("大端\n");
	}
	else
	{

		printf("%d小端\n",ret);
	}
	return 0;
}

本章完。预知后事如何,暂听下回分说。

持续更新大量C语言细致内容,三连关注哈

猜你喜欢

转载自blog.csdn.net/ZYK069/article/details/129036074