使用继承与派生的6大要点

news2024/9/21 18:34:01

概述

        面向对象编程技术非常看重软件的可重用性,在C++中,可重用性是通过继承机制来实现的。继承机制允许程序员在保持原有类的数据和功能的基础上进行扩展,增加新的数据和功能,从而构成一个新的类,也称为派生类。原有类,一般称之为基类。派生类不仅拥有基类的成员,还拥有自身新增加的成员。继承与派生是C++的重要组成部分,也是C++的基础知识。掌握好了继承与派生,就对面向对象编程技术有了更深刻的理解。关于继承与派生的入门知识,这里就不赘述了,下面将介绍继承与派生相关的一些知识要点。

访问权限

        派生类从基类派生时,有三种继承方式,分别是:公有继承、保护继承、私有继承,分别对应关键字public、protected、private。

        公有继承时,基类中public成员和protected成员在派生类中的访问权限不变,private成员在派生类中不可访问。

        保护继承时,基类中public成员和protected成员在派生类中的访问权限变为protected,private成员在派生类中不可访问。

        私有继承时,基类中public成员和protected成员在派生类中的访问权限都变为private,private成员在派生类中不可访问。

        可通过下表更清晰地看到不同继承方式下,基类成员在派生类中的访问权限。

基类public成员

基类protected成员

基类private成员

公有继承

public

protected

不可访问

保护继承

protected

protected

不可访问

私有继承

private

private

不可访问

        派生类能否改变基类成员在自身的访问权限呢?答案是肯定的,通过using关键字即可。参看下面的示例代码。

class CBase
{
public:
    CBase();

    int m_nData1;

protected:
    int m_nData2;

private:
    int m_nData3;
};

CBase::CBase() : m_nData1(6), m_nData2(88), m_nData3(999)
{
    NULL;
}

class CDerived : public CBase
{
public:
    using CBase::m_nData2;
        
protected:
    using CBase::m_nData1;
    using CBase::m_nData3;                    // 编译错误
};

CDerived derived;
printf("data1 is %d\n", derived.m_nData1);    // 编译错误
printf("data2 is %d\n", derived.m_nData2);    // 编译正常

        可以看到,通过using 基类::基类成员的方式,可以修改基类成员在派生类中的访问权限。m_nData1原来为public,修改后变为protected。m_nData2原来为protected,修改后变为public。由于m_nData3在基类CBase中为private,故在派生类中无法访问,因此无法通过using CBase::m_nData3修改private成员的访问权限。

构造顺序

        构造派生类的对象时,会按照顺序依次调用以下函数。

        1、所有基类的构造函数。注意是根据继承基类的顺序,而不是派生类中初始化列表的顺序来调用的。

        2、派生类所有成员变量的构造函数。注意是根据声明成员变量的顺序,而不是初始化列表的顺序来调用的。

        3、派生类的构造函数。

        在派生类的初始化列表中,如果没有显式调用基类和成员变量的构造函数,则自动调用基类和成员变量默认的构造函数。另外,销毁派生类对象时,调用析构函数的顺序与上面的顺序正好相反。

        可以通过下面的示例代码更好地理解构造顺序和析构顺序。

class CTemp1
{
public:
        CTemp1()
        {
                printf("CTemp1 constructor\n");
        }

        ~CTemp1()
        {
                printf("CTemp1 destructor\n");
        }
};

class CTemp2
{
public:
        CTemp2(int nNumber)
        {
                printf("CTemp2 constructor: %d\n", nNumber);
        }

        ~CTemp2()
        {
                printf("CTemp2 destructor\n");
        }
};

class CTemp3
{
public:
        CTemp3()
        {
                printf("CTemp3 constructor\n");
        }

        ~CTemp3()
        {
                printf("CTemp3 destructor\n");
        }
};

class CBase1
{
public:
        CBase1()
        {
                printf("CBase1 constructor\n");
        }

        ~CBase1()
        {
                printf("CBase1 destructor\n");
        }
};

