240607 继承

news2024/10/11 21:32:02

面向对象三大特性:封装、继承、多态

RE: 封装

  1. C++把数据和方法封装在类里面
  2. 迭代器和适配器

继承

1 基类 & 派生类

一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名,形式如下:

class 派生类(子类): 访问修饰符 基类(父类)
	注:
	访问修饰符是 publicprotectedprivate 其中的一个,
	未使用它则默认是 private

假设有一个基类 Shape,Rectangle 是它的派生类,如下所示:

#include <iostream>
using namespace std;
 
// 基类
class Shape 
{
   public:
      void setWidth(int w)
      {
         width = w;
      }
      void setHeight(int h)
      {
         height = h;
      }
   protected:
      int width;
      int height;
};
 
// 派生类
class Rectangle: public Shape
{
   public:
      int getArea()
      { 
         return (width * height); 
      }
};
 
int main(void)
{
   Rectangle Rect;
 
   Rect.setWidth(5);
   Rect.setHeight(7);
 
   // 输出对象的面积
   cout << "Total area: " << Rect.getArea() << endl;
 
   return 0;
}

2 访问控制和继承

访问			public	 protected	 private
同一个类		yes		 yes		 yes
派生类		yes		 yes		 no
外部的类		yes		 no			 no

3 赋值兼容转换(切片)

派生类对象 可以赋值给 基类的对象/基类的指针/基类的引用
(切片:把派生类中基类那部分切来赋值过去) 赋值语句:父类对象 = 子类对象
基类对象不能赋值给派生类对象
注:单独的语法规则,不是类型转换,没有产生临时变量,与下述引例机制不同

引例:截断和提升

// 类型转换会产生临时变量
int i = 1234;
printf("%x\n", i);// 4d2
// 截断
char ch = i;
printf("%x\n", ch);// ffffffd2
// 提升
i = ch;
printf("%x\n", i);// ffffffd2

const int& ref_i = i;// 临时变量具有常性
printf("%d\n", ref_i);// -46

const char& ref_ch = ch;// 临时变量具有常性
printf("%d\n", ref_ch);// -46

1234 的二进制表示为:10011010010(除 2 取余法)
补全至16位:0000 0100 1101 0010
然后,将每一组二进制转换为十六进制:
0000 = 0
0100 = 4
1101 = D(大小写皆可)
0010 = 2
最终组合成十六进制:4d2

第一个输出结果:4d2

截断
因为 char 类型只占用 1 个字节(8 位),而 i 是一个 32 位的整数,在赋值给 char 时,只保留了最低 8 位的内容(1101 0010,即 0xd2),其余高位被截断。
留下的内容最高位是 1,表明这是一个负数,当 char 被提升为 int 以打印时,会进行符号扩展,高位会被填充为 1,使得 ch 变成 0xFFFFFFD2(在 32 位系统上)。

第二个输出结果:ffffffd2

提升:

ch 是有符号类型且其值为 1101 0010,
找到补码:当前的二进制数 11010010 就是补码。
求反码(将所有位取反):11010010 取反后得到:00101101
加 1:00101101 + 1 = 00101110
这个结果是 00101110,对应的十进制值是 46。
因为符号位是 1,表示这是一个负数,所以最终值为:-46。

ch 是有符号类型且其值为 0xD2(-46 in decimal),类型提升时高位会填充符号位,因此 i 的值为 0xFFFFFFD2

第三个输出结果:ffffffd2

// 赋值兼容转换
Shape Sh1;
Rect1.name = "RECT";
Sh1 = Rect1;
Shape* ptr = &Sh1;
Shape& ref = Sh1;
ptr->name += "x";
ref.name += "x";
Rect1.PrintName();
Sh1.PrintName();
cout << endl;

两次打印结果如下:

RECT
RECTxx

