7.2 单继承

7.2 单继承

前一节讲述了单继承的基本概念。这一节讲述单继承的使用。在单继承中,每个类可以生成多个派生类,但是每个派生类只能有一个基类。派生类中只有一个基类是单继承的主要特点。

7.2.1 派生类构造函数和析构函数

派生类的构造函数和析构函数的构造是重要问题,读者需要掌握它们。

1. 派生类的构造函数

派生类的数据成员包含了基类中的数据成员和派生类中新定义的数据成员。由于构造函数不能被继承,因此,派生类的构造函数必须通过调用基类的构造函数来初始化基类的数据成员。如果派生类中还有子对象,也应包含对子对象初始化的构造函数。

派生类构造函数的一般格式如下:

派生类名(派生类构造函数参数表) : 基类构造函数(参数表1), 子对象名(参数表2) {
    // 派生类中数据成员初始化
}

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

  1. 基类的构造函数
  2. 子对象类的构造函数(如果有)
  3. 派生类构造函数

下面通过一个例子说明派生类构造函数的使用。

[例7.4] 分析下列程序的输出结果
#include <iostream>
using namespace std;

class A {
public:
    A() {
        a = 0;
        cout << "A's default constructor called.\n";
    }
    A(int i) {
        a = i;
        cout << "A's constructor called.\n";
    }
    ~A() {
        cout << "A's destructor called.\n";
    }
    void Print() const {
        cout << a << ",";
    }
    int GetA() {
        return a;
    }
private:
    int a;
};

class B : public A {
public:
    B() {
        cout << "B's default constructor called.\n";
    }
    B(int i, int j, int k) : A(i), aa(j) {
        b = k;
        cout << "B's constructor called.\n";
    }
    ~B() {
        cout << "B's destructor called.\n";
    }
    void Print() {
        A::Print();
        cout << b << ",";
        cout << aa.GetA() << endl;
    }
private:
    int b;
    A aa;
};

int main() {
    B bb[2];
    bb[0] = B(1, 2, 5);
    bb[1] = B(3, 4, 7);
    for(int i = 0; i < 2; i++) {
        bb[i].Print();
    }
    return 0;
}

执行该程序输出结果如下:

A's default constructor called.
A's default constructor called.
B's default constructor called.
A's default constructor called.
A's default constructor called.
B's default constructor called.
A's constructor called.
A's constructor called.
B's constructor called.
B's destructor called.
A's destructor called.
A's destructor called.
A's constructor called.
A's constructor called.
B's constructor called.
B's destructor called.
A's destructor called.
A's destructor called.
1,5,2
3,7,4
B's destructor called.
A's destructor called.
A's destructor called.
B's destructor called.
A's destructor called.
A's destructor called.

说明

  1. 该程序中,先定义了类A,接着定义类B,它是类A的派生类,继承方式为公有继承。

  2. 派生类B的构造函数格式如下:

    B(int i, int j, int k) : A(i), aa(j) {
        b = k;
        cout << "B's constructor called.\n";
    }
    

    其中,B是派生类构造函数名,它的总参数表中有3个参数:参数i用来初始化基类的数据成员,参数j用来初始化类B中的子对象aa,参数k用来初始化类B中的数据成员b。冒号后面的是成员初始化列表,如果该表有多项,它们之间用逗号分隔。

  3. 对该程序执行后的输出结果作如下分析:

    • 先创建具有两个对象元素的对象数组。调用两次类B的默认构造函数,每调用一次类B的默认构造函数时会调用两次类A的默认构造函数和一次类B的默认构造函数,于是出现了输出结果的前6行。
    • 接着,程序中出现两个赋值语句,即给两个已定义的对象(对象数组元素)赋值。系统要建立一个临时对象,通过调用B类的三个参数的构造函数对它初始化,并将其值赋给左值对象,再调用B类的析构函数将临时对象删除。在B类的析构函数中隐含了基类的析构函数和子对象aa的析构函数,其调用顺序与相应的构造函数相反,结果中出现了两个调用派生类B的构造函数和析构函数的12行输出信息。最后,程序结束前由系统自动调用派生类B的析构函数,显示输出了删除两个对象的6行信息。
