存储类别 Storage Class

前言

对象 Object

硬件层面:被储存的值需占用一定物理内存

软件层面:通过声明变量来指定硬件内存中的对象以及提供储存在对象中的值

左值——指定对象的表达式

可修改的左值——可以用来改变对象中的值

作用域、链接->标识符

存储期->对象

基本概念

作用域 Scope

定义:程序中可访问标识符的区域

分类:

  1. 块作用域(Block Scope)

    在花括号{}内的代码区域

    局部变量(包括函数形参)都具有块作用域

    C99允许在块中的任意位置声明具有变量

    for(int i = 0;i < 10;i++)printf("%d",i);
    
  2. 函数作用域(Function Scope)

    仅用于goto语句的标签

    即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数

  3. 函数原型作用域(Function Prototype Scope)

    用于函数原型的形参名

    范围为从形参定义到原型声明结束

  4. 文件作用域(File Scope)

    变量定义在函数外,具有文件作用域

    范围为从它的定义处到该定义处所在文件的末尾

    文件作用域变量也称为全局变量(Global Variable)

注:C预处理实际上是用包含的头文件内容替换#include指令

这里引入翻译单元的概念

[Translation Unit]: A translation unit is the output of the C preprocessor – a source file after it has been preprocessed.

一个程序由多个源代码组成==将由多个翻译单元组成 翻译单元==一个源代码文件和它的包含文件

当我们描述一个具有文件作用域的变量时,它的实际可见范围为整个翻译单元

举例

#include <stdio.h>

int a;//外部链接文件作用域  a

void printnum(int x);//函数原型作用域 x 
//这里x可省略

int main()
{
    printnum(5);
    return 0;
}

void printnum(int x)
{
    for(int i = 0;i < x;i++)printf("%d",i);//块作用域   x/i
}

链接 Linkage

分类:外部链接内部链接无链接

外部链接内部链接:具有文件作用域的变量

外部链接变量能在多文件程序中使用,而内部链接只能在其翻译单元中使用

无链接:具有块作用域、函数作用域或函数原型作用域的变量

通过查看是否使用static来判断外内部链接变量

#include <stdio.h>
int test = 0;//外部链接
static int tes = 1;//内部链接

存储期 Storage Duration

作用域、链接->标识符可见性

存储期 ->访问对象生存期

分类:

  1. 静态存储期:在程序执行期间

    文件作用域变量具有静态存储期

    块作用域变量也能具有静态存储期

    void test()
    {
    	static int i = 1;//静态存储期
    }
    
  2. 线程存储期:从被声明时到线程结束

  3. 自动存储期:从程序进入定义变量的块(分配变量内存)到退出该块(释放变量内存)

    相当于把自动变量占用的内存视为一个可重复使用的暂存区

    例:一个函数结束调用后,其变量占用的内存可用于储存下一个被调用函数的变量

    局部变量具有自动存储期

  4. 动态分配存储期

    变长数组,从声明处到块末尾

五种基本存储类别

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

自动变量

介绍

存储类别存储期作用域链接声明方式
自动自动块内

为了有意覆盖外部变量定义,或者强调不要把该变量改为其他存储类别,可以显式使用关键字auto

注意:如果编写C/C++兼容的程序,请不要用auto作为存储类别说明符

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

如果内层块中声明变量与外层块中变量同名,内层块会隐藏外层块的定义,退出内层块后外层块变量恢复作用域

没有花括号的块:C99特性,作为循环或if语句的一部分,即使不使用花括号{},也是一个块

for循环为例

#include <stdio.h>

int main()
{
    int i = 6;
    for(int i = 0;i < 5;i++)printf("%d ",i);//输出结果为1 2 3 4 5 
    return 0;
}

显式初始化

#include <stdio.h>

int main()
{
    int i;
    int m = 6;//m变量被初始化为5
    int n = 6 * m;//非常量表达式初始化,可使用之前定义过的变量
    return 0;
}

寄存器变量

介绍

存储类别存储期作用域链接声明方式
寄存器自动块内,使用关键字register

绝大多数寄存器变量自动变量一样,块作用域自动存储期无链接

与普通变量相比,访问和处理这些变量的速度会更快

但由于寄存器变量储存在寄存器而非内存当中,所以无法获取寄存器变量的地址

声明

使用存储类别识别符register声明寄存器变量

The keyword register hints to compiler that a given variable can be put in a register. It’s compiler’s choice to put it in a register or not. Generally, compilers themselves do optimizations and put the variables in register.

正如C Primer Plus书上所言,register比起命令更像是一种请求,寄存器变量要么被存入寄存器中,要么就变成普通的自动变量,就算如此也不能使用地址运算符

