文章目录

C 语言结构体:struct 定义与使用

发布于 2026-04-04 22:18:01 · 浏览 20 次 · 评论 0 条

C 语言结构体:struct 定义与使用

结构体是C语言中最强大的数据类型之一,它允许你将不同类型的数据组合在一起,形成一个自定义的数据类型。无论是描述一个学生的信息(姓名、年龄、成绩),还是表示一个坐标点(x、y、z),结构体都能帮你轻松实现。


结构体的本质:为什么需要结构体

在实际的编程问题中,我们经常需要处理复杂的数据。比如,一个学生档案包含姓名(字符串)、年龄(整数)、平均分(浮点数)等多种数据类型。如果把这些信息分散存储在不同的变量中,代码会变得杂乱无章,管理起来也非常困难。

结构体的核心价值在于将相关数据打包在一起。你可以把结构体想象成一个容器,这个容器可以同时容纳多种不同类型的数据,而你需要为这个容器起一个名字来标识它的用途。这种数据组织方式让你的代码逻辑更加清晰,数据的封装性和可读性都会大幅提升。


定义结构体:创建自定义数据类型

最基本的定义方式

定义结构体的语法由 struct 关键字开头,后面跟着结构体名称(称为标签),最后用大括号包裹所有成员变量。让我们看一个具体的例子:

struct Student {
    char name[50];      // 姓名,最多49个字符 + 结束符
    int age;            // 年龄
    float score;        // 平均成绩
};

这段代码完成了一件重要的事情:它定义了一个新的数据类型 struct Student。这个类型包含三个成员,分别用于存储姓名、年龄和成绩。一旦定义完成,你就可以像使用 intfloat 那样使用这个新类型来声明变量。

定义与声明一体化

有些情况下,你希望在定义结构体的同时就声明变量。这种写法可以让你一步完成定义和声明,代码更加紧凑:

struct Point {
    int x;
    int y;
} pt1, pt2;

在这个例子中,pt1pt2 都是 struct Point 类型的变量。需要注意的是,如果你后续还需要创建更多的同类型变量,这种写法可能会让你陷入困惑——因为看到代码的人可能会搞不清楚这个结构体是否已经被定义过。所以,除非你确定只会使用这几个变量,否则建议将定义和声明分开。

定义匿名结构体

当结构体只需要使用一次时,你可以省略结构体标签,直接定义变量:

struct {
    double width;
    double height;
} rectangle;

这种方式创建的变量 rectangle 确实可以正常使用,但你无法在代码的其他地方再次声明同类型的新变量了。因此,这种写法只适合那些只用一次的简单场景。


声明结构体变量:创建具体实例

结构体定义完成后,就可以用它来声明变量了。声明的方式与普通变量类似,只是需要在类型前面加上 struct 关键字:

// 先定义结构体
struct Book {
    char title[100];
    char author[50];
    float price;
};

// 再声明变量
struct Book book1;
struct Book book2;

声明完成后,book1book2 就成为了两个独立的结构体变量,它们各自拥有完整的成员变量集合。修改 book1 的成员不会影响 book2,反之亦然。


初始化结构体:给成员变量赋值

使用花括号初始化

在声明结构体变量的同时,可以直接给成员变量赋予初始值。C语言提供了花括号初始化语法,让这个过程变得简洁直观:

struct Person {
    char name[30];
    int age;
    char gender;
};

// 声明时初始化所有成员
struct Person p1 = {"张三", 25, 'M'};

// 部分初始化,未指定的成员会被设置为0
struct Person p2 = {.name = "李四", .age = 30};

注意第二行代码中的点号(.)语法,它允许你显式地指定要初始化的成员。未被提及的成员会自动被初始化为零值(对于数值类型是0,对于指针类型是NULL,对于字符类型是'\0'`)。这种指定初始化器的写法让代码的可读性更好,也避免了因成员顺序记错而导致的bug。

逐个成员赋值

如果变量已经声明,后来才需要赋值,可以使用点号(`.)运算符逐个访问成员并赋值:

struct Person p3;
strcpy(p3.name, "王五");
p3.age = 28;
p3.gender = 'M';

需要特别注意的是,结构体中的字符数组不能直接用等号赋值,必须使用 strcpystrncpy 函数来拷贝字符串。这是一个常见的初学者陷阱。


访问结构体成员:读写数据

点号运算符的使用

