C语言知识点精细详解——数据类型和变量【3】——局部变量与全局变量,作用域与生存期

本文主要是介绍C语言知识点精细详解——数据类型和变量【3】——局部变量与全局变量,作用域与生存期,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • 预热:几个基本概念辨析
  • 局部变量
    • 定义
      • 使用说明
    • 静态局部变量
      • 定义
      • 使用说明
  • 全局变量
      • 定义
      • 使用说明
      • 补充
    • 静态全局变量
      • 定义
      • 使用说明
  • 寄存器变量
      • 定义
  • 总结
    • 关键词总结
  • 后话


前言

本节学习关键字:局部变量 全局变量 静态变量 自动变量 寄存器变量

  • 在学习本节主要内容之前,先对几个容易混淆的概念做个简易的辨析以便更好认识本章后续内容。

预热:几个基本概念辨析

整型 定点数 真值 机器数 常量和变量

  1. 整型:C语言中CPU通用寄存器和运算器的默认位宽,即int的4字节。整型的表示可以为多种进制数形式(如最常用的十进制,还有十六进制0x,八进制0),且整型可以分为短整型,整型和长整型。
  2. 定点数:表示在整数和⼩数的数字中⼩数点固定不动的数:

①定点数主要分为无符号数和有符号数。
② 整数也有小数点,只不过该点在整数最低(十进制)的位权之后(如十进制数100,小数点隐含在权最低位0的右侧,写作100.),与浮点数的⼩数点位置不固定有区别,但浮点数的小数用的是原码的定点小数表示。

  1. 真值:一般指有符号数原码数值位的十进制输出值,因为最高符号位不作为数值纳入考量。无符号数也可以使用真值概念,但其原码本身(所有位)就代表了实际的输出值。
  2. 机器数:各种变量代表的值或常量数据实际在计算机中处理和存储二进制0或1的数据形式。

①所有基本数据类型都是以机器二进制数的形式在内存和CPU中存储计算的。
②机器数又分为几种形式,对于整型数据而言,分为原码,反码,补码和移码。
③有符号数最高位(最左边的一个比特位),0为正,1为负,无符号数没有符号位,全部位都根据比特位所处位置的权值来存储数据。

  1. 变量:变量是内存中一个数据存储空间的表示,这个区域的标识由人为定义的名字(标识符)和类型(数据类型)构成。

①一个变量必须先声明,后使用,且声明和变量初始化时不占据存储空间,函数实际调用时系统才隐性开辟内存存放该类型数据。
②一个变量可以在一种数据类型中不断被覆盖新值(如定义整型变量int a = 1;后续在同一个函数中可以更改为a = 2; a = 3等)。
③C语言中,变量的存储类型分为自动型(auto),静态型(static),extern(外部型)和寄存器型(register)。

  1. 常量:分为字面常量(如’a’),常变量,#define宏定义的常量和enum枚举常量。

局部变量

定义

局部变量为声明在一个函数中仅限于该函数体使用的其存储空间存放的值。局部变量一般存在的情况及位置:

  1. 一个独立函数体的开头——以供函数后续功能对该变量的赋值及调用。
  2. 另一个函数体传参列表中的形式参数。
  3. 函数体中间——对声明该变量之前的代码块无法使用该参数,声明后的代码块才可使用。

使用说明

  1. 局部变量是创建在内存空间的栈上(stack),用户对一个局部变量进行声明赋值时,系统不会对其分配存储空间,只有实际调用该变量才会。
  2. 局部变量调用前必须先赋值,否则编译器会报错,描述错误类型为使用了未初始化的变量。
  3. 一个局部变量的作用范围只在一个独立的函数体中有效,如在主函数中定义的局部变量只能在主函数里使用,而在用户自定义函数中定义的局部变量只能在该函数中使用。


本例中,主函数体中定义了局部变量a,但在其外的函数print_a中显示未定义,由此验证该种变量的“局部性”

在这里插入图片描述
对局部变量进行传值调用,外部函数新声明的局部形参a被赋值2,并在函数体内打印输出2。

