九月鹰飞

慎独自修,忠恕宽容,至诚尽性。

  VC知识库BLOG :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 登录 ::
  19 随笔 :: 13 文章 :: 298 评论 :: 5 Trackbacks
<2010年3月>
28123456
78910111213
14151617181920
21222324252627
28293031123
45678910

留言簿(3)

随笔分类

随笔档案

文章分类

文章档案

相册

搜索

最新评论

阅读排行榜

评论排行榜


关于可变参数函数的若干问题

iwaswzq 2005/6/15

------------------------------------------------------------------

c/c++支持可变参数的函数,即函数的参数是不确定的。

一、为什么要使用可变参数的函数?

    一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的所有实际参数。但在某些情况下希望函数的参数个数可以根据需要确定,因此c语言引入可变参数函数。这也是c功能强大的一个方面,其它某些语言,比如fortran就没有这个功能。

    典型的可变参数函数的例子有大家熟悉的printf()、scanf()等。

二、c/c++如何实现可变参数的函数?

    为了支持可变参数函数,C语言引入新的调用协议, 即C语言调用约定 __cdecl 。 采用C/C++语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明,例如WIN32 API使用PASCAL调用约定,函数名字之前必须加__stdcall关键字。

    采用C调用约定时,函数的参数是从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由函数调用者负责堆栈清理。举个例子:

   //C调用约定函数
   int __cdecl Add(int a, int b)
   {
      return (a + b);
   }
  
   函数调用:
   Add(1, 2);
   //汇编代码是:
   push        2            ;参数b入栈
   push        1            ;参数a入栈
   call        @Add         ;调用函数。其实还有编译器用于定位函数的表达式这里把它省略了
   add         esp,8        ;调用者负责清栈

    如果调用函数的时候使用的调用协议和函数原型中声明的不一致,就会导致栈错误,这是另外一个话题,这里不再细说。

    另外c/c++编译器采用宏的形式支持可变参数函数。这些宏包括va_start、va_arg和va_end等。之所以这么做,是为了增加程序的可移植性。屏蔽不同的硬件平台造成的差异。

    支持可变参数函数的所有宏都定义在stdarg.h 和 varargs.h中。例如标准ANSI形式下,这些宏的定义是:

 typedef char *  va_list;  //字符串指针

 #define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

 #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
 #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
 #define va_end(ap)      ( ap = (va_list)0 )

    使用宏_INTSIZEOF是为了按照整数字节对齐指针,因为c调用协议下面,参数入栈都是整数字节(指针或者值)。


三、如何定义这类的函数。

    可变参数函数在不同的系统下,采用不同的形式定义。

1、用ANSI标准形式时,参数个数可变的函数的原型声明是:

 type funcname(type para1, type para2, ...);

    关于这个定义,有三点需要说明:

    一般来说,这种形式至少需要一个普通的形式参数,可变参数就是通过三个'.'来定义的。所以"..."不表示省略,而是函数原型的一部分。type是函数返回值和形式参数的类型。
例如:

 int  MyPrintf(char const* fmt, ...);

    但是,我们也可以这样定义函数:

 void MyFunc(...);

    但是,这样的话,我们就无法使用函数的参数了,因为无法通过上面所讲的宏来提取每个参数。所以除非你的函数代码中的确没有用到参数表中的任何参数,否则必须在参数表中使用至少一个普通参数。

    注意,可变参数只能位于函数参数表的最后。不能这样:

 void MyFunc(..., int i);

2、采用与UNIX 兼容系统下的声明方式时,参数个数可变的函数原型是:

 type funcname(va_alist);

    但是要求函数实现的时候,函数名字后面必须加上va_dcl。例如:

  #include <varargs.h>
  int average( va_list );

  void main( void )
  {
   。。。//代码
  }

  /* UNIX兼容形式*/
  int average( va_alist )
  va_dcl
  {
   。。。//代码
  }

    这种形式不需要提供任何普通的形式参数。type是函数返回值的类型。va_dcl是对函数原型声明中参数va_alist的详细声明,实际是一个宏定义。根据平台的不同,va_dcl的定义稍有不同。

    在varargs.h中,va_dcl的定义后面已经包括了一个分号。因此函数实现的时候,va_dcl后不再需要加上分号了。

    
