Cmake(Server项目学习)

本文是根据项目30daymakecppserver进行理解cmake的用法,以下是项目地址

yuesong-feng/30dayMakeCppServer: 30天自制C++服务器,包含教程和源代码

day13    

sudo apt-get install clang-tidy
chmod +x /home/felix/Linux\ study/30dayMakeCppServer/code/day13/build_support/cpplint.py
sudo apt-get install clang-format

rm -rf build
mkdir build
cd build
cmake ..
make

set()

  • set 是 CMake 中最常用的命令之一,用于定义和修改变量。
  • 它可以设置简单的变量值、列表、缓存变量以及修改编译器选项、路径等。
  • 通过适当的选项(如 CACHE),可以控制变量的作用域,允许用户在命令行或 GUI 中修改这些值
  • cmake_minimum_required(VERSION 3.10)
    set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
    set(BUILD_SHARED_LIBS ON)
    set(CMAKE_CXX_STANDARD 17)
    set(CMAKE_CXX_STANDARD_REQUIRED ON)
    
    
    # Expected directory structure.
    set(PINE_BUILD_SUPPORT_DIR "${CMAKE_SOURCE_DIR}/build_support")#设定 build_support 目录 存放编译相关工具或脚本
    #设定 Clang 可能的路径	方便 CMake 自动查找 Clang
    set(PINE_CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/usr/local/opt/llvm/bin" "/usr/local/opt/llvm@8/bin" "/usr/local/Cellar/llvm/8.0.1/bin")
    
    # 编译器设置
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall -Wextra -std=c++17 -pthread")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-attributes") #TODO: remove
    set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -ggdb -fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls")
    set(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS} -fPIC")
    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fPIC")
    set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} -fPIC")
    
    # Output directory.  目标文件存放目录
    set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
    set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
    set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

project()

  • <project_name>: 项目的名称(必需)。
  • <languages>: 一个可选参数,指定项目中使用的编程语言,通常包括 CCXX(C++)、FortranCUDA 等。如果不指定,CMake 会根据实际的源代码文件类型自动推断。

project(Pine

        VERSION 0.1

        DESCRIPTION "pine"  #描述

        LANGUAGES C CXX

        )

详细解析 CMake 语法:

file(GLOB_RECURSE pine_sources ${PROJECT_SOURCE_DIR}/src/*.cpp)
add_library(pine_shared SHARED ${pine_sources})

该 CMake 代码用于在 CMake 构建系统中定义一个共享库(Shared Library)。下面我们逐步解析它的语法和功能。


 file()

file(GLOB_RECURSE pine_sources ${PROJECT_SOURCE_DIR}/src/*.cpp)

作用:

file(GLOB_RECURSE …)用于递归地查找指定目录下符合某个模式的所有文件,并将它们的路径存储到变量 pine_sources 中。

语法解析:

  • file(GLOB_RECURSE <变量名> <搜索路径>)

    • GLOB_RECURSE:递归查找所有匹配的文件。

    • <变量名>:存储搜索结果的变量(这里是 pine_sources)。

    • <搜索路径>:文件搜索路径,可以包含通配符(*.cpp)。

在这行代码中:

  • file(GLOB_RECURSE pine_sources ${PROJECT_SOURCE_DIR}/src/*.cpp)

    • PROJECT_SOURCE_DIR:CMake 变量,表示 CMakeLists.txt 所在的项目根目录。

    • src/*.cpp:匹配 src 目录下及其子目录中的所有 .cpp 源文件。

    • 例如,假设 src 目录结构如下:
      src/
      ├── main.cpp
      ├── utils/
      │   ├── helper.cpp
      │   ├── math.cpp
      ├── core/
          ├── engine.cpp
      
      那么 pine_sources 变量会包含:
      src/main.cpp
      src/utils/helper.cpp
      src/utils/math.cpp
      src/core/engine.cpp
      

⚠️ 注意:

  • GLOB_RECURSE 会递归查找 所有子目录,但 CMake 不推荐 在大项目中使用 GLOB,因为它不会在新文件添加时自动更新,可能导致不稳定的构建行为。

add_library()

​​​​​​​add_library(pine_shared SHARED ${pine_sources})

作用:

add_library() 用于创建一个库目标(Library Target)。

语法解析:

add_library(<库名> <库类型> <源文件列表>)
  • <库名>:目标库的名称(这里是 pine_shared)。

  • <库类型>

    • SHARED:创建 共享库(动态库),即 .so(Linux/macOS)或 .dll(Windows)。

    • STATIC:创建 静态库,即 .a(Linux/macOS)或 .lib(Windows)。

    • MODULE:创建 模块库,用于动态加载的插件(通常用于 CMake 插件或特殊用途)。

  • <源文件列表>库的源代码文件(这里是 pine_sources,即 file(GLOB_RECURSE …) 找到的所有 .cpp 文件)。

示例:

如果 pine_sources 变量包含:

src/main.cpp
src/utils/helper.cpp
src/utils/math.cpp
src/core/engine.cpp

那么 add_library(pine_shared SHARED ${pine_sources}) 等价于:

add_library(pine_shared SHARED
    src/main.cpp
    src/utils/helper.cpp
    src/utils/math.cpp
    src/core/engine.cpp
)

这会告诉 CMake 编译这些 .cpp 文件并生成一个共享库,例如:

  • Linux/macOSlibpine_shared.so

  • Windowspine_shared.dll


共享库:

在 C++ 和其他编程语言中,共享库(Shared Library) 是一种动态链接库,允许多个程序共享相同的代码,提高代码复用性和减少可执行文件的体积。常见的共享库格式有:

  • Linux/macOSlibXXX.so
  • WindowsXXX.dll

总结

这两行 CMake 代码的作用是:

  1. 查找 src 目录及其子目录中 所有 .cpp 源文件 并存储在变量 pine_sources 中。

  2. 使用这些 .cpp 文件 生成一个 共享库 pine_shared

如果你还需要将 .h 头文件包含到编译过程中,可以使用:

target_include_directories(pine_shared PUBLIC ${PROJECT_SOURCE_DIR}/include)

这样 CMake 就会在编译时查找 include 目录中的头文件。

🚀 如果你的项目较大,建议手动列出源文件,而不是使用 GLOB_RECURSE,这样能避免因 CMake 缓存未更新而导致的问题。

add_custom_targe:

命令定义的自定应目标,可以使用特定的名称来调用,并执行相应的命令。

add_custom_target(build-tests COMMAND ${CMAKE_CTEST_COMMAND} --show-only)

add_custom_target(check-tests COMMAND ${CMAKE_CTEST_COMMAND} --verbose)

🔹 作用

  • build-tests:调用 ctest --show-only,列出所有测试,但不会执行。
  • check-tests:调用 ctest --verbose,执行所有测试,并输出详细信息。

📌 这样就可以使用 make check-tests 来运行所有测试

在CMake中,通过add_custom_target命令定义的自定义目标,可以使用特定的名称来调用,并执行相应的命令。

总cmake代码:

########################################
foreach (pine_test_source ${PINE_TEST_SOURCES})
    # Create a human readable name.
    get_filename_component(pine_test_filename ${pine_test_source} NAME)
    string(REPLACE ".cpp" "" pine_test_name ${pine_test_filename})

    # Add the test target separately and as part of "make check-tests".
    add_executable(${pine_test_name} EXCLUDE_FROM_ALL ${pine_test_source})
    add_dependencies(build-tests ${pine_test_name})
    add_dependencies(check-tests ${pine_test_name})

    target_link_libraries(${pine_test_name} pine_shared)

    # Set test target properties and dependencies.
    set_target_properties(${pine_test_name}
        PROPERTIES
        RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
        COMMAND ${pine_test_name}
    )

endforeach(pine_test_source ${PINE_TEST_SOURCES})

foreach ()

       foreach (pine_test_source ${PINE_TEST_SOURCES})

 file(GLOB PINE_TEST_SOURCES "${PROJECT_SOURCE_DIR}/test/*.cpp")

作用

  • 遍历 PINE_TEST_SOURCES 变量中的每个测试 .cpp 文件,并为每个测试文件创建独立的可执行目标。

get_filename_component(....... NAME)

get_filename_component(pine_test_filename ${pine_test_source} NAME)

作用:提取 .cpp 文件的文件名(不包含路径)。----pine_test_source表示。.cpp文件名的路径


string(REPLACE ".cpp" "" ...)

去掉 .cpp 后缀,得到可执行文件的名字。

📌 示例: 假设 test/ 目录下有 server_test.cpp,经过这段代码后:

  • pine_test_filename = "server_test.cpp"
  • pine_test_name = "server_test"

最终 server_test 作为目标可执行文件的名称。

add_executable()

add_executable(${pine_test_name} EXCLUDE_FROM_ALL ${pine_test_source})

🔹 作用

  • add_executable(<name> EXCLUDE_FROM_ALL <sources>)
    • 创建可执行文件 <name>,但不默认编译,只有 make server_test 才会编译该文件。
    • EXCLUDE_FROM_ALL 表示不会被 make all 自动构建。

📌 示例

add_executable(server_test EXCLUDE_FROM_ALL server_test.cpp)

这样不会影响 make all,但可以单独 make server_test

add_dependencies()

仅用于构建顺序的依赖,而不是链接依赖,用于管理目标之间的构建顺序依赖

add_dependencies(build-tests ${pine_test_name})
add_dependencies(check-tests ${pine_test_name})

build-testscheck-tests 依赖于所有测试可执行文件

🔹 作用

  • 1.make build-tests 会确保所有测试可执行文件被创建。
  • 2.make check-tests 也会先构建测试可执行文件,然后运行测试。
  • 3.(make check-tests 不会自动执行所有测试可执行文件,它只是构建(编译)所有测试可执行文件,但不会真正运行它们。)
  • 4.执行以上命令时会先构建依赖的文件,然后再执行make "自定义名称"---make build-tests

add_test()可以明确指定了要运行的可执行文件-----上面3情况

target_link_libraries()

target_link_libraries(${pine_test_name} pine_shared)

作用

  • 让测试可执行文件链接 pine_shared 共享库(add_library(pine_shared SHARED ...))。
  • 这样测试代码可以调用共享库 pine_shared 中的所有函数。

set_target_properties()

set_target_properties(${pine_test_name}
    PROPERTIES
    RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
    COMMAND ${pine_test_name}
)

作用

  • RUNTIME_OUTPUT_DIRECTORY:设置测试可执行文件的输出目录,默认为 ${CMAKE_BINARY_DIR}/bin
  • COMMAND ${pine_test_name}:定义测试的执行命令(通常用于 ctest,但这里没有完整定义)。

总结:

这个 CMake 代码的核心作用是:

查找 test/ 目录下所有 .cpp 文件。
构建有意义的可执行文件名(去掉 .cpp 后缀)。
遍历每个 .cpp 文件:
单独构建成可执行文件,而不是整个项目的一个模块。
添加共享库依赖,确保测试程序能访问 pine_shared 代码。
设置可执行文件的输出目录 为 bin/,方便管理。
定义 make check-tests 目标:
让 make check-tests 编译所有测试程序。
但 当前代码不会自动运行测试,需要手动 add_test() 或修改 check-tests 目标。
🚀 最终效果:

make server_test 只编译 server_test.cpp 并生成 bin/server_test。
make check-tests 会编译所有 test/*.cpp 代码,但不会自动执行它们(需要额外修改)。
所有测试可执行文件存放在 bin/ 目录,方便运行和调试。

add_subdirectory()

add_subdirectory(src)
add_subdirectory(test)

这两行 CMake 代码的作用是srctest 目录作为子目录添加到 CMake 构建系统,并让 CMakeLists.txt 继续在这些子目录中查找新的 CMakeLists.txt 文件。

  • 查找 src/test/ 目录 下的 CMakeLists.txt

  • 递归构建

    • src/CMakeLists.txt 负责编译源代码(生成库或可执行文件)。

    • test/CMakeLists.txt 负责编译测试代码(通常会生成测试可执行文件)。

  • 使主 CMakeLists.txt 能够管理整个项目的构建

include_directories()

set(PINE_SRC_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/src/include)
set(PINE_TEST_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/test/include)
include_directories(${PINE_SRC_INCLUDE_DIR} ${PINE_TEST_INCLUDE_DIR})
  • include_directories(...) 告诉编译器去这些目录查找头文件
  • 作用:
    • 编译 src 源代码时,可以使用 #include "my_header.h",而不需要指定完整路径。
    • 编译 test 目录下的测试代码时,同样能找到 test/include/ 内的头文件。
    • PROJECT_SOURCE_DIR 是 CMake 内置变量,表示 CMakeLists.txt 所在的项目根目录。

CMake 内置的编译标志变量

变量名称作用
CMAKE_CXX_FLAGSC++ 代码的全局编译选项
CMAKE_C_FLAGSC 代码的全局编译选项
CMAKE_CXX_FLAGS_DEBUGDebug 模式下的 C++ 代码编译选项
CMAKE_CXX_FLAGS_RELEASERelease 模式下的 C++ 代码编译选项
CMAKE_CXX_FLAGS_RELWITHDEBINFORelease + Debug 信息模式下的编译选项
CMAKE_CXX_FLAGS_MINSIZEREL最小化大小的 Release 模式编译选项

CMAKE_BINARY_DIR内置变量----CMAKE_BINARY_DIR 默认是执行 cmake .. 命令时所在的 构建目录

大总结:

总结
项目初始化:设置基本信息和C++标准。
CMake运行目录检查:避免从错误的目录运行CMake。
工具和Clang配置:设置工具路径、查找并配置 clang-format、clang-tidy、cpplint。
编译器设置:配置编译器和链接器选项。
输出目录设置:设置目标文件、库文件的存放目录。
头文件目录设置:配置头文件的包含路径。
添加子目录:添加源代码和测试代码目录。
代码质量工具:
格式化:使用 clang-format 进行代码格式化。
格式化检查:检查代码是否符合格式要求。
静态分析:使用 cpplint 进行代码风格检查。
高级分析:使用 clang-tidy 进行潜在问题分析。




cmake_minimum_required(VERSION 3.10)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_SHARED_LIBS ON)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

project(Pine
        VERSION 0.1
        DESCRIPTION "pine"
        LANGUAGES C CXX
        )


# People keep running CMake in the wrong folder, completely nuking their project or creating weird bugs.
# This checks if you're running CMake from a folder that already has CMakeLists.txt.
# Importantly, this catches the common case of running it from the root directory.
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" PATH_TO_CMAKELISTS_TXT)
if (EXISTS "${PATH_TO_CMAKELISTS_TXT}")
    message(FATAL_ERROR "Run CMake from a build subdirectory! \"mkdir build ; cd build ; cmake .. \" \
    Some junk files were created in this folder (CMakeCache.txt, CMakeFiles); you should delete those.")
endif ()


# Expected directory structure.
set(PINE_BUILD_SUPPORT_DIR "${CMAKE_SOURCE_DIR}/build_support")#设定 build_support 目录 存放编译相关工具或脚本
#设定 Clang 可能的路径	方便 CMake 自动查找 Clang
set(PINE_CLANG_SEARCH_PATH "/usr/local/bin" "/usr/bin" "/usr/local/opt/llvm/bin" "/usr/local/opt/llvm@8/bin" "/usr/local/Cellar/llvm/8.0.1/bin")

######################################################################################################################
# DEPENDENCIES
######################################################################################################################

# CTest
# enable_testing()
# 查找 clang-format 并设置格式化
# clang-format
if (NOT DEFINED CLANG_FORMAT_BIN)
    # attempt to find the binary if user did not specify
    find_program(CLANG_FORMAT_BIN
            NAMES clang-format clang-format-8
            HINTS ${PINE_CLANG_SEARCH_PATH})
endif ()
if ("${CLANG_FORMAT_BIN}" STREQUAL "CLANG_FORMAT_BIN-NOTFOUND")
    message(WARNING "Pine/main couldn't find clang-format.")
else ()
    message(STATUS "Pine/main found clang-format at ${CLANG_FORMAT_BIN}")
endif ()

# 查找 clang-tidy 并启用编译命令导出
# clang-tidy
if (NOT DEFINED CLANG_TIDY_BIN)
    # attempt to find the binary if user did not specify
    find_program(CLANG_TIDY_BIN
            NAMES clang-tidy clang-fidy-8
            HINTS ${PINE_CLANG_SEARCH_PATH})
endif ()
if ("${CLANG_TIDY_BIN}" STREQUAL "CLANG_TIDY_BIN-NOTFOUND")
    message(WARNING "Pine/main couldn't find clang-tidy.")
else ()
    # Output compile_commands.json
    set(CMAKE_EXPORT_COMPILE_COMMANDS 1)
    message(STATUS "Pine/main found clang-fidy at ${CLANG_TIDY_BIN}")
endif ()

# 代码检查工具 cpplint
# cpplint
find_program(CPPLINT_BIN
        NAMES cpplint cpplint.py
        HINTS ${PINE_BUILD_SUPPORT_DIR})
if ("${CPPLINT_BIN}" STREQUAL "CPPLINT_BIN-NOTFOUND")
    message(WARNING "Pine/main couldn't find cpplint.")
else ()
    message(STATUS "Pine/main found cpplint at ${CPPLINT_BIN}")
endif ()

######################################################################################################################
# COMPILER SETUP
######################################################################################################################

# 编译器设置
# Compiler flags.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall -Wextra -std=c++17 -pthread")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-parameter -Wno-attributes") #TODO: remove
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -O0 -ggdb -fsanitize=address -fno-omit-frame-pointer -fno-optimize-sibling-calls")
set(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS} -fPIC")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fPIC")
set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_STATIC_LINKER_FLAGS} -fPIC")

set(GCC_COVERAGE_LINK_FLAGS "-fPIC")
message(STATUS "CMAKE_CXX_FLAGS: ${CMAKE_CXX_FLAGS}")
message(STATUS "CMAKE_CXX_FLAGS_DEBUG: ${CMAKE_CXX_FLAGS_DEBUG}")
message(STATUS "CMAKE_EXE_LINKER_FLAGS: ${CMAKE_EXE_LINKER_FLAGS}")
message(STATUS "CMAKE_SHARED_LINKER_FLAGS: ${CMAKE_SHARED_LINKER_FLAGS}")

# Output directory.  目标文件存放目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)

# Includes.    设置并包含项目的头文件目录,确保编译时可以正确找到头文件
#设定 PINE_SRC_INCLUDE_DIR 变量,指向 源代码头文件目录(通常存放 .h 头文件)。
#PROJECT_SOURCE_DIR 是 CMake 内置变量,表示 CMakeLists.txt 所在的项目根目录。
set(PINE_SRC_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/src/include)
set(PINE_TEST_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/test/include)
include_directories(${PINE_SRC_INCLUDE_DIR} ${PINE_TEST_INCLUDE_DIR})

#添加子目录
add_subdirectory(src)
add_subdirectory(test)
######################################################################################################################
# MAKE TARGETS
######################################################################################################################

##########################################
# "make format"
# "make check-format"
##########################################
string(CONCAT PINE_FORMAT_DIRS
        "${CMAKE_CURRENT_SOURCE_DIR}/src,"
        "${CMAKE_CURRENT_SOURCE_DIR}/test,"
        )

        # 代码格式化 make format
# runs clang format and updates files in place.
add_custom_target(format ${PINE_BUILD_SUPPORT_DIR}/run_clang_format.py
        ${CLANG_FORMAT_BIN}
        ${PINE_BUILD_SUPPORT_DIR}/clang_format_exclusions.txt
        --source_dirs
        ${PINE_FORMAT_DIRS}
        --fix
        --quiet
        )

#cpplint 静态检查
# runs clang format and exits with a non-zero exit code if any files need to be reformatted
add_custom_target(check-format ${PINE_BUILD_SUPPORT_DIR}/run_clang_format.py
        ${CLANG_FORMAT_BIN}
        ${PINE_BUILD_SUPPORT_DIR}/clang_format_exclusions.txt
        --source_dirs
        ${PINE_FORMAT_DIRS}
        --quiet
        )

##########################################
# "make cpplint"
##########################################

file(GLOB_RECURSE PINE_LINT_FILES
        "${CMAKE_CURRENT_SOURCE_DIR}/src/*.h"
        "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp"
        "${CMAKE_CURRENT_SOURCE_DIR}/test/*.h"
        "${CMAKE_CURRENT_SOURCE_DIR}/test/*.cpp"
        )

# Balancing act: cpplint.py takes a non-trivial time to launch,
# so process 12 files per invocation, while still ensuring parallelism
add_custom_target(cpplint echo '${PINE_LINT_FILES}' | xargs -n12 -P8
        ${CPPLINT_BIN}
        --verbose=2 --quiet
        --linelength=120
        --filter=-legal/copyright,-build/include_subdir,-readability/casting
        )

###########################################################
# "make clang-tidy" target
###########################################################
# runs clang-tidy and exits with a non-zero exit code if any errors are found.
# note that clang-tidy automatically looks for a .clang-tidy file in parent directories
add_custom_target(clang-tidy
        ${PINE_BUILD_SUPPORT_DIR}/run_clang_tidy.py                     # run LLVM's clang-tidy script
        -clang-tidy-binary ${CLANG_TIDY_BIN}                              # using our clang-tidy binary
        -p ${CMAKE_BINARY_DIR}                                            # using cmake's generated compile commands
        )
# add_dependencies(check-clang-tidy pine_shared)                    # needs gtest headers, compile_commands.json

#总结:
#这段 CMake 代码为项目配置了以下目标:

# 代码格式化:使用 clang-format 格式化代码并应用到指定目录。
# 代码格式化检查:检查代码是否符合格式要求,并在不符合时返回错误。
# 静态检查:使用 cpplint 进行静态代码分析,确保代码符合规定的编程风格。
# 高级静态分析:使用 clang-tidy 对代码进行深入分析,查找潜在问题和优化建议

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值