对象的赋值是拷贝赋值;
Rect1 和 Sh1 是两个独立的对象: 由于 Sh1 是 Rect1 的副本,对 Sh1 的修改不会影响 Rect1,反之亦然。

4 继承的作用域

各作用域的影响:

作用域语法编译查找规则生命周期
局部域
全局域
命名空间域
(默认不查找,除非展开或指定)
不存在
类域不存在
4.1 示例1
class Person
{
protected:
    string _name = "小李子"; // 姓名
    int _num = 111; // 身份证号
};

class Student : public Person
{
public:
    void Print()
    {
        cout << "姓名:" << _name << endl;
        cout << "学号:" << _num << endl;// Student的_num和Person的_num构成隐藏关系
        cout << "身份证号:" << Person::_num << endl;
    }
protected:
    int _num = 999; // 学号
};

void TestStu()
{
    Student s1;
    s1.Print();
};

姓名:小李子
学号:999
身份证号:111

4.2 示例2
class A {
public:
    void ft() {
        cout << "void ft()" << endl;
    }
};

class B :public A {
public:
    void ft(int i) {
        A::ft();
        cout << "void ft(int i), i = " << i << endl;
    }
};

void TestB() {
    B b;
    b.ft(1);
    // Q:重载,隐藏,编译报错,运行报错?
    // A:两者构成隐藏,函数重载的前提是在同一个作用域
}

void ft()
void ft(int i), i = 1

变式

class A {
public:
    void ft() {
        cout << "void ft()" << endl;
    }
};

class B :public A {
public:
    void ft(int i) {
        cout << "void ft(int i), i = " << i << endl;
    }
};

void TestB() {
    B bb;
    bb.ft();
    // Q:重载,隐藏,重写,编译报错,运行报错?(不定项选择)
    // A:两者构成隐藏且编译报错
    // 如何调用父类?
	b2.A::ft();
}

5 继承过程中涉及的构造、拷贝构造和析构函数的工作机制

5.1 构造函数在继承中的作用和调用顺序
5.1.1 构造函数的调用顺序

当创建派生类对象时,基类的构造函数会先于派生类的构造函数被调用。这是因为派生类需要依赖基类的成员和功能,所以必须先初始化基类部分。
在构造派生类对象时,不能先初始化派生类再初始化基类,因为派生类的构造函数可能依赖于基类的成员。如果基类未先初始化,这些成员将包含随机值,从而导致不确定的行为。

示例:

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        cout << "Base Default Constructor" << endl;
    }
};

class Derived : public Base {
public:
    Derived() {
        cout << "Derived Default Constructor" << endl;
    }
};

int main() {
    Derived d;
    return 0;
}

输出:

Base Default Constructor
Derived Default Constructor

解释:

  • 基类成员当成一个整体:在构造派生类对象时,基类部分被当作一个整体,调用其默认构造函数来初始化。
  • 派生类自己的成员
    • 内置类型成员:根据编译器的实现,可能会自动初始化(如置零),也可能不处理,产生未定义的值。
    • 自定义类型成员:会调用它们的默认构造函数进行初始化。
5.1.2 派生类构造函数如何初始化基类

如果基类没有默认构造函数,或者需要传递参数,可以在派生类的构造函数的初始化列表中显式调用基类的构造函数。

示例:

class Base {
public:
    int baseValue;
    Base(int x) : baseValue(x) {
        cout << "Base Parameterized Constructor" << endl;
    }
};

class Derived : public Base {
public:
    int derivedValue;
    Derived(int x, int y) : Base(x), derivedValue(y) {
        cout << "Derived Parameterized Constructor" << endl;
    }
};

解释:

  • 派生类的构造函数在初始化列表中调用了基类的构造函数 Base(x),并初始化了自己的成员 derivedValue(y)

5.2 拷贝构造函数在继承中的行为
5.2.1 默认拷贝构造函数的生成

