LV005-变量简介
前边我们知道,数据都会以二进制的形式存在于内存中,那想要使用这个数据怎么办?现实生活中我们会找一个盒子或者袋子来存放物品,既显得整洁,也方便以后找到。计算机也是这样,我们需要先在内存中找一块区域,规定用它来存放某个数据,那么我们怎么找到这个数据?一来可以通过内存中这个区域的编号(后面会学习到,称为地址或者指针)找到,二来就是给这个数据起一个名字,方便以后查找。这个数据的名字,就叫做 变量。变量在内存空间中的首地址,称为 变量的地址。
Tips:变量是一个标识符,代表的是这个数据。
一、变量的定义
在 C 语言中,声明变量的一般形式是:
<存储类型> <数据类型 > <变量名> ;【说明】
| 存储类型 | 有四种,分别是 auto、register、static 和 extern |
| 数据类型 | 可以是基本数据类型,也可以是自定义的数据类型 |
| 变量名 | 由字母、数字、下划线组成,不能以数字开头,不能和 C 语言的关键字重名。 |
int a; /* 声明了一个整型变量 a */
char b; /* 声明了一个字符型变量 b */【注意】
(1)变量在声明的时候可以一次声明多个。例如,
int a, b, c;(2)声明变量是一条语句,注意后边的分号( ; )不可缺少。
二、变量的使用
1. 变量的引用
当我们想要使用一个变量的时候,就 直接使用变量名 即可。例如,
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a;
printf("a = %d\n", a);
return 0;
}此时在命令行执行以下命令:
gcc test.c -Wall # 编译程序这个时候会发现程序有警告:
test.c:7:2: warning: ‘a’ is used uninitialized in this function [-Wuninitialized]说的就是这个变量没有进行初始化,下边我们就来看一看如何初始化变量。
2. 变量的赋值
我们上边仅仅是定义了一个变量,还没有放数据进去,在编译的时候就报了警告。变量的赋值有两种方式:
2.1 赋值方式一
- 在声明变量之后再进行单独赋值
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a;
a = 10;
printf("a = %d\n", a);
return 0;
}此时在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件这个时候会发现终端有以下信息输出:
a = 102.2 赋值方式二
- 在声明变量的同时进行赋值,这也被称为 变量的初始化。
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 10;
printf("a = %d\n", a);
return 0;
}此时在终端执行以下命令:
gcc test.c -Wall # 编译程序
./a.out # 执行可执行文件这个时候会发现终端有以下信息输出:
a = 10其实下边的写法是等价的
int a = 10
// 等价于
int a;
a = 10;三、变量的作用域
变量作用域的定义:程序中可以访问一个指示符的一个或者多个区域,即变量出现的有效区域决定了程序的哪些部分通过变量名来访问变量。其实我自己的理解就是:作用域就是这个变量的生存区域,在这个区域内变量有效,区域外变量无法访问。
1. 变量作用域类别
- (1)函数原型作用域
它指的是在 声明函数原型时所指定的参数标识符的作用范围。因为作用范围是小括号内,所以函数原型声明中的标识符可以与函数定义中说明的标识符名称不同。只要让函数声明和函数定义中小括号内每个变量的类型及数目一致即可,也可以省略掉参数名(函数定义在后,调用在前),例如,
int func(int a, char b);- (2)块作用域
块作用域也称为局部作用域,也就是语句块的作用域。一般在函数中起到分割的作用。
{
int a = 5; /* a 的作用域起始处 */
if(a > 0)
{
int b; /* b 的作用域起始处 */
b = a - 3;
} /* b 的作用域结束处 */
}/* a 的作用域结束处 */- (3)函数作用域
在函数内部定义的一些变量,只能在函数内部使用,一旦离开了这个函数,就必须重新定义。其实可以把函数看做一个块,然后根据作用域的定义。例如,
int func(int x, int y)
{
int a = 5; /* a 的作用域起始处 */
if(a > 0)
{
int b; /* b 的作用域起始处 */
b = a - 3;
} /* b 的作用域结束处 */
}/* a 的作用域结束处 */- (4)文件作用域
在所有函数外定义的标识符称为全局标识符,定义的变量称为 全局变量。全局标识符的作用域是文件作用域,即从它声明开始到文件结束都是可见的。标识符的文件作用域一般有三种情况:
①、全局常量或全局变量的作用域是从定义开始到源程序文件结束。例如,
int a = 10;
int main()
{
// ... ...
return 0;
}②、函数的定义中包含了函数声明,所以一旦声明了函数原型,函数标识符的作用域就从定义开始到源程序文件结束。例如,
int func1(); /* 函数 fun1 的作用域从此开始到文件结束 */
int func2() /* 函数 fun2 的作用域从此开始到文件结束 */
{
// ... ...
return 0;
}
int main()
{
// ... ...
return 0;
}
int func1()
{
// ... ...
return 0;
}③、还有一种在头文件中定义的标识符,预编译时,编译器会将头文件的内容在源文件的相应位置展开,所以在头文件中定义的标识符的作用域可以看成从 #include 头文件开始的位置到源程序文件结束。
2. 作用域的重叠会发生什么?
当 标识符的作用域完全相同时,不允许出现相同的标识符名,而当 标识符有不同作用域时允许标识符同名。如果是作用域嵌套的情况下,如果内层和外层的作用域声明了同名的标识符,那么在外层作用域中声明的标识符对于该内层作用域时不可见的。也就是说,在内层中声明的变量 i 和外层变量 i 无关,当内层变量改变时,与之同名的外层变量的值不受影响。例如
#include <stdio.h>
int main(int argc, char *argv[])
{
int a = 5;
{
int a = 7;
printf("in{} :a=%d\n", a);
a = 10;
}
printf("out{}: a=%d\n", a);
return 0;
}我们编译后运行,会得到以下输出情况:
in{} :a=7
out{}: a=53. 不同存储类型变量的作用域
| 存储类别 | 存储期 | 作用域 | 声明方式 |
|---|---|---|---|
| auto | 动态 | 块 | 块内 |
| register | 动态 | 块 | 块内,使用关键字 register |
| static(局部) | 静态 | 块 | 块内,使用关键字 static |
| static(全局) | 静态 | 文件内部 | 所有函数外,使用关键字 static |
| extern | 静态 | 文件外部 | 所有函数外 |
四、变量的存储模型
变量是程序中数据的存储空间的抽象。变量的存储方式(也可以被称之为 存储期)可分为 静态存储 和 动态存储 两种:
- 静态存储变量,通常是在程序编译时就分配一定的存储空间并一直保特不变,直至整个程序结束。全局变量的存储方式就属于这种存储方式
- 动态存储变量,是在程序执行过程中使用它时才分配存储空间,使用完毕的时候内存空间将会被立即释放。
变量的 存储类型 大概分为四种:自动类型、寄存器类型、静态类型 和 外部类型。变量的存储模型若由 作用域、链接点和 存储期(存储期描述的是变量在内存中的生存时间)三大属性来描述的话,变量的存储模型可以分为五种,这五种其实就相当于把四种存储类型细分了,下边将详细介绍。
Tips:链接属性的正确分类(C 语言标准定义)
链接属性 英文 说明 外部链接 external linkage 标识符可在其他源文件中引用 内部链接 internal linkage 标识符仅在定义它的源文件内可见 无链接 no linkage 标识符只能在定义它的作用域内使用
1. 自动类型
自动变量就是指 非静态局部变量,自动变量声明的语法格式为
[auto] 数据类型 变量名;auto 为存储类说明符,它是可选的,一般我们会选择省略。说明这个变量为自动变量,这种自动变量具有 动态存储期、代码块的作用域和 空链接。
【自动变量的特点】
(1)自动变量的 作用域仅限于定义该变量的模块内。在函数中定义的自动变量,只在该函数内有效。在复合语句中定义的自动变量,只在该复合语句中有效。
(2)自动变量属于 动态存储方式,只有在定义该变量的函数被调用时才给它分配存储单元,开始它的生存期。函数调用结束,释放存储单元,结束生存期。因此函数调用结束之后,自动变量的值不能保留。在复合语句中定义的自动变量,在退出复合语句后也不能再使用,否则将引起错误。
(3)由于自动变量的作用域和生存期都局限于定义它的模块内(函数或复合语句内),因此 不同的模块中允许使用同名的变量 而不会混淆。即使在函数内定义的自动变量也可与该函数内部的复合语句中定义的自动变量同名,但我们应尽量避免使用这种方式。
【注意】
(1)代码块或者函数头部定义的变量,可以使用存储类修饰符 auto 来明确标识属于自动存储类型。若没有 auto 修饰,默认也是自动类型。
(2) auto 型变量如果 没有进行初始化,那么它默认是随机值。
2. 寄存器类型
在一个代码块内(或在一个函数头部作为参量)使用修饰符 register 声明的变量属于 寄存器存储类。一般声明格式为:
register 数据类型 变量名;register 修饰符暗示编译程序相应的变量 将被频繁使用,如果可能的话,应将其保存在 CPU 的寄存器中,从而加快其存取速度。该类与自动存储类相似,具有 自动存储期、代码块作用域和 空链接。
【使用 register 修饰符有几点限制】
(1) register 变量必须是能被 CPU 寄存器所接受的类型,这通常意味者 register 变量必须是一个单个的值,并且其长度应小于或等于整型的长度,这与处理器的类型有关。
(2)声明为 register 仅仅是一个请求,而非命令,因此变量仍然可能是普通的自动变量,没有放在寄存器中。
(3)只有局部变量和形参可以作为 register 变量,全局变量不行:
(4)实际上有些系统并不把 register 变量存放在奇存器中,而优化的编译系统则可以自动识别使用频繁的变量而把它们放在奇存器中。
【注意】
(1)不能用 & 来获取 register 变量的地址。
(2) register 型变量 如果没有进行初始化,那么它默认是随机值。
3. 静态类型
静态变量的类型说明符是 static 。它分为两种,一种是静态局部变量(静态、无链接),一种是静态全局变量(静态、内部链接)。静态局部变量属于静态存储方式。
3.1 静态、无链接
在一个 代码块 内使用存储类修饰符 static 声明的局部变量属于静态无链接存储类。该类具有 静态存储期、代码块作用域和 无链接。一般格式如下:
int func()
{
static 数据类型 变量名;
}静态变量的存储空间是在编译完成后就分配了,并且在程序运行的全部过程中都不会撤销。但是 属于静态存储方式的变量不一定就是静态变量。例如,外部变量虽属于静态存储方式,但不一定是静态变量,必须由 static 加以定义后才能称为静态外部变量,或称静态全局变量。

