第一章、初识C语言 先快速了解一下C语言的大概内容,点到为止,以后会具体介绍。
1.什么是C语言 C语言是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发。
2.第一个C语言 1 2 3 4 5 6 #include <stdio.h> int main () { printf ("Hello,world!\n" ); return 0 ; }
3.数据类型 1 2 3 4 5 6 7 8 9 char short int long long long float double
4.常量与变量 常量 —不变的量
变量 —可变的量
4.1 定义变量的方法 1 2 3 int age = 150 ;float weight = 45.5f ;cha ch = x;
1 2 3 4 5 6 7 8 9 10 11 12 13 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main () { int num1 = 0 ; int num2 = 0 ; int sum = 0 ; scanf ("%d %d" , &num1, &num2); sum = num1 + num2; printf ("%d\n" , sum); return 0 ; }
4.2 变量的分类 全局变量;局部变量
1 2 3 4 5 6 7 8 #include <stdio.h> int year = 1998 ;int main () { int year = 2021 ; printf ("year = %d\n" , year); return 0 ; }
4.3 变量的作用域和生命周期 作用域 —作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的,限定这个名字的可用性的代码范围就是这个名字的作用域。
局部变量的作用域:变量所在的局部范围。
全局变量的作用域:整个工程。
生命周期 —变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。
局部变量的生命周期:进入作用域生命周期开始,出作用域生命周期结束。
全局变量的生命周期:整个程序的生命周期。
4.4 常量 C语言中的常量分为以下以下几种:
字面常量;const修饰的常变量;#define定义的标识符常量;枚举常量;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <stdio.h> enum Sex { MALE, FEMALE, SECRET }; int main () { 3.14 ; 1000 ; const float pai = 3.14f ; #define MAX 100 printf ("max = %d\n" , MAX); printf ("%d\n" , MALE); printf ("%d\n" , FEMALE); printf ("%d\n" , SECRET); return 0 ; }
5.字符串+转义字符+注释 5.1 字符串 字符串—由双引号(Double Quote)引起来的一串字符称为字符串字面值(String Literal)。
注:字符串的结束标志是一个 \0 的转义字符。在计算字符串长度的时候 \0 是结束标志,不算作字符串内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main () { char arr1[] = "bit" ; char arr2[] = {'b' , 'i' , 't' }; char arr3[] = {'b' , 'i' , 't' ,'\0' }; printf ("%s\n" , arr1); printf ("%s\n" , arr2); printf ("%s\n" , arr3); return 0 ; }
5.2 转义字符
转义字符
释义
?
在书写连续多个问号时使用,防止他们被解析成三字母词
\‘
用于表示字符常量’
\“
用于表示一个字符串内部的双引号
\\
用于表示一个反斜杠,防止它被解释为一个转义序列符
\a
警告字符,蜂鸣
\b
退格符
\f
进纸符
\n
换行
\r
回车
\t
水平制表符
\v
垂直制表符
\ddd
ddd表示1~3个八进制的数字。 如: \130 X
\xdd
dd表示2个十六进制数字。 如: \x30 0
1 2 3 4 5 6 7 #include <stdio.h> int main () { printf ("%c\n" , '\'' ); printf ("%s\n" , "\"" ); return 0 ; }
5.3 注释 代码中有不需要的代码可以直接删除,也可以注释掉
代码中有些代码比较难懂,可以加一下注释文字
6.选择语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int main () { int learn = 0 ; printf ("你会认真学习吗?(选择 1 or 0):" ); scanf ("%d" ,&learn); if (learn == 1 ){ printf ("你会进步!\n" ); }else { printf ("你会落后!\n" ); } return 0 ; }
7.循环语句 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> int main () { int num = 0 ; while (num <= 10 ){ printf ("%d" ,num); num++; } do { printf ("%d" ,num); num++; }while (num <= 10 ); for (num=0 ;num <= 10 ;num++){ printf ("%d" ,num); } return 0 ; }
8.函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 #define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> int Add (int x,int y) { int sum = x + y; return sum; } int main () { int num1 = 0 ; int num2 = 0 ; int sum = 0 ; printf ("输入两个数:" ); scanf ("%d %d" ,&num1,&num2); sum=Add(num1,num2); printf ("sum=%d" ,sum); return 0 ; }
9.数组 9.1 数组的定义 数组—一组相同类型元素的集合 。(c语言中的定义)
1 int arr[10 ] = {1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 ,10 };
9.2 数组的下标 数组的每个元素都有一个下标,下标是从0开始的。 数组可以通过下标来访问的。
10.操作符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 + - * / % >> << & ^ | = += -= *= /= &= ^= != >>= <<= ! - + & sizeof ~ -- ++ * > >= < <= != == && || exp1 ? exp2 : exp3 exp1,exp2,exp3 [] () . ->
11.常见关键词 1 auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while
注:先介绍下面几个,之后会详细介绍
11.1 关键字 typedef typedef—定义自己习惯的数据类型名称,来替代系统默认的基本类型名称、数组类型名称、指针类型名称与用户自定义的结构型名称、共用型名称、枚举型名称等。
1 2 typedef unsigned int uint_32;
11.2 关键字 static static是用来修饰变量和函数的。
11.2.1 修饰局部变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> void test1 () { int i = 0 ; i++; printf ("%d " ,i); } void test2 () { static int i = 0 ; i++; printf ("%d " ,i); } int main () { int i = 0 ; for (i=0 ;i<10 ;i++){ test1(); } printf ("\n" ); for (i=0 ;i<10 ;i++){ test2(); } return 0 ; }
输出:
1 1 1 1 1 1 1 1 1 1 1 2 3 4 5 6 7 8 9 10
结论:
static修饰局部变量改变了变量的生命周期,让静态局部变量出了作用域依然存在,到程序结束,生命周期才结束,也就是保持变量内容的持久性 。
11.2.2 修饰全局变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int year_1 = 2021 ;static int year_2 = 2021 ;#include <stdio.h> int main () { extern int year_1; extern int year_2; printf ("%d" ,year_1); return 0 ; }
11.2.3 修饰函数 修饰函数与修饰全局变量效果类似。
一个函数被static修饰,使得这个函数只能在本源文件内使用,不能在其他源文件内使用。
12.#define 定义常量和宏 1 2 3 4 5 6 7 8 9 10 11 12 #define MAX 1000 #define ADD(x,y) ((x)+(y)) #include <stdio.h> int main () { int sum = ADD(2 ,3 ); printf ("%d %d" ,MAX,sum); return 0 ; }
13.指针 每一个变量都有一个内存位置,每一个内存位置都定义了可使用&运算符访问的地址,它表示了在内存中的一个地址。
指针也就是内存地址,指针变量是用来存放内存地址的变量。
指针大小在32位平台是4个字节,64位平台是8个字节。
1 2 3 4 5 6 7 8 9 #include <stdio.h> int main () { int num = 100 ; int *p; p = # printf ("num变量的地址:%p\n" ,p); printf ("num变量的值:%d\n" ,*p); return 0 ; }
结果:
num变量的地址:00A4F88C num变量的值:100
14.结构体 结构体是C语言中特别重要的知识点,结构体使得C语言有能力描述复杂类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct Stu { char name[20 ]; int age; char sex[5 ]; char id[15 ]; }; struct Stu s = {"小白" ,20 ,"男" ,"20210101" };printf ("name = %s;age = %d" , s.name, s.age); struct Stu *ps = &s;printf ("name = %s;age = %d" , ps->name, ps->age);
第二章、分支和循环语句 1.什么是语句 C语句可分为以下五类:
表达式语句
函数调用语句
控制语句
复合语句
空语句
本节介绍的是控制语句 。
控制语句用于控制程序的执行流程,以实现程序的各种结构方式,它们由特定的语句定义符组成,C语言有九种控制语句。 可分成以下三类:
条件判断语句也叫分支语句:if语句、switch语句;
循环执行语句:do while语句、while语句、for语句;
转向语句:break语句、goto语句、continue语句、return语句。
2.分支语句(选择语句) 2.1 if语句 语法结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if (表达式){ 语句; } if (表达式){ 语句1 ; }else { 语句2 ; } if (表达式1 ){ 语句1 }else if (表达式2 ){ 语句2 }else { 语句3 }
例子:
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main () { int num1 = 0 ; int num2 = 0 ; if (a>b){ printf ("a比较大" ); }else if (a<b){ printf ("b比较大" ); }else { printf ("a与b值相同" ); } }
2.2 switch语句 switch语句也是一种分支语句,常常用于多分支的情况。
1 2 3 4 5 6 7 switch (整型表达式){ case 整型常量表达式: 语句; dafault: 语句; }
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> int main () { int day = 0 ; 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 ; default : printf ("数值范围应该在1~7" ); }
3.循环语句 3.1 while循环
例子:
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main () { int i = 1 ; while (i<=10 ){ printf ("%d\n" ,i); i++; } return 0 ; }
3.2 do…while循环
do…while循环和while循环类似,区别在于do…while循环先执行循环语句再判断,while循环则是先判断再循环,也就是说do…while循环至少执行一次 。
例子:
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main () { int i = 1 ; do { printf ("%d\n" ,i); i++; }while (i<=10 ); return 0 ; }
3.3 for循环 1 2 3 for (表达式1 ;表达式2 ;表达式3 ){ 循环语句; }
表达式1——初始化 部分
初始化循环变量;
表达式2——条件判断 部分
满足条件,则执行循环语句(判断循环何时终止)
表达式3——调整 部分
一次循环结束后执行表达式3(用于循环变量的调整)
注:for循环中的初始化部分,条件判断部分,调整部分是可以省略的
例子:
1 2 3 4 5 6 7 8 #include <stdio.h> int main () { for (int i=1 ;i<=10 ;i++){ printf ("%d\n" ,i); } return 0 ; }
3.4 break和continue break——在循环中只要遇到break,就直接终止循环。
注:如果是循环嵌套的情况下,break只会结束一层循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include <stdio.h> int main () { inr i = 0 ; int j = 0 ; for (i=1 ;i<=3 ;i++){ for (j=1 ;j<=3 ;j++){ if (i=3 ){ break ; } printf ("%d*%d=%d " ,i,j,i*j); } } return 0 ; }
continue——用于终止本次循环的,也就是本次循环中continue后边的代码不会再执行, 直接跳转到循环语句的判断部分。进行下一次循环的条件判断。
3.5 goto语句 C语言中提供了可以随意滥用的 goto语句和标记跳转的标号。
从理论上 goto语句是没有必要的,实践中没有goto语句也可以很容易的写出代码。
但是某些场合下goto语句还是用得着的,常见的用法就是终止程序在某些深度嵌套的结构的处理过程。
例如:一次跳出两层或多层循环。多层循环这种情况使用break是达不到目的的,它只能从内层循环退出到上一层的循环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 for (...){ for (...){ for (...){ if (disaster){ goto error; } } } } error: if (diaster){ }
第三章、函数 1.函数是什么 在计算机科学中,子程序(英语:Subroutine, procedure, function, routine, method, subprogram, callable unit),是一个大型程序中的某部份代码,由一个或多个语句块组成。它负责完成某项特定任务,而且相较于其他代码,具备相对的独立性。
一般会有输入参数并有返回值,提供对过程的封装和细节的隐藏。这些代码通常被集成为软件库。
函数在面向过程的语言中已经出现。是结构(Struct)和类(Class)的前身。本身就是对具有相关性语句的归类和对某过程的抽象。
2.库函数 有许多基础功能,它们不是业务性的代码,我们在开发的过程中每个程序员都可能用的到,为了支持可移植性和提高程序的效率,所以C语言的基础库中提供了一系列类似的库函数,方便程序员进行软件开发。
www.cplusplus.com
http://en.cppreference.com(英文版)
http://zh.cppreference.com(中文版)
该网站有各个函数、语法的实例代码。
使用库函数,必须包含 #include 对应的头文件。
3.自定义函数 自定义函数和库函数一样,有函数名,返回值类型和函数参数。这些我们都可以自己来设计,这给了我们一个很大的发挥空间。
1 2 3 4 5 6 7 ret_type fun_name (para1, * ) { statement; }
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> void swap (int &x,int &y) { int temp = 0 ; temp = x; x = y; y = temp; } int main () { int num1 = 1 ; int num2 = 100 ; printf ("交换前,num1=%d,num2=%d\n" ,num1,num2); swap(num1,num2); printf ("交换前,num1=%d,num2=%d\n" ,num1,num2); return 0 ; }
4.函数参数 4.1 实际参数(实参) 真实传给函数的参数,叫实参。
实参可以是:常量、变量、表达式、函数等。
无论实参是何种类型的量,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参。
4.2 形式参数(形参) 形式参数是指函数名后括号中的变量,因为形式参数只有在函数被调用的过程中才实例化(分配内存单元),所以叫形式参数。
形式参数当函数调用完成之后就自动销毁了。
因此形式参数只在函数中有效。
注:我们可以简单的认为,形参实例化之后其实相当于实参的一份临时拷贝 。
5.函数调用 5.1 传值调用 函数的形参和实参分别占有不同内存块,对形参的修改不会影响实参。
5.2 传址调用 传址调用是把函数外部创建变量的内存地址传递给函数参数的一种调用函数的方式。
这种传参方式可以让函数和函数外边的变量建立起真正的联系,也就是函数内部可以直接操作函数外部的变量。
6.函数的嵌套调用和链式访问 函数和函数之间可以根据实际的需求进行组合的,也就是互相调用的。
6.1 嵌套调用 函数可以嵌套调用,但是不能嵌套定义。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> int multiply (int &x,int &y) { int num = 0 ; num = x*y; return num; } void show (int &i) { for (int j=1 ;j<=i;j++){ printf ("%d*%d=%2d\t" ,i,j,multiply(i,j)); } } int main () { for (int i=1 ;i<10 ;i++){ show(i); printf ("\n" ); } return 0 ; }
6.2 链式访问 把一个函数的返回值作为另外一个函数的参数。
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> #include <string.h> int main () { char arr[20 ] = "hello" ; int ret = strlen (strcat (arr,"world" )); printf ("%d\n" , ret); return 0 ; }
7.函数的声明和定义 7.1 函数声明 告诉编译器有一个函数叫什么,参数是什么,返回类型是什么,但是具体是否存在,函数声明无法决定。
函数的声明一般出现在函数的使用之前。要满足先声明后使用。
函数的声明一般要放在头文件中的。
7.2 函数定义 函数的定义是指函数的具体实现,交待函数的功能实现。
8.函数递归 8.1 什么是递归 程序调用自身 的编程技巧称为递归( recursion),递归做为一种算法在程序设计语言中广泛应用。
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:大事化小
8.2 递归的两个必要条件 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
每次递归调用之后越来越接近这个限制条件
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> int factaoial (int n) { if (n<=1 ){ return 1 ; }else { return n*factaoial(n-1 ); } } int main () { int n = 6 ; printf ("%d!=%d\n" ,n,factaoial(n)); return 0 ; }
在调试 factorial 函数的时候,如果你的参数比较大,那就会报错: stack overflow(栈溢出)这样的信息。
系统分配给程序的栈空间是有限的,但是如果出现了死循环,或者(死递归),这样有可能导致一直开辟栈空间,最终产生栈空间耗尽的情况,这样的现象我们称为栈溢出。
解决方法:
1.将递归改写成非递归。
2.使用static对象替代 non-static 局部对象。
在递归函数设计中,可以使用 static 对象替代 non-static 局部对象(即栈对象),这不仅可以减少每次递归调用和返回时产生和释放 non-static 对象的开销,而且 static 对象还可以保存递归调用的中间状态,并且可为各个调用层所访问
第四章、数组 1.一维数组的创建和初始化 1.1 一维数组的创建 数组是一组相同类型元素的集合。
1 2 3 4 5 6 type_t arr_name[conse_n] int array [10 ];
注:
数组创建,在C99标准之前,[]中要给一个常量才可以,不能使用变量。
在C99标准支持了变长数组的概念。
1.2 一维数组的初始化 数组的初始化 是指在创建数组的同时给数组的内容一些合理初始值。
1 2 3 4 5 6 int arr1[10 ] = {1 ,2 ,3 };int arr2[] = {1 ,2 ,3 ,4 };int arr3[5 ] = {1 ,2 ,3 ,4 ,5 };char arr4[3 ] = {'a' ,98 , 'c' };char arr5[] = {'a' ,'b' ,'c' };char arr6[] = "abcdef" ;
数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
2.一维数组在内存中的存储 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> int main () { int array [10 ]={0 }; int i = 0 ; int len = sizeof (array )/sizeof (array [0 ]); for (i=0 ;i<len;i++){ printf ("&array[%d] = %p\n" ,i,&array [i]); } return 0 ; }
随着数组下标的增长,元素的地址,也在有规律的递增。
由此可以得出结论:数组在内存中是连续存放的。
3.二维数组的创建和初始化 3.1 二维数组的创建 1 2 3 int array [5 ][6 ];char array [5 ][6 ];double array [5 ][6 ];
3.2 二维数组的初始化 1 2 3 4 int arr[3 ][4 ] = {1 ,2 ,3 ,4 };int arr[3 ][4 ] = {{1 ,2 },{4 ,5 }};int arr[][4 ] = {{2 ,3 },{4 ,5 }};
4.二维数组在内存中的存储 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #include <stdio.h> int main () { int array [3 ][4 ] = { 0 }; int i = 0 ; int j = 0 ; for (i = 0 ; i < 3 ; i++) { for (j = 0 ; j < 4 ; j ++ ) { printf ("&array[%d][%d] = %p\n" , i,j, &array [i][j]); } } return 0 ; }
二维数组在内存中也是连续存储的。
5.数组越界 数组的下标是有范围限制的。
数组的下规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1。
所以数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问。
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的,
二维数组的行和列也可能存在越界。
6.数组练习:三子棋 6.1 简介 三子棋,在一个3*3组成的棋盘上,只要一方将自己的三个棋子走成一条线,就算胜利,如果棋盘上下满棋子却没有分出胜负则算平局。
6.2 思路 用一个3*3的二维数组来存储玩家与电脑下棋数据;
用”*“表示玩家下的棋子,”#”表示电脑下的棋子;
电脑下棋用rand函数模拟;
6.3 代码 ThreeChess.h (函数声明)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #pragma once #define _CRT_SECURE_NO_WARNINGS 1 #define ROW 3 #define COL 3 #include <stdio.h> #include <stdlib.h> #include <time.h> void InitBoard (char board[ROW][COL], int row, int col) ;void DisplayBoard (char board[ROW][COL], int row, int col) ;void PlayerMove (char board[ROW][COL], int row, int col) ;void ComputerMove (char board[ROW][COL], int row, int col) ;char CheckWin (char board[ROW][COL], int row, int col) ;
ThreeChess.cpp (游戏的函数具体实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 #include "ThreeChess.h" void InitBoard (char board[ROW][COL], int row, int col) { int i = 0 ; int j = 0 ; for (i = 0 ; i < row; i++) { for (j = 0 ; j < col; j++) { board[i][j] = ' ' ; } } } void DisplayBoard (char board[ROW][COL], int row, int col) { int i = 0 ; int j = 0 ; for (i = 0 ; i < row; i++) { for (j = 0 ; j < col; j++) { printf (" %c " , board[i][j]); if (j < col - 1 ) { printf ("|" ); } } printf ("\n" ); if (i < row - 1 ) { for (j = 0 ; j < col; j++) { printf ("---" ); if (j < col - 1 ) { printf ("|" ); } } } printf ("\n" ); } } void PlayerMove (char board[ROW][COL], int row, int col) { int x = 0 ; int y = 0 ; printf ("玩家走\n" ); printf ("请输入坐标:" ); while (1 ) { scanf ("%d %d" , &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (board[x - 1 ][y - 1 ] == ' ' ) { board[x - 1 ][y - 1 ] = '*' ; break ; } else { printf ("该坐标被占用,请重新输入!\n" ); } } else { printf ("该坐标非法,请重新输入!\n" ); } } } void ComputerMove (char board[ROW][COL], int row, int col) { int x = 0 ; int y = 0 ; printf ("电脑走\n" ); while (1 ) { x = rand() % row; y = rand() % col; if (board[x][y] == ' ' ) { board[x][y] = '#' ; break ; } } } int isFull (char board[ROW][COL], int row, int col) { int i = 0 ; int j = 0 ; for (i = 0 ; i < row; i++) { for (j = 0 ; j < col; j++) { if (board[i][j] == ' ' ) { return 0 ; } } } return 1 ; } char CheckWin (char board[ROW][COL], int row, int col) { int i = 0 ; int j = 0 ; for (i = 0 ; i < row; i++) { if (board[i][0 ] == board[i][1 ] && board[i][1 ] == board[i][2 ] && board[i][0 ] != ' ' ) { return board[i][0 ]; } } for (j = 0 ; j < col; j++) { if (board[0 ][j] == board[1 ][j] && board[1 ][j] == board[2 ][j] && board[0 ][j] != ' ' ) { return board[0 ][j]; } } if (board[0 ][0 ] == board[1 ][1 ] && board[1 ][1 ] == board[2 ][2 ] && board[0 ][0 ] != ' ' ) { return board[0 ][0 ]; } if (board[2 ][2 ] == board[1 ][1 ] && board[1 ][1 ] == board[2 ][0 ] && board[2 ][2 ] != ' ' ) { return board[2 ][2 ]; } if (isFull(board, row, col) == 1 ) { return 'q' ; } return 'c' ; }
test.cpp (测试)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 #include "ThreeChess.h" void menu () { printf ("****************************************\n" ); printf ("********** 1. play **********\n" ); printf ("********** 0. exit **********\n" ); printf ("****************************************\n" ); } void threeChess () { char ret = 0 ; char board[ROW][COL] = { 0 }; InitBoard(board, ROW, COL); DisplayBoard(board, ROW, COL); while (1 ) { PlayerMove(board, ROW, COL); DisplayBoard(board, ROW, COL); ret = CheckWin(board, ROW, COL); if (ret != 'c' ) { break ; } ComputerMove(board, ROW, COL); DisplayBoard(board, ROW, COL); ret = CheckWin(board, ROW, COL); if (ret != 'c' ) { break ; } } if (ret == '*' ) { printf ("玩家赢了\n" ); } else if (ret == '#' ) { printf ("电脑赢了\n" ); } else if (ret == 'q' ) { printf ("平局\n" ); } } int main () { int input = 0 ; srand((unsigned int )time(NULL )); do { menu(); printf ("请选择:>" ); scanf ("%d" , &input); switch (input) { case 1 : threeChess(); break ; case 0 : printf ("退出游戏\n" ); break ; default : printf ("选择错误\n" ); } } while (input); return 0 ; }
7.数组练习:扫雷 7.1 简介 扫雷,一款经典的小游戏。
7.2 思路 用两个二维数组来分别存储布置好的地雷数据和排查出来的地雷数据;
在存储布置地雷数据 的二维数组中,用“0”表示没有地雷,用“1”表示布置了地雷;
在存储排查地雷数据 的二维数组中,用“*”表示还没有排查的位置,用“0”-“9”表示排查位置周围地雷的数量;
布置地雷用rand函数模拟;
7.3 代码 Minesweeper.h (函数声明)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #pragma once #define _CRT_SECURE_NO_WARNINGS 1 #define ROW 9 #define ROWS ROW+2 #define COL 9 #define COLS COL+2 #include <stdio.h> #include <stdlib.h> #include <time.h> void InitBoard (char board[ROWS][COLS], int rows, int cols,char set ) ;void DisplayBoard (char board[ROWS][COLS], int row, int col) ;void SetMine (char board[ROWS][COLS], int row, int col,int count) ;void FindMine (char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int num) ;
Minesweeper.cpp (游戏的函数具体实现)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 #include "Minesweeper.h" void InitBoard (char board[ROWS][COLS], int rows, int cols,char set ) { int i = 0 ; int j = 0 ; for (i = 0 ; i < rows; i++) { for (j = 0 ; j < cols; j++) { board[i][j] = set ; } } } void DisplayBoard (char board[ROWS][COLS], int row, int col) { int i = 0 ; int j = 0 ; printf ("-----------------------------------------\n" ); for (i = 0 ; i <= col; i++) { printf ("%d " , i); } printf ("\n" ); for (i = 1 ; i <= row; i++) { printf ("%d " , i); for (j = 1 ; j <= col; j++) { printf ("%c " , board[i][j]); } printf ("\n" ); } printf ("-----------------------------------------\n" ); } void SetMine (char board[ROWS][COLS], int row, int col,int count) { while (count) { int x = rand() % row+1 ; int y = rand() % col+1 ; if (board[x][y] == '0' ) { board[x][y] = '1' ; count--; } } } int GetMineCount (char mine[ROWS][COLS], int x, int y) { int sum = 0 ; for (int i = x - 1 ; i <= x + 1 ; i++) { for (int j = y - 1 ; j <= y + 1 ; j++) { sum += mine[i][j]; } } return sum - 9 * '0' ; } void ShowMine (char mine[ROWS][COLS],char show[ROWS][COLS],int x, int y) { int count = GetMineCount(mine, x, y); if (count == 0 ) { show[x][y] = count + '0' ; for (int i = x - 1 ; i <= x + 1 ; i++) { for (int j = y - 1 ; j <= y + 1 ; j++) { if (i >= 1 && i <= ROW && j >= 1 && j <= COL && show[i][j] == '*' ) { ShowMine(mine, show, i, j); } } } } else { show[x][y] = count + '0' ; } } void FindMine (char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int num) { int x = 0 ; int y = 0 ; int win = 0 ; while (win<row*col-num) { printf ("请输入要排查的坐标:>" ); scanf ("%d %d" , &x, &y); if (x >= 1 && x <= row && y >= 1 && y <= col) { if (mine[x][y] == '1' ) { printf ("\n很遗憾,你被炸死了!\n" ); DisplayBoard(mine, row, col); break ; } else { ShowMine(mine, show, x, y); DisplayBoard(show, row, col); win++; } } else { printf ("坐标非法!\n" ); } } if (win == row * col - num) { printf ("恭喜你,排雷成功\n" ); DisplayBoard(mine, row, col); } }
test.cpp (测试)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include "Minesweeper.h" void menu () { printf ("****************************************\n" ); printf ("********** 1. play **********\n" ); printf ("********** 0. exit **********\n" ); printf ("****************************************\n" ); } void Minesweeper () { char mine[ROWS][COLS] = {0 }; char show[ROWS][COLS] = {0 }; InitBoard(mine, ROWS, COLS,'0' ); InitBoard(show, ROWS, COLS,'*' ); SetMine(mine, ROW, COL, 10 ); DisplayBoard(show, ROW, COL); FindMine(mine, show, ROW, COL,10 ); } int main () { int input = 0 ; srand((unsigned int )time(NULL )); do { menu(); printf ("请选择:>" ); scanf ("%d" , &input); switch (input) { case 1 : Minesweeper(); break ; case 0 : printf ("退出游戏\n" ); break ; default : printf ("选择错误\n" ); } } while (input); return 0 ; }
第五章、操作符详解 1.操作符分类
算术操作符
移位操作符
位操作符
赋值操作符
单目操作符
关系操作符
逻辑操作符
条件操作符
逗号表达式
下标引用、函数调用和结构成员
2.算术操作符
除了 % 操作符之外,其他的几个操作符可以作用于整数和浮点数。
对于 / 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除法。
% 操作符的两个操作数必须为整数,返回的是整除之后的余数。
3.移位操作符
左移操作符 移位规则:
运算符将其左侧操作数向左移动右侧操作数定义的位数,左边抛弃、右边补0 。
右移操作符 移位规则:
运算符将其左侧操作数向右移动右侧操作数定义的位数。
右移运算有2种:
1.逻辑 移位—左边用0填充,右边丢弃
2.算术 移位—左边用原该值的符号位填充,右边丢弃
警告 :对于移位运算符,不要移动负数位,这个是标准未定义的。
4.位操作符
一道有趣的面试题:
不能创建临时变量(第三个变量),实现两个数的交换。
1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main () { int a=10 ; int b=20 ; a = a ^ b; b = a ^ b; a = a ^ b; printf ("a=%d b=%d\n" ,a,b); return 0 ; }
5.赋值操作符 赋值操作符可以给不满意的值重新赋值。
复合赋值符
1 2 += -= *= /= %= >>= <<= &= |= ^=
赋值符使用时要注意两边的数据类型
6.单目操作符 1 2 3 4 5 6 7 8 9 10 ! - + & sizeof ~ -- ++ * (type)
7.关系操作符
在编程过程中,要小心将 == 和 = 写错
8.逻辑操作符
9.条件操作符 1 2 3 4 5 6 7 8 9 10 exp1 ? exp2 : exp3 if (a>0 ){ b=10 ; }else { b=-10 ; } a>0 ?b=10 :b=-10 ;
10.逗号表达式
逗号表达式,就是用逗号隔开的多个表达式。
从左向右依次执行,整个表达式的结果是最后一个表达式的结果。
11.下标引用、函数调用和结构成员
12.表达式求值 表达式求值的顺序一部分是由操作符的优先级和结合性决定。
有些表达式的操作数在求值的过程中可能需要转换为其他类型。
12.1 隐式类型转换 C的整型算术运算总是至少以缺省整型类型的精度来进行的。
为了获得这个精度,表达式中的字符和短整型操作数在使用之前被转换为普通整型,这种转换称为整型提升 。
整型提升的意义 :
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准度。
通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
1 2 3 4 char a,b,c;... a = b + c;
b和c的值被提升为普通整型,然后再执行加法运算,加法运算完成之后,结果将被截断,然后再存储于a中。
如何进行整形提升 :
整形提升是按照变量的数据类型的符号位来提升的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 char c1 = -1 ;变量c1的二进制位(补码)中只有8 个比特位: 1111111 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为1 提升之后的结果是: 11111111111111111111111111111111 char c2 = 1 ;变量c2的二进制位(补码)中只有8 个比特位: 00000001 因为 char 为有符号的 char 所以整形提升的时候,高位补充符号位,即为0 提升之后的结果是: 00000000000000000000000000000001
12.2 算术转换 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作就无法进行。
12.3 操作符的属性 复杂表达式的求值有三个影响的因素。
操作符的优先级
操作符的结合性
是否控制求值顺序。
两个相邻的操作符先执行哪个取决于他们的优先级。
如果两者的优先级相同,取决于他们的结合性。
在编程中,要尽量避免编写非法表达式 ,例如:
1 2 int i = 10 ;i = i-- - --i * ( i = -3 ) * i++ + ++i;
上述表达式在不同编译器有不同的结果,原因是该表达式不能通过操作符的属性确定唯一的计算路径,是有歧义的。
第六章、指针 1.指针的概念 指针 是内存中一个最小单元的编号,也就是地址。
平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量。
指针变量 ,用来存放地址的变量。
我们可以通过&(取地址操作符)取出变量的内存其实地址,把地址可以存放到一个变量中,这个变量就是指针变量。
2.指针的定义 1 2 3 4 5 type + * char *pc = NULL ;int *pi = NULL ;short *ps = NULL ;
char*类型的指针是为了存放char类型变量的地址。
int*类型的指针是为了存放int类型变量的地址。
指针的类型决定了指针向前或者向后走一步有多大(距离)。
指针的类型决定了,对指针解引用的时候有多大的权限。(比如: char*的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。)
3.野指针 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针的成因 :
1.指针未初始化
2.指针越界访问
1 2 3 4 5 int array [10 ] = {0 };int *p = array ;for (int i=0 ;i<=10 ;i++){ *(p++) = i; }
3.指针指向的空间释放
1 2 3 4 5 6 7 8 9 int *test () { int a = 10 ; return &a; } int main () { int *p = test(); *p = 20 ; return 0 ; }
野指针的规避方法:
1.指针初始化
2.小心指针越界
3.指针指向空间释放即使置NULL
4.避免返回局部变量的地址
5.指针使用之前检查有效性
4.指针运算 因为两个地址相加无意义也可能越界,所以规定不允许指针相加
4.1 指针+-整数
1 2 3 4 5 6 7 8 9 10 11 #define NUM 5 #include <stdio.h> int main () { int array [NUM]; int * p; for (p = &array [0 ]; p < &array [NUM];) { *p++ = 0 ; } return 0 ; }
4.2 指针-指针
1 2 3 4 5 6 7 int my_strlen (char * s) { char * p = s; while (*p != '\0' ) { p++; } return p - s; }
4.3 指针的关系运算
1 2 3 4 5 6 7 8 9 for (p = &array [0 ]; p < &array [10 ];) { *p++ = 0 ; } for (p = &array [0 ]; p < &array [10 ];p++) { *p = 0 ; }
5.指针和数组 1 2 3 4 5 6 7 8 9 10 #include <stdio.h> int main () { int array [5 ] = { 1 ,2 ,3 ,4 ,5 }; printf ("%p\n" , array ); printf ("%p\n" , &array [0 ]); return 0 ; }
数组名表示的是数组首元素的地址。
注:两种情况除外
1.sizeof(数组名):数组名表示整个数组,计算的是整个数组的大小,单位是字节
2.&数组名:数组名代表整个数组,返回的是整个数组的地址
6.二级指针 指针变量也是变量,是变量就有地址。
那指针变量的地址存放在哪里,这就是二级指针 (指向指针的指针)。
1 2 3 4 5 6 7 #include <stdio.h> int main () { int num = 10 ; int * p = # int ** pp = &p; return 0 ; }
第七章、结构体 1.结构体的声明 1 2 3 4 5 6 7 8 9 10 11 struct Point { int x; int y; }p1; struct Stu p2 ; struct Node { int data; struct Point p ; struct Node * next ; }n1={10 ,{4 ,5 },NULL };
2.结构体成员的访问
结构变量的成员是通过点操作符 . 访问的。点操作符接受两个操作数。
有时候我们可以得到一个指向一个结构体的指针,可以通过 -> 访问。
1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> struct Point { int x; int y; }; int main () { struct Point p1 = { 4 ,5 }; struct Point * ps = &p1; printf ("x = %d ; y = %d\n" , p1.x, p1.y); printf ("x = %d ; y = %d\n" , ps->x, ps->y); return 0 ; }
3.结构体传参 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct S { int data[1000 ]; int num; }; struct S s = {{1 ,2 ,3 ,4 }, 1000 };void print1 (struct S s) { printf ("%d\n" , s.num); } void print2 (struct S* ps) { printf ("%d\n" , ps->num); } int main () { print1(s); print2(&s); return 0 ; }
上面的 print1 和 print2 函数哪个好些?
答案:首选print2函数。
原因:函数传参的时候,参数是需要压栈的。如果传递一个结构体对象的时候,结构体过大,参数压栈的的系统开销比较大,所以会导致性能的下降。
结论 :结构体传参的时候,要传结构体的地址。