1. C++ 与 Java/Kotlin 的根本差异

7 阅读25分钟

第一部分:C++ 与 Java/Kotlin 的根本差异

[!IMPORTANT] Java/Kotlin 开发者必读

这是整个知识体系中最重要的章节。C++ 和 Java/Kotlin 的差异不是"语法不同",而是根本性的设计哲学差异

  • Java/Kotlin 的假设:运行时有 GC、有虚拟机、有安全网
  • C++ 的假设:没有 GC、直接编译到机器码、没有安全网——你写的就是硬件要执行的

如果跳过这一章直接写 C++ 代码,你会用 Java 的思维写 C++,结果就是:内存泄漏、段错误、未定义行为、性能灾难。

花 30 分钟认真读完这一章,后面的学习效率会翻倍。


1.1 编译模型与运行时 [L1]

Java/Kotlin 的编译模型

.java/.kt  ──编译器──►  .class (字节码)  ──JVM──►  机器码 (运行时)
                          │
                     运行时链接
                     运行时加载
                     JIT 优化

Java/Kotlin 编译到字节码,由 JVM 在运行时解释执行或 JIT 编译。类型检查在编译期,但内存布局、内联、优化大量发生在运行时。

C++ 的编译模型

.cpp 文件  ──预处理器──►  编译单元(.i)  ──编译器──►  目标文件(.o)  ──链接器──►  可执行文件
     │                         │                      │                      │
  #include 展开            语法/语义分析            符号解析               直接机器码
  #define 替换             代码生成                  重定位                 无运行时依赖
  条件编译                 优化 (O2/O3)             库链接

C++ 分为三个独立阶段:预处理、编译、链接。每个 .cpp 文件独立编译为一个目标文件,最后由链接器合并。

关键概念对比

