结构体基本概念
C语言提供了众多的基本类型,但现实生活中的对象一般都不是单纯的整型、浮点型或字符串,而是这些基本类型的综合体。比如一个学生,典型地应该拥有学号(整型)、姓名(字符串)、分数(浮点型)、性别(枚举)等不同侧面的属性,这些所有的属性都不应该被拆分开来,而是应该组成一个整体,代表一个完整的学生。
在C语言中,可以使用结构体来将多种不同的数据类型组装起来,形成某种现实意义的自定义的变量类型。结构体本质上是一种自定义类型。
结构体的声明:
声明语句不是在生产某一个产品,只是向系统报备某一个类型的模型。后期通过该模型来定义变量才真正的分配内存。
struct 结构体标签
{
成员1;
成员2;
...
}; // 声明语句后面必须有一个分号代表该语句的结束
- 语法:
- 结构体标签,用来区分各个不同的结构体。
- 成员,是包含在结构体内部的数据,可以是任意的数据类型。
结构体声明:
// 声明结构体 。 声明语句不需要占用内存空间
struct BookNode
{
char Name [32] ;
float Price ;
int Page ;
// .......
};
结构体定义:
// 使用自己声明的结构体类型来定义变量
// 变量的类型 变量名 ;
int Num ;
struct BookNode C ; // 定义语句 才向系统申请内存空间
结构体成员赋值及访问:
// 如何给结构体中各个数据进行赋值
// C.Name = "Even" ; // 【错误操作】 C.Name 实际上是一个数组的名字.....
// char arr [32] ;
// arr = "Even";
char * Name = "Even" ;
int Len = strlen(Name);
printf("Len:%d\n" ,Len );
// 使用内存拷贝函数对结构体中的数组进行赋值
memcpy( C.Name , Name , Len+1 );
// 可以直接使用赋值符号对普通的基础数据类型进行赋值
C.Page = 100 ;
C.Price = 230.57 ;
// 如何访问数据
printf("Neme:%s Page:%d Price:%f\n" , C.Name , C.Page , C.Price) ;

- 试自行设计一个结构体用于描述一只猫的各项属性(性别、姓名、身高、身长、食量等)
- 可以尝试从键盘输入信息对猫的各项属性进行设置
- 尝试对猫进行赋值已经访问各项成员

