Skip to main content

Inheritance

 

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

Popular posts from this blog

Templates

  Template - Polymorphism is the ability of the same code to operate on different types. This ability to operate on multiple types reduces code duplication by allowing the same piece of code to be reused across the different types it can operate on. - Polymorphism comes in a variety of forms. What we are interested in at the moment is parametric polymorphism, meaning that we can write our code so that it is parameterized over what type it operates on.  -That is, we want to declare a type parameter T and replace int with T in the above code. -Then, when we want to call the function, we can specify the type for T and get the function we desire. C++ provides parametric polymorphism through templates. Templated Functions - We can write a templated function by using the keyword template followed by the template parameters in angle brackets (<>). - Unlike function parameters, template parameters may be types, which are specified with typename where the type of the parameter wo...

前端 优化代码体积

当我使用npm run build的时候,项目构建了很久。所以考虑用create-react-app网站下面的工具来缩小代码体积  Analyzing the Bundle Size https://create-react-app.dev/docs/analyzing-the-bundle-size 更改完成后 npm run build npm run analyze 可以看到以下的图片: 其中main.js有1.57mb 然后起服务 serve -s build -l 8000 进入到首页之后,打开network,查看js,发现main.js有500kb。这个500kb是已经用gzip压缩过了,但是却还有这么大。500*3=1500说明源文件有1.5mb左右 其中, antd占了25%, recharts占了13%, react-dom占了7.6%,dnd-kit占了2.8% 其中recharts用于统计页面,dnd-kit用于拖拽排序-编辑器页面。 所以在加载首页的时候,先不加载编辑页面和统计页面的js的话,体积会小很多。 路由懒加载 因为项目中,体积占比最大的是Edit和Stat页面(编辑和统计页面),所以考虑使用路由懒加载,拆分bundle,优化首页体积 router文件中,之前: import Edit from "../pages/question/Edit" ; import Stat from "../pages/question/Stat" ; 现在: const Edit = lazy (() => import ( "../pages/question/Edit" )); const Stat = lazy (() => import ( "../pages/question/Stat" )); 为了让生成的文件更加可读,可以改成下面这样: const Edit = lazy ( () => import ( /* webpackChunkName: "editPage" */ "../pages/question/Edit" ) ); const Stat ...

useMemo的使用场景

 useMemo是用来缓存 计算属性 的。 计算属性是函数的返回值,或者说那些以返回一个值为目标的函数。 有些函数会需要我们手动去点击,有些函数是直接在渲染的时候就执行,在DOM区域被当作属性值一样去使用。后者被称为计算属性。 e.g. const Component = () => { const [ params1 , setParams1 ] = useState ( 0 ); const [ params2 , setParams2 ] = useState ( 0 ); //这种是需要我们手动去调用的函数 const handleFun1 = () => { console . log ( "我需要手动调用,你不点击我不执行" ); setParams1 (( val ) => val + 1 ); }; //这种被称为计算属性,不需要手动调用,在渲染阶段就会执行的。 const computedFun2 = () => { console . log ( "我又执行计算了" ); return params2 ; }; return ( < div onClick = { handleFun1 } > //每次重新渲染的时候我就会执行 computed: { computedFun2 () } </ div > ); }; 上面的代码中,在每次点击div的时候,因为setParams1的缘故,导致params1改变,整个组件都需要重新渲染,也导致comptedFunc2()也需要重新计算。如果computedFunc2的计算量很大,这时候重新计算会比较浪费。 可以使用useMemo: const Com = () => { const [ params1 , setParams1 ] = useState ( 0 ); const [ params2 , setParams2 ] = useState ( 0 ); //这种是需要我们手动去调用的函数 const handleFun1 ...