目录
频道首页
类的继承和多态详解_多态公有继承_无梦
收藏
0
The_Byte_Station 最近修改于 2023-10-29 11:57:19

在这里插入图片描述

类的继承

1.继承的基本概念和派生类的定义

继承是面向对象编程中的一个重要特性,它允许我们创建一个新的类(称之为派生类),从一个已有的类(称之为基类或父类)中继承它的属性和方法(函数)。在继承中,派生类可以继承基类的公共成员以及这些成员的所有属性和方法。这样,通过继承,我们可以让派生类获得从基类中的一些特性,并且可以在这些特性的基础上进行扩展和定制。

2.案例

下面我们举一个通俗易懂的例子来说明派生类的定义。

假设我们现在要定义一个汽车类,其中包括汽车的品牌、颜色、价格等属性以及汽车的加油、启动、停止和驾驶等方法。汽车类的定义如下所示:

class Car {
public:   
    string brand_;
    string color_;
    double price_;
    // 构造函数
 Car(string brand, string color, double price) : brand_(brand), color_(color), price_(price) {} 
    void refuel() { cout << "加油" << endl; } // 加油
    void start() { cout << "启动" << endl; } // 启动
    void stop() { cout << "停止" << endl; } // 停止
    void drive() { cout << "驾驶" << endl; } // 驾驶
};

接着,我们要定义一种新的汽车类,它是从 Car 类继承而来的,并定义它的一些新属性和方法。这个新的汽车类我们称之为 SUV 类。SUV 类的定义如下所示:

class SUV : public Car{ // 派生类定义
public:
    // 构造函数,调用基类构造函数初始化(这里我后面会详细讲解)
  SUV(string brand, string color, double price, bool offroad) : Car(brand, color, price), offroad_(offroad) {} 
    // 汽车在越野场地上驾驶
    void offroad_drive() { cout << "越野驾驶" << endl; } 
private:
    bool offroad_;
};

3.讲解案例

在这个例子中,我们定义了一个 新的 SUV 类,它从 Car 类中继承了汽车的基本属性和方法,包括品牌、颜色、价格和加油、启动、停止和驾驶等方法。然后,SUV 类还定义了一个新增属性 offroad_ 来表示越野能力,并且新增了一个方法 offroad_drive() 用于在越野场地上驾驶。

在派生类中,我们使用关键字 public 来指出继承基类的方式,这里是公有继承(也是最常见的继承方式)。这意味着基类中的公有成员在派生类中保持不变,并且可以通过派生类的对象来访问这些成员。

在派生类的构造函数中,我们还调用了基类的构造函数来初始化基类的成员变量。这是因为基类的成员变量在派生类中并不会被自动初始化。因此,在派生类的构造函数中,可以调用基类的构造函数进行初始化。在这里,我们使用了初始值列表的方式来调用基类的构造函数,以初始化基类的属性。

这个例子比较通俗易懂,通过继承机制的实现,我们成功定义了一个新的汽车类 SUV,它继承了基类 Car 的基本属性和方法,并在此基础上增加了越野能力和相应的方法。这样,我们就可以将 Car 类型的对象用作 SUV 类型的对象,在 SUV 类型对象上增加新的方法和属性,并且可以使代码更加灵活和可复用。

三种继承方式

在C++中有三种继承方式,分别是公有继承、私有继承和保护继承,下面我们将分别进行详细解释。

1. 公有继承

公有继承指的是派生类继承基类的属性和方法时,基类中公有的成员和受保护的成员在派生类中仍然是公有的和受保护的,而基类中私有成员在派生类中不能被访问。公有继承是最常用的继承方式,因为它能够最大程度地利用基类的资源,并对派生类的扩展提供了最大的灵活性。

例如,我们定义一个形状类 Shape:

class Shape {
public:
    string color;
protected:
    string name;
private:
      int size;
public:
    void show() {
        cout << "形状:" << name << ", 颜色:" << color << endl;
    }
};

