基础

## 一、前言 ## 二、目录 ### C语言初阶 #### 1.初识C语言 > [史上最强C语言教程----万字初识C语言](https://blog.csdn.net/m0_57304511/article/details/122280010 "史上最强C语言教程----万字初识C语言") #### 2、分支和循环 > [史上最强C语言教程----分支和循环(1)](https://blog.csdn.net/m0_57304511/article/details/120982188 "史上最强C语言教程----分支和循环(1)") > > [史上最强C语言教程----分支和循环(2)](https://blog.csdn.net/m0_57304511/article/details/120998708 "史上最强C语言教程----分支和循环(2)") > > [史上最强C语言教程----分支和循环(3)](https://blog.csdn.net/m0_57304511/article/details/121019146 "史上最强C语言教程----分支和循环(3)") > > [史上最强C语言教程----分支和循环(4)](https://blog.csdn.net/m0_57304511/article/details/121228404 "史上最强C语言教程----分支和循环(4)") > > [史上最强C语言教程----分支和循环(5--终篇)](https://blog.csdn.net/m0_57304511/article/details/121323066 "史上最强C语言教程----分支和循环(5--终篇)") #### 3、函数 > [史上最强C语言教程----函数(1)](https://blog.csdn.net/m0_57304511/article/details/121379393 "史上最强C语言教程----函数(1)") > > [史上最强C语言教程----函数(2)](https://blog.csdn.net/m0_57304511/article/details/121861105 "史上最强C语言教程----函数(2)") #### 4、数组 > [史上最强C语言教程----数组(初阶)](https://blog.csdn.net/m0_57304511/article/details/121916437 "史上最强C语言教程----数组(初阶)") #### 5、操作符 > [史上最强C语言教程----操作符详解](https://blog.csdn.net/m0_57304511/article/details/122016293 "史上最强C语言教程----操作符详解") #### 6、指针 > [史上最强C语言教程----指针(初阶)](https://blog.csdn.net/m0_57304511/article/details/122259605 "史上最强C语言教程----指针(初阶)") #### 7、结构体 > [史上最强C语言教程----结构体(初阶)](https://blog.csdn.net/m0_57304511/article/details/122268935 "史上最强C语言教程----结构体(初阶)") #### 8、三子棋 > [呆头呆脑的电脑----三子棋小游戏(C语言版)](https://blog.csdn.net/m0_57304511/article/details/121193448 "呆头呆脑的电脑----三子棋小游戏(C语言版)") #### 9、扫雷 > [扫雷(C语言版)](https://blog.csdn.net/m0_57304511/article/details/121252613 "扫雷(C语言版)") #### 10、函数栈帧的创建与销毁 > [超硬核---从汇编角度带你了解函数(建议保存)](https://blog.csdn.net/m0_57304511/article/details/121432378 "超硬核---从汇编角度带你了解函数(建议保存)") ### C语言进阶 #### 1、数据的存储 > [史上最强C语言教程----数据在内存中的存储](https://blog.csdn.net/m0_57304511/article/details/121568443 "史上最强C语言教程----数据在内存中的存储") #### 2、指针的进阶 > [史上最强C语言教程----指针(进阶部分1)](https://blog.csdn.net/m0_57304511/article/details/122290553 "史上最强C语言教程----指针(进阶部分1)") > > [史上最强C语言教程----指针(进阶部分2)](https://blog.csdn.net/m0_57304511/article/details/122324412 "史上最强C语言教程----指针(进阶部分2)") > > [史上最强C语言教程----指针(笔试题1)](https://blog.csdn.net/m0_57304511/article/details/122362852 "史上最强C语言教程----指针(笔试题1)") > > [指针相关笔试题(附解析)----史上最强C语言教程](https://blog.csdn.net/m0_57304511/article/details/122367969 "指针相关笔试题(附解析)----史上最强C语言教程") #### 3、字符串+内存函数的介绍 > [史上最强C语言教程----字符函数&字符串函数](https://blog.csdn.net/m0_57304511/article/details/122435397 "史上最强C语言教程----字符函数&字符串函数") > #### 4、自定义类型讲解 > [史上最强C语言教程----结构体(进阶)](https://blog.csdn.net/m0_57304511/article/details/122456486 "史上最强C语言教程----结构体(进阶)") > > [史上最强C语言教程----枚举和联合](https://blog.csdn.net/m0_57304511/article/details/122501569 "史上最强C语言教程----枚举和联合") > > [史上最强C语言教程----位段](https://blog.csdn.net/m0_57304511/article/details/122460735 "史上最强C语言教程----位段") #### 5、动态内存管理 > [史上最强C语言教程----动态内存管理(1)](https://blog.csdn.net/m0_57304511/article/details/122547455 "史上最强C语言教程----动态内存管理(1)") > > [史上最强C语言教程----动态内存管理(2)](https://blog.csdn.net/m0_57304511/article/details/122567229 "史上最强C语言教程----动态内存管理(2)") #### 6、文件操作 > [史上最强C语言教程----文件操作(1)](https://blog.csdn.net/m0_57304511/article/details/122736071 "史上最强C语言教程----文件操作(1)") > > [史上最强C语言教程----文件操作(2)](https://blog.csdn.net/m0_57304511/article/details/122745619 "史上最强C语言教程----文件操作(2)") #### 7、程序的编译与预处理 > [史上最强C语言教程----程序的编译与预处理(1)](https://blog.csdn.net/m0_57304511/article/details/123164352 "史上最强C语言教程----程序的编译与预处理(1)") > > [史上最强C语言教程----程序的编译与预处理(2)](https://blog.csdn.net/m0_57304511/article/details/123211919 "史上最强C语言教程----程序的编译与预处理(2)") > #### 8、通讯录管理系统 > > [C语言实现通讯录管理系统](https://blog.csdn.net/m0_57304511/article/details/122607100 "C语言实现通讯录管理系统") > > [C语言实现通讯录管理系统(动态内存分配版)](https://blog.csdn.net/m0_57304511/article/details/122641937 "C语言实现通讯录管理系统(动态内存分配版)") > > [C语言实现通讯录管理系统(文件操作版本)](https://blog.csdn.net/m0_57304511/article/details/122765316 "C语言实现通讯录管理系统(文件操作版本)") ## 1.什么是C语言? > C语言是一门通用计算机编程语言,广泛应用于底层开发。 > > C语言的设计目标是提供一种能以简易 的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。 > > 保持着良好跨平台的特性,以一个标准规格写出的 [C语言程序](https://so.csdn.net/so/search?q=C%E8%AF%AD%E8%A8%80%E7%A8%8B%E5%BA%8F&spm=1001.2101.3001.7020)可在许多电脑平台上进 行编译,甚至包含一些嵌入式处理器(单片机或称MCU)以及超级电脑等作业平台。 > > 二十世纪八十年代,为了避免各开发厂商用的C语言语法产生差异,由美国国家标准局为C语言制 定了一套完整的美国国家标准语 法,称为ANSI C,作为C语言最初的标准。 > > 目前2011年12月8日,国际标准化组织(ISO)和 国际电工委员会(IEC)发布的C11 标准是C语言的第三个官方标准,也是C语言的最新标准,该标准更好的支持了汉字函数名和汉字标识符,一定程度上实现了汉字编程。 > > **C语言是一门面向过程的计算机编程语言**,与C++,Java等面向对象的编程语言有所不同。 其编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC、Turbo C等。 ## 2\. 第一个C语言程序
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>//引用头文件,使我们可以使用C语言库本身就已经提供给我们的函数,即库函数
int main()//main()是主函数
{
printf("hello world\n");//printf()是输出函数,'\n'是换行的意思
return 0;//使程序退出,0的意思是程序正常退出
}

//解释:
//main函数是程序的入口
//一个工程中main函数有且仅有一个
> 程序执行结果: ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/298358adaf8e91c4d9727bd63596f429.png) ## 3\. 数据类型 > char        //字符数据类型 1 > short       //短整型 2 > int         //整形 4 > long        //长整型 4 > long long   //更长的整形 8 > float       //单精度浮点数 4 > double      //双精度浮点数 8 > > //C语言有没有字符串类型? > > 答案:**C语言本身并没有字符串类型,我们在C语言程序中使用的字符串实际上是字符数组,即多个字符构成的就是字符串!** > 下面是字符串的两种定义方式:
1
2
3
4
5
6
7
#include<stdio.h>
int main()
{
char string1[] = "abc";
char string2[] = { 'a','b','c' };
return 0;
}
> 这两种方式的区别并不在此处进行详细的讲解 > > + 为什么出现这么的类型? > > 答案:一方面是能够存储更加多样的数据,便于进行数据处理; > 另一方面的原因就是为了能够更好节约我们的内存空间 > + 每种类型的大小是多少?
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
printf("%d\n", sizeof(char));
printf("%d\n", sizeof(short));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof(long));
printf("%d\n", sizeof(long long));
printf("%d\n", sizeof(float));
printf("%d\n", sizeof(double));
printf("%d\n", sizeof(long double));
return 0;
}
> 下面即为代码的运行结果: > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/88bbf9745b0e479b31dfa3bf949b7abe.png) > 类型的使用:
1
2
3
char ch = 'w';
int weight = 120;
int salary = 20000;
### 1.1 类型的基本归类 **整型类型** > ![](https://i-blog.csdnimg.cn/blog_migrate/2adcbbd08df477e9ed16480ab260fc16.png) > 注意:这个地方为什么char类型也归为整型家族里面呢?因为**char类型的数据本质上也是在内存中存储的其ascii码值**,而其ascii码就是整数,所以char类型自然也就列为整型家族里面了。  **浮点类型** ![](https://i-blog.csdnimg.cn/blog_migrate/4055885ae2748432e27d05fcc388b2ee.png)  **构造类型** ![](https://i-blog.csdnimg.cn/blog_migrate/9aece1d99c6322e541154913732f684b.png)  **指针类型** ![](https://i-blog.csdnimg.cn/blog_migrate/719a910fbaf2a59deac8412c6c48c9db.png) **空类型**  > void 表示空类型(无类型) > > 通常应用于函数的返回类型、函数的参数、指针类型。 ## 3\. 变量、常量 > 生活中的有些值是不变的(比如:圆周率,性别,身份证号码,血型等等) > > 有些值是可变的(比如:年龄,体重,薪资)。 > > 不变的值,C语言中用常量的概念来表示,变的值C语言中用变量来表示。 ### 3.1 定义变量的方法
1
2
3
int age = 150;
float weight = 45.5f;
char ch = 'w';
### 3.2 变量的分类 > + 局部变量 > > + 全局变量 >
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int global = 2019;//全局变量
int main()
{
int local = 2018;//局部变量
//下面定义的global会不会有问题?
int global = 2020;//局部变量
printf("global = %d\n", global);
return 0;
}
> 输出结果展示: > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/f0a3afaf2a944dff5b19997f0a5d9ba8.png) > > 总结: > > 上面的局部变量global变量的定义其实没有什么问题的 > > **当局部变量和全局变量同名的时候,局部变量优先使用**,这就是我们常说的**局部优先原则**! ### 3.3 变量的使用
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
int num1 = 0;
int num2 = 0;
int sum = 0;
printf("输入两个操作数:>");
scanf("%d %d", &num1, &num2);
sum = num1 + num2;
printf("sum = %d\n", sum);
return 0;
}
> printf( )函数:输出函数。在双引号内的将进行输出,而%就是控制变量的输出格式,即起到了格式控制的作用,比如%d就是将变量以整型的形式进行打印输出到屏幕上来,%c则是以字符型的形式进行打印输出!在逗号后面的就是我们想要输出的变量,%格式控制一定要与后面的变量进行意义对应! > scanf( )函数:输入函数。跟上面的一样,%也是进行格式控制,不过此处与前面的区别就是此处是进行输入的格式控制,比如%d,就是将我们的输入的内容以整型的形式存储到我们的变量中,此处与前面也一样,都要与逗号后面的变量进行严格的对应,但此处仍然有一个需要大家进行注意的点,比如我们在输入时,**我们在%d %d两个%d中间有一个空格进行分隔,那么我们在通过键盘进行输入的时候也要在两个变量中间进行分隔**,即我们的输入要与双引号之间的内容进行严格第对应! #### 3.4 变量的作用域和生命周期 > (1)作用域 > > 作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用 的,而限定这个名字的可用性的代码范围就是这个名字的作用域。 > > + **1\. 局部变量的作用域是变量所在的局部范围。** > + **2\. 全局变量的作用域是整个工程。** > > (2)生命周期 > > 变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段 > > + 1\. 局部变量的生命周期是:**进入作用域生命周期开始,出作用域生命周期结束。** > + 2\. 全局变量的生命周期是:**整个程序的生命周期。** ### 3.5 常量 > C语言中的常量和变量的定义的形式有所差异。 > > C语言中的常量分为以下以下几种: > > + 字面常量 > + const 修饰的常变量 > + #define 定义的标识符常量 > + 枚举常量 > > const修饰的常变量:
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
#include <stdio.h>
//举例
enum Sex
{
MALE,
FEMALE,
SECRET
};

