《VC++深入详解》学习笔记 第十七章 进程间通信

1.当一个进程启动后,操作系统为其分配4GB的私有地址空间,位于同一进程中的多个线程共享同一个地址空间,因此线程间的通信非常简单,但因为进程地址空间都是私有的,所以进程间通信比较困难

1)共享内存(剪贴板)

2)匿名管道

3)命名管道

4)邮槽

5)消息

2.剪贴板

1)剪贴板实际上是系统维护管理的一块内存区域,当在一个进程中复制数据时,是将这个数据放到该块内存区域中,当在另一个进程中粘贴数据时,是从该块内存区域中取出数据,然后显示在窗口上

2)数据发送

     1.打开剪贴板:CWnd类的OpenClipboard成员函数

       函数声明:BOOL OpenClipboard();//如果某个程序打开了剪贴板,则其他应用程序将不能修改剪贴板,直到前者关闭剪贴板(CloseClipboard)并且只有调用了EmptyClipboard()函数后,打开剪贴板的当前窗口才拥有剪贴板

     2.向剪贴板上放置内容:SetClipboardData函数(这个函数是以指定的剪贴板格式向剪贴板上放置数据)

      函数原型:HANDLE SetClipboardData(UINT uFormatHANDLE hMem);

// uFormat指定剪贴板格式,这个格式可以是已注册的格式,或者是任一种标准的剪贴板格式(eg:CF_TEXT

// hMem:具有指定格式的数据的句柄,该参数若为NULL,指示调用窗口直到有对剪贴板数据的请求时,才提供指定剪贴板格式的数据,如果窗口采用延迟提交技术,则该窗口必须处理WM_RENDERFORMATWM_RENDERALLFORMATS消息

         3.如果hMenu参数标识了一个内存对象,那么这个对象必须是利用GMEM_MOVEABLE标识调用GlobalAlloc函数为其分配内存

          HGLOBAL GlobalAlloc(UINT uFlags,SIZE_T dwBytes);

// dwBytes指定分配的字节数

// uFlags用来指定分配内存的方式

GMEM_MOVEABLE:分配一块可移动的内存(返回值是一块内存对象句柄,若要转换为一个指针需要调用GlobalLock函数将内存锁定,每一次调用GlobalLock函数后,最后一定要调用GlobalUnlock函数(GlobalRealloc函数将重新分配)) LPVOID GlobalLockHGLOBAL hMem);

GMEC_FIXED:分配一块固定的内存(返回值是一个指针)

         4.示例:

        void CClipboardDlg::OnBtnSend()

   {

        if(OpenClipboard())//打开剪贴板

        {

        CString str;

        HANDLE hClip;

        char* pBuf;

        EmptyClipboard();

        GetDlgItemText(IDC_EDIT_SEND,str);

        hClip = GlobalAlloc(GMEM_MOVEABLE,str.GetLength() + 1);

        pBuf = (char*)GlobalLock(hClip);

        strcpy(pBuf,str);

        GlobalUnlock(hClip);

        SetClipboardData(CF_TEXT,hClip);

        CloseClipboard();//关闭剪贴板(一定要记得关闭,否则其他程序无法使用剪贴板)

        }

    }

3)数据接收

     1.示例:

       void CClipboardDlg::OnBtnRecv()

   {

        if(OpenClipboard())//打开剪贴板

        {

           if(IsClipboardFormatAvailable(CF_TEXT))

           {

               HANDLE hClip;

               char* pBuf;

               hClip = GetClipboardData(CF_TEXT);

               pBuf = (char*)GlobalLock(hClip);

               GlobalUnlock(hClip);

               SetDlgItemText(IDC_EDIT_RECV,pBuf);

           }

           CloseClipboard();//关闭剪贴板

        }

    }

    2.在获取数据之前,应该查看一下剪贴板中是否有我们想要的特定格式的数据,这可以通过调用IsClipboardFormatAvailable函数实现

      函数原型:BOOL IsClipboardFormatAvailableUINT format;

    3.从剪贴板上获得数据:GetClipboardData函数

      函数原型:HANDLE GetClipboardData(UINT uFormat);

       (要获取数据还要把句柄转化为指针)

3.匿名管道

