Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← C++ 编程 / C++ Programming

基础语法与编译模型 / Language & Compilation

1. C++ 语言基础完整学习手册 / Complete C++ Language Basics Guide

2. C++ 语言基础与程序运行 / C++ Language Basics and Program Execution

3. C++ 程序是怎样做出来的:从源码到可执行文件 / From Source to Executable

4. 值、类型与对象:C++ 程序中的第一层现实 / Values, Types, and Objects

5. 表达式、运算符与控制流 / Expressions, Operators, and Control Flow

6. 函数、作用域与重载 / Functions, Scope, and Overloading

7. 数组、指针与字符串:从地址到借用范围 / Arrays, Pointers, and Strings

8. 引用、const 与类型推导:从复制到借用 / References, const, and Type Deduction

9. 让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases

10. 一个头文件怎样影响整个程序:预处理、头文件与命名空间 / Preprocessor, Headers, and Namespaces

11. 基础 I/O、错误与调试:让程序会观察自己的失败 / Basic I/O, Errors, and Debugging

本页目录

让数据说清楚自己:枚举、结构体、联合体与类型别名 / Enums, Structs, Unions, and Aliases ​

1. 先看一个“数字都能编译”的问题 ​

假设我们在写一个下载程序。任务有等待、下载中、完成和失败几种状态。

最直接的写法是用整数:

cpp
#include <iostream>

void print_status(int status) {
    if (status == 0) {
        std::cout << "waiting\n";
    } else if (status == 1) {
        std::cout << "downloading\n";
    } else if (status == 2) {
        std::cout << "finished\n";
    } else if (status == 3) {
        std::cout << "failed\n";
    }
}

