c++ primer 第十五章面向对象程序设计

15.1 OOP:概述

面向对象程序设计的核心思想是数据抽象、继承和动态绑定。

  • 使用数据抽象使类的接口与实现分离。
  • 使用继承定义相似的类型并对其相似关系建模。
  • 使用动态绑定一定程度上忽略相似类型的区别,以统一的方式使用它们的对象。

继承将相似的类联系在一起构成一种层次关系。其中有基类和派生类。

基类希望它的派生类各自定义适合自身版本的函数,在基类中声明为虚函数。

类派生列表表示派生类从哪个(哪些)基类中继承而来。

派生类必须在内部对重新定义的虚函数进行声明。可以使用override关键字表明其对于虚函数改写的显示注明。

动态绑定是指在函数调用时函数的运行版本由实参决定,即运行时选择函数的版本,因此有时也叫作运行时绑定。
在C++中,使用基类的引用或指针调用一个虚函数时将发生动态绑定。

15.2 定义基类和派生类

15.2.1 定义基类

继承关系中根节点的类通常都会定义一个虚析构函数。

基类必须将两种成员函数区分开来:一种是基类希望派生类进行覆盖的函数,定义为虚函数。第二种是不需要覆盖的。
关键字virtual声明虚函数,任何构造函数之外的非静态函数都可以是虚函数。非虚函数解析过程发生在编译时而非运行时。

派生类能够访问基类的公有成员和保护成员,但不能访问私有成员。

15.2.2 定义派生类

派生类需要指明派生类列表指明其继承的基类。形式是冒号后面跟着的逗号分隔的基类列表,每个基类前面可以有访问说明符:public、protected和private。

派生类必须将其继承而来的成员函数中需要覆盖的那些重新声明。

如果一个派生是公有的,则基类的公有成员也是派生类接口的组成部分。可以将公有派生类型的对象绑定到基类的引用或者指针上。

派生类经常覆盖它继承的虚函数。如果派生类没有覆盖基类某个虚函数,派生类会直接继承其在基类中的版本。

派生类对象包含多个组成部分:一个含有类自己定义的(非静态)成员的子对象,以及一个或多个与该派生类继承的基类对应的子对象。对象中继承自基类的部分和派生类自定义的部分不一定是连续储存的。

尽管派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。需要调用基类的构造函数初始化基类部分,而且要先初始化基类部分。

派生类的作用域相当于嵌套在基类的作用域之内。因此派生类的成员函数调用派生类成员和调用基类成员方式一样。

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。

派生类的声明不需要包含派生列表。在类定义的类名后面加final关键字可以防止类被继承。

15.3 虚函数

必须为每个虚函数都提供定义,因为编译器也无法确定使用哪个虚函数。

对于虚函数的调用可能在运行时才会被解析,比如在使用动态绑定的时候即使用基类引用或者基类指针调用虚函数的时候。

一个派生类覆盖了基类的虚函数那么这个新的函数依然是虚函数。形参和返回类型和函数名必须完全相同才能覆盖。除非返回类型分别是各自类型的指针时返回类型可以不同。

如果派生类定义函数名相同其它有所不同的函数无法覆盖基类虚函数,可能会导致未预料的错误。关键字overrid表明该函数是一个覆盖基类虚函数的函数,出现上述问题编译器会报错。而final关键字表明该虚函数无法被继续覆盖。

虚函数也可以有默认实参,而默认实参的值是根据静态类型来定,即使用基类指针调用派生类的虚函数使用的是基类的默认实参,因此若虚函数有默认实参在不同类型中应该一致。

如果像指定调用的虚函数的版本,可以直接使用作用域运算符指定,如baseP->Quote::net_price(42)。调用在编译时完成解析。

15.4 抽象基类

纯虚函数无需定义,在类内部函数声明后加=0表明该函数是纯虚函数。也可以为其提供定义,但必须在类外部。

含有纯虚函数的类是抽象基类。抽象基类只负责定义接口,后续的其它类可以覆盖该接口。不能直接创建一个抽象基类的对象。

派生类的构造函数只初始化它的直接基类。每个派生类构造函数调用直接基类的构造函数。

15.5 访问控制和继承

protected关键字声明那些它希望与派生类分享但是不想被其他公共访问使用的成员。

  • protected声明的成员对于类的用户来说是不可访问的。
  • 受保护的成员对于派生类的成员和友元函数是可访问的。
  • 派生类的成员或友元只能通过派生类对象访问基类的受保护成员。即派生类的成员只能访问派生类对象中的基类部分的受保护成员而不能直接访问其它的基类对象的受保护成员。

