当前位置: 首页 > news >正文

C语言 自定义类型---结构体(1)

目录

1.结构体类型的声明

2.结构体变量的定义和初始化

3.结构体内存对齐


1.结构体类型的声明

之前在讲解操作符的文章中,已经学习了结构体的概念,这里稍微复习一下。


1.1结构体回顾


结构是一种集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量。

1.1.1结构体的声明

struct tag
{member-list;
}variable-list;

例如描述一个学生:

struct Stu
{char name[20];//名字int age;char sex[5];//性别char id[20];//学号
};//分号不能丢

2.结构体变量的定义和初始化

struct Stu
{char name[20];//名字int age;char sex[5];//性别char id[20];//学号
};int main()
{// 结构体变量的创建和初始化struct Stu s = {"张三", 20, "男", "2023081001"};printf("Name: %s\n", s.name);printf("Age: %d\n", s.age);printf("Sex: %s\n", s.sex);printf("Id: %s\n", s.id);struct Stu *ps = &s;printf("Name: %s\n", ps->name);printf("Age: %d\n", ps->age);printf("Sex: %s\n", ps->sex);printf("Id: %s\n", ps->id);return 0;
}

在这块代码的printf语句中, . 和 -> 都是用于访问结构体成员的操作符,但使用场景有所不同:

2.1点操作符(. )

• 用法:当你有一个结构体变量时,使用点操作符来访问它的成员。语法格式为 结构体变量名.成

员名 。

• 示例:假设有如下结构体定义:

struct Point {int x;int y;
};
struct Point p;
p.x = 10;  // 使用点操作符给结构体变量p的成员x赋值
p.y = 20;  // 使用点操作符给结构体变量p的成员y赋值


2.2箭头操作符(-> )

• 用法:当你有一个指向结构体的指针时,使用箭头操作符来访问结构体的成员。语法格式为 结构

体指针名->成员名 。它等效于 (*结构体指针名).成员名  ,本质上是先通过指针找到结构体变量,

再访问其成员。

• 示例:

struct Point {int x;int y;
};
struct Point *ptr;
struct Point p = {10, 20};
ptr = &p;
ptr->x = 30;  // 使用箭头操作符通过指针ptr给结构体成员x赋值
ptr->y = 40;  // 使用箭头操作符通过指针ptr给结构体成员y赋值

总的来说,. 用于直接通过结构体变量访问成员,而 -> 用于通过结构体指针访问成员,合理使用这

两个操作符能方便地操作结构体数据。

2.3结构的特殊声明

在声明结构的时候,可以不完全的声明

比如:

//匿名结构体类型
struct
{int a;char b;float c;
}x;
struct
{int a;char b;float c;
}*p;

上面的两个结构在声明的时候省略了结构体标签(tag)。

那么问题来了:

// 在上面代码的基础上,下面的代码合法吗?
p = &x;

警告:

编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的。

匿名结构体类型,如果你有相同的结构体类型要声明的话,基本上要使用同一次。

2.4结构体的自引用

 在结构中包含一个类型为该结构本身的成员是否可以呢?

比如,定义一个链表的节点:

struct Node
{int data;struct Node next;
};

仔细分析,其实是不行的,因为一个结构体中再包含一个同类型的结构体变量,这样结构体变量的

大小就会无穷的大,是不合理的。

正确的自引用方式:

struct Node
{int data;struct Node* next;
};

在结构体自引用的过程中,夹杂了 typedef 对匿名结构体类型重命名,也容易引入问题,看看下面

的代码,可行吗?

typedef struct
{int data;Node* next;
}Node;

答案是不行的,因为 Node 是对前面的匿名结构体类型的重命名产生的,但是在匿名结构体内提前

使用 Node 类型来创建成员变量,这是不行的。

解决方案如下:定义结构体不要使用匿名结构体了

typedef struct Node
{int data;struct Node* next;
}Node;

3.结构体内存对齐

我们已经掌握了结构体的基本使用了。

现在我们深入讨论一个问题:计算结构体的大小。

这也是一个特别热门的考点:结构体内存对齐

3.1对齐规则

1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处

2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

对齐数 = 编译器默认的一个对齐数与该成员变量大小的较小值。
 
- VS 中默认的值为 8
- Linux中 gcc 没有默认对齐数,对齐数就是成员自身的大小
 
1. 结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的

整数倍。

2. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构

体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

3.1.1练习

struct S1
{char c1;int i;char c2;
};
printf("%d\n", sizeof(struct S1));

结构体第一个成员 c1 是 char 类型,占1字节, 对齐到偏移量为0 的地址处, 此时已用1 字节。
 
第二个成员 i 是 int 类型 , 大小为4 字节 , 对齐数取编译器默认对齐数8 和int  类型大小4 中的较小

值,即4 。所以 i 要从4 的整数倍地址开始存储,前面 c1占了1 字节,因此要填充3字节,使得

 i  从偏移量为4 的地址开始存储 , i 存储后 , 共占用4 + 4 = 8 字节。
 
第三个成员 c2 是 char 类型,占1 字节 , 对齐数为1(char  类型大小),从当前偏移量8 处存储,占

用1 字节。
 
最大对齐数是4 (int  类型的对齐数) , 当前总占用9 字节,要满足是最大对齐数4 的整数倍,需

再填充3 字节。
 
得出结果:所以  sizeof(struct S1)  的结果是12 字节。

