『 C++类与对象』继承

news2024/11/15 13:39:57

文章目录

    • 继承的概念
    • 继承方式与访问限定符
    • 基类和派生类对象赋值转换
    • 继承中的作用域
      • 隐藏
    • 派生类的默认成员函数
      • 构造函数
      • 拷贝构造函数
      • 赋值运算符重载
      • 析构函数
    • 继承与友元
    • 静态成员与继承关系

继承的概念

继承的机制为,允许在以该类为基础上对类进行扩展,增加功能;

通常原来也就是被用来继承的类称之为父类或者是基类,而通过继承产生的新类被称为子类或者是派生类;

派生类将会以继承规则或者是基类原有的访问限定符为限定对属性进行一定的调整;

class Person
{//基类/父类
  protected:
    string _name = "LiHua";
    int _age = 18;
};

class Student : public Person //语法
{//派生类/子类
  void Func()
  {
    cout<<"study now"<<endl;
  }
};

从该代码可以大概了解继承的概念;

即子类在继承父类属性的基础上再进行拓展;

由上述代码可以得知Sudent类为Person类的派生类,Student类中的属性除了Func()函数以外也包含了Person 的属性;

继承体现了在多个类中对于同一属性的复用;


继承方式与访问限定符

在对类与对象的学习中,提到了访问限定符;

  • public 公有
  • private 私有
  • protected 保护

访问限定符也就是在不同使用场景中,类中的成员将会根据访问限定符的修饰产生一定的限制;

类中的public公有成员类内外都可被访问;

类中的private私有成员与protected保护成员类内可以访问,类外不能被访问;

而在继承方式中也同样有三种方式,也为:

  • public
  • private
  • protected

继承是所谓的派生类继承基类的各个属性;

而派生类中基类的成员将会根据访问限定符与继承方式的不同而做出一定的变化;

继承方式\访问限定符public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

从上表中可以看出,基类的private成员无论是用什么方式继承,最终在派生类中都处于一种不可见的状态;

同时可以推断出,派生类中成员的权限一般为继承方式权限与基类成员权限中较小的那个( public > protected > private );

从该处可以进行一个分类,分为基类的private成员与其他;

class Person
{
  public:
  protected:
    string _name = "LiHua";
    int _age = 18;
  private:
    int _todel = 10;
};

class Student : public Person 
{
  void Func()
  {
    cout<<Person::_todel<<endl; // errory : _todel为Person中的私有成员,在派生类中不可见也不可被调用
  }
};

当然对于继承方式来说也可以省略:

  • class Student : Person class派生类的默认继承方法为private;
  • struct Techer : Person struct派生类的默认继承方式为public;

但是一般在写的过程中尽量不要省略;


基类和派生类对象赋值转换

在一般的变量当中,两个相同变量可以相互进行赋值;

int main()
{
    int a = 10,b = 0;
    b = a;
    return 0;
}

对于类型不同的两个内置类型变量而言也可以相互进行赋值;

int main()
{
    double a = 12.2;
    int b = 10;
    b = a;//发生隐式类型转换
    const int &c = a;//引用具有常性的数据应用const修饰
}

首先我们知道,对于赋值而言并不是直接将数据本身进行赋值,在赋值的过程中将会生成一个临时对象,最终这个临时对象会赋值给这个需要赋值的变量当中;

但在继承当中,派生类对象是否可以赋值给其基类的基类对象?

事实上是可以的,由于派生类对象归根结底属于基类对象的延申\拓展,所以它具备了对应基类的所有属性;

在赋值过程中将会取出对应的基类对象中的属性,将其属性赋值给基类对象;

/*
class Person{};
class Student:public Person{};
*/
int main()
{
    Person per;
    Student stu;
    
    per = stu;//切片\切割;
    
    Person &per_1 = stu;
    //该处的引用并不需要进行const修饰,中途不产生临时对象;
    //同时该处在进行引用时,per_1 将为stu中Person对应成员的别名;
    
    Person *ptrp = &stu;//取出Person对应stu中成员的地址;
    
    return 0;
}