结构体初始化
结构体的初始化与基础数据类型一样只有在定义语句中同时赋值,才称为初始化操作。
结构体的初始化一般有两种方法:
方法一顺序初始化:
// 结构体声明
struct Cat
{
char Name [32] ;
float Length;
float Height;
//....
};
struct Cat Huang = { "大黄" , 32.67 , 45.66 } ;
int arr[5] = { 1, 2, 3, 4, 5} ;
注意顺序初始化的操作写法,看着比较简洁,但是不太方便对代码进行升级迭代,比如在结构体的成员之间有添加新的成员,那么顺序初始化就不顺序了(初始化代码需要修改顺序..)。
因此为了项目的更新迭代更加方便具有更好的拓展性建议使用第二种初始化的方法(指定成员初始化)
指定成员初始化:
// 指定成员初始化
struct Cat Lv = {
.Name = "小绿" ,
.Length = 123.435 ,
.Height = 5432.65 ,
.Sex = 1
};
结构体指针
概念: 它是一个指针, 他所指向的类型是一个结构体类型。
语法: struct 标签 * ptr ;
// 定义一个结构体变量
struct Node Even = {
.Name = "Even",
.Num = 12 ,
.Sex = true
};
// 定义了一个结构体类型的指针变量, 并把 even 的地址存入到该变量中
struct Node * ptr = &Even ;
// 如何通过结构体指针来访问结构体中的各个成员
// -> 结构体指针的成员引用符
printf("Name:%s Num:%d Sex:%d\n" ,
ptr->Name , ptr->Num , ptr->Sex );
// 如何在堆内存中申请结构体的内存空间并合理利用它
struct Node * ptr1 = calloc(1 , sizeof(struct Node));
// 使用字符串拷贝函数 把 数据 "Even" 拷贝到 ptr1->Name 数组中, 最大拷贝32字节 。
strncpy( ptr1->Name , "Even" , 32 );
ptr1->Num = 10 ;
ptr1->Sex = false ;
printf("Name:%s Num:%d Sex:%d\n" ,
ptr1->Name , ptr1->Num , ptr1->Sex );
// 释放堆中的结构体内存
free(ptr1) ;
ptr1 = NULL ;
结构体数组
概念: 他是是一个数组,该数组中的每一个元素都是一个结构体 。
语法: struct Node arr [5] ;
如何定义并初始化结构体数组:
// 使用结构体类型来定义一个数组Cats
struct Node Cats [5] = {
{
.Name = "大黄",
.Num = 1 ,
.Type = 'M'
} ,
{
.Name = "小绿",
.Num = 2 ,
.Type = 'M'
} ,
{ "啊狗" , 3 , 'G' } ,
{ "啊猫" , 4 , 'G' } ,
{ "翠花" , 5 , 'M'}
} ;
如何访问结构体数组:
printf("************正常访问结构体数组****************\n");
for (int i = 0; i < 5 ; i++)
{
printf("Name:%s Num:%d Type:%c\n" , Cats[i].Name , (*(Cats+i)).Num , (Cats+i)->Type );
}
printf("************使用结构体指针来访问结构体数组****************\n");
// 定义一个结构体指针 ,并让该指针指向了结构体数组 Cats 的第一个元素的入口地址(首元素的首地址)
struct Node * ptr = Cats ;
for (int i = 0; i < 5; i++)
{
printf("Name:%s Num:%d Type:%c\n" ,
(ptr+i)->Name , (*(ptr+i)).Num , ptr[i].Type);
}
printf("************使用结构体指针数组来访问结构体数组****************\n");
// 结构体数组指针 ( 它是一个指针, 该指针指向一个数组,该数组的每一个元素都是结构体类型 )
struct Node (* ptr1) [5] = &Cats ;
// ptr1 ==== & Cats
// *ptr1 ==== *& Cats
// *ptr1 === Cats
for (int i = 0; i < 5; i++)
{
printf("Name:%s Num:%d Type:%c\n" ,
(*ptr1)[i].Name , ((*ptr1)+i)->Num , (*((*ptr1)+i)).Type );
}
结构体声明语句变形
型态一:
在声明结构体的同时,定义变量(可以是多个,也可以是指针,甚至可以是数组....)
语法:
// 声明结构变量类型
struct Node
{
char Name [32];
int Num ;
char Type ;
} 变量名 , .... ; // 在分号以及大括号之间写上需要定义的变量名
示例:
// 声明结构变量类型
struct Node
{
char Name [32];
int Num ;
char Type ;
} Cat1 , Cat2 , *ptr ; // 在声明的同时定义了两个 结构体变量, 以及一个结构体指针
// 注意 所有的指针或变量由于都是在函数体外面定义的,因此他们都是全局变量
型态二:
把结构体的标签名字省略,
// 声明结构变量类型
struct
{
char Name [32];
int Num ;
char Type ;
} Cat1 , Cat2 , *ptr , arr[5] ;
注意:
这种形态的结构体类型,只能通过声明语句一起来定义变量, 其他地方无法定义这种类型的结构体变量。
型态三:
使用typedef 来对结构体类型进行取别名。
// 声明结构变量类型
// 使用typedef 来给 struct Node 取别名为 Cat
typedef struct Node
{
char Name [32];
int Num ;
char Type ;
} Cat , *P_Cat ;
// Cat 就等价于 struct Node 是结构体的类型名
// P_Cat 就等价于 struct Node *
// 使用别名 Cat 来定义变量 mao
Cat mao = {
.Name = "条条猫",
.Num = 12 ,
.Type = 'M'
};
printf("Name:%s Num:%d Type:%c\n" , mao.Name , mao.Num , mao.Type );
// 使用别名 P_Cat 来定义指针ptr
P_Cat ptr = &mao ;
printf("Name:%s Num:%d Type:%c\n" , ptr->Name , ptr->Num , ptr->Type );
注意:
关注声明语句前面有没有关键字 typedef 如果有则表示取别名,如果没有则表示顺便定义变量。
结构体的嵌套
概念: 在结构体中有每一个或多个成员是其他的结构体
语法:
struct Node {
int Num ;
char Name[32];
struct {
.......
} info ;
}
示例:
typedef struct Node {
int Num ;
char Name[32];
// 嵌套在结构体中的小结构体
struct {
char type ;
int Test ;
} info ;
} Node , *P_Node ;
int main(int argc, char const *argv[])
{
Node mao = {
.Name = "翠翠",
.Num = 10 ,
.info.Test = 123 ,
.info.type = 'P'
};
printf("Name:%s Num:%d Test:%d Type:%c\n" ,
mao.Name , mao.Num , mao.info.Test , mao.info.type );
return 0;
}
CPU字长
字长的概念指的是处理器在一条指令中的数据处理能力,当然这个能力还需要搭配操作系统的设定,比如常见的32位系统、64位系统,指的是在此系统环境下,处理器一次存储处理的数据可以达32位或64位。

