# 前言

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
2
3
4
5
#include<stdio.h>
int main(){
printf("Hello World");
return 0;
}

按下 F5,或者点击窗口上方的本地 Windows 调试器运行第一个程序。

!!注意!!

代码中所有的符号,都需要使用英文输入法输入!(以后所有代码都是!)

# 注释

注释,即对代码的注解,以第一个程序为例:

1
2
3
4
5
6
7
8
9
/*
多行注释1
多行注释2
*/
#include<stdio.h>
int main(){
printf("Hello World");//在控制窗口打印字符串"Hello World"
return 0;
}

注释有利于别人阅读代码,也有利于自己长时间之后复读自己的代码。

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
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main(){
int var1;//声明一个整型变量
var1 = 0;//给var1赋值
float var2 = 0.123f;//声明并初始化一个单精度浮点型变量
double var3 = 0.123456;//声明并初始化一个双精度浮点型变量
char var4 = 'c';//声明并初始化一个字符型变量
printf("%d,%f,%lf,%c", var1, var2, var3, var4);//使用占位符%d,%f,%lf与%c输出相应变量。
return 0;
}

声明变量,即创建一个变量,然后使用 = 为其赋值。

注意:赋值行为是将等号右边的数值分配给等号左边的变量,不能写反!

第一次给变量赋值的行为,被叫做初始化,声明与初始化可以写在同一行,如 var2 与 var3

注意 2:请不要在未初始化的情况下调用变量,会导致程序错误!

IDE 会默认浮点数为双精度浮点数,在浮点数后面添加 f (或 F),标志其为单精度浮点数 (也可以不加)。

打印时,使用占位符进行占位,占位符与后面的变量需要一一对应。其中 % d 为整型的占位符,% f 为单精度浮点型的占位符,% lf 为双精度浮点型的占位符。

# 变量名:

变量命名时需要遵循一定的规则:

  • 变量名只能包含字母、数字、下划线和 $
  • 变量名只能以字母、下划线或者 $ 开头
  • 变量名不能使用关键字
  • 变量名严格区分大小写
1
2
3
4
5
6
7
8
9
//例如
int 123abc;//这是一个不符合语法的变量声明,会导致报错!
int int;//这是一个非法声明!
int 变量;//非法声明!

int var;//合法声明
int _var;//合法声明
int $var;//合法声明
int VAR;//合法声明,但需要注意,VAR不等于var

关键字:C 语言使用到的单词,例如:int,float,void 等,在起变量名或者函数名时需要避开。

关键字并不需要记忆,在 VS2022 中,当使用到关键字时,会被特殊的颜色标出。


除了必要的语法外,我们在日常编程中也有一些默认规则。

  • 变量名要做到见名知意
  • 变量名遵循驼峰法,或者下划线法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//例如
int var;//var是variable(变量)的缩写
int count;//count(计数),通常用于计数
int sum;//sum(和),通常用于求和

//上面的变量名,看到名字便知道其作用。
//需要使用两个及以上的单词去描述变量时,通常使用驼峰法或下划线法命名
//驼峰法即:单词的首字母大写,如:
int studentId;//学号
char studentAddress;//学生家庭地址

//下划线法即:使用下划线分割单词,如:
int student_name;//学生姓名
int student_class;//学生班级

驼峰法与下划线法的选择看个人喜好。

# 运算符

需要注意,运算符区分优先级,大致为:

数值运算符 > 比较运算符 > 逻辑运算符 (不绝对)

其中逻辑运算符中!> && > ||

具体优先级可以自行搜索

# 数值运算符

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
int main(){
int var = 1;//声明并初始化整型变量var

var += 10;//加运算示例
printf("%d\n", var);

var -= 10;//减运算示例
printf("%d\n", var);

var *= 10;//乘运算示例
printf("%d\n", var);

var /= 10;//除运算示例
printf("%d\n", var);

var = 11;//求余运算示例
var %= 10;
printf("%d\n", var);

printf("%d\n", var++);//后++示例
printf("%d\n", ++var);//前++示例
return 0;
}

运行上述代码,可以发现打印结果为:

1
2
3
4
5
6
7
11	//加法示例打印,var为1,var+=10即为var=var+10,所以结果为11
1 //减法示例打印,var此时为11,var-=10即为var=var-10,所以结果为1
10 //乘法示例打印,var此时为1,var*=10即为var=var*10,所以结果为10
1 //除法示例打印,var此时为10,var/=10即为var=var/10,所以结果为1
1 //求余示例打印,计算前给var赋值为11,所以var此时为11,var%=10即为var=var%10,所以结果为1
1 //后++示例
3 //前++示例

需要注意的是后 ++ 与前 ++ 的区分:

使用后 ++ 时,是首先使用变量 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
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main() {
int a = 1, b = 5;
printf("%d\n", a & b);
printf("%d\n", a | b);
printf("%d\n", a ^ b);
printf("%d\n", 1 << 5);
printf("%d\n", 32 >> 3);
return 0;c
}

会发现,最终输出的结果分别是 1,5,4,32 和 4 接下来我会对位运算做详细解释。

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
//因为会有对齐操作,为了避免转换为网页时缩进错序,所以使用代码块进行解释
//a的值是1,其对应的二进制数可以写为001,b的值是5,其对应的二进制数可以写为101
//当a和b进行与运算时,我们首先将其对齐,如下
//001
//101
//然后逐个按位比较,如果都是1,那么结果取1,否则取0,于是得到结果
//001
//转化为十进制数后,值是1,所以第一个printf的输出值为1

//而或运算则是:如果都是0,那么结果取0,否则取1。
//001
//101
//101
//按照或运算的规则,可以得到最后的结果为101,转化为十进制则是5,所以第二个printf的输出值为5

//异或运算则是:如果相同则取0,不同则取1
//001
//101
//100
//按照异或运算的规则,可以得到最后的结果为100,转化为十进制则是4,所以第三个printf的输出值为4

