【C语言】结构体详解

发布时间:2023年12月25日


前言

提示:这里可以添加本文要记录的大概内容:
在计算机编程的世界中,数据的组织和存储是至关重要的。C语言作为一门强大而灵活的编程语言,提供了结构体(Structures)这一强大的工具,使得我们能够更加有效地组织和管理数据。

结构体是C语言中一种用户自定义的数据类型,它允许我们将不同数据类型的元素组合在一起,形成一个更为复杂的数据结构。通过结构体,我们可以模拟现实世界中的实体,将相关的信息组织起来,使得代码更加清晰、可读性更强。

在这篇博客中,我们将深入探讨C语言结构体的概念、用法以及它在程序设计中的实际应用。无论你是初学者还是有一定经验的程序员,通过理解结构体的原理和应用,你将能够更好地利用C语言来构建复杂而有序的程序。


提示:以下是本篇文章正文内容,下面案例可供参考
当涉及到C语言中结构体类型的声明、结构体变量的创建和初始化时,我们需要明确以下两个方面的内容。

结构体类型的声明:

在C语言中,结构体通过struct关键字进行声明。结构体声明的一般形式如下:

struct 结构体名称 {
    数据类型1 成员1;
    数据类型2 成员2;
    // ...
    数据类型n 成员n;
};

例如,声明一个表示人的结构体:

struct Person {
    char name[50];
    int age;
    float height;
};

在这个例子中,我们声明了一个名为 Person 的结构体,它包含了名为 nameageheight 的成员。

结构体变量的创建和初始化:

结构体变量的创建和初始化可以在声明结构体类型之后进行。创建结构体变量的一般形式如下:

struct 结构体名称 变量名称;

而结构体变量的初始化可以通过花括号 {} 来实现:

struct 结构体名称 变量名称 = {初始化值1, 初始化值2, ..., 初始化值n};

例如,创建并初始化一个表示人的结构体变量:

struct Person person1 = {"John Doe", 25, 175.5};

在这个例子中,我们创建了一个名为 person1 的结构体变量,并用花括号初始化了该结构体的成员。

在C语言中,如果你想为结构体的特定成员进行初始化而不是按照声明的顺序初始化所有成员你可以通过指定成员的名称来完成。以下是一个示例:

#include <stdio.h>

// 结构体类型的声明
struct Person {
    char name[50];
    int age;
    float height;
};

int main() {
    // 创建结构体变量并指定成员初始化
    struct Person person1 = {.age = 25, .name = "John Doe", .height = 175.5};

    // 访问结构体成员并打印信息
    printf("Person Information:\n");
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);
    printf("Height: %.2f\n", person1.height);

    return 0;
}

在这个例子中,我们使用花括号 {} 初始化结构体 Person 的成员,但是通过在花括号中使用 .成员名 = 初始化值 的形式,我们可以明确指定初始化的是哪个成员。这样,我们可以按照需要选择性地初始化结构体的部分成员。

这种方式使得代码更具有可读性,尤其是当结构体包含许多成员时。这样,你可以灵活地根据具体需求选择性初始化结构体的成员。


匿名结构体类型

匿名结构体是指在声明结构体的同时创建结构体变量,而不给结构体类型命名。这样的结构体类型没有具体的名称,只能通过创建的结构体变量来使用。以下是关于匿名结构体的详细阐述:

匿名结构体的声明和使用:

在C语言中,我们可以在需要的地方直接声明结构体变量,而不必事先命名结构体类型。例如:

#include <stdio.h>

int main() {
    // 匿名结构体的声明和初始化
    struct {
        char name[50];
        int age;
        float height;
    } person1 = {"John Doe", 25, 175.5};

    // 访问结构体成员并打印信息
    printf("Person Information:\n");
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);
    printf("Height: %.2f\n", person1.height);

    return 0;
}

在这个例子中,我们直接在 main 函数中声明了一个匿名结构体,并同时创建了一个结构体变量 person1,并在初始化时赋予了具体的值。这样,我们就可以在不命名结构体类型的情况下使用结构体。

优点和使用场景:

  1. 简洁性: 匿名结构体的声明更为简洁,适用于只需在一个地方使用的小型结构体。

  2. 局部性: 匿名结构体通常用于局部范围,避免全局污染。

  3. 一次性使用: 如果结构体仅在一个地方使用,而不需要在其他地方复用,可以考虑使用匿名结构体。

尽管匿名结构体在某些情况下非常方便,但在需要在多个地方重复使用相同结构体时,命名结构体类型可能更为合适。在选择使用匿名结构体时,考虑代码的复杂性和可维护性是很重要的。

结构体的自引用

**结构体的自引用指的是在结构体的定义中包含对同一类型结构体的指针。这样的设计允许我们在结构体内部引用自身,通常用于构建包含递归关系的数据结构。**以下是关于结构体的自引用的详细阐述:

结构体的自引用定义:

#include <stdio.h>

// 定义包含自引用的结构体
struct Node {
    int data;
    struct Node* next; // 结构体包含指向相同类型的指针
};

int main() {
    // 创建结构体变量并进行初始化
    struct Node node1 = {10, NULL};
    struct Node node2 = {20, NULL};

    // 将结构体变量连接起来形成链表
    node1.next = &node2;

    // 访问结构体成员并打印信息
    printf("Node 1 Data: %d\n", node1.data);
    printf("Node 2 Data: %d\n", node1.next->data);

    return 0;
}

在这个例子中,我们定义了一个包含自引用的结构体 Node,其中包含一个整数 data 和一个指向相同类型结构体的指针 next。在 main 函数中,我们创建了两个结构体变量 node1node2,并通过 node1.next 将它们连接起来,形成一个简单的链表。

用途和场景:

  1. 链表: 结构体的自引用经常用于链表的实现,其中每个结点包含一个数据元素和指向下一个结点的指针。

  2. 树结构: 在树的表示中,结构体的自引用也是常见的。每个结点包含数据以及指向其子结点的指针。

  3. 图的表示: 图的表示中,结构体的自引用可用于构建邻接表或邻接矩阵。

  4. 复杂数据结构: 当需要表示具有递归关系的复杂数据结构时,结构体的自引用提供了一种灵活而强大的方式。

注意事项

在处理结构体的自引用时,有一些常见的错误和注意事项需要考虑,以避免在程序中引入潜在的问题。以下是一些常见的结构体自引用错误和解决方法:

  1. 未完全定义的结构体类型: 如果结构体在自己内部引用自己的类型时,确保结构体的完整定义在引用之前。否则,编译器可能无法理解结构体的大小。

    // 错误示例
    struct Node {
        int data;
        struct Node next;  // 错误:结构体类型未完全定义
    };
    
    // 正确示例
    struct Node {
        int data;
        struct Node* next; // 正确:结构体类型已完全定义
    };
    
  2. 内存泄漏: 当使用动态内存分配时,务必在不再需要时释放内存,防止内存泄漏。

    // 错误示例
    struct Node* createNode(int data) {
        struct Node newNode = {data, NULL};
        return &newNode;  // 错误:返回了局部变量的地址
    }
    
    // 正确示例
    struct Node* createNode(int data) {
        struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));
        if (newNode != NULL) {
            newNode->data = data;
            newNode->next = NULL;
        }
        return newNode;
    }
    
  3. 死循环: 在处理自引用时,确保递归或循环引用不会导致死循环。例如,在链表中,最后一个结点应该指向NULL,而不是指向自身。

    // 错误示例
    struct Node {
        int data;
        struct Node* next;
    };
    
    // 创建一个循环链表
    struct Node node1 = {10, NULL};
    struct Node node2 = {20, &node1};
    node1.next = &node2;  // 错误:形成循环引用
    

    在处理递归结构体时,确保有合适的终止条件,防止无限递归。

  4. 使用未初始化的指针: 当使用指向结构体的指针时,确保在访问结构体成员之前对指针进行初始化。

    // 错误示例
    struct Node* nodePtr;
    printf("%d\n", nodePtr->data);  // 错误:未初始化的指针
    
    // 正确示例
    struct Node* nodePtr = malloc(sizeof(struct Node));
    if (nodePtr != NULL) {
        printf("%d\n", nodePtr->data);  // 正确:已初始化的指针
        free(nodePtr);
    }
    

确保在处理结构体的自引用时小心谨慎,遵循良好的内存管理实践,以确保程序的稳定性和健壮性。

结构体内存对齐(在找工作时,笔试中是重点)

结构体的对齐规则

  1. 结构体的第一个成员对齐到和结构体变量起始位置偏移量为零的地址处。
  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
    对齐数 = 编译器默认的对齐数与该成员变量大小的较小值
    -(VS中默认值是8)
    - Linux中gcc编译器没有默认对齐数,对齐数就是成员自身的大小
  3. 结构体的总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
  4. 如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍。

假设我们有以下结构体定义(我是在vs2022下测试的):

#include <stdio.h>

struct Example {
    char a;      // 1字节(默认对齐数是8)
    int b;       // 4字节(默认对齐数是8)
    double c;    // 8字节(默认对齐数是8)
};

根据对齐规则,我们可以计算结构体的大小:

  1. 结构体的第一个成员 a 对齐到偏移量为零的地址处,占用1字节。

  2. 成员 b 的对齐数是4(默认对齐数与成员自身大小的较小值),因此对齐到4的整数倍的地址处,偏移量为4字节。

  3. 成员 c 的对齐数是8(默认对齐数与成员自身大小的较小值),因此对齐到8的整数倍的地址处,偏移量为8字节。

