Note/青空笔记/C语言程序设计笔记/C语言(二).md

73 KiB
Raw Blame History

C语言基础

前面我们已经搭建好了基本的学习环境现在就让我们开始C语言的学习吧

C语言的语法层面内容相比其他语言来说其实算少的了但是它的难点在于很多概念上的理解这也是为什么上一章一直在说一些计算机基础相关内容包括这一章还会继续补一点这样会有助于各位对于语言的理解C语言可以说是步入编程领域的分水岭跨过了这道坎后续其他编程语言的学习都会无比轻松。

学习编程的过程可能会很枯燥,但是请各位一定不要心急,一步一个脚印,相信大家一定能通关。

C程序基本格式

前面我们在创建项目之后自动生成了一个.c文件,这个就是我们编写的程序代码文件:

#include <stdio.h>

int main() {
    printf("Hello World!");
  	return 0;
}

操作系统需要执行我们的程序但是我们的程序中可能写了很多很多的代码那么肯定需要知道从哪里开始执行才可以也就是程序的入口所以我们需要提供一个入口点我们的C语言程序入口点就是main函数(不过现在还没有讲到函数,所以各位就理解为固定模式即可)它的写法是:

int main() {  //所有的符号一律采用英文的,别用中文
    程序代码...
}

注意是int后面空格跟上main(),我们的程序代码使用花括号{}进行囊括(有的人为了方便查阅,会把前半个花括号写在下面)

然后我们看到,如果我们需要打印一段话到控制台,那么就需要使用printf(内容)来完成,这其实就是一种函数调用,但是现在我们还没有接触到,我们注意到括号里面的内容就是我们要打印到控制台的内容:

printf("Hello World!");   //注意最后需要添加;来结束这一行,注意是英文的分号,不是中文的!

我们要打印的内容需要采用双引号进行囊括,被双引号囊括的这一端话,我们称为字符串,当然我们现在还没有学到,所以各位也是记固定模式就好,当我们需要向控制台打印一段话时,就要用双引号囊括这段话,然后放入printf即可。我们会在后续的学习中逐渐认识printf函数。

最顶上还有一句:

#include <stdio.h>

这个是引入系统库为我们提供的函数,包括printf在内所以我们以后编写一个C语言程序就按照固定模式

#include <stdio.h>

int main() {
    程序代码
}

除了程序代码部分我们会进行编写之外,其他的地方采用固定模式就好。

我们在写代码的过程中可以添加一些注释文本,这些文本内容在编译时自动忽略,所以比如我们想边写边记点笔记,就可以添加注释,注释的格式为:

#include <stdio.h>   //引入标准库头文件

int main() {   //主函数,程序的入口点
    printf("Hello World!");  //向控制台打印字符串
}

当然我们也可以添加多行注释:

#include <stdio.h>

/*
 * 这是由IDE自动生成的测试代码
 * 还是可以的
 */
int main() {
    printf("Hello World!");
  	//最后还有一句 return 0; 但是我们可以不用写,编译器会自动添加,所以后面讲到之后我们再来说说这玩意。
}

OK基本的一些内容就讲解完毕了。

基本数据类型

我们的程序离不开数据比如我们需要保存一个数字或是字母这时候这些东西就是作为数据进行保存不过不同的数据他们的类型可能不同比如1就是一个整数0.5就是一个小数A就是一个字符C语言提供了多种数据类型供我们使用我们就可以很轻松的使用这些数据了。

不同的数据类型占据的空间也会不同,这里我们需要先提一个概念,就是字、字节是什么?

我们知道计算机底层实际上只有0和1能够表示这时如果我们要存储一个数据比如十进制的3那么就需要使用2个二进制位来保存二进制格式为11占用两个位置再比如我们要表示十进制的15这时转换为二进制就是1111占用四个位置4个bit位来保存。一般占用8个bit位表示一个字节B2个字节等于1个字所以一个字表示16个bit位它们是计量单位。

我们常说的内存大小1G、2G等实际上就是按照下面的进制进行计算的

8 bit = 1 B 1024 B = 1KB1024 KB = 1 MB1024 MB = 1GB1024 GB = 1TB1024TB = 1PB基本上是1024一个大进位但是有些硬盘生产厂商是按照1000来计算的所以我们买电脑的硬盘容量可能是512G的但是实际容量可能会缩水

在不同位数的系统下基本数据类型的大小可能会不同因为现在主流已经是64位系统本教程统一按照64位系统进行讲解。

原码、反码和补码

原码

上面我们说了实际上所有的数字都是使用0和1这样的二进制数来进行表示的但是这样仅仅只能保存正数那么负数怎么办呢

比如现在一共有4个bit位来保存我们的数据为了表示正负我们可以让第一个bit位专门来保存符号这样我们这4个bit位能够表示的数据范围就是

  • 最小1111 => - (2^2+2^1+2^0) => -7
  • 最大0111 => + (2^2+2^1+2^0) => +7 => 7

虽然原码表示简单但是原码在做加减法的时候很麻烦以4bit位为例

1+(-1) = 0001 + 1001 = 怎么让计算机去计算虽然我们知道该去怎么算但是计算机不知道计算机顶多知道1+1需要进位

我们得创造一种更好的表示方式!于是我们引入了反码:

反码

正数的反码是其本身 负数的反码是在其原码的基础上, 符号位不变,其余各个位取反 经过上面的定义,我们再来进行加减法:

1+(-1) = 0001 + 1110 = 1111 => -0 (直接相加,这样就简单多了!)

思考1111代表-00000代表+0在我们实数的范围内0有正负之分吗

0既不是正数也不是负数那么显然这样的表示依然不够合理

补码

根据上面的问题,我们引入了最终的解决方案,那就是补码,定义如下:

正数的补码就是其本身 (不变!) 负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1. (即在反码的基础上+1) 其实现在就已经能够想通了,-0其实已经被消除了我们再来看上面的运算

1+(-1) = 0001 + 1111 = (1)0000 => +0 (现在无论你怎么算,也不会有-0了

所以现在4bit位能够表示的范围是-8~+7C使用的就是补码

整数类型

我们首先来看看整数类型,整数就是不包含小数点的数据,比如199666等数字,整数包含以下几种类型:

  • int - 占用 4 个字节32个bit位能够表示 -2,147,483,648 到 2,147,483,647 之间的数字,默认一般都是使用这种类型
  • long - 占用 8 个字节64个bit位。
  • short - 占用2个字节16个bit位。

浮点类型

浮点类一般用于保存小数不过为啥不叫小数类型而是浮点类型呢因为我们的一个小数分为整数部分和小数部分我们需要用一部分的bit位去表示整数部分而另一部分去表示小数部分至于整数部分和小数部分各自占多少并不是固定的而是浮动决定的在计算机组成原理中会深入学习这里就不多介绍了

  • float - 单精度浮点占用4个字节32个bit位。
  • double - 双精度浮点占用8个字节64个bit位。

字符类型

除了保存数字之外C语言还支持字符类型我们的每一个字符都可以使用字符类型来保存

  • char - 占用1个字节-128~127可以表示所有的ASCII码字符每一个数字对应的是编码表中的一个字符

image-20220603114358826

编码表中包含了所有我们常见的字符,包括运算符号、数字、大小写字母等(注意只有英文相关的,没有中文和其他语言字符,包括中文的标点符号也没有)

某些无法直接显示的字符(比如换行,换行也算一个字符)需要使用转义字符来进行表示:

img

有关基本类型的具体使用我们放到下一节进行讲解。


变量

前面我们了解了C语言中的基本类型那么我们如何使用呢这时我们就可以创建不同类型的变量了。

变量的使用

变量就像我们在数学中学习的xy一样,我们可以直接声明一个变量,并利用这些变量进行基本的运算,声明变量的格式为:

数据类型 变量名称 = 初始值;    //其中初始值可以不用在定义变量时设定
// = 是赋值操作,可以将等号后面的值赋值给前面的变量,等号后面可以直接写一个数字(常量)、变量名称、算式

比如我们现在想要声明一个整数类型的变量:

int a = 10;   //变量类型为int常用变量名称为a变量的初始值为10
int a = 10, b = 20;   //多个变量可以另起一行编写,也可以像这样用逗号隔开,注意类型必须是一样的

其中,变量的名称并不是随便什么都可以的,它有以下规则:

  • 不能重复使用其他变量使用过的名字。
  • 只能包含英文字母或是下划线、数字,并且严格区分大小写,比如aA不算同一个变量。
  • 虽然可以包含数字,但是不能以数字开头。
  • 不能是关键字(比如我们上面提到的所有基本数据类型,当然还有一些关键字我们会在后面认识)
  • (建议)使用英文单词,不要使用拼音,多个词可以使用驼峰命名法或是通过下划线连接。

初始值可以是一个常量数据比如直接写10、0.5这样的数字)也可以是其他变量,或是运算表达式的结果,这样会将其他变量的值作为初始值。