//括号中的MALE,FEMALE,SECRET是枚举常量
int main()
{
//字面常量演示
3.14;//字面常量
1000;//字面常量

//const 修饰的常变量
const float pai = 3.14f; //这里的pai是const修饰的常变量
pai = 5.14;//是不能直接修改的!会报警告!!!

//#define的标识符常量 演示

#define MAX 100
printf("max = %d\n", MAX);

//枚举常量演示
printf("%d\n", MALE);
printf("%d\n", FEMALE);
printf("%d\n", SECRET);
//注:枚举常量的默认是从0开始,依次向下递增1的
return 0;
}
> 注: > > 上面例子上的 pai 被称为 const 修饰的常变量, const 修饰的常变量在C语言中只是在语法层面限制了 变量 pai 不能直接被改变,但是 pai 本质上还是一个变量的,所以叫常变量。 ## 4\. 字符串+[转义字符](https://so.csdn.net/so/search?q=%E8%BD%AC%E4%B9%89%E5%AD%97%E7%AC%A6&spm=1001.2101.3001.7020)+注释 ### 4.1 字符串
1
"hello bit.\n"
> 这种由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal),或者简称字符串。 > > 注:字符串的结束标志是一个 \\0 的转义字符。在计算字符串长度的时候 \\0 是结束标志,不算作字符串 内容。
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
//下面代码,打印结果是什么?为什么?(突出'\0'的重要性)
int main()
{
char arr1[] = "bit";
char arr2[] = { 'b', 'i', 't' };
char arr3[] = { 'b', 'i', 't','\0'};
printf("%s\n", arr1);
printf("%s\n", arr2);
printf("%s\n", arr3);
return 0;
}
> ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/a28fb89f217c901f4cc30cc98b8771dd.png) > >  在着我们可以看到,我们在输出arr2时出现了乱码形式,为什么会出现这种情况呢?因为在arr2中并不包含字符串的结束标志即'\\0',所以会出现上面的乱码! > > 注意: > > **1、在我们用printf()函数以字符串形式进行输出时,只有遇到'\\0'才会停止输出!** > > **2、我们在使用上面arr1这种形式进行定义字符串时,在""里面的字符串中就已经自动包含了字符串的结束标志即'\\0'!** > > **3、当我们在使用{}字符串的定义的 方式时我们一定要记得加上字符串的结束标志,虽然我们有时候用不到,但这却是我们必须要做的,这也是一个合格的程序员必备的素养,当然,有的时候确实并不需要加上,在新手期间,建议还是加上。** ### 4.2 转义字符 > 假如我们要在屏幕上打印一个目录: c:\\code\\test.c > > 我们该如何写代码?
1
2
3
4
5
6
#include <stdio.h>
int main()
{
printf("c:\code\test.c\n");
return 0;
}
> 但其实打印结果是这样的: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/08b2d0f6956176e9b0fe301ba522ae36.png) > >  这里就不得不提一下转义字符了。转义字符顾名思义就是转变意思。 > > 下面看一些转义字符。 > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/2798d43d91bf036843a7de1afbf44097.png) > >  下面是一个简单的练习:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
//问题1:在屏幕上打印一个单引号',怎么做?
//问题2:在屏幕上打印一个字符串,字符串的内容是一个双引号“,怎么做?
printf("%c\n", '\'');
printf("%s\n", "\"");
return 0;
}
> 下面是一道笔试题,带大家来了解一下吧!
1
2
3
4
5
6
7
8
9
//程序输出什么?
#include <stdio.h>
int main()
{
printf("%d\n", strlen("abcdef"));
// \62被解析成一个转义字符
printf("%d\n", strlen("c:\test\628\test.c"));
return 0;
}
> 下面是我们在VS上的运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/c5c5c2b56b7ee3ee48acaf6d799620ba.png) > >  为什么会得到上面的结果呢? > > 我们首先来了解一下strlen( ),这个函数的作用是求字符串的长度,当遇到字符串的结束标志时将停止,但是大家需要注意一点,就是字符串的结束标志'\\0'本身并不作为字符串长度的一部分。 > > 我们先看上面这一段代码,strlen( )括号中的字符串内容为abcdef还有字符串的结束标志'\\0',但是由于字符串的结束标志并不作为字符串长度的一部分,所以字符串的长度为6。 > > 接下来我们来看下一段代码,上面我们已经了解了转义字符,此处需要给大家明确一个概念,转义字符被我们看作成是一个字符,即使它的形式是\\后面跟了1个或者多个字符,但strlen()函数只把它看成是一个字符。所以为什么我们的编译器给出上面的结果也就不难理解了,在上面的结果也就不难理解了,\\t是一个转义字符,\\62也是一个转义字符,为什么8不跟着一块呢?因为\\后面跟的是八进制,即只包含0到7的数字,不能包含8,所以8不被包含在内!后面还有已给\\t也是一个转义字符!最终得出14的结果 ## 5\. 注释 > 1. 代码中有不需要的代码可以直接删除,也可以注释掉  > 2. 代码中有些代码比较难懂,可以加一下注释文字 > > 比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
/*C语言风格注释
int Sub(int x, int y)
{
return x-y;
}
*/
int main()
{
//C++注释风格
//int a = 10;
//调用Add函数,完成加法
printf("%d\n", Add(1, 2));
return 0;
}
> 注释有两种风格: > > + C语言风格的注释 /\*xxxxxx\*/                                                                                                      缺陷:不能嵌套注释 > + C++风格的注释 //xxxxxxxx                                                                                                       可以注释一行也可以注释多行 ## 6\. 选择语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main()
{
int coding = 0;
printf("你会去敲代码吗?(选择1 or 0):>");
scanf("%d", &coding);
if (coding == 1)
{
printf("坚持,你会有好offer,女朋友陪在身边\n");
}
else
{
printf("毕业即失业,女朋友跟你分手\n");
}
return 0;
}
>  下面是运行截图: > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/d967a5f48154efc232d2b696086003fe.png) > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/b475973c72746c902cfd96e9f5faa6bc.png) ## 7\. [循环语句] > **分支语句** > if > switch > **循环语句** > while > for > do while > 相信大家都听说过一句话是:C语言是结构化程序设计语言,但是为什么这么说呢?大家想一下我们生活中做一件事,有三种情况,语言表示呢,不是很好表示,先给大家画图表示一下! > ![](https://i-blog.csdnimg.cn/blog_migrate/68a8b1f9cc3ba44387a89c5abe8abfd5.png) ## 1.什么是语句? > C语言语句可分为以下五类: > > (1)表达式语句(例如 y=x+3;假设变量y和x均已定义) > (2)函数调用语句(MAX(x,y);假设函数MAX()已经定义) > (3)控制语句 > (4)复合语句(把多种语句复合在一起形成的语句) > (5)空语句(例如 ;分号本身就可以作为一条语句,称为空语句,至于空语句的作用,后续会讲到 ) > > 无论上述哪一种语句,都必须以分号结束 > C语言有九种控制语句。 > > 可分成以下三类: > > 1\. 条件判断语句(也叫分支语句):**if**语句、**switch**语句; > > 2\. 循环执行语句:**do while**语句、**while**语句、**for**语句; > > 3\. 转向语句:**break**语句、**goto**语句、**continue**语句、**return**语句。 ## 2、分支语句(选择结构) ### 2.1if语句 > 那if语句的语法结构是怎么样的呢? > > ![](https://i-blog.csdnimg.cn/blog_migrate/136d1cbd79702fa8b5404b989d4d79a1.png) > if()括号中的表达式如果为真,就执行后面的语句;如果为假,就不执行后面的语句,去执行else后面的语句,那么什么是真?什么是假呢? > > C语言规定,0为假,非0为真。 > 首先是单分支的情况: > > ![](https://i-blog.csdnimg.cn/blog_migrate/827eb3543c3f469d16fef7b9e24886c0.png) > > 然后是双分支的情况: > > ![](https://i-blog.csdnimg.cn/blog_migrate/9333aad623b94feff845c14e94d5c321.png) > >  接下来是多分枝的情况:![](https://i-blog.csdnimg.cn/blog_migrate/83b940d8f0f866356691b38ae9d9d1fa.png) > 哎,很多同学们看到这就想问了,多分支情况下后面的条件判断是不是不写前面的age>=18也是可以的呢?很明显当然是可以的,因为前面不满足age<18的时候此时age是一定大于18的了,那么此时按照道理来说,我们**不写age>=18也是没有问题的**,但这个地方,**推荐同学们还是要加上的**,为什么呢?我们将来写代码,不仅仅是给自己看的,也是要给别人看的,加上之后,条理就很清晰明了,方便别人查看的同时,也有利于我们后期的维护与调试。 > 看到这,相信同学们也会有这样的疑问,就是我们在写条件表达式(age>=18&&age<60)的时候可不可以这样写呢?(18<=age<60)相信大家会有这样的疑问,其实有这样的疑问也是正常的,因为我们在数学中就是这样写的,但这样写到底行不行呢?我们直接代码走起! > ![](https://i-blog.csdnimg.cn/blog_migrate/64fae2d16456f80dc25888f37480321a.png) > >  ![](https://i-blog.csdnimg.cn/blog_migrate/85ef0111e6abedb4a6ca5b5487768ec1.png) > 在同学们看到第一张图的时候,哎,感觉没问题啊,但看到第二张图的时候,出问题了!为什么会出现这种问题呢?接下来呢,我给大家分析一下! 我们知道,>    >=    <    <=  == ,这些运算符都是关系运算符,如果结果为真,就返回1,如果结果为假,就返回0,并且结合方向是自左向右, > > 在第二个例子中,我们输入了100,首先对第一个if后面的表达式进行判断,很明显,100>18,不满足条件,接下来进入了第一个else if语句进行判断,18<=100,是真的,返回1,然后1<60,是真的,最终,返回1结果是成立的,所以会输出“青年”,相信大家看到这就明白了,在计算机中是不可以这样写的,虽然语法上没有问题,但逻辑上却无法正确表达我们的意思,无法满足我们的要求,所以**不要这样使用** > 看到这很多同学们又有问题了,那我们在if()的后面直接跟一个变量是不是也是可以的呢?是的,完全没有问题!因为上面已经说了,c语言规定,0为假,非0为真,所以在**if()后面直接跟变量是完全没有问题的**!如果变量值为非0,那么判断结果为真,就执行后面的语句,如果变量值为0,那么判断结果为假,就不执行后面的语句。 > 同时在这给大家顺便提一下,**赋值表达式的返回值是赋的值本身**,即if(i = n)中(n为常数),如果n为0,那么返回值为0,如果n为非0,那么返回值为n,即返回值为非0,为真,会执行后面的语句,同时告诉大家,**printf和scanf函数也是有返回值的**,小伙伴们课下可以自己去查一下哦! > 如果条件成立,要执行多条语句,则应该使用代码块!事实上,我前面就全部都使用代码块了,那么什么是代码块呢?C语言中,被{}括起来的叫做代码块,接下来给大家代码展示一下,如果不用{}会怎样! > > ![](https://i-blog.csdnimg.cn/blog_migrate/e5b68457b251450d9683598f17b403b1.png) > 大家都看到了,如果我们想在if后面跟多条语句的话,我们不用{}就会出错,这是由C语言语法本身所决定的,因为这样的话,后面的else就没有匹配的if了,默认情况下,if后面只能跟一条语句,而**计算机把{}内的语句当作一条语句**,所以我们**如果if后面想跟多条语句的话,就要用{}括起来形成一个语句块**。 > > 简单给大家代码展示一下: > ![](https://i-blog.csdnimg.cn/blog_migrate/26cf39cee5cd510ac45820369cf1b672.png) > >  我给大家的建议呢。就是**无论后面跟的是几条语句,我们都加上{}**,为什么这么说呢,第一个原因就是避免我们在if后面跟多条语句时可能会忘了加{};第二个原因是就代码块更加简洁有条理;第三个原因就就是以后方便我们以后再向里面添加代码。 #### 2.1.1 悬空else > 下面给大家一段代码,希望大家能够给出输出的结果! > > ![](https://i-blog.csdnimg.cn/blog_migrate/b1342d7639273f4aa7808a9045f1ba39.png) > 相信大家有人会给出"hehe" 的输出结果,也会有人给出"haha"的输出结果,当然也有人会给出一些其它的结果,大家都各有各的想法,这并不会让人意外,那我告诉大家,输出结果是空白,大家会感到意外吗?大家可能会觉得我说的是假的,那我就给大家代码展示一下! > > ![](https://i-blog.csdnimg.cn/blog_migrate/ae3e87d7390d859ab1d27f660bb22956.png) > > 没错,输出结果确实是空白,这个结果确实让人感到意外,这个题中我们需要学习的知识点是,**else总是跟最近的If进行匹配!**  > 该如何理解这句话呢?我们就以下面这段代码为例给大家解释一下吧! > > 首先只看这张图的话,很容易让人误解后面的这个else会跟if(a==1)进行匹配,但是我们应该记住else符合就近原则,即总跟最近的未配对的if进行匹配,从这个else向前找前面的if语句,首先找到的一个未配对的if语句是if(b==2),相信大家清楚了这个这个就不难理解了,下面给大家展示一下代码的本来面目! > > ![](https://i-blog.csdnimg.cn/blog_migrate/d506cc064ee7ff2e3f416e604ed2eb40.png) > > 看到这大家应该都能理解了吧,首先对if(a==1)进行判断,不成立,然后后面的if(b==2)与else语句均不执行了,我们可以这么理解:if和else组成的是一个语句 。 > 实际上,这个代码我们还能够去改进一下,使这个代码变得更加的清晰,更加的方便我们的理解: > > ![](https://i-blog.csdnimg.cn/blog_migrate/7510e9a00b50052c4e3ab86c739fce7f.png) > > 这样加上一个代码块是不是更有助于大家的理解了呢? 所以**代码规范很重要!** > #### 2.1.2if书写形式的对比 > 首先大家看代码1和代码2,它们表达的意思是完全一样的,就是如果condition条件成立,就执行return x;语句返回x,反之不成立就执行return y;返回y,但是代码块2表达意思就比代码块1更容易理解,逻辑也更加的清晰,希望大家在写代码的时候能像代码2一样这样写
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
//代码1

if (condition) {
return x;
}
return y;

//代码2

if(condition)
{
return x;
}
else
{
return y;
}

//代码3

int num = 5;
if(num == 5)
{
printf("hehe\n");
}

//代码4

