00000004_复合数据类型

复合数据类型

5. 结构体与联合体、枚举 、位域

基础数据类型或者是数组,通常不能满足全部的开发需求,因为数据的存储结构并不完全单一,于是就需要使用自定义数据类型了,或者说复合数据类型。

结构体(Struct)

结构体用于将多个相关变量组合成一个单独的实体。

定义结构体与结构体变量
  1. 定义结构体:使用 struct 关键字可以定义一个新的数据类型。结构体可以包含不同类型的变量
#include <stdio.h>

// 定义结构体          'struct Student' 是类型,  'Student' 是标签名
struct Student {
    char name[50];
    int age;
    float grade;
};

int main() {
    // 定义结构体变量并初始化
    struct Student student1 = {"Alice", 20, 88.5};

    // 访问结构体成员
    printf("Name: %s\n", student1.name);
    printf("Age: %d\n", student1.age);
    printf("Grade: %.2f\n", student1.grade);

    return 0;
}
  1. 简化结构体类型名:使用 typedef 可以为结构体起一个别名,减少书写。
#include <stdio.h>

typedef struct {     // 这里省略了标签名,如果没有typedef,就是一个匿名结构体类型
    char name[50];
    int age;
    float grade;
} Student; 

int main() {
    Student student1 = {"Alice", 20, 88.5};
    printf("Name: %s\n", student1.name);

    return 0;
}
嵌套结构体与指向结构体的指针
  1. 嵌套结构体:一个结构体可以作为另一个结构体的成员,称为嵌套结构体。
#include <stdio.h>

// 嵌套结构体
struct Address {
    char city[50];
    char state[50];
};

struct Person {
    char name[50];
    struct Address address;  // 嵌套结构体成员
};

int main() {
    struct Person person = {"Bob", {"New York", "NY"}};

    printf("Name: %s\n", person.name);
    printf("City: %s\n", person.address.city);
    printf("State: %s\n", person.address.state);

    return 0;
}
  1. 指向结构体的指针:使用结构体指针可以动态访问结构体成员。
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main() {
    struct Point p = {10, 20};
    struct Point *ptr = &p;  // 定义指针指向结构体

    // 使用 -> 操作符访问成员
    printf("x: %d, y: %d\n", ptr->x, ptr->y);

    return 0;
}
结构体作为函数参数
  1. 按值传递:结构体可以作为参数按值传递,但会复制结构体的全部内容。
#include <stdio.h>

struct Rectangle {
    int length;
    int width;
};

// 按值传递结构体
void printArea(struct Rectangle r) {
    printf("Area: %d\n", r.length * r.width);
}

int main() {
    struct Rectangle rect = {10, 5};
    printArea(rect);  // 传递整个结构体

    return 0;
}
  1. 按引用传递:使用指针传递结构体避免拷贝,提升效率。
#include <stdio.h>

struct Rectangle {
    int length;
    int width;
};

// 按引用传递结构体
void printArea(struct Rectangle *r) {
    printf("Area: %d\n", r->length * r->width);
}

int main() {
    struct Rectangle rect = {10, 5};
    printArea(&rect);  // 传递结构体地址

    return 0;
}

关于结构体的定义和初始化,小结后面继续说明。

联合体(Union)

联合体是特殊的结构体,其所有成员共享同一块内存。

联合体与结构体的区别
  1. 定义与访问:
#include <stdio.h>

// 定义联合体
union Data {
    int i;
    float f;
    char str[20];
};

int main() {
    union Data data;

    data.i = 10;
    printf("i: %d\n", data.i);

    data.f = 3.14;  // 此时覆盖了 data.i 的值
    printf("f: %.2f\n", data.f);

    return 0;
}
  1. 区别:
    • 结构体: 各成员有独立的内存,大小为所有成员内存之和。
    • 联合体: 所有成员共享内存,大小为最大成员的内存大小。
联合体的内存管理与使用场景
  1. 内存管理:联合体节省内存,其大小等于最大成员的大小。适用于同一时间只使用一个成员的场景。
  2. 使用场景:
    • 类型转换
    • 网络协议解析
    • 嵌入式开发
枚举(Enum)

枚举是一组相关常量的集合。

定义枚举与使用枚举常量
#include <stdio.h>

// 定义枚举
enum Color {
    RED = 0,
    GREEN = 1,
    BLUE = 2
};

int main() {
    enum Color color = GREEN;
    printf("Color: %d\n", color);  // 输出 1

    return 0;
}
与整数的转换
#include <stdio.h>