对于基类和派生类对象赋值转换也有另外一种说法切片\切割;

派生类对象可以赋值给基类对象,但是基类对象不能赋值给派生类对象;


继承中的作用域

存在两个类分别为AB,其中BA的派生类;

class A
{
  public:
    int a = 9;
    int b = 10;
};

class B : public A
{
  public:
    int c = 500;
};

void test_2()
{
  B b1;
  cout<<b1.a<<" "<<b1.b<<endl;
}

在该处对test_2()函数进行调用时结果为{ 9 , 10 };

结果是在默认情况下由于派生类为以基类为基础的拓展/延申,所以具有基类的属性,可以直接调用对应的成员;

但在实际的定义当中,派生类与基类都有独立的域,所以说在自己所在的域内进行操作将不会影响另一个类(基类/派生类);

这也包括在对应的类中的同名成员;


隐藏

为什么在基类与派生类中可以存在同名的成员?

class A
{
  public:
    int a = 9;
    int b = 10;
};

class B : public A
{
  public:
    int a = 100;
    int b = 200;
};

void test_2()
{
  B b1;
  cout<<b1.a<<" "<<b1.b<<endl;
}

运行上面这段代码的结果将是[ 100 , 200 ];

原因是因为在默认情况下(基类与派生类中不存在同名成员时)若是需要调用的成员为基类成员时可以直接进行调用;

但是在这里基类和派生类都有同名的成员a,b,由于基类与派生类都有各自独立的域,所以在此处若是想调用对应成员的时候将首先调用派生类自己域中的对应成员;

当然基类的成员并不是不能被调用,只是在调用的时候应该使用域作用限定符::指定域;

cout<<b1.A::a<<" "<<b1.A::b<<endl;

通常我们称作:

  • 当基类与派生类中存在同名成员时,将构成隐藏\重定义,在该种情况下若从派生类中调用该同名成员时将会优先调用派生类自己域中的对应成员,若是想调用基类中的同名成员则需要使用域作用限定符::进行指定域;

当然,构成隐藏\重定义的条件不仅仅是只有成员变量:

  • 当基类与派生类中存在同名成员函数时(只要是同名,不需要考虑参数或返回值),即可构成隐藏\重定义;
在使用过程中尽量不要定义同名

派生类的默认成员函数

构造函数

在类中,默认成员函数分别为:

构造函数拷贝构造函数析构函数赋值运算符重载取地址重载const取地址重载

以该段代码为例:

class Parent
{
  public:
    Parent()
  {
    cout<<"Parent()"<<endl;
  }
    ~Parent(){
      cout<<"~Parent()"<<endl;
    }
  protected:
    string _name;
    int _age;
};

class Child : public Parent
{};
void test(){
	Child ch;    
}

若是调用test()函数的结果为:

该处实例化一个派生类对象,但是调用的确是基类的构造函数与析构函数;

说明在实例化一个派生类时,将会去自动调用它的基类的构造函数来初始化派生类中的基类,并在析构的过程中去调用它的基类的析构函数去清理掉派生类中的基类的数据;

在继承当中,基类与派生类当中都属于独立的域,所以在对派生类进行初始化时将会去调用该派生类的基类中的构造函数(析构函数);

假设需要显式调用其构造函数应该怎么进行调用?

class Parent //基类
{
  public:
    Parent(const char*name)
    :_name(name)
  {
    cout<<"Parent()"<<endl;
  }

    ~Parent(){
      cout<<"~Parent()"<<endl;
    }
  protected:
    string _name;
};

class Child : public Parent  //派生类
{
  public:

    Child(const char* name,size_t age)
      :	//_name(name) - error
       	//Parent::_name(name) - error
    	Parent(name)
      ,_age(age)
    {
      cout<<"Child()"<<endl;
    }
    
    ~Child(){
      cout<<"~Child()"<<endl;
    }
    
  protected:
    size_t _age;
};

void test(){
    Child chi("Lihua",18);
}

