[iOS]static、extern、const关键字比较
文章目录
介绍这三个关键字之前先补一下课
全局区地址如何分配
先来一段代码
不难看出下面的clA bssA bssStr1属于未初始化的全局变量和静态变量,在BSS段
clB bssB bssStr2属于已初始化的全局变量和静态变量,在DATA段
int clA; int clB = 10; static int bssA; static NSString *bssStr1; static int bssB = 10; static NSString *bssStr2 = @"bss"; - (void)testConst { NSLog(@"clA == \t%p",&clA); NSLog(@"bssA == \t%p",&bssA); NSLog(@"bssStr1 == \t%p",&bssStr1); NSLog(@"clB == \t%p",&clB); NSLog(@"bssB == \t%p",&bssB); NSLog(@"bssStr2 == \t%p",&bssStr2); }
然后我们看打印结果
它们的内存分配有什么规律
可以看出:
未初始化的全局变量和静态变量,在BSS段
BSS段的地址分配时,是低地址 -> 高地址
已初始化的全局变量和静态变量,在DATA段
DATA段的地址分配,与变量定义的顺序无关
静态区安全测试
静态变量的作用范围是当前文件内。
相当于引用别的文件时,底层会深拷贝一份静态变量,放在了自己的文件中,以后访问及操作的都是本文件内的这个变量,对别的文件没有影响。
当前文件更改静态变量后,本文件内再访问,是更改后的值,但不影响别的文件中的这个静态变量的值。
别的文件引入静态变量后,拿到的是静态变量的初始值,修改后再访问是自己修改后的值。
static、extern、const关键字
externextern关键字用来声明变量或者函数是一个外部变量或者外部函数
,也就是说告诉编译器该变量是在其他文件中定义的,编译的时候不要报错,在链接的时候按照字符串寻址可以找到这个变量或者函数
如果A.h中定义了全局变量比如int a;,那么在其他文件中的函数调用变量a的时候需要在对应头文件或者定义文件中(保证在使用这个变量前)使用extern int a;来声明这个变量,但是这样做有一个弊端,首先如果A.h中集中定义了大量的全局变量供其他文件使用,那么其他的调用文件中会重复的出现大量的extern语句,第二,如果其他文件直接引用A.h,那么会造成全局变量的重复定义,编译不过,等等
所以我们应该
1、在定义文件中定义全局变量, 比如A.m中定义全局变量 int aa;
2、在对应的头文件A.h中声明外部变量 extern int aa;
3、在使用aa变量的文件B.m中包含A.h;
在编译阶段,B编译单元虽然找不到该函数或变量,但它不会报错,它会在链接时从A编译单元生成的目标代码中找到此函数。
static
使用static修饰变量,就不能使用extern来修饰,即static和extern不可同时出现
static修饰的全局变量的声明与定义同时进行
,即当你在头文件中使用static声明了全局变量,同时它也被定义了。
static修饰的全局变量的作用域只能是本身的编译单元。如果有多个文件包含了定义static变量的头文件,在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。(与头文件中定义const类似)
即在其他编译单元A使用它时,它所在的物理地址,和其他编译单元B使用它时,它所在的物理地址不一样,A和B对它所做的修改都不能传递给对方
多个地方引用静态全局变量所在的头文件,不会出现重定义错误,因为在每个编译单元都对它开辟了额外的空间进行存储。
一般定义static 全局变量时,都把它放在.m文件中而不是.h文件中,这样就不会给其他包含此头文件的编译单元里边重复生成变量。
在标准C++中引入命名空间之前,程序必须将名字声明为static,使他们用于当前编译单元。C++文件中静态声明的使用从C语言继承而来,在C语言中,声明为static的局部实体在声明它的文件之外不可见。
C++不赞成文件静态声明。C++标准取消了在文件中声明静态声明的做法。应该避免static静态声明,而在源文件中使用未命名的命名空间,在未命名的命名空间中定义变量。
未命名的命名空间仅在文件内部有效,其作用范围不会横跨多个不同的文件。
具体的例子
关于extern关键字
在头文件里这么写是合理的
//.h static NSString * const myString = @"foo";
但是其实这是不正确也并不安全的一种写法
想让这个常量字符串被其他的类所正确使用
那么在我的头文件里应该这么声明
//.h extern NSString * const myString;
然后在每个引用头文件的源文件内
//,m NSString * const myString = @"foo";
何意呢家人们 为什么要这么写 到底安全在哪里?
在每个编译单元内,也就是通俗说在每个.m文件内
用static const
修饰的myString内容都是 foo 没问题
但这些myString其实是不同的对象
static修饰的全局变量的作用域只能是本身的编译单元。如果有多个文件包含了定义static变量的头文件,在其他编译单元使用它时,只是简单的把其值复制给了其他编译单元,其他编译单元会另外开个内存保存它,在其他编译单元对它的修改并不影响本身在定义时的值。
也就是说这里的static和const功能重复了
多个.m文件文件内存在的都是myString对象的复制
并没有实现多个文件共同使用咱们的一个myString对象
而extern关键字使我们的myString对象变成可共同使用的外部变量
然后就是注意extern的使用事项
1、在定义文件中定义全局变量, 比如A.m中定义全局变量 int aa;
2、在对应的头文件A.h中声明外部变量 extern int aa;
3、在使用aa变量的文件B.m中包含A.h;
关于static关键字
关于前面提到static的部分
我们来一段栗子
先在test1.h定义字符串 g_str
//test1.h #ifndef TEST1H #define TEST1H static char g_str[] = "123456"; void fun1(); #endif
在test1.cpp中使用变量 g_str
//test1.cpp #include "test1.h" void fun1() { cout << g_str << endl; }
在test2.cpp中使用变量 g_str
//test2.cpp #include "test1.h" void fun2() { cout << g_str << endl; }
如果较真的同学偷偷调试上面代码会发现两个编译单元的g_str的内存地址相同
于是你下结论static修饰的变量也可以作用于其他模块
但那是你的编译器在欺骗你
大多数编译器都对代码都有优化功能,以达到生成的目标程序更节省内存,执行效率更高,当编译器在连接各个编译单元的时候,它会把相同内容的内存只拷贝一份
比如上面的"123456", 位于两个编译单元中的变量都是同样的内容,那么在连接的时候它在内存中就只会存在一份了
来个复杂的例子拆穿编译器的谎言:
在test1.cpp中使用变量 g_str
//test1.cpp #include "test1.h" void fun1() { g_str[0] = 'a'; cout << g_str << endl; }
在test2.cpp中使用变量 g_str
//test2.cpp #include "test1.h" void fun2() { cout << g_str << endl; } void main() { fun1(); // a23456 fun2(); // 123456 }
这个时候你在跟踪代码时就会发现两个编译单元中的g_str地址并不相同
因为你在一处修改了它,所以编译器被强行的恢复内存的原貌,在内存中存在了两份拷贝给两个模块中的变量使用
正是因为static有以上的特性,所以一般定义static全局变量时,都把它放在原文件中而不是头文件,这样就不会给其他模块造成不必要的信息污染
关于静态变量
全局静态变量
优点:不管对象方法还是类方法都可以访问和修改全局静态变量,并且外部类无法调用静态变量,定义后只会指向固定的指针地址,供所有对象使用,节省空间。
缺点:存在的生命周期长,从定义直到程序结束。
建议:从内存优化和程序编译的角度来说,尽量少用全局静态变量,因为存在的生命周期长,一直占用空间。程序运行时会单独加载一次全局静态变量,过多的全局静态变量会造成程序启动慢。
局部静态变量
优点:定义后只会存在一份值,每次调用都是使用的同一个对象内存地址的值,并没有重新创建,节省空间,只能在该局部代码块中使用。
缺点:存在的生命周期长,从定义直到程序结束,只能在该局部代码块中使用。
建议:局部和全局静态变量从本根意义上没有什么区别,只是作用域不同而已。如果值仅一个类中的对象和类方法使用并且值可变,可以定义全局静态变量,如果是多个类使用并可变,建议值定义在model作为成员变量使用。如果是不可变值,建议使用宏定义。
类的静态变量
在 iOS 中,类的静态变量具有以下特点和用途:
特点:
- 全局唯一性:静态变量在类的所有对象之间共享,只有一份存储空间。
- 生命周期:静态变量的生命周期从程序开始一直到程序结束。
用途:
- 共享数据:可以用于在类的不同对象之间共享一些通用的数据,例如全局的配置信息、计数器等。
- 实现单例模式:通过将构造函数私有化,并使用静态变量来存储唯一的实例,实现单例模式。
例如,以下是一个简单的示例,展示了在 iOS 中类的静态变量的使用:
@interface MyClass : NSObject + (void)incrementCounter; + (NSInteger)getCounter; @end @implementation MyClass static NSInteger counter = 0; // 定义静态变量 + (void)incrementCounter { counter++; } + (NSInteger)getCounter { return counter; } @end
在上述示例中,counter
就是 MyClass
类的静态变量,可以通过类方法进行操作和获取。
const
const修饰的全局常量据有跟static相同的特性(有条件的,const放在只读静态存储区),即它们只能作用于本编译模块中,但是const可以与extern连用来声明该常量可以作用于其他编译模块中
因为const对象默认在文件内有效,所以当多个文件中出现同名const时,其实等同于在不同文件中分别定义了独立的变量。
不同于变量,常量的值是固定不可变的,一般用于只读值。
优点:只可以读取值,不能修改。一般用于接口或者文字显示这种固定值。添加extern可以对外全局常量,任意位置都可以访问。
缺点:存在的生命周期长,从定义直到程序结束。需要在.h .m中分别定义代码较多。
建议:良好的编码习惯而言,少使用宏,多使用常量。因为常量声明是有明确类型的,而宏只是替换并不能进行类型判断。不够严谨。
《C++primer》中,讲到头文件中不可以包含定义,有三个例外: 类,常量表达式初始化的const对象,inline
在 C 和 C++ 编程中,通常建议头文件中不包含定义,主要是为了避免多重定义的问题。然而,确实存在三个例外情况:
类:在头文件中定义类是常见且被允许的。因为类的定义包含了成员函数的声明,而成员函数的实际定义可以在源文件中实现。
例如:
class MyClass { public: void myMethod(); };
常量表达式初始化的
const
对象:如果一个const
对象是用常量表达式进行初始化的,那么它可以在头文件中定义。因为具有常量初始化的const
对象在多个编译单元中的定义是相同的,不会导致多重定义的问题。例如:
const int MAX_SIZE = 100;
inline
函数:inline
函数在调用处展开,所以在多个源文件中包含其定义不会引起问题。例如:
inline int add(int a, int b) { return a + b; }
这样的规则有助于保持头文件的简洁和避免潜在的链接错误。在实际编程中,遵循这些规则可以提高代码的可维护性和可移植性。
参考博客
一文看懂const extern static如何定义?究竟放在源文件还是头文件?
C++ extern/static/const区别与联系
019*:内存五大区:(栈、堆、全局静态区、常量区、代码区)(线程、函数栈、栈帧)
【iOS】—— 内存的五大分区
static const Vs extern const