Files
BlogPosts/Collection/YoudaoyunNotes/02C语言/12-作用域与存储期.md

12 KiB
Raw Blame History

函数栈内存

局部变量与栈内存

  • 局部变量概念:凡是被一对花括号包含的变量,称为局部变量

  • 局部变量特点:

  • 某一函数内部的局部变量,存储在该函数特定的栈内存中(函数的栈帧<当函数被执行时出现>中)

  • 局部变量只能在该函数内可见,在该函数外部不可见

  • 当该函数退出后,局部变量所占的内存立即被系统回收,因此局部变量也称为临时变量

  • 函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量

  • 栈内存(函数的栈)特点:

  • 每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量

  • 每当一个函数退出时,系统将自动回收其栈内存

  • 系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配的原则

  • 示例代码:

int max(int x, int y) // 变量 x 和 y 存储在max()函数的栈中
{
    int z;            // 变量 z 存储在max()函数的栈中
    z = x>y ? x : y;
    return z;         // 函数退出后栈中的x、y 和 z 被系统回收将max函数栈中的z的值反馈传递
}

int main(void)
{
    int a = 1; // 变量 a 存储在main()函数的栈中
    int b = 2; // 变量 b 存储在main()函数的栈中
    int m;     // 变量 m 存储在main()函数的栈中,未赋值因此其值为随机值
    
    m = max(a, b); // 将main栈中的a和b的值传递给max函数
}

函数调用时代码的执行流程

  • 技术要点:

  • 栈内存相对而言是比较小的,不适合用来分配尺寸太大的变量。

  • return 之后不可再访问函数的局部变量,因此返回一个局部变量的地址通常是错误的。

全局变量

  • 全局变量概念:凡是定义在花括号外部的变量,称为全局变量

  • 全局变量特点:

  • 全局变量定义函数体外,存储在该程序特定的内存中(数据段)

  • 全局变量在整个程序中可见

  • 当该程序退出后,全局变量所占的内存立即被系统回收

作用域

基本概念

C语言中标识符都有一定的可见范围这些可见范围保证了标识符只能在一个有限的区域内使用这个可见范围被称为作用域scope

在软件开发中,尽量缩小标识符的作用域是一项基本原则,一个标识符的作用域超过它实际所需要的范围时,就会对整个软件的命名空间造成污染,导致一些不必要的名字冲突和误解。

函数声明作用域

  • 概念:在函数的声明式中定义的变量,其可见范围仅限于该声明式。

  • 示例:

void func(int fileSize, char *fileName);
  • 要点:

  • 变量 fileSize 和 fileName 只在函数声明式中可见。

  • 变量 fileSize 和 fileName 可以省略,但一般不这么做,它们的作用是对参数的注解。

局部作用域

  • 概念:在代码块(被一对花括号包含)中定义的变量,其可见范围从其定义处开始,到代码块结束为止。

  • 示例:

int main()
{
    int a=1;
    int b=2;     // 变量 c 的作用域是第4行到第9行
    {
        int c=4;
        int d=5; // 变量 d 的作用域是第7行到第8行
        int a = 100;
    }
    c = 100; // 超出c的作用域此次视为未定义c
}
  • 要点:

  • 代码块指的是一对花括号 { } 括起来的区域。

  • 代码块可以嵌套包含,外层的标识符会被内嵌的同名标识符临时掩盖变得不可见。

  • 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。

全局作用域

  • 概念:在代码块外定义的变量,其可见范围可以跨越多个文件。

  • 示例:

#include <stdio.h>

int global = 888; // 变量 global 的作用域是第2行到本文件结束

extern void f1();  // 外部声明
extern void f2();

int main()
{
    global = 666;
    printf("%s:global = %d\n", __FUNCTION__, global);

    f1();
    f2();
}

void f()
{ 
    printf("a.c :%s is running\n", __FUNCTION__);
}
#include <stdio.h>

