OOP 学习笔记(5)——组合与继承

OOP 学习笔记(5)——组合与继承

组合与继承

对象类之间的关系

一般有两种:

  • has-a:一个类是另一个类的组成部分,
  • is-a:一个类是另一个类的特殊形式。

也就是整体与部分,一般与特殊两种。

组合

如果一个对象 A 是另一个 B 的一个组成部分,则称 B 是 A 的整体对象,A 是 B 的部分对象

B 和 A 的关系称为“整体-部分”关系(组合关系,has-a 关系)。

两种实现方法

  • 已有类的对象作为新类的公有数据成员,通过允许直接访问子对象而“提供”旧类接口。
  • 已有类的对象作为新类的私有数据成员,新类可以调整旧类的对外接口(新建接口),可以不适用旧类原有接口。

组合关系中的构造与析构

子对象构造时若需要参数,则在当前类的构造函数的初始化列表中进行。

若使用默认构造函数,则不做任何处理。

具体构造时,先进行子对象的构造,相互顺序由类中声明顺序决定(见前),之后再完成当前当前对象的构造。

析构时顺序则相反。

继承

”一般-特殊“结构,也称分类结构,是由一组具有”一般-特殊“关系的类所组成的结构。

  • 如果类 A 具有类 B 全部的属性和服务,而且具有自己特有的某些属性和服务,则称 A 为 B 的特殊类,B 为 A 的一般类

  • 如果类 A 的全部对象都是类 B 的对象,而且类 B 中存在不属于类 A 的对象,则 A 是 B 的特殊类,B 是 A 的一般类

类之间的继承

C++ 中使用继承来描述类之间的”一般-特殊“结构。

上面的类 A 就是继承了类 B。

基类与派生类

被继承的已有类,被称为基类(base class),又称父类

通过继承得到的新类,被称为派生类(derived class),又称子类扩展类

继承方式

常见继承方式有两种:publicprivate

  • class Derived : [private] Base {/*...*/}; 缺省继承方式默认为 private 继承。
  • class Derived : public Base {/*...*/};

protected 继承很少被使用。

  • class Derived: protected Base {/*...*/};

派生类对象构造与析构

基类中的数据成员,通过继承成为派生类对象的一部分,需要在构造派生类对象的过程中调用基类构造函数来正确初始化。

  • 若没有显式调用,则编译器自动调用一次基类的默认构造函数
  • 若想要显式调用,则只能在派生类构造函数的初始化成员列表中进行,默认构造函数与其他构造函数均可被调用。

具体构造执行时,先执行基类的构造函数来初始化继承来的数据,再执行派生类的构造函数。

析构时,先执行派生类的析构函数,再执行由编译器自动调用的基类析构函数。

实例一

class Base 
{
    int data;
public:
    Base() : data(0) /// 默认构造函数
    {
        cout << "Base::Base(" << data << ")\n";
    }
    Base(int i) : data(i)
    {
        cout << "Base::Base(" << data << ")\n";
    }
};

class Derive : public Base
{
public:
    Derive()
    {
        cout << “Derive::Derive()” << endl; /// 无显示调用基类构造函数,则调用基类默认构造函数
    }
};

int main()
{
    Derive obj; 
    return 0;
}

若没有定义基类的默认构造函数(基类也无自动生成的构造函数),则派生类不能无显式调用基类构造函数。

实例二

class Base
{
    int data;
public:
    Base() : data(0) /// 默认构造函数
    {
        cout << "Base::Base(" << data << ")\n";
    }
    Base(int i) : data(i)
    {
        cout << "Base::Base(" << data << ")\n";
    }
};

class Derive : public Base
{
public:
    Derive(int i) : Base(i)
    {
        cout << “Derjve::Derive()” << endl; /// 显示调用基类构造函数
    }
};

int main()
{
    Derive obj(356);    
    return 0;
}

继承基类构造函数

这是 C++11 的特性。

