本文为刚毕业参加工作时,初学C语言时做的一些笔记,适合初学者读阅,内容不保证百分百正确,若您对其中的描述有任何疑问可在留言板块进行留言。

以下为我使用的C语言的运行和编译环境:
System Version : Ubuntu22.04.1
Linux kernel Version : 5.19.0
GCC Version : 11.3.0
STDC Version : 201710L

摘要: 简单的描述了C语言中的变量的分类、作用域、生命周期等内容,对staticexternconstvolaite关键字对变量的作用进行了简要分析。

1. 变量的分类

  • 局部变量:在代码块中定义的变量叫做局部变量。局部变量具有临时性。局部变量的作用域是。进入代码块中,没有被 static 修饰符修饰定义的变量,自动形成局部变量,退出代码块时该变量自动释放。

  • 全局变量:在所有函数外定义的变量,叫做全局变量。全局变量具有全局性。

代码块:用 {} 括起来的一个区域,就叫做代码块。花括号可以嵌套,最外层花括号定义的变量可以作用于内层花括号中,而内层花括号中定义的变量不可作用于外层花括号。

2. 变量的作用域

作用域:指的是该变量的可以被正常访问的代码区域,也就是变量的有效区域

  • 局部变量:只在本代码块内有效,从 定义 的位置起,到代码块结束。
  • 全局变量:整个程序运行期间,都有效,从 声明 的位置起,直到文件结束。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    int 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
2
3
4
5
int a=0; //声明、定义、初始化全部完成

int b; //声明和定义,未进行初始化

extern int b; //声明,声明变量b是具有外部链接属性的变量,一般放在头文件中

程序是由一个或者多个翻译单元组成。

  • 翻译单元由实现文件以及它直接或者间接包含的所有标头组成。

  • 实现文件通常具有 ccppcxx 的文件扩展名。

  • 标头文件通常具有 hhpp 的文件扩展名。

每个翻译单元都是由编译器单独编译的,编译完成之后,链接器会将编译的翻译单元合并到一个程序中,ODR的冲突通常显示为链接器错误。当同一名称在不同的翻译单元中具有两个不同的定义时,将会发生链接器链接错误。链接的概念仅适用于全局变量。链接的概念不适用于在代码块内声明的变量。

通常,使全局变量在多个文件中可见的最佳方式是将其放在标头文件中,然后在每个需要引用该变量的源文件中添加 #include 指令,将其包含到源文件中,通过在标头内容中添加 #ifndef 的声明保护,可以确保它声明的名称只被定义一次。

当然也可以通过 #include 一个源文件将其他源文件的内容置于同一个翻译单元中,但必须保证在进行翻译单元的链接时,所有的翻译单元中仅有一次定义。

5.2. 外部链接(extern)

在程序的任何翻译单元中都可见,全局变量使用 extern 关键字进行声明。具有外部链接属性的变量,在所有翻译单元中只能被定义一次。

5.3. 内部链接(static)

只能在定义它的翻译单元中可见,全局变量使用 static 关键字进行声明。具有内部链接属性的变量,可以在其他翻译单元存在定义。

6. 变量的数据类型

数据类型用于定义变量在内存中所占用的内存大小,不同位数的编译器中数据的基本类型所占用的内存大小会有所不同,即数据类型所占用的空间大小由使用的编译器决定,可通过 sizeof 关键字求变量所占用的内存大小。

但有几条铁定的原则( ANSI/ISO 制订):

1
2
3
4
sizeof(short int) <= sizeof(int)
sizeof(int) <= sizeof(long int)
sizeof(short int) >= 2
sizeof(long int) >= 4

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
2
3
char str1[5];      //一维字符型数组,该数组所占用的内存大小为5个字节
int counts[5]; //一维整型数组,该数组所占用的内存大小为 5*sizeof(int) 个字节
long money[5][10]; //二维长整型数组(5行10列),该数组所占用的内存大小为 5*10*sizeof(int) 个字节

注意:在使用较老的编译器时(仅支持C99之前标准的编译器),在定义数组时,数组的元素个数([]内的数)需是常量表达式(C99标准开始支持变长数组,数组元素的个数可以是变量)。

6.3.1.2. 数组的初始化

数组的初始化即给数组里放一些初始值,数组使用{}进行初始化,在大小给定的情况下,可以完全初始化,也可以不完全初始化(其余默认放0),在大小没有给定的情况下根据初始化内容确定数组的大小。

1
2
3
4
5
6
7
8
//声明、定义并初始化一个大小为5字节的字符类型数组,内容为"hello"
char str1[5] = {'h', 'e', 'l', 'l', 'o'};

