基类、派生类、多态

文章详细阐述了C++中的继承、派生类的概念和目的,强调了派生类与基类的关系,包括访问权限、类型转换和赋值操作中的切片问题。接着,讨论了动态多态,特别是虚函数在实现多态性中的关键作用。最后,区分了重载、隐藏和多态的区别,强调了函数覆盖和隐藏的机制。
摘要由CSDN通过智能技术生成

目录

基类、派生类、多态

一、概念引领

1. 继承和派生,一个过程,两个角度

2. 派生类的目的

3. 如果不使用派生类,会遇到的问题

二、派生类规范,派生类与基类关系

1. 派生类规范

2. 派生类与基类的指针,访问权限,类型转换

3. 基类与派生类之间赋值操作,切片问题

4. 指向基类的指针与指向派生类的指针

5. 基类的引用与派生类的引用

三、给定基类的指针,他到底指向谁?动态多态

0. 概念引入:同质,异质,类型字段 (Type Fields)

1. 一种不成熟的解决方案,限定单一指向

2. 第二种不成熟的解决方案,类型字段,差不多是用switch来判断

3. 虚函数,动态多态(dynamic binding)

4. 使用多态

四、区分重载、隐藏、多态


一、概念引领

1. 继承和派生,一个过程,两个角度

  • 保持已有类的特性而构造新类的过程称为继承。

  • 在已有类的基础上新增自己的特性而产生新类的过程称为派生。

  • 被继承的已有类称为基类(或父类),派生出的新类称为派生类(或子类)。

2. 派生类的目的

  • 吸收基类成员

默认情况下派生类包含了全部基类中除构造和析构函数之外的所有成员。

派生类不继承基类的构造和析构!!!

c++11规定可以用using语句继承基类构造函数。

class Derived : public Base {
public:
    using Base::Base; // 继承基类的构造函数
    // 其他成员函数和数据成员
};

  • 改造基类成员

如果派生类声明一个和某基类成员同名的新成员,派生的新成员就隐藏或覆盖了外层同名成员。

即:同名覆盖

  • 添加新的成员

派生类增加新成员使派生类在功能上有所发展。

3. 如果不使用派生类,会遇到的问题

假设现在有两个结构体,员工(Employee)和经理(Manger)

经理会管理一批员工,但经理同时也是员工

struct Employee
{
};
struct Manager
{
    Employee list[];//管理的员工的数组
};

问题出在哪儿?ManagerEmployee是彼此独立的,严格意义来讲,你并不能让Manager很好的体现“经理也是个员工”这一特性

即:你是无法把一个Manager放进Employee的数组中的,因为Manager*Employee*是完全不同的

二、派生类规范,派生类与基类关系

1. 派生类规范

  • 派生类只能出现在基类之后

class Derived : public Base; //错误,前向声明会导致编译错误
class Base;
class Derived; //正确
  • 基类的声明之后,只能有派生类的声明,如果基类未被定义,绝不可定义派生类

class Employee;
class Manager:public Employee{
…    // error, 基类在使用前必须先定义 
};
  • 友元关系不可继承

  • 构造函数,复制构造,析构函数,赋值操作永不继承,自行实现!!!

  • 如果基类定义了静态成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个静态成员只有一个实例。

  • 静态成员遵循常规访问控制:如果静态成员在基类中为私有的,则派生类不能访问它。如果该静态成员在基类是公有的,则基类可以访问它,派生类也可以访问它。

2. 派生类与基类的指针,访问权限,类型转换

基类指针可以隐式转换,指向派生类对象 基类引用可以隐式转换,引用派生类对象

核心:一个类型的指针总是要访问到该类型应该访问到的。派生类往往含有比基类更多的“独特”的成员,而如果要想通过派生类的指针访问基类对象,则会有“访问不存在的东西”的事情发生。

3. 基类与派生类之间赋值操作,切片问题

将派生类对象赋值给基类对象是合理的(前提:public继承方式)

一般来说,只有派生类的对象可以赋值给基类的对象,反之,则不可以

但!这会引发切片问题:不属于基类的部分将被切除。

  • 基类中没有派生类有的东西,编译器找不到,报错!

  • 基类需要的,派生类都能提供

4. 指向基类的指针与指向派生类的指针

基类的指针可以指向派生类对象,但是反过来则不行,派生类的指针不可以指向基类的指针。

  • 基类对象所占用的内存空间通常小于派生类对象,所以基类指针不会超出派生类对象去操作数据。

  • 反之,就可能越界访问内存,极其危险

  • 基类的指针只能指向派生类继承的基类空间(相当于指向基类本身)

5. 基类的引用与派生类的引用

基类的引用可以作为派生类对象的别名,但是反过来则不行,派生类的引用不可以作为基类对象的别名。

三、给定基类的指针,他到底指向谁?动态多态

0. 概念引入:同质,异质,类型字段 (Type Fields)

