11. 数据的存储类别

一、什么是存储类别

  C++ 使用四种不同的方案存储数据,这些方案的区别在于数据保留在内存中的时间。

  • 自动存储持续性:在函数定义中声明的变量(包括函数参数)的存储持续性为自动的。它们在程序开始执行其所属的函数或代码块时被创建,在执行完成函数或代码块时,它们使用的内存被释放。C++ 有两种存储持续性为自动的变量。
  • 静态存储持续性:在函数定义定义的变量和使用关键字 static 定义的变量的存储持续性都为静态。它们在程序整个运行过程中都存在。C++ 有 3 种存储持续性为静态的变量。
  • 线程存储持续性:当前,多核处理器很常见,这些 CPU 可同时处理多个执行任务。这让程序能够将计算放在可行并行处理的不同线程中。如果变量是使用关键字 thread_local 声明的,则其声明周期与所属的线程一样长。
  • 动态存储持续性:用 new 运算符分配的内存将一直存在,直到使用 delete 运算符将其释放或程序结束。这种内存的存储持续性为动态,有时被称为自由存储或堆。

  我们可以用 存储期(storage duration)描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,我们可以用 作用域(scope)和 链接(linkage)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。

  不同的存储类别具有不同的存储期、作用域 和 链接。标识符可以在源代码的多文件中共享,也可以用于特定文件的任意函数中,还可以在仅限于特定函数中使用,甚至只在函数中某部分使用也可以。对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。我们可以通过函数调用的方式显示分配和释放内存。

1.1、作用域

  作用域(scope)描述了名称在文件(翻译单元)的多大范围内可见。作用域为局部的变量只有在它的代码块中可见。代码块是由花括号括起来的一系列语句。作用域为全局(也叫文件作用域)的变量在定义位置到文件结尾之间都可用。自动变量的作用域为局部、静态变量的作用域为全局还是局部取决于它是如何定义的。在函数原型作用域中使用的名称只在包含参数列表的括号内可用。在类中声明的成员的作用域为整个类。在名称空间中声明的变量的作用域为整个名称空间。

  C++ 函数的作用域可以是整个类或整个名称空间(包含全局的),但不能是局部的(因为不能在代码块内定义函数,如果函数的作用域为局部,则只对自己可见,因此不能被其它函数调用。这样的函数将无法运行)。

  函数作用域(function scope)仅用于 goto 语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。

void function(void)
{
    int i = 0;

    FLAG:
    for(i = 0; i < 10; i++)
    {
        if(i == 5)
        {
            goto FLAG;
        }
    }
}

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

int prototype(int, int);

int prototype(int a, int b)
{
    return a + b;
}

  变量的定义在函数的外面,具有 文件作用域(file scope)具有文件作用于的变量,从它的定义处到该定义所在文件的末尾均可见。文件作用域变量也称为 全局变量(global varibale)。

int units = 0;      // 该变量具有文件作用域

void critic(void)
{
    ...
}

1.2、链接

  C++ 变量具有 3 种链接属性:外部链接内部链接无链接。具有 块作用域函数作用域函数原型作用域 的变量都是 无链接变量。这意味着这些变量属于定义它们的 块、函数 或 原型 私有。具有 文件作用域 的变量可以是 外部链接内部链接外部链接变量 可以在多文件中使用,内部链接变量只能在一个翻译单元(即一个源代码文件和它包含的头文件)中使用。我们可以通过查看外部定义中是否使用了存储类别说明符 static 来判断 文件作用域 的变量是 内部链接 还是 外部链接。

int aiants = 5;             // 文件作用域,外部链接
static int dodgers = 5;     // 文件作用域,内部链接

int main(void)
{
    ...
    return 0;
}

1.3、存储期

  作用域链接 描述了 标识符的可见性存储期 描述了通过这些标识符访问的 对象的生存期。C++ 有 4 种存储期:静态存储期线程存储期自动存储期动态分配存储期

  如果对象具有 静态存储期,那么它在程序的执行期间一直存在文件作用域变量 具有 静态存储期。对于 文件作用域变量,关键字 static 表示了其 链接属性,而非存储期。以 static 声明的 文件作用域变量 具有 内部链接。但是无论是内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。

  线程存储期 用于并发程序设计,程序执行可被分为多个线程。具有 线程存储期 的对象,从被声明到线程结束一直存在以关键字 _Thread_local 声明一个对象时,每个线程都获得该变量的私有备份

  块作用域 的变量通常具有 自动存储期当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。例如,一个函数调用结束后,其变量占用的内存可用于储存下一个被调用函数的变量。

void bore(void)
{
    int index = 0;
    index++;
    return index;
}

  然而,块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字 static。

void more(void)
{
    static int count= 0;
    count++;
    return count;
}

  这样,变量 count 储存在静态内存中,它从程序被载入到程序结束期间都存在。但是,它的作用域定义在more() 函数块中。只有在执行该函数时,程序才能使用 count 访问它指定的对象(但是,该函数可以给其它函数提供该存储区的地址以便间接访问该对下个,例如通过指针形参或返回值)。

