首页 > 技术文章 > C++ one more time

ariel-dreamland 2018-04-26 16:35 原文

写在前面:我们学习程序设计的方法先是模仿,然后举一反三。在自己的知识面还没有铺开到足够解决本领域的问题时,不要将精力过分集中于对全局无足轻重的地方!!!

以下参考钱能老师的《C++程序设计教程 第二版》。

                                                     第一部分  基础编程

Chapter 1 概述(Introduction)

学习程序设计,首先要搞清楚程序开发的过程,否则,无法以成功的运行来验证编程技能的提高。

用编程语言编写完了,之后就要翻译成机器代码,以便让计算机运行获得结果。

 

翻译的方式一般有两种:

一种是解释型,也就是边读程序边翻译,翻译成机器代码后就执行。

另一种是编译型,也就是先整篇翻译成机器代码,保存在可执行程序文件中,然后启动该程序文件,运行获得结果。

C++语言的程序因为体现高性能,所以都是编译型的。

 -

一般的编程操作流程为:编辑(edit)---编译(compile)---链接(link/make/build)---调试(debug),该过程循环往复,直至完成。

 

下面这段话相信当初不少人都不会怎么在意^_^:

程序员编辑的程序,也称源程序,或称源代码(source code),简称代码(code),存放在文本形式的以.cpp(在Windows环境中)作为文件扩展名的文件中。

在比较少的情形下,机器指令集代码也称源代码。程序被编译(compile)后,会生成目标代码(object code),存放在目标文件中,在Windows中的C++编译器通常将目标文件以.obj作为文件扩展名。

目标代码即机器代码,是计算机能够识别的指令集合。但是,目标指令(也称目标代码)还不能在具体的计算机上运行,因为目标代码只是一个个独立的程序段,程序段之间还没有彼此呼应,程序段中用到的C++库代码和其他资源还没有挂上,需要相互衔接成适应一定操作系统环境的可执行程序整体。为了把成组的程序段转换为可执行程序,必须进行链接(link),链接的过程就是将目标代码整合(或称转换)成可执行文件,可执行文件通常以.exe为文件扩展名。

 

C++程序在编译后,通过同时链接若干个目标文件与若干个库文件而创建可执行程序。库文件是系统提供的程序链接资源。标准C++提供C++标准库,用户库是由软件开发商或程序员提供的。目标文件与库文件链接的结果,是生成计算机可执行的程序。

一个著名的公式:

程序=算法+数据结构   (非常不陌生O(∩_∩)O~)

。。。后来,更加直截了当而又具体的编程模式变成了:

程序=算法+抽象数据类型

 

过程化程序设计:

将复杂的过程简单地按功能分层从而达到解决问题的目的,这种思想就是过程化程序设计的思想。过程化程序设计以一系列过程的划分和组织来观察、分析和解决问题。

结构化程序设计:

描述任何实体的操作序列只需用“顺序、选择、循环”这三种基本控制结构,而且这三种基本结构对描述任何问题都是足够的。

程序设计采用“从上到下、逐步细分”的方法展开,即过程化的程序设计方法。

面向对象的程序设计!

C++入门容易,但要做个真正的C++编程编程高手却很难。因此至少一定要有坚持不懈的精神和源源不断的主观能动性。

学习编程,在构造了起码的程序框架后,就要开始涉及算法,而算法又涉及数据结构。因而,学习编程,首先是要学习简单的算法和数据结构。接下来我们就这样一步步地展开。

读书是要有方法的,读书与实验应该是结伴而行的,必须要有这样的心理准备并确实的付诸实施。学钢琴光看乐谱是没有用的,看一点就要弹一弹,听听自己弹的音准不准,渐渐地步入门径……

 

Chapter 2 基本编程语句(Basic Programming Statements)

C++的基本编程语句有说明语句、赋值语句、表达式语句和过程控制语句。

过程控制语句又分条件语句、循环语句和转移语句。

转移语句(move statement):

循环中的continue并不是必须的,更多的情况是为了表示逻辑上的清晰性和语句的优美性。

continue语句和break语句的区别是:continue语句只结束本次循环的执行,而不是终止整个循环,而break语句则是结束整个循环,不在进行循环条件判断。二者差别如下图所示:

goto语句:

goto语句是低级语言的表征,它很灵活,灵活到不受任何拘束,可在函数体内直来直往。但是,现代程序设计不能容忍它在过程中任意穿梭而破坏过程体的结构。没有goto语句,过程体结构更清晰,程序更易读。

在C++中还有一个地方还有使用goto的价值:当从多重循环深处直接跳转到外围循环之外时,如果用break,就只能一重一重地退,而且还要边退边做记号,若用goto则显得更为直观。

conclusion

一个完整的程序通常有两部分:说明部分、过程部分。过程部分即为操作和计算语句,这些操作计算语句所要用到的数据类型、变量、对象和函数都在说明语句中说明。说明语句一般包括变量和对象的定义和函数声明以及定义,也包括这里没有涉及的类型声明和定义,在初级编程阶段,读者应需要什么,就说明什么,没有更多的“预谋”;随着学习的深入,说明部分将体现程序的架构,会变得越来越重要。在程序规模扩大之后,还要始终保持过程部分的清晰和简明,就必须让过程语句更抽象,因而就得让说明部分做更多的事情。这就是编程方法不断进化的方向。

循环是程序设计入门中最重要的内容之一了,用的最多结构是for语句,因为它能描述循环体的初始和结束状态,以及中间步长。

 

Chapter 3 数据类型(Data Type)

二进制补码

通常的计算机语言在计算机内部都是以二进制补码的形式表示整数的。因为二进制补码用来表示整数具有高度的一致性,并且统一了加减法的意义,简化了乘除法运算,甚至直接简化了计算机的硬件结构。

编译器与整型长度

C++编译器在不同的计算机硬件上的表现是不同的。

所谓32位编译器是指它能将程序源代码编译成最高为32位的CPU指令系统代码。或者更加直接地说,int类型的程度是32位的。

文法就是语法。C++语言都是由语法规定的。

字符型

字符型是针对处理ASCII字符而设的。字符型在表示方式和操作上与整数吻合,在表示范围上是整数的子集。它由一个字节(8bit)组成,所以只能表示28即256个状态值。由于ASCII有128个字符,所以它可以用signed char(即char)中的所有正数表示所有ASCII码值,而负数表示非正常状态,以示区别。由于它可以看作整数的子集,所以其运算可以参与到整数中去,只要不超过其范围。例如:

char a=31;

int b=a+'\a';

然而它与整数毕竟还是有区别的,最大的区别是在输出方式上,字符型的输出不是整数,而是该整数所代表的ASCII码字符。如:

int a=65;

char b=65;

cout<<a<<" "<<b<<endl;//其值虽然都为65,但是输出结果分别为:65 A

枚举型

枚举符一旦定义则不能改变。所以它常常代替整数常量使用。这才是语言中设计枚举的真实意图,有时候甚至比整数常量还管用,因为在进入函数调用或其他模块时,常量需要初始化,而枚举却是一种类型,无须定义其实体,便可直接使用其枚举符!

十进制浮点数转换为二进制浮点数

在计算机内部,浮点数都是以二进制表示的,所以,对于十进制浮点数,要先转换为二进制浮点数。转换分两步,整数部分的切换,采用“除2取余法”;小数部分的转换,采用“乘2取整法”,即:对被转换的十进制小数乘以2,取其整数部分(0或1)作为二进制小数部分,取其小数部分,再乘以2,又取其整数部分作为二进制小数部分......如此循环往复,直到小数部分为0或者已经取到了足够位数。每次取的整数部分按照先后次序,构成二进制小数从高位到低位的数字排列。例如:

字符指针与字符数组

指针是表示 内存空间位置 的类型。字符指针就是所指向的空间位置上的值,当作字符来操作的类型。例如:

str是字符指针变量。*str是字符指针变量的间接引用。即,若str指向“Hello”的首地址,则*str表示该地址代表的空间上的值——“H”。

输出字符指针就是输出C-串。所以输出str时,便从“H”字符的地址开始,输出所有字符直到遇到0。

输出字符指针的间接引用,就是输出单个字符。所以输出*str时,便输出str所指向的字符“H”,如下图:

为了比较C-串的字典序大小,在C库函数中,专门设计了C-串的比较函数strcmp。此外,还有C-串的复制问题:

因而C库函数为其设计了strcpy函数。总之,C库函数设计了一系列的C-串库函数,解决了C-串的赋值、赋值、复制、修改、比较、连接等问题。

数组arrays:

数组中,常量表达式的值只要是整数或整数子集就行。例如:

int  a['a'];      //表示int a[97];

注意上述形式中,花括号中的初始值个数不能多于数组定义的元素个数。初始值不能通过逗号的方式省略,初始值也不能为空。在总体上,初始值可以少于数组定义的元素个数。如:

只要动用了花括号,就是实施了初始化。对于实施初始化的数组,如果初始值个数不足等号左边方括号中规定的元素个数,则后面的元素值全补为0。

除此之外,字符数组比其他数组有一点书写上的特殊性,它的初始化有以下三种形式:

其中,最后一种形式最简单。需要注意的是,第二种形式没有C-串的结束符,因此不能将数组名拿来做C-串操作。而第1、3两种情况的实际字符数应为6,如果元素个数少于6,则将编译出错。这样的设计完全是为了满足编程方便。

向量(Vector):

vector是向量类型,它是一种对象实体,具有值,所以可以看作是变量。它可以容纳许多其他类型的相同实体,如若干个整数,所以称其为容器。 使用它时,只要包含头文件vector即可。

vector可以有四种定义方式:

(1)vector<int> a(10);

(2)vector<int> b(10,1);

(3)vector<int> c(b);

(4)vector<int> d(b.begin(), b.begin()+3);

vector<int>是模板形式,尖括号中为元素类型名,它可以是任何合法的数据类型。

第一种形式定义了10个整数元素的向量,但并没有给出初值,因而,其值是不确定的。

第二种形式定义了10个整数元素的向量,且给出每个元素的初值为1,。这种形式是数组望尘莫及的。数组只能通过循环来成批地赋给相同初值。

第三种形式应另一个现成的向量来创建一个向量。

第四种形式定义了其值依次为b向量中第0到第2个(共3个)元素的向量。

特别地,向量还可以从数组获得初值。例如:

int a[7]={1,2,5,3,7,9,8};

vector<int> va(a,a+7);

上面的第四种形式的b.begin()、b.end()是表示向量的起始元素位置和最后一个元素之外的元素位置。

向量元素位置也属于一种类型,称为遍历器。遍历器不单表示元素位置,还可以在容器中前后挪动。每种容器都有对应的遍历器。向量中的遍历器类型为:vector<int>::iterator。因此,若要输出向量中的所有元素,可以有两种循环控制方式:

1 for(int i=0;i<a.size();++i)//第1种
2     cout<<a[i]<<" ";
3 
4 for(vector<int>::iterator it=a.begin();it!=a.end();++it)//第2种
5     cout<<*it<<" ";

第1种形式是下标方式,a[i]是向量元素操作,这种形式与数组一样;

第2种形式是遍历器方式,*it是指针间访形式,它的意义是it所指向的元素值。

a.size()是向量中元素的个数。

a.begin()表示向量的第一个元素,这种操作方式是一个对象捆绑一个函数调用,表示对该对象进行某个操作。类似这样的使用方式称为调用对象a的成员函数,这在对象化程序设计中很普遍,它的常用操作有:

向量是编程中使用频率最高的数据类型。这不仅是因为数据顺序排列性在生活中最常见,还因为向量有一些插入、删除、搜索、判空等最简单的常规操作。当数据并不复杂时,可以代替其他数据类型而很好的工作。特别是向量可以自动伸展,容量可以自动增大,这对一些不确定数据量的容器工作带来了极大的方便。

二维向量:

在二维向量中,可以使用vector中的swap操作来交换两个向量。swap操作是专门为提高两个向量之间互相交换的性能而设计的。如果用一般的swap

1 void swap(vector<int>& a,vector<int>& b)
2 {
3          vector<int> temp=a;a=b;b=temp; 
4 }

它要涉及向量的创建、赋值、再赋值,然后还要销毁临时向量。但若用vector的swap操作,这些工作都可以省略掉。只要做微不足道的地址交换工作,岂不美哉?!

例如:

文件aaa.txt中含有一些行,每行中有一些整数,可以构成一个向量。整个文件可以看成是一组向量,其中每个元素又都是向量,只不过作为元素的向量其长度参差不齐。设计一个排序程序,使得按从短到长的顺序输出每个向量。这时候,程序代码如下:

运行结果:

该程序涉及4个函数,其中main函数调用了其他三个函数。这三个函数分别是输入input,排序mySort和输出print。

