Bootstrap

C++继承与派生

1. 引入

在C++中可重用性是通过继承这一机制来实现的,继承是类与对象的精华

1.1. 归类

对于学生和老师进行归类
在这里插入图片描述

1.2. 抽取

在这里插入图片描述

1.3. 继承

在这里插入图片描述

1.4. 重用

// C++通过继承关系,实现了代码的可重用性
// 父类 共性
class Human
{
public:
  void eat(string food)
  {
    cout << "i am eating " << food << endl;
  }
};
// 子类 在父类的基础上增加新的功能,体现的是个性
class Student : public Human
{
public:
  void study(string course)
  {
    cout << "i am a student , i am learning " << course << endl;
  }
};
class Teacher : public Human
{
public:
  void teach(string course)
  {
    cout << "i am a teacher , i am teaching " << course << endl;
  }
};
int main()
{
  Student s;
  s.study("c++");
  s.eat("排骨饭");
  Teacher t;
  t.teach("java");
  t.eat("大盘鸡");
  return 0;
}

2. 定义

类的继承,是新的类从已有类那里得到已有的特性。或从已有类产生新类的过程,就是类的派生。原有的类称为基类或父类,产生的新类称为派生类或子类。
派生与继承,是同一种意义两种称谓

3. 继承

3.1. 关系定性is-a/has-a

is-a是一种属于关系,在面向对象中表现为一种继承关系
has-a是一种包含、组合关系
is-a这种关系可以完成代码复用,是继承。把比较抽象的类作为基类而做出派生类。
因此,如果A是B,则B是A的基类,A是B的派生类。为继承关系。
has-a这种关系可以把一个复杂的类处理为一个个相对简单的类,是聚合。每个类本身相对简单,通过聚合(组合)成为一个复杂的类
因此,如果A包含B,则B是A的组成部分。为聚合关系,可以由组成部分聚合成为一个类

3.2. 语法

派生类的声明

class 派生类名:[继承方式] 基类名
{
	派生类成员声明;
}

一个派生类可以同时有多个基类,这种情况称为多重继承,派生类只有一个基类,称为单继承。

3.3. 继承方式

继承方式规定了如何访问基类继承的成员。继承方式有public、private和protected。继承方式不影响派生类的访问权限,影响了从基类继承来的成员的访问权限,包括派生类内的访问权限和派生类对象。
简单讲:
公有继承:基类的公有成员和保护成员在派生类中保持原有访问属性,其私有成员仍为基类的私有成员
私有继承:基类的公有成员和保护成员在派生类中成了私有成员,其私有成员仍为基类的私有成员
保护继承:基类的公有成员和保护成员在派生类中成了保护成员,其私有成员仍为基类的私有成员
protected对于外界访问属性来说,等同于私有,但可以派生类中可见

class Father
{
public:
  int x;

protected:
  int y;

private:
  int c;
};

class Son : public Father
{
public:
  int pub;
  void func()
  {
    x = 3; // 可以访问基类的public成员
    y = 4; // 可以访问基类的protected成员
    // c = 5; // Error: 无法访问基类的private成员
  }

protected:
  int pro;

private:
  int pri;
};

// public继承方式,父类中的public成员在子类中依然是public,
// protected成员在子类中依然是protected,
// private成员无法在子类中访问。
#if 0
Father   public
pub      pub(public)
pro      pro(protected)
pri      inacceess
#endif

int main()
{
  Son s;
  s.pub; // 可以访问public成员
  // s.pro; // Error: 无法访问protected成员
  // s.pri; // Error: 无法访问private成员
  s.x = 100; // 可以访问基类的public成员
  // s.y = 200; // Error: 无法访问基类的protected成员
  return 0;
}

3.4. 派生类的组成

派生类的成员,包含两大部分,一类是从基类继承的,一类是自己增加的成员。从基类继承过来的表现其共性,而新增的成员体现其个性
在这里插入图片描述
说明:

  1. 全盘接收:除了构造器和析构器。基类有可能会造成派生类的成员冗余,所以说基类是需设计的
  2. 派生类有自己的个性,才使派生有意义
