C编程笔记(二)

12 篇文章 0 订阅

一、回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

回调函数的作用:

在回调中,主程序把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,丝毫不需要修改库函数的实现,这就是解耦。主函数和回调函数是在同一层的,而库函数在另外一层,如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常调用库函数常见的情况。

typedef struct list
{
  void *value_address;
  struct list *next; 
}NODE;

void print_node(NODE *pNode)
{
  printf(" address = %p, value = %d\n", pNode, *((int *)pNode->value_address));
}

int int_compare(void const *a, void const *b)  //定义回调函数,参数类型为void
{  
  if (*(int *)a == *(int *)b)
  {
    return 0;
  }
  else
  {
    return -1;
  }
}

NODE *Search_list(NODE *node, int (*compare)(void const *,void const *), void const *desire_value)
{
  while(node != NULL)
  {
      if(compare((node->value_address),desire_value) == 0)   //回调函数,参数类型为void
      {
         break;
      }
     node = node->next;
  }
  return node;
}
NODE *list_head = NULL, *list_tail = NULL;
int g_list_num = 100;
void init_list()
{
  list_head = (NODE *)malloc(sizeof(NODE));
  list_head->value_address = (int *)malloc(sizeof(int));
  *((int *)list_head->value_address) = 0;
  list_head->next = NULL;
  list_tail = list_head;
  printf(" list_head ");
  print_node(list_head);
  return;
}

void create_list()
{
  int idx;
  NODE *node_tmp;
  for (idx=0; idx< g_list_num; idx++)
  {
     
     node_tmp= (NODE *)malloc(sizeof(NODE));
     list_tail->next = node_tmp;
     list_tail = node_tmp;
     list_tail->value_address = (int *)malloc(sizeof(int));
     *((int *)list_tail->value_address) = idx;
     list_tail->next = NULL;
     *((int *)list_head->value_address)+=1; 
      
  }
  printf(" list_head ");
  print_node(list_head);
  printf(" list_tail ");
  print_node(list_tail);
  return;
}

void destroy_list()
{

  NODE *node_next, *node_current;
  node_current = list_head;
  node_next = list_head->next;

  while (node_next != NULL)
  {
     
     free(node_current->value_address);
     free(node_current);
      node_current = node_next;
     node_next = node_current->next;
      
  }
  free(node_current->value_address);
  free(node_current);

  return;
}

int test_list()
{
  int val = 10;
  init_list();
  create_list();
  NODE *pNode;
  pNode = Search_list(list_head, int_compare, &val);
  if(pNode != NULL)
  {
    print_node(pNode);
  }

  destroy_list();
}

 list_head  address = 0x1fa8040, value = 0
 list_head  address = 0x1fa8040, value = 100
 list_tail  address = 0x1fa9940, value = 99
 address = 0x1fa8300, value = 10

二、可变参数函数

C 语言允许定义参数数量可变的函数,这称为可变参数函数(variadic function)。这种函数需要固定数量的强制参数(mandatory argument),后面是数量可变的可选参数(optional argument)。这种函数必须至少有一个强制参数。可选参数的类型可以变化。可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定。最后一个强制参数将传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。
C 语言中最常用的可变参数函数例子是 printf()和 scanf()。这两个函数都有一个强制参数,即格式化字符串。格式化字符串中的转换修饰符决定了可选参数的数量和类型。
对于每一个强制参数来说,函数头部都会显示一个适当的参数,像普通函数声明一样。参数列表的格式是强制性参数在前,后面跟着一个逗号和省略号(...),这个省略号代表可选参数。

可变参数函数要获取可选参数时,必须通过一个类型为 va_list 的对象,它包含了参数信息。这种类型的对象也称为参数指针(argument pointer),它包含了栈中至少一个参数的位置。可以使用这个参数指针从一个可选参数移动到下一个可选参数,由此,函数就可以获取所有的可选参数。va_list 类型被定义在头文件 stdarg.h 中。
当编写支持参数数量可变的函数时,必须用 va_list 类型定义参数指针,以获取可选参数。在下面的讨论中,va_list 对象被命名为 argptr。可以用 4 个宏来处理该参数指针,这些宏都定义在头文件 stdarg.h 中:

void va_start(va_list argptr, lastparam); 

宏 va_start 使用第一个可选参数的位置来初始化 argptr 参数指针。该宏的第二个参数必须是该函数最后一个有名称参数的名称。必须先调用该宏,才可以开始使用可选参数。

type va_arg(va_list argptr, type);

展开宏 va_arg 会得到当前 argptr 所引用的可选参数,也会将 argptr 移动到列表中的下一个参数。宏 va_arg 的第二个参数是刚刚被读入的参数的类型。

void va_end(va_list argptr);

当不再需要使用参数指针时,必须调用宏 va_end。如果想使用宏 va_start 或者宏 va_copy 来重新初始化一个之前用过的参数指针,也必须先调用宏 va_end。

void va_copy(va_list dest, va_list src)

宏 va_copy 使用当前的 src 值来初始化参数指针 dest。然后就可以使用 dest 中的备份获取可选参数列表,从 src 所引用的位置开始。

使用步骤如下:

  • 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
  • 在函数定义中创建一个 va_list 类型变量,这个变量是存储参数地址的指针,因为得到参数的地址后,再结合参数的类型,才能得到参数的值。
  • 使用 va_start 宏来初始化 va_list 变量为一个参数列表。这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数。
  • 使用 va_arg 宏和 va_list 变量来访问参数列表中的每个项。va_arg 宏可以使va_list 变量依次返回每个参数的地址,得到这个地址之后,结合参数的类型就可以得到参数的值。
  • 设定可变参数约束条件。注意函数调用时是不知道可变参数的正确数目的,必须在代码中指明结束条件,可通过强制参数来传递可变参数数量,也可以在可变参数中用特殊值标记可变参数结束。
  • 使用宏 va_end 来清理赋予 va_list 变量的内存。

根据Microsoft Visual Studio 10.0\VC\include

