[C + +] three features: encapsulation, inheritance and polymorphism

Access rights

C + + controls the access rights of member variables and member functions through three keywords: public, protected and private. They represent public, protected and private respectively, which are called member access qualifiers.

Inside the class (inside the code defining the class), no matter whether the members are declared as public, protected or private, they can access each other without access restrictions.

Outside the class (outside the code defining the class), members can only be accessed through objects, and members of public attributes can only be accessed through objects, but members of private and private attributes cannot be accessed.

?? Whether public inheritance, private inheritance or protected inheritance, private members cannot be accessed by "derived classes (subclasses)", and public and protected members in the base class can be accessed by "derived classes".

?? For public inheritance, only public members in the base class can be accessed by "derived class objects", while protected and private members cannot be accessed by "derived class objects". For private and protected inheritance, all members in the base class cannot be accessed by derived class objects.

encapsulation

Privatize member variables and provide public getter s and setter s for the outside world to access member variables.

Data and code are bundled together to avoid external interference and uncertain access.

Function:

The objective things are encapsulated into abstract classes, and classes can only allow their own data and methods to be operated by trusted classes or objects, and hide the untrusted information.

For example, public data or methods are decorated with public, while data or methods that you do not want to be accessed are decorated with private.

inherit

Subclasses can have all members (variables, functions) of the parent class

struct Student { 
    int m_age; 
    int m_score;    
    void run() { }  
    void study() { }
};
struct Worker {   
    int m_age;
    int m_salary;    
    void run() { }   
    void work() { }
};
// Here you can see that both students and workers have m_age and void run()
// They can extract the common things and leave the characteristic things behind
// The above code can be changed as follows
struct Person {  
    int m_age;    
    void run() { }  
}
;struct Student : Person { 
    // It is equivalent to that Student inherits the Person class    				
    // Bring all the members in Person  
    int m_score;   
    void study() { }
};
struct Worker : Person {   
    int m_salary;   
    void work() { }
};
  • Relationship description

    • Student is a subclass (subclass, derived class)
    • Person is a superclass
  • effect

    • Avoid duplicate code

Person class: 4 bytes

Student class: 8 bytes (inheriting the Person class)

Worker class: 8 bytes (inheriting the Person class)

If defined

Worker wk;
wk.m_age = 10;
wk.m_salary = 200;

In memory, m_age and M_ The addresses of salary are connected, each accounting for 4 bytes, m_age comes first.

Member variables inherited from the parent class are arranged first.

Member access

There are three ways to access and inherit members:

public > protected > private

  • public

    Public, accessible anywhere (struct default)

  • protected

    Both inside the subclass and inside the current class can be accessed

  • private

    Private, accessible only inside the current class (class default)

  • be careful!! The permission to access members of the parent class inside the subclass is the one with the least permission in the following two items

    1. Access rights of members themselves
    2. The inheritance method of the upper parent class

    It is generally inherited in public, because this can completely inherit the original member permissions of the parent class

struct Person { 
    int m_age; 
};
struct Student : private Person { 
    // amount to
    //  private:
    int m_age;
};
struct Worker : Student {
    // You cannot modify m after inheriting Student here_ Age, because m in the parent class_ Age belongs to private   
    int m_salary;
};
// If the Student inherits Person as public, you can modify m in the Worker_ age
// But if m in Person_ If the age is private, it cannot be modified in the Worker
  • Access permissions do not affect the memory layout of the object

    Even if the private member of the parent class cannot be accessed directly, it can also be accessed indirectly through the public function.

Initialization list

A convenient way to initialize object members

Can only be used in constructors

