萍聚社区-德国热线-德国实用信息网

 找回密码
 注册

微信登录

微信扫一扫,快速登录

萍聚头条

查看: 1325|回复: 4

ZWeily的小品文(二)C++入门教程(1)

[复制链接]
发表于 2003-6-30 17:09 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册 微信登录

x
标题     ZWeily的小品文(二)C++入门教程(1)    zweily(原作)
  
关键字     C++、class、面向对象、教程
  


§1 从对象谈起
画外音:最近公司又来了一个新人,被分配到我们这个小组。结果一了解,原来她只学过C,没接触过C++,于是,我们的头——Solmyr——就把培训她的任务交给了我。这样,我们这个小组就又多了一个MM,别的小组都快羡慕死了 ^_^  喔,差点忘了介绍了,新来的叫Young ……

每天的工作还是同往常一样,似乎没什么变化,偶尔看看我那个新徒弟Young,她似乎总是很认真的在看书,似乎还没和大家融合在一起,毕竟,才来了一天啊。

恩,快收工了,我还是按照惯例,打开Outlook,收一下邮件。

突然,MSN 跳出一条信息“Weily,今天收工后有空吗?有空的话就一起吃饭吧,我请 ^_^”。

呵呵,我正愁晚饭呢。于是,我马上回复:“好啊 :)”

……

收工完,就和Young一起去吃饭了。路上,Young就一直在问我学C++学了多久了之类的问题,我也感觉到似乎这顿饭是有目的的……

“师傅啊,你什么时候给我指导一下C++啊?”Young终于说出了请客的目的。

“我?这个 …… 对了,Solmyr叫你去看的《The C++ Programming Language》看了多少了?”我故意扯开话题。

“啊?书我当然在看啊,不过你是Solmyr老大派给我的师傅哦,你可别想推掉培训我的任务哦,否则,这顿饭……”

看不出来,原来她这么精明。还搬出Solmyr的名字来威胁我,这招够狠。

“那么你的想法是?”

“当然是你给我上上课咯”Young使出了她最有魅力的微笑,可我总觉得这个笑容有点邪。

“那还是先回答我刚才问你的问题,那本‘圣经’看的怎么样了?”

“我就是看了那本书,觉得很多东西难以理解,甚至我去找了中文版的,也看不太懂,有好多问题,所以我才要你来给我指导一下啊。而且,我也在网上看过一些评价和讨论,似乎很多人都说这本书不适合入门用……”

“的确,Bjarne自己也说过,这本的确不太适合初学者,不过,如果想要学好C++,那么这本书是必须要看的。那么这样吧,我就把我自己的一些理解和经验逐步地告诉你,这样给你一个对于C++的总体印象,然后,你再去看这本书,就应该有点体会了。”

“好啊,书我一定会看的。那要么我们现在就开始第一节课吧。”Young似乎有一点迫不及待了。

“你应该听说过面向对象吧,也就是Object-Oriented,简称OO。”

“恩,这个我知道。C++就是一种面向对象的高级语言吧。不过我对于OO也就只知道这些了”Young换了一个笑容,似乎很谦逊的样子。

“那么,我们这次就从面向对象开始谈吧。当然,由于时间的关系,我不可能深入的讲解面向对象技术,而且,我也还没达到Solmyr那种境界,所以,我给你讲的这些东西只能算是介绍,也就是入门性质的。”

“好的,具体的我会去看书的,我想书上会有更为详尽的内容的。”

“恩,这个学习态度很好,先表扬一下。喔,在开始之前,我先问你一个问题,你对于‘Object’,也就是‘对象’这个概念有多少了解?”

“对象啊,我觉得就是一样样东西,可以是具体的,也可以是抽象的。”

“大致上可以这么说。其实C++里面的class,也就是类就相当于C中的类型,就像int、char等等,其代表的一种概念(Concept),而对象就是一个个instance(实例)。举个简单的例子,‘书’就是一个class,而一本《The C++ Programming Language》就是书这个class的一个instance。”

“哦,有点懂了。你继续讲吧”

本来想利用她思考的时间吃点菜的,可是我刚用筷子夹起一个菜,还没送到嘴里,她就理解了,可怜我只能将菜放到碗里,等下一个时机。

“C++里有class这个概念的目的,就是为了提供给程序员一种创建他们自己的类型的工具,而且,还要使得这些用户自定义类型和C++的内建类型一样使用方便。所以,一个class,就是一种用户自定义类型。而且,class能够很好的实现数据封装和信息隐藏,使得使用这个class的用户不用去了解这个class的设计细节,只要了解它的公共接口的功能和使用方法就可以了。我记得Solmyr和我说过,你学过C吧,那么应该用过C里面的struct吧?”