int num = 5;
if(5 == num)
{
printf("hehe\n");
}
> 接下来大家看代码3和代码4,它们表达的意思是完全一样的 > 给大家分享的操作符结合性顺序表: > ![](https://i-blog.csdnimg.cn/blog_migrate/400f84697e65c1836094ce8ef3a1d391.png) > > ![](https://i-blog.csdnimg.cn/blog_migrate/d6288a89a99e01234c4a331d7d2d85d2.png) ![](https://i-blog.csdnimg.cn/blog_migrate/56ac5ff6555a2e05c24fc661b5159ca8.png) #### 2.1.3 练习 > 判断一个数是否为奇数(代码+运行) > > ![](https://i-blog.csdnimg.cn/blog_migrate/4ef8f3fb7846f7479839c5d8b019c4f1.png) > ### 2.2 switch语句 switch语句也是一种分支语句。 常常用于多分支的情况。 很多小伙伴就问了,我们为什么要使用switch语句呢?用if语句进行多分支判断不就行了嘛? > 给出下面的情况,让你写出相应的代码! > > 输入1,输出星期一 > 输入2,输出星期二 > 输入3,输出星期三 > 输入4,输出星期四 > 输入5,输出星期五 > 输入6,输出星期六 > 输入7,输出星期日 > > 如果用if ...else if 进行多分支的话形式太复杂,代码太过于冗长,那我们就得有不一样的语法形式。 这就是switch 语句。这也是我们需要switch语句的原因 而**语句项**是什么呢? #### 2.2.1 在switch语句中的break > 在switch语句中,我们没办法直接实现分支,搭配break使用才能实现真正的分支。 > 比如: >
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
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
printf("星期一!");
break;
case 2:
printf("星期二!");
break;
case 3:
printf("星期三!");
break;
case 4:
printf("星期四!");
break;
case 5:
printf("星期五!");
break;
case 6:
printf("星期六!");
break;
case 7:
printf("星期天!");
break;
}
return 0;
}
> 执行截图  > > ![](https://i-blog.csdnimg.cn/blog_migrate/5339b0e9e2792ca02cba5f926c421e50.png) > 这个是正确的代码,假如我们把上面的break全部去掉,看一下程序执行截图 > > ![](https://i-blog.csdnimg.cn/blog_migrate/76b2e89d2ada08fbd03eb97f38842ab8.png) >  **switch语句后面的( )内可以跟整型变量、整型常量、整形表达式(当然这些也可以是字符型,不过字符型的本质仍然是整型),而case后面只能跟整型常量,或者字符型常量,必须是常量,const修饰的常变量是不可以的,因为其本质上仍是变量**,switch语句的执行结果是一个整型数值,然后通过case进行匹配,如果匹配一致,就执行case后面的语句,在这个时候我们需要注意,只要没有遇到break,程序就会一直执行下去,此时无论后面的case是否匹配都将执行,直到跳出语句块,这些由上面的运行截图就可以看出,如果没有break语句是无法实现其分支功能的,这个地方希望大家牢记,即switch本身并不具有分支功能,是通过case和break共同协调实现的,其中,case实现的是匹配功能,或者说是判定功能,而break实现的是分支功能,当然,也可以说break实现的是退出功能。 > 有时候我们的需求变了: > > 1\. 输入1-5,输出的是“weekday”; > > 2\. 输入6-7,输出“weekend”; > 所以我们的代码就应该这样实现了:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
int main()
{
int day = 0;
scanf("%d", &day);
switch (day)
{
case 1:
case 2:
case 3:
case 4:
case 5:
printf("weekday");
break;
case 6:
case 7:
printf("weekend");
break;
}
return 0;
}
> break语句 的实际效果是把语句列表划分为不同的分支部分! > > 注意:case语句的后面不一定非要加break!需求不同,代码不同! > 编程好习惯: > > 在最后一个 case 语句的后面加上一条 break语句。(之所以这么写是可以避免出现在以前的最后一个 case 语句后面忘了添加 break语句)。 #### 2.2.2 defalt子句 > 如果表达的值与所有的case标签的值都不匹配怎么办呢? 其实也没什么,结构就是所有的语句都被跳过而已。 程序并不会终止,也不会报错,因为这种情况在C中并不认为是个错误。 但是,如果你并不想忽略不匹配所有标签的表达式的值时该怎么办呢? 你可以在语句列表中增加一条default子句,把下面的标签 default: 写在任何一个 case 标签可以出现的位置。 当 switch 表达式的值并不匹配所有 case 标签的值时,这个 default 子句后面的语句就会执行。 所以,每个switch语句中只能出现一条default子句。 但是它可以出现在语句列表的任何位置,而且语句流会像执行一个case标签一样执行default子句。 > 编程好习惯: > > **在每个 switch 语句中都放一条default子句是个好习惯,甚至可以在后边再加一个 break 。** > > **default语句可前可后,但我们一般习惯把它放在最后,同时case最好按照从小到大来排,方便后续代码的查看和修改,同时最好把执行频率高、容易被匹配的放在前面,在一定程度上能够提高代码的执行效率!** > 接下来给大家展示一个题,大家先看一下代码,思考一下最后输出的m和n应该是多少。
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 n = 1;
int m = 2;
switch (n)
{
case 1:
m++;
case 2:
n++;
case 3:
switch (n)
{//switch允许嵌套使用
case 1:
n++;
case 2:
m++;
n++;
break;
}
case 4:
m++;
break;
default:
break;
}
printf("m = %d, n = %d\n", m, n);
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/988c6236382d1484e7bdd7afd048f2df.png) > >  可以看到,最后的输出结果是 m = 5 ; n = 3 ;为什么会得出这样的结果呢,接下来我给大家分析一下! > > switch(n),因为n = 1 ,所以执行case 1 后面的语句,m++之后m的值变成了3,因为后面没有break语句,所以继续向后执行,接下来执行case 2后面的语句,n++之后n的值变为了2,因为后面没有break,所以继续向下执行,执行case 3后面的语句,switch(n),此时n的值为2,所以执行case 2后面的语句,m++之后m的值变成了4,n++之后n的值变成了3,后面执行break,注意此时break只能退出一层,即退出之前的case(n),这个地方希望大家能够记住,**无论break用在switch还是接下来讲的循环中,均只能退出一层switch或者一层循环,**退出switch(n)之后,继续执行case 4后面的语句,m++后m的值变成了5,此时出现了break,退出上面的switch(day),然后此时m的值为5,n的值为3,输出后即可结束程序! ## 3 循环语句 > 1. while > 2. for > 3. do while ### **3.1 while循环** 我们已经掌握了if语句: > if(条件) > >           语句; > 当条件满足的情况下,if语句后的语句执行,否则不执行。 但是这个语句只会执行一次。 由于我们发现生活中很多的实际的例子是:同一件事情我们需要完成很多次。 那我们怎么做呢? C语言中给我们引入了: while 语句,可以实现循环。 > //while 语法结构 > > while(表达式) > >        循环语句; > ![](https://i-blog.csdnimg.cn/blog_migrate/fe51a283db13a6633c07db605bc50556.png) > > 给大家举个例子吧!在屏幕上打印1~100的数字:
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main()
{
int i = 1;
while (i < 101)
{
printf("%d ", i);
i ++ ;
}
return 0;
}
> 上面的代码已经帮我了解了 while 语句的基本语法,那我们再了解一下: #### 3.1.1 while语句中的break和continue > break介绍 > > 直接上代码帮助大家感悟break的作用! > > ![](https://i-blog.csdnimg.cn/blog_migrate/6b6ba43a8593b1577ea0ecb777b61762.png) > >  总结: > > 其实在循环中只要遇到break,就停止后期的所有的循环,直接终止循环。 > > 所以:while中的break是用于永久终止循环的。相信大家在演示的代码并结合上面的运行图也能感受出来break在while()循环中的作用,即当i=5的时候,在判断if(5 == i)时,判断结果为真,执行break,退出这一层循环,然后执行return 0语句退出程序。 > continue介绍 > > > ![](https://i-blog.csdnimg.cn/blog_migrate/65c0ebc3522fe91e1ecd330ea062865e.png) > > 总结: > > continue在while循环中的作用就是: > > continue是用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行, 而是直接跳转到while语句的判断部分。进行下一次循环的入口判断。在这个程序中,当i=5时进行判断5 == i后,就执行continue语句,结束本次循环,重新进入到判定部分,即while(i<11),然后又重复的进行判断if(5 == i),再接着重复上面的的流程,所以进入了死循环。
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
//代码什么意思?
//代码1
#include <stdio.h>
int main()
{
int ch = 0;
while ((ch = getchar()) != EOF)
putchar(ch);
return 0;
}
//这里的代码适当的修改是可以用来清理缓冲区的.
//代码2
#include <stdio.h>
int main()
{
char ch = '\0';
while ((ch = getchar()) != EOF)
{
if (ch < ‘0’ || ch > ‘9’)
continue;
putchar(ch);
}
return 0;
}
//这个代码的作用是:只打印数字字符,跳过其他字符的、
> 首先带大家了解一下第一段代码,在了解之前,先带大家认识一下getchar()函数吧! > > getchar的作用: > > 从一个流里读取一个字符,或者从标准输入(键盘)里面获取一个字符。 > > getchar的返回类型: > > getchar的返回类型为int,返回的值就是我们从流或者键盘获取的那个字符。在这个地方我们就应该思考一下,为什么我们用int 来接收getchar返回的值呢?当我们讲一个字符用getchar输入的时候,我们就要将其存储到相应的变量当中(当然,不存储也是完全没有任何问题的),我们在讲我们的字符存入到变量当中时,**本质上是将字符的ascii码值存入到变量所开辟的内存空间中**,例如字符'a'的ascii码值就是97,当我们将其存储时,就是将其ascii码值转换为二进制,存放到我们变量所开辟的空间中,这是其中的一个原因;另一个原因就是如果getchar 在遇到读取错误或者EOF(end of file文件结束标志)时,getchar()会返回一个EOF,那么EOF究竟是什么呢?它有没有具体的值呢?让我们去它的定义看一下吧! > > ![](https://i-blog.csdnimg.cn/blog_migrate/1a6a803db44b9b6d445dffefc51133b5.png) > 相信大家看到了EOF的定义了吧,那么他的**返回值-1**跟计算机定义它的返回类型为整型有什么关系呢?**\-1是整型数据,在内存中占据四个字节,**如果是其它的类型比如char只有一个字节怎么能够放得下呢?综合上面这两点考虑,C语言程序设计者最终采用了int作为getchar()的返回类型。 > getchar的用法: > > ![](https://i-blog.csdnimg.cn/blog_migrate/cd6b3bed6a2dfb03c0bd99f21fd77e10.png) > > 在这个地方大家需要注意,当我们输入字符s之后,我们要敲回车键输入才会停止,然后才会执行printf()语句进行输出,将字符s真正输出出来,在上述代码框中,大家可以看到有两个字符s,这里呢,需要给大家解释一下,前面的字符s是我们输入的字符,后面的一个字符是执行printf()语句后输出的语句,这个点希望大家能够清楚。 > 这个时候有的同学就问了,难道输出字符只能用prinf进行输出吗?难道没有一个函数与getchar进行对应吗?答案是有的,那个函数就是putchar,在这呢,简单给大家简单介绍一下putchar()。 > > putchar的作用: > > 写一个字符到流里面或者到标准输出(显示器)中。 > > putchar的用法: > > ![](https://i-blog.csdnimg.cn/blog_migrate/3da6650f09bd34cb2b157d3335cef756.png) > 在这个地方需要大家注意,getchar和putchar不仅仅是只能输出一个字符,我们通常会将字符给狭义化的认为字符只是a~z和A~Z,但事实上,getchar和putchar不仅仅是只能输出这些,还能输出数和一些其它符号,比如#、@等等。给大家代码展示一下吧! > > ![](https://i-blog.csdnimg.cn/blog_migrate/89425d320ddc6a5434c9c09e7dd505f5.png) > > ![](https://i-blog.csdnimg.cn/blog_migrate/145ee78e3301201c7794c71cf65ad2cf.png) 这个地方呢,无论我们输入什么,getchar只会将其当作一个字符来处理,然后将其转换成相应的ascii码值,然后转成二进制数据,存入到变量所开辟的空间中!即使我们输入多个字符,getchar也只会将接收到的首个字符将其存入到变量开辟的内存空间中。下面进行代码展示! > ![](https://i-blog.csdnimg.cn/blog_migrate/fbb1a1c58e65b6b9e4895da610e9d4ff.png) > >  从这个地方可以看出,我们输入了1234四个字符。这个地方希望大家一定要注意,我们输入的是4个字符,而不是1234一个数字,这个地方希望大家一定要明白,getchar读取和putchar输出都是以字符为单位进行的,接下来大家请看下一段代码! > ![](https://i-blog.csdnimg.cn/blog_migrate/ad7ad20f5d4d49185ee7db62b7b60af6.png) > >  看到这段代码,我想先告诉大家**这种情况下我们该如何停止**!相信这也是大家最疑惑的问题,一般大家只直到强制结束程序进而退出,但其实这种情况下是可以退出的,我们只需要按键盘上的**ctrl+z键然后敲击回车键**即可,这样程序即可正常退出!我们输入的这个也即是文件结束标志EOF。 > 很多同学就问了,写这段代码有什么意义呢?来。大家跟着我看这样一个场景!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>
int main()
{
char input[20] = { 0 };
printf("请输入密码:");
scanf("%s", input);
printf("请确认密码:Y/N");
int ch = getchar();
if ('Y'==ch)
{
printf("确认成功!");
}
else
{
printf("确认失败!");
}
return 0;
}
> 相信大家看到这会有一个疑问,scanf后面的input为什么没有取地址呢?这个地方需要给大家强调一下,**数组名本身就是地址,所以scanf后面就不需要取地址了!** > 那么这段代码究竟能否完成我们想要表达的意思呢?我们代码运行一下就知道了! > > ![](https://i-blog.csdnimg.cn/blog_migrate/eef70d4ec86e867d7da4e2dd288dc032.png) > > 哎,很明显,这段代码并不能完成我们想要它达成的目标,为什么呢?接下来会给大家讲解! > 现在给大家引入一个**输入缓冲区**的概念,getchar读取字符的时候,它并不是直接就从键盘上进行读取的,它首先要看一看输入缓冲区中是否有数据,如果没有,才会从键盘上进行读取数据, 当我们在上面这个程序中输入密码结束后,我们会在键盘上敲击一个回车进行换行,这也标志着我们的输入结束,这个回车本身并没有被放入到我们输入的字符串中去,而是被放入到了输入缓冲区中去,当getchar进行读取字符时,首先看输入缓冲区,发现输入缓冲区并不为空,然后就将输入缓冲区中的回车字符读取了,存入到了变量ch中,进而判定后不等于字符Y,所以会输出“确认失败!”,下面给大家简单画图展示一下! > > ![](https://i-blog.csdnimg.cn/blog_migrate/1d49f3a87c73ee253a850c15014f66fd.png) >  当然scanf也有类似的特性,希望大家了解这个,其实大家如果了解了这个原理,相应的对于上面的解决方法也就显而易见了,下面给大家代码展示!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int main()
{
char input[20] = { 0 };
printf("请输入密码:");
scanf("%s", input);
printf("请确认密码:Y/N");
getchar();
int ch = getchar();
if ('Y'==ch)
{
printf("确认成功!");
}
else
{
printf("确认失败!");
}
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/40dde09a9f158fa5d5900d53c738e8d6.png) > 代码执行过后,果然没问题了,解决的方法很简单,就是**在getchar()确认密码前用一个getchar()将输入缓冲区的回车键吸收即可!** > > 但这个地方呢,还是会有问题,接下来我们输入一段代码,测试一下刚才我们修改后的程序! > > ![](https://i-blog.csdnimg.cn/blog_migrate/38fdb738375d2bf0450aa14a0bdcf814.png) >  这个地方程序又又又出错了,这次究竟是什么原因呢?首先是scanf函数,先查看输入缓冲区中有没有数据,没有,然后从键盘上进行输入,在这个地方大家需要注意的是,**scanf函数在接受输入时遇到空格就会停止**,然后后面的jshfkj\\n均留在了输入缓冲区,所以getchar在接收数据的时候,将第一个j存入到变量ch中,判定并不等于'Y',所以会输出确认失败。那么,我们该如何解决呢? > > 大家可以自己想一下,我们在进行输入的时候,最后输入的是空格符,即'\\n',那么我们在吸收的时候可以加一个判定啊,就是通过while()对缓冲区的字符进行逐个判定,只要不是'\\n',那么我们就通过getchar将缓冲区的数据全部吸收,下面大家来看解决方式! >
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
include<stdio.h>
int main()
{
char input[20] = { 0 };
printf("请输入密码:");
scanf("%s", input);//scanf在将数据存入变量时,遇到空格就停止,但输入并未停止,输入的其它数据会\
存入到输入缓冲区中去,就是说,如果输入了abcde hehe,只有前面的数据abcde被scanf吸收了,输入缓冲区中\
仍然有hehe\n
printf("请确认密码:Y/N:");
int tmp = 0;
while ((tmp = getchar()) != '\n')//这个地方是针对输入密码时输入abcde hehe\n\
最后的\n也会被拿走
{
;//空语句,什么也不干
}
int ch = getchar("%c");
if ( 'Y'==ch)
{
printf("确认成功!");
}
else
{
printf("确认失败!");
}
return 0;
}
> 看到这个地方。相信大家对第一段代码应该是有自己的深刻的理解了吧,这看上去知识一段简单的代码,但当我们深入的进行了解之后,发现并没有我们想象的那么简单,所以希望大家今后能够多加思考! > > 接下来大家来看第二段代码!第二段代码并不复杂,就是首先定义的一个字符变量ch,并对其进行初始化为'\\0',然后一个while()语句对每个输入的字符进行判断,如果是0到9的数字,就输出。如果不是,就运行continue语句重新进行输入,如果想要结束输入就在键盘上输入ctrl+z即可,总结来说,这段代码就是在键盘上接收并在显示器上输出0~9的数字。 ## 3.2 for循环 > 我们已经知道了while循环,但是我们为什么还要一个for循环呢? > > 首先来看看for循环的语法: ### 3.2.1 语法
1
2
for(表达式1; 表达式2; 表达式3)
循环语句;
> **表达式1** > > [表达式](https://so.csdn.net/so/search?q=%E8%A1%A8%E8%BE%BE%E5%BC%8F&spm=1001.2101.3001.7020)1为初始化部分,用于初始化循环变量的。我们称之为**条件初始化**。 > > **表达式2** > > 表达式2为条件判断部分,用于判断循环时候终止。我们称之为**条件判断**。 > > **表达式3** > > 表达式3为调整部分,用于循环条件的调整。我们称之为**条件更新**。 > **循环语句** > > 每一次进入循环条件判断通过后需要执行的语句,如果想要执行多条语句需要用{}将我们需要要执行的多条语句括起来形成一个语句块。 > > 从上面可以看出:for语句与while语句与do while语句不同的地方在于它的条件初始化、条件判断、条件更新更为紧凑,都在同一个()内,方便我们进行后续的修改,而do while 与while语句这三个部分相隔就比较远,不便于进行修改,也因此我们使用for循环的频率更高。 > > 下面给大家举个例子吧,帮助大家了解一下for[循环语句](https://so.csdn.net/so/search?q=%E5%BE%AA%E7%8E%AF%E8%AF%AD%E5%8F%A5&spm=1001.2101.3001.7020)的使用! > > **使用for循环在屏幕上打印1~10的数字。**
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
int main()
{
int i = 0;
//for(i=1/*初始化*/; i<=10/*判断部分*/; i++/*调整部分*/)
for (i = 1; i <= 10; i++)
{
printf("%d ", i);
}
return 0;
}
### 3.2.2 break与continue在for循环中 > ![](https://i-blog.csdnimg.cn/blog_migrate/a4556e00bd3badeac0d1f288cad9c86a.png) > > 通过对比while和do while,它们在使用continue和break时,唯一的区别点在于**for在执行continue后进入的是条件更新语句,而while与do while则进入条件判断语句**,然后进入下一次循环,而使用break时两者都一样,都是跳出循环。  ### 3.2.3 for语句的循环[控制变量](https://so.csdn.net/so/search?q=%E6%8E%A7%E5%88%B6%E5%8F%98%E9%87%8F&spm=1001.2101.3001.7020) > **建议:** > > **1\. 不可在for 循环体内修改循环变量,防止 for 循环失去控制。** > > 2\. 建议for语句的循环控制变量的取值采用“**前闭后开区间**”写法。原因就是左闭右开可以清楚的看出循环次数。(当然,这并不是强制的,有时也应该根据使用场景进行灵活变通) > > 下面给出一个例子,帮助大家理解第二条建议:
1
2
3
4
5
6
7
int i = 0;
//前闭后开的写法
for(i=0; i<10; i++)
{}
//两边都是闭区间
for(i=0; i<=9; i++)
{}
> 观察上面两个例子,前者采用的是左闭右开,后者采用的是完全闭区间,而前者很容易就能看出该循环的循环次数为10次,即10减去0,而后者则需要9-0+1才能得到真正的循环次数,虽然左闭右开不是强制规定的,但却是众多程序员所极力推荐的,因为这样写的话将会有更好的阅读体验,也有助于后续的调试。 ### 3.2.4 一些for循环的变种 #### (1)死循环
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
//代码1
for (;;)
{
printf("hehe\n");
}
//for循环中的初始化部分,判断部分,调整部分是可以省略的,但是不建议初学时省略,容易导致问题。
> 条件初始化,条件判断,条件更新都是可以根据具体需要进行省略的,但我们往往并不推荐进行省略,因为加上之后程序将有更加良好的阅读体验,而且完全省略后将其写在其它地方的话,相当于我们也放弃了for循环的真正作用,即抛弃了我们使用for循环的初衷,所以并不推荐大家去省略。 > > **注意:当条件初始化、条件判断、条件更新都省略后,程序将进入死循环。** #### (2)两段有趣的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//代码1
#include<stdio.h>
int main()
{
int i = 0;
int j = 0;
int count = 0;
//这里打印多少个hehe?
for (i = 0; i < 10; i++)
{
for (j = 0; j < 10; j++)
{
printf("hehe\n");
count++;
}
}
printf("%d\n", count);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//代码2
#include<stdio.h>
int main()
{
int i = 0;
int j = 0;
int count = 0;
//如果省略掉初始化部分,这里打印多少个hehe?
for (; i < 10; i++)
{
for (; j < 10; j++)
{
printf("hehe\n");
count++;
}
}
printf("%d\n", count);
}
> 在这两段代码中,用count进行保存hehe输出的次数,执行程序,程序的执行结果分别是100、10。为什么会发生这种结果呢?下面我来给大家简单分析一下! > > 先看这两段代码,唯一的区别就是后者在二层循环中for的括号中省略了条件初始化语句,这会造成什么影响呢?我们首先看第一段代码:首先赋初值i=0;然后进行条件判断,i<10,进入到了第二层for循环,第二层for循环首先对j进行赋值操作,然后进行条件判断j<10,输出hehe,count变为1,然后进行条件更新,接着又重新进入了条件判断,依次循环,内层循环结束后将跳出。 > > 此时已经输出了10个hehe,然后进入了外层循环的条件更新,i++,i变为1,i<10,进入了第二次的内层循环。 > > 由于此时存在条件初始化语句j又被赋值为0,重复上面的过程,又再次输出了10个hehe,继续重复上面的循环,最终将打印10\*10=100个hehe。 > > 看代码2,代码2中由于不存在条件更新语句,所以在内层循环执行完一次之后,当i赋值为1后进入第二次内层循环时,此时j的值未曾改变,依旧是10,所以内层循环在条件判断时直接跳出,所以最终只能打印10个hehe。 > > 总的来说,就是代码2相比代码1少了条件初始化语句,所以在循环过后j的值没有改变,依旧是上一次循环结束后的值,即10,所以在内层循环条件判断时直接不满足跳出。 > #### (3) 多个变量控制循环
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>
int main()
{
//代码4-使用多余一个变量控制循环
int x, y;
for (x = 0, y = 0; x < 2 && y < 5; ++x, y++)
{
printf("hehe\n");
}
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/7cd04ff7880551d71347977b7e23d67c.png) > > 条件运行结果大家也看到了,最终只打印了两个hehe,为什么会这样呢?因为当x=2时就不满足条件判断语句(判断满足的条件是x<2并且x<5)了,就跳出了循环,此时只进行了2次循环,打印出了两个hehe。 > 这个地方呢,给大家拓展一下,如果我们把&&改为||会怎么样呢?答案是会执行5次循环,因为如果当x =5时就不满足条件判断语句(判断满足的条件是x<2或者x<5满足一个条件即可)了,就跳出了循环,此时只进行了5次循环,打印5个hehe。 #### 注意: > > 很多同学喜欢这样写for循环语句: > 这样写呢,就是进行条件初始化的时候进行条件定义,这样写不是不行,但是此时要注意i的作用域,即只在这个for循环语句内有效,当然,我们并**不推荐**这种写法,我们推荐的是下面这种写法:
1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main()
{
for (int i = 0; i < 10; i++)
{
pritnf("hehe\n");
}
return 0;
}
> > 为什么会推荐这种写法呢?因为我们后面可能会对i进行判断,判断这个循环是否完全完成,或者判断是否因为某些异常原因而终止了,这种代码相对于上面第一种代码更为实用,应用范围更广,所以推荐大家使用第二种!
1
2
3
4
5
6
7
8
9
10
#include<stdio.h>
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
pritnf("hehe\n");
}
return 0;
}
### 3.3 do...while()循环 #### 3.3.1 do语句的语法: > do > >           循环语句; > > while(表达式); #### 3.3.2 执行流程 > ![](https://i-blog.csdnimg.cn/blog_migrate/f07b8122e10a7fbdac98b5c95cb71eac.png) #### 3.3.3 do语句的特点 > 循环至少执行一次,使用的场景有限,所以不是经常使用。一般常常用在某些项目或者游戏的开始界面,因为无论用户做出怎样的选择,菜单或者游戏的开始页面至少会显示一次。 下面代码给大家展示一下:
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int i = 10;
do
{
printf("%d\n", i);
} while (i < 10);
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/4cdafbd9a568f021773462e25e31776a.png) #### 3.3.4 do while循环中的break和continue  > **break:同while与for循环,执行break后直接跳出循环。下面给大家代码展示一下:**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main()
{
int i = 0;

do
{
if (5 == i)
break;
printf("%d\n", i);
i++;
} while (i < 10);

return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/d208eed0919b3ee6bf585d81f7e72c0d.png) >  在i = 0 到4 的时候程序正常执行,当i = 5的时候,执行break语句循环退出结束,因此在屏幕上值打印了0 1 2 3 4。 >  **continue:当do while循环体语句中遇到continue之后,就会跳转到条件判断语句。**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
int i = 0;
do
{
if (5 == i)
continue;
printf("%d\n", i);
i++;
} while (i < 10);

return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/5494aee32252a153a95ac353c82b91e7.png) >  程序在i=5的时候if条件判定成立,执行continue语句,跳转到条件判定语句while(i<10)成立,陷入了死循环,此时i的值始终为5。 ### 3.4 练习 #### 3.4.1. 计算 n的阶乘。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
int main()
{
int n = 0;
int i = 1;
int ret = 1;//ret用来存放阶乘的结果
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
ret *= i;
}
printf("%d", ret);
return 0;
}
> 通过for循环即可实现求阶乘。 #### 3.4.2. 计算 1!+2!+3!+……+10!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int main()
{
int n = 10;
int i = 1;
int ret = 1;//ret用来存放阶乘的结果
int sum = 0;
for (i = 1; i <= n; i++)
{
ret *= i;
sum += ret;

}
printf("%d", sum);
return 0;
}
> 求阶乘的和时,此处用了一个比较巧妙的方法,即n的阶乘等于(n-1)的阶乘再乘n即可得出,当然,也可以用其它的方法,此处需要注意的是,如果用两个for循环求的话不要忘记ret的初始化! #### 3.4.3. 在一个[有序数组](https://so.csdn.net/so/search?q=%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84&spm=1001.2101.3001.7020)中查找具体的某个数字n。
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
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int left = 0;
int right = sizeof(arr) / sizeof(arr[0]) - 1;
int key = 7;
int mid = 0;
while (left <= right)
{
mid = (left + right) / 2;
if (arr[mid] > key)
{
right = mid - 1;
}
else if (arr[mid] < key)
{
left = mid + 1;
}
else
break;
}
if (left <= right)
printf("找到了,下标是%d\n", mid);
else
printf("找不到\n");
}
> 二分查找法主要是弄明白原理,只要原理弄清楚了就能很容易的写出代码,其原理为:定义首元素下标记为left,尾元素下标记为right,中间元素下标记为mid,拿我们向要查找的元素即mid下标对应的元素进行比较,如果比中间元素小,就将mid元素的前一个元素的下标记为right,反之,就将mid的后一个元素的下标记为left,重复这个过程即可,即构成一个循环,另外,需要记住这个循环终止的条件,当left>right时就表明中间已经没有中间元素供我们进行查找,在退出循环之后,如果left仍然小于right,就说明已经找到了我们想要查找的元素,即mid下标所对应的元素,mid即为其对应的下标。 #### 3.4.4. 编写代码,演示多个字符从两端移动,向中间汇聚。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include<string.h>
#include<windows.h>
int main()
{
char arr1[] = "welcome to bit";
char arr2[] = "##############";
int left = 0;
int right = strlen(arr1) - 1;
printf("%s\n", arr2);
//while循环实现
while (left <= right)
{
Sleep(1000);
arr2[left] = arr1[left];
arr2[right] = arr1[right];
left++;
right--;
printf("%s\n", arr2);
}
return 0;
}
> 这段代码并不复杂,就是将我们给出的字符串的左右元素逐个赋值到新建的字符串中即可,同样的,需要注意的是循环终止的条件,因为每次赋值完后,left下标+1,right下标-1,当left下标大于right下标即停止,此时中间已经没有元素了,就无法从两端向中间移动了。 #### 3.4.5. 编写代码实现,模拟用户登录情景,并且只能登录三次。(只允许输入三次密码,如果密码正确则 提示登录成,如果三次均输入错误,则退出程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<string.h>
int main()
{
char psw[10] = "";
int i = 0;
int j = 0;
for (i = 0; i < 3; ++i)
{
printf("please input:");
scanf("%s", psw);
if (strcmp(psw, "password") == 0)
break;
}
if (i == 3)
printf("exit\n");
else
printf("log in\n");
}
> 这个题中需要注意的是两个字符串不能直接进行比较,只能利用string.h库函数中的strcmp()进行比较,当两个字符串的每一个元素都相等的时候,给函数的返回值为0,即可进行判断。 #### 3.4.6.猜数字游戏实现
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void menu()
{
printf("**********************************\n");
printf("*********** 1.play **********\n");
printf("*********** 0.exit **********\n");
printf("**********************************\n");
}
void game()
{
int random_num = rand() % 100 + 1;
int input = 0;
while (1)
{
printf("请输入猜的数字>:");
scanf("%d", &input);
if (input > random_num)
{
printf("猜大了\n");
}
else if (input < random_num)
{
printf("猜小了\n");
}
else
{
printf("恭喜你,猜对了\n");
break;
}
}
}
int main()
{
int input = 0;
srand((unsigned)time(NULL));
do
{
menu();
printf("请选择>:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("选择错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
> 猜数字游戏并不复杂,但运用了函数模块化的思想,其中生成随机数用到了rand()函数和时间戳进行搭配,需要注意的是,时间戳只需要在主函数中声明或者运行一次即可,不需要在每次生成随机数时都进行声明。 ## 4.goto语句 >  C语言中提供了可以随意滥用的 goto语句和标记跳转的标号。 > > 从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。 但是某些场合下goto语句还是用得着的,最常见的用法就是终止程序在某些深度嵌套的结构的处理过程。 > > 例如:一次跳出两层或多层循环。 多层循环这种情况使用break是达不到目的的。它只能从最内层循环退出到上一层的循环。当然,利用多个break也可以跳出循环,不过要加很多条件进行限制,代码会变得过于冗长,且容易出错。 > > goto语言真正适合的场景如下: >
1
2
3
4
5
6
7
8
9
10
11
for (...)
{
for (...)
{
if (disaster)
goto error;
}
}
error :
if (disaster)
//处理错误情况
下面是使用goto语句的一个例子,然后使用循环的实现方式替换goto语句: ### 一个关机程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
char input[10] = { 0 };
system("shutdown -s -t 60");
again:
printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
scanf("%s", input);
if (0 == strcmp(input, "我是猪"))
{
system("shutdown -a");
}
else
{
goto again;
}
return 0;
}
而如果不想用goto语句,则可以使用循环:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
int main()
{
char input[10] = { 0 };
system("shutdown -s -t 60");
while (1)
{
printf("电脑将在1分钟内关机,如果输入:我是猪,就取消关机!\n请输入:>");
scanf("%s", input);
if (0 == strcmp(input, "我是猪"))
{
system("shutdown -a");
break;
}
}
return 0;
}
## 8\. 函数
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 num1 = 0;
int num2 = 0;
int sum = 0;
printf("输入两个操作数:>");
scanf("%d %d", &num1, &num2);
sum = num1 + num2;
printf("sum = %d\n", sum);
return 0;
}
上述代码,写成函数如下:
#include <stdio.h>
int Add(int x, int y)
{
int z = x + y;
return z;
}
int main()
{
int num1 = 0;
int num2 = 0;
int sum = 0;
printf("输入两个操作数:>");
scanf("%d %d", &num1, &num2);
sum = Add(num1, num2);
printf("sum = %d\n", sum);
return 0;
}
> 函数的特点就是简化代码,代码复用。 ## 1.  函数是什么? > 提到函数,我们最先想到的肯定是数学中的函数,那么C语言中的函数究竟是什么呢?接下来带大家看一下吧! > 维基百科中对函数的定义:子程序 > > 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部分代码, 由一个或多个语句块组 成。它负责完成某项特定任务,而且相较于其他代 码,具备相对的独立性。 一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软 件库。 ## 2.  C语言中函数的分类: ### 2.1 库函数 #### 2.1.1 为什么要有库函数 > 1\. 我们知道在我们学习C语言编程的时候,总是在一个代码编写完成之后迫不及待的想知道结果,想把这个结果打印到我们的屏幕上看看。这个时候我们会频繁的使用一个功能:将信息按照一定的格 式打印到屏幕上(printf)。 > > 2\. 在编程的过程中我们会频繁的做一些字符串的拷贝工作(strcpy)。 > > 3\. 在编程是我们也计算,总是会计算n的k次方这样的运算(pow)。 > 像上面我们描述的基础功能,它们不是业务性的代码。我们在开发的过程中每个程序员都可能用的到, **为了支持可移植性和提高程序的效率**,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。 #### 2.1.2 什么是库函数 > 那么什么是库函数呢?举个简单的例子,库函数就是C语言本身给我们已经定义好的函数,作为程序员我们可以直接使用,就像printf()和scanf()。 > > **注意**:**使用库函数必须包含头文件**,例如我们使用printf()与scanf()时要引用stdio.h头文件,即我们通常写的#include #### 2.1.3 主函数只能是main()吗 > 很多小伙伴就疑惑了,那么主函数是什么呢?主函数为什么叫main函数呢?我们必须使用main()函数吗?接下来给大家解除这个疑惑! > 首先给大家一个结论,C语言中默认main作为主函数的名字,但是主函数的名字却不一定一定是main(),实际上,我们可以自己进行设定主函数的名字的,C语言中提供了#pragma comment()可以自己设定主函数的名字,有兴趣的小伙伴可以自己去尝试,在这个地方像哟啊告诉大家,作为程序的入口**主函数的名字不一定必须是main()**,希望大家可以记住这个!至于主函数为什么叫main()函数,这本身就是C语言默认的,如果硬要强行解释一波的话,main()的英文意识 就是主要的意思。 #### 2.1.4常见的库函数 > IO函数 > > 字符串操作函数 > > 字符操作函数 > > 内存操作函数 > > 时间/日期函数 > > 数学函数 > > 其他库函数 ### 2.2 自定义函数 #### 2.2.1自定义函数是什么 > 自定义函数就是程序员自己定义用于首先特定功能的函数!比如我们要完成两个数的相加我们定义的add()函数就属于自定义函数。 #### 2.2.2为什么要有自定义函数 > 自定义函数和库函数一样,有函数名,返回值类型和函数参数。 > > 但是不一样的是这些都是我们自己来设计。这给程序员一个很大的发挥空间。 #### 2.2.3函数的组成
1
2
3
4
5
6
7
ret_type fun_name(para1, *)
{
statement;//语句项
}
ret_type 返回类型
fun_name 函数名
para1 函数参数
#### 2.2.4 举例展示 **(1)写一个函数可以找出两个整数中的最大值。**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
//get_max函数的设计
int get_max(int x, int y)
{
return (x > y) ? (x) : (y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int max = get_max(num1, num2);
printf("max = %d\n", max);
return 0;
}
> 这个地方给大家解释一下三目运算符,即上面的(x>y)?(x):(y),这个地方表示的是,如果x>y成立,就返回x的值,反之就返回y的值,这样就达到了求最大值的目的! **(2)写一个函数求两个数的和**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
//get_add函数的设计
int get_add(int x, int y)
{
return (x + y);
}
int main()
{
int num1 = 10;
int num2 = 20;
int sum = get_add(num1, num2);
printf("sum = %d\n", sum);
return 0;
}
## 3\. 函数的参数 ### 3.1 实际参数(实参) > 真实传给函数的参数,叫实参。 实参可以是:常量、变量、表达式、函数等。 > > 注意:为什么可以是函数呢?因为有的函数是由返回值的,所以自然也就能充当实参。 > > 无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形 参。 ### 3.2  形式参数(形参) > 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内 存单 元),所以叫形式参数。**形式参数当函数调用完成之后就自动销毁**了。因此形式参数只在函数中有效。 > 为什么这样说呢?接下来给大家举个例子吧! > > 例如我们要交换两个变量的值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
int main()
{
int num1 = 10;
int num2 = 20;
swap(num1, num2);
printf("num1 = %d\nnum2 = %d", num1,num2);
return 0;
}
> 下面是代码的运行结果: > > ![](https://i-blog.csdnimg.cn/blog_migrate/406361c07dfcb61bd496c453168ba036.png) > 很明显,并没有达成交换的目的,这就证明了:形式参数当函数调用完成之后就自动销毁了,即我们把num1和num2传给x和y之后,虽然我们在函数例将x和y交换了,但是由于x和y在swap函数调用完成后就销毁了,即并没有真正实现num1和num2的交换。 > > 所以我们可以简单的认为:**形参实例化之后其实相当于实参的一份临时拷贝**。 ## 4\. 函数的调用 ### 4.1 传值调用 > 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。我们上述的交换的例子就是传值调用,即并不能真正达成交换两个变量的值的目的! ### 4.2  传址调用 > 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。 > > 这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操 作函数外部的变量。 > 同样,我们就以前面的交换两个变量的值的例子来给大家进行举例!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
void swap(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
}
int main()
{
int num1 = 10;
int num2 = 20;
swap(&num1, &num2);
printf("num1 = %d\nnum2 = %d", num1,num2);
return 0;
}
> 这个地方为什么会发生这种情况呢?等后期我们学到指针的那一节的时候将会具体讲解! #### 4.3 练习 #### 4.3.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
#include<stdio.h>
int is_leap_year(int year)
{
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
{
return 1;
}
else
{
return 0;
}
}
int main()
{
int year = 0;
int flag = 0;
scanf("%d", &year);
flag=is_leap_year(year);
if (1 == flag)
{
printf("是闰年!");
}
else
{
printf("不是闰年!");
}
return 0;
}
#### 4.3.2.写一个函数可以判断一个数是不是素数。
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
#include<stdio.h>
#include<math.h>
int is_prime_num(int n)
{
int flag = 0;
int i = 0;
for (i = 2; i <= sqrt(n); i++)
{
if (n % i == 0)
{
return 0;
}
}
return 1;
}
int main()
{
int i = 0;
for (i = 100; i < 200; i++)
{
int flag = 0;
flag=is_prime_num(i);
if (1 == flag)
{
printf("%d ", i);
}
else
continue;

}
return 0;
}
#### 4.3.3  写一个函数,实现一个整形有序数组的二分查找。
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
38
39
40
41
42
43
44
45
46
#include<stdio.h>
int find_num(int arr[], int size,int k)
{
int left = 0;
int right = 0;
right = size - 1;
int mid = 0;
while (left <= right)
{
mid = (left + right) / 2;
if (arr[mid] < k)
{
left = mid + 1;
}
else if (arr[mid] > k)
{
right = mid - 1;
}
else
{
return mid;
}
}
if (left > right)
{
return -1;
}
}
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int k = 7;
int size = 0;
size = sizeof(arr) / sizeof(arr[0]);
int ret = 0;
ret = find_num(arr, size,k);
if (-1 == ret)
{
printf("没有找到!");
}
else
{
printf("找到了!下标为%d", ret);
}
return 0;
}
## 9\. 数组 > 要存储1-10的数字,怎么存储? > > C语言中给了数组的定义:一组相同类型元素的集合. ### 9.1 [数组定义](https://so.csdn.net/so/search?q=%E6%95%B0%E7%BB%84%E5%AE%9A%E4%B9%89&spm=1001.2101.3001.7020)
1
int arr[10] = {1,2,3,4,5,6,7,8,9,10};//定义一个整形数组,最多放10个元素
### 9.2 数组的下标 > C语言规定:数组的每个元素都有一个下标,下标是从0开始的。 > > 数组可以通过下标来访问的。 > > 比如:
1
2
int arr[10] = {0};
//如果数组10个元素,下标的范围是0-9
> ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/d28af5be4e7b7565088748790bec20bd.png) ###  9.3 数组的使用
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
> 运行结果如下图所示: > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/ed962382940aaf81685ed5e523bdd64e.png) ## 1、一维数组的创建和初始化 ### 1.1 数组的创建 > 数组是一组相同类型元素的集合。 > > 数组的创建方式:
1
2
3
type_t   arr_name[const_n];
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
> 例子:
1
2
3
4
5
6
7
8
9
10
//代码1
int arr1[10];
//代码2
int count = 10;
int arr2[count];//数组时候可以正常创建?
注意:此时是不可以创建的,因为count是变量,而[]内只能是常量
//代码3
char arr3[10];
float arr4[1];
double arr5[20];
> **注:数组创建,在C99标准之前, [ ] 中要给一个常量才可以,不能使用变量。在C99标准支持了变长数组的概念,即在[ ]中可以用变量。** ### 1.2 数组的初始化 > 数组的初始化是指,**在创建数组的同时给数组的内容一些合理初始值(初始化)**。  > > 下面是例子:
1
2
3
4
5
6
int arr1[10] = { 1,2,3 }; 10
int arr2[] = { 1,2,3,4 }; 4
int arr3[5] = { 1,2,3,4,5 }; 5
char arr4[3] = { 'a',98, 'c' }; 3
char arr5[] = { 'a','b','c' }; 3
char arr6[] = "abcdef"; 7
> **数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。** > 但是对于下面的代码要区分,内存中如何分配。
1
2
char arr1[] = "abc";
char arr2[3] = { 'a','b','c' };
> ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/8b1a52900de5b54484d17c82a6eb8f09.png) >  在arr1中,在内存中有4个数组元素,除了字符'a','b','c'之外,还有一个字符串的结束标志'\\0',而在arr2中,则只有字符'a','b','c',对这两个字符串进行输出,大家可以看一下: > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/c22a4ffde4cda85b9465db537edfa227.png) > 在这个地方需要大家进行注意:printf()函数对字符串进行输出的时候,当遇到'\\0'即字符串的结束标志即停止输出,在arr2中,因为没有字符串的结束标志,所以会出现乱码! > > 综上而言,**arr2的这种初始化方式没有字符串的结束标志‘\\0',而arr1的这种初始化的方式则不包含字符串的结束标志!** ### 1.3 一维数组的使用 > 对于数组的使用我们之前介绍了一个操作符: [ ] ,下标引用操作符。它其实就数组访问的操作符。 我们来看代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int main()
{
int arr[10] = { 0 };//数组的不完全初始化
//计算数组的元素个数
int sz = sizeof(arr) / sizeof(arr[0]); 10
//对数组内容赋值,数组是使用下标来访问的,下标从0开始。所以:
int i = 0;//做下标
for (i = 0; i < 10; i++)//这里写10,好不好?
{
arr[i] = i;
}
//输出数组的内容
for (i = 0; i < 10; ++i)
{
printf("%d ", arr[i]);
}
return 0;
}
//如果您在循环中写了一个不同的数字(比如9),那么您将不会访问或修改数组的最后一个元素(`arr[9]`),这可能会导致未定义的行为(如果稍后尝试访问它)或错过一些重要的数据处理。 > 总结: > 1. **数组是使用下标来访问的,下标是从0开始。** > 2. **数组的大小可以通过计算得到。** > 那么我们如何求数组的个数呢?
1
2
int arr[10];
int sz = sizeof(arr)/sizeof(arr[0]);
> 数组的个数即是数组所占总的空间除以一个数组元素所得的值,简单举个例子来说就有点类似于总价除以单价得到商品的个数! ### 1.4 一维数组在内存中的存储 > 接下来我们探讨数组在内存中的存储。 看代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);