class A
{
public:
  A()
  {
    cout << this << endl;
    cout << typeid(this).name() << endl;
  }
  int a;
};
class B : public A
{
public:
  B()
  {
    cout << this << endl;
    cout << typeid(this).name() << endl;
  }
  int b;
};
class C : public B
{
public:
  C()
  {
    cout << this << endl;
    cout << typeid(this).name() << endl;
  }
  void func()
  {
    cout << &a << endl; // 0x16d0aedb0 P1A
    cout << &b << endl; // 0x16d0aedb0 P1B
    cout << &c << endl; // 0x16d0aedb0 P1C
  }
  int c;
};
int main()
{
  C c;
  cout << "&c " << &c << endl; // &c 0x16d0aedb0
  cout << "********" << endl;
  c.func();
  return 0;
}

4. 派生类的构造

派生类中由基类继承而来的成员的初始化工作还是由基类的构造函数完成,然后派生类中新增的成员在派生类的构造函数中初始化。

4.1. 派生类构造函数的语法

派生类名::派生类名(参数总表) 
	:基类名(参数表), 内嵌子对象(参数表)
{
	派生类新增成员的初始化语句; // 也可出现参数列表中
}

注:

  • 构造函数的初始化顺序并不以上面的顺序进行,而是根据声明的顺序初始化
  • 如果基类中没有默认构造函数(无参),那么在派生类的构造函数中必须显示调用基类构造函数,以初始化基类成员
  • 派生类构造函数执行的次序:基类->成员->子类
    • 调用基类构造函数,调用顺序按照他们被继承时声明的顺序(从左到右)
    • 调用内嵌成员对象的构造函数,调用顺序按照他们在类中声明的顺序
    • 派生类的构造函数体中的内容

4.2. 代码实现

  • 祖父类
    student.h
#ifndef STUDENT_H
#define STUDENT_H

#include <iostream>
using namespace std;

class Student
{
public:
  Student(string sn, int ia, float fs);
  void dis();

private:
  string name;
  int age;
  float score;
};

#endif

student.cpp

#include "student.h"

Student::Student(string sn, int ia, float fs)
    : name(sn), age(ia), score(fs)
{
}

void Student::dis()
{
  cout << "name: " << name << " age: " << age << " score: " << score << endl;
}
  • 父类
    graduate.h
#ifndef GRADUATE_H
#define GRADUATE_H

#include "student.h"
#include "birthday.h"

class Graduate : public Student
{
public:
  Graduate(string sn, int ia, float fs, double ds, int iy, int im, int id);
  void print();
  void getBirth();

private:
  double salary;
  Birthday birth;
};

#endif

graduate.cpp

#include "graduate.h"

Graduate::Graduate(string sn, int ia, float fs, double ds, int iy, int im, int id)
    : Student(sn, ia, fs), salary(ds), birth(iy, im, id)
{
}
void Graduate::print()
{
  dis(); // name age score public
  cout << "salary: " << salary << endl;
}

void Graduate::getBirth()
{
  birth.dis();
}
  • 类成员
    birthday.h
#ifndef BIRTHDAY_H
#define BIRTHDAY_H

#include <iostream>
using namespace std;

class Birthday
{
public:
  Birthday(int y, int m, int d);
  void dis()
  {
    cout << "Birthday: " << year << "/" << month << "/" << day << endl;
  }

private:
  int year;
  int month;
  int day;
};

#endif

birthday.cpp

#include "birthday.h"

Birthday::Birthday(int y, int m, int d)
    : year(y), month(m), day(d)
{
} 
  • 子类
    doctor.h
#ifndef DOCTOR_H
#define DOCTOR_H
#include "graduate.h"

class Doctor : public Graduate
{
public:
  Doctor(string sn, int ia, float fs, double ds, int iy, int im, int id, string st);
  // public dis => public  public print => public
  void dump()
  {
    print();
    cout << "title: " << title << endl;
  }

private:
  string title;
};

#endif

doctor.cpp

#include "doctor.h"

Doctor::Doctor(string sn, int ia, float fs, double ds, int iy, int im, int id, string st)
    : Graduate(sn, ia, fs, ds, iy, im, id), title(st)
{
}
  • 测试代码
#include "student.h"
#include "graduate.h"
#include "doctor.h"
int main()
{
  Student s("wyb", 27, 18.8);
  s.dis();
  Graduate g("xz", 32, 22, 10000, 1999, 9, 9);
  g.print();
  g.getBirth();
  Doctor d("lj", 23, 200, 200000, 1999, 9, 9, "doctor l");
  d.dump();

  return 0;
}