可声明register的数据类型有限

块作用域的静态变量

静态变量(Static Variable):存在内存中的变量

Static variables are allocated memory in data segment, not stack segment

介绍

存储类别存储期作用域链接声明方式
静态无链接静态块内,使用关键字static

具有文件作用域的变量自动具有静态存储期

声明

静态变量和外部变量在程序被载入内存时已执行完毕

不能在函数形参中使用static

外部链接的静态变量

介绍

存储类别存储期作用域链接声明方式
静态外部链接静态文件外部所有函数外

为了指出该函数使用了外部变量,可以在函数中使用关键字extern再次声明

如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern在该文件中声明该变量

初始化

外部变量如果未初始化,会被自动初始化为0

可以被显式初始化,但只能用常量表达式初始化文件作用域变量

PS:只要不是变长数组,sizeof表达式可被视为常量表达式

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

声明

定义式声明、引用式声明

int test = 1;//定义式声明	为变量预留存储空间
int main()
{
	extern int test;//引用式声明	使用在别处已经定义的变量
	return 0;
}

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

extern int x;
//编译器会假设x定义在该程序的别处或者其他文件内,该声明并不会分配存储空间
int main(){}

内部链接的静态变量

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

普通外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数

多文件

C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享

定义式声明->初始化
引用式声明->extern

在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能使用它

存储类别说明符

注:除了_Thread_local可以和static/extern一起使用,不能使用多个存储类别说明符

  • auto
  • register
  • static
  • extern
  • _Thread_local
  • typedef

存储类别与函数

外部函数——默认

静态函数

内联函数——C99新增

void test(int,int);//默认为外部函数
static void add(int,int);//静态函数
extern void del(int);//外部函数

其他文件中的函数可以调用testdel,但不能调用add

以存储类别说明符static创建的函数属于特定模块私有

通常做法:用extern关键字声明定义在其他文件里的函数

如何选择存储类别

个人一直不觉得全部使用外部变量是一个好事,虽然这样避免了传参方面等很多问题,但会存在错误修改、难以细化优化等忧患,请慎重选择存储类别

保护性程序设计:尽量在函数内部解决该函数的任务,只共享那些需要共享的变量

分配内存

用库函数来分配和管理内存——malloc()free()

malloc()free()都在stdlib.h

malloc

介绍

void* __cdecl malloc(_In_ _CRT_GUARDOVERFLOW size_t _Size);

malloc(所需的字节数)

malloc()函数会找到合适的空闲内存块,然后分配内存,而且这种内存是匿名的

malloc()返回动态分配内存块的首字节地址,因此可以将该地址赋给一个指针变量,用指针来访问这块内存

该函数可用于返回指向数组的指针、指向结构的指针等,所以通常返回值会被强制转换为匹配的类型。为了提高代码的可读性,也为了更容易把C程序转换为C++程序,应该坚持使用强制类型转换

ANSIC标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个通用指针。把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题

如果malloc()分配内存失败,将返回空指针

内存分配失败时可以调用exit()函数结束程序,其原型也在stdlib.h

动态数组

可以在程序运行时选择数组的大小和分配内存

1.声明变长数组

2.声明一个指针,调用malloc()并将其返回值赋给指针,使用指针访问数组元素

double array_one(n);//C99变长数组
ptd = (double *)malloc(n * sizeof(double));//合法,且比变长数组更灵活

double *pt;
pt = (double *)malloc(sizeof(double));//用sizeof提高了代码的可移植性

free

介绍

void __cdecl free(_Pre_maybenull_ _Post_invalid_ void* _Block);

free(之前malloc返回的地址)

释放之前malloc()分配的内存

动态分配内存的存储期:从malloc()分配内存到free()释放内存

重要性

静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或者减少,但动态分配的内存数量只会增加,除非用free()释放

多次占用大量内存未及时释放可造成内存泄露问题(Memory Leak)

calloc

void* __cdecl calloc(size_t _Count, size_t _Size);

malloc()十分类似:

1.在ANSI之前返回指向char的指针,在ANSI之后返回指向void的指针;

2.储存不同类型应使用强制类型转换

calloc()接受两个无符号整数(size_t)作为参数,第一个为所需存储单元数量,第二个为存储单元的大小

存储类别和内存分配

静态存储类别所用的内存数量在编译时已经确定

自动存储类别的内存通常作为来处理,新创建的变量按顺序加入内存,然后以相反顺序销毁

动态分配内存由管理员管理,所以可以在一个函数里创建,在另一个函数里释放。因此,该部分用于动态分配的内存会很分散,也就是未使用的内存块分散在已使用的内存块中。

