面向对象笔记
定义
对象:一个面向对象的程序是由一个相互作用的代理团体组成,这些代理被称作对象。每一个对象承担一个角色。每一个对象都提供一种服务或者执行一种动作,以便为团体中其他对象服务。 对象是独立存在的客观事物,它由一组属性和一组操作构成。
属性和操作是对象的两大要素。属性是对象静态特征的描述,操作是对象动态特征的描述。
操作又称为方法或服务,它描述了对象执行的功能。通过消息传递,还可以为其它对象使用。
消息: 面向对象编程中,行为的启动是通过将“消息”传递给负责的代理来完成的。消息对行为的要求进行编码,并随着附加信息(参数)一起传递。
接收器:接受消息的对象,如果接受了消息,那么就要执行相应的方法来实现。每条消息都要有相应的接收器接受
- 信息隐藏:作为某对象提供的服务的一个用户,只需要知道对象将接受的消息的名字。n不需要知道要完成要求,需要执行哪些动作。 在接收到一条消息后,对象会负责将该项任务完成。
类:根据抽象的原则对客观事物进行归纳和划分,只关注与当前目标相关的特征,把具有相同特征的事物归为一个类。它是一个抽象的概念。类是具有相同属性和相同操作(服务)的对象的集合。它包括属性和操作。
继承:类被组织成有单个根节点的树状结构,称为继承层次结构。与类实例相关的内存和行为都会被树结构中的后代自动继承。类可以组织成一个有层次的继承机构。一个子类继承层次树中更高一层的父类的属性。抽象父类是指没有具体实例的类,他只是用来产生子类。
接口:不提供系统实现,接口定义新类型,可以声明变量。类的实例可以赋值给接口类型遍历。
为什么既有接口又有抽象类:提供不同的抽象层次,提高灵活性。抽象类允许有非抽象方法。
组合与继承
- | 优点 | 缺点 |
---|---|---|
继承 | 减少代码重复,提高可维护性 | 类之间关系复杂,子类受到父类限制,降低灵活性 |
组合 | 实现更复杂的功能,避免继承带来的限制 | 增加代码复杂度,增加对象之间的依赖关系 |
静态成员
静态变量分配在全局数据区。静态方法只能使用静态变量。
继承
继承的形式:前两种推荐,中间物种不推荐,最后一种取决于语言是不是直接支持多继承。
替换原则:如果B是A的子类,那么用B来替换A外界将毫无察觉
-
特殊化继承:
符合替换原则
通过is a检查是不是蕴含则继承关系,比如狗is a动物,那么狗就可以继承动物。
子类型:子类型强调新类具有父类一样的行为
新类是基类的一个特定类型。
可以添加新方法,尽量不要改写父方法。
如果改写,必须符合父类原本定义
-
规范化继承:
派生类只实现那些在基类中定义,但是没实现的方法。符合替换原则
-
构造继承:
子类从父类继承所有方法,只是修改方法名和参数。
比如树到独木舟,违反替换原则。形成的子类不是子类型。
-
泛化继承:
派生类扩展基类,形成更泛化的抽象。比如window是黑白背景下的显示,泛化继承成colorwindow,变成有颜色的背景。
必须改写至少父类的一个方法。
-
扩展继承
只是添加新行为,不修改从父类继承来的属性。不违反替换原则,
-
限制继承
子类的行为比父类少
比如继承双向队列实现堆栈。
违反替换原则。
-
变体继承
比如控制鼠标的代码和控制图形输入板的代码。
父类和子类都是对方的变体,可以任意选择父子关系。
-
合并继承(多重继承)
可以通过合并两个或者更多的抽象特性来形成新的抽象。可以有多个父类。
问题:
- 名称歧义。子类的一个名称可能表示多个操作。
- 对替换的影响。为了解决名称歧义,可能会重命名,这时如果把他放到其中一个父类的列表中,可能方法名会改变。
子类不继承父类构造器
可以使用super调用父类构造器。
多级构造器的调用顺序是自上而下。
多态
多态是指声明的静态类型与实际执行的动态类型不一致。
签名
函数名与形参列表(包括形参的类型和顺序)共同决定了一个函数。称为一个函数的签名。
改写(重写、覆盖、重置、覆写)
返回值、名称和形参列表完全相同,也叫重写,覆盖。并且要求夫类的函数是虚函数。
发生在父类与子类之间,要求父类被重写的函数是虚函数
对于改写的解释方式:
代替:在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。
改进:实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。
重载
具有相同的名称,但是形参列表不同,返回值可以相同也可以不同。发生在同一个类中。
如果有多个重载函数,里面是不同的类的类型,决定使用哪个方法。
编译器匹配步骤:
- 寻找所有可能调用的方法。如果找到可以完全匹配所使用参数的方法。那么执行这个方法。结束
- 否则,对于所有可能调用的方法。检查是不是集合中的某个方法A可以把参数列表赋值给某个方法B,如果可以,那么就把方法B删掉。
- 最终删的只剩下一个方法。就执行此方法,否则无法执行。
Void order (Dessert d, Cake c);//e1
Void order (Pie p, Dessert d);//e2
Void order (ApplePie a, Cake c);//e3
order (aPie , aChocolateCake);//use1
order(anApplePie,aChocolateCake)//use2
use1无法调用,将会报错。因为,根据use1去选择,向上造型,可以选择e1和e2。然后e1内的参数不能造型成e2中的,e2中的也不能造型成e1中的。发生错误。
use2会调用e3。先使用use2去选择,发现都可以由use2向上造型。所以三个都保留,然后去检查他们之间的相互关系。发现e3可以造型成e1,所以删除e1,因为e3可以造型成e2,所以删除e2。最后只保留e3。
重定义
名称相同,参数列表可以相同也可以不同。要求父类的函数是非虚函数。也叫做隐藏,只要子类有一个与父类名称相同的函数,那么就会隐藏父类的这个函数,不论签名是否相同。
- | 名称 | 返回值 | 参数列表 | 父类函数 | 范围 | 绑定时机 |
---|---|---|---|---|---|---|
重载 | 相同 | 均可 | 不同 | - | 类内部 | 编译时 |
重定义 | 相同 | 均可 | 不同 | - | 类之间 | 编译时 |
遮蔽 | 相同 | 相同 | 相同 | 非虚函数 | 类之间 | 编译时 |
重写(改写) | 相同 | 相同 | 相同 | 虚函数 | 类之间 | 运行时 |
在C++中,当父类不是虚函数,并且子类中声明了一个相同名称的函数时,父函数会被立刻隐藏,从子类中消失。使用虚函数可以重定义,从而实现多态。
多态的绑定
静态绑定:
-
基于范畴的重载
-
基于签名的重载
同一个类中,签名不同。
-
重定义
子类与父类签名不同。
有融合模型(java)和分级模型(C++)。
动态绑定:
- 改写:子类与父类签名相同。
比较:
动态绑定效率低、灵活、复杂
静态绑定效率高、不灵活、简单。
多态的形式
-
重载(专用多态)
-
改写(包含多态):子类与父类使用相同名称相同。
-
多态变量:多态变量是指可以引用多种对象类型的变量。这种变量在程序执行过程可以包含不同类型的数值。
多态变量包含四种形式
-
简单变量:子类赋值给父类声明的变量
-
接收器变量:this,self等。在对象内部,虽然不指明是this.valueName。但是默认是该对象的变量。如果指向父类的一个方法,会根据对象实际类型来绑定到子类对象上。
-
反多态:可以定义一个父类类型,然后将他赋值为一个子类实例。然后可以把这个父类类型的子类实例,赋值给一个子类类型。这是取消多态赋值的过程,也成为反多态。
Animal aPet=new Dog(); Dog d; d = (Dog) aPet;//反多态
可以使用instanceof检测一个对象是不是属于某个类。用于进行反多态。
Child aChild; If (aVariable d Child ) aChild = ( Child ) aVariable;
-
纯多态(多态方法):多态变量中的父类类型改为Object。高级别的抽象。通过给方法的接收器发送延迟消息来实现。
-
-
泛型(模板)
将类或者函数参数化的方法。将名称定义为类型参数。编译时无法知道类型的属性。
优点:
-
消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。
-
避免由于数据类型的不同导致方法或类的重载。
-
类型安全。 泛型的一个主要目标就是提高程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高。
-
协方差与反协方差
如图,子类的方法中,把Mammal特化成了Cat,这是协方差变换。把Mammal变成了更一般的Animal,这是反协方差变换。
把一个子类对象声明成父类类型,会先去父类中找对应声明,然后绑定到子类中。注意此时各种类型的变化。
ppt上的例子
答案是3 2 3 1 1 1 3 3 3
子类的第一个是对父类的重定义,第二个是对父类的改写。
框架
对于一类相似问题的骨架解决方案
通过类的集合形成,类之间紧密结合,共同实现对问题的可复用解决方案
冒泡排序的例子:冒泡排序作为基本方法,两个元素之间怎么比较放到延迟方法里。冒泡排序的方法就是一个框架。
用延迟方法封装可变性。
在框架中
-
控制流是由框架决定的,并且随应用程序的不同而不同
-
新的应用程序的创建者只需改变供框架调用的例程即可,而无需改变总体结构.
-
框架占主导地位,而应用程序特定的代码处于次要位置.
Class Fruit {
public:
virtual void print (ostream &)=0;//纯虚方法
};
Template <class T>
Class FruitAdapter : public Fruit {
Public:
FruitAdapter (T & f):theFruit (f){ }
T & value ( ){ return theFruit ;}
virtual void print ( ostream & out ){print(theFruit,out);}
Public:
T & theFruit;
};
Apple anApple(“Rome”);
Orange anOrange;
List<Fruit *> fruitList;
fruitList.insert(newFruit(anApple));
fruitList.insert(newFruit(anOrange));
List<Fruit *> ::iterator start = fruitList.begin();
List<Fruit *> ::iterator stop = fruitList.end();
For ( ;start!=stop; ++start) {
Fruit & aFruit = *start;
aFruit.print(cout);
}
内存分配
三种分配方式:
- 最小静态空间分配:只分配基类所需的存储空间。比如C++。把子类赋值给父类的时候,子类拥有父类没有的属性和方法会丢失。
- 最大静态空间分配:无论基类还是派生类,都分配可用于所有合法的数值的最大存储空间。
- 动态内存分配:只分配用于保存一个指针所需的存储空间。在运行时分配对象所需的存储空间。
静态存储分配:编译时就知道每个数据目标的存储空间需求。
栈存储分配:数据区的需求在编译时未知,运行时才确定。但是在进入到一个程序模块时必须知道该模块大小。
堆存储分配:在运行时进入程序模块时也不知道内存大小,比如对象实例和可变长度串。