《C Primer Plus(第6版)中文版》第12章 存储类别、链接和内存管理 学习笔记

对标题和序号稍加修改。

12.1 存储类别

C语言提供了多种存储类别(storage class)在内存中储存数据。每个内存块都可以视为一个对象(object)。可以用标识符来访问对象的值,用作用域(scope)和链接(linkage)描述标识符在程序的哪些部分可以被使用,用存储期(storage duration)描述对象在内存中存留的时间。C语言根据变量的作用域、链接与存储期划分出多种存储类别。

12.1.1 作用域

作用域描述程序中可以访问标识符的区域。分为:

  • 块作用域
  • 函数作用域
  • 函数原型作用域
  • 文件作用域

块作用域

块作用域(block scope)变量定义在**{ }**中,可见范围内从定义处到花括号结尾。函数形参也具有块作用域。示例:

/*函数定义*/
double blocky(double cleo)       //celo作用域开始
{
    double patrick = 0.0;        //patrick作用域开始

    for (int i = 0; i < 10; i++) //i作用域开始
    {
        double q = cleo * i;     //q作用域开始
        patrick *= q;
    }                            //i、q作用域结束
    ......
    return patrick;
}                                //cleo、patrick作用域结束

C99以前必须把块作用域变量声明在块的开头,C99以后放宽了这一限制,将块的概念扩充到循环和选择语句所控制的代码。

函数作用域

函数作用域(funciton scope)仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。如果两个块中使用相同的标签会造成混乱,标签的函数作用域防止了类似事情的发生。

函数原型作用域

函数原型作用域(funciton prototype scope)用于原型的形参声明。示例:

double blocky(double cleo);

函数原型作用域范围从形参定义处到原型声明结束处。这意味着,编译器在处理函数原型中的形参时只关心类型,即使有形参名也不必与定义中的形参名一致,只有在变长数组中,形参名才有用(第10章 10.8)。

文件作用域

定义在函数外面的变量具有文件作用域(file scope)。可见范围从定义处到文件末尾。因为此类变量可用于多个函数,所以也称为全局变量(global variable)。示例:

#include 
int units=0;/*具有文件作用域*/
void crttitc(void);
int main(void)
{
	...
}
void crttitc(void)
{
	...
}

12.1.2 链接

C变量具有3中链接属性:

  • 外部链接
  • 内部链接
  • 无链接

外部链接变量可以在多个翻译单元(translation unit,即源代码文件)中使用,内部链接变量只能在其所在的文件中使用,两者都具有文件作用域。具有块作用域、函数作用域、函数原型作用域的变量都是无链接变量,归所在的块或原型私有。使用static说明符声明内部链接变量。示例:

#include 
int outter;		/*文件作用域,外部链接*/
statin inner;	/*文件作用域,内部链接*/
int main(void)
{
	...
	return 0;
}

12.1.3 存储期

存储期描述通过标识符访问的对象的生存期。C对象有4中存储期:

  • 静态存储期
  • 线程存储期
  • 自动存储期
  • 动态分配存储期

静态存储期

静态存储期变量在程序执行期间一直存在,具有文件作用域的变量有静态存储期。程序在编译时为静态存储期变量分配内存,如果未显式初始化则设置值为0。

线程存储期

线程存储期变量用于并发程序设计,在其声明到线程结束期间存在,以_Thread_local关键字声明一个对象时,每个线程都获得该变量的私有备份。

自动存储期

具有自动存存储期的变量,程序在进入该变量声明所在块时才为其分配内存,在退出该块时释放内存。

变长数组的存储期从声明处到块的末尾。

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

在块内用static声明块作用域变量即可创建静态无链接变量。示例:

void more(void)
{
	static int ct = 0;/*ct具有静态存储期、块作用域,作用域开始*/
	...
	
}					  /*ct作用域结束*/

存储类别(重点)

根据作用域、链接和存储期为变量定义了5种基本的存储类别:

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

12.1.4 自动变量

