从零开始学C++之继承(一):公有/私有/保护继承、overload/overwrite/override之间的区别

一、继承

C++很重要的一个特征就是代码重用。在C语言中重用代码的方式就是拷贝代码、修改代码。C++可以用继承或组合的方式来重用。通过组合或继承现有的的类来创建新类,而不是重新创建它们。

继承是使用已经编写好的类来创建新类,新的类具有原有类的所有属性和操作,也可以在原有类的基础上作一些修改和增补。 新类称为派生类或子类,原有类称为基类或父类 派生类是基类的具体化

(一)、派生类的声明语法为: class 派生类名 : 继承方式  基类名 {            派生类新增成员的声明; }

(二)、公有/私有/保护成员

在关键字public后面声明,它们是类与外部的接口,任何外部函数都可以访问公有类型数据和函数。 在关键字private后面声明,只允许本类中的函数访问,而类外部的任何函数都不能访问。 在关键字protected后面声明,与private类似,其差别表现在继承与派生时对派生类的影响不同

(三)、公有/私有/保护继承

(四)、接口继承与实现继承

我们将类的公有成员函数称为接口。 公有继承,基类的公有成员函数在派生类中仍然是公有的,换句话说是基类的接口成为了派生类的接口,因而将它称为接口继承。 实现继承,对于私有、保护继承,派生类不继承基类的接口。派生类将不再支持基类的公有接口,它希望能重用基类的实现而已,因而将它称为实现继承。

#include <iostream>
using namespace std;

class Base
{
public:
    int x_;
protected:
    int y_;
private:
    int z_;
};

class PublicInherit : public Base
{
public:
    void Test()
    {
        x_ = 10;
        y_ = 20;
        //z_ = 30; error
    }
private:
    int a_;
};

class PublicPublicInherit : public PublicInherit
{
public:
    void Test()
    {
        y_ = 20;
    }
};

class PrivateInherit : private Base
{
public:
    void Test()
    {
        x_ = 10;
        y_ = 20;
        //z_ = 30; error
    }
};

int main(void)
{
    PublicInherit pub;
    pub.x_ = 20;

    PrivateInherit pri;
    //pri.x_ = 10; error
    return 0;
}

(五)、继承与重定义

对基类的数据成员的重定义 对基类成员函数的重定义分为两种

overwrite(隐藏) override(覆盖)

(六)、继承与组合

无论是继承与组合本质上都是把子对象放在新类型中,两者都是使用构造函数的初始化列表去构造这些子对象。 组合通常是在希望新类内部具有已存在的类的功能时使用,而不是希望已存在类作为它的接口。组合通过嵌入一个对象以实现新类的功能,而新类用户看到的是新定义的接口,而不是来自老类的接口。(has-a) 如果希望新类与已存在的类有相同的接口(在这基础上可以增加自己的成员)。这时候需要用继承,也称为子类型化。(is-a)

#include <iostream>
using namespace std;

class Base
{
public:
    Base() : x_(0), y_(48)
    {

    }
    int GetBaseX() const
    {
        return x_;
    }

    int GetBaseY() const
    {
        return y_;
    }
    void Show()
    {
        cout << "Base::Show ..." << endl;
    }
    int x_;
private:
    int y_; //继承后无法被直接访问,可通过GetBaseY访问
};

class Derived : public Base
{
public:
    Derived() : x_(0)
    {

    }
    int GetDerivedX() const
    {
        return x_;
    }
    void Show(int n)//与下面的show 构成重载,基类的show被隐藏
    {
        cout << "Derived::Show " << n << endl;
    }

    void Show()
    {
        cout << "Derived::Show ..." << endl;
    }
    int x_; //重定义x_,基类的x_被隐藏
};

//组合关系
class Test
{
public:
    Base b_;
    int x_;
};

int main(void)
{
    Derived d;
    d.x_ = 10;
    d.Base::x_ = 20; //访问被隐藏的基类x_;
    cout << d.GetBaseX() << endl;
    cout << d.GetDerivedX() << endl;
    cout << d.GetBaseY() << endl;

    d.Show();
    d.Base::Show();//访问被隐藏的基类show

    cout << sizeof(Derived) << endl;
    cout << sizeof(Test) << endl;

    return 0;
}

下面总结一下overload/overwrite/override 之间的区别:

成员函数被重载(overload)的特征: (1)相同的范围(在同一个类中); (2)函数名字相同; (3)参数不同; (4)virtual关键字可有可无。

覆盖(override)是指派生类函数覆盖基类函数,特征是: (1)不同的范围(分别位于派生类与基类); (2)函数名字相同; (3)参数相同; (4)基类函数必须有virtual关键字。

隐藏(overwrite)(派生类与基类) (1)不同的范围(分别位于派生类与基类); (2)函数名与参数都相同,基类无virtual关键字 (3)函数名相同,参数不同,virtual可有可无

当隐藏发生时(实际上是继承了但不可见),如果在派生类的成员函数中想要调用基类的被隐藏函数,可以使用

“ 基类名::函数名(参数)”的语法形式,如果被隐藏的函数是public的,则在类体外也可以使用“ 派生类对象.基类名::函数名(参数)” 的语法,也可用“ 派生类指针->基类名::函数名(参数)”的语法,同理被隐藏的数据成员也可以使用上述列举的方法访问。

或者是 parent* p = new child(); p->func(param);  形式。

注:经试验,即使是覆盖的情况,也可以使用上面说的原则(不包括最后一种方式)去访问父类的虚函数,

此时的调用就不是多态了。

如果不属于上述的情况,则是一般的继承,则使用一般的访问语法即可。