然后,我们定义一个派生类 Circle,它表示圆形,它从 Shape 类公有继承属性和方法,并且增加了自身的属性 radius 和方法 area() 以及方法print():

class Circle : public Shape { // 公有继承
public:
    double radius;
      Circle(string name,string color):name(name),color(color){}
    double area() {
        return 3.14 * radius * radius;
    }
      void  print(){
          // this->size;  无法访问
          cout<<"颜色 "<color<<"名字 "<name<name << endl;
          cout << "年龄:" << this->age << endl;
          cout << "体重:" << this->height << endl;
    }
};

然后,我们定义一个派生类 Student,它从 Person 类私有继承属性和方法,并增加了自身的属性和方法 id 和 study():

class Student : private Person { // 私有继承
private:
    int id;
public:
      Student(int height,int age,int id)
      {
          this->height=20,this->age=3,this->id=15;
      }

    void study() {
            //cout<name<height<<" "<age<id << endl;
    }
      void PrintShow(){
          this->show();
//注意,这里能打印出name,并非说明name同其他两者通过继承后变为private属性
//这里只能说明name实际上是有被派生类继承的,但在派生类中是隐性状态,无法访问,只能通过父类提供的方法间接访问
      }
};

int main()
{
    Student s;
    //s.id  明显访问不到
    //s.name  s.age  s.height  s.show()
    //在私有继承下,不管原先是什么访问权限,对象都是无法直接访问的
        s.study(); //只有通过在自身类内部定义的公开方法,才能访问得到父类中的非私有成员和自身成员  
        s.Printshow(); //通过继承父类提供的方法是可以访问name的,但一般我们并不会这么做.    
    return 0;
}

在这个例子中,我们使用私有继承方式将派生类 Student 与基类 Person 之间建立了继承关系,派生类 Student 对象无法访问基类的所有成员,派生类的外部访问基类的成员只能通过派生类提供的公有函数接口,如 Printshow(),study()等来间接访问。通过私有继承方式,我们可以封装除了派生类之外的外部对象,控制对外部成员的访问,保障数据的安全性。

3. 保护继承

保护继承指的是派生类继承基类的属性和方法时,基类中公有和保护成员都会变成派生类中的保护成员,而基类中的私有成员在派生类中无法访问,无法被派生类或派生类的外部对象访问。

我们来举一个保护继承的例子。假设我们有一个基类 Animal,它有一个声音的成员函数 makeSound(),其中包含一些成员变量,用于记录该动物的食物、体重、年龄等属性。我们要定义一个派生类 Cat,它从 Animal 类保护继承属性和方法,并增加了自己的属性和方法 catchMouse():

class Animal { 
public:
    string food;
protected:
    double weight;
private:
    int age;
public:
    void makeSound() {
        cout << "这是一个动物" << endl;
    }
};

class Cat : protected Animal { // 保护继承
public:
     Cat(string name, double weight):name(name),weight(weight){}
    void catchMouse() {
        cout << "小猫会抓老鼠" << endl;
    }
    void show() {
      //this->age  即使是在类内部,我们也无法直接访问age
        //说明age并非私有继承
   cout << "食物:" << this->food << ", 体重:" <weight<< endl;
        this->makeSound();
    }
};

int main()
{
    Cat c("鱼",14);
    //在保护继承中,所有的数据都不能被对象直接访问到.
    //我们只能通过类的公开函数间接访问
     c.show();
    return 0;
}

在这个例子中,我们使用保护继承方式将派生类 Cat 与基类 Animal 之间建立了继承关系,派生类 Cat 内部可以访问基类的保护成员变量,例如食物、体重等成员。同时,派生类 Cat 自定义了一个抓老鼠的方法 catchMouse(),这个方法属于猫类的特有行为,不能在基类 Animal 中定义。另外,派生类 Cat 新增了一个展示自己属性的公共函数 show(),这个函数用于输出父类中继承过来的属性(包括从基类继承下来的非私有属性和方法),对外提供了一个公共接口,方便外部访问。

