第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地

     “还真累啊。”老C揉着自己的胳膊。
     看着窗明几净的实验室,清理得整整齐齐的网线和交换机,两个人都觉得心情很舒畅。吃完了晚饭,两个人又坐到了白板旁边。
     “老C,今天早上的话题还没有谈论完呢。”小P总是充满好奇心,老C觉得小家伙是个人才。
     “稍等稍等,等我把这一点看完。”老C用目光匆匆掠过一篇起点上的穿越文,恋恋不舍的转过座椅,“好吧,我们再来review一下我们第二版的C代码。”


typedef enum tagFRUIT{ORANGE, APPLE, BANANA} FRUIT;
typedef void (*PRINT_PROC)(char*);
typedef struct tagFRUIT_INFO
{
    FRUIT               fruit_;   
   
const char* const   name_;
}FRUIT_INFO;

void DoPrintFruitName(char* name)
{
    printf("%s\n", name);
}

void DoPrintFruitNameCompany(char* name)
{
    printf("XJTU's %s\n", name);
}

const FRUIT_INFO g_fruitInfo[] =
{
    ORANGE,    "orange"
    ,APPLE,    "apple"
    ,BANANA,   "banana"
};

void
PrintFruitName(FRUIT fruit, PRINT_PROC printProc)
{
    int i;
   
    for (
         i = 0;
         i < (sizeof(g_fruitInfo)/sizeof(g_fruitInfo[0]));
         ++i
        )
    {
        if (g_fruitInfo[i].fruit_ == fruit)
        {
            (*printProc)(g_fruitInfo[i].name_);
        }
    }
}

int main()
{
    FRUIT fruit;
   
    /* Get the information of fruit. */
    fruit = GetFruitInfo();
    /* Print the name of the fruit. */
    if (/* Print fruit's company. */)
    {
        PrintFruitName(fruit,
DoPrintFruitNameCompany);
    }
    else
    {
        PrintFruitName(
fruit, DoPrintFruitName)
    }

    return 0;
}
     “我们来看看PrintFruitName()函数的问题,它的一个比较明显的缺陷是效率。”老C用笔指着代码说道。
     “是啊是啊,”有机会表现一把小P很是高兴,“这个函数做了线性查找,因此它的时间效率应当是n。”
     “对,因此对比原版的switch...case...语句的常数时间效率,我们现在的设计在时间效率上有损失,你有什么好办法解决这个问题吗?”
     “让我想想……”小P又开始发动脑筋,“我觉得我们可以使用一个散列表来储存这些信息,那么在查找的时候算法的时间复杂度就是常数了。”小P找到了解决办法很是高兴,就想着去白板上写下自己的得意之作。
     “等等别急。”老C拦住小P,“只要知道了解决之道,代码是微不足道的,你先不要着急写代码……”老C对小P的反应速度有些惊讶,“我觉得你的基础挺好的啊,就是缺乏正确的引导……”
     “呵呵,谢谢啊,其实我就是比较油菜而已。”听了老C的评论小P有些囧,不知道他是表扬还是批评。
     “我们不着急对这一版进行改写,现在你试着使用C++来完成上面的代码。”老C给小P出了新的题目。
     “嗯,我看看啊。”小P开始思索起来,“但是有什么不同吗?如果使用C++的话,我可能还是会写出第一版那样的代码啊……”
     “所以我说你缺乏正确的引导啊,”老C有些感慨,“因为C++包含了C的部分,因此你完全可以用C的思维方式在C++下编码,反应到代码风格上,就是第一版风格的代码在C++代码中频繁出现……”
     “槑,”小P有些莫名其妙,“那么应该怎么做呢?”
     “嗯,我先简单的写写,然后我们来review。”老C在白板上划了一道线,将白板分为两部分,在原来的代码部分上面写下C,然后在另一个空白部分的头部写下C++。

class Fruit
{
public:
    virtual ~Fruit() {}  

public:
    virtual void printName() = 0;
}

