首页 > 编程语言 >【C++复习】第七章 类的继承(基类、派生类、虚基类)

【C++复习】第七章 类的继承(基类、派生类、虚基类)

时间:2023-02-09 18:34:13浏览次数:37  
标签:成员 基类 C++ class 派生类 public 构造函数

1、基类与派生类

类的继承:面向对象的程序设计中提供了类的继承机制,允许程序员在保持原有类特性的基础上,进行更具体、更详细的定义 

用途:代码的重用性(继承)和可扩充性(派生)

:植物大战僵尸游戏中,可以将普通僵尸作为一个基类,设计其他僵尸(路障僵尸,铁门僵尸等)

1.1 一些概念

类的继承:新类从已有类得到特性(新类继承了已有类的特性)

类的派生:从已有类产生新类的过程(新类自身的新特性)

继承与派生是同一过程,只是观察角度不同

继承的目的:代码重用

派生的目的:为解决新问题,对原有程序进行改造

---------------------------------------

被继承的类称为基类父类

派生出的新类称为派生类子类

---------------------------------------

一个基类可派生出多个派生类

一个派生类可同时拥有多个基类

直接参与派生出某类的基类称为直接基类

基类的基类甚至更高层的基类称为间接基类

--------------------------------------

单继承可以看作是多继承的一个特例,多继承可以看成是多个单继承的组合

一个派生类可以同时有个基类,称为多继承

一个派生类可以同时有1个基类,称为单继承

2、 派生类

 2.1 派生类介绍

派生类定义语法:

class 派生类名: 继承方式 基类名1, 继承方式 基类名2,...,继承方式 基类名n
{
    派生类成员声明;
};
//例:
//类c1继承了c2(公有),c3(私有),c4(保护)
//类c1自身有新数据成员c1_mem,新函数成员fun()
class c1:public c2,private c3,protected c4
{
private:
    int c1_mem=0;
public:
    int fun();
};

 派生类生成过程:

2.2 例7-1 Rectangle类公有继承Point类

/*
7-1 P257
Rectangle类公有继承Point类
派生类Rectangle就包含了它的全部基类中的所有非静态成员(除了构造和析构函数)。
*/
#include <iostream>
using namespace std;
class Point
{
public:
    Point(float xx = 0, float yy = 0) : x(xx), y(yy) {}
    void initPoint(float x = 0, float y = 0)
    {
        this->x = x;
        this->y = y;
    }
    void move(float offX, float offY)
    {
        x += offX;
        y += offY;
    }
    void show() { cout << "this is Point's Show:" << x << y << endl; }
    float getX() const { return x; }
    float getY() const { return y; }

private:
    float x, y;
};
class Rectangle : public Point
{
public:
    void initRectangle(float x = 0, float y = 0, float w = 0, float h = 0)
    {
        initPoint(x, y);
        this->w = w;
        this->h = h;
    }
    void show() { cout << "this is Rectangle's Show:" << w << h << endl; }
    float getW() const { return w; }
    float getH() const { return h; }

private:
    float w, h;
};
int main()
{
    Rectangle r1;
    r1.initRectangle(1, 1, 3.0, 4.0);
    r1.move(2, 1);
    cout << r1.getX() << ',' << r1.getY() << ',' << r1.getW() << ',' << r1.getH() << endl;
    r1.show();
    return 0;
}

2.3 访问控制

2.3.1 总览

基类成员的访问控制

(public)公有继承后

派生类中的访问控制

(private)私有继承后

派生类中的访问控制

(protected)保护继承后

派生类中的访问控制

public public private protected
protected protected private protected
private 不可访问 不可访问 不可访问

详细总览图

基类成员的访问控制

(public)公有继承后

派生类中的访问控制

(private)私有继承后

派生类中的访问控制

(protected)保护继承后

派生类中的访问控制

public

public
在类外通过子类对象访问
可在派生类内访问

