读Linux内核中的vsprintf函数的时候遇到了C语言的可变参数调用,查了挺多资料还是这篇比较详细,而且自己验证了下,确实如此

(一)写一个简单的可变参数的C函数  下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的  C函数要在程序中用到以下这些宏:  void va_start( va_list arg_ptr, prev_param );  type va_arg( va_list arg_ptr, type );  void va_end( va_list arg_ptr );  va在这里是variable-argument(可变参数)的意思.  这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个  头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数  参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.  void simple_va_fun(int i, ...)  {  va_list arg_ptr;  int j=0;  va_start(arg_ptr, i);  j=va_arg(arg_ptr, int);  va_end(arg_ptr);  printf("%d %d\n", i, j);  return;  }  我们可以在我们的头文件中这样声明我们的函数:  extern void simple_va_fun(int i, ...);  我们在程序中可以这样调用:  simple_va_fun(100);  simple_va_fun(100,200);  从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:  1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变  量是指向参数的指针.  2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第  一个可变参数的前一个参数,是一个固定的参数.  3)然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个  参数是你要返回的参数的类型,这里是int型.  4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使  用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获  取各个参数.  如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:  1)simple_va_fun(100);  结果是:100 -123456789(会变的值)  2)simple_va_fun(100,200);  结果是:100 200  3)simple_va_fun(100,200,300);  结果是:100 200  我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果  正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果  的原因和可变参数在编译器中是如何处理的.  (二)可变参数在编译器中的处理  我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,  由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下  面以VC++中stdarg.h里x86平台的宏定义摘录如下('\'号表示折行):  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(n)主要是为了某些需要内存的对齐的系统.C语言的函  数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我  们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再  看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的  地址,所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在堆  栈的地址,如图:  高地址|-----------------------------|  |函数返回地址 |  |-----------------------------|  |....... |  |-----------------------------|  |第n个参数(第一个可变参数) |  |-----------------------------|<--va_start后ap指向  |第n-1个参数(最后一个固定参数)|  低地址|-----------------------------|<-- &v  图( 1 )  然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我  们看一下va_arg取int型的返回值:  j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );  首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回  ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址  (图2).然后用*取得这个地址的内容(参数值)赋给j.  高地址|-----------------------------|  |函数返回地址 |  |-----------------------------|  |....... |  |-----------------------------|<--va_arg后ap指向  |第n个参数(第一个可变参数) |  |-----------------------------|<--va_start后ap指向  |第n-1个参数(最后一个固定参数)|  低地址|-----------------------------|<-- &v  图( 2 )  最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再  指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不  会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.  在这里大家要注意一个问题:由于参数的地址用于va_start宏,所  以参数不能声明为寄存器变量或作为函数或数组类型.  关于va_start, va_arg, va_end的描述就是这些了,我们要注意的  是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.  (三)可变参数在编程中要注意的问题  因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,  可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能  地识别不同参数的个数和类型.  有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数  printf是从固定参数format字符串来分析出参数的类型,再调用va_arg  的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通  过在自己的程序里作判断来实现的.  另外有一个问题,因为编译器对可变参数的函数的原型检查不够严  格,对编程查错不利.如果simple_va_fun()改为:  void simple_va_fun(int i, ...)  {  va_list arg_ptr;  char *s=NULL;  va_start(arg_ptr, i);  s=va_arg(arg_ptr, char*);  va_end(arg_ptr);  printf("%d %s\n", i, s);  return;  }  可变参数为char*型,当我们忘记用两个参数来调用该函数时,就会出现  core dump(Unix) 或者页面非法的错误(window平台).但也有可能不出  错,但错误却是难以发现,不利于我们写出高质量的程序.  以下提一下va系列宏的兼容性.  System V Unix把va_start定义为只有一个参数的宏:  va_start(va_list arg_ptr);  而ANSI C则定义为:  va_start(va_list arg_ptr, prev_param);  如果我们要用system V的定义,应该用vararg.h头文件中所定义的  宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以  用ANSI C的定义就够了,也便于程序的移植.  小结:  可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实  现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必  要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多  态性来实现可变参数的功能,尽量避免用C语言的方式来实现.

===================================================

概述 由于在C语言中没有函数重载,解决不定数目函数参数问题变得比较麻烦;即使采用C++,如果参数个数不能确定,也很难采用函数重载.对这种情况,有些人采用指针参数来解决问题.下面就c语言中处理不定参数数目的问题进行讨论.  定义 大家先看几宏. 在VC++6.0的include有一个stdarg.h头文件,有如下几个宏定义: #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 )                            // 将指针置为无效 如果对以上几个宏定义不理解,可以略过,接这看后面的内容.  参数在堆栈中分布位置 在进程中,堆栈地址是从高到低分配的.当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈过程,堆栈地址不断递减,一些黑客就是在堆栈中修改函数返回地址,执行自己的代码来达到执行自己插入的代码段的目的. 总之,函数在堆栈中的分布情况是:地址从高到低,依次是:函数参数列表,函数返回地址,函数执行代码段. 堆栈中,各个函数的分布情况是倒序的.即最后一个参数在列表中地址最高部分,第一个参数在列表地址的最低部分.参数在堆栈中的分布情况如下: 最后一个参数 倒数第二个参数 ... 第一个参数 函数返回地址 函数代码段  示例代码 void arg_test(int i, ...); int main(int argc,char *argv[])  { int int_size = _INTSIZEOF(int); printf("int_size=%d\n", int_size); arg_test(0, 4);

arg_cnt(4,1,2,3,4); return 0; } void arg_test(int i, ...) { int j=0;  va_list arg_ptr;

va_start(arg_ptr, i);  printf("&i = %p\n", &i);//打印参数i在堆栈中的地址 printf("arg_ptr = %p\n", arg_ptr); //打印va_start之后arg_ptr地址, //应该比参数i的地址高sizeof(int)个字节 //这时arg_ptr指向下一个参数的地址

j=*((int *)arg_ptr); printf("%d %d\n", i, j);  j=va_arg(arg_ptr, int);  printf("arg_ptr = %p\n", arg_ptr); //打印va_arg后arg_ptr的地址 //应该比调用va_arg前高sizeof(int)个字节 //这时arg_ptr指向下一个参数的地址 va_end(arg_ptr);  printf("%d %d\n", i, j);  }  代码说明: int int_size = _INTSIZEOF(int);得到int类型所占字节数 va_start(arg_ptr, i); 得到第一个可变参数地址,根据定义(va_list)&v得到起始参数的地址, 再加上_INTSIZEOF(v) ,就是其实参数下一个参数的地址,即第一个可变参数地址. j=va_arg(arg_ptr, int); 得到第一个参参数的值,并且arg_ptr指针上移一个_INTSIZEOF(int),即指向下一个可变参数的地址. va_end(arg_ptr);置空arg_ptr,即arg_ptr=0; 总结:读取可变参数的过程其实就是堆栈中,使用指针,遍历堆栈段中的参数列表,从低地址到高地址一个一个地把参数内容读出来的过程.  在编程中应该注意的问题和解决办法 虽然可以通过在堆栈中遍历参数列表来读出所有的可变参数,但是由于不知道可变参数有多少个,什么时候应该结束遍历,如果在堆栈中遍历太多,那么很可能读取一些无效的数据. 解决办法:a.可以在第一个起始参数中指定参数个数,那么就可以在循环还中读取所有的可变参数;b.定义一个结束标记,在调用函数的时候,在最后一个参数中传递这个标记,这样在遍历可变参数的时候,可以根据这个标记结束可变参数的遍历; 下面是一段示例代码: //第一个参数定义可选参数个数,用于循环取初参数内容 void arg_cnt(int cnt, ...); int main(int argc,char *argv[])  { int int_size = _INTSIZEOF(int); printf("int_size=%d\n", int_size); arg_cnt(4,1,2,3,4); return 0; } void arg_cnt(int cnt, ...) { int value=0;  int i=0; int arg_cnt=cnt;  va_list arg_ptr;  va_start(arg_ptr, cnt);  for(i = 0; i < cnt; i++) {    value = va_arg(arg_ptr,int);    printf("value%d=%d\n", i+1, value); } }

虽然可以根据上面两个办法解决读取参数个数的问题,但是如果参数类型都是不定的,该怎么办,如果不知道参数的类型,即使读到了参数也没有办法进行处理.解决办法:可以自定义一些可能出现的参数类型,这样在可变参数列表中,可以可变参数列表中的那类型,然后根据类型,读取可变参数值,并进行准确地转换.传递参数的时候可以这样传递:参数数目,可变参数类型1,可变参数值1,可变参数类型2,可变参数值2,.... 这里给出一个完整的例子: #include <stdio.h> #include <stdarg.h> const int INT_TYPE   = 100000; const int STR_TYPE   = 100001; const int CHAR_TYPE   = 100002; const int LONG_TYPE   = 100003; const int FLOAT_TYPE = 100004; const int DOUBLE_TYPE = 100005; //第一个参数定义可选参数个数,用于循环取初参数内容 //可变参数采用arg_type,arg_value...的形式传递,以处理不同的可变参数类型 void arg_type(int cnt, ...); //第一个参数定义可选参数个数,用于循环取初参数内容 void arg_cnt(int cnt, ...); //测试va_start,va_arg的使用方法,函数参数在堆栈中的地址分布情况 void arg_test(int i, ...); int main(int argc,char *argv[])  { int int_size = _INTSIZEOF(int); printf("int_size=%d\n", int_size); arg_test(0, 4);

arg_cnt(4,1,2,3,4); arg_type(2, INT_TYPE, 222, STR_TYPE, "ok,hello world!"); return 0; } void arg_test(int i, ...) { int j=0;  va_list arg_ptr;

va_start(arg_ptr, i);  printf("&i = %p\n", &i);//打印参数i在堆栈中的地址 printf("arg_ptr = %p\n", arg_ptr); //打印va_start之后arg_ptr地址, //应该比参数i的地址高sizeof(int)个字节 //这时arg_ptr指向下一个参数的地址

j=*((int *)arg_ptr); printf("%d %d\n", i, j);  j=va_arg(arg_ptr, int);  printf("arg_ptr = %p\n", arg_ptr); //打印va_arg后arg_ptr的地址 //应该比调用va_arg前高sizeof(int)个字节 //这时arg_ptr指向下一个参数的地址 va_end(arg_ptr);  printf("%d %d\n", i, j);  } void arg_cnt(int cnt, ...) { int value=0;  int i=0; int arg_cnt=cnt;  va_list arg_ptr;  va_start(arg_ptr, cnt);  for(i = 0; i < cnt; i++) {    value = va_arg(arg_ptr,int);    printf("value%d=%d\n", i+1, value); } } void arg_type(int cnt, ...) { int arg_type = 0; int int_value=0;  int i=0; int arg_cnt=cnt;  char *str_value = NULL; va_list arg_ptr;  va_start(arg_ptr, cnt);  for(i = 0; i < cnt; i++) {    arg_type = va_arg(arg_ptr,int);    switch(arg_type)    {    case INT_TYPE:     int_value = va_arg(arg_ptr,int);     printf("value%d=%d\n", i+1, int_value);     break;    case STR_TYPE:     str_value = va_arg(arg_ptr,char*);     printf("value%d=%d\n", i+1, str_value);     break;    default:     break;    } } }  =======================================================================

有关VA_LIST的用法:

VA_LIST 是在C语言中解决变参问题的一组宏

VA_LIST的用法:              (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针       (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。        (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。        (4)最后用VA_END宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用VA_ARG获取各个参数。

VA_LIST在编译器中的处理:

(1)在运行VA_START(ap,v)以后,ap指向第一个可变参数在堆栈的地址。 (2)VA_ARG()取得类型t的可变参数值,在这步操作中首先apt = sizeof(t类型),让ap指向下一个参数的地址。然后返回ap-sizeof(t类型)的t类型*指针,这正是第一个可变参数在堆栈里的地址。然后用*取得这个地址的内容。 (3)VA_END(),X86平台定义为ap = ((char*)0),使ap不再指向堆栈,而是跟NULL一样,有些直接定义为((void*)0),这样编译器不会为VA_END产生代码,例如gcc在Linux的X86平台就是这样定义的。

要注意的是:由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。

使用VA_LIST应该注意的问题:    (1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.     (2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。 小结:可变参数的函数原理其实很简单,而VA系列是以宏定义来定义的,实现跟堆栈相关。我们写一个可变函数的C函数时,有利也有弊,所以在不必要的 场合,我们无需用到可变参数,如果在C++里,我们应该利用C++多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。

==========================================================================

变长参数应用举例:

先得声明一个变长参数的变量va_list list 在使用前要先用va_start(list, last_param)对list进行初始化,last_param为最右边的已知参数,表示list 从last_param的下一个参数开始 va_arg(list, 类型) 最后不要忘了用va_end(list)

eg1: #include<iostream> #include<iomanip> #include<stdarg.h>

using namespace std;

double average(int, ...);

int main() {     double w = 37.5, x = 22.5, y = 1.7, z = 10.2;

cout << setiosflags(ios::fixed | ios::showpoint)         << setprecision(1) << "w = " << w << "\nx = " << x         << "\ny = " << y << "\nz = " << z << endl;

cout << average(2, w, x) << endl;     cout << average(3, w, x, y) << endl;     cout << average(4, w, x, y, z) << endl;

return 0; }

double average(int i, ...) {     double total = 0;     va_list ap;

va_start(ap, i);

for(int j = 1; j <= i; j++)     {         total += va_arg(ap, double);     }

va_end( ap );     return total/i; }

eg2: #include<iostream.h> #include <stdlib.h>  #include <stdarg.h> void error(const char*format...); void main() {     int a;     char c='d';     char s[100];     error("Enter a string:");      //输入一个字符串     cin>>s;     error("Enter an integer:");    //输入一整数     cin>>a;     error("%s\n%d\n%c\n",s,a,c);   //打印输出

} void error(const char*format...)    //实现像printf函数一样的打印输出功能 {     int i;     int j=0;     va_list ap;     va_start(ap,format);     for(i=0;*(format+i)!=0;)     {         int in;         char* pc;         char d;         if(*(format+i)=='%')         {             switch(*(format+i+1))             {             case'd':in=va_arg(ap,int);cout<<in;i=i+2;break;             case's':pc=va_arg(ap,char*);cout<<pc;i=i+2;break;             case'c':d=va_arg(ap,char);cout<<d;i=i+2;break;             default:cout<<'%';i=i+1;break;             }         }         else         {             cout<<*(format+i);             i++;         }

} }

================================================================

C++变长参数函数的用法

书上说,当无法列出传递函数的所有实参的类型和数目时,可用省略号指定参数表 (...)

如:void foo(...);      void foo(parm_list,...); void foo(...) {     //... } 调用:foo(a,b,c);

就是不懂,把a,b,c的值传进函数里面后,用什么变量来接收???如果不能接收,(...)岂不是没意义? 还有就是不明白 int printf(const char*...); printf("hello,&s\n",userName);

这个c的输出函数是怎么用(...)实现的.

首先函数体中声明一个va_list,然后用va_start函数来获取参数列表中的参数,使用完毕后调用va_end()结束。像这段代码:  void TestFun(char* pszDest, int DestLen, const char* pszFormat, ...)  {  va_list args;  va_start(args, pszFormat);  _vsnprintf(pszDest, DestLen, pszFormat, args);  va_end(args);  }

===========================================================

va_list的用法      还记得printf函数调用的时候那个“...”吗?就是可以输入任意的参数。现在你用va_list也可以实现类似的函数声明,printf就是这样做的。

va_list args;                                                 //声明变量  va_start(args, before);                               //开始解析。args指向before后面的参数  参数类型 var = va_arg(args, 参数类型);     //取下一个参数并返回。args指向下一个参数  va_end(args);

stdarg.h详解的更多相关文章

  1. vs2017自动生成的#include“stdafx.h”详解及解决方案

    vs2017自动生成的#include“stdafx.h”详解及解决方案 问题描述: 在高版本的Visual Studio的默认设置中,会出现这么一个现象,在新建项目之后,项目会自动生成#includ ...

  2. windows.h详解

    参考 http://blog.csdn.net/fengningning/article/details/2306650?locationNum=1&fps=1 windows.h解构 刚开头 ...

  3. 时间函数 time.h 详解

    C++对时间的操作也有许多值得大家注意的地方.最近,在技术群中有很多网友也多次问到过C++语言中对时间的操作.获取和显示等等的问题.下面,在这篇文章中,笔者将主要介绍在C/C++中时间和日期的使用方法 ...

  4. CGGeometry.h详解

     本文转载至:http://blog.csdn.net/chengyingzhilian/article/details/7894195 这些是在CGGeometry.h里的 CGPoint.CGSi ...

  5. 51单片机头文件reg51.h详解

    转自:http://www.51hei.com/mcu/2670.html 我们在用c语言编程时往往第一行就是头文件,51单片机为reg51.h或reg52.h,51单片机相对来说比较简单,头文件里面 ...

  6. A​n​d​r​o​i​d​ ​B​l​u​e​t​o​o​t​h​详​解(Android英文文档相关译文)

    一.Bluetooth Android平台包含了对Bluetooth协议栈的支持,允许机器通过Bluetooth设备进行无线数据交换.应用框架通过Android Bluetooth API访问Blue ...

  7. reg51.h 详解

    /* BYTE Register */ sfr P0 = 0x80; //P0口 sfr P1 = 0x90; //P1口 sfr P2 = 0xA0; //P2口 sfr P3 = 0xB0; // ...

  8. iOS学习——(转)NSObject详解

    本文主要转载自:ios开发 之 NSObject详解 NSObject是大部分Objective-C类继承体系的根类.这个类遵循NSObject协议,提供了一些通用的方法,对象通过继承NSObject ...

  9. ios开发之 NSObject详解

    NSObject是大部分Objective-C类继承体系的根类.这个类遵循NSObject协议,提供了一些通用的方法,对象通过继承NSObject,可以从其中继承访问运行时的接口,并让对象具备Obje ...

随机推荐

  1. bzoj3028食物

    http://www.lydsy.com/JudgeOnline/problem.php?id=3028 好吧,这是我第一道生成函数的题目. 先搞出各种食物的生成函数: 汉堡:$1+x^2+x^4+. ...

  2. G - Oil Skimming - hdu 4185(二分图匹配)

    题意:在大海里有一些石油 ‘#’表示石油, ‘.’表示水,有个人有一个工具可以回收这些石油,不过只能回收1*2大小的石油块,里面不能含有海水,要不就没办法使用了,求出来最多能回收多少块石油 分析:先把 ...

  3. java笔记15之this

    this:是当前类的对象引用,记为该类的一个对象 注意:谁调用这个方法,在这个方法内部的this就是代表谁 解决场景: 解决局部变量隐藏成员变量 class Student { private Str ...

  4. ubuntu14.04 制作U盘启动文件

    1.制作U盘启动文件 网上搜索:U盘安装Ubuntu 12.10 图文教程(ultraiso) http://www.jb51.net/os/94398.html 2. 重启,按Del(或F2)进BI ...

  5. 使用cocoapods导入第三方后 报错_OBJC_CLASS_$_XXX

    我们手动导入第三方库的时候,感觉管理不是很方便,于是会选择使用Cocoapods管理.现在记录一下使用心得,当使用cocoapods导入afnetworking或者其他框架的时候,发现调用的时候总是报 ...

  6. hibernate初涉

    好久都不曾写写总结一些东西了,惰性真的是令人难以克制!虽然和许多北漂族一样,艰苦而又迷茫,但是我总能找到一些方向,一点期盼,因为你就我的目标.我会坚持下去,重拾青春的热血,既然人生如戏,那我不当猪脚. ...

  7. Object -C NSValue -- 笔记

    // //  main.m //  NSValue // //  Created by facial on 26/8/15. //  Copyright (c) 2015 facial_huo. Al ...

  8. myeclipse快捷键收集整理

    Ctrl+1 快速修复(最经典的快捷键,就不用多说了) Ctrl+D: 删除当前行  Ctrl+Alt+↓ 复制当前行到下一行(复制增加) Ctrl+Alt+↑ 复制当前行到上一行(复制增加) Alt ...

  9. Meth | ubuntu下安装与卸载软件方法

    1.通过deb包安装的情况: 安装.deb包: 代码:sudo dpkg -i package_file.deb反安装.deb包:代码:sudo dpkg -r package_name 2.通过ap ...

  10. js判断访问者是否来自移动端代码

    <script type="text/javascript"> function is_mobile() { var regex_match = /(nokia|iph ...