面向 C++ 的现代 CMake 教程(一)(5)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 面向 C++ 的现代 CMake 教程(一)

面向 C++ 的现代 CMake 教程(一)(4)https://developer.aliyun.com/article/1526966

循环

CMake 中的循环相当直接——我们可以使用while()foreach()来反复执行相同的命令集。这两个命令都支持循环控制机制:

  • break()循环停止剩余块的执行,并从外层循环中断。
  • continue()循环停止当前迭代的执行,并从下一个开始。

循环块用while()命令打开,用endwhile()命令关闭。只要while()中提供的表达式为true,任何包含的命令都将执行。表述条件的语法与if()命令相同:

while(<condition>)
  <commands>
endwhile()

你可能猜到了——通过一些额外的变量——while循环可以替代for循环。实际上,使用foreach()循环要容易得多——让我们来看看。

foreach 循环

foreach 块有几个变体,为每个值执行包含的命令。与其他块一样,它有foreach()endforeach()命令。

foreach()的最简单形式旨在提供 C++风格的for循环:

foreach(<loop_var> RANGE <max>)
  <commands>
endforeach()

CMake 将从0迭代到(包括)。如果我们需要更多的控制,可以使用第二种变体,提供,可选地提供。所有参数必须是非负整数。此外,必须小于

foreach(<loop_var> RANGE <min> <max> [<step>])

然而,当它处理列表时,foreach()显示了它的真正颜色:

foreach(<loop_variable> IN [LISTS <lists>] [ITEMS <items>])

CMake 将取所有提供的列表变量的元素,然后是所有明确声明的值,并将它们存储在中,一次执行中的每一个项目。你可以选择只提供列表、只提供值,或者两者都提供:

chapter02/06-loops/foreach.cmake

set(MY_LIST 1 2 3)
foreach(VAR IN LISTS MY_LIST ITEMS e f)
  message(${VAR})
endforeach()

上述代码将输出以下内容:

1
2
3
e
f

或者,我们可以使用简短版本(省略 IN 关键字)得到相同的结果:

foreach(VAR 1 2 3 e f)

从版本 3.17 开始,foreach() 学会了如何压缩列表(ZIP_LISTS):

foreach(<loop_var>... IN ZIP_LISTS <lists>)

压缩列表意味着简单地遍历多个列表,并对具有相同索引的相应项目进行操作。让我们看一个例子:

chapter02/06-loops/foreach.cmake

set(L1 "one;two;three;four")
set(L2 "1;2;3;4;5")
foreach(num IN ZIP_LISTS L1 L2)
    message("num_0=${num_0}, num_1=${num_1}")
endforeach()

CMake 将为每个提供的列表创建一个 num_ 变量,并填充每个列表的项目。您可以传递多个 变量名(每个列表一个),每个列表将使用单独的变量存储其项目:

foreach(word num IN ZIP_LISTS L1 L2)
    message("word=${word}, num=${num}")

如果列表之间的项数不同,CMake 不会为较短的列表定义变量。

所以,关于循环的内容就讲到这里。

命令定义

定义自己的命令有两种方法:可以使用 macro() 命令或 function() 命令。解释这些命令之间的区别最简单的方式是通过将它们与 C 语言风格的预处理器宏和实际的 C++函数进行比较:

  • macro() 命令更像是一个查找和替换指令,而不是实际的子程序调用,如 function()。与函数相反,宏不会在调用栈上创建一个单独的条目。这意味着在宏中调用 return() 将会返回到比函数中的 return() 高一个级别的调用语句(如果我们在最外层作用域中,可能会导致执行终止)。
  • function() 命令为局部变量创建了一个单独的作用域,与 macro() 命令在工作域中的调用者不同。这可能会导致混淆的结果。我们在下一节讨论这些细节。

这两种方法都接受可以在命令块内部命名和引用的参数。此外,CMake 允许您使用以下引用访问在命令调用中传递的参数:

  • ${ARGC}:参数的数量
  • ${ARGV}:所有参数的列表
  • ${ARG0}${ARG1}${ARG2}:特定索引处的参数值
  • ${ARGN}: 传递给调用者的一些匿名参数,在最后一个预期参数之后

使用索引访问超出 ARGC 范围的数字参数是未定义行为。

