c++ 语言教程——04结构体,共用体和枚举


今天我们要介绍的是C++ 的三种重要复合数据类型:结构体(struct)、共用体(union)和枚举(enum)

结构体

C++ 中的结构体(struct)是一种强大的工具,它允许你将多个不同类型的数据组合在一起,创建自定义的数据类型,从而更好地描述现实世界中的复杂对象

定义结构体

定义结构体需要使用 struct关键字,后跟结构体名称和一对花括号 {},括号内是成员变量的列表。定义末尾的分号必不可少​

struct Student {
    std::string name;  // 姓名
    int age;           // 年龄
    double score;      // 分数
}; // 注意这里的分号

创建结构体变量

定义好结构体类型后,就可以像使用基本类型(如 int)一样来创建变量了。在C++中,创建结构体变量时,struct关键字通常可以省略 。

主要有三种创建方式:

1.先定义后声明:Student stu1;

2.声明时初始化:Student stu2 = {“李四”, 19, 88.5};

3.定义时直接创建(较少使用):在定义结构体的右花括号后直接写变量名

初始化与成员访问

初始化:可以使用聚合初始化(Aggregate Initialization)按成员声明的顺序直接赋值

Student stu3 = {"王五", 20, 95.0};

如果结构体嵌套了另一个结构体,可以嵌套初始化

struct Teacher {
    int id;
    std::string name;
    Student stu; // 嵌套结构体
};
Teacher t1 = {1001, "张老师", {"赵六", 18, 90}};

访问成员:使用点运算符(.)访问结构体变量的成员 ,指针使用(->)

stu1.name = "张三";
stu1.age = 18;
std::cout << stu1.name << std::endl;

结构体的高级用法

结构体指针与箭头运算符
当使用指向结构体的指针时,访问成员需要使用箭头运算符(->),它等价于先对指针解引用(*)再使用点运算符(.)

Student s = {"小明", 17, 85};
Student *p = &s; // p 是指向结构体 s 的指针
std::cout << p->name << std::endl; // 使用 -> 访问成员
// 等价于 (*p).name

结构体数组
可以创建结构体类型的数组,用于管理多个相同的结构体对象

Student class1[3] = {
    {"学生A", 18, 90},
    {"学生B", 19, 85},
    {"学生C", 20, 92}
};
// 访问数组第一个元素的name成员
std::cout << class1[0].name << std::endl;

结构体作为函数参数
将结构体传递给函数时,有两种主要方式:

值传递:函数接收结构体的一份副本,在函数内修改成员不会影响原始变量

void printStudent(Student stu) {
    stu.age = 100; // 只修改副本
    std::cout << stu.name << std::endl;
}

地址传递(指针):函数接收结构体的地址(指针),在函数内通过指针修改成员会影响原始变量,效率更高,尤其适用于大型结构体

void setStudentScore(Student *pStu, double newScore) {
    pStu->score = newScore; // 修改会影响原始变量
}

如果希望地址传递方式不修改原始结构体的数据,可以使用 const修饰指针参数,这是一种良好的编程习惯

void displayStudent(const Student *pStu) {
    // pStu->age = 100; // 错误!const 禁止修改
    std::cout << pStu->name << std::endl;
}

在结构体中定义函数(成员函数)

C++中的结构体不仅可以包含数据成员,还可以包含函数成员(成员函数),这使得结构体具备了类似“类”的行为,能够将数据和操作数据的行为封装在一起

成员函数:在结构体内部定义的函数,可以直接操作结构体的成员变量

