C_Primer_Plus12.storage_linkage_memorymanage

存储类别、链接和内存管理

  • 要点

auto, extern, static, register, const, volatile, restricted, _Thred_local, _Atomic
rand(), srand(), time(), malloc(), calloc(), free()
如何确定变量的作用于(可见范围)和生命周期(存在时间长短)
设计更复杂的程序

存储类别

对象:内存中用来存储数据的一块区域(与面向对象编程的对象不同)

左值:可以放在赋值操作符左边的变量,即具有对应的内存单元,且可被用户访问或修改。
当一个符号或常量放在操作符的右面,计算机会读取它的右值,即其真实值。
简单地说,左值相当于地址值,右值相当于真实数值。
a=b 的意思是把 b 的右值(真实值)取出来放在 a 代表的存储区域(地址)中。
左右值的详细介绍:https://www.cnblogs.com/starrys/p/13157586.html

作用域

  • 块作用域(block scope)
    • 一对花括号的内部区域(函数的参数列表也计在内)
    • 局部变量、函数、for,if 等的小括号内,都具有块作用域
    • 以前,块作用域的变量必须声明在块的开头,C99 放宽了该限制
  • 函数作用域
    • 函数内部,属于块作用域
  • 函数原型作用域
    • 从形参定义处,到原型声明结束
  • 文件作用域 (file scope)
    • 定义在函数外面,从定义处,到文件末尾均可见
    • 这样的变量可用于多个函数,所以文件作用域变量也可称为全局变量

链接

  • 外部链接
    • 多文件使用
    • 通常称为全局作用域或程序作用域
  • 内部链接
    • 只能在所在翻译单元内使用
    • 通常称为文件作用域
  • 无链接
    • 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量
static:
    int giants = 5;          // 文件作用域,外部链接
    static int dodgers = 3;  // 文件作用域,内部链接
    int main(){
        ...
    }

该文件和同一程序的其他文件都可以使用变量 giants,而变量 dodgers 属于文件私有。

存储期

作用域和链接描述了标识符的可见性,而存储期描述了通过这些标识符访问的对象的生存期

  • 静态存储期
    • 程序执行期间一直存在
    • 文件作用域变量具有静态存储期
    • 对于文件作用域变量,static 关键字表明了其链接属性,而非存储期。以 static 声明的文件作用域变量具有内部链接
    • 不论内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。
  • 线程存储期
    • 用于并发程序设计,具有线程存储期,从被声明时到线程结束一直存在
    • 以关键字 _Thread_local 声明一个对象时,每个线程都获得该变量的私有备份
  • 自动存储期
    • 块作用域变量
    • 当程序进入块时,为这些变量分配内存;当退出这个块时,释放内存
    • 变长数组稍有不同,它的存储期是从声明处到块末尾,而不是从块的开始到块的末尾
  • 动态分配存储期

例:

void more(int number){
    int index;
    static int ct = 0;
    ...
    return 0;
}

变量 ct 存储在静态内存中,它从程序被载入到程序结束都存在。但是它的作用域定义在 more() 函数块中,只有在执行该函数时,程序才能使用 ct 访问它所指定的对象

C 使用作用域、链接和存储期为变量定义了多种存储方案,即五种存储类别:

  • 自动
  • 寄存器
  • 静态块作用域
  • 静态外部链接
  • 静态内部链接
存储类别 存储期 作用域 链接 声明方式
自动 自动 块内
寄存器 自动 块内,使用关键字 register
静态外部链接 静态 文件 外部 所有函数外
静态内部链接 静态 文件 内部 所有函数外,使用关键字 static
静态无链接 静态 块内,使用关键字 static

自动变量

声明在块或函数头中的任何变量都属于自动存储类别,用 auto 关键字修饰,可省略,默认为 auto

int main(void){
    auto int n;
}

auto 是存储类别说明符(注意,C 的 auto 和 C++ 中的 auto 用法完全不同,如果编写 C/C++ 兼容程序,最好不要使用 auto 作为存储类别说明符)

寄存器变量

存储在CPU 的寄存器中的变量,所以速度最快;由于存储在寄存器中,所以无法获取寄存器变量的地址。
大多数情况,寄存器变量和自动变量一样,他们都是块作用域、无链接和自动存储期。

声明变量为 register 类别与直接命令相比更像一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿。此时,寄存器变量就变成普通自动变量。而且即使如此,仍然不能对该变量使用地址运算符。