for (i = 0; i < sz; ++i)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
return 0;
}
> 下面是运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/b85140bed3bea49dca13b4435800041c.png) > > 仔细观察输出的结果,我们知道,随着数组下标的增长,元素的地址,也在有规律的递增。 由此可以得出结论:**数组在内存中是连续存放的。** > > 这个地方需要大家记住这个结论,因为后面我们还会讨论二维数组在内存空间中的存放! > 下面是画图展示: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/d7acbd552c708b3eadb96cf33d15b9d7.png) ##  2. 二维数组的创建和初始化 ### 2.1 二维数组的创建
1
2
3
4
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
### 2.2 二维数组的初始化
1
2
3
4
5
6
//数组初始化
int arr[3][4] = {1,2,3,4};
int arr[3][4] = {{1,2},{4,5}};

int arr[][4] = {{2,3},{4,5}};//二维数组如果有初始化,行可以省略,列不能省略
//等价于 int arr[2][4] = {{2,3,0,0},{4,5,0,0}};
1
2
3
1 2 3 4 
0 0 0 0
0 0 0 0
1
2
3
1 2 0 0 
4 5 0 0
0 0 0 0
1
2
2 3 0 0 
4 5 0 0
### 2.3 二维数组的使用 > 二维数组的使用也是通过下标的方式。 看代码:
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()
{
int arr[3][4] = { 0 };
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
arr[i][j] = i * 4 + j;
}
}
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("%d ", arr[i][j]);
}
}
return 0;
}
> 下面是运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/ae4094cd29c3a1ef6bf4a5c614d4c779.png) ### 2.4 二维数组在内存中的存储 > 像一维数组一样,这里我们尝试打印二维数组的每个元素。 ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/6532fbeefd9219bc758aa1e8eac77ec4.png) >  通过结果我们可以分析到,其实二维数组在内存中也是连续存储的。 > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/37938654fff0f7cf78a27baaad7c3f79.png) ## 3\. 数组越界 > **数组的下标是有范围限制的。** > > **数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。 所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。** > > **C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的, 所以程序员写代码时,最好自己做越界的检查。** > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/2cda90535d91c6b8c26063b1651c6ff9.png) > > 注意:**二维数组的行和列也可能存在越界。** ## 4\. 数组作为函数参数 > 往往我们在写代码的时候,会将数组作为参数传个函数,比如:我要实现一个冒泡排序(这里要讲算法 思想)函数将一个整形数组排序。 > > 那我们将会这样使用该函数: ### 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
//方法一:
#include <stdio.h>
void bubble_sort(int arr[])
{
int sz = sizeof(arr) / sizeof(arr[0]);//这样对吗?
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
bubble_sort(arr);//是否可以正常排序?
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("%d ", arr[i]);
}
return 0;
}
> 下面是运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/c8be28079d5726f29443fc2662095650.png) > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/2f6590112b3eea8f12eec0b30e72a51e.png) > > 很明显,方法1出问题了,并没有实现排序的目的,那我们找一下问题,调试之后可以看到 bubble\_sort 函数内部的 sz ,是1。 难道数组作为函数参数的时候,不是把整个数组的传递过去?下面就要讨论一下数组名究竟代表什么! ### 4.2 数组名是什么?
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
printf("%d\n", *arr);
//输出结果
return 0;
}
> 下面是代码运行结果截图: > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/0fa78e4cbc8913b42fa576319cf83f8b.png) > >  结论:数组名是数组首元素的地址。(有下面两个例外) > > 1. **sizeof(数组名),计算整个数组的大小,sizeof内部单独放一个数组名,数组名表示整个数 组。** > 2. **&数组名,取出的是数组的地址。&数组名,数组名表示整个数组。** > > **除此1,2两种情况之外,所有的数组名都表示数组首元素的地址。** ### 4.3 冒泡排序函数的正确设计 > 当数组传参的时候,实际上只是把数组的首元素的地址传递过去了。 所以即使在函数参数部分写成数组的形式: int arr[ ] 表示的依然是一个指针: int \*arr 。 那么,函数内部的 sizeof(arr) 结果是4。 既然方法1错了,那么冒泡排序法究竟该怎么设计?下面就是冒泡排序法的正确方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//方法2
void bubble_sort(int arr[], int sz)//参数接收数组元素个数
{
//代码同上面函数
}
int main()
{
int arr[] = { 3,1,7,5,8,9,0,2,4,6 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);//是否可以正常排序?
for (int i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
## 10\. 操作符 > 算术操作符 > > + +        加 > + \-         减 > + \*         乘 > + /          除 > + %       取余 > > 运算符有很多,此处不再一一列举!后面的内容中会讲!
1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main()
{
int a = 10;
int b = 5;
int c = 3;
printf("%d\n", a + b);
printf("%d\n", a - b);
printf("%d\n", a * b);
printf("%d\n", a / b);
printf("%d\n", a % c);
return 0;
}
> 下面是运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/8af53111a30efc8e5e4411e9ae66b240.png) ## 1、操作符分类 > + 算术操作符 > + 移位操作符 > + 位操作符 > + 赋值操作符 > + 单目操作符 > + 关系操作符 > + 逻辑操作符 > + 条件操作符 > + 逗号表达式 > + 下标引用函数调用和结构成员 ## 2、算术操作符 > + \+   > +  -   > + \*   > + /   > + % > > 注意: > > 1. **除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。** > 2. **对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。** > 3. **% 操作符的两个操作数必须为整数。返回的是整除之后的余数。** ## 3、移位操作符 > + << 左移操作符 > + \>>右移操作符     > > **注:移位操作符的操作数只能是整数。** ### 3.1 左移操作符 > 移位规则:左边抛弃、右边补0 > > ![](https://i-blog.csdnimg.cn/blog_migrate/c101db721d0fd854bee8641fbd7dabca.png) ### 3.2 右移操作符  > 移位规则: > > 首先右移运算分两种: > > 1. 逻辑移位:                                                                                                                                > 左边用0填充,右边丢弃 > 2. 算术移位                                                                                                                                > 左边用原该值的符号位填充,右边丢弃 > > ![](https://i-blog.csdnimg.cn/blog_migrate/f6185177dc8ea1b9201f6c1b602e1cb2.png) >  警告: > > **对于移位运算符,不要移动负数位,这个是标准未定义的。** > > 例如:
1
2
int num = -1;
num>>-1;//error
## 4、位操作符 > 位操作符有: > > + & //按位与 > + | //按位或 > + ^ //按位异或 > > **注:他们的操作数必须是整数。** ## 5、赋值操作符 ### 赋值操作符 > 赋值操作符是一个很棒的操作符,他可以让你得到一个你之前不满意的值。也就是你可以给自己重新赋值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int weight = 120;//体重
weight = 89;//不满意就赋值
double salary = 10000.0;
salary = 20000.0;//使用赋值操作符赋值。

赋值操作符可以连续使用,比如:
int a = 10;
int x = 0;
int y = 20;
a = x = y+1;//连续赋值

这样的代码感觉怎么样?
那同样的语义,你看看:
x = y+1;
a = x;
这样的写法是不是更加清晰爽朗而且易于调试。所以我们并不推荐连续赋值!
### 复合赋值符 > + += > + \-= > + \*= > + /= > + %= > + \>>= > + <<= > + &= > + |= > + ^= > > 这些运算符都可以写成复合的效果。 > > 比如:
1
2
3
4
int x = 10;
x = x+10;
x += 10;//复合赋值
//其他运算符一样的道理。这样写更加简洁。
## 6、单目操作符 ### 6.1 单目操作符介绍 > + !           逻辑反操作 > + \-           负值 > + \+           正值 > + &           取地址 > + sizeof      操作数的类型长度(以字节为单位) > + ~           对一个数的二进制按位取反 > + \--          前置、后置-- > + ++          前置、后置++ > + \*           间接访问操作符(解引用操作符) > + (类型)       强制类型转换 ### 6.2 sizeof 和数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
void test1(int arr[])
{
printf("%d\n", sizeof(arr));//(4)
}
void test2(char ch[])
{
printf("%d\n", sizeof(ch));//(4)
}
int main()
{
int arr[10] = { 0 };
char ch[10] = { 0 };
printf("%d\n", sizeof(arr));//(40)
printf("%d\n", sizeof(ch));//(10)
test1(arr);
test2(ch);
return 0;
}
> 下面是输出结果:
1
40 10 4 4
>  代码分析: > > 40是整个数组所有元素的大小,数组arr总共是有10个整型元素,所以数组所占字节数是10\*4 = 40个字节。 > > 同理,10是因为ch数组总共有10个字节型元素,所以数组所占的字节数是10\*1 = 10个字节。 > > 根据前面所学的知识,arr和ch都是数组名,类型均为指针类型,而指针所占的字节大小只于我们配置的管理器有关,此处是选择的X86,即32位,而每8位是一个字节,所以是有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()
{
int a = 10;
int x = ++a;
//先对a进行自增,然后对使用a,也就是表达式的值是a自增之后的值。x为11。
int y = --a;
//先对a进行自减,然后对使用a,也就是表达式的值是a自减之后的值。y为10;
return 0;
}
//后置++和--
#include <stdio.h>
int main()
{
int a = 10;
int x = a++;
//先对a先使用,再增加,这样x的值是10;之后a变成11;
int y = a--;
//先对a先使用,再自减,这样y的值是11;之后a变成10;
return 0;
}
## 7、关系操作符 > 关系操作符 > > + \> > + \>= > + < > + <= > + !=   用于测试“不相等” > + \==      用于测试“相等” > > 警告: 在编程的过程中== 和=不小心写错,导致的错误。前者是关系运算符,而后者是赋值运算符。 ## 8、逻辑操作符 > 逻辑操作符有哪些: > > + &&     逻辑与 > + ||        逻辑或 > > 区分**逻辑与**和按位**与** > > 区分**逻辑或**和按位**或** > > 1&2----->0 > > 1&&2---->1 > > 1|2----->3 > > 1||2---->1 - **逻辑与(&&)**:当且仅当两个操作数都为真时,结果才为真。如果任一操作数为假,则结果为假。 - 例如:`1 && 2` 在逻辑上下文中没有意义,因为1和2不是布尔值。但在某些编程语言中(如C、C++、Java等),非零值被视为真,零值被视为假。因此,如果这里是指整数运算后的布尔结果,由于1和2都是非零的,它们被视为真,所以 `1 && 2` 会评估为真(通常表示为1)。 - **逻辑或(||)**:如果任一操作数为真,则结果为真。仅当两个操作数都为假时,结果才为假。 - 例如:`1 || 2` 同样地,在逻辑上下文中,这没有意义,因为1和2不是布尔值。但按照上述非零即真的规则,`1 || 2` 会评估为真。 - **按位与(&)**:对两个数的二进制表示进行逐位与运算。仅当两个相应的位都为1时,结果的相应位才为1。 - 例如:`1 & 2` 在二进制中,1是`0001`,2是`0010`。按位与的结果是`0000`,即0。 - **按位或(|)**:对两个数的二进制表示进行逐位或运算。如果任一相应的位为1,则结果的相应位为1。 - 例如:`1 | 2` 在二进制中,1是`0001`,2是`0010`。按位或的结果是`0011`,即3。 ## 9、条件操作符 > exp1 ? exp2 : exp3 > 练习:
1
temp = (x>y?x:y)  //取x和y中的较大值,并将较大值赋值给temp
## 10、逗号表达式 > exp1, exp2, exp3, …expN > **逗号表达式,就是用逗号隔开的多个表达式。** > > **逗号表达式,从左向右依次执行。整个表达式的结果是最后一个表达式的结果。** > 练习:
1
2
3
int a = 1;
int b = 2;
int c = (a > b, a = b + 10, a, b = a + 1);//逗号表达式
> 首先执行a>b,对程序没有任何的影响,然后执行a = b+10,执行后a的值变为12,然后是a,程序依旧没有任何影响,然后是b = a + 1,执行后b的值为13,因为前面a的值已经变为12了,所以12+1就变为13了,此时c的值等于最后这个表达式的值,即13,即程序运行后,a的值为12,b的值为13,c也是13。 > > ![](https://i-blog.csdnimg.cn/blog_migrate/d873b51b05d90476ff6c210d5fa4df1a.png) > > 程序相应的进行输出后也验证了我们的计算!  > > 在做逗号表达式的题目时需要注意一点,一般只有赋值运算才会改变变量的值!其它操作一般不会引起变量的值的改变。 ## 11、下标引用、函数调用和结构成员 ### 1\. \[ \] 下标引用操作符 > 操作数:一个数组名 + 一个索引值
1
2
3
int arr[10];//创建数组
arr[9] = 10;//实用下标引用操作符。
[ ]的两个操作数是arr和9
### 2\. ( ) 函数调用操作符 > 接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
void test1()
{
printf("hehe\n");
}
void test2(const char* str)
{
printf("%s\n", str);
}
int main()
{
test1(); //实用()作为函数调用操作符。
test2("hello bit.");//实用()作为函数调用操作符。
return 0;
}
//注意,`printf`函数在打印字符串后会自动添加一个换行符,但字符串字面量本身也包含了一个换行符`\n`,所以这里实际上会打印两行空行之间的文本

### 3\. 访问一个结构的成员 > + .            结构体.成员名 > + \->         结构体指针->成员名
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
#include <stdio.h>
struct Stu
{
char name[10];
int age;
char sex[5];
double score;
};
//这个函数接受一个`Stu`类型的结构体作为参数,并尝试将其年龄设置为18。然而,这里存在一个关键问题:函数参数是按值传递的,这意味着传递给函数的实际上是结构体的一个副本,而不是原始结构体本身。因此,对副本的修改不会影响原始结构体。
void set_age1(struct Stu stu)
{
stu.age = 18;
}
//这个函数接受一个指向`Stu`类型结构体的指针作为参数。通过指针,可以直接访问并修改原始结构体的成员。在这个函数中,将结构体的年龄设置为18,这个修改将反映在原始结构体上
void set_age2(struct Stu* pStu)
{
pStu->age = 18;//结构成员访问
}

int main()
{
struct Stu stu;
struct Stu* pStu = &stu;//结构成员访问

stu.age = 20;//结构成员访问
set_age1(stu); //// 这里对stu的副本进行操作,stu.age仍为20

pStu->age = 20;//结构成员访问
set_age2(pStu); // 这里直接修改stu,stu.age变为18
return 0;
}
在`main`函数中,首先创建了一个`Stu`类型的结构体变量`stu`,并初始化了一个指向它的指针`pStu`。然后,尝试通过直接访问和函数调用两种方式修改`stu`的年龄。 - `stu.age = 20;` 直接将`stu`的年龄设置为20。 - `set_age1(stu);` 调用`set_age1`函数,但由于是按值传递,`stu`的年龄保持不变(仍为20)。 - `pStu->age = 20;` 再次将`stu`的年龄设置为20(这一步其实是多余的,因为上一步已经设置了)。 - `set_age2(pStu);` 调用`set_age2`函数,这次通过指针直接修改了`stu`的年龄,将其设置为18。 ## 12、表达式求值 > 表达式求值的顺序一部分是由操作符的优先级和结合性决定。 > > 同样,有些表达式的操作数在求值的过程中可能需要转换为其他类型。 ### 12.1 隐式类型转换 > C的整型算术运算总是至少以缺省整型类型的精度来进行的。 > > 为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升。 > **整型提升的意义:** > > 整型提升的意义: 表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度 一般就是int的字节长度,同时也是CPU的通用寄存器的长度。 > > 因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长 度。 > > 通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令 中可能有这种字节相加指令)。所以,表达式中各种长度可能小于int长度的整型值,都必须先转 换为int或unsigned int,然后才能送入CPU去执行运算。
1
2
3
4
//实例1
char a,b,c;
...
a = b + c;
> b和c的值被提升为普通整型,然后再执行加法运算。 > > 加法运算完成之后,结果将被截断,然后再存储于a中。 **如何进行整体提升呢?** **整形提升是按照变量的数据类型的符号位来提升的**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//负数的整形提升
char c1 = -1;
变量c1的二进制位(补码)中只有8个比特位:
1111111
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为1
提升之后的结果是:
11111111111111111111111111111111
//正数的整形提升
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char
所以整形提升的时候,高位补充符号位,即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
整形提升的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//实例1
int main()
{
char a = 0xb6;
short b = 0xb600;
int c = 0xb6000000;
if (a == 0xb6)
printf("a");
if (b == 0xb600)
printf("b");
if (c == 0xb6000000)
printf("c");
return 0;
}
> 实例1中的a,b要进行整形提升,但是c不需要整形提升 a,b整形提升之后,变成了负数,所以表达式 a== 0xb6 , b== 0xb600 的结果是假,但是c不发生整形提升,则表 达式 c== 0xb6000000 的结果是真. > > 所以程序输出的结果是: > > ![](https://i-blog.csdnimg.cn/blog_migrate/2dcd64d7621e371a8236ac413ee1f43f.png)
1
2
3
4
5
6
7
8
9
//实例2
int main()
{
char c = 1;
printf("%u\n", sizeof(c));
printf("%u\n", sizeof(+c));
printf("%u\n", sizeof(-c));
return 0;
}
> 实例2中的,c只要参与表达式运算,就会发生整形提升,表达式 +c ,就会发生提升,所以 sizeof(+c) 是4个字节 > > 表达式 -c 也会发生整形提升,所以 sizeof(-c) 是4个字节,但是 sizeof(c) ,就是1个字节 > > 所以程序的输出结果是: > ![](https://i-blog.csdnimg.cn/blog_migrate/40242489131e15cb3cd8b1d2be825023.png) > 不过要注意一个特殊情况: > ![](https://i-blog.csdnimg.cn/blog_migrate/1b5cece19d613e9d50c1bcb1be4fa650.png) > >  **即在变量的前面进行!运算后用sizeof对其求大小时并没有发生改变** ### 12.2 算术转换 > 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。下面的层次体系称为寻常算术转换。 > long double > double > float > unsigned long int > long int > unsigned int > int > 如果某个操作数的类型在上面这个列表中排名较低,那么首先要转换为另外一个操作数的类型后执行运算。 > > **警告: 但是算术转换要合理,要不然会有一些潜在的问题。**
1
2
float f = 3.14;
int num = f;//隐式转换,会有精度丢失
### 12.3 操作符的属性 > 复杂表达式的求值有三个影响的因素。 > > 1. 操作符的优先级 > 2. 操作符的结合性 > 3. 是否控制求值顺序。 > > 两个相邻的操作符先执行哪个? > > 取决于他们的优先级。如果两者的优先级相同,取决于他们的结合性。 操作符优先级 > ![](https://i-blog.csdnimg.cn/blog_migrate/29fc0ced8ad9e616ed2fc214a612a96e.png) > ![](https://i-blog.csdnimg.cn/blog_migrate/dcaa5ba192f18f755a524b888bb80af9.png) > ![](https://i-blog.csdnimg.cn/blog_migrate/d670cf18af480caeeb89821e67866be7.png) > 虽然我们已经直到了操作符的优先级和结合性,但是仍然会有一些我们无法确定结果的表达式,为了防止出现这种表达式,我们可以**使用( )来使表达式按照我们想要运算的顺序进行运算**!这是菲方重要的,也是作为一个良好的程序员一定要知道并且经常使用的,虽然有时候这个( )并不是那么的必要,因为默认的优先级顺序已经使表达式按照我们想要进行的顺序进行运算了,但是代码的可读性却不是那么好了,因为讲真的上面的这些顺序太难记了,我们在写代码时,更好的方法是通过( )来进行划分,方便自己后续查看代码以及别人查看我们的代码,我们想要表达的意思也一目了然! > > **结论:我们写出来的正确的代码应该是在上面的优先级顺序和结合性下应该有唯一的运算路径,并且能够得到唯一的运算结果! 如果我们写出的表达式如果不能通过操作符的属性确定唯一的计算路径,那这个表达式就是存在问题的。** ## 11\. 常见关键字  > + auto   > + break   > + case   > + char > +  const   > + continue   > + default   > + do   > + double > + else   > + enum > + extern > + float   > + for   > + goto   > + if   > + int   > + long   > + register     > + return   > + short   > + signed > + sizeof   > + static > + struct   > + switch   > + typedef > + union   > + unsigned   > + void   > + volatile   > + while > > 先不进行全面的介绍,后面会逐步进行介绍的!此处先简要给大家介绍几个! ### 11.1 关键字 typedef > typedef 顾名思义是类型定义,这里应该理解为类型重命名。 > > 比如:
1
2
3
4
5
6
7
8
typedef unsigned int uint_32;
int main()
{
//观察num1和num2,这两个变量的类型是一样的
unsigned int num1 = 0;
uint_32 num2 = 0;
return 0;
}
### 11.2 关键字static > 在C语言中: > > static是用来修饰变量和函数的 > > 1. 修饰局部变量-称为静态局部变量 > 2. 修饰全局变量-称为静态全局变量 > 3. 修饰函数-称为静态函数 #### 11.2.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
32
33
34
35
//代码1
#include <stdio.h>
void test()
{
int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
test();
}
return 0;
}
//代码2
#include <stdio.h>
void test()
{
//static修饰局部变量
static int i = 0;
i++;
printf("%d ", i);
}
int main()
{
int i = 0;
for (i = 0; i < 10; i++)
{
test();
}
return 0;
}
> 代码1运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/7b8dd5eadf5df14ca4ac942a18fb9960.png) > >  代码2运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/747512bbc6e1e3fab9f44af2895ff15d.png) > > 结论: > > + **static修饰局部变量改变了变量的生命周期 让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束。** #### 11.2.2 修饰全局变量 > 注:add.c和test.c代表两个不同的源文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//代码1
//add.c
int g_val = 2018;
//test.c
int main()
{
printf("%d\n", g_val);
return 0;
}
//代码2
//add.c
static int g_val = 2018;
//test.c
int main()
{
printf("%d\n", g_val);
return 0;
}
> 代码1正常,代码2在编译的时候会出现连接性错误。 > 结论: > > +  **一个全局变量被static修饰,使得这个全局变量只能在本源文件内使用,不能在其他源文件内使用。** #### 11.2.3 修饰函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//代码1
//add.c
int Add(int x, int y)
{
return c + y;
}
//test.c
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
//代码2
//add.c
static int Add(int x, int y)
{
return c + y;
}
//test.c
int main()
{
printf("%d\n", Add(2, 3));
return 0;
}
> > 代码1正常,代码2在编译的时候会出现连接性错误. > > 结论: > > + **一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。** ## 12\. #define 定义常量和宏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//define定义标识符常量
#define MAX 1000
//define定义宏
#define ADD(x, y) ((x)+(y))
#include <stdio.h>
int main()
{
int sum = ADD(2, 3);
printf("sum = %d\n", sum);

sum = 10 * ADD(2, 3);
printf("sum = %d\n", sum);

return 0;
}
> 运行结果: > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/572f126352d6a33be901d80790b262db.png) ## 13\. 指针 ### 13.1 内存 > 内存是电脑上特别重要的存储器,**计算机中程序的运行都是在内存中进行的** 。 > > 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,**每个内存单元的大小是1个字节**。 > > 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该**内存单元的地址**。 > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/a404d52346380a26a27f90c999a9b8b5.png) > 变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的。 取出变量地址如下:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int num = 10;
&num;//取出num的地址
//注:这里num的4个字节,每个字节都有地址,取出的是第一个字节的地址(较小的地址)
printf("%p\n", &num);//打印地址,%p是以地址的形式打印
return 0;
}
![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315220135.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250317183035.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250317183018.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250317183007.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250317184105.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250317185015.png) > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/ba1f0df2797e3f4486c176f821547b4e.png) > > 那地址如何存储,需要定义指针变量。
1
2
3
int num = 10;
int *p;//p为一个整形指针变量
p = &num;
> 指针的使用实例:
1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int num = 10;
int* p = &num;
*p = 20;
return 0;
}
> ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/d4afbaa71b131068f4f817223a289411.png) > >  以整形指针举例,可以推广到其他类型,如:
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'q';
printf("%c\n", ch);
return 0;
}
### 13.2 指针变量的大小
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
//指针变量的大小取决于地址的大小
//32位平台下地址是32个bit位(即4个字节)
//64位平台下地址是64个bit位(即8个字节)
int main()
{
printf("%d\n", sizeof(char*));
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(int*));
printf("%d\n", sizeof(double*));
return 0;
}
> ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/64b195863840f1cc16f7117681de9b68.png) > > 上述即为32位平台 > > ![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/a1fa295bbf71fb3d9acb7cc3c164af53.png) 上述为64位平台 > >  **结论:指针大小在32位平台是4个字节,64位平台是8个字节。** ## 1、指针是什么? > 指针是什么? > > 指针理解的2个要点: > > + 1\. 指针是内存中一个最小单元的编号,也就是地址,即我们常说的指针即地址 > + 2\. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量,即指针即变量 > > **总结:指针就是地址,口语中说的指针通常指的是指针变量。** > > 那么我们就可以这样理解: > > **内存** > > ![](https://i-blog.csdnimg.cn/blog_migrate/a404d52346380a26a27f90c999a9b8b5.png) > > 指针变量 > > 我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个 变量就是指针变量。
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int a = 10;//在内存中开辟一块空间
int* p = &a;//这里我们对变量a,取出它的地址,可以使用&操作符。
//a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量
中,p就是一个之指针变量。
return 0;
}
> > **总结:** > > **指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。** > > 那这里的问题是: > > + 一个小的单元到底是多大?(1个字节) > + 如何编址? > > 经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。 > > 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0); > > 那么32根地址线产生的地址就会是: > > 00000000 00000000 00000000 00000000 > > 00000000 00000000 00000000 00000001 > > ... > > 11111111 11111111 11111111 11111111 > > 这里就有2的32次方个地址。 > > 每个地址标识一个字节,那我们就可以给 (2^32Byte == 2^32/1024KB == 2^32/1024/1024MB== 2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。 > > 同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算,计算方法如上所示,此处不再计算。 > > 这里我们就明白: 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地 址。 > > **总结: 指针是用来存放地址的,地址是唯一标示一块地址空间的。 指针的大小在32位平台是4个字节,在64位平台是8个字节。** ## 2\. 指针和指针类型 > 这里我们在讨论一下:指针的类型 我们都知道,变量有不同的类型,整形,浮点型等。 > > 那指针有没有类型呢? 准确的说:有的。 当有这样的代码:
1
2
int num = 10;
p = &num;
> 要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢? 我们给指针变量相应的类型。
1
2
3
4
5
6
char  *pc = NULL;
int *pi = NULL;
short *ps = NULL;
long *pl = NULL;
float *pf = NULL;
double *pd = NULL;
> 这里可以看到,指针的定义方式是: type + \* 。 > > 其实: char\* 类型的指针是为了存放 char 类型变量的地址。 > > short\* 类型的指针是为了存放 short 类型变量的地址。 > > int\* 类型的指针是为了存放 int 类型变量的地址。 那指针类型的意义是什么?下面我们会进行讲解! ### 2.1 指针+-整数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
int n = 10;
char *pc = (char*)&n;
int *pi = &n;

printf("%p\n", &n);
printf("%p\n", pc);
printf("%p\n", pc+1);
printf("%p\n", pi);
printf("%p\n", pi+1);
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/2ff42f7ccc423a156eca303eb0409e72.png) > > **总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。** ### 2.2 指针的解引用
1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
int n = 0x11223344;
char* pc = (char*)&n;
int* pi = &n;
*pc = 0; //重点在调试的过程中观察内存的变化。
*pi = 0; //重点在调试的过程中观察内存的变化。
return 0;
}
> 下图为执行完\*pc = 0之后n的内存空间 > > ![](https://i-blog.csdnimg.cn/blog_migrate/0a35977692999336e611fd6675c43d61.png) > > 由图可以看出,我们执行完该语句后,只改变了一个字节的空间所对应的值! > >  下图为执行完\*pi = 0之后的内存空间,与我们定义的指针类型一致,因为我们定义的pc是char类型的指针变量,char只占据四个字节的内存空间。 > > ![](https://i-blog.csdnimg.cn/blog_migrate/444dde6fd8d87e5c4db1ad52ae2231b4.png) > >  由图可以看出,我们这次是改变了四个字节的空间所对应的值,与我们定义的指针类型所一致,因为pi是int 类型的指针变量,int在内存中占据四个字节的空间。 > > **总结:** > >  指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 > > 比如: char\* 的指针解引用就只能访问一个字节,而 int\* 的指针的解引用就能访问四个字节。 ## 3\. [野指针](https://so.csdn.net/so/search?q=%E9%87%8E%E6%8C%87%E9%92%88&spm=1001.2101.3001.7020) > 概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的) ### 3.1 野指针成因 > 1\. 指针未初始化
1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int* p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;
}
> 2\. 指针越界访问
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
int* p = arr;
int i = 0;
for (i = 0; i <= 11; i++)
{
//当指针指向的范围超出数组arr的范围时,p就是野指针
*(p++) = i;
}
return 0;
}
> 3\. 指针指向的空间释放(后续会讲,此处暂时不讲) ### 3.2 如何规避野指针 > + 1\. 指针初始化 > + 2\. 小心指针越界 > + 3\. 指针指向空间释放即使置NULL > + 4\. 避免返回局部变量的地址 > + 5\. 指针使用之前检查有效性
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
int* p = NULL;
//....
int a = 10;
p = &a;
if (p != NULL)
{
*p = 20;
}
return 0;
}
## 4\. 指针运算 ### 4.1 指针+-整数
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
int main()
{
int x = 0;
int* p = &x;
printf("%p\n", p);
printf("%p\n", p + 1);
char* p2 = (char*)&x;
printf("%p\n", p2);
printf("%p\n", p2+1);
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/41f57c966bde07aaca29bdf60a345180.png) >  指针+-整数实际上是跨越指针所指变量类型的字节数乘以我们加的数字,比如在上面的例子中,我们一开始是将整型指针p进行了+1操作,然后我们对p+1进行以地址形式打印后,相比原来的p,增加了4个字节,同理,后面的p2是char类型的指针,我们对其进行+1操作后,相比原来的p2的地址,增加了1个字节。 ### 4.2 指针-指针
1
2
3
4
5
6
7
8
9
#include<stdio.h>
int main()
{
int arr[5] = { 1,2,3,4,5 };
int* p1 = &arr[4];
int* p2 = &arr[0];
printf("%d", p1 - p2);
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/cfaeda390c9381fc82a722bd586127dc.png) > 上面进行相减后得出的结果是4,为什么会得出4这个结果呢?因为p1和p2之间相差4个整型元素,这个地方为什么我们要强调是整形元素呢?因为我们在运算符两侧的指针类型均是整型指针,实际上**指针类型的变量进行相减是指针所代表的地址进行相减,将得出的值除以指针所指向的空间所占据的字节数**,这样将大家不是很容易理解,有一点点抽象,我给大家简单举一下例子,比如在上面这个例子中p1中所存储的地址值是16,而p2中所存储的值是0,那么我们将p2减去p1后得出的16除以p1和p2所指向的数据类型即整型类型,每个整型元素所占据的字节数为4,所以16除以4之后的结果为4,即最终在屏幕上的输出结果为4,那么很多小伙伴就问了,在上面这个例子中,我们将两个指针的类型进行强制转换为char类型后得出的结果是不是就会改变为16了呢,因为根据上面的我给出的推理方式确实应该是这样的,因为char类型占据的字节数为1,16除以1之后的结果仍为16,接下来我们代码展示一下是不是就像我们的推理一样,得出的结果是16. > ![](https://i-blog.csdnimg.cn/blog_migrate/d9b7325a7e664ae398d648ded2c8895b.png) > >  同我们的推导一样,这就印证了我们的推导是正确的! ### 4.3 指针的关系运算 > 标准规定: > > **允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与 指向第一个元素之前的那个内存位置的指针进行比较。** ## 5\. 指针和数组 > 我们看一个例子:
1
2
3
4
5
6
7
8
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
printf("%p\n", arr);
printf("%p\n", &arr[0]);
return 0;
}
> 运行结果: ![](https://i-blog.csdnimg.cn/blog_migrate/ad9cd7ed915222921f25bee38988828d.png) > 可见数组名和数组首元素的地址是一样的。 > > 结论:**数组名表示的是数组首元素的地址。** > > 那么这样写代码是可行的:
1
2
int arr[10] = {1,2,3,4,5,6,7,8,9,0};
int *p = arr;//p存放的是数组首元素的地址
> 既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。 > 例如:
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <====> p+%d = %p\n", i, &arr[i], i, p + i);
}
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/665db33fbaa24ee1e6c97c775aab90c8.png) > 所以 p+i 其实计算的是数组 arr 下标为i的地址。 > > 那我们就可以直接通过指针来访问数组。 > > 如下:
1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
int* p = arr; //指针存放数组首元素的地址
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
> ![](https://i-blog.csdnimg.cn/blog_migrate/86208c37d0e9b51060d8a162064fff41.png) ##  6. 二级指针 > 指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里? 这就是二级指针 。 > > ![](https://i-blog.csdnimg.cn/blog_migrate/ba49157f2c73033bb26fb7bccef8b9dd.png) > 对于二级指针的运算有: > + \*ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , \*ppa 其实访问的就是 pa .
1
2
int b = 20;
*ppa = &b;//等价于 pa = &b;
> + \*\*ppa 先通过 \*ppa 找到 pa ,然后对 pa 进行解引用操作: \*pa ,那找到的是 a .
1
2
3
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
## 7\. 指针数组 > 指针数组是指针还是数组? > > 答案:是数组。是存放指针的数组。 > > 数组我们已经知道整形数组,字符数组。
1
2
int arr1[5];
char arr2[6];
> > ![](https://i-blog.csdnimg.cn/blog_migrate/b1a7685b7c2c0b7c65a32ac722c3e24a.png) > >  那指针数组是怎样的?
1
int* arr3[5];//是什么?
> arr3是一个数组,有五个元素,每个元素是一个整形指针。 > > ![](https://i-blog.csdnimg.cn/blog_migrate/271ad2d29fee12771577b7595497d338.png) ## 14\. 结构体 > 结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型。 比如描述学生,学生包含: 名字+年龄+性别+学号 这几项信息。 这里只能使用结构体来描述了。 > > 例如:
1
2
3
4
5
6
7
struct Stu
{
char name[20];//名字
int age; //年龄
char sex[5]; //性别
char id[15]; //学号
};
> 结构体的初始化:
1
2
3
4
5
6
7
8
//打印结构体信息
struct Stu s = { "张三"20"男""20180101" };
//.为结构成员访问操作符
printf("name = %s age = %d sex = %s id = %s\n", s.name, s.age, s.sex, s.id);
//->操作符
struct Stu* ps = &s;
printf("name = %s age = %d sex = %s id = %s\n", ps->name, ps->age, ps->sex, ps -
> id);
![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315222942.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315223655.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315223753.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315224127.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315224330.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315225116.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315224832.png) ![image.png](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/20250315225436.png) ## 1\. 结构体的声明 ### 1.1 什么是结构体 > + 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。 ### 1.2 结构体的声明
1
2
3
4
struct tag
{
member - list;
}variable - list;
> 例如描述一个学生:
1
2
3
4
5
6
7
typedef struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}Stu;//分号不能丢
### 1.3 结构成员的类型 > 结构的成员可以是标量、数组、指针,甚至是其他结构体。 ### 1.4 [结构体变量](https://so.csdn.net/so/search?q=%E7%BB%93%E6%9E%84%E4%BD%93%E5%8F%98%E9%87%8F&spm=1001.2101.3001.7020)的定义和初始化 > 有了结构体类型,那如何定义变量,其实很简单。
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
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1

struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };

struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化

struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化

struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化
## 2\. 结构体成员的访问 > + 结构体变量访问成员 > > 结构变量的成员是通过点操作符(.)访问的。点操作符接受两个操作数。 例如:
1
2
3
4
5
6
7
struct Stu
{
char name[20];
int age;
};

struct Stu s;
![](https://smith-1315833455.cos.ap-beijing.myqcloud.com/blog/d3f892e27793ee307163bbec4819d83f.png) > 我们可以看到 s 有成员 name 和 age  > > 那我们如何访问s的成员?
1
2
3
struct S s;
strcpy(s.name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
> +  结构体指针访问指向变量的成员 > > 有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针。 > > 那该如何访问成员。 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Stu
{
char name[20];
int age;
};
void print(struct Stu* ps)
{
printf("name = %s age = %d\n", (*ps).name, (*ps).age);
//使用结构体指针访问指向对象的成员
printf("name = %s age = %d\n", ps->name, ps->age);
}
int main()
{
struct Stu s = { "zhangsan", 20 };
print(&s);//结构体地址传参
return 0;
}
## 3\. 结构体传参 > 直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct S
{
int data[1000];
int num;
};
struct S s = { {1,2,3,4}, 1000 };
//结构体传参
void print1(struct S s)
{
printf("%d\n", s.num);
}
//结构体地址传参
void print2(struct S* ps)
{
printf("%d\n", ps->num);
}
int main()
{
print1(s); //传结构体
print2(&s); //传地址
return 0;
}
> 上面的 print1 和 print2 函数哪个好些? > > 答案是:首选print2函数。 > > 原因: > > + 函数传参的时候,参数是需要压栈的。 > + 如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的 下降。 > > 结论: > > **结构体传参的时候,要传结构体的地址。** ## 1\. 结构体 ### 1.1 结构体的基础知识 > 结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。 ### 1.2 结构体的声明 例如:
1
2
3
4
struct tag//tag:结构体类型名
{
member - list;//member - list:成员列表
}variable - list;//variable - list:变量列表
### 1.3 特殊的声明 在声明结构的时候,可以不完全的声明。 比如:
1
2
3
4
5
6
7
struct Stu
{
char name[20];//名字
int age;//年龄
char sex[5];//性别
char id[20];//学号
}; //分号不能丢
> **匿名结构体类型变量在创建的时候只能放在{ }的后面,而不能单独进行创建,因为不知道结构体类型的名字,所以不能单独创建。**
1
2
3
4
5
6
7
8
9
10
11
12
13
//匿名结构体类型
struct
{
int a;
char b;
float c;
}x;
struct
{
int a;
char b;
float c;
}a[20], * p;
注意: > **编译器会把上面的两个声明当成完全不同的两个类型,所以是非法的!** ### 1.4 结构的自引用 > 结构体中包含自身类型的指针成员变量。 > > 举例:
1
2
3
4
5
struct Node
{
int data;
struct Node* next;
};
### 1.5 [结构体变量](https://so.csdn.net/so/search?q=%E7%BB%93%E6%9E%84%E4%BD%93%E5%8F%98%E9%87%8F&spm=1001.2101.3001.7020)的定义和初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2; //定义结构体变量p2

//初始化:定义变量的同时赋初值。
struct Point p3 = { x, y };
struct Stu //类型声明
{
char name[15];//名字
int age; //年龄
};
struct Stu s = { "zhangsan", 20 };//初始化
struct Node
{
int data;
struct Point p;
struct Node* next;
}n1 = { 10, {4,5}, NULL }; //结构体嵌套初始化
struct Node n2 = { 20, {5, 6}, NULL };//结构体嵌套初始化
### 1.6 [结构体内存对齐](https://so.csdn.net/so/search?q=%E7%BB%93%E6%9E%84%E4%BD%93%E5%86%85%E5%AD%98%E5%AF%B9%E9%BD%90&spm=1001.2101.3001.7020)
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>
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
struct S3
{
double d;
char c;
int i;
};
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%d %d %d %d", sizeof(struct S1), sizeof(struct S2), sizeof(struct S3), sizeof(struct S4));
return 0;
}
结构体的对齐规则: > **1\. 第一个成员在与结构体变量偏移量为0的地址处。** > > **2\. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8 GCC没有默认对齐数(成员的大小就是对齐数)** > > **3\. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。** > > **4\. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整 体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。** #### 练习1 > ![](https://i-blog.csdnimg.cn/blog_migrate/576dae62353d3d011921de1470599a7c.png) 解析: > 首先char c1在与结构体偏移量为0的地址处,在上图中就是首地址处。 > > 存放int a时要考虑对齐数,int所占字节数为4,编译器默认的对齐数为8,4更小,所以对齐数应为4,所以a存储时要从第5个空间位置处开始存放,占据4个字节的空间。 > > 存放char c2时还要考虑对齐数,char所占的字节数为1,与8相比,1更小,所以对齐数为1,所以char在a的后面紧跟着存放,此时一共占据了9个字节的空间。 > > 结构体总大小为最大对齐数的整数倍,上面的最大对齐数的为4,所以结构体所占据的内存空间的字节数为4的整数倍,前面已经占据了9个字节,下一个4的整数倍的数为12,所以这个结构体占据的空间的字节数为12。 #### 练习2 > > ![](https://i-blog.csdnimg.cn/blog_migrate/0a6da2cc2a0a600859d8a39d9722da3d.png) > > 所以一个结构体S2所占的内存空间的字节数为8。 #### 练习3 >  ![](https://i-blog.csdnimg.cn/blog_migrate/848715bed64095aa317c1f94d9caa9f3.png) > > 所以一个结构体S3所占的内存空间的字节数为16。 #### 练习4 > ![](https://i-blog.csdnimg.cn/blog_migrate/519f50aeefab7f7b056a1dd07c4e4393.png) > > 所以一个结构体S4所占内存空间的字节数为32 运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/3909fa1f666f23f6155ceb4fc4fe3ae2.png) #### 为什么存在内存对齐? > **1\. 平台原因(移植原因): 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特 定类型的数据,否则抛出硬件异常。** > > **2\. 性能原因: 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。 原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访 问。** > > **总体来说: 结构体的内存对齐是拿空间来换取时间的做法。** #### 结论 > 那在设计结构体的时候,我们既要满足对齐,又要节省空间,如何做到: 让占用空间小的成员尽量集中在一起。
1
2
3
4
5
6
7
8
9
10
11
12
13
//例如:
struct S1
{
char c1;
int i;
char c2;
};
struct S2
{
char c1;
char c2;
int i;
};
>  S1和S2类型的成员一模一样,但是S1和S2所占空间的大小有了一些区别。 ### 1.7 修改默认对齐数
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>
#pragma pack(8)//设置默认对齐数为8
struct S1
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
#pragma pack(1)//设置默认对齐数为1
struct S2
{
char c1;
int i;
char c2;
};
#pragma pack()//取消设置的默认对齐数,还原为默认
int main()
{
//输出的结果是什么?
printf("%d\n", sizeof(struct S1));
printf("%d\n", sizeof(struct S2));
return 0;
}
> **结论: 结构在对齐方式不合适的时候,我么可以自己更改默认对齐数。** #### 计算偏移量的函数----offsetof() > 头文件 > > ![](https://i-blog.csdnimg.cn/blog_migrate/96539c218a62d0e056fc256d4835966f.png) #### 如何理解偏移量呢?(举个例子)  > ![](https://i-blog.csdnimg.cn/blog_migrate/2003e43ea3082302be9a596a3d0f8865.png) > > c1的偏移量为0,c2的偏移量为1,i的偏移量为4。 下面是代码举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stddef.h>
struct S
{
char c1;
char c2;
int i;
};
int main()
{
printf("%d ", offsetof(struct S, c1));
printf("%d ", offsetof(struct S, c2));
printf("%d ", offsetof(struct S, i));
return 0;
}
 运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/d6e2d6c5cc658c117e2d182447becfe9.png) ### 1.8 结构体传参 > **结构体传参的时候最好传结构体指针,为什么?因为结构体大小一般比指针大小大得多,而且传指针能够在函数内进行修改,当然,如果不需要进行修改,也可对参数使用const进行修饰。** ## 3\. 枚举 ### 3.1 [枚举类型](https://so.csdn.net/so/search?q=%E6%9E%9A%E4%B8%BE%E7%B1%BB%E5%9E%8B&spm=1001.2101.3001.7020)的定义
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
enum Day//星期
{
Mon,
Tues,
Wed,
};
int main()
{
printf("%d %d %d", Mon, Tues, Wed);
return 0;
}
运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/1acc4da58a25a79a9b2e516a2510b2d6.png) > { }中的内容是枚举类型的可能取值,也叫 枚举常量 。 > > **这些可能取值都是有值的,默认从0开始,一次递增1,当然在定义的时候也可以赋初值。** 例如:
1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>
enum Color//颜色
{
RED = 1,
GREEN,
BLUE = 4
};
int main()
{
printf("%d %d %d",RED,GREEN,BLUE);
return 0;
}
运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/8edd1d8c296ae5ccaf6d63a27db65d47.png) ###  3.2 枚举的优点 > **1\. 增加代码的可读性和可维护性** > **2\. 和#define定义的标识符比较枚举有类型检查,更加严谨。** > **3\. 防止了命名污染(封装)** > **4\. 便于调试** > **5\. 使用方便,一次可以定义多个常量** ### 3.3 枚举的使用
1
2
3
4
5
6
7
8
enum Color//颜色
{
RED = 1,
GREEN = 2,
BLUE = 4
};
enum Color clr = GREEN;//只能拿枚举常量给枚举变量赋值,才不会出现类型的差异。
clr = 5;//ok?这样是不可以的,因为左右类型不一致
### 3.4 枚举类型的大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
enum sex
{
MALE,
FEMALE,
SECRET
};
int main()
{
enum sex s = FEMALE;
printf("%d\n", sizeof(enum sex));
printf("%d\n", sizeof(s));
return 0;
}
运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/fdd7659ab497cda3f3b691a57ffc4d3e.png) > **结论:无论是枚举类型的大小,还是枚举类型的变量的大小,在内存中都是占据4个字节。** ## 4\. 联合([共用体](https://so.csdn.net/so/search?q=%E5%85%B1%E7%94%A8%E4%BD%93&spm=1001.2101.3001.7020)) ### 4.1 联合类型的定义 > **联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。** 比如:
1
2
3
4
5
6
7
8
9
10
11
12
13
//联合类型的声明
union Un
{
char c;
int i;
};
//联合变量的定义
union Un un;
//计算连个变量的大小
printf("%d\n", sizeof(un));
printf("%p\n",&un);
printf("%p\n",&un.c);
printf("%p\n",&un.i);
运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/e27c75a0721747d1224bdb6fc7433662.png) ###  4.2 联合的特点 #### 4.2.1 特点阐述 > 联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联 合至少得有能力保存最大的那个成员)。 #### 4.2.2 特点应用----判断[大小端](https://so.csdn.net/so/search?q=%E5%A4%A7%E5%B0%8F%E7%AB%AF&spm=1001.2101.3001.7020) 方法一:运用联合的方式
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
#include<stdio.h>
union S
{
int i;
char ch;
};