“恩,这个用过。在学数据结构的时候还用这个写单链表之类的数据结构呢。”

“C++中的class,从某种意义上说,就是C里面的struct,其区别么,主要就是class中的成员(members)默认为私有的(private),而struct中的members是public的,这是最主要的区别。当然,这个区别可以通过改变访问限制权限来统一的。这些具体的,你都可以从书上看到。下面我们就从例子入手。还记得数据结构中的栈(stack)这个概念吗?”

“记得啊,就是先入后出(FILO)啊。”Young似乎很得意的样子。

“恩。那如果我想建立一个存放整数(int)类型的栈,需要一些什么东西呢?”

“需要一个用于存放变量的数组。”

“数组?如果用数组来建立栈的话,就有一个大小限制。关于这个问题我们先不考虑。那么我再问你,一般对于栈而言,有些什么常用的操作?”

“入栈和出栈,也就是push和pop”Young又一次露出了得意的笑容。

“恩,看来你的数据结构学得还不错嘛……”,我故意拖长最后一个音,似乎带点讽刺,在等她感觉到这点前,我又马上接着问:“这些东西,也就是那个数组和push、pop两个操作是密切相关的吧?”

“应该是的吧。”

“不是应该,是肯定!记住,作为一个程序员要对自己已经明确和掌握的东西有一定的自信!”

“知道了……”

“那么如果在C中,能够很好的体现出这些东西的关系吗?”

“似乎不能。我记得以前写的时候就是写了push和pop两个函数,另外就是建了一个数组,可是看不出直接关系的。”

“对!但如果用C++的class来考虑这个栈,就完全不同了。你所说的那个数组就是这个类的数据成员(data member),而push和pop两个操作就是这个类的两个成员函数(member function),这个类的大致定义就可以像这样……”

我转过身正准备找纸和笔,Young却已经把本子和笔递了过来,看来她的确早有预谋啊。我接过本子和笔,写下了:

class IntStack
{
public:
    ……
    bool push(int data);
    int pop();
    ……
private:
    ……
    int _stack[MAX];
};

“这就是我刚才所说的那个整数堆栈类的大致结构了。当然,我们这里忽略了很多细节,就好比构造函数、析构函数、拷贝构造函数等等,而且一个堆栈用一个数组来实现也不好,这些细节以后我们会一个个来关注的。C++里其实还有很多特性,等讲到泛型和模板(Template)的时候,我们就可以将这个堆栈作成一个模板类。呵呵,你要学的东西还有好多,一点点来啦。”

“喔,知道了,你还要说的就是‘后面的路还很长是吧’?”Young眨着眼睛,微笑着。

“你怎么知道我要这么说啊?”

“这个啊?地球人都知道啊!呵呵……”

昏倒。的确,这句话我常说,熟悉我的人都知道,可是Young才来了一天啊,在吃这顿饭之前,和我说过的话大概不超过五句吧,她怎么会知道?

哎呀,不好,饭还没吃啊。当我想到这点的时候,发现已经完了,桌上的几个菜基本上都已经被“消灭”了。看来,我的晚饭问题还是没解决。而她,似乎很有收获的样子。我终于理解了当年Solmyr带Pisces的时候的痛苦了。


注:
1. 参考了Bjarne Stroustrup的《The C++ Programming Language》
2. 我的C++水平么也不高,写这份小品文式的教程的目的是为了帮助那些还没有入门的初学者,也帮我自己系统地重新学习一下C++ ^_^
Die von den Nutzern eingestellten Information und Meinungen sind nicht eigene Informationen und Meinungen der DOLC GmbH.
 楼主| 发表于 2003-6-30 17:09 | 显示全部楼层
标题     ZWeily的小品文(三)C++入门教程(2)    zweily(原作)
  
关键字     C++、变量的初始化、构造函数方法、const和reference
  

§2  变量的初始化、const和reference

“叮……叮……叮……”

一阵电话铃声把我从梦里吵醒,是哪个可恶的家伙啊,连觉都不让别人睡好,难得双休日不用加班,真是#%%¥# ……(此处省略213字的抱怨)

“喂!谁啊?”还没睡醒,脑袋有点晕,于是语气也有点不客气。

“师傅早!”Young的语气到是十分客气的样子。

“原来是你啊!这么早打电话找我干什么啊?”

“很早吗?”

我看了一下闹钟,“才七点五十啊!”

“啊?你果然没睡醒哦。看看清楚嘛……”Young故意拖长了最后一个字,似乎在给我改正的机会。

我揉了揉眼睛,再看了一下边上那个小闹钟,啊,原来我把长针和短针看反了,已经十点四十了。