在这里插入图片描述
在这里插入图片描述
内存解释:
虽然两个局部变量a和a看似变量名和输出的真值是一致的,但是对比内存地址可以发现,两个变量所拥有的存储空间并不一样,这就说明了main函数中的a与外函数print_a中的a并不是同一个a,且因为是两个独立分别存在的局部变量,彼此的值不会相互影响,即在外函数中对a的重新赋值不会造成main函数中a值的改变:
在这里插入图片描述

  1. 局部变量也称为自动动态局部变量,一个局部变量的声明默认由系统auto关键字所修饰,如上例中int a = 2等价于auto int a = 2,其代表的含义是此类变量的生存期从该函数声明开始(未赋值前),在被函数赋值并调用结束后,出了该函数即停止对该自动变量的内存分配,即在栈区系统就自动释放了他所占用的内存空间。
  2. 未指明修饰前缀的局部变量默认为auto型自动变量,但因为是系统默认修饰,可以忽略而不需要手动添加。与动态局部变量(即auto局部变量)相对应的是静态局部变量。

静态局部变量

定义

该种变量的前缀修饰关键字为static,在一个函数体内声明变量时使用,如static int b。

使用说明

  1. 静态局部变量从声明开始就已经在内存的静态区开辟了内存空间,而不是等待调用才分配。因为静态变量若不手动赋值,其由系统默认初始化为0,与全局变量一致。
  2. 静态局部变量的作用域同动态局部变量,仅能在一个独立的函数体内使用。不同点在于,静态局部变量的生存期比动态局部变量更长,因为其不随函数调用的结束而释放其变量空间,而是一直维持在静态区的地址和存储值。

在这里插入图片描述
在main函数汇总声明和赋值的静态局部变量b和c在外函数中无法使用,这点与局部变量是一致的。

在这里插入图片描述
据图可知,静态局部变量在函数体内不随着函数的结束而结束自己的生命周期,而是一直将地址和值原封不动地保存好,等待下一次函数调用使用,其最终释放只有等该程序结束才释放静态区的内存空间。而动态局部变量则恰恰相反,在函数结束时就及时结束内存分配,释放了空间。但两者的共同点都是不能在非本函数之外进行调用。


全局变量

定义

存储方式与静态局部变量类似,但作用域不同的一种变量类型。其声明方式与局部变量一致,但声明的位置与处于函数中的变量不同,它是在函数之外声明的。

使用说明

在这里插入图片描述> 在这里插入图片描述
全局变量其实作用域并不是“全局”的,因为此种变量仅能被声明在其后的函数体所辨别和使用,定义在一个全局变量之前的函数体无法识别此变量。

在这里插入图片描述
全局变量若使用不当,可能与定义在其作用域内的局部变量重名,在全局变量名与局部变量名冲突时,函数体会优先使用定义在函数里的同名局部变量代表的值,即就近原则。

在这里插入图片描述
定义在全局变量作用域内的函数体可以识别并使用他们,同一个工程文件(可能包含多个头文件和源文件的集合)下的其他源文件也可以通过变量的外部引入关键字extern来发现并识别出位于工程文件集合中的全局变量。

由图中可以得知多个源代码文件由自定义头文件head.h相关联,由于在初始源文件variety.c中定义的三个变量类型为全局变量,其存储在内存的静态区,生存期直到主函数终结才结束,释放内存,期间不仅定义在其后的本文件的函数可以使用该变量,定义在其他源文件的函数通过extern引入全局变量也同样可以访问和使用,但是要注意,全局变量如果在其他文件或其作用域内的函数体内被修改,则所有能够访问到该变量的对应值都会被修改,且不需要返回值:

在这里插入图片描述
在这里插入图片描述

补充

  1. 如果在头文件中宏定义#define一个值,其作用域也可以是包含了该头文件的所有源代码文件,但其实它的引入方式与全局变量并不一样,只是在预编译过程就完成了对应符号的数值替换,运行时不存在对于内存区域的占用:

在这里插入图片描述
全局变量与宏定义有着本质区别,全局变量是在声明起就已经在内存的静态区开辟好了内存并规定其作用域和生存期;而预定义# define将特定符号所转换而成的值仅仅完成了数值的替换。
在这里插入图片描述
在这里插入图片描述
pragma once的作用是防止同一个原文件中多次引用相同的头文件,可能造成的头文件在预编译整段替换时的变量重定义问题。
在这里插入图片描述
引入头文件”head.h”的源代码文件编译后得到了全局变量A的值,并带入输出,将int A视作全局变量,虽然他定义在了源代码文件外部的头文件中,但引用该头文件的.c源文件每次预编译时都会将位于文件开头的头文件整个拷贝替换,即将语句int A = 10替换到了引用的源文件中。

  1. 如果该源代码是多人合作完成,且代码量巨大,不同用户可能重复引用相同头文件,编译器对代码预编译就会多次替换头文件,并使全局变量A重定义导致错误。