4.3. 结论

子类构造器中,要么显示的调用父类的构造器(传参),要么隐式的调用。发生隐式调用时,父类要有无参构造器或是可以包含无参构造器的默认参数函数。子类对象亦然

5. 派生类的拷贝构造

5.1. 格式

派生类::派生类(const 原生类 &another) 
    :基类(another), 派生类新成员(another, 新成员)
{
}

5.2. 代码

  • 父类
    student.h
class Student
{
public:
    Student(string sn, int n, char s);
    Student(const Student &another);
    ~Student();
    void dis();

private:
    std::string name;
    int num;
    char sex;
};

student.cpp

Student::Student(string sn, int n, char s)
    : name(sn), num(n), sex(s)
{
}
Student::~Student()
{
}
void Student::dis()
{
    cout << name << endl;
    cout << num << endl;
    cout << sex << endl;
}
Student::Student(const Student &another)
{
    name = another.name;
    num = another.num;
    sex = another.sex;
}
  • 子类
    graduate.h
class Graduate : public Student
{
public:
    Graduate(string sn, int in, char cs, float fs);
    ~Graduate();
    Graduate(const Graduate &another);
    void dump()
    {
        dis();
        cout << salary << endl;
    }

private:
    float salary;
}

graduate.cpp

Graduate::Graduate(string sn, int in, char cs, float fs)
    : Student(sn, in, cs), salary(fs)
{
}
Graduate::~Graduate()
{
}
Graduate::Graduate(const Graduate &another)
    : Student(another), salary(another.salary)
{
}
  • 测试代码
int main()
{
    Graduate g("wyb", 2001, 'x', 2000);
    g.dump();
    Graduate gg = g;
    gg.dump();
    return 0;
}

5.3. 结论

派生类中的默认拷贝构造器会调用父类中默认或自实现拷贝构造起,若派生类中自实现拷贝构造器,则必须显式的调用父类的拷贝构造器

6. 派生类的赋值运算符重载

赋值运算符函数不是构造器,所以可以继承,语法上就没有构造器的严格

6.1. 格式

子类 &子类::operator=(const 子类 &another)
{
    if (this == &another)
        return *this; // 防止自赋值
    父类::operator=(another); // 调用父类的赋值运算符重载
    this->salary = another.salary; // 子类成员初始化
    return *this;
}

6.2. 代码

  • 基类
    student.h
Student &operator=(const Student &another);

student.cpp

Student &Student::operator=(const Student &another)
{
    this->name = another.name;
    this->num = another.num;
    this->sex = another.sex;
    return *this;
}
  • 派生类
    graduate.h
Graduate &operator=(const Graduate &another);

graduate.cpp

Graduate &Graduate::operator=(const Graduate &another)
{
    if (this == &another)
        return *this;
    Student::operator=(another);
    this->salary = another.salary;
    return *this;
}
  • 测试代码
int main()
{
    Graduate g("wyb", 2001, 'x', 2000);
    g.dump();
    Graduate gg = g;
    gg.dump();
    cout << "-------" << endl;
    Graduate ggg("xz", 2002, 'x', 2000);
    ggg.dump();
    ggg = g;
    ggg.dump();
    return 0;
}

6.3. 结论

派生类的默认赋值运算符重载函数,会调用父类的默认或自实现函数。派生类若自实现,则不会发生调用行为,也不报错(区别拷贝),赋值错误,若要正确,需要显式的调用父类的构造器

7. 派生类友元函数

由于友元函数并非类成员,因而不能被继承,在某种需求下,可能希望派生类的友元函数能够使用基类中的友元函数。为此可以通过强制类型转换,将派生类的指针或是引用强转为其类的引用或指针,然后使用转换后的引用或指针来调用基类中的友元函数

class Student
{
    friend ostream &operator<<(ostream &os, Student &stu);

private:
    int a;
    int b;
};
ostream &operator<<(ostream &os, Student &stu)
{
    out << stu.a << " " << stu.b << endl;
}
class Graduate : public Student
{
    friend ostream &operator<<(ostream &os, Graduate &gra);

private:
    int c;
    int d;
};
ostream &operator<<(ostream &os, Graduate &gra)
{
    out << (Student &)gra << endl;
    out << gra.c << " " << gra.d << endl;
}
int main()
{
    Student a;
    cout << a << endl;
    Graduate g;
    cout << g << endl;
    return 0;
}

