CMAKE应用基础(一)可执行文件及库

本文介绍了CMake的基础用法,包括如何将单个文件编译为可执行文件,切换生成器,构建和链接静态库与动态库。详细讲解了CMakeLists.txt的编写,如添加执行文件目标、设置编译器选项和语言标准,并探讨了条件控制编译、用户选项和指定编译器的方法。示例中展示了如何在CMake中创建静态库和动态库,并讨论了对象库的概念。
摘要由CSDN通过智能技术生成

CMake是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程)。他能够输出各种各样的makefile或者project文件,能测试编译器所支持的C++特性,类似UNIX下的automake。只是 CMake 的组态档取名为 CMakeLists.txt。Cmake 并不直接建构出最终的软件,而是产生标准的建构档(如 Unix 的 Makefile 或 Windows Visual C++ 的 projects/workspaces),然后再依一般的建构方式使用。这使得熟悉某个集成开发环境(IDE)的开发者可以用标准的方式建构他的软件,这种可以使用各平台的原生建构系统的能力是 CMake 和 SCons 等其他类似系统的区别之处。zephyr工程是通过CMAKE组织的,所以我们在学习zephyr开发之前,先掌握cmake的使用。为了方便应用程序中的例子,我们可以在windows10环境下安装wsl(UBUNTU 18.04)。我们需要在wsl后上安装如下的软件:

sudo apt install gawk wget git diffstat unzip texinfo gcc build-essential chrpath socat cpio python3 python3-pip python3-pexpect xz-utils debianutils iputils-ping python3-git python3-jinja2 libegl1-mesa libsdl1.2-dev pylint3 xterm python3-subunit mesa-common-dev zstd liblz4-tool

一、将单个文件编译成可执行文件

在这里插入图片描述

#include <stdio.h>
#include <stdlib.h>

char *say_hello()
{
    return "Hello, CMake world!";
}

int main() {
  printf("%s\n", say_hello());
  return EXIT_SUCCESS;
}

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-01 LANGUAGES C)

add_executable(hello-world hello-world.c)

第一行,设置CMake所需的最低版本。如果使用的CMake版本低于该版本,则会发出致命错误:cmake_minimum_required(VERSION 3.5 FATAL_ERROR)。
第二行,声明了项目的名称(recipe-01)和支持的编程语言(C代表C,CXX代表C++):
project(recipe-01 LANGUAGES C)
第三行,指示CMake创建一个新目标:可执行文件hello-world。这个可执行文件是通过编译和链接源文件hello-world.c生成的。CMake将为编译器使用默认设置,并自动选择生成工具:
add_executable(hello-world hello-world.c)
接下来就可以通过CMAKE来构建我们的helloworld应用。

mkdir build
cd build
cmake ..

构建过程如下图所示:
在这里插入图片描述
也可以使用如下:

 cmake -H. -Bbuild

-H.表示在当前目录中搜索根CMakeLists.txt文件。-Bbuild告诉CMake在一个名为build的目录中生成所有的文件。这是CMAKE的标准使用方式。
CMAKE构建的目标比生成一个HELLOWORLD可执行文件更多,可以打开Makefile:
在这里插入图片描述
1.all:是默认目标,将在项目中构建所有目标。
2.clean:删除所有生成的文件。
3.rebuild_cache:将调用CMake为源文件生成依赖(如果有的话)。
4.其它在后面章节中介绍。

二、生成器切换问题

CMake是一个构建系统生成器,可以使用单个CMakeLists.txt为不同平台上的不同工具集配置项目。您可以在CMakeLists.txt中描述构建系统必须运行的操作,以配置并编译代码。基于这些指令,CMake将为所选的构建系统(Unix Makefile、Ninja、Visual Studio等等)生成相应的指令。可使用:

cmake --help