class CBase2
{
public:
        CBase2(int nNumber)
        {
                printf("CBase2 constructor: %d\n", nNumber);
        }

        ~CBase2()
        {
                printf("CBase2 destructor\n");
        }
};

class CBase3
{
public:
        CBase3()
        {
                printf("CBase3 constructor\n");
        }

        ~CBase3()
        {
                printf("CBase3 destructor\n");
        }
};

class CDerived : public CBase1, public CBase2, public CBase3
{
public:
        CDerived() : m_tmp2(66), m_tmp1(), CBase2(88), CBase1() 
        {
                printf("CDerived constructor\n");
        }

        ~CDerived()
        {
                printf("CDerived destructor\n");
        }

private:
        CTemp1 m_tmp1;
        CTemp2 m_tmp2;
        CTemp3 m_tmp3;
};

CDerived derived;

上述示例的输出如下:

CBase1 constructor
CBase2 constructor: 88
CBase3 constructor
CTemp1 constructor
CTemp2 constructor: 66
CTemp3 constructor
CDerived constructor
CDerived destructor
CTemp3 destructor
CTemp2 destructor
CTemp1 destructor
CBase3 destructor
CBase2 destructor
CBase1 destructor

        从输出可以得出以下几点。

        1、在CDerived的初始化列表中,先初始化了m_tmp2m_tmp1,但依然先调用了三个基类的构造函数。

        2、在CDerived的初始化列表中,先初始化了CBase2,然后才初始化了CBase1,但依然按照继承基类的顺序先调用了CBase1的构造函数。

        3、在CDerived的初始化列表中,没有初始化CBase3,但依然调用了CBase3的默认构造函数。如果CBase3没有默认构造函数,则编译出错。

        4、在CDerived的初始化列表中,先初始化了m_tmp2,然后才初始化了m_tmp1,但依然按照声明成员变量的顺序先调用了m_tmp1的构造函数。

        5、在CDerived的初始化列表中,没有初始化m_tmp3,但依然调用了m_tmp3的默认构造函数。如果m_tmp3没有默认构造函数,则编译出错。

        6、析构的顺序与构造的顺序正好相反。

同名覆盖

        派生类和基类可以拥有同名的成员,此时,派生类会把基类中所有同名的成员(包括多个重载版本的函数)都覆盖掉。要想调用基类的成员,必须加作用域。

class CBase
{
public:
    CBase() : m_nData(66)
    {
        NULL;
    }

    void ShowData()
    {
        printf("CBase data is %d\n", m_nData);
    }

    void ShowData(const std::string &strData)
    {
        printf("CBase data is %s\n", strData.c_str());
    }

    int m_nData;
};

class CDerived : public CBase
{
public:
    CDerived() : CBase(), m_nData(88)
    {
        NULL;
    }

    void ShowData()
    {
        printf("CDerived data is %d, %d\n", m_nData, CBase::m_nData);
    }

    int m_nData;
};

CDerived derived;
derived.ShowData();
derived.ShowData("CSDN");                    // 编译出错
printf("data is %d\n", derived.m_nData);

derived.CBase::ShowData();
derived.CBase::ShowData("CSDN");
printf("data is %d\n", derived.CBase::m_nData);

CBase *pBase = &derived;
pBase->ShowData();

        上述示例的输出如下:

CDerived data is 88, 66
data is 88
CBase data is 66
CBase data is CSDN
data is 66
CBase data is 66

        可以看到,调用派生类对象derived的成员函数ShowData()和成员变量m_nData时,都是使用的派生类中的成员。调用派生类对象derivedShowData("CSDN")时,因为同名会覆盖基类中多个重载版本的函数,导致基类中重载字符串参数的函数在派生类中被隐藏了,不可见,故会报编译错误。加上作用域后,便可以指定访问基类中的成员变量和成员函数。另外,将基类指针指向一个派生类对象时,用基类指针调用的都是基类中的成员(不涉及虚函数,属于静态绑定)。

