简单了解宏以及宏和函数的区别

时间:2023-01-30 23:37:24

 1.=》宏的认识

简单来说:宏定义又称为宏代换、宏替换,简称“宏”。是C提供的三种预处理功能的其中一种。
 宏定义是C提供的三种预处理功能的其中一种,这三种预处理包括:宏定义、文件包含、条件编译.

=》无参的宏定义
 宏定义又称为宏代换、宏替换,简称“宏”。

宏定义的格式很关键。 
基本的开头格式是  比如我们要写一个game.h的头文件

#ifndef _GAME_H__  这一句的意思就是如果game.h没有被定义 然后往下跑,若已存在那就直接cut. #define _GAME_H__   然后这句就是开始定义了                    //举个例子我们现在要定义一个宏#define n =10  ×   //表面的意思你是让N等于10  其实呢  n代表的是   “=10” #define n 10;  ×  //这里又错了   不能再宏后面加“ ;#define n 10  √  //正确的方法
#endif               //_GMAE_H__这只是一个好习惯..
在一个宏定义中,编译器可以检测到绝大多数由多余符号所导致的错误。但不幸的是,编译器会将每一处使用这个宏的地方标为错误,而不会直接找到错误的根源——宏定义本身,因为宏定义已经被预处理器删除了。
=》小细节

 (1)宏名一般用大写
 (2)使用宏可提高程序的通用性和易读性,减少不一致性,减少输入错误和便于修改。例如:数组
      大小常用宏定义
 (3)预处理是在编译之前的处理,而编译工作的任务之一就是语法检查,预处理不做语法检查。
 (4)宏定义末尾不加分号;

 (5)宏定义写在函数的花括号外边,作用域为其后的程序,通常在文件的最开头。

 (6)可以用#undef命令终止宏定义的作用域

 (7)宏定义可以嵌套

 (8)字符串" "中永远不包含宏

 (9)宏定义不分配内存,变量定义分配内存。


=》有参的宏定义
 #define指令—带参数的宏]  #define 标识符(x1, x2,…,xn)替换列表 
#ifndef  _GMAE_H__
#define _GAME_H__
#define MAX(x,y)    ((x)>(y) ? (x) :(y)) //这样的就是一个标准有参宏定义
#define IS_EVEN(n)   ((n)%2==0)        //记住有参的宏定义中每一个参数都要带括号记住!!!                                        //为了避免不必要的错误0.0#endif   //_GAME_H__
 (1)实参如果是表达式容易出问题
          #define S(r) r*r

          area=S(a+b);第一步换为area=r*r;,第二步被换为area=a+b*a+b;

          这就是为什么每个参数都要带括号
           正确的宏定义是#define S(r) ((r)*(r))

 (2)宏名和参数的括号间不能有空格

 (3)宏替换只作替换,不做计算,不做表达式求解

 (4)函数调用在编译后程序运行时进行,并且分配内存。宏替换在编译前进行,不分配内存

 (5)宏的哑实结合不存在类型,也没有类型转换。

 (6)函数只有一个返回值,利用宏则可以设法得到多个值

 (7)宏展开使源程序变长,函数调用不会

 (8)宏展开不占运行时间,只占编译时间,函数调用占运行时间(分配内存、保留现场、值传递、返回值)


2.=》1使用带参数的宏替代实际的函数的优点



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

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

2.=》2带参数的宏也有一些缺点:



=》1编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表,由此导致程

序的源代码增加(因此编译后的代码变大)。宏使用得越频繁,这种效果就越明显。当

宏调用嵌套时,这个问题会相互叠加从而使程序更加复杂。思考一下,如果我们用MAX宏

来找出3个数中最大的数会怎样?
            n = MAX(i, MAX(j,k));  

           下面是预处理后的这条语句:


            n=((i)>(((j)>(k)?(j):(k)))?(i):(((j)>(k)?(j):(k)))); 

 

=》2宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确认它们


是否是正确的类型。如果不是,或者将参数转换成正确的类型,或者由编译器产生一个


出错信息。预处理器不会检查宏参数的类型,也不会进行类型转换。



=》3无法用一个指针来指向一个宏。C语言允许指针指向函数。这一概念在特定的编程条


件下非常有用。宏会在预处理过程中被删除,所以不存在类似的“指向宏的指针”。因


此,宏不能用于处理这些情况。

 常用的预定义的宏

 1、__DATE__
   “替代文字”是一个含有编译日期的字符串字面值,日期格式为“mm dd yyyy”
   (例如:“Mar 19 2006”)。如果日期小于10日,就在日的前面放一个空格符。
 2、__FILE__
   此字符串字面值含有目前源代码文件名称。

 3、__LINE__
   一个整数常量,其值是目前源代码的行号(包含__LINE__宏所指的那一行代码),从文件头
   开始算起。

__LINE__和__FILE__用于打印调试信息会非常方便。

printf("line = %d\n", __LINE__);

printf("file = %s\n", __FILE__);

 4、__TIME__
   此字符串字面值包含编译时间,格式为“hh:mm:ss”(范例:“08:00:59”)。

 5、__STDC__
   整数常量1,表示此编译器遵循ISOC标准。



3.=》#运算符


它仅允许出现在带参数的宏的替换列表中。

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

