zoukankan      html  css  js  c++  java
  • typedef 类型重命名 和 #define 宏定义(1)

    http://www.blogjava.net/jasmine214--love/archive/2010/11/29/339307.html

    在现实生活中,信息的概念可能是长度,数量和面积等。在C语言中,信息被抽象为int、float和double等基本数据类型。从基本数据类型名称上, 不能够看出其所代表的物理属性,并且int、float和double为系统关键字,不可以修改。为了解决用户自定义数据类型名称的需求,C语言中引入类 型重定义语句typedef,可以为数据类型定义新的类型名称,从而丰富数据类型所包含的属性信息

    typedef的语法描述typedef 类型名称 类型标识符;   例如:typedef double LENGTH;  typedef unsigned int COUNT;
    typedef 的主要应用有如下的几种形式
    1) 为基本数据类型定义新的类型名。例如:
    typedef unsigned int COUNT;
    typedef double AREA;
    此种应用的主要目的,首先是丰富数据类型中包含的属 性信息,其次是为了系统移植的需要,稍后详细描述。
    2) 为自定义数据类型(结构体、公用体和枚举类型)定义简洁的类型名称(在c++中没有这个必要了,因为直接可以使用类型名定义变量,前面不用加struct)。例如:
    struct Point
    {
    double x;
    double y;
    double z;
    };
    struct Point oPoint1={100,100,0};
    struct Point oPoint2;
    其中结构体struct Point为新的数据类型,在定义变量的时候均要有保留字struct,而不能像int和double那样直接使用Point来定义变量。如果经过如下的 修改,
    typedef struct tagPoint
    {
    double x;
    double y;
    double z;
    } Point;
    定义变量的方法可以简化为
    Point oPoint;
    由于定义结构体类型有多种形式,因此可以修改 如下:
    typedef struct 
    {
    double x;
    double y;
    double z;
    } Point;
    3) 为数组定义简洁的类型名称。例如,定义三个长度为5的整型数组,
    int a[10],b[10],c[10],d[10];
    在C语言中,可以将长度为10的整型数组看作为一个新的数据类型,再利用typedef为其重 定义一个新的名称,可以更加简洁形式定义此种类型的变量,具体的处理方式如下:
    typedef int INT_ARRAY_10[10];
    typedef int INT_ARRAY_20[20];
    INT_ARRAY_10 a,b,c,d;
    INT_ARRAY_20 e;
    其中 INT_ARRAY_10和INT_ARRAY_20为新的类型名,10 和20 为数组的长度。a,b,c,d均是长度为10的整型数组,e是长度为20的整型数组。
    4) 为指针定义简洁的名称。首先为数据指针定义新的名称,例如
    typedef char * STRING;
    STRING csName={“Jhon”};
    其次,可以为函数指针定义新的名称,例如
    typedef int (*MyFUN)(int a,int b);
    int Max(int a,int b);
    MyFUN *pMyFun;
    pMyFun= Max;
    pMyFun(2,3);

    在使用typedef时,应当注意如下的问题:
    1) typedef的目的是为已知数据类型增加一个新的名称。因此并没有引入新的数据类型。
    2) typedef 只适于类型名称定义,不适合变量的定义。
    3) typedef 与#define具有相似的之处,但是实质不同
    提示 #define AREA double 与 typedef double AREA 可以达到相同的效果。但是其实质不同, #define为预编译处理命令,主要定义常量,此常量可以为任何的字符及其组合,在编译之前,将此常量出现的所有位置,用其代表的字符或字符组合无条件 的替换,然后进行编译。typedef是为已知数据类型增加一个新名称,其原理与使用int double等保留字一致。

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

    接下来聊聊宏 http://www.360doc.com/content/13/0125/13/10906019_262310086.shtml

    1. 简单宏定义

     [#define指令(简单的宏)]  #define  标识符   替换列表

     

    常见错误:

    1. #define N = 100       /*** WRONG ***/  
    2. int a[N];            /* 会成为 int a[= 100]; */  
    1. #define N 100      /*** WRONG ***/  
    2. int a[N];            /*    become int a[100;]; */  

     

     

    简单的宏主要用来定义那些被Kernighan和Ritchie称为“明示常量”(manifest constant)的东西。使用宏,我们可以给数值、字符和字符串命名
    1
    #define STE_LEN 80 2 #define TRUE 1 3 #define FALSE 0 4 #define PI 3.14159 5 #define CR ' ' 6 #define EOS ''

    使用#define来为常量命名有许多显著的优点:

    1) 、 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的“魔法数”,使读者难以理解。

    2) 、 程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。“硬编码的”常量会更难于修改,特别是有时候当他们以稍微不同的形式出现时。(例如,如果一个程序包含一个长度为100的数组,它可能会包含一个从0到99的循环。如果我们只是试图找到所有程序中出现的100,那么就会漏掉99。)

    3) 、可以帮助避免前后不一致或键盘输入错误。假如数值常量3.14159在程序中大量出现,它可能会被意外地写成3.1416或3.14195。

    虽然简单的宏常用于定义常量名,但是它们还有其他应用。

    4) 、可以对C语法做小的修改。实际上,我们可以通过定义宏的方式给C语言符号添加别名,从而改变C语言的语法。例如,对于习惯使用Pascal的begin和end(而不是C语言的{和})的程序员,可以定义下面的宏:

    #define BEGIN  {  

    #define END    }  

    我们甚至可以发明自己的语言。例如,我们可以创建一个LOOP“语句”,来实现一个无限循环:#define LOOP   for (;;)

    当然,改变C语言的语法通常不是个好主意,因为它会使程序很难被其他程序员所理解。

    5) 、对类型重命名。我们通过重命名int创建了一个Boolean类型:#define BOOL int (虽然有些程序员会使用宏定义的方式来实现此目的,但类型定义仍然是定义新类型的最佳方法。)

    6) 、控制条件编译。例如,在程序中出现的宏定义可能表明需要将程序在“调试模式”下进行编译,来使用额外的语句输出调试信息:#define DEBUG(这里顺便提一下,如前面的例子所示,宏定义中的替换

    2. 带参数的宏

    [#define指令—带参数的宏]  #define 标识符(x1x2,…,xn) 替换列表    其中x1x2,…,xn是标识符(宏的参数)。这些参数可以在替换列表中根据需要出现任意次。

     

    在宏的名字和左括号之间必须没有空格。如果有空格,预处理器会认为是在定义一个简单的宏,其中(x1,x2,…,xn)是替换列表的一部分。

    例如,假定我们定义了如下的宏:

    #define MAX(x,y)    ((x)>(y) ? (x) :(y))

    #define IS_EVEN(n)   ((n)%2==0)  

    现在如果后面的程序中有如下语句:

    i = MAX(j+k, m-n);  

    if (IS_EVEN(i)) i++;  

    预处理器会将这些行替换为

    1. i = ((j+k)>(m-n)?(j+k):(m-n));  
    2. if (((i)%2==0)) i++;  

    下面的例子是一个更复杂的宏: #define TOUPPER(c)('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c))  这个宏检测一个字符c是否在'a'与'z'之间。如果在的话,这个宏会用'c'减去'a'再加上'A',来计算出c所对应的大写字母。如果c不在这个范围,就保留原来的c。像这样的字符处理的宏非常有用,所以C语言库在<ctype.h>(23.4节)中提供了大量的类似的宏。其中之一就是toupper,与我们上面的TOUPPER例子作用一致(但会更高效,可移植性也更好)。

     

    带参数的宏可以包含空的参数列表,如例如:#define getchar() getc(stdin)  [空的参数列表不是一定确实需要,但可以使getchar更像一个函数。(没错,这就是<stdio.h>中的getchar,getchar的确就是个宏,不是函数——虽然它的功能像个函数。)]

              

    使用带参数的宏替代实际的函数的优点

    1) 、  程序可能会稍微快些。一个函数调用在执行时通常会有些额外开销——存储上下文信息、复制参数的值等。而一个宏的调用则没有这些运行开销。

    2) 、 宏会更“通用”。与函数的参数不同,宏的参数没有类型。因此,只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用MAX宏从两个数中选出较大的一个,数的类型可以是int,long int,float,double等等。

    但是带参数的宏也有一些缺点。

    1) 、 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表,由此导致程序的源代码增加(因此编译后的代码变大)。宏使用得越频繁,这种效果就越明显。当宏调用嵌套时,这个问题会相互叠加从而使程序更加复杂。思考一下,如果我们用MAX宏来找出3个数中最大的数会怎样?n = MAX(i, MAX(j,k));  ----------------->n=((i)>(((j)>(k)?(j):(k)))?(i):(((j)>(k)?(j):(k))));  

     

    2) 、宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确认它们是否是正确的类型。如果不是,或者将参数转换成正确的类型,或者由编译器产生一个出错信息。预处理器不会检查宏参数的类型,也不会进行类型转换。

    3) 、无法用一个指针来指向一个宏。C语言允许指针指向函数。这一概念在特定的编程条件下非常有用。宏会在预处理过程中被删除,所以不存在类似的“指向宏的指针”。因此,宏不能用于处理这些情况。

    4) 、宏可能会不止一次地计算它的参数。函数对它的参数只会计算一次,而宏可能会计算两次甚至更多次。如果参数有副作用,多次计算参数的值可能会产生意外的结果。考虑下面的例子,其中MAX的一个参数有副作用:n = MAX(i++, j);  ---------------->n =((i++)>(j)?(i++):(j));  (如果i大于j,那么i可能会被(错误地)增加了两次,同时n可能被赋予了错误的值。)

    带参数的宏不仅适用于模拟函数调用。他们特别经常被作为模板,来处理我们经常要重复书写的代码段。如果我们已经写烦了语句   printf("%d" , x);  因为每次要显示一个整数x都要使用它。我们可以定义下面的宏,使显示整数变得简单些:  #define PRINT_INT(x)    printf("%d ", x)  

    3. #运算符 -----> 宏定义可以包含两个运算符:#和##编译器不会识别这两种运算符相反,它们会在预处理时被执行

    #运算符将一个宏的参数转换为字符串字面量, 简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号. 它仅允许出现在带参数的宏的替换列表中)用比较官方的话说就是将语言符号(Token)转化为字符串。

          例如: #define PRINT_INT(x) printf(#x " = %d ", x)   ----调用---> PRINT_INT(i/j);  ----->printf("i/j" " = %d ", i/j);  //x是i/j  所以#x会替换成 "i/j"[在C语言中相邻的字符串字面量会被合并,因此上边的语句等价于:printf("i/j = %d ", i/j); ] 

    //把一个非字符串的东西转换成字符串
    1 #define STR(x) #x
    2 3 int main(int argc char** argv) 4 { 5 printf("%s ", STR(It's a long string)); // 输出 It's a long str 6 return 0; 7 }

    4. ##运算符

             在C语言的宏中,"##"被称为 连接符(concatenator),它是一种预处理运算符, 用来把两个语言符号(Token)组合成单个语言符号。 这里的语言符号不一定是宏的变量。并且双井号不能作为第一个或最后一个元素存在.

    ##运算符可以将两个记号(例如标识符)“粘”在一起,成为一个记号。(无需惊讶,##运算符被称为“记号粘合”。)如果其中一个操作数是宏参数,“粘合”会在当形式参数被相应的实际参数替换后发生。考虑下面的宏:

    如下例子:当MK_ID被调用时(比如MK_ID(1)),预处理器首先用自变量(这个例子中是1)替换参数n。接着,预处理器将i和1连接成为一个记号(i1)。下面的声明使用MK_ID创建了3个标识符:

    #define MK_ID(n) i##n  
    int MK_ID(1), MK_ID(2), MK_ID(3); ---------->int i1, i2, i3;  

            ##运算符不属于预处理器经常使用的特性。实际上,想找到一些使用它的情况是比较困难的。一个比较有意义的用法是用到函数重载上面

            定义一个宏,并使它展开后成为max函数的定义。宏会有唯一的参数type,它表示形式参数和返回值的类型。这里还有个问题,如果我们是用宏来创建多个max函数,程序将无法编译。(C语言不允许在同一文件中出现两个同名的函数。)为了解决这个问题,我们是用##运算符为每个版本的max函数构造不同的名字。下面的例子:请注意宏的定义中是如何将type和_max相连来形成新函数名的。假如我们需要一个针对float值的max函数。(c++不会有这个问题吧,因为c++支持重载??)

    1. #define GENERIC_MAX (type)             
    2. type type##_max(type x,  type y)      
    3. {                                        
    4.   return x > y ? x :y;                
    5. }  
    6. GENERIC_MAX(float)  ---------->float float_max(float x, float y) { return x > y ? x :y; }

    再如:

    1. #define PHP_FUNCTION            ZEND_FUNCTION  
    2. #define ZEND_FUNCTION(name)             ZEND_NAMED_FUNCTION(ZEND_FN(name))  
    3. #define ZEND_FN(name) zif_##name  
    4. #define ZEND_NAMED_FUNCTION(name)       void name(INTERNAL_FUNCTION_PARAMETERS)  
    5. #define INTERNAL_FUNCTION_PARAMETERS int ht, double b, float c,   
    6. char t, long l
    7.    
    8. PHP_FUNCTION(count); -----------> void zif_count(int ht, double b, float c, char t, long l)  (说明:PHP_FUNCTION(count);-------->ZEND_FUNCTION(count)---->ZEND_NAMED_FUNCTION(zif_count)---->void zif_count(INTERNAL_FUNCTION_pARAMETERS)------>void zif_count(int ht, double b, float c, char t, long l))

    宏ZEND_FN(name)中有一个"##",它的作用一如之前所说,是一个连接符,将zif和宏的变量name的值连接起来。 以这种连接的方式以基础,多次使用这种宏形式,可以将它当作一个代码生成器,这样可以在一定程度上减少代码密度, 我们也可以将它理解为一种代码重用的手段,间接地减少不小心所造成的错误。

    5. 宏的通用属性

    现在我们已经讨论过简单的宏和带参数的宏了,我们来看一下它们都需要遵守的规则。
    1) 、宏的替换列表可以包含对另一个宏的调用。例如,我们可以用宏PI来定义宏TWO_PI:
    #define PI      3.14159
    #define TWO_PI  (2*PI)
    当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换掉为止。

    2) 、预处理器只会替换完整的记号,而不会替换记号的片断。因此,预处理器会忽略嵌在标识符名、字符常量、字符串字面量之中的宏名。例如,假设程序含有如下代码行:

    标识符BUFFER_ZISE和字符串"Error:SIZE exceeded"没有被预处理影响,虽然它们都包含SIZE


    • #define SIZE 256  
    • int BUFFER_SIZE;  
    • if (BUFFER_SIZE> SIZE)  
    •     puts("Error : SIZEexceeded"); 
    1. int BUFFER_SIZE;  
    2. if (BUFFER_SIZE> 256)  
    3.   puts("Error :SIZEexceeded");  

    3) 、一个宏定义的作用范围通常到出现这个宏的文件末尾

    4) 、宏不可以被定义两遍,除非新的定义与旧的定义是一样的。小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。

    5) 、宏可以使用#undef指令“取消定义”。#undef指令有如下形式:[#undef指令]  #undef  标识符 
    其中标识符是一个宏名。例如,指令#undef N  会删除宏N当前的定义。(如果N没有被定义成一个宏,#undef指令没有任何作用。)#undef指令的一个用途是取消一个宏的现有定义,以便于重新给出新的定义。

     6. 宏定义中圆括号------------>在我们前面定义的宏的替换列表中有大量的圆括号。确实需要它们吗?答案是绝对需要。

    对于在一个宏定义中哪里要加圆括号有两条规则要遵守:

    首先,如果宏的替换列表中有运算符,那么始终要将替换列表放在括号中:#define TWO_PI (2*3.14159)

    其次,如果宏有参数,每次参数在替换列表中出现时都要放在圆括号中:#define SCALE(x) ((x)*10)

    为了展示为替换列表添加圆括号的重要性,考虑下面的宏定义,其中的替换列表没有添加圆括号:
    #define TWO_PI 2*3.14159  :conver_factor = 360/TWO_PI;---->conver_factor = 360/2*3.14159; 而不是想要的conver_factor = 360/(2*3.14159)
    #define SCALE(x) (x*10)   /* 需要给x添加括号 */ : j = SCALE(i+1); ----->j = (i+1*10); 而不是想要的 j = ( (i+1)*10 )

    7. 创建较长的宏

     1. 较长的宏中的逗号运算符

          在创建较长的宏时,逗号运算符会十分有用。特别是可以使用逗号运算符来使替换列表包含一系列表达式。例如,下面的宏会读入一个字符串,再把字符串显示出来:

    #define ECHO(s) (get(s), puts(s))

          gets函数和puts函数的调用都是表达式,因此使用逗号运算符连接它们是合法的。我们甚至可以把ECHO宏当作一个函数来使用:
    例如 ECHO(str);   /* 替换为 (gets(str), puts(str)); */
    除了使用逗号运算符,我们也许还可以将gets函数和puts函数的调用放在大括号中形成复合语句:#define ECHO(s)  { gets(s);  puts(s);  }
    #define ECHO(s)  { gets(s);  puts(s);  }
    遗憾的是,这种方式并不奏效。假如我们将ECHO宏用于下面的if语句:

    1. if (echo_flag)  
    2.   ECHO(str);  
    3. else  
    4.   gets(str);  
    5. //将ECHO宏替换会得到下面的结果:  
    6. if (echo_flag)  
    7.   { gets(str); puts(str);  };  
    8. else  
    9.   gets(str);  

    2. 宏定义中的do-while循环do 

    do循环必须始终随跟着一个分号,因此我们不会遇到在if语句中使用宏那样的问题了。为了看到这个技巧(嗯,应该说是技术)的实际作用,让我们将它用于ECHO宏中:

    1. #define ECHO(s)         
    2.       do{             
    3.            gets (s) ;        
    4.            puts (s) ;        
    5.       } while  (0)  

    当使用ECHO宏时,一定要加分号:ECHO(str);--------->do {  gets(str); puts(str); } while (0);

    为什么在宏定义时需要使用do-while语句呢? 我们知道do-while循环语句是先执行循环体再判断条件是否成立, 所以说至少会执行一次。当使用do{ }while(0)时由于条件肯定为false,代码也肯定只执行一次, 肯定只执行一次的代码为什么要放在do-while语句里呢? 这种方式适用于宏定义中存在多语句的情况。

    1. #define TEST(a, b)  a++;b++;  
    2.    
    3. if (expr)  
    4.     TEST(a, b);  
    5. else  
    6.     do_else(); 
    1. if (expr)  
    2.     a++;b++;  
    3. else  
    4.     do_else();  

    这样if-else的结构就被破坏了if后面有两个语句,这样是无法编译通过的,那为什么非要do-while而不是简单的用{}括起来呢。 这样也能保证if后面只有一个语句。例如上面的例子,在调用宏TEST的时候后面加了一个分号, 虽然这个分号可有可无, 但是出于习惯我们一般都会写上。 那如果是把宏里的代码用{}括起来,加上最后的那个分号。 还是不能通过编译。 所以一般的多表达式宏定义中都采用do-while(0)的方式。

    3. "空操作"的定义

          使用如下的方式来定义“空操作”,例如很常见的Debug日志打印宏:在编译时如果定义了DEBUG则将LOG_MSG当做printf使用,而不需要调试,正式发布时则将LOG_MSG()宏定义为空, 由于宏是在预编译阶段进行处理的,所以上面的宏相当于从代码中删除了。

    1. #ifdef DEBUG  
    2. #   define LOG_MSG printf  
    3. #else  
    4. #   define LOG_MSG(...)  
    5. #endif  

     

    9. C语言中常用的宏

     : 得到指定地址上的一个字节或字

    #define  MEM_B(x) (*((byte *)(x)))
    #define  MEM_W(x) (*((word *)(x)))

    : 得到一个field在结构体(struct)中的偏移量

    #define FPOS(type,field) ((dword)&((type *)0)->field)

    : 得到一个结构体中field所占用的字节数
    #define FSIZ(type,field) sizeof(((type *)0)->field)

    : 按照LSB格式把两个字节转化为一个Word

    #define FLIPW(ray) ((((word)(ray)[0]) * 256) + (ray)[1])

    : 按照LSB格式把一个Word转化为两个字节
    #define FLOPW(ray,val) (ray)[0] = ((val)/256); (ray)[1] = ((val) & 0xFF)

    : 得到一个变量的地址(word宽度)

    #define B_PTR(var)  ((byte *) (void *) &(var))
    #define W_PTR(var)  ((word *) (void *) &(var))

    : 得到一个字的高位和低位字节
    #define WORD_LO(xxx)  ((byte) ((word)(xxx) & 255))
    #define WORD_HI(xxx)  ((byte) ((word)(xxx) >> 8))

    : 返回一个比X大的最接近的8的倍数
    #define RND8(x) ((((x) + 7)/8) * 8

    : 将一个字母转换为大写

    #define UPCASE(c) (((c)>='a' && (c) <= 'z') ? ((c) – 0×20) : (c))

    : 判断字符是不是10进值的数字

    #define  DECCHK(c) ((c)>='0' && (c)<='9')

    : 判断字符是不是16进值的数字

    #define HEXCHK(c) (((c) >= '0' && (c)<='9') ((c)>='A' && (c)<= 'F')
    ((c)>='a' && (c)<='f'))

    : 防止溢出的一个方法
    #define INC_SAT(val) (val=((val)+1>(val)) ? (val)+1 : (val))

    : 返回数组元素的个数
    #define ARR_SIZE(a)  (sizeof((a))/sizeof((a[0])))

    : 返回一个无符号数n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)
    #define MOD_BY_POWER_OF_TWO( val, mod_by ) ((dword)(val) & (dword)((mod_by)-1))

    18: 对于IO空间映射在存储空间的结构,输入输出处理
    #define inp(port) (*((volatile byte *)(port)))
    #define inpw(port) (*((volatile word *)(port)))
    #define inpdw(port) (*((volatile dword *)(port)))
    #define outp(port,val) (*((volatile byte *)(port))=((byte)(val)))
    #define outpw(port, val) (*((volatile word *)(port))=((word)(val)))
    #define outpdw(port, val) (*((volatile dword *)(port))=((dword)(val)))

    19: 使用一些宏跟踪调试

    可以定义宏,例如:当定义了_DEBUG,输出数据信息和所在文件所在行
    #ifdef _DEBUG
    #define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)
    #else
    #define DEBUGMSG(msg,date)
    #endif
    20: 宏定义防止错误使用小括号包含。
    例如:
    有问题的定义:#define DUMP_WRITE(addr,nr) {memcpy(bufp,addr,nr); bufp += nr;}
    应该使用的定义: #difne DO(a,b) do{a+b;a++;}while(0)
    例如:

    1. if(addr)  
    2.     DUMP_WRITE(addr,nr);  
    3. else  
    4.     do_somethong_else();  
    5. //宏展开以后变成这样:  
    6. if(addr)  
    7.     {memcpy(bufp,addr,nr); bufp += nr;};  
    8. else  
    9.     do_something_else();  


    gcc 在碰到else前面的“;”时就认为if语句已经结束,因而后面的else不在if语句中。而采用do{} while(0)的定义,在任何情况下都没有问题。而改为 #difne DO(a,b) do{a+b;a++;}while(0) 的定义则在任何情况下都不会出错。

  • 相关阅读:
    通过Xshell连接CentOS虚拟机
    Linux学习笔记
    JAVA学习摘要
    4k 对齐,你准备好了吗?
    图片种子制作方法,利用图片上传附件
    利用京东服务免费打造属于自己的网站
    PE制作实录 —— 补充说明
    PE制作实录 —— 定义我的 PE 工具箱
    浏览器 — 各项基准测试
    Windows 8.1 归档 —— Step 3 软件的选择与安装
  • 原文地址:https://www.cnblogs.com/silentNight/p/5430750.html
Copyright © 2011-2022 走看看