跳转至

继承和组合

继承语法

C++是面向对象的语言,面向对象最重要的就是继承。

一个类继承另一个类,用冒号:表示继承。

三种继承: public,私有的,受保护的。

class X {
    int i;
public:
    X() {i = 0;}
    void set(int ii) {i = ii;}
    int read() const {return i;}
    int permute() {return i = i * 47;}
};

Y有两个i,一个是Y自己的,一个是继承的。但是继承的那个是私有的i,Y不能直接使用。

Y也可以自己添加成员函数,名字可以和X的一样:那么就是不要父类X的,而是用自己的函数。重定义。也可以使用X的, 不过要写上X::

class Y : public X {
    int i;
public:
    Y() {i = 0;}
    int change() {
        i = permute();
        return i;
    }
    //重定义
    void set(int ii) {
        i = ii;
        X::set(ii);
    }
};

一个类继承另一个类。会继承数据成员和成员函数,根据需要添加新的功能,自己的成员,或者把继承下来的重定义。

使用继承效率比较高。拿来直接用。

继承中的构造和析构

  • 构造函数的初始化表达式列表
  • 构造函数和析构函数调用的次序
  • 自动调用构造函数
  • 自动调用析构函数

C++可以在初始化列表中初始化,值用小括号括起来。

没有写初始化列表,C++会默认的初始化。全是默认值。

在构造函数中对成员变量进行初始化有点晚了,因为C++会默认的有初始化,相当于初始化了两遍。

两个类

Y 父类

X 继承 Y

创建X的对象的时候要先把Y创建了,调用X构造函数也要调用Y的构造函数。

在X的初始化列表里调用Y的构造函数,把Y构造出来,Y在初始化列表的位置可以随便写,顺序不固定,一般习惯写前面。

如果在X的初始化列表里没有写Y, 则自动的调用Y的没有参数的构造函数(默认的构造函数)

基类 超类 父类, 派生类 子类

class Y {
    int i;
public:
    Y(int ii) : i(ii) {}
};

class X : public Y {
    int i;
    float f;
    char c;
    char* s;
public:
    X():Y(10),i(7),f(1.4),c('x'),s("howdy") {
        std::cout << "X()" << std::endl;
    }
};
void test() {
    X x;
}

高级:

typedef int Member1;
typedef int Member2;
typedef int Member3;
typedef int Member4;
class Base1 {
public:
    Base1(int) {
        std::cout << "Base1 constructor \n";
    }
    ~Base1() {
        std::cout << "Base1 destructor \n";
    }
};
class Derived1 : public Base1 {
    Member1 m1;
    Member2 m2;
public:
    Derived1(int) : m2(1),m1(2),Base1(3) {
        std::cout << "Derived1 constructor \n";
    }
    ~Derived1() {
        std::cout << "Derived1 destructor \n";
    }
};

Derived1的构造函数的初始化列表顺序可以随便写,但是执行调用的时候会先调用Base1的构造函数,然后调用m1,再调用m2。因为成员变量Member1定义在Member2前面。

class Derived2 : public Derived1 {
    Member3 m3;
    Member4 m4;
public:
    Derived2(int) : m3(1),Derived1(2),m4(2) {
        std::cout << "Derived2 constructor \n";
    }
    ~Derived2() {
        std::cout << "Derived2 destructor \n";
    }
};

调用

void test() {
    Derived2 d2(1);
}

只调用了Derived2 d2。因为存在继承则会调用父类Derived1构造方法,Base1构造方法,初始化Derived1数据成员,Base1数据成员。这一行代码,会调用很多东西。

最先调用Base1构造方法,然后调用Derived1构造函数,然后调用Derived2自己的。

析构函数的调用和构造方法的顺序是完全相反的。

这些都是自动的。

同名的成员函数

子类和父类有同名的成员函数,子类调用函数的时候,调用自己的函数不会调用父类的。不会调用自己的还调父类的。

如果子类没有该函数 父类有该函数,子类调用函数的时候就会调用父类的。

