popen() 是如何工作的以及如何在 Linux 上将其实现为 C++ 代码?

2024-03-23

我不知道如何使用popen()从 Linux 中的子程序获取 stdout 到主 C++ 程序。我环顾四周,发现这段代码可以实现我想要的功能。但我无法理解这个东西是如何工作的。我知道 C++ 编程的基础知识(我已经这样做了几个月了),但我很困惑,所以有人可以帮我解释一下吗?

先感谢您。

#include <vector>
#include <string>
#include <stdio.h>
#include <iostream>

using namespace std;

void my_popen( const string& cmd ,vector<string>& out ){
    FILE*  fp;
    const int sizebuf=1234;
    char buff[sizebuf];
    out = vector<string> ();
    if ((fp = popen (cmd.c_str (), "r"))== NULL){

}
    string cur_string = "";
    while (fgets(buff, sizeof (buff), fp)) {
        cur_string += buff;
}
    out.push_back (cur_string.substr (0, cur_string.size() -1));
    pclose(fp);
}




int main () {

    vector<string> output;
    my_popen("which driftnet", output);
    for (vector<string>::iterator itr = output.begin();
        itr != output.end();
        ++itr) {
            cout << *itr << endl;
        }
    return 0;
}

如何使用 popen() 从 Linux 中的子程序中获取 stdout 到 主C++程序

您熟悉 UNIX 哲学吗?您可能会阅读维基百科文章“UNIX 哲学”的开头。这是一个片段:

Doug McIlroy[1] 在 Bell 中记录了 UNIX 哲学 1978 年的系统技术期刊:[2]

1)让每个程序做好一件事。做新工作,重新建设 而不是通过添加新的“功能”来使旧程序复杂化。

2)期望每个程序的输出成为另一个程序的输入,如下所示 尚不清楚,程序。不要让无关的输出变得混乱 信息。避免严格的柱状或二进制输入格式。不 坚持互动输入。

... 和更多 ...

我更喜欢使用 popen 来访问许多我认为有用的 Linux 工具。示例包括 sort、unique 和 sha256sum 以及其他几个。但您可能会注意到许多等效项正在移入 C++ 库(std::sort 等),并且经过努力,我发现了例如 sha256sum 的代码,适合在我的 C++ 应用程序中编译。


1) 启动时,popen 会被赋予一个字符串,该字符串可以是标识要运行的应用程序的一部分,也可以是多个部分,第一个是应用程序,附加字符串是传递给该应用程序的参数,就像命令行一样输入。就像在 shell 中一样(提示)

2) 当以“r”模式启动时,sha256sum 的 stdout 管道将定向到由调用 popen 打开的 FILE* 句柄。因此,输出被“反馈”到您的程序。


但我无法理解这个东西是如何工作的。

这不是一个真正有用的评论,也不是一个问题。你的代码片段对我来说很直接(尽管基本上是c代码)。也许您应该选择一行代码并询问一些具体的信息?


但是,在您对代码过于努力之前,让我执行一些初步调查,我们将手动运行我们希望与 popen 一起使用的代码,并捕获结果以根据您的应用程序的输出进行审查。

调查1 -

在您的代码片段中,您正在尝试命令“whichdriftnet”。当您的路径中不存在程序driftnet时,这是一个不幸的选择。

我的系统上的结果:

$ which driftnet
$

由于该命令不存在,因此命令“which”仅返回空结果。

很难说我们是否做错了什么。没有driftnet,我也帮不了你什么。


调查 2——一个更有趣的试验。

sha256sum 需要一个文件用于输入...这里我提供了我的“C++ Hello World”程序,您可以将其捕获到文件“HelloWorld.cc”(或任何您想要的位置),或使用您自己的任何文件。

// If your C++ 'Hello World' has no class ... why bother?
#include <iostream>

class Hello_t {
public:
   Hello_t()  { std::cout << "\n  Hello"  << std::flush; }
   ~Hello_t() { std::cout <<    "World!" << std::endl; }
   void operator() () { std::cout << " C++ "; }
};
int main(int, char**) { Hello_t()(); }

总共 10 行代码,以注释开头,单个包含后有 1 个空行。我将编辑器设置为自动神奇地删除尾随空格。如果你的留了一个空格,你的结果可能会不同。

那么,当您从命令行计算文件“HelloWorld.cc”的 sha256sum 时会发生什么?无需猜测...只需尝试即可!

