第一章 前言
1.什么是程序(就是软件):
计算机程序(传统PC、嵌入式)。
程序=算法+数据结构
1.2数据结构:
数据类型+组织方式
1.3算法:
对操作的描述,要求计算机的操作步骤,分逻辑型(嵌入式会用到)、数学数据型的算法。
2.进程
跑起来的程序,进程是动态概念,程序是静态概念。
3.程序怎么来
编程、编译、执行
编译:预处理、编译、汇编、链接(生成0和1)
4.计算机语言
机器语言(0和1),符号语言(汇编),高级语言(C语言,接近人类语言)
高级语言:结合化语言(c)、面向对象语言(c++,java,php)
5.学习方法
对视频的代码:
一定要先理解
一开始可以对着打(肌肉记忆)
默写打
错误提示要积累
不要丢弃错误代码,一定要调试通过
多总结(多写博文)
第二章 初识
1.工具:
gcc==mingw(编译器套件)
2.c语言的基础框架
3.数据的表现形式
3.1 变量
计算机的变量规则:变量名、变量值、内存存储单元、数据类型。
3.1.1 变量名(标识符)
不能数字开头,区分大小写,英文,驼峰命名法(小写开头新单词大写)。
下划线开头是为了区分跟系统里的变量。
3.1.2 数据类型
整型数:整数,4个字节,单片机中基本上是2个字节
字符型:ASCII码(字符型和整数型有所联系),1个字节
浮点型:4个字节
4.输入输出
4.1 printf
printf("格式控制",输出列表);
%x–打印出16进制;%p–打印出地址
小数默认打印6位
4.2 scanf
扫描键盘有没有东西输入
scanf("格式控制",地址列表);
最好不要再格式中输入其他的字符
最好输入不同的数据类型要分次输入
当输入不同数据的时候中间不要输入任何其他的符号
4.3其他
puts、gets 、putchar、getchar
4.3.1 puts()
与printf的区别
1、自动加入换行符
2、printf有的花样比较多
重点:
1、变量四要素:类型、名、值、地址
第三章 流程控制
if:代数法交换数值(就是相当于有一个容器)。//用temp来交换
switch case:并列关系。
while:循环语句,括号里也是判断0或者1。
for:表达式1和3可以不写,表达式3不写就是一直循环,表达式1不写就要写在外面。
//死循环:
while(1);
for(;;);
重点:
1、在scanf中不能规定float
错误示例:
float x;
scanf("%.2f",&x);
2、输出错误返回值 (在中断输入)echo %errorlevel%
3、for循环的嵌套:i是列,j是行
4、水仙花数
水仙花数是指一个n位数(n≥3),它的每个位上的数字的n次幂之和等于它本身。
例如,153是一个水仙花数,因为1^3 + 5^3 + 3^3 = 153。
第四章 数组
定义数组:几个元素、什么类型、数组名
用下标来访问,空间连续
数组初始化和遍历:可以结合循环控制语句
int arry[5]={1,2,3,4,5};
//可以写成int arry[]={1,2,3,4,5};
sizeof是关键字不是函数api,计算一段空间内存的大小。
1. 斐波那契数列
应求的数列为前两项相加
#include<stdio.h>
//斐波那契数列
int main()
{
int date[30];
puts("请输入前两位");
scanf("%d%d",&date[0],&date[1]);
for(int i=0;i<sizeof(date)/sizeof(date[0]);i++)
{
if(i>=2)
{
date[i]=date[i-1]+date[i-2];
}
printf("%d ",date[i]);
}
return 0;
}
2.冒泡排序
最外层循环几次:整个数组的元素个数-1
内层的循环次数:最外层的循环次数-第几次外层循环
#include<stdio.h>
//冒泡排序
int main()
{
int date[]={22,55,44,3,45,66,44,55};
int temp;
int num=sizeof(date)/sizeof(date[0]);
for(int i=0;i<num-1;i++)
{
for(int j=0;j<num-1-i;j++)
{
if(date[j]>date[j+1])
{
temp=date[j];
date[j]=date[j+1];
date[j+1]=temp;
}
}
}
for(int i=0;i<num;i++)
{
printf(" %d",date[i]);
}
return 0;
}
总结:每一轮两个元素之间进行比较,每一轮遍历执行,每一轮结束后找到最大或最小的元素放在数据结构的最前面或者最后面。下一轮将上一轮排序出来的最大或者最小元素排除后再进行比较。
3.简单选择排序法
每一次都将第一个与其他数相比,如果比较的不符合结果则进行交换。
外层循环次数:元素个数-1(最外层是不变的对比数)
内层循环次数:从(外层循环次数+1)开始循环到最大的元素(内层是会变的数)
#include<stdio.h>
//简单选择排序
int main()
{
int date[]={22,55,44,3,45,66,44,55};
int temp;
int num=sizeof(date)/sizeof(date[0]);
for(int i=0;i<num-1;i++)
{
for(int j=i+1;j<num;j++)
{
if(date[i]<date[j])
{
temp=date[i];
date[i]=date[j];
date[j]=temp;
}
}
}
for(int i=0;i<num;i++)
{
printf(" %d",date[i]);
}
return 0;
}
总结:每一轮比较,都选出最大或者最小的一个元素排放在数据结构的最前面或者最后面。
4.二维数组
定义:
类型 数组名[行][列];
int temp[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int temp[3][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
int temp[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};
二维数组的遍历
#include<stdio.h>
//二维数组的遍历
int main()
{
int temp[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
for(int i=0;i<3;i++)
{
for(int j=0;j<4;j++)
{
printf("%d\t",temp[i][j]);
}
printf("\n");
}
return 0;
}
4.1 找二维数组中的最大
#include<stdio.h>
//二维数组的遍历
int main()
{
int temp[3][4]={1,2,3,4,5,6,7,8,9,10,11,12};
int max;
int rowOfMax,columnOfMax;
max =temp[0][0];
for(int i=0;i<3;i++)
{
for(int j=0;j<4;j++)
{
if(max<temp[i][j])
{
max=temp[i][j];
rowOfMax=i;
columnOfMax=j;
}
}
}
printf("第%d行第%d列:%d",rowOfMax,columnOfMax,max);
return 0;
}
重点:
可以不写行但是不能不写列。
int temp[][4]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};//可以
int temp[3][]={{1,2,3,4},{5,6,7,8},{9,10,11,12}};//不行
第五章 函数
避免代码冗长、模块化设计
1.函数的要素
函数名、参数列表(形参)、返回值
2.形参和实参
形参是虚拟参数,实参是程序中写入的参数。
局部变量的作用域要会区分
形参的生命周期是在函数被调用到调用结束期间。
3.三目运算
当非a既b的时候
z=x>y?x:y
//当x>y的时候取x,否则取y
4.函数的嵌套
在一个函数中嵌套另外一个函数
5.函数的递归
在一个函数中调用自己(可能会有死循环),所以要有一个退出条件。
一定要找到停止的那一项。
一定要先把公式列出来后面才好进行编写代码。
5.1 求阶乘
#include<stdio.h>
//阶乘
int factorial(int x)
{
if(x==1)
{
return 1;
}
else if(x>1)
{
return factorial(x-1)*x;
}
}
int main()
{
int num;
puts("你要求多大的阶乘");
scanf("%d",&num);
printf("结果为%d",factorial(num));
return 0;
}
6.数组作为函数的参数
数组名作为实际参数
数组名代表整个数组的首地址
7.二维数组作为函数的参数
合法的写法与其定义一样:行不能为空。
#include<stdio.h>
void initArry(int tempArry[][3],int len)
{
//printf("%d\n",len/3);
for(int i=0;i<len/3;i++)
{
for(int j=0;j<3;j++)
{
printf("请输入第%d行,第%d个数据\n",i,j);
scanf("%d",&tempArry[i][j]);
}
}
}
void printfArry(int tempArry[][3],int len)
{
for(int i=0;i<len/3;i++)
{
for(int j=0;j<3;j++)
{
printf("%d\t",tempArry[i][j]);
}
printf("\n");
}
}
int getMaxDate(int tempArry[][3],int len)
{
int max=tempArry[0][0];
for(int i=0;i<len/3;i++)
{
for(int j=0;j<3;j++)
{
if(max<tempArry[i][j])
{
max=tempArry[i][j];
}
}
}
return max;
}
int main()
{
int date[2][3];
int len=sizeof(date)/sizeof(date[0][0]);
initArry(date,len);
printfArry(date,len);
printf("max:%d",getMaxDate(date,len));
return 0;
}
重点:
1、形参和实参数据相同,但是地址不相同,所以要用到指针。
2、如果函数的实现在main函数后面,那么函数需要在main函数之前做一个声明才不会报警告。
//例子
#include<stdio.h>
//比较两个数
int compare(int x,int y);
int main()
{
int date1,date2;
puts("请输入两个数");
scanf("%d%d",&date1,&date2);
printf("最大的数为%d",compare(date1,date2));
return 0;
}
int compare(int x,int y)
{
return x>y?x:y;
}
3、在开发中可以先把框架打出来,用伪代码填补。
4、长整型还是4个字节
long int
//(4*8=32位)
5、形参中不存在数组的概念,即使有在定义中有规定了数组的大小,也是无效。其中传递的是地址,是数组的首地址。
#include<stdio.h>
//阶乘
int printfArry(int arry[],int len)
{
for(int i=0;i<len;i++)
{
printf("%d ",arry[i]);
}
printf("\nfun:arry的大小%d",sizeof(arry));//8
}
int main()
{
int date[]={55,44,22,33,44,55,4};
int len = sizeof(date)/sizeof(date[0]);
printf("main:这个数组总共有%d个元素\n",len);//28
printf("mian:arry的大小%d\n",sizeof(date));
printfArry(date,len);
return 0;
}
6、在操作系统中(不论几位操作系统),8个字节表示一个地址。
7、形参的地址跟实参的地址是不一样的。数值传递,但是内存不同,但是传递的是数组就不一样了。
#include<stdio.h>
int add(int arry[])//传递的地址
{
arry[0]=arry[0]+1000;
printf("fun:area=%p,value=%d\n",arry,arry[0]);
}
int main()
{
int date[]={55,44,22,33,44,55,4};
add(date);
printf("main:area=%p,value=%d\n",date,date[0]);
return 0;
}
8、 当全局变量,放在函数后的时候,前面有调用到全局变量的函数会报错。
9、函数调用的目的:强调通过调用函数得到某种结果。
10、当函数中没有办法返回多个值的时候,就可以结合全局变量。
第六章 指针
0.变量的访问方式
1、通过变量名来访问内存空间的数据
2、通过地址来访问内存空间的数据
1.什么是指针?
指针==地址:地址就是指针;地址的别名。
指针也是变量也有四要素。
*(地址):可以访问变量的值
int a=10;
printf("数值为:%d",a);//10
//可以写成
printf("数值为:%d",*(&a));//10
什么是指针变量?
存放指针的变量–指针==地址–存放地址的变量
2.“*”到底有什么含义
int *p;//这里的*是标识符,代表着这个是一个指针变量
printf("数值为:%d",*(&a));//这里的*是一个取值运算符,把内存中的数据取出来
3.指针变量为什么也要定义类型?
”*“取值运算符,是会根据指针变量的类型来,访问不同大小的空间
#include<stdio.h>
int main()
{
int a=0x1234;
/*在内存中存16进制的数字的方法:
4个字节=32位
所以int a的内存空间就有32位
每位存16进制数的1个位
所以该内存空间的数据是:
00000000|00000000|0001(1)0010(2)|0011(3)0100(4)|
*/
int *p;
char *c;
p=&a;
c=&a;
printf("p=%p\n",p);
printf("c=%p\n",c);//地址一样
printf("*p=%x\n",*p);
printf("*c=%x\n",*c);//打印出的16进制数不一样,此时char只访问后8位,因为char只有1个字节。
printf("++p=%p\n",++p);//地址加4
printf("++c=%p\n",++c);//地址加1
return 0;
}
4.为什么要用到指针?
指针指向固定的区域:单片机开发中需要用到(arm bootloader)
#include <stdio.h>
int main()
{
int a=10;
printf("%p\n",&a);//地址:000000000061FE1C
//把一个值写入一个固定的地址:000000000061FE20
volatile unsigned int* b=(volatile unsigned int*)0x000000000061FE20;
//volatile防止编译器自动优化,以防安排到别的地址去了。
printf("%p\n",&b);//指针b的地址
printf("%p\n",b);
*b=20;
printf("b的数值:%d\n",*(int*)0x000000000061FE20);
return 0;
}
5.指针指向数组
int a[3]={1,2,3};
int* p;
p=&a[0];//指向首地址
p=a;//指向数组名,但是在c中,这个数组名不能是形参的数组名,因为形参不占有实际的内存
5.1 指针的增量和数组的关系
当指针自增1时,地址下移一个同类型的量,偏移后还是一个地址。
5.1.1 指针遍历数组
指针访问数组比下标访问数组更快
#include <stdio.h>
void initArry(int* ptr,int len)
{
for(int i=0;i<len;i++)
{
scanf("%d",ptr+i);
}
}
int main()
{
int arry[5];
int len=sizeof(arry)/sizeof(arry[0]);
int* p=arry;
puts("请输入5个元素:");
initArry(p,len);
for(int i=0;i<len;i++)
{
printf("%d ",*(p+i));
}
return 0;
}
5.2 指针和数组名的区别
1、指针也可以当成数组名使用 ,数组名也可以当成指针操作
但是!!!
数组名不能自增,因为 !!!数组名等同于指针常量!!!
//错误
printf("%d",*arry++)
2、sizeof()中的区别
int arry[3];
int* p;
p= arry;
printf("%d",sizeof(arry));//12:因为这个int型数组中有3个元素。
printf("%d",sizeof(p));//8:因为在操作系统中用八个字节来表示地址
5.2.1 指针变量和指针常量
数组名等同于指针常量
数组名相当于数组的首地址,当你定义好这个数组之后,并不能改变这个数组的首地址,所以数组名不能自增,相当于一个常量。
6.二维数组和指针(感觉很猛)
二维数组的概念引申:(父子数组)可以理解为二维数组也是一维数组,只不过里面存放的元素是一个个一维数组。
地址空间连续:
a是二维数组的数组名,表示的是二维数组的地址;
a[0]是{1,3,5,7}(也就是子数组的数组名,代表的是子数组的数组名也是子数组1的地址;
所以a++的地址是a[1];//a==a[0];
a[0]++的地址是元素“3”的地址;//a[0]==&a[0][0];
//*a=a[0];*a取出来的内容是a[0],也就是a[0][0]的地址
#include <stdio.h>
int main()
{
int arry[3][2]={{1,36},{55,44},{77,8}};
int* p;
p=arry;
printf("*p=%d\n",*p);//1
printf("a[0]=%p\n",arry[0]);//地址
printf("*(&(&arry[0][0]))=%d\n",*(&(arry[0][0])));//1
printf("p=%p,arry=%p\n",p,arry);//000000000061FE00
printf("p+1=%p,arry+1=%p\n",p+1,arry+1);//000000000061FE08
printf("*(arry)=%p,*(arry+1)=%p\n",*arry,*(arry+1));000000000061FE00,000000000061FE08,地址差8个字节
printf("a[1]=%p\n",arry[1]);//000000000061FE08
printf("&a[1]=%p\n",&arry[1]);//000000000061FE08
return 0;
}
6.1数组指针(真正等同与二维数组名)
定义一个指针指向一个数组。
//定义
int (*arryPtr)[4];
#include <stdio.h>
void printfArry(int (*p)[2],int row,int col)
{
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
{
printf("%d ",*(*(p+i)+j));
}
printf("\n");
}
}
int getArry(int (*p)[2],int row,int col)
{
int ret=*(*(p+row-1)+col-1);
return ret;
}
int main()s
{
int arry[3][2]={{1,36},{55,44},{77,8}};
int (*p)[2];
p=arry;
puts("arry:");
printfArry(p,3,2);
int row,column;
puts("请输入要查询的行列号");
scanf("%d%d",&row,&column);
printf("搜索到的结果为:%d",getArry(p,row,column));
return 0;
}
7.函数指针
7.1 定义:
储存函数的地址
函数名就是起始地址
int fun(int a,int b);
int (*p)(int a,int b);
//int* p(int a,int b);指针函数,一个函数返回一个指针
7.2 使用:
函数的使用可以通过函数名,也可以通过函数指针,函数指针也讲究类型。
#include <stdio.h>
void printfFun()
{
printf("this is a printFun\n");
}
int funPtr(int* date)
{
return ++(*date);
}
int main()
{
void (*FunPtr)();
FunPtr=printfFun;
(*FunPtr)();
int a=99;
int* p;
p=&a;
int (*ptrPlus)(int* date)=funPtr;
printf("a++=%d\n",(*ptrPlus)(p));
return 0;
}
7.3 实战:(努力理解)
根据程序运行过程中的不同情况,调用不同的函数(有点像Java的接口)。
例子:
函数指针可以不写形参的变量名但是要写类型。
#include <stdio.h>
int getSmall(int a,int b)
{
return a>b?a:b;
}
int getBig(int a,int b)
{
return a<b?a:b;
}
int getSum(int a,int b)
{
return a+b;
}
int main()
{
int date1,date2,cmd;
puts("请输入要进行操作的两个数据:");
scanf("%d%d",&date1,&date2);
puts("请输入操作符");
scanf("%d",&cmd);
int (*funPtr)(int ,int );
switch(cmd)
{
case 1:
funPtr=getSmall;
break;
case 2:
funPtr=getBig;
break;
case 3:
funPtr=getSum;
break;
default:
break;
}
printf("ret=%d\n",funPtr(date1,date2));
return 0;
}
线程中回调函数的底层逻辑就是一个函数指针,Qt中的信号与槽也是函数指针。
8.指针数组
存指针的数组,元素都是指针
int* p[4];
8.1 实战:
定义一个指针数组,来指向函数。
函数指针数组的定义:
int (*p[3])(int,int);
(*p[0])(a,b);//取内容的时候也要注意写法
#include <stdio.h>
int getSmall(int a,int b)
{
return a>b?a:b;
}
int getBig(int a,int b)
{
return a<b?a:b;
}
int getSum(int a,int b)
{
return a+b;
}
int main()
{
int date1,date2;
puts("请输入要进行操作的两个数据:");
scanf("%d%d",&date1,&date2);
int (*ptrArry[3])(int,int)={getSmall,getBig,getSum};
for(int i=0; i<3;i++)
{
printf("ret=%d\n",(*ptrArry[i])(date1,date2));
}
return 0;
}
9.指针函数
返回值是指针的函数
例题:
#include <stdio.h>
int* getsoc(int Arry[][3],int pos)
{
int* temp;
temp=Arry+pos;
return temp;
}
int main()
{
int soc[2][3]=
{
{55,6,7},
{88,9,8}
};
puts("请输入你要查的学生的号数");
int num;
scanf("%d",&num);
int *p;
p=getsoc(soc,num);
for(int i=0;i<3;i++)
{
printf("%d ",*(p++));
}
return 0;
}
应用:
#include <stdio.h>
//找到有不及格的学生的学号
void getFailStudent(int soc[2][3])
{
int flag=0;
int (*ptr)[3]=soc;
for(int i=0;i<2;i++)
{
flag=0;
for(int j=0;j<3;j++)
{
while(60 > *(*(ptr+i)+j))
{
flag=1;
break;
}
}
if(flag=1)
{
printf("%d ",i);
}
}
}
int main()
{
int soc[2][3]=
{
{55,6,7},
{88,9,8}
};
//函数返回一个一维数组,用来存储学生的学号,参数需要传入一个原始二维数组。
getFailStudent(soc);
return 0;
}
总结(指针数组、数组指针、指针函数、函数指针)
理解:
先看本质是什么,在看形容词
例子:
1、指针函数:
本质是函数:fun(int,int)
形容词是指针:int* fun(int,int);
2、数组指针:
本质是指针:(*p)
形容词是数组:int (*p)[4];
指针数组 | 数组指针 | 指针函数 | 函数指针 | |
---|---|---|---|---|
定义 | 存放指针的数组 | 指向数组的指针 | 返回指针的函数 | 指向函数的指针 |
声明 | int* a[4]; | int (*p)[4] | int* fun(int,int) | int (*fun)(int,int) |
10.二级(多级)指针
10.1 定义:
int **p;
可以跟一级指针一样理解。
一级指针存放普通的变量,而二级指针存放的是指针变量。
一级指针存放普通变量的地址,二级指针存放指针变量的地址。
10.2 为什么?
为什么要二级指针,因为当一级指针(A)指向一个指针(B)时,A无法通过两次取内容操作来获得B指向的空间的内容。
#include <stdio.h>
int main()
{
int a=100;
int* p=&a ;
//p == &a == 1
printf("p = %p---",p);
printf("&a = %p\n",&a);
//*p == a == 100
printf("*p = %d---",*p);
printf("a = %d\n",a);
int* p1 = &p;
//p1 == &p == 2
printf("p1 = %p---",p1);
printf("&p = %p\n",&p);
//*p1 == p == &a
printf("*p1 = %p---",*p1);
printf("p = %p---",p);
printf("&a = %p\n",&a);
//但是(*(*p1))是错误语法不能取到a的值
int **p2 = &p;
//p2 == &p == 2
printf("p2 = %p---",p2);
printf("&p = %p\n",&p);
//*p2 == p == &a
printf("*p2 = %p---",*p2);
printf("p = %p---",p);
printf("&a = %p\n",&a);
//*(*p2) == *p == a
printf("*(*p2) = %d---",*(*p2));
printf("*p = %d---",*p);
printf("a = %d",a);
return 0;
}
10.3 实战
通过函数来改变指针的指向(其意义就跟调用函数要修改某个变量的值一样)
#include <stdio.h>
void getsoc(int Arry[][3],int pos,int** p)
{
(*p)=Arry+pos;
}
int main()
{
int soc[2][3]=
{
{55,6,7},
{88,9,8}
};
puts("请输入你要查的学生的号数");
int num;
scanf("%d",&num);
int *p;
getsoc(soc,num,&p);
for(int i=0;i<3;i++)
{
printf("%d ",*(p++));
}
return 0;
}
10.4 二维数组和二级指针
二级指针不能直接指向二维数组,因为当二级指针直接指向二维数组时,二级指针的取内内容,确实是一维数组,但是这个数组是个野指针,没有真正的指向。
//正确的写法
int soc[2][3]=
{
{55,6,7},
{88,9,8}
};
int (*p)[3]=soc;
int** p1=&p;
//错误写法
int** p2 = soc;
//这时*p2是个野指针
#include <stdio.h>
int main()
{
int soc[2][3]=
{
{55,6,7},
{88,9,8}
};
puts("请输入你要查找的行列");
int r,c;
scanf("%d%d",&r,&c);
int (*p)[3]=soc;
int** p2 = &p;
*(*(p2+r)+c)=100;
printf("%d",soc[r][c]);
return 0;
}
指针思路总结:
地址和指针,指针和数组,数组中的二维数组,二维数组引申到数组指针,数组指针同步到函数指针,函数指针引申到指针数组,指针数组同步到指针函数;最后扩充多级指针
解题方法:从前往后梳理
1、int a;
2、int* p;
3、int** p;int*p1 =&a;p=&p1;
4、int a[10];
5、int* p[10];//指针数组
6、int (*p)[10];//数组指针
7、int (**p)[10];
8、int* (*p)[10];
9、int (*p)(int);
10、int (*(p[10]))(int)
11、int (*(*p)(int,int))(int)
重点:
1、指针变量自己本身也有一个地址
2、volatile:防止编译器自动优化
3、在c中,指针不能指向形参的数组名,因为形参不占有实际的内存
4、“*”运算符的优先级高于“++”
5、指针遍历数组的时候可能会出现数组越界的可能,所以如果要再次遍历,要让指针回到数组的首位
//可以这样解决
int arry[5]={1,2,3,4,5};
int* ptr;
int i;
for(i=0,ptr=arry;i<5;i++)
{
printf("%d",*ptr++);
}
6、for的神奇用法
int i;
int b;
for(i=0,b=33;i<5;i++);
//for(表达式1,表达式1-1...;表达式2;表达式3);
7、指针也可以当成数组名使用 ,数组名也可以当成指针操作
int arry[3]={1,2,3};
int* p;
p=arry;
printf("%d",p[3]);//3
printf("%d",*(arry+1))//2
8、sizeof中指针和数组名的区别
int arry[3];
int* p;
p= arry;
printf("%d",sizeof(arry));//12:因为这个int型数组中有3个元素。
printf("%d",sizeof(p));//8:因为在操作系统中用八个字节来表示地址
9、gdb调试
gcc 【文件名】 -g
gdb 【生成的程序】
(gdb)r//运行
(gdb)q//按q退出
基本上指针出现错误就是:Segmentation fault.
10、数组反转:
//找到中间数,然后头尾交换
void arryReversal(int* ptr,int len)
{
int j=len-1;
for(int i=0;i<len/2;i++)
{
//或者int j=len-1-i;
int temp=*(ptr+i);
*(ptr+i)=*(ptr+j);
*(ptr+j)=temp;
j--;
}
}
11、二维数组的概念引申:可以理解为二维数组也是一维数组,只不过里面存放的元素是一个个一维数组。
12、数组名就是地址
13、不能有这种写法:
printf("%d",*(*p));//不能这样写
14、函数名就是起始地址 (相当于数组名)。
15、函数指针可以不写形参的变量名但是要写类型。
16、函数指针数组的定义:
int (*p[3])(int,int);
(*p[0])(a,b);//取内容的时候也要注意写法
//数组函数指针
int* p[3]=(fun1,fun2,fun3);
17、当你需要用到一个函数来改变一个指针的指向的时候可以用到二级指针。
18、二维数组会用到数组指针(数组指针:指针指向数组的地址)。
19、二维数组可以等价为一个数组指针
int soc[2][3]=
{
{55,6,7},
{88,9,8}
};
int (*p)[3]=soc;
第七章 字符串
字符数组
1.声明
char date[]={"h","e","l","l","o"};//字符串变量,可以修改
char date1[]="hello";
chars *pdate="hello";//字符串常量不能进行修改
2.遍历
//不用for循环来遍历
可以用%s来printf,用putchar、puts来输出
3.字符串常量
只能用,不能进行修改。
指针指向字符串常量,可以保存字符串的地址,但是在对其进行明确的指向之前(野指针),不能对内存空间进行修改操作。
4.字符串和字符数组的区别
可以通过sizeof来区分,字符串以‘\0’来结束。基本上很多API都以‘\0’来区分。
char a[]="hello";
char b[]={"h","e","l","l","o"};
sizeof(a);//6
sizeof(b);//5
5.sizeof和strlen的区别
strlen的传参是一个指针。
strlen | sizeof | |
---|---|---|
计算有效的个数,不算’\0’ | 会自动在末尾加上’\0’ | |
char date[128]=“hello” | 5==strlen(date); | 128=sizeof(date); |
char* p = date; | 5==strlen§; | 8==sizeof§; |
6.malloc
动态开辟字符串,只要被调用不管是在哪里调用,这段空间都不会被释放。
6.1 函数原型
传入一个你要开辟多大的空间,然后返回指向你要开辟的那段空间的指针。
当你malloc多次后,最后一次malloc之前的内存段都是野的,他们在程序结束之前一直占用着空间,这样迟早会占满堆,所以要把他们free掉,防止内存泄露。
void* malloc(size_t size)//形参是int型
#include <stdlib.h>//需要的头文件
6.2 free
函数原型
void free(void* p);//释放资源,防止野指针,防止指针悬挂
free(p);
p=NULL;
//free后要指向空
6.3 空间越界
当你申请的空间比你实际上指向的空间要大的时候,会出现越界。
当出现越界就可以使用relloc。
6.4 relloc
函数原型
void* relloc(void* ptr,size_t t);
//ptr:原本的地址
//t:后面要增加的空间
6.5 memset
将一段分配的空间设置为想要的内容,返回的是一个指针。
函数原型
void* memset(void*p,int a,size_t num);
//p:起始地址也可以理解为目标指针
//a:你要设置的内容
//num:大小
6.7 calloc
分配一段空间,按照几个元素,每个元素多大来分配,并且把每个元素都置为零
函数原型
void* calloc(size_t num,size_t size);
7.常见操作字符串的API
头文件:
#include <string.h>
7.1 strcpy
函数原型
char* strcpy(char* dest,char* src);//src:源地址,dest:目的地
7.2 puts和gets
这两个函数传递的全部都是地址。
puts("zhanghuawei");
char str[128]={'\0'};
scanf("%s",str);
gets(str);
7.3 strncpy
把源地址的前几个字节复制给目的地址
函数原型
char* strncpy(char* dest,char* src,int n);//src:源地址,dest:目的地
7.4 自己实现拷贝函数
strcpy
#include <stdio.h>
#include <stdlib.h>
void* strCopy(char* dest,char* src)
{
char* orahginal = dest;
while(*src != '\0')
{
*dest = *src;
src+=sizeof(char);
dest+=sizeof(char);
}
*dest='\0';
return orahginal;
}
int main()
{
//char str[128] ={'0'};也可以
char* str=malloc(128);
char* strScr ="adkan";
puts(strCopy(str,strScr));
return 0;
}
#include <stdio.h>
#include <stdlib.h>
void* strCopy(char* dest,char* src)
{
char* orahginal = dest;
//while((*dest++ = *src++)!= '\0' );
while(*src != '\0')
{
*dest = *src;
src+=sizeof(char);
dest+=sizeof(char);
//*dest++ = *src++ ;
}
*dest='\0';
return orahginal;
}
int main()
{
//char str[128] ={'0'};也可以
char* str=malloc(128);
char* strScr =malloc(128);
puts("请输入要被拷贝的数据");
gets(strScr);
puts(strCopy(str,strScr));
return 0;
}
strncpy
当要被拷贝的字符串比你要的位数来的小,后面的位数全部置零。
#include <stdio.h>
#include <stdlib.h>
void* strNCopy(char* dest,char* psrc,int num)
{
char* ret=dest;
while(*psrc!='\0'&& num > 0)
{
*dest++ = *psrc++;
num --;
}
while(num>0)
{
*dest='\0';
dest++;
num--;
}
return ret;
}
int main()
{
char* pdest=malloc(128);
char* psrc=malloc(128);
puts("请输入被拷贝的字符串");
gets(psrc);
puts(strNCopy(pdest,psrc,6));
return 0;
}
7.5 assert(断言)
与if…else…差不多
如果表达式为假则先向stdderr(标准的错误)打印出一条出错信息,然后通过abort来终止程序的运行。
缺点:开销大
相当于在表达式不成立时,结束程序。
#include <assert.h>
//assert(表达式);
7.6 strcat
将一个字符串加到另外一个字符串后面。
将src的字符串(包括’\0’)加到删除’\0’后的dest后面,然后返回dest的地址。这个函数需要确定src的字符不变,且dest的空间要够大。
函数原型:
//与strcpy差不多
char* strcat(char* ptrdest,char* ptrsrc);
#include <stdio.h>
#include <stdlib.h>
char* strCat(char* dest,char* src)
{
char* orahginal = dest;
while(*dest != '\0')
{
*dest++;
}
if(*dest =='\0')
{
while(*src !='\0')
{
*dest = *src;
dest++;
src++;
}
}
*dest='\0';
return orahginal;
}
int main()
{
char dest[128]="zhw";
//如果是char* dest="zhw";就不行,因为这个是一个字符串常量,无法改变。
char* strScr =" 111";
printf("%p\n",dest);
printf("%p\n",strScr);
puts(strCat(dest,strScr));
return 0;
}
不同操作系统的库对strcat的写法不一样
//另一种写法
char* strCat(char* dest,char* src)
{
char* orahginal = dest;
strcpy(dest+strlen(dest),src);
return orahginal;
}
7.7 strcmp
当两个字符串相等时,放回0;当字符串1小于字符串2时,返回负数;当字符串1大于字符串2时,返回正数。
在源码中,当两个字符串不一样时,比较的是不一样的地方两个字符的asscii码值的差(字符1-字符2)。(这样不是很严谨)
函数原型
int strcmp(char* str1,char* str2);
//strncmp,比较两个字符串的前几字节
int strncmp(char*str1,char* str2,size_t n);
char* p1="zhw";
char* p2="zhz";
printf("%d\n",strcmp(p1,p2));
//结果是-1,因为w小于z
重点:
1、用指针来指向字符串叫做字符串常量,无法对其中一个字母进行修改,会引发段错误。
char date[]={"h","e","l","l","o"};//字符串变量,可以修改
char date1[]="hello";//字符串常量不能进行修改
2、计算机一般用8字节来表示一个地址,所以不管是什么指针都是八个字节。
3、函数名不能用sizeof,除非声明一个函数指针,在用函数指针sizeof。
4、free后,指针要指向空
5、(*)取内容符号比(++)自增优先级更高。
6、赋值后可以多一层判断。
while((*dest++ = *src++)!= '\0' );
if((*dest++ = *src++)!= '\0');
7、字符串常量
char* p="zhw";//这是一个字符串常量,里面的值无法改变,空间是固定的。
第八章 结构体
1.用途
用很多类型的数据来表达一个整体,这个时候就需要用到结构体。
类比与数组,数组是同种类型的数据集合,而结构体是不同种类型的数据结合。
2.声明
结构体是个模板,里面的数据不一定每个都要用。
结构体也可也理解为一种变量类型。
struct Mystruct//struct:关键字告诉程序这个是个结构体
{
int num;
char name[128];
//结构体中一般不初始化变量,当然要初始化也可以
};//以冒号结尾
每一个成员都是结构体中的一个域,所以结构体中的成员也叫做成员表,也叫做于表。
3.定义和初始化结构体变量
struct Mystruct mystruct1;
mystruct1.num =10;//点运算符来访问结构体中的成员变量
strcpy(mystruct1.name,"zhw");
printf("%d,%s",mystruct1.num,mystruct1.name);
struct Mystruct mystruct2={2,"lll"};//跟给数组初始化一样
4.结构体数组
声明:
struct Mystruct structArry[2]={{1,"zhw"},{2,"jjj"}};//有点像二维数组,但是二维数组是同一类型数据的集合
怎么操作数组,你就怎么操作结构体数组
5.结构体指针
指针就是地址!!!
保存结构体的地址就是结构体指针,结构体指针换汤不换药。
struct Student
{
int num;
char name[128];
}
struct Student stu1;
struct Student* stuptr=&stu1;
stuptr->num=1;
5.1 结构体指针和结构体数组
//投票题目
#include <stdio.h>
#include <string.h>
struct Candidate
{
char name[32];
int numOfTicks;
};
void InitCandidate(struct Candidate* p,int numOfCandidate)
{
for(int i=0;i<numOfCandidate;i++)
{
puts("请输入候选人的名字");
gets(p->name);
p->numOfTicks=0;
p++;
}
}
void searchCandidate(char* temp,struct Candidate* p,int numOfCandidate,int* waiver)
{
while(numOfCandidate)
{
if(strcmp(temp,p->name)==0)
{
p->numOfTicks++;
return;
}
p++;
numOfCandidate--;
}
printf("查无此人\n");
//*waiver=*waiver+1;
(*waiver)++;
}
void voting(struct Candidate* p,int numOfCandidate,int* waiver)
{
char temp[32];
struct Candidate* bat=p;
for(int i=0;i<5;i++)
{
bat=p;
memset(temp,'\0',sizeof(temp));
puts("你要投给谁?");
gets(temp);
//查找出那个人,并且票计加1,如果没有则打印查无此人,弃票加1
//传参传入数组,传入弃票数
searchCandidate(temp,bat,numOfCandidate,waiver);
}
}
int main()
{
struct Candidate vote[3];
int numOfCandidate =sizeof(vote)/sizeof(vote[0]);
struct Candidate winer;
int waiver=0;
InitCandidate(vote,numOfCandidate);
voting(vote,numOfCandidate,&waiver);
for(int i=0;i<numOfCandidate;i++)
{
printf("%s,%d票\n",vote[i].name,vote[i].numOfTicks);
}
winer=vote[0];
for(int i=0;i<numOfCandidate;i++)
{
if(winer.numOfTicks<vote[i].numOfTicks)
{
winer=vote[i];
}
}
printf("winer:%s,%d;waiver:%d\n",winer.name,winer.numOfTicks,waiver);
return 0;
}
6.结构体函数
一个函数,返回的是结构体指针。
malloc函数返回值是一个void*,所以当用malloc开辟一段结构体指针时,需要用到强转。
struct Candidate* candidateInit(struct Candidate* ptemp,int* num)
{
if(ptemp==NULL)
{
puts("总共有几名候选人?");
scanf("%d",num);
ptemp=(struct Candidate*)malloc((*num)*sizeof(struct Candidate));
for(int i=0;i<(*num);i++)
{
printf("请输入第%d位候选人的姓名\n",i+1);
scanf("%s",ptemp->name);
ptemp->numOfTicks=0;
ptemp++;
}
return ptemp-*num;
}
}
7.结构二级指针
与正常的二级指针其实是一样的。
下面的例子是将一个结构体指针的地址直接传入函数,直接改变结构体指针的内容。
void candidateInit(struct Candidate** ptemp,int* num)
{
if(*ptemp==NULL)
{
*ptemp=(struct Candidate*)malloc((*num)*sizeof(struct Candidate));
}
puts("总共有几名候选人?");
scanf("%d",num);
for(int i=0;i<(*num);i++)
{
printf("请输入第%d位候选人的姓名\n",i+1);
scanf("%s",(*ptemp)->name);
(*ptemp)->numOfTicks=0;
(*ptemp)++;
}
(*ptemp)=(*ptemp)-*num;
}
8.联合体与公用体
8.1 联合体
不同类型的数据共享一块内存空间,空间大小由最大的类型来确定。
(一段空间有时候存放int型数据、有时候存放char型数据、有时候存放double型数据)
定义:
union Test
{
int a;
char name[128];
double price;
}
8.1.1 联合体的数据覆盖
联合体中不管什么类型的数据的数值,都等于最后赋值的数值。
union Myunion
{
int a;
char b;
double c;
}
union Myunion u1;
u1.a=1;
u1.c=2;
u1.b='h';
printf("%d\n",u1.a);//打印出来时‘h’的ASCII码值。
8.1.2 联合体的应用
为什么要用到联合体
联合体的实例化也可以写成
union
{
int class;
char job[10];
}mes;
//例子
#include <stdio.h>
struct Person
{
char name[10];
char role;
union
{
int class;
char job[10];
}mes;
};
int main()
{
struct Person people[2];
for(int i=0;i<2;i++)
{
puts("请输入名字");
scanf("%s",people[i].name);
puts("请输入角色");
getchar();
scanf("%c",&(people[i].role));
if(people[i].role=='s')
{
puts("请输入班级");
scanf("%d",&(people[i].mes.class));
}
else
{
puts("请输入职务");
scanf("%s",people[i].mes.job);
}
}
for(int i=0;i<2;i++)
{
printf("%s ,",people[i].name);
if(people[i].role=='s')
{
printf("%d\n",people[i].mes.class);
}
else
{
printf("%s\n",people[i].mes.job);
}
}
return 0;
}
重点:
1、就把结构体当成一个变量来进行操作。
2、malloc函数返回值是一个void*,所以当用malloc开辟一段结构体指针时,需要用到强转。
3、当你的函数想要直接改变一个变量内部的内容的时候,毫不犹豫形参用指针,如果想要改变一个指针变量的内容(比如说改变指针变量的指向),形参就可以用二级指针。
4、结构体的地址有点像俄罗斯方块。
struct Test
{
int a;
char name;
double price;
}//结构体大小为16字节
struct Test
{
int a;
int b;
double price;
}//结构体大小为16字节
第九章 Linux导入
1.Linux的简介
1.1 定义
操作系统;多用户、多任务、多线程;开源
为什么要用:因为开发环境基本上都是用这种免费的系统
1.2 分类
发行版:(基于Linux内核,内核一样,但是页面风格不一样)
ubuntu–嵌入式开发;centOS–web服务器
VM环境只是为了学习,真正研发的时候,会在公司的研发服务器安装Linux系统。
2.虚拟机环境
虚拟机安装很简单不做笔记(记得安装vmtool,共享文件夹会用,网络共享用ftp工具—filezilla)
工作的提交代码的方式:git、svn、马云
常用指令:
查看电脑分辨率命令:xrandr
设置电脑分辨率命令:xrandr -s [分辨率]
创建c文件:vi [文件名].c
创建文件:touch
列出当前文件夹的文件:ls [-a]显示所有文件夹
执行可执行文件(.exe)):./
显示当前文件路径:pwd
建文件夹:mkdir
进入文件夹克:cd [..]回到上级
移动文件:mv
重命名:mv a.c b.c
拷贝:cp
进入文件夹(什么都不加回到当前工作目录):cd
[.]代表当前路径
查看网络配置:ifconfig
配置虚拟机的ip地址:sudo ifconfig eth0 [ip地址]
快捷键:
终端:ctrl+alt+t
清屏:ctrl+L
vim:默认命令行模式->按[i]->输入模式->按[esc]->返回命令行模式->按[:]+[wq]->保存退出
自动补全:tab
3.Linux虚拟机的网络配置
参考文档:林加欣–Vmware虚拟机的三种网络模式详解
Bridger(桥接模式):
将主机网卡与虚拟机的网卡通过虚拟网桥连接。
物理网卡作为交换机,连接到路由器,windows与虚拟机都有一个ip与物理网卡相连,从而达到联网效果,且用桥接模式的虚拟机每一台都能互相通讯。
不足:每一台虚拟机都要设置独立的DNS,有可能有的上不了网。
NAT(网络地址转换模式):
ip地址紧缺,可以使用,但是这时虚拟机的ip地址都是假的,且每一台虚拟机的DNS都是同一的,以至于虚拟机能够访问到windows,但是windows访问不到虚拟机。
物理网卡虚拟出一个交换机,交换机连接到每一台虚拟机,每一台都能独立上网,但是ip地址都是假的,window。
Host-Only(仅主机模式):
NAT模式去掉虚拟NAT设备,将虚拟机成为一个独立的系统,使其只与主机互相通讯。
3.1 添加虚拟的交换机
重点:
常用指令:
查看电脑分辨率命令:xrandr
设置电脑分辨率命令:xrandr -s [分辨率]
创建c文件:vi [文件名].c
创建文件:touch
列出当前文件夹的文件:ls [-a]显示所有文件夹
执行可执行文件(.exe)):./
显示当前文件路径:pwd
建文件夹:mkdir
进入文件夹克:cd [..]回到上级
移动文件:mv
重命名:mv a.c b.c
拷贝:cp
进入文件夹(什么都不加回到当前工作目录):cd
[.]代表当前路径
查看网络配置:ifconfig
配置虚拟机的ip地址:sudo ifconfig eth0 [ip地址]
按照时间顺序把文件显示出来:ls -lt(最新在前) -lt(最新在后)
vim显示行号:":set nu"
查看库的详情:man [你要查询的库]
快捷键:
终端:ctrl+alt+t
清屏:ctrl+L
vim:默认命令行模式->按[i]->输入模式->按[esc]->返回命令行模式->按[:]+[wq]->保存退出
自动补全:tab
第十章 链表
定义:链表是一种数据结构,也就是数据存放的思想,说白了就是你这个数据,要怎么放(比如单个存放,或者用数组),顺序数据。
数组:每个元素地址连续,缺点就是增加元素跟删除元素不灵活。
链表:每一个元素都是一个结构体,这个结构体中有个成员是结构体指针变量,可以指向下一个元素的地址。(当要删除一个元素时,只要把删除的元素的前一个元素的结构体指针指向 要删除元素的下一个元素,即可完成删除。当要增加一个元素时,只要把增加的元素的结构体指针指向 添加位置的下一个元素,再让添加位置的上一个元素的结构体指针指向这个要添加的元素即可。)
1.数组和链表的区别
数组的各个节点的地址是连续的;链表的各个节点的地址是没有任何联系的,通过内部元素的指针来进行指向索引,所以需要一个链表头来索引访问数据,通过链表头就能够遍历整个链表。
2.应用
2.1 遍历链表
抓住一个链表中只有最后一个节点是指向空的,所以只需要定义一个链表头来找到那个指向空的节点就能完成遍历。
#include<stdio.h>
struct Test
{
int num;
struct Test* next;
};
void printfLink(struct Test* head)
{
while(head != NULL)//注意判断head->next!=NULL,这样会少打一个节点
{
printf("%d ",head->num);
head=head->next;
}
printf("\n");
}
int main()
{
struct Test t1={1,NULL};
struct Test t2={2,NULL};
struct Test t3={3,NULL};
t1.next=&t2;
t2.next=&t3;
printfLink(&t1);
}
2.2 链表的查找(查)
遍历链表的同时找到自己要的元素
int findLink(struct Test* head,int dest)
{
int count=1;
while(head != NULL)
{
if(dest==head->num)
{
return count;
}
head=head->next;
count++;
}
printf("没有你要的数据\n");
return 0;
}
2.3 链表的插入(增)
找到你要插入的节点,把这个节点的下一个指向待插入的节点,再把待插入的节点的下一个指向要插入节点的下一个。
不能先把要插入节点的下一个指向待插入节点,这样子的话,插入节点后面的数据就访问不到了。
最好验证一下从头和尾插入是否生效。
后方插入:
void insertLink(struct Test* head,int date,struct Test* new)
{
while(head != NULL)
{
if(date==head->num)
{
new->next=head->next;
head->next=new;
return;
}
head=head->next;
}
}
前方插入:
要考虑插入的位置是不是链表的头。
struct Test* insertLinkBefore(struct Test* head,int i,struct Test* new)
{
struct Test* newLink=head;
if(head->num==i)
{
new->next=head;
return new;
}
else
{
while(head->next!=NULL)
{
if(head->next->num==i)
{
new->next=head->next;
head->next=new;
return newLink;
}
else
{
head=head->next;
}
}
}
}
2.4 链表的删除(删)
删除的时候也要考虑删的是不是第一个节点。
如果没有一个新的节点来存储被删除的节点,然后再把这个要被删除的节点free掉的话,话发生段错误。因为如果你直接释放head的next,会导致head指针悬空,整个链表出现断层。
struct Test* deleteLink(struct Test* head,int date)
{
struct Test* newLink=head;
struct Test* bedeleteLink=NULL;
if(head->num==date)
{
newLink=head->next;
free(head);
head=NULL;
return newLink;
}
else
{
while(head->next!=NULL)
{
if(head->next->num==date)
{
bedeleteLink=head->next;
head->next=head->next->next;
free(bedeleteLink);
bedeleteLink=NULL;
return newLink;
}
else
{
head=head->next;
}
}
puts("没有该节点");
}
}
2.5 链表的动态创建
2.5.1 头插法
头一直是最新的节点,有点向栈(先进后出),新节点作为头。
当你添加的是链表的第一个节点的时候,你就要把第一个节点的next指向NULL,不然打印的时候会发现你当时添加的第一个节点的next是野指针,打印会陷入死循环。
struct Test* linkCreate(struct Test* head,int i)
{
struct Test* LinkNew=(struct Test*)malloc(sizeof(struct Test));
LinkNew->num=i;
if(head==NULL)
{
head=LinkNew;
LinkNew->next=NULL;//第一个节点的next必须指向NULL
}
else
{
LinkNew->next=head;
}
return LinkNew;
}
改进版:
struct Test* insertBefroe(struct Test* head,int date,struct Test* new)
{
if(head==NULL)
{
new->num=date;
new->next=NULL;
}
else
{
new->num=date;
new->next=head;
}
return new;
}
struct Test* linkCreate(struct Test* head)
{
int date;
while(1)
{
puts("你要插入的数据");
scanf("%d",&date);
if(date!=0)
{
struct Test* LinkNew=(struct Test*)malloc(sizeof(struct Test));
head=insertBefroe(head,date,LinkNew);
}
else
{
goto exit;
}
}
exit:
return head;
}
2.5.1 尾插法
指针也是双等号来判断的。
//遍历链表找到最后一个,然后在最后一个插入,返回头节点
struct Test* linkCreatBehind(struct Test* head,int date)
{
struct Test* linkHead=head;
if(head==NULL)
{
struct Test* newLink=(struct Test*)malloc(sizeof(struct Test));
newLink->num=date;
newLink->next=NULL;
return newLink;
}
else
{
while(head->next!=NULL)
{
head=head->next;
}
struct Test* newLink=(struct Test*)malloc(sizeof(struct Test));
newLink->num=date;
newLink->next=NULL;
head->next=newLink;
return linkHead;
}
}
改进版:
//插入节点
struct Test* insertLinkBehind(struct Test* head,struct Test* new)
{
struct Test* newLink=head;
if(head==NULL)
{
return new;
}
else
{
while(head->next!=NULL)
{
head=head->next;
}
head->next=new;
return newLink;
}
}
//创建链表
struct Test* createLinkBehind(struct Test* head)
{
int date;
while(1)
{
puts("请输入你要插入的数据");
scanf("%d",&date);
if(date==0)
{
goto exit;
}
else
{
struct Test* new =(struct Test*)malloc(sizeof(struct Test));
new->num=date;
new->next=NULL;
head=insertLinkBehind(head,new);
}
}
exit:
return head;
}
2.6 链表的修改(改)
再查找的基础上进行修改。
//修改
struct Test* changeLink(struct Test* head)
{
struct Test* newLink=head;
int num;
puts("请输入你要改动的数据");
scanf("%d",&num);
while(head!=NULL)
{
if(head->num==num)
{
int date;
puts("找到你要的数据,你要修改为?");
scanf("%d",&date);
head->num=date;
return newLink;
}
head=head->next;
}
puts("没有找到你要的数据");
return newLink;
}
重点:
1、结构体的初始化也可以跟数组差不多
struct Test
{
int date;
struct Test* next;
}
struct Test
struct Test t1 = {1,NULL};
struct Test t2 = {2,NULL};
struct Test t3 = {3,NULL};
2、对整个链表来说链表头是最重要的
3、如果反复插入同一个节点的话,会发生链表的循环。
4、只有malloc出来的节点才能用free删除掉。
5、指针也是双等号来判断的。
项目一:贪吃蛇
项目介绍:基于Linux环境,利用Ncurse图形库的C语言应用。
意义:锻炼C语言、链表的基础的应用;引入Linux的系统编程,包括文件编程、进程、线程、通信、第三方库的应用等等。
1.ncurse
1.1 使用目的:
如果不用ncurse也可以打印出游戏界面(用printf)。
1、ncruse能够支持这个项目的界面;
2、当你要用方向键来操作蛇的动作,你用getchar、scanf、puts要回车,来结束输入,会导致项目的响应不够快,所以要用到ncruse,ncrse的按键响应速度比较快。
1.2 什么是ncurse
最多用在Linux的内核配置(.config生成的配置界面)。
同样的替代品是C图形库的GTK、C++图形库QT、最多的是嵌入式设备上的Andriod系统。
其实不用过多的了解,因为已经被淡化了,只是这个项目需要用到他的图形化界面函数和按键响应。
总而言之,ncurse就是C语言的一个库,可以理解为C++的QT库。
1.3 怎么用ncurse
#include <curses.h>//头文件
int main()
{
initscr();//ncruse界面的初始化函数
printw("snake\n");//在ncruse模式下的printf
getch();//等待用户输入,如果没有这句话,程序直接退出,也看不到上面的话,相当于while(1);
endwin();//程序退出,恢复终端shell,没有这句话的话终端会出现乱码。(终端会混乱)
return 0;
}
在linux环境中编译cnurse
gcc snake.c -lcurses
1.4 键值获取
//vi /usr/include/curces.h
//"/KEY_UP"进行查找
//头文件对键值进行了宏定义
#define KEY_DOWN 0402 /* down-arrow key */
#define KEY_UP 0403 /* up-arrow key */
#define KEY_LEFT 0404 /* left-arrow key */
#define KEY_RIGHT 0405 /* right-arrow key */
#define KEY_HOME 0406 /* home key */
#define KEY_BACKSPACE 0407 /* backspace key */
#define KEY_F0 0410 /* Function keys. Space for 64 */
#define KEY_F(n) (KEY_F0+(n)) /* Value of function key n */
#define KEY_DL 0510 /* delete-line key */
#define KEY_IL 0511 /* insert-line key */
#define KEY_DC 0512 /* delete-character key */
#define KEY_IC 0513 /* insert-character key */
#define KEY_EIC 0514 /* sent by rmir or smir in insert mode *
int main()
{
initscr();//ncruse界面的初始化函数
printw("snake\n");//在ncruse模式下的printf
keypad(stdscr,1);
int key;
while(1)
{
key=getch();
switch(key)
{
case KEY_DOWN:
printw("down\n");//在ncruse模式下的printf
break;
case KEY_UP:
printw("up\n");//在ncruse模式下的printf
break;
case KEY_LEFT:
printw("left\n");//在ncruse模式下的printf
break;
case KEY_RIGHT:
printw("right\n");//在ncruse模式下的printf
break;
default:
printw("null\n");//在ncruse模式下的printf
break;
}
}
endwin();//程序退出,恢复终端shell,没有这句话的话终端会出现乱码。(终端会混乱)
return 0;
}
2.地图实现
外框:(行20行,列40列,蛇活动范围为:x(1,18);y(1,18))
普通的流程控制打印语句
void initGameMap()
{
int row;
int column;
for(row=0;row<20;row++)
{
if(row==0||row==19)
{
for(column=0;column<20;column++)
{
printw("##");
}
}
else
{
for(column=0;column<40;column++)
{
if(column==0||column==39)
{
printw("#");
}
else
{
printw(" ");
}
}
}
printw("\n");
}
}
3.蛇的实现
通过链表来实现,链表中的数据需要行坐标、列坐标以及下个节点的地址。
扫描(遍历)地图,如果遍历到蛇的节点就显示蛇的身子
3.1 显示蛇的身体
struct Snake
{
int x;
int y;
struct Snake* next;
};
struct Snake snake3 = {1,18,NULL};
struct Snake snake2 = {1,17,&snake3};
struct Snake snake1 = {1,16,&snake2};
struct Snake snakeHead = {1,15,&snake1};
bool scanSnake(struct Snake* snakeHead,int row,int column)
{
while(snakeHead!=NULL)
{
if(row==snakeHead->x&&column==snakeHead->y)
{
return true;
}
else
{
snakeHead=snakeHead->next;
}
}
return false;
}
void initGameMap()
{
int row;
int column;
for(row=0;row<20;row++)
{
if(row==0||row==19)
{
for(column=0;column<20;column++)
{
printw("##");
}
}
else
{
for(column=0;column<20;column++)
{
if(column==0)
{
printw("# ");
}
else if(column==19)
{
printw(" #");
}
else
{
if(true==scanSnake(&snakeHead,row,column))
{
printw("[]");
}
else
{
printw(" ");
}
}
}
}
printw("\n");
}
printw("BY ZHW\n");
}
3.2 动态创建链表:(运用指针)
struct Snake* snakeHead;
struct Snake* snakeTail;
void addNode()
{
struct Snake* new =(struct Snake*)malloc(sizeof(struct Snake));
new->x=snakeTail->x;
new->y=(snakeTail->y)+1;
new->next=NULL;
snakeTail->next=new;
snakeTail=new;
}
void initSnake()
{
snakeHead=(struct Snake*)malloc(sizeof(struct Snake));
snakeTail=(struct Snake*)malloc(sizeof(struct Snake));
snakeHead->x=1;
snakeHead->y=15;
snakeHead->next=NULL;
snakeTail=snakeHead;
addNode();
addNode();
addNode();
addNode();
}
3.3 蛇的移动
右移:
删除头节点,再添加一个节点,蛇头指向原本头节点的下一个节点。
void addNodeBehind()
{
struct Snake* new =(struct Snake*)malloc(sizeof(struct Snake));
new->x=snakeTail->x;
new->y=(snakeTail->y)+1;
new->next=NULL;
snakeTail->next=new;
snakeTail=new;
}
void snakeRight()
{
struct Snake* p=snakeHead;
snakeHead=snakeHead->next;
free(p);
p=NULL;
addNodeBehind();
}
左移:
删除尾节点,在头节点前加一个节点,并且把头节点指向新节点。
void deleteTailNode()
{
struct Snake* p=snakeTail;
struct Snake* headTemp=snakeHead;
while(headTemp->next!=snakeTail)
{
headTemp=headTemp->next;
}
headTemp->next=NULL;
snakeTail=headTemp;
free(p);
p=NULL;
}
void addNodeBefore()
{
struct Snake* new =(struct Snake*)malloc(sizeof(struct Snake));
new->x=snakeHead->x;
new->y=(snakeHead->y)-1;
new->next=snakeHead;
snakeHead=new;
}
void snakeLeft()
{
addNodeBefore();
deleteTailNode();
}
3.4 蛇撞到墙
在移动完蛇之后,判断蛇的位置,如果撞墙了就复原。
bool judgePos(struct Snake* snakeHead)
{
while(snakeHead!=NULL)
{
if(snakeHead->x==0||snakeHead->y==0||snakeHead->x==19||snakeHead->y==19)
{
return false;
}
snakeHead=snakeHead->next;
}
return true;
}
void snakeDie(struct Snake* snakeHead)
{
while(snakeHead!=NULL)
{
struct Snake* p =snakeHead;
snakeHead=snakeHead->next;
free(p);
p=NULL;
}
}
3.5 蛇自动行走
用到refresh();和毫秒级的延迟usleep();
3.6 界面刷新和蛇的方向控制同时进行(线程概念)
怎么样才能同时做两个事情–>线程
如何创建线程:Linux有封装专门创建线程的API–> pthread_create
使用线程的时候你的主进程不能结束。
//头文件
#include<pthread.h>
int pthread_create(
pthread_t* restrict tidp,
const pthread_attr_t* restrict_attr,
void* (*start_rtn)(void*),
void *restrict arg);
/*
(1)tidp:事先创建好的pthread_t类型的参数。成功时tidp指向的内存单元被设置为新创建线程的线程ID。
(2)attr:用于定制各种不同的线程属性。通常直接设为NULL。
(3)start_rtn:新创建线程从此函数开始运行。无参数是arg设为NULL即可。
(4)arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。
*/
跟ncurses一样用线程时,编译需要加后缀:
gcc *.c -lphread
#include <stdio.h>
#include <pthread.h>
struct Test
{
int a;
char p[10];
};
void* pthread1(struct Test st)
{
while(1)
{
printf("pthread1,%d,%s\n",st.a,st.p);
}
}
void* pthread2(struct Test st)
{
while(1)
{
printf("pthread2,%d,%s\n",st.a,st.p);
}
}
int main()
{
struct Test t1={1,"zhw"};
struct Test t2={2,"111"};
pthread_t p1;
pthread_t p2;
pthread_create(&p1,NULL,pthread1,&t1);
pthread_create(&p2,NULL,pthread2,&t2);
while(1);
return 0;
}
3.7 蛇的方向改变
向上:在头节点之前加一个新节点,这个新节点的横坐标是头节点的横坐标-1,纵坐标是头节点的纵坐标不变;把整个链表的头节设置成新节点;删去尾节点。
向下:在头节点之前加一个新节点,这个新节点的横坐标是头节点的横坐标+1,纵坐标是头节点的纵坐标不变;把整个链表的头节设置成新节点;删去尾节点。
向左:在头节点之前加一个新节点,这个新节点的横坐标是头节点的横坐标,纵坐标是头节点的纵坐标-1;把整个链表的头节设置成新节点;删去尾节点。
向右:在头节点之前加一个新节点,这个新节点的横坐标是头节点的横坐标,纵坐标是头节点的纵坐标+1;把整个链表的头节设置成新节点;删去尾节点。
同时,要引入一个方向标志位。当按键按下来改变方向标志位;通过方向标志位来进行上下左右的移动。
#define UP 1
#define DOWN 2
#define LEFT 3
#define RIGHT 4
int DIR=LEFT;
void addNodeBefore()
{
struct Snake* new =(struct Snake*)malloc(sizeof(struct Snake));
switch(DIR)
{
case UP:
new->x=snakeHead->x-1;
new->y=(snakeHead->y);
break;
case DOWN:
new->x=snakeHead->x+1;
new->y=(snakeHead->y);
break;
case LEFT:
new->x=snakeHead->x;
new->y=(snakeHead->y)-1;
break;
case RIGHT:
new->x=snakeHead->x;
new->y=(snakeHead->y)+1;
break;
default:
break;
}
new->next=snakeHead;
snakeHead=new;
}
void snakeLeft()
{
addNodeBefore();
deleteTailNode();
if(judgePos(snakeHead)==false)
{
initSnake();
}
}
void* refreshMap()
{
while(1)
{
refresh();
usleep(100000);
snakeLeft();
GameMap();
}
}
void* keyScanf()
{
while(1)
{
key=getch();
switch(key)
{
case KEY_UP:
DIR=UP;
break;
case KEY_DOWN:
DIR=DOWN;
break;
case KEY_LEFT:
DIR=LEFT;
break;
case KEY_RIGHT:
DIR=RIGHT;
break;
default:
break;
}
}
}
3.7.1 改进
1、运行以上代码的时候会发现蛇可以反方向倒退,这时候就要进行一个判断:当你之前按下的键与新按下的键是在同一个维度上的方向,就不能改变方向。(例如,你刚刚按下了左键,这时你按下右键就不能改变方向为右了;上下同理)
#define UP 1
#define DOWN -1
#define LEFT 2
#define RIGHT -2
void posCheck(int newPos)
{
if(abs(DIR)!=abs(newPos))
{
DIR=newPos;
}
}
void* keyScanf()
{
while(1)
{
key=getch();
switch(key)
{
case KEY_UP:
posCheck(UP);
break;
case KEY_DOWN:
posCheck(DOWN);
break;
case KEY_LEFT:
posCheck(LEFT);
break;
case KEY_RIGHT:
posCheck(RIGHT);
break;
default:
break;
}
}
}
2、当你打印出获取到的键值时,可能会打印出一些乱吗,这是ncurses内部的机制,在是能keypad的时候加入noecho()就可以解决。
4.蛇的食物
与蛇一样创建一个食物的结构体。
在地图获取的时候改变监测食物的位置,如果遍历到食物的位置则显示食物图标。
再刷新界面的线程当中监测,蛇是否吃到食物,如果吃到就加一个节点,食物改变位置。
4.1 食物的随机出现
食物的随机出现需要c里面的一个函数:rand,同时需要判断食物的坐标是否会超出地图的活动范围。可以对取出的数字进行取余,这样子永远都不会超过活动范围。同时,要设计食物出现的坐标不能是此时蛇身体的坐标。
//头文件
#include<stdlib.h>
rand();
5.蛇吃到自己的身体需要死亡
判断头节点是否和自己身体节点的坐标一致的话,蛇就死亡。
void judgeSnakeKillItself()
{
struct Snake* p=snakeHead->next;
while(p!=NULL)
{
if(snakeHead->x==p->x&&snakeHead->y==p->y)
{
initSnake();
}
else
{
p=p->next;
}
}
}
6.项目总结
地图规划–>地图实现–>蛇的规划–>蛇的实现–>线程实现界面刷新和按键监测–>蛇的移动–>食物的实现
重点:
1、线程:
如何创建线程:Linux有封装专门创建线程的API–> pthread_create
//头文件
#include<pthread.h>
int pthread_create(
pthread_t* restrict tidp,
const pthread_attr_t* restrict_attr,
void* (*start_rtn)(void*),
void *restrict arg);
/*
(1)tidp:事先创建好的pthread_t类型的参数。成功时tidp指向的内存单元被设置为新创建线程的线程ID。
(2)attr:用于定制各种不同的线程属性。通常直接设为NULL。
(3)start_rtn:新创建线程从此函数开始运行。无参数是arg设为NULL即可。
(4)arg:start_rtn函数的参数。无参数时设为NULL即可。有参数时输入参数的地址。当多于一个参数时应当使用结构体传入。
*/
跟ncurses一样用线程时,编译需要加后缀:
gcc *.c -lphread
2、使用线程的时候你的主进程不能结束。
反复观看:
1、简单选择排序
2、6.3指针变量为什么要要求类型
3、6.5为什么要用到指针
4、6.13-6.15二维数组
5、6.22指针函数