C语言存储类别、链接与内存管理,你真的都会了吗

437 阅读11分钟

概览

  1. 作用域、链接、存储期
  2. C的五种存储类别:自动变量、寄存器变量、块作用域的静态变量、外部链接的静态变量、内部链接的静态变量;多文件及其编译;函数的存储类别仅两种:extern和static(外部函数和静态函数)
  3. rand和srand、malloc和free(创建动态数组与变长数组的区别:存储类别不同,变长数组是自动存储期)、内存泄漏、静态与自动与动态分配内存
  4. ANSI C类型限定符:const、volatile、restrict、_Atomic;存储类别限定符:auto、register、static、extern、typedef

示例

  1. 作用域、链接、存储期

作用域有:块作用域(C99把if、while、for等语句中的)、函数作用域、函数原型作用域、文件作用域 链接有:无连接(块作用域)、外部链接和内部链接(针对文件作用域) 存储期有:自动、静态

函数作用域(function scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果在两个块中使用相同的标签会很混乱,标签的函数作用域防止了这样的事情发生。

函数原型作用域的范围是从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。而且,即使有形参名,也不必与函数定义中的形参名相匹配。只有在变长数组中,形参名才有用

文件作用域的变量可用于多个函数,所以也称为全局变量

编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。

  1. C的五种存储类别:自动变量、寄存器变量、块作用域的静态变量、外部链接的静态变量、内部链接的静态变量;多文件及其编译;函数的存储类别仅两种

由作用域、链接 、存储期的概念,可以理解C的五种存储类别:

  • 外部链接的静态变量 具有 文件作用域、外部链接、静态存储期,也称外部存储类别,属于该类别的变量称为外部变量。把变量的定义性声明放在所有函数的外面就创建了外部变量,为了指出在函数中使用了外部变量可以在函数中使用关键字extern再次声明,但这完全可以省略,因为外部变量具有文件作用域,到文件结尾都可见。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用 extern 在该文件中声明该变量。外部变量只能初始化一次,且必须在定义该变量时进行。与自动变量(内存中原有的值,即垃圾值)不同的是,如果未初始化外部变量,它们会被自动初始化为 0。这一原则也适用于外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量。

多文件,要在多文件(多个翻译单元)中使用,就是关于外部变量(外部链接的静态变量)、外部函数(默认或extern声明)的探讨。

  1. rand和srand、malloc和free(创建动态数组与变长数组的区别)、内存泄漏、静态与自动与动态分配内存
  • rand 包含于stdio.h 中, srand 包含于 stdlib.h 中;rand(伪随机数生成器)随机获取一个介于0-RAND_MAX,定义于 stdlib.h 中,通常是INT_MAX的整数,如果需要自己指定范围的话需要用上模运算,每次运行程序你会发现,得到的随机数都是一样的,为什么?因为声明为外部链接的静态变量的种子在每次程序载入内存时永远的初始化为1,所以根据公式计算得到的随机数就是一定的了,但随机数的分布是符合统计规律的,这也就是为什么叫伪随机数的原因。既然如此,要想每次运行都获取不一样的随机数就需要对种子进行更改,C提供了srand函数进行“播种”,一般的种子值是unsigned int 类型,你可以手动指定,但更方便的是使用 time.h 中的 time 函数,该函数在参数为空指针时返回系统时间(每次都不一样,随时间的变化而变化),一个 time_t 类型的值,可以强制转换为 unsigned int 类型,可以传入一个 time_t 类型的变量的地址,这样 time 函数直接改变该变量的值,否则传入0通过返回值机制来提供值。
  • malloc、calloc、free都都包含于stdlib.h中;malloc 有一个参数即需要分配的字节数,为 size_t 类型的值,返回的是分配的内存的首字节的地址,通常可以以 char * 类型返回(然后经强制转换后赋值给相对应的指针变量),从 ANSI C 标准开始返回 void * 类型,相当于一个“通用指针”,无需考虑类型匹配问题可以赋给任意类型的指针,但还是建议使用强制转换,提高代码的可读性,同时在C++中时必须的。如果 malloc 内存分配失败则会返回一个空指针。calloc 与 malloc 类似,接收两个无符号整数作为参数,第一个参数是需要的存储单元的数量,第二个是存储单元的大小(字节数),calloc 的一个特性是把块中所有位都设置为0。
  • 如果内存分配失败怎么办?可以通过调用 stdlib.h 中的 exit 函数,该函数需要 一个 int 型参数,可以是 EXIT_FAILURE表示 程序异常中止,也可以是 EXIT_SUCCESS (相当于0)表示普通的程序结束,一些操作系统还接收其他的运行错误整数值。

  • 创建动态数组与变长数组,
  • 现在,有创建数组的3中方法:

  • 内存泄漏:指的是自动存储区(通常是栈内存)中 malloc 后,指针被销毁了而指针指向的存储空间没有free,导致不能被回收而重复使用,一直占用内存,无法被再访问和再使用。

静态内存:静态内存的数量在程序编译时固定,在程序运行期间也不会改变。在程序载入内存时载入数据,在程序运行过程中一直存在。 自动内存:当程序运行到块中时创建,退出该块时销毁。使用的内存数量在程序执行期间自动增加或减少 。 动态内存:使用函数进行分配。内存数量只会增加,除非使用 free 函数进行释放。

#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main(int argc, char * argv[]){
    // 创建块作用域的静态变量
    static int b[3];
    // 创建动态数组
    int a;
    while(scanf("%d",&a) != 1){
        printf("fail!");
    }
    int * p;
    p = (int *)malloc(a*sizeof(int));
    // 垃圾值:自动变量不会初始化,除非显式初始化它。
    // 如果未初始化外部变量,它们会被自动初始化为 0,只能使用常量表达式初始化文件作用域变量。
    printf("a: ");
    for(int i=0; i<a; i++){
        printf("%d ",p[i]);
    }
    // 打印未经初始化的静态变量的值
    printf("\nb: ");
    for(int i=0; i<3; i++){
        printf("%d ",b[i]);
    }
    printf("\n");
    // 创建变长数组
    int c[a];
    printf("c: ");
    for(int i=0; i<a; i++){
        printf("%d ",c[i]);
    }
    // 释放动态数组
    free(p);
}   