二、用C++设计一个不能继承的类

在Java中定义了关键字final,被final修饰的类不能被继承。但在C++中没有final这个关键字,要实现这个要求还是需要花费一些精力。

首先想到的是在C++ 中,子类的构造函数会自动调用父类的构造函数。同样,子类的析构函数也会自动调用父类的析构函数。要想一个类不能被继承,我们只要把它的构造函数和析构函数都定义为私有函数。那么当一个类试图从它那继承的时候,必然会由于试图调用构造函数、析构函数而导致编译错误。

可是这个类的构造函数和析构函数都是私有函数了,我们怎样才能得到该类的实例呢?这难不倒我们,我们可以通过定义静态来创建和释放类的实例。基于这个思路,我们可以写出如下的代码:

///////////////////////////////////////////////////////////////////////
// Define a class which can't be derived from
///////////////////////////////////////////////////////////////////////
class FinalClass1
{
public:
    static FinalClass1 *GetInstance()
    {
        return new FinalClass1;
    }

    static void DeleteInstance( FinalClass1 *pInstance)
    {
        delete pInstance;
        pInstance = 0;
    }

private:
    FinalClass1() {}
    ~FinalClass1() {}
};

这个类是不能被继承,但在总觉得它和一般的类有些不一样,使用起来也有点不方便。比如,我们只能得到位于堆上的实例,而得不到位于栈上实例。能不能实现一个和一般类除了不能被继承之外其他用法都一样的类呢?办法总是有的,不过需要一些技巧。请看如下代码:

///////////////////////////////////////////////////////////////////////
// Define a class which can't be derived from
///////////////////////////////////////////////////////////////////////
template <typename T> class MakeFinal
{
    friend T;

private:
    MakeFinal() {}
    ~MakeFinal() {}
};

class FinalClass2 : virtual public MakeFinal<FinalClass2>
{
public:
    FinalClass2() {}
    ~FinalClass2() {}
};

这个类使用起来和一般的类没有区别,可以在栈上、也可以在堆上创建实例。尽管类MakeFinal<FinalClass2>的构造函数和析构函数都是私有的,但由于类FinalClass2是它的友元函数,因此在FinalClass2中调用MakeFinal<FinalClass2>的构造函数和析构函数都不会造成编译错误。但当我们试图从FinalClass2继承一个类并创建它的实例时,却不同通过编译。

class Try : public FinalClass2
{
public:
    Try() {}
    ~Try() {}
};

Try temp;

由于类FinalClass2是从类MakeFinal<FinalClass2>虚继承过来的,在调用Try的构造函数的时候,会直接跳过FinalClass2而直接调用MakeFinal<FinalClass2>的构造函数。非常遗憾的是,Try不是MakeFinal<FinalClass2>的友元,因此不能调用其私有的构造函数。

基于上面的分析,试图从FinalClass2继承的类,一旦实例化,都会导致编译错误,因此是FinalClass2不能被继承。这就满足了我们设计要求。

为什么需要虚继承?

调用try的构造函数时,会先调用它包含的所有virtual base类的构造函数,然后再调用它上层的base类构造函数,然后是设置vptr,最后是初始化列表和子类构造函数体内的用户代码。try不能调用MakeFinal的私有成员,因此引发编译错误。

如果不是virtual继承,那么try首先调用的是它上层base类的构造函数,也就是FinalClass的构造函数,然后由FinalClass的构造函数来调用MakeFinal的构造函数,由于FinalClass是MakeFinal的友元,因此该调用合法,所以try得以正确构造,而没有编译错误。

参考:

C++ primer 第四版 Effective C++ 3rd C++编程规范

http://zhedahht.blog.163.com/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏WindCoder

Java基础小结(一)

1、default (即缺省,什么也不写): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。

781
来自专栏JetpropelledSnake

Python入门之字典的操作详解

这篇文章主要介绍了Python 字典(Dictionary)的详细操作方法,需要的朋友可以参考下: Python字典是另一种可变容器模型,且可存储任意类型对象,...

40814
来自专栏夏时

PHP 特色:可变变量

1224
来自专栏Java帮帮-微信公众号-技术文章全总结

第二十天 IO-异常file类【悟空教程】

在Java等面向对象的编程语言中,异常本身是一个类,产生异常就是创建异常对象并抛出了一个异常对象。Java处理异常的方式是中断处理。

1225
来自专栏小L的魔法馆

C++重载运算符练习--对people类重载“= =”运算符和“=”运算符

87011
来自专栏java学习

面试题62(关于类的加载顺序问题)

面试题62 2018年1月22日 本期题目 (单选题)下列选项中哪一个是正确的? public class Base{ private String b...

2874
来自专栏我和PYTHON有个约会

11.程序编程基础5:输入输出

python提供了3种输入输出标准文件对象,分别为标准输入、标准输出和标准错误;分别对应了sys模块中的sys.stdin,sys.stdout,sys.std...

872
来自专栏向治洪

Kotlin基础之泛型

泛型 与Java一样,Koltin的类也有类型参数。例如: class Box<T>(t: T){ var value = t } 常规来说,创建这样的...

1837
来自专栏烂笔头

Python标准库笔记(4) — collections模块

目录[-] 这个模块提供几个非常有用的Python容器类型 1.容器 名称 功能描述 OrderedDict 保持了key插入顺序的di...

3397
来自专栏软件开发

JavaScript学习总结(三)——闭包、IIFE、原型、函数与对象

一、闭包(Closure) 1.1、闭包相关的问题 请在页面中放10个div,每个div中放入字母a-j,当点击每一个div时显示索引号,如第1个div显示0,...

2055

扫码关注云+社区