0
点赞
收藏
分享

微信扫一扫

【C++】三大特性:封装、继承、多态

奋斗De奶爸 2022-01-24 阅读 41

三大特性:封装、继承、多态

访问权限

C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,他们分别表示公有的、受保护的、私有的,被称为成员访问限定符。

在类的内部(定义类的代码内部),无论成员被声明为public、protected还是private,都是可以互相访问的,没有访问权限的限制。

在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public属性的成员,不能访问private、private属性的成员。

?? 无论公有继承、私有继承还是保护继承,私有成员不能被“派生类(子类)”访问,基类中的公有保护成员能被“派生类”访问。

?? 对于公有继承,只有基类中的公有成员能被“派生类对象”访问,保护和私有成员不能被“派生类对象”访问。对于私有和保护继承,基类中的所有成员不能被派生类对象访问。

封装

成员变量私有化,提供公共的getter和setter给外界去访问成员变量。

数据和代码捆绑在一起,避免外界干扰和不确定性访问。

功能:

把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

例如:将公共的数据或方法用public修饰,而不希望被访问的数据或方法采用private修饰。

继承

可以让子类拥有父类的所有成员(变量、函数)

struct Student { 
    int m_age; 
    int m_score;    
    void run() { }  
    void study() { }
};
struct Worker {   
    int m_age;
    int m_salary;    
    void run() { }   
    void work() { }
};
// 在这里能看到Student和Worker都有 m_age 和 void run()
// 可以将他们共性的东西提取出来,特性的东西留下
// 将上述代码可以改成如下
struct Person {  
    int m_age;    
    void run() { }  
}
;struct Student : Person { 
    // 相当于Student继承了Person类    				
    // 将Person中所有的成员都拿过来  
    int m_score;   
    void study() { }
};
struct Worker : Person {   
    int m_salary;   
    void work() { }
};
  • 关系描述

    • Student是子类(subclass,派生类)
    • Person是父类(superclass,超类)
  • 作用

    • 避免重复代码

      在这里插入图片描述

Person类 :4个字节

Student类:8个字节(继承了Person类)

Worker类:8个字节 (继承了Person类)

如果定义

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

在内存中,m_age和m_salary的地址是相连的,各占4个字节,m_age在前。

从父类继承的成员变量会排布在前。

成员访问权限

成员访问权限、继承方式有三种:

public > protected > private

  • public

    公共的,任何地方都可以访问(struct默认)

  • protected

    子类内部、当前类内部都可以访问

  • private

    私有的,只有当前类内部可以访问(class默认)

  • 注意!!子类内部访问父类成员的权限,是以下2项中权限最小的那个

    1. 成员本身的访问权限
    2. 上一级父类的继承方式

    一般会以public继承,因为这样可以完整地将父类原本的成员权限继承下来

struct Person { 
    int m_age; 
};
struct Student : private Person { 
    // 相当于
    //  private:
    int m_age;
};
struct Worker : Student {
    // 在这里继承Student后不能够修改m_age,因为在父类中m_age属于private   
    int m_salary;
};
// 如果Student是以public继承Person,那么在Worker中就可以修改m_age
// 但如果Person中m_age是private,那么在Worker中就不可以修改
  • 访问权限不影响对象的内存布局

    就算父类的private成员不能直接访问,也可以通过public函数来间接访问。

初始化列表

一种便捷的初始化对象成员的方式

只能用在构造函数中

