第12章 存储类别、链接和内存管理(3)

发布于 2021-09-18  54 次阅读


12.4 分配内存:malloc()和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()分配内存失败,将返回空指针。
试着用malloc()创建一个数组。除了用malloc()在程序运行时请求一块内存,还需要一个指针记录这块内存的位置。
double *ptd;
ptd = (double *)malloc(30*sizeof(double));
以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置。指针ptd被声明为一个指向double类型,而不是指向30个double类型值的块,数组名是该数组首元素的地址。因此,如果让ptd指向这个元素的首元素,便可以像使用数组名一样使用它。ptd [0]表示首元素。。可以用数组名来表示指针,也可以用指针表示数组。
有3种创建数组的办法:
1. 声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态内存或自动内存创建这种数组。
2. 声明变长数组(C99新增加的特性)时,用变量表达式表示数组的维度,用数组名访问数组的元素。具有这种特性的数组只能在自动内存中创建。
3. 声明一个指针,调用malloc(),将其返回赋值给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。
使用第2种和第3种方法可以创建动态数组(dynamic array)。这种数组和普通数组不同,可以在程序运行时选择数组的大小和分配内存。假设n是一个整型变量。在C99之前。不能这样做:
double item[n];     //C99之前:n不允许是变量
但是,可以这样做:
ptd = (double *)malloc(n*sizeof(double));
    // 可以
这比变长数组更加灵活
通常。malloc()要与free()配套使用。free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。设想malloc()和free()管理着一个内存池。每次调用malloc()分配内存给程序使用,每次调用free()把内存归还内存池中,这样便可重复使用这些内存。free()的参数应该是一个指针,指向由malloc()分配的一块的内存。不能用free()释放通过其他方式分配的内存。
malloc()和free()的原型都在stdlib.h头文件中包含。
使用malloc(),程序可以在运行时才确定数组的大小。如下程序,他把内存块的地址赋给指针ptd,然后便可以使用数组名的方式使用ptd。另外,如果内存分配失败,可以调用exit()函数结束升序,其原形在stdlib.h中。EXIT_FALLURE的值也被定义在stdlib.h中。提供了两个返回值以保证在所有操作系统中都可以正常工作。EXIT_SUCCESS(或者相当于0)表示普通的程序结束,EXIT_FAILURE表示程序异常中止。一些操作系统(UNIX、Linux、和windows)还接受一些表示其他运行错误的整数值。
//dyn_arr.c --动态分配数组
#include<stdio.h>
#include<stdlib.h>    //为malloc()和free()提供原型

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 values (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("Done!");
    free(ptd);

    return 0;

}

file

max控制数组大小,虽然输入了6个数,但是只是处理了5个数。

file

使用动态数组有什么好处?
使用动态数组可给程序带来更多的灵活性。假设你已经知道,在大多数情况下程序所使用的数组都不超过100个元素,但是有时程序确实需要100000个元素的数组。基本上这样做是在浪费内存。如果需要100001个元素,这个程序就会出错。这种情况下,可以使用一个动态数组调整程序以适应不同的情况。

12.4.1 free()的重要性

静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非free()进行释放。假设,有一个创建数组临时副本的函数,其代码框架如下:
...
int main()
{
    double glad[2000];
    int i;
    ...
    for(i = 0;i < 1000;i++)
        gobble(goad,2000);
    ...
}
void gobble(double ar[],int n)
{
    double *temp = (double *)malloc(n*sizeof(double));
    ... //free(temp);假设忘记使用free()
}
第一次调用gobble()时,他创建了指针temp,并且调用分配了16000字节的内存。假设如代码所示忘记了free()。当函数结束时候,作为自动变量的指针temp也会消失。但是它指的16000字节的内存却依旧存在。由于temp指针已被销毁,所以无法访问这块内存,它也不能被重复使用,因为代码中没有调用free()释放这块内存。

12.4.2 calloc()函数

分配内存还可以使用calloc()函数。
long *newmem;
mewmen = (long *)calloc(100,sizeof(long));

file

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