// 结果示例
// 4
// a: 0 -1073741824 0 -1073741824 
// b: 0 0 0 
// c: -503056272 32766 230428283 1
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main(int argc, char * argv[]){
    srand(time(NULL));  // 每次初始化静态种子时播种
    for(int i=0; i<10; i++){
        int a = rand();
        printf("%d ",a%10);    
    }
}   

// 结果示例
// 7 5 3 7 3 5 1 4 2 7
  1. ANSI C类型限定符:const、volatile、restrict、_Atomic;存储类别限定符:auto、register、static、extern、typedef
  • 引用式声明可以有多个,而定义式声明只有一个。定义式声明会为变量预留存储空间而引用式声明只是告知编译器使用之前已经创建的变量。

  • 存储类别限定符

    • auto 表明变量具有自动存储期,只能作用于块作用域的变量声明中,在块作用域中声明的变量本身就具有自动存储期,因此使用 auto 是为了与明确表示与外部同名变量不同的使用意图(即作为局部变量)。
    • register 表明变量为寄存器类别,只能作用于块作用域的变量声明中,它是为了请求把变量存储在寄存器中以最快速度访问,同时不可获取该变量的地址,如果请求的结果失败,那么就作为自动变量。
    • static 表明白创建的对象具有静态存储期,载入程序时创建对象,程序结束时对象消失。如果作用于文件作用域声明,那么该对象为内部链接,作用域受限于该文件(翻译单元),如果作用域块作用域声明,那么该变量为块作用域的静态变量,作用域受限于该块,也是载入程序时创建对象,且在程序结束时才销毁,不会重复创建。
    • extern 表明声明的变量或函数定义在别处(引用式声明,指示编译器去别处查询其定义),如果 extern 声明的变量具有文件作用域,则引用的变量必须具有外部链接,如果 extern 声明的变量具有块作用域,则引用的变量可能具有外部或内部链接,这取决于该变量的定义式声明,通常块作用域中的 extern 声明不是必须的,但如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。。
  • 自动变量具有块作用域、无链接、自动存储期。它们是局部变量,属于其定义所在块(通常指函数)私有。寄存器变量的属性和自动变量相同,但是编译器会使用更快的内存或寄存器储存它们。不能获取寄存器变量的地址。

  • 具有静态存储期的变量可以具有外部链接、内部链接或无链接。在同一个文件所有函数的外部声明的变量是外部变量,具有文件作用域、外部链接和静态存储期。如果在这种声明前面加上关键字static,那么其声明的变量具有文件作用域、内部链接和静态存储期。如果在函数中用 static 声明一个变量,则该变量具有块作用域、无链接、静态存储期。

  • 具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,在退出该块时释放之前分配的内存。如果未初始化,自动变量中是垃圾值。程序在编译时为具有静态存储期的变量分配内存,并在程序的运行过程中一直保留这块内存。如果未初始化,这样的变量会被设置为0。

  • 具有块作用域的变量是局部的,属于包含该声明的块私有。具有文件作用域的变量对文件(或翻译单元)中位于其声明后面的所有函数可见。具有外部链接的文件作用域变量,可用于该程序的其他翻译单元。具有内部链接的文件作用域变量,只能用于其声明所在的文件内。