1)匿名管道是一个未命名的、单向的管道,通常用来在一个父进程和一个子进程之间传输数据,匿名管道只能实现本地机器上两个进程间的通信

2)创建匿名管道:CreatePipe函数

     函数原型:BOOL CreatePipe

          PHANDLE  hReadPipe,//out型,作为返回值使用,返回管道的读取句柄

          PHANDLE  hWritePipe,//out型,作为返回值使用,返回管道的写入句柄

    LPSECURITY_ATTRIBUTES  lpPipeAttributes,//安全属性,在此处不能使用默认的安全描述符(NULL),因为子进程要或的匿名管道的句柄只能从父进程继承而来,所以必须构造一个SECURITY_ATTRIBUTES结构体变量

    

         DWORD  nSize//指定管道的缓冲区大小,若为0系统提供默认值

);

typedef struct _SECURITY_ATTRIBUTES{

DWORD  nLength;//该结构体大小

              LPVOID  lpSecurityDescriptor;//指向安全描述符的指针(NULL

              BOOL  bInheritHandle;//该成员指定所返回的句柄能否被一个新的进程所继承,TRUE:能被继承

             }SECURITY_ATTRIBUTES,*PSECURITY_ATTRIBUTES;

3)进程的创建:CreateProcess函数

     函数原型:BOOL CreateProcess

             LPCTSTR lpApplicationName,//指向字符串,用来指定可执行程序的名称,该名称可以是该程序的完整路径和文件名,也可以是部分名称(当前路径搜索)可以为NULL注意:一定要加上扩展名,系统不会自动加.exe

             LPTSTR lpCommandLine,//指向字符串,用来指定传递给新进程的命令行字符串,(可以将文件名和命令行参数构造成一个字符串,一并传给这个参数)可以为空

        LPSECURITY_ATTRIBUTES lpProcessAttributes,//设置新进程的进程对象安全性

        LPSECURITY_ATTRIBUTES lpThreadAttributes,//设置新进程的线程对象安全性

                  BOOL bInheritHandles,//用来指定该进程创建的子进程能够继承父进程的对象句柄,TRUE:父进程的每个可继承的打开句柄都能被子进程继承

                 DWORD dwCreationFlags,//指定控件优先级类和进程创建的附加标记(0)(标识可以利用位运算组合)

                 LPVOID lpEnvironment,//一个指向环境块的指针,若为NULL,则新进程使用调用进程的环境,通常设为NULL

                LPCTSTR lpCurrentDirectory,//指向字符串,用来规定子进程当前的路径,必须是一个完整的路径名,包括驱动器的标识符,若为NULL,则和父进程相同

             LPSTARTUPINFO lpStartupInfo,//指向STARTUPINFO结构体的指针,用来指定新进程的主窗口如何显示,该结构体中的一些数据成员需要赋值

     LPPROCESS_INFORMATION lpProcessInformation//这个参数作为返回值使用,用来接收有关于新进程的标识信息,PROCESS_INFORMATION结构体有四个成员:新建进程句柄;新建进程的主线程句柄;全局进程标识符;全局线程标识符

);

4)父进程的实现

      1.增加成员变量 HANDLE  hWrite;  HANDLE  hRead;

      2.构造函数中初始化 hRead = NULL;  hWrite = NULL;

      3.析构函数中关闭这两个变量

        If(hRead) CloseHandle(hRead);

        If(hWrite) CloseHandle(hWrite);

      4.创建匿名管道

        void CParentView::On1()