构造函数会都调 调用父类的 自己的, 普通函数只调一个。

继承的其它要点

1、名字隐藏

基类

class Base {
public:
    //三个成员函数
    int f() const {
        std::cout << "Base::f()\n";
        return 1;
    }
    int f(std::string) const {
        return 1;
    }
    void g() {}
};

子类

继承基类

  1. 子类Derived2 继承base类

重写成员函数g()

  1. 子类Derived2 继承base类

把f重写 重新定义

Derived2 调用f的话。是调用的自己的。

调用f(字符串),方法就会报错,因为名为f的所有函数都隐藏了。

  1. 子类Derived3 继承base类

Derived3重定义f,并且返回值类型改为void,即使不一样,这也会把重名称都隐藏。有返回值的,带参数的都隐藏了。

  1. 子类Derived3 继承base类

重定义f,参数类型变了。和基类的参数类型都不一样,但是名称一样,还是会隐藏所有名字为f的函数。只能用自己的函数f,不能用基类的函数f了。

这些函数隐藏针对的都是非虚函数。

2、如果是虚函数重新定义

在多态中用

class Base {
public:
    int virtual f() const {
        std::cout << "Base::f()\n";
        return 1;
    }
    int virtual f(std::string) const {
        return 1;
    }
    void g() {}
};

3、非自动继承的函数

  • 可以自动继承
  • 数据成员
  • 函数成员

  • 不可以继承。自己用自己的。基类有,子类也不可以使用。

  • 构造函数
  • 析构函数
  • 重载=运算符

  • 其它的重载运算符 可以继承

子类可以添加自己的数据成员,函数成员,构造函数,析构函数。

4、公有继承,私有继承,保护继承

一般是用公有继承

继承相当于把基类中的所有成员拷贝下来

  • 公有继承

原来是私有的还是私有的 原来是受保护的还是受保护的 原来是公有的还是公有的。

  • 私有继承

子类拷贝基类成员,成员都变成私有的。 如果不写哪种继承默认是私有继承。

  • 保护继承

相当于子类拷贝基类成员,成员都变成受保护的。

私有继承,保护继承没有用。仅做了解。

5、多重继承

class Z : public X, public Y, public Base {
};

多继承比较复杂,容易出问题。

继承中的向上类型转换

  • 向上类型转换
  • 基类与新类之间的关系:新类属于原有类的类型
  • 向上类型转换和拷贝构造函数

继承:倒立的树形图表示,不断的继承,最上面的类叫根。

把子类型向上转为它的基类。

例:

  • 乐器类
  • 成员函数play 参数是一个枚举 中音、高音、低音

  • 长笛 继承 乐器

  • 创建一个函数 参数是乐器,调用乐器去play。

测试

创建一个乐器对象,调用函数参数传乐器对象

创建一个长笛对象,调用函数 参数传长笛对象。C++会自动的向上转型,把长笛转变为乐器类型。

可以利用这个特点, 写一些通用的函数。例:上面的play就是通用的函数,长笛,钢琴,大鼓都可以play。

向上类型转换和拷贝构造函数

class Parent {
    int i;
public:
    Parent(int ii) : i(ii) {
        cout << "Parent(int ii)\n";
    }
    //拷贝构造函数
    Parent(const Parent& b) : i(b.i) {
        cout << "Parent(const Parent& b)\n";
    }

    //没有参数的构造函数
    Parent() : i(0) {
      cout << "Parent()\n";
    }

    //运算符重载
    friend ostream& operator<<(ostream& os, const Parent& b) {
        return os << "Parent: " << b.i << endl;
    }
};
class Member {
    int i;
public:
    Member(int ii) : i(ii) {
        cout << "Member(int ii)\n";
    }
    //拷贝构造函数
    Member(const Member& m) : i(m.i) {
        cout << "Member(const Member& m)\n";
    }
    //运算符重载
    friend ostream& operator<<(ostream& os, const Member& m) {
        return os << "Member: " << m.i << endl;
    }
};
class Child : public Parent {
    int i;
    Member m;
public:
    //构造函数,调用child的构造函数,所有的i的值都变为传进来的值
    Child(int ii) : Parent(ii),i(ii),m(ii) {
        cout << "Child(int ii)\n";
    }
    friend ostream& operator<<(ostream& os, const Child& c) {
        return os << Parent(c)
        << c.m
        << "Child: " << c.i << endl;
    }
};