//声明、定义并初始化一个大小为5字节的字符类型数组,内容为"world"
char str2[] = {'w', 'o', 'r', 'l', 'd'};

//声明、定义并初始化一个大小为13字节的字符类型数组,内容为"hello world!\0"
char str3[] = "hello world!";

C语言中字符串的存储是以 '\0' 结束的,使用 "" 包含的字符为字符串常量,在内存中其最后一个位置处会被自动填充 '\0'

6.3.2. enum

有些数据的取值往往是有限的,只能是非常少量的整型值,并且最好为每个值都取一个名称,以方便在后续代码中使用。这时就需要用枚举类型来为这些整型值定义一个明确含义的名称。枚举值默认从 0 开始,往后逐个加 1(递增)。

6.3.2.1. 枚举类型的定义

1
2
3
4
5
6
7
8
//定义一个名叫week的枚举类型,其成员的值分别为{0, 1, 2, 3, 4, 5, 6, 7}
enum week{Mon, Tues, Wed, Thurs, Fri, Sat, Sun};

//定义一个名叫dev_id的枚举类型,其成员的值分别为{2, 3, 4}
enum dev_id{OPT_ID = 2, FPGA_ID = 3, DPD_ID = 4};

//定义一个名叫err_code的枚举类型,其成员的值分别为{-1, 0, 1}
enum err_code{failed = -1, ok, unexpected};

6.3.2.2. 枚举变量的定义

1
2
3
4
5
//定义一个类型为week枚举类型的变量today,未初始化
enum week today;

//定义一个类型为BOOLEAN枚举类型的变量result,未初始化
enum BOOLEAN{FALSE = 0, TRUE = 1}result;

6.3.2.3. 枚举变量的赋值

1
2
3
4
5
//使用week枚举变量的成员为today赋值,此时today = 0
today = Mon;

//定义一个类型为dev_id枚举类型的变量dev, 并初始化为FPGA_ID,此时 dev = 3
enum dev_id dev = FPGA_ID;

6.3.3. struct

结构体类型,将不同类型的数据组合成一个有机的整体。结构体的成员可以为变量,也可以是结构体变量,成员变量名可以程序中结构体外的定义的变量名相同。结构体变量所占内存长度是各成员占内存长度之和,每个成员分别占有其自己的内存单元。在实际的操作系统中存在内存对齐,结构体的整体大小是结构体中占用最大字节的成员的整数倍。

6.3.3.1. 结构体类型的定义

1
2
3
4
5
6
//定义一个名叫student的结构体类型,成员有数组类型的name,无符号整型number,单精度浮点型score
struct student {
char name[20];
unsigned int number;
float score;
};

6.3.3.2. 结构体变量的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个student结构体类型的变量xiaoai,未初始化
struct student xiaoai;

//定义school结构体类型的变量qinghua和beida, 未初始化
struct school
{
char addr[100];
unsigned short NO;
}qinghua, beida;

//定义一个匿名结构类型的变量man,未初始化
struct
{
struct student student;
struct school school;
}man;

6.3.3.3. 结构体变量的初始化与赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//定义一个student结构体类型的变量xiaoai,并初始化
struct student xiaoai = {"xiaoai", 0x170222, 59.0f};

//使用已定义的结构体变量xiaoai去初始化结构体变量xiaowen
struct student xiaowen = xiaoai;

//C风格初始化
struct student xiaohua = {.number = 0x170223, .name = "xiaohua", .score = 60.0f};

//C++风格初始化
struct student xiaofan = {number : 0x170224, score : 90.0f, name : "xiaofan"};

//赋值
xiaoai.score = 75.0f;
man = {xiaoai, beida};

6.3.4. union

联合体类型将不同类型的变量存放到同一段内存单元中,联合体变量的成员存放在同一个地址开始的内存单元,其首地址都相同,变量所占内存长度等于最长的成员的长度。联合体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员就失去作用。

6.3.4.1. 联合体类型的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//定义一个名叫bin的联合体类型,sizeof(union bin) = 4
union bin {
char val8;
short val16;
int val32;
float f32;
};

//定义一个名叫data的联合体类型,sizeof(long) = 8, sizeof(union data) = 16
union data {
char bin[16];
struct {
char id;
char sex;
short age;
int number;
long hash;
};
};

//若调整定义变量的顺序, 则此时 sizeof(union data) = 24
union data {
char bin[16];
struct {
char id;
char sex;
short age;
long hash;
int number;
};
};

6.3.4.2. 联合体变量的定义

1
2
3
4
5
6
7
8
//定义一个bin联合体类型的变量value,未初始化
union bin value;

//定义一个匿名联合体类型的变量key,未初始化
union {
char c;
int a;
}key;