// stdarg.h

#define va_start _crt_va_start

#define va_arg _crt_va_arg

#define va_end _crt_va_end

// vadefs.h

typedef char *  va_list;

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )

#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

#define _crt_va_end(ap)      ( ap = (va_list)0 )

#define _ADDRESSOF(v)   ( &(v) )

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

 

1、可变长参数传递

如果需要编写一个函数,将它的可变长参数直接传递给另外的函数,目前尚无办法直接做到,但可以迂回前进。

首先,定义被调用函数的参数类型为va_list,然后在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。

 

2、可变长参数中类型为函数指针

通过va_arg直接提取出可变长参数中类型为函数指针的参数,结果却总是不正确,这是与va_arg的实现有关。如果想从可变参数列表中提取出函数指针类型的参数,例如int (*)(),则va_arg(argp,int (*)())被扩展为( *( int (*)() *)(( argp += _INTSIZEOF(int (*)())) - _INTSIZEOF(int (*)())) )

(int (*)() *)是无意义的。解决这个问题的办法是将函数指针用typedef定义为一个独立的数据类型。例如:

typedef int (*funcptr)();

这时候再调用va_arg(argp, funcptr)将被扩展为( *( funcptr *)(( argp += _INTSIZEOF(funcptr)) - _INTSIZEOF(funcptr)) ),这样就可以通过编译检查了。

 

3、可变长参数的获取:

有一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:

va_arg(argp, float);

这样做不可以。在可变长参数中,应用的是“加宽”原则,也就是float类型被扩展为double,char short被扩展为int。因此如果要取可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double),对于char和short类型则用va_arg(argp, int);

int test1()
{
  
  printf("test1 : \n");
  
}

int test2()
{
  
  printf("test2 : \n");
  
}

int test1()
{
  
  printf("test3 : \n");
  
}

va_printf(int num, va_list argp)  //可变参数传递
{ 
  int idx;
  float tmp;
  printf("sum of: ");
  for(idx = 0; idx<num ; idx++)
  {
/* 
如果写为tmp = va_arg(argp, float);则编译时提示如下,不能正常执行。
警告:通过‘...’传递时‘float’被提升为‘double’ [默认启用]
     tmp = va_arg(argp, float);
                        ^
附注:(因此您应该向‘va_arg’传递‘double’而不是‘float’)
附注:如果执行到这段代码,程序将中止
*/
    tmp = va_arg(argp, double);
    printf("%f, ",tmp); 
  }
  printf(" is:\n");
  
}


va_float_test(int num, ...)
{
  va_list argp;
  va_start(argp, num);
  int idx;
  float sum = 0;

  for(idx = 0; idx<num ; idx++)
  {
    sum += va_arg(argp, double);
  }
  va_start(argp, num);  //再次va_start使argp重新指向第一个可选参数
  va_printf(num, argp);
  printf("%f\n", sum);
  va_end(argp);
  
}

typedef int (*pIntFun)();
va_pfun_test(int num, ...)   //可变长参数中类型为函数指针
{
  va_list argp;
  va_start(argp, num);
  int idx;
  pIntFun pfun;

  for(idx = 0; idx<num ; idx++)
  {
    pfun = va_arg(argp, pIntFun);
    pfun();
  }
 va_end(argp);
}

int test_va()
{
  
  float a = 1.3, b = 3331.134, c = 324.1340;
  float sum = a + b+ c;
  va_pfun_test(3, test1,test2,test3);
  va_float_test(3,a,b,c);
  printf("true value is %f \n", sum);

} 

test1 : 
test2 : 
test3 : 
sum of: 1.300000, 3331.134033, 324.134003,  is:
3656.568115
true value is 3656.568115 

 

三、宏定义

1、宏定义简介:

1)宏定义是用宏名字来表示一个字符串,在宏展开时又以该字符串取代宏名字,这只是一种简单的代换。字符串中可以包含任何字符,可以是常数,也可以是表达式。预处理程序对它不做任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。

2)宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域,可以使用#undef命令。

3)宏定义不是说明语句,在行末不必加分号。如加上分号,则连分号也一起置换。

4)宏名在源程序中若用引号括起来,则预处理程序不对其作宏替换。

5)宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预处理程序层层替换。

 

C语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数,这点和函数有些类似。对带参数的宏,宏展开和用实参替代形参,发生在预处理阶段。

2、常见问题:

1. 算符优先级问题

不仅宏体是纯文本替换,宏参数也是纯文本替换。有以下一段简单的宏,实现乘法:

#define MULTIPLY(x, y) x*y

MULTIPLY(1, 2)没问题,会正常展开成1 * 2。有问题的是这种表达式MULTIPLY(1+2, 3),展开后成了1+2 * 3,显然优先级错了。

在宏体中,给引用的参数加个括号就能避免这问题。

#define MULTIPLY(x, y) ((x)*(y))

MULTIPLY(1+2, 3)就会被展开成(1+2) * (3),优先级正常了。

其实这个问题和下面要说到的某些问题都属于由于纯文本替换而导致的语义破坏问题,要格外小心。

2. 分号吞噬问题

有如下宏定义:

#define SKIP_SPACES(p, limit)  \
     { char *lim = (limit);         \
       while (p < lim) {            \
         if (*p++ != ' ') {         \
           p--; break; }}}

假设有如下一段代码:

if (*p != 0)
   SKIP_SPACES (p, lim);
else ...

一编译,GCC报error: ‘else’ without a previous ‘if’。原来这个看似是一个函数的宏被展开后是一段大括号括起来的代码块,加上分号之后这个if逻辑块就结束了,所以编译器发现这个else没有对应的if。

这个问题一般用do ... while(0)的形式来解决:

#define SKIP_SPACES(p, limit)     \
     do { char *lim = (limit);         \
          while (p < lim) {            \
            if (*p++ != ' ') {         \
              p--; break; }}}          \
     while (0)

展开后就成了

if (*p != 0)
    do ... while(0);