在派生类中使用 using Base::Base; 来继承基类构造函数,相当于给派生类“定义”了相应的构造函数:

class Base
{
    int data;
public:
    Base(int i) : data(i)
    {
        cout << "Base::Base(" << data << ")\n";
    }
};

class Derive : public Base
{
public:
    using Base::Base; // 相当于 Derive(int i) : Base(i){}
};

int main()
{
    Derive obj(356); // 结果为 Base::Base(356)
    return 0;
}

注意上面这个例子不能使用 Derive obj; 构造对象,因为 Derive 没有默认构造函数,也就是 Base 没有默认构造函数。

如果基类存在多个构造函数,使用 using 则会自动构造多个相应的构造函数。

class Base
{
    int data;
public:
    Base(int i) : data(i)
    {
        cout << "Base::Base(" << i << ")\n";
    }
    Base(int i, int j)
    {
        cout << "Base::Base(" << i << ", " << j << ")\n";
    }
};

class Derive : public Base
{
public:
    using Base::Base; // 相当于 Derive(int i) : Base(i){}
                      // 再加上 Derive(int i, int j) : Base(i,j){}
};

int main()
{
    Derive obj(356); // Base::Base(356)
    Derive obj2(356, 789); // Base::Base(356, 789)
    return 0;
}

基类构造函数的参数默认值不会被派生类继承。

默认值会导致产生多个构造函数版本,但都会被派生类继承

如果基类的某个构造函数被声明为私有成员函数private),则不能在派生类中声明继承该构造函数。(也就是不会继承私有的构造函数

如果派生类使用了继承构造函数,编译器就不会再为派生类生成默认构造函数

继承方式的选择

在具体讲三种继承方式之前,先说一下保护(protected)成员

保护成员与私有成员十分相似,但是保护成员可以在派生类中无论哪种继承方式)被访问,而私有成员则只能被基类内成员和友元访问。

而三种继承方式的最大不同就是派生类将基类的成员即成为了什么类型的成员:

  • public 继承(is-a):所有成员类型保持不变。最常用。
  • protected 继承public 成员继承为 protected 成员,其他不变。
  • private 继承(is-implementing-in-terms-of):所有成员均继承为 private 成员。

这里的权限仅用于派生类被继承时和派生类对象直接访问时的权限。

而关于派生类内部的访问权限:

  • 基类 private 成员均不能被派生类访问。
  • 基类 protected 成员均可被派生类访问。

private 继承一般使用组合替代,可用于隐藏或公开基类的部分接口,公开方法为:

using Base::FuncName;

会公开同名的所有重载函数。(注意没有括号)

protected 继承几乎不用,存在只是为了语言的完备性。

对于基类中的 publicprotected 成员,若其不能被派生类对象访问时,可在派生类 public 部分用下列方法声明使得其可被派生类对象访问:

using Base::ValName;

组合与继承总结

组合与继承优点

支持增量开发,也就是允许加入新代码而不影响已有代码正确性。

两者相似点

  • 实现代码重用。
  • 将子对象引入新类。
  • 使用构造函数的初始化成员列表初始化。

两者不同点

  • 组合:
    • 嵌入一个对象以实现新类功能。
    • has-a 关系。
  • 继承:
    • 沿用已存在的类提供的接口。
    • public 继承:is-a
    • private 继承:is-implementing-in-terms-of

重写隐藏与重载

重载(overload)

目的:提供同名函数的不同实现,属于静态多态。

程序编译时系统就能决定调用哪个函数,因此静态多态性又称为编译时的多态性。

多态分为两类:静态多态性动态多态性。之前的函数重载和运算符重载的实现均属于静态多态。静态多态性是通过函数的重载实现的。动态多态性是在程序运行过程中才动态地确定操作所针对的对象,又称运行时的多态,是通过虚函数实现的。

函数名必须相同,函数参数列表必须不同,作用域相同(比如均为全局函数或者同为类的成员函数)。

重写隐藏(redefining)