“喔,抱歉!被你说中了,我的确没睡醒,昨天晚上,哦,不对,是今天早上3点才睡的。不过今天是周六啊,睡晚点也没关系吧?”没睡醒,脑子有点糊,一下子找不出什么体面一点的借口,还是实话实说为好。

“呵呵,没关系啦,干这行的似乎都是这样的,我知道的啦!”

“恩,那你今天找我有什么事啊?”还是转个话题为好。

“上次你给我讲的东西我都懂了,所以,我想问你今天有没有空啊?可不可以继续我们的课程啊?”Young的语气很诚恳。

“今天?让我看看。喔…… 今天我没什么安排,那要么等一下到公司碰面?”

“好啊!那就这样说定咯!”

“好啦,不会放你鸽子的啦,放心啦!”

“恩,那我就先去公司等你,你快点来哦。”

“好的,就这样,拜拜!”

挂了电话,赶忙起床。

……(三十分钟后到了公司)

一进我们小组的工作室,就看见Young已经坐在我的位子上,捧着本书,似乎是《The C++ Programming Language》。

“你这么快就来啦?没吃早饭吧?”我刚想叫她,结果她已经看到我了,又把我的台词抢了。

“恩,还没吃。一般我周末都只吃两顿的。”

“那好,等一下一起吃午饭吧,还是我请哦!”

“你这个徒弟到是够乖的,总是请师傅吃饭啊。恩,这顿饭就算补我的睡眠了,就不责怪你那么早打电话把我吵醒了。”

“呵呵,实在抱歉啦,我也不知道这么晚了,您老人家还在梦中神游。”

“老人家?咳咳…… 注意用词!我看上去有那么老吗?”
“没有啦,我用这个词的目的只是为了表示对您的尊敬”Young一脸鬼笑,我也真是那她没办法。

“好吧,那就开始今天的课程吧。恩,让我想想今天该教你点什么……”说这句话的时候,脑中闪过一个个C++中的特性和关键字,“我们就从C++里面的基本类型讲起吧。”

“好啊。喔,我知道C++支持原来C里面的所有的基本类型,就像int、char、float、double等等。这些东西的用法和原来C里面一样吧。”Young在说这些的时候,眼神中带着一点自信。

“恩,基本正确。”

“啊?难道还有不同?”自信的眼神没了,Young又进入了疑问的状态。

“的确还有不同。就这些C++内建的基本类型而言,C++支持C中的使用方法,但是还增加了一点特性。我问你,怎么对一个int类型初始化?”

Young很有自信的在屏幕上打出:

int a = 3;    //定义int类型变量a,并且初始化为3

“正确,无论在C和C++中,这样做总是正确的。但是,你知道另外一种初始化的方法吗?”

“……不知道。”

“其实还可以这样初始化。”我在屏幕上打出:

int a(3);    //定义int类型变量a,并且用构造函数语法初始化为3

“哦,原来还可以这样啊?那这个和原来的方式有什么不同呢?”

“其实这种方式对于一般的内建数据类型而言,基本上没有区别的。但是这种方式对于C++而言,有着很重要的价值,否则,它也不会存在啊。例如下面这个例子。”

int* p = new int;   //定义一个指向int类型的指针,并且为它动态分配一块内存
*p = 3;   //将p指向的对象赋值为3

“这样写的确没有什么问题,但是我们可以利用构造函数语法将这两行代码变成一行。就象下面这样。”

int* p = new int(3);

“这样就既完成了动态分配,也赋了初值。还记得C里面用来动态申请内存的函数吗?”

“记得,就是那个malloc()!”

“是的。这个函数和C++里的new这个运算符的本质区别就在于new会自动地去调用对象的构造函数,而malloc不会。这也是new的优点,它能保证动态生成的对象在生成的时候就被初始化,这个特点可以避免很多可能发生的错误。”

“我没听错吧,你说new是一个运算符?”

“没错,new是一个operator,其实C++里的运算符就象函数一样,其实就是一种特殊的函数。恩,等到以后给你讲运算符重载的时候我再给你详细解释。”

“哦,大致听懂了。你刚才说的在建立一个对象的时候没有初始化所带来的麻烦,我以前在学C的时候碰到过。恩,我会记住这点的。”

“好,接下去我们讲const这个关键字的使用。”

“const?这个关键字我在看书的时候看到过,而且被它搞晕了,一想到这个就觉得不爽。”Young似乎有点沮丧。

“是不是关键问题出在和指针的关系上啊?”

“是啊!原来师傅你也深有体会啊!”

