首页 > 其他分享 >【继承超详细理解】

【继承超详细理解】

时间:2024-07-02 16:29:50浏览次数:18  
标签:继承 子类 派生类 class 理解 详细 基类 public

目录

一、继承的概念及定义

1、概念

继承(inheritance)机制是面向对象程序设计中可以使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。通过继承产生的新类就叫做派生类(或者子类),被继承的类叫做基类(或者父类)。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。注意:继承是类设计层次的复用!
父类示例如下:

//定义一个名为Father的父类
class Father{
public: 
    string name = "Alex";
    int age = 18;
};

定义一个以public方式继承父类的子类Children:

class Children:public Father{
public:
    string schoolname = "XXschool";//在子类中添加的一个信息
    void print()
    {
    cout<<name<<endl<<age<<endl<<schoolname<<endl;
    }
};

int main()
{
    Children Cd;
    Cd.print();
    return 0;
}

2、定义

(1)定义的格式

由上述示例也可以看出,在继承时,格式应该是:

class 子类的名字:继承方式 父类的名字{};

(2)继承方式和访问限定符

继承中有三种继承方式,分别是public继承protected继承private继承,相应地就有三个访问限定符:public访问protected访问private访问

(3)继承后子类成员访问权限

这是非常重要的一个表

类成员\继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见
  1. 基类中的private成员在派生类中无论以那种方式继承都是不可见的,不可见不是指不能被继承,是指基类的private成员依然被继承到了派生类中,只是无法被访问,语法上限制派生类对象不管是在类里面还是在类外面都不能对其进行访问;
  2. 如果基类成员不想在类外被直接访问,而需要再派生类中被访问,就将该成员定义为protected, protected限定符就是因为继承才出现的;
  3. 由上述表格可以发现,基类的private成员在派生类中始终不可见,而其他成员则根据一个不等式public>protected>private遵循取小的原则;
  4. 使用关键字class构建类时,默认继承方式为private,使用struct时默认为public,但最好是显示地写出继承方式;
  5. 在实际使用中一般都是public继承,很少使用protected和private继承,因为protected和private继承下来的成员只能在派生类中使用,在实际中扩展维护性不强。

二、继承中的作用域

  1. 在继承体系中基类和派生类都有独立的作用域;
  2. 在实际应用中最好不要在继承体系里定义同名成员。

三、基类和派生类(子类和父类)

1、基类和派生类的相互赋值

派生类对象可以赋值给基类的对象、基类的指针、基类的引用。
基类的对象不能赋值给派生类的对象;基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须当基类的指针是指向派生类对象时才是安全的,此处如果基类是多态类型可以使用RTTI(RunTime Type Information)的dynamic_cast来进行识别后进行安全转换。

//定义一个名为Father的父类
class Father{
public: 
    string name = "Alex";
    int age = 18;
};

class Children:public Father{
public:
    string schoolname = "XXschool";//在子类中添加的一个信息
    void print()
    {
    cout<<name<<endl<<age<<endl<<schoolname<<endl;
    }
};

int main()
{
    Children Cd;
    Father Fh;
    Fh = Cd;//将子类的对象赋值给父类
    Fh *p = Cd;//子类的对象赋值给父类的指针
    Fh &pp = Cd;//将子类的对象赋值给父类的引用
    return 0;
}


2、同名的成员变量

基类和派生类中有同名成员,派生类中成员将屏蔽基类中同名成员的直接访问,这种情况叫做隐藏,也叫做重定义(覆盖),若要在派生类成员函数中对同名成员进行访问,可以使用基类::基类成员 进行显示访问。

class father{
    public:
    string name = "father";
};

class son:public father{
    public:
    string name = "son";
    void print(){
         cout<<name<<endl;
     }
};

int main(){
    son s;
    s.print();
    return 0;
}

代码输出结果应该是son而不是father,若要访问父类中的name变量,则需要如下进行:

cout<<father::name<<endl;

在上述理解中,要将重写和重载进行区分,重写是针对不同层次,而重载是针对同一层次。

3、同名成员函数

需要注意的是,如果是成员函数的隐藏,只要是函数名相同就满足了隐藏条件;

四、派生类的默认成员函数

1、构造函数

作用:完成主要的初始化工作;
调用顺序:程序运行时会先调用父类的构造函数,再调用子类的构造函数。