8. 派生类析构函数的语法

派生类的析构函数的功能是在该对象消亡之前进行一些必要的清理工作,析构函数没有类型,也没有参数。析构函数的执行顺序与构造函数相反

  • 析构顺序
子类->成员->基类

无需指明析构关系,析构函数只有一种,无重载,无默认参数

9. 派生类成员的标识和访问

9.1. 作用域分辨符

  • 格式
基类名::成员名; 基类名::成员名(参数表);

如果某派生类的多个基本拥有同名的成员,同时,派生类又新增这样的同名成员,在这种情况下,派生类成员将shadow(隐藏)所有基类的同名成员。这就需要这样的调用方式才能调用基类的同名成员

class Base
{
public:
    void func(int)
    {
        cout << "func" << endl;
    }
};
class Derived : public Base
{
public:
    void func()
    {
        // func(); // func 死循环
        // Base::func(); // 被shadow的成员,可以这样访问
        cout << "Derived::func" << endl;
    }
};
int main()
{
    Derived d;
    d.func(); // 访问派生类成员
    // d.Base::func(3); // 访问基类成员
    return 0;
}
  • 小结
    重载:同一作用域,函数同名不同参(个数、类型、顺序)
    隐藏:父子类中,标识符(函数、变量)相同,无关乎返值和参数(函数),或声明类型(变量)

9.2. 继承方式

9.2.1. 图示

在这里插入图片描述
在这里插入图片描述

9.3. 派生类成员属性划分

四种:公有成员、保护成员、私有成员;不可访问的成员

class Base
{
public:
    int pub;

protected:
    int pro;

private:
    int pri;
};
class Drive : public Base
{
public:
    void func()
    {
        pub = 10;
        pro = 100;
        // pri = 1000;
    }
};
int main()
{
    Base b;
    b.pub = 10;
    // b.pro = 100;
    // b.pri = 1000;
    return 0;
}

10. why public

10.1. 公有继承的意义

class Base
{
public:
    int pub;

protected:
    int pro;

private:
    int pri;
};
class Drvie : public Base
{
public:
};

private 在子类中不可见,但仍可通过父类接口访问。
在这里插入图片描述
public 作用:传承接口 间接的传承了数据(protected)。
在这里插入图片描述
protected 作用:传承数据,间接封杀了对外接口。
在这里插入图片描述
private 统杀了数据和接口

  1. 只要是私有成员到派生类中,均不可访问。正是体现的数据隐藏性,其私有成员仅可被本类的成员函数访问
  2. 如果多级派生当中,均采用public,直到最后一级,派生类中均可访问基类的public、protected成员。
    兼顾了数据的隐藏性和接口传承和数据传递
  3. 如果多级派生当中,均采用private,直到最后一级,派生类中基类的所有成员均变为不可见。
    只兼顾了数据的隐藏性
  4. 如果多级派生当中,均采用protected,直到最后一级,派生类的基类的所有成员即使可见,也均不可被类外调用。
    只兼顾了数据的隐藏性和数据传递
    综上所述:记住public足矣

10.3. 使用Qt类库

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class MainWindow : public / protected / private QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();
};
#endif

10.4. 使用私有继承和保护继承存在意义

此两种方式有效的防止了基类公有接口的扩散,是一种实现继承,而不是一种单独的is-a的关系了。

class Windows
{
public:
    一般常见基础接口
protected:
    常用特效接口
    高级特效接口
private:
    duang特效接口
};

11. 多继承

从继承类别上分,继承可分为单继承和多继承。

11.1. 继承语法

派生类名::派生类名(参数总表)
    :基类名1(参数表1), 基类名2(参数表2), ...基类名n(参数表n)
    内嵌子对象1(参数表1), 内嵌子对象2(参数表2), ...内嵌子对象n(参数表n)
{
    派生类新增成员的初始化语句;
}

11.2. 代码

  • 床类
    bed.h
#ifndef BED_H_
#define BED_H_

class Bed
{
public:
    Bed();
    void sleep();
};

#endif

bed.cpp

#include "bed.h"
#include <iostream>
using namespace std;

void Bed::sleep()
{
    cout << "Sleeping in bed" << endl;
}
  • 沙发类
    sofa.h