异质对象: 在面向对象编程中,具有不同类型的对象集合。这些对象可以是基于相同的基类(例如C++中的继承关系),但它们可能具有不同的派生类型。异质对象的特点是它们的类型和行为可以不同,因此处理这些对象需要特殊的技术或机制来处理不同类型的对象。

同质对象:有相同类型的对象集合。这些对象具有相同的类或类型,并且在行为和属性上没有明显的区别。它们可以通过相同的方法和属性进行处理,因为它们共享相同的类型定义。(可以理解为完全是同一类型,最细化)

1. 一种不成熟的解决方案,限定单一指向

限定Base*只能指向单一类型

2. 第二种不成熟的解决方案,类型字段,差不多是用switch来判断

Type field(类型字段)是在编程中常用的一种技术,用于在对象或数据结构中存储表示类型的信息。它通常是一个额外的字段或属性,用于标识对象的具体类型或类别。

通过使用类型字段,可以在运行时确定对象的类型,从而在处理对象时采取相应的行为。例如,在面向对象的语言中,可以使用类型字段来区分不同的子类或派生类,并根据类型字段的值执行相应的操作或调用适当的方法。

然而,使用类型字段的技术也有一些缺点。它需要维护和更新类型字段的值,容易出错。此外,如果类型字段被错误地设置或更改,可能会导致程序逻辑错误或不一致性。因此,过度依赖类型字段可能会引发维护问题和错误。

#include <iostream>
​
class Animal {
public:
    Animal(const std::string& type) : type_(type) {}
​
    void makeSound() const {
        if (type_ == "cat") {
            std::cout << "Meow!" << std::endl;
        }
        else if (type_ == "dog") {
            std::cout << "Woof!" << std::endl;
        }
        else {
            std::cout << "Unknown animal" << std::endl;
        }
    }
​
private:
    std::string type_;
};
​
int main() {
    Animal animal1("cat");
    animal1.makeSound();  // 输出: Meow!
​
    Animal animal2("dog");
    animal2.makeSound();  // 输出: Woof!
​
    Animal animal3("bird");
    animal3.makeSound();  // 输出: Unknown animal
​
    return 0;
}

3. 虚函数,动态多态(dynamic binding)

多态本质上是基类访问不同派生类的方法

静态多态 : 程序在编译阶段已经 确定将要执行的状态。 (函数重载,运算符重载,模板)

动态多态 :程序在运行阶段才能确定将要执行的状态。 (动态绑定)

动态多态使用前提:

  • 基类要有虚函数 。

  • 派生类重写基类的虚函数 (实现覆盖)。

  • 通过基类的指针或引用指向派生类的对象,并调用重写后的接口。 (类外函数调用)

即:无论实际使用的对象是什么类型,都能从基类函数中获得正确的行为,这被称为多态性(polymorphism)。 具有虚函数的类型被称为多态类型(polymorphic type)。 要获得多态行为,被调用的成员函数必须是虚函数,并且对象必须通过指针或引用来操作。

class father{
public:
    virtual void print();
}
class son{
public:
    void print()
}
father f;
son s;
father* fp = &s;
//声明为基类指针,但指向派生类,在调用函数时
//如果调用了派生类中实现了的虚函数,则产生多态行为

4. 使用多态

  • 将基类的引用或指针作为函数参数传入

  • 当我们将派生类的virtual去掉的时候,仍然可以构成多态(相当于继承了virtual属性),但为了统一性,不建议将virtual拿掉

#include<iostream>
#include<string>
using namespace std;
class Person
{
public: 
    virtual void BuyTicket()
    {
        cout << "全价买票" << endl;
    }
};
class Student :public Person
{
public:
    virtual void BuyTicket()
    {
        cout << "半价买票" << endl;
    }
};
void Func(Person& p)//传入基类的引用
{
    p.BuyTicket();
}
int main()
{
    Person p1;
    Student p2;
    Func(p1);
    Func(p2);
}

四、区分重载、隐藏、多态

  • 函数重载只会发生在同作用域中(或同一个类中),函数名称相同,但参数类型或参数个数不同。 函数重载不能通过函数的返回类型来区分,因为在函数返回之前我们并不知道函数的返回类型。

  • 函数隐藏和函数覆盖只会发生在基类和派生类之间。

  • 函数隐藏是指派生类中函数与基类中的函数同名,但是这个函数在基类中并没有被定义为虚函数,这种情况就是函数的隐藏。 所谓隐藏是指使用常规的调用方法,派生类对象访问这个函数时,会优先访问派生类中的这个函数,基类中的这个函数对派生类对象来说是隐藏起来的。 但是隐藏并不意味这不存在或完全不可访问。通过 b->Base::func()访问基类中被隐藏的函数。

  • 函数覆盖特指由基类中定义的虚函数引发的一种多态现象。在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值