第一部分: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/Kotlin | C++ |
|---|---|---|
| 编译产物 | .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++ 建议:能用
constexpr、inline、template的地方,就不要用#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/Kotlin | C++ | 生命周期 |
|---|---|---|---|
| 栈 (Stack) | 基本类型局部变量、对象引用 | 任意类型的局部变量(包括完整对象) | 函数调用/返回时自动创建/销毁 |
| 堆 (Heap) | 所有 new 创建的对象(由 GC 管理) | new/make_unique/make_shared 创建的对象 | 必须手动 delete 或通过智能指针自动释放 |
| 全局/静态区 | static 字段、类变量 | 全局变量、static 局部变量、static 成员 | 程序启动到结束 |
| 常量区 | String 常量池、static final | constexpr 变量、字符串字面量 | 程序启动到结束 |
| 代码区 | 方法字节码(方法区) | 机器码(.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 GC | C++ 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++ 标准对程序的行为不做任何保证。编译器可以:
- 假设 UB 不会发生,并基于这个假设进行优化(这是最危险的情况)
- 生成错误的机器码
- 看起来正常工作(直到某天换个编译器或优化级别就崩了)
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 |
| 空指针解引用 | 解引用 nullptr | 抛 NullPointerException |
| 有符号整数溢出 | 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]
基本类型大小对比
| 类型 | Java | Kotlin | C++ (常见) | 说明 |
|---|---|---|---|---|
| 布尔 | 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 enum | C++ 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 String | C++ std::string |
|---|---|---|
| 不可变性 | 不可变(immutable) | 可变(mutable) |
| 编码 | UTF-16 | UTF-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/Kotlin | C++ | 为什么重要 |
|---|---|---|---|
| 编译模型 | 编译到字节码,JVM 运行 | 直接编译到机器码 | 理解编译-链接过程是调试的基础 |
| 内存管理 | GC 自动管理 | RAII 确定性销毁 | RAII 是 C++ 资源管理的核心范式 |
| 对象位置 | 堆(GC 管理) | 栈(默认)或堆(显式选择) | 栈分配是 C++ 性能优势的来源之一 |
| 语义 | 引用语义(默认) | 值语义(默认) | 影响拷贝、比较、函数参数传递的一切行为 |
| 未定义行为 | 不存在(有异常/错误) | 编译器可做任何事 | UB 是 C++ bug 的最大来源,必须理解并使用工具防护 |
| 类型大小 | 固定 | 实现定义 | 使用 <cstdint> 保证跨平台一致性 |
下一步:阅读 02-RAII 与资源管理,深入理解 C++ 如何在没有 GC 的情况下安全地管理所有资源。