派生类的访问控制和类型兼容规则

派生类继承了基类的全部数据成员和除了构造、析构函数之外的全部函数成员,但是这些成员的访问属性在派生的过程中是可以调整的。从基类继承的成员,其访问属性由继承方式控制。

基类的成员可以有 public、protected 和 private 三种。基类的自身成员可以对基类中任何一个其他成员进行访问,但是通过基类的对象就只能访问该类的公有成员。

类的继承方式有 public、protected 和 private 三种。不同的继承方式导致原来具有不同访问属性的基类成员在派生类中的访问属性也有所不同。这里的访问有两个方面,一是派生类中的新增成员访问从基类继承的成员;而是在派生类外部(非类族内的成员)通过派生类的对象访问从基类继承的成员。

公有继承

当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可以直接访问。 基类的公有和保护成员继承后仍为派生类的公有和保护成员,派生类的其他成员可以直接访问他们,类族之外则只能通过派生类的对象访问从基类继承的公有成员。派生类的成员或派生类的对象都无法直接访问基类的私有成员。

下面用一个熟悉的 Point 类派生出一个新的 Rectang 类。矩形是由点加上长和宽构成的,点具备了 Point 类的所有特征,同时矩形也具有自身的特点(长、宽),这就需要在继承 Point 类的同时添加新的成员。

//Point.h
#ifndef _POINT_H
#define _POINT_H

class Point
{
public:
    void initPoint(float x = 0, float y = 0) {this->x = x; this->y = y;}
    void move(float offX, float offY){x += offX; y += offY;}
    float getX() const {return x;}
    float getY() const {return y;}
private:
    float x, y;
};
#endif

//Rectangle.h
#ifndef _RECTANGLE_H
#define _RECTANGLE_H
#include "Point.h"

class Rectangle: public Point
{
public:
    void initRectangle(float x, float y, float w, float h)
    {
        initPoint(x, y);
        this->w = w;
        this->h = h;
    }
    float getH() const {return h;}
    float getW() const {return w;}
private:
    float w, h;
};
#endif

//主程序
#include <iostream>
#include <cmath>
#include "Rectangle.h"

using namespace std;

int main()
{
    Rectangle rect;//定义 Rectangle 类的对象
    rect.initRectangle(2, 3, 20, 10);//设置矩形的数据
    rect.move(3, 2);//移动矩形的位置

    cout << "The data of rect(x, y, w, h):" << endl;
    cout << rect.getX() << "," << rect.getY() << "," << rect.getW() << "," << rect.getH() << endl;

    return 0;
}

//程序输出内容为
The data of rect(x, y, w, h):
5,5,20,10

私有继承

当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。(本是公有和保护成员)即基类的公有成员和保护成员被继承后作为派生类的私有成员,派生类的其他成员可以直接访问它们,但是在类族外部通过派生类的对象无法直接访问它们。(本是私有成员)无论是派生类的成员还是通过派生类的对象都无法直接访问从基类继承的私有成员。

经过私有继承后,所有基类的成员都成为了派生类的私有成员或不可直接访问的成员,如果进一步派生的话,基类的全部成员就无法在新的派生类中被直接访问。因此私有继承之后,基类的成员再也无法在以后的派生类中直接发挥作用,实际上相当于中止了基类功能的继续派生,出于这种情况,一般情况下不使用私有继承。

//Point.h
#ifndef _POINT_H
#define _POINT_H

class Point
{
public:
    void initPoint(float x = 0, float y = 0) {this->x = x; this->y = y;}
    void move(float offX, float offY){x += offX; y += offY;}
    float getX() const {return x;}
    float getY() const {return y;}
private:
    float x, y;
};
#endif

//Rectangle.h
#ifndef _RECTANGLE_H
#define _RECTANGLE_H
#include "Point.h"

class Rectangle: private Point
{
public:
    void initRectangle(float x, float y, float w, float h)
    {
        initPoint(x, y);
        this->w = w;
        this->h = h;
    }
    void move(float offX, float offY){Point::move(offX, offY);}
    float getX() const {return Point::getX();}
    float getY() const {return Point::getY();}
    float getH() const {return h;}
    float getW() const {return w;}
private:
    float w, h;
};
#endif

//主函数部分
#include <iostream>
#include <cmath>
#include "Rectangle.h"

using namespace std;

int main()
{
    Rectangle rect;//定义 Rectangle 类的对象
    rect.initRectangle(2, 3, 20, 10);//设置矩形的数据
    rect.move(3, 2);//移动矩形的位置

    cout << "The data of rect(x, y, w, h):" << endl;
    cout << rect.getX() << "," << rect.getY() << "," << rect.getW() << "," << rect.getH() << endl;

    return 0;
}

//程序输出内容为
The data of rect(x, y, w, h):
5,5,20,10