enum Days {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};

int main() {
    enum Days today = FRI;
    printf("Today is day %d\n", today);  // 输出 5

    int day = 3;  // 转换为枚举类型
    today = (enum Days)day;
    printf("Converted day: %d\n", today);

    return 0;
}
位域(Bit-field)

位域允许更紧凑地存储布尔值或小范围的整数。

位域的定义与应用
#include <stdio.h>

struct Flags {
    unsigned int isAvailable : 1;  // 占 1 位
    unsigned int isEnabled : 1;
    unsigned int reserved : 6;     // 占 6 位
};

int main() {
    struct Flags flags = {1, 0, 0};

    printf("isAvailable: %u\n", flags.isAvailable);  // 输出 1
    printf("isEnabled: %u\n", flags.isEnabled);      // 输出 0

    return 0;
}
位域的存储与对齐问题
  1. 存储:位域通常以 unsigned int 的大小分配内存。
  2. 对齐:位域的起始位置可能受编译器对齐规则影响。

示例:

#include <stdio.h>

struct Flags {
    unsigned int a : 3;
    unsigned int b : 5;
    unsigned int c : 8;
};

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

说明:不同编译器可能输出不同的大小,具体取决于内存对齐。

注意:

  1. 位域的类型:每个位域成员必须有一个基础数据类型,通常是 intunsigned intcharunsigned char 等。位域的长度是相对于这个基础类型来定义的,也可以使用位更长的基础数据类型 如 longunsigned long
  2. 位域长度:位域的长度是指一个成员占用的比特数。可以指定任意长度的比特数,但必须在该类型的位数范围内, 即 (定义位长之和 <= 最大类型位长)。例如,对于 unsigned int 类型,位域长度可以从 1 位到 32 位不等(在 32 位系统上)。
示例:位域长度和基础类型的关系

示例 1:基础类型为 unsigned int(32 位系统)

#include <stdio.h>

struct Example {
    unsigned int a : 10;  // 10 位
    unsigned int b : 15;  // 15 位
    unsigned int c : 7;   // 7 位
    // 如果没有变量名,则视作填充位
    // 总共 10 + 15 + 7 = 32 位
};

int main() {
    struct Example ex;
    ex.a = 1023;  // 最大 10 位数值
    ex.b = 32767; // 最大 15 位数值
    ex.c = 127;   // 最大 7 位数值
    printf("a = %u, b = %u, c = %u\n", ex.a, ex.b, ex.c);
    return 0;
}

在这个例子中,位域的总长度为 32 位,正好等于 unsigned int 类型在 32 位系统上的位数。每个位域成员的位数不能超过 32 位(即 unsigned int 类型的位数)。

示例 2:基础类型为 unsigned char(8 位)

cCopy Code#include <stdio.h>

struct Example {
    unsigned char a : 5;  // 5 位
    unsigned char b : 3;  // 3 位
    // 总共 5 + 3 = 8 位
};

int main() {
    struct Example ex;
    ex.a = 31;  // 最大 5 位数值
    ex.b = 7;   // 最大 3 位数值
    printf("a = %u, b = %u\n", ex.a, ex.b);
    return 0;
}

这里,位域总长度为 8 位,正好等于 unsigned char 类型的位数。

注意:虽然指针可以访问和修改位域成员,但不建议进行指针算术运算,如通过偏移量访问位域。因为位域是按位分配内存的,但指针运算通常是按字节来进行的。位域的布局可能并不严格遵循字节对字节的顺序,特别是跨字节边界时,指针运算可能会导致不可预期的结果。

小结
  • 结构体: 用于表示复杂数据结构,支持嵌套和指针访问。
  • 联合体: 节省内存,适合场景如类型转换和嵌入式开发。
  • 枚举: 定义一组常量,便于代码可读性。
  • 位域: 紧凑存储标志位或小整数,适合受限存储的场景。
补充:

熟练掌握结构体的定义、初始化、成员访问、传递等操作,是进行高效C开发的基础。

一、结构体的定义

首先,结构体的定义是一个前提。结构体可以用 struct 关键字定义,格式如下:

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

实际开发中,使用的自定义结构类型往往比较复杂,好的结构应贴合数据的需求。

二、结构体的初始化

结构体的初始化有多种方式,可以在声明时进行初始化,也可以在程序运行过程中进行动态初始化。

  1. 静态初始化(在定义时初始化)

当你定义结构体变量时,可以直接给它们初始化值。这是最常用的初始化方式。

方式 1:使用大括号进行初始化

