第12章:存储类别、链接和内存管理

  • #1. 存储类别
    • 1.1 作用域
    • 1.2 链接
    • 1.3 存储期
    • 1.4 自动变量
    • 1.5 寄存器变量
    • 1.6 块作用域的静态变量
    • 1.7 外部链接的静态变量
    • 1.8 内部链接的静态变量
    • 1.9 多文件
    • 1.10 存储类别说明符
    • 1.11 存储类别和函数
    • 1.12 存储类别的选择
  • #2. 掷骰子
  • #3. 分配内存:malloc()和free()

#1. 存储类别

C提供了多种不同的模型或存储类别在内存中储存数据。从硬件方面来看,被存储的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象

1.1 作用域

作用域描述程序中可访问标识符的区域。一个C变量的作用域可以是块作用域函数作用域函数原型作用域文件作用域

块作用域

块是一对花括号括起来的代码区域。定义在块中的变量具有块作用域(block scope),块作用域变量的可见范围是从定义初到包含该定义的块的末尾。

//变量cleo和patrick均具有块作用域
double blocky(double cleo) {
    double patrick = 0.0;
    ...
    return patrick;
}
函数作用域

函数作用域(function scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。

函数原型作用域

函数原型作用域用于函数原型中的形参名(变量名),如下所示:

int mighty(int mouse,double large);

函数原型作用域(function prototype scope)的范围从形参定义处到原型声明结束。这意味着,编译器在处理函数原型中的形参时只关心它的类型,而形参名(如果有的话)通常无关紧要。

文件作用域

变量的定义在函数的外面,具有文件作用域(file scope)。具有文件作用域的变量,从它的定义处到该定义所在的文件末尾均可见。

#include <stdio.h>

int units = 0;/*该变量具有文件作用域*/
int main(void) {
    ...
}
void critic(void) {
    ...
}

变量units具有文件作用域,main()和critic()函数都可以使用它(更准确的说,units具有外部链接文件作用域)。由于这样的变量可作用于多个函数,所以文件作用域变量也称为全局变量。

标识符作用域示例:
1——>int a;/*文件作用域*/
/*2具有文件作用域*/
2——>int b(3——>int c/*原型作用域*/);
4——>int d(5——>int e/*块作用域*/);/*函数定义的形式参数e在函数体内部也具有代码块作用域*/
{
    6——>int f;/*块作用域*/
    7——>int g(8——>int h/*原型作用域*/);
    ...
    {
        //如果内层代码块有一个标识符的名字与外层代码块的一个标识符同名,内层的那个标识
        //符就将隐藏外层的标识符——外层的那个标识符无法在内层代码块中通过名字访问。
        9——>int f,g,i;/*块作用域*/ 
        //声明9的f和声明6的f是不同的变量,后者无法在内层代码块中通过名字来访问。
        ...
    }
    {
        10——>int i;/*块作用域*/
        ...
    }
}

1.2 链接

C变量有3种链接属性:外部链接内部链接无链接。具有块作用域、函数作用域和函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。

==正式和非正式术语==

C标准用“内部链接的文件作用域”描述仅限于一个翻译单元(即一个源代码文件和它所包含的头文件)的作用域,用“外部链接的文件作用域”描述可延伸至其他翻译单元的作用域。但是,对程序员而言,“内部链接的文件作用域”可以简称为“文件作用域”,把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”。

1.3 存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生命周期。C对象有4种存储期:静态存储期线程存储期自动存储期动态分配存储期

静态存储期

如果对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期。注意,对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域变量具有内部链接属性。但是无论是内部链接还是外部链接,所有的文件作用域变量具有静态存储期。

线程存储期

线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明到线程结束一直存在。

自动存储期

块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。

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

1.4 自动变量

属于自动存储类别的变量具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。

int main(void) {
    auto int plox;//关键字auto是存储类别说明符。
}

auto关键字在C++中的用法完全不同, 如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符。

块作用域和无链接意味着只有在变量定义所在块中才能通过变量名访问改变变量。

#include <stdio.h>

int main(void) {
    int x = 30;//原始的x
    printf("x in outer block: %d at %p\n",x,&x);
    {
        int x = 77;//新的x,隐藏了原始的x
        printf("x in inner block: %d at %p\n",x,&x);
    }
    printf("x in outer block: %d at %p\n",x,&x);
    while (x++<33)//原始的x
    {
        int x = 100;//新的x,隐藏了原始的x
        x++;
        printf("x in while loop: %d at %p\n",x,&x);
    }
    printf("x in outer block :%d at %p\n",x,&x);

    getchar();
    return 0;
}

执行结果如下:
x in outer block: 30 at 0044FD90
x in inner block: 77 at 0044FD84
x in outer block: 30 at 0044FD90
x in while loop: 101 at 0044FD78
x in while loop: 101 at 0044FD78
x in while loop: 101 at 0044FD78
x in outer block :34 at 0044FD90
1. 没有花括号的块

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

int main(void) {
    int n = 8;
    printf("Initially,n = %d at %p\n", n, &n);
    for (int n = 1; n < 3; n++)
        printf("    loop1 : n = %d at %p\n", n, &n);
    printf("After loop1,n = %d at %p\n", n, &n);
    for (int n = 1; n < 3; n++)
    {
        printf("loop2 index n = %d at %p\n", n, &n);
        int n = 6;
        printf("    loop2:n = %d at %p\n", n, &n);
        n++;
    }
    printf("After loop2,n = %d at %p\n", n, &n);

    getchar();
    return 0;
}
//执行结果
Initially,n = 8 at 0044F9D0
        loop1 : n = 1 at 0044F9C4
        loop1 : n = 2 at 0044F9C4
After loop1,n = 8 at 0044F9D0
loop2 index n = 1 at 0044F9B8
        loop2:n = 6 at 0044F9AC
loop2 index n = 2 at 0044F9B8
        loop2:n = 6 at 0044F9AC
After loop2,n = 8 at 0044F9D0
2. 自动变量的初始化

自动变量不会初始化,除非显示初始化它。

int main(void) {
    int rapid; //rapid变量的值是之前分配给rapid的空间中的任意值(如果有的话),别指望这个值是0。
    int tents = 5;
    printf("rapid = %d at %p\n",rapid,&rapid);
    printf("tents = %d at %p\n",tents,&tents);

    getchar();
    return 0;
}

执行结果:    
rapid = -858993460 at 003CFBDC
tents = 5 at 003CFBD0

1.5 寄存器变量

变量通常储存在计算机的内存中。如果幸运的话,寄存器变量存储在cpu的寄存器中。==不能对寄存器变量使用地址运算符。==

void macho(register int n);

int main(void) {
    register int a = 5;
    printf("register var a = %d at %p\n",a,&a);

    getchar();
    return 0;
}
1>c:\users\hhu\source\repos\cpptest\cpptest\test.c(54): error C2103: '&' on register variable

1.6 块作用域的静态变量

静态变量中静态的意思是该变量在内存中原地不动,并不是说它的值不变。具有文件作用域的变量自动具有(也必须是)静态存储期。可以创建静态存储期、块作用域的局部变量。这些变量和自动变量一样,具有相同的作用域,但是程序离开它们所在的函数后,这些变量不会消失。这种变量具有块作用域、无链接,但是具有静态存储期。“静态局部变量”是描述具有块作用域的静态变量的另一个术语。

1.7 外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别,属于该类别的变量称为外部变量。

int errupt;/*外部定义的变量*/
double up[100];/*外部定义的数组*/
extern char coal;/*如果coal被定义在另一个文件,则必须这样声明*/

void next(void);

int main(void) {
    extern int errupt; /*可选的声明*/
    extern double up[]; /*可选的声明*/
    ...
}

void next(void) {
    ...
}

在main()中声明up数组时不用指明数组的大小,因为第1次声明已经提供了数组大小信息。main()中的两条extern声明完全可以省略,因为外部变量具有文件作用域。

1.初始化外部变量

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

int x = 10;
int y = 3 + 20;
size_t z = sizeof(int);
int x2 = 2 * x;//x是变量
2.使用外部变量
#include <stdio.h>

int units = 0;
void critic(void);

int main(void) {
    extern int units;
    printf("How many pounds to a firkin of butter?\n");
    scanf("%d",&units);
    while(units != 56) {
        critic();
    }
    printf("You must have looked it up!\n");
    return 0;
}

void critic(void) {
    printf("No luck, my friend.Try again.\n");
    scanf("%d",&units);
}
3. 外部名称

C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。这修订了以前的标准,即编译器识别局部变量标识符前31个字符和外部标识符前6个字符。

4.定义和声明
int tern = 1;/*ternb被定义*/
void main(void) {
    extern int tern;/*使用在别处定义的tern*/
}

tern被声明了两次。第1次声明为变量预留了存储空间,该声明构成了变量的定义。第2次声明只告诉编译器使用之前已创建的tern变量,所以这不是定义。第1次声明被称为定义式声明,第2次声明被称为引用式声明

1.8 内部链接的静态变量

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

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

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

int traveler = 1;//外部链接
static int stayhome = 1;//内部链接

int main(void) {
    extern int traveler;//使用定义在别处的traveler
    extern int stayhome;//使用定义在别处的stayhome
    ...
    return 0;
}

1.9 多文件

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

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

1.10 存储类别说明符

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

  • auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。
  • register说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。
  • static说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块。
  • extern说明符表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。

PartA.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void report_count();
void accumulate(int k);
int count = 0;//文件作用域,外部链接

int main(void) {
    int value;
    register int i;
    printf("Enter a positive integer (0 to quit):");
    while (scanf("%d", &value) == 1 && value > 0)
    {
        ++count;
        for (i = value; i >= 0; i--)
        {
            accumulate(i);
        }
        printf("Enter a positive integer (0 to quit): ");
    }
    report_count();
    return 0;
}

void report_count() {
    printf("Loop executed %d times\n",count);
}

PartB.c

#include<stdio.h>

extern int count;//引用式声明

static int total = 0;//静态定义,内部链接
void accumulate(int k);//函数原型

void accumulate(int k) {//k具有块作用域,无连接
    static int subtotal = 0;//静态,无链接
    if (k <= 0)
    {
        printf("loop cycle: %d\n",count);
        printf("subtotal: %d;total: %d\n",subtotal,total);
        subtotal = 0;
    }
    else {
        subtotal += k;
    }
    total += k;
}

1.11 存储类别和函数

函数也有存储类别,可以是外部函数(默认)或静态函数。C99新增了第3种类别——内联函数。外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。

double gamma(double);/*该函数默认为外部函数*/
static double beta(int,int);/*该函数为模块私有*/
extern double delta(double,int);/*该函数定义在其他文件中*/

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

1.12 存储类别的选择

保护性程序设计的黄金法则是:“按需知道”原则。尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。


#2. 掷骰子

dicetoll.h

#pragma once
extern int roll_count;
int roll_n_dice(int dice,int sides);

dicetoll.c

#include<stdio.h>
#include<stdlib.h>
#include "dicetoll.h"

int roll_count = 0;

static int rollem(int sides) {
    int roll;
    roll = rand() % sides + 1;
    ++roll_count;
    return roll;
}

int roll_n_dice(int dice, int sides) {
    int d;
    int total = 0;
    if (sides < 2)
    {
        printf("Need at least 2 sides.\n");
        return -2;
    }
    if (dice < 1)
    {
        printf("Need at least 1 die.\n");
        return -1;
    }
    for (d = 0; d < dice; d++)
    {
        total += rollem(sides);
    }
    return total;
}

manydice.c

#define _CRT_SECURE_NO_WARNINGS

#include <stdio.h>
#include <stdlib.h>/*为库函数srand()提供原型*/
#include <time.h>/*为time()提供原型*/
#include "dicetoll.h"/*为roll_n_dice()提供原型,为roll_count变量提供声明*/

int main(void) {
    int dice, roll;
    int sides;
    int status;
    srand(time(0));/*随机种子*/
    printf("Enter the number of sides per die,0 to top.\n");
    while (scanf("%d", &sides) == 1 && sides > 0)
    {
        printf("How many dice?\n");
        if ((status = scanf("%d", &dice)) != 1)
        {
            if (status = EOF)
            {
                break;
            }
            else {
                printf("Yout should have entered an integer.");
                printf("Let's begin again.\n");
                while (getchar() != '\n')
                {
                    continue;
                }
                printf("How many sides?Enter 0 to stop.\n");
                continue;
            }
        }
        roll = roll_n_dice(dice, sides);
        printf("You have rolled a %d using %d %d-sided dice.\n", roll, dice, sides);
        printf("How many sides?Enter 0 to stop.\n");
    }
    printf("The rollem() function was called %d times.\n", roll_count);
    printf("Good fortune to u.");
}

#3 分配内存:malloc()和free()

C函数库提供了两个函数,malloc和free,分别用于执行动态内存的分配和释放。这些函数维护一个可用内存池。

void *malloc(size_t size);
void free(void *pointer);
  • malloc的参数就是需要分配的字节数。如果内存池中的可用内存可用满足这个需求,malloc就返回一个指向被分配的内存块起始位置的指针。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。
  • free的参数必须要么是NULL,要么是一个先前从malloc、calloc或realloc返回的值。向free传递一个NULL参数不产生任何效果。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *ptd;
    int max;
    int number;
    int i = 0;
    srand(time(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 = (int *)malloc(max * sizeof(int));
    if (ptd = NULL)
    {
        puts("Memory allocation failed.Goodbye.");
        exit(EXIT_FAILURE);
    }
    puts("Enter the values (q to quit):");
    while (i < max && scanf("%d",&ptd[i])) {
        ++i;
    }
    printf("Here are your %d entries:\n",number = i);
    for (i = 0; i < number; i++)
    {
        printf("%d ", ptd[i]);
        if (i % 7 == 6)
            putchar('\n');
    }
    if (i % 7 != 0)
        putchar('\n');
    puts("Done.");

    free(ptd);
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350

推荐阅读更多精彩内容