const、extern、static

319 阅读8分钟

1、const

1.1 作用

用来标记右侧的基本变量或者指针变量是 只读 的,即不可以被修改的。

const 在谁前面,谁就是只读的。基本变量或指针变量的类型放在哪里无所谓。

1.2 用法

1.2.1 const int *p

const修饰 *p,p 是一个指针变量,指针指向的对象(解指针)是只读的,指针的值是可变的。


int a = 9;
int b = 10;
const int *p = &a;//将 a 的地址复制给指针变量 p,p的值就是a的地址,p解地址的操作*p的值是9
*p = 10; // 编译报错:Read-only variable is not assignable
p = &b; //将 b 的地址赋值给指针变量p

在上面 const 置于 *p 前面说明 *p 是不可修改的常量,指针变量p是变量,是可变的可修改的。

此时若对 p 进行解指针操作 输出*p得到的值为b的值,如下图内存示意:

1.2.2 int * const p

const 修饰 p,p 是一个指针变量,指针指向的对象(解指针)是可变的,指针的值是不可变的。

int a = 9;
int b = 10;
int * const p = &a;
*p = 11;
p = &b; //编译报错:Cannot assign to variable 'p' with const-qualified type 'int *const'

在上面 const 置于 p 前面,说明 指针变量 p 的值不可改变,也就是p里面存的某个地址值不可改变,也就是说 p 不能指向其他新的地方,但是 *p 也就是对p进行解地址或者说 p指向的这块内存中存储的数据可以改变,在*p = 11之后,a的值即变为了11,如下图内存示意:

1.2.3 const int * const p

由以上两条可知 const 在*前面,则 *p是不可变常量,p是可变量; 若 const 在*之后在指针变量名之前,则*p是可变量, p为不可变常量。 若* 和指针变量名之前都有const,则*p是不可变常量,p也是不可变常量,两者均不可变。

int a = 9;
int b = 10;
const int * const p = &a;
*p = 11;//编译报错:Read-only variable is not assignable
p = &b;//编译报错:Cannot assign to variable 'p' with const-qualified type 'const int *const'

const int * const p = &a; 这行代码说明 p 既是一个 const 指针,同时,也指向了 int 类型的 const 值,也就是 p 这个指针不可以指向除 a 以外的地方(也就是不可以对指针变量p 赋予新的值),而且 p指向的这块内存中存储的数据也不可以被修改为新的值。

const 用法只看修饰在* 前面还是 变量名前面,与指针变量的类型均无关系。

2、extern

2.1 作用

extern 的作用就是为全局变量添加面向外部的入口,使得外部文件可以访问目标文件内的全局变量。

2.2 用法

extern 只用来声明,不可以用来实现,也就是如果想对外开放一个全局变量共外部使用,那么应该在 .h文件中来声明好这个全局变量,再在.m文件中去对这个全局变量来具体实现。

比如:

.h

#import <UIKit/UIKit.h>

extern NSString *fileName;  //此时用 extern 来声明
@interface NextVC : UIViewController


@end

.m

#import "NextVC.h"

@interface NextVC ()

@end


NSString *fileName = @"NextVC";  //此处来实现

@implementation NextVC

@end

2.3 extern修饰的常量和宏定义有什么区别?

extern修饰的变量没有真正内存,extern定义的全局常量的用法和宏定义类似,但是还是有本质上的不同的。 extern定义的全局常量更不容易在程序中被无意窜改(配合const使用)。

比如我们创建一个公用的constant类,里面存放项目中用到的一些全局的常量,在将此文件导入到.pch头文件中供全局使用:

我们在.h文件中:

extern const CGSize QMUIFloatLayoutViewAutomaticalMaximumItemSize;
UIKIT_EXTERN const CGFloat kNavigationBarHeight;
......

.m中:

const CGSize QMUIFloatLayoutViewAutomaticalMaximumItemSize = {-1, -1};
const CGFloat kNavigationBarHeight = 64.0;
......

这样我们既用 extern 增加了这个全局常量的对外接口,又用 const 限制了常量值的不可改变。

3、static

3.1 作用

static 可以用于修饰局部变量、全局变量和函数。

3.2 用法

3.2.1 修饰局部变量

static修饰局部变量时,可以修改局部变量的声明周期,保证此局部变量为静态存储方式,即编译时就为变量分配内存,内存只初始化一次,直到程序退出才释放存储单元。

这样可以使该局部变量有“记忆功能”,可以记忆上次的数据,不过由于是局部变量,因此只能在代码块内部使用。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    int i = 100;
    //static int i = 100;
    i ++;
    NSLog(@"i的值为:%d",i);
}

