【C语言】存储类别(作用域、链接、存储期)、内存管理和类型限定符(主讲const)(一)

简介: 关键字:auto、extern、static、register、const、volatile、restricted、_Thread_local、_Atomic函数:rand()、srand()、time()、malloc()、calloc()、free()如何确定变量的作用域(可见的范围)和生命期(它存在多长时间)设计更复杂的程序

本文内容主要包括:


关键字:auto、extern、static、register、const、volatile、restricted、_Thread_local、_Atomic

函数:rand()、srand()、time()、malloc()、calloc()、free()

如何确定变量的作用域(可见的范围)和生命期(它存在多长时间)

设计更复杂的程序

文章目录

一、存储类别

1.1 作用域

1.2 链接

1.3 存储期

1.4 五种存储类别

1.41 自动变量

1.42 寄存器变量

1.43 块作用域的静态变量

1.44 外部链接的静态变量

1.45 内部链接的静态变量

1.5 多文件

1.6 函数的存储类别

二、应用——随机数生产函数和静态变量

三、内存分配malloc()、calloc()、free()

3.1 malloc()、calloc()、free()

3.2 动态内存分配和变长数组

3.3 存储类别和动态内存分配

四、C语言类型限定符

4.1 const 类型限定符

4.11 在指针和形参声明中使用 const

4.12 对全局数据使用 const

4.13 const变量不是常量(它也可以修改)

4.2 volatile 类型限定符

4.3 restrict类型限定符

4.4 _Atomic类型限定符(C11)


一、存储类别

从硬件方面来看,被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(object)。


对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小(你不要总想着面向对象里面的对象,面向对象编程中的对象指的是类对象,其定义包括数据和允许对数据进行的操作,C不是面向对象编程语言)。


程序需要一种方法访问对象。这可以通过声明变量来完成:

int a = 3;

该声明创建了一个名为 a 的标识符(identifier)。标识符是一个名称,在这种情况下,标识符可以用来指定(designate)特定对象的内容。标识符遵循变量的命名规则。在该例中,标识符 a 即是软件(即C程序)指定硬件内存中的对象的方式。该声明还提供了储存在对象中的值。


标识符,包括变量名、常量名、对象名、函数名、类型名等等


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

int *pt = &a;
int ranks[10];


第1行声明中,pt 是一个标识符(指针变量名),它指定了一个储存地址的对象(它指定了一块内存,这里存储着它指向值的地址)。但是,表达式 *pt 不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与 a 指定的对象相同。


一般而言,那些指定对象的表达式被称为左值。所以,a 既是标识符也是左值;*pt既是表达式也是左值。按照这个思路,ranks + 2 *a既不是标识符(不是名称),也不是左值(它不指定内存位置上的内容)。


但是表达式*(ranks + 2 * a)是一个左值,因为它的确指定了特定内存位置的值,即ranks数组的第7个元素。顺带一提,ranks的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象。


所有这些示例中,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(modifiable lvalue)。

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

程序根据该声明把相应的字符串字面量储存在内存中,内含这些字符值的字符串字面量就是一个对象。


由于字符串字面量中的每个字符都能被单独访问,所以每个字符也是一个对象。


该声明还创建了一个标识符为pc的对象,储存着字符串的地址。由于可以设置pc重新指向其他字符串,所以标识符pc是一个可修改的左值。


const只能保证被pc指向的字符串内容不被修改,但是无法保证pc不指向别的字符串。


由于pc指定了储存’B’字符的数据对象,所以pc是一个左值,但不是一个可修改的左值。与此类似,因为字符串字面量本身指定了储存字符串的对象,所以它也是一个左值,但不是可修改的左值。


可以用 存储期(storage duration) 描述对象,所谓存储期是指对象在内存中保留了多长时间。


标识符用于访问对象,可以用 作用域(scope)和链接(linkage) 描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。


不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。


对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存


1.1 作用域

作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域、函数作用域、函数原型作用域或文件作用域。


