本章主要介绍类的继承相关内容
在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:
(1)有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
(2)常量对象可以调用 const 成员函数,而不能调用非const修饰的函数。
c++允许将方法标记为final,这意味着该方法不能在派生类中被重写。尝试覆盖final修饰的方法会导致编译错误。
有时,可能会意外地创建一个新的虚函数,而没有覆盖基类的方法(函数名写错了或者后续修改了基类函数)。这时候可以采用override写在函数后面来避免这种情况。
class Base { public: virtual void someMethod(double d); }; class Derived : public Base { public: virtual void someMethod(double d); };
如果派生类使用的内存是在构造函数中动态分配并在析构函数中释放的,则如果从来没有调用析构函数,则永远不会释放它(如采用基类指针释放)。类似地,如果派生类的成员在释放类的实例时被自动删除,例如std::unique_ptrs,那么如果从未调用析构函数,这些成员也不会被释放。
class Base { public: Base() {} ~Base() {} }; class Derived : public Base { public: Derived() { mString = new char[30]; cout << "mString allocated" << endl; } ~Derived() { delete[] mString; cout << "mString deallocated" << endl; } private: char* mString; }; int main() { Base* ptr = new Derived(); // mString is allocated here delete ptr;// ~Base is called, but not ~Derived because the destructor is not virtual return 0; }
除非有特定的理由不这样做,或者类被标记为final,否则建议将所有方法(包括析构函数,但不包括构造函数)都设为虚方法。构造函数不能也不需要是虚函数,因为在创建对象时总是指定要构造的确切类。
对象不会一下子突然产生,它们必须与它们的父对象以及包含在其中的任何对象一起构造。c++定义了如下的创建顺序:
class Something { public: Something() { cout << "2"; } //step2: non-static member }; class Base { public: Base() { cout << "1"; } //step1: base class constructor }; class Derived : public Base { public: Derived() { cout << "3"; } //step3: child class constructor private: Something mDataMember; }; int main() { Derived myDerived; return 0; }
由于 析构函数不接受参数,所以总是可以自动调用父类的析构函数。析构的顺序恰好与构造的顺序相反:
对象可以强制转换或分配给它的父类。如果对普通旧对象执行强制转换或赋值,则会导致切片:
Base myBase = myDerived; // Slicing!
在这种情况下会发生切片,因为最终结果是一个Base对象,而Base对象缺少派生类中定义的附加功能。但是,如果将派生类分配给其基类的指针或引用,则不会发生切片
Base& myBase = myDerived; // No slicing!
这种方式通常称为upcasting。
与之相反,由基类向派生类强制转换成为downcasting,downcasting有时是必要的。但是,如果要进行向下downcasting,应该使用dynamic_cast()!dynamic_cast()只适用于具有虚函数表的对象,即至少具有一个虚成员的对象。如果dynamic_cast()在指针上失败,指针的值将是nullptr,而不是指向无意义的数据。
class Dog { public: virtual void bark() { cout << "Woof!" << endl; } virtual void eat() { cout << "The dog ate." << endl; } }; class Bird { public: virtual void chirp() { cout << "Chirp!" << endl; } virtual void eat() { cout << "The bird ate." << endl; } }; class DogBird : public Dog, public Bird { }; int main() { DogBird myConfusedAnimal; myConfusedAnimal.eat(); // Error! Ambiguous call to method eat() return 0; }
解决二义性的方法是使用dynamic_cast()(终于找到基类指针的作用了)显式地向上转换对象,从本质上对编译器隐藏该方法不需要的版本,或者使用消除二义性语法。例如,下面的代码显示了两种调用eat()的Dog版本的方法:
dynamic_cast<Dog&>(myConfusedAnimal).eat(); // Calls Dog::eat() myConfusedAnimal.Dog::eat(); // Calls Dog::eat()
当然,派生类本身的方法也可以通过使用访问父方法所用的相同语法,即::作用域解析操作符,显式地消除相同名称的不同方法之间的歧义。例如,DogBird类可以通过定义自己的eat()方法来防止其他代码中的歧义错误。在这个方法内部,它将决定调用哪个父版本:
class DogBird : public Dog, public Bird { public: void eat() override; }; void DogBird::eat() { Dog::eat(); // Explicitly call Dog's version of eat() }
还有一种情况是菱形继承关系,如图:
使用这些“菱形”类层次结构的最佳方法是使最顶层类成为一个抽象基类,并将所有方法声明为纯虚的。因为类只声明方法而不提供定义,所以基类中没有要调用的方法,因此在该级别上不存在二义性。
using关键字在派生类中显式包括方法的基类定义。这对于普通的类方法非常有效,但也适用于构造函数,允许您从基类继承构造函数(继承除默认构造函数以外的所有构造函数)。下面是基类和派生类的定义:
class Base { public: virtual ~Base() = default; Base() = default; Base(std::string_view str); }; class Derived : public Base { public: Derived(int i); };
只能使用提供的Base构造函数来构造Base对象,可以是默认构造函数,也可以是带string_view形参的构造函数。另一方面,只能使用提供的Derived构造函数构造Derived对象,该构造函数需要一个int作为参数。不能使用接受基类中定义的string_view的构造函数来构造派生对象。
Base base("Hello"); // OK, calls string_view Base ctor Derived derived1(1); // OK, calls integer Derived ctor Derived derived2("Hello"); // Error, Derived does not inherit string_view ctor
如果想要使用基于string_view的Base构造函数构造派生对象,你可以显式地继承派生类中的Base构造函数,如下所示:
class Derived : public Base { public: using Base::Base; //显式继承 Derived(int i); }; Derived derived1(1); // OK, calls integer Derived ctor Derived derived2("Hello"); // OK, calls inherited string_view Base ctor
首先,方法不能同时是静态的和虚的。如果有一个静态方法与基类中的静态方法同名的派生类,实际上有两个独立的方法。下面的代码显示了两个碰巧都有名为beStatic()的静态方法的类。这两种方法没有任何关联:
class BaseStatic { public: static void beStatic() { cout << "BaseStatic being static." << endl; } }; class DerivedStatic : public BaseStatic { public: static void beStatic() { cout << "DerivedStatic keepin' it static." << endl; } };
在c++中,你可以使用一个对象来调用一个静态方法,但是由于这个方法是静态的,它没有this指针,也没有访问对象本身的权限,所以它等同于通过类名::method()来调用它。参考前面的示例类,您可以编写如下代码,但结果可能会令人惊讶。
DerivedStatic myDerivedStatic; BaseStatic& ref = myDerivedStatic; myDerivedStatic.beStatic(); ref.beStatic();
对beStatic()的第一个调用显然调用了DerivedStatic版本,因为它是在声明为DerivedStatic的对象上显式调用的。
第二个调用,该对象是一个BaseStatic引用,但它引用了一个DerivedStatic对象。在这种情况下,调用BaseStatic的beStatic()版本。原因是c++在调用静态方法时并不关心对象实际上是什么。它只关心编译时类型。在本例中,类型是对BaseStatic的引用。(参考前一段内容,等同于通过类名::method()来调用它,他的类型是基类,与具体的对象是什么没有任何关系)
class Base { public: virtual ~Base() = default; virtual void overload() { cout << "Base's overload()" << endl; } virtual void overload(int i) { cout << "Base's overload(int i)" << endl; } }; class Derived : public Base { public: virtual void overload() override { cout << "Derived's overload()" << endl; } };
如果试图调用在派生对象上接受int形参的重载()版本,代码将无法编译,因为它没有显式override:
Derived myDerived; myDerived.overload(2); // Error! No matching method for overload(int).
如果想通过派生对象访问该方法的这个版本。你所需要的只是一个指向Base对象的指针或引用:
Derived myDerived; Base& ref = myDerived; ref.overload(7);
在c++中,未实现的重载方法只是表面上的隐藏。显式声明为派生类实例的对象不能使这些方法可用,但对基类的简单强制转换就可以使它们返回。
using关键字可以在真正想要更改一个版本时避免重载所有版本的麻烦。在下面的代码中,派生类定义使用Base的重载()的一个版本,并显式重载另一个版本:
class Base { public: virtual ~Base() = default; virtual void overload() { cout << "Base's overload()" << endl; } virtual void overload(int i) { cout << "Base's overload(int i)" << endl; } }; class Derived : public Base { public: using Base::overload; virtual void overload() override { cout << "Derived's overload()" << endl; } };
重写私有或受保护的方法绝对没有错。方法的访问说明符决定了谁能够调用该方法。仅仅因为派生类不能调用其父类的私有方法并不意味着它不能重写它们。事实上,覆盖私有或受保护的方法是c++中的一种常见模式。它允许派生类定义在基类中引用的自己的“惟一性”。Java和c#只允许重写公共和受保护的方法,而不允许重写私有方法。