目录
变量的作用域(scope)
变量的作用域指的是变量的有效范围,这是一个编译阶段的概念
-
代码块作用域(block scope)
位于一对
{ }
之间的所有语句称为一个代码块,在代码块中定义的标识符具有代码块作用域,这些标识符从它定义的位置开始到最近的配对的}
之间有效。函数定义的形式参数在函数体内部也具有代码块作用域
当代码块处于嵌套状态时,如果内层代码块的标识符与外层代码块的标识符同名,则内层代码块的标识符将屏蔽外层代码块的标识符(此时,外层代码块的标识符无法在内层代码块中通过名字访问) -
文件作用域(file scope)
任何定义在函数之外的标识符都具有文件作用域,这些标识符从它定义的位置开始到它所在的源文件的末尾,都是有效的
-
原型作用域(prototype scope)
标识符定义在函数原型中,这个函数原型只是一个函数声明而不是一个函数定义(因为没有函数体),那么标识符从定义的位置开始到这个函数原型的末尾之间有效
-
函数作用域(function scope)
标识符在整个函数中都有效。只有语句标号属于函数作用域,标号在函数中不需要先声明后使用,在前面用一个 goto 语句可以跳转到后面的某个标号处,但是仅限于同一个函数之中
-
代码示例
#include <stdio.h> /*声明全局变量 globalVariable*/ // globalVariable 具有文件作用域,此处为文件作用域的开始 int globalVariable; /*声明 fun 函数*/ // 变量 m 具有原型作用域,从变量定义处一直到函数声明的末尾,也就是局限在小括号 () 内 // 此处可以省略变量名 m,只用 int* 告诉编译器参数的类型 // fun 函数声明处的变量名 m 可以不与 fun 函数定义处的变量名 n 相同 void fun(int* m); /*主函数*/ int main(void) { // 函数作用域开始,本质属于代码块作用域 int i = 10; globalVariable = 20; printf("Before change, i = %d\n", i); fun(&i); printf("After change, i = %d\n", i); printf("globalVariable = %d\n", globalVariable); for(int j = 0; j < 5; j++) { // 复合语句作用域开始,本质属于代码块作用域 int m = 0; printf("j = %d\n", j); // 复合语句作用域结束,本质属于代码块作用域 } return 0; // 函数作用域结束,本质属于代码块作用域 } /*定义 fun 函数*/ void fun(int* n) {// 形参变量 n 函数作用域开始,本质属于代码块作用域 *n = 30; // 形参变量 n 函数作用域结束,本质属于代码块作用域 } // 全局变量 globalVariable 的文件作用域结束
变量的链接特性(linkage)
链接特性用于描述链接器会如何链接变量。链接特性决定了一个变量是否可供其他文件使用,还是只能在定义它的文件中使用
-
空链接(no linkage):具有(代码块作用域)或者(原型作用域)的变量就具有空链接特性,这意味着这些变量是由其定义所在的代码块或函数原型所私有,其内存通常在使用时才分配
-
外部链接(external linkage):具有外部链接特性的变量,可以在多文件程序的任何地方使用,为多个程序文件所共享,其内存通常在程序启动前的加载阶段被分配
本编译单元中具有外部链接特性的变量可以被所有其他编译单元访问 -
内部链接(internal linkage):具有内部链接特性的变量,可以在定义该变量的程序文件中的任何地方使用,为该程序文件所私有,其内存通常在程序启动前的加载阶段被分配
其他编译单元不可以访问本编译单元中具有内部链接特性的变量 -
注意:
① 具有(文件作用域的变量)可能有(内部链接特性),也可能有(外部链接特性)
② 一个具有文件作用域的变量默认具有(外部链接特性)
③ 如果一个具有文件作用域的变量,在它的前面使用了关键字static
修饰,则该变量具有(内部链接特性) -
代码示例
#include <stdio.h> int a; // 全局变量:文件作用域、外部链接特性、静态生存周期 static int b; // 静态全局变量:文件作用域、内部链接特性、静态生存周期 int main(void) { int c = 1; // 局部变量:代码块作用域、空链接特性、自动生存周期 static int d = 2; // 静态局部变量:代码块作用域、空链接特性、静态生存周期 return 0; }
变量的生存周期(storage duration)
变量从(被分配内存)到(释放其占用的内存)的这段时间,称之为变量的生存周期
-
① 静态生存周期:程序从(开始运行)到(结束退出)的时期,静态指的是变量的内存地址不会改变,是静态的。供(全局变量)、(静态全局变量)、(静态局部变量)使用
-
② 自动生存周期:程序从(进入变量所在块)到(退出变量所在块)的时期,程序进入块时为块区域的变量分配内存,程序退出块时释放为块区域变量分配的内存,以栈的方式进行处理。供(自动变量)使用
-
③ 动态生存周期:由程序调用内存分配函数(
malloc
、calloc
)所开始,由程序调用内存释放函数(free
、release
)所结束,以堆的方式进行处理。供动态内存分配函数使用 -
④ 线程生存周期:程序的执行通常会被分为多个线程,变量在某个线程中被定义之后就一直存在,直到该线程结束时才被释放,每个线程都有该变量的私有备份(此类型的变量用关键字
_Thread_local
修饰) -
进程的内存分区
进程的内存分区,按低地址到高地址,分别为:- 代码区:存放程序的代码(即 CPU 执行的机器指令),只读(防止运行时被修改)
- 常量区:存放常量(即在程序运行的期间不能够被改变的数据)。例如,
10
,"abcde"
- 全局区(静态区):全局变量和静态变量的存储区域是在一起的,全局区(静态区)的内存在程序开始运行时分配,在程序结束退出时回收
3.1 数据区,用于存放已经初始化的变量(静态存储方式)
3.2 BSS 区,用于存放还未初始化的变量(静态存储方式)。BSS 区的所有字节默认都会被初始化为 0x00。也就是说,如果全局变量、静态全局变量、静态局部变量 没有显式初始化,则默认会被初始化为 0 - 堆区(heap):其内存由程序员手动管理,使用动态内存分配函数创建(
malloc
,calloc
)和释放(free
,release
)。堆区的内存,在使用之前和使用之后,都不会清零,这也是为什么在 Objective-C 中每次alloc
完一个对象之后需要init
的原因 - 栈区(stack):其内存由操作系统自动管理,用于存放函数的参数、函数的返回值和函数的局部变量,遵循先进后出原则。栈区的内存,在使用之前和使用之后,都不会清零,这也是为什么每次定义局部变量若不进行初始化就会得到随机值的原因
变量的存储类(storage class)
-
存储类说明符实际上是(作用域、链接特性、生存周期)的不同组合
存储类 作用域 链接特性 生存周期 声明方式 自动 存储类 代码块作用域 空链接特性 自动生存周期 代码块内 寄存器 存储类 代码块作用域 空链接特性 自动生存周期 代码块内,使用关键字 register
具有外部链接的静态 存储类 文件作用域 外部链接特性 静态生存周期 所有函数之外 具有内部链接的静态 存储类 文件作用域 内部链接特性 静态生存周期 所有函数之外,使用关键字 static
具有空链接的静态 存储类 代码块作用域 空链接特性 静态生存周期 代码块内,使用关键字 static
① 自动变量(局部变量 / 函数形参)
在默认情况下,(局部变量)或者(函数的形式参数)都属于自动变量。自动变量具有:代码块作用域、空链接特性、自动生存周期。可用
auto
修饰,或者不修饰。对于自动变量,除非显式地初始化,否则不会自动初始化(即不会有固定的初值)② 寄存器变量
通过关键字
register
声明,存放在寄存器而非内存中,所以无法获得其地址。注意,虽然请求了把变量放在寄存器中,但是由于寄存器个数的限制,有时候并不一定能满足请求,此时的寄存器变量就成为了普通变量(存放在内存当中),不过依然不能对其取址③ 具有外部链接的静态变量(全局变量)
在一般情况下,具有外部链接的静态变量 指的是 全局变量。其在函数外声明(提供了文件作用域、外部链接特性、静态生存周期),一旦被定义便一直存在直到程序结束。因为 全局变量 被存储在 静态区,所以如果不对其进行初始化,则自动初始化为 0
④ 具有内部链接的静态变量(静态全局变量)
在一般情况下,具有内部链接的静态变量 指的是 静态全局变量。其在函数外声明(提供了文件作用域、静态生存周期)并通过关键字
static
修饰(提供了内部链接特性),一旦被定义便一直存在直到程序结束。因为 静态全局变量 被存储在 静态区,所以如果不对其进行初始化,则自动初始化为 0⑤ 具有空链接的静态变量(静态局部变量)
在一般情况下,具有空链接的静态变量 指的是 静态局部变量。其在代码块内声明(提供了代码块作用域、空链接特性)并通过关键字
static
修饰(提供了静态生存周期),一旦被定义便一直存在直到程序结束。因为 静态局部变量 被存储在 静态区,所以如果不对其进行初始化,则自动初始化为 0 -
存储类说明符
①
auto
用于标明一个变量具有自动生存周期,该关键字只能用在具有代码块作用域的变量中
在局部变量中,该关键字默认被省略
虽然函数的形参也具有代码块作用域,但是在函数的形参中不可以使用该关键字int add(int num1, int num2) { int num3 = 10; // 等价于 auto int num3 = 10; return num1 + num2 + num3; }
②
register
用于请求将一个变量存储在寄存器中,以便快速使用。不能获取(使用该关键字修饰的变量)的地址。该关键字只能用在具有代码块作用域的变量中。现在编译器的优化都做得很好,编译器会自己想办法有效地利用 CPU 的寄存器,所以现在 register 关键字用得比较少③
static
用于具有代码块作用域的变量时,使该变量具有静态生存周期,从而得以在程序运行期间一直存在并保留其值,变量仍保留代码块作用域和空链接特性
用于具有文件作用域的变量时,使该变量具有内部链接特性,从而不能被程序的其他文件使用,变量仍保留文件作用域和静态生存周期④
extern
用于声明一个已经在其他地方定义了的变量
在其他文件中已经定义的具有文件作用域的变量,如果本文件调用的话,则需要用 extern 重新再声明一下
关键字extern
的声明属于引用声明,不会引起内存分配,声明的变量必须在其他文件中有过定义⑤
const
用于将变量的值设置为不可改变(即用于定义常量),常量只能在定义的时候设置初值,以后不可改变其值。在指针类型中使用时,关键字const
的位置决定了,是指针本身不可改变,还是指针指向的数据不可改变⑥
volatile
用于说明变量的值可能会被隐式地改变,说明变量的值除了可能被本程序修改外,还可能被其他程序或硬件所修改,主要用于编译器优化// 因为这是对一个地址进行两次连续的赋值,如果没有使用关键字 volatile 的话 // 那么当编译器遇到这两行代码的时候,为了优化性能,会将第一条赋值语句从程序中删除掉 char* char1; *char1 = 'a'; *char1 = 'b'; // 为了防止出现这种情况,应该把变量 char1 声明为一个 volatile 变量 volatile char* char1; *char1 = 'a'; *char1 = 'b';
// 通常情况下,val1 = i 执行之后,i 的值在寄存器中有缓存,val2 = i 再使用 i 值时默认从寄存器中读取,以优化程序性能 // 使用 volatile,则告诉编译器不要做优化,应该再次从内存地址中查找使用 i 值,因为 i 值可能会被其他程序或硬件修改了 volatile int i = 10; int val1, val2; val1 = i; val2 = i;
⑦
_Thread_local
用于定义具有线程生存周期的变量,使用该关键字修饰的变量在线程中被定义之后会一直存在直到该线程结束,每个线程都有该变量的私有备份
全局变量 && 局部变量 && 静态全局变量 && 静态局部变量
-
全局变量
在函数之外定义的变量,称为全局变量。全局变量具有:文件作用域、外部链接特性、静态生存周期,存储在进程的全局区(静态区)
-
局部变量
在函数内部定义的变量 以及 函数的形式参数,称为局部变量。局部变量具有:代码块作用域、空链接特性、自动生存周期,存储在进程的堆栈区(数据类型 和 指针 存储在栈区,对象类型存储在堆区)
-
静态全局变量
全局变量使用关键字
static
修饰之后就变成了静态全局变量。静态全局变量具有:文件作用域、内部链接特性、静态生存周期,存储在进程的全局区(静态区)对比全局变量和静态全局变量
相同点:全局变量和静态全局变量都具有文件作用域和静态生存周期
不同点:全局变量具有外部链接特性,而静态全局变量具有内部链接特性。即,全局变量在所有文件中都可以被访问到,而静态全局变量只能在其定义的文件中才能被访问到在这里,关键字
static
改变了全局变量的链接特性,从而达到对其他文件隐藏全局变量的目的#include <stdio.h> int a1 = 10; // 全局变量:在其他文件中可以通过关键字 extern 访问到 static int b1 = 20; // 静态全局变量:只能在本文件中被访问到 // 默认情况下,所有的函数具有:文件作用域、外部链接特性。可供其他文件调用 int add(int num1, int num2) { return num1 + num2; } // 加上 static 修饰的函数具有:文件作用域、内部链接特性。仅在本文件中可见 static int sum(int num1, int num2) { return num1 + num2; }
-
静态局部变量
局部变量使用关键字
static
修饰之后就变成了静态局部变量。静态局部变量具有:代码块作用域、空链接特性、静态生存周期,存储在进程的全局区(静态区)对比局部变量和静态局部变量
相同点:局部变量和静态局部变量都具有代码块作用域和空链接特性
不同点:局部变量具有自动生存周期,而静态局部变量具有静态生存周期。即,局部变量过了其作用域之后就会被释放,而静态局部变量在程序运行期间会一直存在(无论定义静态局部变量的函数是否被调用)在这里,关键字
static
改变了局部变量的生存周期,从而达到长久保存局部变量的目的#include <stdio.h> int main(void) { for(int i = 0; i < 5; i++) { // 程序每次进入循环体的时候,会为 c1 分配内存;程序每次退出循环体的时候,会回收为 c1 分配的内存。(存储在栈区) int c1 = 30; // 程序运行时,不会执行以下定义静态局部变量的代码 // 这是因为存储在全局区的变量(全局变量、静态全局变量、静态局部变量)在程序调入内存时已经初始化了 // 把定义静态局部变量的语句放在 for 循环的语句块中,只是为了告诉编译器:只有 for 循环的语句块才能看到静态局部变量 d1 static int d1 = 40; printf("c1 = %d", c1); // 输出:30、 30 、30、 30、 30 printf("d1 = %d", d1); // 输出:40、 41、 42、 43、 44 c1++; d1++; } return 0; }
-
代码示例
#include <stdio.h> int a0; // 全局变量:文件作用域、外部链接特性、静态生存周期。存储在全局未初始化区(BSS 区),默认初值为 0 int a1 = 10; // 全局变量:文件作用域、外部链接特性、静态生存周期。存储在全局初始化区(数据区),初始值为 10 static int b0; // 静态全局变量:文件作用域、内部链接特性、静态生存周期。存储在全局未初始化区(BSS 区),默认初值为 0 static int b1 = 20; // 静态全局变量:文件作用域、内部链接特性、静态生存周期。存储在全局初始化区(数据区),初始值为 20 int main(void) { int c0; // 局部变量:代码块作用域、空链接特性、自动生存周期。存储在栈区,初始值为随机值 int c1 = 30; // 局部变量:代码块作用域、空链接特性、自动生存周期。存储在栈区,初始值为 30 static int d0; // 静态局部变量:代码块作用域、空链接特性、静态生存周期。存储在全局未初始化区(BSS 区),默认初值为 0 static int d1 = 40; // 静态局部变量:代码块作用域、空链接特性、静态生存周期。存储在全局初始化区(数据区),初始值为 40 char* name = "hcg"; // 指针 name 存储在栈区,字符串 "hcg" 存储在常量区 int* intPtr = (int *)malloc(30 * sizeof(int)); // 指针 intPtr 存储在栈区,整型数组存储在堆区 int temp = intPtr[0]; // 整型数组未初始化,此时取到的是随机值 return 0; } static int sum(int num1, int num2) { return num1 + num2; // num1 和 num2 是局部变量:代码块作用域、空链接特性、自动生存周期。存储在栈区 }
static && const && extern
-
static
① 用于具有代码块作用域的变量时,使该变量具有静态生存周期,从而得以在程序运行期间一直存在并保留其值,变量仍保留代码块作用域和空链接特性
② 用于具有文件作用域的变量时,使该变量具有内部链接特性,从而不能被其他程序文件访问,变量仍保留文件作用域和静态生存周期
③ 被关键字
static
修饰的变量,其初始值默认为 0。因为被关键字static
修饰的变量都具有静态生存周期,即被关键字static
修饰的变量都存储在进程的全局区(静态区)。在进程的全局区(静态区)中,已经初始化的变量被存储在数据区,还未初始化的变量被存储在 BSS 区。而在程序启动时,BSS 区所有的字节默认都会被置零 -
const
被关键字
const
修饰的变量的值在程序运行期间不能被修改① 用 const 定义常量
const float PI = 3.14f;
② 当
const
用于修饰指针类型时,根据其放置的位置的不同,有以下三种情况// 1. const 放置在星号(*)左边 // 表示指针本身的值可以被修改,但是指针指向的对象不能被修改 // 因此,指针可以在初始化之后再赋值,表示指向别的常量对象 // 可以这样记忆:因为 const 靠近对象的类型说明符,所以指针指向的对象不能被修改 const NSString * str0; NSString const * str1; str0 = @"hcg"; str1 = @"hzp"; // 2. const 放置在星号(*)右边 // 表示指针本身的值不能被修改,但是指针指向的对象可以被修改 // 因此指针在初始化之后不能再赋新值 // 可以这样记忆:因为 const 靠近指针(*),所以指针本身的值不能修改 NSString * const str2 = @"jack"; // 3. 星号(*)两边都有 const // 表示指针本身的值不能被修改,并且指针指向的对象也不能被修改 // 因此指针不能在初始化之后再赋新值 const NSString * const str3 = @"hcg"; NSString const * const str4 = @"hzp";
-
extern
用于声明一个已经在其他地方定义了的(变量 或 函数)。在其他文件中已经定义的具有文件作用域的(变量 或 函数),要在本文件调用的话,需要用关键字
extern
重新声明一下(提示编译器遇到此 变量或函数 时,在其它模块中寻找其定义)
注意
-
作用域不同的变量同名不冲突,作用域相同的变量同名冲突
-
在文件中定义变量(或函数)时,如果变量(或函数)仅在本文件中调用,那么就用关键字
static
将其限定为内部链接特性,防止外部调用 -
一般不要在 .h 文件里定义(全局变量)和(静态全局变量)。因为一旦 .h 文件被
#import
,就相当于 .h 文件的代码被复制一份过去,会出现多次定义的情况。如果是(全局变量),则直接编译报错 duplicate symbol;如果是(静态全局变量),则多少个源文件#import
就会生成多少个作用于不同源文件的(静态全局变量) -
什么是声明?什么是定义?
按照 C 语言的规则,变量和函数必须先声明后使用。变量和函数可以多次声明,但不可以多次定义
声明仅仅是告诉编译器某个标识符是什么,更具体地:变量是什么类型、函数的参数和返回值是什么?要是后面的代码中再出现该标识符,编译器就知道该如何处理了。记住最重要的一点:声明变量不会导致编译器为该变量分配存储空间,而定义变量会导致编译器为该变量分配存储空间。C 语言中专门有一个关键字用于声明变量或函数:
extern
,带有extern
的语句出现时,编译器将认为你只是要告诉它某个标识符是什么,除此之外什么也不会做(直接使用extern
初始化变量除外)// 声明变量 extern int a; // 定义变量 extern int b = 10; int c = 20; int d; // 声明函数 int sum(int num1, int num2); extern int add(int num1, int num2); // 定义函数 int sum(int num1, int num2) { return num1 + num2; }
-
动态内存分配函数(malloc、calloc、free)
// malloc() 函数会根据所需内存的字节数 10 * sizeof(int),从虚拟内存中找到适合的空闲块,将该空闲块的首地址作为 void 类型的指针返回 // void 类型的指针可以被强制转换为任意类型的指针,如:指向数组的指针、指向结构的指针 等 // malloc() 函数如果找不到合适的空闲块(通常是内存空间已满的情况下),会返回空指针 // malloc() 函数不会对找到的内存空闲块进行清零操作 int* array = (int *)malloc(10 * sizeof(int)); if (!array) { NSLog(@"malloc 内存分配失败!"); } // 释放申请的内存 // malloc() 与 free() 在程序中需要成对地使用,否则会造成内存泄露 free(array);
// calloc() 函数会根据所需存储单元数量 10 以及每个存储单元的大小 sizeof(int),从虚拟内存中找到适合的空闲块,将该空闲块的首地址作为 void 类型的指针返回 // void 类型的指针可以被强制转换为任意类型的指针,如:指向数组的指针、指向结构的指针 等 // calloc() 函数如果找不到合适的空闲块(通常是内存空间已满的情况下),会返回空指针 // calloc() 函数会对找到的内存空闲块进行清零操作 int* array = (int *)calloc(10, sizeof(int)); if (!array) { NSLog(@"calloc 内存分配失败!"); } // 释放申请的内存 // calloc() 与 free() 在程序中需要成对地使用,否则会造成内存泄露 free(array);
-
Objective-C 的一些语法特性
①
因为:
Objective-C 中的类方法属于类对象,类方法中的 self 指的是类对象
Objective-C 中的对象方法属于实例对象,对象方法中的 self 指的是实例对象
所以:
Objective-C 类方法中可以使用 self 调用其他的类方法,但是不能使用 self 调用对象方法
Objective-C 对象方法中可以使用 self 调用其他的对象方法,但是不能使用 self 调用类方法
②
虽然 Objective-C 中的成员变量、属性是定义在类文件中的
但是 Objective-C 中的成员变量、属性是依附于具体的实例对象而存在的
因此,在 Objective-C 的类方法里面,不能通过 self 访问到(成员变量)和(属性)
注意:在 Objective-C 中不能定义类成员变量和类属性
③
在 Objective-C 中,C 函数不依赖于类和对象,可被类方法和对象方法调用,也可调用类方法和对象方法
④
在 Objective-C 中,类方法和对象方法都能访问:全局变量、静态全局变量,都能定义:静态局部变量
换句话说:在 Objective-C 中,全局变量、静态全局变量为类对象和实例对象所公有