C++ 结构体详解

与联合体、类的对比与应用

1. struct 基本定义

在 C++ 中,struct(结构体)是一种用户自定义的数据类型,它允许我们将不同类型的数据项(成员)组合成一个单一的实体。默认情况下,结构体的所有成员和继承都是公有的(public)。

这使得结构体特别适合用于组织相关的数据,例如一个点的坐标、一个学生的信息等。

结构体示例:Point

// 定义一个表示二维点的结构体
struct Point {
    int x;  // x 坐标 (通常占 4 字节)
    int y;  // y 坐标 (通常占 4 字节)
}; // 注意:定义末尾需要分号

// 使用结构体
Point p1; // 声明一个 Point 类型的变量 p1
p1.x = 10; // 访问并设置成员 x
p1.y = 20; // 访问并设置成员 y

内存布局演示 (假设 int 占 4 字节)

基地址+0
?
Point::x
基地址+4
?
Point::y

对象尚未实例化

2. struct vs union

structunion(联合体)都允许组合不同类型的成员,但它们在内存管理上截然不同:

  • struct:为其所有成员分配足够的、独立的内存空间。结构体的大小至少是其所有成员大小的总和(可能因内存对齐而更大)。
  • union:所有成员共享同一块内存区域。联合体的大小由其最大的成员决定。在任何时候,只有一个成员可以被有效存储和访问。

union 常用于节省内存(当不同类型数据不会同时使用时)或实现类型“双关”(type punning,但不推荐)。

内存对比演示: `struct S { int i; float f; };` vs `union U { int i; float f; };`

(假设 int 和 float 都占 4 字节)

结构体 `S` 内存 (大小 ≥ 8 字节)

基地址+0
?
S::i
基地址+4
?
S::f

联合体 `U` 内存 (大小 = 4 字节)

基地址+0
?
U::i / U::f

(共享内存区域)

3. struct vs class

在 C++ 中,structclass 功能上几乎等价,都可以包含数据成员、成员函数、构造函数、析构函数、继承等。最主要的区别在于**默认访问权限**和**默认继承权限**:

特性 struct class
默认成员访问权限 public (公有) private (私有)
默认继承权限 public (公有继承) private (私有继承)
常用语义 主要用于封装**数据**,成员默认可访问。适合 POD (Plain Old Data) 类型或简单的数据聚合体。 主要用于封装**行为和数据**,强调信息隐藏和封装,成员默认不可直接访问。

默认访问权限测试

struct S { int data; };

struct S {
    int data; // 默认 public
};

S myStruct;
myStruct.data = 10; // 尝试从外部访问
外部访问测试: 未执行

class C { int data; };

class C {
    int data; // 默认 private
    // public: // 需要显式声明 public 区域
    //  void setData(int d) { data = d; }
};

C myClass;
// myClass.data = 20; // 尝试从外部访问
外部访问测试: 未执行

实践建议:虽然技术上可以混用,但通常遵循语义约定:使用 struct 表示主要关心数据集合本身,使用 class 表示需要封装、维护不变量、提供接口的对象。

4. struct 高级用法

C++ 的 struct 非常灵活,远不止包含简单数据成员:

嵌套结构体

一个结构体可以包含另一个结构体作为其成员。

struct Address {
    std::string city;
    std::string street;
};

struct Person {
    std::string name;
    int age;
    Address addr; // 嵌套 Address 结构体
};
Person `p`
name: "张三"
age: 25
addr: (Address 对象)
Address `p.addr`
city: "北京"
street: "朝阳路 1 号"

带方法的结构体

struct 可以像 class 一样包含成员函数(方法)。

struct Rectangle {
    int width = 0; // C++11 起支持成员初始化
    int height = 0;

    // 成员函数 (方法)
    int area() const { // const 表示此方法不修改成员变量
        return width * height;
    }

    void resize(int w, int h) {
        width = w;
        height = h;
    }
};

Rectangle r = {5, 3};

r.width = 5, r.height = 3

调用 r.area() = ?

别名 (typedef / using)

为了方便或兼容 C 风格,可以为结构体类型创建别名:

// C++98/03 风格 (常用于 C 兼容)
typedef struct Point3D {
    double x, y, z;
} Vec3; // Vec3 是 Point3D 的别名

// C++11 及以后推荐使用 using
struct Color { int r, g, b; };
using RGBColor = Color; // RGBColor 是 Color 的别名

Vec3 v;    // 等同于 Point3D v;
RGBColor c; // 等同于 Color c;

5. 练习与巩固

5.1 选择题

问题 1/5: struct 与 union 的主要内存区别是什么?

A. struct 的每个成员都有自己独立的内存空间,union 的所有成员共享同一块内存空间。
B. struct 只能包含数据成员,union 可以包含函数。
C. struct 的大小是成员大小之和,union 的大小是最大成员的大小乘以成员数量。
D. struct 用于 C++,union 只用于 C。