在命令行上,调用该命令并在下一行查看其响应:

$ sha256sum HelloWorld.cc
f91c6c288325fb4f72546482a9b4f1fa560f05071a12aa2a1268474ea3eeea3e  HelloWorld.cc

也许你可以尝试your使用 sha256sum 和已知文件打开代码?

现在您可以看到(您的 popen 代码的)输出可能包含什么。对于本次调查,**命令 sha256sum 结果是 1 个长字符串,其中有一个空格。


仅供参考 - 几个月前我在对我的库进行大修时破坏了我的 C++ popen 代码。当我解决这个问题后,我计划重新审视你的努力,看看你的进展如何。

如果您的代码开始工作......我鼓励您提交自己的答案!

如果您可以确定更具体的问题,请完善该问题。

我建议您转向 C++ 代码...如果您要学习一些东西,并且您想学习 C++,那么研究您所发现的内容是没有用的。


我的代码又可以工作了。 (2017/8/2更新)

但是,首先让我分享 UNIX 哲学的一些结论(排名不分先后):


后果 1 - 管道和过滤器

过滤器是只做好一件事的程序。它们接受输入流,并生成输出流,通常是“删除”某些内容。

在代码度量领域,管理层可以检查行数以了解进度(行数增长放缓)和问题(没有放缓)。

每个人对代码指标都有自己的看法……并不是文件中的每一行都应该被计算在内。例如,我认为空格很重要,但是一行空格不应该算作代码。同样,一整行注释也不是代码。恕我直言。

在下面的命令中

 f1 < pfn | f2 | f3 | f4

f1..fn 是过滤器,全部作为一个命令提交到 shell。在我的代码度量工具中,我创建了

f1: ebl    erase blank lines 
f2: ecppc  erase cpp comments
f3: ecc    erase c comments
and several more.

Thus

ebl < HelloWorld.cc | ecppc | ecc | ebl | wc 

是提交给 shell 的单个字符串,最终计算出 3 个数字。

您的系统上可能存在类似 UNIX 的命令“wc”。在 Ubuntu 15.10 上:

$ wc < HelloWorld.cc
 10  52  309

表明HelloWorld.cc有10行(没有过滤)。


结果 2 - 命令列表

命令列表是 shell 命令的列表,每个命令都擅长做一件事。该列表标识了执行的时间顺序。比管道和过滤器更通用的想法。

例如,我使用指定的键 (F8) 设置编辑器 (emacs),以提供以下编译命令:

USER_FLAGS='-O0 ' ; export USER_FLAGS ; time make CC='g++-5 -m64 ' dumy514 ; ./dumy514

特定文件 pfn 可能输入一次,但保存在 emacs 内的历史缓冲区中,因此无需重新输入它们。

请注意 3 个分号。它们将提交给 Linux 命令 shell 的一个字符串中的多个命令分隔开。 3 元素命令列表:设置环境变量以覆盖默认选项;导出新标志以使其可供编译器使用;并启动 make。


所以? popen() 接受命令列表。像 shell 这样的“命令列表”可能会接受。


我终于足够仔细地阅读了“man popen”的回复。

从描述:

popen() 函数通过创建管道、分叉和​​创建管道来打开进程 调用shell.

您给 popen 的命令在 shell 中运行。

但我无法理解这个东西是如何工作的。

现在这个“东西”应该开始有意义了。我们正在访问 shell 来处理命令/命令列表/管道过滤器。这将启动“其他”进程,并使用 PIPE 与第一个进程交互。

那么,在 shell 中处理的命令的输出如何传递到我的代码呢?以下是一些有助于理解的线索。包括一些单元测试演示,您可以在其中轻松更改 popen cmd,然后编译并运行新的 cmd。


下面提供了在两种模式之一中使用 popen 的代码。是的,它们分为 3 类:一个基类和两个派生类。希望这可以让您一次运行多个这些。 (在我的 2 核机器上不是很有用。)随意重构代码,只保留您需要的内容。其余的将在这里为您提供持续指导。

#include <iostream>
#include <sstream>
#include <string>
#include <cstring>   // sterror
#include <cstdio>  // popen, FILE, fgets, fputs
#include <cassert>