因此,结构体的总大小是对齐数中的最大值的整数倍:

最大对齐数 = max(1, 4, 8) = 8

结构体的总大小 = 8的整数倍 = 16字节

所以,结构体 Example 的大小为16字节。这个例子展示了如何根据对齐规则计算结构体的大小。
作图说明在这里插入图片描述

为什么存在结构体内存对齐

为什么存在结构体内存对齐?结构体内存对齐是为了提高计算机程序的性能和效率。以下是详细解释:

1. 硬件要求:

计算机硬件通常要求数据存储在特定的地址上。例如,某些处理器要求特定数据类型从特定的地址开始。如果数据没有正确对齐,可能导致性能下降甚至程序崩溃。

2. 内存访问效率

内存访问通常是按照地址顺序进行的,而内存对齐可以确保访问内存的效率。如果结构体成员没有正确对齐,可能导致需要多次内存访问来读取或写入一个成员,增加了访问时间。

3. 结构体成员的对齐规则:

结构体内存对齐是根据结构体成员的类型和大小来进行的。对于每个成员,编译器选择一个对齐数,然后将成员放置在能够整除对齐数的地址上。这确保了结构体的每个成员都能够按照硬件要求正确对齐。

4. 提高缓存利用率:

内存对齐还有助于提高缓存的利用率。缓存通常按照缓存行的大小来管理数据。如果结构体的成员按照缓存行对齐,可以提高数据的读取效率,因为一个缓存行内的数据可以一次性加载到缓存中。

5. 嵌套结构体的对齐:

当结构体内部包含嵌套结构体时,对齐规则确保嵌套结构体的成员也得到正确对齐。这确保整体结构体的大小是对齐数的整数倍。

总体来说:结构体的内存对齐是拿空间来换取时间的做法。

结构体实现位段

在C语言中,位段(Bit Fields)是一种用于定义结构体成员的方式,允许你精确地控制成员所占用的位数。这对于节省内存空间和进行位级操作非常有用。以下是关于结构体实现位段的详细解释:

1. 位段的内存分配

  1. 位段的成员可以是 int, unsigned int, signed int 或者是 char 等类型
  2. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。
  3. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。

2. 为什么使用位段?

位段允许你在结构体中将一个整数字段拆分为多个较小的部分,每个部分称为一个位段。这对于对硬件寄存器进行编程或节省内存空间都非常有用。例如,当你需要表示一个状态标志集合时,位段可以帮助你更有效地使用位。

3. 位段的定义:

位段通过在结构体中的成员声明中使用冒号来定义,指定每个位段的位数。例如:

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

在这个例子中,flag1 占用一个位,flag2 占用两个位,flag3 占用三个位。

4. 位段的操作:

使用位段可以进行位级操作,例如设置和清除特定位的值。你可以通过位运算符来修改位段的值:

struct Status status;
status.flag1 = 1;  // 设置 flag1 为 1
status.flag2 = 3;  // 设置 flag2 为二进制 11
status.flag3 = 5;  // 设置 flag3 为二进制 101

5. 注意事项:

  • 位段的位数不能超过其底层数据类型的位数。例如,一个 unsigned int 类型的位段不能超过32位。
  • 位段可能会导致可移植性问题,因为底层的字节顺序可能在不同的系统上有所不同。

6. 示例:

考虑一个用于表示颜色的结构体:

struct RGBColor {
    unsigned int red   : 5;
    unsigned int green : 6;
    unsigned int blue  : 5;
};

在这个例子中,red 占用5位,green 占用6位,blue 占用5位,总共16位。这种方式可以精确地表示RGB颜色,并节省内存空间。

通过位段,你可以在结构体中以位为单位精确地控制每个成员的位数,从而更有效地使用内存。这对于嵌入式系统、底层硬件编程和对内存空间有限的环境非常有用。

总结

结构体总结

结构体是C语言中一种强大的数据结构,允许将不同类型的数据组合成一个单一的实体。通过结构体,你可以创建自定义的数据类型,更灵活地组织和存储数据。

要点总结:

  1. 定义和声明: 使用struct关键字定义结构体,通过声明结构体变量来创建实例。
  2. 成员访问: 使用点运算符(.)访问结构体的成员。
  3. 内存对齐: 结构体内存对齐确保成员按照一定规则排列,以提高存取效率。
  4. 位段: 位段允许按位控制结构体成员的位数,节省内存空间。
  5. 嵌套结构体: 可以在结构体中嵌套其他结构体,形成复杂的数据结构。
  6. 灵活性: 结构体提供了组合不同数据类型的灵活性,适用于各种编程场景。

总体而言,结构体在C语言中是一种重要的数据抽象工具,为程序员提供了更高层次的数据组织和管理方式。

文章来源:https://blog.csdn.net/m0_68002296/article/details/135181348
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。