什么是 全局变量 & 宏 & 大程序怎么写,看这一篇就够了
全局变量
认识 全局变量
- 定义在函数外的变量就是全局变量
- 全局变量具有全局的生存期和作用域
- 它们与任何函数无关
- 任何函数(定义在全局变量后的的函数)内部都可以使用它们
例如:
int f(void); |
输出:
in main gAll = 12 |
全局变量的初始化
- 没有初始化的全局变量默认值为 0
- 指针默认为 NULL
- 只能用编译时刻已知[^1]的值来初始化全局变量
- 全局变量的初始化发生在main函数之前
注释1:
int gAll = 12; |
下面这段代码在某些编译器(dev c++)上是可以编译的,但是在 vs 上是不能编译的
const int gAll = 12; |
但是,这种方式是不推荐的
被隐藏的全局变量
- 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。
int f(void); |
输出:
in main gAll = 12 |
即使 gAll 在 main 函数中被覆盖,f 函数中的 gAll 也是不会被该改变的
为什么会这样?自己思考一下。
静态本地变量
- 在本地变量定义时加上 static 修饰符就成为静态本地变量
- 当离开函数的生存期后,静态本地变量会继续存在并保持其值
- 静态本地变量的初始化只会在第一次进入这个函数时进行,以后进入函数时会保持上次离开时的值。
例:
不用static的情况
int f(void); |
输出:
in f All = 1 |
使用static:
int f(void); |
输出:
in f All = 1 |
看看地址
int f(void); |
输出:
1 st |
全局变量 gAll 与 静态局部变量 All 在内存中相邻
总结
- 静态本地变量实际上是特殊的全局变量
- 它们位于相同的内存区域
- 静态本地变量具有全局的生存期,函数内的局部作用域
返回指针的函数
请同学们先看一下下面这个程序:
int* f(void); |
输出:
*p = 12 |
i 和 k 的内存其实是同一块空间
总结
- 返回 本地变量 的地址是危险的
- 返回 全局变量 或 静态局部变量 的地址是安全的
- 返回函数内 malloc 的内存是安全的,但是容易造成问题
- 最好的做法是返回传入的指针
说了这么多,总结一句话
尽量避免使用 全局变量 和 静态本地变量
为什么这里就不深讲了,有兴趣的朋友可以下来自己查查。
编译预处理 与 宏
编译预处理指令
#
开头的是编译预处理指令- 它们不是 C语言的一部分,但是 C语言离不开他们
#define
用来定义一个宏
define 关键字
回想我们刚学 double 的时候,是不是计算过圆的面积。当时我们可能是这样写的:
|
现在我们用 宏 就不需要用 const 修饰的全局变量了,我们也说过,全局变量最好不用。
|
现在,我们打开我们的虚拟机,进入 Linux 系统。
现在多出来了 4 个文件,蓝色的是文件夹,我们不去管它,绿色的是可执行文件,类似 windows 的 .exe 文件
现在我们主要关注这 3 个中间文件
一个 c文件编译的过程文件变化是这样的:
.c
(处理编译预处理指令)->.i
(产生汇编代码)->.s
(汇编生成目标文件) ->.o
(链接等) ->a.out
可以看到 .i 文件时很大
我们发现程序中的宏 PI 被换成了它所表示的 数字
这种替换是简单的文本替换,我们再试试其他的替换方式:
我们再试试这样,定义宏的时候 不带双引号:
因此可知,被 " "
扩起来的字符串 宏 是不会替换的
总结
- 格式:
#define <名字> <值>
- 注意结尾没有分号,因为不是 C 的语句
- 名字必须是一个单词,值可以是任何(注意字符串替换定义时需要带引号)
- 在 C语言的编译器开始编译之前,编译预处理程序(cpp)会把程序中的宏的名字替换为值
- linux/unix
- 编译并保留中间文件指令:
gcc --save-temps
- 查看文件结尾:
tail
- 编译并保留中间文件指令:
宏
- 如果在一个宏的值中有其他宏的名字,这些宏也是会被替换的
- 如果一个宏的值超过一行,最后一行之前的行末需要加 \
- 宏的值后面出现的注释不会被当作宏的值的一部分
没有值的宏
#define _DEBUG
#define _CRT_SECURE_NO_WARNINGS
用 VS 的应该都知道这个吧,加上这个你就可以直接用scanf
而不是scanf_s
了
这类宏是用来做条件编译的,后面有其他编译预处理指令来检查这个宏是否已经被定义过了。
比如有这个宏执行这部分代码,没有则执行另外一部分
预定义的宏
__LINE__
__FILE__
__DATE__
__TIME__
__STDC__
我们来试着用一下:
int main(void) { |
输出:
D:\vscode\练习\12-31\Project1\oj.c : 174 |
值得注意的是,__LINE__
表示的是它自己所在的行数
你们在熟睡,而我还在给你们写教学,关注我/点个赞/转发 不过分吧~
带参数的宏
#define cube(x) ( (x) * (x) * (x) )
例如:
|
输出:
125 |
容易犯的错误
一下这两种写法在程序中会不会有问题?
#define ERROR(1x) (x * 57)
#define ERROR2(x) (x) * 57
思考一下这个程序会的到你想要的结果吗?
|
输出:
115 |
为什么会这样呢?我们不妨来看一下,.i
文件内部:
定义带参数的宏的原则
- 一切都要有括号
- 整个值有括号
- 每个参数都有括号
所以,上面错误的例子的正确的写法就是:
#define ERROR ( (x) * 57 )
带参数的宏的更多用法:
#define MIN(a, b) ((a) > (b) ? (b) : (a))
定义宏切记不要加分号
错误示范:
|
VS 会报错 :没有匹配 if 的非法 else,为什么呢?
因为如果你在宏后面加了 ;
,你又在 if 内的语句后加了;
这样在.i
的阶段,if 后的语句有了两个 ;
,即:
PRETTY_PRINT("less than 10\n");;
第二个;
表示 一个空语句,这样 else 前面就没有对象可以匹配了
总结
- #开头的预处理指令并不是 C语言独有的内容
- 宏的参数时没有类型的
- 大型程序中宏的使用很常见
- 宏可以很复杂,可以产生函数
- 使用运算符
#
和##
- 使用运算符
- 部分宏会被
inline
函数取代 - 中西方差异(国人少用)
Quiz:
请看下面的代码片段,判断这段程序会输出什么?
|
A: B
B: C
C: D
D: E
这道题是需要都脑子的呦!
公众号后台回复:0211 1 查看答案和解析
大程序结构
多个源代码文件
多个源文件.c
引入
回想我们学习的过程,开始是 main()里的代码太长了,我们学习了函数,将其分开
现在如果 一个源文件太长了,我们就可以将其分成几个源文件
怎么让多个源文件联系起来?
在编译器上创建一个项目,将你想操作的 .c 文件放到同一个项目中
头文件 .h
" "
还是 < >
?
#include
有两种形式来指出要插入的文件" "
要求编译器首先在当前目录(.c 文件所在目录)寻找这个文件;如果没有,再去编译器指定的目录寻找。自己的头文件用< >
让编译器只在指定位置寻找 。系统的头文件用
- 编译器知道自己的标准库的头文件在哪里
- 环境变量 和 编译器命令行参数也可以指定寻找头文件的目录
#include
的误区
#include
不是用来引入库的stdio.h
中只有函数的声明,函数的定义在其他的地方- C语言编译器默认会引入所有标准库
#include<stdio.h>
的作用其实就是将 这个头文件的所有内容 插入到这个文件中来。目的是让编译器知道你使用的函数时所给的参数是否正确。(类似函数的声明)
为什么不引用 stdlib.h
依然可以使用 malloc
?
这时因为在你调用函数前没有声明函数(引入头文件),编译器回去猜测 参数 和 函数返回类型都为 int
型
恰好 malloc
的参数 size_t
是 long int
,返回值是个指针,也可以看作是 16进制的 整型。
头文件
- 使用和定义函数的地方都应该包含这个头文件
- 将 函数声明 全局变量 放入
.h
文件
不对外公开的 函数&变量
函数&全局变量前加上 static
就使得这个 函数/变量 只能在当前文件中被使用
声明
extern
当一个c 文件想调用另一个 c文件中定义的全局变量时
需要在头文件中加上 extern <类型> <变量名>
来声明这个变量
例如:
<1.c> |
声明不产生代码
避免重复声明
请看下例:
<1.h> |
如何避免上述这种重定义情况?
条件编译和宏
- 运用条件编译和宏,保证这个头文件在一个编译单元中只会被 include 一次
#pragma once
也能起到相同作用,但不是所有的编译器都支持
这就是我们前面说的预定义的宏的一种使用方法。
应用这种方法我们再看上例
<1.h> |