一个野生程序猿的折腾日记

USTC-MF内培-C语言讲稿

大家好,非常感谢大家来参加本次的C语言讲座。

本次的讲座主要分为两个部分,第一部分是C语言基础知识,第二部分是实验二的解题思路。

(最后要给出相应的参考资料)

C语言基础

根据我前一阵子对同学们的交流,我发现很多人其实没学过C语言,或者学了但是忘光了。但是呢,当时郭老师跟数据结构老师拍胸脯,说我们这边学生的C语言基础都非常好,让老师不用担心C语言的东西,直接上课就行。

显然郭帅的这个发言是有问题的,但是事已至此,我们只能来弥补一下。

为什么要用C

在开始讲之前,我先插一个小话题:我们为什么要学C语言?

下面的这张图可能会说明一些问题:

Benchmark results

从这个图上我们可以看出,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,而且是确定了一个地址。

image-20211112160321496

这里,8就是a的地址,1就是a的值。

因此,我们现在可以采用一种全新的眼光来看待a:

image-20211112154901337

之前我们看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语言在具体处理函数的时候,内部是怎么操作的。

我们来看一个图:

image-20211112222244801

函数的参数分为两种,一种是实际参数,一种是形式参数。其中,在主函数调用的时候,主函数里传入的叫做实际参数,然后函数里面调用的叫做形式参数。在这个例子中,主函数里的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符号,表示字符串中止。

具体的储存方式如图所示

image-20211113112318439

至于后面的空间去干啥了……由于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上的思路

我们先来回顾一下老师给的思路:

image-20211114102520573
image-20211114102530734
image-20211114102625423

老师的思路很清晰,就是每次都用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类型的,每次我们把字符给解析成数字之后,就把他放到里面存储。

有了上面的基础,我们就可以准备开始操作了。

流程图

我们简单画一个流程图,梳理一下我们代码的思路。

flowchart

好了,有这个流程图了,我们就可以开始写代码了

C语言实现

提前说一下,我的代码已经开源到我的博客上面了,地址为www.xiaoyaojiushao.com, 需要源码的可以去上面看一下。

我在备课的时候在想,我要不要讲我自己的代码,因为我的代码还有很多完善的地方,并且讲的太详细了可能会限制住大家的思路,并且也比较耗时。但是后来我想了想,还是一行一行解析代码吧,带大家看一下实现的细节,因为很多人可能就是有想法,但是不知道怎么一步步实现。

我先以中缀表达式为例子,带大家一行一行的看一下一个完整的程序是怎么写出来的。

头文件以及结构体定义

首先我们当然要创建一个项目啦,注意,我用的是C语言,如果想跑我的代码的话,需要在选择C还是C++的时候选择C,不然就会跑不了。

然后,我们引入一些头文件,比较常用的几个先都写上,没用了再说。注意这里的ctype.h是用来判断一个字符是哪种类型的。

接下来把一堆可能用到的#define给复制粘贴过去

然后就到了typedef环节,我们按照老师的代码和自己的想法,定义了了两个栈,并分别给他们起名字叫optrStackopndStack

然后是函数的声明这一部分,由于我们刚开始的时候不知道要用哪些函数,所以我们就先不管他,先往下写,然后写的时候再往上面加就行了。

主函数

作为整个程序的灵魂,主函数当然要最先登场。

首先我们要向用户介绍一下我们的程序能干啥,我们就来一个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语言感兴趣,我推荐一本书和一个网站:

一个网站

https://pintia.cn/

这个网站是一个练C语言题目的网站,里面有免费的题库,质量很高,我当初就是在这里刷题的。

image-20211114211602933

一本书

《C Primer Plus》

image-20211114211652541

大家千万不要嫌弃这个书太厚了而去看什么C程序设计入门之类的,这本书虽然厚,但是读起来很有意思,别的书虽然薄一点,但是几句话就把你绕晕了,直接看不下去。

本次讲座配套资源

我已经把我的代码开源到了我的个人主页上,网址:http://www.xiaoyaojiushao.com/。同步开源的还有本次讲座的文字稿、实验报告、readme文件。

其中本次讲座的文字稿有一万多字,写了我两天,如果大家有没听清的可以直接去上面找哈。


Warning: printf(): Too few arguments in /www/wwwroot/www.xiaoyaojiushao.com/wp-content/themes/simple-flat/inc/template-tags.php on line 58

发表评论

邮箱地址不会被公开。 必填项已用*标注