大家好,非常感谢大家来参加本次的C语言讲座。
本次的讲座主要分为两个部分,第一部分是C语言基础知识,第二部分是实验二的解题思路。
(最后要给出相应的参考资料)
C语言基础
根据我前一阵子对同学们的交流,我发现很多人其实没学过C语言,或者学了但是忘光了。但是呢,当时郭老师跟数据结构老师拍胸脯,说我们这边学生的C语言基础都非常好,让老师不用担心C语言的东西,直接上课就行。
显然郭帅的这个发言是有问题的,但是事已至此,我们只能来弥补一下。
为什么要用C
在开始讲之前,我先插一个小话题:我们为什么要学C语言?
下面的这张图可能会说明一些问题:
从这个图上我们可以看出,C的速度可以说是各种语言里面最快的,尤其是相比Python而言, 有些时候比python要快了有100倍。
因此在做量化交易,特别是高频交易的时候,在语言方面有时候没得选,只能选C/C++。这就是C语言的重要性。
C语言代码整体结构的认识
好,现在我们来开始正式介绍C语言基础。
我觉得既然大家把实验一都做完了,那么对C语言已经有一定的了解了。但是我相信很多人处于一种,“知其然不知其所以然”的状态。我今天就让大家了解一下为什么要这样写。
在介绍细节之前,我们先从宏观上讲一下C语言的各个部分是怎么个安排。
我们来看一个简单但是完整的C语言代码,这个代码用于实现计算圆形面积的功能:
#include <stdio.h>
#include <math.h>
#define PI 4.1415
float getSquare(int r);
void intro();
int main() {
float r, square;
// 介绍功能
intro();
// 获取输入
scanf("%f", &r);
// 计算面积
square = getSquare(r);
printf("半径为 %.2f 的圆的面积为 %.2f \n", r, square);
return 0;
}
void intro(){
// 介绍本代码的功能
printf("圆形面积计算器\n");
printf("请输入圆的半径:\n");
}
float getSquare(int r){
// 计算面积
float square;
square = PI * pow(r, 2);
return square;
}
我们可以看出,这段代大致分为4个部分:
- 引入头文件
- 预定义常数
- 函数声明
- 函数定义
这里面看起来好像专业名称有点多,其实解释起来简单:
引入头文件
引入头文件就是表明这个代码要用到这两个别人已经写好的文件,比如stdio.h
里面包括了printf
这种函数,math.h
里包括了pow
函数等等。待会儿做字符串处理的时候可能会用到string.h
这个头文件,里面有一些关于字符串处理的函数。
预定义常数
预定义常数估计是同学们的一个困惑点:为什么老师上来要写一堆#define
,这个定义常数的目的就是,如果下面有很多地方需要写一个东西,在这里定义好了,下面就直接引用了。比如圆周率,当然可以每次都在程序里面写3.14,但是如果我忽然接到要求,要把全部换成3.1415咋办。有了预定义的话,直接在上面修改就可以了,很方便。
函数声明
函数声明是很多人没有注意到的一个地方。我们C语言在运行的时候,有一个规则,就是他会以main()
函数作为“入口函数”,从这里开始执行。但是C语言又有一个比较睿智的地方,就是一个函数所用到的函数,必须在这个函数在使用的时候就给定义了,不然他就会给报错,说找不到这个函数。于是我看到很多人就先把各种小函数定义好了,把main()
函数给放到最后。
但是前面已经说了,C语言会从main()
函数开始执行,大家交流代码也是从main()
函数开始交流,结果你把main()
函数给放最后了,这就很不方便。
所以C语言就想出了一个办法:你不是要在主函数后面再定义函数嘛,可以,但是你要把函数的返回值,名称,和参数都先跟我说一下。然后你就可以在任意位置定义函数了。
这就是函数声明的用处。同时,函数声明还可以方便理清思路:我先把待会要写的函数给起个名字,待会再写具体的实现,就很方便。
函数定义
在声明完函数之后,就是具体的函数定义与实现了。
相信大家都写过函数,我在这里就讲一下函数的各个组成部分(此处应该放个ppt的图)
函数的主要组成部分为:
- 函数返回类型
- 函数名称
- 参数类型以及名字
函数的返回类型,顾名思义,就是返回值的类型。比如这个intro()函数,就是print了几行字,没有什么需要返回的,所以他返回值就是void
,空的意思。但是下面的这个getSquare
函数就需要返回一个面积的值,所以返回值就是float,一个浮点数。
函数的名称。虽然怎么起名字都行,但是我们程序猿还是自己有一套标准的,比较常用的一种就是“驼峰式”命名,这个也很好理解,你看这个getSquare
,即开头一个单词是小写,从后面开始每个单词首字母都大写,这样子就很清晰,也显得很专业。
至于关于函数的传递参数以及到底带不带星号的问题,这是个大问题,我待会把指针讲完了再讲。
指针
在讲指针之前我先讲一个笑话:有一天,一个同学问了我一个问题:胡新杰,为什么这个p前面有一个乘号啊?
我听完,陷入了沉思……
现在我们开始讲指针。相信很多人似乎经常听到指针,也大概听说指针是什么的地址,但是在实践上,那里加*
号,哪里不加*
号就很容易搞糊涂。现在我来带大家具体认识一下指针。
指针是C语言里一个非常重要的概念,也是C里面比较复杂的一个概念。我在这里试图用最短的时间让大家理解指针是什么,以及指针怎么用。
指针是什么?书上说:
指针是一个数值为地址的变量。就像char类型的变量用字符作为其数值,int用整数作为其数值,指针变量表示的就是地址。
上面说指针指向地址,那么地址是什么呢?
地址是什么
当我们使用int a =1; 定义一个变量a的时候,我们实际上是在电脑的内存里面开辟了一个空间,并且把这个空间的值改变为1,给这个空间起名字叫a。由于这个空间是在内存条上的。大家可以把内存条想象成一个很长很长的方格纸,我们就是相当于在方格纸上写字,当字写下去的时候,这个字肯定有一个所在的位置,这个位置就是变量的地址。
或者说,我们换一种思路,我们定义a 的时候,不仅仅是定义了4,而且是确定了一个地址。