地址对齐
CPU字长确定之后,相当于明确了系统每次存取内存数据时的边界,以32位系统为例,32位意味着CPU每次存取都以4字节为边界,因此每4字节可以认为是CPU存取内存数据的一个单元。
如果存取的数据刚好落在所需单元数之内,那么我们就说这个数据的地址是对齐的,如果存取的数据跨越了边界,使用了超过所需单元的字节,那么我们就说这个数据的地址是未对齐的。
地址未对齐的情形


从图中可以明显看出,数据本身占据了8个字节,在地址未对齐的情况下,CPU需要分3次才能完整地存取完这个数据,但是在地址对齐的情况下,CPU可以分2次就能完整地存取这个数据。
总结:如果一个数据满足以最小单元数存放在内存中,则称它地址是对齐的,否则是未对齐的。地址对齐的含义用大白话说就是1个单元能塞得下的就不用2个;2个单元能塞得下的就不用3个。如果发生数据地址未对齐的情况,有些系统会直接罢工,有些系统则降低性能。
普通变量的m值
以32位系统为例,由于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=4 // 如果是 64位的系统 那么8个字节的数据的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 = 4*N,即:f的地址一定满足4的整数倍 - 注意,变量的m值跟变量本身的尺寸有关,但它们是两个不同的概念。
- 手工干预变量的m值:
- 语法:
- attribute 机制是GNU特定语法,属于C语言标准语法的扩展。
- attribute 前后都是双下划线,aligned两边是双圆括号。
- attribute 语句,出现在变量定义语句中的分号前面,char c __attribute__((aligned(32))); // 将变量 c 的m值设置为32变量标识符后面。
- attribute 机制支持多种属性设置,其中 aligned 用来设置变量的 m 值属性。
- 一个变量的 m 值只能提升,不能降低,且只能为正的2的n次幂。
结构体的M值
- 概念:
- 结构体的M值,取决于其成员的m值的最大值。即:M = max{m1, m2, m3, …};
- 结构体的地址和尺寸,都必须等于M值的整数倍。
- 示例:
struct node { short a; // 尺寸=2,m值=2 double b; // 尺寸=8,m值=4 / 8 char c; // 尺寸=1,m值=1 }; struct node n; // M值 = max{2, 4, 1} = 4; - 以上结构体成员存储分析:
- 结构体的M值等于4或8,这意味着结构体的地址、尺寸都必须满足4或8的倍数。
- 成员a的m值等于2,但a作为结构体的首元素(首元素的地址等于结构体的入口地址),必须满足M值约束,即a的地址必须是4或8的倍数
- 成员b的m值等于4,因此在a和b之间,需要填充2个字节的无效数据(为了成员b的地址值能被他自己的M值所整除)(一般填充0)
- 成员c的m值等于1,因此c紧挨在b的后面,占一个字节即可。
- 结构体的M值为4,因此成员c后面还需填充3个无效数据,才能将结构体尺寸凑足4的倍数。
可移植性
可移植指的是相同的一段数据或者代码,在不同的平台中都可以成功稳定运行。
- 对于数据来说,有两方面可能会导致不可移植:
- 数据尺寸发生变化 比如long 类型
- 数据存储位置发生变化 (特别实在结构体中各个数据之间的相对位置)
第一个问题,起因是基本的数据类型在不同的系统所占据的字节数不同造成的,解决办法是使用教案04讨论过的可移植性数据类型即可。本节主要讨论第二个问题。
考虑结构体:
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 }; - 第二,将结构体压实,也就是每个成员之间不留任何空隙:
struct node { __int8_t a; __int64_t b; __int16_t c; } __attribute__((packed));总结:
以上两种方法的优缺点:
-
- 固定每一个成员的m值: 优点是结构体中每一个成员的存储位置依然是符合地址对齐的规则的,访问效率依然存在
- 将结构体压实: 有点是结构体中每一个成员都只占用实际的大小,不需要为了地址对齐而填充空白,访问的效率相对要低一些
-
【拓展】结构体中的占位符
typedef struct Node { int a:4 ; // a 占用 4 个二进制位 char b:2 ; //b 占用2个二进制位 char c:8 ; // c 占用8个二进制位 }Node;typedef struct Node { int a:4 ; // a 值占用 4 个二进制位 char b:2 ; //.... char c:8 ; }Node; int main(int argc, char const *argv[]) { printf("%ld\n" , sizeof(struct Node)); Node data ; int Num = 1172 ; memcpy( &data , &Num , 4 ); printf("data.a:%d\n",data.a); printf("data.b:%d\n",data.b); printf("data.c:%d\n",data.c); return 0; }
11万+

被折叠的 条评论
为什么被折叠?