如果您决定定义一个带有命名参数的命令,每个调用都必须传递它们全部,否则它将是无效的。

定义宏与其他块类似:

macro(<name> [<argument>…])
  <commands>
endmacro()

在此声明之后,我们可以通过调用其名称来执行我们的宏(函数调用不区分大小写)。

以下示例突出了宏中变量作用域的所有问题:

chapter02/08-definitions/macro.cmake

macro(MyMacro myVar)
  set(myVar "new value")
  message("argument: ${myVar}")
endmacro()
set(myVar "first value")
message("myVar is now: ${myVar}")
MyMacro("called value")
message("myVar is now: ${myVar}")

以下是此脚本的输出:

$ cmake -P chapter02/08-definitions/macro.cmake
myVar is now: first value
argument: called value
myVar is now: new value

发生了什么事?尽管明确将 myVar 设置为 new value,但它并没有影响 message("argument: ${myVar}") 的输出!这是因为传递给宏的参数不是作为真正的变量处理,而是作为常数的查找和替换指令。

另一方面,全局作用域中的 myVar 变量从 first value 变为了 new value。这种行为被称为 副作用,并且被认为是坏实践,因为不阅读宏就很难知道哪个变量可能会受到此类宏的影响。

我建议尽可能多地使用函数,因为这可能会节省你很多头疼的问题。

函数

要声明一个命令作为一个函数,请使用以下语法:

function(<name> [<argument>…])
  <commands>
endfunction()

一个函数需要一个名称,可选地接受一个预期参数的名称列表。如果函数调用传递的参数比声明的参数多,多余的参数将被解释为匿名参数并存储在 ARGN 变量中。

如前所述,函数打开它们自己的作用域。你可以调用 set(),提供函数的一个命名参数,任何更改都仅限于函数内部(除非指定了 PARENT_SCOPE,正如我们在 如何在 CMake 中正确使用变量作用域 部分讨论的那样)。

函数遵循调用栈的规则,通过 return() 命令返回调用作用域。

CMake 为每个函数设置了以下变量(这些变量自 3.17 版本以来一直可用):

  • CMAKE_CURRENT_FUNCTION
  • CMAKE_CURRENT_FUNCTION_LIST_DIR
  • CMAKE_CURRENT_FUNCTION_LIST_FILE
  • CMAKE_CURRENT_FUNCTION_LIST_LINE

让我们在实际中看看这些函数变量:

chapter02/08-definitions/function.cmake

function(MyFunction FirstArg)
  message("Function: ${CMAKE_CURRENT_FUNCTION}")
  message("File: ${CMAKE_CURRENT_FUNCTION_LIST_FILE}")
  message("FirstArg: ${FirstArg}")
  set(FirstArg "new value")
  message("FirstArg again: ${FirstArg}")
  message("ARGV0: ${ARGV0} ARGV1: ${ARGV1} ARGC: ${ARGC}")
endfunction()
set(FirstArg "first value")
MyFunction("Value1" "Value2")
message("FirstArg in global scope: ${FirstArg}")

这会打印出以下输出:

Function: MyFunction
File: /root/examples/chapter02/08-definitions/function.cmake
FirstArg: Value1
FirstArg again: new value
ARGV0: Value1 ARGV1: Value2 ARGC: 2
FirstArg in global scope: first value

正如你所看到的,函数的一般语法和概念与宏非常相似,但这次——它实际上起作用了。

CMake 中的过程式范例

让我们假设一下,我们想要以与在 C++ 中编写程序相同的方式编写一些 CMake 代码。我们将创建一个 CMakeLists.txt 列表文件,它将调用三个定义的命令,这些命令可能还会调用它们自己的定义命令:

[外链图片转存中…(img-w79Yel29-1716544491731)]

](https://gitee.com/OpenDocCN/freelearn-c-cpp-pt2-zh/raw/master/docs/mdn-cmk-cpp/img/Figure_2.3_B17205.jpg)

图 2.3 – 过程式调用图

在 CMake 中以这种过程式风格编写代码有点问题——你被迫提供你计划使用的命令定义。CMake 解析器别无选择。你的代码可能看起来像这样:

cmake_minimum_required(...)
project(Procedural)
function(pull_shared_protobuf)
function(setup_first_target)
function(calculate_version)
function(setup_second_target)
function(setup_tests)
setup_first_target()
setup_second_target()
setup_tests()

多么噩梦般的场景!一切都被颠覆了!这段代码非常难以阅读,因为最微小的细节都放在了文件的最顶部。一段正确结构的代码首先在第一个子程序中列出最一般的步骤,然后提供稍微详细一些的子程序,并将最详细的步骤推到最后一个文件。

这个问题有解决方案:将命令定义移动到其他文件并将作用域分区到目录之间(将在 第三章 中详细解释,设置你的第一个 CMake 项目)。但还有一个简单而优雅的解决方案:在文件顶部声明一个入口点宏,并在文件的最后调用它:

macro(main)
function(...) # key steps
function(...) # details
function(...) # fine details
main()

采用这种方法,我们的代码是以逐渐缩小的范围编写的,并且因为我们实际上直到最后才调用main()宏,所以 CMake 不会抱怨未定义命令的执行!

最后一个问题依然存在——为什么要在宏上而不是推荐函数上使用?在这种情况下,无限制访问全局变量是好的,由于我们没有向main()传递任何参数,所以我们不需要担心常见的警告。

你可以在本书 GitHub 仓库中的chapter-02/09-procedural/CMakeLists.txt清单文件中找到这个概念的一个简单示例。

关于命名约定的一点说明

在软件开发中,命名是以著称困难的,尽管如此,维持一个易于阅读和理解解决方案仍然非常重要。在 CMake 脚本和项目方面,我们应该遵循干净代码方法的规则,就像在任何一个软件开发解决方案方面一样:

  • 遵循一致的命名风格(在 CMake 社区中,snake_case是被接受的标准化风格)。
  • 使用简短而有意义的名称(例如,避免func()f()等)。
  • 避免在你的命名中使用双关语和机智。
  • 使用可以发音、可搜索的名称,不需要进行心智映射。

既然我们已经知道如何正确地使用正确的语法调用命令,那么让我们来探讨哪些命令最初对我们最有益。

有用命令

CMake 提供了许多脚本命令,允许你与变量和环境交互。其中一些在附录部分有广泛的覆盖,例如list()string()file()(我们在这里解释这些命令,并在主章节集中精力于项目)。其他的,如find_...(),更适合在讨论管理依赖的章节中。在本节中,我们将简要介绍对脚本最有用的命令。

message()命令

我们已经知道并喜欢我们可靠的message()命令,它将文本打印到标准输出。然而,它的内涵远不止所见。通过提供一个MODE参数,你可以自定义输出的样式,在出错的情况下,你可以停止代码的执行:message( "text")

已识别的模式如下:

  • FATAL_ERROR:这会停止处理和生成。
  • SEND_ERROR:这会继续处理,但跳过生成。
  • WARNING:这会继续处理。
  • AUTHOR_WARNING:CMake 警告。这会继续处理。
  • DEPRECATION:如果启用了CMAKE_ERROR_DEPRECATEDCMAKE_WARN_DEPRECATED变量,这将相应地工作。
  • NOTICE或省略模式(默认):这会在stderr上打印一条消息,以吸引用户的注意。
  • STATUS:这会继续处理,并且建议用于主要用户信息。
  • VERBOSE:这会继续处理,通常用于不必要太详细的更多信息。
  • DEBUG:这会继续处理,并且应该包含在项目出现问题时可能有助于解决问题的任何详细信息。
  • TRACE:这会继续处理,并且在项目开发期间推荐打印消息。通常,这类消息在发布项目之前会被删除。

以下示例在第一条消息后停止执行:

chapter02/10-useful/message_error.cmake

message(FATAL_ERROR "Stop processing")
message("Won't print this.")

这意味着将根据当前日志级别(默认是STATUS)打印消息。我们在上一章的调试和跟踪选项部分讨论了如何更改此设置。在那部分,我承诺要谈论使用CMAKE_MESSAGE_CONTEXT进行调试,所以让我们开始吧。从那时起,我们已经了解了这个谜题的三个重要部分:列表、作用域和函数。

当我们启用一个命令行标志,cmake --log-context,我们的消息将被点分隔的上下文装饰,并存储在CMAKE_MESSAGE_CONTEXT列表中。考虑以下示例:

chapter02/10-useful/message_context.cmake

function(foo)
  list(APPEND CMAKE_MESSAGE_CONTEXT "foo")
  message("foo message")
endfunction()
list(APPEND CMAKE_MESSAGE_CONTEXT "top")
message("Before `foo`")
foo()
message("After `foo`")

前面脚本的输出将如下所示:

$ cmake -P message_context.cmake --log-context
[top] Before `foo`
[top.foo] foo message
[top] After `foo`

函数的初始作用域是从父作用域中复制的(父作用域中已经有一个列表项:top)。foo中的第一条命令向CMAKE_MESSAGE_CONTEXT中添加了一个新项,该项带有foo函数名称。打印消息,函数作用域结束,丢弃本地复制的变量,并恢复之前的范围(不包含foo)。

这种方法在非常复杂的项目中有很多嵌套函数时非常有用。希望您永远不需要它,但我觉得这是一个非常好的例子,展示了函数作用域在实际中是如何工作的。

message()的另一个酷炫技巧是向CMAKE_MESSAGE_INDENT列表中添加缩进(与CMAKE_MESSAGE_CONTEXT完全相同的方式):

list(APPEND CMAKE_MESSAGE_INDENT "  ")

我们脚本的输出可以变得更加整洁:

Before `foo`
  foo message
After `foo`

由于 CMake 没有提供任何真正的带有断点或其他工具的调试器,因此在事情并不完全按计划进行时,生成干净的日志消息功能非常方便。

include()命令

我们可以将 CMake 代码分割到单独的文件中,以保持事物有序,嗯,分离。然后,我们可以通过调用include()从父列表文件中引用它们,如下例所示:

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>])

