0%

嵌入式与C(侧重调试)

我们大部分组员已经学过C语言了, 即便没学过, C语言也不难上手. 我不打算笼统地讲语法, 即便是单片机特有的C语言语法也不算难, 怎么写中断函数, 怎么写延时函数你们自己也能很容易搞懂的👍
因此这次我打算主要提几个比较重要的理念和一些调试经验.

学习C语言我推荐
这个教程.
如果你担心只是少量操作容易忘记学过的知识,网上有很多练习题的,一搜一箩筐.

理论

几个理念

起顺眼的名字

变量和函数的命名实际上也是个很重要的问题. 试想在团队中你出于个人喜好命名了别人看不懂的标识符,
当别人要审阅你的代码时他不能通过这些名字一眼看出你表达的意思, 极大的降低了效率, 而且假如你的命名习惯和他的完全相反, 他的体感会很差. 因此最好的办法是使用统一而直观的命名法.

驼峰命名法

下划线命名法

匈牙利命名法

面向对象

善用宏定义

一口吃不成大胖子

当要做一个大工程的时候你首先应该想到去GitHub找找有没有有帮助的代码 (Ctrl C Ctrl V), 在网上搜一搜有没有人做过相关工作, 也许应用不一样, 但很与可能他的代码思路对你很有帮助. 林子大了, 你想做的总有人做过或者为你做了铺垫.

另一方面, 你应该总是从一个能正确运行的小规模程序开始,每做一步小的改动就立刻进行调试,这样的好处是总有一个正确的程序做参考:如果正确就继续编程,如果不正确,那么一定是刚才的小改动出了问题。例如,Linux操作系统包含了成千上万行代码,但它也不是一开始就规划好了内存管理、设备管理、文件系统、网络等等大的模块,一开始它仅仅是Linus Torvalds用来琢磨Intel 80386芯片而写的小程序。据Larry Greenfield 说,“Linus的早期工程之一是编写一个交替打印AAAA和BBBB的程序,这玩意儿后来进化成了Linux。”(引自The Linux User’s Guide Beta1版)

有了注释, 妈妈再也不用担心我读不懂代码

代码片段 (snippets)

野指针

一些调试经验

首先要指出Bug分为三种:

  1. 编译时错误

    编译器只能翻译语法正确的程序,否则将导致编译失败,无法生成可执行文件。对于自然语言来说,一点语法错误不是很严重的问题,因为我们仍然可以读懂句子。而编译器就没那么宽容了,只要有哪怕一个很小的语法错误,编译器就会输出一条错误提示信息然后罢工,你就得不到你想要的结果。虽然大部分情况下编译器给出的错误提示信息就是你出错的代码行,但也有个别时候编译器给出的错误提示信息帮助不大,甚至会误导你。在开始学习编程的前几个星期,你可能会花大量的时间来纠正语法错误。等到有了一些经验之后,还是会犯这样的错误,不过会少得多,而且你能更快地发现错误原因。等到经验更丰富之后你就会觉得,语法错误是最简单最低级的错误,编译器的错误提示也就那么几种,即使错误提示是有误导的也能够立刻找出真正的错误原因是什么。相比下面两种错误,语法错误解决起来要容易得多。

  2. 运行时错误

    编译器检查不出这类错误,仍然可以生成可执行文件,但在运行时会出错而导致程序崩溃。比如

  3. 逻辑错误和语义错误

    如果程序里有逻辑错误,编译和运行都会很顺利,看上去也不产生任何错误信息,但是程序没有干它该干的事情,而是干了别的事情。当然不管怎么样,计算机只会按你写的程序去做,问题在于你写的程序不是你真正想要的,这意味着程序的想法(即语义)是错的。找到逻辑错误在哪需要十分清醒的头脑,要通过观察程序的输出回过头来判断它到底在做什么。

烫烫烫屯屯屯锟斤拷

字符的编码格式也是个很令人头疼的问题,一不小心就会出错. 烫烫烫, 屯屯屯, 锟斤拷正是由此而来的梗.
但实际上前两个和锟斤拷的产生原因完全不一样.
烫烫烫和屯屯屯是因为微软编译器 (MSVC) 的内存保护机制,锟斤拷是编码字符集转换问题。

点击
这里进一步了解字符串

具体解释参见以下两个链接:

别忘了编译!

有的时候你可能会陷入绝望, 发现怎么改自己觉得可能出错的地方程序运行仍然报错, 你可能欣喜的发现了自己之前犯的一个愚蠢的语法错误, 觉得改过来肯定就能完美运行了, 可现实是不管你怎么改程序,
就算把所有代码都删了徐行程序仍然是那个报错
. 这时候你要看看你是否在改动代码后重新编译了程序, 不然你的程序运行起来当然不会有任何改变. 在有的工具中, 比如CodeBlocks中有
build, run, build and run 三个按钮, 建议大家认准build and run.

