目录
引子:构造类型不是基本类型的数据结构也不是指针,是由若干个相同或不同类型的数据构成的集合
结构体_struct
结构体的概念
概念:结构体是一种构造数据类型,是由一种或多种基本数据类型或构造类型的数据的集合
结构体的定义
1、先定义结构体类型,再定义结构体变量
struct 结构体类型名{
成员列表;
};
struct 结构体类型名; //定义结构体变量
2、定义结构体类型的同时定义结构体变量
用这种方法定义结构体,此后仍然可以定义结构体变量
struct 结构体类型名{
成员列表;
}结构体变量1,结构体变量2; //定义结构体同时定义结构体变量1,结构体变量2
struct 结构体类型名 变量3,变量4; //定义结构体变量3,4
3、无名结构体类型的定义
在定义结构体类型的时候,由于无名结构体没有结构体类型名,因此只能在定义结构体的同时定义结构体变量,此后就无法再定义结构体变量了
struct{
成员列表;
}变量 1,变量 2; //以后不能再定义相关结构体类型的数据
4、结构体类型重命名定义
将结构体重命名,为其增添一个新的结构体类型名。下方的代码相当于将结构体重命名为A,此时A和struct stu是等价的
typedef struct stu{
成员列表;
}重新定义的结构体类型名 A; //用这种方法就不能再定义结构体同时定义结构体变量了
结构体变量的初始化
结构体变量的初始化方式:
1、定义结构体变量时初始化
例如:
struct student
{
int age;
char name[100];
char sex[20];
};
struct student John = {18,"John","man"};
2、可以通过 .成员变量的形式改变初始化顺序
例如:
struct student
{
int age;
char name[100];
char sex[20];
};
struct student John = { .age = 18, .sex = "man", .name = "John" };
3、定义结构体类型的同时定义结构体变量并初始化
例如:
struct student
{
int age;
char name[100];
char sex[20];
}John = {18,"John","man"};
结构体成员的访问(结构体变量的使用)
使用格式:
结构体变量. 结构体成员(非指针变量)
结构体变量->结构体成员(指针变量)
注意,(*x).val的写法完全等效于x->val。->运算符可以看作是对*的封装(类比数组的'[ ]'运算符),并不是只有指针类型才能用->,这只是一种写法的优化。
例如:
John.name=18;
strcpy(Lucy.name, "John");
结构体的使用案例
结构体的嵌套
相同类型的结构体变量可以相互赋值
结构体指针同样满足此规则
结构体数组
概念:结构体数组是个数组,是由若干个相同类型的结构体变量构成的集合,也就是结构体和数组的结合使用
定义方法:struct 结构体类型名 数组名[元素个数]; 例如:
struct stu{
int num;
char name[20];
char sex;
};
struct stu edu[3];
//定义了一个struct stu 类型的结构体数组edu,
//这个数组有3个元素分别是edu[0] 、edu[1]、edu[2]
结构体数组成员的访问:数组名[下标] .成员
代码案例:
结构体指针
概念:结构体指针即结构体的地址,结构体指针变量用来存放这个地址
定义方法:struct 结构体类型名 *结构体指针变量名; 例如:
struct stu* s;
结构体指针成员的引用:
方式1: (*结构体指针变量名).成员
方式2: 结构体指针变量名->成员
例如:
typedef struct{
int a;
double b;
char c[30];
}try;
int main()
{
try *x,*y;
//首先在堆区为结构体开辟空间
x=(try*)malloc(sizeof(try));
y=(try*)malloc(sizeof(try));
方式1 (*y).a=18;//*y就相当于p指向的变量 try
方式2 x->a=100;
方式2 x->b=3.71828;
strcpy(x->c,"hellow!");
return 0;
}
结构体的内存分配
偏移量与offsetof函数
偏移量:偏移量就是在个位置相对于首位置的之间发生偏移的量,例如30相对12的偏移量是18。其中,结构体的第一个成员永远都放在0偏移处
offsetof函数:
头文件:stddef.h 宏的原型:offsetof(type, member-designator) 宏的功能:offsetof会生成一个类型为 size_t 的整型常量,它是结构成员相对于结构开头的字节偏移量。 参数说明: type - 结构体 member-designator - 成员 |
用法示例:
//下面的实例演示了 offsetof() 宏的用法。
#include <stddef.h>
#include <stdio.h>
struct address {
char name[50];
char street[50];
int phone;
};
int main()
{
printf("address 结构中的 name 偏移 = %d 字节。\n",
offsetof(struct address, name));
printf("address 结构中的 street 偏移 = %d 字节。\n",
offsetof(struct address, street));
printf("address 结构中的 phone 偏移 = %d 字节。\n",
offsetof(struct address, phone));
return(0);
}
运行结果:
address 结构中的 name 偏移 = 0 字节。
address 结构中的 street 偏移 = 50 字节。
address 结构中的 phone 偏移 = 100 字节。
结构体的内存对齐规则
*规则1:给结构体变量分配内存的时候,以所占字节数最大的基本类型大小为单位开辟内存。
但double比较特殊:
vs环境,double类型以8字节为单位开辟内存
gcc环境,double类型以4字节为单位开辟内存
*规则2:每个成员都要对齐到其对齐数的整数倍处。其中,这个对齐数的大小为:该成员大小与默认对齐数的较小值。当没有默认对齐数时,对齐数就是成员自身的大小。
其中,vs环境下默认对齐数是8,gcc环境下没有默认对齐数。
要注意,double类型比较特殊:
vs环境,double是 8 字节对齐,即起始内存单元的编号是8的倍数
gcc环境,double是 4字节对齐,即起始内存单元的编号是4的倍数
但是无论是哪种环境,double型变量都占8字节大小,只是字节对齐方式不同
规则3:成员中出现了数组时,数组可以看成多个变量的集合;如果嵌套了结构体,可以把其展开看待。
规则4:开辟内存的时候,从上向下依次按成员在结构体中的位置顺序开辟空间。整个结构体的大小,必须是最大对齐数的整数倍,最大对齐数包含中嵌套的结构体成员中的对齐数。
示例图解
代码分析
为什么要有内存对齐?
-
性能提升:当数据按照对齐要求存储时,读取和写入操作的效率更高。因为处理器通常会以内存块(比如字、双字、四字等)为单位进行数据传输,而不是以单个字节为单位。如果数据没有对齐,处理器可能需要进行额外的操作来处理不对齐的数据,这会增加访问内存的开销,降低性能。
-
缓存优化:现代计算机通常都有多级缓存,而缓存的数据块也是按照一定的规则进行对齐的。当数据按照对齐要求存储时,缓存的命中率更高,从而提高了缓存读取的效率。
-
平台兼容性:某些硬件平台要求数据以特定的方式进行对齐,如果不满足对齐要求,可能会导致运行时错误或未定义行为。因此,在跨平台开发时,确保内存对齐可以增加代码的可移植性和可靠性。
柔性数组(变长数组)
在结构体的最后可以定义一个数组,这个数组很灵活,可以在不同情况下开辟不同大小的内存空间。详情可以参考这篇博客:C语言 - 柔性数组(变长数组)_真-小白菜的博客-CSDN博客
结构体注意事项
1、C语言规定,结构体内不能进行赋值操作
2、结构体可以定义在全局,也可以定义在局部,但一般都是定义在全局
3、定义结构体变量时不要忘记写struct
4、位段可以很好的节省空间,但是有跨平台的问题存在,需要谨慎使用。
5、结构体传参最好使用指针。这是因为函数在传参的时候,参数是需要压栈,会有时间和空间上的系统开销。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。因此结构体传参的时候,可以用传结构体的地址(即结构体指针)的方式很巧妙的避免这个问题
修改默认对齐数
使用#pragma pack可以改变默认对齐数
使用格式:
#pragma pack (value) 指定对齐值为value
#pragma pack() 取消设置的默认对齐数,还原为默认值
注意事项:
1.value最好设置为1、2、4、8等(2^n)
2.实际对齐数为成员自身大小和默认对齐数的较小值,即如果将默认对齐数设置的过大(大于成员自身大小),可能不会产生任何效果,例如:
补充内容:Pragma
Pragma 指令指定计算机特定或操作系统特定的编译器功能。 以 #pragma 开头行指定 pragma 指令。 使用 Microsoft 特定 __pragma 关键字可以在宏定义内编写 pragma 指令。 C99 中引入并由 C++11 采用的标准 _Pragma 预处理器运算符与之类似。
语法:
#pragmatoken-string
__pragma(token-string) // 两个前导下划线 - Microsoft 特定扩展
_Pragma(string-literal) // C99
详细内容可以查看微软的参考网站:
Pragma 指令与 __pragma 和 _Pragma 关键字 | Microsoft Learn
位段(位域)
在结构体中,以位为单位的成员被称之为位段(位域)。C语言允许在一个结构体中以位为单位来指定其成员所占内存长度,这种以位为单位的成员称为“位段”或称“位域”( bit field) 。利用位段能够用较少的位数存储数据。
如果想要详细了解位段的话可以看这篇博客:C语言位段_小白麋鹿的博客-CSDN博客
共用体(联合体)_union
认识共用体
共用体也叫联合体,和结构体类似,也是一种构造类型的数据结构。在进行某些算法的时候,需要使几种不同类型的变量存到同一段内存单元中,几个变量所使用空间相互重叠。在C语言中,这种几个不同的变量共同占用一段内存的结构被称作“共用体”。
共用体的定义
定义共用体的方法和结构体类似,直接把用union替换stuct就可以了。例如:
union Un {
/*这里与结构体类似*/
};
共用体的内存分配
共用体所有成员占有同一段地址空间,即共用体变量的地址和它的各成员的地址都是同一地址。同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用。共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖。例如:
#include <stdio.h>
union Test_Union {
char ch;
int num;
};
int main()
{
union Test_Union Un;
Un.num = 0x111223344;
printf("%#x\n", Un.ch); /* VS2022输出结果:0x44 */
return 0;
}
共用体大小的计算
共用体的大小也满足对齐原则。所以共用体的大小至少是最大成员的大小,当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。例如:
#include<stdio.h>
union Un1
{
char c[5];
int i;
};
union Un2
{
short c[7];
int i;
};
int main()
{
printf("%d\n", sizeof(union Un1)); //输出结果:8
printf("%d\n", sizeof(union Un2));
//输出结果:16
}
枚举_enum
认识枚举
举可以将变量的值在枚举值表中逐一列举出来,在枚举值表中应列出所有的可用值称为枚举元素,枚举变量只能取枚举值列表中的枚举元素
枚举的定义
枚举的定义与结构体、共用体等基本类似,具体方式如下:
enum 枚举类型名{
枚举值列表;
};
用法示例
#incLude <stdio.h>
enum week{
mon, tue, wed, thu, fri, sat , sun
};//定义一个枚举类型week
int main()
{
//枚举类型变量的定义
enum week day1;
//枚举类型变量的初始化
enum week day2=tue;
return 0;
}
枚举注意事项
1、枚举值是常量,不能在程序中用赋值语句再对它赋值。例如 sun=5;mon=2; sun=mon; 等都是错误的
2、在枚举值表中应列出所有的可用值称为枚举元素,枚举变量只能取枚举值列表中的枚举元素
3、并不是只有枚举变量才能使用枚举元素,而是枚举变量只能使用枚举元素。枚举元素和宏类似,文件的任何地方都可以直接使用枚举元素
4、枚举元素默认是从0开始顺序定义为0,1,2等数字。例如在week中,默认mon值为0,tue值为1……sun值为6
5、在C语言中,枚举类型是被当做 int 或者 unsigned int 类型来处理的,所以枚举元素只能为整数
6、枚举可以改变枚举值的默认值,如
enum week //枚举类型
{
mon=3, tue, wed, thu, fri=4, sat,sun
};
//枚举元素的值:mon=3, tue=4, wed=5, thu=6, fri=4, sat=5,sun=6
解释:在定义枚举类型时可以用等号给枚举元素赋值,用来代表元素从几开始编号在程序中。但是此后就不能再次对枚举元素赋值了,因为枚举元素是常量
最后
结构体,共用体,枚举的的使用大同小异,三者之间有着很大的共性,同时它们还保持各自的特性。如果对这篇博客有任何疑惑或建议,欢迎对小白私信反馈。