struct S2
{char c1;char c2;int i;
};
printf("%d\n", sizeof(struct S2));

 第一步:根据规则1,第一个成员c1为 char 类型,占1 字节,对齐到偏移量为0 的地址处,此时占

用1 字节。

第二步:第二个成员 c2 也是 char 类型,占1 字节,对齐数为1 ( char  类型自身大小 ),紧接在

 c1  后面存储,从偏移量为1 的地址开始,此时共占用2 字节。

第三步:第三个成员 i 是 int 类型,大小为4 字节,对齐数取编译器默认对齐数8 和  int  类型大小4

中的较小值,即4 。所以 i 要从4 的整数倍地址开始存储,前面已占用2 字节,因此需填充2 字

节,使得  i  从偏移量为4 的地址开始存储,  i  存储后共占用4 + 4 = 8 字节。

第四步:依据规则3,最大对齐数是4( int  类型的对齐数)  , 当前总占用8 字节,8 是4 的整数倍,

无需额外填充。
 
结论:

所以  sizeof(struct S2)  的结果是8 字节。若运行包含上述结构体定义和  printf("%d\n",sizeof(struct

S2));  语句的C 语言程序,会输出  8  。

3.2为什么存在内存对齐?

1. 平台原因 (移植原因):

不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。

2. 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。假设一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。


 总体来说:结构体的内存对齐是拿空间来换取时间的做法。
 
那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到:

让占用空间小的成员尽量集中在一起

//例如:
struct S1
{char c1;int i;char c2;
};struct S2
{char c1;char c2;int i;
};S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。
输出结果:12
8

1. 分析struct S1大小

在常见编译器(如VS , 默认对齐数为8)下,依据结构体内存对齐规则:

- 第一个成员c1是 char 类型,占1 字节,对齐到偏移量为0 的地址处 ,此时占用1 字节。

- 第二个成员 i是 int 类型,大小为4 字节 ,对齐数取编译器默认对齐数8 和 int 类型大小4 中的较

小值,即4 。所以  i  要从4 的整数倍地址开始存储,前面  c1  占了1 字节,因此要填充3 字节,使

得 i 从偏移量为4 的地址开始存储, i 存储后,共占用4 + 4 = 8 字节。

- 第三个成员 c2 是 char 类型,占1 字节,对齐数为1( char  类型大小 ),从当前偏移量8 处存

储,占用1 字节。

- 最大对齐数是4 ( int  类型的对齐数 ),当前总占用9 字节,要满足是最大对齐数4 的整数倍,

需再填充3 字节。所以  sizeof(struct S1)  为12 字节。
 
2. 分析  struct S2 大小
 
- 第一个成员 c1 为 char 类型,占1 字节,对齐到偏移量为0 的地址处,占用1 字节。

- 第二个成员 c2 也是 char 类型,占1 字节,对齐数为1 ,紧接在  c1  后面存储,从偏移量为1

的地址开始,此时共占用2 字节

- 第三个成员 i是 int 类型,大小为4 字节,对齐数为4 。前面已占用2 字节,需填充2 字节,使

得  i  从偏移量为4 的地址开始存储,  i  存储后共占用4 + 4 = 8 字节。最大对齐数是4 ,8 是4 的

整数倍,无需额外填充 。所以  sizeof(struct S2)  为8 字节。
 
可见,虽然 S1 和 S2 成员相同,但成员顺序不同,导致内存对齐方式有别,所占空间大小也就

不同 。

所以在设计结构体的时候,我们既要满足对齐,又要节省空间,应该:

让占用空间小的成员尽量集中在一起

在之前关于操作符的文章中,我们只是粗略的介绍了解了结构体,今天正式认识了结构体,包括结构体的声明,初始化,以及内存对齐,希望大家能理解,感谢大家的观看!

相关文章:

  • Redis键(Key)操作完全指南:从基础到高级应用
  • MySQL高可用架构
  • 基于Llama3的开发应用(二):大语言模型的工业部署
  • 基于STM32单片机的高度集成温室环境监测系统设计与实现
  • opencv4.11生成ArUco标记 ArUco Marker
  • 养生精要:五大维度打造健康生活
  • 蓝桥杯-不完整的算式
  • 中间网络工程师知识点5
  • Java零基础学习Day15——面向对象进阶
  • 【RabbitMQ】整合 SpringBoot,实现工作队列、发布/订阅、路由和通配符模式
  • JS手写代码篇---手写 new 操作符
  • 数学复习笔记 18
  • MySQL——4、表的约束
  • 匿名函数与闭包(Anonymous Functions and Closures)-《Go语言实战指南》原创
  • 第6章:文件权限
  • 【降维】PCA
  • Captiks无线惯性动捕及步态分析系统:高频采样+400g超宽动态量程,赋能医疗康复、竞技体育、工业检测三大领域,运动轨迹零盲区追踪!”
  • 如何利用Redis实现延迟队列?
  • Windows系统部署MongoDB数据库图文教程
  • String的一些固定程序函数
  • 人民网:激发博物馆创新活力,让“过去”拥有“未来”
  • 芬兰西南部两架直升机相撞坠毁,第一批救援队已抵达现场
  • 雅安市纪委监委回应黄杨钿甜耳环事件:相关政府部门正在处理
  • 河南一女子被医院强制带走治疗,官方通报:当值医生停职
  • 长三角体育节回归“上海时间”,首次发布赛事旅游推荐线路
  • 国家防汛抗旱总指挥部对15个重点省份开展汛前实地督导检查