当你没有显式定义拷贝构造函数时,编译器会为你生成一个默认拷贝构造函数。对于派生类,默认拷贝构造函数的行为如下:

  • 基类成员当成一个整体:调用基类的拷贝构造函数来复制基类部分的数据。
  • 派生类自己的成员
    • 内置类型成员:逐个成员进行值拷贝(浅拷贝)。
    • 自定义类型成员:调用它们的拷贝构造函数进行复制。

示例:

class Base {
public:
    int baseValue;
    Base(int x) : baseValue(x) {}
    Base(const Base& other) : baseValue(other.baseValue) {
        cout << "Base Copy Constructor" << endl;
    }
};

class Derived : public Base {
public:
    int* derivedValue;
    Derived(int x, int y) : Base(x) {
        derivedValue = new int(y);
    }
    // 默认拷贝构造函数
    // Derived(const Derived& other) : Base(other), derivedValue(other.derivedValue) {}
    ~Derived() {
        delete derivedValue;
    }
};

当我们执行以下代码:

Derived d1(10, 20);
Derived d2 = d1; // 调用默认拷贝构造函数

可能的问题:

  • derivedValue 是一个指针,默认拷贝构造函数会进行浅拷贝,即复制指针的值。
  • 这会导致 d1d2derivedValue 指向同一块内存,可能在析构时造成重复释放(double free)等错误。
5.2.2 需要自定义拷贝构造函数的情况

当派生类的成员涉及到动态内存分配或需要深拷贝时,必须自定义拷贝构造函数。

示例(自定义拷贝构造函数):

class Derived : public Base {
public:
    int* derivedValue;
    Derived(int x, int y) : Base(x) {
        derivedValue = new int(y);
    }
    Derived(const Derived& other) : Base(other) { // 调用基类的拷贝构造函数
        derivedValue = new int(*other.derivedValue); // 深拷贝
        cout << "Derived Copy Constructor" << endl;
    }
    ~Derived() {
        delete derivedValue;
    }
};

解释:

  • 基类成员当成一个整体:在派生类的拷贝构造函数中,显式调用了基类的拷贝构造函数 Base(other)
  • 派生类自己的成员
    • 内置类型成员:如果有内置类型成员,默认会进行值拷贝。
    • 自定义类型成员:需要手动编写代码来实现深拷贝,防止多个对象共享同一块内存。

5.3 析构函数在继承中的作用和调用顺序
5.3.1 析构函数的调用顺序

当销毁派生类对象时,析构函数的调用顺序与构造函数相反

  1. 首先调用派生类的析构函数,清理派生类特有的资源。
  2. 然后调用基类的析构函数,清理基类部分的资源。

示例:

class Base {
public:
    ~Base() {
        cout << "Base Destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived Destructor" << endl;
    }
};

int main() {
    Derived d;
    return 0;
}

输出:

Derived Destructor
Base Destructor

解释:

  • 先销毁派生类部分,释放派生类特有的资源。
  • 然后销毁基类部分,确保对象的所有资源都被正确释放。
5.3.2 虚析构函数的重要性