测试

void test() {
    Child c(2); //调用Child的构造函数 Parent的i为2,Child的i为2,Member的i为2.
    cout << c; //会输出三个2

    Child c2 = c; //调用拷贝构造函数
    cout << c2;//还是三个2  因为没有写Child的拷贝构造函数,只写了parent和Member的拷贝构造函数,会调用C++默认添加的拷贝构造函数。
}

如果要写Child的拷贝构造函数的话,不能随便写,否则可能出错。

在使用Child类的时候,用到向上转型时,则Child的拷贝构造函数也要构造父类Parent。

组合

两种常用的程序设计方法

  • 组合与继承的选择

判断类和类之间的关系

  • has-a 有一个 则组合,
  • is-a。 是一个 则继承。

组合的例子

汽车发动机

成员方法:

  • 启动
  • 后退
  • 停止

汽车轮胎

  • 充气

汽车车窗

  • 打开
  • 关闭

汽车车门

  • 车门有车窗
  • 打开车门
  • 关闭车门

汽车 组合

  • 发动机
  • 4个轮胎
  • 两个门

上面的组合还可以加上继承

  • 新型的发动机(继承发动机)
  • 新型的轮胎(继承轮胎)

汽车 (既有组合 又有继承)

继承的例子

  • 图形形状
  • 填充颜色
  • 线的颜色
  • 线的宽度
  • 画图形
  • 圆 继承形状
  • 添加 半径
  • 长方形 继承图形形状
  • 宽度
  • 高度
  • 三角形 继承图形形状
  • 圆角矩形 继承圆和矩形 双继承 C++可以多继承

组合和继承的联合

C继承B(继承), C里有A(组合)。既有继承又有组合。

class A {
};

class B {
};

class C : public B {
    A a;
};

再论继承和组合

  • 根据是否需要向上类型转换决定继承还是组合
  • 是 --> 继承
  • 否 --> 组合
  • 可以使用组合代替继承,消除多重继承

有一个类简单或者复杂的类A,使用A的时候有下面几种

  1. 可以直接使用
  2. 做一个新的类B继承它
  3. 使用组合。在C类中有类A。做为C的成员。

例:之前做的堆栈stack,保存数据是任何数据,因为使用的万能指针。但是保存字符串的时候不方便,所以自己写一个字符串堆栈StringStack。

有两种方法:继承或组合。

继承

把要修改的地方重新定义。把继承下来的方法重新定义, 原方法就隐藏了。

这里修改的主要是返回值类型,和参数类型,之前是void* 现在改为string。

重写的方法里调用父类原方法。

并且子类可以在析构函数中实现delete,因为是string*,字符串指针,而父类delete要delete void* 万能指针,delete万能指针容易出问题,所以要由客户端自己delete。

子类比父类优点:

  1. 不需要转型,直接就是字符串类型
  2. 不需要delete。析构函数中就已经delete了。

如果需要向上类型转换的话(StringStack转为Stack),则使用继承。

因为上面的没有用到转换,所以应该使用组合而不是继承。

组合

新建一个class类 特殊的堆栈 字符串堆栈

内部有一个Stack通用的堆栈

同样的方法再实现一次,把原来的方法包装一下,然后把返回值参数类型由万能指针转成字符串类型,方便使用。

有A,B,C,X,Y,Z六个class。

新建一个class继承X,Y,Z。多继承。

多继承太复杂,容易出错,所以避免使用多重继承,用组合代替继承。

只用单继承X,另外两个使用组合,类中包含Y,Zclass。