class POpen_t    // access to ::popen
{
protected:
   FILE*        m_FILE;
   std::string  m_cmd;

public:
   POpen_t(void) : m_FILE(nullptr)
      { }

   virtual ~POpen_t(void) {
      if (m_FILE) (void)close();
      m_FILE = 0;
   }

   // on success: 0 == return.size(), else returns error msg
   std::string  close()
      {
         std::stringstream errSS;

         do // poor man's try block
         {
            // pclose() returns the term status of the shell cmd
            // otherwise -1 and sets errno.
            assert(nullptr != m_FILE);  // tbr - some sort of logic error
            // tbr if(0 == m_FILE)  break;  // success?

            int32_t pcloseStat = ::pclose(m_FILE);
            int myErrno = errno;
            if (0 != pcloseStat)
            {
               errSS << "\n  POpen_t::close() errno " << myErrno
                     << "  " << std::strerror(myErrno) << std::endl;
               break;
            }

            m_FILE     = 0;

         }while(0);

         return(errSS.str());
      } // std::string  close(void)

}; // class POpen_t

我认为复制/粘贴将它们分成 3 个部分会更容易。但请记住,我将所有代码都放在一个文件中。不是一个头文件,但它对我有用。如果您愿意,您可以重构为 .h 和 .cc 文件。

class POpenRead_t : public POpen_t    // access to ::popen read-mode
{

public:
   POpenRead_t(void) { }

   // ::popen(aCmd): opens a process (fork), invokes shell, 
   //                and creates a pipe
   // returns NULL if the fork or pipe calls fail,
   //              or if it cannot allocate memory.
   // on success: 0 == return.size(), else returns error msg
   std::string  open (std::string aCmd)
      {
         std::stringstream errSS; // 0 == errSS.str().size() is success
         assert (aCmd.size() > 0);
         assert (0 == m_FILE); // can only use serially
         m_cmd = aCmd; // capture

         do  // poor man's try block
         {
            if(true) // diagnosis only
               std::cout << "\n  POpenRead_t::open(cmd): cmd: '"
                         << m_cmd << "'\n" << std::endl;

            // ::popen(aCmd): opens a process by creating a pipe, forking,
            //                and invoking the shell.
            // returns NULL if the fork or pipe calls fail,
            //              or if it cannot allocate memory.
            m_FILE = ::popen (m_cmd.c_str(), "r"); // create 'c-stream' (FILE*)
            //       ^^ function is not in namespace std::

            int myErrno = errno;
            if(0 == m_FILE)
            {
               errSS << "\n  POpenRead_t::open(" << m_cmd
                     << ") popen() errno "   << myErrno
                     << "  " << std::strerror(myErrno) << std::endl;
               break;
            }
         } while(0);

         return (errSS.str());
      } // std::string  POpenRead_t::open(std::string aCmd)


        // success when 0 == errStr.size()
        // all outputs (of each command) captured into captureSS
   std::string  spinCaptureAll(std::stringstream& captureSS)
      {
         const int BUFF_SIZE = 2*1024;

         std::stringstream errSS; // capture or error
         do
         {
            if(0 == m_FILE)
            {
               errSS << "\n  ERR: POpenRead_t::spinCaptureAll(captureSS) - m_FILE closed";
               break;
            }
            size_t discardedBlankLineCount = 0;
            do
            {
               // allocate working buff in auto var, fill with nulls
               char buff[BUFF_SIZE] = { 0 };
               if(true) { for (int i=0; i<BUFF_SIZE; ++i) assert(0 == buff[i]); }

               // char * fgets ( char * str, int num, FILE * c-stream );
               // Reads characters from c-stream and stores them as a C string
               // into buff until
               //    a) (num-1) characters have been read
               //    b) a newline or
               //    c) the end-of-file is reached
               // whichever happens first.
               // A newline character makes fgets stop reading, but it is considered
               // a valid character by the function and included in the string copied
               // to str
               // A terminating null character is automatically appended after the
               // characters copied to str.
               // Notice that fgets is quite different from gets: not only fgets
               // accepts a c-stream argument, but also allows to specify the maximum
               // size of str and includes in the string any ending newline character.

               // fgets() returns buff or null when feof()
               char* stat = std::fgets(buff,      // char*
                                       BUFF_SIZE, // count - 1024
                                       m_FILE);   // c-stream
               assert((stat == buff) || (stat == 0));
               int myErrno = errno; // capture

               if( feof(m_FILE) ) { // c-stream eof detected
                  break;
               }

               // when stat is null (and ! feof(m_FILE) ),
               //   even a blank line contains "any ending newline char"
               // TBD:
               // if (0 == stat) {
               //   errSS << "0 == fgets(buff, BUFF_SIZE_1024, m_FILE) " << myErrno
               //         << "  " << std::strerror(myErrno) << std::endl;
               //   break;
               // }

               if(ferror(m_FILE)) { // file problem
                  errSS << "Err: fgets() with ferror: " << std::strerror(myErrno);
                  break;
               }

               if(strlen(buff))  captureSS << buff; // additional output
               else              discardedBlankLineCount += 1;

            }while(1);

            if(discardedBlankLineCount)
               captureSS << "\n" << "discarded blank lines: " << discardedBlankLineCount << std::endl;

         } while(0);

         return (errSS.str());

      } // std::string  POpenRead_t::spinCaptureAll(std::stringstream&  ss)

}; // class POpenRead_t