{

          // TODO: Add your command handler code here

SECURITY_ATTRIBUTES sa;   //安全属性

          sa.bInheritHandle = true;

          sa.lpSecurityDescriptor = NULL;

          sa.nLength = sizeof(SECURITY_ATTRIBUTES);

   

          if(!CreatePipe(&hRead,&hWrite,&sa,0))

          {

                   MessageBox("ERROR!");

                   return;

          }

 

          STARTUPINFO sui;  //指定新进程的主窗口如何显示

          ZeroMemory(&sui,sizeof(STARTUPINFO));

          sui.cb = sizeof(STARTUPINFO);

          sui.dwFlags = STARTF_USESTDHANDLES;

          sui.hStdInput = hRead;   //设置子进程的标准输入句柄,子进程调用GetStdHandle函数将得到该句柄

          sui.hStdOutput = hWrite;  //设置子进程的标准输出句柄

          sui.hStdError = GetStdHandle(STD_ERROR_HANSLE);

 

          PROCESS_INFORMATION pi; //接收有关于新进程的标识信息

 

          if(!CreateProcess("..\\Child\\Debug\\Chile.exe",NULL,NULL,NULL,TRUE,0.NULL,NULL,&sui,&pi)

          {

                   CloseHandle(hRead);

                   CloseHandle(HWrite);

                   hRead = NULL;

                   hWrite = NULL;

                   MessageBox("Fail");

                   return;

          }

          else

          {

                   CloseHandle(pi.hProcess);

                   CloseHandle(pi.dwThreadId);

          }

 

}

//ZeroMemory(&sui,sizeof(STARTUPINFO));sui中所有成员全设置为0

//GetStdHandle函数:获得标准输入,标准输出,或者一个标准错误输出句柄

   5. 管道的读取和写入:ReadFileWriteFile

     读取:void CParentView::On2()

{

          // TODO: Add your command handler code here

          char buf[100];

          DWORD dwRead;

          if(!ReadFile(hRead,buf,100,&dwRead,NULL))

          {

                   MessageBox("FAIL");

                   return;

          }

          MessageBox(buf);

}

    写入:void CParentView::On3()

{

          // TODO: Add your command handler code here

          char buf[] = "Pipe";

          DWORD dwWrite;

          if(!WriteFile(hWrite,buf,strlen(buf) + 1,&dwWrite,NULL))

          {

                   MessageBox("Fail");

                   return;

          }

}

5)子进程的实现

     1.增加成员变量 HANDLE  hWrite;  HANDLE  hRead;

     2.构造函数中初始化 hRead = NULL;  hWrite = NULL;

     3.析构函数中关闭这两个变量

        if(hRead) CloseHandle(hRead);

        if(hWrite) CloseHandle(hWrite);

     4.获得管道的读取和写入句柄(即子进程的标准输入、输出句柄)

       要在CChild类窗口完全创建成功后去获取,因此,我们可以为CChildView类增加虚函数OnInitialUpdate,这个函数是窗口创建成功后第一个调用的函数

       void CChildView::OnInitialUpdate()

{

          CView::OnInitialUpdate();

          hRead = GetStdHandle(STD_INPUT_HANDLE);

          hWrite = GetStdHandle(STD_OUTPUT_HANDLE); 

}

     5. 读取和写入:ReadFileWriteFile

        读取:

void CChildView::On1()

{

          char buf[100];

          DWORD dwRead;

          if(!ReadFile(hRead,buf,100,&dwRead,NULL))

          {

                   MessageBox("FAIL");

                   return;

          }

          MessageBox(buf);

}

     写入:

     void CChildView::On2()

{

          char buf[] = "Pipe";

          DWORD dwWrite;

          if(!WriteFile(hWrite,buf,strlen(buf) + 1,&dwWrite,NULL))

          {

                   MessageBox("Fail");

                   return;

          }

}

6)因为匿名管道没有名称,所以只能在父进程中创建子进程时,将管道的读、写句柄传递给子进程

4.命名管道

1)命名管道通过网络来完成进程间的通信,它屏蔽了底层的网络协议细节。命名管道不仅可以在本机上实现两个进程间的通信,还可以跨网络实现两个进程间的通信

2)在创建管道时,可以指定具有访问权限的用户

3)将命名管道作为一种网络编程方案时,它实际上建立了一个客户机/服务器通信体系,并在其中可靠地传输数据。命名管道是围绕Windows文件系统设计的一种机制,采用“命名管道文件系统(NPFS)”接口。命名管道服务器和客户端的区别是:服务器是唯一一个有权创建命名管道的进程,也只有它才能接受管道客户机的连接请求,而客户机只能同一个现成的命名管道服务器建立连接

4)命名管道提供了两种基本通信模式:字节模式和消息模式

      字节模式:数据以一个连续的字节流的形式在客户机和服务器之间流动

      消息模式:客户机和服务器通过一系列不连续的数据单位,进行数据的收发