private
不可在类外通过子类对象访问
可在派生类内访问
protected
不可在类外通过子类对象访问
可在派生类内访问
protected protected
不可在类外通过子类对象访问
可在派生类内访问
private
不可在类外通过子类对象访问
可在派生类内访问
protected
不可在类外通过子类对象访问
可在派生类内访问
private 不可在派生类内访问
可通过基类成员函数访问
可在基类内访问
不可在派生类内访问
可通过基类成员函数访问
可在基类内访问
不可在派生类内访问
可通过基类成员函数访问
可在基类内访问

写简单测试功能的程序时,可把成员全部写为public(可通过子类对象在类外访问)或者protected(只能在类内访问),全部使用公有继承,这样派生类可访问基类中的所有成员,在类外可通过子类对象访问基类和派生类的所有public成员,程序容易跑。

2.3.2 如何记忆

基类中的成员是private --> 派生类中无法访问此成员

基类中的成员不是private --> 派生类中的访问控制向保守方向发展
从左向右变保守(public->protected->private)
例:基类中成员是protected,继承方式是private,private更保守,故基类成员在派生类中是private

基类的公有成员被公有继承,才可在类外通过子类对象访问基类中的公有成员

2.3.3 public/private/protected

对类A来讲它的成员访问控制如下

成员的访问控制 类外 类内
public 在类外通过A类的对象访问 可被其他成员访问
protected 不可在类外通过A类的对象访问
private 不可在类外通过A类的对象访问

2.4 类型兼容性规则

类型兼容性规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代

替代之后派生类仅仅发挥出基类的作用(儿子能干爸爸的活,只干爸爸的活)

2.4.1 三个类型兼容性规则

  1. 派生类对象可以隐含转换为基类对象
    //类定义
    class A
    {
    public:
        void display();
    private:
        int x,y;
    };
    class B:public A
    {
    public:
        void display();
    private:
        int i=0,j=0;
    };
    //程序段
    A a1;
    B b1;
    a1 = b1;//派生类对象可以隐含转换为基类对象
  2. 派生类对象可以初始化基类对象的引用
    //类定义
    class A{
    public:
        void display();
    private:
        int x,y;
    };
    class B:public A{
    public:
        void display();
    private:
        int i,j;
    };
    //程序段
    B b1;
    A &rf=b1;//派生类对象可以初始化基类对象的引用
    
  3. 派生类对象的地址可以隐含转换为指向基类的指针
    //类定义
    class A{
    public:
        void display();
    private:
        int x,y;
    };
    class B:public A{
    public:
        void display();
    private:
        int i,j;
    };
    //程序段
    A *p;
    B b;
    p=&b;//派生类对象的地址可以隐含转换为指向基类的指针
    

2.4.2 替代之后派生类仅仅发挥出基类的作用

例1:

//类定义
class A{
public:
    void display() {cout<<"Class A!";}
private:
    int x,y;
};
class B:public A{
public:
    void display() {cout<<"Class B!";}
private:
    int i,j;
};
//程序段
A *p;
B b;
p=&b;
p->display()
//运行结果
Class A!

例2:

/*
7-3 P264
类型兼容性规则实例
*/
#include <iostream>
using namespace std;
class Base1 //基类Base1定义
{
public:
    void display() const
    {
        cout << "Base1::display()" << endl;
    }
};
class Base2 : public Base1 //公有派生类Base2定义
{
public:
    void display() const
    {
        cout << "Base2::display()" << endl;
    }
};
class Derived : public Base2 //公有派生类Derived定义
{
public:
    void display() const
    {
        cout << "Derived::display()" << endl;
    }
};
void fun(Base1 *p) //参数为执行基类对象的指针
{
    //根据类型兼容性规则。派生类对象的地址可以隐含转换为指向基类的指针
    p->display();
}
int main()
{
    Base1 b1;
    Base2 b2;
    Derived d1;
    fun(&b1);
    fun(&b2);
    fun(&d1);
    return 0;
}

