C++程序设计语言(特别版):第二章 C++概览

来源:互联网 发布:淘宝代购要上传身份证 编辑:程序博客网 时间:2024/05/22 10:45

第二章  C++概览

 

2.1        什么是C++

C++

——是一个更好的C

——支持数据抽象

——支持面向对象程序设计

——支持通用型程序设计

2.2        程序设计泛型

说一种语言支持某种风格的程序设计,是指它提供了一些功能,这些功能能够方便地(比较容易、安全和有效)用于这种程序设计风格。如果必须付出很大的努力或利用各种技巧才能实现,说明这种语言不支持某种风格程序设计。

 

       要支持一种泛型,不仅在于某些能直接用于该泛型的显见形式的语言功能,还在于一些形式上更加细微的,对无意中偏离了这种泛型的情况做编译时或者运行时的检查。类型检查是这类事物中最明显的例子,歧义性检查和运行时检查也被用作对某种泛型的语言支持。语言之外的功能,如库和程序设计环境,也能进一步提供对一种泛型的支持。

 

       语言所拥有的特征是否足以在某个所希望的领域中支持某种所希望的程序设计风格:

       [1]所有特征必须清晰而优美地集成在语言之中。

[2]必须组合使用这些特征去得到一种解决方案,如果无法做到这样的组合,那会要求额外的独立的特征。

[3]应尽可能减少荒谬的和“专用的”特征。

[4]任何特征的实现都不应该给未使用此特征的程序强加明显的额外的开销。

[5]用户只需要了解自己在写程序时所明确使用的那个语言子集。

第一条是美学和逻辑的,随后两条表述的是最小化的思想,最后两条可以总结为“你不知道的不会伤害你”。

 

2.3        过程式程序设计

原始的程序设计泛型是:

确定你需要哪些过程;

采用你能找到的最好的算法。

 

这里所关注的是处理过程——执行预期的计算所需要的算法。支持这种泛型的语言提供了一些功能,如给函数传递参数以及从函数返回结果值等

 

2.3.1              变量和算术

fyhui问:

boolC++内置数据类型吗?C++内置基本类型有哪些?

boolC语言的基本类型吗?

 

2.3.2              检测和循环

2.3.3              指针和数组

所有数组都是以0作为他们的下界。数组下界不能指定吗?好像C语言可以吧?

int    v1[10];

int    v2[10];

for(int i=0; i<10; i++ )          v2[i] = v1[i];   //能直接通过指针赋值操作吗?

 

2.4        模块程序设计

设计程序的重点已经从有关过程的设计转移到对数据的组织了。一些相关的过程与被它们操作的数据组织在一起,通常被称做一个模块。程序设计泛型是:

确定你需要哪些模块;将程序分为

  一些模块,使数据隐藏于模块之中。

 

这一泛型作为数据隐藏原理而广为人知。

 

2.4.1              分别编译

//stack.h

namespace Stack

{

       void push(char);

       char pop();

}

 

//stack.cpp

#include “stack.h”

namespace Stack

{

       const int max_size = 200;

       char v[max_size];

       int top = 0;

}

void Stack::push(char c)       { /*检查上溢并压入c*/ }

char Stack::pop() { /*检查下溢并弹出*/ }

 

fyhui:类型的内部成员变量的定义不是放在头文件(.h)里吗?如果是放在实现文件(.cpp)里,那么头文件只有一些函数的声明了。这样做的唯一意义就是对外开放的.h文件里,其他用户不能知道数据的组织结构和存储方式,为了隐藏。按理说只要数据具体结构不在.h里,而.h可以对应多个.cpp,那么一种声明可以被多种不同的实现。最终用户使用的声明是.h,那么用户调用的函数能知道是对应哪个.cpp的实现吗?应该不能吧,否则就能代替多态了。等学习完namespace再回来看看,有没有解决的方法。

 

2.4.2              异常处理

当一个程序被设计为一个模块后,对应错误的处理必须在此模块考虑范围内。哪个模块应该承担对应哪个错误的处理责任?检查出错误的模块并不知道去做什么,恢复的动作依赖于那些那些调用操作的模块,而不是试图去执行操作并且发现了错误的模块。

       采用异常处理机制能使对错误的处理更加规范,也使它更容易看清楚。

 

2.5        数据抽象

2.5.1              定义类型的模块

基于模块的程序设计趋向于以一个类型的所有数据为中心,在某个类型管理模块的控制之下工作。

       namespace Stack

{

       struct      Rep;        //在另外某地方定义堆栈类型的布局

       typedef    Rep&      stack;

stack       create();                    void        destroy(stack s);

void       push(stack s, char c);           char       pop(stack s);

}

我们可以有多种方式实现Rep,而用户完全不必知道,只要这个接口声明没有变,即使重新实现stack,用户也不会受到任何影响。

这种做法常常不是最理想的,它可以因为表示类型不同而出现很大的变化,而这变化用户又无法得知。——fyhui解释:①表示类型只的是Rep具体实现,包括结构体定义和函数实现;②出现的变化可能会影响用户的操作,而用户又没法知道这变化。

更根本的问题是,通过模块实现的这种用户定义类型,他们所提供对这种类型的访问,在行为上,并不像内部的类型。——fyhui问:所谓行为是指初始化和释放?如初始化可以有构造函数,而不是必须调用stack.Create()

 

2.5.2              用户定义类型

