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
构造函数。因为Car
和Motorcycle
都继承自Vehicle
类,所以它们都继承了它的属性。它们都包含一个保存车轮数量的变量,并且都有一个检索车轮数量的方法。您可以在main
函数中看到这一点,其中GetNumberOfWheels
在myCar
对象和myMotorcycle
对象上都被调用。图 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_NumberOfWheels = 3;
cout << "A car has " << myCar.m_NumberOfWheels << " wheels." << endl;
Motorcycle myMotorcycle;
cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;
myMotorcycle.m_NumberOfWheels = 3;
cout << "A motorcycle has " << myMotorcycle.m_NumberOfWheels << " wheels." << endl;
return 0;
}
任何具有public
访问权限的变量都可以被派生类访问。Car
构造器和Motorcycle
构造器都利用了这一点,并适当地设置了它们拥有的轮数。缺点是其他代码也可以访问公共成员变量。你可以在main
函数中看到这一点,其中m_NumberOfWheels
被读取并分配给myCar
对象和myMotorcycle
对象。图 6-2 显示了该代码生成的输出。
私有访问说明符
您可以将变量设为私有并为其提供公共访问器,而不是将其设为公共。清单 6-3 显示了私有成员变量的使用。
清单 6-3 。private
访问说明符
#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
变量的使用。Car
和Motorcycle
类不再能直接访问m_NumberOfWheels
变量;因此,Vehicle
类提供了一种通过其构造函数初始化变量的方法。这使得类更难处理,但是增加了不允许任何外部代码直接访问成员变量的好处。您可以在main
函数中看到这一点,其中的代码必须通过GetNumberOfWheels
访问器方法获得车轮的数量。
受保护的访问说明符
protected
访问说明符允许混合使用public
和private
访问说明符。对于从当前类派生的类,它就像一个public
说明符,对于外部代码,它就像一个private
说明符。清单 6-4 展示了这种行为。
清单 6-4 。protected
访问说明符
#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 显示了Car
和Motorcycle
都可以直接从它们的父类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 中看到这段代码生成的输出。
通过对象或指向这些类类型的指针直接访问这些方法会产生正确的输出。
注意当你使用多态时,方法隐藏不能正常工作。通过指向基类的指针访问派生类会导致基类上的方法被调用。这很少是你想要的行为。使用多态性时的正确解决方案见配方 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 中的Car
和Motorcycle
类是从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
方法。Vehicle
和Motorcycle
对象在它们的 v 表中有Vehicle::GetNumberOfWheels
方法的地址;因此,两者都为它们的轮数返回 2。
Car
类覆盖了GetNumberOfWheels
方法。这使得Car
用Car::GetNumberOfWheels
的地址替换查找表中Vehicle::GetNumberOfWheels
的地址。因此,当同一个Vehicle
指针被分配了myCar
的地址并随后调用GetNumberOfWheels
时,它调用的是Car
类中定义的方法,而不是Vehicle
类中定义的方法。图 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. Car
和Motorcycle
都覆盖了这个方法并且可以被实例化。您可以在main
功能中看到这种情况。图 6-5 显示这些方法返回了Car
和Motorcycle
的正确值。
包含纯虚拟方法的类被称为接口。如果一个类从一个接口继承,并且您希望能够实例化该类,您必须重写父类中的任何纯虚方法。可以从一个接口派生而不覆盖这些方法,但是这个派生类只能作为进一步派生类的接口。
食谱 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;
}
Car
和Motorcycle
类都来自多个父类。这些类现在都是Vehicle
和Printable
的。你可以在被覆盖的Print
方法中看到两个父类之间的相互作用。这些方法都调用了Car
和Motorcycle
中被覆盖的GetNumberOfWheels
方法。main
函数通过指向Printable
对象的指针访问被覆盖的Print
方法,使用多态调用正确的Print
方法以及Print
中正确的GetNumberOfWheels
方法。图 6-6 显示程序输出正确。
图 6-6 。显示多重继承与多态性一起工作的输出