可使用auto关键字显示声明自动存储类别的变量(如果编写C/C++兼容的程序则不建议使用)。块作用域和无链接意味着只有在变量定义所在的块内才能使用变量名直接访问该变量。具有自动存储期意味着程序在进入变量定义的块时变量存在,退出块后变量消失,该变量占用的内存位置可重复使用。如果其他函数有同名变量则不会冲突。但是内层块内有与外层块同名的变量时,内层块会隐藏外层块的定义,在离开内层快后,外层块变量的作用域再起作用。示例:

#include 
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)
    { /*内层块开始*/
        int x = 100; /*while循环的新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);

    return 0;
}

输出结果:

x in outer block: 30 at 000000000061FE1C
x in inner block: 77 at 000000000061FE18
x in outer block: 30 at 000000000061FE1C
x in while loop: 101 at 000000000061FE14
x in while loop: 101 at 000000000061FE14
x in while loop: 101 at 000000000061FE14
x in outer block: 34 at 000000000061FE1C

1. 没有{}的块

C99的特性:整个循环或与if关联的语句是一个子块(sub-block)。示例:

#include 
int main(void)
{
    int n = 8;
    printf("   Initially, n = %d at %p\n", n, &n);
    for (int n = 1; n < 3; n++)
        printf("      loop 1: n = %d at %p\n", n, &n);
    printf("After loop 1, n = %d at %p\n", n, &n);
    for (int n = 1; n < 3; n++)
    {
        printf(" loop 2 index n = %d at %p\n", n, &n);
        int n = 6;
        printf("      loop 2: n = %d at %p\n", n, &n);
        n++;
    }
    printf("After loop 2, n = %d at %p\n", n, &n);
    return 0;
}

输出结果:

   Initially, n = 8 at 000000000061FE1C
      loop 1: n = 1 at 000000000061FE18
      loop 1: n = 2 at 000000000061FE18
After loop 1, n = 8 at 000000000061FE1C
 loop 2 index n = 1 at 000000000061FE14
      loop 2: n = 6 at 000000000061FE10
 loop 2 index n = 2 at 000000000061FE14
      loop 2: n = 6 at 000000000061FE10
After loop 2, n = 8 at 000000000061FE1C

第1个for循环头中声明的n,其作用域作用至循环末尾,而且隐藏了原始的n。但是,离开循环后,原始的n又起作用了。第2个for循环头中声明的n作为循环的索引,隐藏了原始的n。然后,在循环体中又声明了一个n,隐藏了索引n。结束一轮迭代后,声明在循环体中的n消失,循环头使用索引n进行测试。当整个循环结束时,原始的 n 又起作用了。

2. 自动变量的初始化

自动变量不会初始化,除非显式初始化,否则存储的是垃圾值。也可以用非常量表达式(non-constant expression)初始化自动变量,前提是所用的变量在使用前已定义。示例:

int repid;            /*未显式初始化,值是先前分配给repid空间中的值*/
int ruth = 1;         /*显示初始化*/
int rance = ruth * 5; /*非常量表达式初始化*/

12.1.5 寄存器变量

寄存器变量储存在CPU的寄存器中,储存在最快的可用内存中。与普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。寄存器变量具有块作用域、无链接和自动存储期。使用存储类别说明符register便可请求声明寄存器变量,但是编译器会根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,此情况下,寄存器变量就变成普通的自动变量。但是仍然不能对该变量使用地址运算符。在函数头中使用关键字register,便可请求形参是寄存器变量。其次,可声明为寄存器变量的数据类型有限。

12.1.6 静态无链接变量

在块中(提供块作用域和无链接)以存储类别说明符static(提供静态存储期)声明静态无链接变量。可见性为所在块私有,但其本身并不属于所在块,该类别的变量声明与初始化在程序被载入内存时已执行完毕。只在编译时初始化一次,未显式初始化则设置值为0。该类别的变量也被称为内部静态存储类别(internal
static storage class),此处的内部指的是函数内部。 静态无链接变量具有块作用域、无链接和静态存储期。示例:

#include 
void trystat(void);
int main(void)
{
    int count;
    for (count = 1; count <= 3; count++)
    {
        printf("Here comes iteration %d:\n", count);
        trystat();
    }
    return 0;
}
void trystat(void)
{
    int fade = 1;
    static int stay = 1;
    printf("fade = %d and stay = %d\n", fade++, stay++);
}

输出结果:

Here comes iteration 1:
fade = 1 and stay = 1
Here comes iteration 2:
fade = 1 and stay = 2
Here comes iteration 3:
fade = 1 and stay = 3

12.1.7 静态外部链接变量

静态外部链接变量具有文件作用域、外部链接和静态存储期。该类别有时称为外部存储类别(external storage class),该类别的变量称为外部变量(external variable)。把变量的定义性声明(defining declaration)放在在所有函数的外面便创建了外部变量。为了指出该函数或文件使用了外部变量,可以在函数或文件中用关键字extern再次声明。示例:

int Errupt;       /*外部定义的变量*/
double Up[100];   /*外部定义的数组*/
extern char Coal; /*如果Coal被定义在另一个文件,*/
                  /*则必须这样声明*/
void next(void);
int main(void)
{
    extern int Errupt;  /*可选的声明*/
    extern double Up[]; /*可选的声明*/
    ...
}
void next(void)
{...}

在执行块中的语句时,块作用域中的变量将隐藏文件作用域中的同名变量。如果不得已要使用与外部变量同名的局部变量,可以在局部变量的声明中使用 auto 存储类别说明符明确表达这种意图。

1. 初始化外部变量

只在编译时初始化一次,未显式初始化则设置值为0这一原则也适用于外部定义的数组元素。与自动变量的情况不同,只能使用常量表达式初始化文件作用域变量:

int x = 10;             // 没问题,10是常量
int y = 3 + 20;         // 没问题,用于初始化的是常量表达式
size_t z = sizeof(int); //没问题,用于初始化的是常量表达式
int x2 = 2 * x;         // 不行,x是变量

2. 使用外部变量

示例程序:

#include 
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);
}

