玩转iOS“宏定义”(一)

简介: 玩转iOS“宏定义”

宏定义在C类语言中非常重要,因为宏是一种预编译时的功能,因此其可以比运行时更高层面的对程序流程进行控制。在初学宏定义的时候,大家可能都会有这样一种感觉:就是完全替换么,太简单了。但如果你真这么想,那你就太天真了,不说自己编写宏,在Foundation框架中内置定义的许多宏要看明白也要费一番脑筋。本篇博客,总结了前辈的经验,同时收集了一些编写非常巧妙的宏进行分析,希望可以帮助大家对宏定义有更加深刻的理解,并且可以将心得应用于实际开发中。


一、准备


     宏的本质是预编译时的替换,在开始正文之前,我们需要先介绍一种观察宏替换后结果的方法,这样帮助我们更方便的对宏最终的结果进行验证与测试。Xcode开发工具自带查看预编译结果的功能,首先需要对工程编译一遍,之后选择工具栏中的Assistant选项,打开助手窗口,如下图所示:


image.png


之后选择窗口的Preprocess选项,即可打开预编译结果窗口,可以看到,宏被替换后的最终结果,如下图所示:


image.png


后面,我们将使用这种方式来对编写的宏进行验证。


二、关于“宏定义”


     宏使用#define来进行定义,宏定义分为两种,一种是对象式宏,一种是函数式宏。对象式宏通常对来定义量值,在预编译时,直接将宏名替换成对应的量值,函数式宏在定义时可以设置参数,其作用与函数很类似。


例如,我们可以将π的值定义成一个对象式宏,在使用的时候,用有意义的宏名要比直接使用π的字面值方便很多,例如:


#import <Foundation/Foundation.h>

#define PI 3.1415926

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       // insert code here...

       CGFloat res = PI * 3;

       NSLog(@"%f", res);

   }

   return 0;

}

函数式宏要更加灵活一些,例如对圆面积计算的方法,我们就可以将其定义成一个宏:


#define PI 3.1415926

#define CircleArea(r) PI * r * r

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       // insert code here...

       CGFloat res = CircleArea(1);

       NSLog(@"%f", res);

   }

   return 0;

}

现在,有了这个面积计算宏我们可以更加方便的计算圆的面积了,看上去很完美,后面我们就使用这个函数式宏为例,来深入理解宏的原理。


三、从一个简单的函数式宏说起


    再来看下上面我们编写的计算面积的宏,正常情况下好像没什么问题,但是需要注意,归根结底宏并不是函数,如果完全把其作为函数使用,我们就可能会陷入一系列的陷阱中,比如这样使用:


#define PI 3.1415926

#define CircleArea(r) PI * r * r

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       // insert code here...

       CGFloat res = CircleArea(1 + 1);

       NSLog(@"%f", res);

   }

   return 0;

}

运行代码,运算的结果并不是半径为2个圆的面积,哪里出了问题呢,我们还是先看下宏预编译后的结果:


CGFloat res = 3.1415926 * 1 + 1 * 1 + 1;

一目了然了,由于运算符的优先级问题导致了运算顺序错误,在编程中,所有运算符优先级产生的问题都可以使用一种方式解决:用小括号。对CircleArea宏进行一下改造,如下:


#define CircleArea(r) (PI * (r) * (r))

对执行顺序进行了强制的控制,代码执行又恢复了正常,看上去好像是没有问题了,现在就满意了还为时过早,例如下面这样使用这个宏:


#import <Foundation/Foundation.h>

#define PI 3.1415926

#define CircleArea(r) PI * (r) * (r)

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       // insert code here...

       int r = 1;

       CGFloat res = CircleArea(r++);

       NSLog(@"%f, %d", res, r);

   }

   return 0;

}

运行,发现结果又错了,不仅计算结果与我们的预期不符,变量自加的的结果也不对了,我们检查其展开的结果:


CGFloat res = 3.1415926 * (r++) * (r++);

原来问题出在这里,宏在展开的时候,将参数替换了两次,由于参数本身是一个自加表达式,所以被自加了两次,产生了问题,那么这个问题怎么解决呢,C语言中有一种很有用的语法,即使用大括号定义代码块,代码块会将最后一条语句的执行结果返回,修改上面宏定义如下:


#import <Foundation/Foundation.h>

#define PI 3.1415926

#define CircleArea(r)   \

({                      \

   typeof(r) _r = r;   \

   (PI * (_r) * (_r)); \

})

int main(int argc, const char * argv[]) {

   @autoreleasepool {

     

       int r = 1;

       CGFloat res = CircleArea(r++);

       NSLog(@"%f, %d", res, r);

   }

   return 0;

}

这次程序又恢复的了正常。但是,如果如果在调用宏是变量的名字与宏内的临时变量产生了重名,灾难就又发生了,例如:


#import <Foundation/Foundation.h>

#define PI 3.1415926

#define CircleArea(r)   \

({                      \

   typeof(r) _r = r;   \

   (PI * (_r) * (_r)); \

})

int main(int argc, const char * argv[]) {

   @autoreleasepool {

     

       int _r = 1;

       CGFloat res = CircleArea(_r);

       NSLog(@"%f, %d", res, _r);

   }

   return 0;

}

运行上面代码,会发现宏内的临时变量没有被初始化成功。这确实难受,我们在进一步,比如对临时变量的名字做一些手脚,将其命名为极其不容易重复的名字,其实系统内置的一个宏就是专门用来构造唯一性变量名的:__COUNTER__,这个宏是一个计数器,在编译的时候会自动进行累加,再次对我们编写的宏进行改造,如下:


#import <Foundation/Foundation.h>