class Orange : public Fruit
{
public:
    virtual void printName() { cout <<  "orange" << endl;}
}

class Apple : public Fruit
{
public:
    virtual void printName() { cout << "apple" << endl; }
}

class Banana : public Fruit
{
public:
    virtual void printName() { cout << "bnana" << endl; }
}

void PrintFruitName(const Fruit& fruit)
{
    fruit.printName();
}

int main()
{
    /* Fruit factory function. */
    Fruit* fruit = GetFruit();
    PrintFruitName(*fruit);
   
    delete fruit;
   
    return 0;
}
    
     “唔,”老C揉揉手,“差不多就是这样啦,一个很简单的实现……与现实代码相去甚远……”他又想了想,“在实际情况下我们是不会这样编码的,但在这里只是说明一下思维的差异性。”
     “给我讲讲吧。”小P等待下文。
     “在这个实现里我们使用了C++的多态特性,也有一种说法是晚绑定……但是无论怎么说,其根源也是信息隐藏。”老C开始比较C和C++的实现,“我们已经比较过第一版和第二版的C实现,发现信息隐藏是进行设计的一个关键点……”
     “等等老C,什么叫多态?什么叫晚绑定?”
     “哦,我们来看看PrintFruitName()函数,你能说清楚这个函数具体实现了哪些需求?”如何简单的解释这些术语让老C觉得有些头痛。
     “从代码看它实现了对fruit对象名称的打印……”小P看了看代码,“但是具体如何做的我看不出来,而且打印的内容与函数的输入参数有关,不同的参数会有不同的结果……”
     “没错!保持统一的接口,而具体行为依照对象而定,不同的对象有不同的行为,这个就是对多态的简单解释。”老C觉得小P还是有些悟性的,“具体在C++语 言中,多态通过指针和引用来表现。即接口使用父类的指针或引用来表明抽象的统一的接口,而行为根据父类指针或引用所指向具体子类对象,不同的对象表现出不 同的行为,此乃C++实现多态的风格,具体来说我们在编程的时候需要使用指向父类的指针或者引用,然后在子类中改写父类中的虚函数,最后再把子类对象赋值 到父类指针或引用上。如果现在我们需要再增加一个梨这样的水果,你会怎么做?”
     “好像很简单了?”小P试着在白板上写下如下代码。

class Pear : public Fruit
{
public:
    virtual void printName() { cout << "pear" << endl; }
}

     "然后怎么办?我可能还需要改写GetFruit()函数,使得他可以增加一个返回的对象类型?"小P很细心的发现一个问题,“但是我觉得我们很难在 GetFruit()函数中避免类似if..else...或者switch...case..之类的逻辑选择分支……等等,好像我们实现的第二版C语言 代码也存在类似的问题,就是GetFruitInfo()中也无法避免多选择的判断分支……”
     "没有错!但是起码我们对PrintFruitName()函数的维护会好很多。"老C很是赞同小P的观察力,“这个是另外的问题,涉及到一些 factory模式,我们以后再讨论……无论怎么样,我们先来评判一下现在C++的实现。因为C++的虚函数实际上采用散列表的数据结构实现,所以我们的 C++程序执行效率会比白板另一边的第二版C代码好一些。我觉得我以后一定会和你讨论一下C++的虚函数的实现的,但是今天我们先把这个问题放一边。”
     “看来C++还有很多东西是我不了解的啊。”小P开始觉得自己C++课程好像是白上了。
     “我们来比较一下三个版本的实现。”老C开始回顾早上的讨论,“你发现什么问题没有?”
     “好像一段代码对其具体实现了解的越少,它的维护性就会越好?”小P有些猜测。
     “呵呵,的确,那么我们通过各种不同的方法达到了什么样的看似相同的目的?”老C开始掉小P的胃口。
     “信息隐藏?”小P不太确定。
     “信息隐藏是手段,但不是目的。”老C很确定的否决掉小P,“我们达到的目的是控制问题的规模!”
     老C觉得有必要给小P讲讲哲学:“我们写软件的目的是为了解决现实生活中的具体问题,没错吧?”
     “没错,的确是这样,可是这个和C++有什么关系的?”小P觉得有些莫名其妙。
     “那么你觉得使用高级的语言、先进的设计和合理的开发流程,问题的复杂度会降低吗?”
     “那是啊,问题的复杂度当然会降低啊。”
     “唉,错了,问题的复杂度不会降低的,因为问题的复杂度是客观存在,不会因为人主观的原因而改变!”
     “槑!”小P有些被震住了,他以前还真是没有考虑过这样的问题,“那么为什么我觉得C解决问题比汇编简单呢?”
     “那是因为问题的规模被控制了!”老C开始强调,“因为C的编连器暗地里帮你做了很多事情来控制问题表现给你的规模,使你感觉好像问题变简单了——其实是你面对的问题规模变小了。打个比方,”老C在白板上找出了上午最早的程序。