二、五种存储类别

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

2.1、自动变量

  属于 自动存储类别 的变量具有 自动存储期块作用域无链接默认情况下,声明在块或函数头中的任意变量都属于自动给存储类别。为了更清楚地表达你的意图,我们可以显示使用关键字 auto。关键字 auto 是 存储类别说明符(storage-class specifier)。

  块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。

  变量具有自动存储期意味着,程序在进入该变量声明所在的块时变量存在,程序在退出该块时变量消失、原来该变量占用的内存位置现在可做它用。

  如果内层块中声明的变量与外层块中的变量同名,那么内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到原来的作用域。

  自动变量不会初始化,除非显示初始化它。

int main(void)
{
    int repid = 0;	// 自动变量
    return 0;
}

在 C++ 11 中,关键字 auto 用于自动类型推断。

2.2、寄存器变量

  变量通常储存在计算机内存中。如果幸运的话,寄存器变量储存在 CPU 的寄存器中,或者概括地说,储存在最快地可用内存中。与普普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。寄存器变量 也是 块作用域无链接自动存储期。使用存储类别说明符 register 便可声明寄存器变量。

int main(void)
{
    register int quick;		// 寄存器变量
    return 0;
}

  声明变量为 register 类别与直接命令相比,它更像是一种请求。编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求。在这种情况下,寄存器变量就会变成普通的自动变量。即使这样,仍然不能对该变量使用取地址运算符。

  在函数头中使用关键字 register,便可直接请求形参是寄存器变量。

void macho(register int n)

在 C++ 11 中,关键字 register 只是显示地指出变量是自动的。

2.3、块作用域的静态变量

  静态的意思是该变量在内存中原地不动,并不是说它的值不变。具有 文件作用域 的变量自动具有(也必须是)静态存储期。我们可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。计算机在多次函数调用之际会记录它们的值。在块中(提供块作用域和无链接)以存储类别说明符 static(提供静态存储期)声明这种变量。

#include <iostream>

using namespace std;

int getCount(void);

int main(void)
{
    int i = 0;
    int count = 0;

    for(i = 0; i < 5; i++)
    {
        count = getCount();
        cout << count << endl;
    }

    return 0;
}

int getCount(void)
{
    static int count = 0;	// 块作用域的外部变量
    return ++count;
}

不能在函数的形参中使用 static 关键字;

2.4、外部链接的静态变量

  外部链接的静态变量 具有 文件作用域外部链接静态存储期。该类别有时称为 外部存储类别(enternal strorage class),属于该类别的变量称为 外部变量(external variable)。把变量的定义声明(defining declaration)放在所以后函数的外面便创建了外部变量。为了指出该函数使用了外部变量,我们可以在函数中使用关键字 extern 再次声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用 extern 在该文件中声明该变量。

int errupt;                 // 外部定义的变量
double up[100];             // 外部定义的数组
extern char coal;           // 如果coal被定义在另一个文件中

void next(void);

int main(void)
{
    extern int errupt;      // 可选的声明
    extern double up[];     // 可选的声明

    return 0;
}

void next(void)
{
    ...
}

  注意,在main() 中声明 up[] 数组(这是可选的声明)时不用指明数组大小。这是因为第 1 次声明已经提供了数组大小信息。main() 中的两条 extern 声明完全可以忽略,因为外部变量具有文件作用域,所以 errupt 和 up[100] 从声明处到文件结尾都可见。它们出现在那里,仅为了说明 main() 函数要使用着两个变量。

  如果省略函数中 extern 关键字,相当于创建了一个自动变量。例如:

int errupt;

  这使得编译器在 main() 创建了一个名为 errupt 的自动变量,它时一个独立的局部变量,与原来的外部变量 errupt 不同。该局部变量仅 main() 中可见,但是外部变量 errupt 对于该文件中的其它函数也可见。简而言之,在执行块中的语句时,块作用域中的变量将“隐藏”文件作用域中的同名变量。如果不得已要使用与外部变量同名的局部变量,可以在局部变量的声明中使用 auto 存储类别说明符明确表达这种意图。

  外部变量和自动类似,也可以被显示初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动初始化为 0。这一原则也适用于外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量。

关键字 extern 表示该声明不是定义,因为它指示编译器去别处查询其定义;

外部变量只能初始化一次,且必须在定义该变量时进行;

2.5、内部链接的静态变量

  该存储类别的变量具有 静态存储期、文件作用域 和 内部链接。在所有函数外部用存储类别说明符 static 定义的变量具有这种存储类别。

static int a = 1;	// 静态变量,内部链接

int main(void)
{
    ...
    return 0;
}

  普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。我们可以使用存储类别说明符 extern ,在函数中重复声明任意具有文件作用域的变量。这样的声明并不会改变其链接属性。

int traveler = 1;	            // 外部链接
static int stayhome = 1;        // 内部链接

int main(void)
{
    extern int traveler;        // 使用定义在别处的traveler
    extern int stayhome;        // 使用定于在别处的stayhome

    return 0;
}

  对于该程序所在的翻译单元,traveler 和 stayhome 都具有文件作用域,但是只有 traveler 可用于其它翻译单元(因为它具有外部链接)。这两个声明都使用了 extern 关键字,指明了 main() 中使用的这两个变量的定义都在别处,但是这并未改变 stayhome 的内部链接属性。

