面向对象笔记

定义

对象:一个面向对象的程序是由一个相互作用的代理团体组成,这些代理被称作对象。每一个对象承担一个角色。每一个对象都提供一种服务或者执行一种动作,以便为团体中其他对象服务。 对象是独立存在的客观事物,它由一组属性和一组操作构成。

属性操作是对象的两大要素。属性是对象静态特征的描述,操作是对象动态特征的描述。

操作又称为方法或服务,它描述了对象执行的功能。通过消息传递,还可以为其它对象使用。

消息: 面向对象编程中,行为的启动是通过将“消息”传递给负责的代理来完成的。消息对行为的要求进行编码,并随着附加信息(参数)一起传递。

接收器:接受消息的对象,如果接受了消息,那么就要执行相应的方法来实现。每条消息都要有相应的接收器接受

  • 信息隐藏:作为某对象提供的服务的一个用户,只需要知道对象将接受的消息的名字。n不需要知道要完成要求,需要执行哪些动作。 在接收到一条消息后,对象会负责将该项任务完成。

:根据抽象的原则对客观事物进行归纳和划分,只关注与当前目标相关的特征,把具有相同特征的事物归为一个类。它是一个抽象的概念。类是具有相同属性和相同操作(服务)的对象的集合。它包括属性和操作。

继承:类被组织成有单个根节点的树状结构,称为继承层次结构。与类实例相关的内存和行为都会被树结构中的后代自动继承。类可以组织成一个有层次的继承机构。一个子类继承层次树中更高一层的父类的属性。抽象父类是指没有具体实例的类,他只是用来产生子类。

接口:不提供系统实现,接口定义新类型,可以声明变量。类的实例可以赋值给接口类型遍历。

为什么既有接口又有抽象类:提供不同的抽象层次,提高灵活性。抽象类允许有非抽象方法。

组合与继承

- 优点 缺点
继承 减少代码重复,提高可维护性 类之间关系复杂,子类受到父类限制,降低灵活性
组合 实现更复杂的功能,避免继承带来的限制 增加代码复杂度,增加对象之间的依赖关系

静态成员

静态变量分配在全局数据区。静态方法只能使用静态变量。

继承

继承的形式:前两种推荐,中间物种不推荐,最后一种取决于语言是不是直接支持多继承。

替换原则:如果B是A的子类,那么用B来替换A外界将毫无察觉

  • 特殊化继承:

    符合替换原则

    通过is a检查是不是蕴含则继承关系,比如狗is a动物,那么狗就可以继承动物。

    子类型:子类型强调新类具有父类一样的行为

    新类是基类的一个特定类型。

    可以添加新方法,尽量不要改写父方法。

    如果改写,必须符合父类原本定义

  • 规范化继承:

    派生类只实现那些在基类中定义,但是没实现的方法。符合替换原则

  • 构造继承:

    子类从父类继承所有方法,只是修改方法名和参数。

    比如树到独木舟,违反替换原则。形成的子类不是子类型。

  • 泛化继承:

    派生类扩展基类,形成更泛化的抽象。比如window是黑白背景下的显示,泛化继承成colorwindow,变成有颜色的背景。

    必须改写至少父类的一个方法。

  • 扩展继承

    只是添加新行为,不修改从父类继承来的属性。不违反替换原则

  • 限制继承

    子类的行为比父类少

    比如继承双向队列实现堆栈。

    违反替换原则。

  • 变体继承

    比如控制鼠标的代码和控制图形输入板的代码。

    父类和子类都是对方的变体,可以任意选择父子关系。

  • 合并继承(多重继承)

    可以通过合并两个或者更多的抽象特性来形成新的抽象。可以有多个父类。

    问题:

    • 名称歧义。子类的一个名称可能表示多个操作。
    • 对替换的影响。为了解决名称歧义,可能会重命名,这时如果把他放到其中一个父类的列表中,可能方法名会改变。

子类不继承父类构造器

可以使用super调用父类构造器。

多级构造器的调用顺序是自上而下。

多态

多态是指声明的静态类型与实际执行的动态类型不一致。

签名

函数名与形参列表(包括形参的类型和顺序)共同决定了一个函数。称为一个函数的签名。

改写(重写、覆盖、重置、覆写)

返回值、名称和形参列表完全相同,也叫重写,覆盖。并且要求夫类的函数是虚函数。

发生在父类与子类之间,要求父类被重写的函数是虚函数

对于改写的解释方式:

代替:在程序执行时,实现代替的方法完全覆盖父类的方法。即,当操作子类实例时,父类的代码完全不会执行。

改进:实现改进的方法将继承自父类的方法的执行作为其行为的一部分。这样父类的行为得以保留且扩充。

重载

具有相同的名称,但是形参列表不同,返回值可以相同也可以不同。发生在同一个类中。

如果有多个重载函数,里面是不同的类的类型,决定使用哪个方法。

编译器匹配步骤:

  1. 寻找所有可能调用的方法。如果找到可以完全匹配所使用参数的方法。那么执行这个方法。结束
  2. 否则,对于所有可能调用的方法。检查是不是集合中的某个方法A可以把参数列表赋值给某个方法B,如果可以,那么就把方法B删掉。
  3. 最终删的只剩下一个方法。就执行此方法,否则无法执行。

image-20230623214924244

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。高级别的抽象。通过给方法的接收器发送延迟消息来实现。

  • 泛型(模板)

    将类或者函数参数化的方法。将名称定义为类型参数。编译时无法知道类型的属性。

    优点:

    • 消除强制类型转换。泛型可以消除源代码中的许多强制类型转换,这样可以使代码更加可读,并减少出错的机会。

    • 避免由于数据类型的不同导致方法或类的重载。

    • 类型安全。 泛型的一个主要目标就是提高程序的类型安全。使用泛型可以使编译器知道变量的类型限制,进而可以在更高程度上验证类型假设。如果没有泛型,那么类型的安全性主要由程序员来把握,这显然不如带有泛型的程序安全性高

协方差与反协方差

image-20230624002903793

如图,子类的方法中,把Mammal特化成了Cat,这是协方差变换。把Mammal变成了更一般的Animal,这是反协方差变换。

把一个子类对象声明成父类类型,会先去父类中找对应声明,然后绑定到子类中。注意此时各种类型的变化。

ppt上的例子

image-20230624001505170

答案是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++。把子类赋值给父类的时候,子类拥有父类没有的属性和方法会丢失。
  • 最大静态空间分配:无论基类还是派生类,都分配可用于所有合法的数值的最大存储空间。
  • 动态内存分配:只分配用于保存一个指针所需的存储空间。在运行时分配对象所需的存储空间。

静态存储分配:编译时就知道每个数据目标的存储空间需求。

栈存储分配:数据区的需求在编译时未知,运行时才确定。但是在进入到一个程序模块时必须知道该模块大小。

堆存储分配:在运行时进入程序模块时也不知道内存大小,比如对象实例和可变长度串。

posted @ 2023-06-24 00:49  wxyww  阅读(63)  评论(0编辑  收藏  举报