iOS開發(fā)Tips:objective-c指針解引用

C#語言中很早就開始有了屬性這個概念了,而且很方便使用,也很符合面向?qū)ο蟮奶匦裕琌C2.0的時候也加入了對屬性的支持,屬性用起來確實(shí)方便順手,甚至現(xiàn)在有開發(fā)者可能從來就沒定義過成員變量。本篇文章通過一種對成員變量很不常見的訪問方式來引申出指針變量的本質(zhì)和用法;

內(nèi)容提要

  • 1.我們熟知的訪問形式
  • 2.通過指針解引用訪問
  • 3.一切起源于C語言指針變量解引用
  • 4.iOS開發(fā)中指針變量解引用的使用場景

示例代碼

@interface Girl : NSObject {
    @public
    NSString *_name;
}
@property (nonatomic, copy) NSString *name;

@end

@implementation Girl

@end

1. 我們熟知的訪問形式

  • 屬性訪問式 :
    這是我們最容易掌握的一種使用方式,所以甚至有的開發(fā)者在開發(fā)中只會定義屬性
    girl.name = @"Lucy";

  • 指針訪問式 :
    作為一個有潔癖的程序員,更多時候還是定義成員變量而不是屬性,因?yàn)橹辽贉p少了一次方法調(diào)用,減少了內(nèi)存占用
    girl->_name = @"Lily";

  • KVC訪問式 :
    如果一個類的成員變量是私有的,然后我想訪問它,可以使用KVC的方式
    [girl setValue:@"Poly" forKey:@"name"];

2. 通過指針解引用訪問(有些腦洞的訪問方式)

  • 指針解引用法:
    大家應(yīng)該只用到上面介紹的用法,但OC作為C語言的超集,下面這個用法雖然奇葩但確實(shí)正確無疑的
    (*girl)._name = @"Tom";

為什么我要介紹這種奇葩用法

3. 一切起源于C語言指針變量解引用

C語言中指針變量的值存儲的是一個地址,這個地址指向一個其他變量的(可以是基本類型,結(jié)構(gòu)體,甚至其他指針變量),而當(dāng)我們想要操作這個地址指向的變量時我們需要對指針變量解引用,可以認(rèn)為取地址&和解引用*是一組相對的操作,可以相互轉(zhuǎn)化;

  • 基本類型的指針操作:
    int a = 1;
    int *p1 = &a; // 指針變量p1指向了int變量a的地址(用&符號取地址)
    int b = *p1;  // 效果等同于b = a; *p1取得的是p1指針指向的變量
  • 結(jié)構(gòu)體的指針操作:
    // 定義一個結(jié)構(gòu)體
    typedef struct DemoStruct{
        int age;
    } DemoStruct;
    
    DemoStruct st;
    DemoStruct *p1 = &st; // 指向結(jié)構(gòu)體st的指針
    p1->age = 100; // p1指向的結(jié)構(gòu)體變量的age
    (*p1).age = 101; // 結(jié)構(gòu)體(*p1)的age
    DemoStruct st1 = (*p1); // 效果等同于st1 = st;
我們知道OC中的類最終都會轉(zhuǎn)化成一個結(jié)構(gòu)體而存在,而類的實(shí)例對象其實(shí)是一個指向該結(jié)構(gòu)體的指針,比如id類型就是一個結(jié)構(gòu)體指針,所以對于實(shí)例對象我們可以像C語言(指針)一樣操作,所以我們可以使用obj->語法和(*obj)語法,這兩種使用起來是等效的,至于為什么會有兩種用法可以去百度一下這個歷史問題;
所謂的OC對象其實(shí)是個指針變量

4. iOS開發(fā)中變量取地址和指針解引用的使用場景

  • 值類型的引用傳參