else ...

这样就消除了分号吞噬问题。

这个技巧在Linux内核源码里很常见,比如这个置位宏#define SET_REG_BIT(reg, bit) do { (reg |= (1 << (bit))); } while (0)(位于arch/mips/include/asm/mach-pnx833x/gpio.h)

3. 宏参数重复调用

有如下宏定义:

#define min(X, Y)  ((X) < (Y) ? (X) : (Y))

当有如下调用时next = min (x + y, foo (z));,宏体被展开成next = ((x + y) < (foo (z)) ? (x + y) : (foo (z)));,可以看到,foo(z)被重复调用了两次,做了重复计算。更严重的是,如果foo是不可重入的(foo内修改了全局或静态变量),程序会产生逻辑错误。

所以,尽量不要在宏参数中传入函数调用。

4. 对自身的递归引用

有如下宏定义:

#define foo (4 + foo)

按前面的理解,(4 + foo)会展开成(4 + (4 + foo)),然后一直展开下去,直至内存耗尽。但是,预处理器采取的策略是只展开一次。也就是说,foo只会展开成(4 + foo),而展开之后foo的含义就要根据上下文来确定了。

对于以下的交叉引用,宏体也只会展开一次。

#define x (4 + y)
#define y (2 * x)

x展开成(4 + y) -> (4 + (2 * x))y展开成(2 * x) -> (2 * (4 + y))

注意,这是极不推荐的写法,程序可读性极差。

5. 宏参数预处理

宏参数中若包含另外的宏,那么宏参数在被代入到宏体之前会做一次完全的展开,除非宏体中含有###

有如下宏定义:

#define AFTERX(x) X_ ## x
#define XAFTERX(x) AFTERX(x)
#define TABLESIZE 1024
#define BUFSIZE TABLESIZE
  • AFTERX(BUFSIZE)会被展开成X_BUFSIZE。因为宏体中含有##,宏参数直接代入宏体。
  • XAFTERX(BUFSIZE)会被展开成X_1024。因为XAFTERX(x)的宏体是AFTERX(x),并没有###,所以BUFSIZE在代入前会被完全展开成1024,然后才代入宏体,变成X_1024

3、宏定义使用技巧


1. 防止一个头文件被重复包含
   #ifndef BODYDEF_H
   #define BODYDEF_H
   //
头文件内容
   #endif

2. 重新定义一些类型,防止由于各种平台和编译器的不同,而产生的类型字节数差异,方便移植。
   typedef  unsigned char      boolean;     /* Boolean type. */
   typedef  unsigned long      uint32;      /* Unsigned 32 bit */
   typedef  unsigned short     uint16;      /* Unsigned 16 bit */
   typedef  unsigned char      uint8;       /* Unsigned 8  bit */
   typedef  signed long int    int32;       /* Signed 32 bit */
   typedef  signed short       int16;       /* Signed 16 bit */
   typedef  signed char        int8;        /* Signed 8  bit */

   //下面的不建议使用
   typedef  unsigned char     byte;         /* Unsigned 8  bit type. */
   typedef  unsigned short    word;         /* Unsinged 16 bit type. */
   typedef  unsigned long     dword;        /* Unsigned 32 bit type. */
   typedef  unsigned char     uint1;        /* Unsigned 8  bit type. */
   typedef  unsigned short    uint2;        /* Unsigned 16 bit type. */
   typedef  unsigned long     uint4;        /* Unsigned 32 bit type. */
   typedef  signed char       int1;         /* Signed 8  bit type. */
   typedef  signed short      int2;         /* Signed 16 bit type. */
   typedef  long int          int4;         /* Signed 32 bit type. */
   typedef  signed long       sint31;       /* Signed 32 bit */
   typedef  signed short      sint15;       /* Signed 16 bit */
   typedef  signed char       sint7;        /* Signed 8  bit */

3. 得到指定地址上的一个字节或字
   #define  MEM_B(x)  (*((uint8*)(x)))
   #define  MEM_W(x)  (*((uint16*)(x)))

4. 得到一个field在结构体(struct)中的偏移量
   #define FPOS(type,field) ((uint32) &((type *)0)->field)

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

6. 求最大值和最小值
   #define  MAX(x,y) (((x)>(y))?(x):(y))
   #define  MIN(x,y) (((x)<(y))?(x):(y))

7. 得到一个变量的地址
   #define  B_PTR(var)  ((byte *) (void *) &(var))
   #define  W_PTR(var)  ((word *) (void *) &(var))

8. 按照LSB格式把两个字节转化为一个Word
   #define  FLIPW(ray) ((((word) (ray)[0]) * 256) + (ray)[1])

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

10.得到一个字的高位和低位字节
   #define  WORD_LO(***)  ((byte) ((word)(***) & 0xFF))
   #define  WORD_HI(***)  ((byte) ((word)(***) >> 8))

11.将一个字母转换为大写
   #define  UPCASE(c) (((c) >= 'a' && (c) <= 'z') ? ((c) - 0x20) : (c))

12.判断字符是不是10进值的数字
   #define  DECCHK(c) ((c) >= '0' && (c) <= '9')

13.判断字符是不是16进值的数字
   #define  HEXCHK(c) (((c) >= '0' && (c) <= '9') ||  ((c) >= 'A' && (c) <= 'F') ||  ((c) >= 'a' && (c) <= 'f'))

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

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

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

17.返回一个无符号数n位的值

#define MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)
#define MOD_BY_POWER_OF_TWO(val, n)  ((dword)(val) & (dword)((2^n)-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.使用一些宏跟踪调试
   ANSI标准说明了五个预定义的宏名。它们是:
   __LINE__   (两个下划线),对应%d
   __FILE__  
对应%s
   __DATE__  
对应%s
   __TIME__  