寄存器变量和寄存器形参:

void macho(register int n);

int main(void){
    register int quick;
}

可声明为 register 的数据类型有限,比如处理器中的寄存器可能没有足够大的空间来存储 double 类型的值。

块作用域的静态变量

静态的意思是该变量在内存中原地不动,而不是说它的值不变。
具有文件作用域的变量自动具有(必须具有)静态存储期。
也可以创建具有静态存储期、块作用域的局部变量。这些变量与自动变量具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。
不能在函数的形参中使用 static。

函数中的静态变量在程序退出函数后不会消失,仍然保持函数结束时的值,当再次进入函数时,静态变量的值不变,而且也不会再次执行初始化语句,其值为上次调用函数后的值。

#include <stdio.h>
void trystat(void);

int main(void){
    int count;

    for(count = 1; count <= 3; ++count){
        printf("iteration %d:\n", count);
        trystat();
    }

    return 0;
}

void trystat(void){
    int fade = 1;
    static int stay = 1;

    printf("fade: %d; stay: %d\n", fade++, stay++);
}

output:

iteration 1:
fade: 1; stay: 1
iteration 2:
fade: 1; stay: 2
iteration 3:
fade: 1; stay: 3

静态变量有多种叫法:局部静态变量,内部静态存储类别(内部指的是函数内部)

外部链接的静态变量

外部存储类别(external storage class)
静态存储期,文件作用域,外部链接
把变量的定义性声明放在所有函数的外面边创建了外部变量。为了指出该函数使用了外部变量,可以在函数中用关键字 extern 再次声明。如果一个文件使用了另一个文件中的变量,则必须用 ertern 声明该变量

int Errupt;        // 外部变量
double Up[100];    // 外部数组变量
extern char Coal;  // Coal 被定义在另一个文件中

int main(void){
    extern int Errupt;   // 可选的声明
    extern double Up[];  // 可选的声明,且不用指明数组大小
}

main 方法中若声明变量是省略 extern 关键字,则编译器会在 main 中创建一个名为 Errupt 的自动变量,局部变量。如果不得以要使用与外部变量同名的局部变量,可以在声明局部变量时使用 auto 关键字。

初始化外部变量

int x = 10;              // 没问题
int y = 3 + 20;          // 没问题
size_t z = sizeof(int);  // 没问题
int x2 = 2 * x;          // 不行,x 是变量

外部名称

C99 和 C11 标准都要求编译器识别局部标识符的前 63 个字符和外部标识符的前 31 个字符。(在以前的标准分别是31个字符和6个字符)

定义和声明

extern 关键字告诉编译器该变量定义在程序的别处,应去别处查询该变量,该声明不会引起分配存储空间,所以不可以在使用 extern 的情况下对该变量赋值。因此不要用用关键字 extern 创建外部定义,只能用它来引用现有的外部定义,这通常被称为 引用式声明,区别于 定义式声明

// file1.c
char permis = 'N';

// file2.c
extern char permis = 'Y';  // 错误

内部链接的静态变量

static variable with internal linkage
静态存储期,文件作用域,内部链接
用 static 定义的变量
除了用 static 修饰和只能用在文件内部,其他特性与外部链接静态变量相同

多文件

只有当程序由多个翻译单元组成时,才体现出区别内部链接和外部链接的重要性。
复杂程序一般由多个单独的源代码文件组成,有时,这些文件要共享一个外部变量。C 通过在一个文件中进行定义式声明,然后在其他文件中使用引用式声明来实现共享。

想要使用其他文件中定义的外部链接,则必须事先使用 extern 关键字进行引用式声明。

存储类别说明符

C 语言有6个关键字作为存储类别说明符:auto,register, static, extern, _Thread_local, typedef (把typedef 归于此类是语法上的原因)。
大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为 typedef 的一部分。唯一例外是 _Thread_local, 它可以和 static 或 extern 一起使用。
关键字 static 和 extern 的意义取决于上下文。

auto是默认的块作用域存储期说明符,使用 auto 主要是为了明确表达要使用与外部变量同名的局部变量的意图。
register 也是块作用域变量说明符,请求最快速度访问变量,同时保护了该变量的地址不被获取。
用 static 说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果 static 用于文件作用域,作用域受限于该文件。如果 static 用于块作用域声明,作用域受限于块,因此只有程序在运行该块时,才能通过标识符访问该变量。
extern 说明符表明声明的变量定义在别处。如果 extern 声明在文件作用域中,则引用的变量必须具有外部链接。如果包含 extern 的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,取决于该变量的定义式声明。