//左移运算:将二进制数左移x位,移动后在后面补0
//譬如对于1,其二进制为1,将其左移5位,然后在后面补0,那么结果就是10 0000,转化为十进制数则是32

//右移运算:将二进制数右移x位,移出部分删去
//譬如对于32,其二进制为10 0000,将其右移3位,那么就变为100,转化为十进制则是4

# 数据类型转换

将低精度值赋值给高精度值时,数据会自动转换类型,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
int main(){
char c = 'a';//声明并初始化字符型变量c
int number1 = 10;//声明并初始化整型变量number1
float number2 = 10.10;//声明并初始化单精度浮点型变量number2
double number3 = 100.100;//声明并初始化双精度浮点型变量number3

//当把高精度的数值,赋给低精度的变量时,会导致警告,例如:
number1 = number2;
//因为整型并不存在小数部分,所以将浮点型赋值给整型时,会导致小数部分的数据丢失,因此会被警告
//将高精度数值赋值给低精度变量时,可以强制转换其数据类型,例如:
number1 = (int)number2;//在前面使用(数据类型),来强制转换变量的数据类型

//而当低精度的数值,赋给高精度的变量时,则会自动转换其数据类型,例如:
number2 = number1;//并不需要强制转换

//基本数据类型中,可以自动转换的数据类型级别,大致如下:
//double > float > unsigned long > long > unsigned int > int > short
number3 = number2 = number1 = c;
printf("%lf", number3);//打印结果应为97.000000

return 0;
}

需要注意的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
当浮点型被转换为整型时,并不会采取四舍五入的方式,而是截断。

即var = 1.1得到的结果为var = 1

var = 1.6得到的结果也为var = 1
*/
#include<stdio.h>
int main(){
int var1 = 1.1;
int var2 = 1.6;
float var3 = 3 / 5;

printf("%d,%d,%f", var1, var2, var3);
return 0;
}

得到结果为 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. 去吃沙县小吃
  2. 去吃大盘鸡

而我们根据:哪个更便宜?这个条件对我们要做的选择进行判断。

在编程中,我们也可以实现类似的分支情况,代码如下:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main() {
int sha_xian = 20, da_pan_ji = 58;
if (sha_xian < da_pan_ji) {
printf("今晚吃沙县");
}
else {
printf("今晚吃大盘鸡");
}
return 0;
}

这部分代码,相信许多人只是读一遍,便能够理解,接下来我要详细介绍 if 语句的细节,首先是语法:

1
if (布尔类型) {需要执行的代码块}

这是一个 if 语句的语法,如果 if 后的代码块仅仅只有 1 句 (1 个分号为 1 句),那么可以不写大括号,如下:

1
2
if (布尔类型)
printf("只有一句代码时,可以不写大括号");

当括号内的布尔类型为真时,执行这条 if 语句,为假时,则不会执行。

可以使用上面提到的比较运算符来获得布尔类型,如上面的沙县、大盘鸡比较代码。

需要注意的是,if 语句可以单独使用,而 else 语句不可以,else 语句必须要有对应的 if 语句。

1
2
3
4
5
6
//这样写是正确的
if (布尔类型){需要执行的代码块}
else {需要执行的代码块}
//---------------------------------
//这样写是错误的
else {代码块}

当 if 内的布尔类型为假时,程序便会不执行 if 语句,直接进入 else 语句

如果 else 后面的语句仅仅只有 1 句,也可以不写大括号

else 语句的匹配规则遵循就近原则:

1
2
3
if (布尔类型) {代码块}//这个if语句没有else
if (布尔类型) {代码块}//下面的else语句会匹配离它最近的这个if语句
else {代码块}

回到上面的沙县与大盘鸡案例,我们可以想到,其价格的比较结果并非一定只有两种,还会有相同的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
int main() {
int sha_xian = 20, da_pan_ji = 58;
if (sha_xian < da_pan_ji) {
printf("今晚吃沙县");
}
else if (sha_xian == da_pan_ji) {
printf("吃哪个都可以");
}
else {
printf("今晚吃大盘鸡");
}
return 0;
}

可以在 if 的下面添加 else if 语句来判断这种情况,else if 语句的作用类似于 if 语句,但是不能单独使用

else if 可以存在不止一句

同样的,当 else if 语句后面只有一条语句时,可以不写大括号。

# switch 语句

在生活中,除了如同 if、else 这种分支,还存在多分支的情况,例如:

  • 如果今天是周一,我就去学 C 语言
  • 如果今天是周二,我就去学 C++
  • 如果今天是周三,我就去学 Java
  • 如果今天是周四,我就去学 Python
  • 如果今天是周五,我就去学 C#
  • 如果今天是周六,我就出去玩
  • 如果今天是周日,我就睡懒觉

当然,这种多分支的情况可以使用多个 else if 去实现,但是当分支过多,if 语句的效率通常不如 switch 语句。

首先介绍一下 switch 语句的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
switch(变量){
case1:
执行代码块;
break;
case2:
执行代码块;
break;
case3:
执行代码块;
break;
default:
执行代码块;
}

switch 后面的括号中的变量可以是整型或者字符型。

当变量的值等于值 1 时,便会执行 case 值 1: 后面的代码块。

需要注意的时,case 语句后面的是冒号而不是分号。

当 case 语句后的代码块被执行完后,如果有 break 语句,则会终止 switch,如果没有 break 语句,则会继续向下执行。

例如:

1
2
3
4
5
6
7
8
switch(key){
case 1:
printf("1");
case 2:
printf("2");
case 3:
printf("3");
}

对于上面的这个 switch 语句来说,如果 key 的值为 1,则最后的打印结果会是 123

如果 key 的值为 2,则最后的打印结果会是 23,这种现象被称为穿透效果

default 语句并非必要,可以不写

default 语句类似于 if 语句中的 else,当所有情况都不被匹配到时,会被执行。

如果 default 上面的语句并没有写 break;那么 default 也会被穿透。


