文章目录
- 前言
- 一、学习资料汇总
- 二、学习C语言基础
- 三、C语言学习
- 1.知识点补充
- (1)全局变量与局部变量
- (2)使用另一个源文件中定义的全局变量
- (3)static修饰局部变量
- (4)const修饰的常变量
- (5)代码监控
- (6)转义字符
- (7)!的应用场景
- (7)数值存取
- (8)强制类型转换
- (9)逗号表达式
- (10)函数调用操作符
- (11)指针
- (12)逻辑与使用情况
- (13)输出奇数的另一种思路
- (13)查找特定字定义
- (14)设定随机数
- (15)观察函数内部
- (16)switch语句
- (17)goto语句容易导致死循环
- (18)学习库函数方法
- (19)实现交换整形变量的函数
- (20)if的执行问题
- (21)模块化(多人协作)
- (22)函数调用
- (23)指针类型的意义
- (24)指针+-整数
- (25)野指针
- (26)指针运算
- (27)用指针-指针实现strlen函数
- (28)指针的关系运算
- (29)指针表达式
- (30)二级指针
- (31)函数调用的数据压栈
- (32)创建指针数组
- (33)函数指针
- (34)函数指针数组
- (35)回调函数的应用
- (36)冒泡函数的完全形态
- (37)vs如何查看内存
- 2.错题/改进题/例题汇总
- (1)变量使用
- (2)strlen函数使用
- (3)转译字符
- (4)scanf输入非法字符的检查和处置
- (5)键盘缓冲区
- (6)二分查找
- (7)对称逐字符显示练习
- (8)密码输入练习
- (9)猜字游戏
- (10)数字排列
- (11)最大公约数
- (12)100-200的素数
- (13)判断闰年最简写法!!
- (14)整形有序数组的二分查找
- (15)编写函数不允许创建临时变量,求字符串长度
- (16)用递归计算阶乘
- (17)青蛙跳台阶问题
- (18)函数测试题错题
- (19)打印99乘法表
- (20)字符串逆序
- (21)递归求一个数每位之和
- (22)求n的k次方
- (23)冒泡排序
- (23)小测验
- (24)求水仙花数
- (25)指针与数组试题解析
- (26)指针笔试题
- (27)判断机器的字节序
- (28)数据的存储错题
- (29)浮点数的存储
- (30)求出未知二维数组的行数和列数
- (31)杨氏矩阵
- (32)字符串左旋
- 字符串函数及其模拟实现
- 自定义类型详解
- 动态内存管理
- c/c++的内存管理
- 3.Linux C错题/改进题/例题汇总
前言
今天是2023年7月23日,受up主“鹏哥C语言”《C语言从入门到精通》的启发,我下定决心写CSDN博客来记录自己一系列知识学习的经历。
先回顾个人情况。我们学校大一时开始学习C语言,受错误的学习方法,不太端正的学习态度影响,我的C语言底子比较薄。知识停留在理论层面,基本没有项目动手的经验。
下面记录自己学习C语言的经历,持续更新。。。
一、学习资料汇总
学习资料:
1.选择实践性强的教学视频能提高自身的学习能动性——鹏哥C语言
2.c语言刷题的网站——Dotcpp非常适合入门学习
3.一生一芯的教学讲义中推荐“笨办法学C”的练习题入门
4.学习库函数的网站
链接如下:
C语言学习路径
笨方法学C
一生一芯——复习C语言
库函数
中文c语言网站
二、学习C语言基础
1.Git学习
之前在一生一芯“Linux系统安装和基本使用”中已经了解了git的原理和底层模型,现在通过狂神Git教程进行进阶学习
(1)版本控制
【1】主流的版本控制器
【2】版本控制的分类
*1 本地版本控制
*2 集中版本控制
*3 分布式版本控制
*4 git与svn的区别
(2)Git环境配置
【1】Git配置
查看配置 git config -l
查看不同级别的配置文件:
#查看系统config
git config --system --list
#查看当前用户配置
git config --global --list
Git相关配置文件:
注意:环境变量的作用是为了全局使用!
【2】Git基本理论
*1 工作区域
*2 工作流程
【3】Git项目搭建
*1 创建工作目录和常用指令
*2 创建本地仓库
【4】Git文件操作
*1 文件的四种状态
*2 查看文件的状态
-m后面跟着在编辑器中想输入的信息
eg。 git commit -m “add cat()”
*3 忽略文件
链接如下:
计算机教育中缺失的一课
Git教程
(3)Githup使用技巧
【1】查找资源
常用的前缀后缀
#找百科大全 awesome xxx
#找例子 xxx sample
#找教程 xxx tutorial
#找空项目架子(配置好的样板项目) xxx starter/ xxx boilerplate
【2】怎么找开源项目
• GIithub自带搜索开源项目
• 寻找开源项目的媒体
• 科技爱好者周刊
2.vs学习
(1)操作等注意事项
【1】创建项目
【2】再创建新项目
【3】报错处理
第一个框是输入错误,第二个框是运行时错误
【4】多个源文件问题
1.多个源文件,只能有一个main函数,其他主函数可以加下标
2.只运行一个源文件时,可以把其他主函数注释掉
【5】让乱的代码对齐
Ctrl+a全选 Ctrl+k+f
【6】查看内存分配情况
&a查看变量地址
0a是两个十六进制数,共占8位,也就是1个字节
E8加上前面的这一串,表示的是0a这个数据的内存地址
E9加上前面的这一串,表示的是00这个数据的内存地址
Ea加上前面的这一串,表示的是下一个00这个数据的内存地址
以此类推
问:为什么不是00 00 00 0a?
答:先存低位,后存高位
三、C语言学习
学习资料:
(1)鹏哥c语言板书+课件+视频
先看课件和板书,到重点部分再定位到视频观看
(2)C prime plus
(3)刷题网站+csdn刷题库
(4)Linux c教程+笨方法学c语言
1.知识点补充
(1)全局变量与局部变量
g_val全局变量 a局部变量
(2)使用另一个源文件中定义的全局变量
(3)static修饰局部变量
static的作用:当 static 关键字用于局部变量时,它使得变量的生命周期延长至整个程序的运行期间,而不仅限于函数的执行期间。
也就是说,当 text() 函数执行完毕后,变量 a 并不会被销毁,下次调用 text() 函数时,它会保留上一次的值。
【1】底层分析
【2】a++和++a的区别
#include<stdio.h>
int main()
{
int i = 0, j = 0,x,y;
x=i++;
y=++j;
printf("%d %d", x, j);//输出结果为 0 1
return 0;
}
i++是先将i的值赋给了x再自加
++j是先自加然后再将值赋给y
(4)const修饰的常变量
数组要求方块内的值为常数。const修饰的常变量也不行
(5)代码监控
按F10进入调试
观察数组:
输入数组名得到值
(6)转义字符
??) 防止 ??) 被转译为 ]
转译字符打印出 ‘
三位八进制符号130对应的十进制数为88,88对应的ASCII码为X
(7)!的应用场景
(7)数值存取
int a = 0再按位取反位32个1,此时以补码的形式储存在内存中。但printf以%d打印出来时是原码形式,32个1的原码为-1
(8)强制类型转换
(9)逗号表达式
(10)函数调用操作符
(11)指针
先去看vs内存分配情况的知识点!
【1】指针变量
(12)逻辑与使用情况
(13)输出奇数的另一种思路
(13)查找特定字定义
鼠标悬停时,显示int整型值为32767
(14)设定随机数
在调用rand()之前,使用srand()来设置随机数生成器
(15)观察函数内部
在语句左侧打断点,F10开始调试,调出监控窗口,按F11逐语句监视
(16)switch语句
char是字符类型,可以看作整形,但float是浮点型不成立
(17)goto语句容易导致死循环
goto语句弊端:
(18)学习库函数方法
(1)destination为指针目的地源头,source为指针地址起点
(19)实现交换整形变量的函数
进入函数,形式参数开始实例化。整个过程交换的实质上是x,y两个形式参数变量的值,与a,b没有关系
(20)if的执行问题
if、elseif只会执行一个。条件进入if后判断不成功也不会再进入elseif
(21)模块化(多人协作)
添加其他函数时,需要在main()函数中放置对应的函数实现,main()函数外部放置函数声明
–多人协作中只想给对方使用权而不想让他看到源码该怎么办?
(1)先将代码.h和.c文件放到自己项目路径下
(2)右击项目名称,选择属性
(3)将项目类型改成静态库,先点应用再点确定
(4)按Ctrl+F7编译,在项目目录的debug文件下会生成后缀为.lib的静态库,点进去看都是乱码
(5)将.lib和对应的.h文件发给对方(不要给.c源码)
(6)对方要将.lib和.h文件放在项目中,导入静态库和.h文件
(22)函数调用
找规律:整个过程就是%10和/10交替进行
打印的一般思路:将4321放入一个数组中,再逆序打印出来——可以但麻烦
递归思路:
内部的print用于改变外部print下一次的值
123在print中一直调用到1才开始打印,打印完成后返回上一级调用的函数打印开始打印2。。。以此类推
递归——一层一层递进,一层一层归还!!
-————如果每次递归调用都分配大的内存空间,栈可能被耗干,出现栈溢出的现象
(23)指针类型的意义
指针类型为int时——指针变量解引用操作时全变为0
指针类型为char时——指针变量解引用操作时仅有一个字节的数据变为0
解引用权限与步长,仅仅与指针类型有关,与对象没关
(24)指针±整数
变量类型为char,一次只访问一个字节
若想对数组的每一个字节都指定内容,使用char类型的指针操作
若想对数组以一个元素为单位操作时,使用int类型的指针操作
(25)野指针
1.未初始化
2.指针的空间释放
如何规避野指针
指针使用之前检查其有效性
指针变量不知道指向什么地方时,置为空指针。指针指向的空间被释放后,也置为空指针。当指向有效空间时,给指针地址。故能保证指针为有效空间或者空地址
此时当每次使用指针变量时都判断一下是否为空指针,这样能避免野指针的出现
(26)指针运算
指针运算符和++运算符同级,从右往左算先解引用赋值0,vp再自增
即vp++先解引用再自加。整条程序,先执行*vp=0,再执行vp+1
例子:
1.
2.指针减去指针,得到两个数之间的元素个数,打印出9
3.
(27)用指针-指针实现strlen函数
原来版本:
新版:
(28)指针的关系运算
可以拿数组中的元素与数组后面这块空间比较大小,即可以与向后空间元素比较大小
但vp向前走时不能访问第一个元素前面空间的地址
(29)指针表达式
重点![]是一个二元操作符!
p[2]<=>*(p+2) 2[arr]<=>arr[2]
(30)二级指针
靠近ppa的表示ppa为指针,ppa指向a,而pa的类型为int,**ppa==a
(31)函数调用的数据压栈
压栈:向栈中存放数据
出栈:从栈中取数据
传参时一般参数由右向左传,即先传b再传a
这就是函数传参的压栈操作
通过一定的手段将外空间的x和y拿上来相加,赋值给8。调用结束后,main函数外的其他空间都结束了,只剩下main函数
故传入结构体时,参数在外空间的占用会很大,影响系统性能
(32)创建指针数组
注意这不是二维数组,二维数组的地址是连续的。
注意打印数据的两种表达方式
[]是一个操作符,意思是里面和前面的东西相加再解引用
(33)函数指针
因为Add==&Add,故Add=pf
int ret = (*pf)(3,5) <=>int ret = pf(3,5) <=>int ret = Add(3,5)
(在函数指针中,*号就是摆设)
代码一:
(1)原理剖析:只要能将0转换成函数指针类型,0就会被看作成一种函数的地址
故void(*)()0是对0强制类型转换为函数指针,此时0为该类型的函数的地址
————
若void(*p)()0含p,这是一种函数指针变量。不含p,则为一种函数指针类型
(2)用(void()()0)相当于找到了0地址对应的函数
(3)因0地址对应的函数无参数——解引用后调用函数时括号也为空
(void( )()0)()
代码二:
分析函数名和函数参数——signal(int,void(**)(int))可看出是一个函数指针类型为参数的函数
函数名,函数参数拿走,剩下的就是函数返回类型,void()(int)——也是一个函数指针类型
简化:
语法要求必须与函数名靠在一起
(34)函数指针数组
转移表练习:
限制:Pfarr()数组中各个函数的参数,返回类型必须相同
(35)回调函数的应用
例子:qsort函数
base指向被排序数组中的第一个对象(void*为无具体类型的指针,任意数据的base都可放入函数中)
size为传输数据中一个元素有多大的字节
cmp返回的整形若大于0,表示第一个元素大于第二个元素。若等于0,两个元素相等。若小于0,第一个元素小于第二个元素
qsort函数对结构体按年龄排序:
(36)冒泡函数的完全形态
#include <stdio.h>
int int_cmp(const void * p1, const void * p2)
{
return (*( int *)p1 - *(int *) p2);
}
void _swap(void *p1, void * p2, int size)
{
int i = 0;
for (i = 0; i< size; i++)
{
char tmp = *((char *)p1 + i);
*(( char *)p1 + i) = *((char *) p2 + i);
*(( char *)p2 + i) = tmp;
}
}
void bubble(void *base, int count , int size, int(*cmp )(void *, void *))
{
int i = 0;
int j = 0;
for (i = 0; i< count - 1; i++)
{
for (j = 0; j<count-i-1; j++)
{
if (cmp ((char *) base + j*size , (char *)base + (j + 1)*size) > 0)
{
_swap(( char *)base + j*size, (char *)base + (j + 1)*size, size);
}
}
}
}
int main()
{
int arr[] = { 1, 3, 5, 7, 9, 2, 4, 6, 8, 0 };
//char *arr[] = {"aaaa","dddd","cccc","bbbb"};
int i = 0;
bubble(arr, sizeof(arr) / sizeof(arr[0]), sizeof (int), int_cmp);
for (i = 0; i< sizeof(arr) / sizeof(arr[0]); i++)
{
printf( "%d ", arr[i]);
}
printf("\n");
return 0;
}
-
void* 的作用
void* 是C语言中的一个特殊类型,它被称为“无类型指针”(void pointer)。它的主要作用是在不确定或通用的上下文中存储和传递内存地址,而不关心地址中具体存储的数据类型。以下是 void* 的主要作用和用途:通用性:void* 是通用的指针类型,可以指向任何类型的数据,包括基本数据类型(如整数、浮点数等)和自定义数据类型(如结构体、数组等)。这使得它非常灵活,可以用于处理各种不同类型的数据。
存储内存地址:void* 可以用来存储内存地址,而不关心这个地址指向的具体数据类型。这在需要将地址传递给函数或数据结构时非常有用,因为你可以使用 void* 来表示各种不同类型的数据的地址。
泛型编程:void* 在泛型编程中非常有用,因为它允许你编写通用的函数和数据结构,能够处理多种数据类型。通过使用 void*,你可以实现像泛型排序算法、通用数据结构(如通用链表)等功能。
动态内存分配:在动态内存分配中,函数如 malloc、calloc 和 realloc 返回的指针通常是 void* 类型。你可以将这些指针转换为适当的数据类型指针,然后使用它们来访问分配的内存块。
函数指针:void* 可以用于存储函数指针,这在一些高级应用中非常有用。你可以使用 void* 存储不同类型的函数指针,并在运行时根据需要调用这些函数。
需要注意的是,使用 void* 会失去类型安全性,因为编译器无法检查和强制执行指针的类型正确性。因此,在使用 void* 时需要谨慎,确保在转换为特定类型的指针之前,你已经了解了内存块中的数据类型,以避免潜在的运行时错误。
2.bubble
void bubble(void *base, int count, int size, int(*cmp)(void *, void *)):这是函数的声明,它接受四个参数:base:一个指向待排序数组的指针,但是它的类型是 void*,因此可以接受任何类型的数组。
count:数组中元素的数量。
size:每个元素的大小(以字节为单位)。
cmp:一个函数指针,用于比较两个元素的大小,这个函数接受两个 void* 类型的参数。
int i = 0; int j = 0;:声明两个循环变量 i 和 j,用于遍历数组和执行冒泡排序。for (i = 0; i < count - 1; i++):外层循环,控制排序的轮数。每一轮将确定一个元素的最终位置,因此需要执行 count - 1 轮。
for (j = 0; j < count - i - 1; j++):内层循环,控制每一轮中比较和交换元素的次数。内层循环逐渐减小,因为每一轮都会确定一个元素的位置,所以不需要再考虑它。
if (cmp((char )base + j * size, (char )base + (j + 1) * size) > 0):这是比较元素的部分。它使用 cmp 函数来比较数组中两个元素的大小。注意,由于 base 是 void 类型,所以在比较之前,它被转换成 char 类型,以确保以字节为单位进行偏移。
如果 cmp 返回值大于 0,表示当前元素比下一个元素大,需要交换它们的位置。这里调用了 _swap 函数,将两个元素的数据进行交换,同时传递了元素的大小 size,以确保正确地交换数据。
循环结束后,数组中的元素将根据比较函数的规则排好序。
3._swap函数
void _swap(void *p1, void *p2, int size):这是函数的声明,它接受三个参数:p1 和 p2:这两个参数是指向待交换内存块的指针,类型为 void*,这表示它们可以指向任何类型的数据。
size:表示每个内存块的大小(以字节为单位),这个参数用于确定交换的范围。
int i = 0;:声明一个循环变量 i,用于遍历内存块中的字节。for (i = 0; i < size; i++):这是一个循环,用于遍历内存块中的每个字节。
char tmp = *((char *)p1 + i);:这一行代码执行了以下操作:
(char )p1:将指针 p1 强制转换为 char 类型,这是因为我们要以字节为单位进行操作。
*((char *)p1 + i):使用指针 p1 加上偏移 i 来访问内存块中的第 i 个字节,并将其值赋给 tmp 变量。这是为了保存要交换的值。
*((char *)p1 + i) = *((char *)p2 + i);:这一行代码将 p1 中第 i 个字节的值设置为 p2 中第 i 个字节的值,实现了两个内存块内容的交换。*((char *)p2 + i) = tmp;:这一行代码将 p2 中第 i 个字节的值设置为之前保存在 tmp 中的值,完成了交换操作。
4.int_cmp
return (*(int *)p1 - *(int *)p2);:这一行代码执行了以下操作:(int *)p1 和 (int )p2:首先,将 p1 和 p2 强制转换为 int 类型的指针,这是因为我们知道这两个指针实际上指向整数数据。
*(int *)p1 和 *(int *)p2:然后,通过这些指针来访问指向的整数数据,并获取它们的值。
*(int *)p1 - *(int *)p2:最后,将两个整数值相减,得到一个整数作为比较结果。如果结果为正数,表示第一个整数大于第二个整数;如果结果为负数,表示第一个整数小于第二个整数;如果结果为零,表示两个整数相等。
(37)vs如何查看内存
F10+
输入&+变量名即可观察变量的地址,再根据类型的大小选择对应的列数
2.错题/改进题/例题汇总
(1)变量使用
注意变量要初始化 !!
解决方案:
(1)整个源文件的第一行加上 #define _CRT_SECURE_NO_WARNINGS 1
(2)将声明添加到源文件中
(2)strlen函数使用
(3)转译字符
转移字符要当作一个字符看!---->“\t \32”
8不是八进制符,单独当作一个字符看
所以一共14个字符
(4)scanf输入非法字符的检查和处置
scanf输入非法字符的检查和处置
我的代码:
出错分析:在代码中并没有使用scanf的返回值进行任何处理。scanf函数的返回值在这里是有用的,它可以告诉你成功读取的变量的个数,帮助你判断输入是否合法。
例如,当用户输入一个非法字符(如字母、特殊符号等),scanf将无法将其转换为整数,此时它会返回0,表示未成功读取任何变量。
修改过后:
在修改后的代码中,while循环的条件使用了scanf的返回值和用户输入的值进行判断,只有在成功读取一个整数且其值为0或1时,循环才会结束。这样,当用户输入非法字符时,程序会提示用户重新输入,直到输入合法的选项为止。
(5)键盘缓冲区
输入A与\n后,键盘先读取了a,再读取 \n
原理分析
getchar()和scanf()从中间的缓冲区读取数据
回车\n会触发scanf()读内容,敲回车会将输入放入缓冲区中,然后scanf ()才会读取输入则缓存区内会只剩下 \n。
接下来getchar()会直接读取\n并输出“确认失败”。
解决方案在板书中
其他导致错误的输入情况
scanf()取走函数后,还剩下 abcdef\n这一串字符,而getchar只能解决一个字符
解决方案:
getchar()读取一个字符,会用整形变量来存。(因为ASCII码值是用整形存取的)
空语句 ;用于清除多余的字符
(6)二分查找
下次的进阶点:对无序数组先排序,再二分查找
(7)对称逐字符显示练习
我的代码
缺点:使用sizeof会算入字符串的中止符\n,共多了两个字符。算数组字符需要减2
老师代码:
优化:
1.使用了strlen()函数,可计算字符串字符个数。但要注意引string.h头文件
2.使用sleep()函数,可实现逐行显示效果。要引windows.h头文件
3.将printf()放在赋值后,避免了对最中间字符的赋值的调整
(8)密码输入练习
我的代码:
老师代码
优点:
1.使用了strcmp()函数,可以比较字符串数组内容是否相同。若相同,返回0.注意要引头文件string.h
strcmp()函数比较对应位置上字符的ASCII值,当出现不相等的情况,大的字符所在的字符串大于小的字符所在的字符串。
eg:abcdfg>abccdggggg
(9)猜字游戏
知识点:
1.srand()----随机数生成器–引头文件stdlib.h
缺点:手动传入srand()函数的数字是固定,无法随时改变
传入一个随时发生变化的值——时间戳(随时间发生变化的数字)
2.rand()
3.time()——引头文件time.h
使用time()返回一个时间戳。为不使用它的参数,传入一个NULL值。
又time()的返回类型为time_t
而srand需要unsigned int类型,故使用强制类型转换把time类型转换为unsigned int类型
目前缺点:快速按动按键时,出现的值接近甚至相等,出现的值不够随机
解决方法:srand函数的生成只需要一设置次即可,否则每次玩游戏都需要设置不合适,将其放入主函数中,生成的数将完全随机
目前缺点:此时随机数的范围为0-32767,而题目要求为1-100
解决方法:
(10)数字排列
我的思路:排列位置
老师代码:位置不动,交换数值
但三个模块的代码还是很现实的,同质化也比较高——>写出一个函数调用变量,调用三次函数即可
(11)最大公约数
初版有问题代码:
出现的问题和修正
1.
for (c-1; c > 1; c--)
{
if (a % c == 0)
yueshu_a[c] = c;
}
for (d-1; d > 1; d--)
{
if (b % d == 0)
yueshu_b[d] = d;
}
for 循环的第一个部分应该是初始化表达式,但我写的是 c-1 和 d-1
2.
for (int i = 100; i > 1; i--)
{
if (yueshu_a[i] != 0)
compare[i] = yueshu_a[i];
if (yueshu_b[i] != 0)
compare[i] = yueshu_b[i];
}
这个循环的目的是将 yueshu_a 和 yueshu_b 数组中的非零值放入 compare 数组中,但是这样的逻辑是错误的,因为每次循环都会执行 compare[i] = yueshu_a[i];,然后再执行 compare[i] = yueshu_b[i];,这样最终 compare[i] 的值只会等于 yueshu_b[i] 的值。也就是说,yueshu_a[i] 的值会被覆盖掉。我们应该选择更大的那个数,才能得到正确的最大公约数。
改正的方法是在循环中添加一个判断,如果 yueshu_a[i] 和 yueshu_b[i] 都不为零,应该选择较大的那个值放入 compare[i]
3.在找约数的时候,循环条件为 c > 1 和 d > 1,这会导致 1 无法被正确处理。应该修改循环条件为 c >= 1 和 d >= 1。
4.循环 for (int j = 100; j > 1; j–) 的条件也有问题,它会漏掉 compare[1] 的值,应该修改为 for (int j = 100; j >= 1; j–)。
5.数组越界问题
在代码中,我声明了三个数组 yueshu_a、yueshu_b 和 compare,它们的大小都是 100。但在程序运行时,数组 yueshu_a 和 yueshu_b 的有效索引是从 1 到 99,而不是从 0 到 99。因为在循环中我写了 yueshu_a[c] = c; 和 yueshu_b[d] = d;,这会导致数组访问越界,因为当 c 和 d 等于 a 和 b 时,数组 yueshu_a 和 yueshu_b 将会访问 yueshu_a[a] 和 yueshu_b[b],这是无效的索引。
最终版成功实现代码:
老师思路和代码(1):
1.最大公约数不会大于两个数中的最小数,故先比较得出最小数
2.对最小数用for循环递减,第一个能同时整除a和b循环变量i就是最大公约数
老师思路和代码(2):
辗转相除法
假设两个数m与n,m为24,n为18。m对n取模,余6。(两者无需比较大小)
若余0,则n为最大公约数。
将18赋给m,6赋给n,取余为0.此时6就是m与n的最大公约数
小改进:m%n出现了两次,将t=m%n放入while的括号中
拓展:最小公倍数=m*n/最大公约数
(12)100-200的素数
我的代码:
我的问题:
素数是只能被1和自身整除的正整数。而该代码只排除了一部分可能的因子,但并不完整。例如,25是一个合数,但是它能通过条件检查,因为它既不是2、3、5、7的倍数。
与老师代码输出的对比:
老师代码
标记法:
优化算法:
(1)若c能被两个因子a和b表示,那a和b中必有一个数小于根号c(高中基本不等式)
因此,就可以缩小for循环判断的范围
使用函数——sqrt(),使用头文件math.h
(2)偶数不可能为素数,故最外层for循环可以从101开始,每次加2,又减少了判断的数
(13)判断闰年最简写法!!
(14)整形有序数组的二分查找
我的代码
问题:
1.sizeof 的错误使用:在 function 函数中,使用了 sizeof(arr) / sizeof(arr[0]) 来计算数组的大小,但是这里的 arr 是一个指针,不是一个数组,因此 sizeof(arr) 将返回指针的大小4bit,而不是数组的大小。最终 sizeof(arr) / sizeof(arr[0])的值为4/4=1
注:数组传参实际传递的不是数组本身,而是数组首元素的地址。故函数的形参a[]本质上是指针
2.二分查找的实现问题:在二分查找中,应该将 x 与数组中的元素进行比较,而不是与 mid(中间索引)进行比较。因为 mid 只是一个索引,不是数组中的元素。
3.循环终止条件:while 循环的终止条件为 left < right,这将导致当找到目标元素时,最后一步不会执行,因为 left 和 right 会相等,但是循环终止条件不满足,因此函数不会正确地返回目标元素的索引。
4.函数缺少返回值:在 function 函数中,如果没有找到目标元素,没有返回任何值。应该考虑在函数的末尾返回一个特殊的值来表示未找到目标元素。
老师代码:
函数中若想使用参数的数组元素个数,要先在外部求好个数再传入函数
(15)编写函数不允许创建临时变量,求字符串长度
我的代码:
问题:
- count 在 function 函数内部是按值传递的,函数内对 count 的修改不会影响到 main 函数中的 count 变量。需通过通过指针来修改 count 的值。
修改之后:
老师代码:(更简洁)
不是’\0’,用1表示该字符已被计数,同时指向下一个字符进行判断
在递归时尽量不要使用++,老师的strlen(str+1)传进去的是str+1后的结果,留下的是str。
eg.strlen(str++)传入的是原来的值,留下+1后的值
strlen(++str)传入的是+1的值,留下+1后的值(有的情况就需要原来的值留下)
(16)用递归计算阶乘
我的代码
未加入对0阶乘的考虑,但也能返回1.原因是当输入为0时,函数并没有返回任何值,因此返回值是不确定的(这是未定义行为),在大多数编译器中可能会返回一个垃圾值。碰巧返回1
老师代码
(17)青蛙跳台阶问题
我的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int ji = 0;
int ou = 0;
int fun(int n)
{
if (n % 2 == 0)
{
int circul = (n / 2) -1;
int step = 0;
int one = 0;
int sum = 1;
int count = 1;
while (circul > 0)
{
int j = circul;
step = circul + (one += 2);
for (int i = step; j > 0; i--, count++, j--)
{
sum = (sum * i) / count;
}
ou = ou + sum;
sum = 1;
count = 1;
circul--;
}
return 2 + ou;
}
if (n % 2 == 1)
{
int circul = (n / 2) ;
int step = 0;
int one = 1;
int sum = 1;
int count = 1;
while (circul> 0)
{
int j = circul;
step = circul + one;
for (int i = step; j > 0; i--, count++, j--)
{
sum = (sum * i) / count;
}
ji = ji + sum;
one = one + 2;
sum = 1;
count = 1;
circul--;
}
return 1 + ji;
}
}
int main()
{
int n = 0;
printf("请输入台阶数:>");
scanf("%d", &n);
printf("共有%d种方法爬台阶", fun(n));
return 0;
}
递归代码:
CSDN代码
没有仔细研究跳台阶的规律,可惜了
(18)函数测试题错题
最后一次判断但不一定进去
(19)打印99乘法表
我的代码:
只能从上到下打印乘法表
老师代码
i控制行,j控制列。注意要把第二层循环的条件换为j<i,这样才能控制列的位置
(20)字符串逆序
!题目不允许用库函数,就自己定义一个。。
非递归:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int my_strlen(char* arr)
{
int count = 0;
while ((*arr) != '\0')
{
count++;
arr++;
}
return count;
}
void fun(char* arr)
{
int left = 0;
int right = my_strlen(arr) - 1;
while (left < right)
{
char middle = arr[left];
arr[left] = arr[right];
arr[right] = middle;
left++;
right--;
}
}
int main()
{
char a[10] = { 0 };
scanf("%s", a);
fun(a);
printf("%s", a);
}
递归方法:
(1)思路
a b c d e f \0
整个字符串的逆序看作a,f字符的交换加上bcde的逆序,bcde的逆序看作b、e的交换加上cd的逆序
(2)代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int my_strlen(char* arr)
{
int count = 0;
while ((*arr) != '\0')
{
count++;
arr++;
}
return count;
}
void fun(char* arr)
{
char tmp = *arr;
int len = my_strlen(arr);
*arr = *(arr + len - 1);
*(arr + len - 1)='\0';
if (my_strlen(arr + 1) >= 2)
{
fun(arr + 1);
}
*(arr + len - 1) = tmp;
}
int main()
{
char a[10] = { 0 };
scanf("%s", a);
fun(a);
printf("%s", a);
}
(21)递归求一个数每位之和
老师代码
(22)求n的k次方
老师代码:
注意负数情况要取负,返回类型要从int改成double。k==0时要返回1.0
我的代码:
代码太长,且未考虑负数情况
(23)冒泡排序
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void maopao(int arr[],int sz)
{
for(int i = 0;i<sz-1;i++)
{
int j = 0;
for (j = 0; j < sz - 1 - i; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j+1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[10] = { 5,3,6,2,4,9,7,1,8,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
maopao(arr,sz);
for (int i = 0; i < 10; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
(23)小测验
无符号整型与有符号整型比较时,先把有符号整形转化为无符号整型,再比较
-1在内存中表示为32个1的补码,当成无符号数处理时,32个1的补码也是原码,此时在内存中为很大的数
结果为A
(24)求水仙花数
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <math.h>
int main()
{
for (int i = 0; i < 10000; i++)
{
int count = 1;
int tmp = i;
while (tmp / 10)
{
count++;
tmp = tmp / 10;
}
tmp = i;
int sum = 0;
while (tmp)
{
sum = sum + pow(tmp % 10, count);
tmp = tmp / 10;
}
if (i == sum)
{
printf("%d ", i);
}
}
return 0;
}
(25)指针与数组试题解析
*与&可看作是互逆运算,最后计算的还是数组a的大小
&a+1为取出整个数组之后,下一块空间的起始地址
要注意指针/地址的大小只有4/8,而地址的内容要根据元素类型分析
strlen等价于 int my_strlen(const char* str )
1.strlen找不到‘/0’的位置,故结果为随机值
3.将‘a’的ASCII码值当作地址传入函数中,报错
5.&arr的类型是char (*) [6],传入函数后变为char *,被视为数组首元素的地址。结果同一相同
6.&arr+1跳过该数组数字符,结果为随机值-6
7.结果为随机值-1
注意
1.sizeof()内部表达式不计算,只关注概念
2.表达式的两种属性。对本题,不关注a[3]的内容是什么,只要能推测出属性和个数就可以了
(26)指针笔试题
*1
1.p是结构体指针,指针类型决定了指针的运算。结构体大小为20字节,故指针加一就是0x100000+20,即0x100014
2.unsigned long把指针转化成整型,整型加一就是1.
3.unsigned int*把指针转化成整型指针,加一即跳过一个整型变量。而无符号整型四个字节,可计算大小
*2
a转化成int后+1加的是一个字节!又因为一个字节对应一个地址,ptr2由原来的01指向00.
当*ptr2时,因为ptr2为整型指针,访问范围为4个字节,ptr2从00开始向后访问4个字节。ptr1也同理
*3
注意a相当于int (*)[5],而p能操作的字节数相对少1
指针相减,得到指针间的元素个数。又因为p的地址小于a的地址,故用%d表示为-4.
用%p表示,认为应该是一个地址,不区分正负号形式。储存在内存中-4的补码会被直接打印出来
2.-4
*4
&aa为整个数组地址,+1后指向下一个数组
aa为第一行地址,+1后指向第二行,等价于第二行首元素的地址aa[1],解引用得到6的地址,其地址类型本身就是int*
*5
指针变量不能连续定义,只能写成int * p1,*p2的形式
而int*重定义之后就是完整独立的类型(类似于int,double的形式),用该类型定义的c和d类型相同
(27)判断机器的字节序
通过访问最低位字节的内容判断大端和小端
(28)数据的存储错题
*1
-1在三个变量中存储内容相同,但理解方式不同
a,b都将最高位视为符号位,c都是数值位
以%d打印时要进行整型提升,a和b都补1,而c补0
大部分编译器都是signed char
*2
补充:
1字节8bit,并且char为有符号类型,整型提升时前面加1
10000000不能再被-1,故直接被当作-128
*3
整个序列的值按照圆圈进行循环
按照圆圈,‘\0’前有255个元素(ASCII中0与‘\0’等价)
unsigned char范围:0~255
(29)浮点数的存储
(30)求出未知二维数组的行数和列数
int matrix[][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }; // 这是一个示例二维数组
// 计算行数和列数
int rows = sizeof(matrix) / sizeof(matrix[0]);
int cols = sizeof(matrix[0]) / sizeof(matrix[0][0]);
(31)杨氏矩阵
我的代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void lookup(int(*p)[5], int a, int b, int c)
{
int i = 0;
int j = 0;
int flag = 0;
for (i = 0; i < a; i++) {
int j = 0;
for (j = b-1; j >= 0; j--) {
if (p[i][j] == c) {
flag = 1;
goto hh;
}
else if (p[i][j] < c)
continue;
}
}
hh:
if (flag == 1)
printf("yes");
else
printf("no");
}
int main()
{
int juzheng[5][5] = { {1,6,11,16,21},{2,7,12,17,22},{3,8,13,18,23},{4,9,14,19,24},{5,10,15,20,25} };
int shu = 25;
// 计算行数和列数
int rows = sizeof(juzheng) / sizeof(juzheng[0]);
int cols = sizeof(juzheng[0]) / sizeof(juzheng[0][0]);
int(*p)[5] = juzheng;
lookup(p, rows, cols, shu);
}
老师代码:
找到元素时还能打印所在的行与列
(32)字符串左旋
方法一:
方法二:
先左旋左侧需要旋转的字符,再左旋右侧需要旋转的字符
再对整体字符串继续左旋
方法三:
字符串后面追加上相同的字符串,会包含上所有逆序的可能性
追加字符串函数:strcat()
注意:该函数不能实现自己给自己追加
进阶:strncat()函数——参数n可选择第二个参数中追加的个数
strstr()函数—判断一个字符串是否为另一个字符串的子串
若判断成功,则会返回该子串在字符串中的地址。找不到则返回空指针
代码实现:
字符串函数及其模拟实现
strcpy函数使用
拷贝过程中将字符串的‘/0’也拷贝过去了,也是停止拷贝的中止条件
若目标数组太小,拷贝字符串过大,会出错
若目标空间是一个常量字符串,不可修改,则会报错
strcat函数使用
若目标字符串除了结尾处的‘\0’还有‘\0’,字符串会从此处开始覆盖
模拟实现:
先解引用进行赋值,再将两个指针的位置++。当指针解引用的内容为‘\0’时,表达式的结果为假,while不再循环
疑问1:strcat()函数,其函数声明是 char * strcat ( char * destination, const char * source),为什么一个参数有const而另一个没有?
回答:这里之所以将source参数声明为const char *是为了表明在strcat()函数中,source参数是只读的,不会被修改。strcat()函数的作用是将source字符串的内容追加到destination字符串的末尾,但它不会修改source字符串本身,因此将source声明为const是一种良好的编程实践,以确保不会意外地修改source字符串的内容。
destination参数没有被声明为const,因为strcat()函数会修改destination字符串的内容,将source字符串追加到它的末尾。所以,destination参数是一个可修改的字符数组。
疑问2: 为什么用strcat()函数没法追加自己?
回答:
用strcat()函数自己给自己追加时,一开始就把字符串最后面的’\0’覆盖掉了,所有后面再追加时会因为一直找不到’\0’而陷入死循环
strcmp函数使用
这种方法比较的是两个字符串的首地址,是由编译器分配的,无法比较
模拟实现
改进:
库函数实现:
while条件循环意思:
1.先将指针类型强制转化为unsigned char *,解引用后相减。只有在相减为0的情况下并且指针内容不为’\0’才会进入循环
2.*(unsigned char *)src 和 *(unsigned char *)dst:这两个表达式将 src 和 dst 指针所指向的字符转换为无符号字符,以确保在比较时没有符号扩展的影响。
strncpy函数使用
库函数实现
strncat函数使用
库函数实现
1.front–:如果front为’/0’,会跳过循环,front-- 负责把指针指回’/0’
2.if语句:如果back指向‘/0’时,表达式0==0成立,将返回目标字符串的首地址
3.*front:如果count的个数小于追加字符串的个数,在count减到0后,自动在末尾追加一个’/0’以满足字符串要求
strstr函数使用
模拟实现:
cp指针用于记录开始比较字符的位置
|*s1 && *s2用于避免出现数组越界的情况
return const str1–>函数声明中str1是由const修饰的,而返回值为char*,return要要强制转化类型为char*,其他地方也同理
strtok函数使用
应用场景:切割字符串
课件第三点:
1.将找到的分割符改为’/0’并返回’/0’前第一个字符串的地址
2.创建一个数组存放字符串的备份,用于被切割
课件第四点:
1.把标记的点改成’/0’,返回z的地址,并且记录’/0’的位置
第五点:
1.做完第四步后,若下一次调用strtok函数第一个传入的是NULL指针,会从刚刚标记的’/0’开始,向后找下一个标记。再重复第四步的操作。
(函数内部必然使用了static声明,具备记忆功能)
strtok函数只有在第一次调用时传首字符地址,以后就传入空指针
使用方式:
for循环遇到ret等于空指针时自动停止
用fopen函数打开文件,若打开失败,会返回NULL并会把错误码放入errno中
在目标文件夹下新建一个空的txst.txt文件,运行程序输出为空
相似函数:perror
头文件<stdio.h>
传入的字符串string为自定义的信息
字符分类函数
isdigit函数
讲义上其他的字符分类函数规律都是一样的
内存函数
字符函数的局限性:
想使用strcpy()函数将arr1中的前5个数字复制到arr2中,会有两个问题
1.strcpy()函数用于拷贝字符串,但arr1函数的元素为整型,传参会出问题
2.strcpy()函数在传参时遇到’/0’就停止了
1在内存中为01 00 00 00.若数组为小端存储,strcpy()逐字节拷贝时,拷贝完01后,第二个字节00就是’/0’,拷贝停止
急需一个函数针对内存块进行处理
memcpy函数
void*–>无具体类型的指针,任何数据的地址都可放进来
模拟实现:
易错点:
void *指针不能直接解引用,也不能直接++或–
想法:直接强制类型转化
但如果num的值为3则不行,不够灵活
转换成(char *)指针最适合
要注意不能使用++,强制类型转换为一种临时的状态。再执行++时char已经恢复成void*状态。
¥*((char*)des)++ = ¥*((char*)sou)++;这种在某些编译器上可通过,但不保险
(char *)dest+±->错
(char *)dest+1–>对
讨论:
先把arr1 数组的12345拷贝到arr1+2上面,但出错了
原因:源头和目的地在空间上重叠了
先把1赋给3,再把2赋给4,那么3原本的位置被1替代,接下来5就会被赋1
模拟实现:
从前向后拷贝:从前往后把3,4,5,6,7拷贝到1,2,3,4,5中
从后向前拷贝:从后向前把1,2,3,4,5拷贝到3,4,5,6,7中
判断标准:从低地址往高地址拷贝时,先拷贝高地址,再拷贝低地址
从高地址往低地址拷贝时,先拷贝低地址,再拷贝高地址
从后向前的情况:
若num为20,进入while后变为19,来到某元素的最后一个字节。此时解引用相当于把源端的最后一个字节放入目标端的最后一个字节了。eg.把7的最后一个字节放入9的最后一个字节
memset函数
将ptr指向的num个内存块设定为value值
具体使用以及内存查看
自定义类型详解
结构体
结构体变量范围
数据结构
结构体的对齐规则
(蓝色方块为浪费的空间)
vs对齐数为8,int i的字节数为4,对齐数取最小值为4。i要对齐到4的整数倍处,故i从偏移量为4的地址处开始储存
vs对齐数为8,char c2的字节数为1,对齐数取最小值为1。c2要对齐到1的整数倍处.直接从i的后面开始对齐
此时结构体在内存中占了9个字节。根据规则,结构体的最大对齐数就是4,总大小应该为4X3=12(本身以及占了9字节,只能更大)
嵌套结构体的情况:
s4中最大对齐数为8,s5的最大对齐数也为8,最终s5的大小就是32
(16与8对比要取小的8)
为什么存在内存对齐?——性能原因
上面为内存对齐的情况,下面为不对齐的情况
若计算机一次读四个字节,对齐的情况拿c和i各用了一次—>空间换时间
而不对齐时,为了读取i需要用两次
offsetof宏
头文件<stdef.h>
位段空间分配
进入位段,看到int声明,先开辟4个字节空间。
后继的成员用完4个字节后还剩15个比特位
再根据声明开辟4个字节的空间,所以一共8个字节
但后面开辟的四个字节是先存放上面三个元素剩下的15个bit,再把剩下的17bit给_d,还是先给_d用30bit,这件事无法确定。在不同平台上实现方法不同
vs中,位段在内存中从高地址向地址储存。低地址数据放高位,高地址数据放低位。当当前字节存不下某个成员时,重新开辟一块空间从高向低储存
(注意与大小端存储不同。大小端存储是字节序,但一个字节内的顺序与大小端无关)
枚举
定义了枚举的变量c,它的取值就是枚举声明中三种颜色的一种
与结构体对比
枚举的优点
曾经写法:
使用枚举后,代码可读性也提高了
枚举中定义的符号属于枚举类型。而define定义的量没有类型,是全局的。
所以给枚举变量赋int类型的值会报错
联合体
共用空间
i与c共用第一块空间
应用1:
回顾——大端存储与小端存储
新的判断方法:
c1正好占了四个字节的第一个字节。把数值存入联合体后,把c的值拿出来看就行
应用2——学校人员
设计一个类型为学校人员。一个人要不是学生,要么是老师,职务只会选择一个。故可以把职务设置为联合体
通讯录
代码逻辑:
第一步:编辑头文件
1.为避免后期到处修改姓名数值的长度,先提前用宏定义定义好,其他地方同理
第二步:开始写主函数
1.为实现增删查改的各种功能,先提供一个菜单(函数)
2.类似于猜字游戏,先写一个do while结构
3.写一个枚举结构给各个功能选项赋值
4.完善整体框架
增加存储信息的区域(要引对应的头文件)
1.用typedef重定义PeoInfo类型减小代码输入量
思考:
实现增加信息人的功能既要结构体,有需要知道当前存储人的个数,能否把则两个功能合并?将结构体数组与sz合并!
5.初始化函数
注意函数的声明放在头文件中,实现放在源文件中
记得源文件中包含头文件
pc->data相当于数组的数组名,sizeof计算的是整个数组的大小
(不推荐用contact con ={0},若成员比较复杂,会出问题)
6.增加联系人函数
声明添加与函数实现同上
(1)pc->data[pc->sz]将内容放到pc的数组里面,下标为sz的位置上
(2)但人的信息包含名字,年龄等信息,应该一个个往其中录
(3)名字为数组,不需要取地址
7.打印联系人信息函数
仅仅是打印,参数不修改,指针加上const更安全
%20s\t中的20表示宽度,\t让打印数据的中间有空白让数据形式更好看一点
输出效果,呈现右对齐
添加-号左对齐
注意for循环中打印年龄用%d
8.删除联系人函数
要删除一个人必然要涉及到查找功能,此时发现工程中修改,查找等功能也需要查找——>将该功能封装成函数
查找功能实现:
注意用sataic修饰,供多个函数使用
删除的逻辑:
将本单位的数据移除,将后继单位的数据往前移,但要注意在覆盖的过程中最后一个没必要被覆盖的元素会被覆盖。故在条件判断中pc - sz还需要减1
(i+1==10时访问数组的第11个为空的元素,造成第10个元素被覆盖)
若删除的是最后一个元素,不能进入循环,但是sz同时也减1了。只能访问前9个元素了,相当于也删除了
动态内存管理
临时的变量都放在栈区内,开辟空间有限
realloc与calloc结合使用
realloc函数的返回值需要接收,但不能拿p接收
当无法实现增容时(前后都找不到空间),realloc函数会返回空指针
p中原本存放对应数据的地址,若返回空指针,对应数据的地址也找不到了
拿临时变量存储!!
正确代码:
realloc函数直接开辟空间
通讯录动态增长版本
动态增长版本
1.通讯录初始化后,能放三个人的版本
2.当内存不够时,增加存放两个信息的空间
思路:当开辟好最初三个人的空间后,返回指针,由该指针维护后继开辟的空间。
再新增一个变量表示当前通讯录的最大容量
根据要求改造初始化函数
把默认大小与增量用define定义
动态开辟影响的函数:
1.增加联系人
2.退出通讯录(空间是动态开辟的,退出时需要动态回收)
增加联系人函数
销毁函数
经典笔试题
p为形参变量,出函数范围就销毁了,str仍为空指针。创建空间的地址也找不到了,导致100个内存空间泄露
正确修改:
改法2:
试题2:(返回栈空间地址的问题)
p是一个临时数组,生命周期只在函数内存。当函数即将结束时,str接收到了临时空间的起始地址。
再调用printf函数时临时空间已被系统收回,就会造成非法访问空间
(而上一题是在堆上开辟的空间,不会销毁)
柔性数组的使用
一种类似实现:
不足:1.使用了两次malloc,两次free,更容易出错
2.当申请空间时,空间之间会留下空的内存区域,称为内存碎片。malloc次数越多,内存碎片越多,效率方面也不如柔性数组。
软件设计:内存池。直接为当前程序申请大的内存。当需要使用其中空间时,直接拿走用不需要再申请。
根据计算机的空间局部性原理,在使用一块内存时,接下来80%可能使用紧邻的内存。当采用第一种设计时,n和arr在内存中连续,访问效率更高。采用第二种设计时,两块空间不连续,影响访存效率
c/c++的内存管理
文件
1.不关闭文件会有什么后果?
一个进程或者程序打开文件的数量有限,文件若只打开不释放,后面就会打不开文件
2.输入绝对路径打不开文件的原因——路径出错
\202会被解析成转义字符
正确写法:
流的概念
stdout为流
早期程序要适配各种不同的各种io设备,要求程序员懂各种不同硬件的读写方式。
故在中间抽象一个称为“流”的层,程序向流中写数据,流再写入各种输出设备中。
打开文件时,向文件中写数据就可理解成一个流。
stderr为标准输出错误流
用标准输出流打印数据
fgetc函数
fgetc如果读取正常,返回字符的ASCII码值。出错或读取不到时时返回EOF值
(读取字符都是读取的ASCII数组,所以返回值为整型,要用整型接收。EOF也为整型值,值为-1)
从文件中读取数据
从键盘中读取数据
fputs函数
fputs要添加换行才能体现在文件中
fgets函数
n为读取最多的个数(实际读入n-1个数据–要留一个\0的位置)
fprintf函数
传入结构体数据要用格式化输出函数写
fprintf与printf的对比
实际操作
(5.5f,编译器默认将小数视为double型数据,如果不加f会提示精度丢失之类的)
fscanf函数
fwrite函数
buffer指向被写的数据
size-元素的大小(单位是字节)
count-最多有多少个元素要被写入
使用
(字符串以二进制写入与用文本写入是相同的,其他字符在不同编译器下不同)
fread函数
改进通讯录
修改退出的函数
修改初始化函数增加读文件功能
fread函数返回值为实际读到元素的个数,读不到元素时返回0
读取的元素放到下面的通讯录中
此时发现通讯录初始化时默认为3个,但读取的数据可能不止三个。
应该先检测容量,若容量不够再扩容放入通讯录中——考虑将之前增容的代码写成函数
增容函数
读入文件函数
加上pc->sz++
sscanf/scanf,fscanf/fprintf,scanf/printf对比
sscanf从字符串buffer中读出一个格式化数据
sprintf:将格式化的数据写入字符串buffer中
sprint应用:
sscanf应用:
fseek函数
起始位置有3个,分别是指针当前位置,文件开始,文件末尾
偏移量可为负值,指向前一个元素
实际应用
文件读取结束的判定
将某个文件中的内容拷贝到另一个文件中:
第二次打开失败时,而前一次打开成功,首先应该把打开的文件关闭
程序的编译
预处理符号
_FILE_–提供代码所在源文件的路径
_LINE_–所在行当前行号
预定义符号作用:当c语言工程特别复杂时,调试时不方便。要在代码运行中记录日志信息,通过日志信息分析代码哪里出了问题
写日志本质上是写文件
#与##
想实现输入参数就可以打印出不同字符串的printf
#X会变成参数对应的字符串
“#X”等价于““a””
想实现可打印不同格式类型的printf函数
替换
展开后
最后字符串"the value of"、“F” 、“is”、“%f”、“ \n”会连接在一起
##的作用
宏的副作用
++a把a的值也改了
(a++)与(b++)比较完后,后置++才产生效果,a变为6,b变为9
此时(b++)的值为9,被赋给m后b变为10.
命令行的定义
出现错误
添加命令行参数后
<>与“”的区别
3.Linux C错题/改进题/例题汇总
(1)打印直方图
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define N 10
#define BAND 10
int a[N], histogram[BAND];
void generateRandomNumbers(int bound) {
srand(time(NULL));//生成随机数并存到数组中
for (int i = 0; i < N; i++) {
a[i] = rand() % bound;
}
}
void printHistogram() {
for (int i = 0; i < BAND; i++) {
printf("%d\t", i);//生成直方图
}
printf("\n");
int max_histogram = 0;
for (int i = 0; i < BAND; i++) {
printf("%d\t", histogram[i]);
max_histogram = (histogram[i] > max_histogram) ? histogram[i] : max_histogram;
}
printf("\n\n");
while (max_histogram > 0) {
for (int i = 0; i < BAND; i++) {
printf("%c\t", (histogram[i] >= max_histogram) ? '*' : ' ');
histogram[i]--;
}
printf("\n");
max_histogram--;
}
}
int main() {
generateRandomNumbers(BAND);
for (int i = 0; i < BAND; i++) {
histogram[i] = 0;
}
for (int i = 0; i < N; i++) {
histogram[a[i]]++;
}
printHistogram();
return 0;
}
(2)递归函数求最大公约数
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
int convertToPositive(int num) {
if (num < 0) {
return -num; // 取相反数以获得正数
}
else {
return num; // 正数保持不变
}
}
int digui(int a,int b)
{
if (a % b == 0)
{
return b;
}
else
{
return digui(b,a%b);
}
}
int main()
{
int a = 0;
int b = 0;
printf("请输入数字:>");
scanf("%d %d", &a, &b);
a = convertToPositive(a); // 转换为正数
b = convertToPositive(b); // 转换为正数
printf("%d", digui(a, b));
return 0;
}
不会的地方:负数转成正数。。
(3)打印数组全排列
定义一个数组,编程打印它的全排列。比如定义:
#define N 3
int a[N] = { 1, 2, 3 };
则运行结果是:
$ ./a.out
1 2 3
1 3 2
2 1 3
2 3 1
3 2 1
3 1 2
1 2 3
完成了上述要求之后再考虑第二个问题:如果再定义一个常量M表示从N个数中取几个数做排列(N == M时表示全排列),原来的程序应该怎么改?
最后再考虑第三个问题:如果要求从N个数中取M个数做组合而不是做排列,就不能用原来的递归过程了,想想组合的递归过程应该怎么描述,编程实现它。
第一问
#include <stdio.h>
#define N 3
void swap(int *x, int *y) {//定义了一个 swap 函数,用于交换两个整数的值
int temp = *x;
*x = *y;
*y = temp;
}
void printArray(int a[], int n) {//printArray 函数用于打印数组中的元素
for (int i = 0; i < n; i++) {
printf("%d ", a[i]);
}
printf("\n");
}
void permuteIterative(int a[], int n) {
int stack[N]; // 用于存放每个位置的指针
int index[N]; // 用于存放每个位置的索引
for (int i = 0; i < n; i++) {//我们初始化数组 stack 为全 0,表示每个位置的状态都是初始状态。index 数组初始化为 0, 1, 2, …,表示每个位置的索引。
stack[i] = 0;
index[i] = i;
}
printArray(a, n);
int i = 0;
while (i < n) {
if (stack[i] < i) {
if (i % 2 == 0) {
swap(&a[0], &a[i]);
} else {
swap(&a[stack[i]], &a[i]);
}
printArray(a, n);
stack[i]++;
i = 0;
} else {
stack[i] = 0;
i++;
}
}
}
int main() {
int a[N] = {1, 2, 3};
permuteIterative(a, N);
return 0;
}
这个代码的思路是使用迭代方式生成数组的全排列。它基于一种类似于递归的迭代算法,通过不断交换数组元素来生成排列,同时使用两个数组 stack 和 index 来记录状态和控制迭代。
代码思路:
假设我们有一个数组 a = [1, 2, 3],我们的目标是生成它的全排列。
主要思路是通过迭代,不断生成下一个排列。
初始状态:我们从数组的第一个位置开始,即 i = 0。此时整个数组就是一个排列,我们可以直接打印出来。
第一轮迭代:我们将第 i 个位置的数与自身交换,然后对后面的子数组进行全排列。对于 i = 0,交换后得到 [1, 2, 3],然后对 [2, 3] 进行全排列。
第二轮迭代:将第 i 个位置的数与后面的数逐个交换,得到 [2, 1, 3] 和 [3, 2, 1],分别对 [1, 3] 和 [1, 2] 进行全排列。
第三轮迭代:交换回初始状态,得到 [1, 2, 3],然后对 [2, 3] 进行全排列。
以此类推,继续迭代,直到所有的可能排列都生成并打印出来。
迭代的核心思想是通过不断交换数组中的元素,生成所有的排列组合。在每次迭代中,我们将当前位置的数与后面的数逐个交换,然后继续对剩余的子数组进行全排列。这样,我们逐步生成了所有的排列情况。
代码的原理:
首先,我们定义了一个 swap 函数,用于交换两个整数的值。这将在后面的迭代过程中使用。
printArray 函数用于打印数组中的元素。
permuteIterative 函数是迭代生成全排列的核心部分。它接受两个参数:数组 a 和数组的长度 n。函数内部有两个数组 stack 和 index,分别用于存储每个位置的状态和索引。
我们初始化数组 stack 为全 0,表示每个位置的状态都是初始状态。index 数组初始化为 0, 1, 2, …,表示每个位置的索引。
在 permuteIterative 函数中,我们首先调用 printArray(a, n); 打印初始排列。
接下来,使用一个 while 循环来进行迭代。在循环内部,我们检查 stack[i] 是否小于 i。如果是,说明可以继续交换位置,生成下一个排列。
我们根据奇偶性来决定交换哪两个元素。如果 i 是偶数,我们交换首位元素,否则我们交换 stack[i] 处的元素和当前位置 i 处的元素。
交换完成后,调用 printArray(a, n); 打印当前排列。
然后,递增 stack[i],表示在当前位置已经完成了一次交换。将 i 重置为 0,以便从头开始交换。
如果 stack[i] 大于等于 i,表示当前位置已经完成了所有可能的交换,所以将 stack[i] 重置为 0,同时递增 i,进入下一个位置的交换。
循环一直进行,直到 i 超过了数组长度,此时所有排列已经生成完毕,算法结束。
(4)石头剪刀布
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
char gesture[3][10] = { "scissor", "stone", "cloth" };
int man, computer, result, ret;
srand(time(NULL));
while (1) {
computer = rand() % 3;
printf("\nInput your gesture (0-scissor 1-stone 2-cloth):\n");
ret = scanf("%d", &man);
if (ret != 1 || man < 0 || man > 2) {
printf("Invalid input! Please input 0, 1 or 2.\n");
continue;
}
printf("Your gesture: %s\tComputer's gesture: %s\n",
gesture[man], gesture[computer]);
result = (man - computer + 4) % 3 - 1;
if (result > 0)
printf("You win!\n");
else if (result == 0)
printf("Draw!\n");
else
printf("You lose!\n");
}
return 0;
}
(man - computer + 4) % 3 - 1这个神奇的表达式是如何比较出0、1、2这三个数字在“剪刀石头布”意义上的大小的?
-
(man - computer + 4)
:首先计算玩家手势和计算机手势的差值,加上 4。这里加上 4 的目的是为了确保差值为正数,避免负数情况。 -
% 3
:然后将上面的差值对 3 取余数。这样得到的结果就是一个范围在 0 到 2 之间的数,对应剪刀、石头和布。 -
- 1
:最后,减去 1。这一步的目的是将结果转换为 -1、0 和 1,分别代表玩家输、平局和赢的情况。
综上所述,这个表达式的结果可以正确地比较出玩家和计算机手势在“剪刀石头布”游戏中的输赢关系。通过这个表达式,可以方便地判断游戏结果,使代码更加简洁而又高效。
(5)链表
*链表的尾插法:
void CreatHead(Node *head) {
Node *newNode;
int data;
scanf("%d", &data);
while (data != -1) {
newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = head->next;
head->next = newNode;
scanf("%d", &data);
}
}
假设你开始时有一个空链表,只有一个头结点:
+---+
| H |
+---+
其中 H 是头结点。
调用 CreatHead(Node *head) 函数,传入指向头结点的指针 head。
输入数据 data = 5。
进入循环,由于 data != -1,执行循环体。
newNode = (Node*)malloc(sizeof(Node));:分配内存创建一个新节点 newNode。
newNode->data = data;:将新节点的数据域赋值为输入的数据 5。
newNode->next = head->next;:将新节点的 next 指针指向链表原来的第一个节点(如果有的话)。
head->next = newNode;:将头结点的 next 指针指向新节点,将新节点插入到链表的头部。
输入数据 data = 3。
重复步骤 4-7,创建并插入节点 3 到链表头部。
输入数据 data = 1。
重复步骤 4-7,创建并插入节点 1 到链表头部。
输入数据 data = -1,循环结束。
最终,链表的结构将如下所示
+---+ +---+ +---+ +---+
| H | -> | 1 | -> | 3 | -> | 5 |
+---+ +---+ +---+ +---+
通过这个过程,CreatHead() 函数在链表头部逐个插入了新节点,形成了一个带头结点的单链表。每次插入都会将新节点插入到头部,因此链表中的节点顺序是与输入相反的。
在指定的地方插入结点:
void Insert(Node *head, int x, int data) {
Node *pre = FindNode(head, x - 1);
if (pre == NULL) {
printf("请输入正确的插入点");
}
Node *pNew = (Node *)malloc(sizeof(Node));
pNew->data = data;
pNew->next = pre->next;
pre->next = pNew;
}
假设你开始时有一个链表,只有一个头结点:
+---+
| H |
+---+
其中 H 是头结点。
调用 Insert(Node *head, int x, int data) 函数,传入指向头结点的指针 head,要插入位置的索引 x,以及新节点的数据 data。
调用 FindNode(head, x - 1) 函数,找到索引为 x - 1 的节点,即要插入位置的前一个节点 pre。
检查是否找到了 pre 节点,如果没有找到(即 pre == NULL),则打印错误信息并退出。
Node *pNew = (Node *)malloc(sizeof(Node));:分配内存创建一个新节点 pNew。
pNew->data = data;:将新节点的数据域赋值为输入的数据 data。
pNew->next = pre->next;:将新节点的 next 指针指向 pre 节点的后一个节点。
pre->next = pNew;:将 pre 节点的 next 指针指向新节点 pNew,将新节点插入到链表中。
最终,链表的结构将如下所示(假设在索引 2 处插入数据为 4 的新节点):
+---+ +---+ +---+ +---+
| H | -> | | -> | 4 | -> | |
+---+ +---+ +---+ +---+
^ ^
|
pre
|
pnew
删除指定的结点:
假设你有以下链表,要删除索引为 2 处的节点:
+---+ +---+ +---+ +---+
| H | -> | | -> | 2 | -> | |
+---+ +---+ +---+ +---+
^
|
pre
其中 H 是头结点。
调用 Delete(Node *head, int x) 函数,传入指向头结点的指针 head 和要删除位置的索引 x。
调用 FindNode(head, x) 函数,找到索引为 x 的节点,即要删除位置的前一个节点 pre。
Node *q = pre->next;:将指针 q 指向要删除位置的节点,即节点 2。
pre->next = pre->next->next;:将 pre 节点的 next 指针指向要删除位置的节点的下一个节点,即节点 2 的 next 指针指向的节点。
free(q);:释放节点 2 占用的内存。
最终,链表的结构将如下所示:
+---+ +---+ +---+
| H | -> | | | |
+---+ +---+ +---+
^
|
pre
通过这个过程,Delete() 函数删除了指定位置的节点,保持了链表的连续性。节点 2 被从链表中删除,并且相应的内存也被释放。