现在 POpenWrite_t:

class POpenWrite_t : public POpen_t    // access to ::popen
{
public:

   POpenWrite_t(void) { }


   // ::popen(aCmd): opens a process (fork), invokes the non-interactive shell,
   //                and creates a pipe
   // returns NULL if the fork or pipe calls fail,
   //              or if it cannot allocate memory.
   // on success: 0 == return.size(), else returns error msg
   std::string  open (std::string  aCmd)
      {
         std::stringstream errSS;  // 0 == errSS.str().size() is success
         assert (aCmd.size() > 0);
         assert (0 == m_FILE);
         m_cmd = aCmd;  // capture

         do // poor man's try block
         {
            if(true) // diagnosis only
               std::cout << "\n  POpenWrite_t::open(cmd): cmd: \n  '"
                         << "'" << m_cmd << std::endl;

            m_FILE = ::popen (m_cmd.c_str(), "w"); // use m_FILE to write to sub-task std::in

            int myErrno = errno;
            if(0 == m_FILE)
            {
               errSS << "\n  POpenWrite_t::open(" << m_cmd
                     << ") popen() errno "        << myErrno
                     << "  "  << std::strerror(myErrno)  << std::endl;
               break;
            }
         } while(0);

         return (errSS.str());
      } // std::string  POpenWrite_t::open(std::string  aCmd)

   // TBR - POpenWrite_t::write(const std::string& s) 
   //           work in progress - see demo write mode
}; // class POpenWrite_t

接下来,我有一个称为 T514_t 的类,它定义了我的单元测试或演示工作。条目位于“exec()”中。

事实证明,popen 中使用的 shell 可能与您在终端中遇到的 shell 不同。在会话启动期间,shell 的一部分决定它是处于交互(终端)模式还是非交互(无终端)模式。在我的系统上,终端/交互式 shell 是“bash”,popen/非交互式 shell 是“sh”。在为 popen 开发 shell 命令时请记住这一点。

POpenRead_t 的一个有趣用途是在方法“std::string shellCheck(std::stringstream& rsltSS)”中。

class T514_t
{
   const std::string line = "\n------------------------------------------------------";
   const char escape = 27;

public:

   T514_t() = default;
   ~T514_t() = default;

   std::string exec (int argc, char* argv[],
                     std::stringstream& resultSS )
      {
         std::string errStr;

         errStr = shellCheck(resultSS);  // when not bash, make a choice

         if (0 == errStr.size()) // shell is expected
         {
            // bash reported, continue
            switch (argc)
            {

            case 2 : { errStr = exec (argv, resultSS);       } break;

            default: { errStr = "  User error"; usage(argv); } break;

            } // switch (argc)
         }
         return (errStr); // when no err, errStr.size() is 0
      } // int exec()

private:

   std::string exec(char* argv[], std::stringstream& resultSS)
      {
         std::string       errStr;

         std::cout << clrscr() << std::flush;

         switch (std::atoi(argv[1]))
         {

         case 1:
         {
            std::cout << clrscr() << boldOff() << line;

            errStr = demoReadMode(resultSS);

            std::cout << boldOff() << std::endl;
         } break;

         case 2:
         {
            std::cout << clrscr() << boldOn() << line;

            errStr = demoWriteMode();

            std::cout << boldOff() << std::endl;
         } break;


         default: { usage(argv); } break;

         } // switch (std::atoi(argv[1]))

         return(errStr);
      } // int exec(char* argv[], std::stringstream& resultSS)


