MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C 语言结构体详解

2024-03-152.3k 阅读

一、结构体的基本概念

在C语言中,结构体(struct)是一种用户自定义的数据类型,它允许将不同类型的数据组合在一起,形成一个单一的实体。结构体提供了一种将相关数据项分组的方式,使得代码更具结构性和可读性。

1.1 结构体的定义

结构体的定义使用struct关键字,其一般形式如下:

struct 结构体名 {
    数据类型1 成员1;
    数据类型2 成员2;
    // 可以有更多的成员
    数据类型n 成员n;
};

例如,我们定义一个表示日期的结构体:

struct Date {
    int year;
    int month;
    int day;
};

在上述定义中,struct Date是结构体类型,yearmonthday是结构体的成员,它们都是int类型。

1.2 结构体变量的声明

定义了结构体类型后,就可以声明该类型的变量。有以下几种方式:

  • 先定义结构体类型,再声明变量
struct Date {
    int year;
    int month;
    int day;
};
struct Date myDate;

这里先定义了struct Date类型,然后声明了myDate变量,它的类型是struct Date

  • 在定义结构体类型的同时声明变量
struct Date {
    int year;
    int month;
    int day;
} myDate;

这种方式在定义struct Date类型的同时,声明了myDate变量。

  • 匿名结构体
struct {
    int year;
    int month;
    int day;
} myDate;

这里定义了一个没有名字的结构体类型,并声明了myDate变量。匿名结构体通常用于只需要使用一次的结构体变量。

二、结构体成员的访问

一旦声明了结构体变量,就可以访问其成员。使用点运算符(.)来访问结构体变量的成员。

2.1 结构体成员的赋值和读取

例如,对于上述的myDate结构体变量:

#include <stdio.h>

struct Date {
    int year;
    int month;
    int day;
};

int main() {
    struct Date myDate;
    myDate.year = 2023;
    myDate.month = 10;
    myDate.day = 5;

    printf("日期是:%d-%d-%d\n", myDate.year, myDate.month, myDate.day);
    return 0;
}

在上述代码中,通过点运算符分别给myDateyearmonthday成员赋值,然后使用printf函数输出这些成员的值。

2.2 通过指针访问结构体成员

除了使用点运算符,还可以通过结构体指针来访问结构体成员。当使用结构体指针时,需要使用箭头运算符(->)。

#include <stdio.h>

struct Date {
    int year;
    int month;
    int day;
};

int main() {
    struct Date myDate = {2023, 10, 5};
    struct Date *datePtr = &myDate;

    printf("日期是:%d-%d-%d\n", datePtr->year, datePtr->month, datePtr->day);
    return 0;
}

在上述代码中,首先声明了一个struct Date类型的变量myDate并初始化,然后声明了一个指向myDate的指针datePtr。通过datePtr->year等方式访问结构体成员。

三、结构体的初始化

结构体变量可以在声明时进行初始化,初始化的方式类似于数组的初始化。

3.1 完全初始化

对于前面定义的Date结构体,可以这样初始化:

struct Date myDate = {2023, 10, 5};

这里按照结构体成员的定义顺序,依次为yearmonthday赋值。

3.2 部分初始化

也可以只初始化部分成员,未初始化的成员会被初始化为0(对于数值类型)或空字符(对于字符类型)。

struct Date myDate = {2023};

在这种情况下,myDate.year被初始化为2023,myDate.monthmyDate.day被初始化为0。

3.3 按成员初始化(C99及以后)

C99标准引入了按成员初始化的方式,这种方式更加灵活,可以不按照成员定义的顺序进行初始化。

struct Date myDate = {.day = 5,.month = 10,.year = 2023};

通过这种方式,可以明确指定每个成员的值,顺序可以随意。

四、结构体数组

结构体数组是指数组的每个元素都是一个结构体。结构体数组在处理多个相同类型的结构体数据时非常有用。

4.1 结构体数组的定义和初始化

例如,我们定义一个表示学生信息的结构体,并声明一个结构体数组:

#include <stdio.h>

struct Student {
    char name[50];
    int age;
    float score;
};

int main() {
    struct Student students[2] = {
        {"Alice", 20, 85.5},
        {"Bob", 21, 90.0}
    };

    for (int i = 0; i < 2; i++) {
        printf("学生 %d: 姓名 %s, 年龄 %d, 成绩 %.2f\n", i + 1, students[i].name, students[i].age, students[i].score);
    }
    return 0;
}

