• [iOS]static、extern、const关键字比较


    [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关键字

    extern
    extern关键字用来声明变量或者函数是一个外部变量或者外部函数,也就是说告诉编译器该变量是在其他文件中定义的,编译的时候不要报错,在链接的时候按照字符串寻址可以找到这个变量或者函数

    如果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 中,类的静态变量具有以下特点和用途:

    特点:

    1. 全局唯一性:静态变量在类的所有对象之间共享,只有一份存储空间。
    2. 生命周期:静态变量的生命周期从程序开始一直到程序结束。

    用途:

    1. 共享数据:可以用于在类的不同对象之间共享一些通用的数据,例如全局的配置信息、计数器等。
    2. 实现单例模式:通过将构造函数私有化,并使用静态变量来存储唯一的实例,实现单例模式。

    例如,以下是一个简单的示例,展示了在 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++ 编程中,通常建议头文件中不包含定义,主要是为了避免多重定义的问题。然而,确实存在三个例外情况:

    1. :在头文件中定义类是常见且被允许的。因为类的定义包含了成员函数的声明,而成员函数的实际定义可以在源文件中实现。

      例如:

      class MyClass {
          public:
              void myMethod();
      };
      
    2. 常量表达式初始化的 const 对象:如果一个 const 对象是用常量表达式进行初始化的,那么它可以在头文件中定义。因为具有常量初始化的 const 对象在多个编译单元中的定义是相同的,不会导致多重定义的问题。

      例如:

      const int MAX_SIZE = 100;
      
    3. 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

  • 相关阅读:
    unix环境编程:标准I/O库
    限制条件加入构造范围:Gym - 102832L
    R语言dplyr包summarise_at函数计算dataframe数据中多个数据列(通过向量指定)的均值和中位数、指定na.rm参数配置删除缺失值
    Python seaborn大更新,带来全新绘图方式seaborn.objects
    希尔排序超详细讲解C语言
    初识OAuth2.0
    技术人员怎样提升对业务的理解
    面对数据增量同步需求,怎样保障准确性和及时性?
    trafilatura 网页解析原理分析
    Centos7安装RabbitMQ
  • 原文地址:https://blog.csdn.net/m0_74703932/article/details/140433242