在基类和派生类的关系当中,派生类无法去初始化基类对应的参数,要对对应参数进行初始化时应该去调用它的构造函数;

当然若是基类存在默认构造函数(无参\全缺省)时可以不用显式调用基类的构造函数;

但若是基类不存在默认构造函数时,将必须在初始化列表当中调用基类的构造函数;

当然,在实例化一个派生类对象时,基类与派生类的构造函数与析构函数的调用顺序为:

  • 基类的构造函数 -> 派生类的构造函数 -> 派生类的析构函数 -> 基类的析构函数

拷贝构造函数

存在基类与派生类,代码与上述相同,唯独不同的是在两个类中都存在拷贝构造函数:

class Parent //基类
{
  public:
    //构造 - 打印 "Parent()" 
	Parent(const Parent& par){
        cout<<"Parent(const Parent& par)"<<endl;
    }
    //析构 - 打印 "~Parent()"
  protected:
    string _name;
};

class Child : public Parent  //派生类
{
  public:
    //构造 - 打印 "Child()"
    Child(const Child& chi){
        cout<<"Child(const Child& chi))"<<endl;
    }
    //析构 - 打印 "~Child()"
  protected:
    size_t _age;
};

void test(){
    Child chi("Lihua",18);
    Child chi1(chi);
}

若是在该处调用test()函数的结果为:

若是在派生类中存在拷贝构造函数,且派生类的拷贝构造函数内并没有调用基类的拷贝构造时时,派生类的拷贝构造函数并不会去主动调用基类的拷贝构造函数;

所以在派生类中的拷贝构造函数应该主动去调用基类的拷贝构造函数(同样在初始化列表当中);

Child(const Child& chi)
:Parent(chi)
{
	cout<<"Child(const Child& chi))"<<endl;
}

在这里基类的拷贝构造参数类型为const Parent&,而传过去的类型为const Child&,在这里将会发生切片\切割的行为;

当然若是派生类对象调用拷贝构造函数且派生类中并未显式定义拷贝构造函数时,对于派生类中默认生成的拷贝构造函数将会在拷贝构造时调用基类的拷贝构造函数;

默认生成的拷贝构造函数将会进行值拷贝;


赋值运算符重载

对于赋值运算算符重载也是如此,在调用派生类中的赋值运算符重载时若是派生类中的赋值运算符重载没有显式调用基类赋值运算符重载时,派生类将不会默认去调用基类的赋值运算符重载;

class Parent //基类
{
  public:
    //构造 - 打印 "Parent()" 
	//拷贝构造 - 打印 "Parent(const Parent& par)"
    //析构 - 打印 "~Parent()"
        Parent& operator=(const Parent& par){
      if(&par!=this){
        _name = par._name;
      }
      return *this;
    }
  protected:
    string _name;
};

class Child : public Parent  //派生类
{
  public:
    //构造 - 打印 "Child()"
    //拷贝构造 - 打印 "Child(const Child& chi))"
    //析构 - 打印 "~Child()"
    Child& operator=(const Child&chi){
      cout<<"Child&operator=()"<<endl;
      if(&chi != this){
        _age = chi._age;
      }
        return *this;
    }
  protected:
    size_t _age;
};

void test(){
    Child ch1("Lihua",18);
    Child ch2("Wang",20);
    ch2 = ch1;
}

结果 :