int check_sys()
{
union S s;
s.i = 1;
return s.ch;
}
int main()
{
union S s;
s.i = 1;
int ret = check_sys();
if (1 == ret)
{
printf("小端存储\n");
}
else
{
printf("大端存储\n");
}
return 0;
}
方法二:运用非联合的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check_sys();
if (1 == ret)
{
printf("小端存储\n");
}
else
{
printf("大端存储\n");
}
return 0;
}
### 4.3 联合大小的计算 > **(1)联合的大小至少是最大成员的大小。** > > **(2)当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。** > > **简而言之就是大于成员变量最大空间数,且是最大对齐数的整数倍。** 比如:
1
2
3
4
5
6
union un
{
int a;//对齐数为4,默认对齐数为8,取较小值,即4
char arr[5];//最大成员的大小为5,对齐数为1(拿char来算,而不是拿整个数组所占的内存空间来算),默认对齐数为8,取较小值1
//所以联合体的最大对齐数为4,所以union un的内存空间的大小必须是4的倍数,同时还必须大于5,所以空间大小为8个字节
};
## 1.1 什么是位段 位段的声明和结构是类似的,有两个不同: > **1.位段的成员必须是 int、unsigned int 或signed int(当然,位段也可以是char类型,但是如果一个是char类型,其它就都得是char类型) 。** > > **2.位段的成员名后边有一个冒号和一个数字。** 比如:
1
2
3
4
5
6
7
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
> A就是一个位段类型。 那位段A的大小是多少? > > 上面的位段A输出结果为**8**! **注意:位段中的位是二进制位!** ## 1.2 位段的[内存分配](https://so.csdn.net/so/search?q=%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D&spm=1001.2101.3001.7020) > **注意:像上面的\_a后面的数字最大只能是32,不能超过32,因为我们在进行开辟空间时是以4个字节位单位进行开辟的。** > > **1\. 位段的成员可以是 int unsigned int signed int 或者是 char (属于整形家族)类型** > > **2\. 位段的空间上是按照需要以4个字节( int )或者1个字节( char )的方式来开辟的。** > > **3\. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。** 解释struct A为什么占据8个内存空间? > **首先开辟一个整型内存空间。一共可以放32个比特位,\_a,\_b,\_c总共占据17个比特位,此时这个整型空间只剩15个比特位了,无法将\_d的30个比特位存放进去,此时就要把刚才剩下的15个比特位浪费掉,重新开辟一个整型的内存空间,存放\_d中的30个比特位,此时一共占据了2个整形空间,即8个字节!** 举例说明在内存中的详细布局:
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>
struct S
{
char a : 3;
char b : 4;
char c : 5;
char d : 4;
};
int main()
{
struct S s = { 0 };//下面的数据从左向右依次是原数据----二进制位----在内存中存储的数据
s.a = 10;//10----1010----010
s.b = 20;//20----10010----0010
s.c = 3;//3----11----00011
s.d = 4;//4----100----0100
//下面是在内存中真正存储的数据
//在VS中默认是从右向左存的,但是其它平台不确定,所以位段不具有较好的移植性
//第一个char型空间:0 0010 010----2 2(16进制的形式)
//第二个char型空间:000 00011(c)----0 3(16进制的形式)
//第三个char型空间:0000 0100(d)----0 4(16进制的形式)
//所以在内存中的布局应为22 03 04
return 0;
}
运行截图: > ![](https://i-blog.csdnimg.cn/blog_migrate/fa9f4e1a86451557c0aa5849622b1135.png) ## 1.3 位段的跨平台问题 > **1\. int 位段被当成有符号数还是无符号数是不确定的。** > > **2\. 位段中最大位的数目不能确定。(16位机器最大16,32位机器最大32,写成27,在16位机 器会出问题。** > > **3\. 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义。** > > **4\. 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是 舍弃剩余的位还是利用,这是不确定的。** 总结: > **跟结构相比,位段可以达到同样的效果,但是可以很好的节省空间,但是有跨平台的问题存在。** ## 1、需求分析 > 需求:通过C语言实现三子棋小游戏。 > > 注:由于作者水平有限,只能将计算机下棋设定为随机下棋,所以看起来略显呆笨,并且程序中也没有采用图像界面的方式,敬请谅解! ## 2、程序[架构](https://so.csdn.net/so/search?q=%E6%9E%B6%E6%9E%84&spm=1001.2101.3001.7020) > 程序分为test.c、game.c两个源文件和game.h一个头文件。 > > test.c:主函数接口引入。 > > game.c:游戏的相关函数实现。 > > game.h:头文件引入、函数声明。 ## 3、代码实现(分函数呈现) ### (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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:->");
scanf("%d", &input);
system("cls");
switch (input)
{
case 1:
game();
break;
case 0:
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:->");
scanf("%d", &input);
system("cls");
switch (input)
{
case 1:
game();
break;
case 0:
printf("已退出!\n");
break;
default:
break;
}
} while (input);
return 0;
}
break;
default:
break;
}
} while (input);
return 0;
}
#### 分析: > 1.主函数主要是引入了随机种子(后续计算机下棋时会用到),并且通过switch()提供了游戏的接口,让玩家可以通过输入1开始游戏或者通过输入0结束游戏。 > > 2.为了让用户能够多次进行游戏,采用了do while死循环的方式,同时,采用while语句的另一个原因就是可以利用该语句程序执行语句比条件表达式执行多一次的特点,来让玩家无论是否玩游戏,打开后先看到的是菜单。 #### [异常处理](https://so.csdn.net/so/search?q=%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86&spm=1001.2101.3001.7020): > 在主函数中对于用户输入非0和非1的数字采用了重新进入循环的方式。 ### (2)菜单函数的实现
1
2
3
4
5
6
7
void menu()
{
printf("*************************\n");
printf("******* 1.play ********\n");
printf("******* 0.exit ********\n");
printf("*************************\n");
}
#### 分析: > 利用简单的printf语句进行输出即可将菜单呈现给玩家。(注意不要忘记'\\n\\进行换行操作) ### (3)游戏函数的实现
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
//游戏函数
void game()
{
char board[ROW][COL] = { 0 };
Init_board(board, ROW, COL);//棋盘初始化
Display_board(board, ROW, COL);//棋盘展示
while (1)
{
player_board(board, ROW, COL);//玩家下棋
Display_board(board, ROW, COL);//棋盘展示
printf("\n");
if (is_end(board, ROW, COL) != 'c')
{
is_win(is_end(board, ROW, COL));
break;
}
computer_board(board, ROW, COL);//电脑下棋
Display_board(board, ROW, COL);//棋盘展示
printf("\n");
if (is_end(board, ROW, COL) != 'c')
{
is_win(is_end(board, ROW, COL));
break;
}
}
}
#### 分析: > 1、先定义并初始化存放数据的棋盘(利用二维数组实现) > > 2、初始化棋盘并展示棋盘 > > 3、玩家下棋,然后电脑下棋,只要有一方获胜就停止游戏,同时在任何一方下完棋后都进行棋盘的呈现。 ### (4)棋盘的初始化
1
2
3
4
5
6
7
8
9
10
11
12
void Init_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
#### 分析: > 利用两个for循环将二维数组的每个元素赋值为空格即可。 ### (5)棋盘展示代码实现
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
void Display_board(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (j < col - 1)
printf(" %c |", board[i][j]);
else
printf(" %c \n", board[i][j]);
}
if (i < row-1 )
{
for (int j = 0; j < col; j++)
{
if (j < col - 1)
printf("___|");
else
printf("___\n");
}
}
else
{
for (int j = 0; j < col; j++)
{
if (j < col - 1)
printf(" |");
else
printf(" \n");
}
}
}
}
#### 分析: > 大家先看成品代码: > > ![](https://i-blog.csdnimg.cn/blog_migrate/00a242a2ac8693101a48db628ebc7941.png) > >  具体思路:首先这个地方需要用到双层for循环这是毋庸置疑的,但我们在打印的时候一定要注意,我们在打印一行的每个的时候,前面的元素即前两个形式为   (空格)棋盘元素(空格)(竖线),最后面即第三个元素为(空格)棋盘元素(空格)(换行符),当我们打印完一行带有棋盘的元素后,我们需要打印出分割线,分割线也是通过for循环打印的,思路同上,前两个元素为(下划线)(下划线)(下划线)(竖线),第三个即最后一个元素为(下划线)(下划线)(下划线)(换行符)。 > > 另外需要注意的是最后一行即原本应该打印分割线的位置不需要分割线,所以打印方式为:前两个元素为(空格)(空格)(空格)(竖线)(换行符),与前面相比,少了下划线。 > > 总结来说,就是注意前两个元素或者前两行与最后一个元素或者最后一行的区别即可,此处一样是通过for循环实现。 ### (6)玩家下棋
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
void player_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
while (1)
{
int i = 0;
int j = 0;
printf("请输入下棋位置->");
scanf("%d %d", &i, &j);
if (i <= row && i >= 1 && j <= col && j >= 1)
{
if (board[i-1][j-1] == ' ')
{
board[i-1][j-1] = '*';
break;
}
else
{
printf("坐标已有棋,清重新输入!\n");
}
}
else
{
printf("坐标非法,清重新输入!\n");
}
}
}
#### 分析: > 这个函数中主要是通过对想要下棋的位置判断该位置处有无元素即可,如果没有元素,即可正常下棋。 #### 异常处理: > 对输入的棋盘位置数据进行筛选后对于不符合要求的数据或者输入的坐标位置处有棋都会要求玩家重新输入。 ### (7)电脑下棋
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void computer_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
while (1)
{
i = rand() % 3;
j = rand() % 3;
if (board[i][j] ==' ')
{
board[i][j] = '#';
break;
}
else
continue;
}
}
#### 分析: > 此处利用随机种子生成随机数即可,只要生成的随机的位置处为空白就正常下棋,反之重新生成随机数直到为空白处下棋后停止。 ### (8)判断是否结束
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
//判断是否结束
char is_end(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
//行相同
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] &&board[i][1]== board[i][2] && board[i][0] != ' ')
return board[i][0];
}

