现在的位置: 首页 > 综合 > 正文

C++ Primer学习笔记——$15 面向对象编程

2018年03月30日 ⁄ 综合 ⁄ 共 4499字 ⁄ 字号 评论关闭
题记:本系列学习笔记(C++ Primer学习笔记)主要目的是讨论一些容易被大家忽略或者容易形成错误认识的内容。只适合于有了一定的C++基础的读者(至少学完一本C++教程)。
 
作者: tyc611, 2007-01-31


   本文主要讨论C++中继承和动态绑定,它们和数据抽象一起构成面向对象编程的基础。
   如果文中有错误或遗漏之处,敬请指出,谢谢!


   面向对象编程基于三个基本概念:数据抽象(data abstraction)、继承(inheritance)和动态绑定(dynamic binding)。面向对象编程的关键思想是多态性(polymorphism)。在C++中,多态性仅用于通过继承而相关联的类型的引用或指针。


继承
   派生类可以进一步限制但不能放松对所继承的成员的访问。派生类不能访问基类的private成员,可以访问public和protected成员(注意:虽然不能访问私有成员,私有成员仍被继承)。而对于基类中的public和protected成员在派生类中的访问控制是否改变,则取决于派生列表中使用的访问标号:
   1)如果是公用继承(public inheritance),基类成员保持自己的访问级别;
   2)如果是受保护继承(protected inheritance),基类的public和protected成员在派生类中为protected成员。
   3)如果是私有继承(private inheritance),基类的所有成员在派生类中为private成员。私有继承是默认继承行为。(注意:这里所讲的所有都是针对class保留字定义的类型,而struct保留字的默认行为都是公有的)
 
接口继承与实现继承
 
   public派生类继承基类的接口,在设计良好的类层次中,public派生类的对象可以用在任何需要基类对象的地方。因此,public继承被称为接口继承,是最常用的形式。
   private或protected派生类不继承基类的接口,派生类在实现中使用被继承基类的部分,但并未成为其接口的一部分。因此,这些继承通常被称为实现继承。
 
 
恢复与提升访问控制
 
   从基类继承而来的成员,只要在派生类中可以访问,则可以使用using声明改变它的访问权限。例如:
 

#include <iostream>

class Base {
public:
   Base(std::size_t n): _n(n) {}
   std::size_t size() const {return _n;}
protected:
   std::size_t _n;
};

class Derived: private Base {
public:
   Derived(std::size_t n):Base(n){}
   // restored its public access property
   using Base::size;  
   // changed its access control from protected in base class 
   // to public in derived class
   using Base::_n;    
};

int main()
{
   Derived d(10);
   std::cout<<d.size()<<", "<<d._n<<std::endl;
 
   return 0;
}

 
转换和继承
 
   基类的引用(或指针)能够引用到(或指向)派生类对象。但这种从派生类到基类的转换的可访问性是取决于派生类的派生列表中指定的访问标号(跟继承的成员的可访问性一样):
   1)如果是public继承,则用户代码和后代类都可以使用派生类到基类的转换;
   2)如果是private或protected继承,则用户代码不能使用派生类到基类的转换。如果是private继承,则从private继承类派生的类不能使用派生类到基类的转换;如果是protected继承,则后续派生类的成员可以使用派生类到基类的转换。
   但无论是什么样的派生访问标号,派生类本身的成员和友元总是可以访问这种从派生类到基类的转换。但是有一点与普通的成员不一样,没法使用using来提升其访问控制(因为这是由编译器自动完成的)。
 
   另一种从派生类到基类的转换是,可以用派生类对象对基类对象进行初始化或赋值(前提是前面的第一种转换在当前作用域是可访问的)。因为初始化时调用拷贝构造函数,而赋值时调用赋值操作符,它们的参数都是基类类型的引用,当然基类引用可以引用到派生类对象上。又派生类拥有基类的所有成员,所以能够正常完成初始化或赋值。我们把这种由派生类对象转换得到的基类对象叫做对象切片(object slice)。
 
其它
 
   友元关系不能够被继承。
   如果基类中定义了static成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个static成员只有一个实例。
   继承与组合的选择:派生类与基类应当反映的是“Is A”关系,而组合反映的是“Has A”关系。
 


