C++(week1):C语言基础

文章目录

(零)王道的价值

王道线下比起书、博客的优势,或者付费的价值是什么?
①老师是如何组织/搭建整个知识结构的?
②老师是如何思考问题的?
③写代码的时候,有哪些需要注意的细节?


(一) C语言概述

C语言:函数、结构体、指针

VS的使用:
①X86:
在这里插入图片描述


1.预处理指令:宏定义、宏函数

#:预处理指令
1.#include :头文件复制过来
2.宏定义: 是 文本替换
3.宏函数:也是文本替换用 实参 替换 形参
(1)注意事项
①左括号紧贴宏函数名,否则会当成宏定义
②参数要括起来,整个表达式也要括起来
(2)为什么要用宏函数?/ 使用宏函数的好处:
①宏函数快,仅仅是替换,避免了函数调用开销
应用场景:简短的、频繁调用的函数,写成宏函数,可以降低函数调用开销
②提供了一定的宏编程能力

在这里插入图片描述

#define SWAP(arr, i ,j){	\
	int temp = arr[i];		\
	arr[i] = arr[j];		\
	arr[j] = temp;			\
}

#define SWAP2(a,b){		\
	int temp = a;		\
	a = b;				\
	b = temp;			\
}

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,用于打印月份、日期

%5s:至少输出5个字符宽度的字符串。不足5个字符则左边补空格,超过5个字符则直接输出。
%.5s:至多输出5个字符。不足则直接输出,超过则直接截断。

%s:数字 是至少,点数字 是至多

代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main(void) {
	//%5s不足:左边用空格补足5个字符
	printf("%5s\n", "He");
	//%.5s不足:直接输出
	printf("%.5s\n", "He");
	//%5s超过:直接输出
	printf("%5s\n", "Hello,World!");
	//%.5s超过:截断,只输出前5个字符
	printf("%.5s\n", "Hello,World!");

	return 0;
}

输出结果:
在这里插入图片描述


①格式化输出 与 进制

1.格式化输出:
%d:整数 int
%f:单精度浮点数 float
%lf:双精度浮点数 double
%c:字符型 char
%hd:short
%hu :unsigned short
%u:无符号整数 unsigned
%o:八进制 oct
%x:十六进制数(小写字母) hex
%X:十六进制数(大写字母) HEX
%p:指针、打印地址 pointer


2.进制

int a = 0xa;   //十六进制以0x开头
int b = 010;   // 八进制以0开头

(3)scanf

(1)格式化输入:scan format

(2)工作原理:拿stdin里面的数据和格式串进行匹配,从左到右依次匹配格式串中的每一项。如果有一项匹配不成功,scanf会立刻返回。scanf的返回值匹配成功的转换说明的个数

(3)格式串:
普通字符精确匹配
②空白字符:任意个空白字符 (包括0个)
③转换说明:
%d:忽略前置的空白字符(' ''\t''\n''\r''\v'),匹配一个十进制的有符号整数
%f:忽略前置的空白字符,匹配一个浮点数
%c:匹配一个字符,不会忽略前置的空白字符

Q:匹配第一个非空白字符,转换说明怎么写?
A:空格%c


3.代码即注释

1.代码即
①给变量起好听的名字
②留白,一个功能段写完要空行,不要写成一坨。


4.程序出错的原因、调试程序

详情请见:https://blog.csdn.net/Edward1027/article/details/135511540

1.程序出错的原因:
(1)编译错误:语法错误
(2)链接错误:①没有包含对应的头文件 ②函数名写错了
(3)运行时错误:逻辑错误,即BUG

2.调试程序:
①打断点、取消断点
②逐过程:遇到函数调用,执行整个函数
③逐语句:进入到自定义的函数中
④继续:运行到逻辑上的下一个断点
⑤跳出:执行完该被调函数 callee,返回主调函数 caller

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


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); //0

	float f2 = -1.5;
	i = f2;
	printf("i = %d\n", i); //-1
	return 0;
}

(3)标识符

字符、数字、下划线


(三) 基本数据类型

0.基本数据类型

(1)整型 (整数类型) short、int、long、long long、unsigned short、unsigned int、unsigned long、unsigned long long
(2)浮点型 (浮点数类型) float、double
(3)字符型 (字符类型) char

布尔型bool、指针类型%p


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

在这里插入图片描述


(1)字符处理函数:大小写转换函数
//大小写转换函数
#include <ctype.h>
int tolower(int c);
int toupper(int c);

在这里插入图片描述


扩展了字符类型支持的操作
在这里插入图片描述


5.和用户交互:读/写

