深入解析C/C++链接错误:undefined reference的五大根源与系统化排查指南

1. 从一次深夜编译报错说起

那天晚上,我正为一个嵌入式项目收尾,代码在本地IDE里跑得好好的,逻辑清晰,编译通过。但当我准备将其部署到目标Linux开发板上,切换到交叉编译工具链进行最终构建时,熟悉的终端里蹦出了一行刺眼的红色错误: undefined reference to HAL_RCC_OscConfig'`。那一刻,我知道,又一个“undefined reference to”的夜晚开始了。这几乎是每一个C/C++开发者,无论是刚入门的新手还是像我这样摸爬滚打多年的老手,都必定会遭遇的“成长仪式”。它不像语法错误那样直接告诉你第几行有问题,它更像一个侦探游戏,线索隐藏在编译器输出的最后几行,需要你顺着符号(Symbol)的蛛丝马迹,去梳理整个项目的依赖迷宫。

简单来说,“undefined reference to XXX”是一个 链接错误(Linker Error) ,发生在编译过程的最后阶段——链接(Linking)。我们的源代码(.c, .cpp)先被编译器(如gcc, g++)翻译成目标文件(.o),里面包含了函数和变量的二进制代码以及它们的名字(即符号)。链接器(ld)的任务,就是把所有相关的目标文件以及我们指定的库文件(静态库.a或动态库.so)拼装成一个完整的可执行程序。在这个过程中,如果链接器发现某个地方声明要使用一个函数或变量(比如你调用了 printf ),但在它翻遍了所有提供的目标文件和库之后,依然找不到这个函数或变量具体定义在哪里,它就会抛出这个错误,告诉你:“喂,你让我找的‘XXX’这个东西,我没找到定义啊,它是‘undefined’(未定义)的。”

这个错误背后牵扯到的是C/C++项目构建的核心:编译单元、符号解析和库管理。接下来,我们就层层剥茧,看看这个“幽灵符号”究竟从何而来,又该如何将它“缉拿归案”。

2. 错误根源的五大常见“案发现场”

遇到“undefined reference”,盲目搜索和尝试是效率最低的做法。首先得像个侦探一样,对错误信息进行现场勘查。错误信息中的“XXX”就是我们的核心线索。根据这个符号的性质和项目上下文,我们可以将问题源头归为以下几大类。

2.1 案发现场一:简单的源码遗漏或拼写错误

这是新手最容易踩的坑,也最容易解决。

  • 函数/变量只声明,未定义 :你在头文件(.h)里声明了一个函数 void my_func(); ,或者在某个.c文件里用 extern 声明了一个外部变量,但在任何一个.c/.cpp源文件中都没有给出这个函数的具体实现(即函数体 {...} )或变量的定义(如 int global_var = 10; )。链接器自然找不到。
  • 拼写或签名不匹配 :C/C++区分大小写,且函数签名(包括返回类型、函数名、参数类型)必须完全一致。头文件里声明的是 void processData(int num); ,但实现时写成了 void processdata(int num) (小写d)或者 void processData(float num) (参数类型不同)。对于C++,由于名字修饰(Name Mangling),即使微小的签名差异也会导致链接器看到的符号名完全不同。
  • 源码文件未加入编译 :你的项目由多个.c/.cpp文件组成,但在编译命令或Makefile/CMakeLists.txt中,你只列出了部分文件,遗漏了包含那个函数定义的文件。例如,你只编译了 main.c ,但函数定义在 utils.c 里,而 utils.c 没有被编译成 utils.o 并参与链接。

排查技巧 :首先,检查错误信息中的符号名,在你的工程目录下全局搜索( grep -r “symbol_name” . )。确认它是否在某个源文件中被定义。对于C++项目,可以使用 nm objdump -t 命令查看目标文件(.o)或库文件(.a/.so)中导出的符号,看看你需要的符号是否存在,以及它的修饰名是什么。

2.2 案发现场二:库文件链接的“三宗罪”

