“来,我们来说说所谓的模块是怎么回事。”老C喝了一口茶,又开始和小P聊起来,“你看,我们的程序按照逻辑可以分成几个部分,分别是apple
game这个游戏,child
queue这个游戏的内容以及queue这个抽象的数据结构。在这里我们以名词为中心,将对这些名词的操作分别提炼为函数,放到不同的文件里。这样我们就
可以认为有了三个逻辑单元……”老C一边说,一边在白板上画了三个框框,又用线条将它们连接在一起。
“看,这三个模块有联系,apple game与child queue有关系,而child queue与apple game
和queue都有联系。这三个模块接口的调用就表现出这种联系关系,比如apple game的PlayGame()函数就调用了child
queue的
QueMoveToNextChild()
函数,而child
queue内部又包含了queue这个抽象的数据结构。这是一种新的思考问题的方式,这种思考问题的方法强调以数据为中心,而不是以操作为中心;这种思维
方法与我们熟悉的结构化编程方法有些不同。”看到小P有些摸不到头脑的样子,老C决定打个比方,“就像我们的电脑,CPU,GPU和南北桥芯片各自独立,
提供给外部一些接口,但是芯片内部是如何处理数据的却被封装在内部,我们需要的就是根据一定的规格将它们放到合适的地方就可以了,这样就可以简单的大规模
生产了,只要你可以读懂说明书,你自己就可以组装电脑;但是收音机就不是这样——虽然它的技术复杂度远比电脑小——如果你要去处理没有被封装过的模拟电
路,那你首先得本科毕业。”
“唔……”小P点点头。
“还有一个好处是复用,如果我们可以将queue这个模块提炼出来——使得它保持合理的接口——那么它就可以被用到其它地方,只要这个地方需要队列这样一
个数据结构。”老C挠挠头,“但是在这里我们想要提炼这个queue数据结构还是有些困难的,因为在这里它不是一个典型的queue,或者说它没有表现出
queue的特质。”
“为什么呢?”小P问道。
“因为没有体现出FIFO的需求……queue这个东西还是用在需要first in first out的场合更合适些。”老C答道。
“哦,那么在这里应当使用什么数据结构呢?”小P疑惑道。
“根据我的经验,此处使用linked list会好一些。”老C回答,“因为在这个游戏里更多的是体现在某处插入和删除数据……要不我们改写一下我们的代码,好让你有个更清楚的认识?”
“好啊,怎么更改呢?”小P问道。
“我们把具体需要的动作用函数来表达,这样你就可以更清楚的看到我们需要什么样的数据结构了。”老C说道,“同样我们还是需要伪代码来帮忙。我们先试着不
要陷入为主的使用queue作为child
queue的实现,先用伪代码将空白的地方添上,看看我们到底需要什么样的数据结构。”说着老C开始更改代码。
applegame.c:
#include "applegame.h"
#include "mydebug.h"
static void QueInitQueue (QUEUE* childQueue);
static int QueIsChildExisted (QUEUE* childQueue);
static void QueKickOutChild (QUEUE* childQueue);
static void QueMoveToNextChild (QUEUE* childQueue);
static int QueFindRemainedChild (QUEUE* childQueue);
void InitAppleGame(APPLE_GAME* game)
{
MY_DEBUG("Init the apple game.\n");
game->currCountNum_ = 0;
game->kickOutNum_ = KICK_OUT_NUM;
game->childrenRemained_ = CHILDREN_NUM;
QueInitQueue(&(game->childrenQueue_));
}
int IsGameOver(APPLE_GAME* game)
{
MY_DEBUG_1("The children remained %d\n", game->childrenRemained_);
return (1 == game->childrenRemained_);
}
void PlayGame(APPLE_GAME* game)
{
MY_DEBUG("Play game...\n");
/* If the current child is existed in the queue, count on, then check if she will be kicked out. */
if (QueIsChildExisted(&(game->childrenQueue_)))
{
/* Count on. */
game->currCountNum_++;
/* If the child counts kicked out number, then she is kicked out. */
if (game->currCountNum_ == game->kickOutNum_)
{
QueKickOutChild(&(game->childrenQueue_));
game->childrenRemained_--;
game->currCountNum_ = 0;
MY_DEBUG_1("The child kicked out is %d\n", game->childrenQueue_.queue_[game->childrenQueue_.index_].seatNum_);
}
}
QueMoveToNextChild(&(game->childrenQueue_));
}
int LastChildSeatNum(APPLE_GAME* game)
{
int seatNum;
MY_DEBUG("Searching last child's seat number\n");
seatNum = QueFindRemainedChild(&(game->childrenQueue_));
return seatNum;
}
/************************************************************************/
/* Local functions */
/************************************************************************/
static void QueInitQueue(QUEUE* childQueue)
{
/* Insert all children into the game. */
int i;
childQueue->size_ = CHILDREN_NUM;
childQueue->index_ = 0;
for (i = 0; i < childQueue->size_; ++i)
{
childQueue->queue_[i].seatNum_ = i + 1;
childQueue->queue_[i].existeState_ = EXISTED;
}
}
static int QueIsChildExisted(QUEUE* childQueue)
{
/* Is the child existed in the game? */
return (EXISTED == childQueue->queue_[childQueue->index_].existeState_);
}
static void QueKickOutChild(QUEUE* childQueue)
{
/* Remove the child from the game. */
childQueue->queue_[childQueue->index_].existeState_ = ABSENT;
}
static void QueMoveToNextChild(QUEUE* childQueue)
{
/* Go to the next child. */
childQueue->index_++;
childQueue->index_ %= childQueue->size_;
}
static int QueFindRemainedChild(QUEUE* childQueue)
{
/* Find the last child remained. */
int i;
for (i = 0; i < childQueue->size_; ++i)
{
if (EXISTED == childQueue->queue_[i].existeState_)
{
break;
}
}
return childQueue->queue_[i].seatNum_;
}
“看来我们更加需要一个便于在任意地方插入与删除元素的数据结构,而不是便于在头和尾部插入与删除元素的数据结构。”老C道,“首先我们了解了需求,现在我们看如何将这些需求组织成为我们代码的单元模块。”老C搓搓手,“这样吧,我来写,你来看。”说着他开始改写applegame.c,applegame.h。
applegame.h:
#if !defined(APPLE_GAME_H_)
#define APPLE_GAME_H_
#include "list.h"
#define CHILDREN_NUM 20U
#define KICK_OUT_NUM 7U
typedef struct tagAPPLE_GAME
{
int currCountNum_;
int kickOutNum_;
LIST childrenList_;
}APPLE_GAME;
extern void InitAppleGame (APPLE_GAME* game);
extern int IsGameOver (APPLE_GAME* game);
extern void PlayGame (APPLE_GAME* game);
extern int LastChildSeatNum (APPLE_GAME* game);
#endif /* APPLE_GAME_H_ */
------------------------------------------------------(华丽的分割线)
applegame.c:
#include "applegame.h"
#include "mydebug.h"
#include <assert.h>
#include <stdlib.h>
static void ChListInitChildList (LIST* childList);
static void ChListKickOutChild (LIST* childList);
static void ChListMoveToNextChild (LIST* childList);
static int ChListFindRemainedChild (LIST* childList);
void InitAppleGame(APPLE_GAME* game)
{
MY_DEBUG("Init the apple game.\n");
game->kickOutNum_ = KICK_OUT_NUM;
game->currCountNum_ = 0;
ChListInitChildList(&(game->childrenList_));
}
int IsGameOver(APPLE_GAME* game)
{
int childRemained;
childRemained = ListSize(&(game->childrenList_));
MY_DEBUG_1("The children remained %d\n", childRemained);
return 1 == childRemained;
}
void PlayGame(APPLE_GAME* game)
{
MY_DEBUG("Play game...\n");
++game->currCountNum_;
if (game->kickOutNum_ == game->currCountNum_)
{
/* When kick out child, the index automatically points to the next child. */
ChListKickOutChild(&(game->childrenList_));
game->currCountNum_ = 0;
}
else
{
ChListMoveToNextChild(&(game->childrenList_));
}
}
int LastChildSeatNum(APPLE_GAME* game)
{
int seatNum;
MY_DEBUG("Searching last child's seat number\n");
seatNum = ChListFindRemainedChild(&(game->childrenList_));
return seatNum;
}
/************************************************************************/
/* Local functions */
/************************************************************************/
static void ChListInitChildList( LIST* childList )
{
/* Insert all children into the game. */
int i;
LIST_NODE* child;
/* Make sure that the game at least has one child */
assert(0 != CHILDREN_NUM);
ListInitList(childList);
for (i = 0; i < CHILDREN_NUM; ++i)
{
child = (LIST_NODE*)malloc(sizeof(LIST_NODE));
child->content_.seatNum_ = i + 1;
/* Attach a new node to the end of the list. */
ListPushBack(childList, child);
}
childList->index_ = ListBegin(childList);
}
static void ChListKickOutChild( LIST* childList )
{
MY_DEBUG_1("The child kicked out is %d\n", childList->index_->content_.seatNum_);
/* Remove a node the index pointing to. */
childList->index_ = ListErase(childList, childList->index_);
}
static void ChListMoveToNextChild( LIST* childList )
{
/* Index goes to the next node. */
ListForward(childList);
}
static int ChListFindRemainedChild( LIST* childList )
{
LIST_NODE* iter;
int seatNum;
/* Get the first node of the list. */
iter = ListBegin(childList);
seatNum = iter->content_.seatNum_;
/* Destroy the list. */
ListClear(childList);
return seatNum;
}
“
看,我们现在把linked list数据结构从apple
game的数据声明中拿出,并认为它应当出现在list.h中。然后根据我们的使用环境,在child list模块中提出对抽象的linked
list的具体要求,也就是我们在编写操作child
list时希望list数据结构所具有的接口。”老C指着代码向小P解释,“比如我们在编写child list的初始化函数ChListInitChildList()时,我们希望list模块提供相应的初始化函数ListInitList(),因为作为使用者,我们无需知道也不可能知道list模块是怎么样初始化的。其它的接口也一样,它们都是根据具体的应用环境需求提出来的。”老C揉揉发酸的手指,“现在我们已经有了具体的对list模块的需求,就可以根据这些要求对linked list这个模块进行编码了……”
“等等,”小P插嘴道,“
ChListInitChildList()函数中的assert()是什么意思?”
“哦,这是一种编程习惯,用于查找程序中违反编程约定的地方。一般的程序在运行的时候有各种各样的不变性条件,分为前置不变性,运行不变性和后置不变性条
件,assert()用于检测这些条件是否满足我们编程的约定。因为我们的linked list不能出现为0的情况,且linked
list的大小实在程序中用宏定义出来的,所以在初始化的时候我使用断言确保初始化程序可以在正常的条件下进行。但是如果linked
list是由外部输入,比如键盘输入得到,那么我们最好使用一个if()判断来处理用户输入的异常情况……总之看你认为这些反常的情况是出现在程序内部还
是外部——这些我以后还会谈到的。关于断言,你可以在网上搜索一下。”老C解释。
“好的……又是一个需要经验积累的东西吗?”小P问。
“呵呵,可以这样说,”老C点点头,“你先大概看看代码的结构,不必纠缠于细节,我们再来看看linked list的具体实现。”说完老C新建了list.h和list.c两个文件,然后又开始扣扣扣扣的打字了。
list.h:
#if !defined(LIST_H_)
#define LIST_H_
typedef int SEAT_NUM;
typedef enum tagEXISTE_STATE { ABSENT, EXISTED } EXISTE_STATE;
typedef struct tagCHILD
{
SEAT_NUM seatNum_;
}CHILD;
typedef CHILD LIST_CONTENT;
struct tagLIST_NODE
{
struct tagLIST_NODE* prev_;
struct tagLIST_NODE* next_;
LIST_CONTENT content_;
};
typedef struct tagLIST_NODE LIST_NODE;
typedef struct tagLIST
{
int size_;
LIST_NODE* index_;
LIST_NODE* list_;
}LIST;
extern void ListInitList (LIST* list);
extern int ListSize (LIST* list);
extern LIST_NODE* ListBegin (LIST* list);
extern LIST_NODE* ListEnd (LIST* list);
extern void ListInsert (LIST* list, LIST_NODE* iter, LIST_NODE* newNode);
extern LIST_NODE* ListErase (LIST* list, LIST_NODE* iter);
extern void ListClear (LIST* list);
extern void ListPushBack (LIST* list, LIST_NODE* newNode);
extern void ListPopFront (LIST* list);
extern LIST_NODE* ListForward (LIST* list);
#endif /* LIST_H_ */
------------------------------------------------------(华丽的分割线)
list.c:
#include "list.h"
#include <stdlib.h>
#include <string.h>
/* Create the list head. */
void ListInitList(LIST* list)
{
LIST_NODE* head;
head = (LIST_NODE*)malloc(sizeof(LIST_NODE));
head->next_ = head;
head->prev_ = head;
list->size_ = 0;
list->list_ = head;
list->index_ = list->list_;
}
/* Return the size of the list. (Does not include list head.) */
int ListSize(LIST* list)
{
return list->size_;
}
/* Return the first node of the list. */
LIST_NODE* ListBegin(LIST* list)
{
return list->list_->next_;
}
/* Return the node after the last node of the list */
LIST_NODE* ListEnd(LIST* list)
{
return list->list_;
}
/* Insert a new node before the specified list node. */
void ListInsert( LIST* list, LIST_NODE* iter, LIST_NODE* newNode )
{
list = list;
newNode->next_ = iter;
newNode->prev_ = iter->prev_;
newNode->prev_->next_ = newNode;
iter->prev_ = newNode;
++list->size_;
}
/* Remove a list node specified. */
LIST_NODE* ListErase(LIST* list, LIST_NODE* iter)
{
LIST_NODE* nextNode;
list = list;
nextNode = iter->next_;
if (ListEnd(list) == nextNode)
{
nextNode = ListBegin(list);
}
iter->prev_->next_ = iter->next_;
iter->next_->prev_ = iter->prev_;
memset(iter, 0, sizeof(LIST_NODE));
free(iter);
--list->size_;
return nextNode;
}
/* Destroy all nodes of the list, including the head. */
void ListClear(LIST* list)
{
while (ListSize(list))
{
ListPopFront(list);
}
memset(list->list_, 0, sizeof(LIST_NODE));
free(list->list_);
}
/* Attach a new node to the end of the list. */
void ListPushBack( LIST* list, LIST_NODE* newNode )
{
ListInsert(list, ListEnd(list), newNode);
}
/* Remove the fist node of the list. */
void ListPopFront(LIST* list)
{
if (ListSize(list))
{
ListErase(list, ListBegin(list));
}
}
/* Move the list index to the next node. If reaches the one after the last node,
the index is moved to the first node. */
LIST_NODE* ListForward(LIST* list)
{
list->index_ = list->index_->next_;
if (ListEnd(list) == list->index_)
{
list->index_ = ListBegin(list);
}
return list->index_;
}
“在这里我们实现了一个双向链表,因为这样更容易添加和删除元素;这个双向链表有一个头指针,在list结构体里边叫list_,这样在删除和添加元素的
时候比较好处理;注意在这里我们使用了左闭右开定义域,就是说使用了[first,
last)的形式定义链表的定义域,这样在计算长度和循环迭代上都很方便,关于这方面的内容我们以后还会讨论到……”老C挠
挠头,接着说道,“注意在删除内存前将其内容清0是很好的习惯,这样在数据败坏时程序会迅速崩溃,从而减少大量的调试时间。”他又看了看代码,“list
=
list这样奇怪的用法只是为了去除编译警告——认真的去除所有编译警告是也是比较好的习惯——为什么要在函数形参设置多余的参数?……为了统一,这样可
以减少一些记忆力上的负担……喔,iter是iterator的简写……”老C给小P简单的讲解了一下。
“等等,我再仔细看看……”小P开始在屏幕上翻来翻去,滚动鼠标的滚动轴,“嗯,这下我有些明白了,apple game使用了child
list作为实现,apple game的函数——就是main.c中的函数——调用了child list的接口作为实现的一部分,而child
list的接口就是apple game中的static函数……然后child list又使用linked
list这个模块的接口作为其实现的一部分……嗯,层次结构就是这样……”
“没错,我们将功能分配到各个模块中,最后得到了一个和具体需求——也就是apple game——独立的模块,就是linked
list,这个模块完全不知道apple game的任何信息,这样——的确很有趣——我们实现的linked
list有可能被用在别处。”老C接着补充道。
“等等等等,”小P叫住老C,“我还得再看看代码,熟悉熟悉,学习学习……”他又看了几遍代码,重点看了看linked
list的实现,“不错,只要我们将list.h和list.c加入到其他项目,并修改LIST_CONTENT这个类型的定义,的确可以将这些代码用在
其他地方……”
“没错,由于我们模块的粒度……什么叫粒度?就是规模……由于我们模块的规模足够细致,这样我们有一部分的代码就可以被复用,而且这些代码是经过我们这个项目测试过的,完全可以用在其他地方以减少开发和测试的工作量。”老C习惯性的总结道。
“但是……等等……好像list模块最多只能用一次,因为没有办法在一个工程中定义两个LIST_CONTENT类型……”小P突然注意到什么,很是兴奋的分析道。
“呵呵,这个是下一步的工作了,你先看看这一版的实现是否正确……”老C开始有些佩服小P的观察力了。
“嗯,我来看看程序是否工作正常……”小P修改了
applegame.h中 CHILDREN_NUM 和 KICK_OUT_NUM 宏的定义,观察了调试信息的输出,“嗯,我认为没有什么问题。”他点头说道。
“注意值为1的边界情况和0的非正常情况。”老C提醒。
“知道了,”小P应道,又尝试了一些边界情况和非正常情况,“嗯,程序的执行没有什么问题,在0值的情况下会出现runtime错误。”
“好啦,我们的C编码活动告一段落啦。”老C说完将所有文件拷贝到AppleGame_V1.02,然后将AppleGame_V1.02命名为AppleGame_V2.0。“我们已经进行了简单的开发活动,让我们来总结总结……”
“是么?有哪些需要总结的地方?”小P摸摸头,问道。
“首先是思想和原理性的东西,其次是方法上的东西。”老C道。
“?槑”小P有些莫名其妙。
“呵呵,我来说你来写吧……把白板擦干净……”老C揉揉手指,决定偷懒一下。
“好!”
老C接过彩笔,在白板中间从上到下画了一道线,左边写上思想,右边写上方法。“你先写写思想上的东西吧,”他喝了一口水,“思想是最重要的,我们需要通过
学习语言来学习思想——只要学会了编程的思想,那么你再学习其他任何语言都会很快——要深入语言去学习,而不是只是使用语言。首先我们的第一个经验是,以
数据为中心思考问题,而不是以活动为中心思考问题。”
“嗯,好像没有什么问题,如果我们以数据为中心思考问题,那么总会抽象出一些变化较少的,相对稳定的数据,将对数据的操作与数据捆绑到一个代码单元中,这样就可以有限度的复用已经开发的代码……”小P若有所思。
“呵呵,这只是一个好处,还有一些其他的好处,需要你在以后的编程中体会。”老C笑笑。这样白板的左边出现了第一个和第二个经验的总结。
思想:
1. 以数据为中心思考问题的解决之道。
2. 将对同类数据的操作放在相同的编译单元中。
“唔,如果我们使用这种方法去解决问题,是不是就是面向对象了呢?”小P突然想到了什么,问老C。
“哦,还不是。因为面向对象的三大特性,封装、继承和多态,我们只使用了封装这个特性,所以不能叫面向对象的,只能叫基于对象的。”老C答道,“所以我们第三个总结,就是将对数据的操作,如非必要,尽量隐藏起来,而不要暴露给其他编译单元或者用户。”
“好哩。”小P又飞快的在下面加上了第三条经验。
思想:
1. 以数据为中心思考问题的解决之道。
2. 将对同类数据的操作放在相同的编译单元中。
3. 信息隐藏,而不是暴露,以将问题的规模控制在可理解的范围。
“还有什么?”小P问。
“这三条已经可以了,够你体会一阵子了。贪多嚼不烂,我们就先总结这么多吧。”老C无奈道,“编程的思想是很难学习的,需要经验来体会——不要妄图一次学
会所有的东西,学习任何东西都是螺旋形上升的,编程也一样……就连我们的开发过程也一样……好啦,我们再来总结总结方法吧。”
“那么我就写在右边了?”小P问。
“是啊,当然了。”老C囧道,“我们的方法你也看到了,其实也就是那么几条。首先要从大方面着手,将问题分解为几个基本的步骤,然后用伪代码写出解决问题的思路。”
“哦,是这样的。”小P迫不及待的在白板左面写下第一条关于方法的总结。
“不,不是这样,”老C阻止小P,“无所谓自上向下和自下向上,因为在开发过程中这两个活动是并发的并且始终贯穿于开发活动当中,甚至有人说设计要自上向下,实现要自下向上。”
“那么应当怎么总结方法呢?”小P郁闷道。
“首先着眼于整体,而不是细节。”老C得意的说道。
“好,我不反对。”于是小P将白板右面的文字进行了更改。
“然后呢?”小P问。
“然后使用伪代码,写出问题的解决之道。”老P回答,“然后再根据伪代码,写出相应的数据定义以及需要解决此问题的对数据的操作,并写出测试代码,让我们的框架可以迅速编译通过并运行。”
“好,那么我就这样总结。”小P在白板上涂涂抹抹。
方法:
1. 首先关注整体,而不是细节。
2. 先考虑测试,更先于编码。
3. 先用伪代码编程,以体现程序的意图,再用具体代码完善之。
4. 迅速构建可编译通过的,并且可执行的框架程序,使用测试代码测试之。
“如果我们做到以上几点,我们可以迅速拥有一个正确的可以运行的框架,以后写的代码都可以在此框架下进行测试,避免了后期进行大规模代码集成的风险。”老C道,“这些等你编码规模变大以后就会有一些体会了。”
“是啊,现在我们已经有了框架了,然后怎么办呢?”小P问道。
“因为我们已经将问题分解为几个部分了,对每一个部分反复运用我们以上总结的四个经验,这样我们就得到了更细小的模块,递归下去,直到每个模块的规模都在我们的掌控之中,我们就解决问题了。”老C回答到。
“是啊是啊,这个很像分而治之的思想。”小P道。
“没错,你总结的很到位,这就是分而治之思想的一个运用而已。”老C开始有些佩服小P的总结能力和联想能力了,“还有一些方法需要总结,比如说要在代码环
境中提取对其他模块接口的需求,而不要自底向上的猜测某一模块的接口应当是什么样子。我们在实现linked
list模块就是用这种方法做的,我不管linked
list究竟有多少接口,我只管在applegame.c的函数中实现算法,在其中自然而然的就会有对linked
list接口函数的需求。根据这些需求,我们先写出.h文件,然后抛开杂念专心致志的在其对应的.c文件中实现这些外部对linked
list接口的需要,而模块外面没有表现出来的一些更加具体的操作,自然被隐藏起来成为linked
list内部的static函数。不过在我们的代码中,还没有这样的被隐藏起来的static函数而已,但如果我们继续深入的优化linked
list模块,这些函数迟早会出现的。”
“嗯,那么我们可以这么总结。”小P回应道。
方法:
1. 首先关注整体,而不是细节。
2. 先考虑测试,更先于编码。
3. 先用伪代码编程,以体现程序的意图,再用具体代码完善之。
4. 迅速构建可编译通过的,并且可执行的框架程序,使用测试代码测试之。
5. 将以上方法反复应用于子模块,直到问题被解决。
6. 在上下文环境中提取对其他模块的需求。
7. 先写.h文件,后写.c文件。
“这样就可以了吧。”小P问。
“嗯,还差一些,你有没有观察到我们的程序经历了好几个版本?而每一个版本都比上一个版本稍微好上一点点?”老C问道。
“的确是这样,那么这点应当如何总结呢?”小P问。
“嗯,就是先快速实现一个可行的解法,然后放在实际需求环境中考察这个解法,根据实际需求对解法的反馈,不断修正此程序。”老C回答,“简单的说就是迭代的开发,哲学一些就是螺旋形上升的开发。”
“我还是喜欢更通俗一些的说法。”小P在白板上又增加了一条。
方法:
1. 首先关注整体,而不是细节。
2. 先考虑测试,更先于编码。
3. 先用伪代码编程,以体现程序的意图,再用具体代码完善之。
4. 迅速构建可编译通过的,并且可执行的框架程序,使用测试代码测试之。
5. 将以上方法反复应用于子模块,直到问题被解决。
6. 在上下文环境中提取对其他模块的需求。
7. 先写.h文件,后写.c文件。
8. 先快速实现一个可行的解法,再根据反馈信息优化之。
“呵呵,其实前面几步我们是在做架构设计,也就是概要设计,后面几步是在进行详细设计及编码,一般很少有一次就搞定的事情,比较好的解决之道总是这样反复
来上几次才找到的——除非你所在的团队对问题的领域特别熟悉,对需求了解相当透彻。”老C总结,“好啦,时候也不早了,我们回去睡觉吧,过几天我们来看看
用C++如何解决这个问题。”
“好吧,你先回去吧,我把白板上的东西抄回去,再对照着我们写过的代码仔细琢磨琢磨。”小P道。
“哈哈,算了算了,明天再搞吧,今天已经晚了。劳逸结合才是正道,不如你今晚和我再打打魔兽吧……”老C笑道。
“……也好……看我再给你支几招……”小P笑道。
“呵呵,那就赶快回去吧,我的手有些痒痒了……”老C拉起小P,飞快的走出教研室。
(想知道C++版本的实现么?)