文章目录
- (一) C语言概述
- 1.预处理指令:宏定义、宏函数
- 2.生成可执行程序的过程
- 3.进程与虚拟内存空间
- (二) 格式化输入输出
- 1.变量及命名
- 2.格式化输入输出、输入输出模型
- (1)CPU、内存、外部设备的速度矛盾
- (2)printf
- (3)scanf
- 3.代码即注释
- 4.程序出错的原因、调试程序
- 5.其他
- (1)bool类型
- (2)向0取整
- (3)标识符
- (三) 基本数据类型
- 0.基本数据类型
- 1.整数的编码
- 2.ASCII码 (1字节,低7位)
- 3.转义序列
- 4.C语言中将char类型当作整数来操作
- 5.和用户交互:读/写
- (1)printf + %c 、scanf + %c
- (2)putchar( c)、getchar()
- 6.语言:惯用法
- 7.类型转换
- (1)隐式类型转换
- (2)强制类型转换
- 8.定义别名 typedef
- 9.sizeof()
- 10.void
- (四) 运算符和表达式
- 1.运算符
- (1)属性
- ①运算符优先级
- ②运算符的结合性
- (2)分类
- ①自增运算符
- ②位运算符
- 1)移位运算符
- 2)按位运算符
- ③按位运算的经典面试题
- 2.表达式
- (1)表达式的概述
- (2)逗号表达式
- (五) 语句
- 1.表达式语句
- 2.选择语句
- (1)if、else
- (2)switch、case
- 3.循环语句
- (1)while
- (2)do while
- (3)for
- 4.跳转语句
- 5.空语句
- 6.复合语句
- (六) 数组
- 1.一维数组
- (9)数组长度的宏
- 2.多维数组
- 3.常量数组
- ①伪随机数
- ②随机发牌 问题
- (七) 函数
- 1.函数的定义和调用
- (1)函数的使用准则
- ①判断素数
- ②掷骰子游戏
- ③德克萨斯扑克
- (2)函数声明、函数定义、函数调用、函数指针
- 2.函数的参数传递、实际参数 与 形式参数
- (3)数组作为参数进行传递
- 3.局部变量 和 外部变量
- (1)局部变量
- (2)全局变量
- 4.return语句
- 5.程序终止 exit()
- 6.递归
- (1)递归的概念
- (2)循环不变式
- (3)经典例题
- ①斐波那契数列
- ②汉诺塔问题
- ③约瑟夫环问题
- 1)每隔一个人,出局一个人
- 2)每隔m个人,出局一个人
- (八) 指针
- 1.指针基础
- (1)指针的声明
- (2)指针的两个基本操作
- (3)野指针
- (4)指针的应用
- ①指针作为参数进行传递
- ②指针作为返回值
- 2.指针与数组的关系
- 3.指针的高级应用
(一) C语言概述
C语言:函数、结构体、指针
VS的使用:
①X86:
1.预处理指令:宏定义、宏函数
#:预处理指令
1.#include :头文件复制过来
2.宏定义: 是 文本替换
3.宏函数:也是文本替换,用 实参 替换 形参
(1)注意事项
①左括号紧贴宏函数名,否则会当成宏定义
②参数要括起来,整个表达式也要括起来
(2)为什么要用宏函数?/ 使用宏函数的好处:
①宏函数快,仅仅是替换,避免了函数调用开销。
应用场景:简短的、频繁调用的函数,写成宏函数,可以降低函数调用开销
②提供了一定的宏编程能力
2.生成可执行程序的过程
①预处理:执行预处理指令
②编译:将C语言翻译成汇编语言
③汇编:将汇编语言翻译成机器语言,生成目标文件
④链接:将多个目标文件和库文件链接在一起,生成可执行程序
.h头文件在预处理阶段用
库文件是.o文件,在链接阶段用
3.进程与虚拟内存空间
运行中的程序称为进程(Process),每个进程都有自己的虚拟内存空间
①内核:和内核交互
②栈:管理函数调用
③堆:存放动态数据
④数据段:存储数据,包括静态局部变量等
⑤代码段:存储指令、字符串字面值 (如 0、1、A、B、C)
程序 = 数据 + 指令
冯诺依曼型计算机,又叫存储程序型计算机
(二) 格式化输入输出
1.变量及命名
1.变量的三要素:变量名、类型、值 int a = 10;
(1)变量名:引用变量绑定的值
(2)类型:①限定了变量的取值范围:编码、长度 (所占内存大小) ②限定了值能进行的操作(运算方法)
(3)值:
2.变量的命名
①下划线命名法:current_page、name_and_address
②驼峰命名法:currentPage、nameAndAddress
3.用宏定义避免魔法数字,代码是给人看的,程序才是给机器运行的
2.格式化输入输出、输入输出模型
(1)CPU、内存、外部设备的速度矛盾
(1)CPU、内存:Cache、TLB
(2)CPU、设备:DMA
(3)内存、设备:①缓存(集合,替换机制) ②缓冲区(队列,内存中)
输入输出模型:键盘→stdin→应用程序→stdout→显示器
(2)printf
(1)格式化输出:print format (输出格式)
(2)作用/工作原理:打印格式串中的内容,并用后面表达式的值替换转换说明
(3)格式串:
①普通字符:原样打印
②转换说明:占位符,用后面表达式的值填上占位符的值
(4)占位符的格式:%-m.pX
-:左对齐
m:最小字段长度
p:精度。%d是前面补0,%f是小数点后几位
X:字符数据要转换的类型
%-5.2f
:-是左对齐(默认右对齐),5是占位5个位置,.2是小数点后2位,f是float类型
%.2d
:.2是前面补0,用于打印月份、日期
输出:
%d:整数 int
%f:单精度浮点数 float
%lf:双精度浮点数 double
%c:字符型
%u:无符号整数
%x:十六进制数(小写字母)
%X:十六进制数(大写字母)
%p:指针、打印地址
(3)scanf
(1)格式化输入:scan format
(2)工作原理:拿stdin里面的数据和格式串进行匹配,从左到右依次匹配格式串中的每一项。如果有一项匹配不成功,scanf会立刻返回。scanf的返回值为匹配成功的转换说明的个数。
(3)格式串:
①普通字符:精确匹配
②空白字符:任意个空白字符 (包括0个)
③转换说明:
%d:忽略前置的空白字符(空格、\t、\v、\n),匹配一个十进制的有符号整数
%f:忽略前置的空白字符,匹配一个浮点数
%c:匹配一个字符,不会忽略前置的空白字符
Q:匹配第一个非空白字符,转换说明怎么写?
A:空格%c
3.代码即注释
1.代码即
①给变量起好听的名字
②留白,一个功能段写完要空行,不要写成一坨。
4.程序出错的原因、调试程序
详情请见:https://blog.csdn.net/Edward1027/article/details/135511540
1.程序出错的原因:
(1)编译错误:语法错误
(2)链接错误:①没有包含对应的头文件 ②函数名写错了
(3)运行时错误:逻辑错误,即BUG
2.调试程序:
①打断点、取消断点
②逐过程:遇到函数调用,执行整个函数
③逐语句:进入到自定义的函数中
④继续:运行到逻辑上的下一个断点
⑤跳出:执行完该被调函数,返回主调函数
5.其他
(1)bool类型
#include <stdbool.h>
可以在C语言中使用bool类型
C语言中,%运算符和数学上的mod不一样
C语言中,余数和符号和被除数的符号一致。
(2)向0取整
向0取整。正数向下取整,负数向上取整。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void){//向0取整。正数向下取整,负数向上取整int i;float f1 = -0.5;i = f1;printf("i = %d\n", i); //0float f2 = -1.5;i = f2;printf("i = %d\n", i); //-1return 0;
}
(3)标识符
字符、数字、下划线
(三) 基本数据类型
0.基本数据类型
①整型 int
②浮点型 float、double
③字符型 char
1.整数的编码
1.无符号整数(二进制编码)
(1)无符号整数求值
2.有符号整数(补码)
(1)有符号整数求值
(2)性质:
①补码 11111…11111 的值为 -1
②补码最高位为1,则值一定为负数
③ X + (-X) = 1000…0000
从右向左找到第一个1,此1左边按位取反
例题1:
有符号整数 1101 0100(2),求它的相反数的二进制表示
思路:凑两数相加后为1000 0000。从右向左找到第一个1,此1左边按位取反
答案:0010 1110(2)
(3)为什么计算机采用补码存储有符号整数?
原因:用加法器来实现减法运算,减少硬件成本,代替了减法器。 a - b = a + (-b)
2.ASCII码 (1字节,低7位)
①0-31、127 是 控制字符
②空字符:0
空格:32
字符 `0`:48 【48-57是数字】
A:65 【65-90是大写字母】
a:97 【97-122是小写字母】(小写字母比大写字母大32)
3.转义序列
(1)字符转移序列
\n
换行
\r
回车
\t
水平制表符
\\
反斜杠
\'
单引号
\"
双引号
(2)数字转移序列
①八进制转义序列:反斜杠开头,接1-3个八进制数值
②十六进制转义序列:以 \x
开头,后接十六进制数字
int main(void){printf("%c\n",'A');printf("%c\n",'\101'; //八进制转义序列printf("%c\n",'\x41'); //十六进制转义序列return 0;
}
4.C语言中将char类型当作整数来操作
大小写转换,大写转小写:+32,小写转大写 -32
6.字符处理函数
扩展了字符类型支持的操作
5.和用户交互:读/写
(1)printf + %c 、scanf + %c
%c:匹配一个字符 (不忽略空白字符)
scanf("%c %c",&c1,&c2); //注意%c之间要有空格,匹配空白字符。跳过空白字符,读取下一个非空白字符
(2)putchar( c)、getchar()
putchar( c)、getchar() 更高效,读写一个字符 (字符数据)
char c = 'A';
putchar(c);
char c = getchar();
6.语言:惯用法
1.基本语法
2.惯用法 (代码的惯用法,类似汉语的成语,很不错)
①getchar的惯用法:跳过该行剩余的字符
while(getchar() != '\n');
3.设计模式
7.类型转换
(1)隐式类型转换
①整数提升 (低于int的转换为int,如char、short)
②值的表示范围,表示范围小的会向表示范围大的类型转换,这样没有数据丢失
③同一转换等级,有符号转换为无符号
避免有符号数和无符号数一起运算,比如有符号数-1可能变成无符号数的最大值
(2)强制类型转换
作用:
①求浮点数的小鼠部分
②显式地强调这里有类型转换
③精准控制类型的转换
④避免(int类型)溢出
8.定义别名 typedef
1.格式:
typedef 类型 别名;
2.作用:起别名的好处:
①可读性强
②可移植性强
9.sizeof()
作用:计算某一类型的值在内存中占用的大小
sizeof(数组),可以得到数组的大小
10.void
空类型 void
特点:没有值,不能作为变量的类型
(四) 运算符和表达式
1.运算符
(1)属性
①运算符优先级
单目运算符(自增运算符、位运算符)、加减乘除、比较运算符、位运算符、赋值运算符、逗号运算符
== 优先级高于 &
同一个符号在不同上下文语境中的含义不同:
① a*b:双目运算符,乘号
② *p:单目运算符,解引用
③ int *p:声明语句中的单目运算符,指针
②运算符的结合性
int i;
float f;
f = i = 3.14f; //从右向左结合
(2)分类
①自增运算符
++i:表达式的值为i+1,副作用是i自增
i++:表达式的值为i,副作用是i自增
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void) {//++i:表达式的值i+1,副作用是i自增int i = 1;printf("i is %d\n", ++i);printf("i is %d\n", i);//i++:表达式的值i,副作用是i自增int i = 1;printf("i is %d\n", i++);printf("i is %d\n", i);return 0;
}
②位运算符
位运算符:
移位运算符: <<,>>
按位运算符:~,&,|,^
1)移位运算符
移位运算不会改变变量的值
若想改变,加个等号,移位赋值运算符,<<=
①左移运算符
左移n位,相当于乘以2n
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void){unsigned u = 0xAB;printf("%u\n", u); //171printf("%x\n", u << 2); //0x2acprintf("%u\n", u << 2); //171*4 = 684printf("u = %u\n", u); //移位运算不会改变变量的值u <<= 2;printf("u = %u\n", u); // <<= 移位赋值运算符才能改变变量的值return 0;
}
②右移
右移n位,相当于乘除以2n,向下取整
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void) {unsigned u = 0xAB;printf("%u\n", u); //171printf("%X\n", u >> 2); //0x2Aprintf("%u\n", u >> 2); //171/4 = 42printf("u = %u\n", u); //移位运算不会改变变量的值return 0;
}
2)按位运算符
按位运算符:按位与&
、按位或|
、按位异或^
、按位取反~
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void) {unsigned short i, j, k;i = 21; // 0000 0000 0001 0101j = 56; // 0000 0000 0011 1000k = ~i; //按位取反 1111 1111 1110 1010 ,即 65535-16-4-1 = 65514k = i & j; //按位与 0000 0000 0001 0000 ,即 16k = i | j; //按位或 0000 0000 0011 1101 , 即 61k = i ^ j; //按位异或 0000 0000 0010 1101 ,即 45return 0;
}
1.按位异或 ^
结合律:按位异或的结果 取决于1的个数是奇数个还是偶数个。奇数个为1,偶数个为0。~
③按位运算的经典面试题
法1:数学逻辑
①有问题写法,对负奇数失效。因为 负奇数%2等于-1。%2可能会得到-1、0、1三种结果。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>bool isOdd(int n) {if (n % 2 == 1) return true; //负奇数%2 == -1else return false;
}int main(void) {int i;scanf("%d", &i);int ret = isOdd(i);if (ret) printf("Odd\n");else printf("Even\n");return 0;
}
②正确写法:n % 2 != 0
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>bool isOdd(int n) {return n % 2 != 0;
}int main(void) {int i;scanf("%d", &i);printf("该数字是%s", isOdd(i) ? "奇数" : "偶数");return 0;
}
法2:位运算:n & 0x1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>//奇数与1按位与得1,偶数与1按位与得0
bool isOdd(int n) {return n & 0x1; //与二进制1按位与,把个位截取下来
} int main(void) {int i;scanf("%d", &i);printf("该数字是%s", isOdd(i) ? "奇数" : "偶数");return 0;
}
①传统数学运算
bool isPowerof2(int n){// n > 0while(n % 2 == 0){n /= 2; }return n == 1;
}
②用位运算优化
bool isPowerof2(int n){// n > 0while((n & 0x1) == 0){n >>= 1;}return n == 1;
}
③直接一步到位: n & n-1
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>bool isPowerof2(int n) {return (n & n - 1) == 0;
}int main(void){int i;scanf("%d", &i);printf("%d%s\n", i, isPowerof2 ? "是2的幂次" : "不是2的幂次");//bool ret = isPowerof2(i);//if (ret) printf("%d是2的幂次\n", i);//else printf("%d不是2的幂次\n", i);return 0;
}
①原始做法:从最低位开始截取,为0就左移,直到找到第一个不为0的位,就是最低有效位(last set bit)
int lastSetBit(int n){int x = 0x1;while((n & x) == 0){x <<= 1;} // (n & x) != 0return x;
}
②进阶做法:n与其相反数-n按位与,因为-n的二进制位是 n从最低有效位开始左边按位取反,故 n & -n
就是最低有效位
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int lastSetBit(int n){//int x = 0x1;//while((n & x) == 0){// x <<= 1;//} // (n & x) != 0//return x;return n & -n;
}int main(void){int n;scanf("%d", &n);printf("%d\n", lastSetBit(n));return 0;
}
一对逆运算:加减、异或 异或
①加减法
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void){int a = 3, b = 4;a = a + b;b = a - b;a = a - b;printf("a = %d, b = %d\n", a, b); //a = 4, b= 3return 0;
}
②异或 与 异或 也是逆运算
异或
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int findSingleNum(int nums[], int n) {int singleNum = 0;for (int i = 0; i < n; ++i) {singleNum ^= nums[i]; //连连看,异或消一对}return singleNum;
}int main(void) {int nums[5] = { 1,4,2,1,2 };printf("singleNum = %d\n", findSingleNum(nums, 5)); //单独的数为4return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void) {int nums[6] = { 1,2,1,3,2,5 };int xor = 0;for (int i = 0; i < 6; ++i) {xor ^= nums[i];}// xor = a ^ b (xor != 0),结果为单独的那两个数int lsb = xor & (-xor); //找到了a和b不同的最低有效位//根据这一位将所有元素分类 (根据该lsb将a和b分类)int a = 0, b = 0;for (int i = 0; i < 6; ++i) {if (nums[i] & lsb){//为1a ^= nums[i]; //连连看,异或消除一对}else{//为0b ^= nums[i]; //连连看,异或消除一对}}printf("%d %d\n", a, b);return 0;
}
2.表达式
(1)表达式的概述
1.C语言是一个非常看重表达式的语言
2.表达式的定义:计算某个值的公式
3.最简单的表达式:变量和常量,如:a、i、10、20
4.运算符的作用:连接表达式,创建更复杂的表达式。 如,a + b + c/d
(2)逗号表达式
(表达式1,表达式2,表达式3,… ,表达式n)
计算前n-1个表达式的值后丢弃,最后一个表达式的值作为整体的值
(五) 语句
1.表达式语句
expr
2.选择语句
(1)if、else
if( ){}else if( ){}else{}
(2)switch、case
1.级联式if else的弊端:
①可读性差
②效率低
2.switch case的优点:
①可读性好
②当分支较多时,效率高
3.switch case的限制条件:
①switch后的表达式必须是整型 (int、char、枚举类型)
②switch后的表达式和case的标签,只能用 == 做比较,不能用大于小于
switch(表达式){case 4:情景4;break;case 3:情景3;break;case 2:情景2;break;case 1:情景1;break;case 0:情景0;break; default:情景-1;break;
}
4.注意事项:
①多个case可共用一组语句
switch(grade){
case 4: case 3: case 2: case 1:printf("Passing\n");break;
case 0:printf("Failing\n");break;
default:printf("Illegal grade\n");;break;
}
②警惕case穿透现象:没加break,会往下继续执行。因为case标签只会比较一次。
case穿透现象不是一种错误,而是一种机制。
如果下面的标签,需要上一条标签的代码,则上一条标签的break可以省略。但是要加注释,以免别人误以为你漏写了break。
case 4:情景4;/* break through */
case 3:情景3;break;
3.循环语句
(1)while
(2)do while
(3)for
1.for(exp1;exp2;exp3)
2.用for表示无限循环
for( ; ; ){}
4.跳转语句
continue、break、goto、return
5.空语句
;
6.复合语句
(六) 数组
1.一维数组
1.数组的内存模型?
连续的一片内存空间,并且划分成大小相等的小空间。
2.为什么每个元素的大小要设置相同?或者为什么只能存储同一种类型的数据?
答案:为了可以 随机访问元素
i_addr = base_addr + i * sizeof(elem_type)
3.为什么数组的下标 (索引 index) 要从0开始?
答案:若下标从1开始,则计算地址的公式就变成了 i_addr = base_addr + (i-1) * sizeof(elem_type) ,每次都要做一次 i-1 的减法操作,耗能高,需要优化。
现代某些语言的数组下标会从1开始,做法是在开头多申请一块内存空间(下标0),空着不用。
4.刻板印象:数组效率 > 链表效率 的原因?
①空间利用率高 (链表需要存储指针,信息密度不如数组)
②空间局部性好 (数组是连续的,内存读数据的时候会把目标数据前后附近的数据也放在缓存中)
5.声明一个数组
int a[10] = {1,2,3};
6.数组的类型
int arr[4] 的类型为 int [4]
7.数组的初始化
{ } 是数组的 初始化式。
初始化式的长度不可以为0,至少有一个元素。
若初始化式中只有一个元素0,则意思为将整个数组初始化为0
8.数组的操作:
①取下标 [ ]
arr[5];
②数组和for一对好伙伴,经常用for循环来处理数组
(9)数组长度的宏
#define SIZE(a) (sizeof(a)/sizeof(a[0]))
2.多维数组
1.结论:C语言只有一维数组,多维数组本质上也是一维数组。逻辑上是矩阵。
二维数组实际上就是 元素为一维数组 的数组。
n维数组实际上就是 元素维n-1维数组 的数组。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#define SIZE(a) (sizeof(a)/sizeof(a[0]))int main(void) {int matrix[3][7];printf("SIZE(matrix) = %d\n", SIZE(matrix)); //3printf("SIZE(matrix[0]) = %d\n", SIZE(matrix[0])); //7return 0;
}
2.二维数组的声明
int matrix[3][7];
标识符matrix,向右看,知道matrix类型是长度为3的数组。再向右看,其元素是长度为7的数组,向左看,是int类型的
3.二维数组的初始化
int matrix[3][7] = { 0 }; //将整个二维数组全部初始化为0
int matrix[3][7] = { {1,2,3,4},{2,2,3,4},{3,2,3,4} }; //不要省略大括号
4.二维数组的操作:
①二维数组取下标
matrix[1];
matrix[1][5];
②二维数组和嵌套的for循环是一对好伙伴
3.常量数组
1.常量数组:前面加了const
特性:常量数组的元素值不能改变
const int arr[] = {1,2,3,4};
//arr[0] = 100; //报错
2.用处:
①安全,存储静态数据 (在程序的运行过程中不会发生修改的数据)
②处理速度快,效率高 (编译器可以对常量数组做一些极致的优化)
3.结论:能用常量数组的地方尽量用常量数组
4.使用场景:静态数据 (程序运行过程中不会发生修改的数据),存储到常量数组中。如:扑克牌的花色、大小
const char suit[4] = {'S', 'H' ,'C','D'};
const char ranks[13] = {'2','3','4','5','6','7','8','9','T','J','Q','K','A'};
Spade:黑桃♠,Heart:红心♥,Club:梅花♣,Diamond:方块♦
①伪随机数
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>int main(void) {//伪随机数 rand()//seed -> n1 -> n2 -> n3 -> ...srand(time(NULL)); //设置随机种子,考虑以时间作为随机种子,则每次种子都不同printf("rand() = %d\n", rand());printf("rand() = %d\n", rand());printf("rand() = %d\n", rand());return 0;
}
自1970.1.1 00:00:00 GMT时间 到现在的秒数,时间戳
②随机发牌 问题
①花色、大小
const char suit[4] = {'S', 'H' ,'C','D'};
const char ranks[13] = {'2','3','4','5','6','7','8','9','T','J','Q','K','A'};
②随机发牌?
伪随机数
srand(time(NULL)); //设置随机种子,考虑以时间作为随机种子,则每次种子都不同printf("rand() = %d\n", rand());
printf("rand() = %d\n", rand());
printf("rand() = %d\n", rand());
③如何避免重复发牌?
bool in_hand [4][13] = { false };
//bool in_deck [4][13] = { true }; //这样是错误的!C语法规定只能一键全部初始化为0
④完整代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
#include <time.h>
#include <stdlib.h>int main(void) {int cards;printf("Enter number of cards in hand: ");scanf("%d", &cards);//随机发牌const char suits[] = { 'S','H','C','D'};const char rank[] = { '2','3','4','5','6','7','8','9','T','J','Q','K','A' };bool in_hand[4][13] = { false };srand(time(NULL));printf("Your hand: ");while (cards) {int i = rand() % 4;int j = rand() % 13;if (!in_hand[i][j]) {in_hand[i][j] = true;cards--;printf("%c%c ", suits[i], rank[j]);}} // cards == 0return 0;
}
(七) 函数
1.函数的定义和调用
function,一个函数应该可以实现一个功能。
(1)函数的使用准则
①函数的功能越单一越好 (高内聚,低耦合),则复用的概率更高。函数的实现越高效越好。
②C语言是面向过程(函数)的语言:函数是C语言的基本构件组件 (以函数为单位思考问题)。C语言程序的本质就是函数之间的调用。
C语言程序的本质就是函数之间的调用,举例子,掷骰子游戏。高内聚,低耦合。游戏换了,main函数不需要改,只改play_game()函数。
①判断素数
①判断素数
bool is_prime(int n) {int root = sqrt(n); //square rootfor (int i = 2; i <= root; i++) {if (n % i == 0) {return false;}}return true;
}
②完整代码
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
#include <math.h>bool is_prime(int n) {int root = sqrt(n); //square rootfor (int i = 2; i <= root; i++) {if (n % i == 0) {return false;}}return true;
}int main(void) {int n;printf("Enter a number: ");scanf("%d", &n);if (is_prime(n)) {printf("Prime\n");}else {printf("Not Prime\n");}return 0;
}
②掷骰子游戏
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <time.h>int roll_dices(void) {int a = rand() % 6 + 1;int b = rand() % 6 + 1;printf("You rolled: %d\n", a + b);return a + b;
}bool play_game(void) {int nums = roll_dices();if (nums == 7 || nums == 11) {printf("You win!\n");return true;}else if (nums == 2 || nums == 3 || nums == 12) {printf("You lose!\n");return false;}else {int point = nums;printf("Your point is %d\n", point);while (1) {int current_roll = roll_dices();if (point == current_roll) {printf("You win!\n");return true;}else if (7 == current_roll) {printf("You lose!\n");return false;}else continue;}}
}int main(void) {int wins = 0, losses = 0;char again;srand(time(NULL)); //设置随机种子do {play_game() ? wins++ : losses++;printf("\nPlay again? (Y/y continue) ");//scanf(" %c", &again); //方法一:空格 + %c//scanf("%c", &again); //方法二:getchar()//getchar(); //吃掉换行符scanf("%c", &again);while (getchar() != '\n') //getchar()惯用法:跳过该行剩余的字符;} while (again == 'y' || again == 'Y');printf("\nWins:%d, Loses:%d\n", wins, losses);return 0;
}
③德克萨斯扑克
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>int num_in_suit[4]; //每个花色有几张
int num_in_rank[13]; //每个点数有几张bool straight, flush, four, three;
int pairs; //0,1,2void read_cards(void) {//初始化:清零for (int i = 0; i < 4; ++i) {num_in_suit[i] = 0;}for (int j = 0; j < 13; ++j) {num_in_rank[j] = 0;}bool inHand[4][13] = { false };int readCards = 0;while (readCards < 5) {bool badCard = false;//读点数printf("Enter a card: ");char c = getchar();int rank;switch (c) {case '0': exit(0);case '2': rank = 0; break;case '3': rank = 1; break;case '4': rank = 2; break;case '5': rank = 3; break;case '6': rank = 4; break;case '7': rank = 5; break;case '8': rank = 6; break;case '9': rank = 7; break;case 'T': case 't': rank = 8; break;case 'J': case 'j': rank = 9; break;case 'Q': case 'q': rank = 10; break;case 'K': case 'k': rank = 11; break;case 'A': case 'a': rank = 12; break;default: badCard = true; break;}//读花色c = getchar();int suit;switch (c) {case 'D': case 'd': suit = 0; break;case 'C': case 'c': suit = 1; break;case 'H': case 'h': suit = 2; break;case 'S': case 's': suit = 3; break;default: badCard = true; break;}//处理多余的字符while ((c = getchar()) != '\n') {if (c != ' ' && c != '\t') {badCard = true;}}if (badCard) {printf("Bad card; ignored.\n");}else if (inHand[suit][rank]) {printf("Duplicate card; ignored.\n");}else {readCards++;inHand[suit][rank] = true;num_in_suit[suit]++;num_in_rank[rank]++;}}
}void analyze_hand(void) {//初始化straight = false, flush = false, four = false, three = false;pairs = 0; //0,1,2//判断是否为同花 flushfor (int i = 0; i < 5; ++i) {if (num_in_suit[i] == 5) {flush = true;}}//判断是否为顺子 straight//找到第一个下标为1的点数,往后判断5个,看是不是顺子for (int i = 0; i < 13; ++i) {bool strg = true; //先假定是真if (num_in_rank[i] == 1) {for (int j = i; j < i + 4; ++j) {if (num_in_rank[j] != 1) {strg = false;}}straight = strg;break; //仅有5张牌,判断完毕直接退出循环}}//判断是否为三张、四张、两对、一对for (int i = 0; i < 13; ++i) {if (num_in_rank[i] == 4) {four = true;}else if (num_in_rank[i] == 3) {three = true;}else if (num_in_rank[i] == 2) {pairs++;}}
}void print_result(void) {if (straight && flush) {printf("Straight Flush\n"); //1.同花顺}else if (four) {printf("Four of a kind\n"); //2.四条}else if (three && pairs == 1) {printf("Full House\n"); //3.葫芦}else if (flush) {printf("Flush\n"); //4.同花}else if (straight) { printf("Straight\n"); //5.顺子}else if (three) {printf("Three of a kind\n"); //6.三条}else if (pairs == 2) {printf("Two pairs\n"); //7.两对}else if (pairs == 1) {printf("One pair\n"); //8.一对}else {printf("High card\n"); //9.高牌}printf("\n");
}int main(void) {while (1) {read_cards(); //读取用户输入(一副手牌),内含exit(0)analyze_hand(); //分析手牌 (计算) print_result(); //输出结果}return 0;
}
(2)函数声明、函数定义、函数调用、函数指针
函数的声明 void foo(int a);
函数的定义 void foo(int a){ ... }
函数调用 foo(3);
函数指针 foo
、&foo
(编译器将这两个当作同一种东西)
函数指针是指向函数的指针变量。函数指针是函数的入口地址。
2.函数的参数传递、实际参数 与 形式参数
1.实参与形参
实参(argument):函数调用中的参数
形参(parameter):函数定义中的参数
2.C语言中的实参是值传递的。
实际参数的值,按位置,复制给形式参数,这个过程叫做 参数传递。
局限性:不能通过修改形参来改变实参,即在被调函数中无法修改主调函数中参数的值
解决方法:指针
(3)数组作为参数进行传递
1.数组名作为参数进行传递时,退化为指向数组第一个元素的指针,丢失了数组长度的信息 (需要额外传递一个数组长度参数)
2.为什么这样设计?
答案:有3个好处
①避免大量复制
②解决了“在被调函数中不能操作主调函数中的变量的值”的问题
③让函数调用更灵活
当然也有缺点:新手C程序员会分不清形参中的指针和数组
3.局部变量 和 外部变量
1.定义
①局部变量:定义在函数里面的变量
②外部变量(全局变量):定义在函数外面的变量
2.作用域:作用于编译时,变量可以被引用的本文范围
①局部变量的作用域:块作用域:从变量定义开始,到块的结束。即大括号内。
②外部变量(全局变量)的作用域:文件作用域:从定义开始,到文件的末尾。
3.存储期限:作用于运行时,变量绑定的值可以被引用的时间长度 (存储期限就是变量在程序运行过程中存在的时间长度)
按放到虚拟内存不同位置进行分类:
①存在数据段、代码段,静态存储期限:进程启动→进程销毁 (与天地同寿)
②存在栈上,自动存储期限:栈帧入栈→栈帧出栈
③存在堆上,动态存储期限:由程序员手动管理,malloc→free
(1)局部变量
1.定义:在函数体内声明的变量称为该函数的局部变量
2.局部变量的性质:自动存储期限、块作用域
3.static关键字:
①static int i = 1;
只初始化一次,在程序装载阶段,存放在数据段。
4.静态局部变量:存储期限改为静态存储期限,但是作用域仍然不变,仍是块作用域。
5.静态局部变量的作用 / 使用场景:可以存储上一次函数调用的状态 (并只初始化一次)
(2)全局变量
1.定义:外部变量(全局变量)就是声明在任何函数体外的变量
2.全局变量(外部变量)的性质:静态存储期限、文件作用域
3.外部变量的缺点:不利于定位bug
4.return语句
5.程序终止 exit()
exit(0);
头文件:<stdlib.h>
6.递归
(1)递归的概念
1.递归的概念
递:将大问题分解成若干个子问题,子问题和大问题的求解方式一样,只是问题规模不同。
归:将子问题的解,合并成大问题的解
递归的数学公理:数学归纳法
2.学会用递归的思维思考问题:找到递归结构,把大问题分解为小问题
3.递归三问
(1)找到问题的递归结构
(2)要不要使用递归求解?
①存在重复计算,不要用递归
②问题规模缩减的幅度很小,不推荐用递归,容易栈溢出(StackOverflow)
(3)如果以上两个问题都不存在,则可以大胆地使用递归。
考虑两点:
①边界条件 (递归出口)
②递归公式 (你只需要考虑这一层和下一层之间的关系)
栈空间是受限的:主线程8MB,其他线程2MB
(2)循环不变式
初始化、保持、终止
(3)经典例题
递归的例子:电影院伸手不见五指,问在第几排,把问题往前传递,到第一排后,再归回来。
①斐波那契数列
①低效的递归实现 (大量重复计算),时间复杂度为指数级O(2n)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//斐波那契数列: 0,1,1,2,3,5,8,13,21,34,55
long long int Fibonacci(int n) {if (n == 0 || n == 1) return n;else return Fibonacci(n - 1) + Fibonacci(n - 2);
}int main(void) {int n;scanf("%d", &n);printf("fibonacci(%d) = %lld", n,Fibonacci(n));return 0;
}
教训:不是所有具有递归结构的问题,都要用递归的方式求解。
因为这种自顶向下的思维,可能存在大量重复计算,导致效率低下。
②自底向上思考:动态规划,顺序求解子问题,并将子问题的解保存起来,从而避免重复计算,
最终求解到大问题。动态规划可以将指数级复杂度的问题,转变为多项式级别的算法。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>long long int Fibonacci_2(int n) {long long int a = 0;long long int b = 1;//循环不变式:每次进入循环体之前都成立的调条件for (int i = 2; i <= n; ++i) {long long int t = a + b;a = b;b = t;}return b;
}int main(void) {int n;printf("请输入数字n: ");scanf("%d", &n);printf("Fibonacci(%d) = %lld\n", n, Fibonacci_2(n));return 0;
}
②汉诺塔问题
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>void Hanoi(int n,char start,char middle,char target) {//边界条件if (n == 1) {printf("%c -> %c\n", start,target);return;}//递归公式//将上面n-1个盘子从start,经过target,移动到middle上Hanoi(n - 1, start, target, middle);//将最大的盘子从start直接移动到target上printf("%c -> %c\n", start, target);//将上面n-1个盘子从middle,经过start,移动到target上Hanoi(n - 1, middle, start, target);
}int main(void) {int n;scanf("%d", &n);//计算最少需要移动的次数//S(n) = S(n-1) + 1 + S(n-1)//S(n) + 1 = S(n-1) + 1 + S(n-1) + 1 = 2 * [S(n-1) + 1]//故S(n) + 1 为公比为2的等比数列,且首项为 S(1) + 1 = 1 + 1 = 2//故由等比数列公式 an = a1*2^(n-1),即 S(n) + 1 = 2*2^(n-1) = 2^n, 故 S(n) = 2^n -1printf("Total steps: %lld\n", (1LL << n) - 1);//打印移动步骤Hanoi(n, 'A', 'B', 'C');return 0;
}
③约瑟夫环问题
1)每隔一个人,出局一个人
找到:边界条件、递归公式
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int joseph(int n) {//边界条件if(n == 1 || n == 2){return 1;}//递归公式if(n & 0x1){ //n为奇数return 2 * joseph(n >> 1) + 1; //n >> 1,即 n/2}else{ //n为偶数return 2 * joseph(n >> 1) - 1;}
}int main(void) {printf("每隔一个人,出局一个人,请输入初始玩家人数: ");int n;scanf("%d", &n);printf("joseph(%d) = %d\n", n, joseph(n));return 0;
}
2)每隔m个人,出局一个人
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int joseph(int n,int m) {//边界条件if (n == 1){return 0; //从0开始编号}//递归公式return (joseph(n-1,m) + m) % n;
}int main(void) {int n,m;printf("每隔一个人,出局一个人,请输入初始玩家人数: ");scanf("%d", &n);printf("请输入每隔多少人出局一个人: ");scanf("%d", &m);printf("joseph(%d,%d) = %d\n", n, m, joseph(n,m) + 1); //从0开始编号return 0;
}
(八) 指针
计算机最小寻址单位:字节
变量的地址:变量首字节的地址
指针:就是地址
指针变量:存储地址的变量
1.指针基础
(1)指针的声明
int *p
*说明了p是指针
int是指向对象的类型:①说明对象所占内存大小 ②如何解释那片内存空间 (说明了对象的类型)
变量名:p,类型:int *
(2)指针的两个基本操作
0.示例
i:直接访问,逻辑上访问内存一次
p:间接访问,逻辑上访问内存两次
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>int main(void) {int i = 1;int* p = &i;printf("*p = %d\n", *p);*p = 2; //*p 是 i 的别名,有读写权限printf("i = %d\n", i);return 0;
}
1.取地址运算符 &
2.解引用运算符 *
(3)野指针
1.野指针
野指针:不知道指向哪块数据
int* p;
int* q =0xABCD;
正确地给指针变量赋值
int *p = &i;
int *q = p;
p = NULL;
2.空指针
空指针(null pointer):不指向任何对象的指针,不指向任何有效的内存地址
3.指针变量的赋值 vs 指针变量指向对象的赋值
(4)指针的应用
①指针作为参数进行传递
在被调函数中修改主调函数中变量的值,解引用
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>void swap(int* p, int* q) {int temp = *p;*p = *q;*q = temp;
}int main(void) {int a = 3, b = 4;printf("a = %d, b = %d\n", a, b);swap(&a, &b);printf("a = %d, b = %d\n", a, b);return 0;
}
②指针作为返回值
教训:不要返回指向当前栈帧区域的指针 (因为返回以后,该栈帧就出栈了,变量被销毁)