我们可以使用变量来做一些基本的运算:

#include <stdio.h>

int main() {
    int a = 10;  //将10作为a的值
    int b = 20;
    int c = a + b;   //注意变量一定要先声明再使用这里是计算a + b的结果算式并作为c的初始值
}

这里使用到了+运算符(之后我们还会介绍其他类型的运算符)这个运算符其实就是我们数学中学习的加法运算,会将左右两边的变量值加起来,得到结果,我们可以将运算结果作为其他变量的初始值,还是很好理解的。

但是现在虽然做了运算,我们还不知道运算的具体结果是什么,所以这里我们通过前面认识的printf函数来将结果打印到控制台:

#include <stdio.h>

int main() {
    int a = 10;
    int b = 20;
    int c = a + b;

    printf(c);   //直接打印变量c
}

但是我们发现这样似乎运行不出来结果,不对啊,前面你不是说把要打印到控制台的内容写到printf中吗,怎么这里不行呢?实际上printf是用于格式化打印的,我们来看看如何进行格式化打印,输出我们的变量值:

printf("c的结果是%d", );   //使用%d来代表一个整数类型的数据占位符在打印时会自动将c的值替换上去

我们来看看效果:

image-20220603131740600

这样,我们就知道该如何打印我们变量的值了,当然,除了使用%d打印有符号整数之外,还有其他的:

格式控制符 说明
%c 输出一个单一的字符
%hd、%d、%ld 以十进制、有符号的形式输出 short、int、long 类型的整数
%hu、%u、%lu 以十进制、无符号的形式输出 short、int、long 类型的整数
%ho、%o、%lo 以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数
%#ho、%#o、%#lo 以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数
%hx、%x、%lx %hX、%X、%lX 以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。
%#hx、%#x、%#lx %#hX、%#X、%#lX 以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。
%f、%lf 以十进制的形式输出 float、double 类型的小数
%e、%le %E、%lE 以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。
%g、%lg %G、%lG 以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。
%s 输出一个字符串

比如现在我们要进行小数的运算,还记得我们前面介绍的小数类型有哪些吗?

#include <stdio.h>

int main() {
    double a = 0.5;
    float b = 2.5f;   //注意直接写2.5默认表示的是一个double类型的值我们需要再后面加一个f或是F表示是flaot类型值

    printf("a + b的结果是%f", a + b);   //根据上表得到,小数类型需要使用%f表示这里我们可以直接将a + b放入其中
}

可以看到,结果也是正确的:

image-20220603132459810

当然,我们也可以一次性打印多个,只需要填写多个占位符表示即可:

#include <stdio.h>

int main() {
    double a = 0.5;
    float b = 2.5f;   //整数类型默认是int如果要表示为long类型的值也是需要在最后添加一个l或L

    printf("a = %f, b = %f", a, b);   //后面可以一直添加(逗号隔开),但是注意要和前面的占位符对应
}

结果也是正常的:

image-20220603132713970

我们再来看看字符类型:

char c = 'A';   //字符需要使用单引号囊括,且只能有一个字符,不能写成'AA',这就不是单个字符了
//注意这里的A代表的是A这个字符对应的ASCII码是65实际上c存储的是65这个数字

我们也可以通过格式化打印来查看它的值:

#include <stdio.h>

int main() {
    char c = 'A';
    printf("变量c的值为%c 对应的ASCII码为%d", c, c);   //这里我们使用%c来以字符形式输出%d输出的是变量数据的整数形式其实就是对应的ASCII码
}

image-20220603133727498

当然我们也可以直接让char存储一个数字ASCII码同样也可以打印出对应的字符

#include <stdio.h>

int main() {
    char c = 66;
    printf("变量c的值为%c 对应的ASCII码为%d", c, c);
}

image-20220603133858133

那么现在请各位小伙伴看看下面这段代码会输出什么:

#include <stdio.h>

int main() {
    int a = 10;
    char c = 'a';
    printf("变量c的ASCII码为%d", c);
}

没错,这里得到的结果就是字符a的ASCII码值注意千万不要认为c得到的是变量a的值这里使用的是字符a跟上面的变量a半毛钱关系都没有

image-20220603134234040

但是如果我们去掉引号就相当于把变量a的值给了cc现在的ASCII码就是10了所以这里一定要分清楚。

对于某些无法表示的字符,比如换行这类字符,我们没办法直接敲出来,只能使用转义字符进行表示:

char c = '\n';

详细的转义字符表参见前面的基本数据类型章节。

变量除了有初始值之外,也可以在后续的过程中得到新的值:

#include <stdio.h>

int main() {
    short s = 10;
    s = 20;    //重新赋值为20注意这里就不要再指定类型了指定类型只有在声明变量时才需要
    printf("%d", s);   //打印结果
}

可以看到,得到的是我们最后一次对变量修改的结果:

image-20220603135152184

那要是我们不对变量设定初始值呢?那么变量会不会有默认值:

#include <stdio.h>

int main() {
    int a, b, c, d;
    printf("%d,%d,%d,%d", a, b, c, d);
}

可以看到虽然定义变量但是我们没有为其设定初始值那么它的值就是不确定的了千万注意并不是不设定值默认就是0

image-20220603141341554

所以各位小伙伴以后在使用时一定要注意这个问题至于为什么不是0这是因为内存分配机制我们在下一章高级篇再进行讲解。

我们再来看一个例子:

#include <stdio.h>

int main() {
    char c = 127;    //已经到达c的最大值了
    c = c + 1;   //我不管,我就要再加
    printf("%d", c);    //这时会得到什么结果?
}

image-20220603143909688

怎么127加上1还变成-128了呢这是由于位数不够导致运算结果值溢出

  • 127 + 1= 01111111 + 1
  • 由于现在是二进制满2进1所以最后变成
  • 10000000 = 补码形式的 -128

所以,了解上面这些计算机底层原理是很重要的,我们能够很轻松地知道为什么会这样。

在我们的运算中,可能也会存在一些一成不变的值,比如π的值永远都是3.1415....,在我们的程序中,也可以使用这样不可变的变量,我们成为常量。

定义常量和变量比较类似,但是需要在前面添加一个const关键字,表示这是一个常量:

image-20220603140454728

可以看到,常量在一开始设定初始值后,后续是不允许进行修改的。

无符号数