在这里插入图片描述
CMake针对不同平台支持本地构建工具列表。同时支持命令行工具(如Unix Makefile和Ninja)和集成开发环境(IDE)工具。
从上图可以看到,我们执行的是unix Makefile生成器。如果需要生成其它平台的构建系统,如Ninja、Visual Studio,需要显式指明生成器:
在这里插入图片描述
每个生成器都有自己的文件集,所以编译步骤的输出和构建目录的内容是不同的:
1.build.ninja和rules.ninja:包含Ninja的所有的构建语句和构建规则。
2.CMakeCache.txt:CMake会在这个文件中进行缓存,与生成器无关。
3.CMakeFiles:包含由CMake在配置期间生成的临时文件。
4.cmake_install.cmake:CMake脚本处理安装规则,并在安装时使用。

三、构建和链接静态库和动态库

3.1、静态库与动态库

项目中c通常有多个源文件,分布在不同子目录中。这种实践有助于项目的源代码结构,而且支持模块化、代码重用和关注点分离。同时,这种分离可以简化并加速项目的重新编译。请看下面一个例子,将message.c与message.h生成一个静态或者动态库供hellworld.c主程序调用。
在这里插入图片描述
cmake脚本如下:

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-03 LANGUAGES C)


# generate a library from sources
add_library(message
      STATIC
    # SHARED
    message.c
    message.h
  )

add_executable(hello-world hello-world.c)

target_link_libraries(hello-world message)

1.创建目标——静态库。库的名称和源码文件名相同,具体代码如下:

add_library(message
      STATIC
    # SHARED
    message.c
    message.h
  )

STATIC是创建静态库,SHARED是创建动态库。
2.创建hello-world可执行文件的目标部分不需要修改:

add_executable(hello-world hello-world.c)

3.最后,将目标库链接到可执行目标:

target_link_libraries(hello-world message)

注意:添加库不带lib前缀及后面的a或者so。
1.add_library(message STATIC message.h= Message.c):生成必要的构建指令,将指定的源码编译到库中。add_library的第一个参数是目标名。整个CMakeLists.txt中,可使用相同的名称来引用库。生成的库的实际名称将由CMake通过在前面添加前缀lib和适当的扩展名作为后缀来形成。生成库是根据第二个参数(STATIC或SHARED)和操作系统确定的。
2.target_link_libraries(hello-world message): 将库链接到可执行文件。此命令还确保hello-world可执行文件可以正确地依赖于message库。因此,在消息库链接到hello-world可执行文件之前,需要完成message库的构建。
编译成功后,构建目录包含libmessage.a一个静态库(在GNU/Linux上)和hello-world可执行文件。

CMake接受其他值作为add_library的第二个参数的有效值,我们来看下本书会用到的值:
1.STATIC:用于创建静态库,即编译文件的打包存档,以便在链接其他目标时使用,例如:可执行文件。
2.SHARED:用于创建动态库,即可以动态链接,并在运行时加载的库。可以在CMakeLists.txt中使用add_library(message SHARED message.h Message.c) 从静态库切换到动态共享对象(DSO)。
3.OBJECT:可将给定add_library的列表中的源码编译到目标文件,不将它们归档到静态库中,也不能将它们链接到共享对象中。如果需要一次性创建静态库和动态库,那么使用对象库尤其有用。
4.MODULE:又为DSO组。与SHARED库不同,它们不链接到项目中的任何目标,不过可以进行动态加载。该参数可以用于构建运行时插件。
5.IMPORTED:此类库目标表示位于项目外部的库。此类库的主要用途是,对现有依赖项进行构建。
6.INTERFACE:与IMPORTED库类似。不过,该类型库可变,没有位置信息。
7.ALIAS:顾名思义,这种库为项目中已存在的库目标定义别名。不过,不能为IMPORTED库选择别名。
接下来讲一下object库,其它库在其它章节讲述,目前不要关心及深入。

3.2、object库

先展示一下基于hellworld工程的CMAKE脚本:

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-03 LANGUAGES C)

# generate an object library from sources
add_library(message-objs
    OBJECT
    message.h
    message.c
  )

