全局变量以及静态局部变量

一、全局变量

在C语言中,全局变量也是一种特别常见的变量类型,有时它也被称为外部变量。

这是因为它们在函数之外被定义,并且可以在整个文件内,甚至其他文件中(通过外部链接)被访问和使用。

只要在一个文件内,所有函数的外部,直接声明定义的变量都是全局变量。

二、存储位置以及生命周期 

全局变量被存储在虚拟内存空间当中,一片被称之为"数据段"的内存区域当中。

不同于局部变量随着函数的调用和返回被创建和销毁,全局变量的生命周期从程序开始执行时开始,持续到程序完全结束。

简而言之,全局变量与程序的生命周期同步:它们在程序开始时被创建并初始化,并在程序结束时被销毁。

在C语言中,这种持续存在于程序整个执行周期的生命周期特性被称为“静态存储期限”

三、初始化

当C程序开始执行时,会在程序加载时为数据段中的变量进行初始化,包括全局变量。此时:

  1. 如果全局变量已由程序员明确初始化赋值,该初始值会直接在程序启动时分配给它。
  2. 若程序员未进行显式初始化赋值,系统则会为其设置默认值(也就是0值),如:整型和浮点型变量默认值为0,指针变量默认值为NULL。

注意:

  1. 除非不确定此全局变量的初始值,否则建议对其进行手动初始化。
  2. 全局变量的初始化有且仅有程序启动时的一次。
  3. 局部变量不会自动初始化

 四、作用域

全局变量的作用域从"声明位置"开始,并延申至整个程序。具体来说:

  1. 在定义全局变量的文件内,全局变量可以在其声明之后的任何位置被访问和修改。
  2. 要想在其他源文件中使用该全局变量,可以通过extern关键字来引用它。

 例如,在一个main.c文件中定义全局变量:

#include 

int global_num = 10;    // 从此处开始,整个文件都可以访问和修改global_num

int main(void) {
    printf("在main.c文件中,打印全局变量的值为%d\n", global_num);
    return 0;
}

 如果想在demo.c文件中使用此全局变量,使用extern关键字,演示代码如下:

#include 
// 使用extern声明来表明global_num是在其他文件中定义的
// 从这里开始该文件中也可以使用变量global_num了
extern int global_num;  

void print_global(void) {
    printf("在demo.c文件中,打印全局变量的值为%d\n", global_num);
}

 为了在main.c文件中调用print_global函数,需要借助头文件进行函数声明,最终代码如下:

// main.c
#include 
#include "demo.h"

int global_num = 10;

int main(void) {
    printf("在main.c文件中,打印全局变量的值为%d\n", global_num);
    print_global();

    printf("在main.c中将全局变量的取值改为100\n");
    global_num = 100;
    print_global();
    return 0;
}

// demo.c
#include 
#include "demo.h"

extern int global_num;  

void print_global(void) {
    printf("在demo.c文件中,打印全局变量的值为%d\n", global_num);
}

// demo.h
void print_global(void); // 声明函数,供其他文件使用

运行此程序,结果是:

在main.c文件中,打印全局变量的值为10

在demo.c文件中,打印全局变量的值为10

在main.c中将全局变量的取值改为100

在demo.c文件中,打印全局变量的值为100