所以在派生类中显式定义的赋值运算符重载中需要去显式调用基类的赋值运算符重载;

 Child& operator=(const Child&chi){
      cout<<"Child&operator=()"<<endl;
      if(&chi != this){
        operator=(chi);
        _age = chi._age;
      }

但是若是在该处以这种方式对基类中的赋值运算符重载进行调用时将会造成死循环;

原因是因为,基类中的operator=()与派生类中的operator=()构成了隐藏\重定义;

所以在调用过程中由于没有使用域作用限定符进行域的指定,所以最终导无限调用派生类中的operator=();

正确方式:

  • Child& operator=(const Child&chi){
          cout<<"Child&operator=()"<<endl;
          if(&chi != this){
            Parent::operator=(chi);
            _age = chi._age;
          }
    

同理,当派生类中并未显式定义赋值运算符重载时,调用派生类中的赋值运算符重载时其一样会去调用基类的赋值运算符重载,同样也只有深浅拷贝的问题;


析构函数

从上面几个成员函数来对析构函数进行推断时,将会显然的觉得对于析构函数来说一样也是在派生类中的析构函数去调用基类的析构函数;

~Child(){
    ~Parent();
}

但是在以这种方式去调用派生类的析构函数时将会出现编译不通过;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

[一元表达式参数类型不通过]

原因是因为最终析构函数的函数名将会被处理为destructor;

所以将会构成隐藏\重定义;

只需要使用域作用限定符进行域的限制即可;

~Child(){
    Parent::~Parent();
}

在使用域作用限定符进行修饰后再次对该析构函数进行调用时析构函数将多调用一次;

因为由于需要使得析构的顺序与构造的顺序相反,在进行析构的顺序就应该为:

  • 派生类析构 -> 基类析构(栈后进先出)

而若是用户自行定义的话则不一定会控制析构函数的析构顺序;

所以当派生类析构结束后会自动调用基类的析构函数;

所以派生类在进行析构时不需要去显式调用基类的析构函数;


继承与友元

在继承关系当中,友元关系并不会被继承;

class S ;//提前进行声明,否则在P类型中定义的友元将识别不出S类型
class P{
  public:
  friend void FuncP(const P& par,const S& chi);
  protected:
  int _age = 18;
};

class S : public P{
  public:
  protected:
  int _id = 123;
};

void FuncP(const P& par,const S& chi){
  cout<<par._age<<endl; 
  //cout<<chi._id<<endl; - error 由于友元关系并不会被继承,所以该行代码报错
}

void test_3(){
  FuncP(P(),S());//匿名对象
}

若是需要解决友元关系不能被继承的问题即可在派生类中再次定义友元即可;


静态成员与继承关系

在类中存在一种特殊的成员类型:

  • 静态成员

该类型在类中本质上是一个公共的成员,即属于该类中的所有对象;

但是在继承当中,静态成员并没有表现出其他较为特别的性质;

当在一个基类中定义一个静态成员时;

无论该类被继承了多少次,该类中的静态成员始终为该基类与所有派生类中共同的属性;

所有继承体系当中都只有一个静态成员实例;

class Par{
  public:
    Par()
    {
      ++_count;
    }
  public:
    static int _count ;
};
int Par::_count = 0;

class Chi:Par{
public:
  Chi(){
    ++_count;
  }
  void Print(){
    cout<<Par::_count<<endl;
  }
};

void Test(){
  Chi ch1;
  ch1.Print();
}

上段代码的打印结果为2;

原因是因为由于静态成员变量是所有继承体系中共有的实例,所以当实例化一个Chi对象时调用了基类的构造函数,后又在派生类中调用了一次构造函数都对该静态成员进行了一次++,所以最终的打印结果为2;


中存在一种特殊的成员类型:

  • 静态成员

该类型在类中本质上是一个公共的成员,即属于该类中的所有对象;

但是在继承当中,静态成员并没有表现出其他较为特别的性质;

当在一个基类中定义一个静态成员时;

无论该类被继承了多少次,该类中的静态成员始终为该基类与所有派生类中共同的属性;

所有继承体系当中都只有一个静态成员实例;

class Par{
  public:
    Par()
    {
      ++_count;
    }
  public:
    static int _count ;
};
int Par::_count = 0;

class Chi:Par{
public:
  Chi(){
    ++_count;
  }
  void Print(){
    cout<<Par::_count<<endl;
  }
};

void Test(){
  Chi ch1;
  ch1.Print();
}

上段代码的打印结果为2;

原因是因为由于静态成员变量是所有继承体系中共有的实例,所以当实例化一个Chi对象时调用了基类的构造函数,后又在派生类中调用了一次构造函数都对该静态成员进行了一次++,所以最终的打印结果为2;


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

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

相关文章

社区买菜系统 JAVA开源项目

目录 项目内容 项目获取 项目截图 项目内容 基于VueSpringBootMySQL的社区买菜系统&#xff0c;包含菜品分类模块、菜品档案模块、菜品订单模块、菜品收藏模块、收货地址模块&#xff0c;还包含系统自带的用户管理、部门管理、角色管理、菜单管理、日志管理、数据字典管理、…

【网安AIGC专题10.19】论文6:Java漏洞自动修复+数据集 VJBench+大语言模型、APR技术+代码转换方法+LLM和DL-APR模型的挑战与机会

How Effective Are Neural Networks for Fixing Security Vulnerabilities 写在最前面摘要贡献发现 介绍背景&#xff1a;漏洞修复需求和Java漏洞修复方向动机方法贡献 数据集先前的数据集和Java漏洞Benchmark数据集扩展要求数据处理工作最终数据集 VJBenchVJBench 与 Vul4J 的…

Unity编辑器扩展之CustomPropertyDrawer理解

一、引言&#xff0c; 在上一篇文章中提到&#xff0c;CustomEditor只能自定义单一类&#xff0c;被其他类持有的类自定义没有作用&#xff0c;这个时候就需要使用CustomPropertyDrawer属性。 二、PropertyDrawer介绍 PropertyDrawer用于自定义属性绘制器的基类。使用Proper…

【办公自动化】wps word首字下沉/文字宽度/段落底纹/图片缩放/装订线(Word的相关操作)

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

『第九章』雨燕新量子引擎:结构化并发

在本篇博文中,您将学到如下内容: 1. 千呼万唤始出来:结构化并发2. async/await3. “结构化(structured)”到底是个神马?3.1 async let3.2 TaskGroup4. 非结构化并发(unstructured concurrency)4.1 非异步上下文中的 Task4.2 Detached Task4.3 延时5. 任务(Task)的取消和…

168. Excel表列名称

168. Excel表列名称 Java代码&#xff1a; 26进制&#xff0c;但是每个进制是从1开始的&#xff0c;不是从0开始&#xff1b;因此要计算要构建从0开始的求余&#xff01; class Solution {public String convertToTitle(int cn) {StringBuilder sb new StringBuilder();whi…

ElasticSearch快速入门实战

全文检索 什么是全文检索 全文检索是一种通过对文本内容进行全面索引和搜索的技术。它可以快速地在大量文本数据中查找包含特定关键词或短语的文档&#xff0c;并返回相关的搜索结果。全文检索广泛应用于各种信息管理系统和应用中&#xff0c;如搜索引擎、文档管理系统、电子…

Xtuner——报错解决汇总

文章目录 load_dataset读取jsonl文件报错 load_dataset读取jsonl文件报错 alpaca_en dict(typeprocess_hf_dataset,datasetdict(typeload_dataset, data_filesalpaca_file_path),tokenizertokenizer,max_lengthmax_length,dataset_map_fnalpaca_map_fn,template_map_fndict(t…

【C++初阶(三)】引用内联函数auto关键字

目录 前言 1. 引用 1.1 引用的概念 1.2 引用的特性 1.3 引用的权限 1.4 引用的使用 1.5 引用与指针的区别 2. 内联函数 2.1 什么是内联函数 2.2 内联函数的特性 3. auto关键字 3.1 auto简介 3.2 auto使用规则 3.3 auto不能使用的场景 4. 基于范围的for循环 4.1 范围for…

一文2000字教你从0到1实现Jmeter 分布式压测

你可以使用 JMeter 来模拟高并发秒杀场景下的压力测试。这里有一个例子&#xff0c;它模拟了同时有 5000 个用户&#xff0c;循环 10 次的情况‍。 请求默认配置 token 配置 秒杀接口 ​结果分析 ​但是&#xff0c;实际企业中&#xff0c;这种压测方式根本不满足实际需求。下面…

技术资料MF74:将图像插入单元格注释

【分享成果&#xff0c;随喜正能量】须知往生净土&#xff0c;全仗信、愿。有信、愿&#xff0c;即未得三昧、未得一心不乱&#xff0c;亦可往生。且莫只以一心不乱&#xff0c;及得念佛三昧为志事&#xff0c;不复以信、愿、净念为事。。 我给VBA的定义&#xff1a;VBA是个人…

讯飞星火大模型V3.0 WebApi使用

讯飞星火大模型V3.0 WebApi使用 文档说明&#xff1a;星火认知大模型Web文档 | 讯飞开放平台文档中心 (xfyun.cn) 实现效果 初始化 首先构建一个基础脚手架项目 npm init vuelatest用到如下依赖 "dependencies": {"crypto-js": "^4.2.0",&q…

ClickHouse快速了解

简介 ClickHouse是一个开源列式数据库管理系统&#xff08;DBMS&#xff09;&#xff0c;用于在线分析处理&#xff08;OLAP&#xff09;&#xff1a; 列式存储&#xff1a;与传统的行式数据库不同&#xff0c;ClickHouse以列的形式存储数据&#xff0c;这使得在分析大量数据时…

嵌入式中C++ 编程习惯与编程要点分析

以良好的方式编写C class 假设现在我们要实现一个复数类complex&#xff0c;在类的实现过程中探索良好的编程习惯。 ① Header(头文件)中的防卫式声明 complex.h: # ifndef __COMPLEX__ # define __COMPLEX__ class complex {} # endif 防止头文件的内容被多次包含。 …

2.25每日一题(反常积分的计算:被积函数分母出现e的正负x次幂)

注&#xff1a;被积函数分母出现e的正负x次幂&#xff0c;这种情况需要把分母化成全部都是正次幂的情况再进行计算

【C语言】字符函数、字符串函数与内存函数

简单不先于复杂&#xff0c;而是在复杂之后。 目录 0. 前言 1. 函数介绍 1.1 strlen 1.1.1 介绍 1.1.2 strlen 函数模拟实现 1.1.2.1 计数器方法 1.1.2.2 递归方法 1.1.2.3 指针 - 指针方法 1.2 strcpy 1.2.1 介绍 1.2.2 strcpy 函数模拟实现 1.3 strcat 1…

基于标签的电影推荐算法研究_张萌

&#xff12; 标签推荐算法计算过程 &#xff12;&#xff0e;&#xff11; 计算用户对标签的喜好程度 用户对一个标签的认可度可以使用二元关系来表示&#xff0c;这种关系只有“是”“否”两种结果&#xff0c;实际上难以准确地表达出用 户对物品的喜好程度。因此&#x…

云耀服务器L实例搭配负载均衡部署Linux 可视化宝塔面板

云耀服务器L实例搭配负载均衡部署Linux 可视化宝塔面板 1. 华为云云耀服务器L实例介绍 华为云云耀服务器L实例是一种高性能、高可靠性的云服务器实例&#xff0c;适用于大规模企业级应用、大数据分析等场景。它基于华为最新一代的硬件虚拟化技术&#xff0c;提供了更高的计算…

Azure - 自动化机器学习AutoML Azure使用详解

目录 一、AutoML是如何工作的&#xff1f;二、何时考虑AutoML&#xff1f;三、AutoML助力训练与集成过程四、实战案例五、总结 自动化机器学习&#xff0c;简称为AutoML&#xff0c;旨在将机器学习模型的开发中繁琐且重复的任务自动化。这使得数据科学家、分析师以及开发人员能…

ArcGIS笔记13_利用ArcGIS制作岸线与水深地形数据?建立水动力模型之前的数据收集与处理?

本文目录 前言Step 1 岸线数据Step 2 水深地形数据Step 3 其他数据及资料 前言 在利用MIKE建立水动力模型&#xff08;详见【MIKE水动力笔记】系列&#xff09;之前&#xff0c;需要收集、处理和制作诸多数据和资料&#xff0c;主要有岸线数据、水深地形数据、开边界潮位驱动数…