运行结果:

Base1::display()
Base1::display()
Base1::display()

3、派生类的构造函数和析构函数

3.1 派生类构造函数

派生类构造函数语法:

//派生类构造函数格式
派生类名::派生类名(形参表):
    基类名1(基类1初始化参数表),…, 基类名n(基类n初始化参数表), 
    成员对象名1(成员对象名1初始化参数表),… ,成员对象名m(成员对象名m初始化参数表),
    基本类型成员初始化列表
{
    派生类构造函数的其他初始化操作;
};
//例子,类名Derived
Derived::Derived(int a,int b):
    Base1(a),Base2(b),//这里调用基类构造函数
    dmem1(a),dmem2(b),//这里调用对象所属类的构造函数
    c(a),d(b)//这里是基本数据类型,如int,float,char
{
    //其他操作
}
/*
什么时候可不声明派生类的构造函数?
答:需要满足以下条件
1-不需要调用基类中带有参数的构造函数
2-也不需要调用新增加的成员对象的带参数的构造函数
此时全部采用默认构造函数
--当派生类没有显式的构造函数时,系统会隐含生成一个默认构造函数,
  该函数会使用基类的默认构造函数对继承自基类的数据初始化,
  并且调用类类型的成员对象默认构造函数,对这些成员对象初始化
--注意基本数据类型无默认构造函数
*/

派生类构造函数调用顺序:

3.2 例7-1 派生类构造函数举例

/*
7-4 P266
派生类构造函数举例(多继承,含有内嵌对象)
*/
#include <iostream>
using namespace std;
class Base1
{
public:
    Base1(int i) { cout << "Constructing Base1 " << i << endl; }
};
class Base2
{
public:
    Base2(int i) { cout << "Constructing Base2 " << i << endl; }
};
class Base3
{
public:
    Base3() { cout << "Constructing Base3 *" << endl; }
};
class Derived : public Base2, public Base1, public Base3
{ //基类构造顺序Base2,Base1,Base3
public:
    Derived(int a, int b, int c, int d) : Base1(a), m2(d), m1(c), Base2(b) {}
    //基类Base1和Base2调用有参数的构造函数,基类Base3调用无参数的构造函数(默认构造函数)
    //对象m1和m2调用有参数的构造函数,对象m3调用无参数的构造函数
private:
    //对象构造顺序为m1,m2,m3
    Base1 m1;
    Base2 m2;
    Base3 m3;
};
int main()
{
    Derived obj(1, 2, 3, 4);
    return 0;
}

运行结果:

Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *

3.3 派生类复制构造函数(不k)

//派生类复制构造函数格式
Derived::Derived(const Derived &v):Base(v){}
//类型兼容性规则起了作用,可用派生类对象,初始化基类的引用,调用基类的构造函数
//派生类新成员的复制需要自己添加
/*
若没有构造函数,系统会生成一个隐含的构造函数,
这个构造函数会自动调用基类的复制构造函数,
然后对派生类的新成员一一进行复制
*/

delete构造函数

//类定义
class Base
{
public:
    Base() = default;
    Base(string _info) : info(std::move(_info)) {}
    Base(Base &) = delete; //删除复制构造函数
private:
    string info;
};
class Derived : public Base{};
//代码段
Derived d1;     //正确,合成了默认构造函数
Derived d2(d1); //错误,删除了复制构造函数
//解释
//delete用来禁止默认构造函数或删除复制构造函数阻止拷贝
//基类中删除的构造函数,派生类中也是删除状态。

3.4 派生类析构函数

 派生类析构函数的写法没有特别要求

Derived::~Derived(){}//派生类析构函数写法与普通类(无继承关系的类)相同

派生类析构函数会调用基类和对象的析构函数,调用顺序与构造函数的构造顺序严格相反
析构函数执行顺序如下:

3.5 例7-5 派生类析构函数举例(多继承,含有内嵌对象)

/*
7-5 P269
派生类析构函数举例(多继承,含有内嵌对象)
对例7-4进行修改
*/
#include <iostream>
using namespace std;
class Base1
{
public:
    Base1(int i) { cout << "Constructing Base1 " << i << endl; }
    ~Base1() { cout << "Destructing Base1" << endl; };
};

class Base2
{
public:
    Base2(int i) { cout << "Constructing Base2 " << i << endl; }
    ~Base2() { cout << "Destructing Base2" << endl; };
};
class Base3
{
public:
    Base3() { cout << "Constructing Base3 *" << endl; }
    ~Base3() { cout << "Destructing Base3" << endl; };
};
class Derived : public Base2, public Base1, public Base3
{ //基类构造顺序Base2,Base1,Base3
public:
    Derived(int a, int b, int c, int d) : Base1(a), m2(d), m1(c), Base2(b) {}
    //基类Base1和Base2调用有参数的构造函数,基类Base3调用无参数的构造函数(默认构造函数)
    //对象m1和m2调用有参数的构造函数,对象m3调用无参数的构造函数

    // Derived类中无析构函数,系统会自动生成默认析构函数
    //析构顺序与构造顺序严格相反
private:
    //对象构造顺序为m1,m2,m3
    Base1 m1;
    Base2 m2;
    Base3 m3;
};
int main()
{
    Derived obj(1, 2, 3, 4);
    return 0;
}

运行结果:

Constructing Base2 2
Constructing Base1 1
Constructing Base3 *
Constructing Base1 3
Constructing Base2 4
Constructing Base3 *
Destructing Base3
Destructing Base2
Destructing Base1
Destructing Base3
Destructing Base1
Destructing Base2

