面向对象编程02(继承、封装、多态)

  • 类的封装
    • 概念
      • 封装是将现实生活中的事物定义为类,将事物的数据抽象为属性,将事物的行为抽象成方法。封装的关键字就是class。控制权限有三种private、protected、public
      • 为了实现数据的隐藏,一般属性是私有的,方法是公有的,一个类的外部程序仅仅可以知道该类的公有方法就能调用一些特定属性。
    • 完整的类封装包括一下几个要素
      • 属性、方法
      • 构造函数、析构函数
      • get和set方法
      • 代码实现
      • #include 
        using namespace std;
        
        // 封装一个完整的Rect类
        class Rect
        {
        private:
            int m_nLength;
            int m_nWidth;
        public:
            Rect(); // 无参构造函数
            Rect(int mLength, int mWidth); // 两个int类型的构造函数
            Rect(int mLength); // 一个int类型的构造函数
            // 利用一组get和set方法来实现长度和宽度的设置、获取
            // 设置长度和宽度
            void setLength(int length);
            void setWidth(int width);
            // 获取长度和宽度
            int getLength();
            int getWidth();
            // 定义方法来获取面积
            int getArea();
        };
        
        // 在类外定义函数
        // 定义构造函数
        Rect::Rect(int mLength, int mWidth) :m_nLength(mLength), m_nWidth(mWidth) {}
        Rect::Rect(int mLength) :m_nLength(mLength), m_nWidth(mLength) {}
        void Rect::setLength(int length)
        {
            m_nLength = length;
        }
        
        void Rect::setWidth(int width)
        {
            m_nWidth = width;
        }
        int Rect::getLength()
        {
            return m_nLength;
        }
        int Rect::getWidth()
        {
            return m_nWidth;
        }
        
        int Rect::getArea()
        {
            return m_nWidth * m_nLength;
        }
        int main()
        {
            Rect r1(100, 50);
            cout << "r1矩形的长是:" << r1.getLength() << ",矩形的宽是:" << r1.getWidth() << endl;
            cout << "r1矩形的面积是:" << r1.getArea() << endl;
            
            // 修改r1对象的长和宽
            r1.setLength(300);
            r1.setWidth(100);
            cout << "修改长和宽之后的结果是:" << endl;
            cout << "r1矩形的长是:" << r1.getLength() << ",矩形的宽是:" << r1.getWidth() << endl;
            cout << "r1矩形的面积是:" << r1.getArea() << endl;
            return 0;
        }
  • 类的继承
    • 基本概念
      • 继承是面向对象的重要特种之一,定义类时可以从现有的类来继承,被继承叫做父类、基类或者超类,新定义的类叫做子类或者派生类。
      • 子类可以继承父类的属性和方法,可以实现代码的重用,子类汇总不需要定义父类中已经存在的方法和属性,只需要新增子类成员即可。
    • 继承方式
      • 图示