#define PI 3.1415926

#define PAST(A, B) A##B

#define CircleArea(r)   __CircleArea(r, __COUNTER__)

#define __CircleArea(r, v)      \

({                              \

   typeof(r) PAST(_r, v) = r;         \

   (PI * PAST(_r, v) * PAST(_r, v));     \

})

int main(int argc, const char * argv[]) {

   @autoreleasepool {

       int _r = 1;

       CGFloat res = CircleArea(_r);

       CGFloat res2 = CircleArea(_r);

       NSLog(@"%f, %f", res, res2);

   }

   return 0;

}

这里改造后,我们的宏就没有那么容易理解了,首先__COUNTER__在每次宏替换时都会进行自增,##是一种宏中专用的特殊符号,用来将参数拼接到一起,但是需要注意,使用##符号拼接的如果是另外一个宏,则其会阻止宏的展开,因此我们定义了一个转换宏PAST(A, B)来处理拼接。如果你一下子不能理解为什么这样就可以解决宏展开的问题,你只需要记住这样一条宏展开的原则:如果形参有使用#或##这种处理符号,则不会进行宏参数的展开,否则先展开宏参数,在展开当前宏。上面代码最终预编译的结果如下:


int main(int argc, const char * argv[]) {

   @autoreleasepool {

       int _r = 1;

       CGFloat res = ({ typeof(_r) _r0 = _r; (3.1415926 * _r0 * _r0); });

       CGFloat res2 = ({ typeof(_r) _r1 = _r; (3.1415926 * _r1 * _r1); });

       NSLog(@"%f, %f", res, res2);

   }

   return 0;

}

一个简单的计算圆面积的宏,为了安全,我们就进行了这么多的处理,看来要用好宏,的确不容易。

目录
相关文章
|
iOS开发
iOS宏定义里面多参数输入
想实现类似NSLog那样支持多参数,和格式化等逻辑处理的宏
107 0
|
iOS开发
iOS常用的宏定义
iOS常用的宏定义
409 0
|
iOS开发
iOS 宏定义应用
iOS 宏定义应用
133 0
|
安全 iOS开发
玩转iOS“宏定义”(二)
玩转iOS“宏定义”
206 0
|
API iOS开发
iOS宏定义的使用与规范
http://my.oschina.net/leejan97/blog/354904 宏定义在很多方面都会使用,例如定义高度、判断iOS系统、工具类,还有诸如文件路径、服务端api接口文档。为了对宏能够快速定位和了解其功能,我们最好在定义的时候将其放入特定的头文件中,下面我抛砖引玉,对一些常用的宏进行分类、分文件定义,希望对大家有所帮助。
728 0
|
8天前
|
IDE 开发工具 Android开发
安卓与iOS开发对比:平台选择对项目成功的影响
【9月更文挑战第10天】在移动应用开发的世界中,选择正确的平台是至关重要的。本文将深入探讨安卓和iOS这两大主要移动操作系统的开发环境,通过比较它们的市场份额、开发工具、编程语言和用户群体等方面,为开发者提供一个清晰的指南。我们将分析这两个平台的优势和劣势,并讨论如何根据项目需求和目标受众来做出最佳选择。无论你是初学者还是有经验的开发者,这篇文章都将帮助你更好地理解每个平台的特性,并指导你做出明智的决策。
|
6天前
|
API Android开发 iOS开发
安卓与iOS开发中的线程管理对比
【9月更文挑战第12天】在移动应用的世界中,安卓和iOS平台各自拥有庞大的用户群体。开发者们在这两个平台上构建应用时,线程管理是他们必须面对的关键挑战之一。本文将深入探讨两大平台在线程管理方面的异同,通过直观的代码示例,揭示它们各自的设计理念和实现方式,帮助读者更好地理解如何在安卓与iOS开发中高效地处理多线程任务。
|
8天前
|
开发框架 Android开发 iOS开发
探索安卓与iOS开发的差异:构建未来应用的指南
在移动应用开发的广阔天地中,安卓与iOS两大平台各占半壁江山。本文将深入浅出地对比这两大操作系统的开发环境、工具和用户体验设计,揭示它们在编程语言、开发工具以及市场定位上的根本差异。我们将从开发者的视角出发,逐步剖析如何根据项目需求和目标受众选择适合的平台,同时探讨跨平台开发框架的利与弊,为那些立志于打造下一个热门应用的开发者提供一份实用的指南。
21 5
|
8天前
|
开发工具 Android开发 iOS开发
安卓与iOS开发:平台选择的艺术与科学
在移动应用开发的广阔天地中,安卓与iOS两大平台如同东西方哲学的碰撞,既有共通之处又各具特色。本文将深入探讨这两个平台的设计理念、开发工具和市场定位,旨在为开发者提供一份简明扼要的指南,帮助他们在这场技术与商业的博弈中找到自己的道路。通过比较分析,我们将揭示每个平台的优势与局限,以及它们如何影响应用的性能、用户体验和市场接受度。无论你是初涉江湖的新手,还是经验丰富的老手,这篇文章都将为你的选择提供新的视角和思考。
19 5
|
8天前
|
开发工具 Android开发 Swift
探索安卓与iOS开发的差异:从新手到专家的旅程
在数字时代的浪潮中,移动应用开发已成为连接世界的桥梁。本文将深入探讨安卓与iOS这两大主流平台的开发差异,带领读者从零基础出发,逐步了解各自的特点、开发环境、编程语言及市场策略。无论你是梦想成为移动应用开发者的初学者,还是希望扩展技能边界的资深开发者,这篇文章都将为你提供宝贵的见解和实用的建议。