【C语言笔记】变量
1. 变量的分类
局部变量:在代码块中定义的变量叫做局部变量。局部变量具有临时性。局部变量的作用域是。进入代码块中,没有被
static
修饰符修饰定义的变量,自动形成局部变量,退出代码块时该变量自动释放。全局变量:在所有函数外定义的变量,叫做全局变量。全局变量具有全局性。
代码块:用 {}
括起来的一个区域,就叫做代码块。花括号可以嵌套,最外层花括号定义的变量可以作用于内层花括号中,而内层花括号中定义的变量不可作用于外层花括号。
2. 变量的作用域
作用域:指的是该变量的可以被正常访问的代码区域,也就是变量的有效区域
- 局部变量:只在本代码块内有效,从
定义
的位置起,到代码块结束。 - 全局变量:整个程序运行期间,都有效,从
声明
的位置起,直到文件结束。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20int a = 1;
void func1 (void)
{
a++;
}
int b;
void func2(void)
{
a--;
if (expr) {
b = 1;
do {
int b = 2;
printf("b = %d\n", b); //输出 b = 2
} while(0);
}
} - a为全局变量,作用域从定义的一行起到文件结束,func1和func2都可以访问。
- func2上方声明的b为全局变量,作用域从定义的一行起到文件结束,只有func2可以访问。
- do-while结构中声明的b为局部变量,作用域为do while的整个结构,do while结束,则该变量被释放。若局部变量与全局变量同名,则在该局部变量的作用域内优先使用局部变量。
3. 变量的生命周期
生命周期:指的是该变量从定义到被释放的时间范围,所谓的释放,指的是曾经开辟的空间”被释放”。
- 局部变量:进入代码块,形成局部变量”开辟空间”,退出代码块,”释放”局部变量。
- 全局变量:定义完成之后,程序运行的整个生命周期内,该变量一直都有效。
作用域和生命周期的区别:作用域更多的是指变量有效区域,也就是变量在哪里可以被访问到,而生命周期是一个时间概念,是指变量什么时候开辟和释放。
4. 变量的存储方式
- 静态存储:在被分配内存单元后,内存单元的地址一直保持不变,不会被释放,直到程序结束。
- 动态存储:在程序执行的时候,使用他的时候才分配内存单元,使用完毕后立即释放,再使用在分配。
4.1. 存储方式的修饰符
4.1.1 auto
在C语言中,变量的声明默认使用该关键字,变量自动使用默认的存储方式,一般不写明该关键字。
全局变量全部使用静态存储方式,局部变量默认使用动态存储方式。
4.1.2 static
内部静态变量声明关键字,使用该关键字声明的变量使用静态存储方式。
4.1.3 register
声明寄存器变量关键字,该关键字只能对局部非静态变量使用,即存储方式为动态存储方式,被该关键字声明的变量为寄存器变量,直接存储在CPU的寄存器中,提高该变量的访问效率。寄存器变量的类型只能是cha、int或指针类型。
5. 变量的链接属性
5.1. 翻译单元
符号(比如函数名或者变量)可以在其作用域内多次声明,但是只能定义一次,这就是 ODR
(一个定义规则)。
变量的声明和定义往往是一起的,但可以使用 extern
关键字声明外部全局变量。
1 | int a=0; //声明、定义、初始化全部完成 |
程序是由一个或者多个翻译单元组成。
翻译单元由实现文件以及它直接或者间接包含的所有标头组成。
实现文件通常具有
c
、cpp
或cxx
的文件扩展名。标头文件通常具有
h
或hpp
的文件扩展名。
每个翻译单元都是由编译器单独编译的,编译完成之后,链接器会将编译的翻译单元合并到一个程序中,ODR的冲突通常显示为链接器错误。当同一名称在不同的翻译单元中具有两个不同的定义时,将会发生链接器链接错误。链接的概念仅适用于全局变量。链接的概念不适用于在代码块内声明的变量。
通常,使全局变量在多个文件中可见的最佳方式是将其放在标头文件中,然后在每个需要引用该变量的源文件中添加 #include
指令,将其包含到源文件中,通过在标头内容中添加 #ifndef
的声明保护,可以确保它声明的名称只被定义一次。
当然也可以通过 #include
一个源文件将其他源文件的内容置于同一个翻译单元中,但必须保证在进行翻译单元的链接时,所有的翻译单元中仅有一次定义。
5.2. 外部链接(extern)
在程序的任何翻译单元中都可见,全局变量使用 extern
关键字进行声明。具有外部链接属性的变量,在所有翻译单元中只能被定义一次。
5.3. 内部链接(static)
只能在定义它的翻译单元中可见,全局变量使用 static
关键字进行声明。具有内部链接属性的变量,可以在其他翻译单元存在定义。
6. 变量的数据类型
数据类型用于定义变量在内存中所占用的内存大小,不同位数的编译器中数据的基本类型所占用的内存大小会有所不同,即数据类型所占用的空间大小由使用的编译器决定,可通过 sizeof
关键字求变量所占用的内存大小。
但有几条铁定的原则( ANSI/ISO
制订):
1 | sizeof(short int) <= sizeof(int) |
6.1. 基本类型
关键字 | 类型 | 16位编译器 unit:Byte |
32位编译器 unit:Byte |
64位编译器 unit:Byte |
---|---|---|---|---|
signed char | 有符号字符型 | 1 | 1 | 1 |
unsigned char | 无符号字符型 | 1 | 1 | 1 |
signed short | 有符号短整型 | 2 | 2 | 2 |
unsigned short | 无符号短整型 | 2 | 2 | 2 |
signed int | 有符号整型 | 2 | 4 | 4 |
unsigned int | 无符号整型 | 2 | 4 | 4 |
signed long | 有符号长整型 | 4 | 4 | 8 |
unsigned long | 无符号长整型 | 4 | 4 | 8 |
float | 单精度浮点型 | 4 | 4 | 4 |
double | 双精度浮点型 | 8 | 8 | 8 |
void * | 指针变量 | 2 | 4 | 8 |
6.2. 空类型
6.2.1 void
指示对象为空类型(无类型),即对象无可用的值。常用于函数的返回值或形参列表,表示函数无返回值,或函数调用时不需要传入参数。不可用于变量的声明。
sizeof(void) = 1
6.3. 构造类型
6.3.1. 数组
6.3.1.1. 数组的定义
1 | char str1[5]; //一维字符型数组,该数组所占用的内存大小为5个字节 |
注意:在使用较老的编译器时(仅支持C99之前标准的编译器),在定义数组时,数组的元素个数([]内的数)需是常量表达式(C99标准开始支持变长数组,数组元素的个数可以是变量)。
6.3.1.2. 数组的初始化
数组的初始化即给数组里放一些初始值,数组使用{}进行初始化,在大小给定的情况下,可以完全初始化,也可以不完全初始化(其余默认放0),在大小没有给定的情况下根据初始化内容确定数组的大小。
1 | //声明、定义并初始化一个大小为5字节的字符类型数组,内容为"hello" |
C语言中字符串的存储是以 '\0'
结束的,使用 ""
包含的字符为字符串常量,在内存中其最后一个位置处会被自动填充 '\0'
。
6.3.2. enum
有些数据的取值往往是有限的,只能是非常少量的整型值,并且最好为每个值都取一个名称,以方便在后续代码中使用。这时就需要用枚举类型来为这些整型值定义一个明确含义的名称。枚举值默认从 0 开始,往后逐个加 1(递增)。
6.3.2.1. 枚举类型的定义
1 | //定义一个名叫week的枚举类型,其成员的值分别为{0, 1, 2, 3, 4, 5, 6, 7} |
6.3.2.2. 枚举变量的定义
1 | //定义一个类型为week枚举类型的变量today,未初始化 |
6.3.2.3. 枚举变量的赋值
1 | //使用week枚举变量的成员为today赋值,此时today = 0 |
6.3.3. struct
结构体类型,将不同类型的数据组合成一个有机的整体。结构体的成员可以为变量,也可以是结构体变量,成员变量名可以程序中结构体外的定义的变量名相同。结构体变量所占内存长度是各成员占内存长度之和,每个成员分别占有其自己的内存单元。在实际的操作系统中存在内存对齐,结构体的整体大小是结构体中占用最大字节的成员的整数倍。
6.3.3.1. 结构体类型的定义
1 | //定义一个名叫student的结构体类型,成员有数组类型的name,无符号整型number,单精度浮点型score |
6.3.3.2. 结构体变量的定义
1 | //定义一个student结构体类型的变量xiaoai,未初始化 |
6.3.3.3. 结构体变量的初始化与赋值
1 | //定义一个student结构体类型的变量xiaoai,并初始化 |
6.3.4. union
联合体类型将不同类型的变量存放到同一段内存单元中,联合体变量的成员存放在同一个地址开始的内存单元,其首地址都相同,变量所占内存长度等于最长的成员的长度。联合体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。
6.3.4.1. 联合体类型的定义
1 | //定义一个名叫bin的联合体类型,sizeof(union bin) = 4 |
6.3.4.2. 联合体变量的定义
1 | //定义一个bin联合体类型的变量value,未初始化 |
6.3.4.3. 联合体变量的初始化与赋值
1 | //直接初始化 |
6.4. 指针类型
C语言中将地址形象化的称为”指针”,一个变量的地址称为该变量的”指针”。指针是一个地址,而指针类型的变量是存放地址的变量。指针类型所占用的内存大小与操作系统的位数有关,基本上指针类型所占用的空间大小bit数等于操作系统的位数。
6.4.1. 指针类型变量的定义
1 | //定义一个指向char数据类型的指针变量pval8,该变量存放的值是char类型变量的首地址 |
6.4.2. 指针类型变量的赋值
1 | char c = 8; |
6.5. 自定义类型
6.5.1. typedef
typedef 关键字是为一种数据类型定义一个新名字,这里的数据类型包括基本数据类型(int,char等)、空类型、构造类型(struct等)。typedef 本身是一种存储类的关键字,与 auto、extern、static、register 等关键字不能出现在同一个表达式中。使用 typedef 定义的变量类型,其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置)。
1 | //自定义一个新数据类型u8,该类型等效 unsigned char |
7. 变量的类型限定符
7.1. const
const声明的变量为只读变量,在定义时必须初始化,且定义后不能在只读变量所在的作用域中被改变。
1 | //在定义时初始化,此后PI的值无法被修改 |
- 当一个
const
修饰的全局变量的值,被意外的改变时会引发程序崩溃。
7.2. volatile
volatile声明的变量为易变的变量。用这个限定符主要是让编译器在优化代码的时候不能优化此变量的取值,需要从原始位置进行取值。
1 |
|
当为编译器添加了 -O
选项后,编译器对这部分代码进行了优化,编译器在编译时发现有两个变量都使用了a,但是a的值在这之间并没有被使用。于是编译器在将a的值取出来之后,临时存放到了寄存器中,当变量c需要使用a的时候,编译器直接从寄存器中读取a的值,而不是从存储a的原始位置直接读取。结果就是输出打印 a = 1, b = 1, c = 1
,而不是准确的 a = 20, b = 1, c =20
。
在C语言项目中优化选项是必须的,而那些代表外部寄存器的值的变量的变化编译器是无法感知的,所以需要在这类变量前加上 volatile
关键词进行修饰,避免变量被编译器优化。
volatile对应的变量可能在你的程序本身不知道的情况下发生改变,比如在多线程的程序中,共同访问的内存当中,多个程序都可以操纵这个变量,编译器是无法判定何时这个变量会发生变化,当变量表示一个外部设备的某个状态对应,当外部设备发生操作的时候,通过驱动程序和中断事件,系统改变了这个变量的数值,而编译器也是无法得知的。
对于volatile类型的变量,系统每次用到他的时候都是直接从对应的内存当中提取,而不会利用cache当中的原有数值,以适应它的未知何时会发生的变化,系统对这种变量的处理不会做优化——显然也是因为它的数值随时都可能变化的情况。
const 与 volatile 可同时修饰变量。