目的:在派生类中重新定义基类函数,实现派生类特殊功能。

屏蔽了基类所有的其它同名函数

函数名必须相同,函数参数列表可以不同。

当然可以在派生类中通过 using ClassName::FuncName; 的方法恢复指定基类成员函数,这样只会用重写的函数覆盖掉基类中对应的相同参数列表的函数。

#include <iostream>
using namespace std;

class T {};

class B
{
public:
    void f()
    {
        cout << "B::f()\n";
    }
    void f(int i)
    {
        cout << "B::f(" << i << ")\n";
    }
    void f(double d)
    {
        cout << "B::f(" << d << ")\n";
    }
    void f(T)
    {
        cout << "B::f(T)\n";
    }
};

class D1 : public B
{
public:
    using B::f; // 恢复基类函数
    void f(int i)
    {
        cout << "D1::f(" << i << ")\n";
    }
};

int main()
{
    D1 d;
    d.f(10);
    d.f(4.9);
    d.f();
    d.f(T());
    return 0;
}

运行结果为:

D1::f(10)
B::f(4.9)
B::f()
B::f(T)

向上类型转换

派生类对象/引用/指针转换成基类对象/引用/指针,称为向上类型转换。(向上是因为在继承图上是上升的)

仅对于 public 继承有效。

向上类型转换可以由编译器自动完成,是一种隐式类型转换

凡是接受基类对象/引用/指针的地方(如函数参数),均可使用派生类对象/引用/指针,编译器将自动进行向上类型转换。

对象的向上类型转换实例

#include <iostream>
using namespace std;

class Base
{
public:
    void print()
    {
        cout << "Base::print()" << endl;
    }
};

class Derive : public Base
{
public:
    void print()
    {
        cout << "Derive::print()" << endl;
    }
};

void fun(Base obj)
{
    obj.print();
}

int main() 
{
    Derive d;
    d.print();  
    fun(d);
    return 0;
}

运行结果为:

Derive::print()
Base::print()

此处 dobj 就是产生了对象切片。

对象切片

当派生类对象(不是指针/引用)被转换为基类对象时,派生类对象被切片为对应基类的子对象。

而派生类的指针和引用进行相同操作时(上面实例)虽然也有同样结果,但并不是因为对象切片的产生,而是编译器早绑定导致。

对象切片会导致派生类特有的数据丢失,包括成员函数和数据成员。

实例类似上面。

多重继承

派生类可同时继承多个基类。

实例

class File {};
class InputFile : public File {};
class OutputFile : public File {};
class IOFile : public InputFile, public OutputFile {};

多重继承问题

  • 数据存储
    • 如果派生类 D 继承的两个基类 A,B 是同一基类 Base 的不同继承,则 A,B 中继承自 Base 的数据成员会在 D 中有两份独立的副本,可能带来数据冗余。
  • 二义性
    • 如果派生类 D 继承的两个基类 A,B 有同名成员 a,则访问 D 中 a 时,编译器无法判断要访问哪一个基类成员。

具体示例

#include <iostream>
using namespace std;

class Base
{
public:
    int a{0};
};

class A : public Base
{
public:
    void fooA()
    {
        cout << "a=" << ++a << endl;
    }
    void bar()
    {
        cout << "A::bar()" << endl;
    }
};

class B : public Base
{
public:
    void fooB()
    {
        cout << "a=" << ++a << endl;
    }
    void bar()
    {
        cout << "B::bar()" << endl;
    }
};

class D : public A, public B {};

int main()
{
    D d;
    d.fooA(); // 输出 a=1
    d.fooB(); // 输出 a=1
    // cout << d.a; 编译错误,a 被 A 和 B 共有
    cout << d.A::a; // 输出 A 中成员 a 的值
    // d.bar(); // 编译错误,bar() 被 A 和 B 共有
    d.A::bar(); // 输出 A::bar()
    return 0;
}

 

点赞 0

No Comments

Add your comment