2、析构函数

作用:完成主要的清理工作,释放资源,避免内存泄漏
调用顺序:程序结束时先调用子类的析构函数,再调用父类的析构函数。
注意:不要在子类中调用父类的析构函数!避免造成野指针的问题。

3、拷贝构造

子类中调用父类的拷贝构造时,直接传入子类对象即可。

class human {
public:
	human(string name="小明")
		:_name(name)
	{
		cout << name << endl;
	}
protected:
	string _name;
};
class student:public human {
public:
	student(string name, int age)
		:_age(age)
	{
		cout << name << endl << age << endl;
	}
	student(student& s)//拷贝构造
		:human(s)//直接将st传过来通过切片拿到父类中的值
		,_age(s._age)//拿除了父类之外的值
	{
		cout << s._age << endl<<s._name<<endl;
	}
protected:
	int _age;
};
int main()
{
	student st("小红",18);
	student st2(st);
	return 0;
}

代码输出结果应该是:

小明
小红
18
18
小明

4、赋值运算符重载

子类的operator =必须要显示调用父类的operator=完成父类的赋值。

class human{
public:
    human(string name = "小明")
        :_name(name)
        {
        }
        human& operator = (const human& p)
        {
            if (this != &p)
            {
            cout<<"调用父类"<<endl;
            _name = p._name;
            }
            return *this;
        }
protected:
    string _name;
};

class student:public human{
public:
    student(string name, int age)
        :_age(age)
        {
        }
        stduent(student& s)
            :human(s)
            ,_age(s._age)
            {
            }
            student& operator = (const student& s)
            {
                if (this ! = &s)
                {
                    cout<<"调用子类"<<endl;
                    human::operator=(s);//必须显示调用父类运算符
                    _age = s._age;
                    _name = s._name;
                }
                return *this;
            }
  protected:
     int _age;  
};

int main()
{
    student st("小红", 18);
    student st2(st);
    student st3("小刚", 16);
    st = st3;
    return 0;
}

五、继承与友元

在类中用关键字friend来进行声明得到友元函数或者友元类,可以访问类中private成员,不受控制权限的限制,注意:友元关系不能被继承。

六、继承与静态成员

七、单继承

一个子类只有一个直接父类的继承关系。
class A←class B: public A←class C: public B

八、多继承

一个子类有两个或两个以上直接父类的继承关系。
class A class B

class C: public A, public B

九、复杂的菱形继承及菱形虚拟继承

简单示例菱形继承:
class A

class B: public A class C: public A

class D:public B, public C

1、菱形继承内数据冗余和二义性问题

class A{
public:
    string name;
};

class B:public A{
public:
    int age;
};

class C:public A{
public:
    sting sex;
};

class D:public B, public C{
public:
    int id;
};

int mian(){
    D st;
    st.name = "Alex";
    st.age = 18;
    st.sex = "female";
    st.id = 1;
    return 0;
}

注意,这段代码是有问题的,会报错D::name不明确,因为这里的B和C都继承了A中的name,D不知道继承的是B中的name还是C中的name,这就是菱形继承的二义性问题。

2、解决数据冗余和二义性问题

下面有两种方法来解决上述问题:

1. 加修饰限定
在main函数中做出如下修改:

st.B::name = "Alex";//或者st.C::name = "Alex";

指定name的继承来源就能使代码顺利运行
2. 虚拟继承
在继承方式前面加上virtual。

class B:virtual public A{
public:
    int age;
};

class C: virtual public A{
public:
    string sex;
};

一般不建议设计出多继承,一定不要设计出菱形继承。

十、组合

一个类包含另一个类对象就称为组合。
示例:

class A{
//...
};

class B{
//...
protected:
    A _a;
};

继承是一种is-a的关系,也就是说每个子类对象都是一个基类对象;
组合则是一种has-a的关系,每个B对象中都有一个A对象。

优先使用组合而不是类继承,在必要时候才选择使用继承。

继承允许用户根据基类的实现来定义派生类的视线,这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言的:在继承中,基类的内部细节对派生类可见,继承一定程度上破坏了基类的封装,基类的改变对派生类有很大的影响,派生类和基类间的依赖关系很强,耦合度高。

对象组合式另外一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节不可见。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。