对应%s
   __STDC__

   如果编译不是标准的,则可能仅支持以上宏名中的几个,或根本不支持。记住编译程序也许还提供其它预定义的宏名。
   __LINE__宏指令表示当前指令所在的行,是个整型数
   __FILE__宏指令表示当前指令所在文件,包含完整路径
   __DATE__宏指令含有形式为月//年的串,表示源文件被翻译到代码时的日期。
   __TIME__宏指令包含源代码翻译到目标代码的时间。串形式为时:分:秒。
   如果实现是标准的,则宏__STDC__含有十进制常量1。如果它含有任何其它数,则实现是非标准的。

   可以定义宏,例如:
  
当定义了_DEBUG,输出数据信息和所在文件所在行
   #ifdef _DEBUG
       #define DEBUGMSG(msg,date) do { printf(msg);printf("date: %s, FILE: %S, LINE: %d",__DATE__,__FILE_,__LINE__);} while(0)
   #else
       #define DEBUGMSG(msg,date)
   #endif

20.宏定义防止使用时错误
   多个参数,每个参数、每个操作用小括号包含。

   例如:#define ADD(a,b) ((a)+(b))
   

do{}while(0)语句包含多语句防止错误

   例如:#define DO(a,b)      /
                   a+b;        /
                   a++;
  
应用时:if(...)
             O(a,b); //
产生错误
           else

   解决方法: #difne DO(a,b)       /
                 do{              /
                    a+b;          /
                    a++;          /
                 }while(0)

4、宏定义中"#"、"#@""##"的用法


C语言的宏中,#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号。而#@则是给宏参数加上单引号。

可以看到通过在printf中使用#将变量打印出来了。这里注意printf中多个双引号中的内容可以自动连接起来。

#define DEBUG_PCHAR_MSG(pchar) printf("DEBUG FILE: %s, LINE: %d, " #pchar " is %s\n", __FILE__, __LINE__, pchar);
#define DEBUG_VAL_MSG(val) printf("DEBUG FILE: %s, LINE: %d, " #val " val is %d\n", __FILE__, __LINE__, val);
#define CHECK_POINTER_RETURN_VOID(POINTER) { if(NULL == POINTER) {printf( "POINTER " #POINTER " is NULL\n"); return;}}

char *msg = "test # usage in macro";
int input = 10;
int *pInt = NULL;
DEBUG_PCHAR_MSG(msg)
DEBUG_VAL_MSG(input)
CHECK_POINTER_RETURN_VOID(pInt)

DEBUG FILE: test1.c, LINE: 484, msg is test # usage in macro
DEBUG FILE: test1.c, LINE: 490, input val is 10
POINTER pInt is NULL



##被称为连接符(concatenator),用来将两个Token连接为 一个Token.注意这里连接的对象是Token就行,而不一定是宏的变量。   我们使用#把宏参数变为一个字符串,##把两个宏参数贴合在一起。注意这里连接的是token,而不一定是宏变量。还可以n个##符号连接n+1个token。宏在这里充当一个代码生成器的作用。

#include <stdio.h>

#define STR(s)    (#s"\n")
#define CONS(a,b) ((int)(a##e##b))

   int main()
   {
 
      printf(STR(vck));           // 输出字符串"vck"
      printf("%d\n", CONS(2,3));  // 2e3 输出:2000
      return 0;
   }

 

当宏参数是另一个宏的时候
   需要注意的是凡宏定义里有用"#""##"的地方宏参数是不会再展开。

#define MAX_INT32  0X7FFFFFFF
#define _STR(s) #s
#define STR(s) _STR(s)

#define EXPCAL2(a,b) (a*pow(10,b))
#define _EXPCAL(a,b) ((int)(a##e##b))
#define EXPCAL(a,b) _EXPCAL(a,b) 

#define TWO      2
#define MUL(a,b) ((a)*(b))

#define OPERATION(X,OP,Y) ((X) OP (Y))  

int main()
{ 
  int a = 2,b=3, result;
  result = MUL(a,b);
  printf(_STR(MUL(a,b) result)STR(MUL(a,b) result)" is %d\n", result);

  result = EXPCAL(2,3);
  printf(_STR(EXPCAL(2,3) result)STR(EXPCAL(2,3) result)" is %d\n", result);

  result = EXPCAL2(a,b);
  printf(_STR(EXPCAL2(a,b) result)STR(EXPCAL2(a,b) result)" is %d\n", result);

  printf("%d * %d = %d\n", TWO, TWO, MUL(TWO,TWO));
  
  result = OPERATION(a,*,b);
  printf("result of a * b = %d \n", result);
  printf("int max: %s,  %s \n", _STR(MAX_INT32), STR(MAX_INT32));
  printf("%s, %s,%s, %s, %d\n", _STR(_EXPCAL(TWO, TWO)), STR(_EXPCAL(TWO, TWO)),_STR(EXPCAL(TWO, TWO)), STR(EXPCAL(TWO, TWO)), EXPCAL(TWO,TWO));

}

//GCC 预处理gcc -E test1.c > pre.log,查看pre.log可看出宏替代之后结果为:

int main()
{
  int a = 2,b=3, result;
  result = ((a)*(b));
  printf("MUL(a,b) result""((a)*(b)) result"" is %d\n", result);

  result = ((int)(2e3));
  printf("EXPCAL(2,3) result""((int)(2e3)) result"" is %d\n", result);

  result = (a*pow(10,b));
  printf("EXPCAL2(a,b) result""(a*pow(10,b)) result"" is %d\n", result);

  printf("%d * %d = %d\n", 2, 2, ((2)*(2)));

  result = ((a) * (b));
  printf("result of a * b = %d \n", result);
  printf("int max: %s,  %s \n", "MAX_INT32", "0X7FFFFFFF");
  printf("%s, %s,%s, %s, %d\n", "_EXPCAL(TWO, TWO)", "((int)(TWOeTWO))","EXPCAL(TWO, TWO)", "((int)(2e2))", ((int)(2e2)));
}

//编译gcc test1.c -lm -o test然后./test执行后得到结果:

MUL(a,b) result((a)*(b)) result is 6
EXPCAL(2,3) result((int)(2e3)) result is 2000
EXPCAL2(a,b) result(a*pow(10,b)) result is 2000
2 * 2 = 4
result of a * b = 6 
int max: MAX_INT32,  0X7FFFFFFF 
_EXPCAL(TWO, TWO), ((int)(TWOeTWO)),EXPCAL(TWO, TWO), ((int)(2e2)), 200

有以下几点需要注意

(1)非'#' '##'的情况
   #define TWO      2
   #define MUL(a,b) ((a)*(b))

   printf("%d*%d=%d\n", TWO, TWO, MUL(TWO,TWO));
   这行的宏会被展开为:

   printf("%d*%d=%d\n", 2, 2, ((2)*(2)));
   MUL
里的参数TWO会被自动展开为2

(2)  #define TWO      2不能加括号

如果#define TWO      (2)

则提示如下

错误:毗连“)”和“e”不能给出一个有效的预处理标识符
 #define TWO      (2)
                    ^
附注:in definition of macro ‘_EXPCAL’
 #define _EXPCAL(a,b) ((int)(a##e##b))
 错误:毗连“e”和“(”不能给出一个有效的预处理标识符
 #define _EXPCAL(a,b) ((int)(a##e##b))
                                ^
附注:in expansion of macro ‘_EXPCAL’
 #define EXPCAL(a,b) _EXPCAL(a,b) 
 

错误:毗连“)”和“e”不能给出一个有效的预处理标识符
 #define TWO      (2)
                    ^
附注:in definition of macro ‘_EXPCAL’
 #define _EXPCAL(a,b) ((int)(a##e##b))
 

错误:毗连“e”和“(”不能给出一个有效的预处理标识符
 #define _EXPCAL(a,b) ((int)(a##e##b))
                                ^
附注:in expansion of macro ‘_EXPCAL’
 #define EXPCAL(a,b) _EXPCAL(a,b) 
 

错误:expected ‘)’ before ‘e’
 #define _EXPCAL(a,b) ((int)(a##e##b))
                                ^
附注:in expansion of macro ‘_EXPCAL’
 #define EXPCAL(a,b) _EXPCAL(a,b) 
 

(3)GCC查看宏替代后的结果
GCC 预处理gcc -E test1.c > pre.log,查看pre.log可看出宏替代之后结果。

(4)linux下pow函数使用

编译时执行gcc test1.c -o test报错,提示

对‘pow’未定义的引用
collect2: 错误:ld 返回 1
在linux环境下编译时,需要链接数学库,在编译语句后面加一个 -lm

错误:ld返回1退出状态来自链接器ld(组合目标文件的gcc的一部分),因为它无法找到功能pow定义的位置.

包括math.h引入了各种函数的声明而不是它们的定义. def出现在数学库libm.a中.需要将程序与此库链接,以便解析对pow()等函数的调用.

编译执行gcc test1.c -lm -o test通过

(5)变量和常量宏展开

#define EXPCAL2(a,b) (a*pow(10,b))
#define _EXPCAL(a,b) ((int)(a##e##b))

对于数值,可以直接执行宏_EXPCAL(2,3)展开为2e3

对于变量_EXPCAL(a,b)展开为aeb,需要通过EXPCAL2(a,b) (a*pow(10,b))展开为a*pow(10,b)

(6)未找到原因的错误提示:

#define OPERATION(X,OP,Y) (X##OP##Y) 

result = OPERATION(a,*,b);

展开为:result = (a*b);
但仍然提示:

错误:毗连“a”和“*”不能给出一个有效的预处理标识符
   result = OPERATION(a,*,b);
                       ^
附注:in definition of macro ‘OPERATION’
 #define OPERATION(X,OP,Y) (X##OP##Y) 

 

 

#define OPERATION(X,OP,Y) ((X) (OP) (Y))

result = OPERATION(a,*,b);

展开为result = ((a) (*) (b));提示

called object ‘a’ is not a function or function pointer
 #define OPERATION(X,OP,Y) ((X) (OP) (Y)) 
 

 

#define OPERATION(X,OP,Y) ((X)##OP## (Y))或者(X##OP## Y)

result = OPERATION(a,*,b);

展开为result = ((a)*(b)); 或者result = (a*b);

错误:毗连“)”和“*”不能给出一个有效的预处理标识符
 #define OPERATION(X,OP,Y) ((X)##OP##(Y)) 
                               ^
 附注:in expansion of macro ‘OPERATION’
   result = OPERATION(a,*,b);
            ^
