5. C语言变量的存储类别和生存期

我们知道,变量是有数据类型的,用以说明它占用多大的内存空间,可以进行什么样的操作。

除了数据类型,变量还有一个属性,称为“存储类别”。存储类别就是数据在内存中的存放区域。一个正在运行的C程序的内存空间可以分为五个区域:程序代码区、静态数据区、堆区、栈区和命令行参数区,其中静态数据区和栈区可以用来存放变量的值。

静态数据区的内存在程序启动时就已经由操作系统分配好,占用的空间固定,程序运行期间不再改变,程序运行结束后才由操作系统释放;它可以存放全局变量、静态变量、一般常量和字符串常量。

栈区的内存在程序运行期间由操作系统根据需要来分配(使用到变量才分配内存;如果定义了变量但没有执行到该代码,也不会分配内存),占用的空间实时改变,使用完毕后立即释放,不必等到程序运行结束;它可以存放局部变量、函数参数等。

可以通过C语言中的关键字来控制变量的存放区域;C语言共有 4 个关键字用来指明变量的存储类别:auto(自动的)、static(静态的)、register(寄存器的)、extern(外部的)。

知道了变量的存储类别,就可以知道变量的生存期。通俗地讲,生存期指的是在程序运行过程中,变量从创建到销毁的一段时间,生存期的长短取决于变量的存储类别,也就是它所在的内存区域。

auto 变量

auto 是自动或默认的意思,很少用到,因为所有的变量默认就是 auto 的。也就是说,定义变量时加不加 auto 都一样,所以一般把它省略,不必多此一举。

例如:

  1. int n = 10;

  1. auto int n = 10;

的效果完全一样。

6. C语言extern变量和函数

在所有的代码块(函数、if 块、switch 块等)之外定义的变量称为全局变量,它的作用范围默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。

如果你一直在编写单个 .c 文件的程序,那么请注意,全局变量的作用范围不是从变量定义处到该文件结束,在其他文件中也有效。

虽然全局变量的作用范围是整个程序,但是如果希望在 a.c 中使用 b.c 中的变量,也必须先进行声明。声明使用 extern 关键字,请看下面的代码。

a.c 源码:

1 #include <stdio.h>
2 #include <stdlib.h>
3 extern int num; // 必须对 num 进行声明
4 int main ()
5 {
6     printf("num = %d", num);
7     system("pause");
8     return 0;
9 }

b.c 源码:

1 int num = 100; // 对 num 进行定义

运行结果:
num = 100

我们在 b.c 中定义了一个全局变量 num,在 a.c 中调用了它。extern int num; 的作用是告诉编译器 num 不在 a.c 中,请到其他文件中查找。如果没有 extern,编译器就会在当前文件中查找,发现没有就会报错。

提示:编译是针对单个源文件的,在编译 a.c 时编译器找不到 num,链接时才会在 b.c 的目标代码(.obj 文件)中找到 num。

与其他变量不同,extern 变量有声明和定义之分。

extern 变量的定义格式为:

  1. extern type name = value;

不过 extern 可以省略(我们通常就是这么做的),全局变量默认就是 extern 的,如 b.c 文件所示。

声明格式为:

  1. extern type name;

注意:

  • 在定义 extern 变量时不能省略 value,否则就变成了变量声明。
  • 声明 extern 变量时要指明数据类型(必须和定义时的数据类型一致)。
  • 声明可以有多次,定义只能有一次。

在 a.c 中,我们在所有代码块外部对 num 进行了声明,这个时候 num 的作用范围是 a.c 整个文件(确切的说是从声明开始处到文件结束)。如果在代码块内部声明会怎样呢?

对 a.c 进行更改:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 int main ()
 4 {
 5     {
 6         extern int num;
 7         printf("num = %d", num);
 8     }
 9     printf("num = %d", num);
10     system("pause");
11     return 0;
12 }

编译时报错,第 9 行的 num 未声明。这说明 extern 变量的作用域跟它的声明位置有关,在代码块内声明的 extern 变量在代码块外无效。

extern 函数

从本质上讲,函数和变量是类似的,它们都指向内存中的一块区域:函数指向存放了函数体二进制代码的程序代码区,变量指向静态数据区、栈区或堆区。

extern 除了用于变量,也可以用于函数,请看下面的代码:

sum.c 源码:

1 int sum(int n1, int n2)
2 {
3     return n1 + n2;
4 }

main.c 源码:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 extern int sum(int, int);
 4 int main ()
 5 {
 6     int num1 = 20, num2 = 110;
 7     printf("%d + %d = %d", num1, num2, sum(num1, num2));
 8     system("pause");
 9     return 0;
10 }

运行结果:
20 + 110 = 130

