
AI 编程时代,开发者的技术边界正在被重新定义。作为一个前端开发者,我最近在跨平台框架项目中需要参与 C++ 模块的 Code Review 和协同开发。这篇文章记录了我从 JavaScript/Kotlin 背景切入 C++ 的学习路径——不追求系统性,只追求快速建立可工作的心智模型。
前端开发者学 C++ 最大的障碍不是语法,而是思维模式的切换。
在 JS/Kotlin 的世界里,你创建一个对象后就不用管它了:
// JS — 创建完就忘,GC 帮你收拾
function createUser() {
const user = { name: "Tom", age: 25 };
return user; // 用完了 GC 自动回收
}但在 C++ 中,每一个对象都需要有明确的"主人"——谁创建了它,谁负责销毁它:
// C++ — 你必须为每个对象安排归宿
User* user = new User("Tom", 25); // 你创建了它
// ... 使用 user ...
delete user; // 你必须销毁它,否则内存泄漏好消息是,现代 C++ 已经有了"半自动"方案——智能指针。这是你学 C++ 最该先掌握的东西。
// C++
#include <memory>
auto node = std::make_shared<TreeNode>(1, "Button");
auto alias = node; // 引用计数 +1,两个变量指向同一个对象
// 当 node 和 alias 都离开作用域时,对象才被销毁// JS 等价 — 你从来不需要想这些,GC 自动处理
const node = new TreeNode(1, "Button");
const alias = node;// Kotlin 等价
val node = TreeNode(1, "Button")
val alias = node要点: shared_ptr 内部维护一个引用计数器。每次拷贝 +1,每次销毁 -1,归零时自动 delete。
// C++
auto cache = std::make_unique<CacheData>();
// auto copy = cache; // ❌ 编译错误!unique_ptr 不能拷贝
auto moved = std::move(cache); // ✅ 所有权转移,cache 变成 nullptr类比理解:就像你把一个快递单号给了别人,你手里的单号就作废了。
// C++
std::weak_ptr<RenderView> view_ref; // 弱引用,不阻止对象销毁
if (auto view = view_ref.lock()) { // 尝试获取强引用
view->Render(); // 成功:对象还活着
}
// 失败:对象已被销毁,lock() 返回 nullptr// Kotlin
val viewRef = WeakReference(renderView)
viewRef.get()?.render() // 对象被回收了就返回 null// JS — WeakRef (ES2021)
const viewRef = new WeakRef(renderView);
viewRef.deref()?.render();智能指针 | 口诀 | 场景 |
|---|---|---|
| "大家一起用" | 节点树、被多处引用的对象 |
| "只有我能用" | 缓存数据、工厂创建的对象 |
| "我看看你还在不在" | 回调中引用父对象、避免循环引用 |
C++ 的 std::optional 对应 Kotlin 的 ? 和 JS 的 undefined:
// C++
std::optional<int> parentId; // 没有值
parentId = 42; // 有值
if (parentId.has_value()) {
int id = parentId.value(); // 取值
}
parentId = std::nullopt; // 置空// Kotlin
var parentId: Int? = null
parentId = 42
parentId?.let { id -> /* 使用 id */ }
parentId = null// JS
let parentId = undefined;
parentId = 42;
if (parentId !== undefined) { /* 使用 parentId */ }
parentId = undefined;C++ 把"声明"和"实现"分开,这对前端开发者最反直觉:
// TreeNode.h — 头文件(声明):"我长什么样"
class TreeNode {
public:
int tag;
std::string name;
void AddChild(std::shared_ptr<TreeNode> child, int index);
private:
std::shared_ptr<Prop> FindProp(const std::string& key) const;
};
// TreeNode.cpp — 源文件(实现):"我怎么工作"
void TreeNode::AddChild(std::shared_ptr<TreeNode> child, int index) {
children.insert(children.begin() + index, child);
child->parentTag = tag;
}// Kotlin — 一个文件搞定
class TreeNode(var tag: Int, var name: String) {
fun addChild(child: TreeNode, index: Int) {
children.add(index, child)
child.parentTag = tag
}
private fun findProp(key: String): Prop? { ... }
}为什么要分两个文件? 因为 C++ 的编译模型:编译器处理每个 .cpp 文件时需要知道其他类"长什么样"(靠 #include 头文件),但不需要知道"怎么工作"。这样可以加速编译——修改实现时只重新编译一个 .cpp,不影响其他文件。
关键语法:
public: / private: = Kotlin 的 public / privateTreeNode::AddChild 中的 :: 表示"属于",相当于"TreeNode 类的 AddChild 方法"const 出现在方法末尾(如 FindProp(...) const)= 承诺此方法不修改成员变量这是 C++ 代码中最密集的符号,前端开发者容易懵:
void Process(const std::string& name); // const 引用
void Modify(std::string& name); // 可变引用
void Consume(std::string&& name); // 右值引用(移动语义)用餐厅类比:
写法 | 类比 | 含义 |
|---|---|---|
| 打包一份外卖带走 | 完整拷贝,互不影响 |
| 在餐厅里看菜单拍照 | 借来看看,不拷贝不修改 |
| 去后厨帮厨 | 直接在原数据上修改 |
| 把整个餐车推走 | 转移所有权,原来的变空 |
// JS 开发者理解:
// JS 中对象天然是引用传递,基本类型是值传递
// C++ 默认全是值传递(拷贝),加 & 才是引用传递实战中最常见的是 const &,约 80% 的函数参数都用它——既避免拷贝开销,又保证不被修改。
// C++
auto callback = [this, count](const std::string& msg) {
std::cout << name << ": " << msg << " (" << count << ")" << std::endl;
};// JS — 自动捕获,不需要声明
const callback = (msg) => {
console.log(`${this.name}: ${msg} (${count})`);
};// Kotlin — 也是自动捕获
val callback = { msg: String ->
println("${name}: $msg ($count)")
}关键差异:C++ lambda 必须用 [] 显式声明要捕获哪些变量。
捕获方式 | 含义 |
|---|---|
| 捕获当前对象指针 |
| 按值拷贝 |
| 按引用捕获 |
| 按值拷贝所有外部变量 |
| 按引用捕获所有外部变量(最危险) |
C++ | JS | Kotlin | 说明 |
|---|---|---|---|
|
|
| 动态数组 |
|
|
| 哈希表 |
|
|
| 哈希集合 |
|
|
| 字符串 |
|
|
| 二元组 |
常用操作速查:
// C++ vector
std::vector<int> nums;
nums.push_back(42); // JS: nums.push(42)
nums.insert(nums.begin() + 2, 99); // JS: nums.splice(2, 0, 99)
nums.erase(nums.begin() + 2); // JS: nums.splice(2, 1)
nums.size(); // JS: nums.length
nums.empty(); // JS: nums.length === 0
nums.reserve(100); // JS: 无对应,引擎自动管理
// C++ unordered_map
std::unordered_map<std::string, int> ages;
ages["Tom"] = 25; // JS: ages["Tom"] = 25 或 ages.set("Tom", 25)
auto it = ages.find("Tom"); // JS: ages.has("Tom")
if (it != ages.end()) {
int age = it->second; // JS: ages.get("Tom")
}
ages.erase("Tom"); // JS: ages.delete("Tom") 或 delete ages["Tom"]// C++ — auto 推导
auto node = std::make_shared<TreeNode>(1, "Button"); // 类型: shared_ptr<TreeNode>
auto it = nodeMap.find(tag); // 类型: unordered_map<...>::iterator
auto [updated, hasWatch] = DiffTrees(oldTree, newTree); // C++17 解构绑定// JS — 天生弱类型
const node = new TreeNode(1, "Button");
const entry = nodeMap.get(tag);
const [updated, hasWatch] = diffTrees(oldTree, newTree);// Kotlin — 类型推导
val node = TreeNode(1, "Button")
val entry = nodeMap[tag]
val (updated, hasWatch) = diffTrees(oldTree, newTree)// C++ 接口(纯虚类)
class IRenderLayer {
public:
virtual void CreateView(int tag, const std::string& name) = 0; // = 0 纯虚
virtual void Destroy() = 0;
virtual ~IRenderLayer() = default; // 虚析构函数(接口必备)
};
// C++ 实现
class ProxyRenderLayer : public IRenderLayer {
void CreateView(int tag, const std::string& name) override { ... }
void Destroy() override { ... }
};// Kotlin
interface IRenderLayer {
fun createView(tag: Int, name: String)
fun destroy()
}
class ProxyRenderLayer : IRenderLayer {
override fun createView(tag: Int, name: String) { ... }
override fun destroy() { ... }
}// JS — 没有接口,靠鸭子类型
class ProxyRenderLayer {
createView(tag, name) { ... }
destroy() { ... }
}关键词对照:
C++ | Kotlin | JS |
|---|---|---|
|
| 无 |
|
| 无 |
|
|
|
|
| 无(靠 |
// C++
try {
auto node = Deserialize(data, offset);
} catch (const std::exception& e) {
// e.what() 获取错误信息
return nullptr;
} catch (...) {
// 捕获所有异常(包括非标准异常)
return nullptr;
}// Kotlin
try {
val node = deserialize(data, offset)
} catch (e: Exception) {
return null
}// JS
try {
const node = deserialize(data, offset);
} catch (e) {
return null;
}catch (...) 是 C++ 特有的"兜底捕获",对应 Kotlin 的 catch (e: Throwable) 或 JS 的 catch (e)。
// C++ — RAII 锁(作用域结束自动解锁)
std::mutex mutex;
void WriteFile(const std::vector<uint8_t>& data) {
std::lock_guard<std::mutex> lock(mutex);
// ... 线程安全操作 ...
} // ← lock 离开作用域,自动解锁// Kotlin
private val mutex = Any()
fun writeFile(data: ByteArray) {
synchronized(mutex) {
// ... 线程安全操作 ...
}
}// JS — 单线程模型,通常不需要锁
// Web Worker 间通过 postMessage 通信,无共享内存// C++
class CacheManager {
static std::mutex fileMutex; // 所有实例共享一把锁
static std::string BuildCacheKey(const std::string& page, const std::string& key);
};
// .cpp 中定义(static 成员必须在类外定义一次)
std::mutex CacheManager::fileMutex;// Kotlin
class CacheManager {
companion object {
private val fileMutex = Mutex()
fun buildCacheKey(page: String, key: String): String { ... }
}
}// JS
class CacheManager {
static fileMutex = new Mutex(); // 提案 stage 3
static buildCacheKey(page, key) { ... }
}// C++ — 冒号后面的部分是"初始化列表"
ProxyRenderLayer::ProxyRenderLayer()
: realLayer(std::make_shared<RenderLayer>()),
rootNode(std::make_shared<TreeNode>(-1, "Root")) {
// 构造函数体(可选的额外逻辑)
}// Kotlin — 直接在属性声明时初始化
class ProxyRenderLayer {
private val realLayer = RenderLayer()
private val rootNode = TreeNode(-1, "Root")
}初始化列表的优势:在对象创建的第一时间就赋值,比在构造函数体内赋值更高效(避免先默认构造再赋值)。
// C++17 variant — 类型安全的联合体
std::variant<
std::monostate, // 空状态
std::shared_ptr<Value>, // 普通值
std::function<void(Result)>, // 回调函数
std::shared_ptr<ShadowNode> // 布局节点
> propValue;
// 类型判断
if (std::holds_alternative<std::shared_ptr<Value>>(propValue)) {
auto val = std::get<std::shared_ptr<Value>>(propValue);
}// Kotlin sealed class
sealed class PropValue {
object Empty : PropValue()
data class AnyValue(val value: Value) : PropValue()
data class Callback(val fn: (Result) -> Unit) : PropValue()
data class Shadow(val node: ShadowNode) : PropValue()
}
when (propValue) {
is PropValue.AnyValue -> { val v = propValue.value }
...
}// TypeScript 联合类型
type PropValue =
| { type: "empty" }
| { type: "value"; value: Value }
| { type: "callback"; fn: (result: Result) => void }
| { type: "shadow"; node: ShadowNode };在真实项目中你会反复遇到这些代码模式,贴在手边备查:
C++ 代码 | 含义 | JS/Kotlin 心智模型 |
|---|---|---|
| 引用取元素,不拷贝 |
|
| 指针/智能指针为空 |
|
| 通过指针调方法 |
|
| 解引用取值 |
|
| 显式类型转换 |
|
| 转移所有权 | 无对应(GC 管) |
| 编译时常量 |
|
| 函数类型 |
|
| 导入头文件 |
|
| 命名空间 |
|
以上并不是一份完整的 C++ 教程,而是我作为前端开发者在实际工作中最高频使用的知识子集。掌握这些内容后,你就能看懂大部分现代 C++ 项目代码,足以参与 Code Review 和协同开发。
AI 编程时代,AI 可以帮你写出 C++ 代码,但你至少要能读懂它、审查它。希望这篇文章能帮你迈出这一步。
下一篇文章我们聊聊更实战的话题:C++ 中那些前端开发者难以发现的 Bug。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。