2026/2/22 20:38:44
网站建设
项目流程
财经网站直播系统建设,免费搭建购物网站,wordpress设置文章登录可见,中国联通与腾讯设立合作一、模块的深入分析
在模块的学习中#xff0c;英文文档中反复出现translation unit#xff0c;它其实就是模块单元#xff08;module unit#xff09;的组成者。可以直译为“翻译单元”#xff0c;也可以根据模块的特点理解为“迁移单元”。
C中对模块的应用来说#xf…一、模块的深入分析在模块的学习中英文文档中反复出现translation unit它其实就是模块单元module unit的组成者。可以直译为“翻译单元”也可以根据模块的特点理解为“迁移单元”。C中对模块的应用来说是一个革命的性的变化。所以影响最大的有两种场景编译处理为了理解编译的流程需要先了解模块的整体划分它主要由模块的接口单元moudle interface unit、模块实现单元(module implementation unit)、模块分区 (module partitions)和BMI (binary module interface)编译生成的二进制接口文件组成。也就是说模块分为模块的定义和实现两部分有点类似头文件和cpp文件。其同样也存在着模块的声明和定义一体的情况在一个文件中。特别是在某些小规模的应用场景下开发者不愿意组织成不同的文件形式进行处理即主分区接口和实现单元所以又提供了私有模块分区这一情况。这都对编译器提出出了较强的要求。下面以一个主程序依赖模块B而B又依赖A的情况进行说明首先编译器将编译模块的接口单元B.ixx包括主模块接口单元先进行分区接口的编译然后再依次聚合成主模块接口生成BMI文件B.gcm。即通过解析接口单元代码处理export的声明有效性来生成对应的二进制接口文件前面的例程中的std.gcm。在这个文件中包含着模块定义中对外声明的所有接口信息可以理解为一种头文件件的声明没有实现内容其次编译器开始处理实现单元首先验证gcm文件接口的声明与实现单元中是否保持一致比如函数的签名等然后编译实现单元如B.cpp生成目标文件.o文件但此过程不会有新的gcm文件生成再次编译开始处理模块的依赖模块的编译比如B依赖A模块则开始检查A的BMI是否成生如未生成则开始编译A模块。然后解析B模块中import A中的接口信息与A.gcm中的是否保持一致。编译生成目标文件A.o且不生成新的BMI。即其方式与主编译单元方式一致如果有更深层的编译则依次进行即可。最后编译主程序并链接模块B先从B.gcm导入接口信息并验证如果通过则编译主程序生成目标文件main.o,然后在链接过程中将三个目标文件即A.o,B.o,main.o链接成可执行文件。编译器在处理模块间的依赖时是通过显式的依赖方式来构建依赖图的从而保证编译的依赖顺序大家是不是想到了传统的动态库中的手动控制库的依赖顺序的问题以及头文件顺序的问题防止出现类似库依赖时顺序颠倒就会导致链接错误的情况。另外模块的接口变化仅处理模块A及其依赖相关模块的编译而内部的变化则只影响模块本身的编译想想头文件的污染。由于模块提供了增量编译的功能所以其避免了重复编译和解析提高了编译的效率。与头文件混合处理模块的引入与头文件的使用并不是一种二选一的结果而一种可以混合存在的形式毕竟庞大的C代码库和框架不会允许出现一种推翻性的新技术点的应用。在前面的例程中已经提供了对头文件和模块共同使用的方式。那么如何才能更好的解决新老代码的整合呢首先在新工程中引入模块封装为模块接口单元其次对强使用、相对简单的旧代码优先引入模块机制利用命名模块来处理与传统宏定义的冲突最后对一些很少应用特别复杂的不进行模块的引入。二、模块的结构组织在前面学习的基础上很容易在实际的工程中进行模块的组织架构处理抽象对外导出接口及其相关功能描述说明以模块分区和分片段为基础划分出最底层的实现单元以层级树结构来处理功能实现中模块子模块间的依赖防止循环依赖和重复导出划分命名和匿名模块来处理一些特殊场景的处理通过上述的组织就可以实现编译的解耦保证了功能实现上的隔离。并可以为导出控制提供更好的实现方式。三、混合编程新技术的引进往往是渐进性的模块机制亦是如此。混合编程既可以兼顾模块机制的各种优势如解决依赖顺序、提高编译效率等又可以兼容老的头文件库和框架让开发者不至于陷入重写老代码和编写新代码的混合状态。下面给出一个具体的例程// legacy.h#pragmaonce#includestring#includeiostreamclass LegacyDemo{public:voiddisplay(conststd::stringmsg);};intadd(inta,intb);// legacy.cpp#includelegacy.hvoidLegacyDemo::display(conststd::stringmsg){std::coutold msgstd::endl;}intadd(inta,intb){returnab;}// new_module.cppmexport module new_module;// 全局模块片段包含传统头文件module;// 开始全局模块片段#includelegacy.hexport module new_module;#includeiostream#includestringexport namespace ModuleDemo{exportintadd(inta,intb);export class DemoClass{private:intret_0;public:DemoClass();intsub(inta,intb);};}// new_module.cppmodule new_module;//#include cmathnamespace ModuleDemo{intadd(inta,intb){returnab;}DemoClass::DemoClass(){}intDemoClass::sub(inta,intb){ret_a-b;returnret_;}}// main.cpp#includeiostream#includelegacy.himport new_module;intmain(){// 使用头文件机制std::coutlegacy add : add(10,20)std::endl;LegacyDemo ld;ld.display(Hello include!);// 使用模块机制std::coutmoudle add : ModuleDemo::add(30,40)std::endl;ModuleDemo::DemoClass calc;std::cout计算器结果: calc.sub(5,6)std::endl;return0;}编译的方法如同前文一致不再重复。大家可以在前面的环境中进行验证即可。说明在本次编译中遇到了新问题如果在模块的实现文件中增加了未使用的头文件在直接用命令编译时没有问题但使用CMake编译时则会报编译错误。四、总结在学习了模块的相关开发处理后就出现了一个必须面对的问题即如何在实际的工程中组织相关的模块结构。正如反复说明的正规化和系统化才是一切工程开发的最终的目的。特别是目前软件的规模已经达到了空前巨大并且C还有庞大的历史遗产的情况下引入模块编程是一个解决传统问题的好的方式。模块本身是一个相对较完整的组织体系但为了更方便可以在其内部继续分区正如在进程时可以细分线程一样。不同的粒度可以让模块的组织结构看上去更清晰。这样在不同的层次和不同的角度来控制模块的开发和应用会让其在设计和实现上更加灵活。通过整合模块机制与传统的头文件包含机制可以更好的提高代码整体的编译效率和安全性。大家可以在实际的工程中借鉴这种思想通过混合编程对相关工程在整体上进行思考和设计。