C++最佳实践 | 1. 工具
本系列是开源书C++ Best Practises[1]的中文版,全书从工具、代码风格、安全性、可维护性、可移植性、多线程、性能、正确性等角度全面介绍了现代C++项目的最佳实践。本文是该系列的第一篇。
C++最佳实践:
1. 工具(本文)
2. 代码风格
3. 安全性
4. 可维护性
5. 可移植性及多线程
6. 性能
7. 正确性和脚本
前言
C++最佳实践: 支持Fork的编码标准文档
本文档旨在收集对C++最佳实践所进行的协作性讨论,是《Effective C++》(Meyers) 和《C++ Coding Standards》(Alexandrescu, Sutter) 等书籍的补充。在讨论如何确保整体代码质量的同时,补充了一些没有讨论到的较低级别的细节,并提供了具体的风格建议。
在任何情况下,简单明了都是首选。本文所举示例是为了说明为什么一种选择比另一种更受欢迎。在必要情况下,也会用文字说明。
本文档由Jason Turner编写,根据知识共享署名-非商业4.0国际许可协议[2]授权。
免责声明
本文档的编写基于个人经验,你不需要完全同意其中的观点。本文档保存于GitHub[3]上,任何人都可以fork供自己使用,或者提交修改建议与大家分享。
本文档启发O'Reilly发布了视频: Learning C++ Best Practices[4]
工具
应该在开发过程的早期建立用于执行这些工具的自动化框架,检出源代码、构建和执行测试所使用的命令不应超过2-3个,一旦测试完成,应该对代码的状态和质量有接近完整的了解。
源码管理
对于任何软件开发项目来说,源码管理都是绝对必要的,如果还没有,那就开始使用。
GitHub[5] —— 允许无限制的公共存储库和私有存储库,支持最多3个协作者。
Bitbucket[6] —— 允许无限制的私人存储库,最多5个协作者,免费。
SourceForge[7] —— 仅支持托管开放源码。
GitLab[8] —— 免费提供无限的公共和私有存储库,包括无限的CI执行器(CI Runner)。
Visual Studio Online[9] (http://www.visualstudio.com/what-is-visual-studio-online-vs) —— 无限的公共存储库,私有存储库收费,支持git或TFVC。另外提供: 问题跟踪、项目计划(包括Scrum等多个敏捷模板)、集成托管构建,所有特性都可以集成到Microsoft Visual Studio中,仅支持Windows。
构建工具
使用广泛接受的行业标准构建工具,可以防止在做探索、链接新库、打包产品等等工作时重复发明轮子。例子包括:
CMake[10]
对于构建性能,请考虑: https://github.com/sakra/cotire
对于增强可用性,请考虑: https://github.com/toeb/cmakepp
使用 https://cmake.org/cmake/help/v3.6/command/target_compile_features.html 作为C++标准flag
考虑使用 https://github.com/cheshirekow/cmake_format 自动格式化CMakeLists.txt文件
CMake特定最佳实践请参考后续的延伸阅读[11]部分
cmake --build
提供了平台无关的通用编译接口
Waf[12]
FASTBuild[13]
Ninja[14] —— 可以极大优化大型项目的增量构建时间,可以作为CMake的target。
Bazel[15] —— 基于网络工件缓存和远程执行的快速增量构建
Buck[16] —— 类似于Bazel,对iOS和Android有很好的支持
gyp[17] —— 谷歌chromium的构建工具
maiken[18] —— 具有maven配置风格的跨平台构建工具
Qt Build Suite[19] —— 基于Qt的跨平台构建工具
meson[20] —— 快速、对用户友好的开源构建系统
premake[21]
请记住,这不仅是构建工具,也是编程语言。请尽量维护良好整洁的构建脚本,并遵循正在使用的工具的推荐实践。
包管理器
包管理是C++的重要主题,目前还没有明确的赢家。请考虑使用包管理器来帮助跟踪项目的依赖关系,从而帮助新人更容易开始参与项目。
Conan[22] —— 跨平台C++依赖管理器
hunter[23] —— CMake驱动的跨平台包管理器,适用于C/C++
[C++ Archive Network (CPPAN)](https://cppan.org/ "C++ Archive Network (CPPAN "C++ Archive Network (CPPAN)")") —— 跨平台C++依赖管理器
qpm[24] —— Qt的包管理器
build2[25] —— 类Cargo的C++包管理器
Buckaroo[26] —— 真正去中心化的跨平台依赖管理器,适用于C/C++等等
Vcpkg[27] —— 微软C++库管理器,支持Windows, Linux和MacOS
持续集成
选择了构建工具之后,接下来需要设置持续集成环境。
在更改被推送到存储库时会触发持续集成(CI)工具自动构建源代码,可以私有部署CI工具或使用托管的CI系统。
Travis CI[28]
能很好的与C++一起工作
设计与GitHub一起使用
GitHub公共存储库可以免费使用
AppVeyor[29]
支持Windows、MSVC和MinGW
GitHub公共存储库可以免费使用
Hudson CI[30] / Jenkins CI[31]
需要Java应用服务器
支持Windows、OS X和Linux
可以通过许多插件进行扩展
TeamCity[32]
对开源项目免费
Decent CI[33]
简单持续集成,可以将结果发布到GitHub
支持Windows、OS X和Linux
使用ChaiScript[34]
Visual Studio Online[35] (http://www.visualstudio.com/what-is-visual-studio-online-vs)
与Visual Studio Online的源代码库紧密集成
使用MSBuild (Visual Studio的构建引擎),可在Windows、OS X和Linux上使用
提供托管的构建代理,也允许用户提供构建代理
可以在Microsoft Visual Studio中控制和监控
通过Microsoft Team Foundation Server进行内部安装
GitLab[36]
使用自定义Docker镜像,因此可用于C++
有免费的共享执行器
提供简单的覆盖率结果分析
如果在GitHub上有开源、公开托管的项目:
现在就把Travis Ci和AppVeyor整合起来。关于如何在基于C++ cmake的应用程序中启用的简单示例,请参考: https://github.com/ChaiScript/ChaiScript/blob/master/.travis.yml
启用覆盖工具(Codecov或Coveralls)
启用Coverity Scan[37]
这些工具都是免费的,设置起来也相对容易。一旦把它们都设置好,就可以对项目进行持续的构建、测试、分析和报告,并且免费。
编译器
启用所有可用、合理的告警选项,有些告警选项只在启用了优化的情况下才有效,或者优化级别越高,效果越好,例如GCC中的-Wnull-dereference
。
应该使用尽可能多的编译器,每个编译器对标准的实现略有不同,支持多个编译器将有助于确保实现最可移植、最可靠的代码。
GCC / Clang
-Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic
-Wall -Wextra
合理、标准-Wshadow
如果变量声明覆盖了父上下文中的变量,则警告用户-Wnon-virtual-dtor
如果带有虚函数的类有非虚析构函数,则警告用户,有助于捕获难以跟踪的内存错误-Wold-style-cast
对C风格的类型转换发出警告-Wcast-align
警告有潜在性能问题的强制类型转换-Wunused
警告任何未使用的东西-Woverloaded-virtual
如果重载(而不是覆盖)虚函数,则发出警告-Wpedantic
如果使用了非标准的C++则发出警告(所有版本的GCC, Clang >= 3.2)-Wconversion
对可能丢失数据的类型转换发出警告-Wsign-conversion
对影响到符号的类型转换发出警告(Clang所有版本,GCC >= 4.3)-Wmisleading-indentation
如果代码中有缩进,但没有对应的代码块,则发出警告(仅在GCC >= 6.0中)-Wduplicated-cond
如果if
/else
分支有重复条件,则发出警告(仅在GCC >= 6.0中)-Wduplicated-branches
如果if
/else
分支有重复的代码,则发出警告(仅在GCC >= 7.0中)-Wlogical-op
在可能需要按位操作的地方使用逻辑操作时发出警告(仅在GCC中)-Wnull-dereference
如果检测到空解引用将发出警告(仅在GCC >= 6.0中)-Wuseless-cast
如果执行强制转换到相同的类型,则会发出警告(仅在GCC >= 4.8中)-Wdouble-promotion
如果float
隐式提升为double
则发出警告(GCC >= 4.6, Clang >= 3.8)-Wformat=2
对输出格式化函数(即printf
)的安全问题发出警告-Wlifetime
显示对象生命周期问题(目前只有Clang的特殊分支)
考虑使用-Weverything
,并且只在需要的情况下禁用少数警告。
-Weffc++
警告模式可能太吵了,但如果对项目适用,也可以使用。
MSVC
/permissive-
—— 执行标准一致性[38]
/W4 /w14640
—— 使用并考虑以下内容(参见下面的描述)
/W4
一切合理的警告/w14242
'identifier': 从'type1'到'type1'的转换,可能丢失数据/w14254
'operator': 从“type1:field_bits”到“type2:field_bits”的转换,可能丢失数据/w14263
'function': 成员函数不重写任何基类虚成员函数/w14265
'classname': 类有虚函数,但析构函数不是该类的虚实例,可能无法正确析构/w14287
'operator': 无符号/负常数不匹配/we4289
nonstandard extension used: 'variable': 在for循环中声明的循环控制变量在for循环作用域之外使用/w14296
'operator': 表达式总是'布尔值(boolean_value)'/w14311
'variable': 指针从'type1'转换到'type2'时被截断/w14545
逗号前的表达式计算的是缺少参数列表的函数/w14546
逗号前的函数调用缺少参数列表/w14547
'operator': 逗号前的运算符无效,预期运算符有副作用/w14549
'operator': 逗号前的运算符无效,想要“运算符”吗?/w14555
表达式没有效果,表达式预期带有副作用/w14619
pragma warning: 没有警告号码/w14640
在线程不安全的静态成员初始化时启用警告/w14826
从'type1'到'type_2'的转换会扩展符号,可能会导致意外的运行时行为/w14905
宽字符串字面量转换为'LPSTR'/w14906
字符串字面量转换为'LPWSTR'/w14928
非法的拷贝初始化,已隐式应用多个用户定义转换
不建议
/Wall
会对标准库中包含的文件发出警告,有太多额外的警告,因此没什么用。
通用
一开始就设置非常严格的警告,在项目开始后试图提高警告级别可能会很痛苦。
考虑使用将警告视为错误的设置,例如MSVC中的/Wx
,以及GCC/Clang中的-Werror
。
基于LLVM的工具
基于LLVM的工具与能够输出编译命令数据库的构建系统(例如cmake)配合得最好,例如:
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .
如果没用这样的构建系统,可以考虑Build EAR[39],它可以与现有构建系统挂钩,并生成编译命令数据库。
CMake现在也提供了在正常编译期间调用```clang-tidy```[40]的内置支持。
include-what-you-use[41], 示例结果[42]
clang-modernize[43], 示例结果[44]
clang-check[45]
clang-tidy[46]
静态检查
最好的选择是将静态分析器作为自动化构建系统的一部分运行,cppcheck和clang可以满足免费选项的要求。
Coverity Scan
Coverity[47]提供免费(开源)静态分析工具包,可以用于与Travis CI[48]和AppVeyor[49]集成的每个提交。
PVS-Studio
PVS-Studio[50]是用于检测用C、C++和C#编写的程序源代码中的bug的工具,对个人学术项目、开源非商业项目和个人开发者的独立项目都是免费的,可以在Windows和Linux环境下工作。
Cppcheck
Cppcheck[51]是免费、开源的。它努力争取零误报,并且做得很好。因此,应该启用所有警告: --enable=all
。
备注:
为了正确工作,需要格式完整的头文件路径,所以在使用前不要忘记传递:
--check-config
。查找未使用的头文件时
-j
不能大于1。如果需要检查所有的代码,请记住为带有大量#ifdef的代码添加
--force
。
cppclean
cppclean[52]是开源静态分析器,专注于发现C++源代码中导致大型代码库开发缓慢的问题。
CppDepend
CppDepend[53]通过分析和可视化代码依赖关系、定义设计规则、进行影响分析以及比较不同版本的代码,简化了对复杂C/C++代码库的管理,对开源贡献者是免费的。
Clang的静态分析器
Clang的分析程序的默认选项适用于各个平台,可以直接通过CMake使用[54],也可以通过基于llvm的工具[55]中的clang-check
和clang-tidy
调用。
此外,CodeChecker[56]可以作为clang的静态分析前端。
clang-tidy
可以通过Clang Power Tools[57]扩展轻松的和Visual Studio一起使用。
MSVC的静态分析器
可以通过/analyze
命令行选项[58]启用,可以使用默认选项。
Flint / Flint++
Flint[59]和Flint++[60]是根据Facebook编码标准分析C++代码的linter。
OCLint
OCLint[61]是免费、自由、开源的静态代码分析工具,可以通过许多不同的方式提高C++代码的质量。
ReSharper C++ / CLion
这两种来自JetBrains[62]的工具都提供了一定程度的静态分析和自动修复功能,为开源项目负责人提供了免费许可证选项。
Cevelop
基于Eclipse的Cevelop[63] IDE提供了各种静态分析和重构/代码修复工具。例如,可以用C++的constexprs
替换宏,重构命名空间(提取/内联using
,限定名称),并将代码重构为C++11的统一初始化语法。Cevelop是免费的。
Qt Creator
Qt Creator可以插入clang静态分析器。
clazy
clazy[64]是基于clang的分析Qt使用情况的工具。
IKOS
IKOS[65]是开源静态分析器,由NASA开发。它以抽象解释为基础,用C++编写,使用LLVM为C和C++提供了分析器。源代码可以在Github[66]上找到。
运行时检查
代码覆盖率分析
覆盖率分析工具应该在测试执行时运行,以确保整个应用程序都被测到。不幸的是,覆盖率分析需要禁用编译器优化,这将导致测试执行时间大大延长。
Codecov[67]
与Travis CI和AppVeyor集成
对于开源项目免费
Coveralls[68]
与Travis CI和AppVeyor集成
对于开源项目免费
LCOV[69]
有很多配置项
Gcovr[70]
kcov[71]
可与codecov和coveralls集成
不需要特殊的编译器flag,只需要debug符号,就可以输出代码覆盖率报告
OpenCppCoverage[72]
Windows上的开源代码覆盖率工具
Valgrind
Valgrind[73]是运行时代码分析器,可以检测内存泄漏、竞争条件和其他相关问题,支持各种Unix平台。
Dr Memory
和Valgrind类似。http://www.drmemory.org
GCC / Clang Sanitizers
这些工具提供了许多与Valgrind相同的特性,但内置在编译器中,易于使用,并提供问题报告。
AddressSanitizer
MemorySanitizer
ThreadSanitizer
UndefinedBehaviorSanitizer
注意可用的sanitizer选项,包括运行时选项。https://kristerw.blogspot.com/2018/06/useful-gcc-address-sanitizer-checks-not.html
Fuzzy分析器
如果项目接受用户定义的输入,可以考虑运行模糊输入测试。
这些工具都使用覆盖率报告来寻找新的代码执行路径,并尝试为代码提供新的输入。它们可以发现崩溃、挂起以及一些没有被考虑到的输入。
american fuzzy lop[74]
LibFuzzer[75]
KLEE[76] —— 可以为单独的函数提供模糊测试
变异测试
这些工具获取在单元测试运行期间执行的代码,并改变执行的代码。如果测试在有突变的情况下仍然通过,那可能意味着在测试套件中存在有缺陷的测试。
Dextool Mutate[77]
MuCPP[78]
mull[79]
CCMutator[80]
控制流保护
MSVC的[控制流保护(Control Flow Guard)](https://msdn.microsoft.com/en-us/library/windows/desktop/mt637065%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 "控制流保护(Control Flow Guard "控制流保护(Control Flow Guard)")")增加了高性能的运行时安全检查。
检查STL实现
_GLIBCXX_DEBUG
与GCC的libstdc++的实现。参见Krister的博客文章[81]。
堆分析
https://epfl-vlsc.github.io/memoro —— 一个详细的堆分析器
忽略警告
如果团队一致认为编译器或分析器对不正确或不可避免的错误发出警告,则团队需要尽可能只在最小的范围内禁用特定的错误警告。
在对一段代码禁用该警告后,请确保重新启用该警告,没人希望禁用的警告被泄露到其他代码中[82]。
测试
上面提到的CMake有一个用于执行测试的内置框架,请确保使用的任何构建系统都能够执行内置测试。
为了进一步帮助执行测试,请考虑使用某个单元测试库,如Google Test[83]、Catch[84]、CppUTest[85]或Boost.Test[86],以帮助组织测试。
单元测试
单元测试针对的是可以独立测试的小代码块和独立功能。
集成测试
对于提交的每个特性或bug修复,都应该启用测试。参见上文介绍的代码覆盖率分析。这些测试比单元测试级别更高,但仍然应该被限制在单个特性的范围内。
逆向测试
不要忘记确保测试代码中的错误处理,并且确保其能够正常工作。如果目标是100%的代码覆盖率,很明显这些错误场景也需要被覆盖的。
调试
uftrace
uftrace[87]可以用来生成程序执行的函数调用图。
rr
rr[88]是一个免费、开源的反向调试器,支持C++。
其他工具
Lizard
Lizard[89]提供了针对C++代码库运行复杂性分析的非常简单的接口。
Metrix++
Metrix++[90]可以识别并报告代码中最复杂的部分,从而帮助我们减少复杂代码,帮助编译器更好的理解和优化代码。
ABI Compliance Checker
ABI Compliance Checker[91] (ACC)可以分析两个库版本,并生成关于API和C++ ABI变化的详细兼容性报告,可以帮助库开发人员发现无意的破坏性更改,以确保向后兼容性。
CNCC
Customizable Naming Convention Checker[92](可自定义的命名约定检查器)可以报告代码中不遵循特定命名约定的标识符。
ClangFormat
ClangFormat[93]可以自动检查并纠正代码格式,以匹配组织约定。可以参考关于clang-format的系列文章[94]。
SourceMeter
SourceMeter[95]提供了免费版本,可以为代码提供许多不同的度量,也可以调用cppcheck。
Bloaty McBloatface
Bloaty McBloatface[96]是用于类unix平台的二进制大小分析器。
微信公众号:DeepNoMind
参考资料
[1]
C++ Best Practises: https://lefticus.gitbooks.io/cpp-best-practices/content/
[2]知识共享署名-非商业4.0国际许可协议: http://creativecommons.org/licenses/by-nc/4.0/
[3]GitHub: https://github.com/lefticus/cppbestpractices
[4]Learning C++ Best Practices: http://shop.oreilly.com/product/0636920049814.do
[5]GitHub: https://github.com/
[6]Bitbucket: https://bitbucket.org/
[7]SourceForge: http://sourceforge.net/
[8]GitLab: https://gitlab.com/
[9]Visual Studio Online: https://visualstudio.com/
[10]CMake: http://www.cmake.org/
[11]延伸阅读: https://lefticus.gitbooks.io/cpp-best-practices/content/10-Further_Reading.md
[12]Waf: https://waf.io/
[13]FASTBuild: http://www.fastbuild.org/
[14]Ninja: https://ninja-build.org/
[15]Bazel: http://bazel.io/
[16]Buck: http://buckbuild.com/
[17]gyp: https://chromium.googlesource.com/external/gyp/
[18]maiken: https://github.com/Dekken/maiken
[19]Qt Build Suite: http://doc.qt.io/qbs/
[20]meson: http://mesonbuild.com/index.html
[21]premake: https://premake.github.io/
[22]Conan: https://www.conan.io/
[23]hunter: https://github.com/ruslo/hunter
[24]qpm: https://www.qpm.io/
[25]build2: https://build2.org/
[26]Buckaroo: https://buckaroo.pm/
[27]Vcpkg: https://github.com/microsoft/vcpkg
[28]Travis CI: http://travis-ci.org/
[29]AppVeyor: http://www.appveyor.com/
[30]Hudson CI: http://hudson-ci.org/
[31]Jenkins CI: https://jenkins-ci.org/
[32]TeamCity: https://www.jetbrains.com/teamcity
[33]Decent CI: https://github.com/lefticus/decent_ci
[34]ChaiScript: http://chaiscript.com/ChaiScript-BuildResults/full_dashboard.html
[35]Visual Studio Online: https://visualstudio.com/
[36]GitLab: https://gitlab.com/
[37]Coverity Scan: https://scan.coverity.com/
[38]执行标准一致性: https://docs.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance
[39]Build EAR: https://github.com/rizsotto/Bear
[40]正常编译期间调用clang-tidy
: https://cmake.org/cmake/help/latest/prop_tgt/LANG_CLANG_TIDY.html
include-what-you-use: https://github.com/include-what-you-use
[42]示例结果: https://github.com/ChaiScript/ChaiScript/commit/c0bf6ee99dac14a19530179874f6c95255fde173
[43]clang-modernize: http://clang.llvm.org/extra/clang-modernize.html
[44]示例结果: https://github.com/ChaiScript/ChaiScript/commit/6eab8ddfe154a4ebbe956a5165b390ee700fae1b
[45]clang-check: http://clang.llvm.org/docs/ClangCheck.html
[46]clang-tidy: http://clang.llvm.org/extra/clang-tidy.html
[47]Coverity: https://scan.coverity.com/
[48]Travis CI: http://travis-ci.org/
[49]AppVeyor: http://www.appveyor.com/
[50]PVS-Studio: http://www.viva64.com/en/pvs-studio/
[51]Cppcheck: http://cppcheck.sourceforge.net/
[52]cppclean: https://github.com/myint/cppclean
[53]CppDepend: https://www.cppdepend.com/
[54]通过CMake使用: http://garykramlich.blogspot.com/2011/10/using-scan-build-from-clang-with-cmake.html
[55]基于llvm的工具: https://lefticus.gitbooks.io/cpp-best-practices/content/02-Use_the_Tools_Available.html#llvm-based-tools
[56]CodeChecker: https://github.com/Ericsson/CodeChecker
[57]Clang Power Tools: https://clangpowertools.com/
[58]命令行选项: http://msdn.microsoft.com/en-us/library/ms173498.aspx
[59]Flint: https://github.com/facebook/flint
[60]Flint++: https://github.com/L2Program/FlintPlusPlus
[61]OCLint: http://oclint.org/
[62]JetBrains: https://www.jetbrains.com/cpp/
[63]Cevelop: https://www.cevelop.com/
[64]clazy: https://github.com/KDE/clazy
[65]IKOS: https://ti.arc.nasa.gov/opensource/ikos/
[66]Github: https://github.com/NASA-SW-VnV/ikos
[67]Codecov: https://codecov.io/
[68]Coveralls: https://coveralls.io/
[69]LCOV: http://ltp.sourceforge.net/coverage/lcov.php
[70]Gcovr: http://gcovr.com/
[71]kcov: http://simonkagstrom.github.io/kcov/index.html
[72]OpenCppCoverage: https://github.com/OpenCppCoverage/OpenCppCoverage
[73]Valgrind: http://www.valgrind.org/
[74]american fuzzy lop: http://lcamtuf.coredump.cx/afl/
[75]LibFuzzer: http://llvm.org/docs/LibFuzzer.html
[76]KLEE: http://klee.github.io/
[77]Dextool Mutate: https://github.com/joakim-brannstrom/dextool/tree/master/plugin/mutate
[78]MuCPP: https://neptuno.uca.es/redmine/projects/mucpp-mutation-tool/wiki
[79]mull: https://github.com/mull-project/mull
[80]CCMutator: https://github.com/markus-kusano/CCMutator
[81]Krister的博客文章: https://kristerw.blogspot.se/2018/03/detecting-incorrect-c-stl-usage.html
[82]泄露到其他代码中: http://www.forwardscattering.org/post/48
[83]Google Test: https://github.com/google/googletest
[84]Catch: https://github.com/philsquared/Catch
[85]CppUTest: https://github.com/cpputest/cpputest
[86]Boost.Test: http://www.boost.org/doc/libs/release/libs/test/
[87]uftrace: https://github.com/namhyung/uftrace
[88]rr: http://rr-project.org/
[89]Lizard: http://www.lizard.ws/
[90]Metrix++: http://metrixplusplus.sourceforge.net/
[91]ABI Compliance Checker: http://ispras.linuxbase.org/index.php/ABI_compliance_checker
[92]Customizable Naming Convention Checker: https://github.com/mapbox/cncc
[93]ClangFormat: http://clang.llvm.org/docs/ClangFormat.html
[94]关于clang-format的系列文章: https://engineering.mongodb.com/post/succeeding-with-clangformat-part-1-pitfalls-and-planning/
[95]SourceMeter: https://www.sourcemeter.com/
[96]Bloaty McBloatface: https://github.com/google/bloaty
- END -文章来源: blog.csdn.net,作者:程序员编程指南,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/weixin_41055260/article/details/127063925
- 点赞
- 收藏
- 关注作者
评论(0)