我们知道,所有的数据底层都是采用二进制来进行保存的,而第一位则是用于保存符号位,但是如果我们不考虑这个符号位,那么所有的数都是按照正数来表示,比如考虑了符号位的char类型:

  • 考虑符号表示范围:-128~127
  • 不考虑符号0~255

我们也可以直接使用这些不带符号位的数据类型:

int main() {
    unsigned char c = -65;   //数据类型前面添加unsigned关键字表示采用无符号形式
    printf("%u", c);    //%u以无符号形式输出十进制数据
}

可以看到这里给了无符号char类型c一个-65的值但是现在很明显符号位也是作为数值的表示部分所以结果肯定不是-65

image-20220603142210120

结合我们前面学习的基础知识我们来看看为什么得到的是191这个数字。首先char类型占据一个字节8个bit位

  • 00000000 -> 现在赋值-65 -> -65的补码形式 -> 10111111
  • 由于现在没有符号位一律都是正数所以10111111 = 128 + 32 + 16 + 8 + 4 + 2 + 1 = 191

我们也可以直接以无符号数形式打印:

#include <stdio.h>

int main() {
    int i = -1;
    printf("%u", i);    //%u以无符号形式输出十进制数据
}

image-20220603143441616

得到无符号int的最大值。

类型转换

一种类型的数据可以转换为其他类型的数据,这种操作我们称为类型转换,类型转换分为自动类型转换强制类型转换比如我们现在希望将一个short类型的数据转换为int类型的数据

#include <stdio.h>

int main() {
    short s = 10;
    int i = s;   //直接将s的值传递给i即可但是注意此时s和i的类型不同
}

这里其实就是一种自动类型转换,自动类型转换就是编译器隐式地进行的数据类型转换,这种转换不需要我们做什么,我们直接写就行,会自动进行转换操作。

float a = 3;    //包括这里我们给的明明是一个int整数3但是却可以赋值给float类型说明也是进行了自动类型转换

如果我们使用一个比转换的类型最大值都还要大的值进行类型转换,比如:

#include <stdio.h>

int main() {
    int a = 511;
    char b = a;   //最大127
    printf("%d", b);
}

image-20220606180919318

很明显char类型是无法容纳大于127的数据的因为只占一个字节而int占4个字节如果需要进行转换那么就只能丢掉前面的就只保留char所需要的那几位了所以这里得到的就是-1

  • 511 = int -> 00000000 00000000 00000001 11111111
  • char -> 11111111 -> -1

我们也可以将整数和小数类型的数据进行互相转换:

#include <stdio.h>

int main() {
    int a = 99;
    double d = a;
    printf("%f", d);
}

image-20220606180529600

不过这里需要注意的是,小数类型在转换回整数类型时,会丢失小数部分(注意,不是四舍五入,是直接丢失小数!):

#include <stdio.h>

int main() {
    double a = 3.14;
    int b = a;    //这里编译器还提示了黄标,我们可以通过之后讲到的强制类型转换来处理
    printf("%d", b);
}

image-20220606180719070

除了赋值操作可以进行自动类型转换之外,在运算中也会进行自动类型转换,比如:

#include <stdio.h>

int main() {
    float a = 2;
    int b = 3;
    double c = b / a;   //  "/" 是除以的意思也就是我们数学中的除法运算这里表示a除以b
    printf("%f", c);
}

image-20220606191838425

可以看到这里得到的结果是小数1.5,但是参与运算的既有整数类型,又有浮点类型,结果为什么就确定为浮点类型了呢?这显然是由于类型转换导致的。那么规则是什么呢?

image-20220606191412418

  • 不同的类型优先级不同(根据长度而定)
  • char和short类型在参与运算时一律转换为int再进行运算。
  • 浮点类型默认按双精度进行计算所以就算有float类型也会转换为double类型参与计算。
  • 当有一个更高优先级的类型和一个低优先级的类型同时参与运算时统一转换为高优先级运算比如int和long参与运算那么int转换为long再算所以结果也是long类型int和double参与运算那么先把int转换为double再算。

我们接着来看看强制类型转换,我们可以为手动去指定类型,强制类型转换格式如下:

(强制转换类型) 变量、常量或表达式;

比如:

#include <stdio.h>

int main() {
    int a = (int) 2.5;   //2.5是一个double类型的值但是我们可以强制转换为int类型赋值给a强制转换之后小数部分丢失
    printf("%d", a);
}

我们也可以对一个算式的结果进行类型转换:

#include <stdio.h>

int main() {
    double a = 3.14;
    int b = (int) (a + 2.8);   //注意得括起来表示对整个算式的结果进行类型转换(括号跟数学中的挺像,也是提升优先级使用的,我们会在运算符部分详细讲解),不然强制类型转换只对其之后紧跟着的变量生效
    printf("%d", b);
}

在我们需要得到两个int相除之后带小数的结果时强制类型转换就显得很有用

#include <stdio.h>

int main() {
    int a = 10, b = 4;
    double c = a / b;    //不进行任何的类型转换int除以int结果仍然是int导致小数丢失
    double d = (double) a / b;   //对a进行强制类型转换现在是double和int计算根据上面自动类型转换规则后面的int自动转换为double结果也是double了这样就是正确的结果了
    printf("不进行类型转换: %f, 进行类型转换: %f", c, d);
}

合理地使用强制类型转换,能够解决我们很多情况下的计算问题。


运算符

前面我们了解了如何声明变量以及变量的类型转换,那么我们如何去使用这些变量来参与计算呢?这是我们本小节的重点。

基本运算符

基本运算符包含我们在数学中常用的一些操作,比如加减乘除,分别对应:

  • 加法运算符:+
  • 减法运算符:-
  • 乘法运算符:*
  • 除法运算符:/(注意不是“\”,看清楚一点)

当然,还有我们之前使用的赋值运算符=,我们先来看看赋值运算符的使用,其实在之前我们已经学习过了:

变量 =    //其中,值可以直接是一个数字、一个变量、表达式的结果等

实际上等号左边的内容准确的说应该是一个左值不过大部分情况下都是变量这里就不展开左值和右值的话题了感兴趣的小伙伴可以去详细了解有助于后面学习C++理解右值引用)

最简单的用法就是我们前面所说的,对一个变量进行赋值操作:

int a = 10;

也可以连续地使用赋值操作,让一连串的变量都等于后面的值:

int a, b;
a = b = 20;   //从右往左依次给b和a赋值20

可以看出,实际上=运算除了赋值之外,和加减乘除运算一样也是有结果的,比如上面的 a = 就是b = 20 运算的结果可以看着一个整体只不过运算的结果就是b被赋值的值也就是20。

我们接着来看加减法,这个就和我们数学中的是一样的了:

#include <stdio.h>

int main() {
    int a = 10, b = 5;
    printf("%d", a + b);   //打印 a + b 的结果
}

当然也可以像数学中那样写在一个数或是变量的最前面,表示是正数:

int a = +10, b = +5;

不过默认情况下就是正数,所以没必要去写一个+号。减法运算符其实也是一样的:

#include <stdio.h>

int main() {
    int a = 10, b = 5;
    printf("%d", a - b);   //打印 a - b 的结果
}
#include <stdio.h>

int main() {
    int a = -10;   //等于 -10
    printf("%d", -a);   //输出 -a 的值,就反着来嘛
}

接着我们来看看乘法和除法运算:

#include <stdio.h>

int main() {
    int a = 20, b = 10;
    printf("%d, %d", a * b, a / b);   //使用方式和上面的加减法是差不多的
}

还有一个比较有意思的取模运算:

#include <stdio.h>

int main() {
    int a = 20, b = 8;
    printf("%d", a % b);   //取模运算实际上就是计算a除以b的余数
}