变长数组(VLA)和调用malloc()在功能上有些重合。例如,两者都可以用于创建在运行时确定大小的数组:
int vlamal()
{
    int n;
    int *pi;
    scanf("%d",&n);
    pi = (int *)malloc(n * sizeof(int));
    int ar[n];  //变长数组
    pi[2] = ar[2] = -5;
    ...
}
不同的是,变长数组是自动存储类型。因此,程序在离开变长数组定义所在的块。变长数组会被自动释放,不必使用free().
另一方面,用malloc()创建的数组不必局限在一个函数内访问。例如,可以这样做:别调函数创建了一个数组并返回指针,供主调函数访问,然后主调函数在末尾调用free()释放之前被调函数分配的内存。另外,free()所用的指针变量可以与malloc()指针变量不同,但是两个指针必须储存相同的地址。但是。不能释放一块内存两次。
对多维数组而言,使用变长数组更方便。当然,也可以用malloc()创建二维数组,但是语法比较繁琐。如果编译器不支持变长数组特性,就只能固定二维数组的维度如下所示:
int n = 5;
int m = 6;
int ar2[n][m];
int (*p2)[6];
int (*p3)[m];
p2 = (int (*)[6])malloc(n*6*sizeof(int));
p3 = (int (*)[m])malloc(n*m*sizeof(int));
ar2[1][2] = p2[1][2] =12;
由于malloc()函数返回一个指针,所以p2必须是一个指向合适类型的指针。第1歌指针声明:
int (*p2)[6];
表明p2指向一个内含6个int类型值的数组。因此,p2[i]代表一个由6个整数构成的元素,p2[i][j]代表一个整数。
第2个指针声明用一个变量指定p3所指向数组的大小。因此,p3代表一个指向变长数组指针,这行代码不能在C90标准中运行。

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

存储类别和动态内存分配有何联系?
一个理想化模型:可以认为程序把它可用的内存分为3部分:一部分具有外部链接、内部链接和无连接的静态变量使用;一部分供动态内存分配。
静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可以访问储存在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。
然而,自动存储类别的变量在程序进入变量定义的块时存在。在程序离开块时消失。因此,随者程序调用函数和函数结束,自动变量所用的内存数量也相应的增加和减少。这部分的内存通常作为栈来处理。这意味着创建的变量按顺序加入内存,然后以相反的顺序销毁。
动态内存的在调用malloc()函数或相关函数时存在,在调用free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正因为这样,这部分内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间,另外,使用动态内存通常比使用栈内存慢。
总而言之,程序把静态对象、自动对象和动态分配的对象存储在不同的区域。
//where.c   --数据被存储在何处?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

int static_store = 30;
const char *pcg = "String Literal";
int main(void)
{
    int auto_store = 40;
    char auto_string[] = "Auto char array";
    int *pi;
    char *pc1;

    pi = (int *)malloc(sizeof(int));
    *pi = 35;
    pc1 = (char *)malloc(strlen("Dynamic String")+1);
    strcpy(pc1,"Dynamic String");

    printf("static_store:%d at %p\n",static_store,&static_store);
    printf("auto_store:%d at %p\n",auto_store,&auto_store);
    printf("   *pi:%p at %p\n",*pi,pi);
    printf("  %s at %p\n",pcg,pcg);
    printf("  %s at %p\n",auto_string,auto_string);
    printf("  %s at %p\n",pc1,pc1);
    printf("  %s at %p\n","Quoted String","Quoted String");
    free(pi);
    free(pc1);

    return 0;
}

file

如上所示:静态数据(包括字符串字面量)占用一个区域,动态内存占用另外一个区域,动态分配数据占用第3个区域(通常被称为内存堆或自由内存)。

12.5 ANSI C类型限定符

我们通常用类型和存储类别来描述一个变量。C90还增加了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const和valatile来声明,以这两个关键字创建的类型是限定类型(qualifed type)。C99标准新增了第3个限定符:restrict,用于提高编译器优化,C11标准新增加了第4个限定符:\_Atomic.C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而却\_Atomic是可选支持项。
C99为类型限定符增加了一个新的属性:它们现在是幂等的(idempotent)!这个属性听起来很强大,其实意思是可以在一条声明中多次使用同一个限定符,多余的限定符被忽略:
const const const int n = 6;    //与const int n = 6;相同
有了这个新的属性,就可以编写类似下面的代码:
typedef const int zip;
const zip q = 8;

12.5.1 const 类型限定符