“你这张嘴真是不饶人啊,厉害!算了,我不和你计较。还是先说区分方法。其实要搞清楚它和指针的关系很简单,只要看const这个关键字出现在*之前还是*之后。如果出现在*之前,就表示这个指针所指向的对象是const的;而如果出现在*之后,那么这个指针本身就是const的。当然,我想对于前后都有这种情况就不用我说了吧?”

“喔!原来是这样,这回知道了!”

“还有,由于const的对象是不能被赋值的,所以const对象一定要初始化。”

“恩,记住了。”

“顺便教你一种写法,可以直接从字面上读出const和指针的关系。就像这样”

char *const cp;   //const pointer to char
char const* pc;   //pointer to const char

“运用前面教你的方法,很容易区别出这两个指针的不同。你试着用从右到左的顺序将它念出来。”

“从右到左?倒着念?好的,我试试。第一个是:‘cp const * char’,第二个是:‘pc * const char’”

“恩,接下去,试着在变量名之后加上‘is a ’,再将‘*’改成‘pointer to ’,再把它念一遍。”

“好的。第一个是:‘cp is a const pointer to char’,第二个是:‘pc is a pointer to const char’。啊!这样不就区分出两者的区别了啊!好神奇啊!”

“对了,用这种方式来书写,能够直接读出两者的区别。对于第二个来说,也可以将const这个关键字放在最前面,对于编译器而言,‘const char* pc;’和‘char const* pc;’是一样的,可是后者对于程序员而言更容易读懂。”

“这回终于搞清楚了,以后写程序的时候我一定会注意的,写得清楚能够给别人方便,也给自己方便。”

“不错!能够领悟到这点很不错!要记住,‘从软件工程的角度而言,程序的清晰性是最重要的!’”

“恩,记住了!”

“最后我们再来看看引用(reference)和指针(pointer)的区别和联系。引用又可以称作为变量别名它在它作用域的任何地方都代表其最初被初始化的那个对象。例如,”

int a = 3;
int& r = a;

“在这以后的代码中,r就是变量a。两者完全一样。对于r的任何操作,其实都是作用在a的上面的。而指针我想你已经在学C的时候学过了,应该熟悉了它的用法和特点。为了让函数能够改变调用时送入的变量的值,我们就要使用指针或者引用。例如,”

void swap(int& a, int& b)    //利用引用,交换a和b
{
    int t;
    t = a;
    a = b;
    b = t;
}

void swap(int* a, int* b)    //利用指针,交换a和b
{
    int t;
    t = *a;
    *a = *b;
    *b = t;
}

“看到指针和引用的区别了吗?指针在使用其指向的变量的时候,需要使用*这个解引用(dereference)运算符;而引用就是其引用的对象的别名,直接使用就可以了。但是要注意,利用引用的特点,在函数里改变参数对象的值的做法,缺少可读性,比较容易出错,所以在写这样的代码的时候一定要小心。使用引用或者指针来传递参数其实还有一个优点,就是能够提高程序的效率,避免拷贝构造函数和析构函数的调用。所以,往往比较常见的是‘const T& ref’这种形式作为形参,T代表C++某种内建类型或者用户自定义类型。最后还有一点要注意的就是,引用在初始化之后就不能改换引用的对象,也就是不能再次引用其他对象,所以,引用在定义的时候就必须被初始化。”

“恩,懂了。今天你教了我好多东西哦!喔…… 时间不早了,该去吃饭了,难道你不饿吗?”

我一看屏幕上的时间,天哪!都已经下午一点多了。此时,我的肚子也很配合的叫了一下。

“看你饿的!那就快点走吧!”Young似乎也饿了,一直在催我。

而我,一直在想,是不是带了这个徒弟以后,我的胃总有一天会出问题啊……
Die von den Nutzern eingestellten Information und Meinungen sind nicht eigene Informationen und Meinungen der DOLC GmbH.
 楼主| 发表于 2003-6-30 17:10 | 显示全部楼层
标题     ZWeily的小品文(四)C++入门教程(3)    zweily(原作)
  
关键字     C++、namespace、std、标准库
  

§3  Namespace std

“师傅,能不能帮我过来看看?”

一听这个声音就知道,我那个“好”徒弟Young又用她那种极有“磁性”的声音在召唤我,一定是遇到了什么问题,搞不定了。

“别叫我师傅,这样感觉我很老一样,还是叫我Weily吧。”我一边说,一边走过去,一看,她开着一个IDE,似乎是在写程序。仔细一看,原来在调试最基本、最著名的那个Hello World啊,呵呵,我鼻梁上的眼镜可不是摆设用的,我也看出问题所在了。

“有什么问题啊?”我故意装作什么都不知道的样子。

“恩……我这个程序无法运行,我也不知道错在哪里,编译不通过,老是报错,而且,我这个程序是完全按照书上打的呀,应该没错的呀!可是就是……”

