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

【C语言】结构体篇

目录

  • 结构体的定义
  • 结构体变量的声明和初始化
    • 声明结构体变量
    • 初始化结构体变量
  • 访问结构体成员
  • 结构体数组
  • 结构体指针
  • 结构体嵌套
  • 结构体作为函数参数
    • 值传递
    • 指针传递
  • 结构体的内存对齐
  • 位域

结构体的定义

结构体是一种自定义的数据类型,它把不同类型的数据组合成一个整体,方便管理和操作相关的数据。在定义结构体时,使用 struct 关键字,后面跟着结构体的名称,再用花括号 {} 包含结构体的成员列表,每个成员由数据类型和成员名组成,成员之间用分号 ; 分隔。

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

例如,定义一个描述图书信息的结构体:

struct Book {
    char title[100];  // 书名,用字符数组存储
    char author[50];  // 作者,用字符数组存储
    int year;         // 出版年份,用整数存储
    float price;      // 价格,用浮点数存储
};

注意事项和细节

  • 结构体名的命名规则:遵循标识符的命名规则,不能与关键字重名,尽量使用有意义的名称,以提高代码的可读性。
  • 结构体定义结束的分号:结构体定义的末尾必须有分号 ;,这是 C 语言语法的要求。
  • 结构体只是一种类型定义:定义结构体本身并不分配内存,只是告诉编译器这种新的数据类型的组成结构。

结构体变量的声明和初始化

声明结构体变量

先定义结构体,再声明变量:这是最常见的方式,先定义好结构体类型,然后在需要使用的地方声明该类型的变量。

struct Book {
    char title[100];
    char author[50];
    int year;
    float price;
};
struct Book book1;  // 声明一个 Book 类型的变量 book1

在定义结构体的同时声明变量:这种方式在定义结构体的同时就声明了变量,适用于只在当前代码块使用该结构体类型的情况。

struct Movie {
    char name[80];
    int duration;
} movie1, movie2;  // 定义 Movie 结构体的同时声明了两个变量 movie1 和 movie2

使用匿名结构体声明变量:匿名结构体没有结构体名,只能在定义时声明变量,之后无法再声明该类型的其他变量,使用场景较少。

struct {
    int x;
    int y;
} point;  // 声明一个匿名结构体类型的变量 point

初始化结构体变量

按成员顺序初始化:按照结构体定义中成员的顺序,依次为每个成员赋值。

struct Book book2 = {"C Programming", "John Doe", 2020, 29.99};

指定成员名初始化(C99 及以后支持):可以不按照成员的顺序,通过指定成员名来初始化,提高代码的可读性和可维护性。

struct Book book3 = {.author = "Jane Smith", .title = "Data Structures", .year = 2021, .price = 39.99};

注意事项和细节

  • 成员顺序初始化时的匹配:按成员顺序初始化时,提供的值的类型和顺序必须与结构体成员的类型和顺序一致。
  • 部分初始化:如果只初始化部分成员,未初始化的成员将被自动初始化为 0(对于数值类型)或空字符(对于字符类型)。
struct Book book4 = {"Python Basics"};  // 只初始化了 title 成员,其他成员自动初始化为 0 或空字符
  • 匿名结构体的局限性:匿名结构体无法在其他地方再次使用,因为没有结构体名,不利于代码的复用。

访问结构体成员

使用点运算符 . 来访问结构体变量的成员。点运算符的左边是结构体变量名,右边是结构体的成员名。

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

struct Book {
    char title[100];
    char author[50];
    int year;
    float price;
};

int main() {
    struct Book book;
    strcpy(book.title, "Java Programming");  // 给 title 成员赋值
    strcpy(book.author, "Mark Johnson");     // 给 author 成员赋值
    book.year = 2019;                        // 给 year 成员赋值
    book.price = 49.99;                      // 给 price 成员赋值

    printf("Title: %s\n", book.title);
    printf("Author: %s\n", book.author);
    printf("Year: %d\n", book.year);
    printf("Price: %.2f\n", book.price);

    return 0;
}

注意事项和细节

  • 成员访问的合法性:只能访问结构体中已经定义的成员,访问不存在的成员会导致编译错误。
  • 字符数组成员的赋值:对于字符数组类型的成员,不能直接使用赋值运算符 = 进行赋值,需要使用 strcpy 等字符串处理函数。

结构体数组

结构体数组是由多个相同结构体类型的元素组成的数组。可以将多个相关的结构体变量组织在一起,方便进行批量处理。

#include <stdio.h>

struct Book {
    char title[100];
    char author[50];
    int year;
    float price;
};