以const关键字声明的对象。其值不能通过赋值或递增、递减来修改。在ANSI兼容的编译器中,以下代码:
const int nochange; //限定符nochang的值不能被修改
nochange = 12;  //不允许
编译器会报错。但是,可以初始化const变量。因此下面代码没问题:
const int nochange = 12;    //没问题
该声明让 nochange成为只读变量。初始化后,就不能再改变它的值。
可以让const 关键字创建不允许修改的数组:
const int days1[12] = {31,28,31,30,31,31,30,31,31,30,30,31};

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

声明普通变量和数组时使用const关键字很简单。指针则复杂一些,因为要区分是限定指针本身为const还是限定指针指向的值为const。下面声明:
const float *pf;    //pf指向一个float类型的const值
创建了pf指向的值不能被改变,而pt本身的值可以改变,例如,可以设置该指针指向其他const值。相比之下,下面的声明:
float * const pt;   //pt是一个const指针
创建的指针pt本身的值不能更改。pt必须指向同一个地址,但是它所指向的值可以改变。下面的声明:
const float *const ptr;
表明ptr既不能指向别处,它所指向的值也不能改变。
还可以把const放在第3个位置:
float const *ptc;   //与const float *ptc;相同
把const 放在类型名之后、*之前,说明该指针不能用于改变它所指向的值。简而言之,const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。
const关键字的常见用法是声明为函数形参的指针。例如,假设有一个函数要调用display()显示一个数组的内容。要把数组名作为实际参数传递给该函数,但是数组名是一个地址。该函数可能会更改主调函数中的数据,但是下面的原型保证了数据不会被更改。
void display(const int array[], int limit);
在函数原型和函数头,形参声明const int array[]与const int *array相同,所以该声明表明不能更改array指向的数据。
ANSI C库遵循这种做法。如果一个指针仅用于该函数访问值,应将其声明为一个指向const限定类型的指针。如果要用指针更改主调函数中的数据,就不能使用const关键字。例如,ANSIC中的strcat()原型如下。
char *strcat(char *restrict s1,const char *restrict s2);
strcat()函数在第1个字符串的末尾添加第2个字符串的副本。这更改了第1个字符串,但是未更改第2个字符串。

2.对全局数据使用const

