文章目录
今天我们要介绍的是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的区别与适用场景

441

被折叠的 条评论
为什么被折叠?



