Skip to content

Latest commit

 

History

History
764 lines (544 loc) · 21.2 KB

File metadata and controls

764 lines (544 loc) · 21.2 KB

六、继承

C++ 允许你以多种方式构建复杂的软件应用程序。其中最常见的是面向对象编程(OOP)范式。C++ 中的类用于为包含数据的对象以及可以对该数据执行的操作提供蓝图。

继承通过让您构造复杂的类层次结构而更进一步。C++ 语言提供了各种不同的特性,您可以使用这些特性以逻辑方式组织代码。

食谱 6-1。从类继承

问题

您正在编写一个程序,它在对象之间有一种自然的 is-a 关系,并且希望减少代码重复。

解决办法

从父类继承类允许您将代码添加到父类中,并在多个派生类型之间共享它。

它是如何工作的

在 C++ 中,你可以从一个类继承另一个类。继承类获得基类的所有属性。清单 6-1 显示了一个从共享父类继承的两个类的例子。

清单 6-1 。类继承

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
    uint32_t m_NumberOfWheels{};

public:
    Vehicle(uint32_t numberOfWheels)
        : m_NumberOfWheels{ numberOfWheels }
    {

    }

    uint32_t GetNumberOfWheels() const
    {
        return m_NumberOfWheels;
    }
};

class Car : public Vehicle
{
public:
    Car()
        : Vehicle(4)
    {

    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle()
        : Vehicle(2)
    {

    }
};

int main(int argc, char* argv[])
{
    Car myCar{};
    cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

Vehicle类包含一个成员变量来存储车辆的车轮数量。默认情况下,该值初始化为 0,或者在构造函数中设置。Vehicle后面是另一个名为Car的类。Car类只包含一个用于调用Vehicle构造函数的构造函数。Car构造函数将数字 4 传递给Vehicle构造函数,因此将m_NumberOfWheels设置为 4。

Motorcycle类也只包含一个构造函数,但是它将 2 传递给了Vehicle构造函数。因为CarMotorcycle都继承自Vehicle类,所以它们都继承了它的属性。它们都包含一个保存车轮数量的变量,并且都有一个检索车轮数量的方法。您可以在main函数中看到这一点,其中GetNumberOfWheelsmyCar对象和myMotorcycle对象上都被调用。图 6-1 显示了这段代码生成的输出。

9781484201589_Fig06-01.jpg

图 6-1 。由清单 6-1 中的代码生成的输出

Car类和Motorcycle类都继承了Vehicle的属性,并且都在它们的构造函数中设置了适当的轮数。

食谱 6-2。控制对派生类中成员变量和方法的访问

问题

您的派生类需要能够访问其父类中的字段。

解决办法

C++ 访问修饰符对在派生类中访问变量的方式有影响。使用正确的访问修饰符是正确构造类层次结构的关键。

它是如何工作的

公共访问说明符

public访问说明符 授予对类中变量或方法的公共访问权。这同样适用于成员变量和方法。你可以在清单 6-2 中清楚地看到这一点。

清单 6-2 。访问说明符

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
    uint32_t m_NumberOfWheels{};

    Vehicle() = default;
};

class Car : public Vehicle
{
public:
    Car()
    {
        m_NumberOfWheels = 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle()
    {
        m_NumberOfWheels = 2;
    }
};

int main(int argc, char* argv[])
{
    Car myCar{};
    cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;
    myCar.m_NumberOfWheels3;
    cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;

    Motorcycle myMotorcycle;
    cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;
    myMotorcycle.m_NumberOfWheels3;
    cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;

    return 0;
}

任何具有public访问权限的变量都可以被派生类访问。Car构造器和Motorcycle构造器都利用了这一点,并适当地设置了它们拥有的轮数。缺点是其他代码也可以访问公共成员变量。你可以在main函数中看到这一点,其中m_NumberOfWheels被读取并分配给myCar对象和myMotorcycle对象。图 6-2 显示了该代码生成的输出。

9781484201589_Fig06-02.jpg

图 6-2清单 6-2 生成的输出

私有访问说明符

您可以将变量设为私有并为其提供公共访问器,而不是将其设为公共。清单 6-3 显示了私有成员变量的使用。

清单 6-3private访问说明符

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
private:
    uint32_t m_NumberOfWheels{};

public:
    Vehicle(uint32_t numberOfWheels)
        : m_NumberOfWheels{ numberOfWheels }
    {

    }