结构体成员的访问通过点号(`.)运算符完成。点号前面是结构体变量名,后面是你要访问的成员名。这种语法非常直观,就像在说"这个结构体的这个成员":

struct Employee {
    char name[50];
    int id;
    double salary;
};

struct Employee emp;

// 给成员赋值
strcpy(emp.name, "赵六");
emp.id = 1001;
emp.salary = 7500.50;

// 读取成员的值并使用
printf("员工姓名: %s\n", emp.name);
printf("员工ID: %d\n", emp.id);
printf("员工月薪: %.2f\n", emp.salary);

嵌套结构体的成员访问

结构体的成员本身也可以是结构体,这种情况下需要多次使用点号运算符,逐层深入:

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

struct Student {
    char name[50];
    struct Date birthday;  // birthday 是 Date 类型的成员
    float score;
};

struct Student stu;
strcpy(stu.name, "钱七");
stu.birthday.year = 1998;
stu.birthday.month = 5;
stu.birthday.day = 20;
stu.score = 92.5;

访问嵌套结构体的成员时,路径必须完整。从最外层的变量名开始,一步步深入到最终要访问的成员,每一步都用点号连接。


结构体数组:管理多个结构体

当需要存储多个同类型的结构体时,结构体数组是最自然的选择。声明结构体数组的语法与普通数组完全相同,只是元素类型变成了结构体类型:

struct Product {
    char name[50];
    int code;
    float price;
};

// 声明包含5个 Product 结构体的数组
struct Product inventory[5];

// 初始化数组
struct Product inventory[5] = {
    {"苹果", 101, 5.50},
    {"香蕉", 102, 3.20},
    {"橙子", 103, 4.80}
};

访问结构体数组中特定元素的成员,需要结合数组下标和点号运算符:

// 访问第二个产品的名称和价格
printf("产品名称: %s\n", inventory[1].name);
printf("产品价格: %.2f\n", inventory[1].price);

// 遍历数组,给所有产品打9折
for (int i = 0; i < 5; i++) {
    inventory[i].price *= 0.9;
}

结构体指针:高效传递大数据

指针的基本使用

结构体指针指向结构体变量的内存地址。使用指针访问结构体成员有两种方式,其中一种是显式使用解引用运算符:

struct Rectangle {
    double length;
    double width;
};

struct Rectangle rect = {10.5, 5.2};
struct Rectangle *ptr = &rect;

// 方式一:显式解引用
(*ptr).length = 20.0;

// 方式二:箭头运算符(更简洁)
ptr->width = 8.0;

箭头运算符(->)是结构体指针的专用语法,它的含义是"通过指针访问成员"。相比之下,(*ptr).length 需要先解引用再访问成员,写法繁琐且容易出错。因此,当你使用结构体指针时,务必使用箭头运算符,这不仅是代码风格的问题,更关乎可读性和正确性。

指针传递的优势

在C语言中,结构体作为函数参数时默认是值传递。这意味着函数接收的是结构体的完整副本,如果结构体很大,复制操作会消耗大量的时间和内存。使用指针传递可以避免这种开销,因为只需要复制一个地址(通常是4或8字节):

void updateScore(struct Student *stu, float newScore) {
    stu->score = newScore;  // 直接修改原结构体的成员
}

struct Student s = {"孙八", 22, 88.5};
updateScore(&s, 95.0);  // 传递指针,函数内直接修改原变量
printf("更新后的成绩: %.1f\n", s.score);  // 输出 95.0

结构体与函数:作为参数和返回值

值传递 vs 指针传递

结构体可以作为函数的参数和返回值,但两种传递方式有本质区别。值传递会创建完整副本,指针传递只传递地址:

struct Data {
    int values[1000];  // 大数组
    int count;
};

// 值传递:函数内部操作的是副本
struct Data processData(struct Data input) {
    input.count++;
    return input;  // 需要返回修改后的副本
}

// 指针传递:函数直接操作原始数据
void incrementCount(struct Data *input) {
    input->count++;
}

对于包含大量数据的结构体,指针传递几乎是必然的选择。值传递不仅效率低下,如果忘记返回值,还可能导致数据丢失。

返回结构体的场景

函数返回结构体在某些场景下非常有用,特别是当函数需要计算一个复杂的结果,而这个结果由多个相关数据组成时:

struct Result {
    int sum;
    float average;
    int min;
    int max;
};

struct Result analyzeArray(int arr[], int size) {
    struct Result r;
    r.sum = 0;
    r.min = arr[0];
    r.max = arr[0];

    for (int i = 0; i < size; i++) {
        r.sum += arr[i];
        if (arr[i] < r.min) r.min = arr[i];
        if (arr[i] > r.max) r.max = arr[i];
    }
    r.average = (float)r.sum / size;

    return r;  // 返回完整的分析结果
}

typedef 与结构体:简化类型名

C语言的结构体类型在使用时必须加上 struct 关键字,这会让代码显得冗长。typedef 关键字可以为你创建结构体类型的别名,从此告别繁琐的 struct 前缀:

// 使用 typedef 定义别名
typedef struct {
    char street[100];
    char city[50];
    char zipCode[10];
} Address;

// 现在可以这样声明变量,无需 struct 前缀
Address home = {"科技路123号", "北京市", "100000"};
Address office = {"创新大道456号", "上海市", "200000"};

如果你需要结构体内部引用自身(比如链表节点),必须保留标签:

typedef struct ListNode {
    int data;
    struct ListNode *next;  // 这里必须用原类型名
} ListNode;

实际应用:学生信息管理系统

让我们用一个完整的示例来展示结构体的综合应用。这个简单的学生信息管理系统演示了如何定义结构体、创建数组、排序以及输出结果:

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

typedef struct {
    char name[50];
    int id;
    float gpa;
} Student;

void printStudent(Student *s) {
    printf("姓名: %s, 学号: %d, GPA: %.2f\n", s->name, s->id, s->gpa);
}

void sortByGPA(Student students[], int n) {
    Student temp;
    for (int i = 0; i < n - 1; i++) {
        for (int j = 0; j < n - 1 - i; j++) {
            if (students[j].gpa < students[j + 1].gpa) {
                temp = students[j];
                students[j] = students[j + 1];
                students[j + 1] = temp;
            }
        }
    }
}

int main() {
    Student class2019[3] = {
        {"周九", 2019001, 3.85},
        {"吴十", 2019002, 3.92},
        {"郑十一", 2019003, 3.78}
    };

    printf("排序前:\n");
    for (int i = 0; i < 3; i++) {
        printStudent(&class2019[i]);
    }

    sortByGPA(class2019, 3);

    printf("\n按GPA降序排列后:\n");
    for (int i = 0; i < 3; i++) {
        printStudent(&class2019[i]);
    }

    return 0;
}

运行结果:

排序前:
姓名: 周九, 学号: 2019001, GPA: 3.85
姓名: 吴十, 学号: 2019002, GPA: 3.92
姓名: 郑十一, 学号: 2019003, GPA: 3.78

按GPA降序排列后:
姓名: 吴十, 学号: 2019002, GPA: 3.92
姓名: 周九, 学号: 2019001, GPA: 3.85
姓名: 郑十一, 学号: 2019003, GPA: 3.78

这个示例展示了结构体的几个核心应用:使用 typedef 简化类型名、使用指针传递避免大数据复制、结构体数组的排序操作。实际开发中的系统往往比这复杂得多,但基本原理完全相同。


结构体的高级特性与注意事项

结构体的内存布局

理解结构体的内存布局对写出高效代码至关重要。编译器在分配结构体内存时,可能会在成员之间插入填充字节,以确保每个成员都按其对齐要求存储。这意味着结构体的实际大小可能大于各成员大小之和:

struct Example {
    char a;      // 1字节
    // 可能填充3字节
    int b;       // 4字节
    // 可能填充4字节
    double c;    // 8字节
};

你可以通过 sizeof 运算符查看结构体的实际大小。在某些对内存敏感的场景下(如嵌入式系统或大规模数据处理),调整成员顺序可以减少内存浪费。

赋值与比较

结构体变量之间可以直接赋值,这会复制所有成员的值:

struct Point p1 = {3, 4};
struct Point p2;
p2 = p1;  // 将 p1 的所有成员复制到 p2

但结构体之间不能直接使用 == 运算符进行比较,必须逐个成员比较。这是因为编译器无法自动生成比较函数,你需要手动实现。

结构体的未来发展

C11标准引入了匿名结构体和灵活的数组成员等新特性,让结构体的使用更加灵活。C23标准更是增加了结构体字面量语法,进一步简化了结构体的初始化。但考虑到兼容性问题,这些新特性在实际项目中使用时需要确认编译器的支持程度。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文