如果想在方法內(nèi)部改變外部傳遞進(jìn)來的基本數(shù)據(jù)類型變量我們可以像下面一樣通過地址引用傳參,其實(shí)傳遞的是一個指針參數(shù),使用的時候通過指針解引用可以獲取外面定義的變量:
- (BOOL)takeMoney:(NSUInteger *)money {
    *money = 100; // 解引用后進(jìn)行賦值
    return YES;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSUInteger money;
    NSUInteger *pMoney = &money;
    // 傳參的時候,使用&取得變量的地址
    if ([self takeMoney:&money]) {
    // if ([self takeMoney:pMoney) { // 這里使用這行代碼可達(dá)到同樣的效果
        NSLog(@"取錢成功,本次取出%lu", money);
    }
// 系統(tǒng)數(shù)組遍歷方法中也包含一個BOOL類型的引用傳參,因?yàn)閎lock的調(diào)用者想知道block執(zhí)行后的這個stop值,所以使用引用傳參
    [@[] enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        *stop = YES;
    }];
  • 對象類型的引用傳參

OC的對象是一個結(jié)構(gòu)體指針對象,所以如果從C語言的用法來說我們可以傳遞這個指針對象,然后在方法內(nèi)部通過指針解引用,改變這個結(jié)構(gòu)體的值,但是實(shí)際上是不可行的,我們需要遵循OC對象的生成方式,經(jīng)過alloc init等方法才能創(chuàng)建一個正確的對象:

結(jié)構(gòu)體引用穿參(傳地址)

typedef struct MyStruct {
    int a;
    int b;
} MyStruct;

- (void)method1:(MyStruct *)myStruct {
    *myStruct = ({
        MyStruct st;
        st.a = 1;
        st.b = 2;
        st;
    });
}