如果我们提供一个文件名(带有.cmake扩展名的路径),CMake 将尝试打开并执行它。请注意,不会创建嵌套的独立作用域,因此在该文件中对变量的任何更改都将影响调用作用域。

CMake 如果文件不存在将抛出一个错误,除非我们使用OPTIONAL关键字指定它是可选的。如果我们需要知道include()是否成功,我们可以提供带有变量名称的RESULT_VARIABLE关键字。在成功时,它将填充包含成功包含的文件的完整路径,或在失败时(NOTFOUND)不包含。

当以脚本模式运行时,任何相对路径都将从当前工作目录解析。要强制在脚本本身的关系中搜索,请提供一个绝对路径:

include("${CMAKE_CURRENT_LIST_DIR}/<filename>.cmake") 

如果我们没有提供路径,但是提供了模块的名称(不带.cmake或其他),CMake 将尝试查找模块并将其包含进来。CMake 将在CMAKE_MODULE_PATH中搜索名为<模块>.cmake的文件,然后在其模块目录中搜索。

include_guard()命令

当我们包含具有副作用的文件时,我们可能希望限制它们,使它们只被包含一次。这就是include_guard([DIRECTORY|GLOBAL])发挥作用的地方。

include_guard()放在被包含文件的顶部。当 CMake 首次遇到它时,它将在当前作用域中记录这一事实。如果文件再次被包含(也许是因为我们无法控制我们项目中的所有文件),它将不再被进一步处理。

如果我们想要防止在不相关的函数作用域中包含,这些作用域不会共享变量,我们应该提供DIRECTORYGLOBAL参数。正如这些名称所暗示的,DIRECTORY关键字将在当前目录及其子目录内应用保护,而GLOBAL关键字将对整个构建过程应用保护。

file()命令

为了让您了解您可以用 CMake 脚本做什么,让我们快速浏览一下文件操作命令的最有用变体:

file(READ <filename> <out-var> [...])
file({WRITE | APPEND} <filename> <content>...)
file(DOWNLOAD <url> [<file>] [...])

简而言之,file()命令将让您以系统无关的方式读取、写入和传输文件,以及与文件系统、文件锁、路径和存档进行交互。请参阅附录部分以获取更多详细信息。

execute_process()命令

时不时地,您需要使用系统可用的工具(毕竟,CMake 主要是构建系统生成器)。CMake 为此提供了一个命令:您可以使用execute_process()来运行其他进程并收集它们的输出。这个命令非常适合脚本,也可以在配置阶段项目中使用。以下是该命令的一般形式:

execute_process(COMMAND <cmd1> [<arguments>]… [OPTIONS])