面向对象编程02(继承、封装、多态)_第1张图片

      • 单继承:一个子类只有一个父类
      • 多继承:一个子类继承多个父类
      • 直接继承:教师类是教师管理人员类的直接基类
      • 间接继承:人类是教师管理人员类的间接基类
    • 派生类的定义
      • 单继承派生类的定义语法
        • class 派生类名: 继承方式 基类名{派生类的新增成员的声明};
      • 综合案例演示
        • 要求:
          • 定义动物类Animal,属性有姓名和年龄,方法有构造方法:跑、叫和获取姓名
          • 定义子类Cat,Cat继承Animal类中属性和方法
          • 定义子类Dog,Dog继承Animal中的属性和方法,还需要新增一个lookinghouse方法
    • 方法的隐藏(重定义)
      • 概念
        • 在子类中定义和父类方法名完全相同的方法,就叫做方法的隐藏,隐藏指的是子类的函数屏蔽了父类的方法,主要函数名称一样,不论参数列表、返回值是否想通过,基类中的方法都会被隐藏。
    • 类的继承方式影响权限
      • 概念
        • 父类中的成员被继承到子类时,访问权限受到继承方式的影响,继承方式确定基类成员在派生类中的可见性。
      • 公有继承
        • 基类中的私有成员是不能被派生类中的成员函数访问
        • 基类中的公有成员在派生类中还是公有成员
        • 基类中的保护成员在派生类中还是保护成员
      • 保护继承
        • 基类中的私有成员是不能被派生类中的成员函数访问
        • 基类中的公有成员会在派生类中是保护成员
        • 基类中的保护成员会在派生类中还是保护成员
      • 私有继承
        • 基类中的私有成员不能被派生类的成员函数访问
        • 基类中的公有成员和保护成员在派生类中都变为私有成员
      • 表格图示

    • 派生类的构造过程
      • 概念
        • 由于派生类继承了基类的成员,那么派生类对象既含有本身的数据成员也含有基类的数据成员,因此在构造派生类对象的时候,同时也要创建基类继承的部分。
        • 但是构造函数是不能被继承的,C++在创建派生对象的时候能调用基类的构造函数为基类的数据成员进行初始化。
        • 派生类的析构过程和构造过程类似,在删除派生类对象的时候能自动调用基类的析构函数
      • 派生类构造函数的执行顺序:
        • 1、调用基类的构造函数
        • 2、按照数据成员(包括内嵌对象、常量、引用等等)的声明顺序依次调用数据成员的构造函数或者初始化数据成员。
        • 3、执行派生类的构造函数的函数体
    • 派生类的析构过程
      • 1、执行派生类的析构函数
      • 2、按照内嵌对象声明的相反顺序依次调用内存对象的析构函数
          • 3、调用基类的析构函数
      • 示例

        父类头文件及其cpp

      • #ifndef ANIMAL_H//在C/C++预处理指令中主要用于防止头文件被重复引用,避免重复编译。
        #define ANIMAL_H
        #include 
        using namespace std;
        class Animal
        {
        private:
            char m_cName[20]; // 姓名
            int m_nAge; // 年龄
        public:
            // 构造函数
            Animal(const char* name, int age);
            ~Animal();
            // 获取名字
            char* getName();
            // 声明跑和叫的方法
            void running();
            void barking();
        };
        #endif // !ANIMAL_H
        #include "Animal.h"//注意:不能忘记引入头文件
        
        Animal::Animal(const char* name, int age)
        {
            strcpy_s(m_cName, name);
            m_nAge = age;
            cout << "Animal的构造函数" << endl;
        }
        
        Animal::~Animal()
        {
            cout << "Animal正在析构" << endl;
        }
        char* Animal::getName()
        {
            return m_cName;
        }
        void Animal::running()
        {
            cout << "姓名:" << m_cName << "正在跑" << endl;
        }
        void Animal::barking()
        {
            cout << "姓名:" << m_cName << "正在叫" << endl;
        }
      • 子类头文件及其cpp

      • #ifndef CAT_H
        #define CAT_H
        #include "Animal.h"//派生类继承父类,要引入父类头文件
        class Cat:public Animal//public代表继承方式,表示公有继承
        {
        public:
            Cat(const char* name, int nAge);
        };
        #endif // !CAT_H
        
        
        #include "Cat.h"//由于头文件已经引入过父类头文件,这里直接引入子类自己的头文件
        // 这里是先执行Animal的构造函数,简单理解就是要先有Animal再有Cat,因为Cat要从Animal继承
        //成员
        Cat::Cat(const char* name, int nAge) :Animal(name, nAge)
        {
        
        }
        
        
        #ifndef DOG_H
        #define DOD_H
        #include "Animal.h"
        //class Dog :public Animal
        // 保护继承父类
        //class Dog :protected Animal
        // 私有继承
        class Dog :private Animal
        {
        private:
            // 子类新增成员,表示狗的类型
            char m_cType[20];
        public:
            Dog(const char* name, int nAge, const char *nType);
            ~Dog();
            //定义狗类自己的方法
            void lookingHouse();
            //void barking();
        };
        #endif // !DOG_H
        
        
        #include "Dog.h"
        Dog::Dog(const char* name, int nAge, const char* nType) :Animal(name,nAge)
        {
            // 将狗的类型赋值给私有成员m_cType
            strcpy_s(m_cType, nType);
            cout << "Dog的构造函数" << endl;
        }
        Dog::~Dog()
        {
            cout << "Dog正在析构" << endl;
        }
        //定义狗类自己的方法
        void Dog::lookingHouse()
        {
            cout << "姓名:" << getName() << "正在看家" << endl;
            barking();
        }
        
        
        
        #include "Dog.h"
        int main()
        {
            Dog d("旺财", 18, "中华田园犬");
            d.running();
            d.barking();
            d.lookingHouse();
        }
        
        
        //注:这些是放在不同文件下的,这里整理在了一块
        
  • 方法的隐藏(重定义)
    • 概念
    • 在子类中定义和父类方法名完全相同的方法,就叫做方法的隐藏,隐藏指的是子类的函数屏蔽了父类的方法,主要函数名称一样,不论参数列表、返回值是否想通过,基类中的方法都会被隐藏。
    • #include "Dog.h"
      Dog::Dog(const char* name, int nAge, const char* nType) :Animal(name,nAge)
      {
          // 将狗的类型赋值给私有成员m_cType
          strcpy_s(m_cType, nType);
          cout << "Dog的构造函数" << endl;
      }
      Dog::~Dog()
      {
          cout << "Dog正在析构" << endl;
      }
      //定义狗类自己的方法
      void Dog::lookingHouse()
      {
          cout << "姓名:" << getName() << "正在看家" << endl;
          barking();
      }
      
      // 重定义父类的方法,或者是子类同名方法将父类的方法给覆盖掉
      void Dog::barking()
      {
          cout << "姓名:" << getName() << "正在叫,它的类型是:"   << m_cType <<  endl;
      }
      
      
      #include "Dog.h"
      int main()
      {
          Dog d("旺财", 18, "中华田园犬");
          d.running();
          d.barking();
          d.lookingHouse();
      }
      

      多继承:

    • #include 
      using namespace std;
      // 基类A
      class BaseA
      {
      protected:
          int m_a;
          int m_b;
      public:
          BaseA(int a, int b);
          ~BaseA();
      };
      BaseA::BaseA(int a, int b) :m_a(a), m_b(b)
      {
          cout << "A类正在构造" << endl;
      }
      BaseA::~BaseA()
      {
          cout << "A类正在析构" << endl;
      }
      
      // 基类B
      class BaseB
      {
      protected:
          int m_c;
          int m_d;
      public:
          BaseB(int c, int d);
          ~BaseB();
      };
      BaseB::BaseB(int c, int d) :m_c(c), m_d(d)
      {
          cout << "B类正在构造" << endl;
      }
      BaseB::~BaseB()
      {
          cout << "B类正在析构" << endl;
      }
      
      // 定义类C来同时继承类A和类B
      class C :public BaseA, public BaseB
      {
      private:
          int m_e;
      public:
          C(int a, int b, int c, int d, int e);
          ~C();
          void show();
      };
      C::C(int a, int b, int c, int d, int e) :BaseA(a, b), BaseB(c, d), m_e(e) {
          cout << "C类正在构造" << endl;
      }
      C::~C()
      {
          cout << "C类正在析构" << endl;
      }
      
      void C::show()
      {
          cout << m_a << "\t" << m_b << "\t" << m_c << "\t" <<  m_d << "\t" << m_e << endl;
      }
      int main()
      {
          C c1(10, 20, 30, 40, 50);
          c1.show();
          return 0;
      }
      
      
      /*
      运行结果:
      A类正在构造
      B类正在构造
      C类正在构造
      10      20      30      40      50
      C类正在析构
      B类正在析构
      A类正在析构
      */
  • 类的多态
    • 多态性概念
      • 多态性一般指的一个名字有多种解释(一个事物的多种形态)
      • C++的多态分为静态多态和动态多态
        • 静态多态是通过函数的重载来实现的,后者主要是通过继承和虚函数。
      • 动态多态可以理解为一个接口多种方法,程序在运行的时候才决定要调用哪一个函数,它的是面向对象的核心概念。
    • 案例演示
      • 要求:
        • 买票的行为实现,当普通人买票的时候,调用自己的函数,实现全票买票,
        • 当学生进行买票的时候,调用学生买票的函数,就可以买半价票
        • 当儿童进行买票的税后,调用儿童买票的函数,就可以免票
        • #include 
          using namespace std;
          // 动态多态实现同一个方法的不同实现结果
          class Person
          {
          public:
               void buyTicket();
          };
          void Person::buyTicket()
          {
              cout << "普通人买全票" << endl;
          }
          class Student :public Person
          {
          public:
            void buyTicket()
              {
                  cout << "学生买半票" << endl;
              }
          };
          class Child :public Person
          {
          public:
              void buyTicket()
              {
                  cout << "儿童免票" << endl;
              }
          };
          
          int main()
          {
              Person* p[3];
              Person p1;
              Student s1;
              Child c1;
              // 用父类指针指向子类对象
              p[0] = &p1;
              p[1] = &s1;
              p[2] = &c1;
          
              p[0]->buyTicket();
              p[1]->buyTicket();
              p[2]->buyTicket();
              return 0;
          }
      • 静态多态无法实现用父类的指针来调用子类对象中的方法,所以需要用哪个虚函数来实现
        • 原因:在编译的时候父类指针指向的对象会当做父类对象,所以在调用方法的时候只能访问父类的成员。
        • 如果需要父类的指针来调用子类对象中的方法,就需要使用虚函数
    • 虚函数
      • 作用:实现动态多态
      • 语法:virtual 函数类型 函数名 (形参列表);
      • 目的:可以通过基类的指针对所有的派生类(包括直接派生和间接派生)的成员函数进行全方位的访问,调用同一个函数来实现不同的行为。
      • 基于虚函数的运行时多态需要满足三个条件
        • 基类中要定义虚函数
        • 子类中重写基类中的虚函数
        • 用基类的指针或者引用来指向子类对象并且调用虚函数
        • 代码示例
        • #include 
          using namespace std;
          // 动态多态实现同一个方法的不同实现结果
          class Person
          {
          public:
              // 声明函数是虚函数的时候,只需要在函数类型之前加virtual关键字即可
              
              virtual void buyTicket();
          };
          // 只在类内声明的时候才写virtual关键字,类外定义的时候不需要写
          void Person::buyTicket()
          {
              cout << "普通人买全票" << endl;
          }
          class Student :public Person
          {
          public:
              // 子类中的virtual可写可不写,一般建议写
              virtual void buyTicket()
              {
                  cout << "学生买半票" << endl;
              }
          };
          
          class Child :public Person
          {
          public:
              // 子类中的virtual可写可不写,一般建议写
              void buyTicket()
              {
                  cout << "儿童免票" << endl;
              }
          };
          
          int main()
          {
              // 静态多态能不能用父类的指针指向子类的方法呢?
              Person* p[3];
              Person p1;
              Student s1;
              Child c1;
              // 用父类指针指向子类对象
              p[0] = &p1;
              p[1] = &s1;
              p[2] = &c1;
          
              p[0]->buyTicket();
              p[1]->buyTicket();
              p[2]->buyTicket();
              return 0;
          }
      • 方法的重写/覆盖
        • 方法的重写是只在子类中重写父类中原型相同的函数
          • 不同范围,函数分别在基类和子类中声明
          • 函数的名称相同、参数相同
          • 基类函数必须有virtual关键字,子类函数声明的时候可以有virtual也可以没有,建议写。
      • 虚函数实现动态多态的机制
        • 编译器为每个包含虚函数的类创建了一个虚函数表,并且为每一个对象设置了一个虚函数指针(虚指针)指向这个类的虚函数表
        • 使用基类的指针对虚函数进行调用的时候,就是通过这个虚函数指针,在虚函数表中查找虚函数的地址,从而调用不同的虚函数
        • 图示