三、存储类别说明符

  C++ 有 6 个关键字作为存储类别说明符:auto、register、static、extern、thread_local和 mutable。

  auto 说明符表明变量是自动存储期,只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存储期,所以使用 auto 主要是为了明确表达要使用与外部变量同名的局部变量的意图。在 C++ 11 中,auto 用于自动类型推断。

  register 说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。

  用 static 说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果 static 用于文件作用域声明,作用域受限于该文件。如果 static 用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。

  extern 说明符表明声明的变量定义在别处。如果包含 extern 的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含 extern 的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。

四、存储类别与函数

  函数也有存储类别,可以是 外部函数(默认)、静态函数内联函数外部函数 可以被其它文件的函数访问,但是 静态函数 只能用于其定义所在的文件。以 sattic 存储类别说明符创建的函数属于特定模块私有;用 extern 关键字声明定义在其它文件中的函数。这样做是为了表明当前文件中使用的函数定义在别处。除非使用 static 关键字,否则一般函数声明都默认为 extern。内联函数 的编译代码与其它程序代码 “内联” 起来了。也就是说,编译器将使用相应的函数代码替换函数调用。对于内联代码,程序无需跳到另一个位置处开始执行代码,再跳回来。

double gamma(double);               // 该函数默认为外部函数
static double beta(int, int);       // 静态函数,特定模块私有
extern double delta(double, int);   // 外部函数,该函数被定义其它处
inline double square(double x);     // 内联函数

五、类型限定符

  C++ 增加了两个属性:恒常性(constancy)和 易变性(colatility)。这两个属性可以分别用关键字 constvolatitle 来声明,以这两个关键字创建的变量是 限定类型(qualified type)。

5.1、const类型限定符

【1】、const 修饰基本数据类型变量

  以 const 关键字声明的对象,其值不能通过赋值或递增、递减来修改。但是,我们可以初始化 const 变量。

const int nochange = 3;

  该声明让 nochange 称为只读变量。初始化后,就不能再改变它的值。

【2】、const 修饰数组

  我们可以用 const 关键字创建不能修改的数组。

const int array[5] = {1,2,3,4,5};

【3】、const 修饰指针变量

  const 修饰指针变量时,要区分是限定指针本身为 const ,还是限定指针指向的值为 const。

const float * pf;

  该语句表明了 pf 指向的值不能被改变,而 pf 本身的值可以改变。

float * const pf;

  该语句表明了 pf 本身的值不能更改,pf 必须指向同一个地址,但是它所指的值可以改变。

const float * const pf;

  该语句表明了 pf 既不能指向别处,它所指向的值也不能改变。

float const * pf;

  如果把 const 放在类型名之后,* 之前,说明该指针不能用于改变它所指向的值。简而言之,const 方法 * 左侧任意位置,限定了指针指向的数据不能改变;const 放在 * 右侧,限定了指针本身不能改变。

  const 关键字的常见用法是声明为函数形参的指针。c++ 库遵循这种做法。如果一个指针仅用于给函数访问值,应将其什么声明为一个指向 const 限定类型的指针。如果要用指针更改主调函数中的数据,就不使用 const 关键字。

【4】、const 修饰全局变量

  我们可以使用 const 修饰全局变量,限制程序更改它的值。然而,在文件中共享 const 数据要小心,我们可以采取两种策略:一种是:遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其它文件中使用引用式声明(用 extern 关键字)。另一种方案是:把 const 变量放在一个头文件中,然后在其它文件中包含该文件。

  使用第二种方案时,必须在头文件中用关键字 static 声明全局静态 const 变量。如果去掉 static,那么在 file1.c 和 file2.c 中包含该头文件将导致每个文件中都有一个相同标识的定义声明,C标准 不允许这样做。

5.2、volatile类型限定符

  volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以其在其它程序或同时运行的线程中共享数据。

  假如有下面的一段代码:

value1 = x;
// 一些不使用x的代码
value2 = x;

  智能的(进行优化的)编译器会注意到以上代码使用了两次 x,当并未改变它的值。于是编译器把 x 的值临时储存在寄存器中,然后在 value2 需要使用 x 时,才从寄存器中(而不是从原始内存位置上)读取 x 的值,以节约时间。这个过程被称为 高速缓存(caching)。通常,高速缓存时个不错的优化方案,但是如果一些其它代理在以上两条语句之间改变了 x 的值,就不能这样优化了。如果没有 volatitle 关键字,编译器就不知道这种事情是否发生。因此,为安全起见,编译器不会进行告诉缓存。这是 ANSI C 之前的情况。现在,如果声明中没有 volatile 关键字,编译器会假定变量的值在使用过程中不变,然后在尝试优化代码。

可以用 const 和 valatitle 限定一个值;

posted @ 2023-04-18 21:01  星光樱梦  阅读(18)  评论(0编辑  收藏  举报