小结 - 存储类别

  • 自动变量具有块作用域、无连接、自动存储期。是局部变量,属于所在块私有。寄存器变量属性和自动变量相同,但是访问速度更快,且不能获取其地址
  • 具有静态存储期的变量可以具有外部链接、内部链接或无链接。在同一个文件的所有函数之外声明的变量是外部变量,具有文件作用域、外部链接和静态存储期。如果声明时使用 static 关键字,则具有文件作用域、内部链接和静态存储期。
    • 如果在函数中使用 static 声明,则该变量具有块作用域、无连接和静态存储期
  • 具有自动存储期的变量,程序在进入该变量的声明所在块时才为其分配内存,退出块时释放。如果未初始化,自动变量中是垃圾值。
    • 程序在编译时为具有静态存储期的变量分配内存,并在程序的整个运行期间一直保留这块内存。如果未初始化,这样的变量被设置为0
  • 块作用域的变量是局部的,属于块私有。具有文件作用域的变量对文件中位于其声明后的所有函数可见。
    • 具有外部链接的文件作用域变量,可用于该程序的其他翻译单元。
    • 具有内部链接的文件作用域便来那个,只能用于其声明所在的文件内。

存储类别和函数

函数也有存储类别,可以是外部函数(默认)或静态函数。C99 新增了第三种类别:内联函数

外部函数可以被其他文件的函数访问,静态函数只能用于所在文件。

与变量类似,其他文件使用外部函数时,需要使用 extern 关键字:

// file1.c
double gamma(double);

// file2.c
extern double gamma(double);

存储类别的选择

尽量使用自动存储类别,和 const 数据。
随意使用外部存储类别的变量导致的后果远远超过它所带来的便利。唯一例外的是 const 数据,因为它在初始化后不会被修改,所以不用担心它们被意外篡改。

保护性程序设计的黄金法则:“按需知道”原则。尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。

随机数函数和静态变量

ANSI C 标准允许 C 实现针对特定机器使用最佳算法,然而它还提供了一个可移植的标准算法,在不同系统中生成相同的随机数。实际上,rand() 是一个“伪随机数生成器”,即可以预测生成数字的实际序列。但是数字在其取值范围内均匀分布。

rand() 函数有两个实现,一个是 ANSI C 标准提供的可移植版本,另一个是编译器实现的不可移植版本。以 ANSI C 版本为例。

可移植版本方案开始于一个“种子”数字。函数使用该种子生成新的数,这个新数又称为新的种子。以此类推。随机数函数必须记录它上一次被调用时所使用的种子,因此需要一个静态变量。

// 02rand.c
static unsigned long int next = 1;

unsigned int rand0(void){
    // 生成伪随机数的魔术公式
    next = next * 1103515245 + 12345;
    return (unsigned int) (next / 65535) % 32768;
}

void srand0(unsigned int seed){
    next = seed;
}

使用该函数:

// 02randuse.c
#include <stdio.h>
#include <stdlib.h>
extern void srand0(unsigned int);
extern int rand0(void);

int main(void){
    int count;
    unsigned int seed;

    printf("Enter a seed:\n");
    while(scanf("%u", &seed) == 1){
        srand0(seed);
        for(count = 0; count < 5; ++count){
            printf("%d\n", rand0());
        }
    }
    printf("Done\n");

    return 0;
}

编译:

gcc -o 02randuse 02rand.c 02randuse.c

输出:

Enter a seed:
1
16838
313
5086
28442
21895
Enter another seed:
1
16838
313
5086
28442
21895
Enter another seed:
3
17747
17197
11261
6438
31469
Enter another seed:
q

实际使用时可以利用系统时钟作为种子:

srand((unsigned int) time(0));

一般情况,time() 接受的参数是一个 time_t 类型对象的地址,而时间值就存储在传入的地址上。当然也可以传入空指针 (0) 作为参数,这种情况下,只能通过返回值机制来提供值。

掷骰子

最常见的骰子是六面体,其实一共有5中骰子:4面、6面、8面、12面和20面。他们都是正多面体,所有面都具有相同的形状和大小。而计算机不用考虑集合限制,可以设计任意面数的电子骰子。