在涉及多态的情况下,如果你通过基类指针删除派生类对象,基类的析构函数必须是虚函数(virtual,否则可能导致派生类的析构函数不被调用,造成资源泄漏。

示例:

class Base {
public:
    virtual ~Base() {
        cout << "Base Destructor" << endl;
    }
};

class Derived : public Base {
public:
    ~Derived() {
        cout << "Derived Destructor" << endl;
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr; // 正确调用派生类和基类的析构函数
    return 0;
}

输出:

Derived Destructor
Base Destructor

解释:

  • 基类的析构函数被声明为虚函数后,delete 基类指针时,会先调用派生类的析构函数,再调用基类的析构函数。
  • 如果基类析构函数不是虚函数,只会调用基类的析构函数,派生类的资源可能得不到释放。

5.4 总结与注意事项
5.4.1 默认构造函数的行为
  • 基类成员:在派生类的构造过程中,基类部分被当作一个整体,调用基类的默认构造函数。
  • 派生类的内置类型成员:编译器可能会自动初始化(如置零),也可能不处理,这取决于编译器实现。
  • 派生类的自定义类型成员:会调用它们的默认构造函数进行初始化。
5.4.2 默认拷贝构造函数的行为
  • 基类成员:调用基类的拷贝构造函数,复制基类部分的数据。
  • 派生类的内置类型成员:逐个成员进行值拷贝(浅拷贝)。
  • 派生类的自定义类型成员:调用它们的拷贝构造函数。
5.4.3 何时需要自定义拷贝构造函数
  • 当派生类的成员涉及到动态内存分配、文件句柄、网络连接等需要深拷贝的资源时,必须自定义拷贝构造函数和赋值运算符,以正确管理资源,防止浅拷贝带来的问题。
5.4.4 赋值操作符的注意事项
  • 类似于拷贝构造函数,赋值操作符在默认情况下也会进行浅拷贝。如果涉及到需要深拷贝的成员,应该自定义赋值操作符。
5.4.5 避免资源泄漏和悬垂指针
  • 正确地管理对象的生命周期,确保析构函数能被正确调用,防止资源泄漏。
  • 注意浅拷贝带来的悬垂指针问题(指针指向已被释放的内存)。

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

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

相关文章

如何创建一个node.js项目并配置

要想创建一个node.js项目&#xff0c;首先的话要先有npm和node.js环境&#xff0c;没有的同学可以参考一下连接&#xff1a;npm安装-详细教程-CSDN博客 一、创建node.js项目文件 注意&#xff1a;windows系统创建时文件名不能有汉字和空格 我创建了一个文件夹叫nodejswork 二…

离线使用k8s部署项目

docker的安装与完全卸载&#xff08;亲测可用&#xff09; docker的安装与完全卸载 然后配置镜像加速器 vi /etc/docker/daemon.json 将找到的镜像仓库地址写入 具体内容可以参考此网站时刻更新镜像源仓库 然后保存退出 执行 systemctl daemon-reloadsystemctl restart…

第J3周:DenseNet算法实战与解析

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 文章目录 一、前期工作二、模型复现1、设置GPU2、导入数据3、加载数据4. 配置数据集5. 可视化数据6、构建DenseNet121网络7、编译8、训练模型9、模型评估 三、…

H3C IPsec over GRE VPN 实验

H3C IPsec over GRE VPN 实验 实验拓扑 ​​ 实验需求 按照图示配置 IP 地址,R1 和 R3 配置 Loopback0 口模拟业务网段R1 和 R3 上配置默认路由连通公网R1 和 R3 上配置 IPsec over GRE VPN 来连通两端内网R1 和 R3 配置 OSPF 来传递内网路由实验步骤 按照图示配置 IP 地址…

b站视频下载, b站视频下载助手 如何下载哔哩哔哩视频

1. 链接&#xff1a; 哔哩哔哩(bilibili)视频解析下载 - 保存B站视频到手机、电脑 2. 下载即可

多线程——线程安全

目录 前言 一、观察线程不安全 二、线程安全概念 三、产生线程安全问题的原因 1.分析示例代码 2.线程随机调度 3.修改共享数据 4.原子性 5.可见性 6.指令重排序 四、解决示例代码的问题 结尾 前言 我们学习多线程编程的目的是为了能够实现“并发编程”&#xff0c;…

WAFER连接器在现代电子领域的多样化应用

WAFER连接器是一种广泛应用于现代电子设备中的连接组件&#xff0c;其设计和功能使其在多种应用场景中表现出色。作为一种高效、可靠的连接解决方案&#xff0c;WAFER连接器凭借其小巧、精密的设计赢得了工程师和设计师的青睐。这篇文章将探讨WAFER连接器在不同行业和应用领域中…

力扣第1题:两数之和(图解版)

Golang版本 func twoSum(nums []int, target int) []int {m : make(map[int]int)for i : range nums {if _, ok : m[target - nums[i]]; ok {return []int{i, m[target - nums[i]]}} m[nums[i]] i}return nil }

pip install ERROR: Could not install packages due to an OSError

问题解决 pip install xxx报错&#xff1a; WARNING: Retrying (Retry(total4, connectNone, readNone, redirectNone, statusNone)) ERROR: Could not install packages due to an OSError 使用 pip install xxx --user 安装

游离的 HEAD 如何解决

简介 问题描述&#xff1a;使用 IDEA 在提交代码时&#xff0c;禁止提交 如何解决&#xff1a;迁出分支再提交&#xff0c;最后合并到 main 或 master 上 如何解决

面向抽象和面向接口的区别

‌1.概念 01、抽象类 在 Java 中&#xff0c;通过关键字 abstract 定义的类叫做抽象类。Java 是一门面向对象的语言&#xff0c;因此所有的对象都是通过类来描述的&#xff1b;但反过来&#xff0c;并不是所有的类都是用来描述对象的&#xff0c;抽象类就是其中的一种。 以下示…

接口测试常用工具及测试方法

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 接口一般来说有两种&#xff0c;一种是程序内部的接口&#xff0c;一种是系统对外的接口。 系统对外的接口&#xff1a;比如你要从别的网站或服务器上获取资源或信…

vue基础语法的用法(API组合式风格)

一、项目文件结构 .vscode我们在那个编辑器中编辑时就会有对应的这个文件夹&#xff0c;不是固定的 进行编写代码前先把资源自带的页面删除&#xff0c;以防误导&#xff0c;可以像我一样的删除内容 vue文件结构 二、你好 vue el插值 script代码 v-text插值 script代码 三、…

A2P云短信应用场景

中国联通国际公司产品之 A2P 云短信 在当今这个全球化的商业环境中&#xff0c;企业要想在激烈的市场竞争中脱颖而出&#xff0c;不仅需要提供优质的产品和服务&#xff0c;还需要建立起与客户之间的紧密沟通桥梁。中国联通国际公司凭借其强大的国际通信能力和丰富的行业经验&…

麒麟信安参编《信息技术应用创新 移动智能终端操作系统测试规范》

9月20日&#xff0c;广州信创协会团体标准编制培训会暨参编证书颁发仪式在北京举行。会上颁发了T/GZXC 003-2024《信息技术应用创新 移动智能终端操作系统测试规范》团体标准参编证书。麒麟信安作为重点参编单位之一&#xff0c;凭借在移动智能终端操作系统测试领域的丰富实践经…

Python微震波频散相速分析

&#x1f3af;要点 在二维均匀介质均匀源中合成互相关函数以便构建波层析成像。闭环系统中微积分计算情景&#xff1a;完美弹性体震波、随机外力对模式的能量分配。开环系统中微积分计算情景&#xff1a;无数震源激发波方程、闭合曲线上的随机源、不相关平面波事件。整理地震波…

鸿蒙NEXT开发-面试题库(最新)

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…

LUCEDA IPKISS Tutorial 77:在版图一定范围内填充dummy

案例分享&#xff1a;在给定的Shape内填充dummy 所有代码如下&#xff1a; from si_fab import all as pdk from ipkiss3 import all as i3 from shapely.geometry import Polygon, MultiPolygon import numpy as np import matplotlib.pyplot as pltclass CellFilledWithCon…

【AI换脸】Rope一键整合包,实现视频多人实时换脸

随着人工智能技术的发展&#xff0c;人们越来越注重人机交互的趣味性和实用性。AI换脸技术正是在这种背景下兴起的一种创新应用。Rope换脸工具以其易用性和卓越的效果&#xff0c;成为了众多用户和专业人士青睐的对象。 Rope是什么&#xff1f; Rope是一款开源的deepfake软件&…