4、派生类成员的标识和访问

  1. 作用域分辨符::
    • ::a表示全局作用域的变量a
  2. 如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏
    • 不属于函数重载
    • 调用父类同名函数需使用父类名来限制
    • 只有在相同的作用域中定义的函数才可以重载
    • #include <iostream>
      using namespace std;
      class Base{
      public:
          void print(){cout<<"print()"<<endl;}
          void print(int i){cout<<"print(int i)"<<endl;}
          void print(int i, int j){cout<<"print(int i, int j)"<<endl;}
      };
      class Derived:public Base{
      public:
          void print(int i, int j, int k){cout<<"print(int i, int j, int k)"<<endl;}
      };
      int main() {
          Derived d;
          d.print();     //错误,基类中所有print()的重载形式都被隐藏了
          d.print(1);    //错误,基类中所有print()的重载形式都被隐藏了
          d.print(2,3);  //错误,基类中所有print()的重载形式都被隐藏了
          d.print(1,2,3);//正确
          return 0;
      }
  3. 如果某派生类的多个基类拥有同名的成员,同时,派生类又新增这样的同名成员,则派生类成员将隐藏所有基类的同名成员
    • 如要通过派生类对象访问基类中被覆盖的同名成员,应使用基类名限定
      类名::成员名        //访问数据成员
      类名::成员名(参数表) //访问函数成员
    • 例7-6 多继承同名隐藏规则
      /*
      7-6 P273
      多继承同名隐藏规则
      */
      #include <iostream>
      using namespace std;
      class Base1 //定义基类Base1
      {
      public:
          int var;
          void fun() { cout << "Menber of Base1:" << var << endl; }
      };
      class Base2 //定义基类Base2
      {
      public:
          int var;
          void fun() { cout << "Menber of Base2:" << var << endl; }
      };
      class Derived : public Base1, public Base2 //定义派生类Derived
      {
      public:
          int var;
          void fun() { cout << "Menber of Derived:" << var << endl; }
      };
      
      int main()
      {
          Derived d;
          Derived *p = &d;
      
          d.var = 1; //访问Derived类成员
          d.fun();
      
          d.Base1::var = 2; //访问Base1类成员
          d.Base1::fun();
      
          p->Base2::var = 3; //访问Base2类成员
          p->Base2::fun();
          return 0;
      }
      
      运行结果:
      Menber of Derived:1
      Menber of Base1:2
      Menber of Base2:3
  4. 如果从不同基类继承了同名成员,但是在派生类中没有定义同名成员,则访问成员存在二义性问题(因为基类里的同名函数没有被覆盖,用派生类对象或指针访问该同名函数时,不知道哪一个)
    • 如下程序有二义性
      //类定义
      class A {
      public:
          void f();//二义性
      };
      class B {
      public:
          void f();//二义性
          void g();//被类C中的g()覆盖
      };
      class C: public A, public B {
      public:
          void g();//覆盖了类B中的g()
      };
      //程序段
      C c1;
      c1.f();    //有二义性
      c1.g();    //无二义性
    • 解决1:用类名限定
      //程序段
      C c1;
      c1.A::f();//无二义性
      c1.B::f();//无二义性
    • 解决2:同名隐藏
      //类定义
      class A {
      public:
          void f();//无二义性
      };
      class B {
      public:
          void f();//无二义性
          void g();//被类C中的g()覆盖
      };
      class C: public A, public B {
      public:
          void g();//覆盖了类B中的g()
          void f()//在C 中声明一个同名成员函数f(),f()再根据需要调用
          {
              if(1/*需要调用A中的fun*/)
                  A::f();
              else/*需要调用B中的fun*/
                  B::f();
          }
      };
      //程序段
      C c1;
      c1.f();//调用C中的f()
  5. 如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来限定
    • 解决:
    • 虽然可以通过类名限定来解决这个问题,但是会造成冗余不一致的问题
      • 冗余:表达相同含义的变量有两个,多余了
      • 不一致性:表达相同含义的变量有两个,通过作用域分辨符可以分开赋值,操作上没问题,但是这样就导致相同含义的变量被赋予了不同的值,表意不一致
      • 下面会讲虚基类,可解决以上两个问题

 5、虚基类

  1. 解决的问题:
    • 消除类继承过程中产生的冗余不一致性
  2. 虚基类声明方式
    • class B1:virtual public B
      {}
  3. 注意
    • 第一级继承时就要将共同基类设计为虚基类
  4. 刚才的问题被解决了!
    • 例7-8 虚基类举例
      /*
      7-8 P77
      虚基类举例
      */
      #include <iostream>
      using namespace std;
      class Base0 //定义基类Base0
      {
          // Base0是最远虚基类
          //虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
          //在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。
      public:
          int var0;
          void fun0() { cout << "Base0 var0=" << var0 << endl; }
      };
      class Base1 : virtual public Base0 //定义派生类Base1
      {
      public: //新增外部接口
          int var1;
      };
      class Base2 : virtual public Base0 //定义派生类Base2
      {
      public: //新增外部接口
          int var2;
      };
      class Derived : public Base1, public Base2 //定义派生类Derived
      {                                          // Derived是最远派生类
      public: //新增外部接口
          int var;
          void fun() { cout << "Derived" << endl; }
      };
      int main()
      {
          Derived d;
          d.var0 = 2; //直接访问虚基类的数据成员
          d.fun0();   //直接访问虚基类的函数成员
          return 0;
      }

  5. 虚基类的注意事项
    • 虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
      • 若不需要使用虚基类的带参构造函数,可以让系统默认生成
      • 如果,虚基类声明有非默认形式(带参数的)的构造函数,并且没有声明默认构造函数,那么,在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化。 
      • 在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数其他类对虚基类构造函数的调用被忽略
      • 查看代码
        #include <iostream>
        using namespace std;
        class Base0
        {
        public:
            Base0(int var) : var0(var) {}
            int var0;
            void fun0() { cout << "Member of Base0" << endl; }
        };
        class Base1 : virtual public Base0
        {
        public:
            //构造函数中对虚基类初始化(只有最远派生类中的初始化会执行)
            Base1(int var) : Base0(var) {}
            int var1;
        };
        class Base2 : virtual public Base0
        {
        public:
            //构造函数中对虚基类初始化(只有最远派生类中的初始化会执行)
            Base2(int var) : Base0(var) {}
            int var2;
        };
        class Derived : public Base1, public Base2
        {
        public:
            //构造函数中对虚基类初始化(只有最远派生类中的初始化会执行,就是这儿!)
            Derived(int var) : Base0(var), Base1(var), Base2(var)
            {
            }
            int var;
            void fun()
            {
                cout << "Member of Derived" << endl;
            }
        };
        int main()
        { //程序主函数
            Derived d(1);
            d.var0 = 2; //直接访问虚基类的数据成员
            d.fun0();   //直接访问虚基类的函数成员
            return 0;
        }
    • 派生类类构造函数的执行顺序

 

 

