C语言学习----存储类别

news2024/12/25 12:38:16

存储类别

🌿本文是C Primer Pluse 中文版第12章的部分内容整理
🌱主要是围绕C中作用域 链接 存储期 展开 ,是后面进行多文件管理的基础~

🌈概要
🍎明确对象 变量名 标识符的基本概念和含义
🍐作用域和链接描述了标识符的可见性存储期描述了通过这些标识符访问的对象的生存期 明确作用域 链接 存储期的含义
🍒存储类别特定的用法

文章目录

  • 存储类别
    • 作用域
      • 块作用域
      • 函数作用域
      • 函数原型作用域
      • 文件作用域
      • 翻译单元
    • 链接
    • 存储期
    • 自动变量
    • 寄存器变量
    • 块作用域静态变量
    • 外部链接静态变量
    • 内部链接静态变量
    • 多文件
    • 存储类别说明符
      • auto
      • register
      • static
      • extern
      • 小结:存储类别
    • 存储类别和函数
    • 存储类别的选择
    • 总结

作用域和链接描述了标识符的可见性

存储期描述了通过这些标识符访问的对象的生存期。

编程示例中使用的数据都储存在内存中。

🌿从硬件方面来看,被储存的每个值都占用一定的物理内存,C 语言把这样的一块内存称为对象(object)。对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小(面向对象编程中的对象指的是类对象,其定义包括数据和允许对数据进行的操作,C不是面向对象编程语言)。

标识符: 是一个名称,是可以用于指定特定对象的内容。标识符遵循变量的命名规则。标识符指定硬件内存中的对象方式。

如:int a=3; //a就是一个标识符

变量名不是指定对象的唯一途径。

int *pt=&entity;

pt是一个标识符,它制定了一个存储地址的对象。但是*pt不是标识符,因为它不是一个名称,然而它趋确实指定了一个对象,这个对象和a指定的对象相同。

一般而言,那些指定对象的表达式称为左值所以a和*pt是表达式也是左值,指定了特定内存位置的值。

int rank[10];

ranks的声明创建了一个可容纳10个int类
型元素的对象,该数组的每个元素也是一个对象。

const char * pc = "Behold a string literal!";

程序根据该1️⃣声明把相应的字符串字面量储存在内存中,内含这些字符值的数组就是一个对象。由于数组中的每个字符都能被单独访问,所以每个字符也是一个对象 。2️⃣该声明还创建了一个标识符为pc的对象,储存着字符串的地址。由于可以设置pc重新指向其他字符串,所以标识符pc是一个可修改的左值。 3️⃣ const只能保证被pc指向的字符串内容不被修改,但是无法保证pc不指向别的字符串 。 由于* pc指定了储存’B’字符的数据对象,所以*pc 是一个左值,但不是一个可修改的左值。与此类似,因为字符串字面量本身指定了储存字符串的对象,所以它也是一个左值,但不是可修改的左值

可以用存储期(storage duration)描述对象

存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域(scope)和链接(linkage)描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。

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

作用域

作用域描述程序中可访问标识符的区域

🌿作用域分为:块作用域 函数作用域 函数原型作用域 文件作用域

块作用域

块是用一对花括号括起来的代码区域。 例如,整个函数体是一个块,函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义处到包含该定义的块的末尾。 另外,虽然函数的形式参数声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。所以到目前为止,我们使用的局部变量(包括函数的形式参数)都具有块作用域。

声明在内层块中的变量,其作用域仅局限于该声明所在的块。

函数作用域

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

函数原型作用域

用于函数原型中的形参名

int mighty(int mouse, double large);

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

void use_a_VLA(int n, int m, ar[n][m]);

方括号中必须使用在函数原型中已声明的名称。变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。

文件作用域

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

一般是全局变量。

翻译单元