int main() {
    print_status(2);
    print_status(999);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

它可以编译,也可能可以运行。

问题在于,调用者看到的只是一个 int:

text
调用者看到 int
  |
  v
不知道 2 代表什么
  |
  v
999 也能进入接口
1
2
3
4
5
6
7

我们真正想表达的是“有限的状态集合”。

cpp
enum class DownloadStatus {
    waiting,
    downloading,
    finished,
    failed
};

void print_status(DownloadStatus status) {
    if (status == DownloadStatus::finished) {
        std::cout << "finished\n";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

调用点现在变成:

cpp
print_status(DownloadStatus::finished);
1

这段写法更长,却更容易读,也更难把普通整数误传进来。

本文的主线是:

text
没有语义的数值
  |
  v
有名字的有限选项
  |
  v
相关字段组成一个对象
  |
  v
多个候选类型共享一片存储
  |
  v
让类型表达真实概念和边界
1
2
3
4
5
6
7
8
9
10
11
12
13

枚举、结构体、联合体和类型别名不是四个孤立语法。

它们都在回答:

怎样让数据不仅“存得下”,还能够表达它代表什么?

2. 本文基础词小注释 ​

2.1 类型是什么 ​

类型可以先理解成“编译器解释一块数据的规则”。

cpp
int count = 42;
float ratio = 0.5f;
1
2

二者都需要存储空间,但解释方式不同:

text
二进制位
  |
  +-- 按 int 解释 -> 整数
  |
  +-- 按 float 解释 -> 浮点数
1
2
3
4
5

类型还影响:

  • 可以执行哪些操作;
  • 需要多少字节;
  • 需要怎样对齐;
  • 如何初始化和销毁;
  • 函数参数是否匹配;
  • 编译器能否提前发现错误。

2.2 对象是什么 ​

对象不是变量名。

cpp
int count = 42;
1

可以分成:

text
count
  +-- 名字,供源代码引用

对象
  +-- 有类型的存储
  +-- 有生命周期
  +-- 当前保存 42
1
2
3
4
5
6
7

本文讲内存布局时,主要观察对象占用的存储。

讲类型别名和强类型时,主要观察编译器怎样理解对象。

2.3 表示和语义 ​

语义是“这个值代表什么”。

表示是“它怎样编码在存储中”。

cpp
enum class Level {
    low,
    high
};
1
2
3
4

程序员看到的是 low 和 high。

机器最终看到的是某种整数位模式:

text
Level::low
  |
  v
底层整数表示
  |
  v
内存中的二进制位
1
2
3
4
5
6
7

不能因为底层是整数,就把所有整数都当成合法状态。

2.4 布局、对齐和 padding ​

布局是对象成员在内存中的排列方式。

对齐可以理解为:某些类型更适合从特定地址边界开始。

padding 是编译器为满足对齐而插入的填充字节。

cpp
struct Example {
    char tag;
    int value;
};
1
2
3
4

一种常见布局:

text
offset 0       tag       1 byte
offset 1..3    padding   3 bytes
offset 4..7    value     4 bytes
1
2
3

padding 不是你声明的字段,却会影响 sizeof(Example)。

2.5 ABI ​

ABI 是 Application Binary Interface 的缩写。

它描述编译后的程序怎样互相配合,例如:

  • 结构体怎样布局;
  • 函数参数怎样传递;
  • 返回值放在哪里;
  • 符号如何命名;
  • 动态库如何找到函数。

因此:

text
sizeof(T)
  +-- 可以验证当前编译环境
  +-- 不自动等于跨平台文件格式
1
2
3

3. 枚举:给有限选择起名字 ​

3.1 普通枚举 ​

cpp
enum Color {
    red,
    green,
    blue
};

int main() {
    Color color = green;
}
1
2
3
4
5
6
7
8
9

枚举适合表达“从有限选项中选择一个”。

默认情况下,枚举项通常从 0 开始递增:

text
red   -> 0
green -> 1
blue  -> 2
1
2
3

这只是默认表示,不要把它当成永久协议。

如果以后在中间插入选项,后面的数值可能变化。

3.2 普通枚举的名字可能冲突 ​

cpp
enum Color {
    red,
    green,
    blue
};

enum TrafficLight {
    red,
    yellow,
    green
};
1
2
3
4
5
6
7
8
9
10
11

两个枚举都把 red 和 green 放进外围作用域。

大型项目中,这很容易产生冲突。

text
外围作用域
  +-- red
  +-- green
  +-- blue
1
2
3
4

3.3 enum class ​

cpp
enum class Color {
    red,
    green,
    blue
};

enum class TrafficLight {
    red,
    yellow,
    green
};
1
2
3
4
5
6
7
8
9
10
11

使用时必须写出所属类型:

cpp
Color color = Color::green;
TrafficLight light = TrafficLight::green;
1
2

概念关系变成:

text
Color
  +-- Color::red
  +-- Color::green

TrafficLight
  +-- TrafficLight::red
  +-- TrafficLight::green
1
2
3
4
5
6
7

enum class 的重要特征:

  • 枚举项有自己的作用域;
  • 不会轻易隐式转换成整数;
  • 不同枚举类型不容易混用;
  • 可以明确指定底层整数类型。

3.4 显式转换 ​

cpp
enum class Color {
    red,
    green,
    blue
};

Color color = Color::green;
int raw = static_cast<int>(color);
1
2
3
4
5
6
7
8

static_cast 表示程序员明确要求一次转换。

它表达“我知道这里改变了类型”。

它不代表外部输入已经合法:

cpp
int raw = 999;
Color color = static_cast<Color>(raw);
1
2

读取文件或网络数据时,仍要先检查范围。

cpp
bool is_valid(int raw) {
    return raw >= 0 && raw <= 2;
}
1
2
3

3.5 指定底层类型 ​

cpp
#include <cstdint>

enum class MessageType : std::uint8_t {
    request = 1,
    response = 2
};
1
2
3
4
5
6

std::uint8_t 表示无符号八位整数。

观察大小:

cpp
#include <iostream>

int main() {
    std::cout << sizeof(MessageType) << '\n';
}
1
2
3
4
5

这里的大小是当前实现的观察结果。

它不能单独解决字节序、版本和协议兼容问题。

3.6 枚举和状态机 ​

cpp
enum class ConnectionState {
    disconnected,
    connecting,
    ready,
    failed
};
1
2
3
4
5
6

状态图:

text
+---------------+
| disconnected  |
+---------------+
        |
        | connect
        v
+---------------+
| connecting    |
+---------------+
    |       |
    | ok    | error
    v       v
+-------+ ++--------+
| ready | | failed  |
+-------+ ++--------+
    |
    | close
    v
disconnected
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

枚举只表达“有哪些状态”。

它不自动表达“哪些跳转合法”。

跳转规则需要函数维护:

cpp
bool can_connect(ConnectionState state) {
    return state == ConnectionState::disconnected ||
           state == ConnectionState::failed;
}
1
2
3
4

3.7 实验:观察枚举大小 ​

保存为 enum_demo.cpp:

cpp
#include <cstdint>
#include <iostream>

enum class MessageType : std::uint8_t {
    request = 1,
    response = 2
};

int main() {
    MessageType type = MessageType::request;
    auto raw = static_cast<std::uint8_t>(type);

    std::cout << "enum size: " << sizeof(MessageType) << '\n';
    std::cout << "raw value: " << static_cast<int>(raw) << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

最简单的编译命令:

bash
g++ enum_demo.cpp -o enum_demo
1

命令的各部分:

text
g++
  +-- 启动 C++ 编译器驱动

enum_demo.cpp
  +-- 输入源文件

-o enum_demo
  +-- 把输出程序命名为 enum_demo
1
2
3
4
5
6
7
8

运行:

bash
./enum_demo
1

可能输出:

text
enum size: 1
raw value: 1
1
2

先运行,再解释。

不要把一次运行的地址或大小直接当成所有平台的永久事实。

4. 结构体:把相关字段组成一个对象 ​

4.1 从两个变量到一个类型 ​

cpp
double x = 3.0;
double y = 4.0;
1
2

这是一个点,但变量名没有表达它们属于同一个概念。

cpp
struct Point {
    double x;
    double y;
};

Point point{3.0, 4.0};
1
2
3
4
5
6

现在:

text
Point
  +-- x
  +-- y
1
2
3

Point 是类型,point 是对象。

4.2 结构体的价值不只是少写变量 ​

cpp
struct User {
    int id;
    bool enabled;
};
1
2
3
4

两个字段属于同一个对象。

它们可以一起传递、一起初始化、一起检查。

text
User
  +-- id
  +-- enabled
1
2
3

结构体的价值有两层:

text
第一层:相关数据一起移动
第二层:数据关系进入类型系统
1
2

4.3 默认成员初始化 ​

cpp
struct Config {
    int retries = 3;
    bool verbose = false;
};

Config config{};
Config custom{5, true};
1
2
3
4
5
6
7

config{} 使用默认成员值。

custom{5, true} 使用显式值。

初始化和赋值不是一回事:

cpp
Config first{};
first.retries = 5;
1
2

第一个语句创建对象。

第二个语句修改已经存在的对象。

4.4 struct 和 class ​

两者都可以有:

  • 数据成员;
  • 成员函数;
  • 构造函数;
  • 私有成员;
  • 继承;
  • 虚函数。

主要默认差异:

text
struct
  +-- 默认 public

class
  +-- 默认 private
1
2
3
4
5
cpp
struct PublicPoint {
    int x;
};

class PrivatePoint {
    int x;
};
1
2
3
4
5
6
7

工程习惯通常是:

text
struct
  +-- 公开的简单值类型

class
  +-- 需要维护不变量的对象
1
2
3
4
5

这是一种习惯,不是语言限制。

4.5 不变量 ​

不变量是对象正常使用期间必须保持的条件。

cpp
struct Date {
    int year;
    int month;
    int day;
};
1
2
3
4
5
cpp
Date date{2026, 99, 99};
1

这个对象形式上存在,但语义可能无效。

如果字段公开,调用者可以随时制造非法状态。

需要集中检查时,可以使用类:

cpp
class SafeDate {
public:
    SafeDate(int year, int month, int day);

    int year() const;
    int month() const;
    int day() const;

private:
    int year_;
    int month_;
    int day_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13

5. 结构体在内存中怎样排列 ​

5.1 sizeof ​

cpp
#include <iostream>

struct Point {
    double x;
    double y;
};

int main() {
    std::cout << sizeof(Point) << '\n';
}
1
2
3
4
5
6
7
8
9
10

sizeof(Point) 表示一个对象占用的字节数。

不能普遍认为:

text
sizeof(结构体) = 成员 sizeof 之和
1

因为编译器可能插入 padding。

5.2 观察偏移量 ​

cpp
#include <cstddef>
#include <iostream>

struct Record {
    char tag;
    int value;
    char active;
};

int main() {
    std::cout << "sizeof: " << sizeof(Record) << '\n';
    std::cout << "tag: " << offsetof(Record, tag) << '\n';
    std::cout << "value: " << offsetof(Record, value) << '\n';
    std::cout << "active: " << offsetof(Record, active) << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

offsetof 查看成员相对对象起始位置的偏移。

这个示例是简单标准布局类型,适合观察。

常见实现可能布局为:

text
offset 0       tag       1 byte
offset 1..3    padding   3 bytes
offset 4..7    value     4 bytes
offset 8       active    1 byte
offset 9..11   tail pad  3 bytes
sizeof         12 bytes
1
2
3
4
5
6

具体结果应以你的编译器运行结果为准。

5.3 对齐为什么存在 ​

对齐是类型对地址边界的要求。

一个四字节对象通常更适合从四的倍数地址开始:

text
地址 1000  1001  1002  1003  1004
       |     |     |     |     |
int    +-----------------------+
       从 1000 开始更自然
1
2
3
4

不同 CPU 处理未对齐访问的方式不同:

  • 有些允许但较慢;
  • 有些需要拆成多次访问;
  • 有些场景不允许。

编译器因此插入 padding。

5.4 alignof ​

cpp
#include <iostream>

struct Record {
    char tag;
    int value;
};

int main() {
    std::cout << alignof(char) << '\n';
    std::cout << alignof(int) << '\n';
    std::cout << alignof(Record) << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12

alignof(T) 查询类型 T 的对齐要求。

text
C++ 类型规则
  |
  v
编译器布局
  |
  v
CPU 访问约束
1
2
3
4
5
6
7

5.5 尾部 padding ​

cpp
Record records[2];
1

第二个对象也必须从合适的地址开始。

text
records[0]
+----------------------+
| fields + tail pad    |
+----------------------+
records[1]
+----------------------+
| fields + tail pad    |
+----------------------+
1
2
3
4
5
6
7
8

因此 sizeof(Record) 还要保证数组中下一个对象正确对齐。

5.6 成员顺序和布局 ​

cpp
struct LessCompact {
    char a;
    double b;
    char c;
    int d;
};

struct MoreCompact {
    double b;
    int d;
    char a;
    char c;
};
1
2
3
4
5
6
7
8
9
10
11
12
13

可以分别打印大小。

成员顺序可能影响 padding。

但不能只为省几字节就重排公开结构体。

还要考虑:

  • 可读性;
  • 已发布 ABI;
  • 文件兼容;
  • 缓存访问;
  • 热数据和冷数据。

5.7 实验:打印地址 ​

cpp
#include <cstdint>
#include <iostream>

struct Record {
    char tag;
    int value;
    char active;
};

int main() {
    Record record{'A', 42, true};

    auto base = reinterpret_cast<std::uintptr_t>(&record);
    auto tag = reinterpret_cast<std::uintptr_t>(&record.tag);
    auto value = reinterpret_cast<std::uintptr_t>(&record.value);
    auto active = reinterpret_cast<std::uintptr_t>(&record.active);

    std::cout << "value offset: " << value - base << '\n';
    std::cout << "active offset: " << active - base << '\n';
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

不要关注每次运行的绝对地址。

关注相对偏移是否和 offsetof 一致。

6. 结构体对象不等于文件格式 ​

6.1 序列化 ​

序列化是把对象转换为可保存或传输的字节。

text
内存对象
  |
  | serialize
  v
文件或网络字节
1
2
3
4
5

直接写入:

cpp
// 不要轻易这样做
write(fd, &record, sizeof(record));
1
2

可能把这些实现细节写入文件:

  • padding;
  • 字段宽度;
  • 字节序;
  • 指针值;
  • 编译器布局。

6.2 显式写字段 ​

cpp
#include <cstdint>
#include <vector>

void append_u32_be(std::vector<std::uint8_t>& output,
                   std::uint32_t value) {
    output.push_back(static_cast<std::uint8_t>((value >> 24) & 0xff));
    output.push_back(static_cast<std::uint8_t>((value >> 16) & 0xff));
    output.push_back(static_cast<std::uint8_t>((value >> 8) & 0xff));
    output.push_back(static_cast<std::uint8_t>(value & 0xff));
}
1
2
3
4
5
6
7
8
9
10

>> 是右移。

& 0xff 保留低八位。

代码较长,但字节序意图清楚。

6.3 文件格式还需要什么 ​

稳定格式至少要规定:

text
字段顺序
  |
字段宽度
  |
字节序
  |
编码
  |
版本
  |
未知字段处理方式
1
2
3
4
5
6
7
8
9
10
11

结构体内存布局只是实现细节,不自动是协议。

7. 联合体:多个成员共享存储 ​

7.1 最小例子 ​

cpp
union Number {
    int integer;
    double real;
};
1
2
3
4

联合体更接近:

text
+----------------------+
| integer 或 real      |
+----------------------+
1
2
3

不是:

text
+----------+----------+
| integer  | real     |
+----------+----------+
1
2
3

7.2 联合体大小 ​

cpp
#include <iostream>

union Number {
    int integer;
    double real;
};

int main() {
    std::cout << sizeof(Number) << '\n';
    std::cout << alignof(Number) << '\n';
}
1
2
3
4
5
6
7
8
9
10
11

大小需要能够容纳最大的成员,并满足对齐要求。

7.3 活跃成员 ​

cpp
Number number{};
number.integer = 42;
1
2

可以把 integer 理解为当前使用的成员。

再写:

cpp
number.real = 3.14;
1

同一存储改为按 real 使用。

text
写 integer
  |
  v
按 int 解释

写 real
  |
  v
按 double 解释
1
2
3
4
5
6
7
8
9

不能认为两个成员各自拥有独立值。

7.4 union 没有自动 tag ​

cpp
union Payload {
    int integer;
    double real;
};
1
2
3
4

它没有自动记录当前成员。

需要额外标签:

cpp
enum class PayloadKind {
    integer,
    real
};

struct TaggedPayload {
    PayloadKind kind;
    Payload payload;
};
1
2
3
4
5
6
7
8
9

关系是:

text
TaggedPayload
  +-- kind: 当前类型
  +-- payload: 共享存储
1
2
3

手写版本必须保证两者始终同步。

7.5 std::variant ​

cpp
#include <variant>

using Payload = std::variant<int, double>;

Payload payload = 42;
payload = 3.14;
1
2
3
4
5
6

访问:

cpp
#include <iostream>

std::visit([](const auto& value) {
    std::cout << value << '\n';
}, payload);
1
2
3
4
5

std::variant 管理:

  • 当前选择;
  • 成员对象生命周期;
  • 访问接口。

业务代码通常优先使用它。

7.6 非平凡成员 ​

cpp
union TextValue {
    int number;
    std::string text;
};
1
2
3
4

std::string 可能拥有堆内存。

手动切换成员需要处理:

  • 构造;
  • 析构;
  • 复制;
  • 移动;
  • 异常。

这也是裸 union 容易出错的原因。

7.7 类型双关的边界 ​

cpp
union Bits {
    float value;
    std::uint32_t bits;
};
1
2
3
4

不要把“某个编译器上能读出结果”当成可移植规则。

观察对象表示可以用:

cpp
#include <cstring>

float value = 1.0f;
std::uint32_t bits{};
std::memcpy(&bits, &value, sizeof(value));
1
2
3
4
5

memcpy 表达复制字节表示,而不是同时激活两个不同对象。

8. 类型别名:给已有类型换名字 ​

8.1 using ​

cpp
using UserId = std::uint64_t;
1
cpp
UserId user_id = 42;
1

名字更有语义,但仍是 std::uint64_t。

cpp
std::uint64_t raw = user_id;
1

通常可以直接通过。

8.2 别名不是强类型 ​

cpp
using UserId = std::uint64_t;
using OrderId = std::uint64_t;

void load_user(UserId id);

OrderId order_id = 100;
load_user(order_id); // 通常可以通过
1
2
3
4
5
6
7

编译器看到的是同一个底层类型。

text
UserId
  +-- uint64_t 的别名

OrderId
  +-- uint64_t 的别名
1
2
3
4
5

8.3 强类型包装 ​

cpp
struct UserId {
    std::uint64_t value{};
};

struct OrderId {
    std::uint64_t value{};
};

void load_user(UserId id);
1
2
3
4
5
6
7
8
9

现在:

cpp
OrderId order_id{100};
// load_user(order_id); // 类型不匹配
1
2

更明确的类型会增加少量代码。

但它让“用户 ID 不要当订单 ID”成为编译器可检查的规则。

8.4 别名适合的场景 ​

cpp
using Callback = void (*)(int);
using Scores = std::vector<double>;
1
2

别名适合简化复杂类型。

别名模板:

cpp
template <typename T>
using Buffer = std::vector<T>;
1
2

需要隔离业务概念时,使用新 struct 或 class。

8.5 typedef ​

旧写法:

cpp
typedef unsigned long long UserId;
1

现代 C++ 更常见:

cpp
using UserId = unsigned long long;
1

二者都是别名,不会创建新类型。

9. 位域:把字段压进若干位 ​

9.1 最小例子 ​

cpp
struct Flags {
    unsigned ready : 1;
    unsigned mode : 3;
};
1
2
3
4

冒号后的数字叫位宽。

text
ready : 1
  +-- 尝试使用一位

mode : 3
  +-- 尝试使用三位
1
2
3
4
5

9.2 位域的边界 ​

位域布局可能受实现影响:

  • 位从高位还是低位分配;
  • 存储单元多大;
  • 跨单元怎样处理;
  • 对齐怎样处理。

因此不应直接把位域当成跨平台网络包。

9.3 显式掩码 ​

cpp
#include <cstdint>

constexpr std::uint8_t ready_mask = 0b00000001;
constexpr std::uint8_t mode_mask = 0b00001110;

std::uint8_t flags = 0;
flags = static_cast<std::uint8_t>(flags | ready_mask);
bool ready = (flags & ready_mask) != 0;
1
2
3
4
5
6
7
8

| 是按位或,适合设置位。

& 是按位与,适合提取位。

这种写法较长,却把协议表示写在代码中。

10. 完整例子:一条任务记录 ​

cpp
#include <cstdint>
#include <iostream>
#include <string>
#include <variant>

enum class Priority : std::uint8_t {
    low = 1,
    normal = 2,
    high = 3
};

struct UserId {
    std::uint64_t value{};
};

using Payload = std::variant<int, std::string>;

struct Task {
    UserId owner{};
    Priority priority{Priority::normal};
    Payload payload{0};
};

void print_task(const Task& task) {
    std::cout << "owner: " << task.owner.value << '\n';
    std::cout << "priority: "
              << static_cast<int>(task.priority) << '\n';

    std::visit([](const auto& value) {
        std::cout << "payload: " << value << '\n';
    }, task.payload);
}

int main() {
    Task task{
        UserId{7},
        Priority::high,
        std::string{"learn object layout"}
    };

    print_task(task);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

对象关系:

text
Task
  +-- owner: UserId
  |     +-- value: uint64_t
  |
  +-- priority: Priority
  |
  +-- payload: int 或 string
1
2
3
4
5
6
7

保存为 task_types.cpp。

最简单命令:

bash
g++ task_types.cpp -o task_types
1

如果编译器默认标准较旧:

bash
g++ -std=c++17 task_types.cpp -o task_types
1

运行:

bash
./task_types
1

可能输出:

text
owner: 7
priority: 3
payload: learn object layout
1
2
3

这里每个类型都承担了自己的语义:

text
Priority
  +-- 有限选项

UserId
  +-- 身份概念

Task
  +-- 相关字段组合

variant
  +-- 多种 payload 之一
1
2
3
4
5
6
7
8
9
10
11

11. 常见误解 ​

11.1 结构体大小就是成员大小相加 ​

不一定。

对齐和 padding 会影响大小。

用 sizeof、alignof 和 offsetof 验证。

11.2 结构体内存可以直接写进文件 ​

不一定。

布局、字节序、宽度和 padding 可能不同。

11.3 enum class 只能有列出的值 ​

不一定。

整数显式转换仍可能产生没有对应枚举项的值。

外部输入要验证。

11.4 union 的成员同时保存 ​

不是。

成员共享存储,不能按结构体那样理解。

11.5 using 创建强类型 ​

不是。

using UserId = int 只是换名字。

11.6 位域是稳定协议 ​

不是。

位域布局具有实现相关部分。

11.7 struct 只能放数据 ​

不是。

struct 也可以有构造函数、成员函数和私有成员。

11.8 结构化绑定总是复制 ​

不一定:

cpp
auto [x, y] = point;
auto& [rx, ry] = point;
1
2

前者通常是独立绑定,后者绑定到原对象。

12. 调试和验证:不要靠猜 ​

12.1 static_assert ​

cpp
static_assert(sizeof(std::uint8_t) == 1);
1

static_assert 在编译期检查条件。

如果确实依赖当前布局,可以写:

cpp
static_assert(sizeof(MessageHeader) == 8);
1

它只锁定当前构建假设。

12.2 观察对象布局 ​

验证顺序:

text
先提出模型
  |
  v
用 sizeof / offsetof 观察
  |
  v
用调试器查看字段地址
  |
  v
确认语言保证还是 ABI 行为
1
2
3
4
5
6
7
8
9
10

不要从一次实验直接推出所有平台结论。

12.3 三个边界问题 ​

text
这是 C++ 标准保证的吗?

对象要不要跨机器保存?

这是性能优化,还是公开 ABI?
1
2
3
4
5

如果跨机器保存,就应设计显式序列化。

如果公开 ABI,就要谨慎改变字段顺序和类型。

13. 练习 ​

练习一:改造状态函数 ​

把:

cpp
void set_state(int state);
1

改为:

cpp
enum class State {
    idle,
    busy,
    stopped
};

void set_state(State state);
1
2
3
4
5
6
7

回答:

  • 调用点是否更清楚?
  • 外部整数在哪里校验?
  • 非法状态如何处理?

练习二:观察 padding ​

定义成员顺序不同的三个结构体。

打印:

cpp
sizeof(T)
alignof(T)
offsetof(T, member)
1
2
3

先猜,再运行,再解释差异。

练习三:实现强类型 ID ​

定义:

cpp
struct UserId {
    std::uint64_t value;
};

struct OrderId {
    std::uint64_t value;
};
1
2
3
4
5
6
7

尝试把 OrderId 传给需要 UserId 的函数。

练习四:比较 union 和 variant ​

写一个手动 tag + union 版本。

再写一个 std::variant 版本。

列出手动版本必须维护的生命周期和一致性规则。

练习五:稳定字节格式 ​

定义一个消息头:

text
version: 1 byte
kind:    1 byte
length:  4 bytes
1
2
3

不要直接写结构体内存。

逐字段写入,并明确字节序。

14. 本篇总结 ​

text
枚举
  +-- 给有限选择起名字

结构体
  +-- 把相关字段组织为一个对象

联合体
  +-- 让候选成员共享一片存储

类型别名
  +-- 给已有类型换一个更有语义的名字
1
2
3
4
5
6
7
8
9
10
11

底层链路:

text
语义
  |
  v
C++ 类型
  |
  v
对象表示
  |
  v
内存布局、对齐和 padding
  |
  v
编译器、ABI 和 CPU 约束
1
2
3
4
5
6
7
8
9
10
11
12
13
工具表达什么是否创建新类型跨平台布局是否自动稳定
enum class有限状态是否
struct相关字段是否
union共享存储是否
using已有类型的新名字否不适用
强类型 struct业务概念隔离是否

15. 下一篇连接 ​

本文讨论一个源文件内部怎样表达数据。

下一篇转向多个源文件之间的组织:

text
多个 .cpp
  |
  v
头文件和 #include
  |
  v
预处理后的翻译单元
  |
  v
命名空间、名字查找和链接
1
2
3
4
5
6
7
8
9
10

下一篇:预处理、头文件与命名空间

最后更新于:

Pager
上一篇8. 引用、const 与类型推导:从复制到借用 / References, const, and Type Deduction
下一篇10. 一个头文件怎样影响整个程序:预处理、头文件与命名空间 / Preprocessor, Headers, and Namespaces

持续记录,持续成长

Copyright © Tidenflow