input函数 中,由于每行中的整数个数不知道,所以向量的长短也不知道,又由于总的行数不知道,所以二维向量元素个数也不知道,输入时只能以向量添加元素的方式。

print函数 是一个两重循环,它按序将二维向量中的每个元素(向量)打印出来。并且每打印输出一个向量,就换行。

mySort函数是排序函数,它按向量元素个数的多少进行排序,少的在前,多的在后。使用的是“冒泡排序法”。冒泡排序在很多程序设计和数据结构的书中都有介绍。拍序中所使用的swap就是两个向量相互交换的操作,它在vector中定义。

用typedef来定义Mat这个二维向量的名称,以使程序中的名称易记易用。

【指针与引用(Pointers & References)】

 指针用于数组、函数参数、内存空间申请和释放等。指针对于成功进行C++编程至关重要。也许指针在其他语言转中并不是必要的,但在C++中,显得很必要。指针在提高性能方面,提升C++的产业竞争力上,立下了汗马功劳。指针是强大的,但又是最危险的。学习指针的时候,我们始终强调指针的双刃剑作用。

指针:

每个类型都有对应的指针类型,因此指针定义的形式为:

定义中的“*”可以居左、居中、或居右。由于指针本身也是一种类型,因此,甚至指针本身也有对应的指针类型:

其中ip和iq称为指针变量。

指针变量的定义,由数据类型的后跟星号,再跟指针变量名组成。指针变量在不致引起混淆的情况下也称为指针。

指针可以赋值,也可以在定义指针时初始化,赋值或初始化的值是同类型实体的地址:

“&”表示实体的地址,由于字面值不认为是具有空间地址的实体,所以不能进行&操作:

指针指向的实体,可以通过指针的间访操作(dereference,即在指针变量前加星号的操作)来读写该空间的内容。例如:

显示的结果应该是12 12。因此,间访操作对所指向的实体既可以读也可以写。写就意味着实体的改变,意味着也影响了所关联的变量。

由于指针变量本身也是具有空间的实体,因此也具有空间地址,也可以被别的指针(二级指针)所操纵。例如,下面通过二级指针的两次间访操作,最终操纵整型实体:

1 int iCount=18;
2 int* ip=*iCount;
3 int** iip=&ip;
4 cout<<**iip<<endl;

其现实结果为18。

初学要小心,星号在不同的地方有不同的含义:

间访操作只能用在指针上,否则编译报错:

指针的0值不是表示指向地址0的空间,而是表示空指针。即不指向任何空间。而指针也只有指向具体的实体,才能使间访操作具有意义:

从另一个角度说,指针忘了赋值,比整型变量赋值危险得多。因为这种错误,不能被编译发现,甚至调试的发现能力也很有限,到了运行时发现,可能已经作为发行版本颁发,而来不及挽回损失了。这种不安全性也是C++引入引用的重要意图所在。

指针运算:

指针值表示一个内存地址,因此它内部表示为整数,这在显示的时候可以看到。指针变量所占的空间大小总是等同于整型变量的大小,但它不是整型数。我们重温数据类型的三个要素:数据表示、范围表示、操作集合。指针与整型虽有相同的数据表示,相同的范围表示,但它们具有不同的操作,所以指针与整型不是相同的数据类型。

指针的加减整型数大多数用在数组这种连续的又是同类型元素的序列空间中。可以把数组起始地址赋给一指针,通过移动指针(加减指针)来对数组元素进行操作。数组名本身是表示元素类型的地址,所以可以直接将数组名赋给指针。例如:

然而指针的增减操作应受约束,如果数组元素只有10,而指针获得数组首地址后,进行了+20等超过数组范围的操作是危险的。

指针限定:

引用:

Chapter 4 计算表达(Computation Expressing)

不要小看整数运算,有许多错误幸亏在运行中发现了,但还有一些错误甚至在调试和运行中都发现不了,它们就这样潜伏着,直到有一天突然爆发……。编程是十分敏感的工作,来不得丝毫作假,克服编程漏洞必须从本质上去把握。为什么第3章要介绍整数和浮点数的内部表示?你若看不清他们,则错误永远只是表面现象,你永远也不能根本性排除错误。还有,逻辑操作对于问题表达的必要性、重要性和简洁性 ,你是怎么认识的?位操作的细腻性体现在哪里?增量操作的理解你到位了吗?表达式的副作用你都看清了吗?这些都是编程中的基本功。你必须对本章有一个深刻的印象,因为后面的例题中都要用到,它们都是作者(钱能老师)在编程和教学中遇到的最典型的问题,是经验之谈。

操作符的优先级和结合性:

所有单目、赋值和三目操作符都是右结合的,也只有这些操作符是又结合的,其余都是左结合的。

【相容类型的转换】(Cast Compatible Types):

隐式转换:

从上图各数据类型的表示范围来看,越往下,能力越强,所以构成运算时所进行的隐式转换总是朝表达能力更强的方向。

精度丢失:

关系与逻辑操作:

关系操作符就是比较运算符

关系操作的结果就是真(true)、假(false)两个逻辑之一。逻辑值可以进行逻辑运算(操作),逻辑操作符有:

不论数据类型如何,参与逻辑操作符操作的操作数都是0或1的逻辑值。例如:

条件语句中的a应理解为1而ip因为非0也应理解为1,1&1&的值为1,故将输出haha。

相等比较与赋值操作之所以容易搞错,是因为它们都有值,都是表达式,都能作为条件。

语言设计了这种一切表达式都具有值,而且可以互相操作的特征,使得语句之间具有很好的相容性。但是,操作符的相像性(如=和==)有破坏了编程的自然性。好在编译器在应该出现条件表达式的位置上,如果遇到赋值表达式则会发出一个警告。警示你的条件表达式可能有潜在的错误。请千万要培养不放过任何一个警告的习惯。否则难免搞错的“=”与“==”将对你的程序造成实质性的伤害。

不等式连写的错误:

逻辑表达本来就是这么无奈,C++读表达式的理解太宽泛,以至于很多计算机一不小心就会被编译所误解,从而居然还会运行出结果。但是笔者并没有认为这全是C++的缺点,表达式在表达能力方面却因此很牛啊,只要你能驾驭这头“牛”。

逻辑推演:

当我们求一个逻辑判断类问题时,首先要进行逻辑表达,因此逻辑表达是求解问题的一种基本功。我们来看一下逻辑表达式是如何推演的:

例如:

某任务需要在A、B、C、D、E这五个人中物色人员去完成,但派人受限于下列条件:

(1)若A去,则B跟去

(2)D,E两人中必有人去

(3)B,C两人中必有人去,但只去一人

(4)C,D两人要么都去,要么都不去

(5)若E去,则A,B都去
问这五个条件如何表示:

逻辑的推演技术涉及数理逻辑,应该学学数理逻辑学了。

位操作:

位操作是整数特有的操作,它有<<、>>、&、|、^、~六个操作。

1 左移操作

2 右移操作

3 位与操作

4 位或操作

5 位异或操作

6 位反操作

指针的增量操作:

指针可以加减一个整型数而得到另一个指针值。指针的增量操作也分前增量和后增量,减量操作同理。

在顺序安排中,操作数进行的挪动操作,可能会经历求值运算(因为操作数由表达式构成),而求值运算如果修改了另一个待计算表达式中的变量,则会产生副作用。所以,C++的表达式求值中可以有多个左值一起被改变,这称为表达式的副作用。如果一个改变了值的左值在表达式中出现了多于一次,则该表达式有不良副作用。要严防不良副作用,因为它对程序的正确性提出了严重的挑战。

                            第二部分  过程化编程

Chapter 5 函数机制(Function Mechanism)

函数是C也是C++程序结构中的基本单位,只不过C++函数在内涵上对C进行了诸多扩充。

从结构上或者本质上说,设计程序就是设计函数。

函数的不同组织方式,就形成了不同的程序设计方法。

在C++中,函数的作用并不只是为了对于给定的数据,计算后返回一个函数值那样完成一个预定的目标。C++语言的设计,其函数参数的意义并不仅仅是一个类型的变量值,很多是指向数据流的一个窗口,或者数据集合以及一组相关操作,即对象。完成了一个过程,也就完成了数据集合的处理。所以C++中的函数可以涵盖一切数据操作过程。从这个意义上说,C++的函数是涵盖更广的过程,基于函数的程序设计就是基于过程的程序设计。

正因为函数在C++中是如此重要,所以函数的使用也必须十分规范。函数使用的规范性体现在函数参数的性质上,体现在对待函数参数的传递规则和返回类型与返回值的审查上,体现在函数名字的识别原则上,体现在函数体编译的效率选择上,还体现在函数体中对数据的访问权限上。

函数的形态:

C++的函数形态,无论像数学函数,还是像纯粹的过程,都可以选择有参数或者无参数。所以C++函数形态分为四类:

 

有返回类型的函数可以参加表达式运算,或者直接赋值给对应类型的变量,构成表达式语句。

无返回类型的函数,不能以值的形式赋给其他变量或者参加运算,其调用只能独立构成一条语句。

需要时,有返回类型的函数也可以像无返回类型的函数一样单独构成语句。

函数总有各种各样的实现,就好像人有各种各样的差异一样。所调用的函数性能越好,程序的总体质量就越高。C++的基本库中的函数都是高度精炼的,所以,能调用C++库中的函数就尽量调用,可中没有的函数,才自己动手写,这样可以减少程序员自己的编程量,而且总体质量也高。

传值参数(Value-Passed Parameters):

函数通过参数来传递输入数据,参数通过传值机制来实现。所谓传值即在函数被调用之时,用克隆实参的办法来创建形参。

克隆实参就是用实参来创建形参而实参本身没有任何改变。

【指针参数】

指针和引用参数:

数组是不能整体直接复制的。

数组只能通过传递数组起始地址,达到使用该数组的目的。

函数的副作用:

函数参数传递指针和引用是一把双刃剑,它的负面作用是破坏非本地数据,破坏模块性,甚至为黑客打开方便之门。

函数的黑盒性是指不与外界发生意外沟通,只依赖输入参数,只送出返回结果,因此它是结果可重复的计算过程。但是指针和引用的参数传递向我们展示了函数可以访问非本地数据区的途径,从而破坏了函数的黑盒性。

例:

两个整数向量,元素个数相同,试做其加法,其代码如下:

 1 #include <iostream>
 2 #include <vector>
 3 using namespace std;
 4 
 5 void print(vector<int>& a)
 6 {
 7     for (int i = 0; i < a.size(); ++i)
 8         cout << a[i] << " ";
 9     cout << endl;
10 }
11 
12 vector<int> add(vector<int>& a, vector<int>& b)
13 {
14     for (int i = 0; i < a.size(); ++i)
15         a[i] += b[i];
16     return a;
17 }
18 
19 int main()
20 {
21     int aa[] = { 2, 3, 1, 2, 3, 2, 1 }, bb[] = {5,3,1,1,6,2,2.};
22     vector<int> a(aa, aa + 7), b(bb,bb+7);
23     vector<int> c = add(a,b);
24     print(a); print(b); print(c);
25     return 0;
26 }

程序是将两个初始化了的数组赋值给两个向量a和b,然后调用add相加,并输出这两个向量以及结果向量。设计函数是很机械的,只要你给我输入数据,按照功能要求、性能要求,满足它就行,例中的add就是一个典型的例子。

说实在话,设计者还为自己第16行的“+=”操作符而自得呢,因为针对对象的操作,它的操作效率不差于“a=a+b”,而且,设计中简化了操作,不建临时变量,一切以效率为第一要素,只要完成计算就行的思想贯彻得很好。

不过,main函数可傻眼了,它调用的add函数结果虽然对了,但是原始数据的向量a却被破坏了。这就是函数运行所带来的副作用。

问题出在规定函数参数传递的声明上,传递引用,给了函数超限的权力,函数循着传递的引用名而既读又写地访问了引用的空间,而那一片空间并不是函数所拥有的。因此,语言的对应手段就是在指针和引用参数上加const修饰,以此限制函数体中对参数的写操作。即:

1  vector<int> add(const vector<int>& a, const vector<int>& b)
2  {
3      for (int i = 0; i < a.size(); ++i)
4          a[i] += b[i];   //错,a不能做左值!!
5      return a;
6  }

声明中对参数加上const之后,负责add函数设计的程序员明白了,既要完成加法,又不能在原始数据上“打草稿”。如果还是那样设计,编译这一关就休想过!那只能另辟向量空间的蹊径以存放中间结果了,于是add函数做这样的修改:

1  vector<int> add(const vector<int>& a, const vector<int>& b)
2  {
3      vector<int> c(a);
4      for (int i = 0; i < a.size(); ++i)
5          c[i] += b[i];   //ok
6      return c;
7  }