参考:

C++语言程序设计(第5版),郑莉,清华大学

标签:成员,基类,C++,class,派生类,public,构造函数
From: https://www.cnblogs.com/FishSmallWorld/p/17104745.html

相关文章

  • c++学习 5 预处理
    一 内存分区内存的分区变量存储,一般可以分为以下五个区,它们分别是:可读可写    堆区:使用malloc、calloc、realloc、free以及c++里面的new和delete去动态申请。可读......
  • C/C++核酸检测管理系统[2023-02-09]
    C/C++核酸检测管理系统[2023-02-09]2022级计科1班《高级语言程序设计》课程设计方案一、工作安排1、完成时间:2023年1月10日---2023年2月10日2、地点安排:因新冠疫情影响......
  • 工作线程基类,静默安装
    #if!defined(AFX_BASEWORKTHREAD_H__D104C15C_8BCD_475B_91C4_4960EBE866A4__INCLUDED_)#defineAFX_BASEWORKTHREAD_H__D104C15C_8BCD_475B_91C4_4960EBE866A4__INCLUD......
  • 2020 第十一届蓝桥杯大赛软件赛省赛(第二场),C/C++大学B组题解
    第1题——门牌制作(5分)枚举1到2020,判断有多少个字符2。答案624#include<bits/stdc++.h>usingnamespacestd;intmain(){intcnt=0;for(inti=1;i<=2020;......
  • 2021 第十二届蓝桥杯大赛软件赛省赛(第二场),C/C++大学B组题解
    第1题——求余(5分)直接输出2021%20答案:1#include<bits/stdc++.h>usingnamespacestd;intmain(){cout<<2021%20;return0;}第2题——双阶乘(5分)求2021的双阶乘......
  • C++之字符串string
    反转字符串相加转为int型:利用stoi将字符串转为整型(https://www.geeksforgeeks.org/stdstoi-function-in-cpp/)C++字符串splitintidx=str.find('')如有多个字符......
  • Memory Layout of C++ Object in Different Scenarios
    http://www.vishalchovatiya.com/memory-layout-of-cpp-object/  Inthisarticle,wewillseethememorylayoutofdifferentC++Object.Andhowdifferents......
  • C++变参模板简单使用
    为什么简单使用呢因为目前没遇到实际应用的地方就大概学一下吧template<typenameT,typename...A>voidprint(Tt,A...a){std::cout<<t;print(a...);//这个函数是通用......
  • c++ 保存txt文件
      #include<iostream>#include<stdio.h>#include<fstream>#include<queue>#include<mutex>std::stringgpssavename="/home/dongdong/v2_Project/v3_......
  • C++ 从数组中拿值,每个值不相同
    代码和思路原理就是生成0,n个索引,每个索引不相同即可。索引再到数组拿数据就行#include<iostream>#include<vector>#include<random>usingnamespacestd;defau......