接着我们回到上面的问题,如果使用 switch 语句实现,代码如下:

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
#include<stdio.h>
int main() {
int date = 1;
switch (date) {
case 1:
printf("今天是周一,我去学C语言");
break;
case 2:
printf("今天是周二,我去学C++");
break;
case 3:
printf("今天是周三,我去学Java");
break;
case 4:
printf("今天是周四,我去学Python");
break;
case 5:
printf("今天是周五,我去学C#");
break;
case 6:
printf("今天是周六,我出去玩");
break;
case 7:
printf("今天是周日,我睡懒觉");
break;
default:
printf("今天周几都不是,什么都不干");
}
return 0;
}

我们通过另一个案例来感受一下穿透效果:

小明的爸爸许诺给小明:

  • 如果小明的期末成绩高于 60 分,便给他 100 元钱
  • 如果高于 80 分,便给他买手机、和给他 100 元钱
  • 如果高于 100 分,便给他买电脑、手机,而且给他 100 元钱

使用 switch 实现的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int main() {
int score = 100;
switch (score / 10) {
case 10:
printf("给小明买电脑\n");
case 9:
case 8:
printf("给小明买手机\n");
case 7:
case 6:
printf("给小明100元钱\n");
}
return 0;
}

通过这个代码,我们很轻松的实现了小明爸爸的许诺这个案例。

# printf 与 scanf

printf 与 scanf 是 C 语言的输出和输入函数,其中 printf 已经被我们使用过很多次了,我这里将详细的介绍这两个函数。

# 转义符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//在这里顺便介绍一下转义符,即\
//反斜杠在编程语言中作为转义符存在,转义符,即转变含义的符号。
//例如,\n表示转换字母n的含义为next line 下一行,即换行符
#include<stdio.h>
int main(){
printf("\n\n\n");//换三行,效果相当于敲三次回车
return 0;
}
//使用markdown写东西时,转义符也会被识别,所以我在这里以注释的形式介绍转义符

//同时,转义符还可以将原本有特殊含义的字符转换,例如:
printf("打印双引号\"\n");
//在C语言中,双引号包住的内容是字符串,当使用printf进行打印时,如果写成"打印双引号"\n"
//则会被识别为"打印双引号"为一组内容\n"为另一组内容,此时右边的部分仅有1个双引号,并不能匹配,会导致报错
//这时需要使用转义符,转变其含义

//当需要printf转义符\时,可以使用 转义符 将 转义符 进行转义,即写成\\,这样会打印\

//常用的转义:\n 换行,\t 制表符,\0 空字符

# 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
2
printf("请输入一个整数:");
scanf("%d", &intNumber);

注意:scanf 在 VS2022 中被认为是一个可能导致危险漏洞的函数,因此不被允许使用,而下面的许多案例用到了 scanf 函数,因此需要做一些设置:点击项目,点击 (项目名) 属性,点击 C/C++,将其中的 SDL 检查从是改为否

# 常量、define 与 const

常量,与变量对应,是一个固定的,不可以改变的值,常量又被叫做字面量。

常量可以是任意的基本数据类型,比如整型常量、浮点常量、或字符串字面值、也有枚举常量。

常量可以直接在代码中使用,也可以通过定义常量来使用。

在代码中,前缀为 0x 或 0X 的数表示为十六进制数,例如:0x15AF2C

前缀为 0 的数表示为八进制数,例如:0457233


可以通过 #define 定义常量,例如:

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#define LENGTH 10
#define WIDTH 5
int main(){
int area;
area = LENGTH * WIDTH;
printf("value of area: %d", area);
return 0;
}

定义常量时,通常使用全大写字母的方式命名

需要注意的是,使用 #define 定义常量时,后面不要加分号!

我们通常通过这种常量定义方式,来给常量起名,以至于其在代码中更容易被读懂,或者是便于后期修改数据。


除了 #define,还可以使用 const 关键字声明指定类型的常量:

1
2
3
4
5
6
#include<stdio.h>
int main(){
const int CONSTANT = 10;
return 0;
}
//语法为:const type constant_name = value;

需要注意,使用 const 声明与初始化需要在一个语句内完成,不能如变量一样先声明再初始化。

# 循环语句

# while 语句

生活中也经常会遇到需要循环的情况,例如小明上课睡觉被老师抓到,被老师处罚:大喊一百遍我错了。

如果单纯的使用 printf 来实现这个例子,那么需要写一百条。

而使用循环,能够很简单的实现这种需要机械性重复的行为,以下是代码实现:

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main() {
int times = 0;//声明并初始化控制变量
while (times < 100) {
printf("我错了!");//打印
times++;//控制变量自增
}
return 0;
}

首先介绍一下 while 语句的语法:

1
2
3
4
5
6
7
8
9
//第一部分,声明并初始化控制变量
int var_name = var;
//第二部分,while循环的控制条件
while(条件){
//第三部分,需要被循环的代码块
printf("循环代码块");
//第四部分,控制变量迭代
var_name++;//通常为自增
}

首先在 while 语句的外部,定义一个变量,案例中我使用了 times 作为控制变量。

当 while 后面的括号里所写的表达式结果为真时,便会开始执行 while 语句下需要循环的代码块。

在每次循环结束时 (也可以是开始时),令控制变量自增 1,这样当循环被执行 100 次时,控制变量便自增了 100,循环就会因为不满足条件而被终止。如果循环内不写控制变量迭代的相关代码,很可能会导致死循环。

如果 while 下的代码块仅有一条语句,那么大括号是可以省略的。

我们通过另一个案例来进一步理解 while 循环:打印 1-100 之间的所有奇数:

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main() {
int number = 0;//声明并初始化控制变量
while (number < 100) {//循环条件
number++;//控制变量迭代
if (number % 2 != 0)//while中嵌套if
printf("%d是奇数\n", number);
}
return 0;
}

# do…while 语句

do…while 语句与 while 语句高度相似,其语法为:

1
2
3
4
5
6
7
8
//第一部分,声明并初始化控制变量
int var_name = var;
do {
//第二部分,需要被循环的代码块
printf("循环代码块");
//第三部分,控制变量迭代
var_name++;//通常为自增
}while(条件);//第四部分,while循环的控制条件

其与 while 语句的差别在于:无论是否满足 while 的条件,都会首先进行一次循环代码执行。

# for 语句

在 C 语言中,除了 while 循环,还有另一种循环语句,for 循环。

for 循环的语法如下:

1
2
3
for (声明并初始化控制变量; 条件 ; 控制变量迭代){//请注意,中间使用的是分号
printf("循环代码块");
}

与 while 类似,for 也有控制变量的概念,但不同的是,for 循环将控制变量的定义与迭代放到了同一行下。

for 语句下的循环代码块若仅有一句,可以省略大括号。

使用 for 循环实现打印 1-100 之间所有的偶数:

1
2
3
4
5
6
7
8
#include<stdio.h>
int main() {
for (int i = 1; i < 101; i++) {
if (i % 2 == 0)
printf("%d是偶数\n", i);
}
return 0;
}

for 循环的执行顺序是:

  1. 首先执行声明并初始化控制变量的部分,即第一个分号前的部分
  2. 然后进行条件判断,若符合,则进入循环
  3. 循环结束时,执行控制变量迭代的部分
  4. 控制变量迭代后,再次判断条件是否成立,若成立,则进入循环

# 循环练习,打印九九乘法表

打印九九乘法表是大部分初学循环的人锻炼循环语句的方法。

while 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
int main() {
int i = 1;//使用i控制行
while (i < 10) {
int j = 1;//使用j控制列
//将其写进循环,使得每次进入循环时将j的值重置为1
while (j < i + 1) {
printf("%d * %d = %d\t", j, i, i * j);
j++;//控制变量迭代
}
printf("\n");//每打印完1行,进行换行,以使其美观
i++;//控制变量迭代
}
return 0;
}

for 实现:

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main() {
for (int i = 1; i < 10; i++) {//使用i控制行
for (int j = 1; j < i + 1; j++)//使用j控制列
printf("%d * %d = %d\t", j, i, i * j);
printf("\n");//每打印完1行,进行换行,以使其美观
}
return 0;
}

# 函数

我们在中学时期,学习过一次线性函数:y=kx+b

可以发现,对于函数 y=kx+b,当你给它一个 x 值的时候,它总会返回给你一个 y 的值

而它返回的 y 值,是通过将 x 值乘 k 再加 b 获得的

如果将其转换为代码的形式,则可以写出如下代码:

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

int function(int x);//声明一个函数

int main() {
int a = 10;
int y = function(a);//调用函数
printf("%d", y);
return 0;
}

int function(int x) {//函数内部的具体实现
int y = K * x + B;//函数内部的代码块
return y;//函数的返回值
}

首先,我需要解释一下函数声明、实现的语法:

1
2
3
4
函数返回值的类型 函数名(函数要用到的参数){
函数内部的代码块;
return 返回值;
}

例如上面代码块中的 function,便是一个函数,其返回值的类型是整型,函数名叫 function,需要用的一个整型参数 x

函数与变量一样,在使用之前需要先对其声明,然后才能使用。

可以通过函数名对一个函数进行调用。

我们总是会写的 int main () 也是一个函数,被称为主函数,它的返回值类型是整型,函数名是 main,不需要任何参数。

主函数是一个程序的入口,程序总是会从 main 函数开始执行。一个项目下,只能有一个 main 函数


函数的主要作用,在于将一些可能会被反复使用的代码封装起来,以便于多次对其调用。

举一个例子来进一步说明函数的作用:

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
32
33
34
35
36
37
/*
使用C语言模拟一家咖啡店
*/
#include<stdio.h>
void coffee();//声明一个函数,该函数返回值类型为空,函数名为coffee,调用时不需要任何参数
void tea() {//函数也可以在声明的同时,进行定义(实现)
printf("倒水\n");
printf("煮沸水\n");
printf("泡茶\n");
printf("倒茶\n");
printf("上茶\n");
//函数返回值类型为空的函数可以不写return
}

int main() {
printf("第1位客人来了,他需要茶。\n");
tea();
printf("第2位客人来了,他需要咖啡。\n");
coffee();
printf("第3位客人来了,他需要咖啡。\n");
coffee();
printf("第4位客人来了,他需要咖啡。\n");
coffee();
printf("第5位客人来了,他需要茶。\n");
tea();
return 0;
}

void coffee() {//实现coffee函数
printf("倒水\n");
printf("煮沸水\n");
printf("泡咖啡\n");
printf("倒咖啡\n");
printf("上咖啡\n");
return;//也可以使用空return结束函数
printf("这条语句不会被执行,因为函数已经return了,就被结束掉了");
}

# 封装、头文件与源文件

对于许多编程人员来说,不要重复造轮子,是一句耳熟能详的名言。

而其中的原理,正是封装、头文件与源文件的应用。

我们可以注意到,当创建项目后,在解决方案下会有一个或者多个项目,而项目下会有源文件文件夹与头文件文件夹

右键点击头文件,添加,新建项,创建第一个头文件:FirstHeader.h

在头文件中,可以写下要封装的函数的声明,使用上面函数的咖啡店案例,代码如下:

1
2
3
4
5
6
7
8
//FirstHeader.h
//防止再次编译
#pragma once
//包含stdio.h(Standard Input Output.Header)头文件
#include<stdio.h>
//声明咖啡函数与茶函数
void coffee();
void tea();

然后右键源文件,添加,新建项,创建与其对应的源文件:FirstHeader.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//FirstHeader.c
#include"FirstHeader.h"//包含FirstHeader.c头文件
void coffee() {//实现coffee函数
printf("倒水\n");
printf("煮沸水\n");
printf("泡咖啡\n");
printf("倒咖啡\n");
printf("上咖啡\n");
}
void tea() {//实现tea函数
printf("倒水\n");
printf("煮沸水\n");
printf("泡茶\n");
printf("倒茶\n");
printf("上茶\n");
}