5)创建命名管道:CreateNamedPipe函数

    函数原型:

       HANDLE CreateNamedPipe

           LPCTSTR lpName,//指向字符串,格式必须是:\\.\pipe\pipename

其中:开始是两个连续的反斜杠,其后的圆点表示是本地机器,如果想与远程的服务器建立连接,那么在这个圆点位置处应指定远程服务器名,接下来是”pipe”这个固定的字符串,最后是所创建的命名管道的名称

           DWORD dwOpenMode,//指定管道的访问方式、重叠方式、写直通方式、还有管道句柄的安全访问方式

           DWORD dwPipeMode,//指定管道句柄的类型、读取和等待方式

管道句柄的类型:PIPE_TYPE_BYTE PIPE_TYPE_MESSAGE,同一命名管道的每一个实例必须具有相同的类型  默认字节型

读取方式:PIPE_READMODE_BYTEPIPE_READMODE_MESSAGE

等待方式:PIPR_WAIT(阻塞)和PIPE_NOWAIT(非阻塞)

           DWORD nMaxInstances,//指定管道能够创建的实例的最大数目,最大为:

PIPE_UNLIMITED_INSTANCES;对同一个命名管道的实例来说,在某一时刻,它只能和一个客户端进行通信

           DWORD nOutBufferSize,//指定输出缓冲区所保留的字节数

           DWORD nInBufferSize,//指定输入缓冲区所保留的字节数

           DWORD nDefaultTimeOut,//指定默认的超时值,同一管道的不同实例必须指定同样的超时值

     LPSECURITY_ATTRIBUTES  lpSecurityAttributes//安全属性

);

6)服务器端程序

     1.CNamedPipeSrv类增加一个句柄变量,保存命名管道实例句柄:HANDLE hPipe;

     2.在构造函数中将其初始化为NULL   hPipe = NULL;

     3.在析构函数中关闭该句柄  if(hPipe) CloseHandle(hPipe);

     4.创建命名管道

       void CNamedPipeSrvView::OnPipeCreate()

{

//创建命名管道PIPE_ACCESS_DUPLEX 双向模式 FILE_FLAG_OVERLAPPED 允许重叠

hPipe = CreateNamedPipe("\\\\.\\pipe\\MyPipe",PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,0,1,1024,1024,0,NULL);

          if(INVALID_HANDLE_VALUE == hPipe)

          {

                   MessageBox("创建命名管道失败!");

                   hPipe = NULL;

                   return;

          }

          //创建命名管道的人工重置对象

          HANDLE hEvent;

          hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);

          if(!hEvent)

          {

                   MessageBox("创建事件对象失败!");

                   CloseHandle(hPipe);

                   hPipe = NULL;

                   return;

          }

 

          OVERLAPPED ovlap;

          ZeroMemory(&ovlap,sizeof(OVERLAPPED));

    ovlap.hEvent = hEvent;

 

          //等待客户端请求的到来

          if(!ConnectNamedPipe(hPipe,&ovlap))

          {

                   if(ERROR_IO_PENDING != GetLastError())

                   {

                            MessageBox("等待客户端连接失败!");

                            CloseHandle(hPipe);

                            CloseHandle(hEvent);

                            hPipe = NULL;

                            return;

                   }

          }

          if(WAIT_FAILED == WaitForSingleObject(hEvent,INFINITE))

          {

                   MessageBox("等待对象失败");

                   CloseHandle(hPipe);

                   CloseHandle(hEvent);

                   hPipe = NULL;

                   return;

          }

          CloseHandle(hEvent);     

}

     5.等待客户端请求的到来:ConnectNamedPipe,这个函数的作用是让服务器等待客户端的连接请求的到来

 函数原型:BOOL ConnectNamedPipe(HANDLE hNamedPipe,LPOVERLAPPED lpOverlapped);

//第一个参数指向一个命名管道实例的服务器的句柄,该句柄由CreateNamedPipe返回

//第二个参数是一个指向OVERLAPPED结构的指针,如果hNamedPipe参数所标识的管道是用FILE_FLAG_OVERLAPPED标记打开的,则这个参数不能是NULL,必须是一个有效的指针,若这个参数不是NULL,则必须包含人工重置对象的句柄

     6.读取数据:同匿名管道读取操作

     7.写入数据:同匿名管道写入操作

