二、分支与循环
分支语句
- if
- switch
循环语句
- while
- for
- do while
goto语句
什么是语句?
在C语言中由一个;号隔开的就是一个语句。
1.分支语句(选择结构)
1.1 if语句
if(表达式1)
语句1;
上面if语句中如果表达式结果为真那么执行语句1,如果为假则跳过。
我们可以设置分支
if(表达式1)
语句1;
else
语句2;
这段代码如果表达式1结果为假那么执行语句二。
同样我们可以有多个分支
if(表达式1)
语句1;
else if(表达式2)
语句2;
else
语句3;
如果表达式1的结果为假那么判断表达式2的语句,如果表达式2的语句为真则执行语句2,如果为假则执行语句3.
其中我们可以进行嵌套,例如
#include <stdio.h> int main() { int age = 20; if(age < 18) printf("未成年\n"); else { if(age >= 18 && age <= 60) printf("成年\n"); else printf("老年\n"); } return 0; }
如上我们就进行了一个简单的嵌套。if语句有很多的作用,可以版我们解决很多问题。
如果我们使用if语句需要执行多个代码,那么需要用{}括起来
int age = 20; if(age < 18) { printf("未成年\n"); printf("不准谈恋爱\n"); }
注意:else会与最近的if匹配,如果为了避免这种错误需要加大括号
#include <stdio.h> int main() { int a = 0; int b = 2; if (a == 1) if (b = 2) printf("haha"); else printf("hehe"); return 0; }
如上述代码,输出结果为空,因为第8行的else与第7行的if匹配,导致表达式a==1为假后不再进行分支,如果要与第6行的if匹配需要为第7行的if加上大括号。
注意2
#include <stdio.h> int main() { int a = 0; if (a = 1) printf("haha\n"); printf("%d\n", a); return 0; }
上述代码输出结果为
1.2 switch语句
当我们输入一个数字判断星期几时,如果用if语句需要说明七种情况很麻烦,这时如果我们用switch语句可以简化很多。
#include <stdio.h> int main() { int a = 0; scanf(" %d", &a); switch (a) { case 1: printf("星期一\n"); case 2: printf("星期二\n"); case 3: printf("星期三\n"); case 4: printf("星期四\n"); case 5: printf("星期五\n"); case 6: printf("星期六\n"); case 7: printf("星期七\n"); } return 0; }
如上,switch规定一个整型表达式(switch括号里面的必须是一个整型),后面case后面跟着整型常量表达式(也必须为整型,不能是变量),当变量a和case后面的值对应时就会从此地方进入。
此时我们输入2会发现结果为
说明从case进入后会一直到switch语句最后才结束。
如果我们想中途推出,那需要加上break语句来跳出switch语句。
#include <stdio.h> int main() { int a = 0; scanf(" %d", &a); switch (a) { case 1: printf("星期一\n"); break; case 2: printf("星期二\n"); break; case 3: printf("星期三\n"); break; case 4: printf("星期四\n"); break; case 5: printf("星期五\n"); break; case 6: printf("星期六\n"); break; case 7: printf("星期七\n"); break; } return 0; }
这样我们在运行的时候就不会一直到switch语句最后才结束。
当我们输入8这种超出处理范围的数值时,此时运行不会得到任何结果。
如果我们想对超出的数据进行处理可以用default
#include <stdio.h> int main() { int a = 0; scanf(" %d", &a); switch (a) { case 1: case 2: case 3: case 4: case 5: printf("工作日\n"); break; case 6: case 7: printf("休息日\n"); break; default: printf("输入错误\n"); } return 0; }
这时我们输入超出处理范围的数据时就会直接提示输入错误。
关于case和default没有严格顺序控制,但是建议做好顺序。
2.循环语句
2.1 while
while(表达式)
循环语句;
当表达式的结果为真执行循环语句,知道表达式结果为假跳出循环。
比如一个简单的循环,在屏幕上打印一到十
#include <stdio.h> int main() { int i = 1; while(i <= 10) { printf("%d\n",i); i++; } return 0; }
我们开始定义i=1,然后定义表达式只要i<=10就执行循环语句printf和i自增1,直到i自增到超过10结束循环。
在循环中,我们也可以用break跳出循环。
#include <stdio.h> int main() { int i = 1; while(i <= 10) { if(i==5) break; printf("%d\n",i); i++; } return 0; }
这里我们在while循环中加了一个if语句,当i=5时执行break语句,这时输出结果为1到4,说明从5开始跳出循环不在执行。
循环中还有一个常用关键字continue,它的作用是终止当前循环后面的语句,然后直接跳到循环开始的判断部分。
#include <stdio.h> int main() { int i = 0; while(i < =10) { i++; if(i==5) continue; printf("%d\n",i); } return 0; }
例如上面代码运行结果为1到10,除了5
getchar和putchar
getchar() 获取键盘上的一个字符
我们定义一个变量,然后就可以用变量存储getchar获得的字符
int a;
a = getchar();
和getchar()对应的是putchar(),putchar是在屏幕上输出一个字符
在上面代码后面输入
putchar(a);
就可以把刚才存到a中的字符输出(像printf一样)
当我们规定 ch=getchar() == EOF 才跳出循环时
#include <stdio.h> int main() { int i; while ((i=getchar()) != EOF) { putchar(i); } return 0; }
输入缓冲区
当我们运以下代码时,在对数组进行输入时输入123456然后回车会发现直接返回取消,此时我们没有对ret进行任何赋值。
#include <stdio.h> int main() { char ret; int arr[10]; scanf("%s", arr); ret = getchar(); if (ret == 'Y') { printf("确认"); } else { printf("取消"); } return 0; }
这是因为scanf和getchar都是输入函数,输入函数都会去一个输入缓冲区去取数据。首先运行scanf函数,然后scanf发现输入缓冲区没有东西,此时会等待输入数据。当我们输入123456然后回车的时候123456就被scanf取走,但是此时还有一个\n(因为scanf函数读取数据时到\n结束),然后getchar再去数据缓冲区发现有一个\n,所以直接取走\n,然后if判断返回取消。
如果想要正常运行那么需要清空输入缓冲区,我们可以再用一个getchar取走后面的\n。
但是如果我们输入arr时输入123456 ABC,此时又会返回取消。因为scanf取数据时只取了123456到空格时结束,然后第一个getchar取走了A,第二个getchar取走了B,接着返回取消。此时如果我们想清空输入缓冲区我们可以借助循环一直取走里面的数据,直到清空。
#include <stdio.h> int main() { char ret; char ch; int arr[10]; scanf("%s", arr); while(ch=getchar()!='\n') { ; } ret = getchar(); if (ret == 'Y') { printf("确认"); } else { printf("取消"); } return 0; }
2.for循环
2.1语法
for(表达式1;表达式2;表达式3)
循环语句;
表达式1为初始部分,用于初始化循环变量,表达式2为条件判断部分,用于判断循环时候终止,表达式3为调整部分,用于循环条件的调整。
例:打印1-10
#include <stdio.h> int main() { for(int i=1;i<=10;i++) printf("%d\n",i); return 0; }
for循环的条件和控制很清晰明了,所以使用次数多。
break和continue也可以用于for循环
2.2 for循环建议
1.不可再for循环内修改循环体,防止for循环失去控制。
2.建议for循环语句的循环控制变量的取值采用“前闭后开区间”写法。
2.3 for循环变种
变种1
#include <stdio.h> int main() { for(;;) { printf("hehe\n"); } return 0; }
当我们输入以下代码时会发现时死循环.
for循环的初始化,调整,判断都可以省略,但是for循环的判断部分如果被省略,那么判断条件就是;恒为正。
如果不熟悉建议不要省略。
#include <stdio.h> int main() { int i = 0; int j = 0; for(;i < 10;i++) { for(;j < 10;j++) { printf("hehe\n"); } } return 0; }
当我们运行以上代码时可能会认为打印100个hehe,但实际上只会打印10个。我们在外部声明了i和j两个变量的初始化为0,并且在for循环中省略了i和j的初始化。i初始为0进入循环,j也为0进入循环一直打印十次hehe,之后j=10跳出循环,i=2开始第二次循环。但是此时j没有初始化所以j还是等于10,因此直接跳出循环。以此类推只打印10次hehe。
变种2
#include int main() { int x,y; for (x = 0,y = 0;x < 2 && y < 5; ++x, y++) { printf("hehe"); } return 0; }
这种循环用两个循环变量控制,也可以支持。
例题:请问以下代码循环几次
#include <stdio.h> int main() { int i = 0; int j = 0; for(i = 0,j = 0; k = 0; i++,k++) { k++; } return 0; }
答案为0次,首先定义i和j的变量为0,然后for循环内初始化i和j。接下来判断语句是k=0是个赋值语句,而0为假所以循环没有执行直接跳出循环。
3. do...while()循环
3.1语法
do
{
表达式;
}
while(条件语句)
do...while()循环和其他循环有一点不同不管是否符合循环语句它都会执行一遍循环里面的表达式。
break和continue同样可以用于do...while循环
4.例题
顺数查找
在一个有序数列(1,2,3,4,5,6,7,8,9,10)中查找一个数的下标(比如说7)。
首先我们肯定会想到用循环从1到10挨个对比找到相应数字。
#include <stdio.h> int main() { int k=0; int arr[10]={1,2,3,4,5,6,7,8,9,10}; for(int i=0;i<10;i++) { k=arr[i]; if(k==7) { printf("下标为%d\n",k); break; } } return 0; }
这种方法可以帮我们找到下标,但是需要挨个比对计算量太大效率降低,我们可以采用另一种方法提高效率。
二分查找法:首先我们去数列中间的数5,然后和要找的数比对,如果小于7再从5到10的范围内去中间的数,如果大于7则在1到5范围内取中间的数,以此类推直到找到相应的值。
#include <stdio.h> int main() { int arr[10]={1,2,3,4,5,6,7,8,9,10}; int k=sizeof(arr)/sizeof(arr[0]); int i=0; int j=k-1; int l=0; for(;;) { k=(j+i)/2; l=arr[k]; if(l==7) { printf("下标为%d\n",k); break; } else if(l<7) { i=k; } else { j=k; } } return 0; }
求24和18两个数的最大公约数
首先我们肯定能想到先比较出大小,然后让较小的数递减,判断两个数同时除递减的数取余为0得到最大公约数。
#include <stdio.h> int main() { int m = 24; int n = 18; int k = n; while (k > 0) { k--; if (m % k == 0 && n % k == 0) { printf("最大公倍数为%d\n", k); break; } } return 0; }
然后我们就可以得到最大公倍数,但是这种计算较为复杂,我们可以采用另一种辗转相除法降低复杂程度。
辗转相除法就是先用大的数m除小的数n取余得到一个数c,如果n再除c取余等于0,那么c就为最大公倍数,如果取余不为0,把n的值赋给m,c的值赋给n,再用m%n得c,n%c,以此类推直到n%c等于0.
#include <stdio.h> int main() { int m = 24; int n = 18; int c = 0; while (m % n) { c = m % n; if (n % c == 0) { printf("最大公倍数为%d\n", c); break; } m = n; n = c; } return 0; }
求100-200之间的素数
素数的定义是只能被1和自己本身整除,所以我们首先会想到试除法。试除法就是找一个数a从2开始一直自增,用要判断的数一直除a,如果a自增到要判断的数-1还没有整除,那么这个数为素数。
#include <stdio.h> int main() { int i; int a; for (i = 100; i <= 200; i++) { for (a = 2; a < i; a++) { if (i % a == 0) { break; } } if (i == a) { printf("%d ", i); } } return 0; }
同样这种方法过于繁琐我们可以给它简化一下。我们发现一个数可以写成两个数相乘的形式,其中一个数一定小于或等于这个数的开平方,因此我们可以把a的范围缩小到i的开平方。
#include <stdio.h> #include <math.h> int main() { int i; int a; for (i = 100; i <= 200; i++) { for (a = 2; a < sqrt(i); a++) { if (i % a == 0) { break; } } if (a >= sqrt(i)) //sqrt是数学库函数,所以要引用math { printf("%d ", i); } } return 0; }
在100-200中偶数是不可能是素数的,所以我们在第一个for循环中可以换成for(i=101;i<200;i+=2)来提升运算效率。
注意
注意求数组下标
当我们数组这样定义时
arr[5]={0,1,2,3,4};
我们可以用sizeof(arr)/sizeof(arr[0])-1求得最后一位4的下标。
但是如果我们这样定义
arr[]=abcdefg;
我们想求g的下标则不能-1,因为这个数组不完整,它最后一位还有一个/0,这一位也算在内,所以如果想求g的下标需要-2.
注意==不能用来比较两个字符串是否相等应该用库函数strcmp。
strcmp(字符串1,字符串2)==1;
三、函数和递归
1.库函数
像printf、strcpy等等描述基本功能,它们不是业务性的代码,我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
学习库函数:www.cplusplus.com
C语言常用的库函数
- IO函数 (input、output输入输出函数)
- 字符串操作函数
- 字符操作函数 (判断大小写或者转化)
- 内存操作函数
- 时间/日期函数
- 数学函数
- 其他库函数
2.自定义函数
自定义函数和库函数一样,有函数名,返回值类型和函数参数.但是不一样的是这些都是我们自己来设计.
举个例子(取最大值)
#include <stdio.h> int MAX(int i,int j) { if(i >j ) return i; else return j; } int main() { int num1 = 10; int num2 = 15; int max=MAX(num1,num2); printf("%d\n",max); return 0; }
交换数值
#include <stdio.h> void Swap(int x,int y) { int tmp = 0; tmp = x; x = y; y = tmp; } int main() { int num1 = 7; int num2 = 10; Swap(num1,num2); printf("%d %d\n",num1,num2); }
这时我们发现num1和num2的值并没有互换,这是因为我们函数中定义的int x和int y也是独立开辟的空间,也就是说x,y和num1,num2的地址不是对应的,只是把值赋给x,y而已。如果要避免这种情况,我们可以用指针将地址传过去而不是数值。
#include <stdio.h> void Swap(int*x,int*y) { int tmp = 0; tmp = *x; //*是解引用操作符,表示地址中的值 *x = *y; *y = tmp; } int main() { int num1 = 7; int num2 = 10; Swap(&num1,&num2); //将地址传给函数 printf("%d %d\n",num1,num2); }
3.函数的参数
3.1 实际参数(实参)
真实传给函数的参数,叫实参。实参可以是︰常量、变量、表达式、函数等。无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
比如上面例子中的Swap(num1,num2)。
3.2 形式参数(形参)
形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。形式参数当函数调用完成之后就自动销毁了。因此形式参数只在函数中有效。
比如上面的Swap(int x, int y)。
注意:当实参传给形参的时候,形参其实是实参的一份临时拷贝,对形参的修改不会影响实参。
4.函数的调用
4.1 传值调用
函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
4.2 传址调用
传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外达的变量建立起正真的联系,也就是函数内部可以直接操作函数外部的变量。
例题
使用函数并用二分法查找数据
#include <stdio.h> int chan(int arr[], int b) { int a = 0; int j = 0; int l = sizeof(arr) / sizeof(arr[0]) - 1; while (l >= a) { j = (a + l) / 2; if (arr[j] == b) { return j; } else if (arr[j] < b) { a = j + 1; } else { l = j - 1; } } return -1; } int main() { int arr[] = { 0,1,2,3,4,5,6,7,8,9 }; int k = 8; int rel = chan(arr, k); if (rel == -1) { printf("找不到"); } else { printf("找到了下标为:%d", rel); } return 0; }
首先我们应该会想到这种方法,但是运行结果确实找不到,这是因为arr数组在传给函数的时候传的不是整个数组(节省空间),传的只是数组第一个元素的地址。所以当我们在函数中使用sizeof计算数组的长度时计算的只是第一个元素大小/第一个元素大小,结果为1。因此要避免这种错误要先计算好长度传到函数中。
5.函数的嵌套调用和链式访问
5.1 嵌套调用
嵌套调用就是多个函数之间可以相互调用,比如说函数1、2、3,2函数里面同样可以调用1和3函数进行运算。
5.2 链式访问
把一个函数的返回值作为另外一个函数的参数。
例题
下面运行结果是什么
#include <stdio.h> int main() { printf("%d", printf("%d", printf("%d", 43))); return 0; }
我们观察第一个printf打印值是第二个printf函数的返回值,第二个printf的打印值是第三个printf的返回值,第三个printf的打印值是43,经过查阅得知printf返回值是打印元素的个数,所以第三个printf返回值为2,第二个printf打印值为2,第一个printf打印值为1,所以输出结果为4321.
6.函数的声明与定义
#include <stdio.h> int ADD(int,int); //函数声明 int main() { int a = 1; int b = 2; int c = ADD(a, b); //函数调用 printf("%d", c); return 0; } int ADD(int x, int y) //函数定义 { int z = x + y; return z; }
上面代码是在同一个源文件实现的函数声明、调用和定义,如果我们最开始没有进行声明,可能会发生警告,这是因为编译器从上到下开始读取数据,当调用ADD函数时并没有找到ADD函数从而警告。
同样我们可以把三者放在不同文件中,我们可以创建一个头文件,并把声明部分放到头文件中,然后再创一个源文件,把函数定义放在里面,在我们需要调用函数的源文件中只要引用头文件就可以正常使用函数。注意引用自己的函数需要用双引号。(#include "add.h")
7.函数递归
7.1 什么是递归
程序调用自身的编程技巧称为递归( recursion )。递归做为一种算法在程序设计语言中广泛应用。一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大融复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的主要思考方式在于∶把大事化小
最简单的递归
#include <stdio.h> int main() { printf("hehe\n"); main(); return 0; }
这串代码从main函数开始进入打印hehe,然后引用main函数,依次循环直到栈溢出报错停止。
例题
输入一个数字,并用递归的方法按顺序打印数字的每一位
#include <stdio.h> void print(int n) { if (n > 9) { print(n/10); } printf("%d ", n % 10); } int main() { int num = 0; scanf("%d", &num); print(num); return 0; }
首先从main函数进入,scanf输入一个数值(假设输入1234),之后进入print函数。形参n得到num的数值,然后判断n是否大于9,如果大于9先整除10然后再次进入函数print,以此类推直到n小于9输出n%9的余数。因为我们输入的是1234,第一次判断大于9进入递归此时n为123,第二次进入递归n为12,第三次为1,这时n小于9输出n%10(输出1)结束此次递归然后再回到n=12的递归输出n%10(输出2),以此类推最后打印1 2 3 4
7.2递归的两个必要条件
存在限制条件,当满足这个限制条件的时候,递归不在继续。
每次递归调用之后越来越接近这个限制条件。
例题2
求一个字符串长度,不能调用临时变量
#include <stdio.h> int mystr(char* str) { if (*str != '\0') return 1 + mystr(str + 1); else return 0; } int main() { char arr[] = "bit"; int len = mystr(arr); printf("len=%d\n", len); return 0; }
首先建立一个数组存放字符串,然后建立一个函数mystr用来计算数组内字符串长度(注意数组传到函数内的是第一个元素的地址)
然后我们用解引用操作符*来查看此时指向数组的元素是否为'\0'(数组结束标志),如果不为零返回1+mystr(str+1)
此时会再次进入mystr函数,并且这一次函数判断的是后一个元素是否为'\0',每次进入函数后都会有一个+1,
直到最后一次递归完成每个1都会累加起来最后返回到主函数中。
例题3
求某个斐波那契数列
首先我们尝试一下用递归计算
#include <stdio.h> int fib(int x) { if (x <= 2) return 1; else return fbnq(x - 1) + fbnq(x - 2); } int main() { int n = 0; int m = 0; scanf("%d", &n); m = fib(n); printf("第%d个斐波那契数列为%d\n", n, m); return 0; }
我们发现用函数递归的方式求斐波那契数列效率是非常低的,因为其中运算了大量的重复数据。比如说我们求第40个斐波那契数,我们要知道38和39的斐波那契数,然后我们又要知道第36 37 37 38个斐波那契数。依次类推我们会发现有大量的重复数据,因此用递归的方式不适合。
首先我们知道斐波那契前两个数都是1,我们把a和b分别赋值前两个数,用a+b求得第三个斐波那契数。然后将第二个斐波那契数赋给a,第三个斐波那契数赋给b,再用a+b求得下一个斐波那契数,以此类推求得第n个斐波那契数。
#include <stdio.h> int fib(int x) { int a = 1; int b = 1; int c = 1; //这里c取1是因为下面判断x小于等于2的时候返回1 while (x > 2) { c = a + b; a = b; b = c; x--; } return c; } int main() { int n = 0; int m = 0; scanf("%d", &n); m = fib(n); printf("第%d个斐波那契数列为%d\n", n, m); return 0; }