在上述代码中,定义了struct Student结构体,然后声明了一个包含两个元素的students结构体数组,并进行了初始化。通过循环遍历数组,输出每个学生的信息。

4.2 结构体数组的使用

结构体数组的使用与普通数组类似,可以通过下标访问每个元素,进而访问结构体成员。例如,要修改第二个学生的成绩:

students[1].score = 92.5;

五、结构体嵌套

结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。

5.1 结构体嵌套的定义

例如,我们定义一个表示地址的结构体,然后在表示人的结构体中使用它:

struct Address {
    char city[50];
    char street[100];
    int zipCode;
};

struct Person {
    char name[50];
    int age;
    struct Address addr;
};

struct Person结构体中,addr成员是struct Address类型。

5.2 结构体嵌套的初始化和访问

初始化和访问嵌套结构体时,需要使用多个点运算符。例如:

#include <stdio.h>

struct Address {
    char city[50];
    char street[100];
    int zipCode;
};

struct Person {
    char name[50];
    int age;
    struct Address addr;
};

int main() {
    struct Person p = {
        "Charlie",
        22,
        {"New York", "123 Main St", 10001}
    };

    printf("姓名:%s, 年龄:%d\n", p.name, p.age);
    printf("地址:%s, %s, %d\n", p.addr.city, p.addr.street, p.addr.zipCode);
    return 0;
}

在上述代码中,初始化struct Person变量p时,对嵌套的struct Address成员addr也进行了初始化。通过点运算符的链式使用来访问嵌套结构体的成员。

六、结构体与函数

结构体可以作为函数的参数、返回值以及在函数内部使用。

6.1 结构体作为函数参数

结构体可以像普通数据类型一样作为函数的参数传递。例如:

#include <stdio.h>

struct Date {
    int year;
    int month;
    int day;
};

void printDate(struct Date d) {
    printf("日期是:%d-%d-%d\n", d.year, d.month, d.day);
}

int main() {
    struct Date myDate = {2023, 10, 5};
    printDate(myDate);
    return 0;
}

在上述代码中,printDate函数接受一个struct Date类型的参数d,并输出该日期。

6.2 结构体作为函数返回值

函数也可以返回一个结构体。例如:

#include <stdio.h>

struct Date {
    int year;
    int month;
    int day;
};

struct Date getCurrentDate() {
    struct Date d = {2023, 10, 5};
    return d;
}

int main() {
    struct Date today = getCurrentDate();
    printf("今天的日期是:%d-%d-%d\n", today.year, today.month, today.day);
    return 0;
}

在上述代码中,getCurrentDate函数返回一个struct Date类型的结构体,该结构体表示当前日期。

6.3 结构体指针作为函数参数

为了提高效率,避免结构体的大量复制,可以使用结构体指针作为函数参数。例如:

#include <stdio.h>

struct Date {
    int year;
    int month;
    int day;
};

void incrementDate(struct Date *d) {
    d->day++;
    if (d->day > 30) { // 这里简单假设每月30天
        d->day = 1;
        d->month++;
        if (d->month > 12) {
            d->month = 1;
            d->year++;
        }
    }
}

int main() {
    struct Date myDate = {2023, 10, 5};
    incrementDate(&myDate);
    printf("增加后的日期是:%d-%d-%d\n", myDate.year, myDate.month, myDate.day);
    return 0;
}

在上述代码中,incrementDate函数接受一个struct Date指针d,通过指针修改结构体成员的值。

七、结构体的内存布局

结构体在内存中的布局是按照成员定义的顺序依次排列的,但可能会存在内存对齐的情况。

7.1 内存对齐的概念

内存对齐是指结构体成员在内存中的存储地址按照一定规则进行对齐,以提高内存访问效率。通常,结构体的每个成员的地址都应该是其自身大小的倍数。例如,一个int类型(假设为4字节)的成员,其地址应该是4的倍数。

7.2 内存对齐的影响

内存对齐会导致结构体的实际大小可能大于所有成员大小之和。例如:

#include <stdio.h>

struct Example {
    char c; // 1字节
    int i;  // 4字节
};

int main() {
    printf("结构体Example的大小:%zu\n", sizeof(struct Example));
    return 0;
}

