面向对象编程基础
面向对象编程基于三个基本概念:
- 数据抽象-类
- 继承-基类/派生类
- 动态绑定-基类的函数or派生类的函数
面向对象编程概述
面向对象编程的关键思想是多态性(polymorphism)
。多态性派生于一个希腊单词,意思是“许多形态”,之所以称通过继承而相关联的类型为多态类型,是因为在许多情况下可以互换地使用派生类型或基类型的“许多形态”。在C++中,多态性仅用于通过继承而相关联的类型的引用或指针。
在C++中,基类必须指出希望派生类重写哪些函数,定义为virtual
的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
在C++中,通过基类的引用(或指针)调用虚函数时,发生动态绑定
。引用(或指针)既可以指向基类对象也可以指向派生类对象
,这一事实是动态绑定的关键。用引用(或指针)调用的虚函数在运行时确定,被调用的函数是引用(或指针)所指对象的实际类型所定义的。引用和指针的静态类型与动态类型可以不同,这是 C++ 用以支持多态性的基石。
基类
- 保留字
virtual
的目的是启用动态绑定。成员默认为非虚函数
,对非虚函数的调用在编译时确定。 - 为了指明函数为虚函数,在其返回类型前面加上保留字virtual。
除了构造函数之外,任意非static成员函数都可以是虚函数
。 - 保留字只在
类内部的成员函数声明中出现,不能用在类定义体外部出现的函数定义上。
- 基类通常应将派生类需要重定义的任意函数定义为虚函数。
- 已定义的类才可以用作基类(这一规则暗示着不可能从类自身派生出一个类)
访问控制
- 用户代码可以访问类的public成员而不能访问private成员,private成员只能由基类的成员和友元访问。
-
派生类对基类的public和private成员的访问权限
与程序中任意其他部分一样:它可以访问public成员
而不能访问private成员
。 -
有时作为基类的类具有一些成员,它希望允许派生类访问但仍禁止其他用户访问这些成员。对于这样的成员应使用受保护的访问标号。
protected成员可以被派生类对象访问但不能被该类型的普通用户访问。
- 派生类只能通过派生类对象访问其基类的protected成员,派生类对其基类类型对象的protected成员没有特殊访问权限。[Code1]
- 提供给派生类型的接口是
protected
成员和public
成员的组合 - C++语言不要求编译器将对象的基类部分和派生部分和派生部分连续排列(内存空间)
派生类
- 定义派生类,使用类派生列表指定基类,类派生列表指定了一个或多个基类
class classname: access-label base-class
- 尽管不是必须这样做,派生类一般会重定义所继承的虚函数。派生类没有重定义某个虚函数,则使用基类中定义
- 派生类中
虚函数的声明必须与基类中的定义方式完全匹配
, 但有一个例外:返回对基类型的引用(或指针)的虚函数。派生类中的虚函数可以返回基类函数所返回类型的派生类的引用(或指针)。
- 一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用virtual保留字,但不是必须这样做。
- 如果需要声明(但并不实现)一个派生类,则声明包含类名但不包含派生列表。[Code2]
virtual与其他成员函数
- C++中的函数调用默认不使用动态绑定,触发动态绑定条件:
- 只有指定为
虚函数的成员函数才能进行动态绑定
- 必须通过
基类类型的引用或指针进行函数调用
- 只有指定为
- 因为每个派生类对象都包含基类部分,所以可将基类类型的引用绑定到派生类对象的基类部分,也可以用指向基类的指针指向派生类对象。
- 使用基类类型的引用或指针时,不知道指针或引用所绑定的对象的类型,
编译器都将它当作基类类型对象
。 - 基类类型引用和指针的关键点在于
静态类型
(在编译时可知的引用类型或指针类型)和动态类型
(指针或引用所绑定的对象的类型这是仅在运行时可知的)可能不同。 - 非虚函数总是在编译时根据调用该函数的对象、引用或指针的类型而确定
-
覆盖虚函数机制
- 使用作用域操作符强制函数调用使用虚函数的特定版本[Code3]
- 在派生类中虚函数调用基类版本时,必须显式使用作用域操作符,如果派生类函数忽略了这样做,则函数调用会在运行时确定并且将是一个自身调用,从而导致无穷递归
-
虚函数与默认实参
- 像其他任何函数一样,虚函数也可以有默认实参,如果有用在给定调用中的默认实参值,该值将在
编译时确定
。 - 如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的
类型定义,与对象的动态类型无关
。 通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值
。- 在同一虚函数的基类版本和派生类版本中使用不同的默认实参几乎一定会引起麻烦。
- 像其他任何函数一样,虚函数也可以有默认实参,如果有用在给定调用中的默认实参值,该值将在
为什么会希望覆盖虚函数机制?
最常见的理由是为了派生类虚函数调用基类中的版本。在这种情况下,基类版本可以完成继承层次中所有类型的公共任务,而每个派生类型只添加自己的特殊工作。
例如,可以定义一个具有虚操作的Camera类层次。Camera类中的display函数可以显示所有的公共信息,派生类(如PerspectiveCamera)可能既需要显示公共信息又需要显示自己的独特信息。可以显式调用Camera版本以显示公共信息,而不是在PerspectiveCamera的display实现中复制Camera的操作。 在这种情况下,已经确切知道调用哪个实例,因此,不需要通过虚函数机制。
继承
公用、私有和受保护的继承
- 派生类可以定义零个或多个访问标号,指定跟随其后的成员的访问级别。
- 对类所继承的成员的访问由基类中的成员访问级别和派生类派生列表中使用的访问标号
共同控制
。 - 每个类控制它所定义的成员的访问,
派生类可以进一步限制但不能放松对所继承的成员的访问
。 - 派生类中的访问标号决定派生类成员的访问级别[Code4/5]
-
公用继承
,基类的public成员为派生类的public成员,基类的protected成员为派生类的protected成员。 -
受保护继承
,基类的public和protected成员在派生类中为protected成员。 -
私有继承
,基类的的所有成员在派生类中为private成员。
-
-
在派生类的成员函数内部访问基类的成员,访问级别取决于基类的类型(可访问public/private)
[Code4] 在派生类定义与实现的外部,即派生类的对象或者继承自派生类的类,派生类成员访问级别取决于派生类继承基类的访问标号
[Code5]- 派生类可以恢复继承成员的访问级别,但不能使访问级别比基类中原来指定的更严格或更宽松。[Code6]
-
class
保留字定义的派生默认具有private继承
,而用struct
保留字定义的类默认具有public继承
。
友元关系和继承
- 像其他类一样,基类或派生类可以使其他类或函数成为友元,友元可以访问类的private和protected数据。
-
友元关系不能继承
。基类的友元对派生类的成员没有特殊访问权限。如果基类被授予友元关系,则只有基类具有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
静态成员和继承
- 如果基类定义static成员,则整个继承层次中只有一个这样的成员,无论从基类派生出多少个派生类,每个static成员只有
一个实例
。 - static成员遵循常规访问控制,如果成员在基类中为private,则派生类不能访问它。
- 假定可以访问成员,则既可以通过基类访问static成员,也可以通过派生类访问static成员。一般而言,既可以使用
作用域操作符
也可以使用点或箭头成员访问操作符
。
Code
Code1:派生类只能通过派生类对象访问其基类的protected成员
/*假定 Bulk_item 定义了一个成员函数,接受一个 Bulk_item 对象的 引用和一个 Item_base 对象的引用,该函数可以访问自己对象的 protected 成 员以及 Bulk_item 形参的 protected 成员,但是,它不能访问 Item_base 形 参的 protected 成员。*/ void Bulk_item::memfcn(const Bulk_item &d, const Item_base &b) { // attempt to use protected member double ret = price; // ok: uses this->price ret = d.price; // ok: uses price from a Bulk_item object ret = b.price; // error: no access to price from an Item_base } /*d.price 的使用正确,因为是通过 Bulk_item 类型对象引用 price;b.price 的 使用非法,因为对 Base_item 类型的对象没有特殊访问访问权限。*/
Code2:声明(但并不实现)一个派生类
//error: a forward declaration must not include the derivation list class Bulk_item : public Item_base; //正确的前向声明为: //forward declarations of both derived and nonderived class class Bulk_item; class Item_base;
Code3:覆盖虚函数机制
Item_base *baseP = &derived; //calls version from the base class regardless of the dynamic type of baseP double d = baseP->Item_base::net_price(42);
Code4:在派生类的成员函数内部访问基类的成员,访问级别取决于基类的类型
class Base { public: void basemem(); protected: int i; // ... }; struct Public_derived : public Base { int use_base() { return i; } // ok: derived classes can access i // ... }; struct Private_derived : private Base { int use_base() { return i; } // ok: derived classes can access i };
Code5:在派生类定义与实现的外部,即派生类的对象或者继承自派生类的类,派生类成员访问级别取决于派生类继承基类的访问标号
Base b; Public_derived d1; Private_derived d2; b.basemem(); // ok: basemem is public d1.basemem(); // ok: basemem is public in the derived class d2.basemem(); // error: basemem is private in the derived class //派生访问标号还控制来自非直接派生类的访问: struct Derived_from Private : public Private_derived { // error: Base::i is private in Private_derived int use_base() { return i; } }; struct Derived_from_Public : public Public_derived { // ok: Base::i remains protected in Public_derived int use_base() { return i; } };
Code6:派生类可以恢复继承成员的访问级别
class Base { public: std::size_t size() const { return n; } protected: std::size_t n; }; class Derived : private Base { . . . }; /*在这一继承层次中,size 在 Base 中为 public,但在 Derived 中为 private。为了使 size 在 Derived 中成为 public,可以在 Derived 的 public 部分增加一个 using 声明。如下这样改变 Derived 的定义,可以使 size 成员 能够被用户访问,并使 n 能够被从 Derived 派生的类访问:*/ class Derived : private Base { public: // maintain access levels for members related to the size of the object using Base::size; protected: using Base::n; // ... };