(1)块作用域(block scope)


块是用一对花括号括起来的代码区域。


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


(2)函数作用域(function scope)


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


(3)函数原型作用域(function prototype scope)


用于函数原型中的形参名(变量名),如下所示:

int mighty(int mouse, double large);


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

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

方括号中必须使用在函数原型中已声明的名称。

(4)文件作用域

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

#include <stdio.h>
int units = 0;        /* 该变量具有文件作用域 */
void critic(void);
int main(void)
{
    ...
}
void critic(void)
{
    ...
}

变量units具有文件作用域,main()和critic()函数都可以使用它(更准确地说,units具有外部链接文件作用域,稍后讲解)。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)


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

1.2 链接

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


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


具有文件作用域的变量可以是外部链接或内部链接。


外部链接变量可以在多文件程序中使用(在其他文件中使用时需要使用extern关键字),内部链接变量只能在一个翻译单元中使用。


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


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

int giants = 5;            // 文件作用域,外部链接
static int dodgers = 3;    // 文件作用域,内部链接
int main()
{
    ...
}
...


1.3 存储期

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


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

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

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

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

块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字static。 它的作用域定义在函数块中。只有在执行该函数时,程序才能使用标识符访问它所指定的对象(但是,该函数可以给其他函数提供该存储区的地址以便间接访问该对象,例如通过指针形参或返回值)。

1.4 五种存储类别

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

1.41 自动变量

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