多继承

        多继承是指派生类同时从多个基类派生。如果多个基类中有同名的public和protected成员,则会引起歧义,导致编译出错。由于基类中的private成员在派生类中不可访问,故同名的private成员不会引起问题。解决多基类同名的方法为:使用作用域访问指定基类中的同名成员。可参看下面的示例代码。

class CBase1
{
public:
    CBase1() : m_nNumber(66), m_strText("Hello"), m_bPassed(true)
    {
        NULL;
    }

    int m_nNumber;

protected:
    std::string m_strText;

private:
    bool m_bPassed;
};

class CBase2
{
public:
    CBase2() : m_nNumber(88), m_strText("CSDN"), m_bPassed(false)
    {
        NULL;
    }

    int m_nNumber;

protected:
    std::string m_strText;

private:
    bool m_bPassed;
};

class CDerived : public CBase1, public CBase2
{
public:
    void Show()
    {
        // 编译出错
        printf("data is %d, %s\n", m_nNumber, m_strText.c_str());
        // 输出:CBase1 data is 66, Hello
        printf("CBase1 data is %d, %s\n", CBase1::m_nNumber, CBase1::m_strText.c_str());
        //  输出:CBase2 data is 88, CSDN
        printf("CBase2 data is %d, %s\n", CBase2::m_nNumber, CBase2::m_strText.c_str());
    }
};

        可以看到,由于CBase1CBase2中均有m_nNumberm_strText,在CDerived中直接访问这两个成员,会引发编译错误。通过指定作用域CBase1::CBase2::,可以在派生类中指定访问CBase1CBase2的成员。

虚函数

        虚函数在C++中主要是为了实现多态机制。所谓多态,也就是用基类的指针指向派生类的实例,然后通过基类的指针调用派生类实例的成员函数。可参看下面的示例代码。

class CBase
{
public:
    CBase() : m_nData1(66)
    {
        NULL;
    }

    virtual void Test1()
    {
        printf("CBase Test1\n");
    }

    virtual void Test2()
    {
        printf("CBase Test2\n");
    }
    
    virtual void Test3()
    {
        printf("CBase Test3\n");
    }

private:
    int m_nData1;
};

class CDerived : public CBase
{
public:
    CDerived() : CBase(), m_nData2(88)
    {
        NULL;
    }

    virtual void Test1()
    {
        printf("CDerived Test1\n");
    }

    virtual void Test2()
    {
        printf("CDerived Test2\n");
    }

private:
    int m_nData2;
};

CBase *pBase = new CDerived();
pBase->Test1();        // 输出:CDerived Test1
pBase->Test2();        // 输出:CDerived Test2

        可以看到,通过基类指针pBase调用虚函数Test1Test2时,调用的是派生类中的函数。

        那么,虚函数是如何实现的呢?

        在C++中,具有虚函数的类都有一张虚函数表。这个表相当于一个一维数组,用于存储类中所有虚函数的地址。编译器必须保证对象实例最开始的位置存放指向虚函数表的指针,然后才能存放其他成员。可通过下面的示例代码来理解虚函数表的概念。

typedef void (*Test)();

CBase base;
Test pTest1 = (Test)*((size_t *)*(size_t *)(&base));
pTest1();
Test pTest2 = (Test)*((size_t *)*(size_t *)(&base) + 1);
pTest2();
int nData = (int)*((size_t *)(&base) + 1);
printf("data is %d\n", nData);

CDerived derived;
pTest1 = (Test)*((size_t *)*(size_t *)(&derived));
pTest1();
pTest2 = (Test)*((size_t *)*(size_t *)(&derived) + 1);
pTest2();
Test pTest3 = (Test)*((size_t *)*(size_t *)(&derived) + 2);
pTest3();
int nData1 = (int)*((size_t *)(&derived) + 1);
int nData2 = (int)*((size_t *)(&derived) + 2);
printf("data is %d, %d\n", nData1, nData2);

上述示例的输出如下:

CBase Test1
CBase Test2
data is 66
CDerived Test1
CDerived Test2
CBase Test3
data is 66, 88

        可以看到,我们完全通过指针的操作就访问了类实例中的虚函数和成员变量。在上面的代码中,(size_t *)(&base)为指向虚函数表的指针,*(size_t *)(&base)为虚函数表的地址,相当于一维数组的地址,(size_t *)*(size_t *)(&base)指向虚函数表中第一个虚函数的地址,*((size_t *)*(size_t *)(&base))则是第一个虚函数。其他虚函数和成员变量的指针转换与此类似,这里不再赘述。下图给出了类实例base和derived的内存布局结构,供大家参考。

        上面讨论的都是单继承的情况,多继承时,原理类似,只是更复杂些,这里就不再深入介绍了。

虚继承

        虚继承是为了解决菱形继承中,存在多份公共基类的拷贝,从而导致二义性的问题。在定义派生类时,如果在基类的名字前面加上virtual关键字,则构成虚继承。先通过下面的示例代码来看看菱形继承的问题。

class CBase
{
public:
    CBase() : m_nNumber(1)
    {
        printf("CBase default constructor\n");
    }

    CBase(int nNumber) : m_nNumber(nNumber)
    {
        printf("CBase constructor: %d\n", nNumber);
    }

protected:
    int m_nNumber;
};

class CDerived1 : public CBase
{
public:
    CDerived1() : CBase(66)
    {
        printf("CDerived1 constructor\n");
    }
};

class CDerived2 : public CBase
{
public:
    CDerived2() : CBase(88)
    {
        printf("CDerived2 constructor\n");
    }
};

class CFinal : public CDerived1, public CDerived2
{
public:
    CFinal() : CDerived1(), CDerived2()
    {
        printf("CFinal constructor\n");
    }

    void Show()
    {
        printf("CFinal show: %d\n", m_nNumber);    // 编译出错
        printf("CFinal show 1: %d\n", CDerived1::m_nNumber);
        printf("CFinal show 2: %d\n", CDerived2::m_nNumber);
    }
};

CFinal final;
final.Show();

        上述示例的输出如下:

CBase constructor: 66
CDerived1 constructor
CBase constructor: 88
CDerived2 constructor
CFinal constructor
CFinal show 1: 66
CFinal show 2: 88

        可以看到,CFinal继承了CDerived1CDerived2,而CDerived1CDerived2都继承了CBase。在CFinal中直接访问公共基类CBase中的成员变量m_nNumber时,便会存在二义性问题。虽然可以通过指定作用域来分别访问CDerived1CDerived2中的m_nNumber,但CBase存在多份的问题仍然存在(调用了两次CBase的构造函数)。

        再来看看使用虚继承时的示例代码。

class CBase
{
public:
    CBase() : m_nNumber(1)
    {
        printf("CBase default constructor\n");
    }

    CBase(int nNumber) : m_nNumber(nNumber)
    {
        printf("CBase constructor: %d\n", nNumber);
    }

protected:
    int m_nNumber;
};

class CDerived1 : public virtual CBase
{
public:
    CDerived1() : CBase(66)
    {
        printf("CDerived1 constructor\n");
    }
};

class CDerived2 : public virtual CBase
{
public:
    CDerived2() : CBase(88)
    {
        printf("CDerived2 constructor\n");
    }
};

class CFinal : public CDerived1, public CDerived2
{
public:
    CFinal() : CDerived1(), CDerived2()
    {
        printf("CFinal constructor\n");
    }

    void Show()
    {
        printf("CFinal show: %d\n", m_nNumber);    // 编译正常
        printf("CFinal show 1: %d\n", CDerived1::m_nNumber);
        printf("CFinal show 2: %d\n", CDerived2::m_nNumber);
    }
};

CFinal final;
final.Show();

上述示例的输出如下:

CBase default constructor
CDerived1 constructor
CDerived2 constructor
CFinal constructor
CFinal show: 1
CFinal show 1: 1
CFinal show 2: 1

        可以看到,使用虚继承后,公共基类CBase的构造函数只调用了一次,且调用的是默认构造函数。这是因为虚基类的构造函数比较特殊,与常规的构造函数有所不同。由于从虚基类派生出来的每一个派生类中,其构造函数都调用了基类的构造函数,这样编译器就无法确定到底用哪一个派生类来构造基类对象。最终,编译器会忽略所有派生类中对基类构造函数的调用,而选择调用基类的默认构造函数。

        如果基类的默认构造函数不存在,则编译器将报错。解决该问题的方法是:显式在CFinal中调用CBase的其他构造函数。示例代码如下:

class CFinal : public CDerived1, public CDerived2
{
public:
    CFinal() : CDerived1(), CDerived2(), CBase(2)
    {
        printf("CFinal constructor\n");
    }

    void Show()
    {
        printf("CFinal show: %d\n", m_nNumber);
        printf("CFinal show 1: %d\n", CDerived1::m_nNumber);
        printf("CFinal show 2: %d\n", CDerived2::m_nNumber);
    }
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/341930.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分销系统逻辑

相关概念 主营商户: 提供分销商品和佣金的商户分销商: 拥有自己的销售渠道,能够帮助推动产品销售的个人或商户消费者: 购买分销商品的人。佣金: 主营商户返还给经销商的比例抽成 分销功能设计 (1)分销商准入规则设计 无规则: 没有分销商的准入门槛限制&#xf…

七大设计原则之依赖倒置原则应用

目录1 依赖倒置原则2 依赖倒置应用1 依赖倒置原则 依赖倒置原则(Dependence Inversion Principle,DIP)是指设计代码结构时,高层模块不应该依赖底层模块,二者都应该依赖其抽象。抽象不应该依赖细节;细节应该依赖抽象。…

VHDL语言基础-Testbech

目录 VHDL仿真概述: 基本结构: VHDL一般仿真过程: 仿真测试平台文件: 编写测试平台文件的语言: 一个测试平台文件的基本结构如下: 测试平台文件包含的基本语句: 产生激励信号的方式: 时钟信号: 复位信号: 周期信性信号: 使用延迟DELAYD: 一般的激励信号…

算法leetcode|36. 有效的数独(rust重拳出击)

文章目录36. 有效的数独:样例 1:样例 2:提示:分析:题解:rustgoccpythonjava36. 有效的数独: 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效…

【C++】类和对象---需掌握的功能

目录1.初始化列表1.1构造函数赋值1.2初始化列表格式:编译器执行的顺序:特性:1.3explicit关键字类型替换过程多参数构造函数类型替换(C11)2.static成员编程题3.匿名对象4.友元4.1友元函数4.2友元类5.内部类6.拷贝对象时…

linux高级命令之获取管理员权限的相关命令

获取管理员权限的相关命令学习目标能够知道切换到管理员用户的命令1. sudo命令的使用命令说明sudo -s切换到root用户,获取管理员权限sudo某个命令的执行需要获取管理员权限可以在执行命令前面加上sudosudo -s效果图:sudo 命令效果图:说明:如果只是某次操作需要使用管…

面向对象与面向过程编程

从语言角度来讲: C是面向过程编程; C一半是面向过程编程,一半是面向对象编程; Java是面向对象编程。 一、什么是面向对象编程与面向过程编程? 面向过程(Procedure Oriented 简称 PO)&#xff1…

云计算ACP云服务器ECS实例题库

😘作者简介:一名99年软件运维应届毕业生,正在自学云计算课程。👊宣言:人生就是B(birth)和D(death)之间的C(choise),做好每一个选择。&…

2.你的程序乱码了吗?

学习的动力不止于此&#xff1a; 1. 乱码 #include <QApplication> #include <QLabel> #include <QFont> int main(int argc, char *argv[]) {QApplication a(argc, argv);QLabel lb;lb.setFont(QFont("Sans Serif", 24));lb.setText(" 乱…

Linux文件隐藏属性(修改与显示):chattr和lsattr

文件除了基本的九个权限以外还有隐藏属性存在&#xff0c;这些隐藏属性对于系统有很大的帮助&#xff0c;尤其是系统安全&#xff08;Security&#xff09;上 chattr&#xff08;配置文件隐藏属性&#xff09; chattr 【-】【ASacdistu】文件或目录名称 选项与参数&#xff1a…

Visual Navigation(一):阅读三篇经典论文

文章目录前言一、Learning to Navigate in Cities Without a Map二、Unsupervised Predictive Memory in a Goal-Directed Agent三、Zero-Shot Imitation LearningImitation Learning:前言 研究生不读论文还是不行的呀&#xff0c;在这里结合下别人的总结等下一次组会吹水。 …

大客户市场:阿里云、腾讯云、华为云“贴身肉搏”

配图来自Canva可画 近年来&#xff0c;随着中国逐渐进入数字化经济快车道&#xff0c;国内企业数字化、智能化升级已是刻不容缓。而为了帮助自身或其他企业实现数字化转型升级&#xff0c;阿里、腾讯、百度、京东、字节、网易、华为等众多国内知名企业早在多年以前&#xff0c…

【Git学习】Git如何Clone带有Submodule的仓库?

文章目录一、问题描述二、解决问题三、参考链接四、解决问题4.1 下载主模块4.2 查看主模块的配置4.2 子模块的添加4.3 查看子模块的配置4.4 查看子模块的检出状态4.5 检出submodule4.6 再次查看.git/config4.7 重新打开Android Studio运行代码一、问题描述 在GitHub上下载了一…

Android12 Launcher3 最近任务客制化

实现的最终效果: 目录 修改图标位置+添加应用名称 代码实现: 图标控件的边距调整:

微信小程序 button按钮怎么触发事件? bindtap语法怎么使用?

在前端网页中我们需要触发一个事件如果按钮点击后调用函数&#xff0c;文本、图片、链接被点击后调用一个函数一个事件&#xff0c;我们都知道用click&#xff0c;可是微信小程序中的click是不存在的&#xff0c;他怎么才能和网页中一样的使用click的呢&#xff1f; 1.bindtap语…

编程思想-0x00架构

产生架构的原因&#xff1f; 1、代码均摊 将不同的代码进行分块&#xff0c;然后简历联系&#xff0c;低耦合、高内聚&#xff1b; 原则上&#xff1a;合理的App架构应该是合理分配每个类、结构体、方法、变量的存在都应该遵循单一职责的原则 2、便于测试 测试确保代码质量&…

【编程基础之Python】3、创建Python虚拟环境

【编程基础之Python】3、创建Python虚拟环境创建Python虚拟环境为什么需要虚拟环境Windows上的Anaconda创建虚拟环境conda 命令conda env 命令创建虚拟环境切换虚拟环境验证虚拟环境Linux上的Anaconda创建虚拟环境创建虚拟环境切换虚拟环境验证虚拟环境总结创建Python虚拟环境 …

性能优化方向

性能怎么样就看io的应用&#xff0c;网络和数据库要好好设计&#xff0c;能一次查出来就一次。 对外接口尽量不要多创建对象&#xff0c; 少用bean复制 少用getbean(.class) 缓存不要频繁操作&#xff0c;最好异步 循环不要调用数据库&#xff0c;调用接口最好批量 Compon…

UG NX二次开发(C#)-UIStyler-创建长方体

文章目录 1、前言2、UG NX自动的创建长方体界面3、在块样式编辑器中创建UI界面4、创建一个工程5、在创建按钮中添加代码6、调用dll7、结论1、前言 UG NX二次开发中,UIStyler是一种非常高效的开发方式,UG NX已经为我们提供了比较完善的UIStyler开发模板,只要通过拖动的方式就…

GAN系列基础知识

原始值函数 原始GAN的值函数是 minGmaxDV(D,G)Ex∼pdata(x)[logD(x)]Ez∼pz(z)[log(1−D(G(z)))]min_Gmax_DV(D,G) E_{x \sim p_{data}(x)}[logD(x)]E_{z \sim p_{z}(z)} [log(1-D(G(z)))]minG​maxD​V(D,G)Ex∼pdata​(x)​[logD(x)]Ez∼pz​(z)​[log(1−D(G(z)))] 其中Ex…