3、采用头文件stdarg.h编写的程序是符合ANSI标准的,可以在各种操作系统和硬件上运行;而采用头文件varargs.h的方式仅仅是为了与以前的程序兼容,两种方式的基本原理是一致的,只是在语法形式上有一些细微的区别。 所以一般编程的时候使用stdarg.h。下面的所有例子代码都采用ANSI标准格式。


四、可变参数函数的基本使用方法

下面通过若干例子,说明如何实现可变参数函数的定义和调用。

//================================ 例子程序1 ===============
#include < stdio.h >
#include < string.h >
#include < stdarg.h >

/* 函数原型声明,至少需要一个确定的参数,注意括号内的省略号 */
int demo( char *, ... ); 

void main( void )
{
 demo("DEMO", "This", "is", "a", "demo!", "\0");
}

int demo( char *msg, ... ) 
{
 va_list argp; /* 定义保存函数参数的结构 */
    int argno = 0; /* 纪录参数个数 */
    char *para; /* 存放取出的字符串参数 */

 // 使用宏va_start, 使argp指向传入的第一个可选参数,
 // 注意 msg是参数表中最后一个确定的参数,并非参数表中第一个参数
 va_start( argp, msg ); 
  
    while (1)
 {
  //取出当前的参数,类型为char *
  //如果不给出正确的类型,将得到错误的参数
  para = va_arg( argp, char *);

     if ( strcmp( para, "\0") == 0 ) /* 采用空串指示参数输入结束 */
   break;
  printf("参数 #%d 是: %s\n", argno, para);
     argno++;
    }
  va_end( argp ); /* 将argp置为NULL */
    return 0;
}

//输出结果
参数 #0 是: This
参数 #1 是: is
参数 #2 是: a
参数 #3 是: demo!

注意到上面的例子没有使用第一个参数,下面的例子将使用所有参数

//================================ 例子程序2 ===============

#include <stdio.h>
#include <stdarg.h>
int average( int first, ... );  //输入若干整数,求它们的平均值

void main( void )
{
   /* 调用3个整数(-1表示结尾) */
   printf( "Average is: %d\n", average(2,3,4, -1));

   /*调用4个整数*/
   printf( "Average is: %d\n", average(5,7,9, 11,-1));

   /*只有结束符的调用*/
   printf( "Average is: %d\n", average(-1) );
}

/* 返回若干整数平均值的函数 */
int average( int first, ... )
{
   int count = 0, sum = 0, i = first;
   va_list marker;

   va_start( marker, first ); //初始化
   while( i != -1 )
   {
      sum += i; //先加第一个参数
      count++;
      i = va_arg( marker, int);//取下一个参数
   }
   va_end( marker );
   return( sum ? (sum / count) : 0 );
}

//输出结果
Average is: 3
Average is: 8
Average is: 0


五、关于可变参数的传递问题

    有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏va_arg逐个把参数提取出来使用,能否不提取,直接把它们传递给另外的函数呢?

    我们先看printf的实现:

int __cdecl printf (const char *format, ...)
{
        va_list arglist;
        int buffing;
        int retval;

        va_start(arglist, format); //arglist指向format后面的第一个参数

 。。。//不关心其它代码
        retval = _output(stdout,format,arglist); //把format格式和参数传递给output函数

 。。。//不关心其它代码
        return(retval);
}

我们先模仿这个函数写一个:

#include <stdio.h>
#include <stdarg.h>

int mywrite(char *fmt, ...)
{
     va_list arglist;
     va_start(arglist, fmt);
     return printf(fmt,arglist);
}

void main()
{
 int i=10, j=20;
 char buf[] = "This is a test";
 double f= 12.345;
 mywrite("String: %s\nInt: %d, %d\nFloat :%4.2f\n", buf, i, j, f);
}

    运行一下看看,哈,错误百出。仔细分析原因,根据宏的定义我们知道 arglist是一个指针,它指向第一个可变的参数,但是所有的参数都位于栈中,所以arglist指向栈中某个位置,通过arglist的值,我们可以直接查看栈里面的内容:

arglist -> 指向栈里面,内容包括

0067FD78  E0 FD 67 00  //指向字符串"This is a test"
0067FD7C  0A 00 00 00  //整数 i 的值
0067FD80  14 00 00 00  //整数 j 的值
0067FD84  71 3D 0A D7  //double 变量 f, 占用8个字节
0067FD88  A3 B0 28 40
0067FD8C  00 00 00 00

    如果直接调用 printf(fmt, arglist); 仅仅是把arglist指针的值0067FD78入栈,然后把格式字符串入栈,相当于调用:

    printf(fmt, 0067FD78);

    自然这样的调用肯定会出现错误。

    我们能不能逐个把参数提取出来,再传递给其它函数呢?先考虑一次性把所有参数传递进去的问题。

    如果调用的是系统库函数,这种情况下是不可能的。因为提取参数是在运行态,而参数入栈是在编译的时候确定的。无法让编译器预知运行态的事情给出正确的参数入栈代码。而我们在运行态虽然可以提取每个参数,但是无法将参数一次性全部压栈,即使使用汇编代码实现起来也是很困难的,因为不单是一个简单的push代码就可以做到。

    如果接受参数的函数也是我们自己写的,自然我们可以把arglist指针入栈,然后在函数中自己解析arglist指针里面的参数,逐个提取出来处理。但是这样做似乎没有什么意义,一方面,这个函数没有必要也做成可变参数函数,另一方面直接在第一个函数中解析参数,然后处理不是更简单么?

    我们唯一可以做到的是,逐个解析参数,然后循环中调用其它可变参数函数,每次传递一个参数。这里又有一个问题,就是参数表中的不可变参数的传递问题,有些情况下不能简单的传递,以上面的例子为例, 通常我们解析参数的同时,还需要解析格式字符串:

#include <windows.h>
#include <stdio.h>
#include <stdarg.h>

//测试一下这个,开个玩笑
void t(...)
{
 printf("\n");
}

int mywrite(char *fmt, ...)
{
     va_list arglist;
     va_start(arglist, fmt);

  char temp[255];
  strcpy(temp, fmt); //Copy the Format string
  char Format[255];

  char *p = strchr(temp,'%');
  int i=0;
  int iParam;
  double fParam;
  while(p != NULL)
  {
   while((*p<'a' || *p>'z') && (*p!=0) ) p++;
   if(*p == 0)break;
   p++;

   //格式字符串
   int nChar = p - temp;
   strncpy(Format,temp, nChar);
   Format[nChar] = 0;
   //参数
   if(Format[nChar-1] != 'f')
   {
    iParam = va_arg( arglist, int);
    printf(Format, iParam);
   }
   else
   {
    fParam = va_arg( arglist, double);
    printf(Format, fParam);
   }

   i++;
   if(*p == 0) break;
   strcpy(temp, p);
   p = strchr(temp, '%');
  }
  if(temp[0] != 0)
   printf(temp);

  return i;

}

void main()
{
 int i=10, j=20;
 char buf[] = "This is a test";
 double f= 123.456;
 mywrite("String: %s\nInt: %d, %d\nFloat :%4.2f\nEnd", buf, i, j, f, 0);
 t("aaa", i);
}

//输出:
String: This is a test
Int: 10, 20
Float :123.46
End

当然这里的解析是不完善的。
----------------------------------------
iwaswzq 2005/6/15

posted on 2005-06-20 15:17 九月鹰飞 阅读(7195) 评论(21)  编辑 收藏

评论

# re: 关于可变参数函数的若干问题 2005-06-20 15:42 pAnic
分析的挺仔细,不过大师说过:哪里有"..."(可变参数),哪里就没多少C++了。
对于可变参数,能不用就不用咯~

# 原来老大在耍我们! 2005-06-20 22:18 乾坤一笑

“五、关于可变参数的传递问题”讲的例子,看了一遍,玄乎其玄。我头脑发胀, 隐隐觉得那里有些不对劲,可就是找不到根由。翻来覆去,看了3遍,终于大悟:原来老大在耍我们,玩偷换命题的把戏!

