Modules 模块化:头文件地狱真的要终结了吗?我持怀疑态度

news2026/3/30 18:42:51
各位来宾各位技术同仁大家好今天我们齐聚一堂探讨一个在C社区引发广泛讨论、充满期待又饱含争议的话题C模块化。特别是关于“头文件地狱真的要终结了吗”这个问题我深知在座的许多人包括我自己都对此抱有不同程度的怀疑。这种怀疑是健康的它来源于我们多年与C构建系统和代码组织搏斗的经验。作为一名编程专家我今天不会给大家描绘一个不切实际的“银弹”乌托邦而是会深入剖析C模块的原理、优势、挑战并试图解答——或者至少是厘清——我们的那些怀疑。在C标准委员会历经十余年努力之后C20终于引入了模块Modules特性。这被认为是C自诞生以来最重要的语言特性之一。那么它究竟能为我们带来什么我们又该如何理性看待它的未来和实际应用呢一、 头文件地狱我们为何需要“救赎”在深入模块之前我们首先要回顾一下我们为什么要摆脱“头文件地狱”这个地狱究竟由哪些炼狱组成C传统的代码组织方式依赖于头文件.h 或 .hpp和源文件.cpp。头文件负责声明接口源文件负责实现。这种机制在C早期甚至在C语言时代都是一种有效分离接口与实现的手段。然而随着项目规模的膨胀它逐渐暴露出诸多弊端编译速度的瓶颈Compilation Speed#include指令本质上是一个文本替换操作。当一个源文件#include某个头文件时预处理器会将头文件的全部内容复制粘贴到源文件中。如果这个头文件又#include了其他头文件那么这个过程会递归进行。结果是一个简单的.cpp文件在编译前可能膨胀成一个包含数十万甚至数百万行代码的巨大翻译单元Translation Unit。想象一下你在一个大型项目中修改了一个底层头文件中的一个非关键注释结果导致成百上千个依赖它的.cpp文件都需要重新编译。这种“牵一发而动全身”的连锁反应极大地拖慢了编译时间损害了开发效率。// my_library.h #pragma once #include string #include vector // ... 很多其他头文件 ... class MyClass { public: void doSomething(const std::string name); std::vectorint getData(); // ... 很多其他成员 ... };当成千上万个.cpp文件都#include my_library.h时它们每次都需要重新解析string,vector以及所有其他被包含的头文件无论这些头文件是否真正发生变化。脆弱的依赖关系Fragile Dependencies宏污染Macro Pollution宏是预处理器特性它们不遵循C的命名空间规则具有全局作用域。一个头文件中定义的宏可能意外地与另一个头文件中的标识符冲突导致难以诊断的编译错误。// libA.h #define MAX_SIZE 100 // libB.h // 不小心定义了同名宏或者某个枚举值叫 MAX_SIZE // main.cpp #include libA.h #include libB.h // 冲突可能发生顺序依赖Order Dependency某些头文件必须以特定顺序包含否则会导致编译错误这使得头文件管理变得异常复杂和易错。ODROne Definition Rule违规虽然#pragma once和 include guards (#ifndef ... #define ... #endif) 可以防止同一个头文件在同一翻译单元中被多次包含但它们无法防止不同翻译单元对同一个实体如内联函数、模板特化进行不同定义从而导致链接错误或未定义行为。名称冲突Name Collisions尽管有命名空间但全局命名空间中的函数、变量或类型仍然可能发生冲突尤其是当引入第三方库时。抽象能力的欠缺Lack of Strong Abstraction头文件在提供接口声明的同时也暴露了大量的实现细节。例如通过前向声明forward declarations可以减少一些依赖但对于复杂的类结构你仍然需要在头文件中暴露其所有成员变量和成员函数即使其中一些纯粹是内部实现所需。这使得重构变得困难因为任何对私有成员的修改都可能导致依赖该头文件的所有.cpp文件重新编译。构建系统复杂性Build System Complexity为了应对头文件地狱开发者和构建系统如CMake, Make, Bazel不得不采取各种复杂的策略预编译头文件PCH、分布式编译、精细的依赖追踪。这些策略虽然能在一定程度上缓解问题但增加了构建系统的配置和维护成本且自身也有限制。总结一下传统头文件机制的核心问题在于其基于文本的包含模型而非基于语义的导入模型。它让编译器每次都从零开始解析而非利用之前解析过的语义信息。二、 C模块承诺的“救赎”之道C模块旨在从根本上解决上述问题通过引入一种新的、更强大的代码组织和编译模型。其核心思想是将代码封装成模块模块只暴露明确标记为export的接口而隐藏所有内部实现细节。2.1 模块的核心概念模块接口单元Module Interface Unit, MIU这是模块的“脸面”包含了模块对外暴露的所有export声明。它通常以.ixx或.cppm为后缀。编译器在编译MIU时会生成一个二进制模块接口Binary Module Interface, BMI其中包含了模块的完整语义信息。模块实现单元Module Implementation Unit, MIM这是模块的“躯干”包含了模块内部的实现代码。它不包含export声明可以导入其他模块或头文件。一个模块可以有多个实现单元。它们通常以.cpp为后缀。模块分区Module Partitions为了更好地组织大型模块模块可以被划分为多个分区。分区可以是接口分区export module my_module:part_name;或实现分区module my_module:part_name;。接口分区会贡献到主模块接口中。全局模块片段Global Module Fragment这是模块内部的一段特殊区域用于包含传统的头文件。在这个区域中包含的头文件其宏定义和命名污染不会泄漏到模块外部。命名模块Named Modules每个模块都有一个唯一的名称例如std.core或my_library。2.2 模块的工作原理与头文件的本质区别模块与头文件的根本区别在于其编译模型头文件文本包含预处理器将头文件内容复制到源文件中编译器看到的是一个巨大的文本文件。每次编译都重复解析。模块语义导入当一个模块被编译后编译器会生成一个二进制模块接口BMI文件。这个BMI文件包含了模块的完整、预解析的接口信息包括类型、函数、模板等的所有声明和定义以及它们的语义关系。当另一个源文件import一个模块时编译器不是进行文本替换而是直接读取并利用已编译的BMI文件。这就像编译器直接获取了一个“符号表”和“类型信息表”而不需要重新解析源代码。2.3 模块带来的潜在优势基于上述原理C模块有望带来以下“救赎”显著提升编译速度这是模块最直接、最被期待的优势。通过BMI编译器避免了重复解析头文件内容的开销。一旦一个模块的BMI被生成所有import它的翻译单元都可以快速地使用其接口。概念示意传统main.cpp-#include-lib.h-#include-std::string- 文本展开 - 编译器解析main.cpp lib.h std::string.h模块main.cpp-import lib;- 编译器读取lib.bmi(已预解析的lib模块接口) - 编译器解析main.cpplib.bmi当lib模块本身不发生改变时main.cpp的编译速度会快很多。更强的封装性和更清晰的接口模块只导出明确标记为export的实体。所有未导出的内容包括宏、私有函数、内部类型等都严格限定在模块内部不会泄漏到模块外部。这使得模块成为一个更强大的抽象边界。无宏污染模块内部定义的宏不会影响import它的代码。无名称冲突模块内部的私有名称不会与import它的代码发生冲突。隐式 ODR 保证模块导出的实体在整个程序中只有一份语义定义由编译器在生成BMI时强制执行。这消除了传统头文件可能导致的 ODR 违规问题。消除顺序依赖import语句的顺序不再重要因为它们是语义导入而非文本包含。这大大简化了依赖管理。简化构建系统理论上构建系统可以更简单地管理依赖。它只需要知道哪些源文件属于哪个模块以及模块之间的import关系然后确保模块接口单元在其被导入之前编译完成并生成BMI即可。三、 C模块的实践代码示例让我们通过一系列代码示例来直观感受C模块。3.1 一个简单的模块假设我们有一个数学工具库math_utils。传统头文件方式// math_utils.h #pragma once namespace math_utils { int add(int a, int b); int subtract(int a, int b); } // math_utils.cpp #include math_utils.h namespace math_utils { int add(int a, int b) { return a b; } int subtract(int a, int b) { return a - b; } } // main.cpp #include iostream #include math_utils.h int main() { std::cout Sum: math_utils::add(5, 3) std::endl; std::cout Difference: math_utils::subtract(5, 3) std::endl; return 0; }C20 模块方式我们创建一个模块接口单元math_utils.ixx(或.cppm) 和一个实现单元math_utils_impl.cpp。// math_utils.ixx (Module Interface Unit) // 声明这是一个名为 math_utils 的模块 export module math_utils; // 导出命名空间 math_utils export namespace math_utils { // 导出函数声明 export int add(int a, int b); export int subtract(int a, int b); // 可以在这里包含一些内部类型或函数但不 export它们将不会暴露给导入者 // struct InternalHelper { /* ... */ }; }// math_utils_impl.cpp (Module Implementation Unit) // 属于 math_utils 模块的实现部分 module math_utils; // 注意这里没有 export // 实现导出的函数 namespace math_utils { int add(int a, int b) { return a b; } int subtract(int a, int b) { return a - b; } }// main.cpp (导入并使用模块) #include iostream // 仍然可以包含传统头文件 // 导入 math_utils 模块 import math_utils; int main() { std::cout Sum: math_utils::add(5, 3) std::endl; std::cout Difference: math_utils::subtract(5, 3) std::endl; return 0; }编译命令示例 (GCC 11):# 1. 编译模块接口单元生成BMI g -stdc20 -fmodules-ts -c math_utils.ixx -o math_utils.o # 2. 编译模块实现单元 g -stdc20 -fmodules-ts -c math_utils_impl.cpp -o math_utils_impl.o # 3. 编译主程序导入模块 g -stdc20 -fmodules-ts -c main.cpp -o main.o # 4. 链接所有目标文件 g math_utils.o math_utils_impl.o main.o -o my_program注意实际的编译器命令和BMI文件管理会因编译器和构建系统而异。例如MSVC通常会在编译MIU时自动生成.ifc文件作为BMI。3.2 模块的分区 (Partitions)当一个模块变得非常大时可以将其拆分为多个分区以提高组织性和编译效率。// my_complex_lib.ixx (主模块接口单元) export module my_complex_lib; // 导出分区。这里的 :details 是分区名。 // 这个分区的内容会成为 my_complex_lib 接口的一部分。 export import :details; export import :utilities; // 直接导出一些本模块特有的功能 export void global_feature();// my_complex_lib-details.ixx (模块接口分区单元) // 声明这是一个名为 my_complex_lib 的模块的 :details 分区 export module my_complex_lib:details; // 导出结构体 export struct DataPayload { int id; std::string description; }; // 导出函数 export void process_payload(DataPayload payload);// my_complex_lib-utilities.ixx (模块接口分区单元) export module my_complex_lib:utilities; export int calculate_checksum(const std::vectorchar data);// my_complex_lib_impl.cpp (主模块的实现单元) module my_complex_lib; // 属于主模块 #include iostream // 内部使用不会泄漏宏 void global_feature() { std::cout Executing global feature. std::endl; } // 导入分区以实现分区接口 import :details; import :utilities; // 实现分区导出的功能 void process_payload(DataPayload payload) { std::cout Processing payload ID: payload.id , Desc: payload.description std::endl; } int calculate_checksum(const std::vectorchar data) { int checksum 0; for (char c : data) { checksum static_castint(c); } return checksum; }// main.cpp #include iostream #include string #include vector import my_complex_lib; // 导入主模块 int main() { my_complex_lib::global_feature(); my_complex_lib::DataPayload payload {123, Sample data}; my_complex_lib::process_payload(payload); std::vectorchar data {a, b, c}; int checksum my_complex_lib::calculate_checksum(data); std::cout Checksum: checksum std::endl; return 0; }通过分区我们可以将一个大模块的不同功能领域分别组织在不同的文件中并分别编译但它们最终都贡献给同一个逻辑模块my_complex_lib。3.3 宏隔离的演示传统头文件中宏是全局污染的来源。模块则能有效隔离宏。// macro_module.ixx export module macro_module; #define MY_MODULE_MACRO This macro is internal to macro_module export void print_internal_macro_value();// macro_module_impl.cpp module macro_module; #include iostream void print_internal_macro_value() { std::cout MY_MODULE_MACRO std::endl; }// main.cpp #include iostream import macro_module; int main() { macro_module::print_internal_macro_value(); // 尝试访问模块内部的宏会失败 // std::cout MY_MODULE_MACRO std::endl; // 编译错误MY_MODULE_MACRO undeclared return 0; }main.cpp无法看到MY_MODULE_MACRO这彻底解决了宏污染问题。3.4 模块与传统头文件的混合使用在实际项目中我们不可能一下子把所有代码都转换为模块。模块设计考虑了与传统头文件的互操作性。场景一模块导入传统头文件一个模块内部可以使用import header或#include header来使用传统头文件。注意import header是一种特殊的语法称为“头文件单元Header Unit”或“模块化头文件”它会尝试将头文件编译成一个模块。如果编译器不支持或头文件不适合它会回退到#include行为。为了演示清晰我们这里使用#include。// data_processor.ixx export module data_processor; #include vector // 模块内部导入 std::vector不会污染外部 #include string // 模块内部导入 std::string export class Processor { public: void process(std::vectorstd::string data); std::string get_status() const; private: int processed_count 0; };// data_processor_impl.cpp module data_processor; #include iostream // 仅用于实现细节 void Processor::process(std::vectorstd::string data) { for (const auto item : data) { std::cout Processing: item std::endl; processed_count; } } std::string Processor::get_status() const { return Processed std::to_string(processed_count) items.; }// main.cpp #include iostream #include vector // main.cpp 自己也需要 vector #include string // main.cpp 自己也需要 string import data_processor; // 导入模块 int main() { data_processor::Processor p; std::vectorstd::string messages {Hello, Modules, World}; p.process(messages); std::cout p.get_status() std::endl; return 0; }在这个例子中data_processor模块内部使用了vector和string。这些头文件在模块内部被解析但它们的宏和全局声明不会泄漏到main.cpp。main.cpp如果自己也需要使用std::vector或std::string仍然需要#include相应的头文件。场景二传统头文件导入模块 (通过包装)一个传统头文件不能直接import一个模块。因为import是编译器指令而头文件在预处理阶段就被包含。但是我们可以通过一个“包装”模块来让传统代码间接使用模块。// legacy_interface.h (这是一个传统的头文件) #pragma once #ifdef __cplusplus extern C { // 如果需要 C 语言兼容性 #endif // 声明一个函数其实现将由模块提供 void do_something_from_module_wrapped(); #ifdef __cplusplus } #endif// module_wrapper.ixx (包装模块的接口) export module module_wrapper; // 我们可以选择性地导出一些东西或者只是作为实现模块的桥梁 export void wrapped_module_function();// module_wrapper_impl.cpp (包装模块的实现) module module_wrapper; #include iostream // 内部使用 import math_utils; // 导入我们之前的 math_utils 模块 // 实现包装模块导出的函数 void wrapped_module_function() { std::cout Calling math_utils::add from wrapper: math_utils::add(10, 20) std::endl; } // 实现 legacy_interface.h 中声明的函数 // 注意这个实现必须在某个模块内或者在编译时与模块的BMI一起处理 void do_something_from_module_wrapped() { wrapped_module_function(); }// main_legacy.cpp (一个使用传统头文件的源文件) #include iostream #include legacy_interface.h // 包含传统头文件 int main() { std::cout Calling function declared in legacy header but implemented by module: std::endl; do_something_from_module_wrapped(); return 0; }这个例子展示了如果你有一个庞大的传统代码库想要逐步引入模块可以先创建“包装模块”来暴露功能然后让传统代码通过头文件来调用这些包装后的接口。这是一种增量迁移的策略。3.5 标准库模块C23 进一步标准化了标准库模块如import std;或import std.compat;。import std;会导入整个标准库作为模块包括iostream,vector,string等。import std.compat;会导入标准库的传统头文件版本但会将其视为模块处理以提供兼容性并利用模块的编译优势。// my_app.cpp import std; // 导入整个C标准库模块 #include iostream // 仍然可以包含但如果 std 模块已导入这可能是多余的 int main() { std::vectorint numbers {1, 2, 3, 4, 5}; std::cout Vector size: numbers.size() std::endl; std::string message Hello, C23 Modules!; std::cout message std::endl; return 0; }使用import std;后我们不再需要单独#include vector或string。这将极大地简化标准库的导入并提升编译效率。四、 怀疑与挑战头文件地狱真的要终结了吗现在我们回过头来直面那些挥之不去的怀疑。C模块无疑带来了巨大的潜力但“终结头文件地狱”并非一蹴而就的坦途。4.1 迁移成本巨大这是最大的现实障碍。庞大的遗留代码库绝大多数C项目都拥有数百万行甚至数千万行的代码这些代码是基于传统头文件模型构建的。将它们全部转换为模块其工作量和风险是难以想象的。增量迁移的复杂性模块被设计为与传统头文件互操作支持增量迁移。然而如何优雅地进行增量迁移仍是一个挑战。例如一个模块如何导入一个传统头文件一个传统头文件如何“看到”一个模块导出的接口如前所述需要包装层构建系统如何同时管理模块和传统头文件之间的依赖关系“模块化”的重新思考模块不仅仅是语法上的改变它鼓励更深层次的架构思考。哪些代码应该组成一个模块模块的边界在哪里如何设计模块接口以实现最小暴露原则这需要开发者重新审视和重构现有的代码结构。4.2 工具链支持的成熟度模块的实际可用性严重依赖于工具链编译器、构建系统、IDE的支持。编译器支持GCC、Clang、MSVC 都已实现C20模块但它们的实现细节、BUG修复、性能优化仍在持续进行中。不同编译器之间的BMI通常不兼容这意味着你不能用一个编译器编译的BMI去导入另一个编译器编译的模块。构建系统支持CMake、Bazel、Meson 等主流构建系统正在逐步增强对模块的支持。但配置一个模块化的项目比配置传统项目要复杂得多涉及到如何告诉编译器模块接口单元在哪里。如何管理BMI文件的生成、存储和查找路径。如何正确处理模块之间的依赖顺序。如何将传统头文件转换为头文件单元Header Units。例如CMake 3.25 提供了实验性的模块支持但距离开箱即用、完全自动化还有距离。开发者往往需要手动编写额外的规则来管理BMI。IDE支持现代IDE如Visual Studio, CLion, VS Code with C extensions需要理解模块的语义才能提供准确的代码补全、导航、重构和调试功能。这方面的支持也仍在发展中。4.3 ABI 稳定性问题二进制模块接口BMI是编译器特定的。这意味着编译器版本锁定如果你用GCC 12编译了一个模块那么所有导入这个模块的代码都必须用GCC 12来编译。升级编译器版本通常意味着所有模块及其依赖都需要重新编译。跨平台/跨编译器不兼容你不能在Windows上用MSVC编译一个模块然后在Linux上用GCC导入它。这限制了二进制库的分发和使用。这与传统头文件不同传统头文件本质上是源代码只要编译器支持C标准通常可以在不同编译器和平台之间交换。ABI问题一直是C库开发中的痛点模块并没有从根本上解决它反而可能因为BMI的引入而变得更加显性。4.4 新的“地狱”任何新特性都可能带来新的复杂性。模块版本管理当一个模块升级时如何确保所有导入它的代码都使用了正确版本的BMI这在大型分布式团队中可能是一个挑战。调试复杂性调试器如何理解并处理BMI当你在一个模块的实现中设置断点时调试器能否正确地映射到源代码循环依赖模块本身并不能完全阻止循环依赖只是将其从#include层面转移到import层面。如果设计不当仍然可能出现模块间的循环导入。学习曲线尽管语法相对简单但模块背后的语义模型、与构建系统的集成方式、增量迁移的策略等都需要开发者投入时间和精力去学习和适应。对于一个已经熟悉传统头文件模式的C开发者来说这是一个不小的认知负担。4.5 性能提升的实际效果虽然理论上模块能显著提升编译速度但实际效果会因项目而异。首次构建首次编译模块会生成BMI这本身也有开销。在某些情况下首次编译可能比传统方式更慢。改动频率如果模块接口频繁变动那么依赖它的模块和翻译单元仍然需要重新编译性能优势可能不如预期。I/O瓶颈读取BMI文件代替解析源代码虽然减少了CPU工作量但仍然涉及磁盘I/O。在某些存储系统上这可能成为新的瓶颈。五、 理性展望与实践建议尽管存在诸多挑战我对C模块的未来持谨慎乐观的态度。它不是“银弹”但绝对是C发展史上一个里程碑式的进步。5.1 模块的真正价值模块的价值不仅在于编译速度的提升更在于它提供了更强大的封装机制和更清晰的语义边界。这有助于构建更健壮、更易于理解和维护的大型系统。它推动我们从“文本包含”的思维模式转向“语义导入”的思维模式这本身就是一种进步。5.2 实施策略与建议从小处着手新项目优先不要试图立即将整个遗留代码库转换为模块。从新项目或新模块开始逐步积累经验。新库/新组件新开发的库或相对独立的组件可以直接以模块的形式编写。内部工具/测试代码这些通常是对性能要求不高的代码可以作为试验模块的良好起点。增量迁移“模块化”传统库对于核心的、稳定且变动不频繁的传统头文件库如Boost的一部分或公司内部的基础库可以考虑为其创建“头文件单元”或“模块接口包装”使其能够被新模块导入并享受编译加速。封装遗留代码如果要将遗留代码暴露给新模块可以创建一个薄的“包装模块”它导入并使用遗留代码的头文件然后导出模块化的接口。自底向上从项目底层、依赖最少的组件开始模块化逐步向上推进。关注工具链发展持续关注编译器、构建系统和IDE对C模块的支持进展。随着时间的推移这些工具会变得更加成熟和易用。优先选择对模块支持较好的构建系统如Bazel或最新版CMake。教育与培训模块化不仅仅是语法更是设计哲学。团队成员需要理解模块的工作原理、优势、限制以及最佳实践。投入时间和资源进行内部培训至关重要。设计清晰的模块边界在设计模块时要像设计公共API一样谨慎选择哪些实体应该export。遵循“最小暴露原则”只导出真正需要对外提供的接口将实现细节严格封装在模块内部。避免过度模块化不是所有的.h/.cpp对都必须变成一个模块。对于非常小的、紧密耦合的单元可能仍然使用传统的头文件方式更简单。模块引入了新的构建复杂性需要权衡其带来的收益。C模块不会在一夜之间终结“头文件地狱”这是一种过于天真和不切实际的期望。它更像是一场持久战需要时间、投入和整个C生态系统的共同努力。但我们有理由相信随着编译器和构建系统的成熟以及开发者社区的经验积累C模块将成为现代C开发不可或缺的一部分。它将显著改善大型项目的编译时间提升代码的封装性和可维护性从而在根本上改变我们构建C软件的方式。我们不再需要依赖于各种变通方案和黑科技来管理依赖而是可以回归到语言本身提供的强大、优雅的抽象机制。头文件地狱的“终结”可能不是一个瞬间完成的事件而是一个渐进的过程。模块为我们提供了一把强大的工具去逐步拆除那些我们曾经视为不可避免的障碍。未来我们可能会看到一个更清爽、更高效、更现代的C开发环境。而我们作为开发者需要做的就是拥抱变化学习并实践这些新技术共同塑造C的未来。谢谢大家。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2465852.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…