使用全局变量是一种冒险的方法,因为这样做暴露了数据,程序的任何部分数据都能更改数据。
如果把数据设置为const,就可避免这样的危险,因此使用const限定符声明全局数据很合理。可以创建const 变量、const 数组和const结构(结构是一种复合数据类型)。
然而,在文件间共享const数据要小心。可以采用两个策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义声明,其他文件中使用引用式声明(用extern关键字):
//file1.c   --定义了一些外部const变量
const double PI = 3.14159;
const char *MONTHS = {"January","February","March","April","May","June","July","August","September","October","November","December"};
//file2.c   --使用定义在别处的外部const变量
extern const double PI;
extern const *MONTHS[];
//另一种方案是,把const变量放在一个头文件中,然后再其他文件中包含该头文件:
//constant.h    --定义了一些外部const变量
static const double PI = 3.14159;
static const char *MONTHS = {"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数据包含庞大的数组,就不能视而不见了。

12.5.2 volatile类型限定符

volatile限定符告知计算机,代理(而不是变量所在程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如:一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随着时间的改变而改变。或者一个地址用于接受另一台计算机传入的信息。
volatile的语法和const一样
volatile int locl;  //locl是一个易变的位置
volatile int *ploc; //ploc是一个指向易变的位置的指针
以上代码把locl声明为volatile变量,把ploc声明为指向volatile变量的指针。
为何ANSI委员把volatile关键字放入标准?原因是它设计编译器的优化。例如,有如下代码:
vall = x;
//一些不使用x的代码
val2 = x;
(进行优化的)编译器会注意到以上代码使用了两次x,但并未改变它的值。于是编译器把x的值临时存在寄存器中,然后再val2需要使用x时,才从寄存器(而不是从原始内存位置上)读取x的值,以节约时间。这个过程被称为告诉缓存(caching)。通常,告诉缓冲是个不错的优化方案,但是如果一些其他代理在以上两条语句间改了x的值,编译器就不会进行告诉缓存。这是ANSI之前的情况。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用的过程不变,然后再尝试优化代码。
可以同时用const和volatile限定一个值。例如,通常用const把硬件始时钟设置为程序不能更改的变量,但是通过代理可以更改,这时用volatile,只能再声明中同时使用这两个限定符,它们的顺序不重要,如下所示:
volatile const int loc;
const volatile int *ploc;

12.5.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库由两个函数用于把一个位置上的字节拷贝到另一个位置,
void *memcpy(void *restrict s1,const void *restrict s2,size_n);
void *memmove(void s1,const void *s2,size_t n);
这两个函数都从位置s2把n字节拷贝到位置s1.memcpy()函数要求两个位置不重叠,但是memove()没有这样的要求。声明s1和s2为restrict说明这两个指针都是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足了memcpy()无重叠的要求。memmove()函数允许重叠,它在拷贝数据时不得不更小心,以防在使用函数之前就先覆盖了数据,
restrict关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果字符。

12.5.4 _ATOMIC 类型限定符(C11)& 12.5.5 旧关键字的新位置

file

file

12.6 关键概念

C提供多种管理内存的模型/除了熟悉这些模型外,还要学会如何选择不同的类别。在大多数情况下,最好的选择是自动变量。如果要使用其他类别,应该有充分的理由。通常,使用自动变量、函数形参和返回值进行函数间的通信比使用全局变量安全。但是,保持不变的数据适合用全局变量。
应尽量理解静态内存、自动内存和动态分配内存的属性。
静态内存的数量在编译器时确定;静态数据在载入程序时被载入内存。在程序运行时,自动变量被分配或释放,所以自动变量占用的内存数量随者程序的运行会不断的变化。可以把自动内存看作是可重复的工作区。动态分配的内存也会增加和减少,但是这个过程由函数调用控制,不是自动进行的。

12.7 本章小结

内存用于存储程序中的数据,由存储期、作用域和链接表示。存储期可以是静态的、自动或动态分配的。如果是静态存储期,在程序开始执行时分配内存,并在程序运行时都在。如果是自动存储期,在程序进入变量定义所在块时分配变量的内存,在程序离开时释放内存,如果是动态分配存储期,在调用malloc()时分配内存,在调用free()函数时释放内存。
作用域决定程序的哪些部分可以访问某数据。定义在所有函数之外的变量具有文件作用域,对位于该变量声明之后的所有函数可见。定义在块或作为函数形参内的变量具有块作用域,只对该块以及它包含的嵌套块可见。
链接描述定义在程序某翻译单元中的变量可以被链接的程度。具有块作用域的变量是局部变量,无链接。具有文件作用域的变量可以是内部链接或外部链接。内部链接意味者只有其定义所在文件才能使用该变量。外部链接意味者其他文件也可以使用该变量。
自动寄存器静态、无链接静态、外部链接静态、内部链接
在块中不带存储类别说明符或者带auto存储类别说明符声明的变量(或作为函数头中的形参)属于自动存储类别,具有自动存储期、块作用域、无链接。如果未初始化自动变量,它的值是未定义的
在块中带register存储类别说明符声明的变量(或作为函数头中的形参)属于寄存器存储类别,具有自动存储期、块作用域、无链接,且无法获取其地址。把一个变量声明未寄存器变量即请求编译器将其存储到访问速度最快的区域。如果未初始化寄存器变量,它的值是未定义的。
在块中带static存储类别说明符声明的变量属于“静态、无链接”存储类别,具有静态存储期、块作用域、无链接。只在编译时被初始一次。如果未显示初始化,它的字节都设置为0.
在所有函数外部且都没有使用static存储类别说明符声明的变量属于“静态、外部链接”存储类别,具有静态存储期、文件作用域、外部链接。只能在编译器初始被初始化一次。如果未显示初始化,它的字节都设置为0.
在所有函数外部且使用了static存储类别说明符的变量属于“静态、内部链接”存储类别,具有静态存储期、文件作用域、内部链接。只能在编译器被初始化一次。如果未显示初始化,它的字节都设置为0.
动态分配的内存由malloc(或相关)函数分配,该函数返回一个指向指定字节数内存块的指针。这块内存被free()函数释放后便可以重复利用,free()函数以该内存块的地址作为参数。

类型限定符const、volatile、restrict和\_Atomic.const限定符限定数据在程序运行时不能更改。对指针使用const时,可根据const在声明中的位置,限定指针所指的值还是指针的地址不能改变,volatile限定符表明,限定的数据除了别当前程序修改以外还可以被其他进程修改。该限定符的目的是井盖编译器不要进行假定的优化。restrict限定符也是为了方便编译器钟设置优化方案。resrict限定符的指针是访问它所指向数据的唯一途径。

擦肩而过的概率