这样就可以进一步拆分函数,增强其独立性。当需要使用到这两个函数时,只需要在其他的文件中包含其头文件即可。

需要注意,个人编写的头文件包含时需要使用双引号:""(双引号也可以包含内置的文件)

而平常所写的 <> 仅会检索内置的头文件,使用方式如下:

1
2
3
4
5
6
7
#include<stdio.h>
#include"FirstHeader.h"//包含对应的头文件
int main() {
coffee();//调用函数
tea();//调用函数
return 0;
}

头文件与源文件的名字可以不相同,但通常我们会做成同名,以便于别人在阅读代码时通过头文件寻找源文件。

# 局部变量与全局变量

我们可以发现,当一个函数要调用一些主函数中已经定义了的变量数据时,就需要使用参数将其传递。

那么是否可以在一个函数中直接调用主函数的变量呢?

我们首先在主函数中定义变量:int test = 0;

然后使用另一个函数,在不传递参数的情况下,直接调用 test 去修改值,可以发现,这样是行不通的。

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
void testFunction();
int main(){
int test = 0;
testFunction();
return 0;
}
void testFunction(){
test = 1;//会报错
}

这是因为,在函数中定义的变量,属于局部变量,它的作用域仅在该函数 (仅在该函数中有效)。

不仅仅是函数,对于分支语句 (if、switch) 和循环语句 (for、while) 都是如此。

在语句内定义的变量,作用域仅在该语句中,当脱离了语句 (语句结束),变量就被销毁了。

如果想要定义一个变量,使得任何函数都可以访问和修改,那就需要定义全局变量。

全局变量的定义很简单,只要定义在函数外就可以了,如下:

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int test = 0;
void testFunction();
int main(){
test = 1;
testFunction();
return 0;
}
void testFunction(){
test = 2;
}

# 指针

# 初识

指针是 C 语言中一个十分重要的概念。

指针的长度为 4 个字节,内部存储的是一个十六进制数。

在数据类型的后面添加一个星号,以创建对应的类型指针,如下:

1
2
int * int_pointer;//声明一个整型指针
float * float_pointer;//声明一个浮点型指针

指针存储的十六进制数,是内存中对应的位置,也被称为地址

我们在 scanf 中见到过 & 符号,这个符号用于取出一个变量的地址,如下:

1
2
int number = 0;//声明并初始化一个整型
int * int_pointer = &number;//声明并初始化一个整型指针

地址是一个十分形象的名字,正如我们每个人都有自己的家庭地址,地址记录了一个变量在内存中存储的位置。

指针的初始化通常使用 NULL 来进行:

1
int * int_pointer = NULL;//声明并初始化一个空指针

当你创建了一个指针,但暂时不知道需要让它指向谁时,可以赋给其 NULL (空指针) 来避免错误调用未初始化的指针。

可以通过解引用符号 (星号),来调用一个指针所指向的地址中保存的值,如下:

1
2
3
4
5
int number = 0;//声明并初始化一个整型
int * int_pointer = &number;//声明并初始化一个整型指针
printf("%d\n", *int_pointer);//调用指针指向地址中保存的值
//也可以打印指针本身
printf("%p\n", int_pointer);//打印的结果是一个十六进制数

指针也可以做基本的运算,例如使用指针做自增运算:

1
2
3
int number = 0;//声明并初始化一个整型
int * int_pointer = &number;//声明并初始化一个整型指针
int_pointer++;//指针自增

指针自增时,会根据其类型增加相应的字节数,比如整型指针,实际是自增了一个 int (4 个字节) 的大小。

这种使用方式通常配合内存管理 (malloc、free) 或者数组使用,在后面会详细介绍

注意!这里仅做一个示范,实际上这样使用是错误的,会导致指针指向未知的内存空间。

# 值传递与指针传递

根据已经学习过的知识,我们可以简单的写出一个交换 a,b 变量值的程序:

1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main() {
int a = 10, b = 20;
int temp = a;//使用中间变量temp保存a的值
a = b;//将b的值赋给变量a
b = temp;//将保存的值赋给b,完成交换
printf("%d,%d", a, b);//打印检测
return 0;
}

然后将其封装成一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void Swap(int x, int y);
int main() {
int a = 10, b = 20;
Swap(a, b);
printf("%d,%d", a, b);//打印检测
return 0;
}
void Swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}

通过打印,我们可以发现,a 与 b 的值并未实现交换。

这与值传递、地址传递有关。

对于一个函数的形参 (形式参数,即上面函数中的 x 和 y),在调用函数时,会自动生成新的变量 x, y 然后把 a 和 b 的值赋给对应的形参。

这样我们就不难理解为什么 Swap 函数并未改变 a 与 b 的值,因为函数从始至终都未与变量 a、b 打过交道

那么如果我想要使用函数修改一个变量的值,该怎么做?那就是传递一个变量的地址,修改成如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
void Swap(int* x, int* y);//需要的参数修改为指针
int main() {
int a = 10, b = 20;
Swap(&a, &b);//使用&符号,取出a与b的地址,传递过去
printf("%d,%d", a, b);//打印检测
return 0;
}
void Swap(int* x, int* y) {
int temp = *x;//使用*来调用指针指向的地址中保存的值
*x = *y;
*y = temp;
}

因为每个变量对应的地址是唯一的,所以使用指针通过地址对值进行修改,就一定可以修改到目标变量。

另外,指针传递也可以减少值的复制,这一应用会在数组与结构体中体现。

# 函数指针与回调函数

函数指针是指向函数的指针变量。

通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。

函数指针可以像一般函数一样,用于调用函数、传递参数。

函数指针变量的声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
函数返回值类型 (*函数指针名)(参数类型列表) = 函数名;
#include<stdio.h>
int max(int a, int b){//声明并实现一个函数,该函数的作用:在a与b中取出更大值并返回
if (a > b)
return a;
return b;
}
int main(){
int x = 10, y = 20;
int (*pointer_max)(int, int) = max;//使用示例
printf("max is %d", pointer_max(x, y));//使用函数指针调用函数
return 0;
}