我们在 sum.c 中定义了一个函数 sum() 用来计算两个数的和,在 main.c 中对函数进行了调用。extern 的作用是告诉编译器 sum() 函数不在 main.c 中,请到其他文件中去查找。

但是,函数和变量的声明有所不同,对于函数,你可以省略 extern。例如将 main.c 中的:

  1. extern int sum(int, int);

改为:

  1. int sum(int, int);

仍然能够编译通过并正确运行。

这是因为,函数的定义和声明区别很明显,有函数体就是定义,没有函数体就是声明,所以有没有 extern 都是函数声明。但是变量不一样,没有 extern 就是变量定义,重复定义是错误的。

7. C语言static变量和函数

上一节我们讲到,全局变量和函数的作用范围默认是整个程序,也就是所有的源文件。这给我们带来了很大的方便,让我们能够在 A 文件中调用 B 文件中定义的变量和函数,不必把所有的代码都集中到一个文件,有利于模块化的程序设计。

但是有时候这也会带来冲突,例如在 a.c 中定义了一个全局变量 n,在 b.c 中又定义了一次,编译时就会发生重复定义的错误,因为变量只能定义一次。

如果两个文件都是我们自己编写的或者其中一个是,遇到这样的情况还比较好处理,改变变量的名字就可以;但如果两个文件都是其他程序员编写的,或者是第三方的库,修改起来就颇费精力了。所以,实际开发中我们一般将不需要被其他文件调用的全局变量或函数的作用范围限制在当前文件中。

可以通过 static 关键字来限制,请看下面的代码。

a.c 源码:

1 #include <stdio.h>
2 static int n = 10;
3 void print_n_a()
4 {
5     printf("n(a.c) = %d\n", n);
6 }

b.c 源码:

1 #include <stdio.h>
2 static int n = 20;
3 void print_n_b()
4 {
5     printf("n(b.c) = %d\n", n);
6 }

main.c 源码:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 int n = 100;
 4 int main ()
 5 {
 6     print_n_a();
 7     print_n_b();
 8     printf("n(main.c) = %d\n", n);
 9     system("pause");
10     return 0;
11 }

运行结果:
n(a.c) = 10
n(b.c) = 20
n(main.c) = 100

我们在 a.c、b.c 和 main.c 中都定义了变量 n,a.c 和 b.c 中的变量 n 都只在各自的文件内有效,main.c 中的变量 n 在整个程序内有效。

由此可见,加了 static 的变量或函数的作用范围仅限于当前文件,对其他源文件隐藏,利用这一特性可以在不同的文件中定义同名的变量或函数,而不必担心命名冲突。

static 局部变量

static 声明的变量称为静态变量,不管是全局变量还是局部变量,都存储在静态数据区(全局变量本来就存储在静态数据区,即使不加 static)。

静态数据区的数据在程序启动时就会初始化,直到程序运行结束;对于代码块中的静态局部变量,即使代码块执行结束,也不会销毁。

注意:静态数据区的变量只能初始化(定义)一次,以后只能改变它的值,不能再被初始化,即使有这样的语句,也无效。

请看下面的代码:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 int main ()
 4 {
 5     int result, i;
 6     for(i = 1; i<=100; i++)
 7     {
 8         result = sum(i);
 9     }
10     printf("1+2+3+...+99+100 = %d\n", result);
11     system("pause");
12     return 0;
13 }
14 int sum(int n)
15 {
16 // 也可以不赋初值 0,静态数据区的变量默认初始化为 0
17     static int result = 0;
18     result += n;
19     return result;
20 }

运行结果:
1+2+3+...+99+100 = 5050

我们在 sum() 中定义了一个静态局部变量 result,它存储在静态数据区,sum() 函数执行结束也不会销毁,下次调用继续有效。静态数据区的变量只能初始化一次,第一次调用 sum() 时已经对 result 进行了初始化,所以再次调用时就不会初始化了,也就是说 static int result = 0; 语句无效。

静态局部变量虽然存储在静态数据区,但是它的作用域仅限于定义它的代码块,sum() 中的 result 在函数外无效,与 main() 中的 result 不冲突,除了变量名一样,没有任何关系。

总结起来,static 变量的主要作用有两个。

1) 隐藏

程序有多个源文件时,将全局变量或函数的作用范围限制在当前文件,对其他文件隐藏。

2) 保持变量内容的持久化

将局部变量存储到静态数据区。静态数据区的内存在程序启动时就已分配好(内存中所有的字节默认值都是0x00),直到程序运行结束。

8. C语言register变量

一般情况下,变量的值是存储在内存中的,CPU 每次使用数据都要从内存中读取。如果有一些变量使用非常频繁,从内存中读取就会消耗很多时间,例如 for 循环中的增量控制:

1 int i;
2 for(i=0; i<1000; i++)
3 {
4 // Some Code
5 }

执行这段代码,CPU 为了获得 i,会读取 1000 次内存。

为了解决这个问题,可以将使用频繁的变量放在CPU的通用寄存器中,这样使用该变量时就不必访问内存,直接从寄存器中读取,大大提高程序的运行效率。

寄存器、缓存、内存

为了加深对 register 变量的理解,这里有必要讲一下CPU寄存器。

按照与CPU的远近来分,离CPU最近的是寄存器,然后是缓存,最后是内存。

寄存器是最贴近CPU的,而且CPU只在寄存器中进行存取。寄存的意思是暂时存放数据,不用每次都从内存中取,它是一个临时的存放数据的空间。

而寄存器的数据又来源于内存,于是 CPU <-- 寄存器 <-- 内存,这就是它们之间的信息交换。

那么为什么还需要缓存呢?因为如果频繁地操作内存中同一地址上的数据会影响速度,于是就在寄存器和内存之间设置一个缓存,把使用频繁的数据暂时保存到缓存,如果寄存器需要读取内存中同一地址上的数据,就不用大老远地再去访问内存,直接从缓存中读取即可。

缓存的速度远高于内存,价格也是如此。

注意:缓存的容量是有限的,寄存器只能从缓存中读取到部分数据,对于使用不是很频繁的数据,会绕过缓存,直接到内存中读取。所以不是每次都能从缓存中得到数据,这就是缓存的命中率,能够从缓存中读取就命中,否则就没命中。

关于缓存的命中率又是一门学问,哪些数据保留在缓存,哪些数据不保留,都有复杂的算法。

注意:上面所说的CPU是指CPU核心,从市场上购买的CPU已是封装好的套件,附带了寄存器和缓存,插到主板上就可以用。

从经济和速度的综合考虑,缓存又被分为一级缓存、二级缓存和三级缓存,它们的存取速度和价格依次降低,容量依次增加。购买到的CPU一般会标出三级缓存的容量。

register 变量

寄存器的数量是有限的,通常是把使用最频繁的变量定义为 register 的。

来看一个计算 π 的近似值的例子,求解的一个近似公式如下:

为了提高精度,循环的次数越多越好,可以将循环的增量控制定义为寄存器变量,如下所示:

 1 #include <stdio.h>
 2 #include <conio.h>
 3 int main()
 4 {
 5     register int i = 0; // 寄存器变量
 6     double sign = 1.0, res = 0, ad = 1.0;
 7     for(i=1; i<=100000000; i++)
 8     {
 9         res += ad;
10         sign=-sign;
11         ad=sign/(2*i+1);
12     }
13     res *= 4;
14     printf("pi is %f", res);
15     getch();
16     return 0;
17 }

运行结果:
pi is 3.141593

关于寄存器变量有以下事项需要注意:
1) 为寄存器变量分配寄存器是动态完成的,因此,只有局部变量和形式参数才能定义为寄存器变量。

2) 局部静态变量不能定义为寄存器变量,因为一个变量只能声明为一种存储类别。

3) 寄存器的长度一般和机器的字长一致,所以,只有较短的类型如int、char、short等才适合定义为寄存器变量,诸如double等较大的类型,不推荐将其定义为寄存器类型。

4) CPU的寄存器数目有限,因此,即使定义了寄存器变量,编译器可能并不真正为其分配寄存器,而是将其当做普通的auto变量来对待,为其分配栈内存。当然,有些优秀的编译器,能自动识别使用频繁的变量,如循环控制变量等,在有可用的寄存器时,即使没有使用 register 关键字,也自动为其分配寄存器,无须由程序员来指定。

转载于:https://www.cnblogs.com/chunlanse2014/articles/4422153.html