输出结果:

How many pounds to a firkin of butter?
14
No luck, my friend. Try again.
56
You must have looked it up!

3. 外部名称

C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。这修订了以前的标准,即编译器识别局部标识符前31个字符和外部标识符前6个字符。你所用的编译器可能还执行以前的规则。外部变量名比局部变量名的规则严格,是因为外部变量名还要遵循局部环境规
则,所受的限制更多。

4. 定义和声明

  • 定义式声明(defining declaration):声明变量,分配存储空间,初始化(可选)。
  • 引用式声明(referencing declaration):使用关键字extern引用现有的外部定义,指出该函数或文件要使用这个外部变量,告知编译器查询该变量的定义式声明,任何使用该变量的地方都引用同一个定义。

不能用extern为变量赋值,只有定义式声明可以初始化变量。

12.1.8 静态内部链接变量

静态内部链接变量具有静态存储期、文件作用域和内部链接。用存储类别说明符static定义在所有函数外部。只在编译时初始化一次,未显式初始化则设置值为0。内部链接的静态变量只能用于同一个文件中的函数。可以使用存储类别说明符extern在函数中重复声明静态内部链接变量。这样的声明并不会改变其链接属性。

12.1.9 多文件

C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。

12.1.10 存储类别说明符

C语言有6个关键字作为存储类别说明符:autoregisterstaticextern
_Thread_localtypedef

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

示例程序:

// parta.c --- 不同的存储类别
// 与 partb.c 一起编译
#include 
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 -- 程序的其余部分
// 与 parta.c 一起编译
#include 
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;
    }
}

输出结果:

Enter a positive integer (0 to quit): 5
loop cycle: 1
subtotal: 15; total: 15
Enter a positive integer (0 to quit): 10
loop cycle: 2
subtotal: 55; total: 70
Enter a positive integer (0 to quit): 2
loop cycle: 3
subtotal: 3; total: 73
Enter a positive integer (0 to quit): 0
Loop executed 3 times

12.1.11 存储类别和函数

函数也有存储类别,可以是**外部函数(默认)**或静态函数。C99 新增了第3种类别——内联函数(第16章详述)。外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。假设一个文件中包含了以下函数原型:

double gamma(double);             /*该函数默认为外部函数*/
static double beta(int, int);     /*静态函数*/
extern double delta(double, int); /*引用其他文件中的函数定义*/