不过很遗憾在C中没有指数相关的运算符比如要计算5的10次方在后面学习了循环语句之后我们可以尝试来自己实现一个指数运算。

运算符优先级

和数学中一样,运算符是有优先级的:

#include <stdio.h>

int main() {
    int a = 20, b = 10;
    printf("%d", a + a * b);   //如果没有优先级那么结果应该是400
}

很明显这里的结果是考虑了优先级的:

image-20220606205103154

在数学中加减运算的优先级是没有乘除运算优先级高的所以我们需要先计算那些乘除法最后再来进行加减法的计算而C语言中也是这样运算符之间存在优先级概念。我们在数学中如果需要优先计算加减法再计算乘除法那么就需要使用括号来提升加减法的优先级C语言也可以

#include <stdio.h>

int main() {
    int a = 20, b = 10;
    printf("%d", (a + a) * b);   //优先计算 a + a 的结果,再乘以 b
}

那要是遇到多重的呢?类似于下面的这种:

数学上的写法:[1 - (3 + 4)] x (-2 ÷ 1) = ?

那么我们在C中就可以这样编写

#include <stdio.h>

int main() {
    printf("%d", (1 - (3 + 4)) * (-2 / 1));   //其实写法基本差不多,只需要一律使用小括号即可
}

这样,我们就可以通过()运算符,来提升运算优先级了。

我们来总结一下,上面运算符优先级如下,从左往右依次递减:

  • () > + - (做符号表示,比如-9) > * / % > + - (做加减运算) > =

根据上面的优先级,我们来看看下面a的结果是什么:

int c;
int a = (3 + (c = 2)) * 6;
int b, c;
int a = (b = 5, c = b + 8);  //逗号运算符从前往后依次执行,赋值结果是最后边的结果

自增自减运算符

我们可以快速使用自增运算符来将变量的值+1,正常情况下我们想要让一个变量值自增需要:

int a = 10;
a = a + 1;

现在我们只需要替换为:

int a = 10;
++a;   //使用自增运算符,效果等价于 a = a + 1

并且它也是有结果的,除了做自增运算之外,它的结果是自增之后的值:

#include <stdio.h>

int main() {
    int a = 10;
    //int b = a = a + 1;  下面效果完全一致
    int b = ++a;
    printf("%d", b);
}

当然我们也可以将自增运算符写到后面,和写在前面的区别是,它是先返回当前变量的结果,再进行自增的,顺序是完全相反的:

#include <stdio.h>

int main() {
    int a = 10;
    int b = a++;   //写在后面和写在前面是有区别的
    printf("a = %d, b = %d", a, b);
}

重点内容:自增运算符++在前,那么先自增再出结果;自增运算符++在后,那么先出结果再自增。各位小伙伴可以直接记运算符的位置,来方便记忆。

那要是现在我们不想自增1而是自增2或是其他的数字呢我们可以使用复合赋值运算符正常情况下依然是使用普通的赋值运算符

int a = 10;
a = a + 5;

但是现在我们可以简写:

int a = 10;
a += 5;

效果和上面是完全一样的,并且得到的结果也是在自增之后的:

#include <stdio.h>

int main() {
    int a = 10;
    int b = a += 5;
    printf("a = %d", b);
}

复合赋值运算符不仅仅支持加法,还支持各种各样的运算:

#include <stdio.h>

int main() {
    int a = 10;
    a %= 3;   //可以复合各种运算,比如加减乘除、模运算、包括我们我们还要讲到的位运算等
    printf("a = %d", a);
}

当然,除了自增操作之外,还有自减操作:

#include <stdio.h>

int main() {
    int a = 10;
    a--;   //--是自减操作相当于a = a - 1也可以在前后写规则和上面的自增是一样的
    printf("a = %d", a);
}

注意自增自减运算符和+-做符号是的优先级一样,仅次于()运算符,所以在编写时一定要注意:

#include <stdio.h>

int main() {
    int a = 10;
    int b = 5 * --a;
    printf("b = %d", b);
}

位运算符

前面我们学习了乘法运算符*当我们想要让一个变量的值变成2倍只需要做一次乘法运算即可

int a = 10;
a *= 2;  //很明显算完之后a就是20了

但是我们现在可以利用位运算来快速进行计算:

int a = 10;
a = a << 1;   //也可以写成复合形式 a <<= 1

我们会发现这样运算之后得到的结果居然也是20这是咋算出来的呢实际上<<是让所有的bit位进行左移操作上面就是左移1位我们可以来看看

  • 10 = 00001010 现在所以bit位上的数据左移一位 00010100 = 20

是不是感觉特别神奇就像我们在十进制中做乘以10的操作一样22乘以10那么就直接左移了一位变成220而二进制也是一样的如果让这些二进制数据左移的话那么相当于在进行乘2的操作。

比如:

#include <stdio.h>

int main() {
    int a = 6;
    a = a << 2;   //让a左移2位实际上就是 a * 2 * 2a * 2的平方类比十进制其实还是很好理解的
    printf("a = %d", a);
}

当然能左移那肯定也可以右移:

#include <stdio.h>

int main() {
    int a = 6;
    a = a >> 1;   //右移其实就是除以2的操作
    printf("a = %d", a);
}

当然除了移动操作之外,我们也可以进行按位比较操作,先来看看按位与操作:

#include <stdio.h>

int main() {
    int a = 6, b = 4;
    int c = a & b;   //按位与操作
    printf("c = %d", c);
}

按位与实际上也是根据每个bit位来进行计算的

  • 4 = 00000100
  • 6 = 00000110
  • 按位与实际上就是让两个数的每一位都进行比较如果两个数对应的bit位都是1那么结果的对应bit位上就是1其他情况一律为0
  • 所以计算结果为00000100 = 4

除了按位与之外,还有按位或运算:

int a = 6, b = 4;
int c = a | b;
  • 4 = 00000100
  • 6 = 00000110
  • 按位与实际上也是让两个数的每一位都进行比较如果两个数对应bit位上其中一个是1那么结果的对应bit位上就是1其他情况为0。
  • 所以计算结果为00000110 = 6

还有异或和按位非(按位否定):

int a = 6, b = 4;
int c = a ^ b;    //注意^不是指数运算表示按位异或运算让两个数的每一位都进行比较如果两个数对应bit位上不同时为1或是同时为0那么结果就是1否则结果就是0所以这里的结果就是2
a = ~a;   //按位否定针对某个数进行操作它会将这个数的每一个bit位都置反0变成11变成0猜猜会变成几

按位运算都是操作数据底层的二进制位来进行的。

逻辑运算符

最后我们来看一下逻辑运算符,逻辑运算符主要用到下一节的流程控制语句中。

逻辑运算符用于计算真和假,比如今天要么下雨要么不下雨,现在我们想要在程序中判断一下是否下雨了,这时就需要用到逻辑运算符,我们来举个例子:

#include <stdio.h>

int main() {
    int a = 10;
    _Bool c = a < 0;    //我们现在想要判断a的值是否小于0我们可以直接使用小于符号进行判断最后得到的结果只能是1或0
  	//虽然结果是一个整数但是这里推荐使用_Bool类型进行接收它只能表示0和1更加专业一些
    printf("c = %d", c);  
}

实际上在C语言中0一般都表示为假而非0的所有值包括正数和负数都表示为真上面得到1表示真0表示假。

除了小于符号可以判断大小之外,还有:< <=>=>

比如我们现在想要判断字符C是否为大写字母

#include <stdio.h>

int main() {
    char c = 'D';
    printf("c是否为大写字母%d", c >= 'A');    //由于底层存储的就是ASCII码这里可以比较ASCII码也可以写成字符的形式
}