# this is only needed for older compilers
# but doesn't hurt either to have it
set_target_properties(message-objs
PROPERTIES
    POSITION_INDEPENDENT_CODE 1
  )

add_library(message-shared
SHARED
    $<TARGET_OBJECTS:message-objs>
  )
#libmessage-shared.a -->libmessage.a  
set_target_properties(message-shared
PROPERTIES
    OUTPUT_NAME "message"
  )

add_library(message-static
STATIC
    $<TARGET_OBJECTS:message-objs>
  )
#libmessage-shared.so -->libmessage.so
set_target_properties(message-static
PROPERTIES
    OUTPUT_NAME "message"
  )

add_executable(hello-world hello-world.c)

target_link_libraries(hello-world message-shared)
#target_link_libraries(hello-world message-static)

OUTPUTNAME 是将MESSAGE-STAITC,massage-share的输出文件名统一变成message.这样我们就可以生成libmessage.a,libmessage.so两个库,在调用时使用一个库名message即可。
在这里插入图片描述

四、条件控制编译

在构建过程中,我们希望使用变量或者用户先项传入变量来控制流程。为了确保完全控制构建项目、配置、编译和链接所涉及的所有步骤的执行流,CMake提供了自己的语言。
在这里插入图片描述
完整脚本如下:

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-04 LANGUAGES C)

# introduce a toggle for using a library
#set(USE_LIBRARY OFF)

set(USE_LIBRARY ON)
message(STATUS "Compile sources into a library? ${USE_LIBRARY}")

# BUILD_SHARED_LIBS is a global flag offered by CMake
# to toggle the behavior of add_library
set(BUILD_SHARED_LIBS ON)

#set(BUILD_SHARED_LIBS OFF)
# list sources
list(APPEND _sources message.h message.c)

if(USE_LIBRARY)
  # add_library will create a static library
  # since BUILD_SHARED_LIBS is OFF
  add_library(message ${_sources})

  add_executable(hello-world hello-world.c)

  target_link_libraries(hello-world message)
else()
  add_executable(hello-world hello-world.c ${_sources})
endif()

我们介绍了两个变量:USE_LIBRARY和BUILD_SHARED_LIBS。这两个变量都设置为OFF。如CMake语言文档中描述,逻辑真或假可以用多种方式表示:
1.如果将逻辑变量设置为以下任意一种:1、ON、YES、true、Y或非零数,则逻辑变量为true。
2.如果将逻辑变量设置为以下任意一种:0、OFF、NO、false、N、IGNORE、NOTFOUND、空字符串,或者以-NOTFOUND为后缀,则逻辑变量为false。
这个例子说明,可以引入条件来控制CMake中的执行流。但是,当前的设置不允许从外部切换,不需要手动修改CMakeLists.txt。原则上,我们希望能够向用户开放所有设置,这样就可以在不修改构建代码的情况下调整配置.

五、用户选项

前面的配置中,我们引入了条件句:通过硬编码的方式给定逻辑变量值。不过,这会影响用户修改这些变量。CMake代码没有向读者传达,该值可以从外部进行修改。推荐在CMakeLists.txt中使用option()命令,以选项的形式显示逻辑开关,用于外部设置,从而切换构建系统的生成行为。

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-05 LANGUAGES C)


# expose options to the user
option(USE_LIBRARY  "Compile sources into a library"  OFF)


if (DEFINED USE_LIBRARY)
    message(STATUS "USE_LIBRARY defined: " ${USE_LIBRARY})
else ()     
    message(STATUS "USE_LIBRARY un-defined: " ${USE_LIBRARY})
endif()


message(STATUS "Compile sources into a library? ${USE_LIBRARY}")

include(CMakeDependentOption)

# second option depends on the value of the first
cmake_dependent_option(
  MAKE_STATIC_LIBRARY "Compile sources into a static library" OFF
  "USE_LIBRARY" ON
  )

# third option depends on the value of the first
cmake_dependent_option(
  MAKE_SHARED_LIBRARY "Compile sources into a shared library" ON
  "USE_LIBRARY" ON
  )

