如何在block内部修改外部变量

本文深入探讨了Objective-C中__block关键字的作用及原理,解释了它如何使得在block中修改外部变量成为可能,以及背后涉及的栈和堆内存管理机制。

默认情况下,在block中访问的外部变量是复制过去的,即:写操作不对原变量生效。但是你可以加上 __block
来让其写操作生效,示例代码如下:

__block int a = 0;
 void (^foo)(void) = ^{
     a = 1;
 };
 foo(); //这里,a的值被修改为1

你同样可以在面试中这样回答,但你并没有答到“点子上”。真正的原因,并没有书这本书里写的这么“神奇”,而且这种说法也有点牵强。面试官肯定会追问“为什么写操作就生效了?”真正的原因是这样的:

我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

Block不允许修改外部变量的值。Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。于是栈区变成了红灯区,堆区变成了绿灯区。

我们可以打印下内存地址来进行验证:

 __block int a = 0;
    NSLog(@"定义前:%p", &a);         //栈区
    void (^foo)(void) = ^{
        a = 1;
        NSLog(@"block内部:%p", &a);    //堆区
    };
    NSLog(@"定义后:%p", &a);         //堆区
    foo();
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义前:0x16fda86f8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] 定义后:0x155b22fc8
2016-05-17 02:03:33.559 LeanCloudChatKit-iOS[1505:713679] block内部: 0x155b22fc8

“定义后”和“block内部”两者的内存地址是一样的,我们都知道 block 内部的变量会被 copy 到堆区,“block内部”打印的是堆地址,因而也就可以知道,“定义后”打印的也是堆的地址。

那么如何证明“block内部”打印的是堆地址?

把三个16进制的内存地址转成10进制就是:

定义后前:6171559672
block内部:5732708296
定义后后:5732708296
中间相差438851376个字节,也就是 418.5M 的空间,因为堆地址要小于栈地址,又因为iOS中一个进程的栈区内存只有1M,Mac也只有8M,显然a已经是在堆区了。

这也证实了:a 在定义前是栈区,但只要进入了 block 区域,就变成了堆区。这才是 __block 关键字的真正作用。

__block 关键字修饰后,int类型也从4字节变成了32字节,这是 Foundation 框架 malloc 出来的。这也同样能证实上面的结论。(PS:居然比 NSObject alloc 出来的 16 字节要多一倍)。

理解到这是因为堆栈地址的变更,而非所谓的“写操作生效”,这一点至关重要,要不然你如何解释下面这个现象:

以下代码编译可以通过,并且在block中成功将a的从Tom修改为Jerry。