但是我们发现现在我们的判断只能判断一个条件也就是说只能判断c是否是大于等于'A'的但是不能同时判断c的值是否是小于等于'Z'的,所以这时,我们就需要利用逻辑与和逻辑或来连接两个条件了:

#include <stdio.h>

int main() {
    char c = 'D';
    printf("c是否为大写字母%d", c >= 'A' && c <= 'Z');   //使用&&表示逻辑与,逻辑与要求两边都是真,结果才是真
}

又比如现在我们希望判断c是否不是大写字母

#include <stdio.h>

int main() {
    char c = 'D';
    printf("c是否不为大写字母%d", c < 'A' || c > 'Z');   //使用||表示逻辑或,只要两边其中一个为真或是都为真,结果就是真
}

当然我们也可以判断c是否为某个字母

#include <stdio.h>

int main() {
    char c = 'D';
    printf("c是否为字母A%d", c == 'A');    //注意判断相等时使用==双等号
}

判断不相等也可以使用:

printf("c是否不为字母A%d", c != 'A');

我们也可以对某个结果取反:

#include <stdio.h>

int main() {
    int i = 20;
    printf("i是否不小于20%d", !(i < 20));   //使用!来对结果取反,注意!优先级很高一定要括起来不然会直接对i生效
}

这里要注意一下!如果直接作用于某个变量或是常量那么会直接按照上面的规则0表示假非0表示真非0一律转换为00一律转换为1。

这里我们可以结合三目运算符来使用这些逻辑运算符:

#include <stdio.h>

int main() {
    int i = 0;
    char c = i > 10 ? 'A' : 'B';    //三目运算符格式为expression ? 值1 : 值2返回的结果会根据前面判断的结果来的
    //这里是判断i是否大于10如果大于那么c的值就是A否则就是B
  	printf("%d", c);
}

最后,我们来总结一下前面认识的所有运算符的优先级,从上往下依次降低:

运算符 解释 结合方式
() 同数学中的括号,直接提升到最高优先级 由左向右
! ~ ++ -- + - 否定,按位否定,增量,减量,正负号 由右向左
* / % 乘,除,取模 由左向右
+ - 加,减 由左向右
<< >> 左移,右移 由左向右
< <= >= > 小于,小于等于,大于等于,大于 由左向右
== != 等于,不等于 由左向右
& 按位与 由左向右
^ 按位异或 由左向右
| 按位或 由左向右
&& 逻辑与 由左向右
|| 逻辑或 由左向右
? : 条件 由右向左
= += -= *= /= &= ^= |= <<= >>= 各种赋值 由右向左
, 逗号(顺序) 由左向右

流程控制

前面我们学习了运算符,知道该如何使用运算符来计算我们想要的内容,但是仅仅依靠计算我们的程序还没办法实现丰富多样的功能,我们还得加点额外的控制操作。

分支语句 - if

我们可能会有这样的一个需求,就是判断某个条件,当满足此条件时,才执行某些代码,那这个时候该怎么办呢?我们可以使用if语句来实现:

#include <stdio.h>

int main() {
    int i = 0;
    if(i > 20) {    //我们只希望i大于20的时候才执行下面的打印语句
        printf("Hello World!");
    }
  	printf("Hello World?");   //后面的代码在if之外无论是否满足if条件都跟后面的代码无关所以这里的代码任何情况下都会执行
}

if语句的标准格式如下

if(判断条件) {
    执行的代码
}

当然如果只需要执行一行代码的话,可以省略花括号:

if(判断条件)
  一行执行的代码   //注意这样只有后一行代码生效其他的算作if之外的代码了

现在我们需求升级了我们需要判断某个条件当满足此条件时执行某些代码而不满足时我们想要执行另一段代码我们就可以结合else语句来实现

#include <stdio.h>

int main() {
    int i = 0;
    if(i > 20) {
        printf("Hello World!");   //满足if条件才执行
    } else {
        printf("LBWNB");   //不满足if条件才执行
    }
}

但是这样可能还是不够用比如我们现在希望判断学生的成绩不同分数段打印的等级不一样比如90以上就是优秀70以上就是良好60以上是及格其他的都是不及格那么这种我们又该如何判断呢要像这样进行连续判断我们需要使用else-if来完成:

#include <stdio.h>

int main() {
    int score =  2;
    if(score >= 90) {
        printf("优秀");
    } else if (score >= 70) {
        printf("良好");
    } else if (score >= 60){
        printf("及格");
    } else{
        printf("不及格");
    }
}

if这类的语句包括我们下面还要介绍的三种都是支持嵌套使用的比如我们现在希望低于60分的同学需要补习0-30分需要补Java30-60分需要补C++,这时我们就需要用到嵌套:

#include <stdio.h>

int main() {
    int score =  2;
    if(score < 60) {   //先判断不及格
        if(score > 30) {   //在内层再嵌套一个if语句进行进一步的判断
            printf("学习C++");
        } else{
            printf("学习Java");
        }
    }
}

分支语句 - switch

前面我们介绍了if语句我们可以通过一个if语句轻松地进行条件判断然后根据对应的条件来执行不同的逻辑当然除了这种方式之外我们也可以使用switch语句来实现它更适用于多分支的情况

switch (目标) {   //我们需要传入一个目标,比如变量,或是计算表达式等
  case 匹配值:    //如果目标的值等于我们这里给定的匹配值那么就执行case后面的代码
    代码...
    break;    //代码执行结束后需要使用break来结束否则会继续溜到下一个case继续执行代码
}

比如现在我们要根据学生的等级进行分班学生有ABC三个等级

#include <stdio.h>

int main() {
    char c = 'A';
    switch (c) {  //这里目标就是变量c
        case 'A':    //分别指定ABC三个匹配值并且执行不同的代码
            printf("去尖子班准备冲刺985大学");
            break;   //执行完之后一定记得break否则会继续向下执行下一个case中的代码
        case 'B':
            printf("去平行班!准备冲刺一本!");
            break;
        case 'C':
            printf("去职高深造。");
            break;
    }
}

switch可以精准匹配某个值但是它不能进行范围判断比如我们要判断分数段这时用switch就很鸡肋了。

当然除了精准匹配之外其他的情况我们可以用default来表示

switch (目标) {
    case: ...
    default:
    		其他情况下执行的代码
}

比如:

#include <stdio.h>

int main() {
    char c = 'A';
    switch (c) {
        case 'A':
            printf("去尖子班!");
            break;
        case 'B':
            printf("去平行班!");
            break;
        case 'C':
            printf("去差生班!");
            break;
        default:   //其他情况一律就是下面的代码了
            printf("去读职高,分流");
    }
}

当然switch中可以继续嵌套其他的流程控制语句比如if

#include <stdio.h>

int main() {
    char c = 'A';
    switch (c) {
        case 'A':
            if(c == 'A') {    //嵌套一个if语句
                printf("去尖子班!");   
            }
            break;
        case 'B':
            printf("去平行班!");
            break;
    }
}

循环语句 - for

通过前面的学习,我们了解了如何使用分支语句来根据不同的条件执行不同的代码,我们接着来看第二种重要的流程控制语句,循环语句。

我们在某些时候,可能需要批量执行某些代码:

#include <stdio.h>

int main() {
    printf("伞兵一号卢本伟准备就绪!");   //把这句话给我打印三遍
    printf("伞兵一号卢本伟准备就绪!");
    printf("伞兵一号卢本伟准备就绪!");
}

遇到这种情况我们由于还没学习循环语句那么就只能写N次来实现这样的多次执行。现在我们可以使用for循环语句来多次执行