set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)

# list sources
list(APPEND _sources message.h message.c)

if(USE_LIBRARY)
  message(STATUS "Compile sources into a STATIC library? ${MAKE_STATIC_LIBRARY}")
  message(STATUS "Compile sources into a SHARED library? ${MAKE_SHARED_LIBRARY}")

  if(MAKE_SHARED_LIBRARY)
    add_library(message SHARED ${_sources})

    add_executable(hello-world hello-world.c)

    target_link_libraries(hello-world message)
  endif()

  if(MAKE_STATIC_LIBRARY)
    add_library(message STATIC ${_sources})

    add_executable(hello-world hello-world.c)

    target_link_libraries(hello-world message)
  endif()
else()
  add_executable(hello-world hello-world.c ${_sources})
endif()

看一下前面示例中的静态/动态库示例。与其硬编码USE_LIBRARY为ON或OFF,现在为其设置一个默认值,同时也可以从外部进行更改:
1.用一个选项替换上一个示例的set(USE_LIBRARY OFF)命令。该选项将修改USE_LIBRARY的值,并设置其默认值为OFF:
option(USE_LIBRARY “Compile sources into a library” OFF)
2.现在,可以通过CMake的-DCLI选项,将信息传递给CMake来切换库的行为:cmake -D USE_LIBRARY=ON …
option可接受三个参数:
1.option(<option_variable> “help string” [initial value])
2.<option_variable>表示该选项的变量的名称。
3."help string"记录选项的字符串,在CMake的终端或图形用户界面中可见。
[initial value]选项的默认值,可以是ON或OFF。

六、指定编译器

目前为止,我们还没有过多考虑如何选择编译器。CMake可以根据平台和生成器选择编译器,还能将编译器标志设置为默认值。然而,我们通常控制编译器的选择。CMake将语言的编译器存储在CMAKE__COMPILER变量中,其中是受支持的任何一种语言,对于我们的目的是CXX、C或Fortran。用户可以通过以下两种方式之一设置此变量:
1.使用CLI中的-D选项,例如:
$ cmake -D CMAKE_CXX_COMPILER=clang++ …
2.通过导出环境变量CXX(C++编译器)、CC(C编译器)和FC(Fortran编译器)。例如,使用这个命令使用clang++作为C++编译器:
$ env CXX=clang++ cmake …

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-06 LANGUAGES C CXX)

message(STATUS "Is the C++ compiler loaded? ${CMAKE_CXX_COMPILER_LOADED}")
if(CMAKE_CXX_COMPILER_LOADED)
  message(STATUS "The C++ compiler ID is: ${CMAKE_CXX_COMPILER_ID}")
  message(STATUS "Is the C++ from GNU? ${CMAKE_COMPILER_IS_GNUCXX}")
  message(STATUS "The C++ complier is:${CMAKE_CXX_COMPILER}")
  message(STATUS "The C++ compiler version is: ${CMAKE_CXX_COMPILER_VERSION}")
endif()

message(STATUS "Is the C compiler loaded? ${CMAKE_C_COMPILER_LOADED}")
if(CMAKE_C_COMPILER_LOADED)
  message(STATUS "The C compiler ID is: ${CMAKE_C_COMPILER_ID}")
  message(STATUS "Is the C from GNU? ${CMAKE_COMPILER_IS_GNUCC}")
  message(STATUS "The C compiler is :${CMAKE_C_COMPILER}")
  message(STATUS "The C compiler version is: ${CMAKE_C_COMPILER_VERSION}")
endif()

CMake提供了额外的变量来与编译器交互:
1.CMAKE_COMPILER_LOADED:如果为项目启用了语言,则将设置为TRUE。
2.CMAKE
COMPILER_ID:编译器标识字符串,编译器供应商所特有。例如,GCC用于GNU编译器集合,AppleClang用于macOS上的Clang, MSVC用于Microsoft Visual Studio编译器。注意,不能保证为所有编译器或语言定义此变量。
3.CMAKE_COMPILER_IS_GNU:如果语言是GNU编译器集合的一部分,则将此逻辑变量设置为TRUE。注意变量名的部分遵循GNU约定:C语言为CC, C++语言为CXX, Fortran语言为G77。
4.CMAKE
_COMPILER_VERSION:此变量包含一个字符串,该字符串给定语言的编译器版本。