构造函数和拷贝控制
   构造函数和拷贝控制成员不能继承,每个类定义自己的构造函数和拷贝控制成员。像任何类一样,如果类不定义自己的默认构造函数和拷贝控制成员,就将使用合成版本。
 
   派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。相反,派生类构造函数通过将基类构造函数包含在构造函数初始化列表中来间接初始化继承成员。并且,派生类只能初始化直接基类,而不能初始化间接基类。派生类的构造过程首先初始化基类,然后根据声明次序初始化派生类的成员。
 
   如果派生类定义了自己的拷贝构造函数,该拷贝构造函数一般应显式使用基类拷贝构造函数初始化对象的基类部分。同样,派生类在定义自己的赋值操作符时应该显示调用基类赋值操作符对基类进行赋值(形式有点古怪,见下面的示例)。派生类析构函数不负责撤销基类对象的成员,编译器总是显式调用派生类对象基类部分的析构函数,每个析构函数只负责清除自己的成员。对象的撤销顺序与构造顺序相反:首先运行派生类析构函数,然后按继承层次依次向上调用各基类析构函数。
 

类作用域
   在继承情况下,派生类的作用域嵌套在基类作用域中。
 
   对象、引用或指针的静态类型决定了对象能够完成的行为。甚至当静态类型和动态类型可能不同的时候,静态类型仍然决定着可以使用什么成员。例如,使用基类类型的引用或指针去引用派生类对象时,只能使用基类成员。
 
   如果派生类成员与基类成员同名,则由作用域规则,派生类成员将屏蔽基类成员。这时,可以使用作用域操作符访问被屏蔽成员。特别是,即使派生类与基类中同名的成员函数的函数原型不同,基类成员也会被屏蔽。因为编译器一旦在派生类中找到了名字,就不会在外层作用域(基类)中继续查找了。
 
   因此,一旦派生类中重定义了基类中的重载成员,则通过派生类只能访问派生类中重定义的那些成员。如果派生类想通过自身类型使用所有的重载版本,则派生类必须要么重定义所有重载版本,要么一个也不重定义。但有时确实需要仅仅重定义某些版本,并且想要在派生类也能使用其它版本,而全部重定义显然太繁琐,这时可以通过使用using声明:using BaseName::funcName; 从而把该函数的所有重载版本加到派生类的作用域中。
 
   理解C++中继承层次的关键在于理解如何确定函数调用。确定函数调用遵循如下四个步骤:
   1)首先确定进行函数调用的对象、引用或指针的静态类型。
   2)在该静态类型的类中查找函数,如果找不到,就在直接基类中查找(外层作用域),如此顺着类的继承链往上找,直到找到该函数或者查找完最外层的作用域。如果不能找到该名字,则调用是错误的。注意:不会在派生类中查找。
   3)一旦找到了该名字,就进行常规类型检查,查看该函数调用是否合法。
   4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本;否则,编译器生成代码直接调用函数。
 


虚函数与动态绑定
   除了构造函数,任意非static成员函数都可以是虚函数。保留字virtual只在类内部的成员函数声明中出现,不能用在类定义外部出现的函数定义上。一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,必须对其进行声明,可以使用virtual保留字,但不是必须这样做。派生类中虚函数的声明必须与基类中的定义方式完全匹配,但有一个例外:返回对基类型的引用(或指针)的虚函数,此时,派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。一般情况下,都应当把基类析构函数声明为虚函数。
 
   在函数声明的形参表后面写上 =0,这样的虚函数叫纯虚函数(不用定义)。含有(或继承)一个或多个纯虚函数的类叫抽象基类(abstract base class),不能创建抽象基类的对象。
 
   C++中要触发动态绑定,必须满足两个条件:第一,只有指定为虚函数的成员函数才能进行动态绑定;第二,必须通过基类类型的引用或指针进行函数调用。引用和指针的静态类型与动态类型可以不同,这是C++用以支持多态性的基石。
 
   在有些情况下,希望覆盖虚函数机制并强制函数调用使用虚函数的特定版本。这时,可以使用作用域操作符来指定相应类的特定版本。这种情况常见于派生类虚函数调用基类中的版本,基类版本实现公共任务,派生类只添加自己的特殊工作代码。注意:这种情况一定要记得使用作用域操作符,否则会导致派生类中的虚函数版本无穷递归调用。
 
   虚函数也可以有默认参数,但同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。如果虚函数有默认实参并且在调用虚函数时省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值;如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。由此可见,当通过基类的引用或指针调用派生类中的虚函数版本时,基类版本的默认参数将传递给派生类版本的虚函数,将引起混乱。
 
   在运行构造函数和析构函数的时候,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在构造或析构期间发生了变化。在基类构造函数或析构函数中,将派生类对象当作基类类型对象对待。如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。无论由构造函数(或析构函数)直接调用虚函数,或者从构造函数(或析构函数)所调用的函数间接调用虚函数,都应用这种绑定规则。
 

   如果文中有错误或遗漏之处,敬请指出,谢谢!


 

参考文献:
[1] C++ Primer(Edition 4)
[2] Thinking in C++(Volume Two, Edition 2)
[3] International Standard:ISO/IEC 14882:1998

抱歉!评论已关闭.