在上述代码中,struct Example包含一个char类型成员(1字节)和一个int类型成员(4字节),但sizeof(struct Example)的结果可能是8字节,而不是5字节。这是因为为了满足int类型的内存对齐要求,char成员后面会填充3个字节。

7.3 控制内存对齐

在一些情况下,可以通过编译器特定的指令来控制内存对齐。例如,在GCC编译器中,可以使用__attribute__((packed))来取消内存对齐:

#include <stdio.h>

struct __attribute__((packed)) Example {
    char c;
    int i;
};

int main() {
    printf("结构体Example的大小:%zu\n", sizeof(struct Example));
    return 0;
}

此时,sizeof(struct Example)的结果为5字节,因为取消了内存对齐。

八、共用体(Union)与结构体的比较

共用体(union)也是一种用户自定义的数据类型,它与结构体有一些相似之处,但在内存使用上有很大区别。

8.1 共用体的定义和特点

共用体使用union关键字定义,其所有成员共享同一块内存空间。例如:

union Data {
    int i;
    float f;
    char c;
};

在上述定义中,union Dataifc成员共享同一块内存。这意味着在任何时刻,只能有一个成员有效。

8.2 共用体与结构体的内存占用区别

结构体的每个成员都有自己独立的内存空间,其大小是所有成员大小之和(考虑内存对齐)。而共用体的大小是其最大成员的大小。例如:

#include <stdio.h>

struct ExampleStruct {
    char c;
    int i;
};

union ExampleUnion {
    char c;
    int i;
};

int main() {
    printf("结构体ExampleStruct的大小:%zu\n", sizeof(struct ExampleStruct));
    printf("共用体ExampleUnion的大小:%zu\n", sizeof(ExampleUnion));
    return 0;
}

在上述代码中,struct ExampleStruct的大小可能是8字节(考虑内存对齐),而union ExampleUnion的大小是4字节(假设int为4字节,因为int是最大成员)。

8.3 适用场景的区别

结构体适用于需要同时存储和处理多个不同类型相关数据的情况,而共用体适用于在不同时刻使用不同类型数据,但不需要同时存储它们的情况。例如,在解析二进制数据时,如果某个字段可能是不同的数据类型,可以使用共用体来方便地处理。

九、结构体的高级特性

除了上述基本内容,结构体还有一些高级特性,如结构体自引用和柔性数组。

9.1 结构体自引用

结构体自引用是指结构体中包含一个指向自身类型的指针。这在构建链表、树等数据结构时非常有用。例如:

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

在上述代码中,struct Node结构体包含一个int类型的data成员和一个指向struct Node类型的next指针,通过这种方式可以构建链表结构。

9.2 柔性数组

柔性数组(Flexible Array Member)是C99标准引入的特性,它允许结构体的最后一个成员是大小未知的数组。例如:

struct Buffer {
    int size;
    char data[];
};

在上述代码中,data是一个柔性数组。柔性数组的大小可以在运行时根据需要动态分配。使用柔性数组时,通常会通过malloc函数分配足够的内存,以包含结构体头部和柔性数组的内容。例如:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Buffer {
    int size;
    char data[];
};

int main() {
    int len = 10;
    struct Buffer *buf = (struct Buffer *)malloc(sizeof(struct Buffer) + len * sizeof(char));
    buf->size = len;
    strcpy(buf->data, "Hello, World!");
    printf("缓冲区内容:%s\n", buf->data);
    free(buf);
    return 0;
}

在上述代码中,首先通过malloc分配了足够的内存,然后可以像使用普通数组一样使用柔性数组data

通过以上对C语言结构体的详细介绍,相信读者对结构体的概念、使用方法以及相关的高级特性有了更深入的理解。结构体作为C语言中重要的用户自定义数据类型,在各种程序开发中都有着广泛的应用,熟练掌握结构体的使用对于编写高效、结构清晰的C语言程序至关重要。在实际应用中,需要根据具体的需求合理地设计结构体,充分发挥结构体的优势。同时,要注意内存对齐等细节问题,以确保程序的性能和正确性。在处理复杂数据结构时,结构体自引用和柔性数组等高级特性能够提供强大的功能,帮助开发者构建高效的数据存储和处理方式。希望读者通过不断实践,进一步加深对结构体的理解和运用能力。