圆满完成了人生中的第一个C项目,项目如下:
相当于模拟一个内存管理系统。写项目的过程中不断地调试、也遇到了很多问题,记录几个调过的 bug。
问题代码及输出:
问题分析:
我把 piece_end
定义为一个全局变量。现在的问题是,只是在函数的作用域内改变了 piece_end
的值,但是我实际想实现的是:通过函数在整个程序的作用域内改变 piece_end
变量的值。
上面的代码中,我把 piece_end
作为形参传入、自然只能在函数作用域内改变此变量的值。
解决方案是不把 piece_end
作为形参,只是通过函数改变此变量的值。
改正后的代码及输出:
于是此问题得到了解决。
另一个 bug 是:我老遇到「程序卡住了」的问题。
比如下图中,我输入了新的/将要修改为的字符串 ear
,程序自动计算出其长度为3,然后程序就卡住了:
程序是这样写的:
遇到这种问题的时候,可以这样调试:
最简单的办法是:
在红圈的地方插入一些printf
语句。 看看哪些语句执行了,哪些些没执行。然后在对相应的位置进行单步调试。这样不用关心代码逻辑 直接定位 bug 大致位置。
单步调试一般是看 watch、memory 窗口 看相应的变量是否正常。
可以从上图中看出,主要是在一些 for、while 循环的地方插入 printf
语句,查看程序运行流程是走了哪个分支。
通过这样的调试方法,我定位到了 bug 的位置是在下面的 for 循环中:
我这里比如num=3,b=4。运行了一次不是应该跳出来继续执行修改成功吗,但是就卡住了:
定位了 bug 的位置,继续单步调试:把num
和b
添加到watch窗口观察变量的变化(或者直接 printf
打印里面变量的值、寻找是否有异常值)。
结果发现:piece_end
的值为异常值,所以 piece[piece_end]
访问了不该访问的位置,发生数组越界,造成异常才退出(a[10]数组最多能访问a[
本文主要记录编程过程中遇到的一个小报错,相信也是其他初学者会碰到的问题。
报错是:
C2371 重定义:不同的基类型
我想在 demo 主方法中加一个 a
or b
or c
or d
的单选方法,来进行增删查改不同的操作。可是出现了红框中的一些报错:
这个 get_first()
方法是获取用户输入的首字母的:
单独测试 choice.c 是没问题的。
原因是:
报「未定义」警告的,应该是没把头文件包含进去。
报「重定义」警告的,应该是有多个相同函数名的函数。
根据报错,我给 choice.c
源文件通过预定义处理器包含了各种头文件就解决了此问题:
事情的起因是我在写一个程序的时候,遇到了一件怪事:
就是,我打印 page 数组里的元素,除了第一个字符是正常输出,其他的都是?
。
但是我这个 page 数组是通过 get_strings()
这个函数来赋值的,我在 get_strings()
这个函数里面打印了这个 page
数组,完全正常。这说明赋值成功:
那为什么我在 main 函数里面打出来就是 ?
。
这让我想起了一句歌词:
小问号,你是否有很多的朋友。为什么、别人在那看漫画,我却要学画画,以后长大了别人都抄我写的歌。
经过咨询大佬,我很快得到了问题的原因:
在 main 函数中,声明 page 数组的时候,我一不小心把数组元素类型指定为了 int
,而 page 数组作为用户输入的字符串其实是 char
类型数组。
大佬说:看下内存对应的具体数据是啥?猜测是不可显示字符。
我说:应该不是不可显示字符。因为通过get_strings
方法可以打印出来剩余字符,就是普通的字母而已。
大佬继续进行了解释:%c
输出问号,就表示是不可在屏幕上显示的字符。main
函数中的 page[] 数组是 int 数组,get_strings()
中的是 char 数组。1 int 占 4 bytes,1 char 占 1 byte,位置不一样,导致在对应的 int 中的 page[1] 其实不是你输入的数据。
总之,就是 page 数组的类型不一致导致的这个 bug,当我把 main 函数中的 page 数组的类型改为 char,问题就圆满解决了:
忽略这生涩稚嫩的代码......
找大佬请教了如何通过看内存 debug,大概明白了?记录一下:
先下两个断点:一个在 get_strings()
方法处,一个在 for 循环前。
然后点击本地 Windows 调试器
:
打开 调试
→ 窗口
→ 内存
→ 内存1
;
打开 调试
→ 窗口
→ 监视
→ 监视1
;
现在 VS 出现了两个新窗口,一个内存窗口,一个监视窗口:
在监视窗口添加项 page
,按下回车,就出现
本章要点:
static
&
、*
(一元)什么是数组?
数组由数据类型相同的一系列元素组成。
注:
这里的数据类型是“基本数据类型”。如:float、char、int......
普通变量可以使用的类型,数组元素都可以用。
需要使用数组时,通过声明数组
告诉编译器:
编译器根据这些信息正确地创建数组。
一些数组声明
/* 一些数组声明 */
int main(void)
{
float candy[365]; //内含365个float类型元素的数组
char code[12]; /*内含12个char类型元素的数组*/
int states[50]; /*内含50个int类型元素的数组*/
}
[]
)表示candy、code、states都是数组。访问数组中的元素
要访问数组中的元素,通过使用数组下标数(也称为索引
)表示数组中的各元素。
数组的编号从0开始。
数组被用来储存程序需要的数据。在这种情况下,在程序一开始就初始化数组比较好。
C中初始化数组的语法
int main(void)
{
int powers[8] = {1,2, 4,6,8 , 16,32 ,64}; /* 从ANSI C开始支持这种初始化*/
}
static
可解决此问题。使用const关键字把数组设置为只读
比如:
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};
const
关键字声明和初始化数组。这样把数组设置为只读之后,程序只能从数组中检索值,不能把新值写入数组。
const 使得程序在运行过程中不能修改该数组中的内容。
存储类别警告
数组
C的项目是这样组织的:
1、usehotel.c
- 主流程控制文件,里面包含main()函数
- 需要通过 #include包含头文件 hotel.h
- 使用了hotel.c里面的一些函数,主要放函数调用
2、 hotel.c
- 是一些自定义功能函数的合集
- 主要是放具体函数定义的。
3、hotel.h
- 头文件,主要放define的符号常量和hotel.c中所有的函数原型。
函数的三部曲:原型、定义、调用就这样被组织起来了。
使用的时候:
把 usehotel.c和hotel.c放在一个项目下编译(IDE 前提下),这样就无需跟python一样import函数,即可相互调用。
可以感觉到,使用vs studio 这种IDE时候,每次运行是运行了项目下所有的文件,都没有错误时候才可运行。
usehotel.c和hotel.c都#include "hotel.h"。
这样就把三个文件紧密的结合起来了。
usehotel.c:
hotel.c:
hotel.h:
本章要点
strlen()函数
不是基本数据类型的数组
list不是基本数据类型,这种数组在下一章会讲。
ANSI C风格的函数原型
函数签名
编译器的工作
定义带形式参数的函数
驱动程序
返回值可以是表达式的值
当返回值的类型与函数声明的类型不匹配
在函数中使用多个return语句
为什么需要函数(类型)声明
函数原型的位置
头文件中包含函数声明,但没有具体定义
ANSI C函数原型
函数原型和函数定义的形参名称可以不一致
无参数和未指定参数
函数原型的意义
递归的定义
递归函数停止语句
尾递归的定义
一个尾递归函数
循环和递归的选择问题
编译多源代码文件的程序
头文件的作用
查找地址:&运算符
main()
函数。指针的定义
指针的使用
变量
指向指针
。间接运算符:*
与指针相关的运算符
声明指针
指针是一种单独的类型,不是整数类型
变量:名称、地址和值
变量三属性:
指针:
变量:
使用指针在函数间通信
scanf()函数
传递值
函数签名
本章要点
本章主要介绍用于输入和输出的函数(即I/O函数)。
文件结尾
重定向输入、输出
重定向输入
组合重定向
注释
实现打开一个文件并显示该文件
小结
重定向是一个命令行概念。也可以借助于集成开发环境指定重定向。
注意“重定向输入、输出”的前提:
这里面的C程序需要先经过gcc编译成为可执行文件。
一点输入函数的技巧
丢弃换行符
字节流
菜单浏览
自定义get_first()函数
混用scanf()和getchar()的注意点
本章要点
ctype.h系列的字符函数
else与if配对
标记flag
逻辑运算符
#include <iso646.h>
求值顺序
因为&&
是逻辑运算符,所以&&
连接的条件,先左后右执行:
空白字符的判断
一些包含函数原型的头文件
条件运算符:?
循环辅助:continue和break
while和for循环中continue的区别
细品:
而while中:
break语句
多重选择:switch和break
switch 的用处:
丢弃一行中其他字符
一图看懂switch
switch和if else
break和continue的另一重理解
goto跳出嵌套循环
本章小结
本章要点
控制程序流!!
跳出循环
伪代码
_Bool类型
出口条件循环:do while
char类型数组
pow()函数
使用带返回值的函数
函数接口和函数实现
本章要点
基本运算符
除法运算符:/
typedef
负数求模
递增运算符:++
不要自作聪明
每个表达式都有一个值
语句
总结:表达式和语句
类型转换
带参数的函数
下图是函数原型:
自动类型转换