C语言基础
# 前言
C 是我第一门系统性学习的高级语言,学习中笔记比较混乱,现在对编程有了更深的理解,所以我决定重置 C 语言的笔记,尽量写成一个零基础可以看着入门的笔记。
本笔记开发环境为 Visual Studio 2022
# 开发环境部署:
开发时所使用的工具被称为 IDE (Integrated Development Environment),集成开发环境。
之所以叫集成开发环境,是因为其一般集成了,代码编辑,测试运行等多项功能。
搜索并安装 Visual Studio 2022 社区版(官网),获取到 Visual Studio Installer
点击勾选 (使用 C++ 的桌面开发),点击安装
# C 语言简介
C 语言是一种高级语言,相较于 C 语言,还有更贴近底层的汇编语言,与底层的机器语言。
C 语言是由丹尼斯・麦卡利斯泰尔・里奇 (Dennis MacAlistair Ritchie) 在贝尔电话实验室工作时设计的。
C 语言是一门面向过程式的计算机程序设计语言。
# Hello World
打开安装好的 VS2022,点击创建新项目
选择空项目,语言为 C++。项目名称自定,位置自定,点击创建。
在右侧找到解决方案资源管理器,右键点击源文件 -> 添加 -> 新建项。
修改文件名为 FirstProgram.c
在左侧窗口中写入如下代码:
1 |
|
按下 F5,或者点击窗口上方的本地 Windows 调试器运行第一个程序。
!!注意!!
代码中所有的符号,都需要使用英文输入法输入!(以后所有代码都是!)
# 注释
注释,即对代码的注解,以第一个程序为例:
1 | /* |
注释有利于别人阅读代码,也有利于自己长时间之后复读自己的代码。
IDE 会忽略被注释的内容,并不将其作为代码处理。
C 语言中的注释有两种:
第一种为双斜杠的单行注释,如 printf 后的注释内容
第二种为 /**/ 的多行注释,如上面代码的多行注释
# 位 (bit) 与字节 (byte)
在日常使用计算机的时候,我们通常会看到一些数据单位,如 kb,MB,GB 等
其中 kb 指的是 kilo byte (千字节),mb 指的是 mega byte (兆字节)。诸如此类的还有 TB,PB,EB 等。
计算机是电子产品,对其来说,只存在开路 (0) 与闭路 (1)
我们将一个最基础的单位 (0 或 1) 称为位 (bit),将 8 个位合在一起称为一个字节 (byte)。
当我需要将整数 8 存入计算机时,8 会被转换为二进制数 1000 被存入到内存中。
这在 C 语言中,通常会占用 4 个字节,也就是 32 位。
# 数据类型
# 基本数据类型:
基本数据类型包括:
数值类型:
- 整型
- 浮点型
字符类型 (char)
其中整型包括:短整型 (short)、整型 (int)、长整型 (long)
其中浮点型包括:单精度型 (float)、双精度型 (double)
整型,即为整数。 浮点型,即为小数。
短整型、整型与长整型的差距在于其存储时,占用的字节数
类型 | 字节数 | 存储数值的范围 |
---|---|---|
short | 2 | -2^15, 2^15-1 |
int | 4 | -2^31, 2^31-1 |
long | 8 | -2^63, 2^63-1 |
这是大致的内存占用情况,需要注意的是,这些数据类型在不同的系统上,会占用不同的字节数,而非固定。
通过类型占用的字节数,可以计算出其存储数值范围。
例如:int 是 4 个字节,也就是 32 位,因为其存储的是二进制数,所以理论上范围应该是 [-2^32,2^32 - 1] 但第一位要用于存储其符号,也就是数值是正还是负,所以占用了一个位,则其范围变为 [-2^31,2^31 - 1],正数范围需要减一是为了存储 0
单精度浮点型与双精度浮点型的区别:
类型 | 字节 | 存储数值的范围 |
---|---|---|
float | 4 | -3.4*10^38, 3.4*10^38 |
double | 8 | -1.7*10^308, 1.7*10^308 |
其中单精度浮点数可以存储到小数点后 6 位数字,而双精度浮点数可以存储到小数点后 15 位数字。
打印时,默认保留 6 位小数
# 布尔类型
需要注意,C 语言并没有布尔类型,但是程序中经常会用到布尔类型,因此我在这里单独拿出来使用
在 C89 标准时,C 语言没有布尔类型,在后来的 C99 标准时,才引入了布尔类型。
布尔类型是只有两种值的数据类型,包含真 (True) 与假 (False)
在 C 语言中,通常使用 0 作为布尔类型的 False,非 0 作为布尔类型的 True
# 构造类型
构造类型包括:
- 数组 (array)
- 结构体 (struct)
- 共用体 (union)
- 枚举类型 (enum)
这四种类型会在后面讲到。
# 指针类型
指针类型通常占用 4 个字节,存储十六进制数,用于保存地址值,具体会在指针的部分讲到。
# 空类型
型如其名,空 (void)
# 变量
# 声明变量与赋值:
创建一个新的源文件,并将先前的第一个程序全部注释掉:
1 |
|
声明变量,即创建一个变量,然后使用 = 为其赋值。
注意:赋值行为是将等号右边的数值分配给等号左边的变量,不能写反!
第一次给变量赋值的行为,被叫做初始化,声明与初始化可以写在同一行,如 var2 与 var3
注意 2:请不要在未初始化的情况下调用变量,会导致程序错误!
IDE 会默认浮点数为双精度浮点数,在浮点数后面添加 f (或 F),标志其为单精度浮点数 (也可以不加)。
打印时,使用占位符进行占位,占位符与后面的变量需要一一对应。其中 % d 为整型的占位符,% f 为单精度浮点型的占位符,% lf 为双精度浮点型的占位符。
# 变量名:
变量命名时需要遵循一定的规则:
- 变量名只能包含字母、数字、下划线和 $
- 变量名只能以字母、下划线或者 $ 开头
- 变量名不能使用关键字
- 变量名严格区分大小写
1 | //例如 |
关键字:C 语言使用到的单词,例如:int,float,void 等,在起变量名或者函数名时需要避开。
关键字并不需要记忆,在 VS2022 中,当使用到关键字时,会被特殊的颜色标出。
除了必要的语法外,我们在日常编程中也有一些默认规则。
- 变量名要做到见名知意
- 变量名遵循驼峰法,或者下划线法
1 | //例如 |
驼峰法与下划线法的选择看个人喜好。
# 运算符
需要注意,运算符区分优先级,大致为:
数值运算符 > 比较运算符 > 逻辑运算符 (不绝对)
其中逻辑运算符中!> && > ||
具体优先级可以自行搜索
# 数值运算符
C 语言中提供一些数值运算的符号,如下:
符号 | 作用 | 使用方法 |
---|---|---|
= | 赋值运算符,将等号右侧的值赋给等号左边的变量 | var = 10 |
+ | 加运算符 | var = 1 + 2 |
- | 减运算符 | var = 2 - 1 |
* | 乘运算符 | var = 5 * 10 |
/ | 除运算符 | var = 10 / 5 |
% | 求余数运算符 | var = 11 % 5 (var 的值为 1) |
+= | 可以将右侧式子理解为 var = var + 10 | var += 10 |
-= | 可以将右侧式子理解为 var = var - 10 | var -= 10 |
*= | 可以将右侧式子理解为 var = var * 10 | var *= 10 |
/= | 可以将右侧式子理解为 var = var / 10 | var /= 10 |
%= | 可以将右侧式子理解为 var = var % 10 | var %= 10 |
++ | 自增运算,相当于 var = var + 1 | var++(或者 ++var) |
– | 自减运算,相当于 var = var - 1 | var–(或者–var) |
注意:var++ 与 ++var 使用方式并不相同,var-- 与 --var 同样
1 |
|
运行上述代码,可以发现打印结果为:
1 | 11 //加法示例打印,var为1,var+=10即为var=var+10,所以结果为11 |
需要注意的是后 ++ 与前 ++ 的区分:
使用后 ++ 时,是首先使用变量 var,再进行自增,所以当 var=1 时,使用 var++ 进行打印,首先使用 var 打印出 1,然后对其进行自增,则 var=2.
使用前 ++ 时,是首先自增,再使用变量 var,所以当 var=2 时,使用 ++var 进行打印,首先自增,使 var 变为 3,然后对 var 进行打印,打印出数字 3.
# 比较运算符
C 语言中提供一些比较运算的符号,如下:
符号 | 作用 | 使用方法 |
---|---|---|
< | 判断左值是否小于右值,返回布尔类型 | number1 < number2 |
> | 判断左值是否大于右值,返回布尔类型 | number1 > number2 |
<= | 判断左值是否小于或等于右值,返回布尔类型 | number1 <= number2 |
>= | 判断左值是否大于或等于右值,返回布尔类型 | number1 >= number2 |
== | 判断左值是否等于右值,返回布尔类型 | number1 == number2 |
!= | 判断左值是否不等于右值,返回布尔类型 | number1 != number2 |
注意:C 语言自带的数据类型中,并没有真正的布尔类型,只是使用零和非零模拟出的布尔类型
# 逻辑运算符
C 语言中提供一些逻辑运算的符号,如下:
符号 | 作用 | 使用 |
---|---|---|
&& | 逻辑与,判断左值与右值是否都为真,返回布尔类型 | a && b |
|| | 逻辑或,判断左值或右值是否为真,返回布尔类型 | a || b |
! | 逻辑非,取其相反值,返回布尔类型 (若 a 为 True,则!a 为 False) | !a |
逻辑与,逻辑或,逻辑非的运算结果如下:
&& 运算 | b = True | b = False |
---|---|---|
a = True | True | False |
a = False | False | False |
逻辑与:全真才真,一假皆假。
|| 运算 | b = True | b = False |
---|---|---|
a = True | True | True |
a = False | True | False |
逻辑或:一真即真,全假才假。
短路现象:
对于 a&&b,当 a 为假的时候,我们便可以立即断定 a&&b 就是假的,那么程序将不会再判断 b,这种现象被称为短路现象。
同样的,对于 a||b,当 a 为真,则 a||b 就一定是真的,程序也不会再判断 b。
# 位运算符
C 语言中提供一些位运算的符号,如下:
符号 | 作用 | 使用 |
---|---|---|
& | 对左值与右值进行与运算 | a & b |
| | 对左值与右值进行或运算 | a | b |
^ | 对左值与右值进行位运算 | a ^ b |
<< | 对左值进行左移运算,左移位数取决于右值 | 1 << 5 |
>> | 对左值进行右移运算,右移位数取决于右值 | 32 >> 3 |
使用如下代码进行测试:
1 |
|
会发现,最终输出的结果分别是 1,5,4,32 和 4 接下来我会对位运算做详细解释。
1 | //因为会有对齐操作,为了避免转换为网页时缩进错序,所以使用代码块进行解释 |
# 数据类型转换
将低精度值赋值给高精度值时,数据会自动转换类型,示例代码如下:
1 |
|
需要注意的是:
1 | /* |
得到结果为 1,1,0.000000
其中 var1 与 var2 已经解释过了,而其中为 float 类型的 var3 结果却是 0.000000
这是因为参与计算的数值均为整型,即 3 / 5,运算出来的返回结果也只能是整型,得到的结果是被截断过后的整型 0,最后打印出的结果是 0.000000
若要返回结果为浮点型,则需要使用浮点型数值参与运算,比如改为
1 | float var3 = 3.0 / 5; |
# 分支语句语句
# if、else if、else 语句
在生活中,我们经常会遇到分支情况,例如:
如果沙县小吃比大盘鸡更便宜,我今天就去吃沙县小吃,否则的话就去吃大盘鸡。
我们注意到,在这句话里出现了两条分支:
- 去吃沙县小吃
- 去吃大盘鸡
而我们根据:哪个更便宜?这个条件对我们要做的选择进行判断。
在编程中,我们也可以实现类似的分支情况,代码如下:
1 |
|
这部分代码,相信许多人只是读一遍,便能够理解,接下来我要详细介绍 if 语句的细节,首先是语法:
1 | if (布尔类型) {需要执行的代码块} |
这是一个 if 语句的语法,如果 if 后的代码块仅仅只有 1 句 (1 个分号为 1 句),那么可以不写大括号,如下:
1 | if (布尔类型) |
当括号内的布尔类型为真时,执行这条 if 语句,为假时,则不会执行。
可以使用上面提到的比较运算符来获得布尔类型,如上面的沙县、大盘鸡比较代码。
需要注意的是,if 语句可以单独使用,而 else 语句不可以,else 语句必须要有对应的 if 语句。
1 | //这样写是正确的 |
当 if 内的布尔类型为假时,程序便会不执行 if 语句,直接进入 else 语句
如果 else 后面的语句仅仅只有 1 句,也可以不写大括号
else 语句的匹配规则遵循就近原则:
1 | if (布尔类型) {代码块}//这个if语句没有else |
回到上面的沙县与大盘鸡案例,我们可以想到,其价格的比较结果并非一定只有两种,还会有相同的情况:
1 |
|
可以在 if 的下面添加 else if 语句来判断这种情况,else if 语句的作用类似于 if 语句,但是不能单独使用
else if 可以存在不止一句
同样的,当 else if 语句后面只有一条语句时,可以不写大括号。
# switch 语句
在生活中,除了如同 if、else 这种分支,还存在多分支的情况,例如:
- 如果今天是周一,我就去学 C 语言
- 如果今天是周二,我就去学 C++
- 如果今天是周三,我就去学 Java
- 如果今天是周四,我就去学 Python
- 如果今天是周五,我就去学 C#
- 如果今天是周六,我就出去玩
- 如果今天是周日,我就睡懒觉
当然,这种多分支的情况可以使用多个 else if 去实现,但是当分支过多,if 语句的效率通常不如 switch 语句。
首先介绍一下 switch 语句的语法:
1 | switch(变量){ |
switch 后面的括号中的变量可以是整型或者字符型。
当变量的值等于值 1 时,便会执行 case 值 1: 后面的代码块。
需要注意的时,case 语句后面的是冒号而不是分号。
当 case 语句后的代码块被执行完后,如果有 break 语句,则会终止 switch,如果没有 break 语句,则会继续向下执行。
例如:
1 | switch(key){ |
对于上面的这个 switch 语句来说,如果 key 的值为 1,则最后的打印结果会是 123
如果 key 的值为 2,则最后的打印结果会是 23,这种现象被称为穿透效果
default 语句并非必要,可以不写
default 语句类似于 if 语句中的 else,当所有情况都不被匹配到时,会被执行。
如果 default 上面的语句并没有写 break;那么 default 也会被穿透。
接着我们回到上面的问题,如果使用 switch 语句实现,代码如下:
1 |
|
我们通过另一个案例来感受一下穿透效果:
小明的爸爸许诺给小明:
- 如果小明的期末成绩高于 60 分,便给他 100 元钱
- 如果高于 80 分,便给他买手机、和给他 100 元钱
- 如果高于 100 分,便给他买电脑、手机,而且给他 100 元钱
使用 switch 实现的代码如下:
1 |
|
通过这个代码,我们很轻松的实现了小明爸爸的许诺这个案例。
# printf 与 scanf
printf 与 scanf 是 C 语言的输出和输入函数,其中 printf 已经被我们使用过很多次了,我这里将详细的介绍这两个函数。
# 转义符
1 | //在这里顺便介绍一下转义符,即\ |
# printf 函数
printf 函数是 C 语言的输出函数,其语法是:
1 | printf("打印内容(占位符)", 顶替占位符); |
printf 是我们已经很熟悉的一个函数,它的全称是 print function,打印函数,f 是 function 的缩写。
可以在 printf 的双引号里面写下占位符,从而打印变量。其中比较常用的占位符有:
%d | %f | %c | %i | %s | %% |
---|---|---|---|---|---|
整型 | 浮点型 | 字符型 | 十进制、八进制、十六进制数 | 字符串 | 输出 % |
浮点型占位符 % f 的细节:
1 | printf("%6.3f", floatNumber); |
在上面这条 printf 语句中,小数点前的 6 表示输出位宽为 6 的浮点型,小数点后面保留 3 位小数。
位宽即输出内容所占据的格数,如果 floatNumber 的值为 3.14,那么输出的结果会是:空格 3.140
为了避免看不出效果,我这里将空格直接写出来,其中空格占 1 位,小数点占 1 位,四个数字占 4 位,总共为 6
当输出内容不足位宽时,便会使用空格补足,如果输出内容超过位宽限制,那么会全部输出,不再受位宽限制
浮点型输出时默认保留六位小数。
# scanf 函数
scanf 函数 (scan function 扫描函数) 是 C 语言的输入函数,其语法是:
1 | scanf("占位符", &对应变量) |
需要注意的是,C 语言的 scanf 函数有严格的格式控制,例如:
1 | scanf("%d,%d", &intNumber1, &intNumber2); |
则在控制台输入的时候,必须以 1,2 的形式输入,中间的逗号不能缺少!
变量前面的 & 不能少!该符号为取地址符,地址的具体意义会在指针部分讲到。
占位符与 printf 相似,区别点在于 scanf 使用 % lf 来接收精度更高的数值到 double 变量
通常使用 printf 函数输出提示输入语句来配合 scanf 函数使用,例如:
1 | printf("请输入一个整数:"); |
注意:scanf 在 VS2022 中被认为是一个可能导致危险漏洞的函数,因此不被允许使用,而下面的许多案例用到了 scanf 函数,因此需要做一些设置:点击项目,点击 (项目名) 属性,点击 C/C++,将其中的 SDL 检查从是改为否
# 常量、define 与 const
常量,与变量对应,是一个固定的,不可以改变的值,常量又被叫做字面量。
常量可以是任意的基本数据类型,比如整型常量、浮点常量、或字符串字面值、也有枚举常量。
常量可以直接在代码中使用,也可以通过定义常量来使用。
在代码中,前缀为 0x 或 0X 的数表示为十六进制数,例如:0x15AF2C
前缀为 0 的数表示为八进制数,例如:0457233
可以通过 #define 定义常量,例如:
1 |
|
定义常量时,通常使用全大写字母的方式命名
需要注意的是,使用 #define 定义常量时,后面不要加分号!
我们通常通过这种常量定义方式,来给常量起名,以至于其在代码中更容易被读懂,或者是便于后期修改数据。
除了 #define,还可以使用 const 关键字声明指定类型的常量:
1 |
|
需要注意,使用 const 声明与初始化需要在一个语句内完成,不能如变量一样先声明再初始化。
# 循环语句
# while 语句
生活中也经常会遇到需要循环的情况,例如小明上课睡觉被老师抓到,被老师处罚:大喊一百遍我错了。
如果单纯的使用 printf 来实现这个例子,那么需要写一百条。
而使用循环,能够很简单的实现这种需要机械性重复的行为,以下是代码实现:
1 |
|
首先介绍一下 while 语句的语法:
1 | //第一部分,声明并初始化控制变量 |
首先在 while 语句的外部,定义一个变量,案例中我使用了 times 作为控制变量。
当 while 后面的括号里所写的表达式结果为真时,便会开始执行 while 语句下需要循环的代码块。
在每次循环结束时 (也可以是开始时),令控制变量自增 1,这样当循环被执行 100 次时,控制变量便自增了 100,循环就会因为不满足条件而被终止。如果循环内不写控制变量迭代的相关代码,很可能会导致死循环。
如果 while 下的代码块仅有一条语句,那么大括号是可以省略的。
我们通过另一个案例来进一步理解 while 循环:打印 1-100 之间的所有奇数:
1 |
|
# do…while 语句
do…while 语句与 while 语句高度相似,其语法为:
1 | //第一部分,声明并初始化控制变量 |
其与 while 语句的差别在于:无论是否满足 while 的条件,都会首先进行一次循环代码执行。
# for 语句
在 C 语言中,除了 while 循环,还有另一种循环语句,for 循环。
for 循环的语法如下:
1 | for (声明并初始化控制变量; 条件 ; 控制变量迭代){//请注意,中间使用的是分号 |
与 while 类似,for 也有控制变量的概念,但不同的是,for 循环将控制变量的定义与迭代放到了同一行下。
for 语句下的循环代码块若仅有一句,可以省略大括号。
使用 for 循环实现打印 1-100 之间所有的偶数:
1 |
|
for 循环的执行顺序是:
- 首先执行声明并初始化控制变量的部分,即第一个分号前的部分
- 然后进行条件判断,若符合,则进入循环
- 循环结束时,执行控制变量迭代的部分
- 控制变量迭代后,再次判断条件是否成立,若成立,则进入循环
# 循环练习,打印九九乘法表
打印九九乘法表是大部分初学循环的人锻炼循环语句的方法。
while 实现:
1 |
|
for 实现:
1 |
|
# 函数
我们在中学时期,学习过一次线性函数:y=kx+b
可以发现,对于函数 y=kx+b,当你给它一个 x 值的时候,它总会返回给你一个 y 的值
而它返回的 y 值,是通过将 x 值乘 k 再加 b 获得的
如果将其转换为代码的形式,则可以写出如下代码:
1 |
|
首先,我需要解释一下函数声明、实现的语法:
1 | 函数返回值的类型 函数名(函数要用到的参数){ |
例如上面代码块中的 function,便是一个函数,其返回值的类型是整型,函数名叫 function,需要用的一个整型参数 x
函数与变量一样,在使用之前需要先对其声明,然后才能使用。
可以通过函数名对一个函数进行调用。
我们总是会写的 int main () 也是一个函数,被称为主函数,它的返回值类型是整型,函数名是 main,不需要任何参数。
主函数是一个程序的入口,程序总是会从 main 函数开始执行。一个项目下,只能有一个 main 函数
函数的主要作用,在于将一些可能会被反复使用的代码封装起来,以便于多次对其调用。
举一个例子来进一步说明函数的作用:
1 | /* |
# 封装、头文件与源文件
对于许多编程人员来说,不要重复造轮子,是一句耳熟能详的名言。
而其中的原理,正是封装、头文件与源文件的应用。
我们可以注意到,当创建项目后,在解决方案下会有一个或者多个项目,而项目下会有源文件文件夹与头文件文件夹
右键点击头文件,添加,新建项,创建第一个头文件:FirstHeader.h
在头文件中,可以写下要封装的函数的声明,使用上面函数的咖啡店案例,代码如下:
1 | //FirstHeader.h |
然后右键源文件,添加,新建项,创建与其对应的源文件:FirstHeader.c
1 | //FirstHeader.c |
这样就可以进一步拆分函数,增强其独立性。当需要使用到这两个函数时,只需要在其他的文件中包含其头文件即可。
需要注意,个人编写的头文件包含时需要使用双引号:""(双引号也可以包含内置的文件)
而平常所写的 <> 仅会检索内置的头文件,使用方式如下:
1 |
|
头文件与源文件的名字可以不相同,但通常我们会做成同名,以便于别人在阅读代码时通过头文件寻找源文件。
# 局部变量与全局变量
我们可以发现,当一个函数要调用一些主函数中已经定义了的变量数据时,就需要使用参数将其传递。
那么是否可以在一个函数中直接调用主函数的变量呢?
我们首先在主函数中定义变量:int test = 0;
然后使用另一个函数,在不传递参数的情况下,直接调用 test 去修改值,可以发现,这样是行不通的。
1 |
|
这是因为,在函数中定义的变量,属于局部变量,它的作用域仅在该函数 (仅在该函数中有效)。
不仅仅是函数,对于分支语句 (if、switch) 和循环语句 (for、while) 都是如此。
在语句内定义的变量,作用域仅在该语句中,当脱离了语句 (语句结束),变量就被销毁了。
如果想要定义一个变量,使得任何函数都可以访问和修改,那就需要定义全局变量。
全局变量的定义很简单,只要定义在函数外就可以了,如下:
1 |
|
# 指针
# 初识
指针是 C 语言中一个十分重要的概念。
指针的长度为 4 个字节,内部存储的是一个十六进制数。
在数据类型的后面添加一个星号,以创建对应的类型指针,如下:
1 | int * int_pointer;//声明一个整型指针 |
指针存储的十六进制数,是内存中对应的位置,也被称为地址
我们在 scanf 中见到过 & 符号,这个符号用于取出一个变量的地址,如下:
1 | int number = 0;//声明并初始化一个整型 |
地址是一个十分形象的名字,正如我们每个人都有自己的家庭地址,地址记录了一个变量在内存中存储的位置。
指针的初始化通常使用 NULL 来进行:
1 | int * int_pointer = NULL;//声明并初始化一个空指针 |
当你创建了一个指针,但暂时不知道需要让它指向谁时,可以赋给其 NULL (空指针) 来避免错误调用未初始化的指针。
可以通过解引用符号 (星号),来调用一个指针所指向的地址中保存的值,如下:
1 | int number = 0;//声明并初始化一个整型 |
指针也可以做基本的运算,例如使用指针做自增运算:
1 | int number = 0;//声明并初始化一个整型 |
指针自增时,会根据其类型增加相应的字节数,比如整型指针,实际是自增了一个 int (4 个字节) 的大小。
这种使用方式通常配合内存管理 (malloc、free) 或者数组使用,在后面会详细介绍
注意!这里仅做一个示范,实际上这样使用是错误的,会导致指针指向未知的内存空间。
# 值传递与指针传递
根据已经学习过的知识,我们可以简单的写出一个交换 a,b 变量值的程序:
1 |
|
然后将其封装成一个函数:
1 |
|
通过打印,我们可以发现,a 与 b 的值并未实现交换。
这与值传递、地址传递有关。
对于一个函数的形参 (形式参数,即上面函数中的 x 和 y),在调用函数时,会自动生成新的变量 x, y 然后把 a 和 b 的值赋给对应的形参。
这样我们就不难理解为什么 Swap 函数并未改变 a 与 b 的值,因为函数从始至终都未与变量 a、b 打过交道
那么如果我想要使用函数修改一个变量的值,该怎么做?那就是传递一个变量的地址,修改成如下代码:
1 |
|
因为每个变量对应的地址是唯一的,所以使用指针通过地址对值进行修改,就一定可以修改到目标变量。
另外,指针传递也可以减少值的复制,这一应用会在数组与结构体中体现。
# 函数指针与回调函数
函数指针是指向函数的指针变量。
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数。
函数指针变量的声明:
1 | 函数返回值类型 (*函数指针名)(参数类型列表) = 函数名; |
回调函数:当函数所需的参数列表中,包含函数指针时,该函数被称为回调函数。
即在函数中,通过函数指针调用另一个函数,示例如下:
1 |
|
其中 callback_Function 为回调函数。
# 多级指针
正如每个变量都有自己对应的地址,指针变量作为一个存储指针的变量,也有自己的地址。
我们可以使用一个二级指针来保存指针变量对应的地址,如下:
1 | int number = 1;//声明并初始化一个整型变量 |
根据星号的数量可以判断一个指针的级别,以此类推,还有三级、四级、多级指针。
# 指针与常量
const 关键字与指针在一起使用时,有多种使用方法:
1 | const int * p;//常量指针 |
前两者的效果是相同的,常量指针正如同它的名字,这是指向常量的一个指针。
对于常量指针来说,是不可以通过解引用符 (星号) 去改变其地址中保存的值的,因为地址中保存的值是一个常量,常量是不可修改的
1 | const int *p;//声明常量指针p |
对于常量指针来说,指向的地址中存储的值不可修改,但指向的地址是可以修改的,如下:
1 | int b = 5;//再声明并初始化一个变量b |
而指针常量,指的是指针本身是一个常量,即指针指向的地址不可改变,但指向地址中存储的值可以改变,例如:
1 | int a = 4, b = 5; |
除此之外,还有指向常量的指针常量,即为以上二者的结合
1 | const int *const p;//它存储的地址不允许被改变,地址中保存的值也不允许被改变 |
# 构造类型
# 数组
# 初识
数组,顾名思义,即为一组数。通过如下语法声明一个数组:
1 | //方法1 |
创建数组时,也就相当于申请了一段连续的内存地址, 而数组名就是这串地址的头部位置。所以数组名的本质就是指针,它指向一段连续的内存地址的起始位置。
可以通过中括号调用其中存储的元素:
1 | printf("%d\n", array[0]); |
中括号 ([]) 在这里的应用类似于:
1 | array[0] 即为 *(array + 0)以此类推 |
在指针一节讲过,对于指针来说,加 1 就相当于加 1 个其类型的字节数,比如 int 指针,加 1 相当于加了 4 个字节
所以当使用 array [4] 时,就相当于 *(array + 4),即后移了 16 个字节,调用了这段连续内存中的第 5 个整型
这也解释了为什么数组的下标 (即括号里的数字,也称索引) 为什么总是从 0 开始,因为 0 的位置就是其本身,即数组头
这里提到了一个很重要的点,** 数组的下标总是从 0 开始的!** 例如:
1 | int array[] = {1, 2, 3, 4, 5}; |
# 数组使用案例:存储排序并打印
我们通过一个案例来进一步了解数组:
让用户从键盘输入 10 个整数,将其存储在一个数组中,将数据进行排序,然后再打印出来。
这里我们使用到冒泡排序,实现代码如下:
1 |
|
冒泡排序是一个十分简单的排序算法,它的逻辑是:
从第一个数开始,逐个对比两个数的大小,如果前者更大 (降序排序是更小),就交换他们,直至最后一个。
比如对于:
9,5,6,12,7
我们首先比较第一个数与第二个数,即 9 与 5,9 更大,交换其位置,变为:
5,9,6,12,7
然后比较第二个数与第三个数,即 9 与 6,9 更大,交换其位置,变为:
5,6,9,12,7
然后继续这样的操作,直到最后一个数,这样执行一轮后,我们就一定将最大的数排到了最后一个。第一轮结束的结果如下:
5,6,9,7,12
然后执行第二轮操作,重复上述操作到倒数第二个数字,就能将第二大的数排到倒数第二位。
这样的排序方式像是冒泡泡,每次都将一个最大的 (或者最小的) 数排出来,因此被称为冒泡排序。
# 多维数组
多维数组最简单的形式是二维数组:
1 | 数值类型 数组名[行数][列数]; |
比如我们要实现一个四行三列的二维数组:
1 | 2 | 3 |
---|---|---|
4 | 5 | 6 |
7 | 8 | 9 |
10 | 11 | 12 |
实现如下:
1 | int arr[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}}; |
创建时直接初始化的数组,声明可以省略行数,如下:
1 | int arr[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; |
调用时,其下标 (索引) 仍然是从 0 开始的,譬如第 1 行的第一个数据是:arr [0][0]
以二维数组类推,还有其他多维数组。
# 字符串
字符串,作为一个常用类型,有很多独特之处,需要单独拉出来说明。
C 语言中的字符串,实际上是以空字符’\0’结尾的一个字符数组。因此’\0’是用于标记字符串的结束的。
譬如我们写下一个字符数组:
1 | char str[] = {'S', 'T', 'R', ' I', 'N', 'G', '\0'}; |
C 语言的字符数组是作为常量存在的,我们可以发现,当定义一个字符数组后,它就不可以再整体修改了:
1 |
|
这是因为 str 的本质就是一个指针,在上面讲解数组的时候已经说过了,所以不可以进行整体修改,但我们仍可以逐个修改其元素:
1 | str[0] = 'A';//这是被允许的 |
C 语言的 string.h 头文件下,提供了一些操作字符数组的函数:
1 |
|
后续我们可能会使用到这些函数
# 结构体
# 初识
结构体也是 C 语言中相当重要的一个概念,更好的理解结构体,更有利于理解其他语言中面向对象编程
定义结构体的语法:
1 | struct struct_tag {//结构体标签 |
一般情况下,结构体标签、成员元素、结构变量,这 3 部分至少要出现两个。如下:
1 | //形式1 |
或者这样定义:
1 | //形式2 |
另外结构体经常会配合 typedef 使用,使用方式如下:
1 | //配合typedef使用: |
# typedef
使用 typedef 关键字可以自定义数据类型,如结构体:
1 |
|
也可以用于给原有数据类型起别名:
1 | typedef int ElementType;//给int起别名为ElementType |
# 共用体与枚举类型
# 共用体
共用体的定义方式与结构体类似,关键字为 union,定义如下:
1 | union [union_tag] {//共用体标签 |
共用体最大的特点,在于其共用同一块内存空间,而对于结构体而言,每个成员属性都是相互独立的。
共用体的大小取决于其占用内存最大的成员属性。譬如:
1 | union Data { |
对于结构体中任意一个元素的修改,会导致其他元素也发生变化,这是共用体最大的特点。
# 枚举类型
枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。,它可以让数据更简洁,更易读。
枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。
定义一个枚举类型,需要使用 enum 关键字,后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。
定义枚举类型的语法:
1 | enum 枚举名 {枚举元素1, 枚举元素2, 枚举元素3, ...}; |
比如我们要定义一个星期:
1 | enum DAY { |
使用枚举类型:
1 |
|
枚举类型经常配合 switch 语句使用
# 内存管理
内存被划分为一些区块:栈、堆、静态存储区
通常,变量、函数都是直接开辟在栈上面的,而堆区的内存,通常由程序员手动申请和释放 (在 C 语言中是手动释放的)。
以下几个函数,可以用于申请可用内存:
1 | void* calloc(int num, int size); |
使用案例,动态数组:
1 | /* |
请注意,当使用 malloc 等函数申请内存空间时,使用完一定要记得手动释放内存。否则会导致内存泄露!
# 文件读写
# 打开与关闭文件
在 C 语言中,通过文件类型的变量来实现读写文件,如下:
1 |
|
通过以下函数可以实现读写文件:
1 | FILE *fopen(const char * filename,const char * mode); |
打开模式包括以下几种:
mode | 描述 |
---|---|
r | 以只读的方式打开一个已存在文件 |
w | 以允许写入的方式打开一个文件,如果文件不存在,则会创建一个新的文件。写入时清空原有内容后再写入。 |
a | 以追加写入的方式打开一个文件,如果文件不存在,则会创建一个新的文件。写入时在已有的内容后追加。 |
r+ | 以读写的方式打开一个已存在文件 |
w+ | 以读写的方式打开一个文件,其他同 w |
a+ | 以读写的方式打开一个文件,其他同 a |
使用时需要添加双引号,如下:
1 | fopen("./file_name.txt", "a"); |
# 读取与写入文件
EOF:文件结束的标志 (End Of File),关闭文件时会自动添加,读取到 EOF 意味着文件结束。
相关函数原型:
1 | int fputc(int c,FILE * fp); |
使用案例:
1 |
|
# 结语
写到这里差不多结束了,C 语言的基础大概就是这些,还有一部分不是很常用的 (我认为),例如位运算、原码、反码、补码
又或是三目运算符 (问号) 之类的,这些我都没有写。
如果感兴趣,想要更多了解细节,可以自己去查一查。
学完这里可以尝试写一些算法题,仅是这个教程的代码量,实在太少,纯新手我比较推荐 PTA,上面的题目比较简单,对新手友好。
也可以尝试力扣或是洛谷之类的更知名的算法网站,但我觉得这些算法网站的题目,学完数据结构与算法再写会好一下。
那么就到这里了!