struct Point {
    int x;
    int y;
    // 成员函数:打印坐标
    void print() {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
    // 成员函数:移动点
    void move(int dx, int dy) {
        x += dx;
        y += dy;
    }
};
Point p1 = {10, 20};
p1.print(); // 输出 (10, 20)
p1.move(5, -3);
p1.print(); // 输出 (15, 17)

构造函数:是一种特殊的成员函数,在创建结构体对象时自动调用,用于初始化对象。其名称与结构体名相同,且没有返回类型

struct Point {
    int x;
    int y;
    // 默认构造函数
    Point() : x(0), y(0) {} // 成员初始化列表
    // 带参数的构造函数
    Point(int x_val, int y_val) : x(x_val), y(y_val) {}
};
Point p1;       // 调用默认构造函数,p1为(0,0)
Point p2(3, 4); // 调用带参构造函数,p2为(3,4)

成员函数也可以在结构体内部声明,在外部定义,此时需要使用作用域解析运算符 ::

struct Point {
    int x, y;
    void print(); // 声明
};
// 在外部定义
void Point::print() {
    std::cout << x << ", " << y << std::endl;
}

结构体与类的区别

在C++中,struct和 class最关键的区别在于默认的访问控制权限​ :
struct​ 的成员(包括数据和函数)默认是 public(公有的)。
class​ 的成员默认是 private(私有的)。
除了这个默认访问权限的区别,它们在其他功能上几乎完全相同,都可以包含数据成员、成员函数、构造函数/析构函数,支持继承和多态等 。使用中,当需要的数据结构主要用来封装一组公共数据,行为比较简单时,使用 struct;当需要实现更复杂的对象,强调数据封装和信息隐藏时,使用 class

结构体的内存布局

内存对齐(Padding):编译器为了提升CPU访问内存的效率,可能会在结构体的成员之间插入一些空白字节(填充),确保每个成员都从其类型大小整数倍的地址开始 。例如:

struct Example {
    char a;      // 1字节
    // 编译器可能在此处插入3字节填充(假设int为4字节)
    int b;       // 4字节
    char c;      // 1字节
    // 可能再插入填充使结构体总大小为对齐值的倍数
};

使用 sizeof运算符可以查看结构体实际占用的内存大小。直接对结构体使用 memset函数设置内存内容时需要谨慎,特别是在结构体包含指针或存在内存对齐的情况下

注意事项

1.字符数组赋值:如果结构体成员使用字符数组(如 char name[50];)而不是 std::string,在赋值时不能直接用等号(=)

2.结构体中的指针:如果结构体包含指针成员,并且该指针指向动态分配的内存,要特别小心内存管理。在释放结构体之前,应先释放指针,避免内存泄漏。另外直接对包含动态内存指针的结构体使用 memset可能导致问题

3.优先使用 std::string:在C++中,对于字符串处理,通常更推荐使用 std::string,它比C风格的字符数组更安全、更方便。

共用体

共用体是一种特殊的数据类型,它的所有数据成员共享同一段内存地址。这意味着在同一时刻,只有一个成员是有效的(即最后被赋值的那个成员)。共用体的内存大小由其最大的成员决定,并且会进行适当的内存对齐以满足所有成员的对齐要求

共用体的设计初衷是为了节省内存空间,特别适用于处理互斥的数据(即多个数据项不会同时有效的情况),或者需要对同一段内存进行不同解释的场景

共用体的声明与定义

// 1. 先声明共用体类型,再定义变量
union Data {
    int i;
    char c;
    double d;
};
Data data1, data2;

// 2. 声明类型的同时定义变量
union Data {
    int i;
    char c;
    double d;
} data3, data4;

// 3. 直接定义匿名共用体变量(较少使用)
union {
    int i;
    char c;
} data5;

共用体的核心特性与使用要点

1.内存共享与成员访问

共用体的所有成员都从相同的内存地址开始。当你给一个成员赋值后,再给另一个成员赋值,会覆盖之前的数据。因此,访问哪个成员才有意义,取决于最后一次对哪个成员进行了赋值。

访问共用体成员使用成员运算符(.),如果通过指针访问,则使用箭头运算符(->)

#include <iostream>
using namespace std;

int main() {
    Data data;
    data.i = 65; // 最后一次赋值的是整形成员 i
    // cout << data.c << endl; // 此时如果访问字符成员 c,结果是不确定的(未定义行为)

    data.c = 'A'; // 最后一次赋值的是字符成员 c
    cout << data.c << endl; // 输出 'A',访问是安全的
    // cout << data.i << endl; // 此时访问 i,结果是不确定的

    return 0;
}

2.内存大小与对齐
共用体的大小至少是其最大成员的大小,并且通常会进行内存对齐,使其大小是成员中最严格对齐要求的整数倍

union Example {
    int a;    // 通常占 4 字节
    char b;   // 占 1 字节
    double c; // 通常占 8 字节
};
cout << sizeof(Example) << endl; // 输出可能是 8(取决于double的对齐要求)

3.匿名共用体
匿名共用体没有名称,其成员可以直接在定义它的作用域中被访问,就像普通变量一样。它通常用于结构体或类内部,使一组互斥的成员共享内存

#include <iostream>
using namespace std;

struct Widget {
    int type;
    union { // 匿名共用体
        int number;
        char character;
    }; // 注意没有变量名
};

int main() {
    Widget w;
    w.type = 1;
    w.number = 100; // 直接访问匿名共用体的成员
    // w.character = 'A'; // 与 w.number 互斥

    return 0;
}

重要注意事项与局限性

在C++中,如果共用体包含非POD(Plain Old Data)类型的成员(例如 std::string或具有自定义构造/析构函数的类对象),编译器不会自动为共用体生成默认的构造函数、拷贝构造函数、析构函数和拷贝赋值运算符。你需要手动管理这些非POD成员的构造和析构,这会非常复杂且容易出错。通常建议在共用体中只使用POD类型

由于共用体,特别是包含非平凡类型的共用体,在使用上存在诸多陷阱,现代C++(C++17及以上)提供了更安全、易用的替代品:
std::variant:这是一个类型安全的联合体,可以存储指定类型集合中的一种类型。它自动处理了类型的构造、析构和赋值,大大降低了出错风险。它是现代C++中替代共用体的首选。
std::any:可以存储任意类型的值,但在使用时需要知道其具体类型后才能转换

枚举

枚举的核心目的是用有意义的符号名称来代替“魔法数字”(magic numbers)。通过 enum关键字,我们可以定义一种新的数据类型,其变量的取值被限制在一组预定义的符号常量中

1.定义与默认值