七、切换构建类型

CMake可以配置构建类型,例如:Debug、Release等。配置时,可以为Debug或Release构建设置相关的选项或属性,例如:编译器和链接器标志。控制生成构建系统使用的配置变量是CMAKE_BUILD_TYPE。该变量默认为空,CMake识别的值为:
1.Debug:用于在没有优化的情况下,使用带有调试符号构建库或可执行文件。
2.Release:用于构建的优化的库或可执行文件,不包含调试符号。
3.RelWithDebInfo:用于构建较少的优化库或可执行文件,包含调试符号。
4.MinSizeRel:用于不增加目标代码大小的优化方式,来构建库或可执行文件。

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-07 LANGUAGES C CXX)

# we default to Release build type
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()

message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")

message(STATUS "C flags, Debug configuration: ${CMAKE_C_FLAGS_DEBUG}")
message(STATUS "C flags, Release configuration: ${CMAKE_C_FLAGS_RELEASE}")
message(STATUS "C flags, Release configuration with Debug info: ${CMAKE_C_FLAGS_RELWITHDEBINFO}")
message(STATUS "C flags, minimal Release configuration: ${CMAKE_C_FLAGS_MINSIZEREL}")

message(STATUS "C++ flags, Debug configuration: ${CMAKE_CXX_FLAGS_DEBUG}")
message(STATUS "C++ flags, Release configuration: ${CMAKE_CXX_FLAGS_RELEASE}")
message(STATUS "C++ flags, Release configuration with Debug info: ${CMAKE_CXX_FLAGS_RELWITHDEBINFO}")
message(STATUS "C++ flags, minimal Release configuration: ${CMAKE_CXX_FLAGS_MINSIZEREL}")

设置一个默认的构建类型(本例中是Release),并打印一条消息。要注意的是,该变量被设置为缓存变量,可以通过缓存进行编辑。

cmake -D CMAKE_BUILD_TYPE=Debug ..
​
-- Build type: Debug
-- C flags, Debug configuration: -g
-- C flags, Release configuration: -O3 -DNDEBUG
-- C flags, Release configuration with Debug info: -O2 -g -DNDEBUG
-- C flags, minimal Release configuration: -Os -DNDEBUG
-- C++ flags, Debug configuration: -g
-- C++ flags, Release configuration: -O3 -DNDEBUG
-- C++ flags, Release configuration with Debug info: -O2 -g -DNDEBUG
-- C++ flags, minimal Release configuration: -Os -DNDEBUG

八、设置编译器选项及语言标准

1、编译器选项

CMake为调整或扩展编译器标志提供了很大的灵活性,您可以选择下面两种方法:
1.CMake将编译选项视为目标属性。因此,可以根据每个目标设置编译选项,而不需要覆盖CMake默认值。
2.可以使用-DCLI标志直接修改CMAKE_FLAGS变量。这将影响项目中的所有目标,并覆盖或扩展CMake默认值。

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-08 LANGUAGES C)

message("C++ compiler flags: ${CMAKE_C_FLAGS}")

list(APPEND flags "-fPIC" "-Wall")
if(NOT WIN32)
  list(APPEND flags "-Wextra" "-Wpedantic")
endif()

add_library(geometry
  STATIC
    geometry_circle.c
    geometry_circle.h
    geometry_polygon.c
    geometry_polygon.h
    geometry_rhombus.c
    geometry_rhombus.h
    geometry_square.c
    geometry_square.h
  )

target_compile_options(geometry
  PRIVATE
    ${flags}
  )

add_executable(compute-areas compute-areas.c)

target_compile_options(compute-areas
  PRIVATE
    "-fPIC"
  )

