解析编程中不可或缺的基础:深入了解结构体类型

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 解析编程中不可或缺的基础:深入了解结构体类型

引言

在编程中,结构体是一种自定义的数据类型,它允许开发人员将不同类型的数据组合在一起,并为其定义相关属性和行为。结构体提供了一种灵活的方式来表示复杂的数据结构,使得程序设计更加模块化和可读性更高。

结构体类型的声明

结构的声明

 

声明格式如下:

struct 结构体类型名
{
  成员名-list;
 
}直接声明变量-list;

结构体变量的声明和使用

下面是一个程序。首先创建了一个结构体类型Stu,里面包括了成员变量name、age、sex和 id。在主函数中创建了结构体变量s,并打印。

结构体变量创建格式:

① 按成员顺序初始化:结构体类型名 + 自定义变量名(+ 初始化内容);

② 按指定顺序初始化:结构体变量名 + 自定义变量名 (+ .成员名);

变量的使用:结构体变量名 . 成员名

#include <stdio.h>
 
struct Stu
{
  char name[20];//名字
  int age;//年龄
  char sex[5];//性别
  char id[20];//学号
};
 
int main()
{
  //按照结构体成员的顺序初始化
  struct Stu s = { "张三", 20, "男", "20230818001" };
  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 s2 = { .age = 18, .name = "lisi", .id = "20230818002", .sex = "⼥" };
  printf("name: %s\n", s2.name);
  printf("age : %d\n", s2.age);
  printf("sex : %s\n", s2.sex);
  printf("id : %s\n", s2.id);
  return 0;
 
}

结构的不完全声明

在声明结构体类型的时候可以不完全声明,直接在结构体类型后声明变量,这样创建的变量就是一次性变量,之后只能一次性使用。

声明如下:

struct
{
  int a;
  char b;
  float c;
}x;
 
struct
{
  int a;
  char b;
  float c;
}a[20], * p;

结构的⾃引⽤

结构的自引用典型例子就是链表中对节点的定义,用于连续节点连接,具体有关链表的知识可以点击这段蓝字阅读博主另一篇博客

自引用结构声明格式如下:

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

自定义结构体

声明格式如下:

格式:typedef + struct 结构体类型名

       {

               成员变量;

       }自定义类型名;

示例声明如下:

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

结构体内存对⻬(热门考点)

引子

我们经常会用sizeof运算符计算各个变量的字节大小,例如:

#include<stdio.h>
 
int main()
{
  printf("%d\n", sizeof(int));
  printf("%d\n", sizeof(short));
  printf("%d\n", sizeof(long long));
 
  return 0;
}

得到结果:

如文所示,我们可以用sizeof来计算各个类型的大小,那么结构体变量计算会得到什么结果呢?

对⻬规则

结构体变量在内存中会遵循结构体对齐规则,对齐规则如下:

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

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

  对⻬数=编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。

   VS 中默认的值为 8

   Linux中gcc没有默认对⻬数,对⻬数就是成员⾃⾝的⼤⼩

3.结构体总⼤⼩为最⼤对⻬数(结构体中每个成员变量都有⼀个对⻬数,所有对⻬数中最⼤的)的整数倍。

4.如果嵌套了结构体的情况,嵌套的结构体成员对⻬到⾃⼰的成员中最⼤对⻬数的整数倍处,结构体的整体⼤⼩就是所有最⼤对⻬数(含嵌套结构体中成员的对⻬数)的整数倍。

结构体内存对齐练习

1.非嵌套结构体

#include<stdio.h>
 
int main()
{
  struct S1
  {
    char c1;
    int i;
    char c2;
  };
 
  printf("%d\n", sizeof(struct S1));
 
  return 0;
}

按照内存对齐规则,从编译器行数从上到下进行内存存储。逐个对比各个成员和VS的默认对齐数8对比,取二者最小对齐数作为对齐数。

根据对齐数从0开始偏移计算每个变量开始存储的内存地址,成员变量要对⻬到对⻬数的整数倍的地址处,如图所示。

最后计算结构体总共大小是需要按照结构体中成员变量的最大对齐数进行对齐,最终结构体大小是最大对齐数的整数倍,产生的浪费空间也要计入总大小。

内存存储图示如下:

2.嵌套结构体

#include<stdio.h>
 
struct S3
{
  double d;
  char c;
  int i;
};
 
int main()
{
  struct S4
  {
    char c1;
    struct S3 s3;
    double d;
  };
  printf("%d\n", sizeof(struct S4));
 
  return 0;
}

