如何理解OC中的*、&和**呢?

刚开始开始接触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修饰符。
__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中的 * 、& 和 **有一个更清晰的认识吧~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,340评论 8 265
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,093评论 1 32
  • OC语言基础 1.类与对象 类方法 OC的类方法只有2种:静态方法和实例方法两种 在OC中,只要方法声明在@int...
    奇异果好补阅读 4,259评论 0 11
  • 307、setValue:forKey和setObject:forKey的区别是什么? 答:1, setObjec...
    AlanGe阅读 1,534评论 0 1
  • 这几年最大的收获就是 一.以前任性的总觉得全世界应该理解自己,就算现在不理解,时间久了也应该理解。 而现在自己学着...
    各种扯犊子阅读 225评论 0 0