存储类别、链接和内存管理

1.1 存储类别

从硬件方面来看,被存储的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象。一个对象可能并未存储实际的值,但是它在存储适当的值时一定具有相应的大小。

从软件方面来看,程序需要一种方法访问对象(个人理解:内存)。这可以通过声明变量来完成。

int entity=3;该声明创建了一个名为entity的标识符,标识符是一个名称,在这种情况下,标识符可以用来指定特定对象(个人理解:内存)的内容。

在该例中,标识符entity是软件指定对象的一种方式。

变量名不是指定对象的唯一途径,考虑下面的声明:

int *pt=&entity ; 

int ranks[10];

pt是一个标识符,它指定了一个存储地址的对象。但是,表达式*pt不是标识符,因为它不是一个名称。然而,它确实指定了一个对象,在这种情况下,它与entity指定的对象相同。一般而言,那些指定对象的表达式被称为左值。所以,entity既是标识符也是左值;*pt既是表达式也是左值。按照这个思路,ranks+2 * entity既不是标识符(不是名称),也不是左值(它不指定内存位置上的内容)。但是表达式* (ranks+2 * entity)是一个左值,因为它的确指定了特定内存位置的值,即ranks数组的第7个元素。所有这些示例中,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值。

考虑下面的声明:

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

程序根据该声明把相应的字符串字面量储存在内存中,内含这些字符值的数组就是一个对象。由于数组中的每个字符都能被单独访问,所以每个字符也是一个对象。该声明还创建了一个标识符为pc的对象,储存着字符串的地址。const只能保证被pc指向的字符串内容不被修改,但是无法保证pc不指向别的字符("Behold a string literal!"不能变,但是指针保存的那个地址可以变)。可以用存储期描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域链接描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。

1.1.1作用域

编译器源代码文件和所有的头文件都看成是一个单独文件,这个文件被称为翻译单元。描述一个全局变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的头文件。

1.1.2 链接

具有文件作用域的变量(全局变量)可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。
一些程序员把“内部链接的文件作用域”简称为“文件作用域”,把”外部链接的文件作用域“简称为“全局作用域”或“程序作用域”。

1.1.3 存储期

C对象有4种存储期:静态存储期线程存储期自动存储期动态分配存储期
线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
变长数组稍有不同,它的存储期从声明处到块的末尾,而不是从块的开始到块的末尾。
C使用作用域、链接和存储期为变量定义了多种存储方案。5种存储类别如表所示
在这里插入图片描述

1.1.4 寄存器变量

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

`int  main(void)
{
	register int quick;
}`

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

void macho(register int n)

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

1.1.5 无链接的静态变量

void trystat(void)
{
    int fade=1;
    static int stay=1;
    printf("fade=%d and stay=%d \n",fade++,stay++);
}

静态变量在程序被载入内存时已执行完毕。static int stay=1;这条声明并未在运行时执行。如果未显式初始化静态变量,它们会被初始化为0。

1.1.6 外部链接的静态变量

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

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

1.1.7 存储类别说明符

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

1.1.8 存储类别和函数

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

1.2分配内存

可以在程序运行时分配更多的内存。主要的工具是malloc函数,该函数接受一个参数:所需的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题。

现在,我们有三种创建数组的方法:
**1.**声明数组时,用常量表达式表示数组的维度。
**2.**声明变长数组(C99新增的特性)时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
**3.**声明一个指针,调用malloc(),将其返回值赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。

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

free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。malloc()和free()的原型都在stdlib.h头文件中。
如果内存分配失败,可以调用exit()函数结束程序,其原型在stdlib.h中。标准提供了两个返回值以保证在所有操作系统中都能正常工作:EXIT_SUCCESS(或者,相当于0)表示普通的程序结束,EXIT_FAILURE表示程序异常中止。

//dyn_arr.c--动态分配数组
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
    double * ptd;
    int max;
    int number;
    int i=0;

    puts("What is the maximum number of type double entries?");
    if(scanf("%d",&max)!=1)
    {
        puts("Number not correctly entered--bye.");
        exit(EXIT_FAILURE);
    }
    ptd=(double *)malloc(max*sizeof(double));
    if(ptd==NULL)
    {
        puts("Memory allocation failed.Goodbye.");
        exit(EXIT_FAILURE);
    }
    //ptd现在指向有max个元素的数组
    puts("Enter the value(q to quit):");
    while(i<max&&scanf("%lf",&ptd[i])==1)
        ++i;
    printf("Here are your %d entries:\n",number=i);
    for(i=0;i<number;i++)
    {
        printf("%7.2f",ptd[i]);
        if(i%7==6)
            putchar('\n');
    }
    if(i%7!=0)
        putchar('\n');
    puts("Down.");
    free(ptd);
    return 0;
}

