预处理指令
什么是预处理指令:
在我们的文件翻译成0和1之前做的操作我们称之为预处理指令
一般情况预处理指令都是以#号开头的
宏定义
宏定义的格式
1.不带参数的宏定义
2.带参数的宏定义
1.不带参数的宏定义:
#define 宏名 值
宏定义的作用:
会在程序翻译成0和1之前, 将所有宏名替换为 宏的值
宏定义在什么时候替换
源代码 --> 预处理 -->汇编 -->二进制 -->可执行程序
规范:
一般情况宏名都大写, 多个单词之间用_隔开, 并且每个单词全部大写
有得公司又要求宏名以k开头, 多个单词之间用驼峰命名
注意:
宏定义后面不要写分号
宏定义也有作用域
从定义的那一行开始, 一直到文件末尾
虽然默认情况下宏定义的作用域是从定义的那一行开始, 一直到文件末尾. 但是我们也可以通过对应的关键字提前结束宏定义的作用域
#define COUNT 6
// 提前结束宏定义的作用域
#undef COUNT
宏定义的使用场景:
http://192.168.13.11/accesstoken
#define BASE_URL "http://192.168.13.11/"
获取屏幕的宽度
获取手机系统版本号
做一个单利
判断系统版本
2.带参数的宏定义
#define PF(v1,v2) ((v1)*(v2))
#define 代表要定义一个宏
PF 宏的名称
((v1)*(v2)) 参数, 注意点, 不需要写数据类型
(v1)*(v2) 用于替换的内容
带参数的宏定义注意点
1.一般情况下建议写带参数的宏的时候, 给每个参数加上一个()
2.一般情况下建议写带参数的宏的时候, 给结果也加上一个()
为了避免使用宏出现计算顺序问题
带参数的宏在展开时,只作简单的字符和参数的替换,不进行任何计算操作。所以在定义宏时,一般用一个小括号括住字符串的参数。
什么时候用带参数的宏定义什么时候用函数
如果函数内部的功能比较简单, 仅仅是做一些简单的运算那么可以使用宏定义, 使用宏定义效率更好, 运算速度更快
如果函数内部的功能比较复杂, 不仅仅是一些简单的运算, 那么建议使用函数
条件编译
#define SCORE 90
#define DEBUG 1 // 0是调试阶段 1是发布阶段
#if DEBUG == 0
// 调试阶段
#define NJLog(format, ...) printf(format,## __VA_ARGS__)
//#elif DEBUG == 0
//#elif == else if
//#ifndef SCORE
// 是不是没有定义名称叫做SCORE的宏,没定义执行后面代码
#else
// 发布阶段
#define NJLog(format, ...)
#endif
条件编译和选则结构if的共同点:
都可以对给定的条件进行判断, 添加满足或者不满足都可以执行特定的代码
条件编译和选则结构if的共区别
0.条件编译和if非常非常像
if选择结构会对给定条件进行判断, 如果条件满足就执行if后面大括号中的内容
条件编译也一样, 会对给定的条件进行判断, 如果条件满足就编译条件后面的内容
1.生命周期不同
if 运行时
#if 编译之前
2.#if需要一个明确的结束符号 #endif
为什么需要一个明确的结束符号?
如果省略掉#endif, 那么系统就不知道条件编译的范围, 那么会将满足条件之后的第二个条件之后的所有内容都清除
3.if会将所有的代码都编译到二进制中
#if只会将满足条件的部分一直到下一个条件的部分 编译到二进制中
注意:
预处理指令什么时候执行? 编译之前
变量什么时候定义? 执行了才会定义
注意点: 条件编译不能用来判断变量, 因为不在同一个生命周期
一般情况下, 条件编译是和宏定义结合在一起使用的
为什么要使用条件编译
1)按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。有利于程序的移植和调试。 2)条件编译当然也可以用条件语句来实现。 但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。
条件编译的优点:
缩小应用程序的大小
应用场景:
用于调试和发布阶段进行测试
调试阶段: 程序写代码的阶段
发布阶段: 上传到AppStore的阶段
typedef
typedef可以给一个已知的数据类型起别名 (外号)
利用typedef给数据类型起别名的格式:
typedef 原有的数据类型 别名(外号);
注意:
1. typedef不仅能给系统原有的数据类型起别名, 也可以给一个自定义的数据类型起别名
2. 利用typedef给数据类型起别名, 并不会生成一个新的数据类型, 仅仅是给原有的类型起了一个别名而已
注意: 如果是给指向函数的指针起别名, 那么指向函数的指针的指针名称就是它的别名
int sum(int v1, int v2)
{
return v1 + v2;
}
// functionPotinter == int(*functionPotinter)(int , int)
typedef int(*functionPotinter)(int , int);
int main(int argc, const char * argv[]) {
// int (*sumP)(int , int ) = sum;
// 指针变量用法
functionPotinter sumP = sum;
printf("sum = %i\n", sumP(10 , 20));
return 0;
}
typedef和宏定义的区别
执行时间:
宏定义是由预处理完成的,而typedef则是在编译时完成的,后者更为灵活方便。
一般情况下如果要给数据类型起一个名称建议用typedef, 不要用define
不然连续定义或许会出错
typedef char * String;
#define MY_STRING char *
// 一般情况下如果要给数据类型起一个名称建议用typedef, 不要用define
int main(int argc, const char * argv[]) {
//正确⭕️
String name1, name2;
name1 = "lnj";
name2 = "lmj";
printf("name1 = %s, name2 = %s\n", name1, name2);
//错误❌
//*号只对最近的一个有效, 所以相当于
// char *name3, name4;
// char *name3; char name4;
MY_STRING name3, name4;
name3 = "lk";
name4 = "xb";
printf("name3 = %s, name4 = %s\n", name3, name4);
return 0;
}
const
1.const修饰变量:
将变量转化成常量
2.const修饰指针:
如果const写在指针类型的左边, 那么意味着指向的内存空间中的值不能改变, 但是指针的指向可以改变
如果const写在指针的数据类型和*号之间, 那么意味着指向的内存空间中的值不能改变, 但是指针的指向可以改变
如果const写在指针的右边(数据类型 * const), 那么意味着指针的指向不可以改变, 但是指针指向的存储空间中的值可以改变
规律:
如果const写在指针变量名的旁边, 那么指针的指向不能变, 而指向的内存空间的值可以变
如果const写在数据类型的左边或者右边, 那么指针的指向可以改变, 但是指向的内存空间的值不能改变
//三种写法
// const int *p = # 指针的指向可以改变
// int const *p = # 指针的指向可以改变
// int * const p = # 指向的内存空间的值可以变
*p = 998; // 修改了指针指向的内存空间中存储的值
p = &age;// 修改了指针的指向
为什么用const?
1.函数参数用const修饰,可以放心用
2.节省空间,避免不必要的内存分配
#define PI 3.14159 //常量宏
const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ...... double i=Pi; //此时为Pi分配内存,以后不再分配!
double I=PI; //编译期间进行宏替换,分配内存
double j=Pi; //没有内存分配
double J=PI; //再进行宏替换,又一次分配内存! const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存 中有若干个拷贝。
常见的宏
// 如何判断当前是ARC还是MRC?
// 可以在编译的时候判断当前是否是ARC
#if __has_feature(objc_arc)
NSLog(@"ARC");
#else
NSLog(@"MRC");
#endif
#define interfaceSingleton(name) +(instancetype)share##name
//使用,替换name参数,
//interfaceSingleton(Tools) +(instancetype)shareTools
interfaceSingleton(Tools);
多行一个宏 \