“喔?让我看看。”我装模作样地凑过去看显示屏,显示屏上的程序是这样的:

#include <iostream>
int main( )
{
    cout << “Hello World!\n”;
    return 0;
}

Young还把一本书拿到我面前,的确,那本书上的程序和屏幕上的一模一样。

“你看,我没打错吧?”Young一脸无辜的样子,难得看到她这样,这个表情看上去楚楚可怜,与平时调皮的Young截然不同。

“你别急,我知道了。先提醒你一点,就是别迷信权威和书本,任何人都有犯错的时候,这是一个优秀的程序员应该具有的素质。这个问题么,让我一点点来讲给你听。你先看看编译器报的错是什么错,记住,一个优秀的程序员能充分利用编译器的查错能力,并且,能从编译器给出的错误信息里判断出错误所在,所以,要学会读懂出错信息,这是我对你的最基本的要求。”

“喔!它报的错误是’cout’: undeclared identifier。这个我以前在学C的时候见过。就是当我的程序里使用了一个没有声明过的变量的时候,就会产生这种错误。”

“完全正确!你已经说出了错误的原因了。”

“啊?可是cout应该是在头文件iostream里面已经引入我这个程序了啊,那么就不应该没有声明过啊?”Young又一次陷入了迷茫。

“其实这个问题牵涉到C++里面的一个特性,就是关于名字空间(namespace)的作用。”

“namespace?……”

“让我们还是从你这个程序看起。cout是一个ostream对象,也就是一个输出流对象。它的确在iostream中定义了。但是,它的全称应该写成std::cout。这个std就是一个namespace,所有标准库里的东西都被封装在std这个namespace里。所以,编译器在这里就不能认出cout,因为它不在全局名字空间里。”

“那是不是我只要把cout改成std::cout就可以了啊?”

“恩,这样改动后就能用了。不过这个问题就像孔乙己的那个茴香豆的茴字的四种写法一样,这个问题的解决方法也有四种解决方法。”

“四种?那另外三种是什么?快告诉我啊!”看来我这个比喻引起了Young的兴趣。

“第一种,就是将std这个名字空间引入全局名字空间。这个方法只要在全局里,也就是任何一个函数之外,当然,最好放放在预处理之后,任何一个函数之前,这样,任何一个函数里都可以直接使用std这个namespace里面的东西,就像使用全局对象一样。例如你这个程序,就能改成这样。”

#include <iostream>
using namespace std;   //引入std这个namespace
int main( )
{
    cout << “Hello World!\n”;
    return 0;
}

“这个程序就能通过编译,而且其他的标准库里的东西,也可以像这里的cout一样,直接使用。”

“喔!那么这个方法不是很方便吗?也不容易出错啊!”

“并不是这样。其实这么做了之后,std这个namespace就失去了它存在的意义了。C++里引入namespace的目的就是为了避免污染全局名字空间,简单地说,就是为了避免和减少命名冲突。一旦一个程序写大了,就很难避免重名,特别是多人合作的情况下。过去C中的解决方法只有靠人为的注意,并且加长名字,以避免重名。这样做会使得一些名字看上去没有意义或者难以理解,而程序员在写程序的时候,也受这个问题的限制,不能自由地命名自己使用的变量或者函数。而有了namespace就不存在这些问题了,这就是C++引入namespace这个概念所带来的便利。当然,这也使得C++里很多名字看上去特别长。”

“喔,原来如此!本来我还准备以后就一直这么用呢!”

“接下来我们讨论第二种解决方案。这个方案和前面一种类似,也是引入名字空间,只是,它只引入需要的一部分。就像这个程序,我们只需要cout这个对象,所以,我们只需将std中的cout引入就可以了。就像这样。”

#include <iostream>
using std::cout;
int main( )
{
    cout << “Hello World!\n”;
    return 0;
}

“喔,原来还可以这样啊!这样就可以避免了全部引入所带来的污染名字空间的问题了。那最后一种呢?”

“最后一种解决方法更简单,但是,我先要你注意,这个方法今天看过就算了,不用去记住它,更不要使用它。先让我们来看看用了这个方案的程序是什么样子的。”

#include <iostream.h>   //注意这个头文件与前面所写的头文件的区别
int main( )
{
    cout << “Hello World!\n”;
    return 0;
}

“这个方法的程序看上去似乎最简短啊。对了,这个头文件的问题也是我一直想问你的问题。我记得原来C里面的那些标准库的头文件都是以.h结尾的啊,为什么C++的标准库的头文件都没有.h啊?”