下面是该程序的运行示例。程序通过交互的方式让用户先确定数组的大小,我们设置数组大小为5。虽然我们后来输入了6个数,但程序也只处理前5个数。
在这里插入图片描述

1.2.1calloc()函数

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

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

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

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

free()所用的指针变量可以与malloc()的指针变量不同,但是两个指针必须储存相同的地址。但是,不能释放同一块内存两次。
当然,也可以用malloc()创建二维数组,但是语法比较繁琐。如果编译器不支持变长数组特性,就只能固定二维数组的维度,如下所示:

int n=5;
int m=6;
p2=(int (*)[6])malloc(n*6*sizeof(int));//n*6数组
p3=(int (*)[m])malloc(n*m*sizeof(int));//n*m数组(要求支持变长数组)

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

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

系统中的内存有3种形式:静态存储区堆存储区栈存储区

静态存储区由操作系统分配使用,全局变量、static关键字定义的变量存储在静态存储区;栈存储区由编译器、程序分配使用;堆存储区主要由程序员分配使用。

1.3 ANSI C类型限定符

C90还新增了两个属性:恒常性和易变性。这两个属性可以分别用关键字const和volatile来声明,以这两个关键字创建的类型是限定类型。C99标准新增了第3个限定符:restrict,用于提高编译器优化。C11标准新增了第4个限定符:_Atomic。

C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。

C99为类型限定符增加了一个新属性:它们现在是幂等的!这个属性听起来很强大,其实意思是可以在一条声明中多次使用同一个限定符,多余的限定符将会被忽略:

const const const int n=6;//与const n=6;相同

1.3.1 const类型限定符

以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。在ANSI兼容的编译器中,以下代码:

const int nochange;//限定nochange的值不能被修改
nochange=12;//不允许

编译器会报错。但是,可以初始化const变量。因此,下面的代码没问题:

const int nochange=12;//没问题

该声明让nochange成为只读变量,初始化后,就不能再改变它的值。

1.在指针中使用const
简而言之,const放在✳左侧任意位置,限定了指针指向的数据不能改变;const放在✳的右侧,限定了指针本身不能改变。

2.对全局数据使用const

在文件间共享const数据要小心。可以采用两种策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其它文件中使用引用式声明:

//file1.c--定义了一些外部const变量
const double PI=3.14159;
const char* MONTHS[12]={"January","February","March","April","May","June","July","August","September","October","November","December"};

//file2.c--使用定义在别处的外部const变量
extern const double PI;
extern const *MONTH[]; 

另一种方案是,把const变量放在一个头文件中,然后在其它文件中包含该头文件:

//constant.h--定义了一些外部const变量
static const double PI=3.14159;
static const char* MONTHS[12]={"January","February","March","April","May","June","July","August","September","October","November","December"};

//file1.c--使用定义在别处的外部const变量
#include"constant.h"

//file2.c--使用定义在别处的外部const变量
#include"constant.h"

这种方案必须在头文件中用关键字static声明全局const变量。如果去掉static,那么在file1.c和file2.c中包含constant.h将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样做(然而,有些编译器允许)。实际上,这种方案相当于给每个文件提供了一个单独的数据副本。由于每个副本只对该文件可见,所以无法用这些数据和其它文件通信。不过没关系,它们都是完全相同(每个文件都包含相同的头文件)的const数据,这不是问题。
头文件方案的好处是,方便你偷懒,不用惦记着在一个文件中使用定义式声明,在其它文件中使用引用式声明。所有的文件都只需包含一个头文件即可。但它的缺点是,数据重复的,对于前面的例子而言,这不算什么问题,但是如果const数据包含庞大的数组,就不能视而不见了。

1.3.2 volatile类型限定符

volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其它程序或同时运行的线程中共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而变化。或者一个地址用于接受另一台计算机传入的信息。

volatile int loc1;//loc1是一个易变的位置
volatile int *ploc;//ploc是一个指向易变的位置的指针

以上代码把loc1声明为volatile变量,把ploc声明为指向volatile变量的指针。
读者可能认为volatile是个可有可无的概念,为何ANSI委员把volatile关键字放入标准?原因是它涉及编译器的优化。例如,假设有下面的代码:

val1=x;
//一些不使用x的代码
val2=x;