for (表达式1表达式2;表达式3) {
    循环体
}

我们来介绍一下:

  • 表达式1在循环开始时仅执行一次。
  • 表达式2每次循环开始前会执行一次要求为判断语句用于判断是否可以结束循环若结果为真那么继续循环否则结束循环。
  • 表达式3每次循环完成后会执行一次。
  • 循环体:每次循环都会执行循环体里面的内容,直到循环结束。

一个标准的for循环语句写法如下

//比如现在我们希望循环4次
for (int i = 0; i < 4; ++i) {
    //首先定义一个变量i用于控制循环结束
  	//表达式2在循环开始之前判断是否小于4
  	//表达式3每次循环结束都让i自增一次这样当自增4次之后不再满足条件循环就会结束正好4次循环
}

我们来看看按顺序打印的结果:

#include <stdio.h>

int main() {
    //比如现在我们希望循环4次
    for (int i = 0; i < 4; ++i) {
        printf("%d, ", i);
    }
}

image-20220611152257585

这样,利用循环我们就可以批量执行各种操作了。

注意如果表达式2我们什么都不写那么会默认判定为真

#include <stdio.h>

int main() {
    for (int i = 0; ; ++i) {   //表达式2不编写任何内容默认为真这样的话循环永远都不会结束
        printf("%d, ", i);
    }
}

image-20220612164349847

所以,如果我们想要编写一个无限循环,其实什么都不用写就行了:

#include <stdio.h>

int main() {
    for (;;) {   //什么都不写直接无限循环,但是注意,两个分号还是要写的
        printf("Hello World!\n");   //这里用到了\n表示换行
    }
}

当然,我们也可以在循环过程中提前终止或是加速循环的进行,这里我们需要认识两个新的关键字:

for (int i = 0; i < 10; ++i) {
    if(i == 5) break;   //比如现在我们希望在满足某个条件下提前终止循环可以使用break关键字来跳出循环
    printf("%d", i);
}

image-20220613101128788

可以看到当满足条件时会直接通过break跳出循环循环不再继续下去直接结束掉。

我们也可以加速循环:

for (int i = 0; i < 10; ++i) {
    if(i == 5) continue;   //使用continue关键字会加速循环无论后面有没有未执行完的代码都会直接开启下一轮循环
    printf("%d", i);
}

image-20220613101847762

虽然使用break和continue关键字能够更方便的控制循环但是注意在多重循环嵌套下它只对离它最近的循环生效就近原则

for (int i = 1; i < 4; ++i) {
    for (int j = 1; j < 4; ++j) {
        if(i == j) continue;    //当i == j时加速循环
        printf("%d, %d\n", i, j);
    }
}

image-20220613102100374

可以看到continue仅仅加速的是内层循环而对外层循环没有任何效果同样的break也只会终结离它最近的

for (int i = 1; i < 4; ++i) {
    for (int j = 1; j < 4; ++j) {
        if(i == j) break;    //当i == j时终止循环
        printf("%d, %d\n", i, j);
    }
}

image-20220613102347086

循环语句 - while

前面我们介绍了for循环语句我们接着来看第二种while循环for循环要求我们填写三个表达式而while相当于是一个简化版本它只需要我们填写循环的维持条件即可比如

#include <stdio.h>

int main() {
    while (1) {   //每次循环开始之前都会判断括号内的内容是否为真,如果是就继续循环
        printf("Hello World!\n");   //这里会无限循环
    }
}

相比for循环while循环更多的用在不明确具体的结束时机的情况下而for循环更多用于明确知道循环的情况比如我们现在明确要进行循环10次此时用for循环会更加合适一些又比如我们现在只知道当i大于10时需要结束循环但是i在循环多少次之后才不满足循环条件我们并不知道此时使用while就比较合适了。

#include <stdio.h>

int main() {
    int i = 100;   //比如现在我们想看看i不断除以2得到的结果会是什么但是循环次数我们并不明确
    while (i > 0) {   //现在唯一知道的是循环条件只要大于0那么就可以继续除
        printf("%d, ", i);
        i /= 2;   //每次循环都除以2
    }
}

image-20220612170911315

while也支持使用break和continue来进行循环的控制

int i = 100;
while (i > 0) {
    if(i < 30) break;
    printf("%d, ", i);
    i /= 2;
}

image-20220613102935994

我们可以反转循环判断的位置,可以先执行循环内容,然后再做循环条件判断,这里要用到do-while语句:

#include <stdio.h>

int main() {
    do {  //无论满不满足循环条件,先执行循环体里面的内容
        printf("Hello World!");
    } while (0);   //再做判断,如果判断成功,开启下一轮循环,否则结束
}

image-20220613103504978

实战:寻找水仙花数

“水仙花数Narcissistic number也被称为超完全数字不变数pluperfect digital invariant, PPDI、自恋数、自幂数、阿姆斯壮数或阿姆斯特朗数Armstrong number水仙花数是指**一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身。**例如1^3 + 5^3+ 3^3 = 153。”

现在请你设计一个C语言程序打印出所有1000以内的水仙花数。

实战:打印九九乘法表

image-20220613105029975

现在我们要做的是在我们的程序中也打印出这样的一个乘法表出来请你设计一个C语言程序来实现它。

image-20220613105519595

实战:斐波那契数列解法其一

斐波那契数列Fibonacci sequence又称黄金分割数列因数学家莱昂纳多·斐波那契Leonardo Fibonacci以兔子繁殖为例子而引入故又称为“兔子数列”指的是这样一个数列**1、1、2、3、5、8、13、21、34、……*在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0F(1)=1, F(n)=F(n - 1)+F(n - 2)n ≥ 2n ∈ N)在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从 1963 年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果。

斐波那契数列1123581321345589...不难发现一个规律实际上从第三个数开始每个数字的值都是前两个数字的和现在请你设计一个C语言程序可以获取斐波那契数列上任意一位的数字比如获取第5个数那么就是5。

#include <stdio.h>

int main() {
    int target = 7, result;  //target是要获取的数result是结果

    //请在这里实现算法

    printf("%d", result);
}

数组

现在我们有一个新的需求我们需要存储2022年每个月都天数那么此时为了保存这12个月的天数我们就得创建12个变量

#include <stdio.h>

int main() {
    int january = 31, february = 28, march = 31 ...
}

这样是不是太累了点万一我们想保存100个商品的售价那岂不是得创建100个变量这肯定不行啊。

数组的创建和使用

为了解决这种问题我们可以使用数组什么是数组呢简单来说就是存放数据的一个组所有的数据都统一存放在这一个组中一个数组可以同时存放多个数据。比如现在我们想保存12个月的天数那么我们只需要创建一个int类型的数组就可以了它可以保存很多个int类型的数据这些保存在数组中的数据称为“元素”

int arr[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};  //12个月的数据全部保存在了一起

可以看到,数组的定义方式也比较简单:

类型 数组名称[数组大小] = {数据1, 数据2...};  //后面的数据可以在一开始的时候不赋值,并且数组大小必须是整数

注意数组只能存放指定类型的数据,一旦确定是不能更改的,因为数组声明后,会在内存中开辟一块连续的区域,来存放这些数据,所以类型和长度必须在一开始就明确。

image-20220613113423268

创建数组的方式有很多种:

int a[10];   //直接声明int类型数组容量为10

int b[10] = {1, 2, 4};   //声明后,可以赋值初始值,使用{}囊括不一定需要让10个位置都有初始值比如这里仅仅是为前三个设定了初始值注意跟变量一样如果不设定初始值数组内的数据并不一定都是0

