五、面向对象编程风格

  • 类间的关系依赖于 面向对象编程模型(object-oriented programming model)加以设定。
  • 面向对象编程最主要的特质继承(inheritance)和多态(polymorphism)。前者将一群相关的类组织起来,得以分享其间的共通数据和操作行为,后者让我们如同操控单一个体而非相互独立的类,并赋予加入和移除任何特定类的弹性。
  • 继承机制定义了父子关系。父类定义了所有子类共通的公有接口(public interface)和私有实现(private implementation)。每个子类都可以增加或覆盖(override)继承而来的东西,以实现其自身独特的行为。
  • C++中,父类被称为基类(base class),子类被称为派生类(derived class)。父类和子类之间的关系称为继承体系(inheritance hierarchy)。所谓继承体系,是由最抽象的部分推演至最具体的部分。
  • 抽象基类(abstract base class)指那些并不代表实际某个类别,仅仅为设计上的需要而存在的基类,用来定义共通的操作行为。
  • 多态让基类的pointer或reference得以透明地(transparently)指向任何一个派生类的对象。除非程序实际执行的当下,否则无法确定指向的是哪一个派生对象,且每次函数执行情况都可能不同。
  • 动态绑定(dynamic binding):解析操作“找出实际被调用的是哪一个派生类的函数”会延迟至运行时才进行。
  • 静态绑定(static binding):程序执行之前就已经解析出应该调用哪一个函数。
  • 继承特性让我们得以定义一组互有关联的类并共享共通的接口。多态让我们得以用一种与类型无关(type-independent)的方式来操作这些类对象。
  • 多态和动态绑定的特性只有在使用pointer或reference时才发挥作用。
  • 默认情形下,member function的解析皆在编译时静态进行。若要令其在运行时动态进行,我们要在其声明前加上关键字virtual,表示其为虚函数
  • 当程序定义出一个派生对象,基类和派生类的constructor都会被执行;当派生对象被销毁,基类和派生类的destructor也都会被执行(但次序颠倒,先派生后基)。
  • 定义派生类时,为了清楚标示这个新类是继承自一个已存在的类,其名称后必须接一个冒号:,然后紧跟着关键字public和基类的名称。(*基类可以是public、protected或private三种方式继承而来,本书仅讨论public)。
  • 类进行继承声明之前,其基类的定义必须已经存在(需先行包含含有基类定义的头文件)。
  • 被声明为protected的所有成员都可以被派生类直接访问,除此之外都不能直接访问protected成员。
  • public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性:
    • public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private;
    • protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private;
    • private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private。
  • 但无论哪种继承方式,下面两点都没有改变:
    • private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
    • protected 成员可以被派生类访问。
  • 成员和类的默认访问修饰符是 private
  • 使用派生类时不必刻意区分 继承而来的成员 和 自身定义的成员。两者的使用完全透明。
  • 定义抽象基类的步骤:
    • 第一步:找出所有子类共通的操作行为。这些行为代表的便是基类的公有接口。
    • 第二步:设法找出哪些操作行为与类型相关(type-dependent),即哪些操作行为必须根据不同的派生类而有不同的实现方式。这些行为应该成为整个类继承体系中的虚函数(virtual function)。* static member function无法被声明成虚函数。
    • 第三步:试着找出每个操作行为的访问层级(access level)。若一般程序皆能访问则为public;若在基类之外不需要被用到则为private(即使是该基类的派生类也无法访问基类中的private member);可以让派生类访问却不允许一般程序访问则为protected
  • 将虚函数赋值为0便使其成为一个纯虚函数(pure virtual function)——如对于该类而言,这个虚函数并无实质意义。纯虚函数没有函数定义,接口不完整。
  • 任何类如果声明有一个(或多个)纯虚函数,程序无法为其产生任何对象。这种类只能作为其派生类的子对象使用,并且前提是这些派生类必须为所有虚函数提供确切的定义。
  • 根据一般规则,凡基类定义有一个(或多个)虚函数,应该要将其destructor声明为virtual:
//定义基类指针ps指向派生类Fibonacci的对象 
num_sequence *ps = new Fibonacci(12);
//...使用数列
delete ps; 