CMake 将使用操作系统的 API 来创建子进程(因此,像&&||>这样的 shell 操作符将不起作用)。然而,您仍然可以通过多次提供COMMAND <命令> <参数>参数来链接命令,并将一个的输出传递给另一个。

可选地,您可以使用TIMEOUT <秒>参数来终止进程,如果它没有在规定的时间内完成任务,并且您可以根据需要设置WORKING_DIRECTORY <目录>

所有任务的退出代码可以通过提供RESULTS_VARIABLE <变量>参数来收集在列表中。如果您只对最后执行的命令的结果感兴趣,请使用单数形式:RESULT_VARIABLE <变量>

为了收集输出,CMake 提供了两个参数:OUTPUT_VARIABLEERROR_VARIABLE(这两个参数用法相似)。如果您想合并stdoutstderr,请为这两个参数使用同一个变量。

记住,当为其他用户编写项目时,您应该确保您打算使用的命令在您声称支持的平台上是可用的。

总结

本章打开了使用 CMake 进行实际编程的大门——你现在能够编写伟大的、富有信息性的注释和调用内置命令,并理解如何正确地为它们提供各种参数。这个知识本身将帮助您理解 CMake 列表文件中您可能在其他项目中看到的异常语法。

接下来,我们讲解了 CMake 中的变量——具体来说,是如何引用、设置和取消设置普通、缓存和环境变量。我们深入探讨了目录和函数作用域是如何工作的,并讨论了与嵌套作用域相关的问题(及其解决方法)。

我们还讲解了列表和控制结构。我们讨论了条件的语法、它们的逻辑操作、未引用参数的评估以及字符串和变量。我们学习了如何比较值、进行简单检查以及查看系统文件的状态。这使我们能够编写条件块和 while 循环。在谈论循环的时候,我们也掌握了 foreach 循环的语法。

我相信了解如何使用宏和函数语句定义自己的命令将帮助您以更程序化的风格编写更干净的代码。我们还分享了一些关于如何更好地组织代码和提出更易读名称的想法。

最后,我们正式介绍了message()命令及其多个日志级别。我们还学习了如何分割和包含列表文件,发现了几种其他有用的命令。我相信有了这些材料,我们准备好迎接下一章,并在 CMake 中编写我们的第一个项目。

进一步阅读

关于本章涵盖的主题,您可以参考以下内容:

如果我们没有提供路径,但是提供了模块的名称(不带`.cmake`或其他),CMake 将尝试查找模块并将其包含进来。CMake 将在`CMAKE_MODULE_PATH`中搜索名为`<模块>.cmake`的文件,然后在其模块目录中搜索。
## `include_guard()`命令
当我们包含具有副作用的文件时,我们可能希望限制它们,使它们只被包含一次。这就是`include_guard([DIRECTORY|GLOBAL])`发挥作用的地方。
将`include_guard()`放在被包含文件的顶部。当 CMake 首次遇到它时,它将在当前作用域中记录这一事实。如果文件再次被包含(也许是因为我们无法控制我们项目中的所有文件),它将不再被进一步处理。
如果我们想要防止在不相关的函数作用域中包含,这些作用域不会共享变量,我们应该提供`DIRECTORY`或`GLOBAL`参数。正如这些名称所暗示的,`DIRECTORY`关键字将在当前目录及其子目录内应用保护,而`GLOBAL`关键字将对整个构建过程应用保护。
## `file()`命令
为了让您了解您可以用 CMake 脚本做什么,让我们快速浏览一下文件操作命令的最有用变体:
```cpp
file(READ <filename> <out-var> [...])
file({WRITE | APPEND} <filename> <content>...)
file(DOWNLOAD <url> [<file>] [...])

简而言之,file()命令将让您以系统无关的方式读取、写入和传输文件,以及与文件系统、文件锁、路径和存档进行交互。请参阅附录部分以获取更多详细信息。

execute_process()命令

时不时地,您需要使用系统可用的工具(毕竟,CMake 主要是构建系统生成器)。CMake 为此提供了一个命令:您可以使用execute_process()来运行其他进程并收集它们的输出。这个命令非常适合脚本,也可以在配置阶段项目中使用。以下是该命令的一般形式:

execute_process(COMMAND <cmd1> [<arguments>]… [OPTIONS])

CMake 将使用操作系统的 API 来创建子进程(因此,像&&||>这样的 shell 操作符将不起作用)。然而,您仍然可以通过多次提供COMMAND <命令> <参数>参数来链接命令,并将一个的输出传递给另一个。

可选地,您可以使用TIMEOUT <秒>参数来终止进程,如果它没有在规定的时间内完成任务,并且您可以根据需要设置WORKING_DIRECTORY <目录>

所有任务的退出代码可以通过提供RESULTS_VARIABLE <变量>参数来收集在列表中。如果您只对最后执行的命令的结果感兴趣,请使用单数形式:RESULT_VARIABLE <变量>

为了收集输出,CMake 提供了两个参数:OUTPUT_VARIABLEERROR_VARIABLE(这两个参数用法相似)。如果您想合并stdoutstderr,请为这两个参数使用同一个变量。

记住,当为其他用户编写项目时,您应该确保您打算使用的命令在您声称支持的平台上是可用的。

总结

本章打开了使用 CMake 进行实际编程的大门——你现在能够编写伟大的、富有信息性的注释和调用内置命令,并理解如何正确地为它们提供各种参数。这个知识本身将帮助您理解 CMake 列表文件中您可能在其他项目中看到的异常语法。

接下来,我们讲解了 CMake 中的变量——具体来说,是如何引用、设置和取消设置普通、缓存和环境变量。我们深入探讨了目录和函数作用域是如何工作的,并讨论了与嵌套作用域相关的问题(及其解决方法)。

我们还讲解了列表和控制结构。我们讨论了条件的语法、它们的逻辑操作、未引用参数的评估以及字符串和变量。我们学习了如何比较值、进行简单检查以及查看系统文件的状态。这使我们能够编写条件块和 while 循环。在谈论循环的时候,我们也掌握了 foreach 循环的语法。

我相信了解如何使用宏和函数语句定义自己的命令将帮助您以更程序化的风格编写更干净的代码。我们还分享了一些关于如何更好地组织代码和提出更易读名称的想法。

最后,我们正式介绍了message()命令及其多个日志级别。我们还学习了如何分割和包含列表文件,发现了几种其他有用的命令。我相信有了这些材料,我们准备好迎接下一章,并在 CMake 中编写我们的第一个项目。

进一步阅读

关于本章涵盖的主题,您可以参考以下内容:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
4天前
|
C++
Clion CMake C/C++程序输出乱码
Clion CMake C/C++程序输出乱码
7 0
|
5天前
|
存储 算法 编译器
C++ 函数式编程教程
C++ 函数式编程学习
|
5天前
|
存储 编译器 开发工具
C++语言教程分享
C++语言教程分享
|
5天前
|
存储 编译器 C++
|
26天前
|
缓存 存储 C++
面向 C++ 的现代 CMake 教程(一)(4)
面向 C++ 的现代 CMake 教程(一)
45 0
|
26天前
|
C++ 缓存 存储
面向 C++ 的现代 CMake 教程(一)(3)
面向 C++ 的现代 CMake 教程(一)
43 0
|
26天前
|
缓存 C++ Windows
面向 C++ 的现代 CMake 教程(一)(2)
面向 C++ 的现代 CMake 教程(一)
57 0
|
26天前
|
C++ 容器 Docker
面向 C++ 的现代 CMake 教程(一)(1)
面向 C++ 的现代 CMake 教程(一)
67 0
|
1天前
|
C++
【C++】日期类Date(详解)②
- `-=`通过复用`+=`实现,`Date operator-(int day)`则通过创建副本并调用`-=`。 - 前置`++`和后置`++`同样使用重载,类似地,前置`--`和后置`--`也复用了`+=`和`-=1`。 - 比较运算符重载如`&gt;`, `==`, `&lt;`, `&lt;=`, `!=`,通常只需实现两个,其他可通过复合逻辑得出。 - `Date`减`Date`返回天数,通过迭代较小日期直到与较大日期相等,记录步数和符号。 ``` 这是236个字符的摘要,符合240字符以内的要求,涵盖了日期类中运算符重载的主要实现。
|
4天前
|
C++
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)
7 0
C++职工管理系统(类继承、文件、指针操作、中文乱码解决)