// extern 外部声明,表示这个标识符不属于本文件而是来自外部的文件,这个标识符的作用域足够大
// extern 外部声明的标识符不能初始化
extern int global; // 声明在 a.c 中定义的全局变量,使其在 b.c 中也可见
extern void f();  // f函数定义在本文件外  

void f1()
{
    printf("%s :global=%d\n",__FUNCTION__, global);  // 访问的是a.c中定义的全局变量
}
void f2()
{
    printf("b.c :%s is running\n", __FUNCTION__);
    f();
}
// int main()
// {}

注意由于b.c中存在外部变量和函数此时b.c是不能单独通过编译的因此需要b.c功能则需要将a.c和b.c一并编译生成一个程序“gcc b.c a.c”当多个C语言源文件同时参与编译时多个C语言文件中只能存在一个main函数。

作用域的临时掩盖

如果有多个不同的作用域相互嵌套,那么小范围的作用域会临时 “遮蔽” 大范围的作用域中的同名标识符,被 “遮蔽” 的标识符不会消失,只是临时失去可见性。

  • 示例代码:
#include <stdio.h>
int a = 100;  // 整个程序可见

// 函数代码块1
int main(void)
{
    printf("%d\n", a); // 输出100, 全局变量a
    int a = 200;
    printf("%d\n", a); // 输出200, 局部变量a的作用域掩盖全局变量a的作用域
   
    // 代码块2 
    {
        printf("%d\n", a); // 输出200局部变量a
        int a = 300;
        printf("%d\n", a); // 输出300 当前代码块中定义的a作用域掩盖了代码块外的局部变量a的作用域
    }
    printf("%d\n", a); // 输出200局部变量a

    f();
}

void f()
{
    printf("%d\n", a); // 输出100, 全局变量a
}

static关键字

C语言的一大特色是相同的关键字在不同的场合下具有不同的含义。static关键字在C语言中有两个不同的作用

  1. 将可见范围设定为标识符所在的文件(缩小作用域):
  • 修饰全局变量:使得全局变量由原来的跨文件可见,变成仅限于本文件可见。

  • 修饰普通函数:使得函数由原来的跨文件可见,变成仅限于本文件可见。

  1. 将存储区域设定为数据段(延长存储期):
  • 修饰局部变量:使得局部变量由原来存储在栈内存,变成存储在数据段。

  • 示例:

       int a; // 普通全局变量,跨文件可见
static int b; // 静态全局变量,仅限本文件可见

void f1()        // 普通函数,跨文件可见
{}

static void f2() // 静态函数,仅限本文件可见
{}