// 03diceroll.c
#include "diceroll.h"
#include <stdio.h>
#include <stdlib.h>   // rand() function

int roll_count = 0;

static int rollem(int sides){
    int roll;

    roll = rand() % sides + 1;
    ++roll_count;

    return roll;
}

int roll_n_dice(int dice, int sides){
    int d;
    int total = 0;

    if(sides < 2){
        printf("Need at least 2 sides.\n");
        return -2;
    }
    if(dice < 1){
        printf("Need at least 1 die.\n");
        return -1;
    }

    for(d = 0; d < dice; ++d){
        total += rollem(sides);
    }

    return total;
}

// diceroll.h
extern int roll_count;

int roll_n_dice(int dice, int sides);

// 03roll.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "diceroll.h"

int main(void){
    int dice, roll;
    int sides;
    int status;

    srand((unsigned int) time(0));
    printf("Enter sides per die, 0 to stop:\n");
    while(scanf("%d", &sides) == 1 && sides > 0){
        printf("How many dice?\n");
        if((status = scanf("%d", &dice)) != 1){
            if(status == EOF) break;
            else{
                printf("Please enter an integer:\n");
                while(getchar() != '\n') continue;
                printf("How many sides? Enter 0 to stop.\n");
                continue;
            }
        }
        roll = roll_n_dice(dice, sides);
        printf("You have rolled a %d using %d %d-sided dice.\n", roll, dice, sides);
        printf("How many sides? Enter 0 to stop.\n");
    }
    printf("The rollem() function was called %d times.\nBye!\n", roll_count);

    return 0;
}

编译:

gcc -o 03roll 03diceroll.c 03roll.c

输出:

Enter sides per die, 0 to stop:
6
How many dice?
3
You have rolled a 17 using 3 6-sided dice.
How many sides? Enter 0 to stop.
2
How many dice?
2
You have rolled a 3 using 2 2-sided dice.
How many sides? Enter 0 to stop.
2
How many dice?
1
You have rolled a 1 using 1 2-sided dice.
How many sides? Enter 0 to stop.
0
The rollem() function was called 6 times.
Bye!

分配内存: malloc() 和 free()

所有程序都必须预留足够的内存来存储程序使用的数据。一般情况下,内存是自动分配的,如果想要在运行时动态分配内存,可以用 malloc() 函数实现。它接受一个参数,内存字节数。

malloc() 函数会找到合适的空闲内存块,这样的内存是匿名的。即它会分配内存,但不会为其赋名,它会返回动态分配内存块的首字节地址。因此,可以把改地址赋给一个指针变量,并用指针访问这块内存。

因为 char 表示1个字节,malloc() 最初设计时的返回类型通常被定义为 char 类型。而 ANSI C 标准开始,C使用一个新的类型:指向 void 的指针。该类型相当于一个”通用指针”。

malloc() 函数可用于返回指向数组的指针、指向结构的指针。所以通常该函数的返回值会被强制转换为匹配的类型。应坚持使用强制类型转换,提高代码可读性。

通常,malloc() 要与 free() 配套使用。free() 函数的参数是 malloc() 返回的地址,该函数会释放之前 malloc() 分配的内存。因此,动态分配内存的存储期从调用 malloc() 到调用 free() 为止。这两个函数的原型都在 stdlib.h 头文件中。

如果分配内存失败,则返回空指针,此时可以利用 exit() 函数结束程序,并返回错误代码。stdlib.h 中,定义了两个值,用来表示程序的结束状态,EXIT_SUCCESS (0) 表示程序结束,EXIT_FAILURE 表示程序异常中止。

#include <stdlib.h>

...

// 创建一个30个长度的 double 数组
double*ptd;
ptd = (double*) malloc(30*sizeof(double));
if(ptd == NULL){
    printf("mallocate failure.\n");
    exit(EXIT_FAILURE);
}
free(ptd);

有些操作系统会自动释放动态内存的内存,有些不会,所以不要依赖操作系统来清理,而使用 free(). 忘记使用 free() 可能会造成内存泄漏。

calloc() 函数

功能与 malloc() 类似,只是 calloc() 接受两个无符号整数参数,第一个是单元数量,第二个是每个单元的字节数。释放内存时同样使用 free() 函数。