上述方法的每一次调用,都会输出 i 的值为101,因为每次方法调用,i都会初始化为 i = 100,当我们用static关键字修饰以后,每次调用方法输出的 i 的值为101、102、103...

显然,关键字修饰的局部变量,会在编译时就被分配内存空间和初始化,程序运行的整个期间,“记忆”内存数据,生命周期被改变,与全局变量相似,只是作用域不变依然为方法体内。待程序退出,再释放此处内存空间。

还有其他的常见用法:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   //Cell复用的cellIdentifier设置都是定义为静态的字符串既节省了内存和提高时了效率
    static NSString *cellIdentifier = @"CellIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
    return cell;
}

3.2.2 修饰全局变量

static 修饰全局变量时,全局变量的声明周期和用 static 修饰的局部变量相同,均是在编译时期分配好内存,在程序退出时释放内存。

static 修饰全局变量时,可以限制外部局部变量的作用域,使得这个外部的局部变量只能在本文件内使用,不能被其他文件引用,也就是只在此文件内部有效

先看看全局变量的使用:全局变量需要在 @implementation外定义

#import "NextVC.h"

@interface NextVC ()

@end

NSString *fileName = @"NextVC";

@implementation NextVC

@end

上面的 fileName 就是 NextVC类文件中的全局变量,如果想要在其他文件中访问这个 fileName 变量,只需要在 NextVC.h 中 只用 extern 关键字来添加对外的链接入口即可:

extern  NSString *fileName;

@interface NextVC : UIViewController


@end

这样比如在其他文件中,我们就可以访问这个 fileName 全局变量了。

比如在 ViewController 中使用:

-(void)staticQuanju {
    NextVC *nextVC = [[NextVC alloc] init];
    fileName = @"viewController";
    [self.navigationController pushViewController:nextVC animated:YES];
    NSLog(@"%@",fileName);
}

输出结果即为:

2020-09-09 16:45:48.317199+0800 关键字[6358:223978] viewController

假如此时我在 ViewController 中也要有一个全局变量为 fileName会怎么样呢?

报错:linker command failed with exit code 1

当然可想而知,如果同个项目中两个.m文件中有同样的同名全局变量时,全局变量在编译时分配内存,编译器无法确定这个“fileName” 是 NextVC的,还是ViewController的(不知道此处是否解释正确)。

那么 static 修饰全局变量是怎么回事呢?

前面说到static 修饰全局变量时,可以限制外部局部变量的作用域,使得这个外部的局部变量只能在本文件内使用,不能被其他文件引用,也就是只在此文件内部有效,既然我们不想要外部文件能访问本文件的全局变量,我们自然不能用 extern 再来为全局变量添加外部链接入口了,因为 static是为了禁止外部访问此全局变量,extern 刚好相反,两者会冲突。

那么有个问题是:我在 NextVC.m 文件中我定义了一个全局变量 fileName,我也不打算再外部使用这个全局变量,不就好了吗?我是不是永远就不会用到 static 来修饰这个全局变量了?

那答案是肯定不对的,因为即使在外部我们不打算使用这个fileName,但是文件越来越多,假如我们在其他文件中又需要创建一个全局变量,刚好名字也叫 fileName,这样项目依然会报错,因为一个项目中,不允许有同名的全局变量被编译器编译。所以为了开心的创建新文件,我们使用 static 来修饰全局变量的话就会避免这样的问题,即使多人开发也没关系,自己的文件创建自己的全局变量,不用关心是否重名,两者相安无事,互补干涉。

使用方法如下:

static 修饰全局变量

static NSString *fileName = @"NextVC";

3.2.3 static 和单例

单例的创建也经常会用到 static


...
static ClassName *sharedManagerInstance = nil;
...

@implementation NextVC
+ (instance *)sharedManager
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManagerInstance = [[self alloc] init];
    });
    return sharedManagerInstance;
}

+ (id)allocWithZone:(struct _NSZone *)zone{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedManagerInstance = [super allocWithZone:zone];
    });
    return sharedManagerInstance;
}

@end

上面可以看到创建单例的时候,还重写了 allocWithZone 方法,这是因为如果不重写这个方法,那么我们如果不小心调用了单例类的 alloc 方法的话,会重新开辟一块新的内存(调用 alloc 会调用 allocWithZone 方法),那么这个 sharedManagerInstance 便不再是同一个对象,所以我们只能通过重写 allocWithZone 的方法利用 GCD 的 dispatch_once_t来通过执行这个只执行一次的方法去创建这个单例实例。