有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏va_arg逐个把参数提取出来使用,能否不提取,直接把它们传递给另外的函数呢?” 这就是命题。这个命题可以抽象为:在可变参数函数A中调用另一个可变参数B的函数,把传给A的可变参数(或其一部分)传给B。需要注意的要素有3个:(1)函数A是接受可变参数的;(2)函数B是接受可变参数的;(3)A调用B时传给是可变参数。

老大先举了两个可变参数的例子,都不成功。第三个例子解析了大半天,终于打出了正确结果。诈一看是对了,并且老大也说了,“当然这里的解析是不完善的”,一般人糊里糊涂也就放过去了。可是你仔细想想,不是那么回事。对照上面我们提到的3要素:A就是int mywrite(char *fmt, ...), B就是printf(),mywrite()调用printf()时一定传给它个可变的参数。 可是文中例子,无论是调用printf(Format, iParam)还是调用printf(Format, fParam),都仅仅传给了printf()一个格式表和另一个数值参数,那里来的可变参数嘛?这有啥难度呢?倒是诈有其事的解析了一把format,嘿嘿。

要是这个问题领域单单是解决打印问题的,我看还真的不需要那么费劲。我给你把上面那个函数改改,你看行不?

#include <stdio.h>
#include <stdarg.h>

void mywrite(char *fmt, ...)
{
     va_list arglist;
     char buffer[1024];
     va_start(arglist, fmt);
     vsprintf(buffer,fmt, arglist);
     printf("%s",buffer);
}

int main()
{
 int i=10, j=20;
 char buf[] = "This is a test";
 double f= 12.345;
 mywrite("String: %s\nInt: %d, %d\nFloat :%4.2f\n", buf, i, j, f);
 return 0;
}

不好意思啊,偶花了半个小时拨号费才看完您这篇文章。浪费了银子就得发发牢骚,您多包涵,别往心里去。haha...hahahaha....



# re: 关于可变参数函数的若干问题 2005-06-20 22:21 乾坤一笑
补充:我着重想强调"能否不提取,直接把它们传递给另外的函数呢"这句。:)

# to : 一笑 2005-06-20 22:52 iwaswzq
我只要是想强调,如果要调用的是系统函数,或者其它库函数,则传递可变参数是不可能的。因为参数入栈是编译器的事情,但是如果第二个可变参数的函数也是你自己写的,当然可以自己解析。下面给出一个例子:
#include <stdio.h>
#include <stdarg.h>


void data2(int first, ...)
{

va_list arglist;
va_start(arglist, first);
arglist = (char *) (* ((int*) arglist));
printf("The others:");

int i = 0;

while( i != -1 )
{
i = va_arg( arglist, int);//取下一个参数
printf(" %d", i);
}
printf("\n");

}
void data1(int first, ...)
{
va_list arglist;
va_start(arglist, first);
printf("The First: %d\n",first);
int i = va_arg(arglist, int);
if(i!= -1)
{
printf("The Second: %d\n",i);
data2(first, arglist);
}
}

void main()
{
int i=10, j=20, k = 30, l = 40;
data1(i, j, k, l, -1); // -1 means end
}


# to 一笑 2005-06-20 22:55 九月鹰飞
我下面这个例子中,data1提取第一个和第二个参数,然后输出,剩下的,不论多少个,都交给函数data2进行处理。就是这个意思。

# to 一笑 2005-06-20 23:27 九月鹰飞
1、你修改的很好,vsprintf是个好的例子。但我之所以要使用两个可变参数函数来讨论参数的传递问题,就是想说,这两个函数各有各的目的,如果象你修改的那样,直接把所有东西都给vsprintf了,那么写mywrite函数还有何意义?如果要传递一部分给vsprintf, 不修改格式表是不可能的,肯定要导致错误。
2、我说如果调用的是某个库函数(可变参数函数),则再次传递可变参数是不可能的,也许说的绝对了。现在想想,使用vsprintf这类的函数也许可以做到。

# re: 关于可变参数函数的若干问题 2005-06-21 00:12 乾坤一笑
printf()的format参数起了两个作用:一是va_start宏需要一个不可变参数来确定可变参数的位置,从而初始化va_list;二是每次从va_list取数时都要告诉va_arg宏当前要取的数据的数据类型,以确定指针偏移量。这才是问题的关键。