long * long_arr;
long_arr = (long*) calloc(100, sizeof(long));
free(long_arr);

动态内存分配和变长数组

变长数组是自动存储类型,具有块作用域,在退出块时,内存自动释放,不必使用 free()。

而 malloc() 创建的数组不必局限在一个函数内访问。比如可以用一个函数创建数组并返回指针,供主调函数使用,然后主调函数在函数末尾调用 free() 释放内存。malloc() 创建的指针和 free() 使用的指针不必相同,但是两个指针必须指向相同的内存地址。而且不能释放同一块内存两次。

对多维数组而言,使用变长数组更方便。当然也可以使用 malloc() 创建二维数组,但是语法比较繁琐。

如果编译器不支持变长数组特性,就只能固定二维数组的维度。

存储类别和动态内存分配

我们可以把程序用一个最简单的模型来分析,认为程序把内存分为3部分,一部分具有外部链接、内部链接和无连接的静态变量使用;一部分供自动变量使用;一部分功动态内存分配。

静态内存类别所用的内存数量在编译时确定,只要程序还在运行,就可以访问该部分的数据。该类别的变量在程序开始时被创建,程序结束时销毁。

自动存储类别的变量在程序进入块时创建,离开块时销毁。所以随着程序调用函数和函数结束,自动变量所用的内存数量也相应的增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后按相反的顺序销毁。

动态分配的内存在调用 malloc() 或 calloc() 时创建,调用 free() 后销毁。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正因为这样,这部分的内存用于动态内存分配会支离破碎。即,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈慢。

ANSI C 类型限定符

C90 增加了两个新属性:恒常性 (constancy) 和易变性 (volatility). 他们分别可以用关键字 const 和 volatile 来声明,所以这两个关键字创建的类型是限定类型 (qualified type).
C99 新增了第三个限定符 restrict, 用于提高编译器优化。
C11 增加了第四个限定符 _Atomic (可选支持项),由 stdatomic.h 管理,用于支持并发程序设计。

C99 为类型限定符增加了一个新属性:他们是幂等的 (idempotent),即可以在一条生命中多次使用同一个限定符,多余的限定符将被忽略:

const const const int n = 6;
// 等价于:
const int n = 6;

可以编写以下代码:

typedef const int zip;
const zip q = 0;

const

表示变量不可改变,但用在指针上,会稍复杂:

// 指针指向的值不可变,而指针可变:
const float * ptf;
// 等价于:
float const * ptf;

// 指针不可变,而指向的值可变:
float * const ptf;

// 指针不可变,它指向的值也不能变:
const float * const ptf;

const 的位置与 * 有关,放在左侧则限定了指针指向的数据不能改变,放在右侧则限定指针不能改变。

const 的常见用法是用于函数形参的指针,表示函数不能改变数组或指针指向的值。如果一个指针仅用于给函数访问值,应将其声明为一个指向 const 限定类型的指针。

对于全局变量(文件链接),尽量使用 const. 头文件中定义的变量必须为内部链接静态变量(有的编译器不是必须),然后其他文件使用引用式声明使用这些变量。如果不使用内部链接,而使用外部链接,则会导致每个包含了该头文件的文件都有一个相同标识符的定义式声明。这种方案相当于为每个文件提供了一个单独的数据副本。由于每个副本只对本文件可见,所以无法和其他文件通信。因为他们都是 const 类型,所以不算什么问题,然而对于数据庞大的数据就不能视而不见了。

volatile 类型限定符