可以直接在结构体变量声明时,使用大括号 {} 来初始化它的成员。

#include <stdio.h>

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

int main() {
    struct Person person1 = {"Alice", 30, 5.6};  // 静态初始化
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);
    printf("Height: %.2f\n", person1.height);
    return 0;
}

这里,person1 被初始化为 name="Alice"age=30height=5.6。这种初始化方式必须按照结构体成员的声明顺序来赋值。

方式 2:使用指定成员的初始化(命名初始化)

C99 标准引入了一种方式,你可以通过成员名称来初始化结构体的特定成员,这样可以避免按顺序初始化时出现错误。

#include <stdio.h>

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

int main() {
    struct Person person1 = {.name = "Bob", .age = 25, .height = 5.9};
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);
    printf("Height: %.2f\n", person1.height);
    return 0;
}

在这个例子中,通过指定成员名来初始化 person1,你可以以任何顺序初始化结构体的成员。

  1. 部分初始化

如果在初始化时没有为所有成员提供初始值,未初始化的成员将会被默认赋值为零(对于基本类型来说是 0,对于字符数组是空字符 \0)。

#include <stdio.h>

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

int main() {
    struct Person person1 = {"Charlie"};  // 只初始化 name,age 和 height 会被置为 0
    printf("Name: %s\n", person1.name);   // Charlie
    printf("Age: %d\n", person1.age);     // 0
    printf("Height: %.2f\n", person1.height);  // 0.00
    return 0;
}

此时,name 被初始化为 "Charlie"ageheight 都会被自动初始化为 0。

三、结构体的赋值

结构体变量的赋值有时可能会引起困惑,特别是结构体之间的赋值。我们可以将一个结构体的值赋给另一个同类型的结构体变量。赋值时,结构体成员会逐个赋值。

  1. 通过 = 操作符赋值

可以直接使用 = 操作符将一个结构体的值赋给另一个结构体。这种赋值会将源结构体的每个成员复制到目标结构体中。

#include <stdio.h>

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

int main() {
    struct Person person1 = {"Alice", 30, 5.6};
    struct Person person2;
    
    // 使用 = 操作符进行赋值
    person2 = person1;

    // 输出 person2 的内容
    printf("Name: %s\n", person2.name);
    printf("Age: %d\n", person2.age);
    printf("Height: %.2f\n", person2.height);
    
    return 0;
}

在这个例子中,person1 的值被赋给了 person2,所以 person2nameageheight 都是 person1 的值。

  1. 结构体成员单独赋值

如果你只想修改结构体中的某个成员,可以直接通过成员名来进行赋值。

#include <stdio.h>

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

int main() {
    struct Person person1 = {"Alice", 30, 5.6};
    
    // 只修改 person1 的某些成员
    person1.age = 31;  // 修改 age
    person1.height = 5.7;  // 修改 height
    
    printf("Name: %s\n", person1.name);
    printf("Age: %d\n", person1.age);  // 31
    printf("Height: %.2f\n", person1.height);  // 5.7

    return 0;
}

在这个例子中,我们直接修改了 person1 中的 ageheight 成员。

  1. 结构体的指针赋值

你也可以通过指针来访问和赋值结构体的内容。此时,你需要使用 -> 操作符来访问结构体的成员。

#include <stdio.h>

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

int main() {
    struct Person person1 = {"Alice", 30, 5.6};
    struct Person *personPtr = &person1;

    // 通过指针修改结构体成员
    strcpy(personPtr->name, "licx"); // 将字符串 "licx" 复制到 personPtr->name
    personPtr->age = 35;  // 使用指针修改 age
    personPtr->height = 5.9;  // 使用指针修改 height
    
    printf("Name: %s\n", person1.name); // licx
    printf("Age: %d\n", person1.age);  // 35
    printf("Height: %.2f\n", person1.height);  // 5.9

    return 0;
}
四、注意
  • 结构体赋值是逐个成员的复制,对于包含指针成员的结构体,赋值操作仅复制指针的地址,导致多个指针指向同一内存区域,这种行为称为浅拷贝。浅拷贝可能导致修改或释放内存时出现问题。深拷贝则会复制指针所指向的内存内容,通常需要手动编写代码实现。

  • 结构体成员的初始化顺序:结构体的成员会按照声明的顺序进行初始化和赋值,因此要确保初始化顺序正确。

  • 内存对齐:结构体的大小可能会受到内存对齐的影响,因此有时即使结构体成员看似占用较小的内存,实际的内存大小可能会有所不同。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值