从在.h头文件中赋值并初始化static变量谈谈预编译和链接

写这篇文章的原因:

之前组里一个队友在写OC的时候,在OC的.h头文件里初始化并赋值了一个静态的字符串变量,并在多个.m实现文件引入了这个头文件然后使用了这个静态成员变量(虽然这种做法不提倡),导致程序出现了一些奇奇怪怪的行为(每个.m文件中的静态变量都不一样),他找了很久也百思不得解。这里我就针对这个问题谈谈预编译过程,以及浅谈相关的编译和链接的一些知识,希望对大家有帮助,也欢迎大神吐槽拍砖:

从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第1张图片
文章结构图.png
在.h中添加静态变量的情况如下:
从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第2张图片
s1.png

那我们开始,首先普及一下概念

静态变量:

-----首先普及一下加上 static 关键字的静态变量的特性:OC 是基于 C 语言的,在C语言中一个变量加上 static 关键字代表这个变量是只能在当前文件内使用的,并且在当前文件内同名的静态成员变量只能有一个,如果有多个那么后初始化并赋值的静态变量会把之前同名的覆盖。

预编译:

然后简单讲讲预编译:预编译就是在编译之前由IDE(对我们来说就是XCODE)对文件进行的一些处理,其中包括:1.宏替换(#define); 2.文件包含(#import) ;和 3.条件编译(#if #endif); 三项。这里只介绍概念,后文会细说。当然这个阶段还会对我们写的注释进行处理比如 #pragma 。通俗的说预编译就是对带 “井” 前缀(#***)的部分进行处理和简单的替换操作。那么以下我就针对开头我说的,在头文件中引入静态变量问题开始讲:

1.首先讲讲本次测试用的类的结构和方法,如下图:

  • 首先是 ContainStringClass 类的 .h 文件中有一个静态变量方法:
    static NSString *staticString = @“origin”;
  • 然后是它的两个方法,一个是改变静态变量 staticString 值的方法:
    - (void)changeStaticStringTo:(NSString *)string;
  • 另一个是内部打印静态变量 staticString 值的方法:
    - (void)logContainStringClass:(NSString *)index
从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第3张图片
ContainStringClass.h

这是 ContainStringClass 类的.m实现文件

从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第4张图片
ContainStringClass.m

然后是 ViewController 的 ViewDidLoad() 方法中的代码,目的是为了测试和打印

从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第5张图片
viewController.png

2.下面讲讲以上两个类的作用:

以上测试用的两个类。代码的功能就是:

  • 首先在 ContainStringClass.h 里初始化并赋值静态变量 static NSString *staticString = @"origin”;
  • 然后在 ViewController.m 中 #import ContainStringClass.h 并打印 staticString 的值;
  • 接着在 ContainStringClass 类内部修改这个静态变量的值然后打印;
  • 最后在 ViewController.m 文件中修改这个静态变量的值并打印。
    以下是打印结果:
从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第6张图片
打印结果

怕大家看不清附上打印结果放大版

打印结果放大版.png

3.下面解释一下输出结果

可以看到,虽然在 ViewController.m 中和 ContainStringClass.m 里边访问 “同一个” 在 ContainStringClass.h 中声明的静态变量,可是它们的值是互不影响的,仅在最初他们的值一样都是 staticString 的初值 @“origin” 。
在 ViewController.m 中对静态变量 staticString 的修改仅在 ViewController.m 中有效;同样的,在 ContainStringClass.m 中的修改仅在 ContainStringClass.m 中有效;

为什么呢?下面我们来说明:
  • ①. 首先,在预编译阶段,#import 就是文件包含,预编译对文件包含的处理非常简单,就是文本(代码)拷贝。所以在这里其实就是把 ViewController.m 中 #import ContainStringClass.h 这条语句给替换成 ContainStringClass.h 文件里的内容(代码),其中包括了 static NSString *staticString = @"origin” 这条语句。
  • ②. 同时,在 ContainStringClass.m 文件中也有 #import ContainStringClass.h 这条语句。也仅仅是简单的文本拷贝过来,所以这里也包含了 static NSString *staticString = @“origin” 这条语句。
  • ③. 现在明白什么是文件包含了吧,预编译的文件包含导致本来在 ContainStringClass.h 中的静态变量赋值语句 static NSString *staticString = @“origin”; 被拷贝到 ContainStringClass.m 和 ViewController.m 文件中且各有了一份代码;而由静态变量的特性可以知道静态变量在每个文件内部是唯一的(即每个文件中同名的静态变量仅有一份内存);所以最后演变成 ContainStringClass.m 和 ViewController.m 中各有一个名为 staticString 的静态变量,也就导致了上面的互不影响的打印结果(有点像两个方法中各有一个同名局部变量,只是静态变量作用域是在文件内生命周期也和app一样)。

4.下面我们再来讲讲预编译:

  • 预编译是 IDE 做的简单文件处理(就是针对 # 符号打头的代码的处理).
  • 文件包含:其中 #include 和 #import 是文件包含,而 #import 会对重复包含的文件做去重的处理,文件包含其实就是把文件中的内容(代码)做简单拷贝(.h文件不参与编译,预编译过后.h的内容都被拷贝到.m中了);
  • 然后是宏替换,宏替换就是把 #define 定义和使用的宏都替换成原来的代码,即把宏都干掉;
  • 最后是条件编译 #if #else #endif ,条件编译就是只留下条件为 ‘真’ 部分的代码,即只留下 #if 内的代码或者 #else 内的代码。
  • 最最后还有我们常用的注释 #pragma 在预编译后会被干掉(注释不参与编译);

5.然后我们谈谈编译:

  • 这里我们把编译细分成编译汇编,。如果是 OC 的话,在编译后会先生成 C 代码(因为 OC 是对 C 的封装),然后生成汇编代码(如果是 Swift 就是 Swift => C++ => 汇编 => 机器码 )。
  • 再之后汇编阶段会把汇编代码生成机器码。在编译完成之后会生成符号表,简单理解符号表:用全局变量举个例子,我们知道全局变量只有一份,但是很多类都可能要使用它,所以每个使用全局变量的地方都要有一个方法来找到这个全局变量在内存中的位置。
  • 换句话说就是每个引用这个全局变量的地方都要有一个方式去找到这个全局变量在哪,这个提供找全局变量的方式就是符号表。其实编译器会为每一个引用外部符号(比如全局变量)的地方提供一个方式找到这个外部符号,这就是符号表了。

6.然后是链接:

  • 链接就是把编译完成的所有文件合并成一个可执行文件(编译过后会生成一堆单个的 .o 后缀的文件)。
  • 链接阶段会处理符号表,在合并所有文件过程中,肯定涉及到合并的顺序,可能出现的情况是:使用全局变量的类已经合并完成了,但是给全局变量初始化和赋值的类文件还没有合并进来。那么此时怎么办呢?
  • 这就要靠符号表了,当发现要使用的全局变量找不到,就告诉链接器:“我这里需要一个全局变量,名叫XXX”,然后等全局变量初始化和赋值的类被链接的时候发现有一个全局变量,此时会告诉链接器:“我这有一个名叫XXX的全局变量要加入进来了”;然后链接器会试图把它们相匹配上;
  • 链接器也会把代码,全局变量,静态变量这些东西都区分开并分配地址(相对地址)空间,这就是重定向

7.链接之后的就是可执行程序了

  • Linux上可执行文件后缀是.ELF,MAC OS和iOS上是 Mach-O,但是可能被隐藏了,也就是没有后缀。
  • 首先,要执行的时候操作系统会分配一个进程(分配一个PCB进程控制块)并分配虚拟内存。
  • 然后,分配好之后操作系统调用加载器把代码复制到这块内存后跳转到程序入口点(其实实际是真的执行到某一部分代码的时候才把那部分的代码和数据加载进来,有点像懒加载)。
  • 接着,调用启动函数,启动函数会调用系统启动函数以初始化执行环境。
  • 初始化完成之后调用 main 函数并传入命令行参数和环境变量(下图的argc为命令行参数个数,argv[]为命令行参数数组)。
  • 最后就是我们熟悉的 UIApplicationMain 程序循环的启动了。
main函数

8.拓展:

根据以上知识知道预编译的意思了吧,那么如果:

  • ①.在多个 .h 文件中声明同名静态变量会怎样呢?
    答案是 XCODE 会给你检查出来并报错;
  • ②.如果在多个 .h 文件中初始化并赋值全局变量呢?
    答案是 XCODE 也能检查出来;
  • ③.如果在多个 .h 文件中声明但是不赋值一个全局变量呢?比如:
    NSString *globalString;
    答案是这样XCODE 是检查不出来的,但是链接阶段会出错。这是链接阶段的选项决定的,如果想设置也可以修改以允许多个未赋值的全局变量。下图是未赋值的意思(形容不好如果造成了误导别拍砖,拍也别拍脸>_<):
从在.h头文件中赋值并初始化static变量谈谈预编译和链接_第7张图片
s10.png

最后:

如果修改链接器参数是可以容忍多个未赋值的全局变量的(但是别这么干,因为每次用具体用哪个就不知道了,估计为了安全现在主流IDE都是不允许任何形式的多个全局变量);
如果对计算机和执行过程感兴趣推荐大家看看 《深入理解计算机系统》这本书,有点难,内容也有点多但是写的很好。有兴趣希望能一起多交流。Have fun~~!

你可能感兴趣的:(从在.h头文件中赋值并初始化static变量谈谈预编译和链接)