You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows平台下如何用现代C++创建重定向stdin/stdout的子进程

Windows平台下C++风格的子进程双向IO重定向实现

当然有更贴合现代C风格的方案!你提到的旧版C语言HANDLE写法确实繁琐且容易出错,我们可以结合Win32 API的进程创建能力,再用C的RAII(资源获取即初始化)思想封装原生句柄,既实现同时重定向子进程的stdin和stdout,又能避免手动管理资源的麻烦。

核心思路

用CreateProcess创建子进程时,提前创建两个匿名管道:

  • 一个管道用于父进程写 → 子进程读(绑定子进程的stdin)
  • 另一个管道用于子进程写 → 父进程读(绑定子进程的stdout)
    然后用C智能指针封装HANDLE,自动处理句柄的关闭,完全符合现代C的资源管理习惯。

完整代码示例

第一步:RAII封装HANDLE

先写一个简单的智能指针包装,避免手动调用CloseHandle导致的资源泄漏:

#include <windows.h>
#include <memory>
#include <string>
#include <stdexcept>
#include <iostream>
#include <vector>

// 自定义删除器,自动关闭HANDLE
struct HandleDeleter {
    void operator()(HANDLE handle) const {
        if (handle != INVALID_HANDLE_VALUE) {
            CloseHandle(handle);
        }
    }
};

// 用unique_ptr实现RAII的句柄管理
using UniqueHandle = std::unique_ptr<void, HandleDeleter>;

第二步:创建双向管道并启动子进程

封装一个函数,负责管道创建、进程启动和资源清理:

// 启动子进程并建立双向IO管道
void StartDuplexChildProcess(const std::wstring& cmdLine, UniqueHandle& childStdinWrite, UniqueHandle& childStdoutRead) {
    SECURITY_ATTRIBUTES sa{};
    sa.nLength = sizeof(SECURITY_ATTRIBUTES);
    sa.bInheritHandle = TRUE; // 允许子进程继承管道句柄
    sa.lpSecurityDescriptor = nullptr;

    // 创建子进程stdin管道:父写子读
    HANDLE hChildStdinRead, hChildStdinWrite;
    if (!CreatePipe(&hChildStdinRead, &hChildStdinWrite, &sa, 0)) {
        throw std::runtime_error("Failed to create stdin pipe");
    }
    // 父进程保留的写句柄不需要被子进程继承,取消继承标记
    if (!SetHandleInformation(hChildStdinWrite, HANDLE_FLAG_INHERIT, 0)) {
        throw std::runtime_error("Failed to set stdin write handle inheritance");
    }

    // 创建子进程stdout管道:子写父读
    HANDLE hChildStdoutRead, hChildStdoutWrite;
    if (!CreatePipe(&hChildStdoutRead, &hChildStdoutWrite, &sa, 0)) {
        throw std::runtime_error("Failed to create stdout pipe");
    }
    // 父进程保留的读句柄取消继承标记
    if (!SetHandleInformation(hChildStdoutRead, HANDLE_FLAG_INHERIT, 0)) {
        throw std::runtime_error("Failed to set stdout read handle inheritance");
    }

    // 配置子进程的启动信息,绑定标准输入输出
    STARTUPINFOW si{};
    si.cb = sizeof(STARTUPINFOW);
    si.hStdInput = hChildStdinRead;
    si.hStdOutput = hChildStdoutWrite;
    si.hStdError = hChildStdoutWrite; // 可选:把子进程的stderr也重定向到stdout
    si.dwFlags |= STARTF_USESTDHANDLES;

    PROCESS_INFORMATION pi{};
    // 启动子进程,这里用cmd.exe作为示例,你可以替换成自己的可执行文件
    if (!CreateProcessW(
        nullptr,
        const_cast<wchar_t*>(cmdLine.c_str()),
        nullptr, nullptr,
        TRUE, // 允许子进程继承句柄
        0, nullptr, nullptr,
        &si, &pi)) {
        throw std::runtime_error("Failed to start child process");
    }

    // 关闭不需要的句柄:子进程端的管道句柄、子进程的线程和进程句柄
    CloseHandle(hChildStdinRead);
    CloseHandle(hChildStdoutWrite);
    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);

    // 将父进程需要的句柄交给调用者
    childStdinWrite = UniqueHandle(hChildStdinWrite);
    childStdoutRead = UniqueHandle(hChildStdoutRead);
}

第三步:使用示例

下面是一个简单的调用示例,向子进程发送命令并读取输出:

int main() {
    try {
        UniqueHandle childStdinWrite, childStdoutRead;
        // 启动cmd.exe作为子进程
        StartDuplexChildProcess(L"cmd.exe", childStdinWrite, childStdoutRead);

        // 向子进程写入命令
        const std::string command = "echo Hello from parent!\n";
        DWORD bytesWritten;
        if (!WriteFile(childStdinWrite.get(), command.c_str(), command.size(), &bytesWritten, nullptr)) {
            throw std::runtime_error("Failed to write to child stdin");
        }

        // 读取子进程的输出
        std::vector<char> buffer(4096);
        DWORD bytesRead;
        if (ReadFile(childStdoutRead.get(), buffer.data(), buffer.size(), &bytesRead, nullptr)) {
            std::cout << "Child process output:\n" << std::string(buffer.data(), bytesRead) << std::endl;
        } else {
            throw std::runtime_error("Failed to read from child stdout");
        }

        // 可以继续进行更多的读写操作...

    } catch (const std::exception& e) {
        std::cerr << "Error occurred: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

关键说明

  • RAII的优势:用UniqueHandle自动管理HANDLE,不需要手动调用CloseHandle,避免了资源泄漏和忘记关闭句柄的问题,完全符合C++的资源管理哲学。
  • 双向管道的必要性:不同于_popen只能单向通信,这里用两个独立的管道分别处理输入和输出,实现了完整的双向交互。
  • 句柄继承设置:必须取消父进程保留的句柄的继承标记,否则子进程会持有额外的管道句柄,导致管道无法正确关闭,进而引发读写阻塞。
  • 异步扩展:如果需要异步IO,可以结合Windows的Overlapped机制和C++的异步工具(比如std::async),实现非阻塞的读写操作。

内容的提问来源于stack exchange,提问作者KBOPYM

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 04:25:44