组合类型: 把一个物体的属性组合到一个数据类型中去描述该物体。这个数据类型称为组合类型。
C语言允许程序员定义自己的组合类型: 结构体 共用体(联合体) 枚举
1. 结构体基本概念
C语言提供了众多的基本数据类型,但现实生活中的对象一般都不是单纯的整型、浮点型或字符串,而是这些基本类型的综合体。比如一个学生,典型的应该拥有学号(整型)、姓名(字符串)、分数(浮点型)、性别(枚举)等不同侧面的属性,这些所有的属性都不应该被拆分开来,而是应该组成要一个整体,代表一个完整的学生。
在C语言中,可以使用结构体来将多种不同的数据类型组装起来,形成某种现实意义的自定义的变量类型。结构体本质上是一种自定义类型。
结构体的定义:
struct 结构体标签 { 成员1; 成员2; ... }; struct 结构体名 { 成员类型1 成员名1; 成员类型2 成员名2; 成员类型3 成员名3; ... }; ==> struct 结构体名 就是新类型的名字
-
语法:
-
结构体标签,用来区分各个不同的结构体
-
成员,是包含在结构体内部的数据,可以是任意的数据类型(C 合法)
-
-
示例:
学生:学号 姓名 性别 ...... struct Student { int num; char name[32]; char sex; }; ============== struct Student { int num; char *name; char sex; }; ===> 新类型 的名字 struct Student struct Studnet stu1;//定义了一个结构体变量,变量名叫stu1,里面有3个成员 struct Studnet stu2; struct Student s[5]; stu1 = stu2;//right
2.结构体成员的内存布局
(1)结构体类型所占空间 >= 各成员变量所占空间之和(可能会packing, 填充) (2)结构体内各成员变量按它们定义时出现的次序,依次保存。
3. 结构体成员引用
引用结构体成员与普通变量一样:能做左值,右值还能取地址
结构体相当于一个集合,内部包含了众多成员,每个成员实际上是独立的变量,都可以被独立的引用,引用结构体成员非常简单,只需要使用一个成员引用符即可:
(1) . 翻译为 : 谁的 域操作符: 取某个子成员变量 结构体变量名.成员变量名 =>取结构体变量内部的成员变量 eg: struct Student s; s.num = 100; int a = s.num; scanf("%d", &s.num); (2) (*结构体指针名).成员变量 eg: //定义一个指针变量p,来保存s的地址 struct Student *p = &s; (*p).num = 1002; (3) -> 指向运算符 翻译为:谁指向的 结构体指针->成员变量 p->num = 1003;
3. 结构体初始化
结构体跟普通变量一样,设计定义、初始化、赋值、取地址等等操作,这些操作绝大部分都跟普通变量别无二致,只有少数操作有些特殊。这其实也是结构体这种组合类型的设计初衷,就是让开发者用起来比较顺手,不跟普通变量产生态度差异。
-
结构体定义和初始化
-
由于结构体内部拥有多个不同类型的成员,因此初始化采用与类似列表方式
-
结构体的初始化有两种方式:①普通初始化;②指定成员初始化。
-
为了能使用结构体类型的升级迭代,一般建议采用指定成员初始化。
-
-
示例:
定义结构体变量时就赋初值。 结构体的初始用{} (1) 按定义时的顺序依次初始化各成员变量,用逗号分开: struct Student stu = {1001, "pxl", '1'}; struct Student stu = {1001}; (2)不按顺序, .成员变量名 = 值 ,多个成员赋值用逗号隔开 struct Student stu = { .sex = '1' };
-
指定成员初始化的好处:
-
成员初始化的次序可以改变
-
-
可以初始化一部份成员
-
结构体新增了成员之后初始化语句仍然可用
-
-
定义结构体类型并创建变量且初始化
#include <stdio.h> #include <string.h> struct Student { int num; char name[32]; char sex; }; // struct Student 才是类型名字 int main() { struct Student s = {1001,"pxl", '1'}; printf("%d:%s:%c\n", s.num, s.name, s.sex); }
-
多结构体嵌套
出生日期:年,月,日 学生: 学号,姓名,出生日期 struct Birth { int y,m,d; }; struct Student { int num; char name[32]; struct Birth bir; }; int main() { //所有成员按照定义顺序依次赋值 //struct Student stu = {1001, "pxl", 2000,2,2}; //struct Student stu = {1001, "pxl",{ 2000,2,2}}; //struct Student stu = {1001, "pxl",{ 2000}}; //struct Student stu = {1001, "pxl",{ .m = 12}}; //指定成员初始化 struct Student stu = { .name = "pxl", .bir = {.m = 11} }; }
#include <stdio.h> struct Student { int num; char name[32]; struct Birth { int y,m,d; };//此时该类型只能用于该结构体内部定义变量 struct Birth bir; };//当struct Student这个类型声明完之后,struct Birth这个类型就不能在声明变量了 int main() { //struct Student stu = {1001, "pxl", 2000,2,2}; //struct Student stu = {1001, "pxl",{ .m = 12}}; struct Student stu = { .name = "pxl", .bir = {.m = 11} }; printf("%d:%s:%d/%d/%d\n", stu.num, stu.name, stu.bir.y,stu.bir.m,stu.bir.d); }
4. 结构体指针与数组
跟普通变量别无二致,可以定义指向结构体的指针,也可以定义结构体数组。
-
结构体指针:
struct Student s; struct Student *p = &s; s.name <==> (*p).name <==>p->name
-
结构体数组:
结构体数组: 本质是一个数组,里面的每个元素都是结构体类型 struct Student class[20];
结构体数组的初始化:
(1)按照数组元素的次序依次初始 eg: struct Student { int num; char name[32]; char sex; }; // struct Student 才是类型名字 struct Student class[3] = {1001,“pxl”,'1'}; struct Student class[3] = { {1001,"pxl",'0'},{.pxl = "zxc"},{1002} ; (2)不按数组元素的顺序,[下标] = 这种方式不同的编译器,情况或限制不一样 eg: struct Birth { int y,m,d; }; struct Student { int num; char name[32]; struct Birth bir; }; struct Student class[20] = { [1] = {"pxl",1,18,{.y = 2000}}, [3] = {.age = 19} };
CPU字长
字长的概念指的是处理器在一条指令中的数据处理能力,当然这个能力还需要搭配操作系统的设定,比如常见的32位系统、64位系统,指的是在此系统环境下,处理器一次存储处理的数据可以达32位或64位。
地址对齐
cpu字长确定后,相当于明确了系统每次存取内存数据时的边界,以32位系统为例,32位意味着cpu每次存取都以4字节位边界,因此每4字节可以认为是cpu存取数据的一个单元。
如果存取数据刚好落在所需单元之内,那么我们就说这个数据的地址是对齐的,如果存取的数据跨越了边界,使用了超过所需单元的字节,那么我们就说这个数据的地址是未对齐的。
从图中可以明显看出,数据本身占据了8个字节,在地址未对齐的情况下,CPU需要分3次才能完整地存取完这个数据,但是在地址对齐的情况下,CPU可以分2次就能完整地存取这个数据。
总结: 如果一个数据满足以最小单元数存放在内存中,则称它地址是对齐的,否则是未对齐的。地址对齐的含义用大白话说就是1个单元能塞得下的就不用2个;2个单元能塞得下的就不用3个。 如果发生数据地址未对齐的情况,有些系统会直接罢工,有些系统则降低性能。
普通变量的m值
以64位系统为例,由于CPU存取数据总是以4字节为单元,因此对于一个尺寸固定的数据而言,当它的地址满足某个数的整数倍时,就可以保证地址对齐。这个数就被称为变量的m值。 根据具体系统的字长,和数据本身的尺寸,m值是可以很简单计算出来的。
-
举例:
char c; // 由于c占1个字节,因此c不管放哪里地址都是对齐的,因此m=1 short s; // 由于s占2个字节,因此s地址只要是偶数就是对齐的,因此m=2 int i; // 由于i占4个字节,因此只要i地址满足4的倍数就是对齐的,因此m=4 double f; // 由于f占8个字节,因此只要f地址满足4的倍数就是对齐的,因此m=8 printf("%p\n", &c); // &c = 1*N,即:c的地址一定满足1的整数倍 printf("%p\n", &s); // &s = 2*N,即:s的地址一定满足2的整数倍 printf("%p\n", &i); // &i = 4*N,即:i的地址一定满足4的整数倍 printf("%p\n", &f); // &f = 8*N,即:f的地址一定满足8的整数倍 注意: 1. int a; // 4字节对齐意味中 a的地址首地址编号要是4的倍数 2. 系统默认的m值,以本身类型所占空间大小为主
-
注意,变量的m值跟变量本身的尺寸有关,但它们是两个不同的概念。
-
手工干预变量的m值:
char c __attribute__((aligned(32))); // 将变量 c 的m值设置为32
-
语法:
-
attribute 机制是GNU特定语法,属于C语言标准语法的扩展。
-
attribute 前后都是双下划线,aligned两边是双圆括号。
-
attribute 语句,出现在变量定义语句中的分号前面,变量标识符后面。
-
attribute 机制支持多种属性设置,其中 aligned 用来设置变量的 m 值属性。
-
一个变量的 m 值只能提升,不能降低,且只能为正的2的n次幂。
-
结构体的M值
-
概念:
-
结构体的M值,取决于其成员的m值的最大值。即:M = max{m1, m2, m3, …};
-
结构体的地址和尺寸,都必须等于M值的整数倍。
-
-
示例:
前提:64位机 struct node { short a; // 尺寸=2,m值=2 double b; // 尺寸=8,m值=8 char c; // 尺寸=1,m值=1 }; struct node n; // M值 = max{2, 8, 1} = 8; sizeof(n) ==> 24
可移植性
可移植指的是相同的一段数据或者代码,在不同的平台中都可以成功运行。
-
对于数据来说,有两方面可能会导致不可移植:
-
数据尺寸发生变化
-
数据位置发生变化
-
第一个问题,起因是基本的数据类型在不同的系统所占据的字节数不同造成的,解决办法是使用可移植性数据类型即可。本节主要讨论第二个问题。
考虑结构体:
struct node { int8_t a; int32_t b; int16_t c; };
以上结构体,在不同的的平台中,成员的尺寸是固定不变的,但由于不同平台下各个成员的m值可能会发生改变,因此成员之间的相对位置可能是飘忽不定的,这对数据的可移植性提出了挑战。
解决的办法有两种:
-
第一,固定每一个成员的m值,也就是每个成员之间的塞入固定大小的填充物固定位置:
struct node { int8_t a __attribute__((aligned(1))); // 将 m 值固定为1 int64_t b __attribute__((aligned(8))); // 将 m 值固定为8 int16_t c __attribute__((aligned(2))); // 将 m 值固定为2 };//24
-
第二,将结构体压实,也就是每个成员之间不留任何空隙:
struct node { int8_t a; int64_t b; int16_t c; } __attribute__((packed));
=================
在32bits x86机器,编译器: Microsoft Visual C++ Borland/Code Gear(c++ builder) Digital Mars(DMC) GNU(gcc) 对 A char(1byte) 一字节对齐 a short(2bytes) 二字节对齐 an int(4bytes) 四字节对齐 a long(4bytes) 四字节对齐 a float(4bytes) 四字节对齐 a double(8bytes) 8字节对齐 On windows 4字节对齐 On linux(除非 -malign-double 8字节对齐) a long long(8bytes) 4字节对齐 a long doulbe(10bytes) ?? 具体的对齐方式和编译器、系统有关 any pointer (4bytes) 4字节对齐 在64bits x86机器下,编译器 Microsoft Visual C++ Borland/Code Gear(c++ builder) Digital Mars(DMC) GNU(Gcc) 和32bits机子,只有以下不同: A long (8 bytes) 8字节对齐. A double (8 bytes) 8字节对齐. A long long (8 bytes) 8字节对齐. A long double (8 bytes with Visual C++, 8字节对齐在Visual C++ 16 bytes with GCC, 16字节对齐在GCC Any pointer (8 bytes) 8字节对齐. 对结构体的对齐方式呢?☆☆ (1) 结构体变量按其最大的自然类型的成员变量的对齐方式对齐; (2) 结构体的大小须为其对齐方式的整数倍(一般向上取整). 总结: Linux下: 32位机,超过4字节的空间,通通以4字节对齐 64位机, 通通以所占空间大小来看
struct test { char a;// a的对齐方式 一字节对齐 int b; // b的对齐方式 四字节对齐 short c;// c的对齐方式 二字节对齐 }; struct test :按b的对齐方式 :4字节对齐 struct test 变量(整个结构体)的大小必须是 4 的倍数
练习:
-
struct MixedData { char Data1;//1字节对齐 short Data2;//2字节对齐 int Data3;//4字节对齐 char Data4;//1字节对齐 }; 整个结构体是 4字节对齐
整个结构体空间大小是 4的倍数
sizeof(struct MixedData) ==> 12 _ X _ _ _ _ _ _ _ X X X
-
struct FinalPad
{ float x;//4字节对齐 char n[1];//1字节对齐 };// 4字节对齐 ``` sizeof(struct FinalPad) ==> 8 _ _ _ _ _ X X X
```
-
struct FinalPadShort
{ short x;// 2字节对齐 char n[3];// 1字节对齐 };//2字节对齐 ``` sizeof(struct FinalPadShort) ==> 6 _ _ _ _ _ X ```
-
struct MixedData
{ char Data1;//1字节对齐 short Data2;//2字节对齐 int Data3;//4字节对齐 char Data4;//1字节对齐 };//4字节对齐 struct test { char s; //1字节对齐 struct MixedData m; //4字节对齐 };//4字节对齐
-
sizeof(struct test) ==> 16 算 struct MixedData所占空间 ==> 12 _ X _ _ _ _ _ _ _ X X X _ X X X _ _ _ _ _ _ _ _ _ _ _ _ ==> 16
-
struct test
{
long double a;//16字节对齐
char b;// 1字节对齐
};//16字节对齐
sizeof(struct test) ===> 32
作业: 1. 定义一个学生信息结构体数组(数组元素的个数由用户决定),依次从键盘输入每个学生 信息(学号,姓名,成绩,出生日期),按成绩的降序输出每个学生的信息,降序算法是自己封装函数 eg: 1001 pxl 90 1999/1/1 1003 xxx 89 2000/1/1 //定义数组并赋值 //调用降序函数 // 进行排序 //输出数组 2. 写一个函数,完成从一个给定的完整的文件路径 (如"C:\My Documents\Software Test 1.00.doc")中,析取文件名,扩展名和文件所处目录的功能,编写 程序时,请在必要的地方加以注释 struct file_msg Func(char*file_path_name) { //分析 struct file_msg msg; return msg; } int main() { struct file_msg m = Func(char*file_path_name); }
#include <stdio.h> #include <string.h> struct Birth { int y,m,d; }; struct Student { int num; char name[32]; int score; struct Birth bir; }; struct File_Msg { char file_path[128]; char file_name[128]; char file_extension[128]; }; void scanf_student(struct Student* s, int n) { for(int i = 0; i < n; i++) { scanf("%d%s%d%d%d%d", &(s[i].num),s[i].name, &s[i].score,&s[i].bir.y,&s[i].bir.m,&s[i].bir.d);// &((s+i)->num) } } void printf_student(struct Student* s, int n) { printf("==================\n"); for(int i = 0; i < n; i++) { printf("%d:%s:%d:%d/%d/%d\n", (s[i].num),s[i].name, s[i].score,s[i].bir.y,s[i].bir.m,s[i].bir.d);// &((s+i)->num) } } void sort(struct Student*s, int n) { //选择 for(int i = 0; i < n-1; i++)//代表基准值 s[0] { for(int j = i+1; j < n;j++)// s[j] { if(s[i].score < s[j].score) { struct Student t = s[i]; s[i] = s[j]; s[j] = t; } } } } struct File_Msg Func(char*file_path_name) { struct File_Msg msg = {0}; //分析 //找后缀名 char*p = file_path_name+strlen(file_path_name)-1; while(*p != '.') { p--; } strcpy(msg.file_extension,p); //找文件名 while(*p != '\\') { p--; } int len = strlen(p+1) - strlen(msg.file_extension); strncpy(msg.file_name, p+1, len); //路径 len = strlen(file_path_name) - strlen(p); strncpy(msg.file_path, file_path_name, len); return msg; } int main() { // //定义一个学生结构体数组 // struct Student s[5]; // //从键盘上输入数据 // scanf_student(s, 5); // //排序 :降序 // sort(s, 5); // //输出学生的信息 // printf_student(s, 5); char s[128] = {0}; scanf("%s", s); struct File_Msg msg = Func(s); printf("%s\n", msg.file_path); printf("%s\n", msg.file_name); printf("%s\n", msg.file_extension); }