当错误符号是标准库函数(如 printf )或第三方库函数(如OpenCV的 cv::imread )时,问题通常出在链接库的环节。

  • 罪一:忘记指定链接库(-l) :这是最普遍的原因。你使用了数学库 libm.so 里的 sin 函数,编译命令是 gcc main.c -o main ,却忘了加 -lm 。链接器只在默认的几条路径里找,不会自动去链接所有库。你需要用 -l (小写L)参数指明库名(去掉前缀 lib 和后缀 .so/.a ),例如 -lm -lpthread -lopencv_core
  • 罪二:库文件搜索路径不对(-L) :你通过 -l 指定了库名,但链接器在标准路径(如 /usr/lib , /usr/local/lib )下找不到这个库。如果你的库安装在自定义目录,比如 /home/user/my_libs/ ,就需要用 -L 参数指定额外的搜索路径: gcc main.c -o main -L/home/user/my_libs -lmylib
  • 罪三:库文件顺序至关重要 :链接器处理库文件列表时,是 从左到右 进行的。它只解决当前已读入目标文件中未定义的引用。如果库A依赖库B,那么必须把A放在B的前面。例如,如果你的程序依赖 libfoo.a ,而 libfoo.a 又依赖 libbar.a ,那么链接顺序应该是 -lfoo -lbar 。如果顺序反了,链接器在处理 main.o 时看到对 foo 中函数的引用,但还没读到 libfoo.a ,等读到 libfoo.a 时,它内部的未定义引用(指向 libbar.a )又无法被后面已经处理过的库解决,从而导致错误。一个常见的做法是把最基础的、被依赖最多的库放在命令的 最后面

2.3 案发现场三:C与C++混合编程的符号混淆

在C++项目中调用C语言编写的库,或者在C项目中链接C++编译的库,如果没有正确处理符号的命名约定,必然会导致“undefined reference”。

  • 问题本质 :C++支持函数重载,编译器会对函数名进行“名字修饰”(Mangling),将参数类型等信息编码进最终符号名,以确保链接时能区分 void foo(int) void foo(float) 。而C语言没有这个机制,符号名就是函数名本身。因此,一个用C编译器编译的 libfoo.a 中的函数 bar ,在C++看来,它的符号名就是 bar ;但如果你用C++编译器去编译调用 bar 的代码,并且没有特别说明,C++编译器会认为 bar 是一个C++函数,试图寻找一个被修饰过的符号(如 _Z3barv ),结果当然是找不到。

  • 解决方案: extern “C” :在C++代码中,凡是包含C语言函数或变量声明的头文件,都需要用 extern “C” 包裹起来,告诉C++编译器:“这个块里的声明,请按C语言的规则来处理符号名”。

    // 在C++源文件中,包含C头文件时
    extern “C” {
        #include “my_c_library.h”
    }
    
    // 或者,更规范的做法是在C头文件本身里就做好兼容性声明
    #ifdef __cplusplus
    extern “C” {
    #endif
    // 原有的C函数声明...
    void my_c_function(int);
    #ifdef __cplusplus
    }
    #endif
    

2.4 案发现场四:静态库与动态库的“陷阱”

  • 静态库(.a)的链接特殊性 :静态库本质上是一组目标文件(.o)的打包。链接器在处理静态库时,采用一种“按需提取”的策略。它从库中 只提取 那些能解决 当前已存在 的未定义引用的目标文件。如果静态库中的各个目标文件之间存在循环依赖,或者库的打包顺序不合理,可能会导致某些必需的符号因为“当前未被引用”而没有被提取出来,最终导致链接失败。有时需要将同一个静态库在链接命令行中重复出现多次,或者调整库的顺序。
  • 动态库(.so)的运行时错误 undefined reference 也可能发生在程序 运行时 ,表现为动态链接错误。这通常是因为:
    1. 编译链接时指定了动态库,但运行时找不到 :编译时用 -l 链接了 .so ,但运行时的动态链接器(如 ld-linux.so )在默认路径( LD_LIBRARY_PATH 环境变量或 /etc/ld.so.conf 配置的路径)中找不到对应的 .so 文件。你需要设置 LD_LIBRARY_PATH 或将库路径添加到系统配置中。
    2. 符号版本不匹配 :库升级后,函数签名或ABI(应用程序二进制接口)发生了变化,但你的程序仍然链接着旧版本的头文件或寻找旧版本的符号,导致运行时符号解析失败。

