第二章笔记——《c++ programming language》Bjarne Strousrup著,裘宗燕译本

来源:互联网 发布:编c语言用什么软件 编辑:程序博客网 时间:2024/06/12 01:46

 1、我们要做的第一件事,就是杀掉所有的语言专家。
——The first thing we do, let's kill all the language lawyers.
这句话的意思,按照柯老师的理解,大概是说不要被语言的细节所左右,对编程语言应该有一个宏观的认识。所以,要先“杀掉”所有的语言专家。

2、程序设计范型——过程式程序设计,模块程序设计,面向对象的程序设计,通用型程序设计。
  “面向对象程序设计语言”的含义在于某种程序设计语言特别提供了一些机制,以很好地支持在其中做面向对象风格的程序设计。支持和允许的区分:支持意味着这种语言对于某种风格的程序设计提供了一些功能使其能够方便地使用。方便意味着容易、安全、有效。如果写出这种风格的程序必须使用各种技巧、花费很大的努力,那么就说这种语言不支持但允许这种风格的程序设计。
  支持一种范型,第一要有直接用于该范型的显见形式的语言功能。还在于对于偏离这种范型的情况做编译时或运行时的检查。例如类型检查(c++对类型的强制),歧义性检查,运行时检查等。
  举例:如果在基类中声明了一个virtual成员函数,如果没有在它的派生类中实现,就会产生一个连接错误。如此之类。

3、希望的程序设计风格:美学的和逻辑的、最小化原则(减少“专用”特征),另外,“你不知道的东西不会伤害你”(未使用某种特征,则不能有额外开销;用户只需要了解自己明确使用的语言子集,例如,不懂得模板照样可以进行大多数开发,用c++一样可以进行c程序设计等等)。

4、Stroustrup说,设计c++就是为了支持数据抽象、面向对象的程序设计和通用型程序设计。

5、过程式程序设计的范型:确定你需要哪些过程,采用你能找到的最好的算法。
  确定你需要哪些过程:相当于设计接口interface的阶段,准则是output=f(input)。其关键在于设计使用哪些函数。正如Stroustrup所说,函数被人们用于在许多算法的迷宫中建立起一种秩序。
  采用你能找到的最好的算法:也就是实现的过程,implementation。写算法本身,可以采用函数调用和其他的语言功能写出。
  这里有两点:一,过程式程序是由函数组成的。二,过程式程序是由函数驱动的,原始的驱动力是操作系统。

6、这里简要地提到了变量和算术的问题。
  变量需要声明(废话,除非是那些不要强制声明的语言),一个声明是一个语句,他为程序引进了一个名字,还为这个名字确定了一个类型。
  类型定义了名字或者表达式的正确使用方式,确定可以对他们执行的操作。
  另,c++里的char和int。一个char变量正好能保存给定机器里的一个字符——一般是一个字节;一个int变量正好适合给定机器里的整数算术——通常是一个机器字。也就是说,他们的大小是依赖于机器的。这点和Java不同。所以c++程序的可移植性不如Java,但效率却较高。

7、检测和循环——超简单版本
  这一小节提到的东西没什么新意,就是c的那套。不过有一点需要从另外的角度考虑:以后可以试着把“循环”这种东西看成一个“填空题”,只需填进去循环条件和循环体即可——这个看法其实也很一般,但却比把它看成一次又一次执行的语句要好。

8、模块程序设计
  确定你需要哪些模块;将程序分为一些模块,使数据隐藏于模块之中。
  模块:一集相关的过程与被它们操作的数据组织在一起,称为一个模块。
  程序设计的重点:有关过程的设计-〉对数据的组织
  这一范型也就是数据隐藏。
  书中给出了一个Stack的例子。它把界面,也就是push和pop函数放在一个namespace里声明,进而把它放在一个头文件里。用户代码完全被隔离于Stack的数据表示之外。
  c++允许把任何声明放到名字空间里。这样,函数、类型的名字也容易做成一个模块中局部的东西。这样,数据隐藏就扩展到了信息隐藏。

9、关于分别编译
  我们可以将一个模块界面的声明放进一个头文件,如stack.h,然后把用户代码和模块的实现代码分别写进user.c和stack.c中,可以分别编译。两个.c文件除了共同include一个头文件以外,互不相关。
  但分别编译不是语言要考虑的问题,而是关于如何最好地利用特定语言实现的优点的问题——用语言的特征去逻辑的表示模块化,而后通过能最有效地分别编译的一组文件,物理地利用这种模块化机制。

10、模块化基础上考虑异常处理
   检查出错误的模块往往不能用来处理错误,只有调用操作的模块才知道如何处理。因此,有关异常处理的类一定是界面的一部分。
   书中给出的例子中,界面里声明了一个Overflow类,而在push()检测出上溢出一场后,就执行throw Overflow(); 这里的Overflow()是直接调用构造函数生成一个异常对象,然后将其抛出。相应的,在用户模块里,有catch(Stack::Overflow)的机制。
   采用异常处理机制能使对错误的处理更加规范。这是因为,在try之中的语句不需要再考虑异常问题了。