   // Ubuntu has 5 (or more) shells available
   // 1) Bourne shell (' sh'), 2) C shell (' csh'), 3) TC shell (' tcsh'),
   // 4) Korn shell (' ksh'), 5) Bourne Again shell (' bash')
   //
   // bash is  my interactive  shell
   // sh is my non-interactive shell
   // which (mildly) influences the cmds of the demo's 
   //
   // when not bash, what do you want to do?

   std::string shellCheck(std::stringstream& rsltSS)
      {
         std::stringstream errValSS;
         std::string cmd;
         cmd += "ps -p \"$$\" ";
         {
            POpenRead_t  popenR;

            errno = 0;
            std::string rStat = popenR.open(cmd);
            int myErrno = errno;

            if(0 != rStat.size()) {
               errValSS << "\n  Err: " << cmd << " failed.  rStat: "
                        << std::strerror(myErrno) << std::endl;
               return(errValSS.str());
            }

            do
            {
               errno = 0;
               std::string errSS = popenR.spinCaptureAll (rsltSS);
               myErrno = errno;

               if (false) { // dianosis only
                  std::cout << "\n  demoReadMode() ss/myErrno/s:\n"
                            << rsltSS.str() << "  "
                            << myErrno << "  '"
                            << errSS << "'" << std::endl;
               }
               break;

               if(0 != myErrno)
               {
                  errValSS << "\n  Err: popenR.spinCaputureAll():  cmd / strerror(myErrno): "
                           << cmd << " / " << std::strerror(myErrno) << std::endl;
                  return(errValSS.str());
               }

               if (0 == rsltSS.str().size()) {  // TBR
                  std::cout << "\n  demoReadMode: ss.str().size() is 0, indicating completed" << std::endl;
               } break;

            }while(1);

            // NOTE:  pclose() returns the termination status of the shell command
            // otherwise -1 and sets errno.
            // errno = 0;
            (void)popenR.close(); // return value is term status of the shell command
            if(errno != 0)
            {
               errValSS << "\n  Err: POpen_t::close() - cmd: " << cmd
                        << "   err:" << std::strerror(errno) << std::endl;
            }
         }

         std::string s = rsltSS.str();
         std::string nishell (" sh");     // non-interactive shell expected is: " sh"
         size_t indx = s.find(nishell);   // my interactive shell is bash

         // clear
         rsltSS.str(std::string()); rsltSS.clear(); // too much info

         if (std::string::npos != indx)
         {
            // normally I would not include a 'success' message (not the 'UNIX pholosopy'),
            // but here I have added to the (success) results:
            if(true)  //<-- feel free to comment out or delete this success action
               rsltSS << "\n  the reported non-interactive shell is the required '"
                      << nishell << "' ... continuing.\n" << std::endl;
         }
         else
         {
            // TBR - when non-interactive shell is unexpectedly different that nishell,
            //    this demo code aborts ... (with no results) and the error msg:
            errValSS << "\n  the reported non-interactive shell is not " << nishell
                     << " \n" << s << "\n  ... aborting\n";

            // alternative actions are _always_ possible
            // untested examples:
            //   the shell can be temporarily changed as part of cmd  (untested)
            //   the user could change the non-interactive shell
            //   your use of popen (POpen_t classes above)
            //        can change 'cmd's based on the discovered shell
         }

         return(errValSS.str());
      } // std::string shellCheck(std::stringstream& rsltSS)