2. 派生类的析构函数

当对象被删除时,派生类的析构函数就被执行。由于析构函数也不能被继承,因此在执行派生类的析构函数时,基类的析构函数也将被调用。执行顺序是先执行派生类的析构函数,再执行基类的析构函数,其顺序与执行构造函数时的顺序正好相反。这一点从前面讲过的例7.4可以看出,请读者自行分析。

下面通过一个简单例子进一步说明析构函数的执行顺序。

[例7.5] 分析下列程序的输出结果
#include <iostream>
using namespace std;

class M {
public:
    M() {
        m1 = m2 = 0;
    }
    M(int i, int j) {
        m1 = i;
        m2 = j;
    }
    void print() {
        cout << m1 << "," << m2 << ",";
    }
    ~M() {
        cout << "M's destructor called.\n";
    }
private:
    int m1, m2;
};

class N : public M {
public:
    N() {
        n = 0;
    }
    N(int i, int j, int k) : M(i, j), n(k) {}
    void print() {
        M::print();
        cout << n << endl;
    }
    ~N() {
        cout << "N's destructor called.\n";
    }
private:
    int n;
};

int main() {
    N n1(5, 6, 7), n2(-2, -3, -4);
    n1.print();
    n2.print();
    return 0;
}

执行该程序输出如下结果:

5,6,7
-2,-3,-4
N's destructor called.
M's destructor called.
N's destructor called.
M's destructor called.

该程序的上述结果请读者自己分析。

3. 派生类构造函数使用中应注意的问题

在实际应用中,派生类构造函数中应该隐含或显式包含基类中的构造函数。派生类的构造函数中需要包含基类的默认构造函数时,派生类构造函数隐含着基类的默认构造函数;派生类的构造函数中需要包含基类的带参数的构造函数时,派生类构造函数显式包含基类的带参数的构造函数。

下面举例说明这一点。

[例7.6] 分析下列程序输出结果
#include <iostream>
using namespace std;

class A {
public:
    A() {
        a = 0;
    }
    A(int i) {
        a = i;
    }
    void print() {
        cout << a << ",";
    }
private:
    int a;
};

class B : public A {
public:
    B() {
        b1 = b2 = 0;
    }
    B(int i) : A(i) {
        b1 = i;
        b2 = 0;
    }
    B(int i, int j, int k) : A(i), b1(j), b2(k) {}
    void print() {
        A::print();
        cout << b1 << "," << b2 << endl;
    }
private:
    int b1, b2;
};

int main() {
    B d1;
    B d2(5);
    B d3(4, 5, 6);
    d1.print();
    d2.print();
    d3.print();
    return 0;
}

执行该程序输出如下结果:

0,0,0
5,5,0
4,5,6

说明

该程序中,派生类B内定义了三个构造函数,前两个构造函数没有显式地调用基类构造函数,其实它们却隐式地调用了基类A中的默认构造函数,由于不需要任何参数,所以可在派生类的构造函数的定义中省去对它的调用。第三个构造函数显式地调用了基类A中的第二个构造函数。

 

7.2.2 子类型和赋值兼容规则

1. 子类型

子类型用来描述类型之间的一般和特殊的关系。当有一个已知类型S,它至少提供类型T的行为,同时还可以包含自己的行为,这时则称类型S是类型T的子类型。子类型的概念涉及行为共享,即代码重用的问题,与继承有着密切的关系。在继承中,公有继承可以实现子类型。例如:

class A {
public:
    void Print() const {
        cout << "A::Print() called.\n";
    }
};

class B : public A {
public:
    void f() {}
};

类B继承了类A,并且是公有继承方式。因此,可以说类B是类A的一个子类型。类B具备类A中的操作,或者说类A中的操作可以用于类B的对象。

分析下列程序:

void f1(const A& r) {
    r.Print();
}

int main() {
    B b;
    f1(b);
    return 0;
}

执行该程序将会输出如下结果:

A::Print() called.

从这个程序中可以看出,类B的对象b交给了处理类A对象的函数f1()进行处理。这就是说,对类A的对象操作的函数,也可以对类B的对象进行操作。

子类型关系是不可逆的。这就是说,已知B是A的子类型,而认为A也是B的子类型是错误的。或者说,子类型关系是不对称的。因此,公有继承可以实现子类型化。

2. 类型适应

类型适应是指两种类型之间的关系。例如,B类型适应A类型是指B类型的对象能够用于A类型的对象所能使用的场合。前面讲过的派生类的对象可以用于基类对象所能使用的场合,因此派生类适应于基类。同样道理,派生类对象的指针和引用也适应于基类对象的指针和引用。子类型与类型适应是一致的。如果A类型是B类型的子类型,那么B类型必将适应于A类型。

子类型的重要性在于减轻程序员编写程序代码的负担,同时也是提高代码重用率的措施。因为一个函数可以用于某类型的对象,则它也可用于该类型的各个子类型的对象,这样就不必为处理这些子类型的对象去重载该函数。

3. 赋值兼容规则

前面已讲过,类B公有继承类A时,则类B是类A的子类型,并且类B适应于类A。这就是说,类B的对象可以用于类A对象所能使用的场合。

在C++中,赋值兼容规则规定:

  1. 派生类对象可以赋值给基类的对象。
  2. 派生类对象的地址值可以赋值给基类的对象指针。
  3. 派生类对象可以用来给基类对象引用初始化。

使用上述规则时,必须注意两点:

  1. 必须是派生类公有继承基类的条件。
  2. 上述三条规定是不可逆的。
[例7.7] 分析下列程序的输出结果
#include <iostream>
using namespace std;

class A {
public:
    A() {
        a = 0;
    }
    A(int i) {
        a = i;
    }
    void print() {
        cout << a << endl;
    }
    int getA() {
        return a;
    }
private:
    int a;
};

class B : public A {
public:
    B() {
        b = 0;
    }
    B(int i, int j) : A(i), b(j) {}
    void print() {
        A::print();
        cout << b << endl;
    }
private:
    int b;
};

void fun(A& d) {
    cout << d.getA() * 10 << endl;
}

int main() {
    B bb(9, 5);
    A aa(5);
    aa = bb;
    aa.print();

    A *pa = new A(8);
    B *pb = new B(1, 2);
    pa = pb;
    pa->print();

    fun(bb);

    delete pa;
    delete pb;

    return 0;
}

执行该程序输出如下结果:

9
1
90

说明

  1. 该程序中,类B以公有继承方式继承了类A,则类B是类A的子类型。类B是派生类,类A是基类。类B和类A之间满足赋值兼容规则。

  2. 该程序中有三处用到了赋值兼容规则中的规定。

    1. 在语句 aa = bb; 中,右值是派生类B的对象,左值是基类A的对象。根据赋值兼容规则的第一条,该语句是合法的。请读者上机验证,将上述语句改为 bb = aa; 时,是否出现编译错误。
    2. 在语句 pa = pb; 中,右值是派生类B的对象指针,左值是基类A的对象指针。根据赋值兼容规则的第二条,该语句是合法的。请读者上机验证,将上述语句改为 pb = pa; 时,是否出现编译错误。
    3. 在语句 fun(bb); 中,函数 fun 的实参为类B对象 bb,形参为类A的对象引用。在调用函数 fun 时,需将其实参 bb 传递给形参 d。根据赋值兼容规则第三条,允许将派生类B的对象 bb 给基类A的对象引用 d 初始化。请读者上机验证,将上述函数 fun 的形参改为 B& d,将实参改为 aa 是否出现编译错误。

该程序验证了赋值兼容规则中的三条规定。这三条规定还会在后面的程序中出现,请读者分析。

 

 

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏驰和徐策

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值