7)客户端程序

     1.CNamedPipeClt类增加一个句柄变量,保存命名管道实例句柄:HANDLE hPipe;

     2.在构造函数中将其初始化为NULL   hPipe = NULL;

     3.在析构函数中关闭该句柄  if(hPipe) CloseHandle(hPipe);

     4.连接命名管道

       1)判断是否由可用的命名管道:WaitNamedPipe函数,该函数会一直等待,知道指定的事件间隔已过,或者指定的命名管道的实例可以用来连接了

       函数原型:BOOL WaitNamedPipe(LPCTSTR lpNamedPipeName,DWORD nTimeout);

//第一个参数指定命名管道的名称,这个名称必须包括创建该命名管道的服务器进程所在的机器名,格式为\\.\pipe\pipename,若跨网通信,则圆点位置应指定服务器程序所在主机名

//第二个参数指定超时间隔 NMPWAIT_WAIT_FOREVER 一直等待

       2)打开命名管道:CreateFile函数

     void CNamedPipeCltView::OnPipeConnect()

{

         //判断是否有可以利用的命名管道

         if(!WaitNamedPipe("\\\\.\\.pipe\\MyPipe",NMPWAIT_WAIT_FOREVER))

         {

                   MessageBox("当前没有可用的命名管道实例!");

                   return;

         }

         //打开可用的命名管道,并与服务器进程进行通信

         hPipe = CreateFile("\\\\.\\pipe\\MyPipe",GENERIC_READ | GENERIC_WRITE,0,NULL,\

                   OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

         if(INVALID_HANDLE_VALUE == hPipe)

         {

                   MessageBox("打开命名管道失败!");

                   hPipe = NULL;

                   return;

         }

}

     5. 读取数据:同匿名管道读取操作

     6.写入数据:同匿名管道写入操作

5.邮槽

1)邮槽是基于广播(一对多)通信体系设计出来的,它采用无连接的不可靠的数据传输,邮槽是一种单向通信机制创建邮槽的服务器进程读取数据,打开邮槽的客户机进程写入数据(在Windows平台下,传输消息时,长度要在424字节一下)

2)创建邮槽:CreateMailslot函数,该函数利用指定的名称创建一个邮槽,然后返回所创建的邮槽的句柄

     函数原型:HANDLE CreateMailslot

                 LPCTSTR lpName,//指定邮槽的名称,格式:”\\.\mailslot\[path]name”

                 DWORD nMaxMessageSize,//指定可以被写入邮槽的单一消息的最大尺寸,设为0可以发送任意大小的消息

                 DWORD lReadTimeout,//读取操作的超时时间间隔,设为0,则若无消息,立即返回;设为MAILSLOT_WAIT_FOREVER,则函数一直等待

     LPSECURITY_ATTRIBUTES lpSecurityAttributes//安全属性

);

3)服务器端程序

     void CMailslotSrcView::OnMailslotRecv()

{

          HANDLE hMailslot;

hMailslot = CreateMailslot("\\\\.\\mailslot\\MyMailslot",0,MAILSLOT_WAIT_FOREVER,NULL);

          if(INVALID_HANDLE_VALUE == hMailslot)

          {

                   MessageBox("创建邮槽失败!");

                   return;

          }

          char buf[100];

          DWORD dwRead;

          if(!ReadFile(hMailslot,buf,100,&dwRead,NULL))

          {

                   MessageBox("读取数据失败!");

                   CloseHandle(hMailslot);

                   return;

          }

          MessageBox(buf);

          CloseHandle(hMailslot);

}

4)客户端程序

    void CMailslotCltView::OnMailslotSend()

{

       HANDLE hMailslot;

       hMailslot = CreateFile("\\\\.\\mailslot\\MyMailslot",GENERIC_WRITE,\

                FILE_SHARE_READ,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

       if(INVALID_HANDLE_VALUE == hMailslot)

       {

                MessageBox("打开邮槽失败!");

                return;

       }

       char buf[] = "hello";

       DWORD dwWrite;

       if(!WriteFile(hMailslot,buf,strlen(buf)+1,&dwWrite,NULL))

       {

                MessageBox("写入数据失败!");

                CloseHandle(hMailslot);

                return;

       }

       CloseHandle(hMailslot);

}

5)邮槽可以实现一对多的单向通信