   std::string demoReadMode(std::stringstream& rsltSS)
      {
         std::stringstream errValSS;

         std::string cmd;
         int i = 1;
         cmd += "./HelloWorld ; ";
         cmd += "echo  " + std::to_string(i++)  + " ; ";
         cmd += "./HelloWorld ; ";
         cmd += "sha256sum HelloWorld.cc ; ";
         cmd += "echo  " + std::to_string(i++)  + " ; ";
         cmd += "./HelloWorld ; ";
         cmd += "echo TEST WORKS ; ";
         cmd += "./HelloWorld";

         {
            POpenRead_t  popenR;

            errno = 0;
            std::string rStat = popenR.open(cmd);
            int myErrno = errno;

            if(0 != rStat.size()) {
               errValSS << "\n  Err: " << cmd << " failed.  rStat: "
                        << std::strerror(myErrno) << std::endl;
               return(errValSS.str());
            }

            do
            {
               errno = 0;
               std::string errSS = popenR.spinCaptureAll (rsltSS);
               myErrno = errno;

               if (false) { // dianosis only
                  std::cout << "\n  demoReadMode() ss/myErrno/s:\n"
                            << rsltSS.str() << "  "
                            << myErrno << "  '"
                            << errSS << "'" << std::endl;
               }
               break;

               if(0 != myErrno)
               {
                  errValSS << "\n  Err: popenR.spinCaputureAll():  cmd / strerror(myErrno): "
                           << cmd << " / " << std::strerror(myErrno) << std::endl;
                  return(errValSS.str());
               }

               if (0 == rsltSS.str().size()) {  // TBR
                  std::cout << "\n  demoReadMode: ss.str().size() is 0, indicating completed" << std::endl;
               } break;

            }while(1);

            // NOTE:  pclose() returns the termination status of the shell command
            // otherwise -1 and sets errno.
            // errno = 0;
            (void)popenR.close(); // return value is term status of the shell command
            if(errno != 0)
            {
               errValSS << "\n  Err: POpen_t::close() - cmd: " << cmd
                        << "   err:" << std::strerror(errno) << std::endl;
            }
         }
         return(errValSS.str());
      } //    std::string demoReadMode(std::stringstream& rsltSS)


   std::string demoWriteMode()
      {
         std::stringstream errValSS;

         std::string cmd;
         int i = 1;
         cmd += "./HelloWorld ; ";
         cmd += "echo  " + std::to_string(i++)  + " ; ";
         cmd += "./HelloWorld ; ";
         cmd += "sha256sum HelloWorld.cc ; ";
         cmd += "echo  " + std::to_string(i++)  + " ; ";
         cmd += "./HelloWorld ; ";

         {
            POpenWrite_t  popenW;  // popen in write mode

            // errno = 0;
            std::string wStat = popenW.open(cmd);
            int myErrno = errno;

            if (0 != wStat.size()) {
               errValSS << "\n  Err: " << cmd << "\n failed.  wStat: "
                        << std::strerror(myErrno) << std::endl;
               return(errValSS.str());
            }

            // tbd - Work in Progress - what command to receive what data from here
            //       login - needs root, tbr - can cmd contain sudo?  probably
            //
            //

            // NOTE:  pclose() returns the termination status of the shell command
            // otherwise -1 and sets errno.
            // errno = 0;
            (void)popenW.close(); // return value is term status of the shell command
            if(errno != 0)
            {
               errValSS << "\n  Err: POpen_t::close() - cmd: " << cmd
                        << "   err:" << std::strerror(errno) << std::endl;
            }
         }

         return(errValSS.str());
      } // std::string demoWriteMode()

   void usage(char* argv[])
      {
         std::cout << "  executable: "<< argv[0]
                   << "\n  USAGE - user must select test mode"
                   << "\n  test"
                   << "\n   1  - demoReadMode"
                   << "\n   2  - demoWriteMode"
                   << std::endl;
      }


   std::string boldOff(){std::string rV; rV.push_back(escape); rV += "[21m"; return(rV); } // bold off
   std::string boldOn() {std::string rV; rV.push_back(escape); rV += "[1m";  return(rV); } // bold on

   inline std::string clrscr(void) {
      std::stringstream ss;
      ss << static_cast<char>(escape) << "[H"   // home
         << static_cast<char>(escape) << "[2J"; // clrbos
      return(ss.str());
   }

}; // class T514_t

最后,我的主要......我在这里只关心做几件事。

int main(int argc, char* argv[])
{
   std::string errStr;
   std::stringstream resultSS;
   {
      T514_t   t514;
      errStr = t514.exec(argc, argv, resultSS);
   }

   // display result
   if (resultSS.str().size())
      std::cout << "\n\n  "<< resultSS.str() << std::endl;

   if(errStr.size())
      std::cerr << errStr << std::endl;

   return(static_cast<int>(errStr.size()));
}

因此,构建并使用 std::string cmd;在您感兴趣的模式中。祝您好运。

欢迎提问。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

popen() 是如何工作的以及如何在 Linux 上将其实现为 C++ 代码? 的相关文章

随机推荐