int main() {
    struct Book books[3] = {
        {"C++ Primer", "Stanley Lippman", 2012, 79.99},
        {"Effective Java", "Joshua Bloch", 2018, 59.99},
        {"The Pragmatic Programmer", "Andrew Hunt", 2019, 49.99}
    };

    for (int i = 0; i < 3; i++) {
        printf("Book %d:\n", i + 1);
        printf("Title: %s\n", books[i].title);
        printf("Author: %s\n", books[i].author);
        printf("Year: %d\n", books[i].year);
        printf("Price: %.2f\n", books[i].price);
        printf("\n");
    }

    return 0;
}

注意事项和细节

  • 数组大小的确定:在声明结构体数组时,需要明确指定数组的大小,或者在初始化时由编译器根据初始化列表的元素个数自动确定。
  • 数组元素的访问:通过数组下标访问结构体数组的元素,再使用点运算符访问元素的成员。
    内存连续性:结构体数组的元素在内存中是连续存储的,这有利于提高访问效率。

结构体指针

可以定义指向结构体的指针,通过指针来访问结构体的成员。使用 -> 运算符来访问指针所指向结构体的成员,-> 运算符是 (*指针).成员 的简写形式。

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

struct Book {
    char title[100];
    char author[50];
    int year;
    float price;
};

int main() {
    struct Book book = {"JavaScript: The Definitive Guide", "David Flanagan", 2020, 69.99};
    struct Book *p = &book;  // 定义一个指向 Book 结构体的指针 p,并指向 book 变量

    printf("Title: %s\n", p->title);
    printf("Author: %s\n", p->author);
    printf("Year: %d\n", p->year);
    printf("Price: %.2f\n", p->price);

    return 0;
}

注意事项和细节

  • 指针的初始化:在使用结构体指针之前,必须先将其初始化为一个有效的结构体变量的地址,否则会导致未定义行为。
  • -> 运算符的使用:-> 运算符用于通过指针访问结构体成员,避免了使用 (*指针).成员 这种较为繁琐的写法。
  • 动态内存分配:可以使用 malloc、calloc 等函数动态分配结构体的内存,并使用指针来管理。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct Book {
    char title[100];
    char author[50];
    int year;
    float price;
};