2.5 案发现场五:特定开发环境与工具的“特色”问题

  • 嵌入式开发(如STM32CubeIDE) :就像我开头遇到的 HAL_RCC_OscConfig 错误。这通常是因为:
    • 启动文件(Startup File)或链接脚本(Linker Script)配置不当 ,没有包含必要的芯片外设库或HAL库文件。
    • 在IDE的工程配置中, 没有正确添加对应HAL驱动模块的源文件路径 。STM32CubeMX生成的代码,有时需要手动在工程属性里添加 Drivers/STM32xx_HAL_Driver/Src 目录下的特定 .c 文件。
    • 使用了某个芯片型号特有的HAL函数,但工程配置的芯片型号或HAL库版本不支持该函数。
  • Windows下的MinGW或Cygwin :常见的 undefined reference to WinMain' 错误。这通常是因为你试图将一个控制台应用程序(入口函数是 main )的项目,错误地配置成了Windows GUI应用程序(入口函数是 WinMain )。检查你的编译器链接选项,确保没有误加 -mwindows`之类的参数(对于GUI程序才需要)。
  • CMake项目 :忘记使用 target_link_libraries(your_target PRIVATE/ PUBLIC some_lib) 命令来明确指定目标所依赖的库。或者, find_package 找到了库,但没有将其导入的目标(如 OpenCV::opencv_core )链接到你的目标上。

3. 系统化的诊断与排查流程

面对一个“undefined reference”错误,遵循一个系统的排查流程可以事半功倍。

第一步:精读错误信息 不要只看最后一行。从编译器/链接器输出的顶部开始看,确认是哪个源文件(或目标文件)在调用这个未定义的符号。错误信息通常会给出调用发生的位置(文件名和行号),这是最重要的线索。

第二步:确认符号定义是否存在

  1. 在源码中搜索 :在项目所有源文件和头文件中,搜索错误符号的名称,确认它是否被正确定义(对于函数,要有函数体 {} ;对于变量,要有不带 extern 的定义)。
  2. 在目标文件和库中搜索 :如果怀疑是库的问题,使用 nm 工具。
    • nm -C your_object_file.o | grep symbol_name :查看目标文件中定义的( T D 类型)和未定义的( U 类型)符号。 -C 参数用于解码C++的修饰名。
    • nm -C libyourlibrary.a | grep symbol_name :查看静态库中的符号。注意静态库是 .a 文件。
    • nm -D libyourlibrary.so | grep symbol_name :查看动态库导出的符号( -D 参数)。对于动态库是 .so 文件。
    • 如果 nm 找不到,可以尝试更详细的 objdump -t readelf -s

第三步:检查编译和链接命令 这是解决库相关问题的关键。完整地查看你的构建命令(无论是直接输入的gcc命令,还是Makefile/CMake生成的命令)。

  1. 是否有 -l 参数 ?确保链接了所有必需的库。
  2. 库的顺序是否正确 ?记住依赖关系:被依赖的库放在后面。可以尝试将基础库(如 -lm , -lpthread , -ldl )移到命令末尾。
  3. 是否有 -L 参数指定非标准库路径 ?路径是否正确?
  4. 对于C++调用C库,是否有 extern “C”
  5. 对于静态库依赖,是否需要在命令行中重复出现 ?有时可以尝试将出问题的库在命令行中写两遍。

第四步:检查构建系统(Makefile/CMake)

  1. Makefile :检查 LDFLAGS (链接器标志)和 LDLIBS (链接的库)变量是否包含了所有必要的 -L -l 选项。检查目标(target)的依赖项(prerequisites)是否包含了所有需要编译的源文件。
  2. CMake
    • 使用 message() 打印 CMAKE_CXX_FLAGS , CMAKE_EXE_LINKER_FLAGS 以及目标的链接库,检查是否齐全。
    • 确保 target_link_libraries() 命令被正确执行,并且链接的是 PRIVATE PUBLIC 还是 INTERFACE 符合你的设计。
    • 使用 find_package() 时,检查它是否真的找到了包,并正确设置了相关的变量(如 OpenCV_FOUND , OpenCV_LIBS )。