“你能注意到这点很好,说明你在看书和写程序的时候还在用脑子思考,并且能注意到许多人往往会忽视的细节问题。其实有.h这个后缀的头文件和没有.h的头文件的最大的区别也在于namespace,有.h的头文件里的元素都是暴露在全局中的,也就是没有std这个namespace。而没有.h的头文件中的元素都是被封装在std这个namespace中的。其实C++标准委员会早就去掉了标准库里每个头文件的.h后缀,只是编译器的生产厂商为了向前兼容,为了能让在标准出台之前的程序都能用,于是仍然提供了.h的头文件。记住,这些有.h的头文件都不在C++标准之内,这也是我叮嘱你不要去使用它的原因。还有一点,就是原来C中的那些标准库头文件,其实也存在相应的C++版本的,它们的命名规则很简单,就是以c开头,后面跟上原来的文件名,但是都没有.h这个后缀。并且,其中的元素也都被封装在std中了。”

“喔,原来还有这么一个渊源啊!”

“是的。C++的发展过程中有很多类似的决策和选择,现在标准里定义的那些东西,都是专家们经过深思熟虑后才决定的,其中很多选择和决定都有着它的道理,这个你有兴趣的话可以去看看《The Design and Evolution of C++》这本书,它能告诉你C++为什么是现在这样的。其实因为C++有它的一套设计思想和设计哲学,标准库里的东西在设计上都符合这个思想,并且都保持着高效、安全的原则,所以,在你的程序中尽量地使用标准库已经提供的东西,这样你的代码将更为简短、易读,也更为安全可靠。记住这个原则。”

“恩,我记住了,并且以后会注意的。”

“好了,今天就讲这些吧,关于标准库的其他方面,以后我还会提起的。”

“好的,谢谢师傅!”

“和你说过多少遍了啊,别叫我师傅,我连女朋友都还没有呢,我怕越叫越老的。就像记住我和你说的那些原则一样,记住以后叫我Weily,别叫师傅!”

“遵命!师……”

“!”

“不是啦,我没叫你师傅,我只是说‘是’”Young还是依旧那么调皮。

“好啦,以后注意点。继续写你的程序,看你的书吧。”

“好的,Weily。今天多谢你了!”

“不用客气啦,谁叫Solmyr把你交给了我呢……”

说完,回到自己的位子上,看着桌上那杯已经凉掉了的红茶,哎……
Die von den Nutzern eingestellten Information und Meinungen sind nicht eigene Informationen und Meinungen der DOLC GmbH.
 楼主| 发表于 2003-6-30 17:11 | 显示全部楼层
标题     ZWeily的小品文(五)C++入门教程(4)    zweily(原作)
  
关键字     Overload、Ambiguous、函数重载、默认参数
  


§4  函数重载和默认参数


“:( ……”我的msn上突然弹出了这么一张苦瓜脸,不用看也知道是谁了。

“师傅,过来帮帮忙好吗?”

“不是说过别叫我师傅啊!叫我weily就可以了。怎么了?又有什么问题啊?”

“讲不清楚,还是您……老人家……过来看一下吧 :p”young还故意把“老人家”这三个字与前后文分开,改成了黑体、三号字,还改了颜色,并且加粗了。

还好我只是刚拿起杯子,还没喝水,否则,我的键盘和显示器就要遭殃了。

“生气了?开个玩笑呀,你还是快点过来吧,weily :)”

“我的气量可没那么小,不过下次你在发这种话之前最好先看看我有没有在喝水或者吃什么东西,我怕噎着…… 好了,我马上过来!”按下enter之后,就端着我的茶杯,走了过去。

“怎么了啊?又有什么问题啦?”

“我这边有一些代码不能通过编译,老是报错,可我又不知道为什么错了。”

“嗯,上次我告诉过你,要学会看编译器报的错。”

“我看了,它说:‘ambiguous call to overloaded function’,也就是重载函数的调用有二义性。可是,我没看出有什么二义性啊!”young的样子看上去一脸的无辜和茫然,就像一个被错怪了的孩子。

“喔,原来是二义性问题啊,这个问题的确有时候比较让人头痛的,因为c++有这么一个哲学信仰:它相信潜在的模棱两可状态不是一种错误(注1)。先让我看看你的代码。”

“嗯,就是这里。”young把鼠标指向出错的那行,“我就是想不通,这么一个简单的函数调用怎么会出错。”

我看了看屏幕,只是一个函数调用,就像这样:

func( i );  

“首先我问你,你这个func函数的原型是什么样子的?”

“函数原型?哦,就是函数的声明是吧?在这里。”她按了一下pageup键,然后将函数的声明指给我看。

void func( char );
void func( long );

“嗯,这样啊,那你的i这个变量是什么类型的?”

“是int的。”

“喔,这样就没问题了,我都知道了。”