    uint32_t GetNumberOfWheels() const
    {
        return m_NumberOfWheels;
    }
};

class Car : public Vehicle
{
public:
    Car()
        : Vehicle(4)
    {

    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle()
        : Vehicle(2)
    {

    }
};

int main(int argc, char* argv[])
{
    Car myCar{};
    cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

清单 6-3 显示了private访问说明符与m_NumberOfWheels变量的使用。CarMotorcycle类不再能直接访问m_NumberOfWheels变量;因此,Vehicle类提供了一种通过其构造函数初始化变量的方法。这使得类更难处理,但是增加了不允许任何外部代码直接访问成员变量的好处。您可以在main函数中看到这一点,其中的代码必须通过GetNumberOfWheels访问器方法获得车轮的数量。

受保护的访问说明符

protected访问说明符允许混合使用publicprivate访问说明符。对于从当前类派生的类,它就像一个public说明符,对于外部代码,它就像一个private说明符。清单 6-4 展示了这种行为。

清单 6-4protected访问说明符

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
protected:
    uint32_t m_NumberOfWheels{};

public:
    Vehicle() = default;

    uint32_t GetNumberOfWheels() const
    {
        return m_NumberOfWheels;
    }
};

class Car : public Vehicle
{
public:
    Car()
    {
        m_NumberOfWheels = 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle()
    {
        m_NumberOfWheels = 2;
    }
};

int main(int argc, char* argv[])
{
    Car myCar{};
    cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

清单 6-4 显示了CarMotorcycle都可以直接从它们的父类Vehicle中访问m_NumberOfWheels变量。这两个类都在它们的构造函数中设置了m_NumberOfWheels变量。main函数中的调用代码不能访问这个变量,因此必须调用GetNumberOfWheels方法才能打印这个值。

食谱 6-3。隐藏派生类中的方法

问题

您有一个派生类,它需要一个不同于父类提供的行为的方法中的行为。

解决办法

C++ 允许您通过在派生类中定义一个具有相同签名的方法来隐藏父类中的方法。

它是如何工作的

通过在基类中定义具有完全相同签名的方法,可以隐藏父类中的方法。此示例显示派生类如何使用显式方法隐藏来提供不同于父类的功能。当你使用继承时,这是一个需要理解的关键概念,因为它是用来区分类类型层次的主要方法。

清单 6-5 包含一个Vehicle类、一个Car类和一个Motorcycle类。Vehicle类定义了一个名为GetNumberOfWheels的方法,该方法返回 0。在Car类和Motorcycle类中定义了相同的方法;这些版本的方法分别返回 4 和 2。

清单 6-5 。隐藏方法

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
    Vehicle() = default;

    uint32_t GetNumberOfWheels() const
    {
        return 0;
    }
};

class Car : public Vehicle
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const
    {
        return 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle() = default;

    uint32_t GetNumberOfWheels() const
    {
        return 2;
    }
};

int main(int argc, char* argv[])
{
    Vehicle myVehicle{};
    cout << "A vehicle has " << myVehicle.GetNumberOfWheels() << " wheels." << endl;

    Car myCar{};
    cout << "A car has " << myCar.GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    cout << "A motorcycle has " << myMotorcycle.GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

清单 6-5 中的main函数调用GetNumberOfWheels的三个不同版本,并为每个版本返回适当的值。您可以在图 6-3 中看到这段代码生成的输出。

9781484201589_Fig06-03.jpg

图 6-3 。执行清单 6-5 中的代码生成的输出

通过对象或指向这些类类型的指针直接访问这些方法会产生正确的输出。

Image 注意当你使用多态时,方法隐藏不能正常工作。通过指向基类的指针访问派生类会导致基类上的方法被调用。这很少是你想要的行为。使用多态性时的正确解决方案见配方 8-5。

食谱 6-4。使用多态基类

问题

您希望编写泛型代码,它使用指向基类的指针,并且仍然调用派生类中的正确方法。

解决办法

virtual关键字 允许你创建可以被派生类覆盖的方法。

它是如何工作的

关键字virtual告诉 C++ 编译器你希望一个类包含一个虚拟方法表(v-table)。v-table 包含对方法的查找,允许为给定类型调用正确的方法,即使对象是通过指向其父类之一的指针来访问的。清单 6-6 显示了一个类层次结构,它使用virtual关键字来指定一个方法应该包含在类的 v 表中。

清单 6-6 。创建虚拟方法

#include <cinttypes>

class Vehicle
{
public:
    Vehicle() = default;

    virtual uint32_t GetNumberOfWheels() const
    {
        return 2;
    }
};

class Car : public Vehicle
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle() = default;
};

清单 6-6 中的CarMotorcycle类是从Vehicle类派生而来的。Vehicle类中的GetNumberOfWheels方法被列为虚拟方法。这使得通过指针对该方法的任何调用都将通过 v 表来调用。清单 6-7 显示了一个完整的例子,其中的main函数通过一个Vehicle指针访问对象。

清单 6-7 。通过基指针访问虚方法

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
    Vehicle() = default;

