【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'; /* 错误 */


/

相关文章
|
20天前
|
C语言
【c语言】动态内存管理
本文介绍了C语言中的动态内存管理,包括其必要性及相关的四个函数:`malloc`、``calloc``、`realloc`和`free`。`malloc`用于申请内存,`calloc`申请并初始化内存,`realloc`调整内存大小,`free`释放内存。文章还列举了常见的动态内存管理错误,如空指针解引用、越界访问、错误释放等,并提供了示例代码帮助理解。
31 3
|
21天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
44 1
|
26天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
30天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
存储 编译器 C语言
C语言:数组名作为类型、作为地址、对数组名取地址的区别
在C语言中,数组名可以作为类型、地址和取地址使用。数组名本身代表数组的首地址,作为地址时可以直接使用;作为类型时,用于声明指针或函数参数;取地址时,使用取地址符 (&),得到的是整个数组的地址,类型为指向该类型的指针。
|
1月前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
53 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
22天前
|
存储 C语言
【c语言】字符串函数和内存函数
本文介绍了C语言中常用的字符串函数和内存函数,包括`strlen`、`strcpy`、`strcat`、`strcmp`、`strstr`、`strncpy`、`strncat`、`strncmp`、`strtok`、`memcpy`、`memmove`和`memset`等函数的使用方法及模拟实现。文章详细讲解了每个函数的功能、参数、返回值,并提供了具体的代码示例,帮助读者更好地理解和掌握这些函数的应用。
19 0
|
1月前
|
C语言
保姆级教学 - C语言 之 动态内存管理
保姆级教学 - C语言 之 动态内存管理
18 0
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
366 0
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
41 4