数据溢出

习题

神秘报错

在C语言程序中有这么一种错误, 再怎么看代码也看不出错, 但编译器就是报错. 请找出下面这个程序的错误.

1
2
3
4
5
6
7
#include <stdio.h>

int main(void)
{
printf("Hello, world.\n");
return 0
}

正如前面提到的, 宏定义是个很好的东西, 在接下来的两个例子中我们用宏定义弄些奇怪的东西
😏

一门新语言

请写一个head.h文件, 满足程序以下条件:

  • I love{}来定义主函数
  • perfect来返回主函数值
  • 主程序中只能引用head.h一个头文件
  • fury语句打印Hello World!
  • OYeah语句实现getchar()

你可以用以下程序测试你发明的新语言是否成功 (head.h是否满足要求)

1
2
3
4
5
6
7
8
9
#include "head.h"

I love
{
fury
OYeah
OYeah
perfect
}

编译器是MinGW
(Minimalist GNU for Windows) 的话标识符不能是中文, 但如果你的编译器是MSVC
的话, 标识符允许是中文😏 也就是说你可以通过define关键字写出以下这样的程序

1
2
3
4
5
6
7
8
9
#define "head.h"

整数 主函数()
{
打印("你好");
获取字符();
获取字符();
返回 零;
}

这个比较骚

这个使用define的命题我也觉得很骚

1
#include <stdio.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Print1()
{
printf("11111\n");
}

void Print2()
{
printf("22222\n");
}

int main()
{
Print1;
Print2;
getchar();
getchar();
return 0;
}

请在箭头位置加几行代码使得以上代码输出为:

1
2
11111
22222

只要能够得到这个输出就可以, 不必怀疑自己的方法

先来后到

这类错误也是很容易忽略的错误

现在C99标准的x86平台整型变量
是32位的, 也就是说取值范围是-2147483648~2147483647. 现在我们写一段代码做一些计算并打印出来, 检测以下这个标准.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <math.h>

int main()
{
int a = 2;
int b = 1;
int c = 0;
while(1)
{
a = a * 4;
b = b * 2;
c = a * a / b / b / 2;
printf("%d\n", c);
if(c<=0)
break; // 当幂非正了明显就数据溢出了, 跳出循环
}
getchar();
getchar();
return 0;
}

这段代码打印了一些2为底的幂, 一直打印到数据溢出为止, 只不过是生成幂的方式骚一点.

现在我们脑补一下输出, 应该是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
8
32
128
512
2048
8192
32768
131072
524288
2097152
8388608
33554432
134217728
536870912
-2147483648

然而实际上

1
2
3
4
5
6
7
8
8
32
128
512
2048
8192
32768
0

嗯? 怕不是其实整形的范围是-32767~32768? 为什么到32768就爆了呢???

轻松一刻

好吧看了几个难的我们解决一个简单的轻松一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
int i;
char c[23] = "AaBbCcDdEeFfGgHhIiJjKk";
for(i = -1;i<23;)
{
i = i + 2;
printf("%c\n", c[i]);
}
getchar();
getchar();
return 0;
}

我们试图用这段代码输出小写字母, 但为什么出错了呢?

可能并不会报错只是会有乱码

注释

MSVC

Microsoft Visual C++. MSVC集成于Visual Studio中, 也就是说如果你用的IDE是VS
并且你没有换编译器的话, 那么编译器一定是MSVC,
如果是别的其他IDE一般来说默认编译器是gcc, 但一般也可以换成以MSVC为编译器

x86

上面说到的x86平台是32位的处理器架构, 而另一个很有名的x64x86-64的缩写, 是基于
x86的64位处理器架构, 向后兼容x86. 因为最先由AMD公开64位指令集因此又称AMD64.

整形变量

点击这里进一步认识整形变量

值得注意的是, 正如上面链接中提到的整形变量的位数是Implementation Defined的,
也就是说位数是由平台决定的. 比如x86平台int为32位, x64平台int为64位, 在C51中int为16位. 这正体现了C语言的特性之一: 优先考虑效率,而可移植性尚在其次. C语言与平台和编译器是分不开的, 正如上面MinGW不支持中文标识符而MSVC支持中文标识符, 正如习题
先来后到中的问题. 因此大家如果要将代码移植到其他平台
一定要确认有没有哪些代码是不可移植或者需要改动的.