C语言--结构体
C语言--结构体
- 一、结构体类型的声明
- 1.1 结构的声明
- 1.2 结构体变量的定义和初始化
- 1.3 结构体成员的访问
- 1.3.1 直接访问
- 1.3.2 间接访问
- 1.4 结构体的自引用
- 1.5 结构体的重命名
- 二、结构体内存对齐
- 2.1 对齐规则
- 2.2 内存对齐的原因
- 2.3 修改默认对齐数
- 三、结构体传参
- 四、结构体实现位段
- 4.1 什么是位段
- 4.2 位段的内存分配
- 4.3 位段的跨平台问题
- 4.4 位段使用注意事项
- 4.5 位段的应用
在C语言中,已经为我们提供了许多内置类型,比如说
int
,
char
,
double
等,但是这些内置类型所描述的变量过于单一。如果我们想描述一个复杂的信息,比如说学生,要包括姓名,年龄,性别,学号等等,所以仅仅使用这些内置类型是远远不够的。C语言为了解决这种问题,让我们创造合适的类型和变量,就提供了一种
自定义的数据类型——结构体。
下面我们就来探讨一下结构体的有关知识。
一、结构体类型的声明
1.1 结构的声明
原型:
struct tag
{member-list;//成员变量列表
}variable-list;//结构体变量列表(注意后面有分号)
结构体是一些值的结合,这些值称为成员变量,每一个成员变量可以是不同类型的变量。
member-list
: 各个成员变量集合
variable-list
:结构体变量名称(可以省略,在主函数等地方声明)
tag
:结构体类型的标志
例如我们来声明一个用于描述学生信息的结构体:
struct Student
{char name[20];//姓名char sex[6];//性别int age;//年龄char id[20];//学号
};
1.2 结构体变量的定义和初始化
结构体变量可以在结构体创建时定义,也可以在主函数等其他地方定义
struct Student
{char name[20];//姓名char sex[6];//性别int age;//年龄char id[20];//学号
}s1, s2;//全局变量struct Student s3;//全局变量int main()
{struct Student s4;//局部变量struct Student arr[6];//数组return 0;
}
结构体变量的顺序初始化:
struct Student
{char name[20];//姓名char sex[6];//性别int age;//年龄char id[20];//学号
}s1 = { "zhangsan","male", 20, "001" };//全局变量struct Student s2 = {"lisi","male", 18, "002"};//全局变量int main()
{struct Student s3 = {"wangwu", "female", 19, "003"};//局部变量return 0;
}
当然也可以不按照顺序来初始化,可以指定成员变量进行初始化:
使用点操作符.成员变量名 =
的形式来指定初始化。
struct Student
{char name[20];//姓名char sex[6];//性别int age;//年龄char id[20];//学号
};int main()
{struct Student s4 = { .sex = "famale", .id = "002", .age = 20, .name = "stephen" };return 0;
}
1.3 结构体成员的访问
1.3.1 直接访问
结构体成员的直接访问使用点操作符(.
),使用形式:结构体变量名.成员变量名
struct Point
{int x;int y;
};int main()
{struct Point p1 = { 1,2 };printf("%d %d", p1.x, p1.y);//1 2return 0;
}
1.3.2 间接访问
有时候我们得到的不是一个结构体的变量,而是一个结构体的指针,那么就需要使用使用一个结构体访问操作符->
,使用形式:结构体指针->成员变量名
struct Point
{int x;int y;
};int main()
{struct Point p1 = { 1,2 };struct Point* ptr = &p1;printf("%d %d", ptr->x, ptr->y);//1 2return 0;
}
1.4 结构体的自引用
在结构体中也可以包含该结构体类型的变量
但是下面这种写法是错误的:
struct Node
{int data;struct Node n;
};
struct Node
还没有定义完全,就在内部使用该类型声明其他变量,编译器会无法识别该变量。
而且这种定义,如果使用sizeof(struct Node)
来计算该结构体类型的大小,会发现会无限循环下去,大小就会无限大。
那么想在结构体内部存放另一个该结构体类型的数据,就可以存放指向另一个结构体的指针,通过指针来找到另一个结构体。
正确的自引用方式:
struct Node
{int data;struct Node* next;
};
1.5 结构体的重命名
在定义一个结构体之后,我们每一次在主函数中创建一个该结构体类型的变量都要将整个结构体名字打上,未免有点繁琐。
struct Point
{int x;int y;
};int main()
{struct Point p1 = { 1,2 };printf("%d %d", p1.x, p1.y);return 0;
}
所以我们可以使用typedef
关键字来对结构体重命名,每一次创建该结构体类型变量只需使用重命名之后的类型名
typedef struct Point
{int x;int y;
}SP;int main()
{SP p1 = { 1,2 };printf("%d %d", p1.x, p1.y);return 0;
}
二、结构体内存对齐
在了解了结构体的创建定义和初始化后,如何计算一个结构体的大小呢?下面我们就来学习一下结构体内存对齐。
2.1 对齐规则
- 结构体的第一个成员对齐到结构体起始位置(偏移量为0)的地址处
- 其他成员变量要对齐到 偏移量为某个数字(对齐数)的整数倍的地址处
对齐数 = 编译器默认对齐数 和 该成员变量大小 的 较小值
VS中的默认对齐数是8;
Linux系统中的gcc没有默认对齐数,对齐数就是成员自身的大小
- 结构体总大小是最大对齐数(结构体中每一个成员都有对齐数,所有对齐数中最大的)的整数倍
- 如果结构体中嵌套了结构体,嵌套的结构体要对齐到偏移量为 自己成员最大对齐数 的整数倍,整个结构体的大小就是所有对齐数(包括嵌套结构体中的成员的对齐数)中最大对齐数的整数倍
例1(VS环境下):
struct S1
{char c1;int i;char c2;
};int main()
{printf("%zd ", sizeof(struct S1));return 0;
}
VS默认对齐数是8,char 和 int 类型的大小分别是1, 4,则三个成员的对齐数分别为1, 4, 1。
成员大小 默认对齐数 对齐数
char c1; 1 8 1
int i; 4 8 4
char c2; 1 8 1
图示如下:
当三个成员存放完后,此时结构体大小是9,但是要满足结构体的大小是最大对齐数的整数倍,最大对齐数是4,所以结果该结构体的大小是12个字节。
例2(VS环境下):
struct S2
{char c1;char c2;int i;
};int main()
{printf("%zd ", sizeof(struct S2));return 0;
}
VS默认对齐数是8,char 和 int 类型的大小分别是1, 4,则三个成员的对齐数分别为1, 1, 4。
成员大小 默认对齐数 对齐数
char c1; 1 8 1
char c2; 1 8 1
int i; 4 8 4
图示如下:
当三个成员存放完后,此时结构体大小是8,但是要满足结构体的大小是最大对齐数的整数倍,最大对齐数是4,刚好是4的整数倍,所以结构体大小是8个字节。
对比这两个例子,我们会发现S1和S2这两个结构体,虽然成员都一样,但是顺序不一样所导致的结构体大小就不一样。
再根据结构体内存对齐的方式可以得出结论:
在结构体中,尽量将类型相同的成员放在一起,可以节省结构体的空间
例3:我们来看一下嵌套结构体的大小计算
struct S2
{char c1;char c2;int i;
};struct S3
{char c;struct S2 s2;double d;
};int main()
{printf("%zd ", sizeof(struct S3));return 0;
}
VS默认对齐数是8,char 和 int 和 double类型的大小分别是1, 4,8,成员的对齐数分别为1, 4, 1。
成员大小 默认对齐数 对齐数
char c; 1 8 1char c1; 1 8 1
char c2 1 8 1
int i; 4 8 4double d; 8 8 8
嵌套的结构体对齐到偏移量为 自己成员偏移量最大对齐数的整数倍,S2最大对齐数为4,所以S2对齐到偏移量为4的整数倍。
成员变量d对齐到偏移量为8的整数倍,在所有成员都对齐之后,此时结构体大小是24,刚好是最大对齐数(包括嵌套结构体成员的对齐数比较)8的整数倍。
所以结构体S3的大小是24个字节。
2.2 内存对齐的原因
1.平台原因(移植原因)
并不是所有硬件平台都可以访问任意地址上的任意数据,某些硬件平台只能取特定类型的数据
2.性能原因
数据结构(尤其是栈)应该尽可能的在自然边界上对齐。原因在于,对于未访问的内存,处理器可能需要进行两次内存访问,而对齐的内存可能就需要一次访问。
假设一个处理器在内存中总是取8个字节,则地址必须是8的倍数。那么如果一个double类型(8字节)被分在两个8字节内存块中,内存没有对齐到8的整数倍时,就可能需要两次访问。而如果内存对齐,double类型的数据都对齐到8的整数倍的地址处,就只需要一次访问了。
总体来说,结构体内存对齐就是空间换时间的做法
2.3 修改默认对齐数
我们知道在VS中的默认对齐数是8,这个值也是可以修改的。
预处理指令#pragma
可以修改编译器的默认对齐数
使用方法:
#pragma pack(n)
: 修改默认对齐数为n
#pragma pack( )
: 取消设置的对齐数,还原为默认对齐数
修改后的对齐数一般是2的次方数,这样有利于处理器对结构体成员进行访问。 n = 2t(t >=1)
比如上面的例2中的struct S2结构体的内存大小是8个字节,而将默认对齐数修改为1时,那么所有成员的对齐数都是1。
成员大小 修改后对齐数 对齐数
char c1; 1 1 1
char c2; 1 1 1
int i; 4 1 1
所有成员的对齐数都是1,都对齐到偏移量为1的整数倍处,那就意味着所有的成员都紧挨着存放。
#pragma pack(1)//将默认对齐数修改为1
struct S2
{char c1;char c2;int i;
};
#pragma pack()//取消默认对齐数,还原默认对齐数struct S_2
{char c1;char c2;int i;
};int main()
{printf("%zd\n", sizeof(struct S2));//6printf("%zd\n", sizeof(struct S_2));//8return 0;
}
结果:
三、结构体传参
结构体在函数中的传参形式有两种,一种是直接传结构体(传值调用),另一种是传结构体的指针(传值调用)。
在之前我们学习了函数知道,形参只是实参的一份拷贝,在调用函数时,会为形参在内存中在开辟和实参变量一样大的空间。
所以如果一个结构体的空间大小比较大(例如含有许多元素的数组),如果直接传结构体作为形参,内存中就还会一个开辟相同大小的空间给形参用。就会造成系统压栈开销和空间的浪费,也会导致系统性能下降。
结论:
需要结构体传参的时候,要尽量使用传结构体的地址的方法(传址调用)。
struct S
{int data[100];char c;
};void print1(struct S s)//传结构体
{int i = 0;for (i = 0; i < 5; i++){printf("%d ", s.data[i]);}printf("%c\n", s.c);
}void print2(struct S* ps)//传地址
{int i = 0;for (i = 0; i < 5; i++){printf("%d ", ps->data[i]);}printf("%c\n", ps->c);
}struct S s = { {1,2,3,4,5}, 'a' };//全局变量int main()
{print1(s);//传值调用print2(&s);//传址调用return 0;
}
四、结构体实现位段
4.1 什么是位段
位段是基于结构体的,声明和结构是类似的。
但是有两个不同点:
- 位段的成员必须是整型家族中的,比如
int
,unsigned int
,char
等类型,在C99标准中也可以使用其他类型;- 位段成员后必须加冒号和一个数字。
位段示例:
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};
位段的作用是什么呢?数字有什么含义呢?
比如我们设计的一个结构体的整型类型(int)成员的值只取0,1,2,3,那么只需要两个位(bit)就可以表示(二进制11 就等于 十进制3),使用32个字节未免有点浪费。位段就可以限制使用开辟的内存空间,减少浪费。
比如int a : 2;
就表示仅仅会给变量a两个位(bit)的空间。
4.2 位段的内存分配
那么位段的空间如何计算,各个成员的内存又如何分配呢?
- 位段的空间是以4个字节或1个字节为单位的方式开辟的。
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};int main()
{printf("%zd ", sizeof(struct A));//8return 0;
}
上面的位段大小如何计算?
a是int类型变量,所以先开辟4个字节,a,b,c三个加在一起是17bit,足够使用,剩下15bit,不足以存放d,d是int类型,所以再开辟4个字节给d。
所以该位段的大小是8个字节。
- 在一次开辟的空间内的内存空间如何分配取决于编译器,在VS中是从右向左分配
struct B
{char a : 3;char b : 4;char c : 5;char d : 4;
};int main()
{struct B s = { 0 };s.a = 10;s.b = 12;s.c = 3;s.d = 4;printf("%zd", sizeof(s));return 0;
}
计算上面这个位段的大小
a是char
类型,所以首先开辟1个字节给位段使用,将a放在最右边;
char a : 3;
说明a只有三个bit,剩下5个bit;char b : 4;
说明b有4个bit,足以给b使用,所以b紧接着a在右边存放;
char c : 5;
说明c需要5个bit,前面开辟的一个字节只剩1个bit不够用,在VS中编译器会将其舍弃(舍弃还是利用根据平台和编译器决定),c是char
类型的,所以再开辟一个字节,同样将c放在最右边;
char d : 4;
说明d需要4个字节,前面开辟的一个字节只剩3个bit不够用,d是char
类型的,所以再开辟一个字节,同样将d放在最右边;
所以该位段的大小是3个字节。
我们可以验证一下内存中是否是这样存放的:
a的值是10,二进制是1010,但是a只有三位,所以会截断,剩余低三位010;
同理b是12,占4个bit,二级制是1100;
c是3,占5个比特,二级制是00011;
d是4,占4个bit,二级制是0100;
用16进制来表示这三个字节就是0x 62 03 04
在VS中调试,查看地址:
4.3 位段的跨平台问题
位段涉及许多不确定因素,位段是不跨平台的,注重可移植的程序应避免使用位段
原因:
- int位段被当做有符号还是无符号类型是不确定的
- 位段中的成员在一次开辟的内存中是从左向右分配还是从右向左分配,标准尚未定义;
- 位段中最大位的数目不能确定; (32位机器最大32,16位机器最大16,移植会出现问题;) (int类型在32位和64位机器是4个字节,在16位机器上是2个字节)
- 当一个位段包含多个成员,当一次开辟的空间被一个位段使用了,第二个位段成员比较大,无法容纳第二个位段成员时,是舍弃还是利用剩余的位,这也是不确定的。
4.4 位段使用注意事项
我们知道内存的最小单位是字节,内存空间是按照字节来分割的,每一个字节分配一个地址。取地址也是从起始位置开始。
通过上面的学习,在位段中,有些位段成员并不是放在一个字节的起始位置,或者成员大小太小,不够一个字节。
那么就不能对位段的成员使用取地址操作符&
,所以也就不能通过scanf
函数来直接输入位段成员的值。
只能通过初始化,或者先输入变量的值,然后让变量赋值给位段成员。
struct A
{int a : 2;int b : 5;int c : 10;int d : 30;
};int main()
{struct A sa = { 0 };//scanf("%d", &sa.b);//错误的//正确的sa.a = 1;int num = 10;sa.c = 10;printf("%d %d\n", sa.a, sa.c);// 1 10return 0;
}
4.5 位段的应用
IP数据报就是使用了位段的功能,将各个需要存储发送和接收的数据,充分利用和对齐包装,提高空间利用率和传输效率。
IP数据报格式如下:
结语:C语言-- 结构体 章节到这里就结束了。
本人才疏学浅,文章中有错误和有待改进的地方欢迎大家批评和指正,非常感谢您的阅读!如果本文对您又帮助,可以高抬贵手点点赞和关注哦!