int main()
{
           int c; // 普通局部变量,存储于栈内存
    static int d; // 静态局部变量,存储于数据段
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* 当变量标识符重名时优先使用当前{}内的标识符 */

int a=100; //跨文件可见
static int b = 10; //本文件可见

void fun(void)
{
    static int b = 20; //本函数可见,且只被初始化一次
    printf("%d:b=%d\n",__LINE__, b++);
}

int main(void)
{
    printf("%d:a=%d\n",__LINE__, a);
    int a=10; //当前{}内可见,被定义到}
    
    if(a==10)
    {
        printf("%d:a=%d\n",__LINE__, a);
        int a=20; //当前{}内可见,被定义到}
        printf("%d:a=%d\n",__LINE__, a);
    }
    printf("%d:a=%d\n",__LINE__, a);

    for (int i=0; i<3; i++) //当前{}内可见,被定义到}
    {
        fun();
    }

    return 0;
}

存储期

C语言中变量都是有一定的生存周期的所谓生存周期指的是从分配到释放的时间间隔。为变量分配内存相当于变量的诞生释放其内存相当于变量的死亡。从诞生到死亡就是一个变量的生命周期。

根据定义方式的不同,变量的生命周期有三种形式:

  1. 自动存储期

  2. 静态存储期

  3. 自定义存储期

不同存储区段对应不同存储期

自动存储期

在栈内存中分配的变量,统统拥有自动存储期,因此也都被称为自动变量。这里自动的含义,指的是这些变量的内存管理不需要开发者操心,都是全自动的:在变量定义处自动分配,出了变量的作用域后自动释放。

  • 以下三个概念是等价的:

  • 自动变量:从存储期的角度,描述变量的时间特性。

  • 临时变量:同上。

  • 局部变量:从作用域的角度,描述变量的空间特性。

可以统一把它们称为栈变量,下面是示例代码:

int main()
{
    int a, b;     // 自动存储期
    static int c; // 加了static(修饰静态数据)的局部变量不再是栈变量,而是静态数据了
    
    f(a, b);
}

void f(int x, int y) // 自动存储期
{
}

静态存储期

在数据段中分配的变量,统统拥有静态存储期,因此也都被称为静态变量。这里静态的含义,指的是这些变量的不会因为程序的运行而发生临时性的分配和释放,它们的生命周期是恒定的,跟整个程序一致。

  • 静态变量包含:

  • 全局变量:不管加不加 static任何全局变量都是静态变量。

  • static 型局部变量。

  • 示例代码:

int g1;        // 静态存储期
static int g2; // 静态存储期

int main()
{
    int a, b;
    static int c; // 静态存储期
}
  • 注意1

  • 若定义时未初始化则系统会将所有的静态数据自动初始化为0

  • 静态数据初始化语句,只会执行一遍。

  • 静态数据从程序开始运行时便已存在,直到程序退出时才释放。

  • 注意2

  • static修饰局部变量使之由栈内存临时数据变成了静态数据可见范围不变。

  • static修饰全局变量使之由各文件可见的静态数据变成了本文件可见的静态数据生命周期不变。

自定义存储期

在堆中分配的变量,统统拥有自定义存储期,也就是说这些变量的分配和释放,都是由开发者自己决定的。由于堆内存拥有高度自治权,因此堆是程序开发中用得最多的一片区域。

自由的堆内存

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *fun(void)  //栈空间中会自动释放但数据段和堆空间例外
{
    // char str[20]; //栈空间仅在当前{}有效
    // return str;
    char *p = malloc(20); //堆空间可以在整个程序中有效,只要不释放就一直可用。
    if(p != NULL)
    {
        memset(p, 0, 20);
        return p;  //返回的是指针的值及堆空间地址
    }    
    
    return NULL;
}

int main(void)
{
    char *p;
    p = fun();  //得到堆空间的地址
    char *k = "hello GEC";
    memcpy(p, k, strlen(k));  //将数据拷贝到堆空间中
    printf("__%d__:%s\n", __LINE__, p);

    free(p);  //将fun函数中申请的堆空间释放
    return 0;
}

总结

作用域:是标识符(变量名字、函数名等)的可见范围(可以使用)。

存储期:是标识符的生命周期即标识符拥有内存开始到标识符的内存被释放为止。

标识符的作用域与生命周期在绝大多数情况下相同但除static修饰的标识符外

1、static修饰全局变量和函数时static限制了全局变量和函数的作用域但不修改其生命周期

2、static修饰局部变量时static修改局部变量的生命周期但不改变其作用域

(面试笔试题)堆和栈有什么区别?

  1. 栈是由系统自动分配释放,堆是由用户手动进行申请和释放

  2. 栈空间的效率高于堆空间

  3. 堆空间要比栈空间大

  4. 栈从上往下进行增长范围有限,堆从下往上增长范围没有明确的限制

堆内存的申请和释放方法?

  1. malloc()和calloc()进行申请

  2. free()进行释放

malloc()和calloc()的不同?

  1. malloc只能申请一片连续的堆内存并且不会对内存进行处理。

  2. calloc既可以申请一片空间也可以一次申请多片连续内存并且会对内存进行清零。

内存管理测试题.docx