类对继承而来的成员的访问权限受两个因素影响:一是基类中该成员的访问权限,二是在派生类中的派生列表的访问说明符。

访问说明符只影响派生类的用户代码,目的是控制派生类用户对于基类成员的访问权限。

派生类向基类的转换不仅由代码决定,派生类的派生访问说明符也有影响:

  • 当D公有地继承B时,用户代码才能使用派生类到基类的转换。
  • 不论D怎么继承B,D的成员函数和友元都能转换。
  • 如果D继承B的方式是公有的或者受保护的,则D的派生类的成员和用户代码都可以转换。
    总的来说,如果基类的公有成员是可访问的,那么就可以转换。

友元关系不能传递也不能继承。

可以直接使用using加作用域符号将特定的基类成员改变访问权限。只能声明那些可以访问的名字。

class和struct的区别在于默认的继承方式也不同。

15.6 继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型和动态类型不一样,能够使用哪些成员依然由静态类型决定。

派生类也能重用定义在其直接基类或间接基类的名字,此时定义在派生类中的名字将隐藏基类中的名字。也可以直接使用作用域说明符直接调用基类中的名字。一般除了继承来的虚函数不建议重用其它基类中的名字。

声明在内层作用域的函数并不会重载外层作用域的函数,因此定义派生类的函数也不会重载其基类中的成员。如果同名则外层的名字隐藏。

假如基类和派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针调用派生类的虚函数了。

假如派生类想要覆盖基类的重载成员函数,那么需要覆盖0个或所有。或者使用using加作用域说明符名字来只使用一次覆盖。

15.7 构造函数与拷贝控制

15.7.1 虚析构函数

基类通常应该定义一个虚析构函数,这样我们就能动态分配继承体系中的对象了。delete动态分配的指针时可能出现动态类型和静态类型不同,因此需要虚析构函数。

析构函数的虚属性会被继承。因此只需要将基类析构函数声明为虚函数即可。而且有虚析构函数的类不一定需要其它控制函数。而有了虚析构函数还会阻止合成移动操作。

15.7.2 合成拷贝控制与继承

合成拷贝控制成员与其它合成的函数类似,对类本身的成员依次进行初始化、赋值或销毁。此外,这些合成的成员还负责使用直接基类中对应的操作对直接基类部分进行操作。比如合成构造函数调用直接基类的构造函数,合成析构函数调用直接基类的析构函数。

某些定义基类的方式有可能导致派生类成员成为被删除的函数。

由于基类一般含有虚析构函数,因此不含合成的移动函数。如果需要移动操作需要单独定义。

15.7.3 派生类的拷贝控制成员

派生类的构造函数不仅需要初始化派生类的成员,还要调用基类的构造函数。拷贝和赋值构造函数类似。但析构函数只负责销毁派生类自己分配的资源。

默认情况下,基类的默认构造函数初始化对象的基类部分。因此如果需要显示构造基类部分需要先调用其构造函数。派生类赋值运算符也是相似。

派生类析构函数只需要负责销毁派生类自己分配的资源。基类的析构函数会被自动调用。销毁顺序与构造顺序相反。

在派生类函数进行构造或者析构时,对象都处于一种未完成的状态,而调用不同的构造函数和析构函数。编译器认为对象的类型在构造或析构的过程中仿佛发生变化。

15.7.4 继承的构造函数

C++11中可以重用直接基类的构造函数。一个类只初始化其直接基类,因此也只继承其直接基类的构造函数。使用using Disc_quote::Disc_quote;语句可以继承Disc_quote的构造函数。编译器为这种语句生成一个构造函数,形如:derived(parms) : base(args) {};

构造函数的using声明不改变该构造函数的访问级别。且using声明语句不能指定explicit或constexpr。基类构造函数的默认实参不会被继承而是生成多个有不同形参的继承的构造函数。

有两种例外,其它情况下派生类会继承所有基类的构造函数。第一个是继承一部分,其他部分派生类自己定义。第二个是不能继承默认、拷贝和移动构造函数。

15.8 容器与继承

一般来说存放具有继承关系的对象时,存放的通常是基类的指针或者智能指针。

如果定义需要隐藏指针类型的函数,需要动态申请内存但不知道对象的动态类型,可以在基类中定义一个返回当前类型对象的拷贝的虚函数。因为虚函数的返回类型可以是当前类型的指针,所以可以解决。

猜你喜欢

转载自blog.csdn.net/qq_25037903/article/details/82964862