在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类别说明符创建的函数属于特定模块私有,这样做避免了名称冲突的问题。用extern关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明都默认为extern

12.2 随机数函数

ANSI C库提供了rand()函数生成随机数。实际上,rand()是“伪随机数生成器”,意思是
可预测生成数字的实际序列。但是,数字在其取值范围内均匀分布。同时,用srand()函数设置种子值让生成的数字显得更随机。两个函数包含在stdlib.h头文件中。

也可以搭配time()函数返回值设置种子值,ANSI C提供time()函数返回系统时间,该返回值是一个可进行运算的类型,而且其值随着时间变化而变化。返回值的类型名是time_t,具体
类型与系统有关,可以强制类型转换为想要的类型。接受的参数是一个time_t类型对象的地址,而时间值就储存在传入的地址上。当然,也可以传入空指针(0)作为参数,这种情况下,只能通过返回值机制来提供值。

12.3 掷骰子

/* diceroll.c -- 掷骰子模拟程序 */
/* 与 mandydice.c 一起编译 */
#include "diceroll.h"
#include 
#include           /* 提供库函数 rand()的原型 */
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;
}
//diceroll.h
extern int roll_count;                /*引用式声明*/
int roll_n_dice(int dice, int sides); /*函数原型*/
/* manydice.c -- 多次掷骰子的模拟程序 */
/* 与 diceroll.c 一起编译*/
#include 
#include    /* 为库函数 srand() 提供原型 */
#include      /* 为 time() 提供原型 */
#include "diceroll.h" /* 为roll_n_dice()提供原型,为roll_count变量
提供声明 */
int main(void)
{
    int dice, roll;
    int sides;
    int status;
    srand((unsigned int)time(0)); /* 随机种子 */
    printf("Enter the number of sides per die, 0 to stop.\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("You 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 YOU!\n");
    return 0;
}

输出结果:

Enter the number of sides per die, 0 to stop.
6
How many dice?
2
You have rolled a 5 using 2 6-sided dice.
How many sides? Enter 0 to stop.
6
How many dice?
2
You have rolled a 9 using 2 6-sided dice.
How many sides? Enter 0 to stop.
6
How many dice?
2
You have rolled a 7 using 2 6-sided dice.
How many sides? Enter 0 to stop.
0
The rollem() function was called 6 times.
GOOD FORTUNE TO YOU!

12.4 分配内存:malloc()和free()

使用malloc()函数可以在程序运行时分配更多的内存。该函数接受一个参数:所需的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说malloc()分配内存,但是不会为其赋名。然而,它返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。malloc()的返回类型通常被定义为指向char的指针。然而,从ANSI C标准开始,C使用一个新的类型:指向void的指针。该类型相当于一个通用指针,会被强制转换为匹配的类型。在ANSI C中,应该坚持使用强制类型转换,提高代码的可读性。如果 malloc()分配内存失败,将返回空指针。

free()函数的参数是之前malloc()返回的地址,用于释放之前malloc()分配的内存。因此,动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止。不能用free()释放通过其他方式(如,声明一个数组)分配的内存。malloc()free()的原型都在stdlib.h头文件中。

如果内存分配失败,可以调用exit()函数结束程序,其原型在stdlib.h中。EXIT_FAILURE的值也被定义在stdlib.h中。标准提供了两个返回值以保证在所有操作系统中都能正常工作:EXIT_SUCCESS(相当于0)表示普通的程序结束, EXIT_FAILURE 表示程序异常中止。

示例程序:

/* dyn_arr.c -- 动态分配数组 */
#include 
#include  /* 为 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;
}

输出结果:

What is the maximum number of type double entries?
5
Enter the values (q to quit):
20 30 35 25 40 80
Here are your 5 entries:
  20.00   30.00   35.00   25.00   40.00 
Done.

12.4.1 free()的重要性

静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非用free()进行释放。否则可能会耗尽所有的内存。这类问题被称为内存泄漏(memory leak)。在函数末尾处调用free()函数可避免这类问题发生。

12.4.2 calloc()函数

