刚开始开始接触OC时,对*、& 甚至 ** 这些符号都很茫然,但是急于学习更多功能上的东西,也就没有深究,基本上就是照着写的;后来习惯成自然了,也就随手都会码进去了;随着接触的越来越多,也会逐渐了解一些他们的意义,但是都很零散,不是很系统,印象也就非常不深刻。
直到我看到了不上火喝纯净水的文章,通读之后,感觉豁然开朗!之前的零碎知识点都融会贯通到一起了!为了加深印象,也希望能让各位大佬们指出可能存在的问题,特将该片文章结合自己的理解再更加详细的解读一下。
如何理解OC中的*(星号)、&(取地址符)和 **(两个星号)呢?(这行只是为了方便搜索引擎检索到。。。无视即可。。。)
1. 指针变量
指针是什么?指针就是一个对象的内存地址。
指针变量呢?形象一点:一个两节的盒子,第一节是当前指针变量的地址,第二节是他指向的对象的内存地址。这也就是为什么指针变量被称为指向其他对象的对象。
OC中最常见也是我们最开始接触到的:
NSString *str = @"XTShow";
可能这种形式用的实在是太多了,估计已经不能吸引大家的兴趣了。
int a = 1;
int *p1 = &a;
一般基本数据类型是使用不到指针变量进行间接引用的,但是实际上是可以指向的,因为无论你是什么类型,都要有内存地址,而只要有内存地址,就逃不过我指针变量的“千里夺命追魂”指的,就可以创建一个指向你的指针变量。
再来详细解读下这里:
int *p1
表示创建了一个指针变量p1,*
表示p1是一个指针变量,int
表示该指针变量指向的对象的类型;
&
表示取a的内存地址。
那么整行代码也就表示,将a的内存地址赋给指针变量p1的指向对象地址空间,或者说,让指针变量p1指向a。
接着上面的
int b = *p1;
这句的效果等同于:b = a。我个人的理解是,同样是*变量名
,在=
左右两侧时(这里是通俗的说法,实际上是指是set(设置)还是get(取),不要完全只通过=
的位置来做判断),代表的意义不同。在等号左侧时,代表给指针变量的指向对象地址部分赋值,也就是改变指针变量的指向;而在等号右侧时,代表的是直接取出指向的对象(这里以及全文中的某些对象并不是等同于OC中的对象,而只是一个值或者量)。
如果上面的理解了,我们继续使用一个再稍显复杂一点的例子来演示指针变量:结构体。
typedef struct DemoStruct{
int age;
} DemoStruct;
DemoStruct st;
DemoStruct *p2 = &st;//创建一个指针变量p2,指向结构体的实例对象
p2->age = 100;//通过"->"的方式来向指针变量指向的结构体的具体内容赋值
//st->age = 50;//结构体自身是不支持这种指向的
(*p2).age = 200;//*p2直接取其指向的对象,此时也就相当于结构体st了,因此可以直接通过点语法进行赋值
DemoStruct st1 = (*p2);//原理同上的"int b = *p1;"
回味一下会发现,所有的OC对象我们在初始化的时候,都是使用类名 * 变量名的方式完成的。也就是说,我们实际上创建的都是一个个的指针变量。
而且runtime.h
中:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
在objc.h
中:
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
由此可以发现,OC中无论是类还是实例变量,都是一个指向结构体的指针变量(实例变量是一个结构体,但其中只有一个指向其所属类的指针变量,因此其间接地也就实现了指针变量的效果)。
2. 实际应用
在OC的方法中,例如:
-(void)createTableViewWithName:(NSString *)name;
传入方法的name对象和方法内部是不一样的。
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"XTShowXTShowXTShow";//使长度大于9位,避免NSTaggedPointerString的影响
NSLog(@"1.%p--%p",&str,str);//前者是指针变量p自身的内存地址;后者是指向对象的内存地址
[self logForObj:str];
}
-(void)logForObj:(NSString *)str{
NSLog(@"2.%p--%p",&str,str);
}
打印结果
1.0x16bd55400--0x104123450
2.0x16bd553b8--0x104123450
由此可证:方法内部会自行生成一个指针变量,来指向传入的指针变量指向的对象。(小扩展下:这个局部变量由系统自行管理,存放在栈区)
一般情况下,我个人的习惯,传入方法的参数,在方法内部的修改并不会影响到传入处前后的值;如果需要有影响,也是通过方法返回值的方式,来得到在方法内经过处理后的值。
但是如果不采用这种方式,能够得到方法处理后的值吗?也就是让方法内部对参数的处理,同样在参数传入处生效。
答案是可以的。结合刚刚得出的结论:方法内部会自行生成一个指针变量,来指向传入的指针变量指向的对象可以发现,在作为参数传递时,有变的有不变的,那么把变的包装成不变的不就好了吗~具体点,既然指针变量会在方法内生成临时的,去指向作为参数传入的指针变量指向的对象,那么就把原本传入的指针变量再包一层,变成另一个指针变量指向的对象,这样就能保证,我可以得到与方法外一模一样的参数。
文字描述起来好绕啊。。。上代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"XTShowXTShowXTShow";
printf("\n1.方法外部:\n指针变量自身地址:%p\n指针变量指向地址:%p\n",&str, str);
[self testStr:&str];
printf("\n4.方法外部:\n指针变量自身地址:%p\n指针变量指向地址:%p\n",&str, str);
NSLog(@"\n5.str:%@",str);
}
-(void)testStr:(NSString **)str{
printf("\n2.方法内部:二重指针\n指针变量自身地址:%p\n指针变量指向地址:%p\n",&str,str);
printf("\n3.方法内部:一重指针\n指针变量自身地址:%p\n指针变量指向地址:%p\n",&(*str),*str);
*str = ({
NSString *str = @"75";
str;
});
}
log结果如下:
1.方法外部:
指针变量自身地址:0x16ba61400
指针变量指向地址:0x104417450
2.方法内部:二重指针
指针变量自身地址:0x16ba613b8
指针变量指向地址:0x16ba613f8
3.方法内部:一重指针
指针变量自身地址:0x16ba613f8
指针变量指向地址:0x104417450
4.方法外部:
指针变量自身地址:0x16ba61400
指针变量指向地址:0x1044175f0
5.str:75
...
...
...
什么鬼!!!怎么和想象的不一样!
虽然确实达到了改变方法外部参数值的效果了,但是2中方法内部二重指针指向的指针变量的地址,不应该就是1中方法外的指针自身的地址吗?怎么实际上不是呢!
难道我之前的理解都是错的吗。。。
后来我又翻来覆去想了两三个小时,发现只要是上面的思路,就一定对不上。。。然后我就抱着试试看的态度,找到了文章开始的地方提到的不上火喝纯净水。
大佬还为了我专门更新了文章!简直感激涕零啊!
难道我前面说的都是错的?让各位小伙伴浪费了半天时间吗?我擦!那岂不是应该拉出去枪毙俩小时!恩~所以结论是:上面的思路是正确的!
那么为什么会出现上面打印地址不一致的情况呢?且听我娓娓道来~
1 - 先把符合预期的示例鼓捣出来再说
大神给出的提示是:OC的编译器优化导致的。那么好,我就找一个能够拜托OC编译环境的途径。由于
Objective-C是C语言的严格超集--任何C语言程序不经修改就可以直接通过Objective-C编译器,在Objective-C中使用C语言代码也是完全合法的。Objective-C被描述为盖在C语言上的薄薄一层,因为Objective-C的原意就是在C语言主体上加入面向对象的特性。Objective-C的面向对象语法源于Smalltalk消息传递风格。所有其他非面向对象的语法,包括变量类型,预处理器(preprocessing),流程控制,函数声明与调用皆与C语言完全一致。
——wikipedia
因此我使用C语言重现了一次上文中的情景:
void testForOC(char ** p);
int main(int argc, const char * argv[]) {
char *str= "XTShow";
printf("1. %p\n",&str);
testForOC(&str);
return 0;
}
void testForOC(char ** p){
printf("2. %p\n",p);
}
log结果如下:
1. 0x7ffeefbff608
2. 0x7ffeefbff608
Nice!这样就一致了!也就与预期的结论一致了,也就可以说明,上面不一致的结论是OC特有的,也就存在是因为OC编译器优化的可能性。
2 - OC编译器到底做了怎么的优化呢?
上文中我们定义了这样一个方法:
-(void)testStr:(NSString **)str;
但是在使用的时候,会出现这样的情况:
也就是说,此处编译器会默认对传入的对象加一个
__autoreleasing
修饰符。__autoreleasing
:将其修饰的对象加入autoreleasepool,也就会起到延缓释放的作用。
那么OC编译器为什么要添加这个呢?
因为本着“谁创建谁释放”内存管理原则,在方法内部创建的对象(指向传入的&str),在方法结束后,就会自动释放,但此处我们费劲千辛万苦,还特意用**生成了指针的指针,就是为了在方法外部使用他的,怎么能让方法自己爽完了就随随便便释放掉呢?因此会自动添加__autoreleasing
对其所指向的对象,也就是外部的&str
进行修饰,保证其在方法结束后,也不会被方法释放掉。(感觉他命好苦啊,方法用完他就要抛弃他😔,还好编译器爱他☺️)
这又和地址不对应有什么关系呢?
解释这个问题还需要再扩展一点:
NSError *strongError;//不特意指明时默认是__strong
NSError * __strong *strongErrorPoint = &strongError;
//NSError * __weak *weakErrorPoint = &strongError;//报红错
NSError __weak * weakError;
NSError * __weak *weakErrorPoint = &weakError;
NSError __unsafe_unretained * unsafeError;
NSError * __unsafe_unretained *unsafeErrorPoint = &unsafeError;
weakError = strongError;//虽然上面的指针指向没有任何兼容,但是各类对象间是可以相互赋值的
我们可以看出,指针变量在经过__strong/__weak/__unsafe_unretained修饰后,指向他的指针变量就必须使用相同的修饰符进行修饰,才能正确指向。
但这些指针变量之间相互赋值时可以的,也就是修改他们的指向。
因此我们就可以推测:编译器的优化不只是在方法的参数前多加一个__autoreleasing
,还使用了一个__autoreleasing
修饰的临时变量来承接我们想要传入的变量。
NSString *str = @"XTShowXTShowXTShow";
[self testStr:&str];
//===>模拟内部可能发生的变化
NSString *str = @"XTShowXTShowXTShow";
__autoreleasing NSString *tempStr = str;//使tempStr和str指向同一个对象
[self testStr:&tempStr];
str = tempStr;
这样就实现了,既符合OC内存管理要求,又能修改方法外对象的值的需求;也就解释了为什么上面log的地址不一致。
再优化下,既然是由于修饰符不一致的原因造成的隐式的优化,那么我们可不可以自己创建传入方法的对象时就使用满足要求的__autoreleasing
修饰呢?
__autoreleasing NSString *str = @"XTShowXTShowXTShow";
[self testStr:&str];
这样就可以避免编译器再使用一个临时变量来转接我们自己的变量了。而且通过验证可以发现,这次的log地址也完全符合预期了。
再想想,可以让我们创建的对象“适应”编译器给方法自动生成的__autoreleasing
,那么能不能让方法来“适应”我们创建的对象呢?
-(void)testStr:(NSString * __strong *)str;
一般对付隐式的方式就是用显式来覆盖他。
这种方式中有一个小点需要注意下,就是方法内生成的变量str,正常来说,应该随着方法的结束而被释放掉;但是这里是由一个外部存在的指针指向了他,也就增加了他的引用计数,也就使其不会在testStr:
方法结束时被释放掉,也就保证了方法外部的指针指向的值在方法结束后仍是有效值,也就起到了改变外部指针指向的值的作用。
系统自身的使用实例
通过取地址符&
来使用的对象,最常用的应该就是很多方法中传入的error了。当时可能只是因为习惯而多码了一个&,现在回过来看一下:
果然!这里也是**,即指针的指针;也有
__autoreleasing
修饰;也是为了达到修改方法外对象的需求,与刚才的情况完全一致。不仅侧面验证了刚才分析的正确性,也搞懂了之前没有深究的地方。
希望通过这篇文章,各位小伙伴能够对OC中的 * 、& 和 **有一个更清晰的认识吧~