智能的(进行优化的)编译器会注意到以上代码使用了两次x,但并未改变它的值。于是编译器把x的值临时储存在寄存器中,然后在val2需要使用x时,才从寄存器中(而不是原始内存位置上)读取x的值,以节约时间,这个过程被称为高速缓存。通常,高速缓存是个不错的优化方案,但是如果一些其它代理在以上两条语句之间改变了x的值,就不能这样优化了。如果没有volatile关键字,编译器就不知道这种事情是否会发生。因此,为安全起见,编译器不会进行高速缓存,这是在ANSI之前的情况。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。
可以同时使用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要,如下所示:

volatile const int loc;
const volatile int * ploc;

1.3.3 restrict类型限定

restrict关键字允许编译器优化某部分代码以更好地支持运算。它只能用于指针,表明指针是访问数据对象的唯一且初始的方式。要弄明白为什么这样做有用,先看几个例子,考虑下面代码:

int ar[10];
int * restrict restar=(int *)malloc(10*sizeof(int));
int *par=ar;

这里,指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict。

现在考虑下面稍微复杂的例子,其中n是int类型:

for(n=0;n<10;n++)
{
	par[n]+=5;
	restar[n]+=5;
	ar[n]*=2;
	par[n]+=3;
	restar[n]+=3;
}

由于之前声明了restar是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及restar的两条语句替换成下面这条语句,效果相同:

restar[n]+=8;//可以进行替换

但是如果把与par相关的两条语句替换成下面的语句,将导致计算错误:

par[n]+=8;//给出错误的结果

这是因为for循环在par两次访问相同的数据之间,用ar改变了该数据的值。

在本例中,如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其它的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。

restrict限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其它标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。例如,C库有两个函数用于把一个位置上的字节拷贝到另一个位置。在C99中,这两个函数的原型是:

void * memcpy(void * restrict s1,const void * restrict s2,size_t n);
void * memmove(void * s1,const void *s2,size_t n);

这两个函数都从位置s2把n字节拷贝到位置s1。memcpy()函数要求两个位置不重叠,但是memmove()没有这样的要求。声明s1和s2为restrict说明这两个指针都是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足memcpy()无重叠的要求。

restrict关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。

1.3.4 _Atomic类型限定符(C11)

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

int hogs;//普通声明
hogs=12;//普通赋值

//可以替换成:
_Atomic int hogs;//hogs是一个原子类型的变量
atomic_store(&hogs,12);//stdatomic.h中的宏

这里,在hogs中储存12是一个原子过程,其它线程不能访问hogs。编写这种代码的前提是,编译器要支持这一新特性。

1.3.5 旧关键字的新位置

C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中。对于类型限定符而言,这样做为现有功能提供了一个替代的语法。
例如,下面是旧式语法的声明:

void ofmouth(int * const al,int *restrict a2,int n);//以前的风格

新的等价语法如下:

void ofmounth(int a1[const],int a2[restrict],int n);//C99允许

根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。
static的情况不同,因为新标准为static引入了一种与以前用法不相关的新用法。现在,static除了表明静态存储类别变量的作用域或链接外,新的用法告知编译器如何使用形式参数。例如,考虑下面的原型:

double stick(double ar[static 20]);

static的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
C语言中,指针是一种非常重要的数据类型,用于存储内存地址。通过指针,程序可以访问和操作内存中的数据。指针在C语言中有着广泛的应用,包括动态内存分配、数组和函数调用等方面。 指针的使用使得程序员能够更直接地操作内存,但也带来了内存管理的责任。C语言中的内存管理是程序员需要关注的一个重要方面[1]。在C语言中,内存的分配和释放需要手动进行。如果不正确地管理内存,就容易出现内存泄漏、野指针等问题,导致程序崩溃或出现难以调试的错误。 动态内存分配是指在程序运行时根据需要分配内存空间。C语言提供了一些函数来实现动态内存分配,例如malloc、calloc和realloc函数。这些函数允许程序在运行时动态地请求所需的内存空间。 使用动态内存分配时,程序员需要负责在不再需要使用内存时手动释放已分配的内存空间,以免造成内存泄漏。释放内存的函数是free函数,通过调用free函数可以将先前分配的内存空间释放回系统。 除了动态内存分配外,C语言中还有一些其他的内存管理技术。例如,对于大型数据结构或数组,可以使用指针来减少内存占用和提高程序的效率。此外,C语言中还有一些规则和约定来确保内存的正确使用,如避免野指针、空指针和越界访问等。 综上所述,C语言中的指针和内存管理密切相关。指针使程序能够直接操作内存,但也需要程序员正确地管理内存的分配和释放。通过动态内存分配和其他内存管理技术,可以有效地利用和管理内存,提高程序的性能和稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Poetry _Distance

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值