当然,设计add的程序员还是有很多发挥空间的,与下面这种设计相比,上面代码的性能更好。程序员之间的编程功底还是看得出来的,想想为什么?

1  vector<int> add(const vector<int>& a, const vector<int>& b)
2  {
3      vector<int> c(a,size());
4      for (int i = 0; i < a.size(); ++i)
5          c[i] = c[i]+b[i];  
6      return c;
7  }

参数的const声明,框定了传递的参数只能以形参规定的原则来操作。所以a向量再也不能在add函数中被修改了,即在表达式中不能做左值。

栈机制:

一个程序要运行,就要先将可执行文件装载到计算机的内存中。装载是操作系统掌控的,一般而言,操作系统将程序装入内存后,将形成一个随时可以运行的进程空间,该进程空年分四个区域:

栈是一种先进后出的数据结构。

函数指针

一个运行的程序在内存中的布局,分为四个区域。Data Area、Heap Area、Stack Area这三个区域称为数据区域,Code Area称为代码区域。指向数据区域的指针,称为数据指针(Pointer to Data)。指向代码区域的指针称为指向函数的指针,简称函数指针(Pointer to Function或Function Pointer)。但要注意,与返回指针类型的函数不同,该函数称为指针函数(Pointer Function),例如:

1 int* f(int a);
2 char* copy(char * s1,char* s2);

f函数返回int指针,即int指针函数,copy函数为char指针函数,运行中的程序,其中的每个函数都存放在代码区占据着一个区域。故每个函数都有起始地址,指向函数其实地址的指针完全不同于数据指针,函数指针与数据指针不能相互转换,通过函数指针可以调用所指向的函数。

指向函数的指针:

函数指针也有不同类型,函数有多少种类型,函数指针就有多少种类型。例如:

1 void f();
2 int k();
3 int g(int);
4 int h(char);
5 int m(int,int);

都是不同类型的函数。函数是以参数个数、参数类型、参数顺序甚至返回类型的不同来区分不同类型的。

函数的类型表示是函数声明去掉函数名,所以上面f函数的类型为:void(),同样m函数的类型为int(int,int)。声明一个int(int)类型的函数g,就是把函数名放在返回类型和括号之间“int g(int)”。同样,定义一个int(int)类型的指针gp,就是把指针名放在返回类型和括号之间,即:

int (*gp)(int);

注意,上面是定义一个函数指针,而不是声明。而且容纳函数指针名的括号不能省,表示“*gp”是一个整体,它描述的是一个指针,有无括号,意义完全不一样。

int *gp(int);

表示声明一个含有一个整数参数的整数指针函数,等价于:

int* gp(int a);

定义函数指针还可以初始化。如果在定义中伴随着初始化,则应写成:

int g(int);

int (*gp)(int)=g;

其中,g应该和指针gp所指向的函数类型相同。

当然,函数指针赋值也可以与函数指针定义分开,像下面这样:

int g(int);

int (*gp)(int);

gp=g;

定义了一个函数指针,就拥有了一个指针实体,一个指针实体的大小跟int型实体大小是一样的。

递归函数:

递归函数即在函数体中出现调用自身的函数。例如,阶乘n!的数学函数描述为:

其对应的C++函数描述为:

1 unsigned f(unsigned n)
2 {
3     if(n==1) return 1;
4     return n*f(n-1);
5 }

不过要注意的是,f(13)>232。所以,对于unsigned int型(无符号类型)的表示范围,其n的取值范围只有1<=n<=12了。

又如,Fibonacci数列的数学函数描述为:

其等价的C++函数为:

1 unsigned int f(unsigned int n)
2 {
3     if(n==0||n==1) return n;
4     return f(n-1)+f(n-2);
5 }

递归条件:

该函数虽然逐渐减小递归调用时的参数值,但由于无条件递归,会最终导致栈空间崩溃。

消去递归:

【函数重载】

只要参数个数、类型、顺序不同,函数就可以重载。然而,返回类型不同则不允许重载。例如:

总结:

函数可以重载,即重名,但必须有不同的参数类型、个数和顺序。重载函数带来程序设计概念上的亲和性,使得函数名不必处处相同。

函数参数可以默认,其效果类似函数重载,但本质上是两回事。函数重载和默认参数在使用中存在一些差别。

 

Chapter 6 性能

基于对象或者面向对象程序设计最终都是要体现编程质量的,而程序质量中,差异最大的又最看不见的、最反应程序员功底的是程序转中对空间占用与时间消耗的合理把握。

程序员谈论最多的是效率问题,C++面向对象程序设计方法就是在效率的争辩中发展起来的。

内联函数:

影响性能的一个重要因素是内联技巧。内联函数也可称为内嵌函数。

内联函数使用的场合一般为:

(1)函数宜适当小,这样就使嵌入工作容易进行,不会破坏原调用主体。

(2)函数中特别是在循环中反复执行该函数,这样就使嵌入的效率相对较高。

(3)程序并不多处出现该函数调用,这样就使嵌入工作量相对较少,代码量也不会剧增。

正因为函数调用有开销,而内联函数调用几乎没有调用开销,所以编程时就应尽可能内联函数调用。但并不是任何函数都能内联的,编程时函数虽然打上了内联标记,却并不总是能被编译成内联方式的。因此,程序员能不失时机地构造内联的函数才是重要的。

本章总结:

Chapter 7 程序结构

在命令前面加“#”,称之为预编译指令。
全局数据:

例:矩阵转置

消除全局数据:

现代程序设计,全局数据是上不了大雅之堂的,因为它破坏了模块结构的独立性,也破坏了抽象数据结构的封闭性。程序是各个独立模块的聚集,而全局数据恰恰就牵扯了各个模块,使其无法独立。在上面程序找中,任何一个函数都无法拿到其他地方去重用,因为这些函数都必须以全局数据 的存在为前提,而且更苛刻的是,全局数据一定是向量的向量,对上例来说取名必须为“a”!丧失其模块的独立性,可见一斑。

当然,可以通过参数传递的方法消除全局数据:

静态全局数据:

函数可以看作是模块,程序文件也可以看作是模块。模块便是只认输入/输出,完成一定功能的黑盒。在过程化程序设计中,为了使程序文件发挥模块的作用,有必要定义一种模块的局部量,它区别(独立)于其他程序文件,称之为静态全局数据(也称全局静态数据)。