5.2 编程练习

练习 1: 定义学生结构体

定义一个名为 `Student` 的结构体,包含 `std::string name`、`int id` 和 `double score` 三个成员。编写一个函数 `void printStudent(const Student& s)`,用于打印学生的所有信息。

练习 2: 演示 Union 使用

定义一个名为 `Value` 的联合体,包含 `int i`、`double d` 和 `char c` 三个成员。编写代码,先后向 `i`, `d`, `c` 写入值,并观察每次写入后,尝试读取其他成员会得到什么结果(可能是不确定的值)。

6. 注意事项与陷阱

  • 内存对齐 (Padding): 编译器可能会在结构体成员之间或末尾插入额外的填充字节(padding),以确保成员变量按照特定边界对齐(如 4 字节或 8 字节),这可以提高访问效率。因此,sizeof(MyStruct) 可能大于其所有成员大小的简单总和。
    struct Example {
        char c1;    // 1 byte
        // 3 bytes padding?
        int i;      // 4 bytes
        char c2;    // 1 byte
        // 3 bytes padding?
    }; // sizeof(Example) 可能是 12 而不是 1+4+1=6
    可以使用 #pragma pack 或特定编译器属性来控制对齐,但这通常不推荐,除非有特定需求(如与硬件或固定格式文件交互)。
  • Union 的危险性: 访问联合体中并非“当前活跃”(即最后一次被赋值)的成员,其行为在 C++ 标准中是未定义的(或实现定义的,取决于具体情况)。这意味着结果可能不可预测,或者在不同编译器/平台上表现不同。必须确保你知道哪个成员是有效的。
    union Data { int i; float f; };
    Data d;
    d.f = 3.14f;
    // int x = d.i; // 读取 i 的行为未定义/实现定义!
    // 必须知道最后写入的是 f
    现代 C++ 推荐使用 std::variant (C++17) 或 std::any (C++17) 来更安全地处理类型不确定的值。
  • Struct vs Class 的语义选择: 尽管功能相似,选择 struct 还是 class 应该反映你的设计意图。
    • struct 表示一个主要由数据组成的、其成员逻辑上可以公开访问的简单聚合体。
    • class 表示一个具有行为、需要维护内部状态(不变量)、并对外提供受控接口的对象,其实现细节应该被隐藏(封装)。
    遵循这个约定能让代码更易于理解和维护。

7. 知识延伸

  • POD (Plain Old Data) 类型: 这是一个 C++ 术语,大致指那些可以用 C 语言方式处理的简单数据结构(没有虚函数、虚基类、非静态成员的引用、用户定义的构造/析构/拷贝/移动操作等)。struct 常用于定义 POD 类型。C++11 之后,POD 的概念被更精确地分解为 *Trivial* 和 *Standard-layout* 类型。可以使用类型萃取 (type traits) 如 std::is_trivial::valuestd::is_standard_layout::value (在 头文件) 来检查一个类型是否满足这些属性。POD 类型在底层编程、序列化、与 C 代码交互时很重要。
  • 内存映射与网络协议: 由于 struct 可以精确控制(或至少预测)内存布局(特别是使用 POD 类型和可能的对齐控制),它们经常用于直接映射硬件寄存器、文件格式或网络协议数据包的结构。
  • C++11 及以后的增强:
    • 成员初始化: 可以在 struct 定义中直接为非静态成员提供默认值 (如 `int x = 0;`)。
    • 聚合初始化 (Aggregate Initialization): 对于没有用户声明构造函数的聚合类型(包括许多 struct),可以使用花括号 {} 进行初始化,如 Point p = {10, 20};。C++20 甚至放宽了聚合初始化的条件。
    • constexpr 构造函数和成员函数: 如果满足条件,struct 可以拥有 constexpr 构造函数和成员函数,允许在编译时创建和操作结构体对象。
    • 委托构造函数: 一个构造函数可以调用同一类型的另一个构造函数。
  • std::tuple vs struct: 对于固定数量、类型可能不同的数据集合,std::tuple (来自 头文件) 提供了另一种选择。tuple 的成员通过索引 (std::get<0>(myTuple)) 访问,而 struct 的成员通过名称访问,通常更具可读性。

8. 总结

恭喜你完成了 C++ 结构体的学习!回顾一下关键点:

定义: struct 是用户定义的数据类型,用于将不同类型的数据成员聚合在一起。
默认权限: 成员和继承默认是 public
与 `union` 对比: struct 成员各自独立存储,union 成员共享内存。
与 `class` 对比: 主要区别在于默认权限 (class 默认 private)。语义上,struct 倾向于数据聚合,class 倾向于行为封装。
功能: 可以包含成员函数、构造/析构函数、嵌套其他类型、参与继承,功能强大。
内存: 大小受内存对齐影响,sizeof 可能大于成员大小之和。
应用: 适用于简单数据容器、POD 类型、与 C 代码交互、内存映射等场景。