#运算符将一个宏的参数转换为字符串字面量(字符串字面量(string literal)是指双
引号引住的一系列字符,双引号中可以没有字符,可以只有一个字符,也可以有很多个
字符 简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号.
其实还挺有用的比如下面这个例子
#ifndef _GAME_H__#define _GAME_H__

#define printf_s(x) printf(#x " = %d\n", x)

#endif              // _GAME_H__
然后在函数中 在宏替换中 会变成这样 举个例子x 输入的是   liangliang
printf("liangliang" " = %d\n",x);
然后你就会发现前面自动帮你打印了  这个就很方便了。

4. =》##运算符


刚刚了解了#的用法,现在看看##  他的意思就是一个“连接符”,也是一个预处理运算符,这里的语言符号不一定是宏的变量。并且双井号不能作为第一个和最后一个存在。##运算符可以将两个记号(例如标识符),“粘”在一起,成为一个新的记号。如果其中的一个操作数是宏函数,“粘合”会在当形式参数被相应的实际参数替换后发生。比如下面这个例子#ifndef _GAME_H__#define _GAME_H__
#define int fun_s(x) hehe##x//那么预处理结束后, 如果x=liang 那么结果 声明  int heheliang;
#endif //_GAME_H__ 
虽然感觉## 没什么用... 可能是我的技术还没有达到。但是有很经典的例子 比如
当MAX的参数有副作用时会无法正常工作。一种解决方法是用MAX宏来写一个max函数。遗憾的是,往往一个max函数是不够的。我们可能需要一个实际参数是int值的max函数,还需要参数为float值的max函数,等等。除了实际参数的类型和返回值的类型之外,这些函数都一样。因此,这样定义每一个函数似乎是个很蠢的做法。
解决的办法是定义一个宏,并使它展开后成为max函数的定义。宏会有唯一的参数type,它表示形式参数和返回值的类型。这里还有个问题,如果我们是用宏来创建多个max函数,程序将无法编译。(C语言不允许在同一文件中出现两个重名的函数。)为了解决这个问题,我们是用##运算符为每个版本的max函数构造不同的名字。   #define GENERIC_MAX (type)
  type type##_max(type x,  type y)
    {           return x > y ? x :y;    }
   GENERIC_MAX(float

   //预处理器会将这行展开为下面的代码:
   float float_max(float x, float y) { return x > y ? x :y; }


5=》宏的通用属性


1=》宏的替换列表可以包含对另一个宏的调用。例如,我们可以用宏PI来定义宏TWO_PI:
#definePI      3.14159
#defineTWO_PI  (2*PI)
当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查
替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换掉为止

2=》预处理器只会替换完整的记号,而不会替换记号的片断。因此,预处理器会忽略嵌在标识符名、字符常量、字符串字面量之中的宏名
3=》一个宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,他们不遵从通常的范围规则。一个定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。
4=》宏不可以被定义两遍,除非新的定义与旧的定义是一样的。小的间隔上的差异是允许的,但是宏的替换列表(和参数,如果有的话)中的记号都必须一致。
5=》宏可以使用#undef指令“取消定义”。#undef指令有如下形式:
[#undef指令]  #undef  标识符 
其中标识符是一个宏名。例如,指令
#undef N
会删除宏N当前的定义。(如果N没有被定义成一个宏,#undef指令没有任何作用。)#undef指令的
一个用途是取消一个宏的现有定义,以便于重新给出新的定义。

6=》宏定义中的do-while循环do 


do循环必须始终随跟着一个分号,因此我们不会遇到在if语句中使用宏那样的问题了。

一个例子
#define fun(s)  
 do{  
   get(x);

      }while(0);当使用fun宏时,一定要加分号:
     如果fun(str);
     那么 /* becomes do {  gets(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();  
  7. 代码进行预处理后,会变成:  
  8. if (expr)  
  9.     a++;b++;  
  10. else  
  11.     do_else();  

    这样if-else的结构就被破坏了if后面有两个语句,这样是无法编译通过的,那为什么非要

do-while而不是简单的用{}括起来呢。 这样也能保证if后面只有一个语句。例如上面的例子,

在调用宏TEST的时候后面加了一个分号, 虽然这个分号可有可无, 但是出于习惯我们一般都会

写上。 那如果是把宏里的代码用{}括起来,加上最后的那个分号。 还是不能通过编译。 所以一

般的多表达式宏定义中都采用do-while(0)的方式。



7.=》总结


好多人不知道平时使用宏定义还是函数 其实我个人挺喜欢用函数的,但是呢各有各的好处。。。。

他们的区别,大家可以看着哪个合适用哪个


1.宏会在编译器在对源代码进行编译的时候进行简单替换,不会进行任何逻辑检测,即简单代码复制而已


2.宏进行定义时不会考虑参数的类型。

3.参数宏的使用会使具有同一作用的代码块在目标文件中存在多个副本,即会增长目标文件的大小。
4.参数宏的运行速度会比函数快,因为不需要参数压栈/出栈操作。
5.参数宏在定义时要多加小心,多加括号。
6.函数只在目标文件中存在一处,比较节省程序空间。
7.函数的调用会牵扯到参数的传递,压栈/出栈操作,速度相对较慢。
8.函数的参数存在传值和传地址(指针)的问题,参数宏不存在。
各有利弊吧看着用,但是宏在有的地方用会有意想不到的结果和便利。。 这些就是我个人的一些见解。。
哈哈终于写完!  有诚意的文章.......(有些是我粘贴复制的,但是都搞清楚了).