test1.c:496:25: 错误:毗连“*”和“(”不能给出一个有效的预处理标识符
   result = OPERATION(a,*,b);
                         ^
test1.h:157:34: 附注:in definition of macro ‘OPERATION’
 #define OPERATION(X,OP,Y) ((X)##OP##(Y)) 

(7)当有'#''##'的时候

 printf("int max: %s,  %s \n", _STR(MAX_INT32), STR(MAX_INT32));
  printf("%s, %s,%s, %s, %d\n", _STR(_EXPCAL(TWO, TWO)), STR(_EXPCAL(TWO, TWO)),_STR(EXPCAL(TWO, TWO)), STR(EXPCAL(TWO, TWO)), EXPCAL(TWO,TWO));

展开之后为:

  printf("int max: %s,  %s \n", "MAX_INT32", "0X7FFFFFFF");
  printf("%s, %s,%s, %s, %d\n", "_EXPCAL(TWO, TWO)", "((int)(TWOeTWO))","EXPCAL(TWO, TWO)", "((int)(2e2))", ((int)(2e2)));

   printf("int max: %s\n",  STR(INT_MAX));    
  
这行会被展开为:
   printf("int max: %s\n", "INT_MAX");

 

   printf("%s\n", _EXPCAL(TWO,TWO));               // compile error
   这一行则被展开为:

   printf("%s\n", int(TWOeTWO));

   INT_MAX和TWO都不会再被展开, 然而解决这个问题的方法很简单. 加多一层中间转换宏.
  
加这层宏的用意是把所有宏的参数在这层里全部展开, 那么在转换宏里的那一个宏(_STR)就能得到正确的宏参数


   #define _STR(s)     #s
   #define STR(s)      _STR(s)          //
转换宏
   #define _EXPCAL(a,b) ((int)(a##e##b))
   #define EXPCAL(a,b)   _EXPCAL(a,b)       // 转换宏

   printf("int max: %s/n", STR(INT_MAX));          // INT_MAX,int型的最大值
   输出为: int max: 0x7fffffff
   STR(INT_MAX) -->  _STR(0x7fffffff)
然后再转换成字符串;

   printf("%d/n", EXPCAL(TWO, TWO));
   输出为:200
   EXPCAL(A, A)  -->  _EXPCAL((2), (2))  --> int((2)e(2))

 

注意到STR(_EXPCAL(TWO, TWO)) =》"((int)(TWOeTWO))",这里也只能向下展开一层。


5. "#""##"的一些应用特例


(1)合并匿名变量名

#define ANONYMOUS1(type, var, line)   type var##_##line

#define ANONYMOUS0(type, line)   ANONYMOUS1(type, anonymous, line)

#define ANONYMOUS(type)   ANONYMOUS0(type, __LINE__)

int main()

{

ANONYMOUS(static int); //即: static int anonymous_489;   489表示该行行号; 

}

//第一层:ANONYMOUS(static int);   -->   ANONYMOUS0(static int, __LINE__);

//第二层:                        -->   ANONYMOUS1(static int, anonymous, 489);

//第三层:                        -->   static int   anonymous_489;

//即每次只能解开当前层的宏,所以__LINE__在第二层才能被解开;

 

(2)填充结构
   #define  FILL(a)   {a, #a}

   enum IDD{OPEN, CLOSE};
   typedef struct MSG{
     IDD id;
     const char * msg;
   }MSG;

   MSG _msg[] = {FILL(OPEN), FILL(CLOSE)};
  
相当于:
   MSG _msg[] = {{OPEN,  "OPEN"},
                 {CLOSE, "CLOSE"}};

 

(3)记录文件名
   #define  _GET_FILE_NAME(f)   #f
   #define  GET_FILE_NAME(f)    _GET_FILE_NAME(f)
   static char  FILE_NAME[] = GET_FILE_NAME(__FILE__);

(4)得到一个数值类型所对应的字符串缓冲大小
   #define  _TYPE_BUF_SIZE(type)  sizeof #type
   #define  TYPE_BUF_SIZE(type)   _TYPE_BUF_SIZE(type)
   char  buf[TYPE_BUF_SIZE(INT_MAX)];
     -->  char  buf[_TYPE_BUF_SIZE(0x7fffffff)];
     -->  char  buf[sizeof "0x7fffffff"];
  
这里相当于:
   char  buf[11];

6、变参宏

......符号的使用
   ......C宏中称为Variadic Macro,也就是变参宏。比如:

   #define myprintf(templt, ...) fprintf(stderr,templt,__VA_ARGS__)

   // 或者

   #define myprintf(templt, args...) fprintf(stderr,templt,args)

   第一个宏中由于没有对变参起名,我们用默认的宏__VA_ARGS__来替代它。第二个宏中,我们显式地命名变参为args,那么我们在宏定义中就可以用 args来代指变参了。同C语言的stdcall一样,变参必须作为参数表的最后一项出现。当上面的宏中我们只能提供第一个参数templt时,C标准要 求我们必须写成:myprintf(templt,);的形式。这时的替换过程为:

   myprintf("Error!\n",);

   替换为:

   fprintf(stderr,"Error!\n",);

   这是一个语法错误,不能正常编译。这个问题一般有两个解决方法。首先,GNU CPP提供的解决方法允许上面的宏调用写成:

   myprintf(templt);

   而它将会被通过替换变成:

   fprintf(stderr,"Error!/n",);

   很明显,这里仍然会产生编译错误(非本例的某些情况下不会产生编译错误)。除了这种方式外,c99GNU CPP都支持下面的宏定义方式:

   #define myprintf(templt,......) fprintf(stderr,templt, ##__VAR_ARGS__)

   这时,##这个连接符号充当的作用就是当__VAR_ARGS__为空的时候,消除前面的那个逗号。那么此时的翻译过程如下:

   myprintf(templt)

   被转化为:

   fprintf(stderrtemplt)

   这样如果templt合法,将不会产生编译错误。

   另外,在vxworks中,还可以允许下面的宏定义:

   #define myprintf(arg...)        printf(arg)

   宏的第一个参数就设置为变参,因此下面的几种使用方式都是正确的:

   myprintf("number");
   myprintf("number %d",2);
   myprintf("number %d %d",2,3);

#define myprintf1(templt, ...) printf(templt,##__VA_ARGS__)
#define myprintf2(templt, args...) printf(templt,args)
#define myprintf3(templt...) printf(templt)

void main()
{
  myprintf1("test myprintf1 %s\n", "test1");
  myprintf2("test myprintf2 %s\n", "test2");
  myprintf1("test myprintf1\n");
  myprintf3("number\n");
  myprintf3("number %d\n", 2);
  myprintf3("number %d, %d\n",2,3);
}

结果:
test myprintf1 test1
test myprintf2 test2
test myprintf1
number
number 2
number 2, 3

 

 

四、其它编译命令

1、#define

命令#define定义了一个标识符及一个串。在源程序中每次遇到该标识符时,均以定义的串代换它。ANSI标准将标识符定义为宏名,将替换过程称为宏替换。

2、#error

处理器命令#error强迫编译程序停止编译,主要用于程序调试。

3、#include

命令#include使编译程序将另一源文件嵌入带有#include的源文件,被读入的源文件必须用双引号或尖括号括起来。

如果显式路径名为文件标识符的一部分,则仅在那些子目录中搜索被嵌入文件。否则,如果文件名用双引号括起来,则首先检索当前工作目录。如果未发现文件, 则在命令行中说明的所有目录中搜索。如果仍未发现文件,则搜索实现时定义的标准目录。

如果没有显式路径名且文件名被尖括号括起来,则首先在编译命令行中的目录内检索。如果文件没找到,则检索标准目录,不检索当前工作目录。

4、条件编译命令
有几个命令可对程序源代码的各部分有选择地进行编译,该过程称为条件编译。商业软件公司广泛应用条件编译来提供和维护某一程序的许多顾客版本。

#if、#else,#elif及#endif

#if的一般含义是如果#if后面的常量表达式为true,则编译它与#endif之间的代码,否则跳过这些代码。命令#endif标识一个#if 块的结束。

#else命令的功能有点象C语言中的else;#else建立另一选择(在#if失败的情况下)。注意,# else属于# if块。

#elif命令意义与ELSE IF 相同,它形成一个if else-if阶梯状语句,可进行多种编译选择。

#elif 后跟一个常量表达式。如果表达式为true,则编译其后的代码块,不对其它#elif表达式进行测试。否则,顺序测试下一块。

在嵌套的条件编译中#endif、#else或#elif与最近#if或#elif匹配。

 

# ifdef 和# ifndef

条件编译的另一种方法是用#ifdef与#ifndef命令,它们分别表示"如果有定义"及"如果无定义"。

# ifdef的一般形式是:

# ifdef macroname

statement sequence

#endif 
#ifdef与#ifndef可以用于#if、#else,#elif语句中,但必须与一个#endif关联。

 

5、#undef

命令#undef 取消其后那个前面已定义过有宏名定义。一般形式为:

#undef macroname

6、#line


命令#line改变__LINE__与__FILE__的内容,它们是在编译程序中预先定义的标识符。命令的基本形式如下:

# line number["filename"]

其中的数字为任何正整数,可选的文件名为任意有效文件标识符。行号为源程序中当前行号,文件名为源文件的名字。命令#line主要用于调试及其它特殊应用。

注意:在#line后面的数字标识从下一行开始的数字标识。

7、#pragma的使用【转载】

在所有的预处理指令中,#pragma 指令可能是最复杂的了,它的作用是设定编译器的状态或者是指示编译器完成一些特定的动作。#pragma指令对每个编译器给出了一个方法,在保持与C和C ++语言完全兼容的情况下,给出主机或操作系统专有的特征。依据定义,编译指示是机器或操作系统专有的,且对于每个编译器都是不同的。
其格式一般为: #pragma Para,其中Para 为参数,下面来看一些常用的参数。
(1)message 参数。 它能够在编译信息输出窗口中输出相应的信息,这对于源代码信息的控制是非常 
重要的。其使用方法为:

#Pragma message("消息文本")
当编译器遇到这条指令时就在编译输出窗口中将消息文本打印出来。

当我们在程序中定义了许多宏来控制源代码版本的时候,我们自己有可能都会忘记有没有正确的设置这些宏,此时我们可以用这条指令在编译的时候就进行检查。 
假设我们希望判断自己有没有在源代码的什么地方定义了_X86这个宏可以用下面的方法
#ifdef _X86

#pragma message("_X86 macro activated!")

#endif

当我们定义了_X86这个宏以后,应用程序在编译时就会在编译输出窗口里显示"_X86 macro activated!"。我们就不会因为不记得自己定义的一些特定的宏而抓耳挠腮了。

(2)另一个使用得比较多的pragma参数是code_seg。格式如:


#pragma code_seg( ["section-name"[,"section-class"] ] )


它能够设置程序中函数代码存放的代码段,当我们开发驱动程序的时候就会使用到它。

(3)#pragma once (比较常用)


只要在头文件的最开始加入这条指令就能够保证头文件被编译一次,这条指令实际上在VC6中就已经有了,但是考虑到兼容性并没有太多的使用它。

(4)#pragma hdrstop表示预编译头文件到此为止,后面的头文件不进行预编译。BCB可以预编译头文件以加快链接的速度,但如果所有头文 
件都进行预编译又可能占太多磁盘空间,所以使用这个选项排除一些头文件。

有时单元之间有依赖关系,比如单元A依赖单元B,所以单元B要先于单元A编译。你可以用#pragma startup指定编译优先级,如果使用了 
#pragma package(smart_init) ,BCB就会根据优先级的大小先后编译。

(5)#pragma resource "*.dfm"表示把*.dfm文件中的资源加入工程。*.dfm中包括窗体、外观的定义。

(6)#pragma warning( disable : 4507 34; once : 4385; error : 164 )

等价于:

#pragma warning(disable:4507 34) // 不显示4507和34号警告信息

#pragma warning(once:4385) // 4385号警告信息仅报告一次

#pragma warning(error:164) // 把164号警告信息作为一个错误。

同时这个pragma warning 也支持如下格式:

#pragma warning( push [ ,n ] )

#pragma warning( pop )

这里n代表一个警告等级(1---4)。

#pragma warning( push )保存所有警告信息的现有的警告状态。

#pragma warning( push, n)保存所有警告信息的现有的警告状态,并且把全局警告等级设定为n。 
#pragma warning( pop )向栈中弹出最后一个警告信息,在入栈和出栈之间所作的一切改动取消。例如:

#pragma warning( push )

#pragma warning( disable : 4705 )

#pragma warning( disable : 4706 )

#pragma warning( disable : 4707 )

//.......

#pragma warning( pop )

在这段代码的最后,重新保存所有的警告信息(包括4705,4706和4707)。

(7)pragma comment(...)

该指令将一个注释记录放入一个对象文件或可执行文件中。 
常用的lib关键字,可以帮我们连入一个库文件。

#pragma comment ( lib,"wpcap.lib" )

表示链接wpcap.lib这个库。和在工程设置里写上链入wpcap.lib的效果一样(两种方式等价,或说一个隐式一个显式调用),不过这种方法写的 程序别人在使用你的代码的时候就不用再设置工程settings了。告诉连接器连接的时候要找ws2_32.lib,这样你就不用在linker的lib设置里指定这个lib了。

#pragma comment( comment-type [,"commentstring"] )

该宏放置一个注释到对象文件或者可执行文件。
comment-type是一个预定义的标识符,指定注释的类型,应该是compiler,exestr,lib,linker之一。
commentstring是一个提供为comment-type提供附加信息的字符串,
Remarks:
1、compiler:放置编译器的版本或者名字到一个对象文件,该选项是被linker忽略的。
2、exestr:在以后的版本将被取消。
3、lib:放置一个库搜索记录到对象文件中,这个类型应该是和commentstring(指定你要Liner搜索的lib的名称和路径)这个库的名字放在Object文件的默认库搜索记录的后面,linker搜索这个库就像你在命令行输入这个命令一样。你可以在一个源文件中设置多个库记录,它们在object文件中的顺序和在源文件中的顺序一样。如果默认库和附加库的次序是需要区别的,使用Z编译开关是防止默认库放到object模块。
4、linker:指定一个连接选项,这样就不用在命令行输入或者在开发环境中设置了。
只有下面的linker选项能被传给Linker.
  • /DEFAULTLIB

  • /EXPORT

  • /INCLUDE

  • /MANIFESTDEPENDENCY

  • /MERGE

  • /SECTION

(1)/DEFAULTLIB:library

  /DEFAULTLIB 选项将一个 library 添加到 LINK 在解析引用时搜索的库列表。用 /DEFAULTLIB
  指定的库在命令行上指定的库之后和 .obj 文件中指定的默认库之前被搜索。

    忽略所有默认库 (/NODEFAULTLIB) 选项重写 /DEFAULTLIB:library。如果在两者中指定了相同的 library 名称,忽略库 (/NODEFAULTLIB:library) 选项将重写 /DEFAULTLIB:library。

  (2)/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]

  使用该选项,可以从程序导出函数,以便其他程序可以调用该函数。也可以导出数据。通常在 DLL 中定义导出。entryname 是调用程序要使用的函数或数据项的名称。ordinal 在导出表中指定范围在 1 至 65,535 的索引;如果没有指定 ordinal,则 LINK 将分配一个。NONAME 关键字只将函数导出为序号,没有 entryname。

  DATA 关键字指定导出项为数据项。客户程序中的数据项必须用 extern __declspec(dllimport) 来声明。