通常在源代码(.c扩展名)中包含一个或多个头文件(.h 扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。但是,C预处理实际上是用包含的头文件内容替换#include指令。所以,编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。 这个文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。

链接

C 变量有 3 种链接属性:外部链接、内部链接或无链接

具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有

具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。

C 标准用 “内部链接的文件作用域”描述仅限于一个翻译单元(即一个源代码文件和它所包含的头文件)的作用域,用 “外部链接的文件作用域”描述可延伸至其他翻译单元的作用域。但是,对程序员而言这些术语太长了。

一些程序员把 “内部链接的文件作用域”简称为“文件作用域”,把 “外部链接的文件作用域”简称为“全局作用域”或“程序作用域”

🤔如何知道文件作用域变量是内部链接还是外部链接?

❗️ 可以查看外部定义中是否使用了存储类别说明符static

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

该文件和同一程序的其他文件都可以使用变量giants。而变量dodgers属文件私有,该文件中的任意函数都可使用它。

存储期

作用域和链接描述了标识符的可见性

存储期描述了通过这些标识符访问的对象的生存期。

C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。

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

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

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

例如,一个函数调用结束后,其变量占用的内存可用于储存下一个被调用函数的变量。变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。

变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。

我们到目前为止使用的局部变量都是自动类别。例如,变量number和index在每次调用bore()函数时被创建,在离开函数时被销毁。

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

在这里插入图片描述

自动变量

属于 自动存储类别的变量具有自动存储期、块作用域且无链接。 默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字auto

块作用域和无链接意味着 只有在变量定义所在的块中才能通过变量名访问该变量( 当然,参数用于传递变量的值和地址给另一个函数,但是这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。

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

块中声明的变量仅限于该块及其包含的块使用。

🤔如果内层块中声明的变量与外层块中的变量同名会怎样?

内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。

// hiding.c -- 块中的变量
#include <stdio.h>
int main()
{
    int x = 30;       // 原始的 x
    printf("x in outer block: %d at %p\n", x, &x);
    {
        int x = 77;     // 新的 x,隐藏了原始的 x
        printf("x in inner block: %d at %p\n", x, &x);
    }
    printf("x in outer block: %d at %p\n", x, &x);
    while (x++ < 33)    // 原始的 x
    {
        int x = 100;    // 新的 x,隐藏了原始的 x
        x++;
        printf("x in while loop: %d at %p\n", x, &x);
    }
    printf("x in outer block: %d at %p\n", x, &x);
    return 0;
}

🤔想想程序输出为?

程序输出:
x in outer block: 30 at 0x7fff5fbff8c8
x in inner block: 77 at 0x7fff5fbff8c4
x in outer block: 30 at 0x7fff5fbff8c8
x in while loop: 101 at 0x7fff5fbff8c0
x in while loop: 101 at 0x7fff5fbff8c0
x in while loop: 101 at 0x7fff5fbff8c0
x in outer block: 34 at 0x7fff5fbff8c8

也许该程序最难懂的是while循环。while循环的测试条件中使用的是原
始的x:

while(x++ < 33)

在这里插入图片描述

在该循环中,程序创建了第3个x变量,该变量只定义在while循环中。所以,当执行到循环体中的x++时,递增为101的是新的x,然后printf()语句显示了该值。每轮迭代结束,新的x变量就消失。然后循环的测试条件使用并递增原始的x,再次进入循环体,再次创建新的x。 在该例中,这个x被创建和销毁了3次。 注意,该循环必须在测试条件中递增x,因为如果在循环体中递增x,那么递增的是循环体中创建的x,而非测试条件中使用的原始x。我们使用的编译器在创建while循环体中的x时,并未复用内层块中x占用的内存,但是有些编译器会这样做。

第1个for循环头中声明的n,其作用域作用至循环末尾,而且隐藏了原始的n。但是,离开循环后,原始的n又起作用了。第2个for循环头中声明的n作为循环的索引,隐藏了原始的n。然后,在循环体中又声明了一个n,隐藏了索引n。结束一轮迭代后,声明在循环体中的n消失,循环头使用索引n进行测试。当整个循环结束时,原始的 n 又起作用了。再次提醒读者注意,没必要在程序中使用相同的变量名。如果用了,各变量的情况如上所述。

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

int main(void)
{
    int repid;
    int tents = 5;

tents变量被初始化为5,但是repid变量的值是之前占用分配给repid的空间中的任意值(如果有的话),别指望这个值是0。可以用非常量表达式(non-constant expression)初始化自动变量,前提是所用的变量已在前面定义过:

int main(void)
{
    int ruth = 1;
    int rance = 5 * ruth; // 使用之前定义的变量

寄存器变量

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

int main(void)
{
    register int quick;

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

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

void macho(register int n);

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

块作用域静态变量

静态变量(static variable)听起来自相矛盾,像是一个不可变的变量。实际上,静态的意思是该变量在内存中原地不动,并不是说它的值不变。

具有文件作用域的变量自动具有(也必须是)静态存储期。 前面提到过,可以创建具有静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。也就是说,这种变量具有块作用域、无链接,但是具有静态存储期。计算机在多次函数调用之间会记录它们的值。

在块中(提供块作用域和无链接)以存储类别说明符static(提供静态存储期)声明这种变量。

举个🌰

/* loc_stat.c -- 使用局部静态变量 */
#include <stdio.h>
void trystat(void);
int main(void)
{
    int count;
    for (count = 1; count <= 3; count++)
    {
        printf("Here comes iteration %d:\n", count);
        trystat();
    }
    return 0;
}
void trystat(void)
{
    int fade = 1;
    static int stay = 1;
    printf("fade = %d and stay = %d\n", fade++, stay++);
}

🤔猜猜 最后输出什么

注意,trystat()函数先打印再递增变量的值。该程序的输出如下:
Here comes iteration 1:
fade = 1 and stay = 1
Here comes iteration 2:
fade = 1 and stay = 2
Here comes iteration 3:
fade = 1 and stay = 3

静态变量stay保存了它被递增1后的值,但是fade变量每次都是1。这表明了初始化的不同:每次调用trystat()都会初始化facde,但是stay只在编译strstat()时被初始化一次。如果未显式初始化静态变量,它们会被初始化为0。

下面两个声明很相似:

int fade = 1;
static int stay = 1;

第1条声明确实是trystat()函数的一部分,每次调用该函数时都会执行这条声明。这是运行时行为。第2条声明实际上并不是trystat()函数的一部分。如果逐步调试该程序会发现,程序似乎跳过了这条声明。这是因为静态变量和外部变量在程序被载入内存时已执行完毕。把这条声明放在trystat()函数中是为了告诉编译器只有trystat()函数才能看到该变量。这条声明并未在运行时执行。

不能在函数的形参中使用static:

“局部静态变量”是描述具有块作用域的静态变量的另一个术语。阅读一些老的 C文献时会发现,这种存储类别被称为内部静态存储类别(internalstatic storage class)。这里的内部指的是函数内部,而非内部链接。

外部链接静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别
有时称为外部存储类别(external storage 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[];  /* 可选的声明*/
  
}
void next(void)
{
	...
}

注意,在main()中声明Up数组时(这是可选的声明)不用指明数组大小,因为第1次声明已经提供了数组大小信息。main()中的两条 extern 声明完全可以省略,因为外部变量具有文件作用域,所以Errupt和Up从声明处到文件结尾都可见。它们出现在那里,仅为了说明main()函数要使用这两个变量。如果省略掉函数中的extern关键字,相当于创建了一个自动变量。去掉
下面声明中的extern:

extern int Errupt;

便成为:

int Errupt;

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

外部变量具有静态存储期。因此,无论程序执行到main()、next()还是其他函数,数组Up及其值都一直存在。

外部变量的作用域是:从声明处到文件结尾。 除此之外,外部变量具有生命期。外部变量在程序运行中一直存在,因为它们不受限于任何函数,不会在某个函数返回后就消失。

1️⃣初始化外部变量

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

int x = 10;          // 没问题,10是常量
int y = 3 + 20;       // 没问题,用于初始化的是常量表达式
size_t z = sizeof(int);   //没问题,用于初始化的是常量表达式
int x2 = 2 * x;      // 不行,x是变量

(只要不是变长数组,sizeof表达式可被视为常量表达式。)

2️⃣使用外部变量

下面来看一个使用外部变量的示例。假设有两个函数main()和critic(),它们都要访问变量units。可以把units声明在这两个函数的上面.

把units定义在所有函数定义外面(即外部),units便是一个外部变量,对units定义下面的所有函数均可见。因此,critics()可以直接使用units变量。类似地,main()也可直接访问units。但是,main()中确实有如下声明:

extern int units;

本例中,以上声明主要是为了指出该函数要使用这个外部变量。存储类别说明符extern告诉编译器,该函数中任何使用units的地方都引用同一个定义在函数外部的变量。再次强调,main()和critic()使用的都是外部定义的units。

3️⃣外部名称

C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。这修订了以前的标准,即编译器识别局部标识符前31个字符和外部标识符前6个字符。你所用的编译器可能还执行以前的规则。外部变量名比局部变量名的规则严格,是因为外部变量名还要遵循局部环境规则,所受的限制更多。

4️⃣定义和声明

int tern = 1; /* tern被定义 */
int main()
{
	extern int tern; /* 使用在别处定义的tern */

这里,tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。 第2次声明只告诉编译器使用之前已创建的tern变量,所以这不是定义。 第1次声明被称为定义式声明(defining declaration),第2次声明被称为引用式声明(referencing declaration)。关键字extern表明该声明不是定义,因为它指示编译器去别处查询其定义。
假设这样写:

extern int tern;
int main(void)
{

编译器会假设 tern 实际的定义在该程序的别处,也许在别的文件中。该声明并不会引起分配存储空间。因此,不要用关键字extern创建外部定义,只用它来引用现有的外部定义。

外部变量只能初始化一次,且必须在定义该变量时进行。 假设有下面的代码:

// file_one.c
char permis = 'N';
...
// file_two.c
extern char permis = 'Y'; /* 错误 */

file_two中的声明是错误的,因为file_one.c中的定义式声明已经创建并初始化了permis.

内部链接静态变量

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

static int svil = 1;  // 静态变量,内部链接
int main(void)

这种变量过去称为外部静态变量(external static variable),但是这个术语有点自相矛盾(这些变量具有内部链接)。但是,没有合适的新简称,所以只能用内部链接的静态变量(static variable with internal linkage)。普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。 可以使用存储类别说明符 extern,在函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性。考虑下面的代码:

int traveler = 1;      // 外部链接
static int stayhome = 1;  // 内部链接
int main()
{
    extern int traveler;  // 使用定义在别处的 traveler
    extern int stayhome;  // 使用定义在别处的 stayhome
    ...

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

多文件

只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。接下来简要介绍一下。复杂的C程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。

C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。

注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用 extern关键字)。也就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。

过去,不同的编译器遵循不同的规则。例如,许多 UNIX系统允许在多个文件中不使用 extern 关键字声明变量,前提是只有一个带初始化的声明。编译器会把文件中一个带初始化的声明视为该变量的定义。

存储类别说明符

C语言有6个关键字作为存储类别说明符:auto、register、static、extern、_ Thread_local和typedef。

typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因。尤其是,在绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的一部分。

唯一例外的是_Thread_local,它可以和static或extern一起使用。

auto

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

register

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

static

用 static 说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。1️⃣如果static 用于文件作用域声明,作用域受限于该文件。2️⃣如果 static 用于块作用域声明,作用域则受限于该块。

因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。

extern

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

小结:存储类别

自动变量具有块作用域、无链接、自动存储期。它们是局部变量,属于其定义所在块(通常指函数)私有 。寄存器变量的属性和自动变量相同,但是编译器会使用更快的内存或寄存器储存它们。不能获取寄存器变量的地址。

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

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

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

存储类别和函数

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

外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。假设一个文件中包含了以下函数原型:

double gamma(double);   /* 该函数默认为外部函数 */
static double beta(int, int);
extern double delta(double, int);

在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以在其他文件中可以使用与之同名的函数。

通常的做法是:用 extern 关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明都默认为extern。

存储类别的选择

对于“使用哪种存储类别”的回答绝大多数是“自动存储类别”,要知道默认类别就是自动存储类别。初学者会认为外部存储类别很不错,为何不把所有的变量都设置成外部变量,这样就不必使用参数和指针在函数间传递信息了。

然而,这背后隐藏着一个陷阱。如果这样做,A()函数可能违背你的意图,私下修改B()函数使用的变量。多年来,无数程序员的经验表明,随意使用外部存储类别的变量导致的后果远远超过了它所带来的便利。唯一例外的是const数据。因为它们在初始化后就不会被修改,所以不用担心它们被意外篡改:

const int DAYS = 7;
const char * MSGS[3] = {"Yes", "No", Maybe"};

保护性程序设计的黄金法则是:“按需知道”原则。尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。 除自动存储类别外,其他存储类别也很有用。不过,在使用某类别之前先要考虑一下是否有必要这样做。

总结

在这里插入图片描述在这里插入图片描述

🌈ok,完结~(●’◡’●) 看到这里 点个赞叭 (●’◡’●)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1350577.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

西尔维斯特方程(Sylvester equation)官方求解有问题

西尔维斯特方程&#xff08;Sylvester equation&#xff09;官方求解有问题 1. 西尔维斯特方程&#xff08;Sylvester equation&#xff09;1.1 以上所有矩阵都是2x2矩阵的演算形式 2. 官方求解Sylvester方程-错误2.1 官方求解形式2.2 2x2矩阵举例 3. Nicolas Andreff作者文章O…

PostgreSQL数据库的json操作

1.操作符 select json字段::json->key值 from order -- 对象域 select json字段::json->>key值 from order -- 文本 select json字段::json#>{key值} from order -- 对象域 select json字段::json#>>{key值} from order -- 文本对象域表示还能继续操作&#…

微服务智慧工地信息化解决方案(IOT云平台源码)

智慧工地是指应用智能技术和互联网手段对施工现场进行管理和监控的一种工地管理模式。它利用传感器、监控摄像头、人工智能、大数据等技术&#xff0c;实现对施工现场的实时监测、数据分析和智能决策&#xff0c;以提高工地的安全性、效率和质量。 智慧工地平台是一种智慧型、系…

听GPT 讲Rust源代码--library/alloc

File: rust/library/alloc/benches/slice.rs 在Rust源代码中&#xff0c;rust/library/alloc/benches/slice.rs文件的作用是对&[T]类型&#xff08;切片类型&#xff09;进行性能基准测试。该文件包含了对切片类型的一系列操作的基准测试&#xff0c;例如切片迭代、切片排序…

透明OLED屏批发:从价格到产品选择的全方位指南

随着科技的进步&#xff0c;透明OLED屏作为一种创新的显示技术&#xff0c;在各个领域都展现出巨大的应用潜力。对于希望在商业或个人应用中采用透明OLED屏的企业或个人&#xff0c;批发购买可能是一个经济高效的选择。尼伽小编将从本文将为您详细解析透明OLED屏批发的各个环节…

x-cmd pkg | bit - 实验性的现代化 git CLI

目录 简介首次用户功能特点竞品和相关作品进一步探索 简介 bit&#xff0c;由 Chris Walz 于 2020 年使用 Go 语言开发&#xff0c;提供直观的命令行补全提示和建立在 git 命令之上的封装命令&#xff0c;旨在建立完全兼容 git 命令的现代化 CLI。 首次用户 使用 x bit 即可自…

EasyCode代码生成器插件

EasyCode文档&#xff1a;https://gitee.com/makejava/EasyCode/wikis/pages?sort_id725187&doc_id166248 EasyCode 优点 可以生成controller层的代码。可以一次性生成多张表的各层代码。可以自定义模板。 EasyCode使用 在插件市场下载easy code插件 在idea中进行数据…

MySQL基础学习: linux系统mysql 密码插件 validate_password安装

1、没有安装mysql密码插件&#xff0c;执行命令&#xff1a;SHOW VARIABLES LIKE ‘validate_password%’; 2、安装mysql密码插件&#xff0c;执行命令&#xff1a;install plugin validate_password soname ‘validate_password.so’; 3、再次执行&#xff1a;SHOW VARIABLE…

Docker 实践之旅:项目迁移与高效部署

目录 1 引言2 初识 Docker2.1 Docker简介2.2 Docker优势 3 传统部署流程的问题4 学习 Docker 的过程5 Docker 解决项目部署的实践5.1 迁移关键服务5.2 定制化打包与快速部署 6 项目实践收获6.1 简化了部署流程6.2 节约了部署成本 7 克服难点和经验分享7.1 版本兼容性问题7.2 网…

Python列表数据处理全攻略(七):常用内置方法轻松掌握

引言 亲爱的读者&#xff0c;你好&#xff01;Python的列表在数据结构中占据着核心地位&#xff0c;对于学习与应用Python来说&#xff0c;它无疑是我们不可或缺的得力助手。它不仅能帮助我们有效地存储和整理数据&#xff0c;还为我们提供了众多内置方法&#xff0c;让数据处…

基于RetinaFace+Jetson Nano的智能门锁系统——第二篇(配置环境)

文章目录 设备一、安装远程登录终端Xshell1.1下载Xshell1.2新建回话1.3查询ip地址1.4启动连接 二、安装远程文件管理WinScp2.1下载WinScp2.2连接Jetson Nano2.3连接成功 三、安装远程桌面VNC Viewer3.1下载VNC Viewer3.2在Jetson Nano安装VNC Viewer3.3设置VINO登录选项3.4将网…

Git原理与使用(二):分支管理

Git原理与使用[二]:分支管理 一.分支的基本操作1.理解分支2.创建分支3.切换分支4.删除分支5.补充:创建并切换分支 二.合并分支1.合并分支的基础操作2.分支冲突 三.分支管理策略1.Fast-forward模式2.--no--ff(即:禁用Fast-forward模式)3.分支策略 四.创建临时分支修复bug1.git s…

C#核心学习(面向对象)

目录 封装构造函数基本概念&#xff1a;写法&#xff1a;特殊写法注意&#xff1a; 析构函数基本语法 垃圾回收机制&#xff08;GC&#xff0c;Garbage Collector&#xff09;回收算法C# 中内存回收原理自动回收步骤注意 手动回收&#xff1a; 成员属性基本概念基本语法注意&am…

什么是软件测试?这是我听过最通俗易懂的解释

很多人总是说我要学习软件测试&#xff0c;因为他可以拿到一个不错的薪资。 但是当我问他你知道什么是软件测试吗&#xff1f;这个时候&#xff0c;他总会愣住了&#xff0c;一脸不屑的表情说着&#xff0c;不就是找bug&#xff0c;给软件找问题&#xff0c;找茬吗&#xff1f…

超好用的IDEA插件!免费

IDEA是一款功能强大的集成开发环境&#xff08;IDE&#xff09;&#xff0c;它可以帮助开发人员更加高效地编写、调试和部署软件应用程序。我们在编写完接口代码后需要进行接口调试等操作&#xff0c;一般需要打开额外的调试工具。 今天给大家介绍一款IDEA插件&#xff1a;Api…

Windows电脑引导损坏?按照这个教程能修复

前言 Windows系统的引导一般情况下是不会坏的&#xff0c;小伙伴们可以不用担心。发布这个帖子是因为要给接下来的文章做点铺垫。 关注小白很久的小伙伴应该都知道&#xff0c;小白的文章都讲得比较细。而且文章与文章之间的关联度其实还是蛮高的。在文章中&#xff0c;你会遇…

工程(十六)——自己数据集跑Fast_livo

一、基础环境 Ubuntu20.04 ROS noetic PCL 1.8 Eigen 3.3.4 Sophus git clone https://github.com/strasdat/Sophus.git cd Sophus git checkout a621ff mkdir build && cd build && cmake .. make sudo make install 下面两个直接把包下载下来一起编译…

堆排序(C语言版)

一.堆排序 堆排序即利用堆的思想来进行排序&#xff0c;总共分为两个步骤&#xff1a; 1. 建堆 升序&#xff1a;建大堆 降序&#xff1a;建小堆 2. 利用堆删除思想来进行排序 1.1.利用上下调整法实现堆排序 第一步&#xff1a;建堆 好了&#xff0c;每次建堆都要问自己…

STM32G030F6P6读写flash失败问题(HAL)

STM32G030是F0系列的升级版&#xff0c;其在性能上比F0要好很多&#xff0c;具体G0参数如下&#xff1a; 最开始做项目选用的单片机是STM32F030F4P6&#xff0c;但是在后期使用中发现&#xff0c;我的FLASH&#xff08;16K&#xff09;不够用了&#xff0c;就选择了STM32G030F6…

计算机网络——基础知识汇总(八)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;V…