一、结构体:用户自定义类型的 “组装器”

结构体的本质是将多个基础数据类型(如 int、char、float 等)或其他自定义类型 “组装” 在一起,形成一个新的复合类型。其定义语法如下:

struct 类型名
{
    数据类型1 变量1;
    数据类型2 变量2;
    // ... 更多成员
};

示例:日期与人员信息的结构体定义

// 日期结构体:包含年、月、日
struct date
{
    int year;
    int month;
    int day;
};

// 人员结构体:包含姓名、年龄、分数
struct Person
{
    char name[50];
    int age;
    float score;
};

结构体的定义也支持嵌套,比如我们可以把struct date嵌套到struct Person中,用来表示人员的生日:

struct PersonWithBirth
{
    char name[50];
    int age;
    float score;
    struct date birth; // 嵌套日期结构体
};

二、结构体的初始化:从完全到部分

定义结构体后,我们需要对其变量进行初始化,C 语言提供了完全初始化部分初始化两种方式。

1. 完全初始化

将结构体的所有成员逐一赋值,示例:

struct Person per = {"zhangsan", 20, 90.5};

// 嵌套结构体的完全初始化
struct PersonWithBirth per_birth = {"lisi", 22, 88.0, {2002, 5, 18}};

2. 部分初始化

未初始化的成员会被自动填充为 0,还可以通过指定成员名的方式灵活赋值:

struct Person per2 = 
{
    .name = "wangwu",
    .age = 18,
    // score未初始化,自动为0
};

// 嵌套结构体的部分初始化
struct PersonWithBirth per_birth2 = 
{
    .name = "zhaoliu",
    .birth.year = 2001,
    .birth.month = 8
    // age、score、birth.day自动为0
};

三、结构体的成员访问:.->的分工

访问结构体成员时,需根据变量是结构体变量还是结构体指针,选择不同的运算符。

1. 结构体变量:用.运算符

printf("姓名:%s,年龄:%d,分数:%f\n", per.name, per.age, per.score);

// 访问嵌套结构体成员
printf("姓名:%s,生日:%d年%d月%d日\n", 
       per_birth.name, 
       per_birth.birth.year, 
       per_birth.birth.month, 
       per_birth.birth.day);

2. 结构体指针:用->运算符

struct Person *point = &per;
printf("姓名:%s,年龄:%d,分数:%f\n", point->name, point->age, point->score);

// 访问嵌套结构体指针的成员
struct PersonWithBirth *p_birth = &per_birth;
printf("姓名:%s,生日:%d年%d月%d日\n", 
       p_birth->name, 
       p_birth->birth.year, 
       p_birth->birth.month, 
       p_birth->birth.day);

注意:per.score 和 point->score 都是表达式,其类型与成员变量(如 float)的类型一致,值为成员变量的具体值。

四、结构体字节对齐:为 CPU 高效读写 “铺路”

为了让 CPU 能高效地读写内存,结构体存在字节对齐规则,这是理解结构体大小的关键。

规则 1:成员变量的地址必须是 “自身类型大小的整数倍”

例如:

struct per2
{
    char a;    // char占1字节,地址需是1的整数倍(任意地址都满足)
    short b;   // short占2字节,地址需是2的整数倍,a后填充1字节
    char c;    // char占1字节,b后无填充
    int d;     // int占4字节,c后填充3字节
};

其内存布局因对齐规则会产生 “填充字节”,最终大小为12 字节(可结合内存示意图理解)。

规则 2:结构体最终大小必须是 “最大成员基础类型大小的整数倍”

例如:

struct per3
{
    double a;  // double占8字节
    char c;    // char占1字节,a后无填充
    short s;   // short占2字节,c后填充1字节
};

最大成员是double(8 字节),因此结构体最终大小需是 8 的整数倍,实际为16 字节(c 和 s 共占 4 字节,后填充 4 字节)。

调整字节对齐(编译器指令)