volatile 告诉计算机,代理(其他程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。比如,一个地址上可能存储着当前的时钟时间,不论程序做什么,地址上的值都随时间变化而改变。或者一个地址用于接受另一台计算机传入的信息。

volatile int locl;    // 表示 locl 是个易变的位置
volatile int * ploc;  // ploc 是一个指向易变位置的指针

为什么要设计 volatile

比如以下代码:

val1 = x;
// 其他代码 ...
val2 = x;

编译器在编译时,会对这段代码进行优化。因为 x 的值使用了两次,于是编译器把 x 的值临时存储在寄存器中,然后在 val2 使用 x 时,直接从寄存器中读取 x 的值,而不是从内存读取,这个过程被称为 高速缓存(caching)。如果这个过程中内存中 x 值发生了变化,就不能这样优化了。volatile 关键字就是要告诉编译器该变量是一个易变值,不要对变量进行这样的优化。

restrict

restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。

举例:

int ar[10];
int * restrict restar = (int*) malloc(10 * sizeof(int));
int * par = ar;

for(int n = 0; n < 10; ++n){
    par[n] += 5;
    restar[n] += 5;
    ar[n] *= 2;    //*
    par[n] += 3;
    restrict += 5;
}

其中 ar 有两种方式访问,而 restar 只有通过 restar 访问。
编译器在优化该段代码时,可能对相同变量的运算进行优化,假设优化后的 for 循环代码:

for(int n = 0; n < 10; ++n){
    par[n] += 8;
    restar[n] += 8;
    ar[n] *= 2;   //*
}

对 restar 的优化没有问题,而 par 的优化则是错误的,因为 par 指向的内存可以通过 ar 访问,而 par 的两次操作之间,ar 又做了操作,所以优化后的代码是错误的。当然这里只是为了说明问题,编译器不会按照这种方式优化代码。

restrict 关键字就是要告诉编译器可以按这种方式优化代码,因为 restrict 的指针指向的内存只能通过唯一的变量访问,没有其他访问方式。

restrict 限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,编译器可以尝试对其优化。例如 C 库有两个函数用于把一个位置上的字节拷贝到另一个位置,C99 中,这两个函数原型是:

void * memcpy(void * restrict s1, const void * restrict s2, size_t n);
void * memmove(void * s1, const void * s2, size_t n);

这两个函数都是从位置 s2 把n个字节拷贝到 s1 位置。memcpy() 函数要求这两个位置不会重叠,但是 memmove() 没有这样的要求。这使得使用 memmove() 函数时不得不更小心,防止在使用数据之前先覆盖了数据。

注意,编译器不会检查用户是否遵循内存访问的唯一性,所以 restrict 关键字有两个读者,一个是编译器,告诉编译器可以自由使用一些优化方案,另一个用户是程序员,并假定程序员严格按照访问的唯一性进行编写代码。

_Atomic (C11)

当一个线程访问一个原子类型的对象执行原子操作时,其他线程不能访问该对象。
要功过各种宏函数访问原子类型。C11 通过包含可选头文件 stdatomic.h 和 threads.h,提供了一些可选的管理方法。

// 原子类型变量
_Atomic int hogs;
// 给 hogs 变量赋值为12, atomic_store 是 stdatomic.h 中的宏
atomic_store(&hogs, 12);

旧关键字的新位置

C99 允许把类型限定符和存储类别说明符 static 放在函数原型和函数头的形参的初始方括号中。

void ofmouth(int * const a1, int * restrict a2, int n);
// 等价于: (方括号表示该变量为数组,本质上还是指针)
void ofmouth(int a1[const], int a2[restrict], int n);
// 对于 static:
double stick(double ar[static 20]);

static 的用法表明,函数调用时的实参是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。

小结

  • 存储期
    • 静态
    • 自动
    • 动态分配
  • 作用域
    • 块作用域
    • 文件作用域
  • 链接
    • 外部链接
    • 内部链接
    • 无链接

不同的存储期、作用域和链接组成了5种存储类别(不包括线程的概念):

  • 自动
    • 块作用域,默认存储类别或使用 auto 关键字
    • 自动存储期,块作用域,无连接
  • 寄存器
    • 块作用域,register 存储类别说明符
    • 自动存储期,块作用域,无连接
    • 无法获取地址
  • 静态、无连接
    • 块作用域,static
    • 静态存储期,块作用域,无连接
  • 静态、外部链接
    • 静态存储期,文件作用域,外部链接
    • 只能在编译器初始化一次,未显式初始化则其字节都被设为0
  • 静态、内部链接
    • 静态存储期,文件作用域,内部链接
    • 只能在编译器初始化一次,未显式初始化则其字节都被设为0

动态分配的内存由 malloc() 函数分配,它返回一个指针,若分配失败则返回空指针。动态分配的内存需要程序员手动释放 free().

类型限定符:

  • const
    • 不可变
  • volatile
    • 易变,告诉编译器其他程序可能会改变变量的值,不要进行高速缓存方式的优化
  • restrict
    • 唯一访问方式(必须为指针),告诉编译器可以进行语句合并方式的优化
posted @ 2020-07-28 22:33  keep-minding  阅读(141)  评论(0)    收藏  举报