target_link_libraries(compute-areas geometry)
target_link_libraries(compute-areas m)

在这里插入图片描述
本例中,警告标志有-Wall、-Wextra和-Wpedantic,将这些标示添加到geometry目标的编译选项中; compute-areas和 geometry目标都将使用-fPIC标志。编译选项可以添加三个级别的可见性:INTERFACE、PUBLIC和PRIVATE。
可见性的含义如下:
1.PRIVATE,编译选项会应用于给定的目标,不会传递给与目标相关的目标。我们的示例中, 即使compute-areas将链接到geometry库,compute-areas也不会继承geometry目标上设置的编译器选项。
2.INTERFACE,给定的编译选项将只应用于指定目标,并传递给与目标相关的目标。
3.PUBLIC,编译选项将应用于指定目标和使用它的目标。
上面可以在构建时使用VERBOSE=1来验证:

cmake --build . -- VERBOSE=1

控制编译器标志的第二种方法,不用对CMakeLists.txt进行修改。如果想在这个项目中修改geometry和compute-areas目标的编译器选项,可以使用CMake参数进行配置:

cmake -D CMAKE_CXX_FLAGS="-fno-exceptions -fno-rtti" ..

除些这外还可以根据CMAKE__COMPILER_ID进行定义等。

2、语言标准

et(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
set_target_properties(animals
  PROPERTIES
    CXX_STANDARD 14
    CXX_EXTENSIONS OFF
    CXX_STANDARD_REQUIRED ON
    POSITION_INDEPENDENT_CODE 1
)

1.CXX_STANDARD会设置我们想要的标准。
2.CXX_EXTENSIONS告诉CMake,只启用ISO C++标准的编译器标志,而不使用特定编译器的扩展。
3.CXX_STANDARD_REQUIRED指定所选标准的版本。如果这个版本不可用,CMake将停止配置并出现错误。当这个属性被设置为OFF时,CMake将寻找下一个标准的最新版本,直到一个合适的标志。这意味着,首先查找C++14,然后是C++11,然后是C++98。

九、使用控制流

前面的示例中,已经使用过if-else-endif。CMake还提供了创建循环的语言工具:foreach endforeach和while-endwhile。两者都可以与break结合使用,以便尽早从循环中跳出。本示例将展示如何使用foreach,来循环源文件列表。我们将应用这样的循环,在引入新目标的前提下,来为一组源文件进行优化降级。

# set minimum cmake version
cmake_minimum_required(VERSION 3.5 FATAL_ERROR)

# project name and language
project(recipe-10 LANGUAGES C)

add_library(geometry
  STATIC
    geometry_circle.c
    geometry_circle.h
    geometry_polygon.c
    geometry_polygon.h
    geometry_rhombus.c
    geometry_rhombus.h
    geometry_square.c
    geometry_square.h
  )

# we wish to compile the library with the optimization flag: -O3
target_compile_options(geometry
  PRIVATE
    -O3
  )

list(
  APPEND sources_with_lower_optimization
    geometry_circle.c
    geometry_rhombus.c
  )

# we use the IN LISTS foreach syntax to set source properties
message(STATUS "Setting source properties using IN LISTS syntax:")
foreach(_source IN LISTS sources_with_lower_optimization)
  set_source_files_properties(${_source} PROPERTIES COMPILE_FLAGS -O2)
  message(STATUS "Appending -O2 flag for ${_source}")
endforeach()

# we demonstrate the plain foreach syntax to query source properties
# which requires to expand the contents of the variable
message(STATUS "Querying sources properties using plain syntax:")
foreach(_source ${sources_with_lower_optimization})
  get_source_file_property(_flags ${_source} COMPILE_FLAGS)
  message(STATUS "Source ${_source} has the following extra COMPILE_FLAGS: ${_flags}")
endforeach()

add_executable(compute-areas compute-areas.c)

target_link_libraries(compute-areas geometry)
target_link_libraries(compute-areas m)

在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如之

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值