我们可以通过编译器指令(如#pragma pack)调整对齐规则,例如:

#pragma pack(1) // 设置按1字节对齐
struct per4
{
    char a;
    int b;
};
#pragma pack() // 恢复默认对齐

此时struct per4的大小为5 字节(无填充),但可能降低 CPU 读写效率。

五、结构体数组与传参:效率优先的实践

当处理多个结构体对象时,我们会用到结构体数组;而结构体传参时,为了效率通常选择地址传递

示例:结构体数组定义与传参

struct PER
{
    char name[50];
    float height;
};

// 结构体数组传参:使用地址传递(避免拷贝,提升效率)
void show_array(struct PER *per, int len)
{
    for (int i = 0; i < len; i++)
    {
        printf("第%d人:姓名%s,身高%.2f\n", i+1, per[i].name, per[i].height);
    }
}

// 修改结构体数组元素
void update_height(struct PER *per, int index, float new_height)
{
    per[index].height = new_height;
}

int main()
{
    // 定义并初始化结构体数组
    struct PER per[3] = {
        {"zhangsan", 1.75},
        {"lisi", 1.70},
        {"wangmazi", 1.95}
    };

    // 直接访问数组元素
    printf("初始化数据:\n");
    for (int i = 0; i < 3; i++)
    {
        printf("第%d人:姓名%s,身高%.2f\n", i+1, per[i].name, per[i].height);
    }

    // 修改数组元素
    update_height(per, 1, 1.78);

    // 函数传参(地址传递)
    printf("\n修改后数据:\n");
    show_array(per, 3);
    return 0;
}

六、结构体与指针的进阶应用:动态内存分配

结构体结合动态内存分配,可灵活管理数据,例如:

struct Student
{
    char *name; // 动态分配字符串
    int id;
};

int main()
{
    struct Student *stu = (struct Student *)malloc(sizeof(struct Student));
    if (stu == NULL) exit(1); // 检查分配是否成功

    stu->name = (char *)malloc(20 * sizeof(char));
    strcpy(stu->name, "sunqi");
    stu->id = 2024001;

    printf("学生:%s,学号:%d\n", stu->name, stu->id);

    // 释放内存
    free(stu->name);
    free(stu);
    stu = NULL; // 避免野指针
    return 0;
}

七、memcpy 与 memset:结构体的内存操作利器

在处理结构体时,我们经常需要对内存进行批量复制或初始化,memcpy(内存拷贝)和memset(内存填充)是高效的内存操作函数,能简化结构体的赋值与初始化流程。

1. memset:批量初始化内存

memset用于将某一块内存的内容全部设置为指定的值(通常是 0),语法为:

void *memset(void *ptr, int value, size_t num);
  • ptr:要操作的内存起始地址
  • value:填充的字节值(通常用 0 初始化)
  • num:要填充的字节数

结构体初始化场景:当结构体成员较多时,用memset可快速将所有成员置 0,避免逐个赋值:

struct Person per;
// 将per的所有字节初始化为0(char数组、int、float均置0)
memset(&per, 0, sizeof(struct Person));

注意:memset按字节填充,若用于非字符类型(如 int),仅适合赋值 0 或 0xFF(全 1),否则可能出现意外结果(如用 1 填充 int 会变成0x01010101)。

2. memcpy:批量拷贝内存

memcpy用于将一块内存的内容拷贝到另一块内存,语法为:

void *memcpy(void *dest, const void *src, size_t num);
  • dest:目标内存地址
  • src:源内存地址
  • num:要拷贝的字节数

结构体拷贝场景:直接赋值结构体时(如per2 = per1),本质是浅拷贝;用memcpy可实现相同效果,尤其适合需要精确控制拷贝范围的场景:

struct Person per1 = {"zhangsan", 20, 90.5};
struct Person per2;
// 将per1的内容完整拷贝到per2
memcpy(&per2, &per1, sizeof(struct Person));

结构体数组拷贝:批量拷贝结构体数组时,memcpy比循环赋值更高效:

struct PER per[3] = {{"zhangsan", 1.75}, {"lisi", 1.70}, {"wangmazi", 1.95}};
struct PER per_copy[3];
// 拷贝整个结构体数组
memcpy(per_copy, per, sizeof(per));

注意事项

  • memcpy是浅拷贝:若结构体包含指针(如char *name),仅拷贝指针地址,而非指针指向的内容,可能导致内存重复释放或野指针问题。此时需手动实现深拷贝(先拷贝指针指向的数据,再拷贝指针)。
  • 确保目标内存足够大,避免越界访问。
3. 结构体深拷贝示例(结合 memcpy 与动态内存)

若结构体含动态分配的指针成员,需用memcpy拷贝指针指向的数据:

struct Student stu1;
stu1.name = (char *)malloc(20);
strcpy(stu1.name, "sunqi");
stu1.id = 2024001;

struct Student stu2;
// 深拷贝:先分配内存,再拷贝内容
stu2.name = (char *)malloc(strlen(stu1.name) + 1);
memcpy(stu2.name, stu1.name, strlen(stu1.name) + 1); // 含'\0'
stu2.id = stu1.id;

八、memcpy/memset 的优势与适用场景

  • 效率:两者均为库函数优化实现(通常是汇编级),比手动循环赋值更快,尤其适合大数据量的结构体 / 数组操作。
  • 场景
    • memset:结构体初始化、清空内存(如重置状态)。
    • memcpy:结构体 / 数组的批量拷贝、内存块迁移。

总结

结构体是 C 语言中实现 “复合数据类型” 的核心工具,它让我们能按需组装数据,支持嵌套、动态内存分配等进阶操作。掌握类型定义、初始化、成员访问、字节对齐数组传参这些知识点,就能灵活运用结构体解决复杂的编程问题,为后续学习链表、队列等高级数据结构打下坚实基础。结构体结合memcpymemset,能更高效地管理内存数据。需注意浅拷贝与深拷贝的区别,避免因指针成员导致的内存问题。掌握这些工具,可进一步提升结构体操作的灵活性与性能,为复杂数据处理(如网络数据传输、文件解析)提供支撑。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