面向对象编程02(继承、封装、多态)_第2张图片

      • 虚析构函数
        • 概念
          • 析构函数可以定义为虚析构函数,如果基类的析构定义为虚函数,派生类的析构函数就会自动成为虚析构函数。
        • 作用:
          • 如果基类的指针指向派生类的对象,当用delete删除这个对象的时候,如果析构函数不是虚函数,就要调用父类的析构函数,而不会调用子类的析构函数。
          • 如果基类和派生类对象分配了动态内存空间,那么释放时候只能释放父类的动态空间, 而派生类的就内存就不能被释放了,因为将析构函数定义虚析构可以解决这个问题。
          • 代码示例
          • #include 
            using namespace std;
            // 定义父类 Employee
            class Employee
            {
            private: 
                char* name;
                int age;
            public:
                Employee(const char* n, int a);
                virtual ~Employee();
            };
            Employee::Employee(const char* n, int a)
            {
                // 动态开辟空间,将地址给到name指针变量上
                name = new char[strlen(n) + 1];
                strcpy_s(name, strlen(n) + 1, n);
                age = a;
                cout << "正在构造" << name << endl;
            }
            Employee::~Employee()
            {
                cout << "正在释放:" << name << endl;
                // 如果name不为空就将name中的空间释放掉
                if (name)
                {
                    delete[] name;
                    name = NULL;
                }
            }
            
            // 定义子类Teacher
            class Teacher :public Employee
            {
            private:
                char* project; // 定义成员来存储科目
            public:
                Teacher(const char* n, const char* pro, int a);
                ~Teacher();
            };
            
            Teacher::Teacher(const char* n, const char* pro, int a) :Employee(n, a)
            {
                project = new char[strlen(pro) + 1]; // 动态申请空间,给到指针project上
                strcpy_s(project, strlen(pro) + 1, pro);
                cout << "正在构造教师类" << endl;
            }
            Teacher::~Teacher()
            {
                cout << "正在释放教师类,课程是:" << project << endl;
                if (project)
                {
                    delete[] project;
                    project = NULL;
                }
             }
            int main()
            {
                // 析构函数不是虚函数,释放内存的时候,只调用基类的析构函数,子类数据成员占用的空间没有被释放
                // 当父类的析构函数定义为虚析构函数,子类的析构会自动转变为虚析构函数,在释放父类的同时也会将子类给释放掉
                Employee* p[3];
                // 直接创建对象,会自动调用析构函数,因为对象有自己的生命周期,在它调用结束的时候会自动执行析构函数
                // Employee p1("dijia", 1000);
                p[0] = new Employee("zhangsan", 20);
                p[1] = new Teacher("lisi", "C++", 18);
                p[2] = new Teacher("wangwu", "python", 28);
            
                for (int i = 0; i < 3; i++)
                {
                    delete p[i];
                }
                return 0;
            }
    • 纯虚函数和抽象类
      • 概念
        • 纯虚函数是不需要定义函数体的特殊的虚函数,定义方式
          • virtual 函数类型 函数名(参数列表) = 0;
        • 声明纯虚函数之后,在父类中不定义函数体,在子类中完成定义即可。
        • 拥有纯虚函数的类叫做抽象类
          • 派生类继承了抽象类的纯虚函数,但是没有给出定义,或者派生类也有新的纯虚函数,那么派生类仍然是抽象类。
          • 在多级继承过程中,只要某个派生类所有纯虚函数都被定义,才是非抽象类。
        • 抽象类的特点
          • 主要用于定义派生类共有的数据成员和成员函数
          • 抽象类的纯虚函数没有函数体
          • 抽象类不能创建对象
          • 代码演示
          • #include 
            using namespace std;
            class Person
            {
            public:
                // 声明纯虚函数
                virtual void say() = 0;
            };
            
            class Student:public Person
            {
            public:
                void say()
                {
                    cout << "学生say hi" << endl;
                }
            };
            
            class Child :public Person
            {
            };
            int main()
            {
            
                Student s1;
                s1.say();
                // 如果继承了父类的纯虚函数,并且没有重写,那么子类也是抽象类,不能实例化对象
                // Child c1;
                // 父类有纯虚函数,所以是抽象类,不能实例化
                // Person p;
                return 0;
            }
    • 运算符的重载
      • 赋值运算符重载
        • 如果没有重载赋值运算符,那么C++提供了默认的赋值运算符,可以实现将一个对象赋值给另外一个对象,但是如果类的成员比较复杂,涉及到指针的问题,直接赋值就会报错,必须要进行重载。
        • 代码示例
        • #include 
          using namespace std;
          
          class A
          {
          private:
              char* str;
          public:
              A(const char* s = "no data");
              ~A();
              // 重载赋值运算符
              A& operator=(const A& a);
              void print();
          };
          
          A::A(const char* s)
          {
              str = new char[strlen(s) + 1];
              strcpy_s(str, strlen(s) + 1, s);
          }
          A& A::operator=(const A& a)
          {
              if (str)
              {
                  delete[] str;
              }
              
              // 每次赋值str都会新开辟一段空间,避免赋值的时候多个str使用同一个地址
              str = new char[strlen(a.str) + 1];
              strcpy_s(str, strlen(a.str) + 1, a.str);
              return *this;
          }
          A::~A()
          {
              if (str != NULL)
              {
                  delete[] str;
                  str = NULL;
              }
          }
          
          void A::print()
          {
              cout << str << endl;
          }
          int main()
          {
              // 在主函数中定义class A的对象,将地址赋值给p,再利用默认的赋值运算符将p给到a1
              // 第一次调用print() 可以正常输出,但是删除p所指向对象时,就会调用析构函数
              // 将str所指向的内存空间给释放掉,再次调用print()方法的时候就会报错
              A* p = new A("A String");
              A a1, a2;
              a1 = a2 = *p;
          
              a1.print();
              delete p; // 如果使用了重载赋值,三个str的地址就都不相同了,如果释放了p指向的地址,并不会影响a1 和 a2的空间,所以仍然可以输出
              a1.print();
              a2.print();
              return 0;
          }

你可能感兴趣的:(面向对象编程,开发语言,c++,面向对象三大特征,面向对象,继承,封装,多态)