“嗯?知道什么啦?快说给我听啊!”young那双充满好奇的眼睛盯着我。

“其实,这个是一个常常被人们所忽视的一个模棱两可的状态。你是不是认为,从int转为char,会丢失精度,而转为long不会,所以,你就认为这个函数调用,应该毫无疑问地调用func( long )这个函数?”

“是啊,因为我记得solmyr老大曾经说过,当编译器不知道转成那个类型好的时候,它会报二义性错误。但是在这里,int转为char和int转为long应该很明显的啊,转为char会丢失精度,而转为long应该更好啊。”

“的确,你说的基本上都是对的,而且过去我也是这么认为的。”

“过去?你也是?”

“是的。但是你只要仔细看看tc++pl,里面7.4.3那节就有这样的一个例子的,几乎和你的这段代码一模一样,bjarne在tc++pl里明确地指出,这是一个二义性的函数调用。不过,很遗憾的是,他也没有具体解释为什么这个调用是二义性的。”

“喔,那这个到底是怎么回事呢?”

“你听我慢慢说。首先,在重载解析的时候,编译器是应该按照c++里的一个规则进行函数选择的。一共有五种类型的匹配和转换。最高级别的,就是精确匹配。”

“嗯,这个我知道,就比如我这里如果有一个函数是void func( int )的,那这里的调用就不会有二义性,肯定调用这个以int作为参数的函数。”

“对。但是他还包括数祖名转换成相应的指针类型,函数名转换成相应的函数指针,以及非常量类型转换为对应的常量类型,这些都是很细微的转换,转换后的类型和转换前的是相当的。比如这样,”

void f(const char*);
char p[ ] = “non-const”;
f( p );

“这里就有2个转换,一个是将p这个char的数祖名转换为相对应的char*类型的指针,第二个就是从char*转换为const char*,是非常量转换为常量,这些都属于精确匹配。”

“噢,知道了。”

“第二种级别的,就是类型的提升。其实这个就牵涉到你提出的那个问题的。在类型的提升中,没有从int转换到long的这种提升的。c++里的这个设计,是继承自c的,它就是将运算对象在运算之前,转换到相应的‘自然’的大小。”

“‘自然’大小?这是什么?”

“这个在c里面是非常常见的,例如在c里,char都被转换为int进行处理的。关于char和int的关系,在c里,两者是等价的,可以说在c里没有char这种类型的值,任何char的值都是int的,例如常量’a’,在c里面,它的类型是int的,而不是char,所以很多c的代码里都是从char到int的隐式类型转换。当然,在c++里,还是对两者有所区分的,也就是更匹配的类型存在时,就用更匹配的类型。例如,”

void f( char );
void f( int );

f( ‘a’ );

“噢,我知道,这里就没有二义性,就会调用f( char )这个函数。”young似乎有所领悟了。

“对,但是这样呢?”

void f( int );
void f( unsigned char );

f( ‘a’ );

“这个……”young又陷入了困惑。

“答案就是调用f( int ),这里没有二义性。记住一点,就是那些比较小的整数类型,包括char,在可以保证原有类型精度的情况下,转换为int类型,否则,转换为unsigned int类型(注2)。这就是我前面说的那种‘自然’的转换。关于这种隐式类型转换,你可以去看看tc++pl的c.6,那里有详细地说明。”

“噢,我知道了。”

“其实这些问题的起因,就是c++继承了c里面的一条规则,就是不区分narrowing conversions和widening conversions,所以也就带来了是从int转换为char好,还是int转换为long好这种模棱两可的状态。如果区分了narrowing conversions和widening conversions,那么就要定义隐式的 widening conversions (例如从 int到long) 和显式的 narrowing conversions (例如从int到char就需要一个显式类型转换操作)。但是这种设计是不能接受的,因为像从int到char的这种隐式类型转换太普遍了,如果一定要变成显式类型转换的话,那么将大大的降低c++与c的兼容性,这将使得c++不容易被人们使用和接受,c++的一个设计原则就是不强迫人们去用一种方法完成所需的事情,它会提供选择,让人们可以用自己喜欢的方式,而且c++在最初设计的时候,就要做到原来c能做的事情,那么原来c的代码理所当然的应该能被c++接受。”

“所以c++不区分narrowing conversions和widening conversions,而编译器对于narrowing conversions给出的是警告,而不是错误。是这样子的吧?”

“是的,好的编译器会对这种可能丢失精度的转换给出警告。看来对于这个问题你已经理解了大部分内容了。那么我们再来看看整数和指针的关系。先问你个问题,就是0是什么?”

“0?是一个整数吧。”