#ifndef _SOFA_H_
#define _SOFA_H_

class Sofa
{
public:
    Sofa();
    void sit();
};
#endif // _SOFA_H_

sofa.cpp

#include "sofa.h"
#include <iostream>
using namespace std;

Sofa::Sofa() {}
void Sofa::sit()
{
    cout << "Sofa is sitting" << endl;
}
  • 沙发床类
    sofabed.h
#ifndef SOFABED_H
#define SOFABED_H

#include "bed.h"
#include "sofa.h"

class SofaBed : public Bed, public Sofa
{
};
#endif // SOFABED_H

sofabed.cpp

#include "sofabed.h"
  • 测试代码
    main.cpp
#include "sofa.h"
#include "bed.h"
#include "sofabed.h"
int main10()
{
    Sofa sf;
    sf.sit();
    Bed bed;
    bed.sleep();
    SofaBed sb;
    sb.sleep();
    sb.sit();
    return 0;
}

11.3. 三角问题(二义性问题)

多个父类中重名的成员,继承到子类中后,为了避免冲突,携带了各父类的作用域信息,子类中要访问继承下来的重名成员,则会产生二义性,为了避免冲突,访问时需要还有父类的作用域信息。

class X
{
public:
    X(int d) : _data(d) {}
    void setData(int i)
    {
        _data = i;
    }
    int _data;
};
class Y
{
public:
    Y(int d) : _data(d) {}
    int getData()
    {
        return _data;
    }
    int _data;
};
class Z : public X, public Y
{
public:
    Z() : X(2), Y(3) {}
    void dis()
    {
        cout << X::_data << endl;
        cout << Y::_data << endl;
    }
};
int main()
{
    Z z;
    z.dis();
    z.setData(5);
    cout << z.getData() << endl;
    return 0;
}

11.4. 钻石问题

11.4.1. 三角转四角

采用提取公因式的方法

class M
{
public:
    M(int i) : _data(i)
    {
    }
    int _data;
};
class X : public M
{
public:
    X(int d) : M(d)
    {
    }
    void setData(int i)
    {
        _data = i;
    }
};
class Y : public M
{
public:
    Y(int d) : M(d)
    {
    }
    int getData()
    {
        return _data;
    }
};
class Z : public X, public Y
{
public:
    Z() : X(2), Y(3)
    {
    }
    void dis()
    {
        cout << X::_data << endl;
        cout << Y::_data << endl;
    }
};
int main()
{
    Z z;
    z.dis();
    z.setData(2000);
    cout << z.getData() << endl;
    return 0;
}

11.4.2. 虚继承

class M
{
public:
    M(int i) : _data(i)
    {
    }
    int _data;
};
class X : virtual public M
{
public:
    X(int d) : M(d)
    {
    }
    void setData(int i)
    {
        _data = i;
    }
};
class Y : virtual public M
{
public:
    Y(int d) : M(d)
    {
    }
    int getData()
    {
        return _data;
    }
};
class Z : public X, public Y
{
public:
    Z(int _x, int _y): X(_x), Y(_y), M(100) 
    {
    }
    void dis()
    {
        cout << X::_data << endl;
        cout << Y::_data << endl;
    }
};
int main()
{
    Z z;
    z.dis();
    z.setData(2000);
    cout << z.getData() << endl;
    return 0;
}

11.4.3. 小结

  • 虚继承的意义
    在多继承中,保存共同基类的多份同名成员,虽然有时是不必要的,可以在不同的数据成员中分别存放不同的数据,但在大多数情况下,是我们不希望出现的,因为保存多份数据成员的拷贝,不仅占有较多的存储空间,还增加了访问困难。
    为此,C++提供了虚基类和虚继承机制,实现了在多继承中只保留一份共同成员。
    虚继类,需要设计和抽象,虚继承,是一种继承的扩展
  • 语法总结
  1. M类称为虚基类,是抽象和设计的结果
  2. 虚继承语法
class 派生类名:virtual 继承方式 基类
  1. 虚基类及间接类的实例化
class A
{
    A(int i)
    {
    }
};
class B : virtual public A
{
    B(int n) : A(n)
    {
    }
};
class C : virtual public A
{
    C(int n) : A(n)
    {
    }
};
class D : public B, public C
{
    D(int n) : A(n), B(n), C(n)
    {
    }
};
;