第五步:运行时动态链接检查 如果编译链接成功,但运行时出现 undefined symbol 错误,使用 ldd 命令检查可执行文件的动态库依赖: ldd your_program 查看列出的所有 .so 文件后面是否都显示了有效的路径,而不是 not found 。对于 not found 的库,你需要确保它们所在的目录在 LD_LIBRARY_PATH 环境变量中,或者已通过 ldconfig 注册到系统缓存。

4. 针对高频热词的专项排查指南

结合网络热词,这里对一些特定场景进行深入分析。

4.1 undefined reference to WinMain'`

  • 问题定位 :这是一个经典的Windows平台入口点错误。链接器期望找到一个Windows GUI程序的入口函数 WinMain ,但你编写的是一个控制台程序,入口函数应该是 main
  • 解决方案
    1. 检查编译器/IDE设置 :如果你使用的是MinGW的gcc,确保编译命令中没有 -mwindows 选项。这个选项会指示链接器寻找 WinMain 。对于控制台程序,直接使用 gcc main.c -o main.exe 即可。
    2. 检查项目类型 :在Visual Studio、Code::Blocks等IDE中,创建项目时选择了错误的项目模板(如“Windows Desktop Application”而不是“Console Application”)。需要修改项目属性,将子系统(Subsystem)从“Windows”改为“Console”。
    3. 检查源码 :极少数情况下,可能是你的 main 函数签名写错了,比如写成了 int main(void) 但编译器环境有特殊要求,但这种情况较少见,首要还是检查构建配置。

4.2 STM32CubeIDE中的 undefined reference to HAL_RCC_OscConfig'`

  • 问题定位 :这是嵌入式HAL库链接错误。 HAL_RCC_OscConfig 是STM32硬件抽象层中配置系统时钟源的函数。
  • 解决方案
    1. 确认HAL库文件已添加 :在STM32CubeIDE的“Project Explorer”中,右键点击项目 -> “Properties” -> “C/C++ Build” -> “Settings” -> “Tool Settings”选项卡。
      • 在“MCU GCC Compiler” -> “Include paths”中,确认包含了 Drivers/STM32xx_HAL_Driver/Inc (xx代表你的系列,如F4、L4等)。
      • 在“MCU GCC Linker” -> “Libraries”中,通常不需要手动添加HAL库,因为源文件会直接编译。但更重要的是确保源文件被包含。
    2. 确认HAL源文件在项目中 :在“Project Explorer”中,展开 Drivers/STM32xx_HAL_Driver/Src 目录。确保其中包含了 stm32xx_hal_rcc.c 文件(xx为你的系列)。这个文件里定义了 HAL_RCC_OscConfig 。如果该文件不在项目中(图标是灰色的或带斜线),需要右键点击该文件 -> “Resource Configuration” -> “Exclude from Build…” -> 取消勾选,将其包含到构建中。
    3. 检查芯片型号和HAL库版本 :确保你安装的HAL库支持你选择的具体芯片型号。有时特定型号的某些函数在通用HAL驱动中可能以弱定义( __weak )形式存在,需要用户重写,但 HAL_RCC_OscConfig 通常是强定义的。
    4. 清理并重建项目 :在“Project”菜单中,选择“Clean…”,然后“Build All”。CubeIDE的索引和构建系统有时会不同步。

4.3 动态链接器路径问题 ( LD_LIBRARY_PATH , ldconfig )

  • 问题现象 :程序编译链接成功,但运行时报错: ./myapp: error while loading shared libraries: libxxx.so: cannot open shared object file: No such file or directory 或直接提示某个符号未定义。
  • 解决方案
    1. 临时设置(当前终端有效)
      export LD_LIBRARY_PATH=/path/to/your/lib:$LD_LIBRARY_PATH
      ./your_program
      
    2. 永久设置(对用户生效) :将上面的 export 命令添加到你的shell配置文件(如 ~/.bashrc ~/.zshrc )中,然后执行 source ~/.bashrc
    3. 系统级配置(对所有用户生效)
      • 创建一个新的 .conf 文件,例如 /etc/ld.so.conf.d/myapp.conf ,在里面写入你的库目录 /path/to/your/lib
      • 然后以root权限运行 sudo ldconfig ,更新动态链接器的缓存。
      • 这种方法更规范,适用于软件安装。
    4. 编译时指定运行时路径(RPATH) :在链接时通过 -Wl,-rpath,/path/to/your/lib 选项将库路径硬编码到可执行文件中。这样程序运行时会自动去该路径寻找库,但降低了可移植性。

