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

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

1.45 内部链接的静态变量

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

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

内部链接的静态变量只能用于同一个文件中的函数可以使用存储类别说明符extern在本文件的函数中重复声明任何具有文件作用域的变量。这样的声明并不会改变其链接属性(内部链接)。

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


1.5 多文件

有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。


复杂的C程序通常由多个单独的源代码文件组成。有时,这些文件可能要共享一个外部变量。C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。


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


1.6 函数的存储类别

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


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

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

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


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


小结:


1.静态变量都具有静态存储期,即它们的生命周期都和程序的生命周期相同;

2.静态变量都会初始化为0,除非你手动初始化为其他的值;

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

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

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

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

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

看一下从标准库stdlib.h中的随机数生成函数的用法:


C语言的随机数生成函数是rand();

int  rand(void);

该函数的机制是:有一个具有内部链接的静态变量,声明时初始化为1。调用一次rand()函数,该函数就会将这个变量修改一次(会按照某个数学公式修改),根据前面讲的,每次调用rand函数时,这个静态变量都会保存上一次的值,而他的初始值和修改公式是不变的,所以你每次运行程序,得到的随机数是一样的(伪随机)。

  for (int i = 0; i < 4; i++)
  {
    printf("%d\n", rand());
  }


每次运行上面的代码,都会得到:

41
18467
6334
26500


c语言使用srand()函数来改变这一点:

void  srand(unsigned int _Seed);

给srand()一个输入值,他就会把rand()函数使用的那个静态变量更改为这个值,我们可以通过控制srand()的参数,使得每次rand()输出不同的随机数。

(1)srand()用1作为参数

此时,和之前一样,因为静态变量本来的额初始化值就是1。

  srand(1);
  for (int i = 0; i < 4; i++)
  {
    printf("%d\n", rand());
  }


现在输出的随机数和前面是一样的。

(2)srand()用系统时间做参数

由于时间是一直变化的,运行程序时那个静态变量每次都会被置为不同的值,所以每次产生的随机数也不同。

  srand((unsigned int)time(0));
  for (int i = 0; i < 4; i++)
  {
    printf("%d\n", rand());
  }


第一次输出:

30242
11899
15367
11662

第二次输出:

30304
19510
27106
10113


注:使用求模可以控制随机数的范围:rand()%5,产生[0,4]的随机数。

这就是具有内部链接的静态变量的一个应用。

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

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

前面讨论的存储类别有一个共同之处:在确定用哪种存储类别后,根据已制定好的内存管理规则,将自动选择其作用域和存储期。然而,还有更灵活地选择,即用库函数分配和管理内存。

以下声明:

float x;
char place[] = "Dancing Oxen Creek";

为一个float类型的值和一个字符串预留了足够的内存,或者可以显式指定分配一定数量的内存:

int plates[100];

该声明预留了100个内存位置,每个位置都用于储存int类型的值。声明还为内存提供了一个标识符。因此,可以使用x或place识别数据。


静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。


C语言可以在程序运行时分配更多的内存。即使用malloc()函数,该函数接受一个参数:所需的内存字节数。


malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char表示1字节,malloc()的返回类型通常被定义为指向char的指针。


ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个“通用指针”。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性。然而,把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题。如果malloc()分配内存失败,将返回空指针。


例:

double * ptd;
ptd = (double *) malloc(30 * sizeof(double));

以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置。(ptd指向分配的内存的起始位置,即数组首元素,使用sizeof而不是数字,是为了提高代码的可移植性)


创建数组的方法:


声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态内存或自动内存创建这种数组。

声明变长数组时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。

声明一个指针,调用malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的

使用第2种和第3种方法可以创建动态数组(dynamic array)。这种数组和普通数组不同,可以在程序运行时选择数组的大小和分配内存。


通常,malloc()要与free()配套使用。free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。


free()函数只释放其参数指向的内存块。一些操作系统在程序结束时会自动释放动态分配的内存,但是有些系统不会。为保险起见,请使用free(),不要依赖操作系统来清理。


malloc()和free()的原型都在stdlib.h头文件中。


分配内存还可以使用calloc(),典型的用法如下:

long * newmem;
newmem = (long *)calloc(100, sizeof (long));

calloc()函数还有一个特性:它把块中的所有位都设置为0。


free()函数也可用于释放calloc()分配的内存。


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

变长数组(VLA)和malloc()函数功能是相似的,都可以在程序运行时确定数组大小。


不同之处在于:


1.变长数组是自动存储类型。因此,程序在离开变长数组定义所在的块时,变长数组占用的内存空间会被自动释放,不必使用free()。

2.用malloc()创建的数组不必局限在一个函数内访问。比如在被调函数中使用malloc(),在主调函数中使用free()。

3.free()所用的指针变量可以与malloc()的指针变量不同,但是两个指针必须储存相同的地址。

4.对多维数组而言,使用变长数组更方便。

int n = 5;
int m = 6;
int ar2[n][m]; // 变长数组
int (* p2)[6]; 
int (* p3)[m];
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组
ar2[1][2] = p2[1][2] = 12;

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

可以认为程序把它可用的内存分为 3部分:


一部分供具有外部链接、内部链接和无链接的静态变量使用;

一部分供自动变量使用;

一部分供动态内存分配。

静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可访问储存在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。


然而,自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。


动态分配的内存在调用malloc()或相关函数时存在,在调用free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈内存慢。


四、C语言类型限定符

常用类型和存储类别来描述一个变量。C90还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const和volatile来声明,以这两个关键字创建的类型是限定类型(qualified type)。C99标准新增了第3个限定符:restrict,用于提高编译器优化。C11标准新增了第4个限定符:_Atomic。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。


4.1 const 类型限定符

以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。(const修饰的变量可称为只读变量,而不是常量)


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

声明普通变量和数组时使用const关键字很简单。指针则复杂一些,因为要区分是限定指针本身为const还是限定指针指向的值为const。


其实很简单,看const修饰的是什么就可以了:const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。


例:

const float * pf

指针指向的数据不能变,但指针本身可以变,即它可以指向不同的位置。

float * const pt

指针本身的值不能变,即他只能指向同一个地址,但他指向的值可以改变。

const float * const ptr;

指针以及它指向的值都不能变。

float const * pfc;

和第一个一样。

const关键字的常见用法是声明为函数形参的指针。例如,假设有一个函数要调用display()显示一个数组的内容。要把数组名作为实际参数传递给该函数,但是数组名是一个地址。该函数可能会更改主调函数中的数据,但是下面的原型保证了数据不会被更改

void display(const int array[], int limit);

4.12 对全局数据使用 const

使用全局变量是一种冒险的方法,因为这样做暴露了数据,程序的任何部分都能更改数据。


如果把数据设置为const,就可避免这样的危险,因此用const限定符声明全局数据很合理。可以创建const变量、const数组和const结构。(当然了,这种方式适用于程序只需要读取const变量值的情况)


2种使用方式:


遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字);

把const变量放在一个头文件中,然后在其他文件中包含该头文件。(必须在头文件中用关键字static声明全局const变量)

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

再次强调:以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。但它不是不可修改,比如我们可以使用指针来修改:

  const int a = 9;
  int* pa = &a;
  *pa = 10;
  printf("%d\n",a);

输出:10

或者在不支持变长数组的编译器下:

  const int a = 9;
  int arr[a];

是会报错的。

4.2 volatile 类型限定符

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


4.3 restrict类型限定符

restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。


4.4 _Atomic类型限定符(C11)

发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。


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