静态局部变量在函数内定义,它的生存期为整个程序执行期间,但是其 作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后,尽管该变量还继续存在,但不能使用它。静态局部变量生存周期与作用域如下图:

根据静态局部变量的特点,可以看出它是一种 生存期为整个程序运行期 的变量。虽然离开定义它的函数后不能使用,但如再次调用定义它的函数时,它又可以继续使用,并且保留了上次被调用后的值。因此,当多次调用一个函数且要求在调用之前保留某些变量的值时,可以考虑采用静态局部变量。虽然用全局变量也可以达到上述目的,但全局变量有时会造成意外的副作用,因此仍以采用静态局部变量更好些。
【注意】
(1)可以对构造类静态局部量赋初值,例如数组。若未赋初值,则由 系统自动初始化 为 0 。基本数据类型的静态局部变量若在说明时未赋初值,则 系统自动 赋予 0 。
3.2 静态、内部链接
全局变量在关键字之前再加上 static 就构成了 静态的全局变量,属于静态、内部链接存储类。与静态、外部链接存储类不同的是,具有内部链接,使得这 种变量仅能被与它在同一个文件的函数使用。一般格式如下:
static 数据类型 变量名;
int main()
{
}这样的变量也是只会在在编译时初始化一次。如若未明确初始化,它的初始值被设定为 0 。
非静态全局变量(不加 static 的全局变量)的作用域是整个源程序,但当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的;而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用。由于静态全局变量的作用域局限于一个源文件内,只能被该源文件内的函数使用,因此可以避免在其他源文件中引起错误。两者作用域区别如下图:

非静态的要加extern才能在其他文件中被访问。
4. 外部类型
未使用 static 修饰的全局变量属于静态、外部链接存储类。具有 静态存储期、文件作用域 和 外链接。一般格式如下:
数据类型 变量名;
int main()
{
}只会在在编译时初始化一次。如若未明确初始化,它的初始值也被设定为 0 。在使用外部变量的函数中使 extern 关键字来再次声明。如果是在其他文件中定义的,则必须使用 extern 。
5. static变量加上了extern?
有个疑问,一个static类型的全局变量,我们在另一个文件中使用extern声明它,会发生什么?若无报错,那在另一个文件中是否能访问?我们来尝试一下。
5.1 file1.c
// file1.c
#include <stdio.h>
// 尝试访问file1.c中的静态全局变量 - 这会失败
extern int global_static_var;
// 尝试访问file1.c中的普通全局变量 - 这会成功
extern int global_external_var;
void print_from_file2(void)
{
printf("file2.c - 尝试访问global_static_var...\n");
printf("file2.c - global_static_var = %d\n", global_static_var);
printf("file2.c - 尝试访问global_external_var...\n");
printf("file2.c - global_external_var = %d\n", global_external_var);
}5.2 file2.c
// file2.c
#include <stdio.h>
// 尝试访问file1.c中的静态全局变量 - 这会失败
extern int global_static_var;
// 尝试访问file1.c中的普通全局变量 - 这会成功
extern int global_external_var;
void print_from_file2(void)
{
printf("file2.c - 尝试访问global_static_var...\n");
printf("file2.c - global_static_var = %d\n", global_static_var);
printf("file2.c - 尝试访问global_external_var...\n");
printf("file2.c - global_external_var = %d\n", global_external_var);
}5.3 main.c
#include <stdio.h>
// 声明来自file1.c的函数
extern void print_from_file1(void);
extern void print_from_file2(void);
int main(void)
{
printf("=== 静态全局变量 vs 外部全局变量 测试 ===\n\n");
printf("--- 来自file1.c的输出 ---\n");
print_from_file1();
printf("\n--- 来自file2.c的输出 ---\n");
print_from_file2();
printf("\n=== 测试完成 ===\n");
return 0;
}5.4 编译
gcc -c file1.c -o file1.o
gcc -c file2.c -o file2.o
gcc main.c file1.o file2.o -o test
# 或者一步到位
gcc file1.o file2.c main.c -o test会发现直接报错:

参考资料:
Rationale for International Standard— Programming Languages— C