在程序中,有的函数是为文件中的其他函数服务的,并不对外提供服务,这些函数应声明为静态,表示局部于程序文件。同样有的变量只是为本文件服务,也不是全局数据,应标以static。这些函数和变量称为静态全局函数和静态全局变量,它只在本文件范围内可见,在其他程序文件中不可见。

红圈圈中的话要好好读啊!!!

静态局部数据:

例如,下面的程序演示了全局(静态)变量、静态局部变量和局部变量的区别:

 1 #include<iostream>
 2 
 3 using namespace std;
 4 
 5 void func();
 6 int n = 1;
 7 
 8 int main()
 9 {
10     int a = 0, b = -10;
11     cout << "a=" << a << " ,b=" << b << " ,n=" << n << endl;
12     func();
13     cout << "a=" << a << " ,b=" << b << " ,n=" << n << endl;
14     func();
15 }
16 
17 void func()
18 {
19     static int a = 2;
20     int b = 5;
21     a += 2, b += 5;
22     n += 12;
23     cout << "a=" << a << " ,b=" << b << " ,n=" << n << endl;
24 }

作用域与生命期

C++的作用域有全局作用域、文件作用域、函数作用域、函数原型作用域、类作用域和局部作用域。

函数作用域是说,不管名称在函数的什么地方声明,总是可以在函数的任何位置先使用该名称。

例:可以从下面的例子中看到局部作用域是嵌套的

生命期:

名空间:

名空间的组织:

名空间的定义通过下列形式:

组织模块:

数据名冲突:

名空间的用法:

预编译:

头文件卫士:

#define指令:

总结:

 第三部分 面向对象编程技术

The Objected-Oriented Programming

Chapter 8 类

C++的内部数据类型只有简单的整数和浮点。

类机制定义类class,类是一种类型type。定义类的格式与struct相像,只是在定义体中添上操作。操作是一个个的功能,由函数形式表示。

例如,在定义日期型时,同时将日期的有关操作也一并描述:

 

 1 #include<iostream>
 2 #include<iomanip>
 3 using namespace std;
 4 
 5 class Date
 6 {
 7     int year, month, day;
 8 public:
 9     void set(int y,int m,int d);//赋值操作
10     bool isLeapYear();//判断闰年
11     void print();//输出日期
12 };
13 
14 void Date::set(int y, int m, int d)
15 {
16     year = y; month = m; day = d;
17 }
18 bool Date::isLeapYear()
19 {
20     return(year%4==0&&year%100!=0)||(year%400==0);
21 }
22 void Date::print()
23 {
24     cout << setfill('0');
25     cout << setw(4) << year << '-' << setw(2) << month << '-' << setw(2) << day << '\n';
26     cout << setfill(' ');
27 }
28 
29 int main()
30 {
31     Date d;
32     d.set(2000, 12, 6);
33     if (d.isLeapYear())
34         d.print();
35     return 0;
36     cin.get();
37 }

 

成员函数:

成员函数一定从属于类,不能独立存在,这是它与普通函数的重要区别。这也就是在定义成员函数时,函数名前要冠以类名的道理。

成员函数的定义也可以放在类定义中,如果在类定义中定义成员函数,那么函数名前就不需要冠以类名了,就好像在自己家里,彼此说话都听得懂,能省就省吧。

使用对象指针:

一个类可以创建无数个对象,其任何一个对象都可以使用该类的操作,即调用该类的成员函数。此对象与彼对象调用成员函数的结果是不同的,它们在不同的对象上反映出来。所以调用成员函数一定是与某个对象捆绑在一起的,因此调用函数就有形式:

1 objectName.memberFunctionName(parameters);

如果对象是以对象指针间接访问的形式操作的,则对象与成员函数之间就用双字符的箭头“->”,即形式:

1 objectPointer->memberFunctionName(parameters);

或者将对象指针的间访形式用括号括起来,再加点操作符“.”加成员函数。即形式:

1 (*objectPointer).memberFunctionName(parameters);

不要忘了操作符的优先级:如果对象指针的间访操作不加括号,会先进行点操作符运算,从而招致编译错误,因为指针运行点操作符是非法的。

常成员函数:

成员函数的操作,如果只对对象进行读操作,则该成员函数可以设计为常(const)成员函数。设计为常成员函数的好处是,让使用者一目了然地知道该成员函数不会改变对象值,同时让类的实现者更方便地测试,因为在常成员函数中,任何改变对象值的操作,都将被编译器毫不留情他认定为错误。

经验之谈:能够成为常成员函数的,应尽量写成常成员函数形式。

常成员函数的声明和定义在形式上必须一致,即在函数形参列表的又括号后面加上const。例如,日期定义类可以改写成下面更好的形式:

 1 #include<iostream>
 2 #include<iomanip>
 3 using namespace std;
 4 
 5 class Date
 6 {
 7     int year, month, day;
 8 public:
 9     void set(int y,int m,int d);//赋值操作
10     bool isLeapYear()const;//判断闰年
11     void print()const;//输出日期
12 };
13 
14 inline void Date::set(int y, int m, int d)
15 {
16     year = y; month = m; day = d;
17 }
18 inline bool Date::isLeapYear()const
19 {
20     return(year%4==0&&year%100!=0)||(year%400==0);
21 }
22 void Date::print()const
23 {
24     cout << setfill('0');
25     cout << setw(4) << year << '-' << setw(2) << month << '-' << setw(2) << day << '\n';
26     cout << setfill(' ');
27 }
28 
29 int main()
30 {
31     Date d;
32     d.set(2000, 12, 6);
33     if (d.isLeapYear())
34         d.print();
35     return 0;
36     cin.get();
37 }

注意第10、11、18、22行的“const”。

其中set成员函数因为要修改对象值,所以无法设计成const。

【操作符】

函数重载特征:

性质:

增量操作符:

成员操作符

访问控制:

在类中出现的private、public以及在继承中出现的protected等都是访问控制符。

【静态成员】

静态数据成员:

有一些属性不是类中每个对象分别拥有的,而是共有的。这些共有的属性有些是变化的,比如类对象创建的计数值,有些是不变的,如日期类中要用到的12个月的名称,它是一个数组。这些属性不应该作为全局变量,因为它们是专属于某个类的,而不是属于过眼烟云的程序。类是可以反复使用的模块,它有鲜明的大众性和服务性。而程序只是为了某个特定目的,具有时间上的局限性,完成了使命,就失去了存在的价值