回调函数:当函数所需的参数列表中,包含函数指针时,该函数被称为回调函数。

即在函数中,通过函数指针调用另一个函数,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
void print_Function() {
printf("printFunction被调用\n");
}
void callback_Function(int times, void (*p_f_parameter)(void)) {//回调函数
for (int i = 0; i < times; i++)//使用for循环,调用times次函数指针所指向的函数
p_f_parameter();
}
int main() {
callbackFunction(10, print_Function);//这里仅写函数名,不能加括号,加上括号相当于调用函数,给了一个空值(函数返回为空)
return 0;
}

其中 callback_Function 为回调函数。

# 多级指针

正如每个变量都有自己对应的地址,指针变量作为一个存储指针的变量,也有自己的地址。

我们可以使用一个二级指针来保存指针变量对应的地址,如下:

1
2
3
int number = 1;//声明并初始化一个整型变量
int * int_pointer = &number;//声明并初始化一个指针变量
int ** level_2_pointer = &int_pointer;//声明并初始化一个二级指针变量

根据星号的数量可以判断一个指针的级别,以此类推,还有三级、四级、多级指针。

# 指针与常量

const 关键字与指针在一起使用时,有多种使用方法:

1
2
3
4
const int * p;//常量指针
int const * p;//常量指针

int *const p;//指针常量

前两者的效果是相同的,常量指针正如同它的名字,这是指向常量的一个指针

对于常量指针来说,是不可以通过解引用符 (星号) 去改变其地址中保存的值的,因为地址中保存的值是一个常量,常量是不可修改的

1
2
3
4
5
const int *p;//声明常量指针p
int a = 4;//声明并初始化变量a
p = &a;//将变量a的地址赋值给p
*p = 5;//错误,不能通过指针p来改变值
//但是这里可以通过修改a来修改值,因为a是变量

对于常量指针来说,指向的地址中存储的值不可修改,但指向的地址是可以修改的,如下:

1
2
int b = 5;//再声明并初始化一个变量b
p = &b;//将b的地址复制给p

而指针常量,指的是指针本身是一个常量,即指针指向的地址不可改变,但指向地址中存储的值可以改变,例如:

1
2
3
4
int a = 4, b = 5;
int *const p = &a;//和使用const定义常量一样,声明的同时就需要初始化
*p = 5;//这是被允许的
p = &b;//错误,不能修改一个常量

除此之外,还有指向常量的指针常量,即为以上二者的结合

1
const int *const p;//它存储的地址不允许被改变,地址中保存的值也不允许被改变

# 构造类型

# 数组

# 初识

数组,顾名思义,即为一组数。通过如下语法声明一个数组:

1
2
3
4
5
6
7
8
9
//方法1
数据类型 数组名[数组长度];
//示例
int array[10];//声明一个长度为10的数组

//方法2
数据类型 数组名[] = {数据1, 数据2, 数据3, 数据4, 数据5};
//示例
int array[] = {1, 2, 3, 4, 5};//这样创建的数组,会根据数据个数,自动确认其长度,该数组长度为5

创建数组时,也就相当于申请了一段连续的内存地址, 而数组名就是这串地址的头部位置。所以数组名的本质就是指针,它指向一段连续的内存地址的起始位置。

可以通过中括号调用其中存储的元素:

1
2
3
4
5
printf("%d\n", array[0]);
printf("%d\n", array[1]);
printf("%d\n", array[2]);
printf("%d\n", array[3]);
printf("%d\n", array[4]);

中括号 ([]) 在这里的应用类似于:

1
array[0] 即为 *(array + 0)以此类推

在指针一节讲过,对于指针来说,加 1 就相当于加 1 个其类型的字节数,比如 int 指针,加 1 相当于加了 4 个字节

所以当使用 array [4] 时,就相当于 *(array + 4),即后移了 16 个字节,调用了这段连续内存中的第 5 个整型

这也解释了为什么数组的下标 (即括号里的数字,也称索引) 为什么总是从 0 开始,因为 0 的位置就是其本身,即数组头

这里提到了一个很重要的点,** 数组的下标总是从 0 开始的!** 例如:

1
2
int array[] = {1, 2, 3, 4, 5};
//对于这个数组来说array[0]是1,以此类推

# 数组使用案例:存储排序并打印

我们通过一个案例来进一步了解数组:

让用户从键盘输入 10 个整数,将其存储在一个数组中,将数据进行排序,然后再打印出来。

