C语言变量的作用域,生命周期和链接相关

前言

本文介绍C语言的三个很重要的概念:

  • 变量的作用域
  • 变量的生命周期
  • 变量或者函数的链接

写在前面

先介绍一个概念:翻译单元
C语言中有两种文件,头文件.h代码文件.c
翻译单元指的是包含头文件,并且将头文件展开以后的代码文件.c,而每个翻译单元都有一个文件作用域,实际上,他是编译器编译的完整对象单元

变量的作用域

变量的作用域其实是一个范围,只有在这个范围以内变量才是可用的,作用域是可以嵌套的,变量使用的永远是最内层声明或者定义的那个。注意,声明的变量也算,看下面的例子:

#include <stdio.h>
int main(void)
{
    float PI = 2.14;
    printf("PI = %f\n",PI);
    for(int i = 0;i<1;i++)
    {
        extern float PI;
        printf("PI = %f\n",PI);
    }
    return 0;
}

这个程序运行的结果如下:

PI = 2.140000
PI = 3.141593

我在另外一个calc.c文件中定义了一个PI值,所以第二次打印的时候这个extern表示这是一个声明,他的定义在另外一个翻译单元中。

#include "calc.h"
const float PI=3.1415926;

块作用域

一对大括号括起来就形成了一个块作用域
在一个块作用域内声明的变量的可用范围是从这个块中变量定义处到包含该定义块的结尾,块作用域需要注意几个问题:

  • 函数的参数虽然在块的外面,但是他也属于函数所在的块
    这个一般的理解其实就是函数参数无论是保存在寄存器还是保存在栈上都是在函数调用(call)开始之后进行的
  • 对于if,while,for等循环或者判断的情况,如果没有用花括号括起来,也算是块的一部分,并且我们可以将这些情况理解为两个块,比如for语句,整个for语句看成是一个块,for后面的循环体执行逻辑作为这个块的子块,这样很多问题的比较容易理解了,看下面的例子:
#include <stdio.h>
int main(void)
{
    for(int i = 0;i<10;i++)
    {
        printf("%d|",i);
        int i = 6;
        printf("%d, ",i);
    }
    printf("\n");
    return 0;
}

输出结果:

0|6, 1|6, 2|6, 3|6, 4|6, 5|6, 6|6, 7|6, 8|6, 9|6, 

在第一次执行循环体的时候,定义了一个i = 6,但是再一次回到for,执行i++的时候依然是用的在for语句定义的i,我们可以把for语句这样写一下:

{//第一层块
	int i = 0;
	执行for循环
	{//第二层块
		printf("%d|",i);
		int i = 6;
		printf("%d, ",i);
	}
}

这样每次执行for循环的时候内部定义的i就失效了,使用的还是外部定义的i变量

函数作用域

函数作用域指的是即使在函数的内层块中,也会自动提升作用域到整个函数,这种作用域只用于goto的标签(是不是很没有用!!),也就是即使goto的标签定义在一个函数内层块中,也不能在另一个块中定义相同的标签,因为标签的作用域是整个函数,看下面例子:

#include <stdio.h>
int main()
{
    goto b;
    {
    b:
       printf("bbb");
    }
    return 0;
}

运行会打印字符串bbb,虽然标签b定义在一个函数内层块中,但是他具有函数作用域
否则,我们可以在一个函数的另一个块中定义一个同名的标签跳转,这样我们如果使用goto语句的话就太混乱了。所以C语言才有函数作用域这么个东西

函数原型作用域

函数原型作用域针对函数的声明中形参起作用,形参的作用域从定义开始到函数原型定义结束,一般这种情况是针对于函数声明中有可变数组的情况下,比如下面的声明:

void reset(int m,int n,int arr[m][n],int val);

文件作用域

如果一个变量定义在函数外面,就说该变量具有文件作用域,定义在文件中的函数也具有文件作用域,也就是文件中所有函数都可以直接使用,根据变量或函数定义是否添加static关键字,可以分为具有内部链接的文件作用域和具有外部链接的文件作用域

  • 内部链接的文件作用域:只有当前文件内可以使用
  • 外部链接的文件作用域:外部文件也可以使用,不过外部文件使用时需要使用extern进行声明,这样编译器编译的时候就会在符号表中为该变量或者函数添加符号,链接器在链接的时候就会根据符号去别的文件中寻找定义

变量的生命周期

变量的生命周期就是变量在程序运行过程中可用的时间段,主要有四种:

  • 静态变量
  • 线程变量
  • 局部变量或者叫自动变量
  • 动态变量

静态变量

静态变量也叫全局变量,是指在程序运行过程中一直存在的变量,所谓静态变量,就是指变量不是存储在运行栈或者运行堆上,而是放在固定的内存位置,比如.data数据块等地方。C语言有如下几种静态变量:

  1. 定义在函数外面的变量,也就是具有文件作用域的变量,无论是否使用static修饰,都是全局变量
  2. 使用extern声明的变量,虽然定义在别的地方,但是也是静态变量
  3. 在函数内部使用static关键字定义的变量,该变量虽然作用域只限于当前定义的函数内部,但是在整个程序运行期间都存在,并且我们通过指针使用该变量,看下面例子:
#include <stdio.h>
int* get();
int main(void)
{
    int c = *get();
    printf("%d\n",c);
    return 0;
}

int* get()
{
    static int a = 10;
    return &a;
}

运行结果

10

线程变量

线程变量是通过使用关键字_Thread_local声明的变量,每个线程都会获取变量的一个单独的备份,看下面的例子:

#include <stdio.h>
#include <pthread.h>
_Thread_local int threadVar = 10;
void threadHandle();
int main(void)
{
    int c = 0;
    for(int i = 0;i<6;i++)
    {
        pthread_t threadNo;
        if((c = pthread_create(&threadNo, NULL, threadHandle, NULL)) != 0)
        {
            printf("thread create failed. errno:%d",c);
            continue;
        }
    }
    return 0;
}

void threadHandle()
{
    threadVar++;
    printf("threadHandle::%d\n",threadVar);
    pthread_exit(0);
}

输出

threadHandle::11
threadHandle::11
threadHandle::11
threadHandle::11
threadHandle::11

局部变量或者叫自动变量

局部变量是指在块内部定义的变量,该变量或者在函数执行期间通过寄存器保存,或者保存在程序栈上。该变量在块结束后就失效。
注意:自动变量不会自动初始化,所以一定要初始化自动变量,不然值是不确定的

动态变量

动态变量是指变量的定义和内存分配是通过用户控制,在程序运行期间动态执行的,动态变量一般分配在程序堆上,通过制定的函数接口来分配和释放
使用动态函数需要引用头文件

#include <stdlib.h>

malloc

该函数接受一个参数:所需的内存字节数,然后去找合适的空闲内存块,返回内存的地址。

void	*malloc(size_t __size)

注意:
这个函数分配的内存是未初始化的
如果分配内存失败,返回NULL

free

该函数接受一个参数,就是前面malloc返回的内存指针,该函数的作用是释放分配的内存

可以看出动态内存非常灵活,比如我们可以在程序运行过程中动态创建数组,链表等数据结构,但是动态分配的内存在程序运行期间会一直增加,除非你用free函数释放内存。并且,有些操作系统,即使应用程序结束,动态分配的内存也不会释放,所以,使用动态内存需要注意一下几点:

  1. 需要维护好动态分配的内存指针,并且在不使用的时候用free释放
  2. 释放后的指针需要设置成NULL,避免free多次

calloc

calloc和malloc一样,也是用于动态分配内存,但是calloc是按照存储单元分配内存,该函数接受两个参数,第一个参数是所需的存储单元的数量,第二个参数是存储单元占用的字节数。比如下面的例子:

long* newmem = (long*)calloc(100,sizeof(long));

这种分配方式有个好处就是即使一个系统上long类型占用的字节数不是四个字节,该函数分配的内存依然够用。

free函数也可用于释放calloc分配的内存

变量或者函数的链接

链接的原因

之所以要有链接的概念,是因为我们为了编写大型应用程序,需要将程序进行模块化,分成一个个的翻译单元,比如我们创建一个游戏引擎的程序,需要

  • 日志模块
  • 内存管理模块
  • 音视频解码
  • 图像解码
  • 数学库
  • 材质管理
  • 多媒体播放
  • 事件系统
  • 动画系统
  • 渲染模块
  • 等等
    但是各个模块之间还会相互调用,比如我在数学库中定义的一个矩阵相乘的方法,可能在很多别的模块也需要,但是函数的定义在数学库中,别的模块怎么找到呢,这就是链接的目的,链接就是寻找在外部定义的变量或者函数的地址

链接的方式

C语言中能被外部函数或者内存函数使用的变量或者函数只有两种,前面讲过,就是具有文件作用域的变量或函数:

  • 外部链接:函数外部声明的函数或者变量,没有使用static修饰符,具有外部链接。外部使用时必须使用extern进行声明我们通常把一个翻译单元的外部链接变量或者函数使用extern在头文件中集中进行声明,别的翻译单元只需要引用该头文件即可使用
  • 内部链接:函数外部声明的函数或者变量,使用static修饰符,具有内部链接。内部链接的变量或函数只能供当前翻译单元使用。

最近更新

  1. TCP协议是安全的吗?

    2023-12-13 15:36:03       16 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-13 15:36:03       16 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-13 15:36:03       15 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-13 15:36:03       18 阅读

热门阅读

  1. Babylonjs学习笔记(十)——拉伸多边形

    2023-12-13 15:36:03       33 阅读
  2. 名称空间与函数对象

    2023-12-13 15:36:03       35 阅读
  3. 工具:Jupyter

    2023-12-13 15:36:03       38 阅读
  4. 力扣面试150题 | 209.长度最小的子数组

    2023-12-13 15:36:03       34 阅读
  5. 工厂模式实现

    2023-12-13 15:36:03       40 阅读
  6. 力扣labuladong——一刷day70

    2023-12-13 15:36:03       39 阅读
  7. POJ:1113

    2023-12-13 15:36:03       41 阅读
  8. springboot全局异常处理和自定义异常处理

    2023-12-13 15:36:03       40 阅读
  9. 轻松应用字典树

    2023-12-13 15:36:03       41 阅读