玩转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那样支持多参数,和格式化等逻辑处理的宏
113 0
|
iOS开发
iOS常用的宏定义
iOS常用的宏定义
417 0
|
iOS开发
iOS 宏定义应用
iOS 宏定义应用
138 0
|
安全 iOS开发
玩转iOS“宏定义”(二)
玩转iOS“宏定义”
224 0
|
API iOS开发
iOS宏定义的使用与规范
http://my.oschina.net/leejan97/blog/354904 宏定义在很多方面都会使用,例如定义高度、判断iOS系统、工具类,还有诸如文件路径、服务端api接口文档。为了对宏能够快速定位和了解其功能,我们最好在定义的时候将其放入特定的头文件中,下面我抛砖引玉,对一些常用的宏进行分类、分文件定义,希望对大家有所帮助。
739 0
|
29天前
|
Java Android开发 Swift
安卓与iOS开发对比:平台选择对项目成功的影响
【10月更文挑战第4天】在移动应用开发的世界中,选择合适的平台是至关重要的。本文将深入探讨安卓和iOS两大主流平台的开发环境、用户基础、市场份额和开发成本等方面的差异,并分析这些差异如何影响项目的最终成果。通过比较这两个平台的优势与挑战,开发者可以更好地决定哪个平台更适合他们的项目需求。
97 1
|
1月前
|
设计模式 安全 Swift
探索iOS开发:打造你的第一个天气应用
【9月更文挑战第36天】在这篇文章中,我们将一起踏上iOS开发的旅程,从零开始构建一个简单的天气应用。文章将通过通俗易懂的语言,引导你理解iOS开发的基本概念,掌握Swift语言的核心语法,并逐步实现一个具有实际功能的天气应用。我们将遵循“学中做,做中学”的原则,让理论知识和实践操作紧密结合,确保学习过程既高效又有趣。无论你是编程新手还是希望拓展技能的开发者,这篇文章都将为你打开一扇通往iOS开发世界的大门。
|
1月前
|
搜索推荐 IDE API
打造个性化天气应用:iOS开发之旅
【9月更文挑战第35天】在这篇文章中,我们将一起踏上iOS开发的旅程,通过创建一个个性化的天气应用来探索Swift编程语言的魅力和iOS平台的强大功能。无论你是编程新手还是希望扩展你的技能集,这个项目都将为你提供实战经验,帮助你理解从构思到实现一个应用的全过程。让我们开始吧,构建你自己的天气应用,探索更多可能!
61 1
|
2月前
|
IDE Android开发 iOS开发
探索Android与iOS开发的差异:平台选择对项目成功的影响
【9月更文挑战第27天】在移动应用开发的世界中,Android和iOS是两个主要的操作系统平台。每个系统都有其独特的开发环境、工具和用户群体。本文将深入探讨这两个平台的关键差异点,并分析这些差异如何影响应用的性能、用户体验和最终的市场表现。通过对比分析,我们将揭示选择正确的开发平台对于确保项目成功的重要作用。
|
2天前
|
设计模式 前端开发 Swift
探索iOS开发:从初级到高级的旅程
【10月更文挑战第31天】在这篇文章中,我们将一起踏上iOS开发的旅程。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和技巧。我们将从基础开始,逐步深入到更高级的技术和概念。让我们一起探索iOS开发的世界吧!