4.4 GCC版本与升级问题

  • “gcc升级后为啥还是旧版本” :这通常是因为系统中有多个gcc版本,而你的shell环境中的 PATH 变量仍然指向旧版本的路径。使用 which gcc gcc --version 来检查当前使用的是哪个。你可能需要更新 PATH ,或者使用版本管理工具(如 update-alternatives )来切换默认版本。
  • “gcc能识别txt吗” :GCC本身是编译器,它根据文件后缀名来判断文件类型。 .txt 后缀默认不会被识别为任何可编译的源语言。如果你有一个C代码文件但错误地命名为 .txt ,GCC不会自动将其当作C文件编译。你需要要么重命名文件为 .c ,要么在编译时通过 -x c 选项显式指定语言: gcc -x c mycode.txt -o myprog
  • “gcc fatal error: input file ‘extension-output -felipel…’ is the same file” :这是一个比较奇怪的错误,通常与构建系统(如VSCode的扩展、某些脚本)或命令行参数传递错误有关。它提示输入和输出文件被指定为同一个,这是不允许的。检查你的编译命令,确保 -o 参数指定的输出文件名与输入源文件名不同,并且没有其他参数错误。

5. 构建系统与工具链的防错实践

要减少“undefined reference”错误,良好的项目结构和构建配置习惯至关重要。

1. 使用现代构建系统(强烈推荐CMake) 手动编写Makefile容易出错,尤其是处理复杂的依赖关系时。CMake能自动处理很多细节。

  • 清晰的目标依赖 :使用 add_executable() add_library() 定义目标,用 target_link_libraries() 声明依赖,CMake会自动推导包含路径和链接库。
  • 包管理 :利用 find_package() 查找系统库,它比手动写 -I -L 更可靠。
  • 示例CMake片段
    cmake_minimum_required(VERSION 3.10)
    project(MyProject)
    
    # 查找必需的库
    find_package(Threads REQUIRED)
    find_package(OpenCV REQUIRED)
    
    # 添加可执行文件目标
    add_executable(my_app main.cpp utils.cpp)
    
    # 明确链接库,PUBLIC表示依赖会传递给链接my_app的其他目标
    target_link_libraries(my_app PRIVATE
        ${CMAKE_THREAD_LIBS_INIT} # 链接线程库
        OpenCV::opencv_core       # 现代CMake推荐使用导入的目标
        OpenCV::opencv_highgui
    )
    
    # 添加包含目录
    target_include_directories(my_app PRIVATE
        ${PROJECT_SOURCE_DIR}/include
        ${OpenCV_INCLUDE_DIRS} # 传统变量方式,现代方式已融入上面的目标
    )
    

2. 规范的头文件管理

  • 头文件守卫(Include Guards) :防止头文件被重复包含,避免潜在的符号重定义警告。
    // myheader.h
    #ifndef MYHEADER_H
    #define MYHEADER_H
    // ... 声明内容 ...
    #endif // MYHEADER_H
    
  • 声明与定义分离 :头文件只放声明(函数原型、外部变量声明、类/结构体定义),实现一律放在对应的 .c/.cpp 文件中。这迫使你思考接口,并让链接错误更容易定位。

3. 理解并善用链接器选项

  • -Wl,--verbose :让链接器输出详细的搜索和处理过程,对于诊断复杂的库依赖问题非常有帮助。
  • -Wl,--no-undefined (或 -z defs ):让链接器在遇到任何未定义的符号时就报错,而不是等到运行时。这有助于在构建阶段就发现所有问题。
  • -Wl,--start-group -Wl,--end-group :这对选项可以解决静态库之间的循环依赖问题。它们告诉链接器,在这两个选项之间的库列表需要被反复扫描,直到所有未定义符号都被解析或确定无法解析。
    gcc main.o -Wl,--start-group -lfoo -lbar -lbaz -Wl,--end-group -o main
    

4. 为大型项目建立清晰的目录结构和依赖文档 对于多人协作或长期维护的项目,一个清晰的 README.md docs/ 目录,写明项目的第三方依赖、如何获取和安装它们、以及内部的模块依赖关系,能极大减少环境配置和构建问题。