6.比较:邮槽容量小,管道容量较大

        邮槽可以一对多,管道只能一对一

        邮槽只能是单项的,管道可以双向

        邮槽不可靠数据传输,管道可靠

        剪贴板和匿名通道只能在本机通信,邮槽和命名管道可以跨网络

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 《VC深入详解第3版PDF》是一本介绍Visual C++编程的书籍,该书主要针对具有一定编程基础的开发者,讲解了VC编程的高级应用和技巧。 该书分为10章,从Windows程序设计基础、MFC应用程序框架、视图类、文档类、数据库编程、多线程编程、网络编程、COM组件开发、ActiveX控件开发以及VC程序调试与优化等方面进行了深入系统的讲解,全面介绍了VC编程的相关知识和技术,对开发者有很大的帮助。 在该书中,作者通过丰富的实例和详细的代码讲解,让读者深入了解VC编程的常见问题和解决方法。同时,书中还介绍了一些重要的编程工具和技巧,如调试工具的使用、Windows消息机制、数据类型转换等,这些内容可以帮助读者从更高的角度理解VC编程,并更好地掌握其技术特点。 总之,《VC深入详解第3版PDF》是一本对VC编程进行深入研究和学习的优秀书籍,同时也适用于希望提高Windows程序设计和开发的程序员。该书具备丰富的内容和深入的讲解,对VC编程感兴趣的开发者将有所裨益。 ### 回答2: VC 深入详解 第3版 PDF 是一本介绍 Microsoft Visual C++(VC++)程序设计语言的详尽指南。本书在深入介绍 C++ 语言和编程基本概念的基础上,重点讲解了 VC++ 的程序设计和开发理念、各种功能和应用方法。本书内容全面,包括 VC++ 编译器、Windows 应用程序开发、图形用户界面设计、多线程编程、应用程序框架、数据库编程、网络编程等多个方面,对新手和资深程序员都有较高的指导作用。 本书第三版相对于前两版更新换代,其主要变化在于增加和更新了一些章节,完善了一些概念和应用。例如,本版增加了针对 Windows 8、Windows 10、Visual Studio 2013 和 2015 等新技术和工具的章节,更新了若干图形界面设计和控件使用的方法,增加了多线程、数据库和网络编程等方面的实例等等。同时,本书也对一些旧版章节进行了深入拓展和重制,以提高其可读性和可操作性。 值得注意的是,本书虽然以 VC++ 为主轴,但其对 C++ 语言本身也有广泛的涉猎,对于学习 C++ 编程的初学者也具有较好的指导作用。此外,本书的内容偏向实战操作,有大量具有代表性的应用实例供读者参考和尝试,这对于帮助读者掌握 VC++ 编程技术和提高实战操作能力都是有益的。 总之,《VC 深入详解 第3版 PDF》是一本帮助程序员深入理解 VC++ 编程技术和应用的权威性指南,具有较高的指导作用和实际价值。 ### 回答3: 《VC深入详解》是一本介绍Microsoft Visual C++编程语言和开发工具的书籍,此书分为入门篇和深入篇两部分,全书共分22章,对VC语言的基础知识、面向对象编程、MFC程序框架、窗口、对话框、菜单、工具栏、绘图、多媒体等方面进行了详尽的讲解和实践操作。第3版更新了最新版本的VC++ 2013和MFC,对原有的内容进行了更新和完善。 此书的深入详解部分具有较强的实践性和实用性, chapters中包括了Windows应用程序设计、高级算法、网络编程、多线程编程、数据库编程、动态链接库、ActiveX控件与COM、ATL和.NET等内容。并且此书结合了理论和实践,每个章节都有完整的实例程序,并且结合具体应用场景进行分析,让读者不仅能够掌握基本知识,还能够提升对VC编程的实际应用能力。 总体评价而言,《VC深入详解》是一本较为全面和权威的VC编程实战教程,适合于具有C/C++编程基础者进行深入学习,对于从事Windows软件开发的程序员和学生都具有很高的参考价值。在VC++编程学习中,这本书是一份必备工具书,对于提高编程能力有很大的帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值