Bootstrap

9.结构体

组合类型: 把一个物体的属性组合到一个数据类型中去描述该物体。这个数据类型称为组合类型。

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 的倍数	
练习:
  1. struct MixedData { char Data1;//1字节对齐 short Data2;//2字节对齐 int Data3;//4字节对齐 char Data4;//1字节对齐 }; 整个结构体是 4字节对齐

    整个结构体空间大小是 4的倍数

    sizeof(struct MixedData) ==> 12
    
    _ X _ _ _ _ _ _ _ X X X
    
    
    1. struct FinalPad

    {
    	float x;//4字节对齐
    	char n[1];//1字节对齐
    };// 4字节对齐
    
    ```
    sizeof(struct FinalPad) ==> 8
    _ _ _ _ _ X X X
    

    ```
    
    1. struct FinalPadShort

    {
    	short x;// 2字节对齐
    	char n[3];// 1字节对齐
    };//2字节对齐
    
    ```
    sizeof(struct FinalPadShort) ==> 6
    _ _ _ _ _ X
    
    ```
    

    1. 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

  1. 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);
}

;