所以A在调用B之前先从va_list中取一些数出来,再把va_list传给B是完全行的通的。只是需要保证B能够知道它所接受的va_list中每个要取出的数据的数据类型就可以了。而对于printf()函数来说,format参数就提供了va_list中每个参数的类型(通过对%d、%f之类的的解析就可顺序的得出数据的类型)。所以,如果真的像你的那个例子一样去完成同样的功能,我到宁可去修改format表:在A中取出若干数据后,把取出数据的数据类型相关信息从format表中删掉,在把处理过的format表和自动更新过的va_list传给B。我觉得这才是正招,如果B只能一个一个的接收参数,那么就相当于A在替B做事(B每次接受固定的参数,就没有必要用va_arg从va_list取数据了,B可变参数的意义何在?)这样的讨论也就没有什么意义了。

这就是我的拙见。


# re: 2005-06-21 01:08 anonymous
偶认为正真需要注意的是float类型:

void foo(int i, ...){
    float f;
    va_list li;
    va_start(li, i);
    f=va_arg(li, float);
    printf("%f\n", f);
}

main(){
    foo(0, 1.234); //  -369098.750000
}

在C++上面要注意的是输出转型问题:
class MyString{
public:
    MyString(char const* str):_str(str){}
    operator char const*()const{
           assert(0);
           return _str;
    }
private:
    char const* str;
};

MyString str=MyString("bar");
printf("%s\n", str); 

# to 楼上兄弟 2005-06-21 06:50 九月鹰飞
float和int 一样,现在是4个字节。而编译器把1.234压栈的时候,是作为double处理的,使用了8个字节。所以不能使用

f = va_arg(li, float); //出错

必须采用

f = va_arg(li, double);

# to : 一笑 (总结如下) 2005-06-21 07:20 九月鹰飞
总结一下:

1、你认为“在A中取出若干数据后,把取出数据的数据类型相关信息从format表中删掉,在把处理过的format表和自动更新过的va_list传给B。我觉得这才是正招,如果B只能一个一个的接收参数,那么就相当于A在替B做事(B每次接受固定的参数,就没有必要用va_arg从va_list取数据了,B可变参数的意义何在?)这样的讨论也就没有什么意义了。”
我同意你的部分观点。但是,即便A逐个取出参数,然后传递给B,也不是A在替B做事,而是A要使用B函数,它只能如此,B函数还可以在其它地方直接使用,所以定义成可变参数也是有意义的。

2、如果我们讨论的B函数,是指vsprintf 或者 vpintf 之类的话, 就和我要说明的问题有些区别了。虽然你修改的那个例子,完全可以实现mywrite调用vsprintf, 并且把参数传递过去,甚至还可以在mywrite里面解析部分参数,把剩下的传递给vsprintf,但是和我原帖中的问题有所差别。我暂时不认为这类函数属于可变参数函数,它们只能接受固定个数的参数,其中有一个是arg_list类型。
例如你直接调用 vprintf("%s: %d", "Test", 10);
这样是不行的。编译器会产生一个错误,提示你vprintf函数不是3个参数,对于可变参数函数,怎么也不会产生这样的编译错误。

3、我的原帖主要讨论象printf这样的ANSI标准的库函数, 如果向这类函数二次传递可变参数是做不到的(目前我认为如此,可能你会有高明的做法)。具体的原因就是我对参数入栈的说明。
虽然传递可变参数做不到,但是传递单个参数当然可以,只能象原帖那样,A自己解析参数表,然后每次传递1个参数给B。原帖主要讨论的就是这个问题。


# to 九月鹰飞 : 2005-06-21 20:36 乾坤一笑
道不同不相与为谋!你我的设计思维差别太大了。:)

