你好,我是goldsunC
让我们一起进步吧!
题外话
在学计算机相关知识的时候,基本上是离不开C语言的。并且C语言是绝大部分高校计算机相关专业都会学的。往往你说自己是学编程的,别人就基本默认你学过C语言啦。最近看操作系统、算法、计网啥的,发现总会跟C/C++扯上点关系,鉴于自己是大一上期学的C语言,到现在时间稍微有点久远没有用过且当时并没有学的很好,所以决定花点时间重新学习下C语言。
这篇文章总结一下这两天重学的C语言觉得重要的一些知识。
枚举常量
枚举
相信大家都很熟悉,就是"一一列举"的意思,当一些量仅有有限个数据值组成时,通常用枚举类型来表示。枚举数据类型
描述的是一组整型值的集合。在C语言中用enum
关键字来定义这种类型。
例如:
enum PeopleName{goldsunC, gold, sun, C};
enum PeopleName name;
上面第一条语句定义了名为PeopleName
的枚举数据类型,给它定义了四个不同的取值:goldsunC, gold, sun, C
。
第二条语句用该枚举类型定义了一个名为name
的变量。
name
这个变量可以被赋予四种取值中的任何一种,例如:
name = sun;
然后我们就可以在如条件语句中来使用name
了,比如如下代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
enum PeopleName{goldsunC, gold, sun, C};
enum PeopleName name;
name = sun;
if(name == sun) {
printf("Hello,sun\n");
}else{
printf("error!\n");
}
if(name == 2)
printf("666\n");
return 0;
}
上面的代码输出结果是什么?是:
Hello,sun
666
这个输出结果说明了name == sun == 2
,等等,为什么等于2?这是因为在定义枚举类型的那个花括号中,我们定义的那些goldsunC, gold
等都是整型常量
,除非特别指定,否则它们按照顺序即值为0, 1, 2 ...
依次加一。
那什么是特别指定呢?实际上你可以这样:
enum PeopleName{goldsunC = -1, gold = 1, sun = 0, C = 3}
它们它们就等于你指定的值了,在程序中可以使用这些值。
你也可以这样:
enum PeopleName{goldsunC = 2, gold, sun ,C}
我们只给第一个枚举常量指定了值为2
,那么后面的枚举常量值将会从2
依次向上递加的。
总结
虽然枚举常量就是简单的整型常量,不过可以提高程序的可读性,使用goldsunC
肯定比使用0
可读性好呀。
变量的作用域
在C语言中,被花括号括起来的区域,往往被称为语句块(Block)
。无论函数体、循环体还是分支都是语句块。我们在每一个语句块的头部都可以定义变量,这个变量的作用域(Scope)
为:每个变量仅在定义它的语句块(包含下级语句块)
内有效,并且拥有自己的内存空间
。
如下代码:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 1;
{
int a = 2;
printf("%d\n",a);
}
printf("%d\n",a);
return 0;
}
输出结果为:
2
1
也就是两个a
的值并不相同,即语句块内的a
和语句块里面的a
是不同的变量,它们各自拥有自己的内存空间,彼此毫无瓜葛。不过在实际中建议不要定义相同的变量名。不然容易混淆、导致误解。
全局变量
我们知道,main
函数也是包含一个大的语句块,那么如果我们按照变量作用域的规则,如果在与main
函数平行的位置定义一个变量,那么这个变量在整个程序中的所有位置都有效,这就是全局变量
了。那么相对而言,如果变量定义在内部的其它语句块中,它们就是局部变量
了。
需要知道的是全局变量在程序员不指定初值的情况下会自动初始化,而局部变量在定义时不会初始化。
全局变量在程序运行时即占据内存,在程序运行过程中可以随时的访问它,而局部变量在进入语句块的时候分配内存,语句块结束的时候释放内存,不再有效。
如下示例代码:
#include <stdio.h>
#include <stdlib.h>
int global; //定义全局变量
void GlobalPlusPlus(void);
int main()
{
global = 1;
printf("调用函数之前, 它是 %d\n", global);
GlobalPlusPlus();
printf("调用函数之后, 它是 %d\n",global);
return 0;
}
void GlobalPlusPlus(void)
{
printf("在++之前, 它是 %d\n", global);
global++;
printf("在++之后, 它是 %d\n", global);
}
代码输出结果为:
调用函数之前, 它是 1
在++之前, 它是 1
在++之后, 它是 2
调用函数之后, 它是 2
上面示例中global
定义在与main
函数平行的位置,因此它就相当于全局变量。因此在程序中每个地方使用的时候,这个global
就一直是一个变量。
如果我们现在对上面代码仅做一点改动:删除掉int global;这一行,然后给语句块中的global加上int,在函数中也定义上global
,如下:
#include <stdio.h>
#include <stdlib.h>
void GlobalPlusPlus(void);
int main()
{
int global = 1;
printf("调用函数之前, 它是 %d\n", global);
GlobalPlusPlus();
printf("调用函数之后, 它是 %d\n",global);
return 0;
}
void GlobalPlusPlus(void)
{
int global = 1;
printf("在++之前, 它是 %d\n", global);
global++;
printf("在++之后, 它是 %d\n", global);
}
输出结果为:
调用函数之前, 它是 1
在++之前, 它是 1
在++之后, 它是 2
调用函数之后, 它是 1
这里的global
长的一模一样,可它们最多算个双胞胎,并不是一个变量。
变量的存储类型
其实C语言中有三个关键字:
-
auto
:自动变量 -
register
:寄存器变量 -
static
:静态变量
可能大家对static
还稍微熟悉一点,但是对前两个压根没啥印象啊。
实际上,我们使用的绝大部分局部变量都是自动变量,这个自动
的意思是它会自动申请内存,并且在退出的时候自动释放内存。你想想我们的语句块中的变量不都是这样的吗?因此这种变量实在是太长用了,然后C语言就把它设计成可以省略了。
第二个寄存器变量又是什么东西呢?我们知道在CPU中有一种容量有限但是速度超级超级快的存储器,就是寄存器(Register)
,程序访问内存去取得数据是比较浪费时间的,因此我们可以把经常需要访问使用的数据存储在寄存器里面,这样的话程序的性能就会提高,使用寄存器变量就相当于你让这个变量直接使用寄存器来存储啦。
不过要注意的一点是,你可别直接就认为把所有变量都定义为register
程序就会变快了。因为寄存器的个数只有有限的几个,多出来的变量编译器还是会自动处理成普通变量。同时,编译器有权以任何理由阻止你定义的任何变量成为register,它也有权以任何理由让你定义的普通变量成为register。
这是因为现在的编译器太聪明了,它会自动优化程序的。因此我们平常几乎不用操心要把哪个变量放进寄存器,因此register是一个用户不怎么需要的关键字了。
对于静态变量就不需要说太多了,学过其它语言的都能很容易理解它。我们知道在语句块中,每次进入和退出时里面的变量都会重新分配内存和释放内存,也就是说你每次进入相同的语句块内其变量值都是一样的。那么如果你在退出语句块时想要保存某个变量的值怎么办?设置它为静态变量就OK了,这样你下次去访问那个局部静态变量的时候,它还保持着上次退出时的那个值。
预处理指令
我们知道C语言程序是不能直接运行的,需要使用编译器进行编译
,而如果把编译
的过程更加细分一下,它包含预处理、编译、链接
三个步骤。
预处理
在编译之前进行,根据源代码中的预处理指令,在后台调整源代码,编译器编译的都是预处理过后的源代码。
预处理指令就是那些以#
开头的指令,比如#include<stdio.h>、#define PI 3.1415926
还有很多其它的预处理指令。
#include
#include
被称为源代码包含指令
,它有两种语法:
#include <filename>
#include "filepath"
尖括号和双引号是定位文件的两种不同方式。
前者是在编译器指定的目录内查找filename
文件,它通常就是一个叫"include"
的目录,目录下有很多的.h
文件,包括我们非常熟悉的stdio.h、math.h
等等,如下所示:
而后者是按照filepath
所描述的路径查找文件。通常我们给定的filepath
里面并不含有路径,仅仅是一个文件名,表示在与源文件相同的目录下查找filepath
,像我们一般自己写的一些函数文件就是这样引入的。
如果能成功定位文件(没有成功定位会出现编译错误)
,则预处理器会用该文件的内容替换#include
指令的所在行。替换后的代码再被编译器编译。
例如在同一目录下有两个文件:file.h、file.c
,其中
file.h
:
int var1;
int var2;
file.c
:
#include "file.h"
int main(void)
{
var1 = var2 = 0;
return 0;
}
那么file.c
经过预处理就变成了:
int var1;
int var2;
int main(void)
{
var1 = var2 = 0;
return 0;
}
所以我们就知道为什么要包含stdio.h
等文件了,因为该文件中包含了我们需要使用的一些库函数以及宏等。只有导入它们之后我们的代码才能正确编译运行。
#define和#undef
#define
我们很熟悉了,就是宏定义指令,例如:
#define PI 3.1415926
那么程序中所有出现PI
的地方都被3.1415926
代替,然后再编译。
而#undef
决定了宏的有效范围,即宏的有效范围就是从定义它的那一行开始,直到遇见#undef
为止结束。如果没有遇到的话,就会作用知道整个程序末尾,它的用法更简单,如下:
#undef PI
就行了。
宏在取消后可以重新定义,并且仅在被取消后才能重新定义。