/*
当delete表达式应用于ps时,destructor会先应用于ps所指
的对象身上,于是此对象占用的内存空间归还。 

non-virtual函数在编译时便已完成解析,所以ps调用的
destructor一定是Fibonacci的,而不是基类的。 

正确做法是根据实际对象的类型选择调用哪一个destructor, 
因此解析操作应该在运行时进行。 
*/ 
  • 派生类由两部分组成:基类构成的子对象(由基类的non-static data member组成) + 派生类的部分(由派生类的non-static data member组成)。
  • 派生类的虚函数必须精确吻合基类中的函数原型。在类之外对虚函数进行定义时,不必指明关键字virtual
  • 通过class scope运算符指明调用对象,可以跳过虚函数机制,使函数在编译时就完全解析,不必等到运行时才解析。
  • 每个派生类有某个member与其基类的member同名,便会遮掩住基类的那份member。如果要在派生类内使用继承来的那份member,必须利用class scope运算符加以限定
  • 如果基类和派生类中的同名函数为non-virtual的,那么每次通过基类的pointer或reference来调用该函数,解析出来的都是基类的那一份。
  • data member选择指针还是引用:
    • reference必须再constructor的member initialization list中加以初始化。一旦初始化,就再也无法指向另一个对象。
    • pointer可以在constructor内加以初始化,也可以初始化为null再令它指向某个有效的内存地址。
  • 派生类的constructor不仅必须为派生类的data member进行初始化操作,还需要为其基类的data member提供适当的值。
  • 不同于constructor,基类的destructor会在派生类的destructor结束之后被自动调用。我们无须在派生类中对它做明确的调用操作。
  • 如果我们继承了纯虚函数,那么这个派生类也会被视为抽象类,也就无法为它定义任何对象。
  • 如果覆盖基类所提供的虚函数,那么派生类提供的新定义其函数原型必须完全符合基类所声明的函数原型:参数列表、返回类型、常量性(const-ness)。
    • 例外:当基类的虚函数返回某个基类形式(通常是pointer或reference)时,派生类中的同名函数便可以返回该基类所派生出来的类型:
      class num_sequence {
              
              
      public:
      	//派生类的clone()函数可以返回一个指针
      	//指向num_sequence的任何一个派生类 
      	virtual num_sequence *clone() = 0;
      	
      	//...
      }; 
      
      class Fibonacci : public num_sequence  {
              
              
      public:
      	//Fibonacci派生自num_sequence
      	//在派生类中,关键字virtual并非必要
      	Fibonacci *clone() {
              
               return new Fibonacci( *this ); }
      	
      	//... 
      };
      
  • 派生类覆盖虚函数而进行声明操作时,不一定加关键字virtual编译器会依据两个函数的原型声明,决定某个函数是否会覆盖其基类的同名函数。
  • 虚函数机制的例外:(1)基类的constructor和destructor内,(2)使用的是基类的对象,而非基类对象的pointer或reference时。

当我们构造派生类对象时,基类的constructor先被调用。如果在基类的constructor中调用某个虚函数,会调用派生类所定义的那一份吗?
答:不会。此时派生类的data member尚未初始化,此时调用可能会访问到未初始化的data member发生错误,因此,在基类的constructor内,派生类的虚函数绝对不会被调用。在基类的destructor中调用虚函数同理。

为了能够在单一对象中展现多种类型,多态需要一层间接性。
在C++中,唯有用基类的pointer和reference才能够支持面向对象编程的概念。

void print (LibMat object,
			const LibMat *pointer,
			const LibMat &reference)
{
    
    
	//以下必定调用LibMat::print()
	object.print();
	
	//以下一定会通过虚函数机制来进行解析,
	//我们无法预知哪一个派生类的print()被调用
	pointer->print();
	reference.print(); 
} 

int main()
{
    
    
	AudioBook iWish("Her Pride of 10",
					"Stanley Lippman",
					"Jeremy Irons");
	print( iWish, &iWish, iWish );
	//...
}

如上所示,我们为基类声明一个实际对象(print()的第一参数),同时也就分配了足以容纳该实际对象的内存空间。如果稍后传入的是一个派生类对象(AudioBook),那就没有足够的内存放置派生类中的各个data member。
只有iWish内的基类子对象(即属于LibMat的成分)被复制到为参数object而保留的内存中,其他子对象则被切掉(sliced off)了。
至于另两个参数pointerreference,则被初始化为iWish对象所在的内存地址。这便是它们能够指向完整AudioBook对象的原因。

  • typeid运算符,由程序语言支持,是运行时类型鉴定机制(Run-Time Type Identification, RTTI)的一部分。让我们得以查询多态化的class pointer或class reference,获得其所指对象的实际类型。
  • 头文件#include <typeinfo>typeid运算符会返回一个 type_info 对象,其中储存着与类型相关的各种信息。每一个多态类都对应一个 type_info 对象,该对象的name()函数会返回一个 const char* ,用以表示类名。
    #include <typeinfo>
    
    inline const char* num_sequence:: what_am_i() const
    {
          
          
    	return typeid( *this ).name();
    }
    
  • type_info class也支持相等和不等两个比较操作。
  • static_cast<>其实有潜在危险,因为编译器无法确认我们所进行的转换操作是否完全正确。dynamic_cast运算符提供有条件的转换,是一个RTTI运算符。
  • 具象(concrete)基类与抽象(abstract)基类相对,能表现出应用程序中实际存在的对象。
  • member function是一种特殊的函数。两者都有返回类型、函数名、参数列表、函数定义。不过member function附属于某个class之下,它可能是virtual,可能是const,也可能是static……。
  • constructor本身是一种特殊的member function。

猜你喜欢

转载自blog.csdn.net/pppppppyl/article/details/114155754