
好继续。现在进入C17 核心基础。这一块我们不从“什么是类、什么是变量”这种最基础的内容开始而是直接围绕你简历里真正会用到的C17 ↓ 对象生命周期 ↓ RAII ↓ 智能指针 ↓ 接口 / 抽象类 ↓ Strategy / Adapter ↓ 多线程 ↓ 锁 ↓ 条件变量 ↓ 队列 ↓ 异常先把整个体系建立起来。一、先理解为什么机器人项目特别依赖 C你的机器人程序里有很多东西UDP Socket ZMQ Protobuf GMR EtherCAT 线程 队列 ROS2 传感器 电机这些东西都有一个共同特点资源很多而且生命周期很重要。例如Socket 线程 内存 文件 ZMQ Context ZMQ Socket EtherCAT Master EtherCAT Domain如果程序员没有管理好生命周期就容易出现内存泄漏 野指针 重复释放 线程没退出 Socket没关闭 资源提前释放 程序崩溃所以 C 最核心的问题之一其实不是“怎么写语法”。而是谁创建谁使用什么时候销毁二、先搞懂“生命周期”比如void test() { int x 10; }程序执行进入 test ↓ 创建 x ↓ 使用 x ↓ 离开 test ↓ x 销毁这就是对象生命周期。三、对象生命周期为什么重要假设class Robot { public: Robot() { // 初始化 } ~Robot() { // 清理 } };使用void test() { Robot robot; }执行Robot robot; ↓ 构造函数 ↓ 对象开始存在 ↓ 使用 ↓ 离开作用域 ↓ 析构函数 ↓ 对象销毁所以构造函数 创建/初始化资源 析构函数 释放/清理资源这是后面 RAII 的基础。四、RAII是什么这个词你简历里非常重要。全称Resource Acquisition Is Initialization不用死记英文。直接记一句资源跟着对象走对象死了资源自动释放。例如{ std::mutex mutex; // 使用 mutex } // 离开作用域 // 自动释放更典型{ std::lock_guardstd::mutex lock(mutex); // 临界区 } // 自动 unlock你甚至不需要mutex.lock(); ... mutex.unlock();因为lock_guard创建 ↓ 自动加锁 ↓ 执行代码 ↓ 离开作用域 ↓ 析构 ↓ 自动解锁这就是 RAII。五、为什么RAII特别适合机器人程序因为机器人程序有很多资源。比如程序启动 ↓ 打开Socket ↓ 创建线程 ↓ 创建ZMQ ↓ 连接EtherCAT ↓ 运行如果中间出现异常程序异常怎么办如果全部靠人工open(); ... close();很容易漏。RAII则是对象创建 ↓ 资源获取 ↓ 使用 ↓ 对象销毁 ↓ 资源自动释放所以 C 工程里非常强调 RAII。六、智能指针就是RAII思想的一个重要应用你应该见过std::unique_ptr std::shared_ptr std::weak_ptr先别急着背区别。先理解普通指针 ↓ 我自己管理内存 智能指针 ↓ 对象帮助我管理内存七、普通new有什么问题例如Robot* robot new Robot();你必须delete robot;否则内存泄漏如果中途return;或者throw exception;就很容易忘记释放。八、unique_ptr现代 C 更推荐auto robot std::make_uniqueRobot();它表示这个对象只有一个主人。例如robot_controller ↓ unique_ptr ↓ Robot当robot_controller销毁unique_ptr销毁 ↓ Robot自动销毁不用自己delete九、为什么叫unique因为unique_ptr强调唯一拥有者。不能这样auto p1 std::make_uniqueRobot(); auto p2 p1; // 错因为一个对象不能同时拥有两个unique_ptr。但是可以auto p2 std::move(p1);意思p1 ↓ 所有权转移 ↓ p2转移之后p1 空 p2 Robot十、这个“所有权”概念非常重要以后看到std::move()不要第一反应认为“把对象移动一下。”更重要的是可能发生所有权转移。比如Controller │ │ unique_ptr ↓ Robot把它移动给Manager变成Controller Manager │ │ × ↓ Robot所有权发生了变化。十一、shared_ptr如果一个对象确实需要多个地方共同拥有auto robot std::make_sharedRobot();可以┌── Controller │ Robot ←───┼── Manager │ └── Monitor多个shared_ptrController ↓ shared_ptr ↘ Robot ↗ shared_ptr ↑ Manager它内部会维护引用计数。例如Robot 引用计数 3三个shared_ptr都存在。销毁一个3 → 2再销毁2 → 1最后一个消失1 → 0于是Robot ↓ 自动销毁十二、unique_ptr和shared_ptr怎么选先记一个非常实用的原则默认优先 unique_ptr只有真正存在多个所有者的时候再考虑shared_ptr不要因为“shared_ptr比较方便”就到处使用。因为 shared_ptr 会带来引用计数 所有权关系复杂 生命周期不容易看清 循环引用风险十三、weak_ptr是什么它解决一个典型问题我想观察一个对象但我不拥有它。例如Manager ↓ shared_ptr Robot ↑ MonitorMonitor只是想“看看Robot还在不在。”不应该因此增加一个拥有关系。所以可以std::weak_ptrRobot理解成shared_ptr 我拥有你 weak_ptr 我只是看看你十四、一个非常重要的生命周期问题假设A shared_ptr → B B shared_ptr → A变成A ←→ B即使外部已经没有shared_ptrA和B互相引用引用计数不会变成0于是A没释放 B没释放这就是循环引用。所以shared_ptr weak_ptr经常一起出现。十五、现在进入接口 / 抽象类这与你之前的MotionInput XsensUdpSource NoitomMocapApiSource直接对应。你之前的设计可以理解为MotionInput ↑ ┌─────────┴─────────┐ │ │ XsensUdpSource NoitomMocapApiSourceMotionInput是接口。例如class MotionInput { public: virtual NoitomFrame receive() 0; virtual ~MotionInput() default; };这里 0表示纯虚函数。于是MotionInput成为抽象类。不能直接MotionInput input;十六、为什么要接口因为你的上层程序根本不应该关心到底是Xsens 还是Noitom 还是以后换成别的设备上层只关心MotionInput ↓ receive() ↓ NoitomFrame于是MotionInput ↑ ┌─────────────┼─────────────┐ ↓ ↓ ↓ Xsens Noitom Future这就是解耦。十七、这就是你简历里的Adapter思想假设 Xsens 原始数据XsensDataNoitomNoitomData但 GMR 只接受NoitomFrame怎么办做 AdapterXsens原始数据 ↓ XsensUdpSource ↓ Adapter ↓ NoitomFrame另一边Noitom API ↓ NoitomMocapApiSource ↓ NoitomFrame于是 GMR 完全不用知道输入到底是谁。十八、Strategy又是什么Strategy主要解决同一个任务有不同算法/实现方式。例如MotionInput可以看成策略。┌─────────────────────┐ │ MotionInput Strategy│ └──────────┬──────────┘ │ ┌─────┴─────┐ ↓ ↓ Xsens Noitom上层std::unique_ptrMotionInput input;运行时选择Xsens或者选择Noitom上层代码不需要修改。这就是 Strategy 的核心价值把“变化的东西”独立出来。十九、Adapter和Strategy千万别混你可以这样区分Adapter解决接口不一样怎么接起来A接口 ↓ Adapter ↓ B接口Strategy解决同一个功能有多种实现运行时选择哪一个统一接口 ↓ 策略A 策略B 策略C你的动捕系统里两者可能同时存在。二十、现在把C核心思想串起来你现在看到C17 │ ├── OOP │ ├── 封装 │ ├── 继承 │ └── 多态 │ ├── 生命周期 │ ├── RAII │ ├── 智能指针 │ ├── unique_ptr │ ├── shared_ptr │ └── weak_ptr │ ├── 接口 │ ├── Strategy │ └── Adapter实际上不是一堆零散知识。它们最终都是在解决大型机器人程序怎么做到模块化、安全、可替换、生命周期清晰。二十一、拿你的项目再看一次你的程序可以抽象成Controller │ unique_ptr ↓ MotionInput ↑ ┌───────────┴───────────┐ │ │ XsensAdapter NoitomSource │ │ └───────────┬───────────┘ ↓ NoitomFrame ↓ GMR ↓ MocapFrame ↓ Protobuf ↓ ZMQ ↓ MuJoCo这时候你就应该理解为什么要接口为什么要unique_ptr为什么要Adapter为什么要Strategy而不是把所有代码写进一个main.cpp二十二、这一节先记住6句话① 生命周期对象什么时候创建、什么时候销毁。② RAII资源跟着对象生命周期走。③ unique_ptr一个明确的所有者。④ shared_ptr多个所有者共同管理。⑤ 接口上层只依赖能力不依赖具体实现。⑥ Strategy / AdapterStrategy解决“实现可替换”Adapter解决“接口不兼容”。下一节我们继续C多线程线程 ↓ std::thread ↓ 生产者/消费者 ↓ mutex ↓ lock_guard ↓ unique_lock ↓ condition_variable ↓ 线程安全队列 ↓ 线程退出 ↓ 生命周期这一块会直接把你之前项目中的UDP接收线程 ↓ 队列 ↓ GMR线程 ↓ ZMQ线程彻底串起来。好继续。现在进入C 多线程 锁 条件变量 生产者消费者。这部分和你之前做的机器人实时数据链路直接对应所以我们重点理解为什么这么设计不陷入特别底层的线程实现。一、先看你的机器人程序为什么需要多个线程假设你的系统只有一个线程main ↓ 接收UDP ↓ 解析动捕 ↓ GMR ↓ Protobuf ↓ ZMQ ↓ MuJoCo ↓ 再接收UDP问题是如果 GMR 计算用了20ms那么这20ms里UDP接收可能就没办法及时处理。所以通常会拆UDP线程 ↓ Queue ↓ GMR线程 ↓ Queue ↓ ZMQ线程这就是多线程最基本的意义让不同任务可以同时进行。二、std::threadC创建线程最基本的方式#include thread void receive() { // 接收数据 } int main() { std::thread t(receive); t.join(); }这里std::thread t(receive);就是创建一个线程执行receive()。三、join是什么这是线程生命周期里非常重要的一个概念。t.join();意思当前线程等待 t 线程执行结束。例如主线程 │ ├── 创建线程 │ ↓ │ t │ ├── 等待 │ ←──── t执行结束 │ 继续执行所以join 等它结束四、detach是什么还有t.detach();意思让线程独立运行不再由当前线程等待。但是对于工程程序尤其机器人控制程序不要随便 detach。因为你很容易遇到主对象已经销毁 ↓ 后台线程还在运行 ↓ 线程访问已经不存在的对象 ↓ 崩溃所以实际工程里通常更关注线程怎么启动 线程怎么退出 线程怎么等待 对象什么时候销毁而不是随便开后台线程。五、真正的问题来了共享数据假设UDP线程 ↓ shared_data ↑ GMR线程两个线程同时访问NoitomFrame frame;比如 UDP线程frame new_frame;GMR线程use(frame);这时候就出现数据竞争data race。六、为什么数据竞争危险例如线程A正在修改 frame 线程B同时读取 frame可能出现A修改到一半 ↓ B读取 ↓ 拿到一个“不完整”的数据对于机器人关节1 关节2 关节3 ...如果只更新了一半就可能产生非常奇怪的问题。所以需要锁。七、mutex是什么std::mutex mutex;它可以理解成一把钥匙。某个线程拿到钥匙线程A ↓ lock ↓ 拿到钥匙 ↓ 访问共享数据 ↓ unlock此时线程B想访问 ↓ 发现钥匙没了 ↓ 等待八、最基本的写法std::mutex mtx; mtx.lock(); shared_data new_data; mtx.unlock();但实际工程中不推荐这样裸写。因为mtx.lock(); do_something(); mtx.unlock();如果do_something();中间发生异常throw那么unlock()可能根本执行不到。结果死锁。这就是 RAII 再次发挥作用的地方。九、lock_guard推荐std::lock_guardstd::mutex lock(mtx); shared_data new_data;执行lock_guard创建 ↓ 自动lock ↓ 执行临界区 ↓ 离开作用域 ↓ lock_guard析构 ↓ 自动unlock这就是RAII 多线程。所以你前面学的 RAII 并不是孤立的。十、什么叫临界区例如{ std::lock_guardstd::mutex lock(mtx); shared_data new_data; }这里shared_data new_data;就是临界区。意思同一时间只允许一个线程操作这部分共享数据。原则锁的范围不要太大。不要lock(); 做大量计算 ↓ GMR ↓ 网络 ↓ 文件IO ↓ unlock();这样其他线程会一直等。应该加锁 ↓ 快速读写共享数据 ↓ 解锁 ↓ 耗时计算十一、unique_lock是什么你还会看到std::unique_lockstd::mutex lock(mtx);和lock_guard类似也是 RAII。区别先简单记lock_guard → 简单加锁 unique_lock → 更灵活比如条件变量std::unique_lockstd::mutex lock(mtx); cv.wait(lock);所以你以后看到condition_variable unique_lock不要觉得奇怪。这是标准组合。十二、为什么需要条件变量现在来到机器人程序非常核心的一个问题。假设UDP线程不停往队列放数据Frame 1 Frame 2 Frame 3 ...而GMR线程需要从队列拿。如果队列为空GMR线程怎么办最笨的方法while (queue.empty()) { // 什么也不干 }这叫忙等busy waiting。CPU会一直转。十三、条件变量解决什么条件变量std::condition_variable cv;它可以让线程没数据的时候睡觉有数据的时候叫醒。生产者UDP线程 ↓ push(frame) ↓ notify_one()消费者GMR线程 ↓ 没有数据 ↓ wait() ↓ 睡眠收到通知UDP线程 ↓ notify_one() ↓ GMR线程被唤醒 ↓ 取数据十四、标准生产者消费者模型这就是你项目非常典型的结构Producer UDP接收线程 │ ↓ ┌──────────────┐ │ Queue │ └──────┬───────┘ ↓ Consumer GMR线程代码结构大概std::queueFrame queue; std::mutex mtx; std::condition_variable cv;生产者{ std::lock_guardstd::mutex lock(mtx); queue.push(frame); } cv.notify_one();消费者std::unique_lockstd::mutex lock(mtx); cv.wait(lock, [] { return !queue.empty(); }); auto frame queue.front(); queue.pop();这里最重要的是理解流程不是现在背代码。十五、为什么wait里面要写条件你经常会看到cv.wait(lock, [] { return !queue.empty(); });意思只要队列为空就继续等队列有数据了才能继续。实际上wait ↓ 睡眠 ↓ 被唤醒 ↓ 检查条件 ↓ 如果还是没有数据 ↓ 继续等待这就是为什么不能简单理解成notify 一定有数据更准确的是notify只是通知你“可能有变化了”醒来后还要重新检查条件。十六、这和你的50Hz动捕系统怎么对应你的动捕数据50 Hz也就是大约20 ms来一帧。可以设计Xsens ↓ UDP Receive Thread ↓ Frame Queue ↓ GMR Thread ↓ MocapFrame ↓ ZMQ Thread ↓ MuJoCo每20msFrame ↓ 进入队列GMR线程等待 ↓ 收到Frame ↓ 处理 ↓ 发送 ↓ 继续等待十七、为什么还需要“有界队列”这是实时系统很重要的一点。假设生产者50帧/s 消费者30帧/s如果queue无限大那么1秒 → 20帧积压 10秒 → 200帧 1分钟 → 1200帧最后延迟越来越大。虽然没有丢数据但机器人已经在处理“过去的数据”。这对实时控制反而更糟。十八、所以实时机器人经常需要有界队列例如Queue capacity 10最多10帧满了以后怎么办根据业务可以丢最旧或者丢最新或者阻塞生产者对于实时机器人“保留最新状态”通常比“保证每一帧都处理”更重要。但具体策略要看业务不能一概而论。十九、为什么你以前的“丢帧测试”非常重要你之前分析7746 7747 7748连续缺失。现在把它放回整个系统UDP ↓ Frame ID ↓ Queue ↓ GMR ↓ ZMQ ↓ MuJoCo出现7746 7747 7748你就应该问是UDP没收到 ↓ 还是Queue丢了 ↓ 还是GMR没处理 ↓ 还是ZMQ没收到 ↓ 还是MuJoCo没消费这就是多线程 队列 Frame ID 日志结合起来之后真正的工程排障能力。二十、线程退出问题还有一个非常容易被忽略的问题while (true) { ... }这种线程怎么办程序退出时主线程退出但UDP线程 GMR线程 ZMQ线程还在运行。所以实际工程需要线程生命周期管理。常见思路std::atomicbool running{true};线程while (running) { ... }退出running false;然后thread.join();最终停止信号 ↓ 线程退出循环 ↓ join ↓ 线程结束 ↓ 对象销毁这就是一个完整的生命周期。二十一、把你简历上的多线程全部串起来现在你应该能看到C RAII ↓ 锁的自动释放 ↓ 多线程安全 std::thread ↓ 并行执行 mutex ↓ 保护共享数据 condition_variable ↓ 线程等待/唤醒 queue ↓ 线程之间传递数据 bounded queue ↓ 防止无限堆积 atomicbool ↓ 线程停止信号 join ↓ 等待线程安全退出这其实就是一个完整的生产者—消费者架构。二十二、最终形成你的机器人线程模型你以后看到类似工程可以先在脑子里画┌─────────────────┐ │ UDP Receive │ │ Thread │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ Bounded Queue │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ GMR / IK Thread │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ Protobuf │ └────────┬────────┘ │ ↓ ┌─────────────────┐ │ ZMQ Thread │ └────────┬────────┘ │ ↓ MuJoCo旁边还有mutex condition_variable atomicbool join负责保证整个系统能够安全运行 ↓ 高效等待 ↓ 控制队列 ↓ 正常停止这一节你只需要真正记住 8 个东西std::thread ↓ 创建线程 mutex ↓ 保护共享数据 lock_guard ↓ 简单、安全地加锁 unique_lock ↓ 更灵活的锁常配合条件变量 condition_variable ↓ 没数据就睡有数据再醒 queue ↓ 线程之间传递数据 atomic ↓ 线程停止标志等简单共享状态 join ↓ 等待线程结束多线程这一块的核心框架已经建立起来了。下一节我们把最后几个 C 工程必备点补完异常处理 ↓ try / catch ↓ 异常安全 ↓ 对象生命周期 ↓ 构造/析构顺序 ↓ 接口析构函数 ↓ RAII 智能指针 线程然后C核心部分就基本完成再进入你的Python 全栈部分。继续。根据你简历原文这一轮我们把C 最后几个工程核心点补完然后就进入 Python。你简历明确写了 C17、RAII、智能指针、多线程同步、生产者-消费者、有界队列以及“排空退出与异常联动停止”。C 最后一部分异常、生命周期、线程安全退出一、异常到底解决什么问题先看一个最简单的例子void run() { connect_device(); if (连接失败) { // 怎么办 } start_motor(); }如果设备连接失败程序需要把错误向上层传递。C可以使用throw例如if (!connected) { throw std::runtime_error(device connect failed); }然后上层try { run(); } catch (const std::exception e) { std::cout e.what() std::endl; }整体底层 ↓ 发现错误 ↓ throw ↓ 向上层传播 ↓ catch ↓ 统一处理二、为什么机器人程序需要异常你的项目里面有很多可能失败的地方UDP连接 ZMQ Protobuf EtherCAT 设备初始化 配置文件 线程 文件 测试任务 SSH例如EtherCAT初始化失败 ↓ 抛出异常 ↓ 控制线程停止 ↓ 电机进入安全状态 ↓ 上层报告错误这就是所谓异常联动停止。而你的简历确实明确写到了这一点。三、异常不是“程序崩溃”这是初学者特别容易误解的地方。throw std::runtime_error(error);并不意味着程序一定马上崩溃。如果有人接住try { ... } catch (const std::exception e) { ... }那么程序可以继续按照设计处理。例如EtherCAT线程 ↓ 发生异常 ↓ 通知控制器 ↓ 停止相关线程 ↓ 记录日志 ↓ 关闭设备 ↓ 输出测试失败这反而是比较正规的工程处理方式。四、异常和RAII为什么经常一起出现这是 C 很重要的一组组合。例如void test() { std::lock_guardstd::mutex lock(mutex); do_something(); }假设do_something();突然throw ...那么虽然函数提前退出throw ↓ 离开作用域 ↓ lock_guard析构 ↓ 自动unlock所以不会因为异常导致锁永远不释放。这就是RAII 异常 异常安全五、智能指针也是同样的道理例如void test() { auto robot std::make_uniqueRobot(); do_something(); }如果do_something()发生异常throw ↓ 离开test ↓ robot析构 ↓ Robot析构 ↓ 资源释放所以现代 C 很重要的一种思想就是尽量让资源管理依赖对象生命周期而不是依赖人工记忆。六、这就是为什么你简历里的RAII非常重要你的简历不是简单写了“会C”。而是明确写C17 RAII 智能指针 多线程同步 生产者-消费者 有界队列这些实际上是一个完整的现代 C 工程体系。可以串成C对象 ↓ 生命周期 ↓ RAII ↓ 智能指针 ↓ 资源自动释放 ↓ 异常安全 ↓ 多线程安全七、再看线程退出机器人程序最怕这种代码void worker() { while (true) { process(); } }因为while(true)意味着永远不退出。如果主程序要关闭怎么办所以应该有std::atomicbool running{true};线程void worker() { while (running) { process(); } }停止running false;然后worker_thread.join();八、但是你的项目还有一个问题如果线程正在condition_variable.wait(...)那么running false;可能还不够。因为线程正在睡觉。所以停止的时候running false; cv.notify_all();然后线程被唤醒 ↓ 检查running ↓ 发现false ↓ 退出 ↓ join这就是正规的线程退出机制。九、为什么你的“排空退出”很重要你的简历明确提到了排空退出。这个词非常值得理解。假设接收线程 ↓ Queue ↓ 处理线程现在系统收到停止命令不一定意味着马上把Queue全部丢掉可能需要停止接收新数据 ↓ 允许已有数据继续处理 ↓ Queue逐渐变空 ↓ 处理线程退出 ↓ 系统关闭就是停止生产 ↓ 消费剩余数据 ↓ Queue empty ↓ 退出这叫排空退出drain shutdown。十、为什么机器人系统可能需要排空例如Frame 100 Frame 101 Frame 102 Frame 103已经进入队列。如果突然停止直接clear(queue)那么这些数据全部消失。但有些场景需要停止接收新的Frame ↓ 把已有任务处理完 ↓ 再关闭这取决于业务。十一、有界队列再回来看你简历还明确写了有界队列。为什么不是普通std::queue一直往里面塞因为生产速度 消费速度就会Queue ↓ 越来越大 ↓ 延迟越来越高 ↓ 内存越来越多实时机器人最危险的不是单纯“少处理一帧”。而是机器人一直在处理很久以前的数据。所以需要最大容量例如Queue capacity 10超过以后按照系统策略丢旧或者丢新或者阻塞生产者十二、背压是什么你简历里还有一个很重要的工程词背压。假设Producer 50 frame/s而Consumer 30 frame/s如果没有限制Queue ↑ ↑ ↑ 越来越多背压的意思就是下游处理不过来了上游必须感知这个压力。形成生产者 ↓ Queue ↓ 消费者 消费者处理不过来 ↓ Queue接近上限 ↓ 通知生产者 ↓ 限制/丢弃/阻塞这就是实时系统里的一个核心思想。十三、把你简历里的整套并发架构串起来现在终于可以把你的简历原话完整理解了使用 C17 拆分动捕接收、GMR 计算与动作发送线程通过 RAII、智能指针、互斥锁、条件变量及有界队列实现背压、排空退出与异常联动停止。翻译成人话就是动捕UDP ↓ 接收线程 Producer ↓ ┌─────────────┐ │ 有界Queue │ └──────┬──────┘ ↓ GMR计算线程 ↓ 动作数据 ↓ 发送线程 ↓ ZMQ/机器人旁边还有mutex ↓ 保护Queue condition_variable ↓ 没数据就睡 RAII ↓ 资源自动释放 unique_ptr ↓ 明确所有权 atomic ↓ 停止控制 exception ↓ 异常传播 join ↓ 安全退出 bounded queue ↓ 限制积压 drain ↓ 处理剩余任务 backpressure ↓ 控制生产速度这才是你简历这段话真正代表的技术能力。十四、现在做一个小型“面试式检查”你不用写代码直接想答案。问题1为什么不推荐到处new delete而推荐std::make_unique std::make_shared问题2为什么std::lock_guard比mutex.lock(); ... mutex.unlock();更安全问题3为什么消费者线程不能while(queue.empty()) {}一直检查问题4为什么实时机器人不能让队列无限增长问题5为什么线程停止时经常需要running false; cv.notify_all(); thread.join();问题6为什么Adapter和Strategy不是一个东西如果你能用自己的话回答这 6 个问题C 这一大块的核心工程思想就基本真正建立起来了。