C/C++

C语言简介

​ C语言诞生于1970年~1973年,在 肯·汤普逊 和 丹尼斯·里奇 的编写下完成,归属于美国贝尔实验室。

​ C语言专门用于编写操作系统而发明的编程语言,所有天生适合对硬件编程,也以运行速度快而著称,也非常适合实现数据结构和算法。

​ 由于出现时间过早,有很多缺陷,已经存在着很多的陷阱,但是我们的前辈给我们总结了一些避免陷阱的经验教训 《c陷阱与缺陷》

​ C语言的语法很自由,但是也意味着危险。

​ 自由源于自律!

​ C89语法标准,默认是gcc语法编译器的语法标准

​ C99语法标准,对C语言的扩展和增强 Ubuntu 16.04 默认C99 (-std=gnu99 指定为C99语法标准)

​ C11语法标准,全新的升级

第一个C语言程序

程序员所写的代码不是标准C代码,需要一段程序把它翻译成标准C代码,负责翻译的程序叫做预处理器,翻译的过程叫预处理,需要被翻译的代码佳作预处理指令,以#开头的代码叫做预处理指令

gcc E hello.c 只执行hello.c 的预处理

#include 功能是导入头文件

​ #include

​ <> 从系统指定路径查找头文件并导入

​ #include “xxx.h”

​ “” 从当前的工作路径找头文件,如果找不到再从系统指定路径找并导入

#include
int main(){
   
    printf("Hello World!\n");
    return 0;
}

​ 1. vim xxx.c 创建.c源文件

​ 2. 编译代码,并保存退出

​ 3. gcc xxx.c 编译.c源文件,成功会得到a.out文件

		4.	./a.out	运行可执行文件

注意: gcc xxx.c && ./a.out 可以编译并执行

stdio.h 标准输入输出文件

​ 头文件:以.h结尾,里面存储的是辅助性代码,绝大多数都是函数的说明

main函数:

​ C语言中以函数为单位管理代码,一个函数就是具有某项功能的代码段

​ main函数是程序的执行入口,必须有且只能有一个

​ int 是一个数据类型,表示main函数的执行结果是个整数。

​ return 功能有两个:1.返回一个结果给函数的调用者

​ 2.结束函数进程

​ main函数的调用者是操作系统,它的返回值给了操作系统。* vim echo 程序 查询操作系统接收的return返回值

​ 正数 出现异常

​ 0 一切正常

​ 负数 出现错误

​ printf/scanf 是标准库中的函数,负责输出、输入数据

​ printf(“想要输出的内容”);

​ 转义字符:键盘上一些无法直接打印显示的符号,用一些特殊的字符组合来表示,这种特殊的字符组合称为转义字符, \n 换行

​ \r 回到行首

​ \t 制表符,相当于Tab键

​ \b 退格键

​ \a 响铃

​ \\ 表示一个 \

​ %% 表示一个%

​ C语言中以分号作为一行代码的结束,使用大括号划分区域

C语言编译器gcc

​ 负责把人能看懂地记录着代码的文件,翻译成计算机能看得懂的二进制文件,由预处理器,编译器,链接器组成。

​ gcc是由GNU社区为了编译Linux内核代码而开发的一款免费的编译器

​ gcc编译器常用编译参数:

​ -E 只显示预处理的结果到终端

​ -std=gnu99 设置C99语法标准

​ -c 只编译不链接

​ -o 指定编译结果名 -oname / -o name

​ -S 生成汇编代码

​ -I(大写 i) 指定头文件的加载路径 -I 加载路径

​ -Wall 尽可能多地产生警告

​ -Werror 将警告当错误处理

​ -l(小写L) 指定要加载(使用)的代码库 -lm(加载math.h库)

C代码变成可执行文件的详细过程

  1. 预处理 把源文件翻译成预处理文件
    gcc -E code.c 显示预处理结果到终端
    gcc -E code.c -o code.i 生成以.i结尾的预处理文件
  2. 编译 把预处理文件翻译成汇编文件
    gcc -S code.i 生成以.s结尾的汇编文件
  3. 汇编 把汇编文件翻译成二进制的目标文件
    gcc -c code.s 生成以.o结尾的目标文件
  4. 链接 把若干个目标文件合并成一个可执行文件
    gcc a.o b.o c.o ···· 默认生成一个a.out可执行文件

C语言的文件类型

​ .c 源文件

​ .h 头文件

​ .h.gch 头文件的编译结果文件,它会被优先使用

​ .i 预处理文件

​ .s 汇编文件

​ .o 目标文件

​ .a 静态库文件

​ .so 共享库文件

存储空间的单位

​ Bit(位) 比特 一个二进制位,只能存储0或1,计算机中存储数据的最小单位

​ byte 字节 八个二进制位,计算机存储器描述存储容量的基本单位

​ KB 1024字节

​ MB 1024KB

​ GB 1024MB

​ TB 1024GB

数据类型