这里,8就是a的地址,1就是a的值。
因此,我们现在可以采用一种全新的眼光来看待a:

之前我们看int a = 1;
这个式子,我们可以知道我们定义了一个变量a,并且他的值为1。现在我们要改变一下这个观点,即a
这个变量,不仅是一个数字,还有一个地址
属性。
那么a既然有地址
这么个东西,我们怎么把他取出来呢?对,就是用&
符号来取出来,把a的地址取出来,打印给大家看一下。
#include <stdio.h>
int main() {
int a = 1;
printf("a的值为:%d \n", a);
printf("a的地址为:%p \n", &a);
return 0;
}
因为地址一般都是以16进制保存的,所以输出的就是一个16进制数。
好了,讲了半天地址,那么指针是什么?
指针是什么
上面说了,指针就是储存地址的嘛。上面这一串地址,我们不想每次都用&a
这种这么麻烦的表达来定义,所以我们就决定创建一个储存地址的东西,来储存地址。我们给这种东西起名叫指针
;把“指针p储存了a的地址”称为“指针p指向a”。
指针的定义
指针的定义与赋值方式如下:
int a = 1;
int *p;
p = &a;
在这里我们定义了一个int类型的指针变量p,这个变量可以储存int类型数据的地址。我们把a的地址赋值给p,就相当于“p指向了a”
我们来看看a的地址和p储存地址是不是相等:
printf("a的地址为:%p \n", &a);
printf("p的值为:%p\n", p);
可以看出,结果是相等的。
用指针来访问值
好,那么有一个问题,p存储了a的地址,那么能不能用p来访问这个地址储存的变量的值呢?
当然是可以的,只要用到*
符号即可。
printf("a的值为:%d \n", a);
printf("用指针p获取到的值为:%d \n", *p);
用指针来修改值
既然我们都可以用指针来访问值了,那我们是不是也可以用指针来实现修改值呢?
我们来试一下:
printf("原来:a的值为:%d \n", a);
*p = 2;
printf("修改后,a的值为:%d \n", a);
不错,修改成功了。
相信到这里,大家已经对指针的基本概念有了了解。那么接下来我们就要讲指针的应用了。
指针的应用(与C++不同)
上面巴拉巴拉这么多,讲了指针的定义,那么指针这东西在实践中到底有啥用。另外,也没有回答同学们一开始问的,到底什么时候加*
号什么时候不加啊?
先别急,我们来看一个例子。
#include <stdio.h>
void swap1(int front, int back);
int main() {
int a, b;
a = 1;
b = 2;
printf("交换前:a = %d, b = %d\n", a,b);
swap1(a, b);
printf("交换后:a = %d, b = %d", a,b);
return 0;
}
void swap1(int front, int back){
// 试图交换a和b
int temp;
temp = front;
front = back;
back = temp;
}
从输出结果来看,我们的得到的结论是很奇怪的:我们明明交换了,但是输出却不是想的这样。那么原因是什么呢?
现在是时候讲一下函数参数传递的问题了。
函数参数传递规则(严格C语言
我们刚刚讲了一下函数的定义,说的是函数的结构。我们现在来看一下C语言在具体处理函数的时候,内部是怎么操作的。
我们来看一个图:

函数的参数分为两种,一种是实际参数,一种是形式参数。其中,在主函数调用的时候,主函数里传入的叫做实际参数,然后函数里面调用的叫做形式参数。在这个例子中,主函数里的a和b就是形式参数,函数里面的front和back就是形式参数。
在C语言的机制里,函数在执行的时候,会把实际参数复制一份给形式参数,让形式参数给存储着,然后形式参数参与各种运算。因此在这个例子里,我们相当于是,系统新创建了俩变量front和back,然后把a复制给了front,b复制给了back,然后front和back交换了值。但是他俩交换值,跟a和b又有啥关系嘞?因此这是一种无效的修改方式。
那么,如何才能修改a和b的值呢?
指针作为函数参数
前面说过,我们可以通过地址来访问到一个变量的内容,那么我们是不是可以这样,我告诉这个函数,说a的地址是0004,b的地址是0006,你去把地址为0004和0006的值给交换了。
我们来试试。(这里要手敲代码)
#include <stdio.h>
void swap1(int front, int back);
void swap2(int *front, int *back);
int main() {
int a, b;
a = 1;
b = 2;
printf("交换前:a = %d, b = %d\n", a,b);
swap1(a, b);
printf("交换后:a = %d, b = %d\n", a,b);
printf("\n交换前:a = %d, b = %d\n", a,b);
swap2(&a, &b);
printf("交换后:a = %d, b = %d\n", a,b);
return 0;
}
void swap1(int front, int back){
// 试图交换a和b
int temp;
temp = front;
front = back;
back = temp;
}
void swap2(int *front, int *back){
int temp;
printf("front的值为:%p\n", front);
printf("back的值为:%p\n", back);
printf("a为:%d\n", *front);
temp = *front;
*front = *back;
*back = temp;
}
可以看出,在主函数里使用的是swap2(&a, &b);
, 即传递过去的是a的地址和b的地址。在函数定义里面,我们定义的形式参数是两个接收地址的指针。
通过打印的结果可以看到,front和back确实存储了a和b的地址,并且我们可以用*
运算符来访问到对应地址里存储的数据。
需要特别注意,函数定义里面的
int* front
和调用里面的*front
虽然都有*
,但是意义是不一样的。函数定义里面的*
是表明这个*front
是一个int类型的指针,是必须要有的;调用里面的*front
的*
是访问内容运算符,表示访问这个地址里的内容,如果不加*
的话,就是直接使用front这个地址值来做事情,比如打印等等。
然后我们确实像刚才所说的那样,使用地址访问到了变量值,并且成功的把变量给交换了。
上面这个例子是理解指针的一个非常好的例子,取自《C primer plus》里的第九章,讲函数的那里,如果感觉没怎么听明白的可以课后再去看一下。如果这个例子搞明白了,那么你对指针已经掌握了70%啦。
结构体与typedef
大家知道,程序猿本质上都是懒b,于是就会有一些投机取巧的办法,这在带来方便的同时带来了一定的学习成本。
有了函数和指针的基础,我们现在可以来讲一下更贴近作业的结构体了。
为什么要有结构体
有没有同学想过,我们不是有int,char,float上面的了吗?为什么还要定义结构体啊?
说来也简单,就是这些int,char什么的虽然强大,但是不够用的。就比如我们像定义一个学生,里面要有他的姓名,班级,考试分数。这几个明显是不同的数据类型,但是我们想把他们组合到一起,叫一个新的变量,怎么搞?
C语言给我们提供了一个解决方案:结构体(struct
)。意思就是说我们可以自由组合已有的变量,然后给这个组合起个名字。
结构体的定义方法如下:
#include <stdio.h>
struct student{
int class;
char name;
float score;
};
int main() {
struct student tom;
tom.class = 1;
tom.name = 'T';
tom.score = 99.99;
return 0;
}
大家可以注意到我这里的定义方法和使用的方法跟老师的都不一样:我定义的时候没有写typedef,并且在使用的时候是用struct student
这种形式来使用,最后在访问内部元素的时候我用的是.
而不是->
符号。这
这都是为什么呢?
其实在C语言里面,标准的结构体的定义就是我这样的:定义的时候要使用struct
关键字和结构体名称整体作为一个变量类型;并且结构体当他是一个实体而不是一个指针的时候,访问元素的值确实就是要用.
运算符来操作。
结构体变量的修改
按照上面swap的思路,我们这里设计一个结构体指针相关的函数:
我们定义了一个结构体student,创建了一个结构体的实例tom,在赋予了初值之后,我们试图用add10
函数来给tom加十分。
#include <stdio.h>
struct student{
int class;
char name;
float score;
};
void add10(struct student);
int main() {
struct student tom;
tom.class = 1;
tom.name = 'T';
tom.score = 89.99;
printf("tom之前的分数:%f\n", tom.score);
add10(tom);
printf("tom现在的分数:%f\n", tom.score);
return 0;
}
void add10(struct student stu){
stu.score += 10;
}
不过,从运行的结果来看,tom并没有获得加分。具体原因的话跟上面其实一样:系统把tom的信息复制了一份给stu这个变量了,然后改变了stu的值,但是显然没有改变tom的值。
那么如何取修改?我们当然可以按照上面的思路,即传递结构体的指针过去,然后用*
运算符来取值并赋值。
按照这个思路,代码如下:
#include <stdio.h>
struct student{
int class;
char name;
float score;
};
void add10(struct student stu);
void add10_pointer(struct student * stu);
int main() {
struct student tom;
tom.class = 1;
tom.name = 'T';
tom.score = 89.99;
printf("tom之前的分数:%f\n", tom.score);
add10(tom);
printf("tom现在的分数:%f\n", tom.score);
printf("\ntom之前的分数:%f\n", tom.score);
add10_pointer(&tom);
printf("tom现在的分数:%f\n", tom.score);
return 0;
}
void add10(struct student stu){
stu.score = stu.score + 10;
}
void add10_pointer(struct student * stu){
(*stu).score = (*stu).score + 10;
}
在这里,我们使用了*
运算符来取得stu变量的内容,然后使用.
符号来取值与赋值,思路与上面提到的swap
函数思路基本上完全一致。如果上面那个看懂了,这里应该很快可以理解。
从.
到->
但是有个问题:上面函数里面的这个(*stu).score
写起来实在是太麻烦了,但是结构体指针又是一个很常用的东西,所以说我们就专门设计了一个符号->
,用于访问结构体指针指向的内容。
所以上面的函数也可以写成下面这个样子:
void add10_pointer(struct student * stu){
stu->score = stu->score + 10;
}
这样子大家可能就熟悉一些啦、
typedef的妙用
我相信,很多同学在做上次实验的时候,基本上都没写过struct
这个单词。而我这里,定义的时候要写struct
,引用的时候还要写struct
,就显得非常麻烦。
那么上次同学们为什么可以做到这样?关键就在typedef
上面。
我们来看一下typedef
是干什么用的。
typedef
是用来为复杂的声明定义简单的别名。翻译成人话就是,比如我觉得float
这个名字太长了,每次调用的时候还要写五个字母,我就typedef
一下,定义成flt
,这样子每次就只用写三个字母了。
typedef float flt;
讲完了基本定义,我们来看一下老师写的这一行代码是什么意思:
typedef int status;
status create(struct student);
同学们说,老师这是在干啥,人家int
本来就三个字母,现在status
是六个,这不费事的吗?
实际上啊,老师写成这样,是有别的目的的。因为函数返回值可以有很多种,比如这个create
函数,我们想让他返回一个状态
,并且用OK
表示1,用FALSE
表示0。当然这个可以用int
来表示,但是如果代码写多了,你这个int就不知道是干啥的了,但是如果你返回值前面写的是status
,那在写的时候就知道要返回一个状态,就很清晰。
好了扯远了,我们回到刚才那个话题:如何才能少写struct
,即每次要用的时候,不需要写struct student
,只需要写student
就可以了。
那么我们就来typedef一下嘛
typedef struct student{
int class;
char name;
float score;
} student;
大家注意,在这里,typedef后面的一堆是一个整体,第一个student可有可无,后面的student相当于是上面的flt
,是一个缩写。定义了这个之后,我们就可以在主程序里面直接用student来表示结构体了:
#include <stdio.h>
typedef struct student{
int class;
char name;
float score;
} student;
void add10(student stu);
void add10_pointer(student* stu);
int main() {
student tom;
tom.class = 1;
tom.name = 'T';
tom.score = 89.99;
printf("tom之前的分数:%f\n", tom.score);
add10(tom);
printf("tom现在的分数:%f\n", tom.score);
printf("\ntom之前的分数:%f\n", tom.score);
add10_pointer(&tom);
printf("tom现在的分数:%f\n", tom.score);
return 0;
}
void add10(student stu){
stu.score += 10;
}
void add10_pointer(student* stu){
(*stu).score += 10;
}
大家知道,程序猿的本质是偷懒。我们看这个student* stu
,这里的*
不就是表示这个是一个指针变量嘛,我们不想每次都写他,我们直接定义一个新的变量名,比如就叫stu_p
,这样子我下次在定义的时候就不用写这个*
了。
具体代码如下:
typedef struct student{
int class;
char name;
float score;
}student, *stu_p;
大家注意,后面的这个*stu_p
就是代表指向student类型变量的指针。这个东西的用法就是当你每次要用student类型的指针的时候,就直接写stu_p
就行了。举个例子:
student tom;
tom.class = 1;
stu_p p;
p = &tom;
然后在函数的定义里面:
void add10_pointer(stu_p stu){
stu->score += 10;
}
现在我们的代码看起来是不是简洁多了~短短的几行代码,后面藏着这么多小技巧。
字符串
好了,我们现在讲C语言的最后一个知识点,字符串。
字符串是C语言里面一个最最最恶心的地方,我可以吐槽他整整一天,但是由于时间关系,我们就以之战为主,简单讲一下C语言里字符串应用相关的东西。
字符串的储存方式
我们知道,在python里面有一种专门的变量类型叫字符串变量,但是C语言很辣鸡啊,没有专门的字符串变量,他把字符串视为一个个字符组成的数组,然后以数组的方式来储存他们。
比如说,我想存储”tom”这个字符串,那么C语言就会把tom
当作是t
,o
, m
三个字符,分别放到字符数组里面,然后在最后一个字符,即m
后面一个位置,放上一个\0
符号,表示字符串中止。
具体的储存方式如图所示

至于后面的空间去干啥了……由于C语言的辣鸡机制,他们被浪费掉了。
讲这个主要是为了方便大家理解C语言对字符串的处理机制。对实际的用处就是:定义一个大一点的字符串来接收用户输入,防止用户输入太多了出问题。
另外,C语言提供了string.h
头文件,里面有一些可以调用的函数,比如strlen
函数可以获取字符串长度,strcmp
可以比较两个字符串是否相等
字符串的输入输出
讲完了字符串在C里面的储存方式,下面我们来输入一个字符串试试。
#include <stdio.h>
#include <string.h>
int main() {
char sentence[20];
printf("请输入一个句子\n");
scanf("%s", sentence);
printf("%s", sentence);
return 0;
}
我们来输入一个名字,tom,哎,对了。
但是我们来输入一个句子,比如Do you love me?
,然后发现了啥,哎,怎么Do后面的都没有了??
这里的原因我给大家解释一下,就是scanf
在输入字符串的时候,遇到空格,他就认为是输入结束了,自动给你取消了,后面的就都不作数了,非常不行。
那么我们咋搞呢?C还是提供了解决方案:用gets
函数;
#include <stdio.h>
#include <string.h>
int main() {
char sentence[20];
printf("请输入一个句子\n");
gets(sentence);
printf("%s", sentence);
return 0;
}
大家看,现在有空格也可以输入进来了。
所以说,我们如果没什么特别的需求,大家以后输入字符串就用gets
就行了。
字符数字转数字
最后来讲一个令人迷惑的点:如何把字符'1'
给变成数字1
?
我们知道,在python里面,只要int('1')
就行了,但是我们C显然不能。我们先看一下C语言里面是如何实现字符数字转数字的:
int char2int(char c)
{
// 用于把单个字符转成数字
return c-48;
}
这个是不是看起来很诡异……
我简单介绍一下原理,如果感兴趣的可以继续深究,不感兴趣的听一下就行:C语言在存储字符的时候,其实存的不是这个符号本身,而是这个字符对应的ASCII码,比如字符'1'
,在计算机里存储的就是他的ASCII码49
,所以我们想要获得1
这个数字,就只需要用'1'-48
就好了。
在这里我放几行代码运行一下,让大家感受一下char
类型和int
类型本质上并无区别:
char c;
c = '1';
printf("char c='1'类型变量c的字符表示:%c\n", c);
printf("char c='1'类型变量c的数字表示:%d\n", c);
int d;
d = 49;
printf("int d=49 类型变量d的数字表示:%d\n", d);
printf("int d=49 类型变量d的字符表示:%c\n", d);
更进一步,如果我们想把一个字符串数字转为一个多位数字呢?比如把字符串'728'
转成数字728
咋搞?其实也不难,就是挨个把字符转成数字,然后第一位乘100,第二位乘10,最后一位乘1,加起来就得到多位数字啦。
实验二的思路讲解
眼下最急切的事情,除了金融经济学的小测,可能就是数据结构的实验了。这个实验是下周三交,并且我看了一下那个系统,上面还有一个实验三,叫二叉树的运算,虽然现在还没开放啊,但是我觉得就是时间问题。
关于实验二,我想先讲思路,把思路理清楚了,再去讲代码是如何实现的。
思路
首先我们来看题目的要求,就是计算三种表达式的值。其实这个算法老师上课的时候已经讲过了,我再次简单回顾一下,并且说明一下上课的思路在具体的问题中会出现什么问题。
我们先以最熟悉的中缀表达式为例,来探讨一下这个题应该怎么做。
老师PPT上的思路
我们先来回顾一下老师给的思路:



老师的思路很清晰,就是每次都用c=getchar()
来接收一个字符,然后对每一个输入进行处理。就比如这个3*(7-2)
,就是先输入3,根据判断逻辑,数字就放到和操作数栈里,然后进行下一个,比如是*
,比较一下优先级,然后选择是放到运算符栈里还是进行运算。
这样子看起来很不错啊,但是在实际运用中存在几个很严重的问题:
第一个:我如果想输入一个两位数字怎么办?比如我想输入中缀表达式,1+10,那么按照老师这个程序,会发生什么事情?这个程序就会把我的输入认为是1 + 1 0,显然也不会算出来个正确的结果;当我使用后缀表达式的时候,问题更严重,比如+111
确实是想说+,1,11,但是显然,无法判别。
第二个:如何进行错误检查?这个助教前几天更新了那个评分标准,加入了一个健壮性检验的标准,如果用这种输入一个处理一个的办法,那么错误检查根本就无从谈起。
第三个:老师给的这个算法,仔细想一下,他把运算符和数字都存到一个栈里面去了,可是我们的这个数字是数字,符号是符号,怎么能用同一个类型的栈来处理呢?如果是把数字放到char类型的栈,那么多位数字怎么放?比如10
,一个字符格子里只能放一个字符,那10
就得放在两个格子里,这还玩锤子。那么反过来,把字符放到int类型的数字里可不可以呢?(其实是可以的hhh
基于以上这几个原因,我们认为,老师给的这个做法只是一个初步的算法,并不能够胜任做实验的要求。所以,我们要对这个思路要进行一些改进。
对老师思路的改进
有同学跟我说,可以把各种表达式都给转成逆波兰式表达式然后计算,我觉得这个可以尝试一下,但是在这里我还是先按照老师给的思路来讲。
既然老师给的思路不能胜任,那我们就来魔改一下。
首先要进行改变的就是数据的输入和处理方式。老师刚刚给的思路是划入一个字符就处理一个字符,我们现在不是,我们现在让用户把整个表达式全输入完了再处理。储存用户输入的自然就是之前提到的那个字符数组。
并且,我们对用户的输入要做一定的比较苛刻的限制,即要求用户在输入两个数字,或符号与数字之间,必须有一个空格。举个例子,上面的3*(7-2)
现在就要变成3 * ( 7 - 2 )
,如果符号和数字连在一起输入了的话,那就给他报错,让他重新输入。这样子的好处显而易见,当用户输入了一个多位数字的时候,因为他分离了,所以就可以对他单独处理。当用户输入的是前缀或者后缀表达式的时候尤其有用。
最后,关于栈的类型的问题也要解决一下。根据老师的思路,我们在处理中缀表达式的时候需要用到两个类型的栈,在不考虑int和char之间联系的时候,我们现在就直接定义两种类型的栈就好了。一个就储存运算符,叫optrStack
,是char类型的,用于存储+-*/()
这种,一个就叫opndStack
,是int类型的,每次我们把字符给解析成数字之后,就把他放到里面存储。
有了上面的基础,我们就可以准备开始操作了。
流程图
我们简单画一个流程图,梳理一下我们代码的思路。

好了,有这个流程图了,我们就可以开始写代码了
C语言实现
提前说一下,我的代码已经开源到我的博客上面了,地址为www.xiaoyaojiushao.com, 需要源码的可以去上面看一下。
我在备课的时候在想,我要不要讲我自己的代码,因为我的代码还有很多完善的地方,并且讲的太详细了可能会限制住大家的思路,并且也比较耗时。但是后来我想了想,还是一行一行解析代码吧,带大家看一下实现的细节,因为很多人可能就是有想法,但是不知道怎么一步步实现。
我先以中缀表达式为例子,带大家一行一行的看一下一个完整的程序是怎么写出来的。
头文件以及结构体定义
首先我们当然要创建一个项目啦,注意,我用的是C语言,如果想跑我的代码的话,需要在选择C还是C++的时候选择C,不然就会跑不了。
然后,我们引入一些头文件,比较常用的几个先都写上,没用了再说。注意这里的ctype.h
是用来判断一个字符是哪种类型的。
接下来把一堆可能用到的#define给复制粘贴过去
然后就到了typedef
环节,我们按照老师的代码和自己的想法,定义了了两个栈,并分别给他们起名字叫optrStack
和opndStack
。
然后是函数的声明这一部分,由于我们刚开始的时候不知道要用哪些函数,所以我们就先不管他,先往下写,然后写的时候再往上面加就行了。
主函数
作为整个程序的灵魂,主函数当然要最先登场。
首先我们要向用户介绍一下我们的程序能干啥,我们就来一个intro
函数,这个函数很简单,就是printf()
了一些东西。
接着我们看,我定义了一个select变量,用于存储用户选择哪个,并用一个do...while
循环,这样子的话,只要用户不输入4,程序就会一直运行下去。
这个menu
函数就是展示菜单并且获取用户输入的地方,这个函数返回一个int值,然后接收。
注意我这个menu()
函数里面有个getchar()
,这一行代码很简单,但是我当时调bug调了好久好久,具体原因在注释里,感兴趣可以看一下,或者这行代码删了,看看会发生什么。
好了,接下来我们以中缀表达式为例,讲一下代码的具体实现
中缀表达式的计算calcMiddle()
好了,我们现在来看中缀表达式的计算函数。
首先上来啥也不多说了,先让用户输入表达式。因为在要进行错误处理,但是这并不应该占据我们这个函数的大部分篇幅,所以我们设置一个getInput
函数,用这个函数来获取输入。
我们来看这个getInput
函数。第一步就是用gets来获取输入,然后用一个while来判断是否有问题,否则就重新输入。
这个判断是否有错误实在是太多了,我就专门又写了个函数,叫isError
,如果有错就返回1,没错就返回0.
这里的isError
里包含的错误情况有好多,我们简单看一下。我们首先定义一个result变量,如果有错就result就是1,不然就是0。这个count我们后面会经常用到,就是用来一个个来数字符,一个字符处理完了就count++。定义完之后就是进入循环环节,首先是判断是否为非法输入,即既不是操作数又不是运算符。然后看一下括号是顺序,然后看一下同一个段内是否都是一个东西(这里板书一下)。然后就是括号数量啊,操作数和运算符数量啊等等。
特别说明一下,一个函数如果执行了return
语句,那么会立马跳出函数,就是结束了。
好了我们终于把错误判断讲完了,现在回到一开始的计算函数。
现在假设输入没问题了,我们就要进入计算环节了。根据ppt的思路,首先我们要初始化俩个栈,一个是char的一个是int的,要写个初始化函数。
我们于是摸出ppt,第9-12页,基本上原封不动照抄一下,抄两份。可以看出我们这个代码就是把.
给换成了->
,因为我们用的是指针。
再次提醒,老师ppt上的这种在函数形参定义的时候用&
的方法是C++里面的,用C编译器会跑不通。
好了我们继续进行,我们定义了一堆常数东西,方便后来调用。
现在正式进入主循环开始计算。然后接下来的步骤就跟ppt一样了,如果是操作数,入栈;如果是运算符,比较一下,做个计算、
需要特别进行说明的就是多位数字的情况,稍微有点复杂。我们来看下面这几行代码
if (middleExp[count + 1] == ' ' || middleExp[count + 1] == '\0') {
// 说明就是一位数字,直接入栈就行(入的int数字)
pushND(&opnd, char2int(c));
count++;
} else {
// 但多位数字的情况,比如124就要占三个字符
// 所以要用个循环看一下
while (middleExp[count + 1] != ' ' && middleExp[count + 1] != '\0') {
// 那说明是一个多位数字了
digitCount++;
count++;
}
int multiDigitInt = str2int(middleExp, count, digitCount);
pushND(&opnd, multiDigitInt);
count++;
digitCount = 0;
}
这个的思想就是,如果一个数字后面跟的不是空格或者\0
,那么他肯定就是多位数字。因此我们就统计一下这个有几位,在字符串中在哪个位置。有了这些信息之后,我们就可以写个函数,把这几个连续的数字字符转化成一个int数字。
逆波兰式的计算
既然最难的中缀表达式都讲完了,我就顺便来讲一下另外两个吧。
我觉得最简单的就是逆波兰式计算。只要一个数进来,如果是数字,就进栈,如果是符号,就把两个数字弄出来计算一下再进栈。代码也跟上一个差不多,我们简单看一下
……
波兰式计算
波兰式计算,简单来说,就是反过来的逆波兰式。我们在计算的时候就只需要把数组从最后一个开始遍历就行了。在计算的时候需要注意一些小细节,比如多位数字在转换的时候要注意幂的次序。为此我专门写了一个str2intTraverse()
函数。
编程学习路径
最后再说点闲话,就是关于编程学习路线的问题。
有些同学问我为什么一个金融专业的C语言学的还可以,我觉得应该是兴趣
+时间
。我本科是武汉理工大学,我们学校大一新生必修C语言,然后我当时觉得编程很有意思,就把老师给的题都做完了,后来就自己看了书,多动手,才获得了一定的水平。
在这里我也不说多,如果大家真的对C语言感兴趣,我推荐一本书和一个网站:
一个网站
这个网站是一个练C语言题目的网站,里面有免费的题库,质量很高,我当初就是在这里刷题的。

一本书
《C Primer Plus》

大家千万不要嫌弃这个书太厚了而去看什么C程序设计入门之类的,这本书虽然厚,但是读起来很有意思,别的书虽然薄一点,但是几句话就把你绕晕了,直接看不下去。
本次讲座配套资源
我已经把我的代码开源到了我的个人主页上,网址:http://www.xiaoyaojiushao.com/。同步开源的还有本次讲座的文字稿、实验报告、readme文件。
其中本次讲座的文字稿有一万多字,写了我两天,如果大家有没听清的可以直接去上面找哈。