标签:继承,子类,派生类,class,理解,详细,基类,public
From: https://blog.csdn.net/Pumpkin_O/article/details/140119643

相关文章

  • 对Transformer的一些理解
    在学习Transformer这个模型前对seq2seq架构有个了解时很有必要的先上图输入和输出首先理解模型时第一眼应该理解输入和输出最开始我就非常纠结有一个Inputs,一个Outputs(shiftright)和一个OutputProbabilities,首先需要借助这三个输入/输出来初步了解该模型的运行方式。这......
  • 机器学习笔记 LightGBM:理解算法背后的数学原理
    一、简述        在一次数据科学的比赛中,我有机会使用LightGBM,这是一种最先进的机器学习算法,它极大地改变了我们处理预测建模任务的方式。我对它在数千个数据点上进行训练的速度感到着迷,同时保持了其他算法难以达到的准确性。LightGBM是LightGradientBoostingMac......
  • 【实物测评PK】多款声波清洗机测评报告小熊、希亦、大宇详细测评
    现如今戴眼镜的人群增多,对于眼镜清洗的需求无疑是增大的!在选购超声波清洗机洗眼镜也不能盲目跟风!购买超声波清洗机最重要的就是看清洁力如何,买超声波清洗机回来就是为了能给自己解放双手的!一款超声波清洗机清洁力强不强其实就看它清洗物品如何,所以本次小编自费购入了市面上比较......
  • Python类中的初始化方法理解
    在Python类的初始化方法(__init__方法)中,通常会执行以下操作:1、设置实例属性:这些属性将用于存储与实例相关的数据。属性名通常以self.开头,以便它们可以在类的其他方法中访问。例如:self.attribute_name=value2、传递并存储参数:初始化方法通常会接受参数,这些参数可以用于定制......
  • .Net Core Web Api 框架搭建详细步骤
    1、建立.NetCoreWebApi项目2、新建类库,分类结构层 3、使用EFCORE链接数据库,关联实体创建表,添加以下Nuget包创建DbContext 实例  4、appsettings.json配置数据库链接字符串,我当前是链接的mysql数据库5、Program依赖关系注入DbContextbuilder.Services.Ad......
  • k8s安装详细介绍
    Kubernetes(简称K8s)是一个开源的容器编排平台,用于自动化容器化应用程序的部署、扩展和管理。下面是一个关于如何安装Kubernetes的简要介绍,包括一些基本步骤和注意事项。安装Kubernetes的方法有很多种,这里将以使用kubeadm工具进行安装为例,因为这是官方推荐且相对简便的安装方式之......
  • 通俗大白话理解Docker
    什么是DockerDocker本质上是一种容器化技术,用于将应用程序及其所有依赖打包到一个标准化的单元中。这些单元(容器)可以在任何运行Docker的机器上运行。每个容器是相互隔离的,具有自己的文件系统、网络和进程空间。以下是大白话的理解,也是我在理解Docker中的一些误区:之前大......
  • 理解MySQL核心技术:触发器功能特点与应用案例解析
    触发器(Trigger)是MySQL中一个重要的功能,它能够在特定的数据表操作发生时自动执行预定义的SQL语句,从而实现在数据库层面的自动化操作和数据维护。在这篇文章中,我们将进一步了解MySQL触发器的相关知识,包括触发器的定义、作用、使用方法以及一些高级应用案例。一、什么是触发......
  • steam游戏商城怎么共享游戏给好友?最详细的操作方法介绍
    在Steam平台上共享游戏给好友,实际上是通过Steam的家庭图书馆共享功能实现的。这允许你在一个家庭内与最多五位家庭成员共享你的游戏库,但他们必须使用同一台电脑。请注意,你不能直接将游戏共享给不在同一物理位置的好友。以下是启用家庭图书馆共享的步骤:1.登录Steam:首先,确保你......
  • 【机器学习】FFmpeg+Whisper:二阶段法视频理解(video-to-text)大模型实战
    目录一、引言二、FFmpeg工具介绍2.1什么是FFmpeg2.2FFmpeg核心原理2.3FFmpeg使用示例三、FFmpeg+Whisper二阶段法视频理解实战3.1FFmpeg安装3.2Whisper模型下载3.3 FFmpeg抽取视频的音频3.3.1 方案一:命令行方式使用ffmpeg3.3.2方案二:ffmpeg-python库使用ff......