这里我们使用到冒泡排序,实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>
//冒泡排序
void BubbleSort(int* arr, int len) {//第一个参数是数组,第二个参数是数组的长度
for (int i = len; i > 0; i--) {
for (int j = 0; j < i - 1; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

int main() {
int arr[10];
for (int i = 0; i < 10; i++)//从键盘输入10个整型数据
scanf("%d", &arr[i]);
BubbleSort(arr, 10);//因为数组名本身就是指针,所以不需要取地址符
for (int i = 0; i < 10; i++)//打印数组
printf("%d\n", arr[i]);
return 0;
}
//随便输入一组(10个整型)测试数据,来测试代码是否有问题

冒泡排序是一个十分简单的排序算法,它的逻辑是:

从第一个数开始,逐个对比两个数的大小,如果前者更大 (降序排序是更小),就交换他们,直至最后一个。

比如对于:

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
2
3
int arr[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}};
//或者这样
int arr[4][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};

创建时直接初始化的数组,声明可以省略行数,如下:

1
2
int arr[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12};
//但我感觉这并没有什么用,它增加了阅读难度,也许只有学校会考这个东西

调用时,其下标 (索引) 仍然是从 0 开始的,譬如第 1 行的第一个数据是:arr [0][0]

以二维数组类推,还有其他多维数组。

# 字符串

字符串,作为一个常用类型,有很多独特之处,需要单独拉出来说明。

C 语言中的字符串,实际上是以空字符’\0’结尾的一个字符数组。因此’\0’是用于标记字符串的结束的。

譬如我们写下一个字符数组:

1
2
3
4
5
6
7
8
char str[] = {'S', 'T', 'R', ' I', 'N', 'G', '\0'};
//也可以写成这样:
char str2[] = "STRING";
//这两种写法是等同的,编辑器会自动在字符串的后面补上'\0'
//但需要注意的是,'\0'也是一个字符,请不要这样定义一个字符串
char str3[6] = "STRING";//这样是错误的!
//因为STRING已经是6个字符了,所以字符数组已经满了,就不能在填充'\0'了
//但是在str3被使用时,编辑器会一直读到发现'\0'为止,这会导致越界访问

C 语言的字符数组是作为常量存在的,我们可以发现,当定义一个字符数组后,它就不可以再整体修改了:

1
2
3
4
5
6
#include<stdio.h>
int main() {
char str[] = "STRING";//定义一个字符数组
str = "ABCDEF";//会被提示:表达式必须是可修改的左值
return 0;
}

这是因为 str 的本质就是一个指针,在上面讲解数组的时候已经说过了,所以不可以进行整体修改,但我们仍可以逐个修改其元素:

1
str[0] = 'A';//这是被允许的

C 语言的 string.h 头文件下,提供了一些操作字符数组的函数:

1
2
3
4
5
6
7
#include<string.h>
strcpy(s1, s2);//复制字符串s2替换字符串s1
strcat(s1, s2);//将字符串s2连接到s1的末尾
strlen(s1);//返回字符串s1的长度
strcmp(s1, s2);//如果s1和s2相同,则返回0,如果s1<s2则返回小于0,否则返回大于0
strchr(s1, ch);//返回一个指针,指向字符串s1中字符ch第一次出现的位置
strstr(s1, s2);//返回一个指针,指向字符串s1中字符串s2第一次出现的位置

后续我们可能会使用到这些函数

# 结构体

# 初识

结构体也是 C 语言中相当重要的一个概念,更好的理解结构体,更有利于理解其他语言中面向对象编程

定义结构体的语法:

1
2
3
4
5
6
7
struct struct_tag {//结构体标签
member_element;//结构体成员元素
member_element;
member_element;
...
}variable_struct;//结构变量
//结构变量不能与结构体标签同名

一般情况下,结构体标签、成员元素、结构变量,这 3 部分至少要出现两个。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//形式1
#include<stdio.h>
struct {
int a;
char b;
float c;
}my_struct;
//使用这种方法,直接定义了一个结构变量my_struct,但是不再能创建第二个结构变量
//可以直接调用
int main() {
my_struct.a = 0;//通过点(.)调用结构体的成员元素
my_struct.b = '\0';
my_struct.c = 0.0;
return 0;
}

或者这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//形式2
#include<stdio.h>
struct struct_tag {
int a;
char b;
float c;
};
//使用这种方法,定义了一个struct_tag的结构,需要声明变量再使用,如下:
int main() {
struct struct_tag my_struct;//声明一个结构变量
my_struct.a = 0;//通过点(.)调用结构体的成员元素
my_struct.b = '\0';
my_struct.c = 0.0;
return 0;
}

另外结构体经常会配合 typedef 使用,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//配合typedef使用:
#include<stdio.h>
typedef struct {
int a;
char b;
float c;
}struct_name;
//使用这种方式,定义了一个叫做struct_name的新变量类型,可以把它当成int之类的变量类型使用
int main() {
struct_name my_struct;//声明一个结构变量
my_struct.a = 0;//通过点(.)调用结构体的成员元素
my_struct.b = '\0';
my_struct.c = 0.0;
return 0;
}

# typedef

使用 typedef 关键字可以自定义数据类型,如结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
#include<string.h>
typedef struct {
int id;
char name[10];//长度为10的字符数组
}Student;

int main() {
Student student;//声明一个结构变量
student.id = 123;
strcpy(student.name, "张三");//使用string.h头文件下的strcpy函数操作字符数组
printf("学生:%s, 学号:%d", student.name, student.id);//打印测试
return 0;
}

也可以用于给原有数据类型起别名:

1
2
3
4
5
6
typedef int ElementType;//给int起别名为ElementType
int main(){
ElementType data = 123456;//等同于int data = 123456;
printf("%d", data);
return 0;
}

# 共用体与枚举类型

# 共用体

共用体的定义方式与结构体类似,关键字为 union,定义如下:

1
2
3
4
5
6
union [union_tag] {//共用体标签
member_element;//共用体成员属性
member_element;
...
}[variable_union];//共用体变量
//与结构体一样,3取其2

共用体最大的特点,在于其共用同一块内存空间,而对于结构体而言,每个成员属性都是相互独立的。

共用体的大小取决于其占用内存最大的成员属性。譬如:

1
2
3
4
5
6
union Data {
int data1;
float data2;
char str[10];
}
//该共用体占用10个字节内存,因为其中str占用的空间是最大的

对于结构体中任意一个元素的修改,会导致其他元素也发生变化,这是共用体最大的特点。

# 枚举类型

枚举是 C 语言中的一种基本数据类型,用于定义一组具有离散值的常量。,它可以让数据更简洁,更易读。

枚举类型通常用于为程序中的一组相关的常量取名字,以便于程序的可读性和维护性。

定义一个枚举类型,需要使用 enum 关键字,后面跟着枚举类型的名称,以及用大括号 {} 括起来的一组枚举常量。每个枚举常量可以用一个标识符来表示,也可以为它们指定一个整数值,如果没有指定,那么默认从 0 开始递增。

定义枚举类型的语法:

1
enum 枚举名 {枚举元素1, 枚举元素2, 枚举元素3, ...};

比如我们要定义一个星期:

1
2
3
enum DAY {
MON=1, TUE, WED, THU, FRI, SAT, SUN//指定一个整数,然后它会从这个整数自动递增
};

使用枚举类型:

1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
enum DAY {
MON=1, TUE, WED, THU, FRI, SAT, SUN//指定一个整数,然后它会从这个整数自动递增
};
int main(){
enum DAY day;
day = WED;
printf("%d", day);
return 0;
}

枚举类型经常配合 switch 语句使用

# 内存管理

内存被划分为一些区块:栈、堆、静态存储区

通常,变量、函数都是直接开辟在栈上面的,而堆区的内存,通常由程序员手动申请和释放 (在 C 语言中是手动释放的)。

以下几个函数,可以用于申请可用内存:

1
2
3
4
5
6
7
8
9
10
11
void* calloc(int num, int size);
//在内存中动态的分配num个长度为size的连续空间,并将每一个字节都初始化为0,返回这段内存的首地址
void* malloc(int num);
//在堆区分配一块指定大小的内存空间,用来存放数据。这块数据不会被初始化,返回这段内存的首地址
void* realloc(void* address, int newsize);
//将address重新分配内存,新分配的内存大小为newsize

void free(void* address);//释放内存

//分配内存的函数都是void*类型的,在C语言中表示未分配类型,需要手动转换其类型。
//这些函数被包含在stdlib.h头文件下(standard library)

使用案例,动态数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
在之前讲到的数组中,我们通常定义一个已知大小的数组来存放数据。
但如果需要用户从键盘输入一组数据,而我们并不知道数据的个数,该怎么办呢?
这时,可以使用内存管理的函数,来实现动态数组
*/
#include<stdio.h>
#include<stdlib.h>
int main() {
int len = 0;
printf("请输入要存储的数组的元素个数:");
scanf("%d", &len);//让用户输入需要的数组长度
int* arr = (int*)malloc(sizeof(int) * len);//sizeof()可以返回类型的大小
for (int i = 0; i < len; i++) {
scanf("%d", &arr[i]);//让用户输入数据
}
for (int i = 0; i < len; i++) {
printf("%d\n", arr[i]);//打印数据,验证结果
}
free(arr);//释放申请的内存
return 0;
}

请注意,当使用 malloc 等函数申请内存空间时,使用完一定要记得手动释放内存。否则会导致内存泄露!

# 文件读写

# 打开与关闭文件

在 C 语言中,通过文件类型的变量来实现读写文件,如下:

1
2
3
4
#include<stdio.h>
int main(){
FILE* fp;//定义一个文件指针,指针名为fp
}

通过以下函数可以实现读写文件:

1
2
3
4
5
FILE *fopen(const char * filename,const char * mode);
//打开路径为filename的文件,打开模式为mode
int fclose(FILE * fp);
//关闭fp所打开的文件
//注意,在文件读写操作完成后,一定要记得关闭,否则可能会导致意外错误

打开模式包括以下几种:

mode 描述
r 只读的方式打开一个已存在文件
w 允许写入的方式打开一个文件,如果文件不存在,则会创建一个新的文件。写入时清空原有内容后再写入
a 追加写入的方式打开一个文件,如果文件不存在,则会创建一个新的文件。写入时在已有的内容后追加
r+ 读写的方式打开一个已存在文件
w+ 读写的方式打开一个文件,其他同 w
a+ 读写的方式打开一个文件,其他同 a

使用时需要添加双引号,如下:

1
2
3
4
fopen("./file_name.txt", "a");
//文件名前的./表示代码文件的同目录下,../表示上一级目录
//也可以使用绝对路径:如D:/File/file_name.txt
//文件路径需要用双引号包含

# 读取与写入文件

EOF:文件结束的标志 (End Of File),关闭文件时会自动添加,读取到 EOF 意味着文件结束。

相关函数原型:

1
2
3
4
5
6
7
8
int fputc(int c,FILE * fp);
//函数fputc()把参数c的字符写入到fp所指向的输出流中。如果写入成功,它会返回写入的字符,如果发生失误,则会返回EOF。
int fputs(const char *s,FILE *fp);
//函数fputs()把字符串s写入到fp所指向的输出流中。如果写入成功,它会返回一个非负值,如果错误,则会返回EOF。
int fgetc(FILE * fp);
//函数fgetc()从fp所指向的输入文件中读取一个字符。返回值是读取的字符,如果发生错误则返回EOF。
char * fgets(char * buf,int n,FILE * fp);
//函数fgets()从fp所指向的输入流中读取n-1个字符。它会把读取的字符串复制到缓冲区buf,并在最后追加一个null字符来终止字符串。

使用案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#define MAX_SIZE 50//定义常量MAX_SIZE
int main() {
FILE* fp;
fp = fopen("./file_name.txt", "w");//以重写的方式打开file_name.txt文件
char str[] = "Hello World!\n";//定义字符数组str
if(fputs(str, fp) == EOF)//写入str,如果返回EOF,报错
printf("写入错误!");
fclose(fp);//关闭文件

fp = fopen("./file_name.txt", "r");//以只读的方式打开file_name.txt文件
char buf[MAX_SIZE];//声明缓冲字符串buf
fgets(buf, MAX_SIZE, fp);//读取,当读取到换行符(或者EOF)会自动停止,因此第二个参数直接填MAX_SIZE就可以
printf("%s", buf);//打印缓冲字符串buf
fclose(fp);//关闭文件
return 0;
}

# 结语

写到这里差不多结束了,C 语言的基础大概就是这些,还有一部分不是很常用的 (我认为),例如位运算、原码、反码、补码

又或是三目运算符 (问号) 之类的,这些我都没有写。

如果感兴趣,想要更多了解细节,可以自己去查一查。

学完这里可以尝试写一些算法题,仅是这个教程的代码量,实在太少,纯新手我比较推荐 PTA,上面的题目比较简单,对新手友好。

也可以尝试力扣或是洛谷之类的更知名的算法网站,但我觉得这些算法网站的题目,学完数据结构与算法再写会好一下。

那么就到这里了!