typedef enum tagFRUIT{ORANGE, APPLE, BANANA} FRUIT;

void PrintFruitName(FRUIT fruit)
{
    switch (
fruit)
    {
    case ORANGE:
        pirntf("orange\n");
    break;
   
    case APPLE:
        printf("apple\n");
    break;
   
    case BANANA:
        printf("banana\n");
    break;
   
    default:
        return;  
    }
}


int main()
{
    FRUIT fruit;
   
    /* Get the information of fruit. */
    fruit = GetFruitInfo();
    /* Print the name of the fruit. */
    PrintFruitName(fruit);

    return 0; 
}

     “看看吧,根据你刚才的发现,是不是如果需求发生变化时,GetFruitInfo()和PrintFruitName()这两个函数都要发生变更?”老C指着代码问小P。
     “是啊,没有错。”
     “现在我们代码的规模还很小,如果我们在多处需要涉及到水果的信息,那么根据这种风格,如果需求发生变更时,是不是每个地方都需要对源代码进行修改?”老C开始循循善诱。
     “嗯,好像是的。我们这里只是打印了水果的名称,如果我们还需要水果的形状,水果的颜色,哦,好像我们维护时候的复杂度会按照问题的规模成倍的增长。”
     “是啊是啊,这就叫牵一发而动全身。”老C总结道,“那么第二个做法呢?”
     “好像可以好一些,起码我们只用修改某些表格,哦,还要修改哪个GetFruitInfo()的函数,但是起码问题的扩散没有那么严重了……”小P现在隐约觉得自己好像脑袋里面有只手,快要抓住什么东西却又抓不到,有些迷蒙起来。
     “但是维护表格的工作量也不小啊。”老C补充道,“那么最后一种做法呢?”
     “我个人感觉好像C++编连器在帮助我们维护这些表格?”小P好像猛的明白过来,“比如将虚函数的实现隐藏在编连器的后面……”他又开始有些迷蒙……
     “是的,是这样的。”老C点点头,“我们实际上面对问题的复杂度并没有改变,只是由于语言的帮助,我们可以设计出一些代码来限制我们接触问题的规模,把一 个复杂的问题逐步划分到我们自己可以理解的规模上来,这样好像问题变简单了一样。”老C接着说道,“这就是我为什么说我们所使用的语言会影响到我们思考问 题的方法,而我们思考问题的方法会反过来影响我们编码的风格。”
     小P突然感觉自己来到了一个更宽阔的世界,好像自己突然明白了什么又好像不是很明白……小P开始觉得C++充满了神秘和乐趣,下决心一定要把C++学好。
     “老C,和你聊聊太有收获了,我要把这些代码抄下来回去再看看,体会体会,”小P做激昂状,“我一定要在3年内学好C++……”
     “等等,3年时间好像太短了吧?”老C有些被雷到了。
     “?”
     “建议你看看《Teach Yourself Programming in Ten Years》这篇文章吧,急是急不来的。”老C觉得年轻人就是浮躁。
     “哦?好,那我回宿舍后查查这篇文章。但是以后你要多教教我啊。”
     “互相讨论,互相学习。”老C谦虚道,“C++还有template种类的编程风格,但是我想我们还是讨论到此吧,时间也不早了,我们回去吧。”
     “好啊,回去打一盘魔兽,看看谁更厉害!”小P决定也给老C当一回老师过过瘾!

     (欲知后事如何,且听下回分解)