如果让我设计函数库,做一类相似功能的变参函数,我一定会取其公共子集(或者说普适代码),来把它实现成接受arg_list以供其他特例型函数调用。我相信所有库函数设计者都会这么做——事实也证明却是如此,如VC抽出一个公共函数int __cdecl _output ( FILE *, const char *, va_list ) ,gcc抽象出extern int vprintf (__const char *__restrict __format, _G_va_list __arg)等等。这就是一般的设计思路,或者说是模式。库设计者绝对应该考虑到了用户能做的扩展,所以你不比杞人忧天在某些情况下用不成。

va_list只是一个char *的指针,如果你要非把它当作一个类型我也无话可说。对于C语言这种弱类型语言(相对于C++)来说,所有的型别都能强制非强制的转来转去。所以va_list我只认为是一个地址,一个指向某堆栈的top指针,它只要是指针就可以了,是什么类型的指针都没有很大区别。因为用va_arg取值的时候只是利用了这个地址,并没有用到va_list这个char*的型别。接受va_list类型参数的函数不是可变参数的函数我还真的是今天第一次听说。:D


# re: 乾坤一笑 2005-06-21 21:09 pAnic
不考虑调用规范的问题,可变参数可以简化成一个参数缓冲,其实也就是个void *

# to 一笑和pAnic 2005-06-22 23:59 九月鹰飞
说的有道理,同意。

# re: 关于可变参数函数的若干问题 2005-08-19 10:53 ay
好文! 

# re: 关于可变参数函数的若干问题 2005-08-19 10:54 anomymous
讨论的也很好. :)

# re: 关于可变参数函数的若干问题 2005-08-21 13:15 passerby
感谢 乾坤一笑和九月鹰飞 两位大侠
讨论对大家都有好处,讨论促使人思考.

特别感谢九月鹰飞,因为我现在对
第五部分以后的内容,看不懂.



# re: 关于可变参数函数的若干问题 2006-07-12 20:51 4如同
又快到九月了。
狐狸该出来了。
鹰也该出来了。
狐狸再狡猾,
也会被鹰捉到。

# re: 关于可变参数函数的若干问题 2006-10-24 10:10 洪水涛
谢谢各位了!!
受益非浅!!

# re: 关于可变参数函数的若干问题 2007-07-07 13:59 ***
printf直接调用outinfo的参数,学习一下PE头就知道原理了
void OutInfo( const char* s, ... )
{
static char* OldEsp;
static __w64 int EspLen;
static char* pData;
//。。。操作
va_list lv = NULL;
va_start(lv, s); //去 。。。 第一个参数地址
char* p = (char*)lv-4; //减4得到s的堆栈值
    EspLen = (__w64 int)p; //得到s的堆栈地址准备运算
__asm
{
mov OldEsp, esp; //得到当前esp值
}
EspLen = p - OldEsp; //算出要保存的esp长度
pData = new char[EspLen]; //申请保存地址
memcpy( pData, OldEsp, EspLen ); //保存
__asm
{
mov esp, p; //替换当前esp,替换完esp就是OutInfo的参数地址
call printf; //调用printf
mov esp, OldEsp; //恢复esp
}
memcpy( OldEsp, pData, EspLen ); //恢复esp数据
//。。。继续操作
}

# re: 关于可变参数函数的若干问题 2007-07-12 23:14 bird
我的大问题啊。。。

 怎么在DLL导出函数中使用可变参数?就是导出可变参数的函数??
有点挑战吧,哈。。

# re: 关于可变参数函数的若干问题 2009-03-09 18:54 tanchuhan@sina.com
例如WIN32 API使用PASCAL调用约定,函数名字之前必须加__stdcall关键字
------------------------------------------------
这句错了吧,呵呵
cdecl    从右到左压栈 调用者清栈
pascal  从左到右压栈 被调用者清栈
stdcall  从右向左压栈 被调用者清栈

stdcall是cdecl和pascal的混合体
当年DOS为了减少代码尺寸而采用了pascal协议
后来WINDOWS采用了stdcall.
网上经常有人把pascal等同stdcall,实际上两者的参数压栈顺序不一样.

另外,cdecl也不是为了可变参数而出来的,C语言很久以前就已经采用这个协议了.

以上是我个人的浅见.

标题  
姓名  
主页
验证码 *
内容   
  登录  使用高级评论  Top
[使用Ctrl+Enter键可以直接提交]