回到我开头的那个嵌入式问题,最终的解决方案是:在STM32CubeIDE中,我发现工程虽然通过CubeMX生成了,但 Drivers/STM32F4xx_HAL_Driver/Src/stm32f4xx_hal_rcc.c 这个文件被意外地排除在构建之外了(可能是之前误操作)。将其重新包含进来,清理并重建,那个恼人的 undefined reference to HAL_RCC_OscConfig'`错误便消失了。这个过程再次印证了,解决这类问题的关键不在于记住所有答案,而在于掌握一套从错误信息出发,逐步缩小范围,最终定位到缺失的那块“拼图”的系统性方法。

用 AI 写代码,常见两种翻车: 过重——技能十几门、文档写两遍,二开被流程拖死; 过轻——一句话丢给 Cursor,边界不清、难验收、难回溯。 SW Harness(AI 软件开发工程框架 v1.2) 取中间态:保留「想清楚→设计→实现→验证→可选上云」闭环,体量按个人/小团队砍到能扛住。 不是提示词合集,是可装进 Cursor 的工程工作流。 主路径: /sw req → asd → sdd → dev → review → (env/deploy) → commit 需求写范围成功标准;架构做边界选型;模块方案才出接口、时序逻辑图;开发用例先行;审查一次过质量基础安全。小改动可走 req→sdd→dev→review。/sw status 看进度,/sw continue 断点续跑。 你会得到: 唯一 Workflow Skill(全套 /sw 门禁)· PRD/ASD/SDD/测试/审查模板 · 完整 DEMO 文档 · 可跑 Java 示例(mvn test)· 云配置轻量部署脚本 · 二开 context 位。 适合: 真实项目、旧系统二开、接单、个人产品。 不适合: 只要万能提示词、要代开发、要企业多 Agent 重型流水线。 怎么用: Skill 拷到 .cursor/skills/ → 对照 DEMO → 复制空白模板 → 对自己的小需求说 /sw req 开跑。有 VPS 再配云;没有就本地验收即可。 首发 ¥49(标 ¥69),一次买断,支付后自动下 ZIP。 写代码走 /sw;授权 APK 分析可另配 /re——同一套 harness 思路。 轻量可学可二开,不是企业合规流水线。数字商品售出不退,请按需购买。
内容概要:本文围绕“基于蜣螂优化算法的无线传感器网络覆盖优化研究”展开,提出了一种创新且可复现的智能优化方法。通过引入新型群智能优化算法——蜣螂优化算法(DBO),对无线传感器网络(WSN)中的节点部署问题进行建模求解,旨在最大化网络覆盖率、均衡节点能耗、延长网络生命周期并提升系统整体稳定性。研究基于Matlab平台完成了算法的仿真实现,构建了合理的适应度函数,设计了关键参数调整策略,并通过大量仿真实验验证了该算法在不同规模监测区域下的优化性能。相较于传统优化算法如粒子群优化(PSO)、遗传算法(GA)等,DBO在收敛速度、全局寻优能力、避免早熟收敛以及覆盖均匀性方面表现出更优异的性能,充分体现了其在复杂工程优化问题中的应用潜力。; 适合人群:具备一定Matlab编程基础和优化算法理论知识,从事智能计算、物联网、无线传感器网络、自动化控制等相关领域的高校研究生、科研人员及工程技术人员。; 使用场景及目标:①应用于无线传感器网络的节点布局优化,有效提升监控区域的感知覆盖质量;②作为新型群智能算法的学习研究案例,深化对蜣螂优化算法机理的理解,并拓展其在路径规划、资源分配、参数优化等其他工程领域的应用;③为学术论文撰写、科研项目申报、毕业课题设计及算法竞赛提供可靠的技术支持参考范例。; 阅读建议:建议读者结合提供的Matlab代码深入理解算法的具体实现流程,重点关注适应度函数的构造逻辑、算法参数的敏感性分析及优化迭代过程的可视化展示,并尝试在不同环境设定下复现实验结果,以全面掌握蜣螂优化算法的核心思想及其在WSN覆盖优化中的实际应用价值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值