使用动态内存通常比使用栈内存慢

程序把静态对象、自动对象和动态分配对象储存在不同区域,动态分配的区域通常称为内存堆或自由内存

ANSIC类型限定符

介绍

我们一般用类型和存储类别来描述一个变量,C90、C99、C11新增四个限定符分别为constvolatilerestrict_Atomic,以这四种关键字创建的类型为限定类型(Qualified Type)

其中,const表示恒常性,volatile表示易变性,restrict用于提高编译器优化,_Atomic用于并发程序设计

C99为限定类型新增一个属性——幂等性(Idempotent),即在一条声明里多次使用同个限定符,多余限定符将被忽略

const const const int test = 8;//与const int test = 8;等效

优点:增强文件变量可读性

typedef const int aaa;
const aaa var1;

const

const初始化不能被修改的数值和数组

const int test = 1;
const int arr[3] = {110,119,120};

在指针和形参中使用

指针:

const*的左侧,则指针所指向的值不能修改,const*的右边,则指针的地址不能发生改变

const int * pt;
int const * pt;//等效于上式

int * const pt;

形参:

当在函数形参中引入地址的时候为防止无意修改原数组,使用const关键字来保证其不会被修改,用法和指针一致

void showdb(const int arr[],int uid);

在全局变量中使用

const定义全局变量较为安全,参考原书,建议使用以下两种策略

1.在一个文件中使用定义式声明,在其他文件中使用引用式声明

const int test = 1;
//file1中定义的外部变量

extern const int test;
//file2中使用外部定义的变量

2.把const变量放入头文件中,其他文件包含该头文件

static const double num = 9.9;//在numset.h中定义

include "numset.h"//file1包含该头文件

使用static的原因是C标准不允许每个文件都有一个相同标识符的定义式声明

该策略的优势在于给每个包含该头文件的文件创建了一个单独的数据副本

volatile

告诉计算机,代理(不是该变量所在的程序)可以改变该变量的值,通常被用于硬件地址和在其他程序或同时进行的线程中共享数据

例如,一个地址上可能存放着本地时间,该数值无论程序运行过程怎样都随着时间变化而变化

volatile语法和const一致

在何时会用到volatile?当涉及到编译器的优化时

val_1 = x;
/*
中间省略了一些不影响x数值的代码
*/
val_2 = x;

高速缓存(caching):在上述代码中两次使用了x的值但没有修改其值,优化过的编译器会把x值临时存到寄存器中,在val_2调用x值时从寄存器中读取x值来节省时间

如果在这两个语句发生其他代理改变了x值,就不能使用这种优化了。如果声明中没有volatile限定符,编译器会假定该变量的值在这个过程中不会发生变化,并尝试优化

可以使用constvolatile同时限定一个变量,声明中不分先后顺序

volatile const int localtime;
const volatile int *loc;

restrict

允许编译器优化某部分代码以提高性能

该限定符只用于指针,表明该指针是访问数据对象的唯一且初始的方式。直白点就是,有且仅有该指针能访问

restrict也可用于形参中的指针,但编译器不会检查用户是否遵循该规定,所以传入参数的时候请注意这点

_Atomic

C11通过包含可选头文件stdatomic.hthreads.h,提供一些可选的管理方法。

要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象

int a;
a = 1;

//可换为

_Atomic int a;//a是一个原子类型的变量
atomic_store(&a,1);//stdatomic.h中的宏

在a中存储1是一个原子过程,其他线程不能访问a

旧关键字的新位置

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

限定类型符

void run(int *const a1.int *restrict a2,int n);//旧式

void run(int a1[const],int a2[restrict],int n);//C99

static

int recall(int arr[static 100]);

该用法意为,函数调用的实参应是一个指向数组首元素的指针,且该数组的长度至少为100

这种用法的目的为让编译器使用这些信息来优化函数编码

关键概念

通常来说应使用自动变量、函数形参和返回值来进行函数间的通信

在使用的值不变时建议使用全局变量


静态内存的数量在编译时已经确定,数值在程序运行时被载入内存

自动变量内存随着程序运行不断变化,视为可重复利用的工作区

动态分配内存也会不断变化,但是由函数控制的

小结

初识内存管理,学到很多(然后没过几天就会忘掉了23333

以上内容大部分皆是取自C Primer Plus原著,因为我觉得书上很多翻译过来的语句能很好地表达意思而不失严谨性就基本写入的原句,个人的总结和看法偏少,这可能也是不足吧

学习笔记陆陆续续写的,正式完结于2021-11-17 23:02:00

Q.E.D.


怀着一颗虔诚谦虚的心学习