11、模块化的数据抽象
   模块化是一切成功的大型程序的一个最基本特征。
   书中介绍了一种定义类型的模块,也就是Strousrup所说的“假类型”。还是stack的例子,但这时候stack其实是一种数据结构的引用(stack是名字空间Stack里的一个引用的别名——typedef Rep& stack)。这种数据结构本身用户不需要关心,只是可以使用它的引用——stack。用stack来声明变量的实质是让stack模块返回一个可用堆栈的引用,销毁这个变量实质上是使这个引用不再指向一个可用的堆栈。这个过程中,并没有真正的局部“堆栈对象”的生成。——Stroustrup说,一个Stack::stack用起来很像一个内部类型的变量。但是,一个Stack::Rep能被使用的期间由Stack::creat()和Stack::destroy()控制,而不是由普遍性的语言规则控制。(也就是说,它既使用在函数中,也不能看作普通的局部变量,还是一种特殊的数据结构)

12、用户定义类型
  c++支持用户直接定义类型,这种类型的行为方式几乎与内部类型一样。这样的类型就是抽象数据类型或用户定义类型。于是,面向对象的程序设计范型就是:确定你需要哪些类型,为每个类型提供完整的一组操作。
  但在那些对于每个类型都只需要一个对象的地方,采用模块方式实现数据隐藏风格的程序设计也就足够了。
  这里书中给出的例子是complex,私有数据只有re、im两个double型数据,在此之上定义的操作有三个构造函数,以及重载的一系列算术运算符。
  注1:由于+ - * / == != 都是二元运算符,适合用友元函数而非成员函数。详细内容以后再讨论。注2:c!=b的意思是operator!=(c,b),而1/a的意思是operator/( complex(1), a )

13、stack的具体类型
  缺点是表示方式没有与用户界面分离。因为与stack实现有关的数据出现在class stack 的private部分。这样,如果这个实现有了某种显著变化,那些使用它的代码也要重新编译。
  优点是,对象的行为方式完全像内部的类型。

14、stack的抽象类型
  为了得到界面与表示的完全分离,这时要放弃的就是真正的局部变量。——界面与表示完全分离,意味着我们不可能知道一个类型表示方式的大小,那么,也就无法获得这个类型真正的局部变量。
  因此,只在Stack类中写界面,也就是异常处理类以及一些纯虚函数,使Stack成为一个抽象类。具体实现由其派生类完成,使用时声明一个基类的指针或引用即可通过虚函数表调用派生类中的实现。ps:多态也正是由此实现。
  这样做会有时间和空间的额外开销:时间开销:间接调用函数(通过虚函数表指针),空间开销:每个派生类对象都有一个虚函数表指针。

15、面向对象的程序设计
  模块化以及具体类型的优点在于数据抽象,封装性很好,它们定义了一类黑盒子——但是它们不够灵活,扩充性不好。
  最典型的是图形处理里面的Shape类,如果用类型域来表示各种形状从而进行不同的操作,则要用到类似switch这样的分支语句。缺点在于增加新的形状非常麻烦,或者根本做不到——在你没有实现代码的情况下。
  于是需要引入类层次结构。以上的问题在于没有一种方式来区分所有形状的共同特征和特定形状的特殊特征。——表达这种区分并由此获益,就定义了面向对象的程序设计。
  书中给出了抽象类Shape的声明(顺便说一句,这里有一个成员函数名字起得很好——where(),向大师学习)。其中的draw()和rotate()是基类无法实现的,需要由派生类实现。通过多态,很容易调用各种具体形状的函数,同时也很容易使形状的种类得到扩展——通过继承。
  于是,又一种范型(面向对象)出现了:
  确定你需要哪些类;为每一个类提供完整的一组操作;利用继承区明确地表示共性。
  自上而下是求异的过程,自下而上是求同的过程。

16、通用型程序设计
  确定你需要哪些算法;把它们参数化,使它们能够对各种各样适当的类型和数据结构工作。
  把类型参数化——用模板。如果用继承,就会引起派生类数量激增,即类爆炸。模板是一种编译时机制,对它的使用不会引起运行时额外开销。
  一个能保存某种类型的一集元素的类,一般被称为一个容器类。
  通用型算法——往往和集合运算有关。c++的标准库,将注意力集中到序列(相当于数据结构中抽象的线性表)和通过迭代器操作序列的观念。
  迭代器提供两种操作:引用一个元素,以及通过它转而引用下一个元素。
  有一个特殊的迭代器:结束(引用最后一个元素后面的那个不存在的元素)

说明:部分摘抄书中原话,部分改写,部分是自己的笔记

原创粉丝点击