分配内存还可以使用calloc()在ANSI之前,calloc()返回指向char的指针;在ANSI之后,返回指向void的指针。如果要储存不同的类型,应使用强制类型转换运算符。calloc()函数接受两个无符号整数作为参数(ANSI规定是size_t类型)。第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。calloc()函数还有一个特性:它把块中的所有位都设置为0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。free()函数也可用于释放calloc()分配的内存。示例:

long * newmen;
newmen = (long *)calloc(100, sizeof(long));/*分配内存*/
free(newmen);/*释放内存*/

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

变长数组和malloc()calloc都可以创建在程序运行时确定大小的数组,但是变长数组是自动存储类型。因此,程序在离开变长数组定义所在的块时,变长数组占用的内存空间会被自动释放,不必使用free()另一方面,malloc()创建的数组不必局限在一个函数内访问。另外,free()所用的指针变量可以与malloc()的指针变量不同,但
是两个指针必须储存相同的地址,但是不能释放同一块内存两次。

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

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

自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。

动态分配的内存在调用malloc()或相关函数时存在,在调用 free()后释放。使用动态内存通常比使用栈内存慢。动态分配的数据占用内存堆(自由内存)。

总而言之,程序把静态对象、自动对象和动态分配的对象储存在不同的区域。示例:

// where.c -- 数据被储存在何处?
#include 
#include 
#include 
int static_store = 30;
const char *pcg = "String Literal";
int main()
{
    int auto_store = 40;
    char auto_string[] = "Auto char Array";
    int *pi;
    char *pcl;
    pi = (int *)malloc(sizeof(int));
    *pi = 35;
    pcl = (char *)malloc(strlen("Dynamic String") + 1);
    strcpy(pcl, "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: %d 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", pcl, pcl);
    printf("   %s at %p\n", "Quoted String", "Quoted String");
    free(pi);
    free(pcl);
    return 0;
}

输出结果:

static_store: 30 at 0000000000403010
  auto_store: 40 at 000000000061FE0C
         *pi: 35 at 00000000001F1420
  String Literal at 0000000000404000
 Auto char Array at 000000000061FDF0
  Dynamic String at 00000000001F1440
   Quoted String at 000000000040406E

12.5 ANSI C 类型限定符

C90新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字constvolatile来声明,以这两个关键字创建的类型是限定类型(qualified type)。C99标准新增了第3个限定符:restrict,用于提高编译器优化。C11标准新增了第4个限定符:_AtomicC11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。C99为类型限定符增加了一个新属性:它们现在是幂等的(idempotent),意思是可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略

12.5.1 const 类型限定符

const关键字声明的对象,其值不能通过赋值或递增、递减来修改,因此可以用来创建不允许修改的数组。但是,可以初始化const变量。

当用const修饰指针变量时,const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。

其次,将全局变量设置为const,就可避免程序的任何部分都能更改数据的危险。注意,如果多个文件要使用同一个头文件中定义的const数据时,要将头文件中全局const数据声明为static,这为每个使用该全局变量的文件提供了单独的数据副本。

12.5.2 volatile 类型限定符

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

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

当某个变量被多次使用但是未修改其值时,编译器会把该变量的值临时储存在寄存器中,当再次需要读取该值时会从寄存器中读取以节约时间,这一优化被称为高速缓存(caching)。如果某代理在再次读取值之前修改了这一值,就不能如此优化。在ANSI之前,声明时没有volatile关键字,编译器不会进行高速缓存。在ANSI之后,声明中没有此关键字时,编译器假定变量的值在使用过程中不变,然后再尝试优化代码。

可以同时用constvolatile限定一个值。只能在声明中同时使用这两个限定符,它们的顺序不重要。示例:

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;
...
for (int i = 0; i < 10; i++)
{
    par[i] += 5;
    restar[i] += 5;
    ar[i] * +2;
    par[i] += 3;
    restar[i] += 3;
}

指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不能把它设置为restrict。在for循环体中编译器会把涉及restrict的两条语句替换为restar[i] += 8;。因此使用restrict关键字,编译器就可以选择捷径优化计算。

restrict限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。

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

12.5.4 _Atomic 类型限定符

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

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

可以替换成:

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

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


第12章练习题

你可能感兴趣的:(C/C++,c语言)