NSMutableString *a = [NSMutableString stringWithString:@"Tom"];
    NSLog(@"\n 定以前:------------------------------------\n\
          a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
    void (^foo)(void) = ^{
        a.string = @"Jerry";
        NSLog(@"\n block内部:------------------------------------\n\
         a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区
        a = [NSMutableString stringWithString:@"William"];
    };
    foo();
    NSLog(@"\n 定以后:------------------------------------\n\
          a指向的堆中地址:%p;a在栈中的指针地址:%p", a, &a);               //a在栈区

result.png

这里的a已经由基本数据类型,变成了对象类型。block会对对象类型的指针进行copy,copy到堆中,但并不会改变该指针所指向的堆中的地址,所以在上面的示例代码中,block体内修改的实际是a指向的堆中的内容。

但如果我们尝试像上面图片中的65行那样做,结果会编译不通过,那是因为此时你在修改的就不是堆中的内容,而是栈中的内容。

上文已经说过:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。栈区是红灯区,堆区才是绿灯区。

    <p>转自:http://www.jianshu.com/p/a1c8532e172d</p>
<think> 我们正在讨论外部变量的使用。根据提供的引用,外部变量是在一个源文件中定义,而在另一个源文件中需要使用的变量。在C语言中,使用`extern`关键字来声明外部变量。同时,在Shell脚本中使用awk时,也有引用外部变量的方法。此外,在iOS的Block中,外部变量的捕获也有特定的规则。 我将从以下几个方面详细解释外部变量的使用方法: 1. C语言中外部变量的定义和声明 2. awk中引用外部变量的方法 3. iOS Block中对外部变量的捕获 ### 1. C语言中外部变量的定义和声明 在C语言中,外部变量(全局变量)可以在多个源文件之间共享。使用步骤如下: - **定义**:在某个源文件(如`file1.c`)中定义全局变量,例如: ```c // file1.c int globalVar = 10; // 定义并初始化 ``` - **声明**:在其他需要使用该变量的源文件(如`file2.c`)中,使用`extern`关键字声明该变量(不分配内存,只是告诉编译器该变量在其他地方定义): ```c // file2.c extern int globalVar; // 声明外部变量 void func() { printf("%d\n", globalVar); // 使用外部变量 } ``` 注意事项: - 外部变量的定义只能出现在一个源文件中(避免重复定义链接错误)。 - 声明可以出现在多个文件中(使用`extern`)。 - 外部变量的生存期是整个程序运行期间(程序开始分配内存,结束释放)[^1][^3]。 ### 2. awk中引用外部变量的方法 在Shell脚本中,awk命令常常需要引用外部定义的变量。以下是四种常用方法(引用[4]): - **方法1:双引号内使用转义双引号**(适用于变量替换在awk命令内部): ```bash var="bash" echo "unix script" | awk "{gsub(/unix/,\"$var\"); print}" ``` 输出:`bash script` - **方法2:单引号内用双引号包裹变量**(变量在函数外): ```bash var="bash" echo "unix script" | awk 'gsub(/unix/,"'"$var"'")' ``` - **方法3:单引号内三个单引号**(变量在函数内): ```bash i=10 df -h | awk '{if(int($5)>'''$i'''){print $6":"$5}}' ``` - **方法4:使用`-v`选项**(推荐,清晰且安全): ```bash var="bash" echo "unix script" | awk -v var="$var" '{gsub(/unix/,var); print}' ``` 这种方法通过`-v`选项将Shell变量传递给awk的内部变量,避免了复杂的引号嵌套,且可读性更强[^4]。 ### 3. iOS Block中对外部变量的捕获 在Objective-C中,Block可以捕获外部变量,但行为取决于变量类型: - **基本类型局部变量**:默认捕获值(值捕获),且在Block内部不能修改。例如: ```objectivec int age = 10; void (^myBlock)(void) = ^{ NSLog(@"age = %d", age); // 输出10 }; age = 18; myBlock(); // 输出10,捕获的是执行Block定义时的值 ``` - **使用`__block`修饰的变量**:捕获变量引用(引用捕获),可以在Block内部修改。例如: ```objectivec __block int age = 10; void (^myBlock)(void) = ^{ age = 20; // 允许修改 NSLog(@"age = %d", age); }; age = 18; myBlock(); // 输出20,且外部age也变为20 ``` 注意事项: - 全局变量或静态变量Block中可以直接修改(无需`__block`修饰)。 - 使用`__block`修饰的变量会被特殊处理,存储到堆上以实现跨Block访问[^5]。 ### 总结 外部变量的使用在不同场景下有不同方法: - C语言:通过`extern`声明跨文件共享的全局变量。 - awk脚本:通过引号嵌套或`-v`选项传递Shell变量。 - iOS Block:通过值捕获或`__block`修饰实现引用捕获。 在实际编程中,需注意作用域、生命周期和访问权限,避免错误。 §§相关问题§§ 1. C语言中外部变量和静态全局变量有何区别? 2. 如何在Python中实现类似C语言的外部变量共享? 3. awk中使用`-v`选项传递变量有哪些局限性? 4. Swift闭包中捕获外部变量的规则与Objective-C Block有何异同? </think> ### 外部变量的使用方法详解 根据引用内容,外部变量指在**当前作用域外定义**的变量,通过特定语法实现跨文件/跨作用域访问。下面按编程场景分类说明: --- #### 一、C语言中的外部变量(基于引用[1][3]) **1. 定义与声明** - **定义**:在某个源文件(如`file1.c`)中分配存储空间 ```c // file1.c int globalVar = 10; // 实际定义(分配内存) ``` - **声明**:在其他文件(如`file2.c`)中通过`extern`引用 ```c // file2.c extern int globalVar; // 声明(不分配内存) void func() { printf("%d\n", globalVar); // 输出10 } ``` **2. 关键规则** - 定义只能出现一次(避免重复定义错误) - 声明可多次出现(需用`extern`) - 生命周期:整个程序运行期(从启动到结束) - **错误示例**(重复定义): ```c // file1.c int var; // file2.c int var; // 链接错误:multiple definition ``` --- #### 二、awk脚本引用外部变量(基于引用[2][4]) **1. 四种方法对比** | 方法 | 语法示例 | 适用场景 | |-----------|---------------------------------------------|-----------------| | 转义双引号 | `awk "{print \"$var\"}"` | 简单变量替换 | | 单引号嵌套 | `awk 'gsub(/old/, "'"$var"'")'` | 正则替换操作 | | 三重单引号 | `awk '{if($1 > '''$var''') print}'` | 条件判断内部 | | `-v`参数 | `awk -v awk_var="$shell_var" '{print awk_var}'` | **推荐方法**(安全清晰) | **2. 最佳实践** 优先使用`-v`选项(避免引号嵌套混乱): ```bash # 示例:计算列总和(引用外部变量$i) awk -v col_index="$i" -F'\t' '{sum += $col_index} END{print sum}' table.txt ``` --- #### 三、iOS Block捕获外部变量(基于引用[5]) **1. 默认行为(值捕获)** ```objectivec int age = 10; void (^myBlock)(void) = ^{ NSLog(@"%d", age); // 输出10(捕获定义时的值) }; age = 20; myBlock(); // 仍输出10 ``` **2. 使用`__block`(引用捕获)** ```objectivec __block int age = 10; // 添加修饰符 void (^myBlock)(void) = ^{ age = 30; // 允许修改 NSLog(@"%d", age); }; age = 20; myBlock(); // 输出30(外部age同步变为30) ``` **3. 核心区别** | 类型 | 修饰符 | Block内是否可修改 | 内存管理 | |------------|----------|------------------|------------| | 局部变量 | 无 | ❌(编译错误) | 值拷贝 | | 外部变量 | `__block`| ✅ | 引用传递 | --- ### 关键总结 1. **C语言跨文件访问** - 定义:`int var;` - 声明:`extern int var;` 2. **Shell/awk脚本** - 首选 `awk -v var="$value"` 3. **iOS Block** - 需修改变量时用 `__block` 修饰 4. **通用原则** - 避免重复定义 - 注意作用域边界 - 明确生命周期(如Block可能引发循环引用)[^1][^3][^4][^5]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值