//列相同
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] &&board[1][i]== board[2][i] && board[0][i] != ' ')
return board[0][i];
}
//’\'对角线
if (board[0][0] == board[1][1] && board[1][1]== board[2][2] && board[0][0] != ' ')
return board[0][0];
//'/'对角线
if (board[2][0] == board[1][1] == board[0][2] && board[2][0] != ' ')
return board[2][0];
//未结束
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 'c';
}
}
//平局
return '-';
}
#### 分析: > 当棋盘并未填满并且玩家与电脑均为未胜利时返回字符'c'说明可以继续,同时根据获胜的要求返回不同的值,当返回字符'\*'时玩家获胜,返回'#'说明电脑获胜,返回字符'-'说明平局,返回字符'c'说明可以不符合上面的任意一种条件,可以正常继续。 ### (9)判断谁获胜
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//判断谁获胜
void is_win(char flag)
{
if (flag == '*')
{
printf("玩家获胜!\n");
system("pause");
system("cls");
}
else if (flag == '#')
{
printf("电脑获胜!\n");
system("pause");
system("cls");
}
else if (flag == '-')
{
printf("平局!\n");
system("pause");
system("cls");
}
}
#### 分析: > 根据上面判断是否结束的返回时来进行判断并且输出即可。 ### (10)特别说明 > 1、代码中使用的system("cls")是清屏命令。system("pause")是使屏幕暂停的命令,按任意键后可以使程序正常进行。这两者均需要引windows.h头文件。 > > 2、生成随机数的函数rand需要引stdlib.h头文件,使用时间戳time()需要引time.h头文件。 ## 4、代码实现(总代码) ### test.c代码
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
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:->");
scanf("%d", &input);
system("cls");
switch (input)
{
case 1:
game();
break;
case 0:
break;
default:
printf("已退出!\n");
break;
}
} while (input);
return 0;
}
### game.c代码
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//菜单实现
void menu()
{
printf("*************************\n");
printf("******* 1.play ********\n");
printf("******* 0.exit ********\n");
printf("*************************\n");
}
//游戏函数
void game()
{
char board[ROW][COL] = { 0 };
Init_board(board, ROW, COL);//棋盘初始化
Display_board(board, ROW, COL);//棋盘展示
while (1)
{
player_board(board, ROW, COL);//玩家下棋
Display_board(board, ROW, COL);//棋盘展示
printf("\n");
if (is_end(board, ROW, COL) != 'c')
{
is_win(is_end(board, ROW, COL));
break;
}
computer_board(board, ROW, COL);//电脑下棋
Display_board(board, ROW, COL);//棋盘展示
printf("\n");
if (is_end(board, ROW, COL) != 'c')
{
is_win(is_end(board, ROW, COL));
break;
}
}
}
//棋盘初始化
void Init_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = ' ';
}
}
}
//棋盘展示
void Display_board(char board[ROW][COL], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
if (j < col - 1)
printf(" %c |", board[i][j]);
else
printf(" %c \n", board[i][j]);
}
if (i < row-1 )
{
for (int j = 0; j < col; j++)
{
if (j < col - 1)
printf("___|");
else
printf("___\n");
}
}
else
{
for (int j = 0; j < col; j++)
{
if (j < col - 1)
printf(" |");
else
printf(" \n");
}
}
}
}
//玩家下棋
void player_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
while (1)
{
int i = 0;
int j = 0;
printf("请输入下棋位置->");
scanf("%d %d", &i, &j);
if (i <= row && i >= 1 && j <= col && j >= 1)
{
if (board[i-1][j-1] == ' ')
{
board[i-1][j-1] = '*';
break;
}
else
{
printf("坐标已有棋,清重新输入!\n");
}
}
else
{
printf("坐标非法,清重新输入!\n");
}
}
}
//电脑下棋
void computer_board(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
while (1)
{
i = rand() % 3;
j = rand() % 3;
if (board[i][j] ==' ')
{
board[i][j] = '#';
break;
}
else
continue;
}

}