struct Person {   
    int m_age;  
    int m_height;     
    Person(int age, int height): m_age(age), m_height(height) {}   
    // Equivalent to (assembly code is exactly the same)  
    /*Person(int age, int height) { 
    m_age = age;    
    m_height = height; 
    }*/
};
int main() {  
    Person(18, 180);   
    getchar();  
    return 0;
}
  • be careful! Class is initialized in the order of variable declaration!

    struct Person {  
        int m_age;   
        int m_height;   
        Person(int age, int height): m_height(height), m_age(m_height) {}  
    };
    int main() { 
        Person person(18, 180);
        cout << person.m_age << person.m_height << endl;  
        getchar(); 
        return 0;
    }
    

    m_age in m_height is initialized before, so m is used_ Height to M_ When age is assigned, m_height has not been initialized.

    So the output is:

    -858993460 180
    

    If yes, initialize m first_ Height reinitialization m_age, you can put M_ The value of height is assigned to m_age

    struct Person {  
        int m_height;   
        int m_age;    
        Person(int age, int height): m_height(height), m_age(m_height) {}  
    };
    int main() {   
        Person person(18, 180); 
        cout << person.m_age << person.m_height << endl;   
        getchar(); 
        return 0;
    }
    

    Output is:

    180 180
    
  • If the function declaration and implementation are separate

    The initialization list can only be written in the implementation of the function

    Default parameters can only be written in function declarations

    struct Person { 
        int m_height;   
        int m_age;   
        Person(int age = 0, int height = 0); 
        // The default parameter can only be written in the function declaration};
        Person::Person(int age, int height) m_height(height), m_age(m_height) {} 
        // The initialization list can only be written in the implementation of the function
    

Mutual calls of constructors

struct Person {  
    int m_age;
    int m_height;    
    Person() {    
        m_age = 0;      
        m_height = 0;  
        /*      
        ... There are other functions       
        */    
    }       
    Person(int age, int height) {  
        m_age = age; 
        m_height = height;  
    }   
    // The code of the above two constructors is a little repetitive. You can modify the first constructor as follows   
    /* Person() : Person(0, 0){
    // Must be placed in the initialization list!!!    
    ... Other functions     
    }    */
};
int main() { 
    Person(18, 180);   
    getchar();    
    return 0;
}

If the function implemented by the constructor contains another function, and there are other personalized functions in addition, they can be directly called to each other to reduce repeated code.

  • be careful! Constructor call constructor, must be placed in the initialization list!

    Person() {  
        Person(10, 20); 
        // Equivalent to   
        /*   
        Person person;   
        person.m_age = 10;  
        person.m_height = 20;  
        */   
        // What we want to achieve is 
        /* 
        this->m_age = 10;  
        this->m_height = 20;  
        */  
    }
    

    This does not initialize, but creates a temporary Person object

    10 and 20 are assigned to the new person object

Constructor of parent class

  • The constructor of the subclass will call the parameterless constructor of the parent class by default. Call the constructor of the parent class first and then the constructor of the subclass

    struct Person() {   
        int m_age;     
        Person() {   
            cout << "Person::person" << endl; 
        }   
        //   Person() :person(0) {}
        //   Person(int age) :m_age(age) {}
    };
    struct Student : Person {   
        int m_no;     
        Student() {      
            cout << "Student::student" << endl;  
        }       
        //   Student() :Student(0,0) {}
        //    Student(int age, int no) {}
    };
    int main() {   
        Student student;   
        getchar();   
        return 0;
    } 
    // output
    // Person::person
    // Student::student
    // Two constructors are called
    
  • If the constructor of a subclass explicitly calls the parameterless constructor of the parent class, it will not call the parameterless constructor of the parent class by default

    struct Person() {   
        int m_age;  
        Person() {     
            cout << "Person::person" << endl; 
        }   
        Person(int age) {     
            cout << "Person::Person(int age)" << endl;  
        }
    };
    struct Student : Person { 
        int m_no;  
        Student() :Person(10){ 	
            // The parameterized constructor of the parent class was explicitly called  
            cout << "Student::student" << endl;  
        }
    };
    int main() {  
        Student student; 
        getchar();  
        return 0;
    } 
    // output
    // Person::Person(int age)
    // Student::student
    
  • If the parent class lacks a parameterless constructor, the constructor of the child class must explicitly call the parameterless constructor of the parent class

    struct Person() {  
        int m_age;  
        Person(int age) {     
            cout << "Person::Person(int age)" << endl;  
        }
    };
    struct Student : Person {  
        int m_no;      
        Student() { 	
            // An error will be reported because the parent class lacks a parameterless constructor and must explicitly call the parameterless constructor of the parent class    
            cout << "Student::student" << endl; 
        }
    };
    int main() {
        Student student;  
        getchar();
        return 0;
    } 
    

Tectonic and destructional sequence

struct Person() {   
    Person() {  
        cout << "Person::Person()" << endl; 
    }  
    ~Pereson() {   
        cout << "Person::~Person()" << endl; 
    }
};
struct Student : Person { 
    Student() {     
        // call Person::Person     
        cout << "Student::Student()" << endl; 
    }   
    ~Student() {     
        cout << "Student::~Student()" << endl; 
    }
};
int main() {  
    {     
        Student student;  
    }  
    getchar();  
    return 0;
}
// output 	
// Person::Person()
// Student::Student()
  • Student inherits the parent class Person. The code of Person is called in front of student. In what order are the destructors called?
    • First call the destructor of the child class, and then call the destructor of the parent class

Construction: call the parent class first and then the child class; Destruct: call the subclass destruct first and then the parent destruct

Parent class pointer, child class pointer

The parent class pointer can point to the child class object, which is safe. It is often used in development * * (the inheritance method must be public)**

(struct inherits from public by default, and class inherits from private by default)

struct Person {  
    int m_age;
};
struct Student : Person { 
    int m_score;
};
int main() {      
    // The parent class pointer points to the child class object
    Person *p = new Student(); 
    p->m_age = 10; 
    getchar();  
    return 0;
}

  • *p can only access m_age, that is, the member of the parent class inherited by the Student. Because the Person itself only occupies 4 bytes, the M pointing to the Student_ Age does not cross the border and is safe.

    If the reverse

    Student *p = (Student *) new Person();
    p->m_age = 10;
    p->m_score = 100;
    

The first one is assigned to m_age is no problem. It is in the Person object itself.

But there is no m in the Person object_ The location of score, which is more dangerous, will overwrite the unknown things in the back memory.

Therefore, it is not safe for subclass pointers to parent objects.

polymorphic

  • The same operation acts on different objects, which can have different interpretations and produce different execution results.

    struct Animal {
        void speak() {
            cout << "Animal::speak" << endl;
        }    
        void run() {
            cout << "Animal::run" << endl;
        }
    };
    struct Dog : Animal {
        // Take the function inherited from the parent class and change it into its own
        // Overwrite (overwrite, overwrite, override)
        // As like as two peas, the return value, function name and parameter are exactly the same as the parent class.
        void speak() {
            cout << "Dog::speak" << endl;
        }    
        void run() {
            cout << "Dog::run" << endl;
        }
    };
    struct Cat : Animal {
        void speak() {
            cout << "Cat::speak" << endl;
        }    
        void run() {
            cout << "Cat::run" << endl; 
        }
    };
    struct Pig : Animal {
        void speak() {
            cout << "Pig::speak" << endl;
        }    
        void run() {
            cout << "Pig::run" << endl;    
        }
    };
    void liu(Animal *p) {
        p->speak();
        p->run();
    }
    int main() {
        liu(new Dog());
        liu(new Cat());
        liu(new Pig());
        getchar();
        return 0;
    }
    

    The above code cannot be polymorphic, because by default, the compiler will only call the corresponding function according to the pointer type, and there is no polymorphism.

    Polymorphism in C + + is realized by virtual functions.

    Virtual function: the function modified by virtual is called virtual function

    That is, modify the function in the Animal class to a virtual function

    struct Animal {
        virtual void speak() {
            cout << "Animal::speak" << endl;
        }    
        virtual void run() {
            cout << "Animal::run" << endl;
        }
    };
    

    As long as it is declared as a virtual function in the parent class, the function overridden in the subclass will automatically become a virtual function (that is, the subclass can omit virtual)

    • At runtime, you can identify the real object type and call the function of the corresponding subclass
  • Polymorphic elements

    • The subclass overrides the member function of the parent class (override)
    • The parent class pointer points to the child class object
    • Calling (overridden) member functions with parent class pointers

Virtual table: implementation principle of virtual function

It stores the address of the virtual function that needs to be called finally. The virtual table is also called the virtual function table.

Once it becomes a virtual function, the memory occupied by the function will be four more bytes. If it is not a virtual function, the first address of cat function is the address of the first variable. After it becomes a virtual function, four bytes will be added to store the address of the virtual table, pointing to the address of the first virtual function.

Assembly code:

// Call speakAnimal 
*cat = new Cat();
cat->speak();
// ebp-8 is the address of the pointer variable cat
// eax is the address of the Cat object
	mov	eax, dword ptr [ebp-8]
// Take out the first 4 bytes of Cat object and give them to edx
// Get the address value of the virtual table to edx
	mov edx, dword ptr [eax]
// Take out the first 4 bytes of the virtual table and give them to eax
// Take out the function call address of Cat::speak and give it to eax
	mov eax, dword ptr [edx]   
// call Cat::speak
	call eax 
// Call run
cat->run();
// ebp-8 is the address of the pointer variable cat
// eax stores the address of the Cat object
	mov eax, dword ptr [ebp-8]  
// Take out the first 4 bytes (virtual table address) of Cat object and give it to edx
	mov edx, dword ptr [eax]
// Skip the first function speak, and then take out 4 bytes to assign to eax
	mov eax, dword ptr [edx+4]
// call Cat::run
	call eax
  • Details of virtual table

    • All cat objects (no matter in the global area, stack or heap) share the same virtual table

      Animal *cat1 = new Cat();
      cat1->speak();
      cat1->run();
      Animal *cat2 = new Cat();
      cat2->speak();
      cat2->run();
      

      The virtual tables pointed to by cat1 and cat2 are the same because the same function is called

    • If the parent class has only one virtual function, there is only one virtual function in the virtual table of the child class

      struct Animal { 
          void speak() {   
              cout << "Animal::speak" << endl;  
          }  
          virtual void run() {  
              cout << "Animal::run" << endl; 
          }
      };
      
    • If both parent classes are virtual functions, but there is only one in the definition of a subclass, there are two virtual functions in the virtual table, one is the virtual function of the parent class and the other is the virtual function of the subclass

      class Animal {
      public:  
          int m_age;      
          virtual void speak() {    
              cout << "Animal::speak" << endl;  
          }  
          virtual void run() {     
              cout << "Animal::run" << endl;  
          }
      };
      class Cat : public Animal {
      public:  
          int m_life; 
          void run() {  
              cout << "Cat::run()" << endl;  
          }
      }
      int main() {  
          Animal *cat = new Cat();  
          cat->m_age = 20;  
          cat->speak(); 
          cat->run();     
          getchar(); 
          return 0;
      }
      /*  output  
      Animal::speak  
      Cat::run()
      */
      

virtual destructor

When there is a parent class pointer pointing to a subclass object (when there is polymorphism), the destructor should be declared as a virtual function (virtual destructor)

  • Only when the parent class pointer is delete d will the destructor of the child class be called to ensure the integrity of the destructor

If the destructor of the parent class is a virtual function, the destructor of the child class is also a virtual function.

Pure virtual function

A virtual function with no function body and initialized to 0 is used to define the interface specification.

// Because we don't know what "animal" is called / runs (because we don't know which animal it is)
// So there's no way to define how to call / run
// Not defined here. speak and run are pure virtual functions
struct Animal { 
    virtual void speak() = 0;  
    virtual void run() = 0;
};
struct Dog : Animal {  
    void speak() {   
        cout << "Dog::speak" << endl;  
    }   
    void run() {   
        cout << "Dog::run" << endl;  
    }
};
struct Cat : Animal {   
    void speak() {     
        cout << "Cat::speak" << endl; 
    }  
    void run() {   
        cout << "Cat::run" << endl; 
    }
};
struct Pig : Animal { 
    void speak() {     
        cout << "Pig::speak" << endl;  
    }   
    void run() {       
        cout << "Pig::run" << endl; 
    }
};
  • abstract class

    • Classes containing pure virtual functions cannot be instantiated (objects cannot be created)

      That is, the Animal in the above code cannot create an Animal object, but can only create subclass objects.

    • Abstract classes can also contain impure virtual functions and member variables

      struct Animal {  
          int m_age;  
          virtual void speak() = 0;  
          void run() {          
          }
      };
      

      Therefore, as long as it contains pure virtual functions, it is an abstract class.

    • If the parent class is an abstract class and the subclass does not completely override the pure virtual function, the subclass is still an abstract class

      // Animal is an abstract class
      struct Animal { 
          virtual void speak() = 0;  
          virtual void run() = 0;
      };
      // Dog only overrides one of the pure virtual functions in the parent class, so dog is also an abstract class
      struct Dog : Animal {   
          void run() { 
              cout << "Dog::run" << endl; 
          }
      };
      // Teddy completely overrides the pure virtual function of the parent class, so Teddy is not an abstract class
      struct Teddy : Dog {  
          void speak() {   
              cout << "Teddy::speak" << endl;  
          }   
          void run() {   
              cout << "Teddy::run" << endl; 
          } 
      };
      

Keywords: C++

Added by mrmachoman on Tue, 25 Jan 2022 21:52:30 +0200