【友元】

友元可以看作是类操作中的一种访问权限不到位的补充。因为在类中过分强调了安全性,过分强调了责任,过分的公事公办,伤害了彼此具有亲情的对象们的心,使得具有恋爱情结的对象(如矩阵和向量)之间无法近距离接触,其直接影响是性能受损。很多书都依次抨击C++,说它不是纯正地支持面向对象,因为友元破坏了数据封装。其实世界上根本没有纯粹纯正的东西,任何事物都是相对的。C++作为这一历史时期的产物而风华正茂,只要适应它的编程手法,理解它的编程道理,用它就能成功。

Chapter 9 对象生灭

构造函数设计

初始化要求:

封装性要求:

函数形式:

无返回值:

set的缺憾:

一次性对象:

构造函数的重载

重载构造函数:

构造函数毕竟是函数,是函数就可以重载。不但可以重载,还可以设置默认参数。例如:

无参构造函数:

成员的初始化:

 

创建对象的唯一途径是调用构造函数!!构造函数是一段程序,所以构造对象的先后顺序不同,直接影响程序执行的先后顺序。

 【拷贝构造函数】

创建对象需要额外内存:

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class Person
 5 {
 6     char *pName;
 7 public:
 8     Person(char* pN = "noName")
 9     {
10         cout << "Constructing " << pN << "\n";
11         pName = new char[strlen(pN)+1];
12         if (pName) strcpy(pName,pN);
13     }
14 
15     ~Person()
16     {
17         cout << "Destructing " << pName << "\n";
18         delete[] pName;
19     }
20 };
21 
22 int main()
23 {
24     Person p1("Randy");
25     Person p2;
26 }

从运行结果中看到,程序先创建p1对象,再创建p2对象,p2因为没有初始化,所以就给了默认的noName名称。

由于创建对象时,申请分配了动态内存空间,所以当对象被销毁时,也要释放相应的空间,对象被销毁的瞬间,C++会调用一个析构函数,析构函数专门做对象销毁时的善后工作,取名为波浪号加上类名(~类名),表示正好与构造函数相反。对象执行析构函数的顺序与构造函数的顺序相反,先是p2被析构,再是p1被析构。

默认拷贝构造函数:

应该可以以其他对象为依据来创建对象,也就是像变量在定义时复制其他变量的形式:

1 int a=3;
2 int b=a;
3 Person x("Randy");
4 person y=x;

我们称这种对象创建活动为拷贝构造

例:从对象中复制出另一个对象

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class Person
 5 {
 6     char *pName;
 7 public:
 8     Person(char* pN = "noName")
 9     {
10         cout << "Constructing " << pN << "\n";
11         pName = new char[strlen(pN)+1];
12         if (pName) strcpy(pName,pN);
13     }
14 
15     ~Person()
16     {
17         cout << "Destructing " << pName << "\n";
18         delete[] pName;
19     }
20 };
21 
22 int main()
23 {
24     Person p1("Randy");
25     Person p2(p1);
26 }

结果发现,没有第二次输出“Constructing……”,也就是创建p2时并没有调用Person的构造函数。而且析构函数的表现也不正常了。原因是对象进行了C++的默认拷贝构造,而默认拷贝构造仅仅拷贝了对象本体。如下图:

于是,先析构p2时,将存有Randy的空间先行释放了,轮到p1析构时,Randy已经不复存在,因此访问该空间的操作变得不可预料的怪异。

自定义拷贝构造函数:

为了达到对象实体也就是对象整体复制的目标,就需要另外定义一个拷贝构造函数,以覆盖默认的拷贝构造函数:

例:拷贝构造函数

 1 #include<iostream>
 2 using namespace std;
 3 
 4 class Person
 5 {
 6     char *pName;
 7 public:
 8     Person(char* pN = "noName")
 9     {
10         cout << "Constructing " << pN << "\n";
11         pName = new char[strlen(pN)+1];
12         if (pName) strcpy(pName,pN);
13     }
14     Person(const Person& s)
15     {
16         cout << "copy Constructing " << s.pName << "\n";
17         pName = new char[strlen(s.pName) + 1];
18         if (pName) strcpy(pName, s.pName);
19     }
20     ~Person()
21     {
22         cout << "Destructing " << pName << "\n";
23         delete[] pName;
24     }
25 };
26 
27 int main()
28 {
29     Person p1("Randy");
30     Person p2(p1);
31 }

自定义拷贝构造函数名也是类名,它是构造函数的重载,一旦定义了拷贝构造函数,默认的拷贝构造函数就不再起作用了。

拷贝构造函数的参数必须是类对象的向量引用

Person(const Person& s);

因为对象复制的语义本身尚处于当前定义当中,参数传递若为传值形式,则对象复制操作调用的拷贝构造函数在哪里?!所以只能是引用或者指针。

但是指针参数将影响复制的语法:

Person p2 (*p1);// 或者Person p1=*p2;

这种语法并不优雅,所以用对象的引用。

const限定符有两个作用,一个是防止被复制的对象变样,另一个是扩大使用范围。有一条编程经验,就是自定义的对象作为参数传递,能用引用就尽量使用引用,能用常量引用就尽量使用常量引用因为被复制的对象也有可能是类对象

const Person p1("Jone");
Person p2(p1);

如果拷贝对象是常对象,而拷贝构造函数的参数不是常量引用,也就是说,置一个常对象于可能被修改的危险之中,这是编译无论如何也要奋不顾身报告错误的。

在自定义拷贝构造函数之前,我们进行拷贝对象构造时,都是在用默认的拷贝构造函数,因为那时候的对象本体与对象实体是一致的。所以,自定义拷贝构造函数在对象本体与对象实体不一致时,便是需要的,否则无此必要。

【析构函数】

从运行结果中看出,析构函数总是出现在对象的生命周期结束之时。静态对象在程序运行结束之时析构。

【对象转型与赋值】

对象赋值:

 总结:

Chapter 10 继承(Inheritance)

继承也是C++语言中类机制的一部分,该机制使类与类之间可以建立一种上下级关系。

人类都不是单亲繁殖的,一个人总是得到父母的双重的继承和关怀。C++的类也可以多重继承,然而,我们将更多地从技术上来看多重继承实现上的问题,以及其解决的手法。

只要解决了继承中的技术问题,使用继承来构造类框架,何乐而不为呢?继承也是面向对象程序设计的重要基础。