posted on 2009-01-18 14:26 Anderson 阅读(2143) 评论(11)  编辑 收藏 引用

评论

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-01-18 19:55 虫牙

Teach Yourself Programming in Ten Years
http://norvig.com/21-days.html
  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-01-18 20:14 ZJOK

很好,再详细点,通俗点!!  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-01-18 21:29 orz

太~~太~~~有才了~~  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-01-18 22:20 adon

是讲设计模式的吧,我是从设计模式才明白面向对象的  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-01-19 09:26 weimi

顶楼主!
用简单的代码说明抽象的问题,很好!  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地[未登录] 2009-01-19 10:34 Anderson

@adon
哦,设计模式是内容的一部分,主要包括
1. 从C到C++
1.1 首先接触类
1.2 越早接触UML越好
1.3 直接学习C++习语和设计模式
1.4 代码格式和编程素质,写优雅的代码
1.5 C++对象模型
2. 过程,工具,人。
2.1 IDE,配置管理
2.2 渐进的迭代式开发
2.3 以需求(用例)为引导
3. 讨论一些高级技巧
3.1 异常安全性
3.2 线程安全
4. 模版与meta programming
5. STL和BOOST,TR1
6. Qt和MFC
7. 其他,可能包括一些XML之类

不会按照顺序写,应当是穿插起来的吧。后面的还没有想好,接下来应该讨论一些从C过渡到C++的故事,中间穿插一些关于IDE,配置管理和C++对象模型的讨论吧  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-01-19 10:58 你好

你好,在你第一篇文章中我发现2个bug,
请作者核实下:
typedef enum tagFRUIT{ORANGE, APPLE, BANANA} FRUIT;

typedef struct tagFRUIT_INFO
{
FRUIT fruit_;
const char* name_;
//const char* const name_;
} FRUIT_INFO;

const FRUIT_INFO test[] = {
{ORANGE,_T("xxx")},
{APPLE,_T("fff")}
};
  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地[未登录] 2009-01-19 15:04 Anderson

@你好
const char* const name_ 指的是name_所指向的字符串内容不能被改变,而且name_这个指针也不能被改变,指向其他内容是不行的。

初始化结构体的时候,{}也是可以不用的,不过用上更清晰一些。谢谢。
至于_T,应当是windows开发环境定义的宏吧,用于在windows头文件定义的WCHAR和CHAR之间做编译选择用的,标准C中并没有定义这个宏,而且
不涉及国际化的时候wchar_t这个类型也是可以不用的吧。

我用的是最新的GCC,代码可以编译通过。VS2005上也试了一下,可以通过。

还是谢谢您的评论。  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地[未登录] 2009-01-22 17:01 Len

现在程序员小说很流行,这个系列看来要用大话文来解析细节了,非常不错  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-03-22 21:09 coast

很有才。  回复  更多评论   

# re: 第一桶 从C到C++ 第二碗 陈老C初论C++ 潘小P进入新天地 2009-09-17 15:01 乱78糟

写出了我想说但表达不出来的 内容,关注ing...  回复  更多评论   


只有注册用户登录后才能发表评论。
【推荐】超50万行VC++源码: 大型组态工控、电力仿真CAD与GIS源码库
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2009年2月>
25262728293031
1234567
891011121314
15161718192021
22232425262728
1234567

导航

统计

常用链接

留言簿(6)

随笔档案(21)

文章档案(1)

搜索

最新评论

阅读排行榜

评论排行榜