 enum Weekday {
    Monday,    // 值为 0
    Tuesday,   // 值为 1
    Wednesday, // 值为 2
    Thursday,  // 值为 3
    Friday     // 值为 4
};

2.自定义枚举值
你可以为枚举常量显式地赋予特定的整数值。未被初始化的枚举值将比前一个值大1

enum Status {
    Success = 200,
    NotFound = 404,
    ServerError = 500,
    UnknownError // 编译器会自动将其设为 501
};

3.声明与使用枚举变量

Weekday today = Tuesday;
Status response = NotFound;

if (today == Tuesday) {
    // 处理星期二的事务
}

枚举变量通常只能被赋予其类型定义的枚举值。直接赋整数值需要强制类型转换,但反之,枚举值可以参与整型运算,因为它们本质上是整型常量

C++11 强类型枚举 (enum class)

传统的C++枚举存在作用域污染(枚举常量直接暴露在外层作用域,容易引发命名冲突)和隐式类型转换(枚举值可以自动转换为整型,可能引发意外比较)的问题 。C++11引入了枚举类(enum class,也称为强类型枚举)来解决这些问题

// 使用 enum class 定义
enum class Color { Red, Green, Blue };
enum class TrafficLight { Red, Yellow, Green }; // 不会与 Color 的 Red 冲突

Color wallColor = Color::Red; // 必须通过作用域运算符访问
TrafficLight light = TrafficLight::Red; // 即使常量名相同,也因为作用域不同而不会冲突

// int colorCode = Color::Red; // 错误!不能隐式转换为 int
int colorCode = static_cast<int>(Color::Red); // 正确,必须显式转换 [1,9]

枚举常量必须通过枚举类型名加作用域解析运算符(::)访问,避免了命名污染和冲突
不会隐式转换为整型,防止了意外的类型转换和比较,使代码更安全

指定底层类型
无论是传统 enum还是 enum class,从C++11开始,你都可以显式指定枚举的底层数据类型,这有助于控制内存占用或确保与外部系统(如硬件寄存器、网络协议)的兼容性

enum class Priority : unsigned char {
    Low = 1,
    Medium = 5,
    High = 10
}; // 此枚举变量将只占用1字节内存

// 查看大小
std::cout << sizeof(Priority::Medium) << std::endl; // 通常输出 1

枚举的常见应用场景

状态机与状态标识:如表示网络连接状态

enum class ConnectionState { Disconnected, Connecting, Connected };

选项与配置:如定义日志级别

enum class LogLevel { Debug, Info, Warning, Error };

错误码定义:使用枚举可以集中管理错误码,使错误处理逻辑更清晰

enum class ErrorCode {
    Success = 0,
    FileNotFound = 2,
    PermissionDenied = 5
};

结合位运算表示标志位 (Flags):通过为枚举值赋予2的幂次方,可以用按位或(|)操作组合多个标志

enum class Permissions {
    Read = 1 << 0,   // 1 (二进制001)
    Write = 1 << 1,  // 2 (二进制010)
    Execute = 1 << 2 // 4 (二进制100)
};

Permissions myRights = Permissions::Read | Permissions::Write; // 组合权限
if (static_cast<int>(myRights) & static_cast<int>(Permissions::Read)) {
    // 检查是否具有读权限
}

重要注意事项与最佳实践

1.枚举的取值范围:枚举变量实际上可以持有在其定义的“取值范围”内的任何整数值,即使该值不是显式列出的枚举常量。这个范围基于定义的枚举常量最大值和最小值计算 。使用强制转换赋予一个在取值范围内但非枚举常量的值是合法的,但结果可能不符合预期,需要谨慎 。

2.优先使用 enum class:在现代C++项目中,除非需要与C语言代码交互,否则应优先使用 enum class,因为它提供了更好的作用域控制和类型安全性 。

3.匿名枚举:可以定义不指定类型名的枚举,此时其作用相当于定义一组作用域受限的整型常量 。

enum { BufferSize = 1024, MaxUsers = 100 };

4.与 switch语句配合:枚举类型与 switch语句是天作之合,编译器通常能检查是否覆盖了所有枚举值,有助于写出更安全的代码

Color c = Color::Green;
switch (c) {
    case Color::Red: /* ... */ break;
    case Color::Green: /* ... */ break;
    case Color::Blue: /* ... */ break;
    // 如果没有default,且所有枚举值已处理,编译器可能不会警告
}

C++枚举是一个强大的工具,它能有效替代魔法数字,提升代码的清晰度和安全性。关键在于理解传统 enum和现代 enum class的区别与适用场景

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值