Inheritance
- C++ supports inheritance like many object-oriented languages. Inheritance is the ability to declare a class (called the child class or subclass) in such a way that it obtains all of the fields (called the parent class or superclass) and methods of another class.
- It is best used when two classes exhibit an is-a relationship. E.g. an imageButton class, instead of Button class. It add more features and overrides some behaviors of the parent class.
Button: parent class, super class or base class
ImageButton: child class, subclass or derived class.
Writing Classes with Inheritance
- To use inheritance, the class declaration for the child class has a colon, followed by and access specifier, and then the parent class name between the class's name and the open curly brace of the class declaration.
e.g.
class BankAccount {
double balance;
unsigned long acctNumber;
public:
void deposit(double amount);
double withdraw(double amount);
double getBalance() const;
};
class InvestmentAccount : public BankAccount {
vector<pair<Stock *, double> > stocks;
unsigned tradesThisMonth;
public:
void buyStock(Stock whichStock, double numShares);
void sellStock(Stock whichStock, double numShares);
double getMarketValue() const;
};
- In a child class, we should not declare a field which has the same name as that in its parent class. It is a poor design.
- public BankAccount here shows how the access of inherited members should be changed. Using public inheritance specifies that the access should be unchanged from that declared in the parent class: public members remain public, and private members remain private.
- If we use private BankAccount, then all inherited members become private in the child class.
- A third option is protected. Using protected inheritance makes public members of the parent class into protected members of the child class. Members of a class may be declared protected, which means that they may be accessed by members of the class or by members of any of its child classes.
e.g.
class A {
protected:
int x;
};
class B : public A {
void someFunction() {
x++;
}
};
- Here, the reference inside of class B is legal. However, code outside of class A and B would not be able to access x directly. Or this class can declare other classes as friends to grant them access to private / protected members.
- C++ has one slightly subtle restriction on the use of protected members in a child class: they may only be accessed through a pointer/ reference/ variable of the child class's own type.
-Example above, the access to x is performed through the implicit this pointer, which has type B const *, therefore, it's legal according to this rule.
However, for the following
e.g.
class A {
protected:
int x;
};
class B : public A {
void someFunction (A * anA) {
ans->x++;
}
}
- Here we will receive an error. It is because x accessed through type A*
Construction and Destruction
- When objects that use inheritance are constructed or destroyed, the constructors and destructors for their parent classes are run to initialize/cleanup the parent portion of the objects.
- The first thing that happens when creating a new object is the constructor for its parent-most ancestor is run to initialize that portion of the object. Then, the constructor for the next closest ancestor is run, etc.
- When it comes to destruction, it is run in the reverse order: form the class itself, up the chain of parent classes.
e.g.
class A {}
class B : public A{}
class C: public B {}
- In C++, the type of an object actually changes during this construction process. The type of the object is initially set to A, only after the constructor for A finishes, does the type of the object change to B. Then after the constructor for B completes, the type of the object becomes C.
- The destruction process happens in reverse. However, it stops if any parent class's destructor is trivial.
- When destroying a C, C's destructor executes first if it's nontrivial. Then the destructor for any fields in C (in reverse order) executes. Then, if the parent class's destructor is nontrivial, it is called.
- The type of the object changes from C to B once control enters B's destructor, right before any of the code in B's destructor begins. After B's destructor completes and its fields are destructed, A's destructor must be run unless it is trivial.
- Once control enters A's destructor, the type of the object changes to A.
- After all nontrivial destructors complete, the memory allocated to the object can be released.
- Java does not have destructors in the same fashion as C++.
- If the programmer does not explicitly specify a call to the parent class's constructor, then the default constructor is implicitly used. If there's no default constructor, or the default constructor is private, then it will produce an error.
- If the programmer wishes to call some other constructor explicitly, then they can write the call to the parent class's constructor as the first element of the initializer list, by writing the parent class's name, then parentheses with the argument list.
Subtype Polymorphism
- One way to increase code reuse is through polymorphism. The form of polymorphism we saw before is parametric polymorphism because the type is a parameter.
- Another form of polymorphism, which is related to inheritance, is subtype polymorphism.
- Subtype polymorphism arises when one type (InvestmentAccount) is a subtype of another type. (BankAccount), meaning that an instance of InvestmentAccountis substitutable for an instance of BankAccount.
- In C++ or Java, a child class is a subtype of its parent class.
- Polymorphism is restricted by the access modifier used in inheriting the parent class. If public inheritance is used , then it is freely used anywhere.
- If it is private or protected inheritance, then polymorphism is only permissible where a field with that access restriction could be used. (In the class or its friends for private, or in the class, its subclass, or any friends for protected inheritance)
- Polymorphism is only applicable when used with pointers or references.
e.g.
void f(A * a) {
...
}
void g(B * b) {
f(b);
}
- Here, function f is declared to take a pointer to an A. In g, b is a pointer to a B. C++ treats b as if it is an instance of its parent class.
- The benefits of subtype polymorphism is easy to see:
e.g.
class Bank {
vector<BankAccount *> allAccounts;
public:
InvestmentAccount * createInvestmentAccount(double balance) {
InvestmentAccount * newAccount = new InvestmentAccount(balance);
allAccounts.push_back(newAccount);
return newAccount;
}
void accrueInterestOnAllAccounts() {
vector<BankAccount *>::iterator it = allAccounts.begin();
while (it != allAccounts.end()) {
BankAccount * currentAccount = *it;
currentAccount->accrueInterest(fractionOfYear);
++it;
}
}
}
- In this example, we can push investmentAccount into allAccounts because C++ knows an InvestmentAccount is a BankAccount.
- When dealing with pointers ot objects in the presence of polymorphism, it's important to understand the difference between the static type and the dynamic type of the object it points at.
- Static type is the type obtained by the type checking rules of the compiler, which only uses the declared types of variables. The dynamic type of the object is the type of object that is actually pointed at.
e.g.
BankAccount * b = new InvestmentAccount();
- Here, the static type of *b is BankAccount. b is declared as a pointer to a BankAccount. So, no matter what type it actually points at, the static type is the type obtained by the type checking rules of the compiler. It only uses the declared types of variables.
- The dynamic type of the object is the type of object that is actually pointed at. Here, the dynamic type of *b is InvestmentAccount.
Method Overriding
- A child class may override method in its parent class, specifying a new behavior for that method rather than using the one it inherits.
- We can just write another method with the same name and with same parameter list in the child class.
e.g.
#include <iostream>
#include <cstdlib>
class A {
public:
void sayHi() {
std::cout << "Hello from class A\n";
}
};
class B : public A {
public:
void sayHi() {
std::cout << "Hello from class B\n";
}
};
int main(void) {
A anA;
B aB;
A * ptr = &aB
anA.sayHi();
aB.sayHi();
ptr->sayHi();
};
- Here, the output will be:
Hello from class A
Hello from class B
Hello from class A
- The first two output is easy to get. However, for the third output, A's sayHi was invoked for the ptr->sayHi. The approach of having the static type determine which method to call is called static dispatch. This is the default behavior in C++ unless we request otherwise.
- However, it disagrees with what we typically would want in the way we would use inheritance and polymorphism.
- The behavior we desire is dynamic dispatch. We want the method in its dynamic type to be called. If we want a method to be dynamically dispatched, we have to declare it as virtual.
e.g.
#include <iostream>
#include <cstdlib>
class A {
public:
virtual void sayHi() {
std::cout << "Hello from class A\n";
}
};
class B : public A {
public:
virtual void sayHi() {
std::cout << "Hello from class B\n";
}
};
int main(void) {
A anA;
B aB;
A * ptr = &aB
anA.sayHi();
aB.sayHi();
ptr->sayHi();
};
- In this way, the output will be:
Hello from class A
Hello from class B
Hello from class B
- When the static and dynamic types are the same, this change does not make any difference. However, when the static and dynamic types differ, the result changes.
- The declaration of the method as virtual must appear in the parent class. It's because the compiler only knows the static type of ptr. It looks in the definition of class A to see whether sayHi should be statically or dynamically dispatched.
- The compiler then generates different code based on whether the function is virtual or not.
- Once a method is called virtual, it remans virtual in all child classes even if not explicitly declared so. However, itis good to explicitly declare them virtual to make the behavior clearer to anyone reading the code.
- Classes that contain virtual methods are never POD(plain old data) types, as they contain extra information to allow dynamic dispatch.
- An object with at least one virtual method has an extra field that is particular to its type.
- In C++, whenever you use a class that may participate in polymorphism, its destructor should be declared virtual to avoid improperly destroying objects.
- The default option is static dispatch also for destructor. Therefore, we should also declare destructor as virtual.
- If we want to call the parent class's version of a method, we can do so by explicitly requesting it with the fully qualified name.
e.g.
InvestmentAccount::buyStock(s, numShare);
- Overridden method may have a more permissive access restriction. If the parent declares the method as private, the child could declare its overridden version as public. However, we cannot declare a public to be private.
- An overridden method may change the return type in a covariant fashion, meaning that the return type in the subclass is a subtype of the return type in the superclass.
e.g.
class Animal {
public:
virtual Animal * getFather() {
...
}
virtual Animal * getMother() {
...
}
};
class Cat : public Annimal {
public:
virtual Cat * getFather() {
...
}
virtual Cat * getMother() {
...
}
};
- In this example, the overriding is legal. But if the method returns Animal or Cat (by value, not by pointer), then this overriding would be illegal. As polymorphism only works on pointers or references.
Abstract Methods and Classes
- We can write a method in the parent class and tell the compiler "there is no way I can define this method in this class, but any child class of mine must override this method with a real implementation". Such a method is called an abstract method or a pure virtual member function. We declare a virtual method as abstract by placing = 0; at the end of its declaration:
i.e.
class Shape {
public:
virtual bool containsPoint(const Point & p) const = 0;
};
- Abstract methods must be virtual, as it only makes sense to use them with dynamic dispatch.
- When a class has an abstract method in it, that class becomes an abstract class.
- There are a few special rules that go along with abstract classes.
1) An abstract class cannot be instantiated. We cannot use new Shape, nor can we declare a variable to have type shape. However, we can have type Shape*, Shape &. These pointers and references can be used to polymorphically to reference an instance of a concrete subclass of Shape - Circle, Rectangle or Triangle which have implemented actual methods for abstract.
2) Any subclass of an abstract class is also abstract unless it defines concrete implementations for all abstract methods in its parents.
- These two rules ensures that the object we actually instantiate will have an implementation for all of the methods declared in it.
Inheritance and Templates
- Templates are not fully composable with inheritance, mostly with respect to the rules that relate to virtual methods.
Aspects That are Composable
- It's fine to have a templated class inherit from another class, to inherit from an instantiation of a templated class, or to mix the two.
e.g.
template<typename T>
class MyFancyVector : public std::vector<T> {
...
};
- We can also do the following:
template<typename T>
class MyClass : public T {
...
};
- This is called mixin.
- It is also fine for a templated class to have virtual methods.
e.g.
template<typename T>
class MyClass {
public:
virtual int computeSomething(int x) {
...
}
virtual void someFunction() = 0;
virtual ~MyClass() {}
};
Aspects That are Not Composable
- A templated method cannot be virtual.
i.e.
class MyClass {
public:
template<typename X> virtual
int doSomething() {
...
}
}
- The above code is illegal.
- If we want to have a variety of virtual methods with similar functionality in the base class, we can instead make a protected non-virtual template, and have non-templated methods call it.
i.e.
class MyClass {
protected:
template<typename X>
int doSomething_implementation(const X & arg) {
...
}
public:
virtual int doSomething(const int & arg) {
return doSomething_implementation<int> (arg);
}
virtual int doSomething(const double & arg) {
return doSomething_implementation<double> (arg);
}
};
-Suppose we have a parent class andn a child class
class Parent {
public:
virtual void something() {
std::cout << "Parent::something\n";
}
};
class Child : public Parent {
public:
template<typename T> void something() {
std::cout << "Child::something\n";
}
};
If we have:
Parent * p = new Child();
p->something();
- Then it will print Parent::something because the method has to be virtual to override a virtual method.
Planning Your Inheritance Hierarchy
1. Determine what classes you need and what member they have
2. Loop for similarities between classes. Is there's similarities, we can determine if we can pull the similarities out into another class that exhibits an "is-a" relationship with the classes in question.
3. Look to see if there are anything with natural "is-a" relationship that are not related by inheritance. If so, consider making one subclass of the other.
4. Repeat step 2 and 3 until you run out of opportunities for good uses of inheritance.
5. Determine which classes should be abstract.
summary& understanding after reading <<All of Programming>> Chapter 18
Comments
Post a Comment