struct Person {   
    int m_age;  
    int m_height;     
    Person(int age, int height): m_age(age), m_height(height) {}   
    // 等价于(汇编代码完全一样)  
    /*Person(int age, int height) { 
    m_age = age;    
    m_height = height; 
    }*/
};
int main() {  
    Person(18, 180);   
    getchar();  
    return 0;
}
  • 注意!初始化类的时候是按照变量声明的顺序!

    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 在 m_height 之前初始化,所以在用 m_height 给 m_age 赋值的时候,m_height 还没有初始化。

    所以输出为:

    -858993460 180
    

    如果是先初始化 m_height 再初始化 m_age,就可以将 m_height 的值赋给 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;
    }
    

    输出为:

    180 180
    
  • 如果函数声明和实现是分离的

    初始化列表只能写在函数的实现中

    默认参数只能写在函数声明中

    struct Person { 
        int m_height;   
        int m_age;   
        Person(int age = 0, int height = 0); 
        // 默认参数只能写在函数声明中};
        Person::Person(int age, int height) m_height(height), m_age(m_height) {} 
        // 初始化列表只能写在函数的实现中
    

构造函数的互相调用

struct Person {  
    int m_age;
    int m_height;    
    Person() {    
        m_age = 0;      
        m_height = 0;  
        /*      
        ... 还有其他功能       
        */    
    }       
    Person(int age, int height) {  
        m_age = age; 
        m_height = height;  
    }   
    // 以上两个构造函数的代码有点重复,可以将第一个构造函数修改如下   
    /* Person() :Person(0, 0){
    // 必须放在初始化列表里!!!    
    ... 其他功能     
    }    */
};
int main() { 
    Person(18, 180);   
    getchar();    
    return 0;
}

如果构造函数实现的功能包含另一个,除此之外还有其余的个性化功能,那么可以直接互相调用来减少重复代码。

  • 注意!构造函数调用构造函数,必须放在初始化列表里!

    Person() {  
        Person(10, 20); 
        // 等同于   
        /*   
        Person person;   
        person.m_age = 10;  
        person.m_height = 20;  
        */   
        // 我们想要实现的是 
        /* 
        this->m_age = 10;  
        this->m_height = 20;  
        */  
    }
    

    这样并不能实现初始化,而是又创建了一个临时的Person对象

    10和20赋值给了新的person对象

父类的构造函数

  • 子类的构造函数默认会调用父类的无参构造函数,先调用父类的构造函数再调用子类的构造函数

    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;
    } 
    // 输出
    // Person::person
    // Student::student
    // 会调用两个构造函数
    
  • 如果子类的构造函数显式地调用了父类的有参构造函数,就不会再去默认调用父类的无参构造函数

    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){ 	
            // 显式地调用了父类的有参构造函数  
            cout << "Student::student" << endl;  
        }
    };
    int main() {  
        Student student; 
        getchar();  
        return 0;
    } 
    // 输出
    // Person::Person(int age)
    // Student::student
    
  • 如果父类缺少无参构造函数,子类的构造函数必须显式调用父类的有参构造函数

    struct Person() {  
        int m_age;  
        Person(int age) {     
            cout << "Person::Person(int age)" << endl;  
        }
    };
    struct Student : Person {  
        int m_no;      
        Student() { 	
            // 会报错,因为父类缺少无参构造函数,必须显式调用父类的有参构造函数    
            cout << "Student::student" << endl; 
        }
    };
    int main() {
        Student student;  
        getchar();
        return 0;
    } 
    

构造和析构顺序

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;
}
// 输出 	
// Person::Person()
// Student::Student()
  • Student 继承了父类Person,Person的代码在Student前面调用,那么各个的析构函数是以什么顺序调用的?
    • 先调用子类的析构再调用父类的析构

构造:先调用父类再调用子类;析构:先调用子类析构再调用父类析构

父类指针、子类指针

父类指针可以指向子类对象的,是安全的,在开发中经常用到**(继承方式必须是public)**

( struct 默认是 public 继承,class 默认是 private 继承 )

struct Person {  
    int m_age;
};
struct Student : Person { 
    int m_score;
};
int main() {      
    // 父类指针 指向 子类对象
    Person *p = new Student(); 
    p->m_age = 10; 
    getchar();  
    return 0;
}

在这里插入图片描述

  • *p 只能访问到 m_age,即Student所继承父类的成员。因为Person本身只占4个字节,所以在指向Student的m_age中并不会越界,是安全的。

    如果反过来

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

在这里插入图片描述

​ 第一个赋值给m_age是没有问题的,本身就在Person对象中。

​ 但Person对象中没有m_score的位置,这样就比较危险,会将后面内存未知的东西给覆盖掉。

所以子类指针指向父类对象是不安全的。