int main() {
    struct Book *p = (struct Book *)malloc(sizeof(struct Book));
    if (p == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    strcpy(p->title, "Python Crash Course");
    strcpy(p->author, "Eric Matthes");
    p->year = 2015;
    p->price = 39.99;

    printf("Title: %s\n", p->title);
    printf("Author: %s\n", p->author);
    printf("Year: %d\n", p->year);
    printf("Price: %.2f\n", p->price);

    free(p);  // 释放动态分配的内存
    return 0;
}

结构体嵌套

结构体可以嵌套,即一个结构体的成员可以是另一个结构体类型。通过结构体嵌套,可以更复杂地组织数据。

#include <stdio.h>

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

struct Book {
    char title[100];
    char author[50];
    struct Date publishDate;  // 嵌套 Date 结构体
    float price;
};

int main() {
    struct Book book = {
        "The C Programming Language",
        "Brian Kernighan",
        {1978, 2, 22},  // 初始化嵌套的 Date 结构体
        29.99
    };

    printf("Title: %s\n", book.title);
    printf("Author: %s\n", book.author);
    printf("Publish Date: %d-%d-%d\n", book.publishDate.year, book.publishDate.month, book.publishDate.day);
    printf("Price: %.2f\n", book.price);

    return 0;
}

注意事项和细节

  • 嵌套结构体的初始化:在初始化包含嵌套结构体的结构体变量时,需要按照嵌套的层次依次初始化。
  • 成员访问的层次:访问嵌套结构体的成员时,需要使用多个点运算符,从外层结构体逐步访问到内层结构体的成员。
  • 内存布局:嵌套结构体的内存布局是按照成员的定义顺序依次分配的,嵌套结构体的成员也遵循内存对齐的规则。

结构体作为函数参数

值传递

将结构体变量的值复制一份传递给函数,函数内部对参数的修改不会影响原结构体变量。

#include <stdio.h>

struct Point {
    int x;
    int y;
};

// 函数接受一个 Point 结构体作为参数
void printPoint(struct Point p) {
    printf("Point: (%d, %d)\n", p.x, p.y);
}

int main() {
    struct Point p = {3, 4};
    printPoint(p);  // 传递结构体变量 p 的值给函数
    return 0;
}

注意事项和细节

  • 内存开销:值传递会复制整个结构体变量,当结构体较大时,会占用较多的内存和时间。
  • 数据的独立性:函数内部对参数的修改不会影响原结构体变量,保证了数据的独立性。

指针传递

将结构体变量的地址传递给函数,函数内部可以通过指针修改原结构体变量的值。

#include <stdio.h>

struct Point {
    int x;
    int y;
};

// 函数接受一个指向 Point 结构体的指针作为参数
void movePoint(struct Point *p, int dx, int dy) {
    p->x += dx;
    p->y += dy;
}

int main() {
    struct Point p = {3, 4};
    movePoint(&p, 1, 2);  // 传递结构体变量 p 的地址给函数
    printf("New Point: (%d, %d)\n", p.x, p.y);
    return 0;
}

注意事项和细节

  • 内存开销小:指针传递只传递结构体变量的地址,不复制整个结构体,内存开销小。
  • 数据的修改:函数内部可以通过指针修改原结构体变量的值,需要注意避免意外修改数据。
  • 指针的有效性:在函数内部使用指针时,需要确保指针指向的是有效的结构体变量,避免空指针引用。

结构体的内存对齐

结构体的成员在内存中并不是连续存储的,编译器会根据成员的类型和平台的要求进行内存对齐,以提高访问效率。内存对齐的规则如下:

  • 结构体的第一个成员的偏移量为 0。
  • 每个成员的偏移量必须是该成员类型大小的整数倍。
  • 结构体的总大小必须是最大成员类型大小的整数倍。
#include <stdio.h>

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

int main() {
    printf("Size of struct Example: %zu\n", sizeof(struct Example));
    return 0;
}

在这个例子中,由于内存对齐的原因,struct Example 的大小可能不是 6 字节(1 + 4 + 1),而是 12 字节。char c 从偏移量 0 开始存储,占 1 个字节;int i 由于偏移量必须是 4 的整数倍,所以从偏移量 4 开始存储,占 4 个字节;char d 从偏移量 8 开始存储,占 1 个字节;为了满足结构体总大小是最大成员类型大小(4 字节)的整数倍,结构体的总大小为 12 字节。

注意事项和细节

  • 内存浪费:内存对齐会导致一定的内存浪费,特别是当结构体中包含不同大小的成员时。
  • 平台相关性:不同的平台和编译器可能有不同的内存对齐规则,因此结构体的大小可能会有所不同。
  • 调整对齐方式:可以使用 #pragma pack 指令来调整结构体的对齐方式,但这可能会影响访问效率。

位域

位域允许在一个结构体中以位为单位来指定成员所占的存储空间,用于节省内存。

#include <stdio.h>

struct Flags {
    unsigned int flag1 : 1;  // 占 1 位
    unsigned int flag2 : 1;  // 占 1 位
    unsigned int flag3 : 1;  // 占 1 位
    unsigned int flag4 : 1;  // 占 1 位
};

int main() {
    struct Flags f;
    f.flag1 = 1;
    f.flag2 = 0;
    f.flag3 = 1;
    f.flag4 = 0;

    printf("Flag1: %d\n", f.flag1);
    printf("Flag2: %d\n", f.flag2);
    printf("Flag3: %d\n", f.flag3);
    printf("Flag4: %d\n", f.flag4);

    return 0;
}

在这个例子中,struct Flags 的四个成员 flag1、flag2、flag3 和 flag4 各占 1 位,总共只占 1 个字节的存储空间。

注意事项和细节

  • 位域的类型:位域的类型必须是 int、unsigned int 或 signed int,有些编译器也支持 char 类型。
  • 位域的宽度:位域的宽度不能超过其类型的大小,例如 unsigned int 类型的位域宽度不能超过 32 位。
  • 位域的可移植性:位域的实现可能因编译器和平台而异,因此位域的使用可能会影响代码的可移植性。
  • 未命名位域:可以使用未命名的位域来填充字节,以达到特定的对齐要求。
struct Example {
    unsigned int flag1 : 1;
    unsigned int : 3;  // 未命名位域,占 3 位
    unsigned int flag2 : 1;
};

http://www.kler.cn/a/581039.html

相关文章:

  • 联核科技AGV无人叉车能给企业带来哪些效益?
  • 【面试】JVM
  • 计算机考研C语言
  • C++设计模式-工厂模式:从原理、适用场景、使用方法,常见问题和解决方案深度解析
  • 工作记录 2017-01-04
  • 【CXX】6 内置绑定
  • Redis--Set类型
  • JVM、MySQL常见面试题(尽力局)
  • vue3中的深度选择器
  • Python----数据可视化(Seaborn合集:介绍,应用,绘图,使用FacetGrid绘图)
  • 每天一道算法题【蓝桥杯】【最长递增子序列】
  • MVCC的理解(Multi-Version Concurrency Control,多版本并发控制)
  • Spring (十)事务
  • golang从入门到做牛马:第十三篇-Go语言指针:内存的“导航仪”
  • 【day10】智慧导览:学习LBS定位精度标准
  • QwQ-32B企业级本地部署:结合XInference与Open-WebUI使用
  • PySide(PyQT),QGraphicsItem的pos()和scenePos()区别
  • 【Agent】Windows 和 CentOS 安装 Conda
  • 代理模式的C++实现示例
  • 54. 螺旋矩阵(C++)