概念Java/KotlinC++
编译产物.class 字节码机器码(目标文件 .o / .obj
运行时JVM(解释 + JIT)无(直接执行机器码)
链接时机运行时(类加载)编译后(静态链接或动态链接)
代码组织包 + 类(一个文件一个 public 类)头文件 .h + 实现文件 .cpp(自由分离)
声明与定义统一(接口即定义)分离(声明在头文件,定义在 .cpp
模块系统module-info.java / Gradle 模块C++20 Modules(传统上用 #include

前向声明与头文件分离

Java 中,编译器自动查找依赖类的 .class 文件。C++ 中,编译器一次只看到一个 .cpp 文件,你必须通过头文件告诉它其他地方定义了什么。

// 代码片段(无 main 函数)
// math_utils.h —— 声明(告诉编译器"这个函数存在,签名是这样的")
#pragma once  // 防止头文件被重复包含(传统方式)
// 或者使用 #ifndef include guard

int add(int a, int b);
double factorial(int n);
// 代码片段(无 main 函数)
// math_utils.cpp —— 定义(告诉链接器"这个函数的实现在这里")
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

double factorial(int n) {
    if (n <= 1) return 1.0;
    return n * factorial(n - 1);
}
// main.cpp —— 使用
#include <iostream>  // 标准库头文件(尖括号)
#include "math_utils.h"  // 项目头文件(双引号)

int main() {
    std::cout << "3 + 5 = " << add(3, 5) << "\n";  // 8
    std::cout << "5! = " << factorial(5) << "\n";    // 120
    return 0;
}

编译与链接过程:

# 分步编译(理解过程)
g++ -std=c++23 -c math_utils.cpp -o math_utils.o   # 编译 math_utils.cpp → math_utils.o
g++ -std=c++23 -c main.cpp -o main.o               # 编译 main.cpp → main.o
g++ math_utils.o main.o -o program                  # 链接 → program
./program

# 一步到位(编译器帮你做了上面三步)
g++ -std=c++23 math_utils.cpp main.cpp -o program && ./program

编译单元与 ODR

编译单元 (Translation Unit):一个 .cpp 文件经过预处理后的结果。每个编译单元独立编译,互不可见。

ODR (One Definition Rule):C++ 的核心规则之一——每个实体(函数、变量、类)在整个程序中只能有一个定义。如果多个编译单元中定义了同名实体且定义不一致,链接器会报错(或更糟,静默产生错误行为)。

Java 对比:Java 没有这个概念。Java 的类加载机制保证一个全限定名只加载一次。C++ 中,ODR 违反是链接期错误,且有时不会被检测到。

预处理器

预处理器在编译之前执行纯文本替换,它不理解 C++ 语法。

// 代码片段(无 main 函数)
// 宏定义 —— 纯文本替换,没有类型安全
#define PI 3.14159265
#define SQUARE(x) ((x) * (x))  // 注意括号!否则 SQUARE(1+2) 会变成 1+2*1+2 = 5

// 条件编译 —— 类似 Java 的 @FeatureFlag,但在编译期
#ifdef NDEBUG
    // Release 模式:调试代码被完全移除
    #define LOG(msg) ((void)0)
#else
    // Debug 模式:输出调试信息
    #define LOG(msg) std::cout << "[DEBUG] " << msg << "\n"
#endif

现代 C++ 建议:能用 constexprinlinetemplate 的地方,就不要用 #define。宏是 C 时代的遗产,缺乏类型检查和作用域。

C++20 Modules 简述

C++20 引入了模块系统,旨在替代 #include 和宏:

// 代码片段(无 main 函数)
// math_utils.cppm (C++20 模块)
export module math_utils;

export int add(int a, int b) {
    return a + b;
}
// 代码片段(无 main 函数)
// main.cpp
import math_utils;  // 替代 #include
import std.core;    // 标准库模块(逐步替代 <iostream> 等)

int main() {
    // ...
}

现状:C++20 Modules 的编译器支持仍在完善中(GCC 14+、Clang 18+、MSVC 19.30+)。目前大多数项目仍使用传统头文件。建议了解概念,实际项目中视编译器支持情况决定是否采用。

Demo:头文件分离与编译链接

// demo_1_1_utils.h
#pragma once
#include <string>

// 声明:告诉编译器这些函数存在
std::string greet(const std::string& name);
int compute(int x, int y);
// demo_1_1_utils.cpp
#include "demo_1_1_utils.h"
#include <sstream>  // 替代 <format>,GCC 11 兼容

// 定义:函数实现
std::string greet(const std::string& name) {
    std::ostringstream oss;
    oss << "Hello, " << name << "!";
    return oss.str();
}

int compute(int x, int y) {
    return x * x + y * y;
}
// demo_1_1_main.cpp
#include <iostream>
#include "demo_1_1_utils.h"

// Java 对比:
// - Java 中你只需要 import,不需要关心声明和定义的分离
// - Java 中 greet() 和 compute() 可以在同一个文件或不同文件,编译器自动处理
// - C++ 中你必须通过头文件显式声明,并通过编译+链接两步完成

int main() {
    std::cout << greet("C++ Developer") << "\n";
    std::cout << "compute(3, 4) = " << compute(3, 4) << "\n";  // 25

    // 验证编译单元独立性:
    // 如果 demo_1_1_utils.cpp 没有被编译/链接,
    // 链接器会报错:undefined reference to `greet(std::string const&)`
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_1_1_utils.cpp demo_1_1_main.cpp -o demo_1_1 && ./demo_1_1

1.2 内存模型根本差异 [L2]

[!WARNING] 这是整个知识体系中最关键的章节。

Java 有 GC,你几乎不需要关心内存。C++ 没有 GC,内存管理是的责任。 但 C++ 的方案不是"手动 malloc/free"(那是 C 时代),而是 RAII——一种比 GC 更确定、更高效的资源管理模式。

理解这一节,你就理解了 C++ 和 Java 最大的设计哲学分歧。

内存布局对比

内存区域Java/KotlinC++生命周期
栈 (Stack)基本类型局部变量、对象引用任意类型的局部变量(包括完整对象)函数调用/返回时自动创建/销毁
堆 (Heap)所有 new 创建的对象(由 GC 管理)new/make_unique/make_shared 创建的对象必须手动 delete 或通过智能指针自动释放
全局/静态区static 字段、类变量全局变量、static 局部变量、static 成员程序启动到结束
常量区String 常量池、static finalconstexpr 变量、字符串字面量程序启动到结束
代码区方法字节码(方法区)机器码(.text 段)程序启动到结束

核心差异:对象在哪里

// Java:对象永远在堆上,变量只是引用
void javaExample() {
    Point p = new Point(1, 2);  // 对象在堆上,p 是引用
    // 离开方法后,对象由 GC 在某个不确定的时刻回收
}
// C++:对象可以在栈上或堆上,这是你的选择
void cpp_stack_example() {
    Point p{1, 2};  // 对象在栈上!完整的 Point 对象,不是引用
    // 离开作用域时,析构函数自动调用,内存立即释放
}

void cpp_heap_example() {
    auto p = std::make_unique<Point>(1, 2);  // 对象在堆上
    // p 离开作用域时,unique_ptr 析构函数自动 delete
    // 不会泄漏,因为 RAII 保证了这一点
}

GC vs RAII:确定性销毁

特性Java GCC++ RAII
释放时机不确定(GC 周期决定)确定的(离开作用域立即释放)
性能开销GC 暂停(Stop-the-World)零运行时开销(编译期决定)
资源类型仅内存内存 + 文件句柄 + 锁 + 网络连接 + 任何资源
异常安全try-with-resources自动(析构函数保证执行)
循环引用GC 自动处理shared_ptr 可能泄漏,用 weak_ptr 解决

栈 vs 堆:选择原则

C++ 的默认选择是栈。 堆是当你需要更长的生命周期或动态大小时才使用的。

场景选择原因
局部临时对象零开销,自动释放
函数返回值栈(移动语义)NRVO/RVO 优化,零拷贝
生命周期超出函数堆(智能指针)栈变量在函数返回时销毁
大小在编译期未知堆(std::vector栈大小有限(通常 1-8MB)
多所有者共享堆(std::shared_ptr引用计数管理共享所有权
多态(基类指针)堆(智能指针)对象切片问题

引用语义 vs 值语义

这是 Java 和 C++ 最根本的语义差异之一。

// Java:一切都是引用语义(基本类型除外)
Point a = new Point(1, 2);
Point b = a;       // b 和 a 指向同一个对象!
b.setX(10);        // a.x 也变成了 10
// C++:默认是值语义(拷贝)
Point a{1, 2};
Point b = a;       // b 是 a 的完整拷贝!两个独立的对象
b.x = 10;          // a.x 仍然是 1

// 需要引用语义时,显式使用引用或指针
Point& ref = a;    // ref 是 a 的引用
ref.x = 10;        // a.x 变成了 10

Point* ptr = &a;   // ptr 是指向 a 的指针
ptr->x = 20;       // a.x 变成了 20

Demo:对象生命周期对比

// demo_1_2_lifetime.cpp
#include <iostream>
#include <memory>
#include <string>

// 一个简单的类,带构造函数和析构函数
// Java 没有"析构函数"的概念——对象的清理由 GC 负责
// C++ 的析构函数在对象销毁时自动调用,这是 RAII 的核心
class Tracer {
    std::string name_;
public:
    explicit Tracer(std::string name) : name_(std::move(name)) {
        std::cout << "  构造: " << name_ << " (地址: " << this << ")\n";
    }
    ~Tracer() {
        std::cout << "  析构: " << name_ << " (地址: " << this << ")\n";
    }

    // 禁止拷贝(防止意外拷贝导致追踪混乱)
    Tracer(const Tracer&) = delete;
    Tracer& operator=(const Tracer&) = delete;

    // 允许移动
    Tracer(Tracer&& other) noexcept : name_(std::move(other.name_)) {
        std::cout << "  移动: " << name_ << " (从 " << &other << " 到 " << this << ")\n";
    }
};

void stack_demo() {
    std::cout << "=== 栈上对象 ===\n";
    // Java 对比:Java 中 new Tracer("stack") 会在堆上创建对象
    // C++ 中直接在栈上创建,离开函数时自动销毁
    Tracer stack_obj{"stack"};
    std::cout << "  栈上对象使用中...\n";
    // stack_obj 在这里离开作用域,析构函数自动调用
    // Java 中,对应的对象要等 GC 回收,时机不确定
}

void heap_demo() {
    std::cout << "\n=== 堆上对象(智能指针) ===\n";
    // Java 对比:Java 的 new 和这里的 make_unique 类似
    // 区别:Java 依赖 GC 回收,C++ 的 unique_ptr 在离开作用域时立即 delete
    auto heap_obj = std::make_unique<Tracer>("heap");
    std::cout << "  堆上对象使用中...\n";
    // heap_obj 离开作用域,unique_ptr 析构 → 调用 delete → Tracer 析构
    // 释放时机是确定的、即时的
}

void scope_demo() {
    std::cout << "\n=== 作用域与生命周期 ===\n";
    {
        Tracer inner{"inner_scope"};
        std::cout << "  内层作用域\n";
    }  // inner 在这里销毁!
    std::cout << "  外层作用域(inner 已经销毁了)\n";
}

void value_vs_reference_demo() {
    std::cout << "\n=== 值语义 vs 引用语义 ===\n";

    // 值语义:C++ 默认行为
    // Java 对比:Java 中 Point b = a; 是引用赋值,指向同一对象
    // C++ 中如果 Tracer 允许拷贝,Tracer b = a; 会创建一个完整副本
    // 这里我们禁用了拷贝,所以只能用移动或引用

    auto original = std::make_unique<Tracer>("original");
    Tracer& ref = *original;  // 引用:ref 和 *original 是同一个对象
    std::cout << "  ref 和 original 是同一个对象\n";
    std::cout << "  ref 地址: " << &ref << ", original 地址: " << original.get() << "\n";
}

int main() {
    std::cout << "程序开始\n";
    stack_demo();
    heap_demo();
    scope_demo();
    value_vs_reference_demo();
    std::cout << "\n程序结束\n";
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_1_2_lifetime.cpp -o demo_1_2 && ./demo_1_2

预期输出:

程序开始
=== 栈上对象 ===
  构造: stack (地址: 0x...)
  栈上对象使用中...
  析构: stack (地址: 0x...)
=== 堆上对象(智能指针) ===
  构造: heap (地址: 0x...)
  堆上对象使用中...
  析构: heap (地址: 0x...)
=== 作用域与生命周期 ===
  构造: inner_scope (地址: 0x...)
  内层作用域
  析构: inner_scope (地址: 0x...)
  外层作用域(inner 已经销毁了)
=== 值语义 vs 引用语义 ===
  构造: original (地址: 0x...)
  ref 和 original 是同一个对象
  ref 地址: 0x..., original 地址: 0x...

程序结束

1.3 未定义行为 (UB) [L2]

[!CAUTION] UB 是 C++ 中最危险的概念,Java/Kotlin 中不存在对应物。

Java 中,越界访问抛 ArrayIndexOutOfBoundsException,空指针抛 NullPointerException。 C++ 中,这些操作的结果是未定义行为——程序可能崩溃、可能产生错误结果、可能看起来正常工作。

UB 不是"未指定行为",不是"实现定义行为",是"编译器可以做任何事"。

UB 的本质

当你的代码触发 UB 时,C++ 标准对程序的行为不做任何保证。编译器可以:

  1. 假设 UB 不会发生,并基于这个假设进行优化(这是最危险的情况)
  2. 生成错误的机器码
  3. 看起来正常工作(直到某天换个编译器或优化级别就崩了)
int a = 0;
int b = a + 1;  // OK
int c = a - 1;  // UB!有符号整数溢出
// 编译器可能优化掉后续所有使用 c 的代码,因为它"假设"溢出不会发生

行为分类对比

分类含义Java/Kotlin 对应C++ 示例
未定义行为 (UB)标准不做任何保证不存在越界访问、空指针解引用
实现定义行为编译器必须文档化其选择不存在sizeof(int) 通常为 4
未指定行为标准允许几种可能,不要求文档化存在(如 HashMap 迭代顺序)函数参数求值顺序
已定义行为标准明确规定了结果大部分行为正常的算术运算

常见 UB 分类

类别典型场景Java/Kotlin 中的对应行为
内存越界数组/容器越界访问IndexOutOfBoundsException
空指针解引用解引用 nullptrNullPointerException
有符号整数溢出INT_MAX + 1包装(Java)/ 饱和(Kotlin +
未初始化变量读取未初始化的局部变量编译错误(Java)/ 默认值(Kotlin 不允许)
悬挂引用/指针引用已销毁的对象不可能发生(GC 保证对象存活)
数据竞争两个线程同时写同一变量(无同步)可能发生但行为更可预测
重复释放对同一指针 delete 两次不可能发生(GC 管理内存)
类型双关通过不兼容类型的指针访问内存不允许(类型安全)
栈溢出无限递归或超大栈变量StackOverflowError

UB 与编译器优化的关系

这是 UB 最阴险的地方——编译器会利用 UB 做优化

// 示例:编译器如何利用"有符号溢出是 UB"来优化
bool is_positive(int x) {
    return x + 1 > x;  // 看起来总是 true,对吧?
}

// 编译器 reasoning:
// 1. 如果 x + 1 溢出,那是 UB
// 2. UB 不会发生(编译器假设)
// 3. 因此 x + 1 > x 总是 true
// 4. 优化为:return true;
//
// Java 对比:Java 中 int 溢出是定义行为(包装),
// 所以 x + 1 > x 在 x == Integer.MAX_VALUE 时为 false
// 示例:空指针检查被优化掉
void process(int* p) {
    int val = *p;  // 如果 p 为空,这里是 UB
    if (p == nullptr) {
        // 编译器可能删除这个分支!
        // reasoning:如果 p 为空,上面的 *p 已经是 UB 了
        // UB 不会发生,所以 p 不可能为空
        std::cout << "null!\n";
    }
}

工具防护

工具检测目标使用方式
ASan (AddressSanitizer)内存越界、use-after-free、内存泄漏-fsanitize=address
UBSan (UndefinedBehaviorSanitizer)未定义行为(整数溢出、空指针等)-fsanitize=undefined
MSan (MemorySanitizer)使用未初始化内存-fsanitize=memory
TSan (ThreadSanitizer)数据竞争-fsanitize=thread
Valgrind内存错误(运行时,较慢)valgrind ./program
静态分析潜在 UB(编译期)Clang-Tidy, CPPCheck, SonarQube

建议:开发阶段始终开启 ASan + UBSan。CI/CD 中必须启用。Release 构建可以关闭以获得最大性能。

Demo:UB 检测

// demo_1_3_ub.cpp
#include <iostream>
#include <vector>
#include <climits>

// === 示例 1:未初始化变量 ===
// Java 对比:Java 中局部变量必须初始化后才能使用,编译器会报错
// C++ 中,内置类型(int, double, 指针等)的局部变量默认不初始化
void uninitialized_demo() {
    std::cout << "=== 未初始化变量 ===\n";

    int x;  // 未初始化!值是不确定的
    // std::cout << x << "\n";  // UB!值可能是任何东西

    // 正确做法:始终初始化
    int y = 0;       // OK
    int z{};         // OK,C++11 列表初始化(零初始化)
    std::cout << "y = " << y << ", z = " << z << "\n";
}

// === 示例 2:数组越界 ===
// Java 对比:Java 会抛 ArrayIndexOutOfBoundsException
// C++ 中,std::vector 的 operator[] 不做边界检查(性能考虑)
// 使用 .at() 可以获得边界检查(抛 std::out_of_range)
void out_of_bounds_demo() {
    std::cout << "\n=== 数组越界 ===\n";

    std::vector<int> v = {10, 20, 30};

    // 危险:operator[] 不做边界检查,越界是 UB
    // std::cout << v[100] << "\n";  // UB!可能崩溃、可能输出垃圾值

    // 安全:.at() 做边界检查
    try {
        std::cout << v.at(100) << "\n";  // 抛 std::out_of_range
    } catch (const std::out_of_range& e) {
        std::cout << "  捕获越界异常: " << e.what() << "\n";
    }

    // 建议:开发阶段使用 ASan 检测越界访问
}

// === 示例 3:有符号整数溢出 ===
// Java 对比:Java int 溢出是定义行为(低位包装)
// C++ 中,有符号整数溢出是 UB
void overflow_demo() {
    std::cout << "\n=== 有符号整数溢出 ===\n";

    int max = INT_MAX;
    std::cout << "INT_MAX = " << max << "\n";

    // 以下操作在 C++ 中是 UB!
    // int overflow = max + 1;  // UB
    // std::cout << overflow << "\n";

    // 安全的替代方案:
    // 1. 使用更大的类型
    long long safe1 = static_cast<long long>(max) + 1;
    std::cout << "安全的溢出处理 (long long): " << safe1 << "\n";

    // 2. 使用 __builtin_add_overflow (GCC/Clang 内建函数)
    int result;
    if (__builtin_add_overflow(max, 1, &result)) {
        std::cout << "  检测到溢出!\n";
    }
}

// === 示例 4:悬挂引用 ===
// Java 对比:不可能发生,GC 保证对象在使用期间存活
// C++ 中,引用/指针可以指向已销毁的对象
int& dangling_reference_demo() {
    std::cout << "\n=== 悬挂引用 ===\n";

    int x = 42;
    int& ref = x;
    std::cout << "  ref = " << ref << "\n";  // OK: 42

    {
        int y = 100;
        ref = y;  // OK: ref 现在引用 y
        std::cout << "  ref = " << ref << "\n";  // OK: 100
    }  // y 被销毁

    // std::cout << "  ref = " << ref << "\n";  // UB!ref 现在是悬挂引用
    std::cout << "  (悬挂引用的读取被注释掉了,否则是 UB)\n";

    return x;  // 返回局部变量的引用也是 UB!
}

// === 示例 5:编译器优化与 UB ===
// 展示编译器如何利用 UB 进行优化
bool check_overflow(int x) {
    // 在 Java 中,当 x == INT_MAX 时,x + 1 会溢出为 INT_MIN,结果为 false
    // 在 C++ 中,有符号溢出是 UB,编译器可以假设它不会发生
    // 因此编译器可能将整个函数优化为 return true;
    return x + 1 > x;
}

int main() {
    std::cout << "UB 演示程序\n";
    std::cout << "编译建议: g++ -std=c++23 -Wall -Wextra -g -fsanitize=undefined,address\n\n";

    uninitialized_demo();
    out_of_bounds_demo();
    overflow_demo();
    dangling_reference_demo();

    std::cout << "\n=== 编译器优化与 UB ===\n";
    std::cout << "check_overflow(INT_MAX) 可能返回 true(被优化掉了)\n";
    std::cout << "因为编译器假设有符号溢出不会发生\n";

    return 0;
}

编译运行(带 UBSan + ASan):

g++ -std=c++23 -Wall -Wextra -g -fsanitize=undefined,address demo_1_3_ub.cpp -o demo_1_3 && ./demo_1_3

1.4 类型系统差异 [L1]

基本类型大小对比

类型JavaKotlinC++ (常见)说明
布尔boolean (1 byte)Boolean/Boolean (1 byte)bool (1 byte)C++ bool 可隐式转为 int
字符char (2 bytes, UTF-16)Char (2 bytes)char (1 byte, ASCII)C++ 有 char8_t(UTF-8), char16_t, char32_t
字节byte (1 byte)Byte (1 byte)int8_t / uint8_t (1 byte)C++ 用 <cstdint> 中的定宽类型
短整short (2 bytes)Short (2 bytes)int16_t (2 bytes)C++ short 通常是 16 位
整数int (4 bytes)Int (4 bytes)int / int32_t (4 bytes)C++ int 通常是 32 位,但不保证
长整long (8 bytes)Long (8 bytes)long long / int64_t (8 bytes)C++ long 在 64 位 Linux 是 8 字节,Windows 是 4 字节
单精度float (4 bytes)Float (4 bytes)float (4 bytes)IEEE 754
双精度double (8 bytes)Double (8 bytes)double (8 bytes)IEEE 754
无大小类型size_t (平台相关)无符号,表示大小/索引,类似 Java 没有

关键差异:Java 的类型大小是固定的(跨平台一致)。C++ 的 int/long 大小是实现定义的,需要定宽类型 <cstdint> 来保证。

枚举:Java enum vs C++ enum class

// Java enum:功能完整的类
enum Color {
    RED("#FF0000"),
    GREEN("#00FF00"),
    BLUE("#0000FF");

    private final String hex;
    Color(String hex) { this.hex = hex; }
    public String getHex() { return hex; }
}
// 代码片段(无 main 函数)
// C++ enum class:类型安全的枚举(C++11)
enum class Color : uint32_t {
    Red = 0xFF0000,
    Green = 0x00FF00,
    Blue = 0x0000FF
};

// C++ 旧式 enum(不推荐,会污染命名空间)
// enum Color { Red, Green, Blue };  // 不要用这种

// C++ enum class 可以有方法(通过重载运算符等方式)
enum class Direction {
    North, East, South, West
};

// 为 enum class 添加方法的方式
constexpr Direction operator+(Direction d, int offset) {
    int val = static_cast<int>(d) + offset;
    val = ((val % 4) + 4) % 4;  // 确保正数取模
    return static_cast<Direction>(val);
}

constexpr Direction opposite(Direction d) {
    return d + 2;
}
特性Java enumC++ enum class
类型安全是(不会隐式转为 int)
命名空间枚举常量在 enum 体内枚举常量需要 Color::Red 访问
方法可以有构造函数、方法不能直接有方法,通过运算符重载/自由函数实现
实现接口可以 implements不可以
底层类型无(对象引用)可指定(uint8_t, int, 等)
遍历values()需要手动实现或使用魔法

数组:Java 数组 vs std::array

// Java:数组是对象,有运行时边界检查
int[] arr = new int[5];      // 默认初始化为 0
arr[0] = 42;                  // OK
int x = arr[10];              // ArrayIndexOutOfBoundsException
int len = arr.length;         // 属性访问
// 代码片段(无 main 函数)
// C++:std::array 是固定大小、零开销的容器
#include <array>
std::array<int, 5> arr{};    // 值初始化为 0(注意 {})
arr[0] = 42;                  // OK
// arr[10] = 1;              // UB!operator[] 不做边界检查
int x = arr.at(10);           // 抛 std::out_of_range(安全)
auto len = arr.size();        // 方法调用,返回 size_t
特性Java 数组C++ std::array
大小运行时确定编译期确定
边界检查始终检查([][] 不检查,.at() 检查
默认初始化数字类型为 0需要 {}= {} 才会零初始化
长度.length 属性.size() 方法
拷贝引用拷贝(浅拷贝)值拷贝(深拷贝)
性能堆分配(new栈分配(内联)

字符串:String vs std::string

特性Java StringC++ std::string
不可变性不可变(immutable)可变(mutable)
编码UTF-16UTF-8(通常)
底层存储char[] + 偏移量/长度SSO(小字符串优化)+ 堆分配
空值可以为 null不能为 null(空字符串是 ""
拼接+ 运算符(编译器优化)+std::format(C++20)
子串substring()substr()(C++20 起用 std::string_view
比较.equals()(值比较)==(值比较,C++ 直接用 ==
// 代码片段(无 main 函数)
#include <string>
#include <string_view>
#include <iostream>
#include <sstream>

void string_demo() {
    // 创建
    std::string s1 = "hello";           // 从 C 字符串
    std::string s2{"world"};            // 列表初始化
    std::string s3(5, 'x');             // "xxxxx"

    // 拼接
    std::string greeting = s1 + " " + s2;  // "hello world"

    // 格式化(使用 ostringstream,GCC 11 兼容)
    std::ostringstream oss;
    oss << 1 << " + " << 2 << " = " << 3;
    std::string formatted = oss.str();

    // 字符串视图(零拷贝的只读引用,类似 Java 的 CharSequence)
    std::string_view sv = greeting;  // 不拷贝数据
    std::cout << sv.substr(0, 5) << "\n";  // "hello"

    // Java 对比:
    // - Java String 是不可变的,任何"修改"都创建新对象
    // - C++ std::string 是可变的,可以原地修改
    // - C++ std::string_view 类似 Java 的子串视图,但不拥有数据
}

布尔转换差异

// 代码片段(无 main 函数)
// Java:boolean 不能和 int 互转
// int x = true;  // 编译错误
// boolean b = 1;  // 编译错误

// C++:bool 可以隐式转为 int,int 也可以隐式转为 bool
bool b = 42;       // true(非零值转为 true)
int x = true;      // 1(true 转为 1)
int y = false;     // 0

// 这个特性是 bug 的常见来源:
// if (x = 5) { ... }  // 赋值,不是比较!x 变为 5,条件为 true
// if (x == 5) { ... } // 比较

// 防护措施:
// 1. 编译器警告:-Wall -Wextra 会警告 if (x = 5)
// 2. 使用 constexpr 和 auto 减少隐式转换
// 3. C++ 的条件必须是 bool 类型(不像 C 语言)

Demo:类型系统全览

// demo_1_4_types.cpp
#include <iostream>
#include <iomanip>
#include <sstream>
#include <array>
#include <string>
#include <string_view>
#include <cstdint>
#include <cstring>

// === 枚举演示 ===
// Java 对比:Java enum 是完整的类,可以有字段和方法
// C++ enum class 是类型安全的,但不能直接有方法
enum class Color : uint32_t {
    Red   = 0xFF0000,
    Green = 0x00FF00,
    Blue  = 0x0000FF
};

// 为 enum class 添加自定义格式化支持
std::string_view color_name(Color c) {
    switch (c) {
        case Color::Red:   return "Red";
        case Color::Green: return "Green";
        case Color::Blue:  return "Blue";
    }
    return "Unknown";
}

// === 类型大小演示 ===
void type_sizes_demo() {
    std::cout << "=== 类型大小 ===\n";
    std::cout << std::left;

    // Java 对比:Java 中所有类型大小是固定的
    // C++ 中,char/short/int/long 的大小是"实现定义的"
    // 使用 <cstdint> 中的定宽类型来保证跨平台一致性
    std::cout << std::setw(20) << "bool"       << sizeof(bool)        << " byte(s)\n";
    std::cout << std::setw(20) << "char"       << sizeof(char)        << " byte(s)\n";
    std::cout << std::setw(20) << "char16_t"   << sizeof(char16_t)    << " byte(s)\n";
    std::cout << std::setw(20) << "char32_t"   << sizeof(char32_t)    << " byte(s)\n";
    std::cout << std::setw(20) << "int8_t"     << sizeof(int8_t)      << " byte(s)\n";
    std::cout << std::setw(20) << "int16_t"    << sizeof(int16_t)     << " byte(s)\n";
    std::cout << std::setw(20) << "int32_t"    << sizeof(int32_t)     << " byte(s)\n";
    std::cout << std::setw(20) << "int64_t"    << sizeof(int64_t)     << " byte(s)\n";
    std::cout << std::setw(20) << "float"      << sizeof(float)       << " byte(s)\n";
    std::cout << std::setw(20) << "double"     << sizeof(double)      << " byte(s)\n";
    std::cout << std::setw(20) << "long"       << sizeof(long)        << " byte(s) (平台相关!)\n";
    std::cout << std::setw(20) << "long long"  << sizeof(long long)   << " byte(s)\n";
    std::cout << std::setw(20) << "size_t"     << sizeof(size_t)      << " byte(s)\n";
    std::cout << std::setw(20) << "指针"        << sizeof(void*)       << " byte(s)\n";
}

// === 枚举演示 ===
void enum_demo() {
    std::cout << "\n=== enum class ===\n";

    Color c = Color::Red;
    std::cout << "Color: " << color_name(c)
              << ", 值: 0x" << std::hex << std::setfill('0') << std::setw(6)
              << static_cast<uint32_t>(c) << std::dec << "\n";

    // enum class 不能隐式转为 int(类型安全)
    // int x = c;           // 编译错误!
    // int y = Color::Red;  // 编译错误!
    // 必须显式转换:
    uint32_t raw = static_cast<uint32_t>(c);
    std::cout << "显式转换后的值: " << raw << "\n";

    // enum class 可以用作 switch 的 case
    switch (c) {
        case Color::Red:   std::cout << "红色\n"; break;
        case Color::Green: std::cout << "绿色\n"; break;
        case Color::Blue:  std::cout << "蓝色\n"; break;
    }
}

// === 数组演示 ===
void array_demo() {
    std::cout << "\n=== std::array ===\n";

    // Java 对比:Java int[] arr = new int[5]; 在堆上分配
    // C++ std::array 在栈上分配(零开销抽象)
    std::array<int, 5> arr{};  // {} 确保零初始化!
    std::cout << "大小: " << arr.size() << "\n";
    std::cout << "初始值: ";
    for (int v : arr) std::cout << v << " ";
    std::cout << "\n";

    // 填充
    for (size_t i = 0; i < arr.size(); ++i) {
        arr[i] = static_cast<int>(i * i);
    }

    // 范围 for 循环(类似 Java 的 for-each)
    std::cout << "平方数: ";
    for (const auto& v : arr) {
        std::cout << v << " ";
    }
    std::cout << "\n";

    // 值语义:拷贝是深拷贝
    auto arr2 = arr;  // arr2 是 arr 的完整副本
    arr2[0] = 999;
    std::cout << "arr[0] = " << arr[0] << " (不变)\n";    // 0
    std::cout << "arr2[0] = " << arr2[0] << " (已修改)\n"; // 999
}

// === 字符串演示 ===
void string_demo() {
    std::cout << "\n=== std::string ===\n";

    // Java 对比:Java String 是不可变的,C++ std::string 是可变的
    std::string s = "Hello";
    s += " World";  // 原地修改,不创建新对象
    std::cout << "拼接: " << s << "\n";

    // 格式化(使用 ostringstream,GCC 11 兼容)
    std::ostringstream fmt_oss;
    fmt_oss << "整数: " << 42 << ", 浮点: " << std::fixed << std::setprecision(2) << 3.14159;
    std::string formatted = fmt_oss.str();
    std::cout << "格式化: " << formatted << "\n";

    // string_view:零拷贝的只读引用
    // Java 对比:类似 CharSequence 或 String 的子串视图
    std::string_view sv = s;
    std::cout << "string_view: " << sv << "\n";
    std::cout << "子串: " << sv.substr(0, 5) << "\n";

    // std::string 不会为 null
    // Java: String s = null; // 合法
    // C++: std::string s = null; // 编译错误
    // C++ 中空字符串用 "" 或 std::string{} 表示
    std::string empty{};
    std::cout << "空字符串长度: " << empty.length() << "\n";

    // 比较直接用 ==
    // Java: s1.equals(s2) 或 Objects.equals(s1, s2)
    // C++: s1 == s2(直接值比较)
    std::string a = "hello";
    std::string b = "hello";
    std::cout << "\"hello\" == \"hello\": " << std::boolalpha << (a == b) << "\n";
}

// === 布尔转换演示 ===
void bool_demo() {
    std::cout << "\n=== 布尔转换 ===\n";

    // C++ 中,整数可以隐式转为 bool
    // Java 中这是不允许的
    std::cout << "bool(0) = " << std::boolalpha << static_cast<bool>(0) << "\n";
    std::cout << "bool(1) = " << static_cast<bool>(1) << "\n";
    std::cout << "bool(42) = " << static_cast<bool>(42) << "\n";
    std::cout << "bool(-1) = " << static_cast<bool>(-1) << "\n";

    // bool 可以隐式转为 int
    std::cout << "int(true) = " << static_cast<int>(true) << "\n";
    std::cout << "int(false) = " << static_cast<int>(false) << "\n";

    // 危险:if 中的赋值
    int x = 5;
    if (x == 5) { std::cout << "比较: x 等于 5\n"; }
    // if (x = 0) { ... }  // 赋值!x 变为 0,条件为 false。-Wall 会警告
}

int main() {
    std::cout << "C++ 类型系统演示\n\n";

    type_sizes_demo();
    enum_demo();
    array_demo();
    string_demo();
    bool_demo();

    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_1_4_types.cpp -o demo_1_4 && ./demo_1_4

预期输出:

C++ 类型系统演示

=== 类型大小 ===
bool                 1 byte(s)
char                 1 byte(s)
char16_t             2 byte(s)
char32_t             4 byte(s)
int8_t               1 byte(s)
int16_t              2 byte(s)
int32_t              4 byte(s)
int64_t              8 byte(s)
float                4 byte(s)
double               8 byte(s)
long                 8 byte(s) (平台相关!)
long long            8 byte(s)
size_t               8 byte(s)
指针                  8 byte(s)

=== enum class ===
Color: Red, 值: 0x00ff0000
显式转换后的值: 16711680
红色

=== std::array ===
大小: 5
初始值: 0 0 0 0 0
平方数: 0 1 4 9 16
arr[0] = 0 (不变)
arr2[0] = 999 (已修改)

=== std::string ===
拼接: Hello World
格式化: 整数: 42, 浮点: 3.14
string_view: Hello World
子串: Hello
空字符串长度: 0
"hello" == "hello": true

=== 布尔转换 ===
bool(0) = false
bool(1) = true
bool(42) = true
bool(-1) = true
int(true) = 1
int(false) = 0
比较: x 等于 5

本节总结

概念Java/KotlinC++为什么重要
编译模型编译到字节码,JVM 运行直接编译到机器码理解编译-链接过程是调试的基础
内存管理GC 自动管理RAII 确定性销毁RAII 是 C++ 资源管理的核心范式
对象位置堆(GC 管理)栈(默认)或堆(显式选择)栈分配是 C++ 性能优势的来源之一
语义引用语义(默认)值语义(默认)影响拷贝、比较、函数参数传递的一切行为
未定义行为不存在(有异常/错误)编译器可做任何事UB 是 C++ bug 的最大来源,必须理解并使用工具防护
类型大小固定实现定义使用 <cstdint> 保证跨平台一致性

下一步:阅读 02-RAII 与资源管理,深入理解 C++ 如何在没有 GC 的情况下安全地管理所有资源。