6.3.4.3. 联合体变量的初始化与赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//直接初始化
union bin bit8 = {10};

//指定成员初始化
union bin bit16 = {.short = 10};

//使用已定义的同类型联合体初始化
union bin bit32 = bit8;

//赋值
union bin bit32f;
bit32f.val32 = 0xff00;
bit32f.val8 = 0xff;

printf("bit32f.val32 = %x\n", bit32f.val32);
// 输出 bit32f.val32 = ffff

6.4. 指针类型

C语言中将地址形象化的称为”指针”,一个变量的地址称为该变量的”指针”。指针是一个地址,而指针类型的变量是存放地址的变量。指针类型所占用的内存大小与操作系统的位数有关,基本上指针类型所占用的空间大小bit数等于操作系统的位数。

6.4.1. 指针类型变量的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义一个指向char数据类型的指针变量pval8,该变量存放的值是char类型变量的首地址
char *pval8;

//定义一个指向float数据类型的指针变量pval32,该变量存放的值是float类型变量的首地址
float *pval32;

//定义一个空类型的指针变量pvoid,该变量存放的值是一个未知数据类型的变量的首地址
void *pvoid;

//定义一个指向enum week数据类型的指针变量pweek,该变量存放的值是enum week类型变量的首地址
enum week *pweek;

//定义一个指向struct student数据类型的指针变量pstu,该变量存放的值是struct student类型变量的首地址
struct student *pstu;

//定义一个指向union bin数据类型的指针变量pbin,该变量存放的值是union bin类型变量的首地址
union bin *pbin;

6.4.2. 指针类型变量的赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
char c = 8;

//将变量c的地址赋值给指针变量pval8
char *pval8 = &c;

//C语言中NULL是((void *)0)的宏定义,即将指针变量赋空值,不指向任何有效地址
int *pval32 = NULL;

//将char类型的变量地址值赋值给空类型指针
void *pvoid = (void *)&c;

//将int类型的指针变量的值赋值给union bin类型的指针变量,并显示的转化其指针类型
union bin *pbin = (union bin *)pval32;

6.5. 自定义类型

6.5.1. typedef

typedef 关键字是为一种数据类型定义一个新名字,这里的数据类型包括基本数据类型(int,char等)、空类型、构造类型(struct等)。typedef 本身是一种存储类的关键字,与 auto、extern、static、register 等关键字不能出现在同一个表达式中。使用 typedef 定义的变量类型,其作用范围限制在所定义的函数或者文件内(取决于此变量定义的位置)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//自定义一个新数据类型u8,该类型等效 unsigned char
typedef unsigned char u8;

//自定义一个新数据类型u16,该数据类型等效 unsigned short
typedef unsigned short u16;

//自定义一个新数据类型string,该类型等效 unsigned char *
typedef u8 *string;

//自定义一个新数据类型student_t,该类型等效 struct student
typedef struct student student_t;

//自定义一个新数据类型path,该类型是一个大小为64的数组,等效 unsigned char [64];
typedef u8 path_t[64];

//自定义类型的使用
u8 id = 8;
string str = "hello world";
student_t xiaoai = {"xiaoai", 170222, 59.0f};
path_t local_path = "/home/k0191/c-study";

7. 变量的类型限定符

7.1. const

const声明的变量为只读变量,在定义时必须初始化,且定义后不能在只读变量所在的作用域中被改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//在定义时初始化,此后PI的值无法被修改
const float PI = 3.1415926;

char local_path1[64] = "/home/k0191/c-study";
char local_path2[64] = "/home/k0191/cpp-study";

//常量指针,无法通过c_path指针去修改数组local_path1的值,但可以修改c_path指针的值
const char *c_path = local_path1;

//const char *与char const *等效
c_path = local_path2;

//指针常量,不可修改_cpath指针的值,但可以通过_cpath指针修改local_path1的值
char * const _cpath = local_path1;
_cpath[6] = 'y';

//不可修改ccpath指针的值,也不可用通过ccpath指针修改local_path1的值
const char * const ccpath = local_path1;
  • 当一个 const 修饰的全局变量的值,被意外的改变时会引发程序崩溃。

7.2. volatile

volatile声明的变量为易变的变量。用这个限定符主要是让编译器在优化代码的时候不能优化此变量的取值,需要从原始位置进行取值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(int argc, char **argv)
{
int a = 1;
int b = a;
int *ptr = &a;

//使用汇编,通过指针ptr修改变量a的值,让编译器无法感知a的值发生变化
asm ("mov %[var], (%[ptr])" : : [ptr] "r", [var] "r" (20));

int c = a;

printf("a = %d, b = %d, c = %d\n", a, b, c);

return 0;
}

当为编译器添加了 -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 可同时修饰变量。