派生类 Rectangle 私有继承了 Point 类的成员,这时 Point 类中的公有和保护成员在派生类中都以私有成员的身份出现。派生类的成员函数和对象无法访问基类的私有成员(如 x, y)。派生类的成员仍可以访问从基类的公有和保护成员(如 initPoint 函数)。但在类外部通过派生类的对象无法访问到基类的任何成员(如基类的 getX() 和 getY() )。

因此在私有继承的情况下为了保证基类的外部接口功能在派生类中也存在,就必须在派生类中声明同名的成员(如 move, getX, getY),利用派生类成员对基类成员对访问能力,实现这些原有基类成员的功能。根据同名隐藏的规则,在调用时即会自动调用派生类的函数。

可以看到,与公有继承相比较,私有继承时只修改了派生类 Rectangle 的内容,基类和主函数没有做任何改动,这就是面向对象程序设计的封装性的体现。Rectangle 类的外部接口不变,内部成员的实现做了改造,但并没有影响到程序的其他部分,这正是对象程序设计重用与可扩充性的一个实际体现。

保护继承

保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员不可直接访问。 这样,派生类的其他成员则可以直接访问从基类继承来的公有和保护成员,但在类的外部无法通过派生类的对象直接访问它们。无论是派生类的成员还是派生类的对象都无法直接访问基类的私有成员。

基类的保护成员可能被它的派生类访问,但是绝不可能被其他外部使用者(比如程序中的普通函数,与该派生类平行的其他类等)访问。这样,如果合理的利用保护成员,就可以在类的复杂层次关系中在共享与成员隐蔽之间找到一个平衡点,既能实现成员隐蔽,又能方便继承,实现代码的高效重用和扩充。

如果 B 是 A 的派生类,B 的成员函数只能通过 B 的对象访问 A 中定义的保护成员,而不能通过 A 的对象访问 A 的保护成员。

类型兼容规则

类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。 通过公有继承,派生类得到了基类中除构造函数、析构函数之外的所有成员,这样实际上公有派生类具备了基类的所有功能,凡是基类可以解决的问题,公有派生类都可以解决。类型兼容规则中所指的替代包括以下的情况。

  • 派生类的对象可以隐含转换为基类对象
  • 派生类的对象可以初始化基类的引用
  • 派生类的指针可以隐含转换为基类的指针
  • 在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员。

    若 D 类为基类 B 的公有派生类,则在基类 B 的对象可以出现的任何地方,都可以用派生类 D 的对象来代替。

    class B{...};
    class D: public B{...};
    
    B b1, *pb1;
    D d1;
    
    //派生类对象可以隐含转换为基类对象,即用派生类对象中从基类继承来的成员逐个赋值给基类对象的成员
    b1 = d1;
    
    //派生类对象可以初始化基类对象的引用
    B &rb = d1;
    
    //派生类对象的地址可以隐含转换为指向基类的指针
    pb1 = &d1;

由于类型兼容规则的引入,对于基类及其公有派生类的对象,可以使用相同的函数统一进行处理。因为当函数的形参为基类的对象(或引用、指针)时,实惨可以是派生类的对象(或指针),而没有必要为每一个类设计单独的模块,大大提高了程序的效率。这正是 C++ 的又一重要特色,即多态性。

#include <iostream>
using namespace std;

class Base1
{
public:
    void display() const {cout << "Base1::display()" << endl;}
};

class Base2: public Base1
{
public:
    void display() const {cout << "Base2::display()" << endl;}
};

class Derived: public Base2
{
public:
    void display() const {cout << "Derived::display()" << endl;}
};

void fun(Base1 *ptr)
{
    ptr->display();
}

int main()
{
    Base1 base1;
    Base2 base2;
    Derived derived;

    fun(&base1);
    fun(&base2);
    fun(&derived);

    return 0;
}

//输出内容为:
Base1::display()
Base1::display()
Base1::display()

这样,通过对象名.成员名或者对象指针->成员名的方式,就可以访问到各派生类中继承自基类的成员。可以将派生类对象的地址赋值给基类 Base1 的指针,但通过这个基类类型的指针只能访问到从基类继承的成员。

尽管指针指向的不是 Base1 的对象,但是 fun 函数也只能访问到从 Base1 继承来的成员函数 display() 而不是 Base2 或者 Derived 的同名成员函数。因此,主函数中三次对 fun 函数的调用结果都是一样的——访问了 Base1 的 display() 函数。

根据类型兼容规则,可以在基类对象出现的场合使用派生类对象进行替代,但替代之后的派生类仅仅发挥出基类的作用。

上一篇:手把手教你如何在阿里云服务器上搭建PHP环境?[阿里云良心作品]


下一篇:揭秘微软将进行开源的10大证据