多态

  • 同一个操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。

    struct Animal {
        void speak() {
            cout << "Animal::speak" << endl;
        }    
        void run() {
            cout << "Animal::run" << endl;
        }
    };
    struct Dog : Animal {
        // 将父类中所继承的函数拿过来改成自己的
        // 重写(覆写、覆盖、override)
        // 返回值、函数名、参数都和父类一模一样
        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;
    }
    

    以上代码也无法实现多态,因为默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态。

    C++中的多态通过虚函数来实现。

    虚函数:被virtual修饰的函数叫虚函数

    即将Animal类中的函数修改为虚函数

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

    只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类可以省略virtual

    • 在运行时,可以识别出真正的对象类型,调用对应子类的函数
  • 多态的要素

    • 子类重写父类的成员函数(override)
    • 父类指针指向子类对象
    • 利用父类指针调用(重写的)成员函数

虚表:虚函数的实现原理

里面存储着最终需要调用的虚函数地址,虚表也叫虚函数表。

在这里插入图片描述

一旦变成了虚函数,函数的占用内存会多四个字节。不是虚函数的话 cat 函数的首地址即是第一个变量的地址,变成虚函数后会增加四个字节存放虚表的地址,指向第一个虚函数的地址。

汇编代码:

// 调用speakAnimal 
*cat = new Cat();
cat->speak();
// ebp-8是指针变量cat的地址
// eax是Cat对象的地址
	mov	eax, dword ptr [ebp-8]
// 取出Cat对象最前面的4个字节给edx
// 取出虚表的地址值给edx
	mov edx, dword ptr [eax]
// 取出虚表的最前面4个字节给eax
// 取出Cat::speak的函数调用地址给eax
	mov eax, dword ptr [edx]   
// call Cat::speak
	call eax 
// 调用run
cat->run();
// ebp-8是指针变量cat的地址
// eax存储的是Cat对象的地址
	mov eax, dword ptr [ebp-8]  
// 取出Cat对象的最前面4个字节(虚表地址)给edx
	mov edx, dword ptr [eax]
// 跳过第一个函数speak,再取出4个字节赋值给eax
	mov eax, dword ptr [edx+4]
// call Cat::run
	call eax
  • 虚表的细节

    • 所有的cat对象(不管在全局区、栈、堆)共用同一份虚表

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

      cat1 和 cat2 所指向的虚表是一样的,因为调用的是同样的函数

    • 如果父类只有一个虚函数,那么子类的虚表中也只有一个虚函数

      struct Animal { 
          void speak() {   
              cout << "Animal::speak" << endl;  
          }  
          virtual void run() {  
              cout << "Animal::run" << endl; 
          }
      };
      
    • 如果父类两个都是虚函数,但子类的定义中只有一个,那么虚表中有两个虚函数,一个是父类的虚函数一个是子类的虚函数

      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;
      }
      /*  输出  
      Animal::speak  
      Cat::run()
      */
      

    在这里插入图片描述

虚析构函数

存在父类指针指向子类对象的时候(有多态的时候),应该将析构函数声明为虚函数(虚析构函数)

  • delete 父类指针时,才会调用子类的析构函数,保证析构的完整性

    在这里插入图片描述

父类的析构函数是虚函数那么子类的析构函数也是虚函数。

纯虚函数

没有函数体且初始化为0的虚函数,用来定义接口规范

// 因为我们不知道“动物”是怎么叫的/跑的(因为不知道是具体哪个动物)
// 所以没法定义怎么叫/跑
// 在这里就不定义,speak和run就是纯虚函数
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; 
    }
};
  • 抽象类

    • 含有纯虚函数的类,不可以实例化(不可以创建对象)

      即上述代码中的Animal,不可以创建Animal对象,只可以建立子类的对象。

    • 抽象类也可以包含非纯虚函数,成员变量

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

      所以说只要包含纯虚函数就是抽象类

    • 如果父类是抽象类,子类没有完全重写纯虚函数,那么这个子类依然是抽象类

      // Animal为抽象类
      struct Animal { 
          virtual void speak() = 0;  
          virtual void run() = 0;
      };
      // Dog中只重写了父类中的其中一个纯虚函数,所以Dog也是抽象类
      struct Dog : Animal {   
          void run() { 
              cout << "Dog::run" << endl; 
          }
      };
      // Teddy完全重写了父类的纯虚函数,所以Teddy不是抽象类
      struct Teddy :Dog {  
          void speak() {   
              cout << "Teddy::speak" << endl;  
          }   
          void run() {   
              cout << "Teddy::run" << endl; 
          } 
      };
      
举报

相关推荐

0 条评论