int c[10] = {1, 2, [4] = 777, [9] = 666}; //我们也可以通过 [下标] = 的形式来指定某一位的初始值注意下标是从0开始的第一个元素就是第0个下标位置比如这里数组容量为10那么最多到9

int c[] = {1, 2, 3};  //也可以根据后面的赋值来决定数组长度

基本类型都可以声明数组:

#include <stdio.h>

int main() {
    char str[] = {'A', 'B', 'C'};   //多个字符
  
    char str2[] = "ABC";  //实际上字符串就是多个字符的数组形式,有关字符串我们会在下一节进行讲解
}

那么数组定义好了如何去使用它呢比如我们现在需要打印12个月的天数

#include <stdio.h>

int main() {
    int arr[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    for (int i = 0; i < 12; ++i) {
        int days = arr[i];   //直接通过数组 名称[下标] 来访问对应的元素值再次提醒下标是从0开始的不是1
        printf("2022年 %d 月的天数是:%d 天\n", (i + 1), days);
    }
}

image-20220613114739872

当然我们也可以对数组中的值进行修改:

#include <stdio.h>

int main() {
    int arr[] = {666, 777, 888};
    arr[1] = 999;   //比如我们现在想要让第二个元素的值变成999
    printf("%d", arr[1]);   //打印一下看看是不是变成了999
}

image-20220613114928435

注意和变量一样如果只是创建数组但是不赋初始值的话因为是在内存中随机申请的一块空间有可能之前其他地方使用过保存了一些数据所以数组内部的元素值并不一定都是0

#include <stdio.h>

int main() {
    int arr[10];
    for (int i = 0; i < 10; ++i) {
        printf("%d, ", arr[i]);
    }
}

image-20220613115108971

不要尝试去访问超出数组长度位置的数据,虽然可以编译通过,但是会给警告,这些数据是毫无意义的:

#include <stdio.h>

int main() {
    int arr[] = {111, 222, 333};
    printf("%d", arr[3]);  //不能去访问超出数组长度的元素,很明显这里根本就没有第四个元素
}

多维数组

数组不仅仅只可以有一个维度,我们可以创建二维甚至多维的数组,简单来说就是,存放数组的数组(套娃了属于是):

int arr[][2] = {{20, 10}, {18, 9}};   //可以看到,数组里面存放的居然是数组
//存放的内层数组的长度是需要确定的,存放数组的数组和之前一样,可以根据后面的值决定

比如现在我们要存放2020-2022年每个月的天数那么此时用一维数组肯定是不方便了我们就可以使用二维数组来处理

int arr[3][12] = {{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, //2020年是闰年2月有29天
                  {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
                  {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};

这样,我们就通过二维数组将这三年每个月的天数都保存下来了。

image-20220613121156024

那么二维数组又该如何去访问呢?

#include <stdio.h>

int main() {
    int arr[3][12] = {{31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}, //2020年是闰年2月有29天
                      {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
                      {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
    printf("%d", arr[0][1]);   //比如现在我们想要获取2020年2月的天数首先第一个是[0]表示存放的第一个数组,第二个[1]表示数组中的第二个元素
}

当然除了二维还可以上升到三维、四维:

int arr[2][2][2] = {{{1, 2}, {1, 2}}, {{1, 2}, {1, 2}}};

有关多维数组,暂时先介绍到这里。

实战:冒泡排序算法

现在有一个int数组但是数组内的数据是打乱的现在请你通过C语言实现将数组中的数据按从小到大的顺序进行排列:

#include <stdio.h>

int main() {
    int arr[10] = {3, 5, 7, 2, 9, 0, 6, 1, 8, 4};  //乱序的
    //请编写代码对以上数组进行排序
}

这里我们使用冒泡排序算法来实现,此算法的核心思想是:

  • 假设数组长度为N
  • 进行N轮循环每轮循环都选出一个最大的数放到后面。
  • 每次循环中,从第一个数开始,让其与后面的数两两比较,如果更大,就交换位置,如果更小,就不动。

动画演示:https://visualgo.net/zh/sorting?slide=2-2

实战:斐波那契数列解法其二

学习了数组,我们来看看如何利用数组来计算斐波那契数列,这里采用动态规划的思想。

动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。

我们可以在一开始创建一个数组,然后从最开始的条件不断向后推导,从斐波那契数列的规律我们可以得知:

  • fib[i] = fib[i - 1] + fib[i - 2](这里fib代表斐波那契数列)

得到这样的一个关系(递推方程)就好办了,我们要求解数列第i个位置上的数,只需要知道i - 1i - 2的值即可这样一个大问题就分成了两个小问题比如现在我们要求解斐波那契数列的第5个元素

  • fib[4] = fib[3] + fib[2]现在我们只需要知道fib[3]fib[2]即可,那么我们接着来看:
  • fib[3] = fib[2] + fib[1]以及fib[2] = fib[1] + fib[0]
  • 由于fib[0]fib[1]我们已经明确知道是1了,那么现在问题其实已经有结果了,把这些小问题的结果组合起来不就能得到原来大问题的结果了吗?

现在请你设计一个C语言程序利用动态规划的思想解决斐波那契数列问题。

实战:打家劫舍

我们继续通过一道简单的算法题来强化动态规划思想。

来源力扣LeetCodeNo.198 打家劫舍https://leetcode.cn/problems/house-robber/

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1

输入:[1,2,3,1] 输出4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。

示例 2

输入:[2,7,9,3,1] 输出12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。

image-20220613124415156

这道题我们也可以很轻松地按照上面的动态规划思路来处理,首先我们可以将问题分为子问题,比如现在有[2,7,9,3,1]五个房屋这个问题看起来比较复杂我们不妨先将大问题先简化成小问题我们来看看只有N个房屋的情况

  • 假设现在只有[2]这一个房屋那么很明显我可以直接去偷一号房得到2块钱所以当有一个房子时最大能偷到2块钱。
  • 假设现在有[2, 7]这两个房屋那么很明显我可以直接去偷二号房得到7块钱所以当有两个房子时最大能偷到7块钱。
  • 假设现在只有[2, 7, 9]这三个房屋,我们就要来看看了,是先偷一号房再偷三号房好,还是只偷二号房好,根据前面的结论,如果我们偷了一号房,那么就可以继续偷三号房,并且得到的钱就是从一号房过来的钱+三号房的钱也就是2+9块钱但是如果只偷二号房的话那么就只能得到7块钱所以三号房能够偷到的最大金额有以下关系dp是我们求出的第i个房屋的最大偷钱数量value表示房屋价值max表示取括号中取最大的一个
    • dp[i] = max(dp[i - 1], dp[i - 2] + value[i]) -> 递推方程已得到
  • 这样就不难求出:dp[2] = max(dp[1], dp[0] + value[i]) = dp[2] = max(7, 2 + 9) = dp[2] = 11所以有三个房屋时最大的金额是11块钱。
  • 所以,实际上我们只需要关心前面计算出来的盗窃最大值即可,而不需要关心前面到底是怎么在偷。
  • 我们以同样的方式来计算四个房屋[2, 7, 9, 3]的情况:
    • dp[3] = max(dp[2], dp[1] + value[3]) = dp[3] = max(11, 7 + 3) = dp[3] = 11
  • 所以当有四个房屋时我们依然采用先偷一后偷三的方案不去偷四号得到最大价值11块钱。

好了现在思路已经出来了我们直接上算法吧现在请你实现下面的C语言程序

#include <stdio.h>

int main() {
    int arr[] = {2,7,9,3,1}, size = 5, result;

    //请补充程序

    printf("%d", result);
}

力扣提交,建议各位小伙伴学习了函数和指针之后再回来看看,这里暂时可以跳过。

image-20220613130702396

int max(int a, int b) {
    return a > b ? a : b;
}

int rob(int* nums, int numsSize){
    if(numsSize == 0) return 0;
    if(numsSize == 1) return nums[0];
    if(numsSize == 2) return max(nums[1], nums[0]);

    int dp[numsSize];
    dp[0] = nums[0];
    dp[1] = max(nums[1], nums[0]);

    for (int i = 2; i < numsSize; ++i) {
        dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
    }

    return dp[numsSize - 1];
}

字符串

前面我们学习了数组而对于字符类型的数组比较特殊它实际上可以作为一个字符串String表示字符串就是一个或多个字符的序列比如我们在一开始认识的"Hello World!",像这样的多个字符形成的一连串数据,就是一个字符串,而printf函数接受的第一个参数也是字符串。

那么,我们就来认识一下字符串。

字符串的创建和使用

在C语言中并没有直接提供存储字符串的类型我们熟知的能够存储字符的只有char类型但是它只能存储单个字符而一连串的字符想要通过变量进行保存那么就只能依靠数组了char类型的数组允许我们存放多个字符这样的话就可以表示字符串了。

比如我们现在想要存储Hello这一连串字符:

char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};  //直接保存单个字符,但是注意,无论内容是什么,字符串末尾必须添加一个‘\0字符ASCII码为0表示结束。
printf("%s", str);   //用%s来作为一个字符串输出

不过这样写起来实在是太麻烦了,我们可以使用更加简便的写法:

char str[] = "Hello";   //直接使用双引号将所有的内容囊括起来,并且也不需要补充\0但是本质上是和上面一样的字符数组
//也可以添加 const char str[] = "Hello World!"; 双引号囊括的字符串实际上就是一个const char数组类型的值
printf("%s", str);

这下终于明白了,原来我们一直在写的双引号,其实表示的就是一个字符串。

那么现在请各位小伙伴看看下面的写法有什么不同:

"c"
'c'

我们发现一个问题char类型只能保存ASCII编码表中的字符但是我们发现实际上中文也是可以正常打印的

printf("你这瓜保熟吗");

image-20220616114344138

这是什么情况那么多中文字符差不多有6000多个用ASCII编码表那128个肯定是没办法全部表示的但是我们现在需要在电脑中使用中文。这时我们就需要扩展字符集了。

我们可以使用两个甚至多个字节来表示一个中文字符这样我们能够表示的数量就大大增加了GB2132方案规定当连续出现两个大于127的字节时注意不考虑符号位此时相当于是第一个bit位一直为1了表示这是一个中文字符所以为什么常常有人说一个英文字符占一字节一个中文字符占两个字节这样我们就可以表示出超过7000种字符了不仅仅是中文甚至中文标点、数学符号等都可以被正确的表示出来。

10000011 10000110   //这就是一个连续出现都大于127的字节注意这里是不考虑符号位的

不过这样能够表示的内容还是不太够除了那些常见的汉字之外还有很多的生僻字比如龘、錕、釿、拷这类的汉字后来干脆直接只要第一个字节大于127就表示这是一个汉字的开始无论下一个字节是什么内容甚至原来的128个字符也被编到新的表中这就是Windows至今一直在使用的默认GBK编码格式。

虽然这种编码方式能够很好的解决中文无法表示的问题但是由于全球还有很多很多的国家以及很多很多种语言所以我们的最终目标是能够创造一种可以表示全球所有字符的编码方式整个世界都使用同一种编码格式这样就可以同时表示全球的语言了。所以这时就出现了一个叫做ISO的国际标准化组织组织来定义一套编码方案来解决所有国家的编码问题这个新的编码方案就叫做Unicode规定每个字符必须使用俩个字节即用16个bit位来表示所有的字符也就是说原来的那128个字符也要强行用两位来表示

但是这样的话实际上是很浪费资源的因为这样很多字符都不会用到两字节来保存但是又得这样去表示这就导致某些字符浪费了很多空间。所以最后就有了UTF-8编码格式区分每个字符的开始是根据字符的高位字节来区分的比如用一个字节表示的字符第一个字节高位以“0”开头用两个字节表示的字符第一个字节的高位为以“110”开头后面一个字节以“10开头”用三个字节表示的字符第一个字节以“1110”开头后面俩字节以“10”开头用四个字节表示的字符第一个字节以“11110”开头后面的三个字节以“10”开头

Unicode符号范围十六进制 UTF-8编码方式(二进制)
0000 0000 ~ 0000 007F 0xxxxxxx
0000 0080 ~ 0000 07FF 110xxxxx 10xxxxxx
0000 0800 ~ 0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000 ~ 0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

所以如果我们的程序需要表示多种语言最好采用UTF-8编码格式。

简而言之我们的中文实际上是依靠多个char来进行表示的。

image-20220616134302528

image-20220616134330805

这样,我们就了解了字符串的使用。

scanf、gets、puts函数

函数我们会在下一章详细介绍,不过这里还是要再提到一个比较重要的函数。

前面我们认识了printf函数,实际上这个函数就是用于打印字符串到控制台,我们只需要填入一个字符串和后续的参数即可。

#include <stdio.h>

int main() {
    const char str[] = "Hello World!";   //注意printf需要填写一个const char数组进去也就是字符串
    printf(str);
}

现在我们知道该如何输出,那么输入该如何实现呢,比如我们现在希望将我们想要说的话告诉程序,让程序从控制台读取我们输入的内容,这时我们就需要使用到scanf函数了:

#include <stdio.h>

int main() {
    char str[10];
    scanf("%s", str);   //使用scanf函数来接受控制台输入并将输入的结果按照格式分配给后续的变量
  	//比如这里我们想要输入一个字符串,那么依然是使用%s和输出是一样的占位符后面跟上我们要赋值的数组存放输入的内容
    printf("输入的内容为:%s", str);
}

可以看到,成功接收到用户输入:

image-20220616141313060

当然除了能够扫描成字符串之外,我们也可以直接扫描为一个数字:

#include <stdio.h>

int main() {
    int a, b;
    scanf("%d", &a);   //连续扫描两个int数字
    scanf("%d", &b);   //注意,如果不是数组类型,那么这里在填写变量时一定要在前面添加一个&符号(至于为什么,下一章在指针小节中会详细介绍)这里的&不是做与运算,而是取地址操作。

    printf("a + b = %d", a + b);   //扫描成功后我们来计算a + b的结果
}

除了使用scanf之外,我们也可以使用字符串专用的函数来接受字符串类型的输入和输出:

#include <stdio.h>

int main() {
    char str[10];
    gets(str);   //gets也是接收控制台输入然后将结果丢给str数组中
    puts(str);   //puts其实就是直接打印字符串到控制台
}

当然也有专门用于字符输入输出的函数:

#include <stdio.h>

int main() {
    int c = getchar();
    putchar(c);
}

由于我们目前还没有学习函数,所以这里稍微提及一下即可。

实战:回文串判断

“回文串”是一个正读和反读都一样的字符串请你实现一个C语言程序判断用户输入的字符串仅出现英文字符是否为“回文”串。

ABCBA 就是一个回文串,因为正读反读都是一样的

ABCA 就不是一个回文串,因为反着读不一样

实战字符串匹配KMP算法

现在有两个字符串:

str1 = "abcdabbc"

str2 = "cda"

现在请你设计一个C语言程序判断第一个字符串中是否包含了第二个字符串比如上面的例子中很明显第一个字符串包含了第二个字符串。

  • 暴力解法
  • KMP算法

有关C语言的基础部分内容我们就讲解到这里从下一章开始难度将会有一定的提升所以请各位小伙伴务必将本章知识点梳理清楚牢记心中。