“基本正确,更精确地说,0是一个整数字面常量(literal integer constant)。对于0这个整数字面常量,或者值为0的常量表达式,都可以被隐式的转化为任何类型的指针。所以,对于下面这段代码,是二义性的函数重载。”

void func( char* );
void func( int* );

func( 0 );    //二义性函数调用!

“嗯,原来如此,知道了。”

“那么关于函数的默认参数你知道多少?”

“默认参数?哦,这个我已经看过了,好像最关键的一点就是默认参数只能是形式参数列表中的最后的几个。”

“不是好象,是肯定!这点一定要记住。其实,默认参数也和重载有一定的关系。你看看下面这段代码有什么问题。”

void func( int i );
void func( int i, char ch = ‘a’);

int i;
func( i );

“喔!模棱两可,这个函数调用是二义性的!”young的样子大概不亚于当年哥伦布发现了新大陆。

“对!对于多参数的函数重载,编译器在决定调用哪个函数的时候,还是遵循最佳匹配的规则,也就是让尽量多的参数能够得到最好的匹配。前面说过的两条,再加上标准转换、用户自定义的转换和省略号在函数定义中的使用,就构成了五条匹配规则,越前面的级别越高,当然,后面这3条规则中,还牵涉到用户自定义的类的设计问题,这个以后等你学到类和继承的时候,我们再说。这些规则,在tc++pl的7.4中有描述,你应该去看一下的。”

“书啊?我在看。其实,今天我遇到的问题,就是我刚看了一点关于函数重载的东西,但还有一些没搞明白。昨天你不在,我就去问pisces,学函数重载的时候要注意点什么。结果,她就说,我给你一些代码,你去把里面的错误全都找出来,并且搞清楚这些错误的原因,那就能学好了。最后,还丢给我一句话,尽力避免模棱两可的状态!”

“那么刚才那段代码就是她给你的咯?”

“是啊,而且,我搞不定了,去问过她,她也没说出个所以然,就是叫我以后写代码的时候别写出这样的。”

原来又是pisces搞的鬼,自己搞不定,救扔给新来的young,然后再通过young来让我帮忙解决这个问题。如果我没猜错的话,她现在应该就在附近…… 我一转头,果然看到她就在边上扮鬼脸,而且,看样子就是有了收获就想溜走。

“pisces啊,你自己有问题就可以直接来问我或者趣闻solmyr啊,何必欺负人家新来的呢?”

“这个…… 我只是看她现在在研究这方面的东西嘛,而且我看她和你混得挺熟的,所以就……”这家伙还真好意思说。

“好了,这次不和你计较,下次可不能这样哦……”我话还没说完,pisces已经回到自己的位子上了,真是的!

咦?怎么一会儿young也不见了?我用左手的食指和中指推了推鼻梁上的眼镜,然后四处张望了一下。结果就看到young手上端着个杯子,向这边走过来。那个杯子不是我的吗?怎么会在她手里?

“师傅,请喝茶,这可是您老人家最喜欢的红茶,而且还加了蜂蜜的哦!”

红茶加蜂蜜?我没听错吧?我把杯子接了过来,果然闻到了熟悉的茶香,杯子拿在手里,有一种温暖的感觉。

“你怎么知道我这个习惯的?”我觉得有点好奇,她才来不久啊,怎么就知道我喜欢红茶加蜂蜜啊?像pisces来了这么久了,上次还不是给我弄了一杯咖啡。

“这个啊?保密!”young得意地笑着。

“嗯,不错!味道刚好!哎,不对哦!你前面叫我什么来着?我怎么又听到‘师’字,还听到‘老’字,你……”我才反应过来,又被她耍了……

注:1.这个说法来自scott meyers的《effective c++》中的条款26
    2. tc++pl中c.6.1 promotions中有详细的规则描述
Die von den Nutzern eingestellten Information und Meinungen sind nicht eigene Informationen und Meinungen der DOLC GmbH.
发表于 2003-11-23 23:37 | 显示全部楼层

Sie sind sehr hilfreich

Es ist sehr sch&ouml;n, dass ich jetzt sie hier gefunden habe. Gerad such ich etwas über C++ für Anf&auml;nger. Danke sehr!!!

Gibt es noch weiter Fortsetzung?
Die von den Nutzern eingestellten Information und Meinungen sind nicht eigene Informationen und Meinungen der DOLC GmbH.
您需要登录后才可以回帖 登录 | 注册 微信登录

本版积分规则

手机版|Archiver|AGB|Impressum|Datenschutzerklärung|萍聚社区-德国热线-德国实用信息网 |网站地图

GMT+2, 2024-5-21 21:03 , Processed in 0.072321 second(s), 19 queries , MemCached On.

Powered by Discuz! X3.4

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表