有三种导出函数或者数据的方法,按照建议的使用顺序依次为:

  1. 源代码中的 __declspec(dllexport)

  2. .def 文件中的 EXPORTS 语句

  3. LINK 命令中的 /EXPORT 规范

所有这三种方法可以用在同一个程序中。LINK 在生成包含导出的程序时还创建导入库,除非生成中使用了 .exp 文件。
LINK 使用标识符的修饰形式。编译器在创建 .obj 文件时修饰标识符。如果 entryname 以其未修饰的形式指定给链接器(与其在源代码中一样),则 LINK 将试图匹配该名称。如果无法找到唯一的匹配名称,则 LINK 发出错误信息。当需要将标识符指定给链接器时,请使用 Dumpbin 工具获取该标识符的修饰名形式。

(3)/INCLUDE:symbol

  /INCLUDE 选项通知链接器将指定的符号添加到符号表。

  若要指定多个符号,请在符号名称之间键入逗号 (,)、分号 (;) 或空格。在命令行上,对每个符号指定一次 /INCLUDE:symbol。
链接器通过将包含符号定义的对象添加到程序来解析 symbol。该功能对于添包含不会链接到程序的库对象非常有用。用该选项指定符号将通过 /OPT:REF 重写该符号的移除。

(8)用pragma导出dll中的函数

