个人整理面向入门基础级别的C语言学习教程。
关键词:C语言
此教程会用一些注记符号,表示某种格式。
C语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。
C语言标准有C89、C99、C11、C17、C2x ……等,后面的数字是标准发布的年份。如果想了解每个标准有什么差异,自行必应搜索。本教程的完整代码在GNU C17下成功运行。
编写C语言,就连基本的文本编辑器——记事本也可以完成。但我们并不会只想着使用那么奇奇怪怪的记事本,去编写十来行代码甚至上千行代码,至少是真的不习惯。
接触C语言,会有很多人推荐你用那个什么什么写啊,一点一动就行;但如果在大学学习,教师可能会让你使用Visual C++ 6.0(下简称VC 6.0)去写。
感受过VC 6.0软件界面的人都知道,很不舒服,甚至降低自己的效率。VC 6.0甚至比我的年龄还大,算是老古董了。但是目前教材用的是VC 6.0,实验室配置的是VC 6.0,乃至你的考试,甚至计算机二级,都是VC 6.0,所以用VC 6.0而不是别的软件,更加容易应付教学。
所以在此,我推荐使用的C语言编辑器是:
基础小白或图方便可使用Dev-C++
非基础小白可使用Visual Studio或Visual Studio Code
此教程的代码使用Dev-C++,均在Dev-C++下成功运行。
按 Ctrl
+ N
可以新建文件,或者点击左上角文件新建文件。
在 工具 → 编译选项 → 代码生成/优化 → 代码生成 处可以找到语言标准。
在上一节我提到了用什么写C语言,这是关于编辑器(Editor)的事情。字面意思,它只负责编辑,也就是敲上一个个的字母数字,就像写英语日记、写英语文章一样。
事实上,你在编辑器上写的内容,还不能完全算是程序,这个内容称为 源代码(Source Code) 。存放源代码的文件称为 源文件 。
我们还需要经过一些步骤,使得源代码变成 可执行文件(Executable file) ,也就是你计算机里面那些.exe文件。把这些步骤称之为 编译(Build) 和 链接(Link) 。
编译:编译需要 编译器 完成。编译器把一个源代码翻译成可执行文件的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。常见的编译器有:MinGW、MSVC等。
链接:链接需要 链接器 完成。链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。链接可以执行于编译时,也就是在源代码被翻译成机器代码时;也可以执行于加载时,也就是在程序被加载器加载到内存并执行时;甚至执行于运行时,也就是由应用程序来执行。链接的意义在于我们不用将一个大型的应用程序组织成一个巨大的源文件,而是可以把它分解成为更小、更好管理的模块,可以独立的修改和编译这些模块。当我们改变这些模块中的一个时,只需要简单的重新编译它,并重新链接应用,而不必重新编译其他文件。
这些只是一些知识补充,想详细了解可参阅编译原理。另外,如果你选择了Dev-C++或者Visual Studio这样的集成软件,编译器和链接器的配置会相对简单。
学习C语言的开始,从输出一个“Hello World!”开始,这似乎是所有编程语言一开始都经历的事情。
我在此直接给出完整代码,心急的可以复制到Dev-C++运行试试。
1 | // Hello World.c |
现在来逐行认识基本的C语言代码结构。
//
表示当前整行被编译器忽略,又称注释,供程序员理解语句。
#include<stdio.h>
中的 #
(井号)表示预处理命令,即在程序编译前进行提前处理的部分。若是头文件,可用 <>
(单书名号)括起来,也可用 " "
(引号)包含起来。 其中由 <>
括起来的头文件表示在系统头文件的文件夹中查找,由 ""
括起来的头文件表示在源文件当前文件夹查找。
int main()
是一个函数的开头,再加以 {}
(大括号)包含主函数内容。此处是主函数,主函数是一个程序最重要的函数,程序从此开始编译。
printf("Hello World!");
为程序工作语句,其作用是输出“Hello World!”。
C语言中以 ;
(分号)作为一条语句的结束标志。
return 0;
是函数的结构之一,在函数一节介绍。
一般情况下, 一个C语言程序包含预处理器指令、函数、变量、语句(表达式和注释)等 。函数是一个进行某种功能操作的模块,目前认识到的是主函数,还可以自定义函数。变量即程序运行时其值可以改变的量,变量的功能就是存储数据。
C语言代码按 顺序结构 ,由上到下顺序执行代码。
还有一件比较重要的事情,养成良好的代码风格,即令人舒适的缩进、空格位置等;千万不要所有代码都挤到一块或者全都左对齐。
在上一节提到了预处理命令包含的头文件,那么头文件是什么?
头文件是扩展名为.h的文件,包含了C语言一些函数甚至几乎所有函数的提前 声明(Statement) 和 定义(Definition) ,没有这些声明和定义,编译器不会认识这些函数,故而不能运行。
有两种类型的头文件:程序员编写的头文件和编译器自带的头文件。
在程序中要使用头文件,需要使用预处理指令 #include 来引用它,引用头文件相当于复制头文件的内容。如stdio.h头文件,它包含了printf()
这一函数。
C语言的关键字是C语言的基本构成要素,共有32个,根据关键字的作用,可分其为数据类型关键字、控制语句关键字、存储类型关键字和其它关键字四类。
1 数据类型关键字(12个):
(1) char
:声明字符型变量或函数
(2) double
:声明双精度变量或函数
(3) enum
:声明枚举类型
(4) float
:声明浮点型变量或函数
(5) int
:声明整型变量或函数
(6) long
:声明长整型变量或函数
(7) short
:声明短整型变量或函数
(8) signed
:声明有符号类型变量或函数
(9) struct
:声明结构体变量或函数
(10) union
:声明共用体(联合)数据类型
(11) unsigned
:声明无符号类型变量或函数
(12) void
:声明函数无返回值或无参数,声明无类型指针
2 控制语句关键字(12个):
A循环语句
(1) for
:一种循环语句(可意会不可言传)
(2) do
:循环语句的循环体
(3) while
:循环语句的循环条件
(4) break
:跳出当前循环
(5) continue
:结束当前循环,开始下一轮循环
B条件语句
(1) if
: 条件语句
(2) else
:条件语句否定分支(与 if 连用)
(3) goto
:无条件跳转语句
C开关语句
(1) switch
:用于开关语句
(2) case
:开关语句分支
(3) default
:开关语句中的“其他”分支
D返回语句
return
:函数返回语句(可以带参数,也可不带参数)
3 存储类型关键字(4个)
(1) auto
:声明自动变量 一般不使用
(2) extern
:声明变量是在其他文件正声明(也可以看做是引用变量)
(3) register
:声明积存器变量
(4) static
:声明静态变量
4 其它关键字(4个):
(1) const
:声明只读变量
(2) sizeof
:计算数据类型长度
(3) typedef
:用以给数据类型取别名
(4) volatile
:说明变量在程序执行中可被隐含地改变
你可能不明白这些关键字什么意思怎么用,但你只需要知道这些关键字不能随便用,它们被C语言内部定义占用了。
函数是用来完成特定任务的一组语句。在C语言中,每个程序至少有一个主函数 main()
。此外,C语言还提供了许多内置函数,可以在相应的头文件中找到。有时候,我们也可以称函数为方法。
当我们编写代码时,我们可以将其分割成不同的函数。划分代码到不同的函数中是我们自己决定的。但是从逻辑上来说,我们通常会根据每个函数执行特定的任务来进行划分。例如,如果我们正在编写一个计算器程序,我们可以使用四个函数分别处理加法、减法、乘法和除法。这样的划分可以让代码更有组织性,并且更易于理解和维护。
函数的结构应当如下,包括 函数返回类型return_type , 函数名字function_name 、 参数arguments 和 函数主体Body 等。
1 | return_type function_name(arguments) |
函数返回类型:一个函数可以返回一个值,return_type是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,return_type 是关键字 void
。
函数名字:这是函数的实际名称。函数名和参数列表一起构成了 函数签名(Function signature) 。
参数列表:当函数被调用时,可以向参数传递值。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
函数主体:函数主体包含一组定义函数执行任务的语句。若有返回值的函数使用 return
返回。函数主体中遇到返回表示结束函数。
函数的产生有两个概念: 函数声明(Function Statement) 和 函数定义(Function Definition) 。函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的实际主体。 使用函数之前必须先声明(也可以直接声明并定义)。
开玩笑地说,声明就是你骗机器有这么一个函数,但是当机器要找这个函数用的时候,你最好是有个定义。
1 | // 函数声明 |
1 | // 函数声明并定义 |
C语言从上到下运行,如果是先写了 main 函数,再写 sub 函数,而在 main 函数调用了 sub 函数的情况下,需要有一个声明在 main 函数的前面,告知编译器有这个函数,否则按从上到下的顺序,此时并未出现 sub 函数;
如果是先写了 sub 函数,再写 main 函数,而在 main 函数调用了 sub 函数的情况下,在 main 函数之前的 sub 函数就相当于函数声明并定义,main 函数调用 sub 函数也自然成功。
函数声明中的参数列表可以不具体写变量名,但函数定义中的参数列表必须写变量名。如上述代码的声明可以写成:
1 | int add(int, int); |
当我们写完我们的函数,使用时直接称呼 函数签名(函数名字+参数列表)即可。
1 | // 声明并定义函数 |
如果函数要使用参数,接受参数值的变量称为函数的 形式参数 。如上面 int add(int a, int b)
中的 a
和 b
。
如果调用函数时,传递进入的参数称为 实际参数。如上面 add(1, 2)
中的 c
和 d
。
参数传递有两种方式: 值传递 和 地址传递 。
值传递:把参数的数值复制给函数的形式参数。在这种情况下,修改函数内的形式参数对实际参数没有影响。
地址(引用)传递:把参数的地址复制给函数的形式参数。在函数内,该地址用于访问调用中要用到的实际参数。这意味着,修改形式参数会影响实际参数。 对于地址传递,经过指针的学习可能更清晰。
数据以常量、变量、常变量或标识符的形式出现。
变量、常变量的出现包括 声明 、 定义 、 初始化 三个步骤。 为变量分配地址和存储空间的称为定义,不分配地址的称为声明。
变量定义:用于为变量分配存储空间,还可为变量指定初始值,程序中,变量有且仅有一个定义。
变量声明:用于向程序表明变量的类型和名字。
定义也是声明:当定义变量时我们声明了它的类型和名字。
所有变量使用之前需要有声明或定义。
常量 ,其值不能改变的量。其分为 整型常量 (即整数,如1,2,1000,666)、 实型常量 、 字符常量 。
实型常量 ,包括十进制小数形式(由数字和小数点组成)和指数形式,指数形式以E或e代表以10为底的指数,如1e6,注意:e或E之前必须有数字,且e或E之后必须为整数;
字符常量之字符常量 ,由 ‘’
(单引号)包括的一个字符,如 ‘a’
、 ‘A’
等。普通字符,详见ASCII字符集;转义字符,特殊形式的字符,以 \
开头。 引号在此充当界限符,字符常量和字符串常量不包括引号。
转义字符 | 字符值 | 输出结果 |
---|---|---|
\' |
一个单引号 | ' |
\" |
一个双引号 | " |
\? |
一个问号 | ? |
\\ |
一个反斜杠 | \ |
\a |
警告alert | 产生声音或视觉信号 |
\b |
退格backspace | 将光标后退一个字符 |
\f |
换页form feed | 将光标移到下一页 |
\n |
换行 | 将光标移到下一行 |
\r |
回车carriage return | 将光标移到本行开头 |
\t |
水平制表符 | 将光标移到下一个Tab位置 |
\v |
垂直制表符 | 将光标移到下一个垂直制表符 |
\o |
与该八进制码对应的ASCII字符 | 与该八进制码对应的ASCII字符 |
\xh[h…] |
与该十六进制码对应的ASCII字符 | 与该十六进制码对应的ASCII字符 |
字符常量之字符串常量 ,由 “ ”
(双引号)包括的一个字符串,可以超过一个字符,如 “boy”
。
字符常量之符号常量 ,用 #define
指令指定一个符号名称代表一个常量。
1 |
#define宏定义与常变量const的区别是,#define宏定义的值为实型常量,且#define为预处理命令,不分配内存,但常变量具有使程序稳定性提高的优点,使用方便。
1 | int a; //定义整型变量 a |
1 | const int a = 3; //定义一个整数常变量 a,后续不可修改。 |
变量的命名规则:
变量名的开头必须是字母或下划线,不能是数字。实际编程中最常用的是以字母开头,而以下划线开头的变量名是系统专用的。
变量名中的字母是区分大小写的。比如 a 和 A 是不同的变量名,num 和 Num 也是不同的变量名。
变量名绝对不可以是C语言关键字。
变量名中不能有空格。这个可以这样理解:因为上面我们说过,变量名是字母、数字、下划线的组合,没有空格这一项。
类型 | 存储大小 | 表示值范围(十进制) |
---|---|---|
char |
1字节 | -128~127 |
unsigned char |
1字节 | 0~255 |
signed char |
1字节 | -128~127 |
int |
4字节 | -2147483648( )~2147483647( ) |
unsigned int |
4字节 | 0~4294967295( ) |
short |
2字节 | -32768~32767 |
unsigned short |
2字节 | 0~65535 |
long |
4字节 | -2147483648( )~2147483648( ) |
unsigned long |
4字节 | 0~4294967295( ) |
long int |
4字节 | -2147483648( )~2147483648( ) |
unsigned long int |
4字节 | 0~4294967295( ) |
long long int |
8字节 | ~ |
unsigned long long int |
8字节 | 0~ |
注意,各种类型的存储大小与系统位数有关,但目前通用的以64位系统为主,下同。为了得到某个类型或某个变量在特定平台上的准确大小,您可以使用 sizeof 运算符。表达式 sizeof(type)
得到对象或类型的存储字节大小。
上述只讨论十进制,实际上C语言还可以表示八进制(int a = 010
)和十六进制(int a = 0x10
)。
特别指出,unsigned表示无符号数据,即非负数,但只有整型和字符型数据可以加unsigned修饰符。
由于ASCII码的存在, char
类型的数据范围恰好最小完整表达字符,所以 char
类型又称为字符类型。
类型 | 存储大小 | 表示值范围 | 精度 |
---|---|---|---|
float |
4字节 | 1.2E-38~3.4E+38 | 6位有效数字 |
double |
8字节 | 2.3E-308~1.7E+308 | 15位有效数字 |
long double |
16字节 | 3.4E-4932~1.1E+4932 | 19位有效数字 |
需要注意,计算机对于浮点数的存储并不完全精准。
若我们声明定义的变量超过数据类型的数据范围时,编译会警告我们,我们可在数据末尾加专用字符进行类型转换。如在 float
类型数据后加“f”,指定为 float
类型,如在实型变量后加“L”,指定为 long double
型。
1 | float a = 3.14159; |
void
它通常用于以下三种情况下:
void
。void
:类型为 void *
的指针代表对象的地址,而不是类型。返回指向 void
的指针,可以转换为任何数据类型。类型转换是将一个数据类型的值转换为另一种数据类型的值。
C 语言中有两种类型转换:
1 | int i = 10; |
1 | double d = 3.14159; |
运算符 | 描述 | 实例 |
---|---|---|
+ |
左操作数和右操作数相加 | A + B |
- |
左操作数减去右操作数 | A - B |
* |
左操作数和右操作数相乘 | A * B |
/ |
左操作数除以右操作数 | A / B |
% |
左操作数除以右操作数后的余数 | A % B |
++ |
操作数整数值自增1 | A ++ |
-- |
操作数整数值自减1 | A -- |
%
运算符的操作数只可以是整数。
1 | // test.c |
应当注意, ++i
和 i++
, --i
和 i--
是两回事。
1 | // test2.c |
运算符 | 描述 | 实例 |
---|---|---|
== |
检查两个操作数的值是否相等,如果相等则条件为真 | A == B |
!= |
检查两个操作数的值是否相等,如果不相等则条件为真 | A != B |
> |
检查左操作数的值是否大于右操作数的值,如果是则条件为真 | A > B |
< |
检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | A < B |
>= |
检查左操作数的值是否大于等于右操作数的值,如果是则条件为真。 | A >= B |
<= |
检查左操作数的值是否小于等于右操作数的值,如果是则条件为真。 | A <= B |
运算符 | 描述 | 实例 |
---|---|---|
&& |
逻辑与运算符,如果两个操作数都非零,则条件为真。 | A && B |
\|\| |
逻辑或运算符,如果两个操作数中有任意一个非零,则条件为真 | A \|\| B |
! |
逻辑非运算符,用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !A |
位运算符是对数据转化为二进制,再逐位进行运算。如,再对每一位进行逻辑运算。
运算符 | 描述 | 实例 |
---|---|---|
& |
按位与运算符,对两个操作数的每一位执行逻辑与操作 | A & B |
\| |
按位或运算符,对两个操作数的每一位执行逻辑或操作 | A \| B |
^ |
按位异或运算符,对两个操作数的每一位执行逻辑异或操作 | A ^ B |
~ |
按位取反运算符,对两个操作数的每一位执行逻辑取反操作 | ~A |
<< |
将操作数的所有位向左移动指定的位数。左移n位相当于乘以2的n次方 | A << n |
>> |
将操作数的所有位向右移动指定的位数。右移n位相当于除以2的n次方 | A >> n |
1 | // bit.c |
运算符 | 描述 | 实例 |
---|---|---|
= |
把右操作数的值赋给左操作数 | C = A + B |
+= |
把右边操作数加上左边操作数的结果赋给左边操作数 | C += A 相当于 C = C + A |
-= |
把右边操作数减去左边操作数的结果赋给左边操作数 | C -= A 相当于 C = C - A |
*= |
把右边操作数乘以左边操作数的结果赋给左边操作数 | C *= A 相当于 C = C * A |
/= |
把右边操作数除以左边操作数的结果赋给左边操作数 | C /= A 相当于 C = C / A |
%= |
把右边操作数取余左边操作数的结果赋给左边操作数 | C %= A 相当于 C = C % A |
<<= |
左移且赋值运算符 | C <<= A 相当于 C = C << A |
>>= |
右移且赋值运算符 | C >>= A 相当于 C = C >> A |
&= |
按位与且赋值运算符 | C &= A 相当于 C = C & A |
^= |
按位异或且赋值运算符 | C ^= A 相当于 C = C ^ A |
\|= |
按位或且赋值运算符 | C \|= A 相当于 C = C \| A |
赋值运算符参与的表达式中,赋值表达式返回赋值符号右边的值。
(? : )
:其形式为 (表达式1)? 表达式2 : 表达式3
,先求解表达式1,若其值为真(非0)则将表达式2的值作为整个表达式的取值,否则(表达式1的值为0)将表达式3的值作为整个表达式的取值。1 | // test.c |
,
:其形式为 表达式1,表达式2
,逗号运算符确保操作数被顺序地处理:先计算左边的操作数,再计算右边的操作数。右操作数的类型和值作为整个表达式的结果,而左操作数只是当作副作用被计算,其值和类型不会被返回。1 | // test.c |
指针运算符 &
和 *
。 *
:取值符,指向一个变量; &
:取地址符,取址运算符的操作数必须是在内存中可寻址到的地址。换句话说,该运算符只能用于函数或对象。
成员运算符 .
和 ->
。用于返回复杂数据类型的子成员。
下标运算符 []
。用于访问下标,在数组中用到。
求字节运算符 sizeof()
。返回变量的大小,将需要求字节内存的变量或其他数据结构放于括号中。
强制类型转换运算符 (类型名)(表达式)
。 如 (float)(a + b)
。
1 | //条件语句 |
1 | //循环语句 |
1 | //循环语句 |
1 | continue; |
1 | //条件语句 |
1 | return ……; |
1 | //转向语句 |
函数调用语句:右一个函数调用加分号构成。
复合语句:用 {}
括起来的语句,也称语句块。
空语句:只有一个分号。
表达式语句:一个表达式加上一个分号构成。用C语言运算符将运算对象连接起来的式子称为表达式。
任何一种编程中, 作用域 是程序中定义的变量所存在的区域,超过该区域变量就不能被访问。
C语言中有三个地方可以声明变量:
在函数或块(由花括号包括起来的称为代码块)内部的局部变量
在所有函数外部的全局变量
在形式参数的函数参数定义中
变量按作用范围可分为 局部变量 、 全局变量 、 形式参数 。
在某个函数或块的内部声明的变量称为局部变量。它们只能被该函数或该代码块内部的语句使用。局部变量在函数外部是不可知的。
1 | // test.c |
全局变量是定义在函数外部,通常是在程序的顶部。全局变量在整个程序生命周期内都是有效的,在任意的函数内部能访问全局变量。也就是说,全局变量在声明后整个程序中都是可用的。
1 |
|
函数的参数就是形式参数,被当作该函数内的局部变量,如果与全局变量同名它们会优先使用,同样如果局部变量与全局变量同名时优先使用当前区域的局部变量。
1 | // test.c |
1 | 输出结果: |
全局变量保存在内存的全局存储区中,占用静态的存储单元;
局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动对其初始化,如下所示:
数据类型 | 初始化默认值 |
---|---|
int |
0 |
char |
\0 |
float |
0 |
double |
0 |
pointer (指针) |
NULL |
格式化的输入输出函数位于 stdio.h
头文件中。
printf()
函数进行格式化的输出,其函数定义如下:1 | int printf(const char *fmt, ...); |
也可以参考这样的格式: printf(格式控制,输出表列)
1 | // printf.c |
1 | 输出结果: |
格式控制,是使用双引号括起来的一个字符串,称为格式控制字符串,包括格式声明和普通字符。
格式声明,由 %
(百分号)和格式字符组成,其作用是将输出的数据转换为指定的格式后输出。格式声明总是由 %
字符开始。
普通字符,即原样输出的字符,包括逗号、空格等。
应当注意的是:
当我们想在显示器输出一个 %
时,我们需要在格式控制字符串中连续使用两个 %
表示一个 %
。
当我们想在显示器输出一个 \
时,我们需要在格式控制字符串中连续使用两个 \
表示一个 \
。
对于格式字符,有如下:
格式字符 | 说明 |
---|---|
d 或 i |
以带符号的十进制输出整数(正数不带符号) |
ld 或 lld |
数据类型为 long int 或 long long int 的输出 |
o |
以无符号的八进制输出整数(不输出前导 0 ) |
x 或 X |
以无符号的十六进制输出整数(不输出前导符 0x ) |
u |
以无符号的十进制输出整数 |
c |
以字符形式输出一个字符 |
s |
输出一整个字符串,直至遇到 \0 |
f |
以小数形式输出单精度数,默认输出6位小数 |
lf |
以小数形式输出双精度数,默认输出6位小数 |
e 或 E |
以指数形式输出实数 |
g 或 G |
选用 %f 或 %e 中宽度较短的格式,不输出无意义 0 |
0
,表示该数字为八进制。上述中不输出前导 0
,但当格式为 %#o
时,输出前导 0
;一个数字的前面加 0x
,表示该数字为十六进制。上述中不输出前导符,但当格式为 %#x
时,输出前导 0x
。输入下面的代码可以直观感受输出的格式问题:会因为格式字符与输出的数据类型不匹配而导致输出乱码。
1 | // test.c |
1 | 运行结果: |
应该清晰格式字符,不乱用不混用。 输出表列的数量需与%d(或其他格式声明)的数量一致。
%f
默认保留6位小数,而 %m.nf
可以自定义小数位,m指输出宽度,n指保留小数位。如果想用 0
控制宽度,在宽度控制数字前加 0
可以这么使用 %06d
、%07.2f
,但只能补前导 0
。而单独的 %.nf
就可以控制输出的小数位。
当m前面添加 -
,则输出数据向左对齐,再补空格或 0
。当m前面无 -
,则输出数据向右对齐,再补空格或 0
,此时称前导空格或前导 0
。
如 %7.2f
,表示输出数据占7列(小数点也占一列),保留2位小数。运行以下代码可深刻理解:
1 | // test.c |
1 | 运行结果: |
由 %f
拓展, %d
等格式声明都可在 %
和格式字符之间加一个数字(正或负),表示控制整个输出所占宽度,如 %7d
、 %4c
等。
如果事先并不知道保留多少小数位,而是后续输入的情况,可以使用占位符 %.*f
解决。
1 | // test.c |
另外, printf()
函数在运行时的形参是 从右到左 处理,具体效果如下:
1 | // test.c |
1 | 输出结果: |
由于是 从右到左 的顺序处理,所以先是 a += 1
,a变成6,再接着 b = a + 1
,b变成7。
函数原型: int putchar(int _Character)
,函数定义在 stdio.h
中。
函数原型: int puts(const char *s);
,函数定义在 stdio.h
中。
1 | // test.c |
1 | 运行结果: |
scanf()
函数进行格式化的输出,其函数定义如下:1 | int scanf(const char * restrict format,...); |
也可以参考这样的格式: scanf(格式控制,地址表列);
。格式控制字符串同输入的规则。在使用 scanf()
的时候,我们应当注意, scanf()
函数的地址表列是需要输入的变量地址,而不是变量名,因此,我们要在变量名前加 &
取地址符。如:
1 | scanf("%d %d %d", a, b, c); //此写法错误 |
当我们不加取地址符的时候,程序会因为储存内存溢出(输入的内容没有正确的地方存放)而终止程序。因此,当我们的程序莫名中断崩掉时,我们可以优先考虑是否在输入时加取地址符。
从另一个角度理解,对于函数而言,我们希望输入一个值,也就是修改变量里面的值,应当传入其地址,而不是直接传入值。
还需要注意的是, scanf()
的格式控制字符串应与我们输入的格式严格一致,即格式控制字符串中有逗号或空格等的时候,我们输入也需加上逗号或空格等。如:
1 | scanf("%d %d %d", &a, &b, &c);//此时应当输入 1 2 3 |
如果没有留意到这个问题,那么当我们输入数据的时候,可能会出现数据吞没的情况,因为输入的跟字符串规定的不匹配,不读入。
该函数还有自动处理的地方,如
当我们的输入格式控制为 %d%d%d
时,系统会自动把空格或者换行(回车)作为分隔两个数据的标志。
当我们的输入格式控制为 %f%f%f
时,系统会自动把小数点、空格或者换行(回车)作为分隔两个数据的标志。
1 | // test.c |
1 | 运行窗口: |
虽然系统会自动把空格作为分隔两个数据的标志而不读入空格,但是总有办法可以使得 scanf()
函数输入带空格,如下代码:
1 | // getstring.c |
实际上, scanf()
函数的注意点与其的键盘缓冲区和输入流有关。该函数会从输入流中选择数据放入键盘缓冲区中。在未输入满前忽略掉空白字符(空格、制表符和新行符),在输入满时把空白字符当作中断的标志。所以要注意反思输入流和键盘缓冲区里面的内容是什么,是否有键盘缓冲区遗留问题导致提前终止输入,是否有数据仍在输入流中没有进入键盘缓冲区而丢弃等。
getchar()
:输入一个字符函数原型: int getchar(void);
,返回值则是输入的字符的ASCII码,若读取失败返回EOF或-1,函数定义在 stdio.h
中。
1 | // test.c |
1 | 运行窗口: |
gets()
:输入一个字符串,一般使用 scanf()
输入字符串的时候不能输入含空格的字符串,而使用 gets()
则可以输入含空格的字符串。由于 gets()
函数的不安全性,在C99标准中,已经不再建议使用该函数,而在C11中更是直接抛弃了这个函数。
实际上,在很多情况下,需要根据某个条件是否满足来决定是否执行指定操作任务,或者从给定的两种或多种操作选择其一。这就是选择结构需要解决的问题。再举个详细的例子,当我们处于岔路时,做出方向的选择;比如数学上的分段函数,条件不同对应函数关系不同。
通过条件结构,可以完成一些简单的语法题,如交换两个数,输出两数最大值和最小值,由键盘输入三个数输出最大值,判断闰年……
条件的真假如果对应到数字上,0为假,非0为真,即正负数都为真。
if
语句的结构如下:
1 | if( condition is true ) |
需要注意, if
语句在没有花括号的情况下,只连带其后的一句语句。即 if(condition)
后只能跟一个语句,其可以是单个简单语句,也可以是由花括号包含起来的复合语句。
再加之 else
(另外,否则),可以组成以下三种形式:
1 | // 情况一,无else子句部分,只有单个if语句 |
if
语句无论写几行,都是一个整体,属于同一个语句,只是将其分成 if
部分和 else
部分。在 if
语句中要对给定的条件进行检查,判定所给定的条件是否成立。判断的结果是一个逻辑值“是”或“否”。
if
语句小括号中的 condition
,应该是由关系运算符与逻辑运算符构成的表达式,运算符号详见运算符。如表达x大于1时写成 x>1
,表达x在10到20之间时写成 x >=10 && x <= 20
,表达x不等于4时写成 x != 4
,表达x等于4时写成 x == 4
。可以试着巧妙应用 ? :
三目运算符转化繁琐的if结构。
下面结合一个例子剖析 if-else
语句,在数学中有阶跃函数(分段函数),当x>0时,y=1;当x=0时,y=0;当x<0时,y=-1。
1 | // test.c |
if
语句可以多个嵌套, if
中又带有 if-else
等情况,请注意辨识。应当注意 if
与 else
的配对关系。 else
总是与它上面最近的未配对的 if
配对, else
一般不能单独出现,上面必须先有 if
部分。上面的例子用 if
语句嵌套可如下表示:
1 | // test.c |
switch
语句的结构如下:
1 | switch (object) //对于某个变量或表达式进行switch |
在此强调, switch
语句中 case
情况部分的 break
语句必须存在,此为语法规定。当去掉 break
语句时,系统将逐句往下运行。更具体地说,如果 switch
时 case
情况1的话,你想运行情况1部分的语句,但因为缺少 break
语句,系统还会运行下面 case
情况的语句,造成逻辑表达上的错误。可以对下面的代码中每个 break
试着取消注释,让其运行,观察系统运行语句输出区别。
1 | // test.c |
switch
语句的对象应该是 整型和字符类型的变量或变量表达式 ,如 int
变量 number
、 char
变量 ch
、 number%10
、 ch+32
……相对于的 case
情况应该是整型常量或者是字符常量。如整数1、2、3……和字符’A’、‘B’、‘c’……
对于 switch
语句,还有以下规则说明。操作部分语句可以不止一个语句,其一直运行到 break
语句为止。可以没有 default
的情况,但此时如果没有与 switch
表达式相匹配的 case
常量时,不执行任何语句。 case
情况出现的次序不影响执行结果,每个 case
情况常量必须互不相同,不能存在一个值有多种执行情况。
这里有一个例子,对于输入分数,输出分数评级,可以辅助理解:
1 | // test.c |
switch
语句也是可以嵌套的,但结构上比较繁琐。
有时候用条件结构还是不能简便解决问题,还需要使用循环结构,如处理求多个整数的和、处理多个判断。
再如在输入的时候,我希望输入50个数字,我们可以复制粘贴50句 scanf()
来实现这个要求。学了循环之后,至少,不再需要50句语句。
需要注意的是, 循环语句一定要设置终止条件,否则将进入死循环,无法得出结果!!!
通过循环结构,可以完成一些简单的语法题,如判断一个数是否为素数,计算斐波那契数列,计算阶乘……
while
语句的一般形式如下:
1 | while( condition is true ) |
此处 condition
为表达式,只有当循环条件为真,即表达式为真,就执行循环体语句。循环体可以是一条语句,也可以是一个语句块(用花括号包起来)。 while
循环的特点是先判断条件表达式,后执行循环体语句。
while
语句的运行顺序是:从上至下,先判断条件表达式是否为真,为真则执行循环体。循环体运行完后再次判断条件表达式,为真则执行循环体。一直循环直至判断条件表达式为假。
这里给出一个例子,求1到100的和,希望通过例子更好理解 while
循环:
分析此问题,将100个数相加,要重复进行100次加法,显然是需要循环结构介入。有兴趣的可以尝试画画流程图(此处不做介绍)。
1 | // add-100-number.c |
while
括号里的 condition
条件表达式实际上也会被执行的,如果条件表达式是赋值语句(赋值成功则表达式为真)、自增自减等也会生效。
do-while
语句。其一般形式如下:
1 | do |
此处 condition
为表达式,道理同上。 do-while
循环的特点是先无条件执行循环体,然后判断循环条件是否成立。即 do-while
循环至少进行一次循环,注意与 while
语句区分。
do-while
语句的运行顺序是:从上至下,先无条件执行一次循环体,再判断条件表达式是否为真,为真则回到循环体开始重新执行循环体。循环体运行完后再次判断条件表达式,为真则执行循环体。一直循环直至判断条件表达式为假。
此处同样解决上面的问题,从1加到100求和。
1 | // add-100-number.c |
while
括号里的 condition
条件表达式实际上也会被执行的,如果条件表达式是赋值语句(赋值成功则表达式为真)、自增自减等也会生效。
for
语句的一般形式如下:
1 | for( sentence1; condition; sentence2 ) |
此处 sentence1
和 sentence2
为普通表达式, sentence1
只执行一次,而 sentence2
在每次执行完循环体后都会运行一次。 condition
为条件控制表达式,只有当循环条件为真,即表达式为真,就执行循环体语句。
for
语句的运行顺序是:先进行一次 sentence1
,接着判断 condition
是否为真,为真则执行循环体。循环体运行完后执行 sentence2
。然后再次判断 condition
,一直循环直至条件为假。
所以延展出常用的 for
语句使用格式:
1 | for(循环变量赋值初始化; 循环条件; 循环变量变化) |
做个补充,在老版本的C语言中, sentence1
不支持声明变量并同时初始化,如: for(int i = 1; i <= 100; i ++)
。但是在C99版本后是允许的。
事实上, for
语句中的 sentence1
语句、 sentence2
语句和 condition
语句并非必要。即这三句语句可以省略,省略 condition
语句默认为真,写成 for( ; ; )
,但是两个分号绝不可少。在省略掉这三句语句时,我们仍要思考如何终止循环。例如在 for
语句上面就把变量初始化,在循环体中设置判断 if
什么条件就终止循环,在循环体中处理条件变化……
break
语句终止循环。之前使用 break
语句,可能是条件结构中跳出 switch
语句。实际上, break
语句还可以用来跳出循环体,即终止循环。
以求1到100和的例子,假如我希望在和刚好大于等于2500的时候结束,并想知道此时是多少个数的和,那怎么实现呢?
1 | // use-break.c |
当然也可以这样解决:
1 | // use-for.c |
注释中我写到,终止 break
所在的一层 for
循环,因为当有多个循环嵌套时, break
不会终止所有的循环,而是一层循环。(可以自己做实验研究)
continue
语句跳过本次循环。有时候不希望终止整个循环,而是想结束这一次循环,还得执行下次循环。这时可以使用 continue
语句。
还是求1到100和的例子,但我希望求1到100中是偶数的和,那么奇数就没必要加进去,此时可以使用 continue
语句跳过本次循环。
1 | // Find-the-sum-of-even-numbers-from-1-to-100.c |
需要注意的是,结束本次循环并不是完全不进入循环,而是不执行 continue
下面的循环体部分。上面的代码,如果 sum += i
在 continue
的上面,结果还是会加上奇数的部分。
枚举是C语言中的一种基本数据类型,用于定义一组具有离散值的常量。枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
定义一个枚举类型,需要使用 enum
关键字,后面跟着枚举类型的名称,以及用大括号 {}
括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从0开始递增。
枚举语法定义格式为:
1 | enum 枚举名 {枚举元素1,枚举元素2,……}; |
举个例子,一周有7天,如果想将星期一映射为数字1,星期天映射为数字7,我们可以使用枚举。
1 | enum Day |
如果想让星期三之后特殊一些,星期三映射到8,星期天映射到12,只需在中间添加赋值即可。
1 | enum Day |
使用时直接输出即可:
1 | printf("%d", MON); |
然而上面是对枚举类型的使用,下面介绍枚举变量的定义和使用。
枚举类型是一种数据类型,可以对标 int
理解;枚举变量是一个变量,可以对标 int number
中的 number
理解。
可以通过三种方式定义枚举变量:
1 | enum DAY |
1 | enum DAY |
1 | enum |
给出使用枚举变量的情形:
1 | // test.c |
在 枚举类型值连续 的情况下还可以进行循环:
1 | // test.c |
如果将将整数转换为现有枚举值,只需强制类型转换:
1 | // test.c |
1 | 输出结果: |
数组是一种可以存储固定大小的相同类型元素的顺序集合。简单来说,数组可以存放一串相同的数据类型。
数组的结构如下图:
数组的每一个单体称作 元素 ,每个元素有它的 索引号 ,用于访问该元素。 索引号从0开始。
数组的定义十分简单,其定义格式是:
1 | type arrayName [arraySize]; |
如 int array[5];
就定义了一个大小为5的整型数组,数组名为array,它可以存放5个整型数据,索引号从0到4。
访问数组时直接使用 arrayName[index]
的方式进行,如 array[0]
就是array数组的第一个元素的值。使用循环还可以将数组的内容输出:
1 | // test.c |
补充个冷知识,我们比较经常使用 array[0]
访问元素,实际上还能以 0[array]
的方式访问元素。
通过上面的例子,我们可以通过循环给数组中的每个元素赋值。但是还可以通过声明时的初始化语句进行相同的操作。
1 | int array[5] = {1, 2, 3, 4, 5}; |
大括号 { }
之间的值的数目不能大于我们在数组声明时在方括号 [ ]
中指定的元素数目。如果省略掉了数组的大小,数组的大小则为初始化时元素的个数。即等价于:
1 | int array[] = {1, 2, 3, 4, 5}; |
C 语言支持多维数组。多维数组声明的一般形式如下:
1 | type name[size1][size2]...[sizeN]; |
下面以二维数组作为示例。一个二维数组,在本质上,是一个元素为一维数组的数组。定义形式如下:
1 | type arrayName [x][y]; |
如一个二维数组初始化如下:
1 | int array[3][4] = { |
二维数组中的元素是通过使用下标(即数组的行索引和列索引)访问。
1 | // test.c |
sizeof
关键字,将整个数组占的内存大小除以每个元素占的内存大小,得到的就是元素个数。1 | int numbers[] = {1, 2, 3, 4, 5}; |
1 | void fun(int param[10]) |
1 | void fun(int param[]) |
为了函数的通用性,通常使用未定义大小的数组,再将数组大小作为第二参数传入。
1 | double getAverage(int arr[], int size) // 求数组的平均值 |
需要注意的是,我们传入的是数组,在函数内部中如若有对数组元素的修改,也会同步对外部的数组进行修改。因为此处形式参数是以地址传递的形式传入。
为什么传递数组名字会是以地址传递的形式传入呢?那是因为 数组名实际上是一个指针 ,它存放数组中首元素的地址。至于什么是指针,可查阅下一节。
指针是C语言很重要的一环。
数据都有一个内存空间,内存空间都有一个地址,我们形象地把这个地址称为指针。指针也就是内存地址,指针变量是用来存放内存地址的变量。
指针变量定义的一般形式为:
1 | type *var_name; |
type
是指针的基类型,它必须是一个有效的数据类型, var_name
是指针变量的名称。用来声明指针的星号 *
与乘法中使用的星号是相同的,但在此处的意义是标记改变量是指针变量,称为指针运算符。一个指针对应一个 *
。
所有实际数据类型,不管是整型、浮点型、字符型,还是其他的数据类型,对应指针的值的类型都是一样的,都是一个 代表内存地址的长的十六进制数 。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同。
当我们需要取一个变量的地址时,我们需要加 &
(取地址符);当我们需要取一个指针的数据时,我们需要加 *
。
1 | int a = 1; |
之前使用 scanf()
函数时所加的 &
,表明我们是把数据放到某变量名所处的地址,而不是塞到变量名里面。
如果想输出地址的话,可以使用 %p
或 %x
输出:
1 | int a = 1; |
指针使用前必须有初始化值,即定义时必须赋值,若无值则指向NULL。
1 | int *p = NULL; |
如果使用前没有初始化值时,该指针会变成 野指针 ,指针指向了一块随机的空间,不受程序控制。有可能是系统重要软件的地址,贸然使用可能使得系统崩溃。
指针之间不能进行 +
运算,无意义。
指针进行 <
运算时,表示实际内存高低位的比较;
指针进行 =
运算时,表示把右边的地址赋值给左边;
指针进行 -
运算时,表示在两指针之间相隔相同类型元素的个数。
指针进行 ++
运算时,表示在指针递增,会指向下一个元素的存储单元。指针在递增时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
指针进行 --
运算时,表示在指针递减,会指向上一个元素的存储单元。指针在递减时跳跃的字节数取决于指针所指向变量数据类型长度,比如 int 就是 4 个字节。
通过指针输出:
1 | // test.c |
1 | 输出结果: |
通过指针将两个变量存放的值交换,用地址传递实现存储空间上的交换。
1 | // swap-ab.c |
1 | 输出结果: |
数组名即是该数组首元素(即序号为0的元素)的地址。所以我们可以直接用指针指向数组名,无需取地址符。
1 | int a[10]; |
同时,我们可以对数组首元素进行取地址,即下代码等价:
1 | p = a; //p指向数组首元素的地址,即a[0]的地址; |
当我们正确把指针指向一个数组的时候,可以对指针进行以下运算: p++
, ++p
, p--
, --p
, p = p + 1
, p = p - 1
等。
1 | // test.c |
1 | 输出结果: |
因为数组名也有地址的属性,所以输出语句还可以改成:
1 | printf("%d ", *a); |
但数组名不能进行自增自减,如不能 a++
或 a--
等。
指针还可以引用字符串(字符数组),字符串的本质是一个字符数组。我们可以类似指针引用一维数组那样引用字符串。
1 | // test.c |
1 | 输出结果: |
1 | //以上也可用字符指针变量表示,如下所示: |
char *p="computer!"
的意思是将字符指针变量 p
指到"computer!"的首地址即"c"的地址。
之前,我们用字符数组也能实现打印字符串,而使用字符指针的不同是:
①可以对字符指针变量赋值,但不能对数组名赋值,初始化不等于赋值,如不能:
1 | char a[10] = {"computer!"}; //此句合法 |
②储存内容不同,字符数组把每一个元素的值都存放,而字符指针只存放第一个元素的地址。
二维数组有两个下标,那么一个指针怎么指,我们将指针分为 行指针 和 列指针 。行指针,即指向某一行元素的指针;列指针,即指向某一列元素的指针。
下有二维数组 a[4][5]
:
像 int a[5];
这样的定义数组,我们称之为 静态数组 。在程序编译时,静态数组的内存空间就被分配好了,存储在栈上或者全局数据区。静态数组的大小在声明时确定且无法在运行时改变。静态数组的生命周期与其作用域相关。如果在函数内部声明静态数组,其生命周期为整个函数执行期间;如果在函数外部声明静态数组,其生命周期为整个程序的执行期间。
动态数组 是在运行时通过动态内存分配函数(如 malloc
和 calloc
)手动分配内存的数组。动态数组的内存空间在运行时通过动态内存分配函数手动分配,并存储在堆上。需要使用 malloc
、 calloc
等函数来申请内存,并使用 free
函数来释放内存。动态数组的大小在运行时可以根据需要进行调整。可以使用 realloc
函数来重新分配内存,并改变数组的大小。动态数组的生命周期由程序员控制。需要在使用完数组后手动释放内存,以避免内存泄漏。
1 | int size = 5; |
数组指针,其本质是指针,指向一个一维数组的指针。所以定义时,由运算符优先级,我们需要带上小括号
定义数组指针: 类型名 (*指针变量名)[长度];
1 | // test.c |
1 | 输出结果: |
指针数组,其本质是数组,是一个元素均为指针类型的数组。
定义指针数组: 类型名 *数组名[长度];
举例:(将数字转化为对应的英文月份)
1 | // test.c |
1 | 运行结果: |
函数名也代表函数的起始地址。
此内容直接看以下例子——比较两个数字的大小
1 | // test.c |
上代码等价于:
1 | // test.c |
注意 int (*p)(int,int)
;定义指向函数的指针变量: 类型名 (* 指针变量名)(函数参数表列);
指向函数的指针变量的一个重要用途是把函数的入口地址作为参数传递到其他函数,下附一个使用函数指针的例子;
1 | // test.c |
定义返回指针值得函数形式为: 类型名* 函数名(参数表列);
依然是使用一个例子:对若干学生成绩,找出不合格得课程得学生及其学生号,代码如下:
1 | // test.c |
指向指针数据的指针,可以嵌套,如:
1 | // test.c |
1 | 输出结果: |
共用体(Union) 是一种特殊的数据类型,允许在相同的内存位置存储不同的数据类型。可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。
为了定义共用体,必须使用 union
语句。 union
语句定义了一个新的数据类型,带有多个成员。定义格式如下:
1 | union [union tag] |
union tag
是可选的,每个 成员定义
是标准的变量定义,比如 int i;
或者 float f;
或者其他有效的变量定义。在共用体定义的末尾,最后一个分号之前,可以指定一个或多个共用体变量,这是可选的。
下面定义一个名为 Data
的共用体类型,有三个成员 i
、 f
和 str
:
1 | union Data |
也可以像枚举类型定义枚举类型的同时定义枚举变量一样,定义共用体类型的同时定义共用体变量。
1 | union Data |
Data
类型的变量可以存储一个整数、一个浮点数,或者一个字符串。这意味着一个变量(相同的内存位置)可以存储多个多种类型的数据。可以根据需要在一个共用体内使用任何内置的或者用户自定义的数据类型。
共用体占用的内存应足够存储共用体中最大的成员。 例如,在上面的实例中,Data
将占用10个字节的内存空间,因为在各个成员中,字符串所占用的空间是最大的。
使用共用体时,我们需要先创建共用体类型变量,再根据变量访问成员。
1 | union Data data; |
为了访问共用体的成员,使用成员访问运算符 .
,如 data.i
、 data.f
、 data.str
,但是在同一时间只能使用一个成员。
结构体 与共用体类似,不同的是结构体的成员并列存在,可同时使用。
1 | struct tag |
tag
是结构体标签,每个 成员定义
是标准的变量定义,比如 int i;
或者 float f;
或者其他有效的变量定义。在结构体定义的末尾,最后一个分号之前,可以指定一个或多个结构体变量,这是可选的。
如:
1 | //此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c |
第一个和第二声明被编译器当作两个完全不同的类型,即使他们的成员列表是一样的,如果令 t3=&s1
,则是非法的。
如果将结构体与基本数据类型做对比的话,举个例子:
1 | int a; |
那么 int
、 struct Tag
、 Tag2
是对等的,都是数据类型;而 a
、 b
和 c
是同级的,都是变量。
结构体的成员可以包含其他结构体,也可以包含指向自己结构体类型的指针。如果两个结构体互相包含,则需要对其中一个结构体进行不完整声明,如下所示:
1 | struct B; //对结构体B进行不完整声明 |
为了访问结构的成员,使用成员访问运算符 .
。对于结构体的初始化,可以使用 {}
:
1 | //book.c |
结构体作为函数的参数传入时,传参方式与其他类型的变量或指针类似,如: void printBooks(struct Books b);
,之后在函数里访问成员即可。
可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似,如:
1 | struct Books *struct_pointer; |
需要注意的是,使用指向该结构的指针访问结构的成员时,必须使用 ->
运算符。
使用 sizeof
运算符来计算结构体的大小,这在使用链表时有帮助。
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。而这些储存单元需要及时开辟,此时需要使用动态储存分配函数。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的 数据域 ,另一个是存储下一个结点地址的 指针域 。如:
1 | struct test |
链表还可分为动态链表和静态链表,此处不讨论静态链表,均为动态链表。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
关于动态储存分配函数有 calloc()
、 malloc()
、 free()
、 realloc()
。他们都存在于 stdlib.h
头文件中。
有的编译系统要求用 malloc.h
头文件而不是 stdlib.h
头文件,实际上这两个头文件都包含以上的动态储存分配函数。
除 free()
函数,其他函数都会返回一定的地址,需要对地址进行强制类型转换,转换为我们所需的数据类型。
函数名 | 函数原型 | 功能 | 返回值 |
---|---|---|---|
calloc() |
void *calloc(unsigned n, unsign size) |
分配n个大小为size的内存连续空间 | 成功时返回分配内存的起始地址,不成功返回0 |
malloc() |
void *calloc(unsigned size) |
分配1个大小为size的储存区 | 成功时返回分配内存起始地址,内存不足返回0 |
free() |
void free(void *p) |
释放p所指的内存区 | 无 |
realloc() |
void *calloc(viod *p, unsigned isze) |
将p所指的已分配内存区大小改为size,size可大于或小于原来大小 | 返回指向该内存的指针 |
单向链表,指针指向单向,连接方向单向,优点是简单,缺点是效率略低。
单向链表是由结点构成,所讨论的单向链表结点如下:
1 | struct test |
1 | //链表创建,返回一个表头head |
根据上述代码,演示过程如图:
链表的插入思路如图:
1 | // 在某链表中插入一个新的由指针引用的结构体 |
链表的删除思路如图:
1 | // 在某链表中查找num并删除该结构体,返回更新后的表头 |
思路简单,自行理解代码。
1 | // 在链表中查找值为obj的结构体,找到返回该结构体的指针,找不到返回NULL |
思路简单,自行理解代码。
1 | // while循环输出链表 |
双向链表,指针指向双向,连接方向双向。
双向链表是由结点构成,所讨论的双向链表结点如下:
1 | struct test |
双向链表结构如下图:
至于双向链表的创建,插入,删除,查找,不详细介绍。多一个方向的指针使得该链表可以往前遍历,提高其方便性。
循环链表的首位是相接的,构成一个环。所以还可以分成单向循环链表和双向循环链表。
可以使用 fopen()
函数来创建一个新的文件或者打开一个已有的文件,这个调用会初始化类型 FILE
的一个对象,类型 FILE
包含了所有用来控制流的必要的信息。下面是这个函数调用的原型:
1 | FILE *fopen(const char *filename, const char *mode); |
函数的第一参数是打开的文件名,常量字符指针类型,也能看成字符串类型;第二参数是打开文件的模式,有如下模式:
模式 | 描述 |
---|---|
r |
打开一个已有的文本文件,允许读取文件 |
r+ |
打开一个已有的文本文件,允许读写文件 |
w |
打开一个文本文件,允许写入。若文件不存在将新建文件,若文件存在则覆盖原有内容重新写入 |
w+ |
打开一个文本文件,允许读写。若文件不存在将新建文件,若文件存在则覆盖原有内容重新写入 |
a |
打开一个文本文件,允许写入。若文件不存在将新建文件,若文件存在则在原有内容上追加写入 |
a+ |
打开一个文本文件,允许读写。若文件不存在将新建文件,若文件存在则在原有内容上追加写入 |
如果是处理二进制文件的情况,则在原来的基础上加上 b
,如: rb
, wb
, ab
, rb+
, r+b
, wb+
, w+b
, ab+
, a+b
。
打开文件处理完成之后记得关闭文件,使用 fclose()
函数。
1 | int fclose(FILE *fp); |
如果成功关闭文件, fclose()
函数返回零,如果关闭文件时发生错误,函数返回 EOF
。这个函数实际上,会清空缓冲区中的数据,关闭文件,并释放用于该文件的所有内存。 EOF
是一个定义在头文件 stdio.h
中的常量,上述打开文件函数和关闭文件函数也都在 stdio.h
头文件中。
1 | // test.c |
fputc()
:把参数 c
的字符值写入到 fp
所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生错误,则会返回 EOF
。1 | int fputc(int c, FILE *fp); |
fputs()
:把字符串 s
写入到 fp
所指向的输出流中。如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF
。1 | int fputs(const char *s, FILE *fp); |
fprintf
:把一个字符串写入到文件中。该函数形式与 printf
函数类似,参考 printf
函数使用。1 | int fprintf(FILE *fp,const char *format, ...) |
1 | // 两函数比较 |
fgetc()
:从 fp
所指向的输入文件中读取一个字符,返回值是读取的字符,发生错误时返回 EOF
。1 | int fgetc(FILE *fp); |
fgets()
:从 fp
所指向的输入文件中读取n-1个字符,并把字符串复制到 buf
中,最后追加 NULL
终止字符串。该函数如果在读取最后一个字符之前就遇到一个换行符 ‘\n’ 或文件的末尾 EOF
,则只会返回读取到的字符,包括换行符。1 | char *fgets(char *buf, int n, FILE *fp ); |
fscanf()
:从文件中读取字符串,但是在遇到第一个空格和换行符时,它会停止读取。该函数形式与 scanf
函数类似,参考 scanf
函数使用,记得添加取地址符 &
。1 | int fscanf(FILE *fp, const char *format, ...) |
1 | // 两函数比较 |