计算嵌套结构体的字节大小时对待被嵌套的结构体时,就相当于把嵌套结构体当做结构体的一个成员进行内存对齐。最终计算字节总大小的时候用所有成员中最大对齐数(包括被嵌套结构体中的成员)进行整数倍的计算。

下图即为上述代码的演示图例:

为什么存在内存对⻬?

1. 硬件访问要求: 计算机硬件对于访问内存通常有一定的要求,例如某些硬件可能只能从特定地址开始读取数据,或者只能按照特定的字节长度进行读取。通过内存对齐,可以保证结构体中的字段在内存中按照一定的规则排列,满足硬件访问的要求。

2. 性能优化: 在数据结构中,尤其是在涉及栈这种数据结构时,我们应该尽可能使数据在自然边界上对齐。这样做的原因在于,处理器访问未对齐的内存时需要进行两次内存访问,而对齐的内存访问只需要一次访问。举例来说,如果一个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能够保证所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。因此,通过合理地对数据进行内存对齐,我们可以提高程序的执行效率和性能表现。

3. 内存空间利用: 内存对齐可以使数据结构更加紧凑,减少内存空间的浪费。如果结构体中的字段按照对齐规则排列,编译器可以合理地利用内存空间,避免由于未对齐而导致的内存浪费。

4. 平台移植性: 不同的计算机架构可能对内存对齐有不同的要求。合理地处理结构体的内存对齐可以增强程序在不同平台上的移植性,使程序更容易地在不同平台上移植和运行

针对于性能优化,我们可以了解到结构体对齐是为了优化性能,用空间换时间,那么有没有什么办法让我们尽量的减少浪费的空间呢?

我们可以利用结构体对齐的规则,将小的结构体尽量的凑在一起,这样他们会在空间上连续存储,因为对齐数小的和大的之间会存在大对齐数所造成的空间浪费,,所以将小的放一块这样就可以将其中的浪费空间给合理利用起来。

具体如下代码示例及图示:

struct S1
{
  char c1;
  int i;
  char c2;
};
struct S2
{
  char c1;
  char c2;
  int i;
};

修改默认对齐数

#pragma 这个预处理指令,可以改变编译器的默认对⻬数。

例如,我们要将编译器的默认对齐数修改为1,那么勇以下代码实现:

#pragma pack(1)

如果需要取消修改的默认对齐数,使用以下代码即可实现:

#pragma pack()

 

位段结构体

当我们需要在C或C++中表示一些具有特定位长度的数据时,位段(bit fields)结构体就成为了一种非常有用的工具。位段结构体允许我们将数据按位组织,并且可以更加高效地使用内存空间。

什么是位段结构体?

位段结构体是C和C++中的一种特殊结构,它允许我们定义结构体的成员为特定位长度的字段,从而更为灵活地管理数据。通过位段结构体,我们可以精确地控制每个字段的位数,从而在内存中节约空间。

如何定义位段结构体?

在C和C++中,我们可以使用结构体来定义位段。

位段的声明和结构是类似的,有两个不同:

1. 位段的成员必须是 int、unsigned int 或signed int ,在C99中位段成员的类型也可以

选择其他类型。

2. 位段的成员名后边有⼀个冒号和⼀个数字

下面是一个简单的例子:

struct BitFieldStruct 
{
    unsigned int flag1 : 1;
    unsigned int flag2 : 2;
    unsigned int flag3 : 3;
};

位段的内存分配

 

分配规则:

1. 位段的成员可以是 int  unsigned int  signed int 或者是 char 等类型

2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的⽅式来开辟的。

3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使⽤位段。

 

通过上文已经得知位段结构体如何创建,下面请通过示例代码和图示来了解位段结构体再内存中的分配原理。

代码如下:

struct S
{
  char a : 3;
  char b : 4;
  char c : 5;
  char d : 4;
};
struct S s = { 0 };
 
s.a = 10;
s.b = 12;
s.c = 3;
s.d = 4;

代码中定义了一个结构体类型S,在main函数中创建S型变量s并初始化为0。重点在于,在已经规定的位段情况下,后面的a,b,c,d赋值后在内存中是如何存储的呢?

图示操作如下:

最后的d由于在第二个字节段中无法存储,所以会直接存到下一个字节中,大小位4比特。

使用位段的注意事项

位段的⼏个成员共有同⼀个字节,这样有些成员的起始位置并不是某个字节的起始位置,那么这些位

置处是没有地址的。内存中每个字节分配⼀个地址,⼀个字节内部的bit位是没有地址的。