传统的导出 DLL 函数的方法是使用模块定义文件 (.def),Visual C++ 提供了更简洁方便的方法,那就是"__declspec()" 关键字后面跟"dllexport",告诉连接去要导出这个函数,例如:

__declspec(dllexport) int __stdcall MyExportFunction(int iTest);把"__declspec(dllexport)"放在函数声明的最前面,连接生成的 DLL 就会导出函数"_MyExportFunction@4 "。

上面的导出函数的名称也许不是我的希望的,我们希望导出的是原版的"MyExportFunction"。还好,VC 提供了一个预处理指示 符"#pragma" 来指定连接选项 (不仅仅是这一个功能,还有很多指示功能) ,如下:

#pragma comment(linker,"/EXPORT:MyExportFunction=_MyExportFunction@4")
这下就天如人愿了:)。如果你想指定导出的顺序,或者只将函数导出为序号,没有 Entryname,这个预处理指示符 (确切地说是连接器) 都能够实现,看看 MSDN 的语法说明:

/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]

@ordinal 指定顺序;NONAME 指定只将函数导出为序号;DATA 关键字指定导出项为数据项。

⑨每个编译程序可以用#pragma指令激活或终止该编译程序支持的一些编译功能。例如,对循环优化功能:

#pragma loop_opt(on) // 激活

#pragma loop_opt(off) // 终止

有时,程序中会有些函数会使编译器发出你熟知而想忽略的警告,如"Parameter xxx is never used in function xxx",可以这样:

#pragma warn -100 // Turn off the warning message for warning #100

int insert_record(REC *r)

{ /* function body */ }

#pragma warn +100 // Turn the warning message for warning #100 back on

函数会产生一条有唯一特征码100的警告信息,如此可暂时终止该警告。

每个编译器对#pragma的实现不同,在一个编译器中有效在别的编译器中几乎无效。可从编译器的文档中查看。

⑩#pragm pack()的使用

#pragma pack规定的对齐长度,实际使用的规则是:

结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。而结构整体的对齐,则按照结构体中最大的数据成员和 #pragma pack指定值之间,较小的那个进行。

注意:文件使用#pragma pack(n) 改变了缺省设置而不恢复,通常可以使用#pragma pack(push, n)和#pragma pack(pop)进行设置与恢复。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值