C++解决上面问题的方式就是允许用户直接定义类型,这种类型的行为方式(几乎)与内部类型一样。这样的类型常常被称做抽象数据类型,也叫用户定义类型。——fyhui问:①抽象数据类型就是用户定义类型?②数据抽象与抽象数据类型什么关系?③抽象数据类型,“抽象”在哪里?仅仅是存储方式不定吗?

 

 

       现在程序设计范式:

确定你需要哪些类型;

  为每个类型提供完整的操作。

       在哪些对于每个类型都只需要一个对象的地方,采用模块方式实现数据隐藏风格的程序设计也就足够了。——fyhui①类型只有一个成员变量吗?②其初始化和释放也不方便啊。

 

       各种算术类型,如有理数或复数,是用户定义类型的最常见实例。——fyhui:举复数例子,想说明什么?说明类型只用了一个对象?仅仅举个用户定义类型的实例?如果是后者,说明用户定义类型的行为与基本类型几乎一致?

 

2.5.3              具体类型

现在考虑一个遵循complex那种定义方式的用户定义的Stack类型。——哪种方式?定义成类、有构造和析构、同时数据变量在声明文件里??

class Stack

{

       char*     v;                  //剥离这3个成员变量,就成抽象类型了?

       int           top;

       int           max_size;

 

public:

       class Underflow() {};           class Overflow() {};             class Bad_size() {};

       Stack (int s);

       ~Stack();

       void push(char c);

       char pop();

};

这个Stack类型遵循与内部类型同样的规则,包括命名、作用域、存储分配、生存时间、复制等等。——任何一个类,系统默认生存哪些函数?构造、析构、赋值、复制,还有呢?

       fyhui问:int   *p = new int(7);在局部函数内,存放数值7的内存会自动释放吗?基本类型的newdelete是如何实现的?

      

       complexStack这样的类型成为具体类型,它们与抽象类型对应。——“这样”指的是成员变量定义在声明文件中?抽象类型指存储方式未定?上节提到抽象数据类型同义用户定义类型。那么抽象数据类型抽象类型意义不一样吗,还是Stack这样的具体类型不算用户定义的类型?

 

2.5.4              抽象类型

上节Stack表示方式没有与用户接口分离,反而成为了使用Stack的程序片段里将要包含的一个部分。如果这个表示有了显著的变化,哪些使用它的代码就必须重新编译,这是一个代价。特别因为在不知道一个类型表示的大小的情况下,就无法获得这个类型的真正的局部变量。——在这,说这句话,何意?

       在类型不常变,而且局部变量有确实提供了我们极需要的清晰性和效率的地方,这种方式可以接受,也理想。但是,如果需要用户与Stack实现的修改完全隔离开,就不尽人意了。

 

       好的方案是,函数声明作为放在基类中,而且是虚基类,数据存储方式和方法实现放在子类里。这样用户使用基类的接口统一,多样的存储方式与实现完全与用户分离。

 

2.5.5              虚函数

虚函数有一张对应的虚函数表,类保留了指向虚函数表的指针vtbl。虚函数表里的各个函数具体指向是在运行时识别的。

 

2.6        面向对象程序设计

2.6.1              具体类型的问题

考虑某个图形系统里定义一个类型Shape,假定需要支持圆形、三角形和正方形,而且我们已经有了class Pointclass Color

       enum Kind { circle,  triangle,  square };

       class Shape

{

       Kind       k;    //类型域,为了说明一个类所存储数据的不同情况而放的标志。一般不良!

       Point       center;

       Color      col;

public:

       void        draw();          //画图

       void        rotate(int);      //旋转角度

};   

       函数draw()的实现可能如下:

       void Shape::draw()

{

       switch(k)

{

       case circle:    

              //画圆

break;

       case triangle:  

              //画三角形

break;

case square:   

              //画正方形

break;

}

}

       这是一个糟糕的方式,draw()必须知道所有的图形种类,而且以后新增加图形类别时,涉及到“触动”在形状上每个重要操作的代码。不利于扩展,甚至信息隐藏。

 

2.6.2              类层次结构

上节的困境应该设计虚基类,然后子类去完成具体的实现。

现在程序设计的泛型是:

确定你需要哪些类;

  为每个类提供完整的一组操作;

  利用继承去明确地表示共性。

 

在不存在共性的地方,数据抽象就够了。在类型之间,能够通过使用继承和虚函数挖掘出的共性的数量,可以看做是面向对象程序设计对于该问题的适用性的一个检验。

 

2.7        通用程序设计

独立于具体类型的操作和算法可以抽象出来,这时程序设计的泛型是:

确定你需要哪些算法;

  将它们参数化,使它们能都对

    各种适当的类型和数据结构工作。

 

2.7.1              容器

一个能保存某种类型的一集元素的类,一般被称为一个容器类,简称容器。

模板是编译时的机制。

fyhui:模板是类的类,容器是管理一类对象的管理器。

 

2.7.2              通用型算法

template<class In, class Out> void copy(In from, In too_far, Out to)

{

       while(from != too_far)   //while( (*to++ = *from) != too_far )哪个更好?

{                                 //注意,它们有一点不等价,多拷了too_far,危险!

       *to = *from;

       ++to;

       ++from;

}

}

2.8        附言

2.9        忠告

[1]不要害怕,一切都会随着时间的推移而逐渐明朗起来;

[2]你并不需要在知道了C++的所有细节后才能写出好的C++程序;

[3]请特别关注程序设计技术,而不是各种语言的特征。

 

原创粉丝点击