所以不能对位段的成员使⽤&操作符,这样就不能使⽤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);
  //正确的⽰范
  int b = 0;
  scanf("%d", &b);
  sa._b = b;
  return 0;
}

 

位段结构体的优势

  1. 节省内存空间:位段结构体可以将多个字段压缩到一个字节中(或者更少),这样可以减少内存使用量。在一些嵌入式系统或需要高效利用内存的场景中,位段结构体可以发挥重要作用。
  2. 更好的可移植性:位段结构体可以帮助开发者更好地处理不同机器上的字节顺序问题和对齐方式。因为位段结构体的字段是按照位来处理的,所以不受机器的字节顺序和对齐方式的影响。
  3. 方便地操作位数据:位段结构体可以方便地处理二进制数据,例如一些硬件寄存器中的位标志。使用位段结构体可以使得代码更加简洁和易读,降低出错的风险。
  4. 更好的兼容性:位段结构体的语法与普通结构体非常相似,因此可以很容易地与其他代码进行交互和集成。此外,C++11标准中还引入了新的标准化位字段类型,称为 std::bitset,可以更加方便地处理位数据。


I'm Kevin, and we'll see you in the next blog


目录
相关文章
|
18天前
|
开发框架 供应链 监控
并行开发模型详解:类型、步骤及其应用解析
在现代研发环境中,企业需要在有限时间内推出高质量的产品,以满足客户不断变化的需求。传统的线性开发模式往往拖慢进度,导致资源浪费和延迟交付。并行开发模型通过允许多个开发阶段同时进行,极大提高了产品开发的效率和响应能力。本文将深入解析并行开发模型,涵盖其类型、步骤及如何通过辅助工具优化团队协作和管理工作流。
51 3
|
7天前
|
缓存 监控 网络协议
|
3天前
|
安全 程序员 API
|
6天前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
5天前
|
Dart 安全 编译器
Flutter结合鸿蒙next 中数据类型转换的高级用法:dynamic 类型与其他类型的转换解析
在 Flutter 开发中,`dynamic` 类型提供了灵活性,但也带来了类型安全性问题。本文深入探讨 `dynamic` 类型及其与其他类型的转换,介绍如何使用 `as` 关键字、`is` 操作符和 `whereType&lt;T&gt;()` 方法进行类型转换,并提供最佳实践,包括避免过度使用 `dynamic`、使用 Null Safety 和异常处理,帮助开发者提高代码的可读性和可维护性。
62 1
|
11天前
|
Java 开发者 UED
Java编程中的异常处理机制解析
在Java的世界里,异常处理是确保程序稳定性和可靠性的关键。本文将深入探讨Java的异常处理机制,包括异常的类型、如何捕获和处理异常以及自定义异常的创建和使用。通过理解这些概念,开发者可以编写更加健壮和易于维护的代码。
|
19天前
|
Java 关系型数据库 MySQL
【编程基础知识】Eclipse连接MySQL 8.0时的JDK版本和驱动问题全解析
本文详细解析了在使用Eclipse连接MySQL 8.0时常见的JDK版本不兼容、驱动类错误和时区设置问题,并提供了清晰的解决方案。通过正确配置JDK版本、选择合适的驱动类和设置时区,确保Java应用能够顺利连接MySQL 8.0。
90 1
|
21天前
|
Java
【编程基础知识】《Java 中的神秘利器:this 关键字深度解析》
《Java 中的神秘利器:this 关键字深度解析》深入探讨了 Java 中 this 关键字的作用、用法及应用场景。文章详细解释了 this 如何指向当前对象、区分成员变量和局部变量、调用构造函数、实现方法链式调用和传递当前对象。通过阅读本文,读者将全面掌握 this 关键字的巧妙应用,提升 Java 编程技能。
25 2
|
21天前
|
开发框架 Oracle Java
【编程基础知识】《Java 世界探秘:JRE、JDK 与 JDK 版本全解析》
JRE(Java Runtime Environment)是运行Java程序所需的环境,包含JVM和Java核心类库,适合普通用户使用。JDK(Java Development Kit)则是Java开发工具包,不仅包含JRE,还提供了编译器、调试器等开发工具,适用于开发者。两者的主要区别在于JDK用于开发,而JRE仅用于运行Java程序。JDK各版本不断引入新特性,如Java 8中的Lambda表达式和默认方法等。环境配置方面,Windows和Linux系统都有详细的步骤,确保Java程序能够顺利编译和运行。
28 1
|
6天前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树

热门文章

最新文章

推荐镜像

更多