
本文从一个具体问题出发同一个项目中存在两套名称相同、实现不同的main()、use()和and()如何根据输入条件选择其中一套执行文章将依次说明普通函数调用、编译链接、execl()进程替换、CMake 多目标构建以及 PowerShell 调用运算符的原理。一、问题背景假设项目中有两套 C 程序。第一套位于根目录main.c - use.c - and.c第二套位于to目录to/main.c - to/use.c - to/and.c两套程序中的函数名完全相同但函数内部可以执行不同逻辑。项目还需要一个入口程序in.c读取整数c当c 3时执行根目录程序否则执行to目录中的程序。最终目录如下move/ ├── CMakeLists.txt ├── build.sh ├── in.c ├── main.c ├── use.c ├── and.c └── to/ ├── main.c ├── use.c └── and.c这个项目可以称为多可执行程序 C 项目CMake 多目标项目进程启动器或进程调度器示例。理解它的关键是先区分函数、可执行程序和进程。二、函数、可执行程序与进程并不是一回事1. 函数函数是程序内部的一段代码。调用use()时CPU 跳转到use()对应的指令执行结束后再返回调用位置。根目录程序的调用链为main() - use() - and()这三个函数始终运行在同一个进程内。函数调用不会启动一个新程序也不会创建新的独立地址空间。2. 可执行程序C 源文件不能直接由操作系统执行。它们需要经过预处理、编译、汇编和链接最后形成可执行文件.c 源文件 - 预处理 - 编译 - 汇编 - .o 目标文件 - 链接 - .exe 可执行文件根目录的三个源文件可以生成一个程序gcc main.c use.c and.c -o main.exeto目录中的三个源文件生成另一个程序gcc to/main.c to/use.c to/and.c -o to/main.exe3. 进程进程是可执行程序的一次运行实例。磁盘上的main.exe是文件运行后的main.exe才是进程。普通函数调用发生在一个进程内部而execl()操作的是整个进程它使用另一个可执行程序替换当前进程正在运行的程序。三、为什么两套同名函数不会冲突根目录和to目录中都定义了main()、use()和and()。这些名称能够共存是因为两套源码分别链接成两个可执行程序。main.c use.c and.c | ---- main.exe to/main.c to/use.c to/and.c | ---- to/main.exe每个可执行程序都有自己的符号表。根目录main.exe中的use()与to/main.exe中的use()不属于同一个链接单元因此不会冲突。如果把六个源文件一次性链接gcc main.c use.c and.c to/main.c to/use.c to/and.c -o wrong.exe链接器会发现多个main、use和and定义并报告重复符号错误。这里需要的不是重命名函数而是建立两个独立的构建目标。四、使用execl()选择目标程序1.execl()的参数结构execl()的基本形式为execl(program_path, argv0, arg1, arg2, ..., NULL);参数含义如下参数作用program_path要执行的可执行文件路径argv0目标程序收到的argv[0]通常填写程序名arg1、arg2传给目标程序的命令行参数NULL表示可变参数列表结束不能省略execl()执行的是编译后的程序而不是.c源文件。因此不能写成execl(./main.c, main.c, (char *)NULL); /* 错误思路 */必须先把源码构建为main.exe再执行它。2. 启动程序的实现本项目的in.c如下#include stdio.h #include process.h int main(void) { int c; printf(input: ); fflush(stdout); if (scanf(%d, c) ! 1) { fputs(invalid input\n, stderr); return 1; } if (c 3) execl(./main.exe, main.exe, root-argument, (char *)NULL); else execl(./to/main.exe, main.exe, to-argument-1, to-argument-2, (char *)NULL); perror(execl); return 1; }程序首先读取c。输入无法转换为整数时scanf()返回值不是1程序输出错误并结束。当c 3时执行execl(./main.exe, main.exe, root-argument, (char *)NULL);根目录程序收到argc 2 argv[0] main.exe argv[1] root-argument当c 3时执行execl(./to/main.exe, main.exe, to-argument-1, to-argument-2, (char *)NULL);to/main.exe收到argc 3 argv[0] main.exe argv[1] to-argument-1 argv[2] to-argument-23.execl()成功后为什么不返回execl()不是普通函数调用。调用成功后当前进程的程序代码、数据和调用栈会被目标程序替换。in.exe 读取 c | -- c 3 ---- main.exe | -- c 3 ---- to/main.exe因此调用成功时不会继续执行in.c中execl()后面的代码。只有调用失败时程序才会执行perror(execl); return 1;perror()会根据系统保存的错误码输出原因例如目标文件不存在或路径错误。五、两个main()如何接收不同参数两个程序都使用标准入口形式int main(int argc, char *argv[])这里不需要发明两种不同的main()函数签名。真正不同的是execl()传入的参数数量和内容。根目录main.c接收一个业务参数#include stdio.h void use(void); int main(int argc, char *argv[]) { if (argc ! 2) return 1; puts(move/main.c); printf(argument: %s\n, argv[1]); use(); return 0; }to/main.c接收两个业务参数#include stdio.h void use(void); int main(int argc, char *argv[]) { if (argc ! 3) return 1; puts(move/to/main.c); printf(arguments: %s, %s\n, argv[1], argv[2]); use(); return 0; }argc包含argv[0]所以一个业务参数对应argc 2两个业务参数对应argc 3。六、用 CMake 管理三个可执行程序手动执行三条 GCC 命令可以完成构建但随着文件数量增加命令会变得难以维护。CMake 的作用是描述“哪些源文件属于哪个目标”再生成底层构建工具需要的文件。本项目的CMakeLists.txt如下cmake_minimum_required(VERSION 3.15) project(move_example C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) add_executable(move_main main.c use.c and.c) set_target_properties(move_main PROPERTIES OUTPUT_NAME main RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} ) add_executable(to_main to/main.c to/use.c to/and.c) set_target_properties(to_main PROPERTIES OUTPUT_NAME main RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/to ) add_executable(in in.c) set_target_properties(in PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} )1. CMake 目标名与输出文件名下面两条语句定义了两个不同的 CMake 目标add_executable(move_main main.c use.c and.c) add_executable(to_main to/main.c to/use.c to/and.c)move_main和to_main是 CMake 内部使用的逻辑目标名必须不同。通过OUTPUT_NAME main它们最终都可以输出为main.exe。两个文件不会相互覆盖因为输出目录不同move_main - build/main.exe to_main - build/to/main.exe in - build/in.exe这个输出结构与in.c中的路径严格对应./main.exe ./to/main.exe2. CMake 的两个阶段CMake 通常分为“配置与生成”和“构建”两个阶段。配置与生成cmake -S . -B build -G MinGW Makefiles-S .源码目录是当前目录-B build生成文件和构建产物放在build-G MinGW Makefiles使用 MinGW Makefiles 生成器。执行构建cmake --build buildCMake 本身通常不直接编译 C 源码。它生成 Makefile再由mingw32-make根据 Makefile 调用 GCC 和链接器。七、用build.sh自动完成构建当前build.sh将配置、构建和运行组合起来#!/usr/bin/env sh set -eu root$(CDPATH cd -- $(dirname -- $0) pwd) cmake -S $root -B $root/build -G MinGW Makefiles cmake --build $root/build cd $root/build ./in.exeset -e表示某条命令失败后立即停止避免构建失败后仍然尝试运行旧程序。set -u表示读取未定义变量时立即报错。下面这行取得脚本所在目录的绝对路径root$(CDPATH cd -- $(dirname -- $0) pwd)脚本最后切换到build目录再运行in.exe。这一步非常重要因为execl()使用的是相对路径。程序查找./main.exe时依据的是当前工作目录而不是in.exe文件自身所在目录。八、为什么不能使用gcc build.sh以下命令是错误的gcc build.shGCC 用于处理 C 源文件、汇编文件、目标文件和库。build.sh是 Shell 脚本需要由 Shell 解释器逐行执行。把脚本交给 GCC 后链接器会尝试把文本内容当作目标文件或链接脚本因此出现file format not recognized syntax error正确思路是bash 解释 build.sh - build.sh 调用 CMake - CMake 调用构建工具 - 构建工具调用 GCC九、PowerShell 为什么需要在 PowerShell 中带引号的路径只是一个字符串C:\Program Files\Git\bin\bash.exe引号解决了路径中包含空格的问题但字符串不会自动变成命令。PowerShell 的调用运算符用于把后面的字符串或变量内容作为命令执行 C:\Program Files\Git\bin\bash.exe ./build.shPowerShell 会把它解析为可执行程序C:\Program Files\Git\bin\bash.exe 参数 ./build.sh如果不使用引号C:\Program Files\Git\bin\bash.exe ./build.shPowerShell 会在空格处截断路径并尝试执行C:\Program所以出现“无法将C:\Program识别为命令”的错误。如果只使用引号而不使用PowerShell 得到的仍然只是一个字符串。两者的职责不同引号保证含空格的路径被视为一个整体明确要求 PowerShell 执行这个路径指向的程序。调用运算符也可以执行变量保存的程序路径$bash C:\Program Files\Git\bin\bash.exe $bash ./build.sh需要注意PowerShell 开头的表示“调用”。Linux Shell 中放在命令末尾的表示“后台运行”。二者只是符号相同语义并不相同。十、Git Bash、WSL 和 PowerShell 的环境差异Windows 上可能同时存在多个名字相近的 Bash但它们不是同一个环境。1. Git BashGit Bash 是 Git for Windows 附带的类 Unix 命令环境。应使用公开入口 C:\Program Files\Git\bin\bash.exe ./build.sh不建议直接调用 C:\Program Files\Git\usr\bin\bash.exe ./build.shusr/bin/bash.exe是内部组件。绕过正常入口可能导致环境没有正确初始化随后出现dirname: command not found2. WSL下面的路径通常会启动 Windows Subsystem for LinuxWSLC:\Users\30126\AppData\Local\Microsoft\WindowsApps\bash.exeWSL 中运行的是 Linux 版 CMake。Linux 版 CMake 不提供 Windows 专用的MinGW Makefiles生成器因此会报告CMake Error: Could not create named generator MinGW Makefiles当前in.c还使用了 MinGW/Windows 的process.h和.exe文件名所以当前版本应在 Windows PowerShell 或 Git Bash 环境构建不应直接放到 WSL 原生构建。3. PowerShellPowerShell 可以直接执行 Windows 版 CMake无须经过 Bashcmake -S . -B build -G MinGW Makefiles cmake --build build Set-Location build .\in.exebuild.sh的价值是把这些步骤自动化它不是完成 CMake 构建的唯一入口。十一、准备构建环境在 PowerShell 中检查工具Get-Command cmake Get-Command gcc Get-Command mingw32-make Test-Path C:\Program Files\Git\bin\bash.exe当前构建方式需要Windows 版 CMakeMinGW GCCmingw32-makeGit Bash仅在执行build.sh时需要。如果系统没有 CMake可以使用 Windows Package Manager 安装winget install --id Kitware.CMake -e安装完成后重新打开 PowerShell使新的PATH生效再检查cmake --version使用 Git Bash 自动构建 C:\Program Files\Git\bin\bash.exe ./build.sh或者直接在 PowerShell 中构建cmake -S . -B build -G MinGW Makefiles cmake --build build Set-Location build .\in.exe十二、运行结果输入大于3的值例如5input: 5 move/main.c argument: root-argument move/use.c: use() move/and.c: and()输入不大于3的值例如2input: 2 move/to/main.c arguments: to-argument-1, to-argument-2 move/to/use.c: use() move/to/and.c: and()这证明in.exe根据条件选择了不同程序并向两个main()传入了不同数量的参数。十三、常见错误及定位方法错误现象根本原因解决方法file format not recognized使用 GCC 处理了 Shell 脚本使用 Bash 执行build.sh无法识别C:\Program路径包含空格但未正确调用使用 完整路径 参数dirname: command not found直接启动 Git 内部 Bash环境未初始化使用Git\bin\bash.execmake: command not foundCMake 未安装或不在PATH中安装 CMake 并重启终端不支持MinGW Makefiles在 WSL/Linux 中使用了 Windows 生成器改用 Windows CMakeexecl: No such file or directory工作目录不正确或目标程序未构建从build目录运行并检查输出文件重复定义main、use或and两套源码被链接到同一个目标为两套源码定义不同 CMake 目标十四、进一步理解几个容易忽略的细节1. 为什么调用fflush(stdout)下面的提示没有换行符printf(input: );输出内容可能暂存在缓冲区中。调用fflush(stdout);可以确保提示在程序等待输入之前显示。程序即将执行exec类操作时主动刷新需要保留的输出也是稳妥做法。2. 为什么声明和定义必须一致如果use.c中声明void and(void);那么and.c中也应定义为void and(void)一处声明为void、另一处定义为int会造成类型不一致。小型示例可以直接声明正式项目通常把共享声明放入.h头文件避免多个源文件各自维护声明。3.execl()与execlp()的区别execl()使用给定路径execl(./main.exe, main.exe, (char *)NULL);execlp()可以根据PATH环境变量搜索程序。当前项目需要明确区分根目录和to目录中的两个main.exe因此指定路径更合适。4. Windows 与 POSIX 的可移植性在 POSIX 系统中execl()通常由unistd.h声明当前 MinGW/Windows 示例使用process.h。文件扩展名、头文件和构建生成器都与平台有关。如果要让同一份源码同时支持 Windows 和 Linux需要通过条件编译处理这些差异而不能只修改 Shell 命令。十五、什么时候应该采用这种结构这种结构适合根据配置或输入启动不同工具启动器完成参数验证后切换到实际程序一个仓库需要构建客户端、服务端和管理程序不同功能必须拥有独立的进程和地址空间需要分别部署、测试或替换不同程序。如果两个分支只是执行少量不同计算直接调用函数通常更合理if (c 3) root_use(); else to_use();进程替换增加了构建目标、路径、参数传递和错误处理成本。只有确实需要独立程序边界时才应使用execl()。十六、结语这个示例连接了 C 项目中几个经常被分开学习的概念。main() - use() - and()展示的是同一进程内的函数调用两套同名函数通过独立链接目标形成两个可执行程序execl()根据输入条件使用其中一个程序替换启动器CMake 负责描述三个目标及其输出位置build.sh将构建步骤自动化PowerShell 的则负责调用路径中包含空格的 Bash 程序。完整调用链可以概括为PowerShell 的 - 启动 Git Bash - Git Bash 解释 build.sh - build.sh 调用 CMake - CMake 调用 MinGW 构建工具 - 构建工具调用 GCC - 生成 in.exe 和两个 main.exe - in.exe 使用 execl() 选择目标程序 - 目标 main() 调用 use() - use() 调用 and()理解这条链路后命令解释、构建生成、编译链接、进程替换和函数调用之间的边界就会清晰每一层只负责自己的任务上一层通过明确的接口驱动下一层。附录完整项目源码以下代码与本文对应可以按照开头的目录结构分别保存。1.in.c#include stdio.h #include process.h int main(void) { int c; printf(input: ); fflush(stdout); if (scanf(%d, c) ! 1) { fputs(invalid input\n, stderr); return 1; } if (c 3) execl(./main.exe, main.exe, root-argument, (char *)NULL); else execl(./to/main.exe, main.exe, to-argument-1, to-argument-2, (char *)NULL); perror(execl); return 1; }2. 根目录main.c#include stdio.h void use(void); int main(int argc, char *argv[]) { if (argc ! 2) return 1; puts(move/main.c); printf(argument: %s\n, argv[1]); use(); return 0; }3. 根目录use.c#include stdio.h void and(void); void use(void) { puts(move/use.c: use()); and(); }4. 根目录and.c#include stdio.h void and(void) { puts(move/and.c: and()); }5.to/main.c#include stdio.h void use(void); int main(int argc, char *argv[]) { if (argc ! 3) return 1; puts(move/to/main.c); printf(arguments: %s, %s\n, argv[1], argv[2]); use(); return 0; }6.to/use.c#include stdio.h void and(void); void use(void) { puts(move/to/use.c: use()); and(); }7.to/and.c#include stdio.h void and(void) { puts(move/to/and.c: and()); }8.CMakeLists.txtcmake_minimum_required(VERSION 3.15) project(move_example C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) add_executable(move_main main.c use.c and.c) set_target_properties(move_main PROPERTIES OUTPUT_NAME main RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} ) add_executable(to_main to/main.c to/use.c to/and.c) set_target_properties(to_main PROPERTIES OUTPUT_NAME main RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/to ) add_executable(in in.c) set_target_properties(in PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} )9.build.sh#!/usr/bin/env sh set -eu root$(CDPATH cd -- $(dirname -- $0) pwd) cmake -S $root -B $root/build -G MinGW Makefiles cmake --build $root/build cd $root/build ./in.exe