通过保护继承方式,派生类可以继承基类的保护成员和公有成员,在派生类中,使其都为保护属性,但不能访问基类的私有成员,保证了基类的封装性,同时也保证了派生类的必要的访问权限,避免了可能存在的安全问题。

4. 总结

在这里插入图片描述

需要注意的是, C++父类的私有成员,不管是哪种继承方式,子类都是有继承的,但是子类不能直接访问,需要使用父类提供的方法才能间接访问该成员.但我们一般不做去做,因为这样破坏了程序的封装性和安全性.

三种继承方式各有特点,在面向对象编程中,我们应该根据具体的需求来选择不同的继承方式,以达到最好的编程效果和代码复用。

公有继承是最常用的继承方式,可以最大程度地利用基类的资源,保证了派生类的可扩展性;私有继承和保护继承可以保障数据的安全性,让外部对象无法直接访问核心属性和方法,同时提供了必要的访问接口,有利于程序的封装性和安全性。

继承中的构造和析构

前言

在C++中,使用继承时,子类可以显性的继承所有父类的非private成员(函数和变量),但子类不能继承父类的构造函数和析构函数。

继承中的构造函数

为什么不能继承?即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。

虽然,构造函数不会被继承过来,但是在做子类初始化时,很多场景都需要使用到父类的构造函数,此时,还是可以在子类中调用父类的构造函数的。

//子类中调用父类的构造函数
//语法
ChildClassName (string name,int age,float score):SuperClassName(name,age),score(score)
{}

代码案例

#include

class Animal{
public:
    string name;
    int age;
    //构造函数使用参数列表方式初始化
  Animal(string name,int age):name(name),age(age){}
    //打印变量的值
    void Print()
    {
cout<name<<" "<age<Print();  //调用父类继承的方法
        cout<weight<nick<name<salary<如果没有多态,只能访问成员变量。

### 构成多态的条件

1. 必须存在继承关系
2. 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
3. 存在基类的指针,通过该指针调用虚函数。

多态案例 //Person类 class Person { public: Person(string name,int age):name(name),age(age){} void info() { cout<<"Person.info"info();

//错误案例,没有使用virtual Person*person=new Person("zs",20); person->info(); person=new Student("ww",21,90); person->info();

return 0;

}

上例:定义了一个Person和一个Student类,Student类继承Person,是Person的子类,然后再main里分别实例一个Person和Student对象。

最后,分别调用Person类和Student类的方法,结果是两者各自调用自己的info函数。

现在,用Student来实例Person类。最终调用了info函数,但此时的info函数还是调用Person类的,这并不是想要的结果,若为多态的话,应该是调用Student类的info函数。

为了能够访问派生类的成员函数,C++增加了虚函数。

在C++中,使用virtual关键字修饰的函数被称为虚函数,虚函数对于多态具有决定性的作用,有虚函数才能构成多态。

一般只有当成员函数所在的类作为基类,且成员函数在类的继承后希望更改其功能的,我们会将其声明为虚函数。

virtual void info() //虚函数的语法


//正确案例 //Person类 class Person { public: Person(string name,int age):name(name),age(age){} virtual void info() { cout<<"Person.info"info();

//指针实现多态 Person*person=new Person("zs",20); person->info(); person=new Student("ww",21,90); person->info();

return 0; }

当然,除了使用指针实现多态,我们也可以使用引用实现多态。但引用不像指针那么灵活,指针可以随时改变指向,而引用只能指代固定的对象,再多态性缺乏表现力。

//引用实现多态 int main() {

//引用类似于常量,只能在定义时初始化,之后不能再引用
//其他数据,所以本例必须定义两个变量。

Person& person=new Person("xiao",100);
person.info();