    virtual uint32_t GetNumberOfWheels() const
    {
        return 2;
    }
};

class Car : public Vehicle
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle() = default;
};

int main(int argc, char* argv[])
{
    Vehicle* pVehicle{};

    Vehicle myVehicle{};
    pVehicle = &myVehicle;
    cout << "A vehicle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    Car myCar{};
    pVehicle = &myCar;
    cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    pVehicle = &myMotorcycle;
    cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

main函数在第一行定义了一个指向Vehicle对象的指针。然后这个指针被用在每个cout语句中来访问当前对象的GetNumberOfWheels方法。VehicleMotorcycle对象在它们的 v 表中有Vehicle::GetNumberOfWheels方法的地址;因此,两者都为它们的轮数返回 2。

Car类覆盖了GetNumberOfWheels方法。这使得CarCar::GetNumberOfWheels的地址替换查找表中Vehicle::GetNumberOfWheels的地址。因此,当同一个Vehicle指针被分配了myCar的地址并随后调用GetNumberOfWheels时,它调用的是Car类中定义的方法,而不是Vehicle类中定义的方法。图 6-4 显示了清单 6-7 中的代码生成的输出,你可以看到情况就是这样。

9781484201589_Fig06-04.jpg

图 6-4 。执行清单 6-7 中的代码生成的输出

override关键字用在Car类中GetNumberOfWheels方法签名的末尾。该关键字是对编译器的一个提示,即您希望此方法重写父类中的虚方法。如果您输入的签名不正确,或者您正在重写的方法的签名后来被更改,编译器将会引发错误。这个特性非常有用,我推荐你使用它(虽然override关键字本身是可选的)。

食谱 6-5。防止方法重写

问题

您有一个不想被派生类重写的方法。

解决办法

你可以使用关键字final来防止类覆盖方法 。

它是如何工作的

关键字通知编译器你不希望一个虚方法被派生类覆盖。清单 6-8 展示了一个使用final关键字的例子。

清单 6-8 。使用final关键字

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
    Vehicle() = default;

    virtual uint32_t GetNumberOfWheels() const final
    {
        return 2;
    }
};

class Car : public Vehicle
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle() = default;
};

int main(int argc, char* argv[])
{
    Vehicle* pVehicle{};

    Vehicle myVehicle{};
    pVehicle = &myVehicle;
    cout << "A vehicle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    Car myCar{};
    pVehicle = &myCar;
    cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    pVehicle = &myMotorcycle;
    cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

Vehicle类中的GetNumberOfWheels方法使用final关键字来防止派生类试图重写它。这导致清单 6-8 中的代码无法编译,因为Car类试图覆盖GetNumberOfWheels。您可以注释掉此方法来编译代码。

关键字final也可以在一个更长的链中停止方法的进一步重写。清单 6-9 展示了这是如何实现的。

清单 6-9 。防止继承层次结构中的重写

#include <cinttypes>

class Vehicle
{
public:
    Vehicle() = default;

    virtual uint32_t GetNumberOfWheels() const
    {
        return 2;
    }
};

class Car : public Vehicle
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const final
    {
        return 4;
    }
};

class Ferrari : public Car
{
public:
    Ferrari() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 5;
    }
};