...
int main(void)
{
    auto int plox;
    ...

关键字auto是存储类别说明符(storage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符。


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


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


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


注意以下两点:


1.没有花括号的块: 个C99特性:作为循环或if语句的一部分,即使不使用花括号({}),也是一个块。更完整地说,整个循环是它所在块的子块(sub-block),循环体是整个循环块的子块。与此类似,if语句是一个块,与其相关联的子语句是if语句的子块。这些规则会影响到声明的变量和这些变量的作用域。(不用花括号时,只能有一个语句。有的编译器可能不支持)

for (int a = 0; a < 8; a++)
   printf("%d\n",a);

2.自动变量的初始化:自动变量不会自动初始化,除非显式初始化它:

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;    // 使用之前定义的变量

1.42 寄存器变量

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

int main(void)
{
    register int quick;


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


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

void macho(register int n)

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


1.43 块作用域的静态变量

静态变量,即与程序有着相同生命周期的变量。那么具有文件作用域的变量肯定都属于静态变量。


那么,在块中,用static关键字声明一个变量,它也具有静态存储期,也即具有静态存储期的局部变量,完整的来说是:具有静态存储期、块作用域、无链接的变量。(记住一个变量的3个要素:作用域、连接、存储期)。


如果未显式初始化静态变量,它们会被初始化为0(这与自动变量不同)。


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

int wontwork(static int flu); // 不允许

你可能要问了:这种变量只有块作用域、却有着静态存储期,它有什么用处呢?

静态变量用处很多,它只会被初始化一次(不初始化时自动初始化为0),以后的值就是上一次调用后的值。比如,你可以用它来检查某个函数是否被调用过,调用了几次。

看如下的测试程序:

#include<stdio.h>
int * static_test();
int *auto_test();
int main()
{
  int* pa;
  pa = static_test();
  printf("用指针访问局部静态变量:%d\n\n", *pa);
  *pa += 6;
  static_test();
  printf("用指针访问自动变量:%d\n\n", *auto_test());
  for (int i = 1; i < 4; i++)
  {
    static_test();
    auto_test();
  }
  return 0;
}
int *static_test()
{
  static int a;
  printf("局部静态变量的值:%d\n", a++);
  return &a;
}
int *auto_test()
{
  int b=2;
  printf("自动变量的值:%d\n", b++);
  return &b;
}

输出:

局部静态变量的值:0
用指针访问局部静态变量:1
局部静态变量的值:7
自动变量的值:2
用指针访问自动变量:3
局部静态变量的值:8
自动变量的值:2
局部静态变量的值:9
自动变量的值:2
局部静态变量的值:10
自动变量的值:2

解读:


1.局部静态变量会自动初始化为0;而自动变量需要手动初始化(否则他的值就是他在内存中的位置处以前存在的值,当然也可能是0);

2.具有块作用域的变量,可以在块之外,用指针访问。但是,自动变量必须在调用这个快的同时访问(应为他的存储期和这个块是一致的),而局部静态变量可以在任何时候用指针在块之外访问;

3.自动变量每次调用都会重新初始化,上面每次输出都是2;

4.局部静态变量只初始化一次,保存上一次调用后的值。

除了具有块作用域的静态变量外,还有具有文件作用域的静态变量,它们又分为外部链接的静态变量和内部链接的静态变量。


1.44 外部链接的静态变量

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


(1)初始化外部变量


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

int x = 10;                // 没问题,10是常量
int y = 3 + 20;            // 没问题,用于初始化的
是常量表达式
size_t z = sizeof(int);    //没问题,用于初始化的是
常量表达式
int x2 = 2 * x;            // 不行,x是变量
(只要不是变长数组,sizeof表达式可被视为常量表达式。


(2)定义和声明

例:

int tern = 1;     /* 定义具有外部链接的静态变量 */
int num = 2;
extern int TEST;  /* 声明在别的文件中定义的变量TEST */
main()
{
    extern int tern; /* 使用在函数外面定义的tern,在定义该变量的文件中,也可以直接使用。即注释掉这句,直接使用tern变量 */
    int num = 3; /* 这个定义会覆盖外面定义的num,即在这个函数中,外面定义的那个num失效。(这两个num虽然名称相同,但存储期、作用域都不容易*/)

这里,tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只告诉编译器使用之前已创建的

tern变量,所以这不是定义。第1次声明被称为定义式声明(defining declaration),第2次声明被称为引用式声明(referencingdeclaration)。关键字extern表明该声明不是定义,因为它指示编译器去别处查询其定义。


不要用关键字extern创建外部定义,只用它来引用现有的外部定义。

extern char permis = 'Y'; /* 错误 */


/

相关文章
|
21天前
|
存储 编译器 C语言
C语言存储类详解
在 C 语言中,存储类定义了变量的生命周期、作用域和可见性。主要包括:`auto`(默认存储类,块级作用域),`register`(建议存储在寄存器中,作用域同 `auto`,不可取地址),`static`(生命周期贯穿整个程序,局部静态变量在函数间保持值,全局静态变量限于本文件),`extern`(声明变量在其他文件中定义,允许跨文件访问)。此外,`typedef` 用于定义新数据类型名称,提升代码可读性。 示例代码展示了不同存储类变量的使用方式,通过两次调用 `function()` 函数,观察静态变量 `b` 的变化。合理选择存储类可以优化程序性能和内存使用。
137 82
|
12天前
|
监控 算法 Java
深入理解Java中的垃圾回收机制在Java编程中,垃圾回收(Garbage Collection, GC)是一个核心概念,它自动管理内存,帮助开发者避免内存泄漏和溢出问题。本文将探讨Java中的垃圾回收机制,包括其基本原理、不同类型的垃圾收集器以及如何调优垃圾回收性能。通过深入浅出的方式,让读者对Java的垃圾回收有一个全面的认识。
本文详细介绍了Java中的垃圾回收机制,从基本原理到不同类型垃圾收集器的工作原理,再到实际调优策略。通过通俗易懂的语言和条理清晰的解释,帮助读者更好地理解和应用Java的垃圾回收技术,从而编写出更高效、稳定的Java应用程序。
|
22天前
|
存储 人工智能 C语言
数据结构基础详解(C语言): 栈的括号匹配(实战)与栈的表达式求值&&特殊矩阵的压缩存储
本文首先介绍了栈的应用之一——括号匹配,利用栈的特性实现左右括号的匹配检测。接着详细描述了南京理工大学的一道编程题,要求判断输入字符串中的括号是否正确匹配,并给出了完整的代码示例。此外,还探讨了栈在表达式求值中的应用,包括中缀、后缀和前缀表达式的转换与计算方法。最后,文章介绍了矩阵的压缩存储技术,涵盖对称矩阵、三角矩阵及稀疏矩阵的不同压缩存储策略,提高存储效率。
|
24天前
|
存储 算法 C语言
数据结构基础详解(C语言): 二叉树的遍历_线索二叉树_树的存储结构_树与森林详解
本文从二叉树遍历入手,详细介绍了先序、中序和后序遍历方法,并探讨了如何构建二叉树及线索二叉树的概念。接着,文章讲解了树和森林的存储结构,特别是如何将树与森林转换为二叉树形式,以便利用二叉树的遍历方法。最后,讨论了树和森林的遍历算法,包括先根、后根和层次遍历。通过这些内容,读者可以全面了解二叉树及其相关概念。
|
24天前
|
存储 机器学习/深度学习 C语言
数据结构基础详解(C语言): 树与二叉树的基本类型与存储结构详解
本文介绍了树和二叉树的基本概念及性质。树是由节点组成的层次结构,其中节点的度为其分支数量,树的度为树中最大节点度数。二叉树是一种特殊的树,其节点最多有两个子节点,具有多种性质,如叶子节点数与度为2的节点数之间的关系。此外,还介绍了二叉树的不同形态,包括满二叉树、完全二叉树、二叉排序树和平衡二叉树,并探讨了二叉树的顺序存储和链式存储结构。
|
24天前
|
存储 算法 C语言
C语言手撕数据结构代码_顺序表_静态存储_动态存储
本文介绍了基于静态和动态存储的顺序表操作实现,涵盖创建、删除、插入、合并、求交集与差集、逆置及循环移动等常见操作。通过详细的C语言代码示例,展示了如何高效地处理顺序表数据结构的各种问题。
|
3天前
|
编译器 Linux API
基于类型化 memoryview 让 Numpy 数组和 C 数组共享内存
基于类型化 memoryview 让 Numpy 数组和 C 数组共享内存
11 0
|
29天前
|
存储 大数据 C语言
C语言 内存管理
本文详细介绍了内存管理和相关操作函数。首先讲解了进程与程序的区别及进程空间的概念,接着深入探讨了栈内存和堆内存的特点、大小及其管理方法。在堆内存部分,具体分析了 `malloc()`、`calloc()`、`realloc()` 和 `free()` 等函数的功能和用法。最后介绍了 `memcpy`、`memmove`、`memcmp`、`memchr` 和 `memset` 等内存操作函数,并提供了示例代码。通过这些内容,读者可以全面了解内存管理的基本原理和实践技巧。
|
29天前
|
缓存 Linux C语言
C语言 多进程编程(六)共享内存
本文介绍了Linux系统下的多进程通信机制——共享内存的使用方法。首先详细讲解了如何通过`shmget()`函数创建共享内存,并提供了示例代码。接着介绍了如何利用`shmctl()`函数删除共享内存。随后,文章解释了共享内存映射的概念及其实现方法,包括使用`shmat()`函数进行映射以及使用`shmdt()`函数解除映射,并给出了相应的示例代码。最后,展示了如何在共享内存中读写数据的具体操作流程。
|
29天前
|
存储 缓存 程序员
c语言的存储类型-存储类
本文详细介绍了C语言中的存储类型及其分类,包括基本类型(如整型、浮点型)和复合类型(如数组、结构体)。重点讲解了不同存储类别(`auto`、`static`、`register`、`extern`、`typedef`、`volatile`、`const`)的特点及应用场景,并展示了C11/C99引入的新关键字(如`_Alignas`、`_Atomic`等)。通过示例代码解释了每个存储类别的具体用法,帮助读者更好地理解和运用这些概念。
下一篇
无影云桌面