【c语言】基础篇学习笔记
C语言基础
C语言的语法层面内容相比其他语言来说比较少,它的难点在于很多概念上的理解
注
:语言标准为C99及其以上
- C程序基本格式
在创建项目之后自动生成了一个.c
文件,这个就是我们编写的程序代码文件:
#include <stdio.h>
int main() {
printf("Hello World!");
return 0;
}
操作系统需要执行我们的程序,需要知道从哪里开始执行,也就是需要找到程序的入口,C语言程序入口点就是main函数,写法:
int main() { //所有的符号一律采用英文的,别用中文
程序代码...
}
如果需要打印一段话到控制台,那么就需要使用printf(内容)来完成,这其实就是一种函数调用,括号里面的内容就是要打印到控制台的内容:
printf("Hello World!");
//注意最后需要添加;来结束这一行,注意是英文的分号,不是中文的!
要打印的内容需要采用双引号进行囊括,被双引号囊括的这一端话,我们称为字符串。
最顶上还有一句:
#include <stdio.h>
这个是引入系统库为我们提供的函数,包括printf在内,所以就有一个固定的模板:
#include <stdio.h>
int main() {
程序代码
}
写代码的过程中可以添加一些注释文本,这些文本内容在编译时自动忽略,单行注释:
#include <stdio.h> //引入标准库头文件
int main() { //主函数,程序的入口点
printf("Hello World!"); //向控制台打印字符串
}
多行注释:
#include <stdio.h>
/*
* 这是由IDE自动生成的测试代码
* 还是可以的
*/
int main() {
printf("Hello World!");
//最后还有一句 return 0; 但是我们可以不用写
}
基本数据类型
程序离不开数据,比如需要保存一个数字或是字母,这时候这些东西就是作为数据进行保存,不过不同的数据他们的类型可能不同,比如1就是一个整数,0.5就是一个小数,A就是一个字符,C语言提供了多种数据类型供我们使用
不同的数据类型占据的空间也会不同,这里需要先提一个概念,就是字、字节
我们知道,计算机底层实际上只有0和1能够表示,这时如果要存储一个数据,比如十进制的3,那么就需要使用2个二进制位来保存,二进制格式为11,占用两个位置,再比如我们要表示十进制的15,这时转换为二进制就是1111占用四个位置(4个bit位)来保存。一般占用8个bit位表示一个字节(B),2个字节等于1个字,所以一个字表示16个bit位,它们是计量单位。
常见的内存大小1G、2G等,实际上就是按照下面的进制进行计算的:
8 bit = 1 B ,1024 B = 1KB,1024 KB = 1 MB,1024 MB = 1GB,1024 GB = 1TB,1024TB = 1PB(基本上是1024一个大进位,但是有些硬盘生产厂商是按照1000来计算的,所以我们买电脑的硬盘容量可能是512G的但是实际容量可能会缩水)
在不同位数的系统下基本数据类型的大小可能会不同。
原码、反码和补码
- 原码
计算机中所有的数字都是使用0和1这样的二进制数来进行表示的,但是这样仅仅只能保存正数,那么负数怎么办呢?
比如现在一共有4个bit位来保存我们的数据,为了表示正负,可以让第一个bit位专门来保存符号,这样,我们这4个bit位能够表示的数据范围就是:
- 最小:1111 => - (22+21+2^0) => -7
- 最大:0111 => + (22+21+2^0) => +7 => 7
虽然原码表示简单,但是原码在做加减法的时候很麻烦。以4bit位为例:
1+(-1) = 0001 + 1001 = ???
我们得创造一种更好的表示方式!于是引入了反码:
- 反码
正数的反码是其本身
负数的反码是在其原码的基础上, 符号位不变,其余各个位取反
经过上面的定义,再来进行加减法:
1+(-1) = 0001 + 1110 = 1111 => -0
思考:1111代表-0,0000代表+0
0既不是正数也不是负数,那么显然这样的表示依然不够合理!
- 补码
根据上面的问题,引入了最终的解决方案,那就是补码,定义:
正数的补码就是其本身
负数的补码是在其原码的基础上, 符号位不变, 其余各位取反, 最后+1.
其实现在就已经能够想通了,-0其实已经被消除了!我们再来看上面的运算:
1+(-1) = 0001 + 1111 = (1)0000 => +0 (现在无论你怎么算,也不会有-0了!)
所以现在,4bit位能够表示的范围是:-8~+7(C使用的就是补码!)
整数类型
首先来看看整数类型,整数就是不包含小数点的数据,比如1,99,666等数字,整数包含以下几种类型:
- 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码字符,每一个数字对应的是编码表中的一个字符:
编码表中包含了所有常见的字符,包括运算符号、数字、大小写字母等
某些无法直接显示的字符(比如换行,换行也算一个字符)需要使用转义字符来进行表示:
变量
变量,顾名思义:可以改变的量。
- 变量的使用
变量就像在数学中学习的x,y一样,我们可以直接声明一个变量,并利用这些变量进行基本的运算,声明变量的格式为:
数据类型 变量名称 = 初始值; //其中初始值可以不用在定义变量时设定
// = 是赋值操作,可以将等号后面的值赋值给前面的变量,
等号后面可以直接写一个数字(常量)、变量名称、算式
比如现在想要声明一个整数类型的变量:
int a = 10;
//变量类型为int(常用),变量名称为a,变量的初始值为10
int a = 10, b = 20;
//多个变量可以另起一行编写,也可以像这样用逗号隔开,注意类型必须是一样的
其中,变量的名称并不是随便什么都可以的,它有以下规则:
- 不能重复使用其他变量使用过的名字。
- 只能包含英文字母或是下划线、数字,并且严格区分大小写,比如a和A不算同一个变量。
- 虽然可以包含数字,但是不能以数字开头。
- 不能是关键字(比如我们上面提到的所有基本数据类型,当然还有一些关键字我们会在后面认识)
- (建议)使用英文单词,不要使用拼音,多个词可以使用驼峰命名法或是通过下划线连接。
- 初始值可以是一个常量数据(比如直接写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("c的结果是:%d", );
//使用%d来代表一个整数类型的数据(占位符),在打印时会自动将c的值替换上去
这样,就知道该如何打印变量的值了,除了使用%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放入其中
}
当然,也可以一次性打印多个,只需要填写多个占位符表示即可:
#include <stdio.h>
int main() {
double a = 0.5;
float b = 2.5f; //整数类型默认是int,如果要表示为long类型的值,也是需要在最后添加一个l或L
printf("a = %f, b = %f", a, b); //后面可以一直添加(逗号隔开),但是注意要和前面的占位符对应
}
再来看看字符类型:
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码
}
当然,也可以直接让char存储一个数字(ASCII码),同样也可以打印出对应的字符:
#include <stdio.h>
int main() {
char c = 66;
printf("变量c的值为:%c 对应的ASCII码为:%d", c, c);
}
看看下面这段代码会输出什么:
#include <stdio.h>
int main() {
int a = 10;
char c = 'a';
printf("变量c的ASCII码为:%d", c);
}
这里得到的结果就是字符a的ASCII码值
对于某些无法表示的字符,比如换行这类字符,没办法直接敲出来,只能使用转义字符进行表示:
char c = '\n';
变量除了有初始值之外,也可以在后续的过程中得到新的值:
#include <stdio.h>
int main() {
short s = 10;
s = 20; //重新赋值为20,注意这里就不要再指定类型了,指定类型只有在声明变量时才需要
printf("%d", s); //打印结果
}
可以看到,得到的是最后一次对变量修改的结果:
那要是不对变量设定初始值,那么变量会不会有默认值:
#include <stdio.h>
int main() {
int a, b, c, d;
printf("%d,%d,%d,%d", a, b, c, d);
}
虽然定义变量但是我们没有为其设定初始值,那么它的值就是不确定的了
在使用时一定要这个问题,至于为什么不是0,这是因为内存分配机制。
来看一个例子:
#include <stdio.h>
int main() {
char c = 127; //已经到达c的最大值了
c = c + 1; //我不管,我就要再加
printf("%d", c); //这时会得到什么结果?
}
怎么127加上1还变成-128了呢?这是由于位数不够,导致运算结果值溢出:
- 127 + 1= 01111111 + 1
由于现在是二进制,满2进1,所以最后变成
- 10000000 = 补码形式的 -128
所以,了解上面这些计算机底层原理是很重要的,我们能够很轻松地知道为什么会这样。
在我们的运算中,可能也会存在一些一成不变的值,比如π的值永远都是3.1415…,在我们的程序中,也可以使用这样不可变的变量,我们成为常量。
常量
定义常量和变量比较类似,但是需要在前面添加一个const关键字,表示这是一个常量
- 常量的使用
常量在一开始设定初始值后,后续是不允许进行修改的。
无符号数
所有的数据底层都是采用二进制来进行保存的,而第一位则是用于保存符号位,但是如果我们不考虑这个符号位,那么所有的数都是按照正数来表示,比如考虑了符号位的char类型:
- 考虑符号表示范围:-128~127
- 不考虑符号:0~255
也可以直接使用这些不带符号位的数据类型:
int main() {
unsigned char c = -65; //数据类型前面添加unsigned关键字表示采用无符号形式
printf("%u", c); //%u以无符号形式输出十进制数据
}
可以看到这里给了无符号char类型c一个-65的值,但是现在很明显符号位也是作为数值的表示部分,所以结果肯定不是-65
为什么得到的是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以无符号形式输出十进制数据
}
得到无符号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);
}
很明显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);
}
需要注意的是,小数类型在转换成整数类型时,会丢失小数部分
#include <stdio.h>
int main() {
double a = 3.14;
int b = a; //这里编译器还提示了黄标,我们可以通过之后讲到的强制类型转换来处理
printf("%d", b);
}
除了赋值操作可以进行自动类型转换之外,在运算中也会进行自动类型转换,比如:
#include <stdio.h>
int main() {
float a = 2;
int b = 3;
double c = b / a; // "/" 是除以的意思,也就是我们数学中的除法运算,这里表示a除以b
printf("%f", c);
}
可以看到,这里得到的结果是小数1.5,但是参与运算的既有整数类型,又有浮点类型,结果确定为浮点类型。这显然是由于类型转换导致的。规则:
- 不同的类型优先级不同(根据长度而定)
- 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);
}
合理地使用强制类型转换,能够解决很多情况下的计算问题。
运算符
了解了如何声明变量以及变量的类型转换,既可以去使用这些变量来参与计算
- 基本运算符
基本运算符包含我们在数学中常用的一些操作,比如加减乘除,分别对应:
- 加法运算符:+
- 减法运算符:-
- 乘法运算符:*
- 除法运算符:/
当然,还有赋值运算符=
变量 = 值 //其中,值可以直接是一个数字、一个变量、表达式的结果等
实际上等号左边的内容准确的说应该是一个左值,不过大部分情况下都是变量
最简单的用法就是我们前面所说的,对一个变量进行赋值操作:
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
}
很明显这里的结果是考虑了优先级的
在数学中,加减运算的优先级是没有乘除运算优先级高的,而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);
}
重点内容:自增运算符++在前,那么先自增再出结果;自增运算符++在后,那么先出结果再自增。
也可以使用复合赋值运算符,正常情况下依然是使用普通的赋值运算符:
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 * 2,a * 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变成1,1变成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的值是否是小于等于’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一律转换为0,0一律转换为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);
}
总结一下前面认识的所有运算符的优先级,从上往下依次降低:
运算符 | 解释 | 结合方式 |
---|---|---|
() | 同数学中的括号,直接提升到最高优先级 | 由左向右 |
! ~ ++ – + - | 否定,按位否定,增量,减量,正负号 | 由右向左 |
* / % | 乘,除,取模 | 由左向右 |
+ - | 加,减 | 由左向右 |
<< >> | 左移,右移 | 由左向右 |
< <= >= > | 小于,小于等于,大于等于,大于 | 由左向右 |
== != | 等于,不等于 | 由左向右 |
& | 按位与 | 由左向右 |
^ | 按位异或 | 由左向右 |
l | 按位或 | 由左向右 |
&& | 逻辑与 | 由左向右 |
ll | 逻辑或 | 由左向右 |
? : | 条件 | 由右向左 |
= += -= *= /= &= ^= l = <<= >>= | 各种赋值 | 由右向左 |
, | 逗号(顺序) | 由左向右 |
流程控制
仅仅依靠计算的程序没办法实现丰富多样的功能,还得加点额外的控制操作。
- 分支语句 - 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分需要补Java,30-60分需要补C++,这时就需要用到嵌套:
#include <stdio.h>
int main() {
int score = 2;
if(score < 60) { //先判断不及格
if(score > 30) { //在内层再嵌套一个if语句进行进一步的判断
printf("学习C++");
} else{
printf("学习Java");
}
}
}
- 分支语句 - switch
通过一个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可以精准匹配某个值,但是它不能进行范围判断
当然除了精准匹配之外,其他的情况可以用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);
}
}
这样,利用循环就可以批量执行各种操作了。
注
:如果表达式2什么都不写,那么会默认判定为真:
#include <stdio.h>
int main() {
for (int i = 0; ; ++i) { //表达式2不编写任何内容,默认为真,即死循环
printf("%d, ", i);
}
}
所以,如果要编写一个死循环,什么都不用写就行:
#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);
}
可以看到,当满足条件时,会直接通过break跳出循环,循环不再继续下去,直接结束掉。
也可以加速循环:
for (int i = 0; i < 10; ++i) {
if(i == 5) continue; //使用continue关键字会加速循环,无论后面有没有未执行完的代码,都会直接开启下一轮循环
printf("%d", i);
}
虽然使用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);
}
}
可以看到,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);
}
}
- 循环语句 - while
第二种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
}
}
while也支持使用break和continue来进行循环的控制:
int i = 100;
while (i > 0) {
if(i < 30) break;
printf("%d, ", i);
i /= 2;
}
可以反转循环判断的位置,可以先执行循环内容,然后再做循环条件判断,这里要用到do-while语句:
#include <stdio.h>
int main() {
do { //无论满不满足循环条件,先执行循环体里面的内容
printf("Hello World!");
} while (0); //再做判断,如果判断成功,开启下一轮循环,否则结束
}
- 实战:寻找水仙花数
水仙花数是指 一个 3 位数,它的每个位上的数字的 3次幂之和等于它本身。 例如:13 +53+ 33 = 153。
现在请你设计一个C语言程序,打印出所有1000以内的水仙花数。
#include<stdio.h>
int main(){
for(int i=100;i<1000;i++){
int a=i%10;//个位
int b=i/10%10;//十位
int c=i/10/10;//百位
if(a*a*a+b*b*b+c*c*c==i)printf("%d\n",i);
}
}
- 实战:打印九九乘法表
现在要打印一个乘法表出来,请你设计一个C语言程序来实现它。
#include<stdio.h>
int main(){
for(int i=1;i<10;i++){
for(int j=1;j<=i;j++){
printf("%d*%d=%d\t",j,i,j*i);
}
printf("\n");
}
}
- 实战:斐波那契数列解法其一
斐波那契数列指的是这样一个数列: 1、1、2、3、5、8、13、21、34、…… 在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N)
斐波那契数列:1,1,2,3,5,8,13,21,34,55,89…,不难发现一个规律,实际上从第三个数开始,每个数字的值都是前两个数字的和,现在请你设计一个C语言程序,可以获取斐波那契数列上任意一位的数字,比如获取第5个数,那么就是5。
#include <stdio.h>
int main() {
int target = 7, result; //target是要获取的数,result是结果
//请在这里实现算法
int a=1,b=1,c;
for (int i = 2; i <target; ++i) {
c=a+b;
a=b;b=c;
}
result=c;
printf("%d", result);
}
数组
现在有一个新的需求,需要存储2022年每个月都天数,那么此时,为了保存这12个月的天数,需要创建12个变量:
#include <stdio.h>
int main() {
int january = 31, february = 28, march = 31
...
}
这样太累了点,这肯定不行。
- 数组的创建和使用
为了解决这种问题,可以使用数组保存多个int类型的数据,这些保存在数组中的数据,称为“元素”:
int arr[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
//12个月的数据全部保存在了一起
可以看到,数组的定义方式也比较简单:
类型 数组名称[数组大小] = {数据1, 数据2...};
//后面的数据可以在一开始的时候不赋值,并且数组大小必须是整数
数组只能存放指定类型的数据,一旦确定是不能更改的,因为数组声明后,会在内存中开辟一块连续的区域,来存放这些数据,所以类型和长度必须在一开始就明确。
创建数组的方式有很多种:
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);
}
}
也可以对数组中的值进行修改:
#include <stdio.h>
int main() {
int arr[] = {666, 777, 888};
arr[1] = 999; //比如我们现在想要让第二个元素的值变成999
printf("%d", arr[1]); //打印一下看看是不是变成了999
}
和变量一样,如果只是创建数组但是不赋初始值的话,因为是在内存中随机申请的一块空间,有可能之前其他地方使用过,保存了一些数据,所以数组内部的元素值并不一定都是0:
#include <stdio.h>
int main() {
int arr[10];
for (int i = 0; i < 10; ++i) {
printf("%d, ", arr[i]);
}
}
不要尝试去访问超出数组长度位置的数据,虽然可以编译通过,但是会给警告,这些数据是毫无意义的:
#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}};
这样,就通过二维数组将这三年每个月的天数都保存下来了。
访问方式:
#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}; //乱序的
//算法实现:
for (int i = 0; i <9; ++i) {
_Bool flag=0;//优化一:加锁
for (int j = 1; j <10-i; ++j) {//优化二:减少已排序的比较次数
if(arr[j]<arr[j-1]){
int temp=arr[j];
arr[j]=arr[j-1];
arr[j-1]=temp;
flag=1;
}
}
if(flag==0) break;
}
for (int i = 0; i <10; ++i) {
printf("%d\t",arr[i]);
}
}
此算法的核心思想是:
假设数组长度为N
进行N轮循环,每轮循环都选出一个最大的数放到后面。
每次循环中,从第一个数开始,让其与后面的数两两比较,如果更大,就交换位置,如果更小,就不动。
- 实战:斐波那契数列解法其二
这里采用动态规划的思想。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,其基本思想是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
我们可以在一开始创建一个数组,然后从最开始的条件不断向后推导,从斐波那契数列的规律我们可以得知:
- fib[i] = fib[i - 1] + fib[i - 2](这里fib代表斐波那契数列)
1、1、2、3、5、8、13、21、34、……
现在请你设计一个C语言程序,利用动态规划的思想解决斐波那契数列问题。
#include <stdio.h>
int main() {
int target=10;
int dp[target];
dp[0]=dp[1]=1;
//算法实现:
for (int i = 2; i <10; ++i)
dp[i]=dp[i-1]+dp[i-2];
printf("%d",dp[target-1]);
}
- 实战:打家劫舍
继续通过一道简单的算法题来强化动态规划思想。
来源:力扣(LeetCode)No.198 打家劫舍
首先将问题分为子问题,比如现在有[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]) -> 递推方程已得到
现在请你实现下面的C语言程序:
解法一状态转移方程法
#include <stdio.h>
int main() {
int arr[] = {2,7,9,3,1}, size = 5, result;
//请补充程序
int dp[size];
dp[0]=arr[0];
dp[1]=arr[0]>arr[1]?arr[0]:arr[1];
for (int i = 2; i <size; ++i)
dp[i]=dp[i-1]>dp[i-2]+arr[i]?dp[i-1]:dp[i-2]+arr[i];
result=dp[size-1];
printf("%d", result);
}
解法二标准化解法
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];
}
解法三剪树枝法
int arr[] = {2,7,9,3,1}, size = 5;
int dp[] = {-1,-1,0,1,2};//辅助数组,用来构造二叉树
int dptest(int a){
if(a==-1) return 0;
return arr[a]+ dptest(dp[a])>arr[a-1]? arr[a]+ dptest(dp[a]):arr[a-1];
}
int main() {
int result= dptest(size-1);
printf("%d", result);
}
解法四: …
方法很多,可以自行探究
字符串
对于字符类型的数组,比较特殊,它实际上可以作为一个字符串(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("你这瓜保熟吗");
那么多中文字符(差不多有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来进行表示的。
- 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);
}
可以看到,成功接收到用户输入:
也可以直接扫描为一个数字:
#include <stdio.h>
int main() {
int a, b;
scanf("%d", &a); //连续扫描两个int数字
scanf("%d", &b); //注意,如果不是数组类型,那么这里在填写变量时一定要在前面添加一个&符号,这里的&不是做与运算,而是取地址操作。
printf("a + b = %d", a + b); //扫描成功后,我们来计算a + b的结果
}
字符串专用的函数来接受字符串类型的输入和输出:
#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语言程序,判断第一个字符串中是否包含了第二个字符串,比如上面的例子中,很明显第一个字符串包含了第二个字符串。
#include <stdio.h>
#include <string.h>//操作字符串头文件
int main() {
char str[20];
gets(str);
int len= strlen(str),left=0,right=len-1;
_Bool flag=1;
while (left<len){
if(str[left]!=str[right]){
flag=0;
break;
}
left++;
right--;
}
puts(flag?"是回文串":"不是回文串");
}
- 实战:字符串匹配
现在有两个字符串:
str="abcdabbc";
str2="cda";
请你设计一个c语言程序,判断第一个字符串中是否包含第二个字符串
- 暴力解法
#include <stdio.h>
#include <string.h>
int main() {
char str[20];
char str2[20];
gets(str);
gets(str2);
int len= strlen(str),len2= strlen(str2),i=0,j=0;
_Bool flag=0;
while (i<len){
if(str[i]==str2[j]){
i++;j++;
}
else{
i++;j=0;
}
if(j==len2) flag=1;
}
puts(flag?"字符串1包含字符2":"字符串1不包含字符串2");
}
- KMP算法
暴力解法虽然很好理解,但是可能会做一些毫无意义的比较
当发生不匹配时,又会重新开始比较下一个
但是不匹配的位置发生在第三个字符,而前面是a,b两个字符都匹配,显然完全没有必要再继续挨着去比较a和b,很明显不可能匹配。
实际上可以直接跳过b,直接跳过从后面继续判断,能节省不少的时间。
关键点就在于 怎么在程序中得知该不该跳过,又该跳过多少个字符。 所以我们在拿到子串的时候,就需要根据子串来计算一个叫做next的数组,与子串的长度相同,它存储了当不匹配发生在对应的位置上时,应该在哪一个位置开始继续比较。
求法:
从第一位开始依次推导。
next数组的第一位一定是0。
从第二位开始(用i表示),将第i-1个字符(也就是前一个)与其对应的next[i - 1] - 1位上的字符进行比较。
如果相等,那么next[i]位置的值就是next[i - 1] + 1
如果不相等,则继续向前计算一次next[next[i-1] - 1] - 1位置上的字符和第i-1个字符是否相同,直到找到相等的为止,并且这个位置对应的值加上1就是next[i]的值了,要是都已经到头了都没遇到相等的,那么next[i]直接等于1。
比如:
首先一二位明确是0和1,从第三位开始根据我们前面的规则:
首先判断str[next[1] - 1] == str[1],显然不相等。
此时无法继续向前走了,next[2]直接等于1即可。
第四位:
首先判断str[next[2] - 1] == str[2],发现相等。
此时next[2]直接等于next[2 - 1] + 1即可。
最后一位:
首先判断str[next[3] - 1] == str[3],发现相等。
此时next[3]直接等于next[3 - 1] + 1即可。
至此,next数组求解完毕,之后比较只需要多考虑一下next数组即可:
当不匹配发生在第三位时,此时next[2] = 1, 所以我们将第一个元素移动到c的位置重新开始比较:
发现不匹配,直接继续向后比较,重复上述操作,像这样这样跳着去比较就大大节省了时间。
#include <stdio.h>
#include <string.h>
int main() {
char str[20];
char str2[20];
gets(str);
gets(str2);
int len= strlen(str),len2= strlen(str2);
// 求next数组
int next[len2];
next[0]=0;
for (int i = 1; i <len2; ++i) {
int j=i-1;
while (1){
if(next[j]==0||str2[i-1]==str2[next[j]-1]){
next[i]=next[j]+1;
break;
}
j=next[j]-1;
}
}
int j=0,i=0;
// 判断字符匹配
while (i<len){
if(str[i]==str2[j]){
i++;j++;
}
else{
if(j==0)
i++;
else
j=next[j]-1;
}
if(j==len2)
break;
}
puts(j==len2?"ok":"no");
}