Person& student= new Student("zhang",120,88);
student.info();

}

### 虚析构函数

在C++中,使用virtual关键字修饰的函数称为虚函数。C++的构造函数不可以被声明为虚函数,但析构函数可以,且有时候必须将析构函数声明为析构函数。(在C++开发中,用来做基类的析构函数一般都是虚函数)

#### 虚析构函数的作用

虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的。

当父类的析构函数不声明成虚析构函数,且子类继承父类,父类的指针指向子类时,若此时delete掉父类指针,只会调动父类的析构函数,而不会调动子类的析构函数。

#### 虚函数的实现(虚函数表)

在C++中,多态是由虚函数实现的,而虚函数主要是通过虚函数表来实现的。在多态中,对象不包括虚函数表,只有虚指针,类才包括虚函数表,派生类会生成一个兼容基类的函数表。

如果一个类中包含虚函数(virtual修饰的函数),则该类就会包括一张虚函数表,虚函数表存储的都是虚函数的地址。

虚函数表是一个指针数组,其元素是虚函数的指针,每一个元素都是函数是指针。虚函数表在编译器的编译阶段就形成了。

虚函数表是属于类的,一个类只需一个虚函数表,同一个类的对象共用一张虚函数表。

class A{ public: virtual void func1(); virtual void func2();

private: int data1,data2; };

如图:![在这里插入图片描述](https://img-blog.csdnimg.cn/893cfb2a27f14c749068777f28b24313.png)

## 动态绑定

### 1. 什么是动态绑定

动态绑定是指在程序运行时根据对象的实际类型来确定调用哪个方法或属性。

在面向对象编程中,如果一个类继承自其他类或实现了某个接口,那么它可以被看作是父类或接口的一个实例。动态绑定允许在调用方法或访问属性时,根据实际对象的类型来确定要执行的代码。这样可以实现多态性,提高代码的灵活性和可扩展性。

### 2. C++如何实现动态绑定

在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建也就是在对象初始化调用构造函数的时候。

编译器会首先默认在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象所属类的虚函数表。

起初,子类继承基类,继承基类的vptr指针,这个vptr指针是指向基类的虚函数表,当子类调用构造函数,使得子类的vptr指针指向子类在虚函数表。

### 3. 多态的原理:

链接:[link](https://www.jianshu.com/p/fa50296b301c)

## 抽象基类和纯虚函数

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际地创建一个基类的对象。

要做到这点,可以在基类中加入至少一个纯虚函数,来使基类成为抽象类。纯虚函数使用关键字virtual,并且在其后面加上=0。如果某人试着生成一个抽象类的对象,编译器会制止他,这个工具允许生成特定的设计。

当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。创建一个纯虚函数允许在接口中放置成员函数,而不一定要提供一段可能对这个函数毫无意义的代码。同时,纯虚函数要求出的类对它提供一个定义。纯虚函数总是变成“哑”函数。

建立公共接口,也就是纯虚函数抽象类。它能对于每个不同的子类有不同的表示,它建立一个基本的格式。

抽象基类至少要有一个纯虚函数。

//虚函数 virtual type funName(plist){}

//纯虚函数 virtual type fuName(plist)=0;

### 案例:

class AbstractDrinking{ public: virtual void Boil()=0; virtual void Brow()=0; virtual void PourInCup()=0; virtual void PutSomething()=0;

void MakeDrink() { Boil(); Brow(); PourInCup(); PutSomething(); }

virtual ~AbstractDrinking() {

} };

class Coffee:public AbstractDrinking { public:

virtual void Boil() { cout<<"煮山泉"<MakeDrink(); delete drink; }

int main() { DoBussiness(new Coffee); cout<在这里插入图片描述

内容大纲
批注笔记
类的继承和多态详解_多态公有继承_无梦
ArticleBot
z
z
z
z
申请加入
主页
会议室
Git管理
文章
云文档