上图,从上到下是派生的关系,从下到上是继承的关系。每个类都有且只有一个父类,除了最顶层的交通工具类。其他所有的类都可以是子类。当一个类派生出其他类的时候,自己便是父类了。

描述事物一般是从属性和操作上描述的。

继承就是让子类继承父类的属性和操作,子类可以声明新的属性和操作,还可以剔除那些不适合其用途的父类和操作。在新的应用中,父类的代码已经存在,无须修改。所要做的是派生子类,并在子类中增加和修改。所以,继承可以让你重用父类的代码,专注于为子类编写新代码。

继承也是我们理解事物、解决问题的方法,继承帮助我们描述事物的层次关系,有效而精确地描述事物,理解事物直至本质。一旦看清了事物所处的层次结构位置,也就可以找到相关的解决办法。继承可以使已经存在的类无须修改就可适应新应用。继承是比过程重用规模更广的重用,是已经定义的良好的类的重用。

派生类对象结构:

如果类BaseClass是基类:

1 class BaseClass
2 {
3     int a,b;
4     //其他私有成员
5 public:
6     //公有成员
7 };

则其对象本体含有两个整型空间。派生类继承的方式是在类定义的class类名的后面加上:“public”再加上基类名。如果B继承了BaseClass类,则:

1 class B:public BaseClass
2 {
3     int c;
4     //其他私有成员
5 public:
6     //公有成员
7 };

派生类对象本体包括两个部分,一个为基类部分,即含两个整型空间,另一个为派生类部分,含一个整型空间:

继承父类成员:

在类中,还有一种保护(protected)型的访问控制符,保护成员与私有成员一样,不能被使用类的程序员进行公共访问,但可以被类内部的成员函数访问。除此之外,如果使用的类是派生类成员,则可以被访问,这是私有成员所不具有的能力。也就是说,只要将类成员声明为保护成员,则其派生类在继承之后,就可以坐享其父类的公有和保护操作了。

类内访问控制符:

继承可以公有继承,也可以保护继承和私有继承。多数情况是公有继承,就像前面所看到的。也就是说,在class类名后面加上public关键字再加基类名称。保护继承和私有继承的描述在后面。

基类为了长远考虑,可以留下保护成员,但派生类简单而清晰的设计应该是不使用任何的保护成员,即只使用公有成员。

【派生类的构造】

默认构造:

自定义构造:

【继承方式】

继承访问控制:

在不同的作用域中可以访问不同的访问控制属性的成员,下列代码及注释告诉我们哪些成员可以访问。哪些成员不可以访问:

调整访问控制:

在派生类中,可以调整成员的访问控制属性。例如,可以将公有成员调整为私有成员,将保护成员调整为公有成员,等等。

【继承与组合】

对象结构:

性质差异:

对象分析:

继承设计:

组合设计:

【多继承概念】

多继承结构:

 Chapter 11 基于对象编程(Object-Based Programming)

到了该回顾一下对象化编程效果的时候了,让我们整体性地来看看对象化编程是怎么回事。没什么特别的,新鲜感也已经过去了,它不过是通过类型来产生工作的对象,用对象以及捆绑的操作来描述动作序列,用动作序列来堆积算法而已。然而这个“而已”却使得程序设计变得更加游刃有余了,不仅方便灵活、安全可靠、可维护,而且更是可移植。

过程化的编程是通过函数模块的堆积来展开的,它是一种行为抽象的编程。而基于对象的编程是通过抽象数据类型描述的数据(对象)来展开的,它是一种数据抽象的编程。

【抽象编程】

抽象分行为抽象和数据抽象。

行为抽象:

忽略某些细节程度而只关心自己分内的细节程度,就是抽象。因此,抽象也是人们生活学习的策略。

抽象也是编程的策略,C++所提供的类机制正是这样的一种编程分工手段,让程序员处于不同层次的编程细节,来达到方便编程的目的。

数据抽象:

1 数据

2 数据类型

3 数据抽象

4 抽象数据类型

 第四部分 高级编程

(这一部分在实战中各个击破吧!)

Chapter 12 多态

 

Chapter 13 抽象类

 

Chapter 14 模板

函数模板的定义

函数模板的用法:

使用函数模板,就是以函数模板名为函数名的函数调用。其形式为:

函数模板参数

苛刻的类型匹配:

模板函数调用是寻求函数模板的类型参数匹配,类型实参与类型形参的匹配规则与函数的数据实参类型与数据形参类型匹配规则不同。类型实参与类型形参匹配规则更苛刻。例如:

数据形参:

常量引用型形参

引用型形参:

函数模板重载:

类模板

容器类的困惑:

类模板定义:

类模板的实现:

一个完整的类模板定义、实现和使用的实例如下:

模板类与类模板:

……

……

……

总结:

Chapter 15 异常

面向对象编程是一门深不见底的学问。它有许多技术细节需要在宏观学习中补充。人们尝到了抽象编程的甜头,就像千方百计地完善它。可是,在抽象作用中,明摆着模块的一层层相互作用,当模块不能正常运行时,现场一般是没有能力恢复的,应该尽可能快速的返回到想要知道处理结果的模块,而且该模块具备对出错进行处理的能力。然而,出现错误和错误处理处相隔甚远,回馈途中将饱受函数机制的繁琐手续,可函数机制却仍心安理得地按着固有的节奏在冷冷地呻吟。

让一种新的机制来满足程序运行中的结构跳跃吧,它就是异常机制。异常机制相对独立,与函数机制谈不上互相制约,但绝对谈得上互相补充。

在弥漫着类层次结构信息的程序空间中,异常处理也必须要有多态,这是对异常要求的底线。同时,先入为主的函数所构建的程序运行结构,必须在异常的破坏性处理中,保留可运行程序部分的数据和恢复先前分发出去的资源。

我们对异常似乎充满了厚望,希望它能实质性地帮助面向对象编程,解决彼此独立的程序世界中,更融洽地相处的问题。没想到,它不但能胜任这个职责,而且还给了我们另一种过程控制的模式,令我们惊喜。

错误处理的复杂性

使用异常

捕捉异常

异常的申述

异常继承体系

异常的应用

非错误处理

总结:

因为面向对象程序设计问世,才有了异常这个产物。程序中的产物是多样化的,但发现错误总想加以纠正。错误处理对于嵌套调用深处的独立模块来说,有点勉为其难,因为调用者的用意只有调用者自己明白,错误处理理当由调用者承担。

推荐阅读