- (void)viewDidLoad {
    [super viewDidLoad];

    MyStruct s;
    [self method1:&s]; // 這行代碼運(yùn)行后s變量的值會被改變

嘗試把OC類當(dāng)做普通結(jié)構(gòu)體來用(當(dāng)然報(bào)錯)

    MyStruct *st1;
    MyStruct *st2;
    *st1 = *st2; // 沒問題,結(jié)構(gòu)體可以這樣賦值
    
    UIView *v = [UIView new];
    UIView *v1 = [UIView new];
    *v = *v1; // 編譯報(bào)錯 can not assign class object ('UIView' invalid)

OC方法內(nèi)部改變外部OC對象只能使用指針的指針方式了

@interface MyObject : NSObject {
 @public
    NSInteger _age;
    NSInteger _score;
}
@end

@implementation MyObject

- (NSString *)description {
    return [NSString stringWithFormat:@"age:%ld, score:%ld", _age, _score];
}

@end
- (void)method2:(MyObject **)obj {
    // obj是一個指針變量(指向另一個指針變量 = MyObject的實(shí)例對象)
    // *obj將obj解引用后將得到這個指針指向的一個指針變量 = 外部的MyObject的實(shí)例對象
    *obj = ({
        MyObject *o = [MyObject new];
        o->_age = 22;
        o->_score = 59;
        o;
    });
    
    // 這里的ob改變后只是將指針指向了另外一個對象并不能改變外部的MyObject的實(shí)例對象
    __autoreleasing MyObject *o1 = ({
        MyObject *o1 = [MyObject new];
        o1->_age = 25;
        o1->_score = 60;
        o1;
    });;
    obj = &o1;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *o;
    [self method2:&o];
    NSLog(@"調(diào)用方法后的變量o:%@", o); // 輸出:調(diào)用方法后變量O:age:22, score:59

Foundation框架中有不少的API方法都使用了對象地址傳參的方式
比如:

- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error NS_AVAILABLE(10_5, 2_0);

現(xiàn)在回到上面那個奇葩的用法

// girl可以看做是一個結(jié)構(gòu)體指針,我們需要訪問這個結(jié)構(gòu)體成員變量時,只需要對這個指針解引用
// (*girl)可以看做是一個結(jié)構(gòu)體,可以訪問它的成員,但不能對它進(jìn)行賦值
(*girl)._name = @"Tom";

總結(jié)總結(jié):

因?yàn)镺C方法的形參變量都是對實(shí)參變量進(jìn)行了拷貝(拷貝基本類型變量的值,或者拷貝指針變量存儲的地址),如果方法內(nèi)部想改變外部的變量那么只要傳遞外部變量的地址即可;

.

不過(*girl)._name這個用法與girl->_name相比確實(shí)沒有什么好處,敲起來也不順手,不值的模仿

--------- 重要更新 -----------

著重提一下:文中method2中關(guān)于形參,實(shí)參,傳值方式的注釋本身是沒有問題的,但是有細(xì)心的朋友運(yùn)行后發(fā)現(xiàn)原本方法中的*obj和方法外的形參&o理論上應(yīng)該是相等,然而運(yùn)行后并不相等,這個問題的原因歸結(jié)為編譯器的優(yōu)化。這個優(yōu)化是基于下面2個事實(shí)

事實(shí)一:OC的內(nèi)存管理的三大原則其中一條是“誰生成的對象誰負(fù)責(zé)釋放”,那么method2內(nèi)部生成的對象就應(yīng)該method2負(fù)責(zé)釋放,但是這個對象生成本來就是給方法外部使用的,所以不能在方法作用域結(jié)束的時候直接釋放該對象,而是要延遲釋放,沒錯,將這個對象加入到自動釋放池中即可;這點(diǎn)系統(tǒng)編譯器已經(jīng)給我們做了。編譯器會將方法轉(zhuǎn)換成如下

// 方法內(nèi)部給obj指向的對象賦值以后會添加到自動釋放池中
- (void)method2:(MyObject * __autoreleasing *)obj {

事實(shí)二:__strong修飾的局部變量,編譯器會在方法結(jié)束之前調(diào)用release方法釋放該局部變量,這種前提下method2內(nèi)部生成的對象(引用計(jì)數(shù)1)已經(jīng)由自動釋放池來管理了,如果o在出作用域之前調(diào)用了release(引用計(jì)數(shù)變?yōu)?), 那么在自動釋放池清理調(diào)用該對象的release方法導(dǎo)致過度釋放出現(xiàn)崩潰; 所以系統(tǒng)編譯器自動為我們做了轉(zhuǎn)換,由于編譯器是不會改變我們聲明的對象o的內(nèi)存管理方式,所以會再生成了一個臨時對象,最后在方法調(diào)用完以后又將臨時對象temp賦給o,這樣就給我們造成一直使用的都是o的假象,真是完美的偽裝。

NSObject *o; 
[self method2:&o]; 

上面代碼在編譯器會將它變成下面的樣子

__strong NSObject *o; // nil
__autoreleasing NSObject *temp = o;  // nil,這句話會將temp指向的對象加入到自動釋放池,因?yàn)榇藭r指向?qū)ο鬄閚il,系統(tǒng)不會將這個nil添加到自動釋放池
[self method2:&temp]; // 調(diào)用后temp指向?qū)ο笠糜?jì)數(shù)為1
o = temp; // 因?yàn)閛是__strong修飾的,所以此時o指向?qū)ο笠糜?jì)數(shù)變?yōu)? , 最后當(dāng)o出作用域時會調(diào)用一次release, 然后自動釋放池會調(diào)用一次release 

引申思考:

  1. 如果我們聲明的對象NSObject *o前面加上__autoreleasing會怎樣?
__autoreleasing  NSObject *o; 
  1. 如果我們method2方法參數(shù)強(qiáng)制使用__strong修飾會怎樣?
- (void)method2:(MyObject * __strong *)obj {

|
|
|
|
|

|

|

|

|
|
|
|

  1. 結(jié)果就是編譯器不會再為我們添加temp的臨時變量了,這樣我們方法調(diào)用前的&o和方法內(nèi)部的obj是相等的;這種就避免了編譯器的優(yōu)化,但編譯器優(yōu)化同時會帶來額外的代價(至少多了2行代碼),所以如果你想更好的話,就在變量聲明的時候顯示指定用__autoreleasing來修飾?!就扑]做法】
  2. 因?yàn)閰?shù)obj是__strong修飾的所以方法內(nèi)部賦值的時候*obj = xxx 就會使指向的對象的引用計(jì)數(shù)+1,因?yàn)?obj指針(外面的o)并不是在方法內(nèi)部聲明的,所以方法結(jié)束之前編譯器并不會插入[*obj release]所以該對象在出了方法作用域以后也不會釋放, 等外層調(diào)用方法viewDidLoad執(zhí)行之前,編譯器會添加[o release]方法從而釋放該對象;這種方式雖然是可行的但是卻要求我們每次都指定__strong修飾符,并且這種方式的內(nèi)存管理其實(shí)不符合內(nèi)存管理的“誰申請誰釋放”的原則,所以系統(tǒng)默認(rèn)缺省修飾符就是__autoreleasing;【不推薦使用】
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容