来源: www.lqqm.net,作者: lingjie 发布日期:Fri Apr 1 15:23:49 2005
此文是我初学c++时候写的一点东西,很粗浅只能帮助一些人习惯这本书的思维和作
者的风格,别的不敢有任何妄想。到目前只有13篇,将来也许会接着写一些。但不
做任何保证,如果写出来误人子弟或者老调重谈,倒不如不写,对不对?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
前言
实话说,我真的不知道面对这样一本名满天下的书,该如何去读、以怎样的态度去
读、带着怎样的问题去读。按照作者的意见,我尽量以一个对C++一无所知的初学者
的身份来理解这本书(事实上,我本来就是初学者),希望得到较好的效果,为了写
下点滴体会,为了避免每次写的太杂而造成主题不明,我想把笔记篇幅以书中的小
节为单位来写,当然由于我有限的编程学习时间和经验,难免肤浅,大家多多指点
。这也是我写这个笔记的根本目的。
注:本笔记用的是中国电力出版社的《c++ primer》第三版中译本,潘爱民主编的
。有时候会参考原著。
第一章 基础与习惯
引言和1.1节
因为以前的读书习惯,我对这本书的开头抱着一种轻视的态度。自以为是的认为那
不过是类似开场白那样的成篇废话。事实证明我错了。在不到四页的内容里。作者
就一点不给面子的指出了我在读这本书之前的几个错误观点、
第一:对oop的理解,
这里我不得不指出在国内大部分本科教材中把封装性作为oop的独有的特点之一、以
至于让很多人(包括我)认为写了个class就进入了oop的大门,这本书就明确的指出
数据的封装只是对象的特点。是ADT的结果,设计方法的划分中属于基于对象(obj
ect based),没有inheritance机制和dynamic binding(动态绑定)的扩展就根本
谈不上oop。
第二:对c++的认识
作者明确指出c++是一种多种设计方法集于一体的语言。而我们更多的是把他作为一
种oop的语言来学习,把c++理解为带类的c,个人认为这种错误的观点导致的一个严
重的问题是,我们带着c的思想来学习c++,使得我们在应用过程中用的是c++东西。
比如class。I/o流,但是我们思想还是c的,这使得我们难以接受oop以及其他设计
方法;这使得我对认为学习c++,未必要学c的这种观点的真理性更深信不疑、
第三:对编程习惯的认识
在1.1节中作者提出了一个问题。而不是像很多教材千篇一律的以"hello world"开
头。其实,我一直反对学习一种语言一上来就动手的习惯、正如这本书那样,从思
维入手,对于任何问题都以一个工程的态度去面对,在这里作者提出了分而治之和
独步求精的思想。对于一个还没养成坏习惯的初学者来说,这好处实在不可估量。
1.2节
在1.1节中,作者提出一个问题并用分而治之和独步求精的思想给出了解决方案,我
在这里再重复一遍。在学习一个新东西之前。该从它的思想入手,我的建议是:在
读完第二篇之前最好不要动手上机,这样做的好处是你可以避免很多莫名其妙的笨
问题。关于这点,我会在以后的笔记中试图证明。
在本节中,作者又提出了一种重要的东西。渐进式设计方法,我第一次看到这种方
式是在那本伟大的《The C Programming Language》中,这种思想对于我来说实在
是受益匪浅。作者在这里的解释是:"为控制在一个程序中不可避免的错误提供了一
个良好的方法 ,"在我看来。其好处远远不止如此。在学习过程中,这种方法使得
我们更清楚的知道每次增加的新东西的作用,这个好处在本节中得到了很好的体现
。在这个设计过程中,作者教给我们四个方面的东西。最基础的东西。
第一:基本概念
很多人认为,这本书不适合真正意义上的初学者。在今年第二期《程序员》中就提
到:在读这本书之前。你至少要有那么一点编程知识,个人认为这样的说法不是绝
对的。只要你认真的读。深入的理解这部分内容,什么是深入的理解呢?举个例子
:在讲到标识符的时候,作者提出了两个其它的名字。符号变量和对象。有点经验
的人都知道。变量和对象是有区别的,那么当我们写下这样的句子时
int a;
我们是否能肯定a是一个整数变量呢?还是int类的对象呢?当然,我们如果增加点
东西就能判断了。
int a=10;//显然,这是一般变量的用法
int a(10)//这几乎是类对象的方法
除了引言中简单的说明,我们还没得到更多的关于对象的解释。我们还不能肯定。
但是做这样的思考是我们能做的。对初学者要求太高?or yes,但是对这种习惯的
养成是绝对必要。
第二:关于编译器的有关说明
这块我最大的收获是明白了using和。h的关系,很多初学者的讨论就是关于这个。
这也是一上来就动手的弊端,他们根本不明白自己在用什么东西。况且页底的说明
告诉我们这还要看编译器的情况。因此有时候这个是对的,换了个地方就错了。看
了这段,至少节省了你很多问笨问题的时间。接下来是编译器的查错功能。显然这
儿只有两类。这可以告诉了你对于编译出错的候,你要努力的方向。
第三:关于数据类型
这里作者告诉我们两类数据类型,那就是内置的基本数据类型和扩展的基本数据类
型。在读这段的时候。有两个问题使我非常纳闷,第一个问题是:我们那可悲的 本
科教材中内置的少了一个(bool),扩展的完全没有。我把这个问题扔给了我的教
授。可是他的回答使我更纳闷了。他说这些是新东西(他指的是1998年的标准。前
言中有说明)、关于这点,我不方便说什么(我期末的生杀大权在他手里)。我只
想提醒大家现在2004年了。我的另一个问题是为什么没有数组和指针?在我读到2.
1节前这个问题一直留着。我在这里不想说我的理解。我只想给初学者一个建议,第
一篇的东西是有点难度的,我们该学会在读这些东西时,提出自己的问题并且给出
自己认为合理的答案,或者干脆留在脑子里,到你悟了,你会感到无比的。。。恩
。我把它叫做"成就感"吧
第四:控制语句
这块。我说不出什么新东西。可能我已经不算是对编程一无所知的人吧,容我什么
时候找个小朋友,谈谈他的感觉再说吧
1.3节到1.4节
说来惭愧,在学c的时候对于1.3节中所讲的内容就没有好好读过,以至于在相当长
的一段时间里,过得糊里糊涂的,当然,主要的责任在我自己。但是,背了这么大
的罪,有点不甘心。恩,得找点理由。我的理由是:正如大部分朋友那样,学c是从
谭浩强那本书开始的,单文件的习惯简直是深入人心。在大谈特谈之后。他老人家
大概觉得不好意思,书的最后才拿出来亮下,而我这个笨蛋自以为读了那么多,已
经大功告成了,也没放在心上,上面的这些废话,只想说明一点。那就是读好书是
绝对必要的。
在1.3节里,作者以最简单的语言回答了初学者一定会问的笨问题:
第一:关于include指令
很多人都会问。#include<>和#include" "有什么区别?书上的回答很明白,对头文
件查找的方式不同而已。可能我们唯一不满意的是。作者没有告诉我们什么是预定
义的目录,汗,看来作者还是高估了我们这些可怜的人,所谓预定义的目录就是我
们通常说的系统目录。标准库文件所在的地方,再说的直接点。就是你安装编译器
所在的那个目录的某个子目录。什么是当前用户目录?别玩我了,你一定知道的。
除非你没用过操作系统。
第二:关于名字空间和头文件的习惯
c++对c的兼容性是大家都知道的。但是我们会很奇怪的发现当我们根据c++的标准写
法写下如下句子时
#include<stdio>
using namespace std;
编译器很不给面子。作者告诉我们原来c名字和c++名字是不同的。也就是说在名字
空间std111111111里根本就没有stdio这个东西。所有的c名字在c++名字中都得加个
c前缀,在也证明了我在上篇笔记里说的。一上来就动手上机的一大弊端就是我们根
本不知道自己在用什么。出了错当然也就莫名其妙了,
第三:关于某些识别符
由于c++对c的兼容,我们也许会问:我怎么知道那个是c文件,那个是c++文件呢?
看扩展名?这是个主意,但是,有个不幸的消息。作者已经多次告诉我们在不同的
系统里文件的扩展名是不同的,作者告诉我们一个很好的方法:用宏,c++有个"__
cplusplus"c有个"__STDC__"这两个名字不会同时存在。ok,这样的话,我们#ifnd
ef或者#ifdef就可以了,另外,作者还告诉我们四个宏,对初学者来说几呼没什么
用?呵呵,多懂点也是好的,这也是作者对我们的信任,不像某些教材,,,,唉
,不提也罢,什么是初学者的东西。什么不是。谁有这个权力判断呢?除了你自己
,教育者的任务该是把完整的东西拿来看,取舍是学生的事情,不是吗?
除了上面这些知识外,作者还告诉我们该养成那些好习惯。比如写头文件时,该写
成如下形式:
#ifndef FILENAME_H
#define FILENAME_H
//文件主体,
#endif
以便当文件包涵层次多了犯糊涂,另外这里还有个暗示:鼓励写多文件程序,这样
可以避免一个文件过长难以读懂,我当年就写过一个长达1100行的文件。修改时,
我自己都晕了。还有个好处是,当我们学习I/O时文件操作就成可以理解的东西了,
尽管两者关系不大。但是这是一种思维习惯,如果你只写单文件的东西。你怎么理
解文件外的操作呢?
第四:注释
还有一种习惯就是注释,我把它并到这里来写,是因为它简单,但是简单并不意味
着不重要,相反,它很重要。无论在任何时候我们都不要忘记,人类能成为整个世
界的主宰,是因为我们的合作能力超过了任何一种生物(不明生物除外)。而注释
正是为你以后的合作提供方便,因为你的设计灵感是很容易忘记的,到时候,连自
己都不知道。这段代码干嘛的?
1.5节
本节的内容不是很难,学过一点编程的人都会很容易接受,但是,我一直以来就有
一种感觉。自己用的最多但又用的最糊涂的也就是i/o系统,很明显i/o实现了对计
算机设备的直接操作。在c++里我们知道它是通过ios类的对象来完成的。从感觉上
讲,远远不如c来的直接,明白。当然这样做的好处是大大简单了我们的操作,但是
不幸的是这会给我们的理解造成一定的困难,下面我将提出自己疑问。
第一:关于预定义io对象的实现
大家都知道,我们的io操作是通过三个对象来完成的。cin、cout、cerr。作者在描
述的时候用了"绑定"这个词,很明显被抽象的数据类型和具体设备实现了完美的对
接。但是我的问题是,从ADT的思路来说,这些对象封装了那许数据?提供那些操作
?这些数据和接口是如何对设备进行控制的?很显然,以我们现在的知识很难回答
这些问题。就算是自以为合理的估计也不能,但是这样的思考,有助于我们对类设
计思维的熟悉,但是思考到此为止罢。不然我就误人子弟了。
第二:cout和cerr的区别
还有一个问题,初学者也许会问,作者告诉我们cout和cerr都可以用来输出。他们
的区别一个是一般的输出,一个是输出错误信息。可是这样的解释并不能使我们这
些好奇的人满意。于是我做了个恶作剧,改写了我们那个知名的"hello world!"
int main() {
cerr<<"hello world!"<<endl;
return 1;
}
事实证明 用户如果不看代码。他们不会知道自己看到的是个err。那么这两
个对象的区别只是个习惯或约定吗?我们同样不能解释。把它留着罢,相信总会"悟
"的
第三, 我想谈谈那个"endl",在很多教材里把它等价于"/n"正确吗?no,你可以写
下如下句子。
cout<<"aaa";
a=getchar();//.假设a已经定义
cout<<a;
在VC6中你会发现输入输出的顺序乱了(刚才在VC++.NET 2003又恢复正常了、奇怪。
因此不建议在c++中用c的东西),为什么呢?作者告诉我们endl除了/n的功能外还有
个刷新的功能,刷新什么?看仔细点,刷新缓冲区。什么是缓冲区?哦。这个问题
也许你需要一些别的知识,比如。《计算机组成原理》
关于文件的操作,与单文件非常类似,唯一要注意的是流进出的参照物、内存?or
文件?
一点提醒:
我还是要提醒大家。我提出了很多牛角尖的问题。只是对初学者的思考方法提出一
些个人建议。对于这些问题本身有些并没有讨论价值。有些不是现在该解决的问题
。在这节里你的任务其实很简单,搞清每个对象和运算符的作用。就是这样,c++之
父把c++说成难学易用的语言,我们可以理解这句话的涵义。要不断的想起这句话,
2.1节到2.2节
很多人都会觉得第二章很难,作者在第一篇的引言中也提到:如果读者觉得第二章
的内容难以理解,就跳过他。而个人认为这样做不是最好的选择,事实上,第一篇
的目的在于对这语言有个很好的整体的理解,思维上的习惯,方法论的形成才是本
章的重点,前面这些废话,是对我们将要面对的难度以及我们要采取的态度作一个
简单的说明和建议,
我们在第一章中学的那些东西,都是比较现成的、可以直接使用的、在用的过程中
不太会受到语法上的限制的东西,我想大家还记得我在读到数据类型的时候,有过
一个疑问:为什么那个时候作者不说数组?在2.1节里,作者给出了个答案:数组不
属于基本型,什么是基本类型呢?作者的解释是:语言本身对该类型的赋值、一般
算术运算和关系运算提供内置支持。显然数组和指针都不能满足这个条件,按作者
的原话:他们不是一等公民,
关于这两节的知识点。我想要说的并不多,对于学过一点编程的人来说数组和指针
不是什么陌生的东西。在这本书的前言里作者就说过"这本书适合c++的初学者,但
不是适合编程的初学者",但是这里仍然有两个很多初学者会犯的低级错误:
数组方面:我来举个例子:
int a[8];.
a[8]=10;
这两个8几乎完全相同。事实是:在意义、作用上都完全是两回事,前者是size,后
者是下标、这里我不得不产生一个疑问:[]运算符到底是干嘛的?在这里我们几乎第
一次看到了一符多用的例子(也许不是,姑且这样认为)。
指针方面。我见过很多类似这样的用法:
int *p = NULL;
cout << *p << end ;
显然,一个合格的程序员不会写这样的代码的、但是要不是一个朋友的提醒。我也
就自以为是的认为它只能是个笨蛋的错误,这里有两个问题:NULL是什么?我以前
很天真的把它和ASCII联系起来,事实上。它就是0。为一能的赋予p的值。接下来就
是空间分配的问题了,我们都知道NULL的作用是不让指针指向任何对程序有用的空
间。那么0放那呢?显然这儿有个关于空间的误解,那就是指针的空间和指针指向的
空间是不同的。指针的空间是定义时已经分配了,但它只能放内存地址和NULL(p)
, 指向的空间是容器里的东西(*p),显然,上面的*p事实上并不存在。取一个不
存在的东西,当然是不能不出毛病的。
作者在这里很讲到new。一直以来我对new总是有点恐惧感,他远远不如c的函数来的
明白,作者告诉我们它是动态分配内存的运算符。可是请注意作者对于动态分配的
解释:"运行时刻库函数的行为"。库函数?no运算符?这里我们几乎感觉到了,函
数和运算符之间存在着某种联系。再深入点思考,想想我们在开头提到的关于数组
的缺点,数组不能进行某些运算?well,如果我们的假设成立,函数和运算符存在
联系,那么我们自己是不是可以完善下我们的数组呢?事实证明我们是正确的。这
就引出了ADT的思想-类的设计(后面的内容就是个不断设计完善的过程)。可见我们
以前的学习方法存在一个大问题,我们不知道自己为什么要学一样东西,作者已经
暗示我们这样的答案。需求引发灵感,灵感带动设计,设计成就技术,技术造成学
习。学习满足需求
2.3节
早在2.1里作者就留给我们一个问题:要成为c++的一等公民需要那些条件(练习2.
2)?个人认为这个问题非常重要。它直接关系到我们对类的设计思想和努力的方向
,显然我们的方向该是:尽可能的让我们设计的类成为语言的一等公民,换句话说
,在类的设计中,对于运算符的支持要比一般的成员函数更重要,这样做的另一个
考虑是为我们以后学习STL提供思维的延续,可见。如果我们只把c++当成oo的语言
,是不会这样想的 ,
对于初学者来说,想在本节完全学会很好类的代码实现,几乎是不现实的,在本节
的内容里,我们的任务是理解设计思路、了解类的基本结构,学会用伪代码设计一
个能满足需求的class,下面是我在本节中所理解的东西,
第一步:明确设计需求
我们可以仔细看一下作者写出的功能表,显然,就功能而言,它并不完善,但能满
足一方的需求,这里我们可以感觉一下作者的专业品质:以用户需求为目标,我们
的软件常常有这样问题,几百mb的程序,事实上我们真正用到的可能不足百分之一
,成本和资源都得到很大的浪费,好像只满足了设计者的虚荣心,
第二步:确定要封装的数据
当然,我们先要明白的是:封装的目的是为了保证对象数据的相对安全,要通分理
解封装的涵义,比如:
class a {
int i
public:
….};
class b {
a s;
public:
….}
几天前就有人问我这样的问题。,b是否可以操作s中的数据,答案是:yes。但是如
果这样问。。s中的数据在b是否可用,答案则是,no,原因在于对于封装的数据而
言,只有通过s自身的方法才有权使用。那么b如何操作s的数据呢?这就引出了第三
步.
第三步,设计公有接口
个人觉得接口和方法是有区别的,接口包括运算符和方法,当然他们都靠函数完成
。但对函数的实现目标有着明显的不同,运算符考虑的是对象之间的关系。而方法
考虑的是对象本身属性的操作,对于设计一个数据类型来说显然前者更重要,对于
内置的数组来说。我们并没有考虑其本身的东西。但是,谁能说他不算class呢?不
过不是我们设计的罢了,
接下来,谈谈一些习惯问题,可能先看国内教材的人都会发现。在本书中public写
在private之前。看起来好像没有多大的区别,其实我们应该发现,如果私有成员写
前面关键字private可以不写,这不利于代码的可读性,我们要时刻为自己以后的合
作提供良好习惯和代码风格。这样也该是程序员的基本素质之一吧?
2.4节
在上一节中作者带着我们初步建立一个class。正如作者自己所说:它能满足一些用
户的需要,但是我们都知道我们的世界是多元的。当然需求也就会分不同的层次,
但是生产的各类资源都极其有限,因此对工作效率的追求就成了我们这个时代技术
发展的主要目标。
对于编程工作来说。效率主要来自代码的重用性。因为计算机硬件技术的发展成果
,可以让程序员合理的忽视一些代码执行效率的考虑(并非完全忽视)。对于代码
的重用性,根据作者的描述,我想把他理解为两个方面的努力,以方法(函数)的
调用来划分。
在调用者这一端:
我们希望写下的一句代码,由计算机自己决定其代码的具体实现方法。正如书上所
写的,当我们写下:
int x = max( a, b );
我们希望计算机自己根据a,b的数据类型来决定具体该调用那个max()实例。在就是
重载。(其实这是上节的内容。但我自己觉得把它放到这里来总结更有利理解其作
用和本质)
在被调用的一端:
我们一方面希望自己的代码能被不同的程序调用,另一方面我们也希望自己代码能
根据被调用端的具体情况做出相应的反应,这两个方面都造就了伟大的技术。前者
导致了oo理论的诞生,后者引出了泛型设计思想。
终于看到了oo,呵呵。在高兴之余,发现oo并没有想象的那么复杂,那么神秘。是
他本身就不难?no,不是这样的,这完全归功于我们伟大的作者,他让我们从一开
始就走对了路,从技术产生的根源出发。更容易理技术的本质涵义。我们会惊讶的
发现原来如此,正如下面的例子:
1. 同类的不同对象方法具体实现细节不同?ok,虚函数应运而生。
2. 想简单的加入几项功能形成另一相关的class?ok,继承机制就是为了这个。
3. 如果还想新类博采各类之长。多继承。
可以看到,在这种思想下,很多难以理解的东西。就变的简单多了。
注意:
在方便了编程方式的同时我们不要忘记这些方便都是,放弃运行效率为代价的。有
时候我们不得不回过头来,考虑一下这些麻烦的问题。作者也提供了一些思考的方
向,如。inline技术等等,但是正如上面所说,这些不是最重要的,
最后,我想说明下。从2.3节开始一直到本章结束,这里所有的内容都是值得玩味的
。每次重读都会有不同的感受。 强烈建议在以后的学习中要不断的回头读这些内容
,你会发现这样做的必要性,正如我在这里回避了代码细节级的技术。
2.4节
随着这本书的深入,越来越发现自己对c++的理解是多么的浅薄,现在的我对于这些
笔记真是诚惶诚恐,要知道这些自以为是的理解也许在今后的某个日子里信手翻阅
,恐怕自己都会问。这些废话是那个笨蛋写的?简直一无是处,但是,我们的成熟
正是来自这些一无是处的错误,作为初学者的我,希望在这里留下学习的思维旅程
,不管自己的水平如何。这本笔记我还是会坚持,
之所以说上面那些感言,是因为自己觉得到了本节,作者将带领我们超越我们还有
待深入理解的oo,关于泛型的讨论在各个技术社区随处可见,对于初学者来说,很
少能看得懂这是在说什么?这无疑是个神秘的,令人恐惧的领域。然而,正如上节
一样。跟着作者的思路我们不难理解泛型设计给程序员带来的好处,在读完作者行
云流水般的讲述之后,回过头来看看自己真的理解了那些东西?结果是令人遗憾的
,在读到第六章之前,我对这块总是疑问不断,主要如下:
一些疑问
什么是泛型?
只是模板吗?至少在本节范围内看来几乎是这么回事,为了提高代码的通用性,将
数据类型参数化,让该代码的用户(本身也是编程者)决定类实例的数据类型,就
这么简单?就从模板设计后对编译速度的影响,我们就可以肯定显然不是这么简单
,以我们现在的水平无法真正回答这个问题,但是我们必须清楚的是:泛型设计不
等于模板设计和简单的实例化类和函数。而模板一定是泛型设计的基础。
泛型设计是完全一种高于oop的设计思想吗?
我的感觉好像不是,从过程到基于对象到面向对象的努力方向几乎都是数据处理的
相对独立性,考虑的是数据的相对安全,而我所理解的泛型设计(主要是模板的用
法),几乎只是为了代码的通用性,当然更大可能是我对泛型的理解本身是个错误
,可见在第一个问题没解决之前。这个问题无从谈起,但是对于这节内容来说,带
着几个笨问题离开,总比自以为是的态度更有价值。
应当理解的内容
当然,对于本节的内容,上面的那些牛角尖的事情不是完全必要做的。我们要掌握
的是模板最基本的用法,以及模板在实例化过程中的一些编译期的特点,总结要点
如下。
模板是对数据类型的参数化:
说得非专业点,就是拿数据类型作"变量",只不过在使用时,这些"变量"的值是类
型而不是数据。比如:
tenplate<class xxx> class A {…….};
int main() {
A<int> obj;
Return 1 ;
}
上面的xxx就是我所谓的"变量",理论上可以是你喜欢的任何标识符(当然
它得符合标识符的基本要求)。
模板实例化的过程:
关于这些,书中讲的非常详细,我唯一能说的是,要仔细看,特别是对p42的内容(
以第三版中文版为准)。
对于oo的模板支持,
在理解前面的内容的基础上,这几乎没什么难度,值得看看的是作者的代码。恐怕
在国内教材里没有什么地方能找到如此优雅,聪明的代码了。这本书中代码哪怕只
是抄一遍,也是好处不可估量的。呵呵。
笔记范围:2.6节
每个程序员都知道,在程序运行过程中,一些情况是不可预料的,无论程序的设计
看起来是多么完善,在某个特定的环境里同样会出错,但是这样的错误往往会有些
共同之处,比如new的时候也许会空间不足。显然这样的错误。不能完全说是程序设
计的问题,但是如果你的程序能对这些情况做出反应,那么这样的程序实用性就会
更强。异常机制正是为此而生。
注意,这里用了"机制"这个词,也就意味者规则,正如本节开头所说的:对于异常
各人都有自己的处理方式。比如就用if语句处理。本来这是没什么问题的,但是我
们生活在一个不幸的时代,程序的规模大得远远超出了个人能力能够接受的程度。
我们需要合作,因此统一的编码风格成了必要的事情了,这也是为什么程序员经常
告诉初学者编程是最不具备个性化的东西之一的一个原因,
使用异常机制还有一个好处。他可以大大减少代码的长度和规模。对于相同的异常
情况可以集中统一处理。这样说很空泛,我们来举一个小例子.,假设我们写个函数
分别为,int, char 的指针分配相同单位的空间,如不要异常机制,这样写:
bool new_space( int *&pev,char *&ch,int size ) {
pev = new int[ size ];
ch = new char[ size ];
if( !pev )
return false;
if( !ch )
retuen false;
return true;
}
这样的东西恐怕没人喜欢,因为如果我们这样调用他,new_space( p, c, 6 ),无
论空间分配成功与否。调用者都不知道( 问:你不会定义个bool量吗??答:会。
烦。我用别的 ),恩,得让我们的东西人性化点。修改如下:
bool new_space( int *&pev,char *&ch,int size ){
pev = new int[ size ];
ch = new char[ size ];
if( !pev ) {
cerr << "err1" <<endl;//在帮助系统里把err1说明成空间不足
return false;
}
if( !ch ) {
cerr << "err1" <<endl;
retuen false;
}
return true;
}
呵呵,这下对得起可爱的用户了吧?可是发现有点对不起自己。cerr << "err1" <
<endl; retuen false; 我写了两遍,这是两个指针,如果十个呢?天哪。老板,
加工资吧(回答你们一定知道),那好,试试异常机制,再改。
bool new_space( int *&pev,char *&ch,int size ){
try
{
string word ( "err1" );
pev = new int[ size ];
ch = new char[ size ];
if( !pev )
throw word;
if( !ch )
throw word;
}
catch( string err )
{
cerr << err << endl;
return false;
}
return true;
}
呵呵,这下比较舒服了。大家可能看到上面那函数好像并没有减少什么长度,yes,
但如果处理的参数不只两个,很多,你就会发现他省了你很多力气,
上面主要想说明异常机制的好处和用法(水平有限,可能例子并不好)在本节中
还有个重要的内容,那就是程序处理异常的方式和顺序。比如上面的程序,我们想
让它在当分配成功时显示ok,那么cout << "ok" << endl;写在那儿呢?如果我们
的try抛出的异常在函数里没有相应的catch怎么办呢?这些书上都说很详细。仔细
看吧,一定收获不少。
笔记范围:2.7节
对于大多数人而言,学习编程的第一步就是模仿,说的直接点就是从教材上抄几段
代码(比如那个知名的hello world)到机器上,然后慢慢的习惯,不错,这是一个
学习语言的好方法。但是这里有个问题,人们对于已经成为习惯的东西往往不会给
以思考,导致很长时间以后对于自己几乎每天都在写的句子却不知道它的具体涵义
,举个例子:只要你用的是c++,那么相信你每次都少不了写这样的句子:"using
namespace std;",很多第一次见到c++源程序的初学者多数会问:what is it?而
真正学了一段时间的人对于这个问题却没感觉了,然而真正该思考这个问题的却是
后者,这不能不说是这个学习方法的一个大弊端,这也是我一直坚持先搞清基本概
念再动手的原因。
正如我在上篇笔记中所说的:为了合作的需要使得编程成为了这个世界上最不具
备个性化的行为之一,因此程序员在命名组件的时候选择的名字往往都差不多。而
全局名字又不可避免,那么名字污染的概率也就非常大,理解了这些,相信对于na
mespac的概念的理解应该不是问题了,无非是改变了名字的可视性,当然,这不是
我们最关心的,我们更想知道的是如何用好它。这样就要解决下面两个个问题。
第一:如何定义?
书上给出一个比较明确的回答,形如这样:
namespace owl {
int x;
char y;
class obj { ….. };
void max( const int* );
}
但是这样的回答,我们并不是很满意,比如,我们都知道,为了减少编译时间和避
免重复函数定义。往往把函数的声明放在头文件中(abc.h),而把实现放在相应的
源文件中(abc.cpp)。那么名字空间该如何定义呢?难道一个"{"在abc.h,一个"
}"在abc,.cpp中吗?如果真的能这样,多个头文件又该如何呢?显然这是不可能的
,这个我们可以到8.5节中得到解释。现在我们只要提出这样的问题,并把它留在脑
子里,不要让这个概念成为麻木的习惯,
第二:怎么用它?
本节给出了三种用法,下面我们通过那个hello world 程序,作一个简单的比较。
1 using提示符:
#include
#include
using namespace std;
int main()
{
cout << "hello world!" << endl;
system( "pause" );
return 0;
}
呵呵。很舒服?是吧?是的。大多数人是这个用法,但是正如书上所说,这个做法
让名字空间形同虚设,我就遇到过这样的事情。有一次我写main()所在文件的时候
忘了写using提示符,编译器居然给我通过了。当时真的吓了一跳的。后来才发现我
include的文件已经写了。可见这个写法的弊端,万一我在不想用此名字空间的时候
include了该文件,后果是有点麻烦的,当然对于std来说,这样的忧虑有些多余了
,因为没人会去和ISO抢名字(恩,某些特殊人士除外)
2. 名字空间修饰符
#include
#include
int main()
{
std::cout << "hello world!" << std::endl;
system( "pause" );
return 0;
}
这个写法好是好。就是太烦,每次都得写std ,程序越长写的越多,老板又不加工
资,咱不干。(什么?设别名?namespac A = std; ? 你干吧。咱还是不干)
3 using声明。
#include
#include
using std::cout;
using std::endl;
int main()
{
cout << "hello world!" << endl;
system("pause");
return 0;
}
恩。只声明要用的,这样不错。尽管多写了些,程序越长写的越值,呵呵。等等,
这样的话。如果声明在头文件中,岂不是又回到第一个问题上去了,可以再改下,
#include
#include
int main()
{
using std::cout;
using std::endl;
cout << "hello world!" << endl;
system("pause");
return 0;
}
啊哈,妙极了,using 可以进一步改变名字的域,这个时候你肯定会问,using提示
符可以这样用吗? 如果能,岂不更省事?很遗憾,不能的,你在8.5节中会找到答案
,
2.8节
从本章开始,我们就一直沿着一条主线前进,跟着作者设计了一个比较完整的arra
y类,从设计的角度来说,(当然除了库的实现者外)我们没有必要花那么多时间在
实现数据结构上,只要调用各种库,我们就可以把自己精力放在更重要的设计上了
,在今天,对于库的使用,几乎成了程序设计能力的重要指标之一,作为初学者,
首先要学的当然是已经和语言合为一体的标准库了,而在所有的数据结构中,用的
最多的无疑是线性表了。本书的标准库描述当然也从这里开始,
本节的内容这章来说算是最简单的了,vector的一般用法和内置数组几乎没什么差
别,初学者很容易理解,当然个人认为。如果仔细看的话还是有些东西值得注意和
思考的,下面将其列出:
一.标准库的设计理念。
正如作者提到的:vector并不是像前几节中设计array那样提供一个大而全的方法集
合,而是提供了最小的接口集合,把那些具有通用性的方法单独组成泛型算法。可
以使用在各种数据结构上,这个思想正好反映了作者在本章开头提出的问题,设计
一个类,是要尽量让他成为语言的一等公民,而不是设计一个面面俱到的东西。
二.对于iterator的初步认识,
这个也许是本节唯一的难点了,迭代器?很多初学者对这个陌生的术语不理解。但
他几乎是这个泛型算法的基础,因此我们现在至少要对于有些初步的认识,以保证
下面的阅读,为了达到这个目的,我们仔细的看了下书中的例子、不难发现他与指
针的用法几乎完全相同。因此事实上,你就可以把他当成指针的一种类模板,当你
读到十五章重载->的时候你就可以理解这样做的好处了,另外还有一些特殊的iter
ator,比如begin().end().书上都有详细的说明,仔细看看,我的建议是不求全解
,但起码得会模仿,
一些题话。
本章结束了,我们对于c++也有了一个比较整体的理解了,下面我想对于一些朋友对
我的笔记的一些建议,作一个简单的回答,到现在为止我写的东西,都是一些人人
都知道的东西,作为真正的笔记是罗索了点,正如这两章的目的想让初学者有个语
言的整体理解一样,我的这些笔记的根本目的也是尽可能的向各位初学c++的朋友推
荐这本好书,和让大家习惯作者的描述方式的思路,因此我评论了些书以外的东西
,对比下,以便更好的推荐这本书。作为学生,我对于一切教育工作者(比如谭浩
强老师)都是尊敬的。但是这本书的确比他们写的好些,
从下章开始,让笔记回归本来的面貌把。我不再严格按照小节来写他了。毕竟这本
书我已经看完了。我将根据具体内容。把整本书中前后提到的相关内容结合起来,
把我自己完整的理解写出来。当然这样做会暴露出更多的问题。希望大家给我更多
的指点,谢谢大家的支持,尤其饮水思源和水木清华及复旦光华站的各位朋友
第二章 一些基本概念
关于指针的一些讨论
距离本笔记的上一篇已经过了很长时间了,很多人问我为什么不写了,其实理由很简
单,我写不出什么东西,正如上篇笔记所说的在这之前的东西只是为了向朋友们推
荐一本好书以及帮助初学者熟悉作者的思考习惯,这个很容易,但也很肤浅,这样
的东西是不合适写的太多的(地球人都知道的东西,还是看书为好)。这就意味着
必须选择话题来写(当然也大大增加了笔者犯错的机会)。上面这些话算是对一直
支持我的朋友们做个解释,也希望再得到你们的支持。
在本篇文字中,我选择的话题是指针,是的,我已经听到很多人开始抱怨这个麻烦
的该死的东西,但是我不得不说离开了这个东西(那个麻烦的该死的)你很难干得
成什么事情,对于c/c++程序员来说这是个不可能回避的问题,那么我们最好还是看
看这个东西到底麻烦在那儿,这样比抱怨更能解决问题。
第一,指针的定义和初始化:
在本书的3.3节在这方面作了详细的描述,很简单,指针是一种间接操作对象的方式
,指针中存放的是所操作对象在内存中的地址,但请注意,如果指针表示的仅仅是
地址的话,事情就好办得多,但是指针还必须与他表示的对象的类型保持一致,就
象本书p73的例子:
int *p = NULL;"
double dval = 3.14;
p = &dval; // error
不是p物理上不能存放dval的地址,而是两种类型的内存布局和内容的解释完全不同
(对编译器而言),编译器只好从一开始就拒绝这种赋值形式,当然,从你开始看
上面的代码的那一刻,我就知道你不以为然了,一个学过编程的人是不会犯这种低
级错误的,真的是这样吗?那么好,我想有时候你会希望对指针直接赋个地址,你
是否会这样写?
int *p = 0x00001010;
然后,编译器就开始骂人了,"0x00001010是那个对象的地址?什么类型?一定是i
nt吗?你让我咋解释他呢?重写!"呵呵,也难怪他脾气这么大,你这个地址根本就
没有告诉编译器类型信息嘛,当然如果你非要这样做,要么告诉他你需要这个地址
的类型:
int *p = (int*)0x00001010;
要么干脆先不管类型的事情:"
void *p = 0x00001010;
但不管你选那个方式,cast总是免不了,建议少用。
第二,野指针:
当指针赋值的时候,编译器会检查地址的类型信息,这很好,但赋值之后他就不管
了,这个正是一切麻烦的根源,比如有个典型的代码:
#include <iostream>
#include <cstdlib>
using namespace std;
int*f()
{
int a = 0;
return &a;
}
int g()
{
double a;
cin >> a;
return 0;
}
int main()
{
int *p = f();
cout << *p << endl;
g();
cout << *p << endl;
system("pause");
return 0;
}
我们发现编译器没有报错,而p中存放的地址也没被改变,但两次提领的结果完全不
同,这是因为一旦函数f结束后,局部对象a进入了栈粉碎机,但是,请注意,栈粉
碎机除去的不是该对象的地址编号和数据。而是该对象地址的类型标志(当然这是
编译器行为,和纯内存无关)。而指针则仍然按照之前的约定对该对象地址进行提
领等操作,一旦该对象地址被改变了类型信息,就必然错误。一个指针指向的对象
的类型信息被改变或者被消除,从而使得指针操作的结果不可预测,就成为野指针
。
第三,内存泄漏:
这个问题跟堆上分配有直接的关系,首先,我们要明白什么是内存泄漏,下面这个
函数f内存泄漏了吗?
int*f()
{
int *p = new(int);
return p;
}
int main()
{
int *p = f();
*p = 11;
cout << *p << endl;
system("pause");
delete p;
return 0;
}
我们还是引用《effective c++》里的一段话来回答这个问题:
"引起内存泄露的原因在于内存分配后指向内存的指针丢失了。如果没有垃圾处理或
其他语言之外的机制,这些内存就不会被收回。"
显然我们没有丢掉控制该地址的指针,被分配的内存仍掌握在我们手里。只不过要
相当注意罢了。特别是当有异常出现的时候,不要忘记在程序退出之前delete这个
内存。(当然有时候auto_ptr也是个不错的选择)