在这里插入图片描述

  1. 避免重定义有多种解决方法,最常见有两种。

①头文件中加入# pragma once只编译一次语句:
在这里插入图片描述

②头文件中加入# ifndef # define__HEAD_H 和# end if语句:
在这里插入图片描述

该语句相较于# pragma once的好处在于,它的使用范围更广,受多种编译器的语法支持,而前者只能被新版编译器支持,老版本gcc等编译器则无法识别。其次,该语句可以框定防止重复编译的函数声明或宏范围,而# pragma once则作用于整个头文件的所有函数声明和宏,无法手动选择。

  1. 值得注意的是,库头文件(如#include<stadio.h>,#include<string.h>)等头文件即使反复引入,编译器编译时也不会报错,因为程序员在封装这类C基本库函数时也同样加入了# ifndef防止重复编译的语句块,查看头文件定义可知:

在这里插入图片描述
部分库头文件的函数声明

  1. 除此之外还需注意:避免在头文件中声明全局变量,因为如果同上述在头文件中定义全局变量,多个源代码文件若引用了同一个头文件,则这几个源文件在编译的预编译阶段对头文件进行替换时,会发现同一个全局变量int A = 10在多个源文件里出现,从而产生的变量重定义问题。解决方式如下图:

在这里插入图片描述
所以推荐在头文件中仅针对需要操作的类型数据进行类型重命名,而不是对变量本身的声明和赋值,防止重定义的同时,多个源代码可以引用相同的头文件,编程和传参更方便。


静态全局变量

定义

由全局变量引出的与静态局部变量类似的概念,它可以简单理解为:

静态全局变量的作用域仅限于定义该变量的本源文件内,且作用在定义于其后的函数体中,而不能用于外部文件的函数里。
在这里插入图片描述

使用说明

全局变量,静态全局变量和静态局部变量的一般用途可以用来记录一个函数被调用的次数,但需要注意的是,后两者只能记录本文件内某个独立函数的被主函数的调用次数,而全局变量可以被外部源文件引入调用,并可同时记录外引用次数:
在这里插入图片描述


寄存器变量

定义

此种变量一般针对于待频繁调用的变量,如在for循环中(参数为循环变量初始化; 循环判断表达式; 循环结束逼近表达式)的循环变量定义(C语言汇总一般用i表示),i作为循环变量在每次循环结束时,都会自增并逼近循环结束的条件,该变量一直频繁被运算器进行加法运算,所以可以人为定义这个循环变量i为寄存器变量,或编译器自动识别哪些需要频繁运算的变量并定义为register类型,这种转换往往是隐性的。

寄存器变量与普通变量的内存调用差异代码:
在这里插入图片描述
注:此代码引用了https://blog.csdn.net/firefly_2002中关于register变量与其他类型变量的时间效率证明,侵删。

由此可以看出,寄存器变量与存放于栈中的局部变量,静态区的全局变量的频繁调用有着明显的时间差异(1000ms)。


总结

关键词总结

生存期 作用域 静态变量 自动变量 寄存器变量

  1. 变量按生存期可分为动态与静态变量,分别使用auto与static关键字修饰。按作用域可分为局部变量和全局变量。
  2. 动态变量一般指存在于同一个源文件内一个独立函数中的局部变量,它的作用域仅针对这个函数,随着函数调用结束而释放,它的内存分配在栈上。
  3. 静态变量包含局部静态变量,全局和静态全局变量。三者空间都分配在内存空间的静态区,且不随函数的调用结束而释放内存,只有程序结束后系统才自动释放,所以它们的值会一直被函数内部保留,并且只初始化一次,但这不代表它们不能被赋值修改。
  4. 静态局部变量只能在定义它的独立函数中可见,其他函数不能访问该static型局部变量,即该变量不对外,具有无链接性。而静态全局变量是定义在文件开头的static型变量,其具有内链接性,即该源文件的所有函数均可访问和修改其值,但不能被其他源文件所使用。
  5. 全局变量的作用域是整个工程文件,包含在整一个工程文件中的其他源文件或头文件需要对某个定义的全局变量引用时需要加上关键字extern + 类型 + 变量名进行引用。
  6. extern不仅可以修饰变量,也可以修饰函数。因为只要是在源文件内定义的函数都是默认动态函数(auto),存放在程序代码区,工程文件内部所有源文件都可以共享和调用:
    在这里插入图片描述
    但是同时也需要注意,static同样可以修饰函数,使一个函数的作用域只在本文件内生效,而对其他源文件无法引入:
    在这里插入图片描述
  7. 全局变量最好少使用,因为程序设计遵循“强内聚,弱耦合”,即变量间的相互调用应尽量局限于一个独立的函数内部,而不应依赖于其他函数或全局区域性质的变量。因为全局变量的修改可能会对多个文件的多个函数内部值产生连锁影响,降低程序的清晰性,若注意不周可能会造成程序的崩溃。且全局变量不会及时释放,会更占用内存空间。
  8. 寄存器变量register可以更方便CPU运算器对变量的调用和值修改,相比于栈区的普通局部变量和静态区的全局变量,寄存器变量对于运算器ALU的使用时间效率更高,但需要注意的是,寄存器存储空间是很有限的,且该种寄存器变量的转换一般是由编译器自动完成的。

后话

  1. 博客项目代码开源,获取地址请点击右侧VelvetShiki_Not_VS的Gitee仓库。
  2. 若阅读中存在疑问或不同看法欢迎在博客下方或码云中留下评论。
  3. 欢迎访问我的CSDN博客主页和Gitee码云主页,更多学习资料记得随时关注和三连点赞,您的支持是我分享的动力~

这篇关于C语言知识点精细详解——数据类型和变量【3】——局部变量与全局变量,作用域与生存期的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/172160

相关文章

Redis中6种缓存更新策略详解

《Redis中6种缓存更新策略详解》Redis作为一款高性能的内存数据库,已经成为缓存层的首选解决方案,然而,使用缓存时最大的挑战在于保证缓存数据与底层数据源的一致性,本文将介绍Redis中6种缓存更... 目录引言策略一:Cache-Aside(旁路缓存)策略工作原理代码示例优缺点分析适用场景策略二:Re

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

MySQL数据库约束深入详解

《MySQL数据库约束深入详解》:本文主要介绍MySQL数据库约束,在MySQL数据库中,约束是用来限制进入表中的数据类型的一种技术,通过使用约束,可以确保数据的准确性、完整性和可靠性,需要的朋友... 目录一、数据库约束的概念二、约束类型三、NOT NULL 非空约束四、DEFAULT 默认值约束五、UN

Python使用Matplotlib绘制3D曲面图详解

《Python使用Matplotlib绘制3D曲面图详解》:本文主要介绍Python使用Matplotlib绘制3D曲面图,在Python中,使用Matplotlib库绘制3D曲面图可以通过mpl... 目录准备工作绘制简单的 3D 曲面图绘制 3D 曲面图添加线框和透明度控制图形视角Matplotlib

MySQL中的分组和多表连接详解

《MySQL中的分组和多表连接详解》:本文主要介绍MySQL中的分组和多表连接的相关操作,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一起看看吧... 目录mysql中的分组和多表连接一、MySQL的分组(group javascriptby )二、多表连接(表连接会产生大量的数据垃圾)MySQL中的

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

redis中使用lua脚本的原理与基本使用详解

《redis中使用lua脚本的原理与基本使用详解》在Redis中使用Lua脚本可以实现原子性操作、减少网络开销以及提高执行效率,下面小编就来和大家详细介绍一下在redis中使用lua脚本的原理... 目录Redis 执行 Lua 脚本的原理基本使用方法使用EVAL命令执行 Lua 脚本使用EVALSHA命令

SpringBoot3.4配置校验新特性的用法详解

《SpringBoot3.4配置校验新特性的用法详解》SpringBoot3.4对配置校验支持进行了全面升级,这篇文章为大家详细介绍了一下它们的具体使用,文中的示例代码讲解详细,感兴趣的小伙伴可以参考... 目录基本用法示例定义配置类配置 application.yml注入使用嵌套对象与集合元素深度校验开发

Python中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多