(1)printf + %c 、scanf + %c

%c:匹配一个字符 (不忽略空白字符)

scanf("%c %c",&c1,&c2); //注意%c之间要有空格,匹配空白字符。跳过空白字符,读取下一个非空白字符

(2)putchar( c)、getchar()

putchar( c)、getchar() 更高效,读写一个字符 (字符数据)

char c = getchar();
putchar(c);

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);      //171
	printf("%x\n", u << 2); //0x2ac
	printf("%u\n", u << 2); //171*4 = 684

	printf("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);      //171
	printf("%X\n", u >> 2); //0x2A
	printf("%u\n", u >> 2); //171/4 = 42

	printf("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 0101
	j = 56;            // 0000 0000 0011 1000
	k = ~i;    //按位取反 1111 1111 1110 1010 ,即 65535-16-4-1 = 65514
	k = i & j; //按位与   0000 0000 0001 0000 ,即 16
	k = i | j; //按位或   0000 0000 0011 1101 , 即 61
	k = i ^ j; //按位异或 0000 0000 0010 1101 ,即 45

	return 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 == -1
	else            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 > 0
	while(n % 2 == 0){
		n /= 2; 
	}
	return n == 1;
}

②用位运算优化

bool isPowerof2(int n){
	// n > 0
	while((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) != 0
	return 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= 3
	return 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)); //单独的数为4

	return 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){
			//为1
			a ^= nums[i]; //连连看,异或消除一对
		}else{
			//为0
			b ^= 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(表达式){
case1://代码块1
		break;
case2://代码块2
		break;
...
...
case 值n://代码块n
		break;
default://默认代码块
}

②缩进一级

switch (expression) {
    case 1:
        // 代码块
        break;
    case 2:
        // 代码块
        break;
    default:
        // 代码块
}

3.switch case的限制条件:
①switch后的表达式必须是整型 (int、char、枚举类型)
②switch后的表达式和case的标签,只能用 == 做比较,不能用大于小于


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循环来处理数组

注意:数组没有赋值操作!不可以将一个数组赋值给另一个数组。比如 str2 = str1是错误的!需要一个元素一个元素的修改


(9)求数组长度的宏:SIZE(a)
#define SIZE(a) (sizeof(a)/sizeof(a[0]))

注意:SIZE(a)仅能对数组使用,不可以对字符串、指针使用!
注意区分数组与指针!(数组作为参数传递时会退化为指针)


在这里插入图片描述


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));        //3
	printf("SIZE(matrix[0]) = %d\n", SIZE(matrix[0]));  //7
	return 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>
#include <time.h>

int main(void) {
	srand(time(NULL)); //设置随机种子,考虑以时间作为随机种子,则每次种子都不同
	int arr[10];
	for (int i = 0; i < 10; i++) {
		arr[i] = rand(); //伪随机数 rand(),seed -> n1 -> n2 -> n3 -> ...
	}

	for (int i = 0; i < 10; i++) {
		printf("%d ", arr[i]);
	}
	printf("\n");

	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 == 0
	return 0;
}



(七) 函数

1.函数的定义和调用

function,一个函数应该可以实现一个功能。

(1)函数的使用准则

函数的功能越单一越好 (高内聚,低耦合),则复用的概率更高。函数的实现越高效越好。
C语言是面向过程(函数)的语言函数是C语言的基本构件组件 (以函数为单位思考问题)。C语言程序的本质就是函数之间的调用

C语言程序的本质就是函数之间的调用,举例子,掷骰子游戏。高内聚,低耦合。游戏换了,main函数不需要改,只改play_game()函数。

在这里插入图片描述


①判断素数

在这里插入图片描述
①判断素数

bool is_prime(int n) {
	int root = sqrt(n); //square root
	for (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 root
	for (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,2

void 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

	//判断是否为同花 flush
	for (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 flag = true; //先假定是真
		if (num_in_rank[i] == 1) {
			for (int j = i; j < i + 4; ++j) {
				if (num_in_rank[j] != 1) {
					flag = false;
				}
			}
			straight = flag;
			break;   //仅有5张牌,判断完毕直接退出循环
		}
	}
	if(straight)	return;  //若为顺子,则不可能为四张、三张、两对

	//判断是否为四张、三张、两对、一对
	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.数组名作为参数进行传递时,退化为指向数组第一个元素的指针,丢失了数组长度的信息 (需要额外传递一个数组长度参数)
&arr[0] 就可以写为 arr

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()

#include <stdlib.h>

exit(0); //正常退出
exit(1); //异常退出

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 -1

	printf("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;
}
  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员爱德华

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值