​ 为什么要对数据进行分类?

  1. ​ 现实中的数据本身就自带类别属性

  2. 对数据进行分类可以节约存储空间,提升运行速度

    C语言中数据分类为两大类:自建类和内建类

    自建类:程序员自己设计的类型

    内建类:C语言自带的类型

    注意:运算符 sizeof 可以计算类型,变量的字节数

       整型:
    
       	signed		有符号
    
       		signed char					1Btye				-128~127
    
       		signed short					2 Btye				-32768~32767
    
       		signed int					4 Btye				+-20亿
    
       		signed long					4/8 Btye				+- 9开头的19位整数
    
       		signed long long				8 Btye
    
       	unsigned	无符号
    
       		unsigned char					1Btye				0~255
    
       		unsigned short					2 Btye				0~65535
    
       		unsigned int					4 Btye				0~40亿
    
       		unsigned long					4/8 Btye				0~1开头的20位整数
    
       		unsigned long long				8 Btye				
    

    signed不加就代表了加!由于定义无符号整型时比较麻烦,C标准库中把一些类型重定义成一些新的简单的类型名:(需要导入同文件:

    uint8_t uint16_t uint32_t uint64_t

    int8_t int16_t int32_t int64_t

       浮点型:	有小数部分的类型
    
       		float			单精度		4Btye
    
       		double		双精度		8Btye
    
       		long double				12Btye/16Btye
    

    注意:小数点后六位有效!编程时,尽量使用整性。

    注意:采用一定的算法对真实的浮点数型到二级制数据进行转化,

    这个过程比存储,读取整数要慢得多。

       模拟型:
    
       		字符型:	char
    
       				字符就是符号或者图案,在内存中存储的依然是整数,需要显示出字符时,
    
       				会根据ASCII表中对应的关系显示出对应的字符或者图案
    
       		‘0’	~	48		‘A’	~	65		‘a'	~	97		'\0'	~	空字符(NULL或空格)
    
       		布尔型:	bool
    
       				先有了C语言后有的bool类型,所以C语言中不可能有真正的布尔类型,
    
       				在头文件stdbool.h 中对布尔类型进行了模拟
    
       				bool			TRUE			FALSE
    

 # 变量和常亮

 		什么是变量:程序运行期间的数值可以发生变化的佳作变量,相当于一个存储数据的盒子

 		定义:	类型名	变量名;

 						int		number;

 						变量名取名规则:

 						1.由字母,数字,下划线组合

 						2.不能以数字开头

 						3.不能与C语言的32个关键字重名

 						4.见名知意(功能,类型,作用范围)

 		使用:

 				赋值:	变量名	=	常量;

 				参与运算:		变量名嵌入表达式

  C语言中变量的初始值是随机的,为了安全起见,一般在定义时初始化为0。

 关键字:

 char	short	int	long	void	float	double

 struct	union	enum	sizeof

 auto	const	static	volatile	register	

 typedef	extern	signed

 unsigned

 if	else	switch	case	default

 for	do	while

 break	continue	goto	

 变量的输入输出:

 		int	printf(const	char	*format,	· · ·  );

 		功能:输出数据

 		format:”双引号包含的提示信息	+	占位符“

 		· · · :变量名列表

 		printf返回值:输出字符的个数

 	类型占位符:C语言中通过类型占位符传递变量的类型

	signed:%hhd	%hd	%d	%ld	%lld

 unsigned:%hhu	%hu	%u	%lu	%llu

		float:%f

	double:%lf

 long double:%LF

 字符型  char:%c	

		int	scanf(const	char	*format, · · · );

		功能:输入数据

		format:“双引号包含占位符”

		· · · :	变量地址列表

		scanf返回值:成功输出的变量的个数

	scanf需要提供变量的地址(	&变量名	==	地址符	)

练习1:定义各种类型的变量并初始化,使用printf显示它们各自的值

练习2:定义各种类型的变量并初始化为0,使用scanf输入,使用printf显示

 #include
 #include
 int main(){
     uint16_t num = 0;
     printf("请输入num的值:");
     scanf("%hu",&num);
     printf("num = %hu\n",num);
     return 0;
 }
 ```

​ 什么是常量:程序运行期间数值不能改变的叫做常量

​ 100 默认int类型

​ 100l long

​ 100ll long long

​ 100u unsigned int

​ 100lu unsigned long

​ 100llu unsigned long long

​ 3.14 默认double

​ 3.14f float

​ 3.14l long double

​ 格式化输入输出

​ %nd 显示n个字符宽度,不够则补充空格,右对齐

​ %-nd 显示n个字符宽度,不够则补充空格,左对齐

​ %0nd 显示n个字符宽度,不够则补充0,右对齐

​ %n.mf 显示n个字符宽度(小数点也占一位),不够则补充则空格,m表示小数点后的位数(四舍五入),右对齐

​ %g 不显示小数点后,多于的0

​ 运算符

​ 自变运算符 ++、-- 使变量的值自动加一和减一

​ 前自变:++num/–num 立即生效

​ 后自变:num++/num-- 下一行语句才有效

​ 注意:不要再一行内,多次使用自变运算

#include
int mainf(){
   
    int num = 10;
    printf("num = %d\n",++num);
    printf("num = %d\n",num++);
    return 0;
}

​ 算术运算符:+ - * / %

​ 整数/整数 结果还是整数,无小数点,只保留整数部分

​ 整数%整数 取余

​ / % 除数不能为0,否则就会浮点数例外,(核心已转存),这是个运行报错,一旦产生程序立即停止,后面不在执行

​ 关系运算符:> < >= <= == !=

​ 比较后得到结果为0(不成立)或1(成立),比较结果可以继续参与后续的计算

​ int n = -100;

​ if(10 < n <100) 恒成立

​ 注意: == 建议常量放左边

​ 逻辑运算符:&& || !

​ 先把运算的对象转化成逻辑值,0转化为假,非0转化为真

​ A && B 一假即假

​ A || B 一真即真

​ !A 求反

​ && 和 || 运算符的短路特性:

	int num = 10;
	if( 100 < num && num++ ){
   	printf("Yes\n");}
	printf("%d\n",num);
//	当左边的值以及确定了结果,则右边不执行
    int n = 0;
	if( (100 > num) || (num++) && (n=10) ){
   	parint("ES");}
	printf("%d\n",n);

​ 三目运算符:判断A的值是否为真,为真则执行B,否则就执行C A ? B : C ;

​ 赋值运算符

​ a = 10; a += 10; a -= 10; a *= 10; a /= 10; a %= 10;

​ 位运算符: & | ~ ^ << >>

分支语言

 1. if(表达式){
   	表达式为真,则执行代码,否则不执行	}

 2. if(表达式){
   
    	表达式为真执行
    }else{
   
    	表达式为假执行
    }

 3. if(表达式1){
   
       表达式1真执行
    }else if(表达式2){
   
       表达式2真执行
       ······
    }else{
   
       如果以上都为假,则执行
    }

练习:输入三个整数,从小到大显示

练习:输入一个年份,判断是否是瑞年

练习:输入一个年份和月份,判断该月有多少天

练习:输入一个成绩判断等级

90~100 A

80~89 B

70~79 C

60~69 D

0~59 E

other 成绩有误

类型转换

​ 只有相同类型的数据才能运算,如果类型不相同的数据需要先转换相同类型后再进行计算。

​ 自动类型转换:

​ 转换规则:以不丢失数据为基础,可以适当牺牲一些空间

​ 1.字节少的,向字节多的转

​ 2.有符号的,向无符号的转

​ 3.整数,向浮点型转化

​ 注意: char与short如果与不同类型的数据运算时,会优先提升为int类型后参与运算

​ sizeof( 不计算内容,以大的字节数为基准! )

​ 强制类型转换:

​ (新类型名)数据;

​ 这种方式有可能会丢失数据,谨慎使用

switch分支语句

switch(n){
   	//	n可以是常量,变量,表达式(表达式的结果必须是整数常量,不能是浮点数类型)。
    case val:	//	val必须数常量,如果val==n则打开开关
        · · ·
        break;	//	关闭开关。
    case val2:
    default:	//	无开关打开,则执行!并且,放在任意位置都可以最后打开。
}

​ case 1 ··· 3:可以表示[a,b]的范围,但是只有在GNU编译器才支持该语法,不建议使用。

练习:输入一个月份,判断它是什么季节

(春:123月份,夏:456月份,秋:789月份,冬:10,11,12月份)

练习:输入一个月份,判断该月有多少天。(不考虑闰年)

for循环语句

​ 循环就是一种让代码反复执行的方法,到达你想要效果for循环是一种非常灵活,变样多样且危险的循环

//	for循环,一般使用一个变量来引导的执行,该变量叫做循环变量
for([1];[2];[3]){
   
	[4]
}
/*	[1]给循环变量初始值,C99以上标准才可以在此处定义变量	std:gun99
	[2]判断循环变量是否到达边界,进入循环变量
	[4]被反复执行的代码,称之为循环体
	[3]改变循环变量,防止出现死循环,一般对循环进行自加,自减
*/

​ 大括号建议使用:

​ 1.建议上下对齐

​ 2.如果循环体中,只有一行代码,大括号可以省略

​ 但是不利于扩展,一般的商业代码都要求大括号不能省略

//	for循环的各种写法
for(;;)
{
   
    //	死循环
}

int i = 0;
for(;i<10;i++){
   }

for(int i = 0 ;; i ++ ){
   
    if( i > 10 ) break;
}

for(int i = 0 ; i < 10 ;){
   
    · · ·
    i++;
}

练习:计算出所有的水仙花数(abc=a3+b3+c^3)

练习:输入一个正整数,判断是否是素数

while循环语句

while(表达式)
{
   
    //	循环体
}
//	当表单式为真执行循环体,直到表达式为假,结束循环
do{
   
    //	循环体
}while(表达式);
//	先执行循环体,再判断循环条件,该循环至少执行一次

​ 当明确直到循环次数时,用for循环

​ while循环专门负责不知道循环次数

循环嵌套

​ 循环里嵌套循环,外成循环执行一次,内层循环执行n次

练习:输入一个数,判断是否是回文数

练习:模拟输入六位密码,输入的密码正确显示“登录成功”,输入错误提示还有几次机会,并输入密码,最多错三次,否则显示“账号已锁定,请联系柜台”,并结束程序

练习:打印九九乘法表

练习:白钱白鸡问题

练习:计算出100~1000之间所有素数

跳转语句

  1. goto
    可以在函数内,任意跳转。

    标签名:
        ···
        goto 标签名
    /*		可能会破坏已经设计好的分支或者循环结构,因此绝大多数公司禁止使用goto
        	但是在驱动编程时特别时候处理异常
    */
        练习:计算N的的阶乘,不能使用循环语句实现
        	int s = 1;
    lx:		
    		s *= N--;
    		if( N )
            {
         
                goto lx;
            }
    		printf("%d\n",s);
    
  2. break

    1.switch中关闭case执行开关
    2.	跳出循环,只能跳一层循环
    
  3. continue

    结束本次循环,进入下一次循环
    
  4. return

    1.	结束函数的执行,返回到调用位置
    2.	返回一个数据给函数的调用者
    

数组

​ 什么是数组:变量的组合,是一种批量定义类型相同的变量的方式

//	定义:	变量名	数组名[数量];
	int array[100];
//	使用方式:数组名[下标号];(下标号:从零开始。范围:[0,n-1])
/*	数组遍历:与for循环配合,使用循环变量当做数组的下标
	数组的初始化:		int array[100] = {0,···};
	1.因为数组的值默认是随机的,所有一般都会进行初始化。
	2.初始化的数据过多,编译器会产生警告并把多出的数据丢弃
	3.初始化的数据不够,编译器会默认在末尾补0零
	4.   
	5.这种初始化语法只能在定义中使用,并且只能逐个赋值,不能整体赋值
	6.初始化时,数组的数量可以省略,编译器会自动统计初始化中数据的个数,并且告诉数据确认数组的数量,一旦数组数量确定,后期无法更改
	sizeof(array)/sizeof(array[0]) == 数组的成员个数
	sizeof(array) == 数组的字节长度
	sizeof(array[0]) == 数组的成员的字节数
*/

练习:定义一个长度为10的数组并进行初始化,计算出最大值,最小值和平均值

练习:定义一个长度为10的数组并初始化,进行升序排列

数组越界

为了程序的编译,运行效率,编译器不去检查数组的下表越界

​ 数组越界的后果:

​ 1.段错误

​ 2.一些正常

​ 3.脏数据

​ 在使用数组的过程中,要注意不要越界

练习4:定义一个长度为10的数据并初始化,找出数组中第二大的数,不允许排序

#include 
int main()
{
   
    int array[10] = {
   -1,23,9,-32,-93,345,76,43,26,10};
    int max_1 = array[0] > array[1] ? array[0] : array[1];
    int max_2 = array[0] < array[1] ? array[0] : array[1];
    for(int i = 1 ; i < 10 ; i ++ )
    {
   
        if( max_1 < array[i] )
        {
   
            max_2 = max_1;
            max_1 = array[i];
        }
        else if( max_2 < array[i] )
        {
   
            max_2 = array[i];
        }
    }
}

二维数组

​ 一维数组相当于把变量排成一排,通过编号访问

​ 二维数组相当于把变量排成一个矩阵,通过行号和列号访问定义

​ 定义: 类型名 数组名[行数][列数]

​ 使用: 数组名[行下标][列下标]

​ 遍历: 需要于双重循环配合使用,一般外层循环负责遍历行,内层循环负责遍历列

二维数组初始化:

​ 类型名 数组名[行数][列数] = { {第一行},{第二行}, · · · · · ,{} };

练习:定义一个5*5的二维数组,找出数组中最大的值的坐标

变长数组

​ 定义数组时使用变量作为数组的长度,在代码编译期间数据的长度是不确定的,当运行到数组的定义语句时数据到长度才最终确定下来,这种数组称为变长数组

​ 优点:可以根据实际情况确定数组大小,以此节约内存空间

​ 缺点:不能进行初始化,因为初始化发生在程序编译时

练习:输入两个整数n,m(1<=n,m<=6),然后输入数组array[m][n],各元素的值,然后统计每个元素之和,统计非零元素的个数,计算出所有元素的平均值,大于平均值的元素个数

#include 
int main(){
   
    int m = 0 , n = 0;
    printf("请输入m,n的值")scanf("%d%d",&m,&n);
    int arr[m][n];
    double sum = 0 , avg = 0;
    int nozero_count = 0 , more_count = 0;
    
    printf("请输入各个元素的数据:");
    for(int i = 0 ; i < m ; i ++ )
    {
   
        for(int j = 0 ; j < n ; j ++ )
        {
   
            scanf("%d",&arr[i][j]);
            sum += arr[i][j];
            if( arr[i][j] )
            {
   
                nozero_count ++;
            }
        }
        avg = sum / (n*m);
        for(int i = 0 ; i < m ; i ++ )
        {
   
            for(int j = 0 ; j < n ; j ++ )
            {
   
                if( arr[i][j] > avg )
                {
   
                    more_count ++ ;
                }
            }
        }
    }
    printf("sum=%lf\n",sum);
    printf("nozero_count=%d\n",nozero_count);
    printf("avg=%lf\n",avg);
    printf("more_count"more_count);
    return 0;
}

练习:定义一个5*5的二维数组并初始化,找出最小值的坐标,并计算出最小值一圈数据之和

#include 
int main(){
   
    int arr[5][5] = {
   
        {
   1,2,3,4,5},
        {
   6,7,8,9,10}.
        {
   5,6,7,8,9},
        {
   4,1,4,7,4},
        {
   7,1,5,3,1}
    };
    int min = arr[0][0] , min_x = 0 , min_y = 0 ;
    for(int i = 0 ; i < 5 ; i ++ )
    {
   
        for(int j = 0 ; j < 5 ; j ++ )
        {
   
            if( min < arr[i][j] )
            {
   
                min = arr[i][j];
                min_x = i;
                min_y = y;
            }
        }
    }
    
    int sum = 0;
    for(int x = min_x-1 ; x <= min_x+1 ; x ++ )
    {
   
        for(int y = min_x-1 ; y <= min_y+1 ; y ++ )
        {
   
            if( 0 <= x && x <= 4 && 0 <= y && y <= 4 )
            {
   
                sum += arr[x][y];
            }
        }
    }
    printf("sum = %d\n",sum - min);
    return 0;
}

练习:输入N,显示N层杨辉三角

1
1	1
1	2	1
1	3	3	1
1	4	6	4	1

练习:输入一个日期(yyyy-mm-dd),计算该日期距离1年1月1日过了多少天

走迷宫代码

​ 数据分析:

​ 1.定义二维字符数组作为迷宫地图

​ 2.定义变量记录角色的位置 x y

​ 逻辑分析:

​ 一.进入死循环:

​ 1.显示地图,遍历二维数组

​ 2.等待获取方向键并处理

​ 判读前方是不是路(空格字符)

​ 如果是:

​ 1.把旧位置变成空格字符

​ 2.把新位置变成"@"

​ 3.更新角色位置坐标 x y

​ 3.判断是否到达出口

​ 如果是:程序结束

#include 
#include 	//	系统命令头文件
#include 	//	自定义头文件
#include 	//	

int main(){
   
    char maze[10][10] = {
   
        {
   '#','#','#','#','#','#','#','#','#','#'},
        {
   '#',' ','#',' ','#',' ',' ',' ',' ','#'},
        {
   '#','@','#',' ','#',' ','#','#',' ','#'},
        {
   '#',' ',' ',' ','#',' ','#','#',' ','#'},
        {
   '#',' ','#','#','#',' ','#','#',' ','#'},
        {
   '#',' ','#','#','#',' ',' ','#',' ','#'},
        {
   '#',' ','#','#','#',' ','#',' ',' ','#'},
        {
   '#',' ','#','#','#',' ','#','#',' ','#'},
        {
   '#',' ',' ',' ',' ',' ','#','#',' ',' '},
        {
   '#','#','#','#','#','#','#','#','#','#'}
    };
    //	记录角色初始位置坐标:man_x,man_y;
    char man_x = 2, man_y = 1;
    time_t start_time = time(NULL);
    for(;;)
    {
   
        //	判断是否到达出口
        if( man_y == 9 )
        {
   
            printf("到达出口,游戏结束!")
            return 0;
        }
        //	清理屏幕
        system("clear");
        //	显示地图
        for(int i = 0 ; i < 10 ; i ++ )
        {
   
            for(int j = 0 ; j < 10 ; j ++ )
            {
   
                printf("%c ",maze[i][j]);
            }
            printf("\n");
        }
        
        //	获取反向键处理
        switch( getch() )
        {
   
            case 183:	case 'w':	//	上
                if( ' ' == maze[man_x-1][man_y] )
                {
   
                    maze[man_x][man_y] = ' ';
                    maze[--man_x][man_y] = '@';
                }
                break;
            case 184:	case 's':	//	下
                if( ' ' == maze[man_x+1][man_y] )
                {
   
                    maze[man_x][man_y] = ' ';
                    maze[++man_x][man_y] = '@';
                }
                break;
            case 186:	case 'a':	//	左
                if( ' ' == maze[man_x][man_y-1] )
                {
   
                    maze[man_x][man_y] = ' ';
                    maze[man_x][--man_y] = '@';
                }
                break;
            case 185:	case 'd':	//	右
                if( ' ' == maze[man_x][man_y+1] )
                {
   
                    maze[man_x][man_y] = ' ';
                    maze[man_x][++man_y] = '@';
                }
                break;
        }
            if( 8 == man_x && 9 == man_y )
    		{
   
        		printf("游戏胜利,过了%lu秒!\n",time(NULL)-start_time);
        		return 0;
   		 	}
    }

    return 0;
}
推箱子

​ 数据分析:

​ 1.确定数值与字符的对应关系

​ 0 ‘ ’

​ 1 ‘@’

​ 2 ‘#’

​ 3 ‘$’

​ 4 ‘o’

​ 5 ‘@’

​ 7 ‘$’

​ 2.定义8*8的整数地图并初始化

​ 3.定义记录角色位置的变量 x y

​ 4.定义记录步数的变量

​ 逻辑分析:

​ 一.进入死循环

​ 1.清理屏幕,显示地图

​ 2.判断是否游戏胜利

​ 3.获取方向键并处理

​ 1.前方是路、目标点

​ 2.前方是箱子,箱子的前方是路或目标点

进制转换

​ 为什么要使用二进制,八进制,十进制,十六进制?

​ 1.因为现在的CPU只能识别高低电平,只能对二进制的数据进行计算

​ 2.虽然二进制可以直接被CPU识别计算,但是不方便书写,记录,把二进制的数据转换成八进制是为了方便记录到文档

​ 3.由于CPU的位数的不断发展不断增加,由于8位逐渐发展到现在的64位,因此八进制就不能满足需求了,所以发展出了十六进制,但是由于历史原因八进制不能完全淘汰

十进制转N进制:

​ 1.对十进制数进行求余数,然后继续对商求余,直到商为零,倒取余数,得到结果

​ 2.求权法:用数值去减 最高位的权值*数值 (不能出现负数),直到把数据减完

练习:输入一个正整数m,输入一个n(n>=2),显示m的n进制数,超过10的用字母表示 10->A

二进制转十进制:每位加权求和

二进制位转八进制位:每三位二进制位转换为一位八进制位

二进制转十六进制:每四位对应一位十六进制数

在C语言中,以0开头的数是八进制数,以0x开头的数是十六进制数;

​ %o 以八进制显示

​ %x 以十六进制显示

​ %#o %#x 可以把对应的前缀显示出来

原码,反码,补码

​ 原码:数据的二进制

​ 反码:正数反码就是它的原码

​ 负数的反码就是它原码的,除符号位外,其它按位求反

​ 补码:正数补码就是它们的原码

​ 负数的补码是它的反码+1

内存中所有数据的存储都是以他的补码的形式存储

1.负数转换为二进制

2.符号位不变,其余按位求反,得到反码

3.反码+1得到补码

补码转数据:

​ 1.先看是否有符号位

			- 如果是无符号的,直接转成原码
			- 如果是有符号的,且最高位是零,也就直接转换成十进制,最高位不动
  • 有符号且最高位是1:
    • 补码-1得到反码
    • 符号位不变,其它位,按位求反得到原码
      - 原码转化成十进制

位运算符

​ A & B 按位与

​ A | B 按位或

​ ~A 按位求反 // 在位运算中优先级最高

​ A ^ B 按位异或 相同为0,相异为1

​ A << n 把A的补码向左移动n位 左边丢弃右面补0

​ A >> N 把A的补码右移N位 右边丢弃,左边补符号位

(左移动,相当于乘2;右移动,相当于除2;

练习:输入一个整数,把它的4~7位设置为1010,其它位不能变;

优先级:单目 算数 位 关系 逻辑 三目 赋值

表达式中出现了位运算符,转换成二进制计算

函数

​ 一段具有某项功能的代码,是C语言中管理代码的最小单位

​ 把代码封装成一个个函数,可以方便管理和调用代码

  • 函数的分类:

    • 标准库函数

      • 由C语言标准委员会为C语言以函数的形式提供的一些基础功能,被封装在libc.so库中,使用时需要包含对应的头文件,通过 对应的函数名(实参)方式即可调用标准库中的函数

      • 随机数:

        #include 
        #include 
        srand( time(NULL) );
        int num = rand();
        
        
        > ​	练习:	获取10个[100,1000]之间的随机数,循环不超过10次
        >
        > ​					rand() % 901+100
        >
        > ​	练习:	红球6组,每组从1-33中抽取一个,六个相互不重复。
        >
        > ​					然后篮球是从1-16中抽取一个数字
        >
      > ​					随机产生一组	双色球号码
      
    • 系统函数

    • 是操作系统以函数的接口形式提供的一系列功能,但是它不是真正意义上的函数

  • 内存管理,文件管理,文件IO,信号处理,进程管理,进程通信,线程管理,线程同步,网络通信

    • 第三方库函数

      • 由第三方提供的 开源或者收费的代码库
      • MD5 加密算法
    • Json 序列化,反序列化

    • Xml 配置文件解析算法

    • 自定义函数

      • 为了个更好地管理代码,减少代码冗余把代码封装成函数形式

        • 函数申明:

          • 函数申明,为了告诉其他代码该函数的调用格式

          • 返回值类型 函数名(类型1 形式参数1,类型2 形式参数,· · · );

            1.C语言中函数名一般全小写,下划线分隔

            2.如果返回值不需要,则写void

            3.如果不需要形式参数,建议也需要写void

        4.就算形式参数类型名相同,也要每个都加类型名

        • 函数定义:函数的实现代码

          • 返回值类型 函数名(类型1 形式参数1,类型2 形式参数,· · · ){

            ​ // 函数体

            ​ return 返回值类型;

            }

        • 函数调用

          • 函数名(实参)

练习:实现一个函数,判断是否是素数,调用它显示100~1000以内所有的素数

练习:输入两个日期(yyyy-mm-dd),计算两个日期间相隔多少天

练习:实现一个函数,判断一个整数是否是回文数,调用它显示1亿~10亿的所有有回文数

练习:输入一个数,显示它的补码

#include 
int main(){
   
    int n;
    scanf("%d",&n);
    char bits[32] = {
   };
    for(int i = 0 ; i < 32 ; i ++ )
    {
   
        bits[i] = n >> i & 1;
    }
    for(int i = 31 ; i >= 0 ; i -- )
    {
   
        printf("%hhd",bits[i]);
    }
    return 0;
}

练习:计算出100的阶乘

#include 
int main(){
   
    char rets[256] = {
   1};
    int cnt = 1;
    /*将一个数放到rets数组中,rets[0]为这个数的个位*/
    for(int i = 2 ; i <= 100 ; i ++ )	
    {
   
        int carry = 0;	//	记录进位
        for(int j = 0 ; j < cnt ; j ++ )
        {
   
            int num = rets[j] * i + carry;	//	将rets数组记录的数,的每一位从个位开始,乘以将要
            rets[j] = num % 10;				//	被阶乘的数i,放入num中。更新rets数组中,正在执行
            carry = num/10;		//	的rets[j]。carry表示记录num中未被rets数组记录的*进位*
        }
        while( carry )		//		将进位carry赋值给rets数组中,当前的最高位cnt
        {
   	//	rets数组的每一位记录,阶乘的一位
            rets[cnt++] = carry % 10;	//	rets数组的最高位cnt,取carry进位的末位
            carry /= 10;	//	更新carry进位的值,除去以存储的末位
        }
    }
    //	打印数组
    for(int i = cnt-1 ; i >= 0 ; i -- )
    {
   
        printf("%hhd",rets[i]);
    }
    return 0;
}

函数传参

​ 1.形式参数,函数内定义的变量都只属于它所在的函数,出了该函数就不能再用

​ 2.普通 实参与形参之间是通过赋值的方式传递数据的(单向值传递)

​ 3.return 其实是将数据存放在一个公共区域(函数都可以访问),如果不写return语句,那么就会读该区域原来的数据,就调用一个 垃圾 数据

​ 4.当数组作为函数的参数时,中括号中的长度就会丢失,需要额外增加一个变量传递数组的长度

​ 5.数组作为函数参数传递是,传递是数组的首地址,叫做“址传递”。函数和函数的调用者可以共享一个数组!

练习:使用函数,实现找出数组中的最大值

练习:实现一个函数,对数组进行升序排序

练习:实现一个函数,查找数组是否存在某个值,如果存在则返回数组的下标。

设计函数的建议:

​ 1.一个函数最好就解决一个问题,减低错误率,提高可读性

​ 2.尽量减少函数之间的依赖层数(降低耦合度)

​ 3.数据由调用者提供,结果返回给调用者(提高函数的通用性)

​ 4.考虑函数的非法参数,可以通过返回值的方式告诉调用者参数有误,也可以通过注释方式写明情况

进程映像

程序:存储在磁盘上的可执行文件(二进制文件,脚本文件)

进程:在系统中运行中的程序

进程映像:指的是进程内存的分布情况

​ text 代码段 存储二进制指令,常量数据,权限是只读。强制地修改就会产生段错误

​ data 数据段 初始化的全局变量/初始化的静态局部变量

​ bss 静态数据段 未初始化的全局变量/未初始化的局部变量。在程序进程运行前,该段内存会自动清理为0

​ stack 栈 局部变量 操作系统自动管理,会自动申请和释放内存(小)

​ heap 堆 由程序员手动管理 内存(大)

变量分类:局部变量 和 全局变量

  • 全局变量:定义在函数外的变量

    • 存储位置:data(初始化) 或者 bss(未初始化)
    • 生命周期:从程序开始到程序结束
    • 作用范围:在程序的任意位置都可以使用
  • 局部变量:

    • 存储位置:stack 栈内存
    • 生命周期:从函数调用开始,到函数结束
    • 作用范围:只能在函数内使用

    块变量: 定义在语句块中的变量

    • 存储位置:stack 栈内存
    • 生命周期:从函数调用开始,到函数结束
    • 作用范围:只能在函数内使用

注意:局部变量可以和全局变量同名,在函数内使用局部变量会屏蔽同名的全局变量,块变量在语句块内

会屏蔽同名的全局变量,块变量在语句块内会屏蔽同名的全局变量,局部变量,因此建议全局变量首字母大写

类型限定符

auto 用于定义自动分配内存,释放内存的变量(局部变量),不加就代表了加

注意:全局变量不能用auto修饰的

C11中用于自动识别类型

extern 声明变量 extern 类型名 变量名

​ 告诉编译器此变量已经在别处定义过了,请放心使用

​ 它只能临时让编译通过,但是在 链接时,如果找不到该变量,依旧会报错

​ 在多文件编程中使用:假设a.c中定义了全局变量N,想要在b.c中使用N,需要在使用前声明变量

static

  • 改变存储位置:

    • 改变局部变量的存储位置,从stack改为data或者bss(取决于是否初始化)
      • 被static修饰的局部变量,叫做静态局部变量
  • 延长生命周期:

    • 延长了局部变量的生命周期,直到进程结束
  • 限制作用范围:

    • 限制 全局变量 · 函数 的使用范围,只能在本文件内使用

    可以防止全局变量,函数命名冲突,也可以防止被别的文件使用

const

	- "保护"变量的值不能被显示地修改,但是能可以通过访问内存来修改值
	- 但是如果修饰的是初始化的全局变量,初始化的静态变量,则该变量会从data改为text,变成“常量”

volatile

	- 如果变量的值没有被显示的修改,那么在使用该变量时,不会从内存中读取,而是继续使用上一次读取的结果,这个过程叫做取值优化,一般变量都会进行。
	- 变量被volatile修饰后,编译器不对该变量进行取值优化,每次都是从内存中重新读取
	- 一般硬件编程,多线程编程时会使用到

register

  • 申请把变量的存储介质由内存改为寄存器,由于寄存器数量有限,不一定申请成功
  • 存储介质:硬盘 - 内存 - 高级缓存 - 寄存器
  • 寄村器变量不能取地址

typedef

	- 类型重定义,在定义变量时,在类型前加typedef,那么变量名就变成了这个类型的新名字
	- 注意:typedef不是替换关系
小项目:五子棋

​ 需要的数据:

  • 定义一个字符类型的数组15*15

  • 定义变量,记录落子位置

  • 定义变量,记录该落子为哪位棋手(黑棋“@” ,白棋 “$" )

    逻辑:

    • 定义需要的数据
    • 是否需要对数据进行初始化
    • 清理屏幕,显示棋盘
    • 落子。判断坐标是否合法,该位置不能有其它棋子。
    • 判断:检查是否五子连珠
    • 交换棋手
#include 
#include 
#include 

// 棋盘数组
char board[15][15];
//	棋子的位置
char key_x , key_y;
//	角色字符	黑棋 @	白棋 $
char role = '@';
//	初始化棋盘
void init_board(void){
   
    for(int i = 0 ; i < 15 ; i ++ )
    {
   
        for(int j = 0 ; j < 15 ; j ++ )
        {
   
            board[i][j] = '*';
        }
    }
}
//	显示刷新棋盘
void show_board (void){
   
    system("clear");
    for(int i = 0 ; i < 15 ; i ++ )
    {
   
        for(int j = 0 ; j < 15 ; j ++ )
        {
   
            printf("%c ",board[i][j]);
        }
        printf("\n");
    }
}

void get_key(void){
   
    while(1){
   
        printf("请%c输入棋子坐标:",role);
        scanf("%hhd%hhd",&key_x,&key_y);
        //	检查坐标是否合法
        if( key_x < 0 || key_y < 0 || key_x > 14 || key_y > 14 ){
   
            printf("坐标不合法,请重新输入!");
            continue;
        }
        //	检查是否有棋子
        if( '*' != board[key_x][key_y] ){
   
            printf("该位置已有棋子!请重新输入!");
            continue;
        }
        board[key_x][key_y] = role;
        return;
    }
}
//	重写get_key函数,并起名Luozi
void Luozi(void){
   
    printf("请%c落子",role);
    while(1){
   
        printf("\33[%hhd;%hhdH",key_x+1,(key_y+1)*2);
        switch( getch() ){
   
            case 183:	key_x > 0	&&	key_x--;	break;
            case 184:	key_x < 14	&&	key_x++;	break;
            case 186:	key_y > 0	&&	key_y--;	break;
            case 185:	key_y < 14	&&	key_y++;	break;
            case 10:	if( '*' == board[x][y] ){
   
                board[key_x][key_y] = role;
                return;
            }
        }
    }
}

//	检查是否五子连珠
bool is_win(void){
   
    //	左上 右下
    int cnt = 0;
    //	左上
/*    for(int x = key_x-1, y = key_y-1 ; x >= 0 && y >= 0 ; x-- , y-- )
    {
        if( role == board[x][y] ){
            cnt ++ ;
        }else{
            break;
        }
    }
    if( cnt >= 4 ) return true;
*/  
    if( count_key(-1,0)  + count_key(1,0)  >= 4 ) return true;
    if( count_key(0,-1)  + count_key(0,1)  >= 4 ) return true;
    if( count_key(-1,-1) + count_key(1,1)  >= 4 ) return true;
    if( count_key(-1,1)  + count_key(1,-1) >= 4 ) return true;
    return false;
}
//	检查是否五子连珠重写
int count_check(int ox,int oy){
   
    int count = 0;
    for(int x=key_x+ox, y=key_y+oy; x>0&&x<15&&y>0&&y<15; x+=ox,y+=oy ){
   
        if( role == board[x][y] ){
   
            count ++ ;
        }else{
   
            break;
        }
    }
    return count;
}

int main(){
   
    init();
    for(;;)
    {
   
    	show_board();
//      get_key();
        Louzi();
        if( /*is_win()*/ )
        {
   
            show_board();
            printf("%c胜利\n",role);
            return 0;
        }
        role = '@'==role? '$' : '@';
    }
    return 0;
}


函数递归

​ 函数自己调用自己的这种行为叫做函数递归,可能会产生死循环。

​ 递归是可以实现分治的这种思想,把一个大问题,分解成多个小问题,知道所有问题全部解决

  • 如何写好递归
    • 写好一个出口
      • 解决一个小问题
        • 调用自己

练习:计算前N项斐波那契数列

1 1 2 3 5 8 13 21 ······

  • 递归函数每一次调用都会在栈内产生一份自己的拷贝,直到执行到达出口,才会释放一层递归函数,因为此与循环相比递归非常消耗内存,速度很慢,因此如果能用循环解决的问题不要使用递归
  • 递归优缺点:
    • 好理解,思路清晰
    • 很好地解决非线性问题
    • 耗费内存,速度很慢

练习:使用递归模拟N层 汉诺塔

#include 

void hanio(int n,char star,char m,char end){
   
    if( 1 == n )
    {
   
        printf("%d:form %c to %c\n",star,end);
    }
    else
    {
   
        hanio(n-1,star,end,m);
        printf("%d:from %c to %c\n",star,end);
        hanio(n-1,m,star,end);
    }
}

思考:显示出0~9的全排列

指针

  • 什么是指针?

    • 指针是一种特殊的数据类型,使用它可以定义指针变量,
      指针变量存储的是整数数据,代表了内存的编号。
    • 通过编号可以直接访问对应的内存
  • 为什么要使用用指针?

    • 函数之间是相互独立的,但是有使用需要共享变量。
      传参是单向值传递
      全局变量容易命名冲突
      数组使用不便,还需要额外传递数组长度
      虽然函数命名空间是独立的,但是地址空间是同一个,因此指针可以解决共享变量的问题
    • 由于函数之间的传参是值传递(内存拷贝),对于字节数较多的变量,值传递的效率较低,如果传递变量的地址只需要传递 4 或 8 个字节。
    • 堆内存无法取名字,它不像data,bss,stack让变量名与内存建立联系,只能使用指针记录堆内存的地址来访问对应的内存
  • 如何使用指针:

    • 定义: 类型名* 指针变量名_p;
      类型名 *指针变量名_p;

    • 指针变量与普通变量的用法有很多区别,建议在取名时,以p结尾以式区分

    • 指针的类型表示,指针指向的是什么类型变量的地址。它决定了通过这个指针变量可以访问的字节数

    • 一个*号只能定义一个指针变量

      • int *p1,p2,p3; // p1是指针,p2,p3是int

        int *p1,*p2,*p3; // p1,p2,p3都是指针

      • 指针变量与普通变量一样默认值是随机的,一般初始化为NULL(空指针)

    • 赋值:变量名_p = 地址;// 必须是有意义且有权限的地址

      • 指向栈内存:
        • int num;
        • int* p = #
      • 指向堆内存:
        • int* p = malloc(4);
    • 解引用: *变量名_p

      • 通过指针变量中记录的内存的编号去访问对应的内存,该过程可能会产生段错误,原因是里面存储的内存编号是非法的。
      • 注意:访问的字节数由指针定义时类型决定,后面都不会改变

练习:实现一个交换变量的函数,用着函数进行排序

练习:实现一个函数计算两个整数的最大公约数,最小公倍数,最大公约数用return返回,最小公倍数使用指针输出型参数

#include 
int max_min_num(int x,int y,int* p){
   
    int max = 1;
    for(int i = 2 ; i <= x ; i ++ ){
   
        if( x % i == 0 && y % i == 0 ) max = i;
    }
    for(int i = x*y ; i >= x && i >= y ; i-- ){
   
        if( 0 == i%x && 0 == i%y ) *p = i;
    }
    return max;
}
int main(){
   
    
}

使用指针时需要注意的问题:

  • 空指针:

    • 值为NULL的指针变量叫做空指针
      • 如果对空指针解引用,一定产生段错误
        • NULL一般作为一种错误标志,当一个函数的返回值是指针类型时,可以使用NULL指针作为返回出错的结果
  • 如何避免空指针带来错误:

    • 使用来历不明的指针前先进性判断
    if( NULL == p )		if( !p )
    //	注意:NULL在绝大部分操作系统上是0,也有个别老的操作系统中是1
    
    • 当函数的参数是指针,别人传给你的指针可能是空指针
    • 从函数获取的返回值是指针类型时,可能会返回空指针
  • 野指针:

    • 指向不确定的内存空间的指针叫做野指针
    • 对野指针解引用的后果:
      • 一切正常
      • 段错误
      • 脏数据
    • 野指针比空指针的危害更严重,因为它无法判断出来,而且可能是隐藏性的错误,短时间不暴露。
    • 所有的野指针都是程序员自己制造出来的,如何避免产生野指针
      • 定义指针变量时一定要初始化( int *p = NULL )
      • 函数不要返回栈内存(函数内的局部变量)的地址
      • 指针指向的内存被释放后,指针变量要及时置空NULL
  • 指针的运算

    • 指针变量中存储的是整数,理论上整数可以使用的运算符它都可以使用,
      但是绝大部分运算符无意义的
    • 指针 + n:指针+指针类型的宽度*n <==> 相当于指针前进n个元素
    • 指针 - n:指针-指针类型的宽度*n <==> 相当于指针后退n个元素
    • 指针- 指针:(指针 - 指针)/指针类型宽度 <==> 相当于计算两个指针之间,间隔多少个指针元素
  • 指针与const

    • 当我们为了提高传参效率而使用指针为函数传参时,传参效率提高了,但是变量被共享在被修改的风险,可以使用const保护指针所指向内存
    • const int* p; // 保护指针所指向的内存不被修改
    • int const *p; // 保护指针所指向的内存不被修改
    • int* const p; // 保护指针变量不被修改
    • const int* const p; // 指针变量和指所指向内存都不能改
    • int const * const p; // 指针变量和指所指向内存都不能改
  • 指针数组和数组指针

    • 指针数组:由指针变量组成的数组,指针数组。

      ​ 它的成员变量都是 类型相同的指针变量

      ​ 类型* arr[长度];

      ​ int *arr[10] = {};

    • 数组指针:专门指向数组的指值

      ​ 类型 (*int)arr[长度];

  • 数组名和指针:

    • 数组名就是一种特殊的指针

    • 数组名是常量,不能修改它的值,数组名没有自己的存储空间,它与数组首地址之间是映射关系。
      数组名 == &数组名

      当指针变量指向数组首地址时,指针可以当做数组名使用,数组名也可以当做指针使用

      数组名[i] == *(数组名+i)

      *(p+i) == p[i];

      注意:当数组作为函数的参数时锐变成了指针,所以长度丢失

    • 指针是变量是拥有自己存储空间,它与所指向的内存,是指向关系

  • 二级指针

    • 二级指针就是指向指针的指针,里面存储的是指针变量的地址

    • 定义:类型名** 变量名_pp

    • 赋值:变量名_pp = &指针变量

    • 解引用:*变量名_pp <> 指针变量
      **变量名_pp <
      > 变量的地址

      注意:但需要函数之间共享指针变量,传递指针的地址(二级指针)

  • 函数指针

    • 函数名就是一个地址,函数名代表了函数在代码段(data)中所处的入口位置
    • 函数指针就是指向函数的指针,它里面存储的是函数在代码段处的入口位置
    • 返回值类型 (*p)(类型1,类型2,······ );
    • 可以通过函数指针,把函数当做参数传递给另一个函数,这种方式称之为函数的回调模式;

堆内存(heap)

一.什么是堆内存

​ 是进程的一个内存段(text/data/bss/heeap/stack),由程序员手动管理

​ 优点足够大,缺点使用麻烦

二.为什么要使用堆内存

	1. 随程序的复杂,数据量变多

 		2. 其它内存段的申请释放不受控制,堆内存的申请释放受控制,可以适时地节约内存

三.如何使用堆内存

​ 注意:C语言中没有控制堆内存的语句,只能使用C语言标准库中的函数

​ #include

​ void *malloc (size_t size);

功能:从堆内存中申请size个字节的内存,申请的内存数据的值不确定

返回值:成功申请返回值的连续内存的首地址,失败返回NULL

​ void free(void *prt);

功能:释放一块堆内存

prt:要释放堆指针的首地址

你可能感兴趣的:(c语言)