动态内存分配及变量存储类别(第二部分)相关推荐

  1. c语言内存分配与释放 不同类别变量的内存分配和释放的区别

    1.不同变量使用内存的区别 静态存储类别的变量:内存数量在编译时确定,程序开始执行时创建,程序结束时销毁.(static.const) 自动存储类别的变量:程序进入变量定义所在块时存在,离开块时销毁. ...

  2. 【C 语言】内存管理 ( 动态内存分配 | 栈 | 堆 | 静态存储区 | 内存布局 | 野指针 )

    相关文章链接 : 1.[嵌入式开发]C语言 指针数组 多维数组 2.[嵌入式开发]C语言 命令行参数 函数指针 gdb调试 3.[嵌入式开发]C语言 结构体相关 的 函数 指针 数组 4.[嵌入式开发 ...

  3. 各种存储分配算法java代码实现_Java实现操作系统中四种动态内存分配算法:BF+NF+WF+FF...

    1 概述 本文是利用Java实现操作系统中的四种动态内存分配方式 ,分别是:BF NF WF FF 分两部分,第一部分是介绍四种分配方式的概念以及例子,第二部分是代码实现以及讲解. 2 四种分配方式 ...

  4. 释放变量所指向的内存_C++动态内存分配(学习笔记:第6章 15)

    动态内存分配[1] 动态申请内存操作符 new new 类型名T(初始化参数列表) 功能: 在程序执行期间,申请用于存放T类型对象的内存空间,并依初值列表赋以初值. 结果值: 成功:T类型的指针,指向 ...

  5. C语言学习笔记10-指针(动态内存分配malloc/calloc、realloc、释放free,可变数组实现;Tips:返回指针的函数使用本地变量有风险!;最后:函数指针)

    C语言:指针 1. 指针:保存地址的变量 *p (pointer) ,这种变量的值是内存的地址.   取地址符& 只用于获取变量(有地址的东西)的地址:scanf函数-取地址符   地址的大小 ...

  6. 第12章-cpp类和动态内存分配

    本章内容包括: • 对类成员使用动态内存分配. • 隐式和显式复制构造函数. • 隐式和显式重载赋值运算符. • 在构造函数中使用new所必须完成的工作. • 使用静态类成员. • 将定位new运算符 ...

  7. C++中的动态内存分配

    1.Cpp中的内存分配 了解动态内存在C++中是如何工作的是成为一名合格的C++程序员必不可少的.C++程序中的内存分为两个部分: 栈:在函数内部声明的所有变量都将占用栈内存. 堆:这是程序中未使用的 ...

  8. C++ 高级数据类型(四)—— 动态内存分配

    到目前为止,我们的程序中我们只用了声明变量.数组和其他对象(objects)所必需的内存空间,这些内存空间的大小都在程序执行之前就已经确定了.但如果我们需要内存大小为一个变量,其数值只有在程序运行时 ...

  9. C语言中动态内存分配的本质是什么?

    摘要:C语言中比较重要的就是指针,它可以用来链表操作,谈到链表,很多时候为此分配内存采用动态分配而不是静态分配. 本文分享自华为云社区<[云驻共创]C语言中动态内存分配的本质>,作者: G ...

最新文章

  1. python黑科技库:FuckIt.py,让你代码从此远离bug
  2. 计算机软件基础操作,计算机软件基础 第九章 Excel基本操作(1页)-原创力文档...
  3. python流程控制-Python 流程控制
  4. Html和CSS的关系
  5. lazada店铺成长案例分享!把握小众类目商机,店铺月销过万单!
  6. 线段树-简单线段树模板
  7. 【语义分割】ICCV21_Self-Regulation for Semantic Segmentation
  8. python中#!/usr/bin/python与#!/usr/bin/env python的区别
  9. 什么是document对象?如何获取文档对象上的元素?_dom对象
  10. 计算机网络学习笔记(七)——传输层、TCP三次握手、四次挥手、TCP流量控制、大总结
  11. 机器人搏击大赛冠军_机器人的江湖 谁主沉浮——2017格斗机器人大赛侧记
  12. 用户登录界面 - 记事本风格HTML代码
  13. 哈啰出行完成新一轮2.8亿美元融资 阿里首次入股
  14. css带占位符的搜索框,superplaceholder.js-功能强大的超级输入框占位符插件
  15. Go 单元测试从 0 到 1
  16. 高并发架构消息队列面试题解析
  17. DeepFM算法详解-推荐算法中的皇冠
  18. python输出奇数个数_Python实践|输出0-7组成八位奇数总数
  19. centos查询 硬盘序列号查询_CentOS查看主板型号、CPU、显卡、硬盘等信息
  20. 解决 OneNote 项目符号快捷键 Ctrl+. 在 Windows 10 下无效问题

热门文章

  1. 计算机一级考试无法显示评分,计算机一级评分各题标准
  2. 计算机动画人机交互,北大计算机系多媒体与人机交互.ppt
  3. 解决 Gedit 中文乱码
  4. Mac OSX中memcached安装测试
  5. 感知算法论文(十):Towards Universal Object Detection by Domain Attention(2019)
  6. 目标检测系列(四)——Fast R-CNN译文
  7. 单词嵌入_单词嵌入与单词袋:推荐系统的奇怪案例
  8. 地震中房子变废墟了,贷款还需要还吗?
  9. 《恐怖小说在中国》之四:恐怖小说的流行与陷阱?
  10. 与鬼古女夜晚后海边的“恐怖”聚会