//判断是否结束
char is_end(char board[ROW][COL], int row, int col)
{
int i = 0;
int j = 0;
//行相同
for (i = 0; i < row; i++)
{
if (board[i][0] == board[i][1] &&board[i][1]== board[i][2] && board[i][0] != ' ')
return board[i][0];
}

//列相同
for (i = 0; i < col; i++)
{
if (board[0][i] == board[1][i] &&board[1][i]== board[2][i] && board[0][i] != ' ')
return board[0][i];
}
//’\'对角线
if (board[0][0] == board[1][1] && board[1][1]== board[2][2] && board[0][0] != ' ')
return board[0][0];
//'/'对角线
if (board[2][0] == board[1][1] == board[0][2] && board[2][0] != ' ')
return board[2][0];
//未结束
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
if (board[i][j] == ' ')
return 'c';
}
}
//平局
return '-';
}
//判断谁获胜
void is_win(char flag)
{
if (flag == '*')
{
printf("玩家获胜!\n");
system("pause");
system("cls");
}
else if (flag == '#')
{
printf("电脑获胜!\n");
system("pause");
system("cls");
}
else if (flag == '-')
{
printf("平局!\n");
system("pause");
system("cls");
}
}
### game.h代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>
#define ROW 3
#define COL 3
//菜单函数声明
void menu();
//游戏函数声明
void game();
//棋盘初始化函数声明
void Init_board(char board[ROW][COL], int row, int col);
//棋盘展示函数声明
void Display_board(char board[ROW][COL], int row, int col);
//玩家下棋函数声明
void player_board(char board[ROW][COL], int row, int col);
//电脑下棋函数声明
void computer_board(char board[ROW][COL],int row,int col);
//判断是否结束函数声明
char is_end(char board[ROW][COL], int row, int col);
//判断谁获胜函数声明
void is_win(char flag);
# # 超硬核---从汇编角度带你了解函数 ## 1、我们将要解决的问题 ![](https://i-blog.csdnimg.cn/blog_migrate/7d352e9191dac685458034dba26f37ab.jpeg) ## 2、[寄存器](https://so.csdn.net/so/search?q=%E5%AF%84%E5%AD%98%E5%99%A8&spm=1001.2101.3001.7020) > 首先给大家普及一下寄存器的类型,当然,并不详细讲解! > > 寄存器: > > eax ebx ecx edx > ebp esp这两个寄存器中存放的是地址,这两个地址是用来维护[栈帧](https://so.csdn.net/so/search?q=%E6%A0%88%E5%B8%A7&spm=1001.2101.3001.7020)的。 > > 那么这两个寄存器是如何来维护栈帧的呢? > > 每一个函数的调用,都要在栈区创建一块空间。 ## 3、源代码展示 > ![](https://i-blog.csdnimg.cn/blog_migrate/c52a8104b9a474ddc6e8b1801d19656e.jpeg) ## 4、函数的调用关系(看汇编代码) > 在vs2013中,main()函数也是要被调用的,被\_\_tmainCRTStartup调用,而这个函数,又被mainCRTStartup函数调用,关系如下图所示: > ![](https://i-blog.csdnimg.cn/blog_migrate/49353e8c0033938395552b284edf9935.jpeg) ## 5、正片开始(危) > ![](https://i-blog.csdnimg.cn/blog_migrate/0601692e0e4a5042f59b0fc89add12b9.jpeg) > 每当函数被调用的时候,ebp和esp两个寄存器就用来维护相应的栈帧空间,一个是栈底指针,一个是栈顶指针。 > ![](https://i-blog.csdnimg.cn/blog_migrate/a383dd0f4fcd31a6bd99ee6b3151eb13.png) > **注意:ecx中保存的39h是rep stos这条指令执行的次数。** > ![](https://i-blog.csdnimg.cn/blog_migrate/1389ce014a052a8234df560100157c3d.jpeg) > ![](https://i-blog.csdnimg.cn/blog_migrate/939be8d05255e66c4518b665b97401b2.jpeg) > ![](https://i-blog.csdnimg.cn/blog_migrate/ec8f024716ff8873c8c84f17fc1dd2e0.jpeg) > 当然,今天并没有完全讲完,后面还有一小部分没有讲出,不过相信带大家了解到这个地方后,后面的大家也能根据前面的讲解,大致推导出后面汇编代码的意思,事实上,对于栈帧的开辟,大同小异,好了,今天的分享就到这了,觉得对你有所帮助的话,点一个小小的赞吧!