总结:

  1. global_num变量是在main.c文件中定义的,它在本文件中声明位置以下的部分,都可以随意使用。
  2. 在demo.c文件中,使用extern关键字只是告诉编译器global_num变量在其余文件中定义,它并不会创建了一个新的变量。
  3. 关于跨文件调用函数,一般步骤如下:

    1. 在一个头文件中声明你想跨文件调用的函数。
    2. 在一个源文件中包含头文件,然后定义这个函数。(包含自定义头文件,使用#include "xx.h"
    3. 在另一个源文件中,包含头文件并直接调用该函数。

我们再来回顾一下C语言中关于变量声明和定义的概念:

  1. 变量的声明:声明是给编译器看的,告诉编译器变量的类型和名字等信息,但变量的具体内存分配发生在运行时期。
  2. 变量的定义是声明一个变量,并且为运行时期为变量分配内存空间的组合动作。

变量的定义总是一个声明,但某些变量的声明并不是定义,也就是说某些变量的声明不会在运行时期分配内存空间。

在以往,我们见到的所有声明语句都是变量的定义,也就是在运行时期会为变量分配内存。那么在这里,我们就见到了仅声明,不分配内存空间的变量声明语句:

 五、全局变量的使用建议

总之,对于全局变量的使用,我们给出以下建议:

  1. 程序应当尽可能的不使用全局变量。比如多个函数要共享数据,可以优先考虑用参数传递的方式共享,而不是全局变量。
  2. 如果非要使用全局变量,尽量限制其不能跨文件使用,即使用static修饰的全局变量。
  3. 如果非要使用全局变量,那么一定要给出一个清晰的命名约定。比如约定以"global_"开头的都是全局变量。
  4. 在代码的文档或注释中清晰地描述全局变量的用途、范围和正确使用方式。

在适当的场景下,全局变量是有其合理用途的,不必完全回避,但应当谨慎使用。

六、 static修饰全局变量

tatic修饰的全局变量的主要效果是将其作用域限制在声明它的文件中,这意味着该变量只能在它所在的源文件中被访问和修改。其它的特性,如生命周期、初始化方式和存储位置,与普通的全局变量是相同的。

比如上面的global_num全局变量,一旦使用static关键字修饰,再次运行程序,就会产生链接错误。因为此时的全局变量已经不能被链接到外部使用了。

七、静态局部变量

静态局部变量,简单来说,就是static修饰的局部变量。

静态局部变量就是在原本局部变量定义的基础上,使用static关键字声明得到的变量。比如:

void foo() {
    static int static_var = 0; // 定义了一个静态局部变量
}

 1. 存储位置及生命周期

静态局部变量的存储位置和生命周期和全局变量是一致的:

  1. 都是存储在数据段区域当中。
  2. 生命周期都是从程序启动到程序结束,都是"静态存储期限"。

这就意味着,静态局部变量与一般的局部变量不同:

静态局部变量不会随着函数的返回而销毁,它会始终保留到程序执行完毕。

2. 初始化

不要将静态局部变量理解成"活得更长"的局部变量,局部变量和静态局部变量在初始化方面有非常大的区别。

静态局部变量的初始化特性

  1. 默认初始化:如果不显式地为静态局部变量提供一个初始值,系统会默认将其初始化为0值。
  2. 初始化只有一次:静态局部变量只会在其所在函数,第一次被调用时初始化一次。此后,不论调用几次该函数,都不会再次初始化了。

下面我们举一个代码案例,来讲解这两个特点。

#include 

void call_counter() {
    static int count = 0; // 静态局部变量,仅在首次调用时初始化为0,再次调用不会再次初始化
    count++; // 每次调用函数时,增加count计数器
    printf("此函数已经被调用了%d次!\n", count);
}

int main(void) {
    for (int i = 0; i < 5; i++) {
        call_counter();
    }
    return 0;
}

程序的输出结果是:

此函数已经被调用了1次!

此函数已经被调用了2次!

此函数已经被调用了3次!

此函数已经被调用了4次!

此函数已经被调用了5次!

3. 作用域

静态局部变量的作用域仅限于其所在的函数。这意味着:

尽管静态局部变量的生命周期与程序的整个执行期间一致,但它只能在定义它的函数内部被访问。

4. 静态局部变量的使用场景

静态局部变量,可以在同一个函数的多次调用之间进行数据保存和修改,这非常有用。也就是说,如果一个数据需要被一个函数的多次调用进行操作,那么就非常适合使用静态局部变量。

一个非常经典的例子就是"生成独立序号":

定义一个函数,每次调用该函数都生成一个独立的序号。

参考代码如下:

int get_id() {
    static int current_id = 1;  // 初始ID为1
    return current_id++;        // 返回当前ID,然后进行累加
}

int main(void) {
    printf("New ID: %d\n", get_id());  // 输出: New ID: 1
    printf("New ID: %d\n", get_id());  // 输出: New ID: 2
    printf("New ID: %d\n", get_id());  // 输出: New ID: 3
    return 0;
}

这个代码虽然很简单,但在许多实际应用中都会用到类似的机制来生成唯一独立序号。

八、补充

静态存储期限的变量包括:

  1. 全局变量
  2. 静态局部变量
  3. static修饰的全局变量

这些变量即使不手动初始化,也会自动得到一个默认的0值。但如果你想显式地为这些变量指定一个初始值,那么初始化操作,赋值运算符的右侧必须是一个常量表达式

int get_value() {
    return 5;
}

void func() {
    int a = 10;
    static int num = get_value(); // 这是错误的
    static int num = a; // 这是错误的
    static int num = 100; // 这是正确的
    static int num = 100 / 20; // 这是正确的
}

这些变量,之所以有同样的初始化特点是由于它们都存储在数据段这一内存区域。

在程序启动的过程中,数据段内的变量就需要进行初始化。此时,由于运行时上下文还未完全建立,用依赖于运行时的变量表达式、函数调用等作为初始值是不可行的,也是不可能的。

因此,使用常量表达式作为这些变量的明确初始值,也就成了必然。

小tips:

我们上面讲:”静态局部变量是在函数第一次调用时被初始化“。

这种说法实际上是不准确的,只是为了方便大家理解采取的一个折衷的说法。

实际上:

静态局部变量也是在程序启动时就进行了初始化,也就是说在函数调用之前,静态局部变量的初始值就已经确定了。

但静态局部变量的作用域始终局限于函数内,如果不调用函数必然无法访问它,所以粗略的认为”静态局部变量是在函数第一次调用时被初始化“也问题不大。

你可能感兴趣的:(c,c++,开发语言)