Vehicle定义了一个名为GetNumberOfWheels的虚拟方法,该方法返回值 2。Car覆盖这个方法返回 4(这个例子忽略了不是所有的汽车都有四个轮子的事实)并声明这个方法是最终的。不允许从Car派生的其他类覆盖相同的方法。如果需求只需要支持四轮汽车,这对应用程序来说是有意义的。当编译器到达任何从Car派生的类或者从任何其他层次结构中有Car的类派生的类并且试图覆盖GetNumberOfWheels方法时,它将抛出一个错误。

食谱 6-6。创建界面

问题

您有一个基类方法,它不应该定义任何行为,而应该简单地被派生类重写。

解决办法

您可以在 C++ 中创建不定义方法体的纯虚拟方法。

它是如何工作的

你可以在 C++ 中通过在方法签名的末尾添加= 0来定义纯虚方法。清单 6-10 显示了一个例子。

清单 6-10 。创建纯虚拟方法

#include <cinttypes>
#include <iostream>

using namespace std;

class Vehicle
{
public:
    Vehicle() = default;

    virtual uint32_t GetNumberOfWheels() const = 0;
};

class Car : public Vehicle
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 4;
    }
};

class Motorcycle : public Vehicle
{
public:
    Motorcycle() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 2;
    }
};

int main(int argc, char* argv[])
{
    Vehicle* pVehicle{};

    Car myCar{};
    pVehicle = &myCar;
    cout << "A car has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    Motorcycle myMotorcycle;
    pVehicle = &myMotorcycle;
    cout << "A motorcycle has " << pVehicle->GetNumberOfWheels() << " wheels." << endl;

    return 0;
}

Vehicle类将GetNumberOfWheels定义为一个纯虚拟方法。这就确保了Vehicle类型的对象永远不会被创建。编译器不允许这样做,因为它没有一个方法来调用GetNumberOfWheels. CarMotorcycle都覆盖了这个方法并且可以被实例化。您可以在main功能中看到这种情况。图 6-5 显示这些方法返回了CarMotorcycle的正确值。

9781484201589_Fig06-05.jpg

图 6-5 。执行清单 6-10 中的代码生成的输出

包含纯虚拟方法的类被称为接口。如果一个类从一个接口继承,并且您希望能够实例化该类,您必须重写父类中的任何纯虚方法。可以从一个接口派生而不覆盖这些方法,但是这个派生类只能作为进一步派生类的接口。

食谱 6-7。多重继承

问题

您有一个希望从多个父类派生的类。

解决办法

C++ 支持多重继承 。

它是如何工作的

在 C++ 中,可以使用逗号分隔的父类列表从多个父类中派生出一个类。清单 6-11 展示了这是如何实现的。

清单 6-11 。多重继承

#include <cinttypes>
#include <iostream>

using namespace std;

class Printable
{
public:
    virtual void Print() = 0;
};

class Vehicle
{
public:
    Vehicle() = default;

    virtual uint32_t GetNumberOfWheels() const = 0;
};

class Car
    : public Vehicle
    , public Printable
{
public:
    Car() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 4;
    }

    void Print() override
    {
        cout << "A car has " << GetNumberOfWheels() << " wheels." << endl;
    }
};

class Motorcycle
    : public Vehicle
    , public Printable
{
public:
    Motorcycle() = default;

    uint32_t GetNumberOfWheels() const override
    {
        return 2;
    }

    void Print() override
    {
        cout << "A motorcycle has " << GetNumberOfWheels() << " wheels." << endl;
    }
};

int main(int argc, char* argv[])
{
    Printable* pPrintable{};

    Car myCar{};
    pPrintable = &myCar;
    pPrintable->Print();

    Motorcycle myMotorcycle;
    pPrintable = &myMotorcycle;
    pPrintable->Print();

    return 0;
}

CarMotorcycle类都来自多个父类。这些类现在都是VehiclePrintable的。你可以在被覆盖的Print方法中看到两个父类之间的相互作用。这些方法都调用了CarMotorcycle中被覆盖的GetNumberOfWheels方法。main函数通过指向Printable对象的指针访问被覆盖的Print方法,使用多态调用正确的Print方法以及Print中正确的GetNumberOfWheels方法。图 6-6 显示程序输出正确。

9781484201589_Fig06-06.jpg

图 6-6 。显示多重继承与多态性一起工作的输出