C++ 02 继承与多态

本文围绕C++的继承和多态展开。继承方面,介绍了继承方式、单继承、多继承、菱形继承等概念,以及虚基类、虚函数等解决问题的方法。多态部分,阐述了编译时多态和运行时多态,说明了动态多态的条件和好处,还提及抽象类和虚析构函数。

1. 继承

在 C++ 中,继承是面向对象编程的一个核心概念,它允许我们创建一个新的类(称为派生类)来继承另一个类(称为基类)的属性和方法。

派生类会自动获得基类的所有公有(public)和受保护(protected)成员,而私有(private)成员则不可直接访问。

继承的主要目的是实现代码的重用,并且支持多态性。通过继承,派生类可以扩展或修改基类的行为。

1.1 继承方式:

  1. 公有继承(public):这是最常见的继承方式。公有继承的成员在派生类中保持其原有的访问级别。基类的公有成员在派生类中仍然是公有的,基类的保护成员在派生类中仍然是保护的。

  2. 保护继承(protected):保护继承会将基类的公有成员和保护成员在派生类中变为保护成员。

  3. 私有继承(private):私有继承会将基类的公有成员和保护成员在派生类中变为私有成员。这意味着派生类的成员函数可以访问这些继承来的成员,但派生类的用户则不能。

基类的成员属性分类基类的Public成员基类(protected成员)基类的Pirvate成员
01 子类public方式继承基类在子类是Public权限在子类是protected权限在子类是Pirvate权限
02 子类protected方式继承基类在子类是protected权限在子类是protected权限在子类是Pirvate权限
03 子类protected方式继承基类在子类是Pirvate权限在子类是Pirvate权限在子类是Pirvate权限

1.1.1 生活实例

像小猫和小狗这种,行为和属性类似的事物或者组织、行为,我们可以把这些事物的共同的点抽出来组成一个基类(动物类);

我们可以让小猫和小狗这些有着动物类公有属性和行为的类(子类)继承基类(父类)的属性和方法

1.2 作用


继承在C++中起到了以下几个作用,并解决了以下问题:

1.代码复用:
    继承允许派生类复用基类的方法和属性,减少代码冗余,提高代码的可维护性。
2.建立层次模型:   
    通过继承,可以创建一种层次结构,使得类的创建更加模块化,易于管理和扩展。例如,可以创建一个通用的基类“动物”,然后派生出“哺乳动物”、“鸟类”等子类。
3. 多态性:
    继承是实现多态的基础。通过基类指针或引用调用派生类的方法,可以在运行时根据对象的实际类型来执行相应的操作,这大大提高了代码的灵活性和可扩展性。
4.接口与实现分离:
    基类可以提供接口(即抽象方法),而派生类负责具体实现。这样,派生类可以在不改变接口的情况下改变实现,有助于遵循开闭原则,即对扩展开放,对修改封闭。
5.类型转换:
    继承关系中,派生类可以向上转型为基类,这意味着可以将派生类的对象视为基类的对象,这在某些情况下可以简化编程。
6.访问控制:
    C++中提供了三种继承方式:public、protected和private。这些方式可以控制派生类对基类成员的访问权限,帮助实现封装。

总之,继承在C++中是一个强大的机制,它通过模型化现实世界的复杂关系,解决了代码复用、扩展性、维护性和模块化等问题,使得程序设计更加符合人类对问题领域思考的习惯,同时保持了程序结构的清晰和简洁。

1.3 单继承

单继承是指一个类只从一个基类继承属性和方法。这种继承方式是最简单和直接的一种,它构成了类层次结构的基础。在单继承中,派生类将继承基类的所有公有成员和保护成员,而基类的私有成员是不可访问的。

1.3.1 继承语法
class 子类名:继承方式 父类名{
​
}
/*默认是私有继承:
    私有继承会将基类的公有成员和保护成员在派生类中变为私有成员。这意味着派生类的成员函数可以访问这些继承来的成员,但派生类的用户则不能。*/
1.3.1 1 例子
class Animal{
public:
    char *name;
    char* type;         //类型
    int weight;         //体重
    int bodyLength;     //身长
    char* mouth;        //嘴巴
    char* ear;          //耳朵
    void eat();
    void sleep();
    void wagTail();
    Animal();
    Animal(char *name);
    ~Animal();
};
​
class HelloCat:public Animal{
private:
    char *tail;
public:
    HelloCat();
    HelloCat(char *name);
    ~HelloCat();
    void fallInLove();//谈恋爱
};
#endif // !_ANIMAL_DEBUG_H_
​
Animal::Animal(char *name){
    std::cout << "animal Init : " << std::endl;
    this->name = new char[128];
    this->type = new char[128];
    this->mouth = new char[128];
    this->ear = new char[128];
    this->weight = 0;
    this->bodyLength = 0;
    strcpy(this->name,name);
}
​
HelloCat::HelloCat(char *name):Animal(name){
    this->tail = new char[128];
    std::cout << "HelloCat Class Init" << std::endl;
}
HelloCat::~HelloCat(){
     std::cout << "HelloCat Class destroy" << std::endl;
  if(this->tail != NULL)
        delete []this->tail;
}
Animal::~Animal(){
    std::cout << "animal destroy : " << std::endl;
    delete []this->mouth;
    delete []this->type;
    delete []this->ear;
    this->weight = 0;
    this->bodyLength = 0;
}
int main(int argc, char const *argv[]){
    HelloCat Cat1((char *)"中华田园猫");
}

注意:

在使用继承类构造函数时应该使用参数化列表的方式初始化基类

1.3.1.2 子类和基类的空间关系

在内存中,子类对象的布局通常是先放置基类的成员,然后是子类新增的成员。这样,当子类对象调用基类的方法时,它能正确访问到基类的成员变量。

当创建一个子类对象时,它包含了一个父类对象。这个父类对象是子类对象的一部分,通常位于子类对象的内存空间的最开始部分。当子类对象被销毁时,它的析构函数首先被调用,用于释放子类特有的资源。然后,编译器自动调用父类的析构函数,以释放父类对象所占用的资源。

Animal::Animal(){
    this->Animal::mouth = new char[128];
    this->mouth = new char[128];
}

int main(int argc, char const *argv[]){
    Animal anm((char * )"天下第一");        //基类
    HelloCat Cat1((char *)"中华田园猫");    //子类
​
    std::cout << "Name : " << anm.name << std::endl;
    std::cout << "Animal size = " << sizeof(anm) << std::endl;
​
    std::cout << "Name : " << Cat1.Animal::name << std::endl;
    std::cout << "HelloCat size = " << sizeof(Cat1) << std::endl;
}
        animal Init : 
        animal Init : 
        HelloCat Class Init
        Name : 天下第一
        Animal size = 40
        Name : 中华田园猫
        HelloCat size = 48
        HelloCat Class destroy
        animal destroy : 
        animal destroy : 

1.3.1.3 子类比父类“先死”的原因是:
1.析构顺序:
    C++规定,析构函数的调用顺序与构造函数的调用顺序相反。在创建对象时,首先调用父类的构造函数,然后调用子类的构造函数。因此,在销毁对象时,首先调用子类的析构函数,然后调用父类的析构函数。
2.资源管理:
    子类可能使用了父类的资源。为了确保资源被正确释放,子类需要在父类之前被销毁。这样可以避免子类在释放资源时依赖已经释放的父类资源,从而避免潜在的错误。
3.子类与父类的依赖关系:
    子类可能依赖于父类提供的方法和属性。在子类对象的生命周期中,父类对象始终是存在的。因此,在销毁子类对象时,首先需要确保子类不再依赖于父类,然后才能安全地销毁父类对象。

总之,C++中子类比父类“先死”的原因是为了确保资源被正确释放,以及遵循析构函数的调用顺序。这种设计使得C++的对象模型更加安全和可靠。

1.3.1.4 子类的拷贝构造函数设计,父类记得给对应指针申请空间
Animal::Animal(char *name)
{
    std::cout << "animal Init : " << std::endl;
    this->name = new char[128];
    this->type = new char[128];
    this->mouth = new char[128];
    this->ear = new char[128];
    this->weight = 0;
    this->bodyLength = 0;
    strcpy(this->name,name);
}
HelloCat::HelloCat(const HelloCat &dest):Animal(dest.name){
    //this->tail = NULL;
    std::cout << "HelloCat(const HelloCat &dest)" << std::endl;
    if(this->Animal::mouth != NULL) 
        strcpy(this->Animal::mouth,dest.Animal::mouth);
    if(this->Animal::type!= NULL)    
        strcpy(this->Animal::type,dest.Animal::type);
    if(this->Animal::ear != NULL)    
        strcpy(this->Animal::ear,dest.Animal::ear);
​
    this->Animal::weight = dest.Animal::weight;
​
    this->Animal::bodyLength = dest.Animal::bodyLength;  
​
    //可能条件不适配 记得定义tail时赋值为NULL,否者指向未知内存,this->tail == NULL的条件不成立
    if(this->tail == NULL){
        std::cout << "tail == NULL" << std::endl;
        this->tail = new char[128];
        strcpy(this->tail,dest.tail);
    }
    else{
        std::cout << "tail != NULL" << std::endl;
        strcpy(this->tail,dest.tail);
    }
}

1.4 组合和继承的区别

组合(Composition)和继承(Inheritance)是面向对象编程中两种不同的对象复用机制。它们在概念上和实际应用中有明显的区别:

1.关系类型:
    -继承 是一种“是-a”关系。当类A继承自类B时,类A“是”类B的一种类型。例如,如果有一个动物类和一个哺乳动物类,哺乳动物类可以继承自动物类,因为哺乳动物“是”一种动物。
    -组合 是一种“有-a”关系。当类A包含类B的实例时,类A“有”类B的一个组件。例如,一个汽车类可能包含一个引擎类的实例,因为汽车“有”一个引擎。
    
2.实现方式:
    -继承 是通过扩展一个已有的类来创建新的类。子类继承了父类的方法和属性,并且可以添加新的方法或覆盖父类的方法。
    -组合 是通过在新类中包含其他类的实例来实现。新类不继承其他类的接口,但它可以使用包含的类的功能。
    
3.灵活性:
    -继承 创建了一个固定的层次结构,子类与父类紧密耦合。这意味着对父类的修改可能会影响到所有的子类。
    -组合 提供了更高的灵活性,因为类可以通过包含不同的组件来改变其行为。组件可以在运行时替换,这使得组合更加灵活。
    
4.适用性:
    -继承 适用于当你需要创建一个具有已有类的所有特性并且还需要添加额外特性或行为的类时。
    -组合 适用于当你需要将多个不同的功能组合到一个类中,而这些功能在逻辑上并不共享同一个层次结构时。
    
5.多态性:
    -继承 支持多态性,因为子类可以覆盖父类的方法,并且在运行时可以通过父类的引用或指针来调用子类的方法。
    -组合 通常不直接支持多态性,因为包含的组件是通过其具体的类类型来访问的。但是,可以通过接口类或抽象类来实现多态性。
    
6.代码复用:
    -继承 允许代码复用,因为子类可以直接使用父类的实现。
    -组合 通过使用其他类的实例来实现代码复用,这通常被称为委托。

在实际应用中,选择继承还是组合取决于具体的需求和设计目标。通常,组合被认为比继承更安全,因为它促进了低耦合和高内聚,而继承则可能导致紧密耦合。因此,设计原则中有一条是“组合优于继承”,建议优先考虑使用组合,只有当继承真正必要时才使用它。

1.5 高内聚,低耦合


我们在进行模块之间程序之间一定要注意,高内聚,低耦合;

"高内聚,低耦合"是一个重要的设计原则,用于指导如何构建模块化、可维护和可扩展的软件系统。这个原则强调模块或组件应该高度内聚,同时与其他模块或组件之间的依赖关系应该尽量减少

高内聚(High Cohesion)

    高内聚指的是一个模块或组件内部的元素(如函数、类、服务等)紧密相关,并且共同完成一个单一的任务或功能。这意味着模块的各个部分应该高度协作,并且每个部分都应该对模块的整体任务做出贡献。高内聚的模块通常更易于理解和维护,因为它们的设计和目的非常清晰

低耦合(Low Coupling)

    低耦合指的是模块或组件之间的依赖关系尽可能少。耦合度低的系统意味着一个模块的变更对其他模块的影响很小。每个模块应该通过明确定义的接口与其他模块通信,而不是直接依赖于其他模块的内部实现。低耦合的系统更容易重用和测试,因为模块可以独立于其他模块工作。

为了实现高内聚和低耦合,可以采用以下一些设计策略和实践:

  • 单一职责原则: 每个模块或类应该只有一个改变的理由,即它应该只负责一个功能。

  • 封装: 隐藏内部实现细节,只暴露必要的接口。

  • 接口隔离原则: 客户端不应该被迫依赖于它们不使用的接口。

  • 依赖注入: 通过外部注入依赖,而不是在类内部创建依赖,从而减少耦合。

  • 使用事件、回调或观察者模式: 允许模块在不直接依赖的情况下进行通信。

  • 模块化设计: 将系统划分为独立的模块,每个模块都有明确的职责和接口。

遵循"高内聚,低耦合"的原则可以帮助开发人员创建更加灵活、可维护和可扩展的软件系统。这种设计哲学是许多现代软件开发方法,如面向对象设计、微服务架构和组件化开发的基础。

1.5.1回调函数降低模块间的耦合性。

在C和C++中,回调函数是一种常用的技术,用于降低模块间的耦合性。回调函数是一个作为参数传递给另一个函数的函数,这个函数会在某个适当的时刻被调用。这样,一个函数可以将一些操作委托给另一个函数,而不需要知道那个函数的具体实现细节。

1.5.1.1 以下是使用回调函数来降低耦合性的几个步骤:

定义回调函数类型:首先,你需要定义一个回调函数的类型,这通常是通过函数指针或者函数对象来实现的。
typedef void (*Callback)(void* data);
实现回调函数:然后,你实现一个或多个具体的回调函数,这些函数应该符合回调函数类型的签名。
void myCallback(void* data) {
    // 处理数据
}
传递回调函数:接下来,你将回调函数作为参数传递给另一个函数,这个函数会在合适的时机调用回调函数。
void doSomething(Callback callback, void* data) {
    // 执行一些操作
    // ...
​
    // 调用回调函数
    callback(data);
}
调用回调函数:最后,doSomething函数在其执行过程中的某个时刻调用回调函数,这样它就可以执行特定的操作,而不需要知道这些操作的具体实现。
int main() {
    // 数据
    void* data = /* 初始化数据 */;
​
    // 调用函数,传递回调函数和数据处理
    doSomething(myCallback, data);
​
    return 0;
}

通过这种方式,doSomething函数与myCallback函数之间的耦合性被降低了。doSomething函数不需要知道myCallback函数的具体实现,它只需要知道如何调用它。这意味着你可以轻松地替换myCallback函数,而不会影响到doSomething函数的实现。

在C++中,还可以使用更高级的回调机制,比如使用函数对象、Lambda表达式或者绑定器(std::bind和std::function),这些机制提供了更多的灵活性和类型安全。

1.6 覆盖与隐藏


当子类和基类有同名同参的成员时,默认调用子类的成员,如果你想调用父类继承过来的成员可以使用::操作符来显示调用;

在C++中,当子类(派生类)和基类有同名同参的成员时,这通常是指成员函数。如果子类重新定义了基类中的非虚拟成员函数,这被称为隐藏,而不是覆盖。隐藏可以是故意的,也可以是无意中发生的,而且可能会导致代码的行为与预期不符。

以下是关于C++中子类和基类同名同参成员的情况:

  1. 隐藏:如果子类定义了一个与基类签名完全相同的成员函数(即函数名和参数列表都相同),那么基类的成员函数将被隐藏。即使子类的函数有不同的实现,基类的函数也不会被自动调用。在子类对象上调用该函数时,将会调用子类中定义的版本。

  2. 方法覆盖:如果基类中的函数被声明为虚函数(使用virtual关键字),子类可以覆盖这个函数。在这种情况下,通过指向基类的指针或引用调用该函数时,将会调用最派生类中定义的版本,这称为多态

  3. 属性隐藏:如果子类定义了一个与基类同名的成员变量,那么在引用该变量时,将会引用子类中定义的变量。这同样适用于静态成员变量。

例子::子类显式调用从基类继承过来的方法
类与成员设计
//类设计 ------------------------------------------
class Father{
    int age;
    char *name;
public:
    void setInfo(char *name,int age);
    void showInfo();
    void Printf_class();
    Father();
    ~Father();
};
class Son:public Father{
    int age;
    char *name;
public:
    void setInfo(char *name,int age);
    void showInfo();
    Son(/* args */);
    ~Son();
    void Printf_class();
};
//类方法设计 ----------------------------------------
Son::Son():Father(){
    std::cout << "Fun : Son() strart !" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
Son::~Son(){
    std::cout << "Fun : ~Son() strart !" << std::endl;
    delete []this->name;
}
Father::Father(){
    std::cout << "Fun : Father() strart !" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
Father::~Father(){
    std::cout << "Fun : ~Father() strart !" << std::endl;
    delete []this->name;
}
​
void Father::Printf_class(){
    std::cout << "基类的打印函数" << std::endl;
}
void Son::Printf_class(){
    std::cout << "子类的打印函数" << std::endl;
}
void Father::setInfo(char *name,int age){
    std::cout << "设置基类成员数据" << std::endl;
    strcpy(this->name,name);
    this->age = age;
}
void Son::setInfo(char *name,int age){
    std::cout << "设置子类成员数据" << std::endl;
    strcpy(this->name,name);
    this->age = age;
}
void Father::showInfo(){
    std::cout << " 打印基类成员数据" << std::endl;
    std::cout << "name is : " << this->name<< std::endl;
    std::cout << "age is : "  << this->age<< std::endl;
}
void Son::showInfo(){
    std::cout << " 打印子类成员数据" << std::endl;
    std::cout << "name is : " <<  this->name<< std::endl;
    std::cout << "age is : "  <<  this->age<< std::endl;
}
主功能设计和程序结果
int main(int argc, char const *argv[]){
    Son son;
    son.setInfo((char *)"派森",888);
    son.Father::setInfo((char *)"父亲继承的姓",10000);
    son.showInfo();
    son.Father::showInfo();
    return 0;
}
//结果
Fun : Father() strart !
Fun : Son() strart !
设置子类成员数据
设置基类成员数据   
打印子类成员数据
name is : 派森
age  is : 888 
打印基类成员数据
name is : 父亲继承的姓
age  is : 10000  
Fun : ~Son() strart !
Fun : ~Father() strart !
运行流程

1.6 多继承


C++支持多继承,这意味着一个类可以同时继承多个基类。多继承在某些情况下非常有用,比如当一个类需要组合多个类的特性时。然而,多继承也带来了一些复杂性和潜在的问题,因此在实际编程中需要谨慎使用。

1.6.1多继承的基本语法如下:

class 子类名 : 继承方式 基类名1 , 继承方式 基类命2{
    // 类定义
};
多继承的一些关键点包括:
01称冲突:
    如果两个或多个基类中有相同名称的成员,那么在派生类中将出现名称冲突。这时,派生类需要明确指定使用哪个基类的成员,例如使用作用域解析运算符`::`。
    
02菱形继承(Diamond Problem):
    当两个类B和C都继承自同一个类A,然后另一个类D同时继承自B和C时,就会出现菱形继承问题。这会导致D类对象中有两份A类的成员,这可能会导致混淆和错误。C++通过虚继承来解决这个问题,使得A类在D类中只有一份成员。
    
03虚继承:
    为了解决菱形继承问题,C++引入了虚继承。在虚继承中,共享的基类在派生类中只存在一份副本。这通过在继承声明中使用virtual关键字来实现。
    
04造函数和析构函数的调用顺序:
    在多继承中,构造函数的调用顺序是按照继承声明的顺序进行的,与它们在派生类构造函数初始化列表中的顺序无关。析构函数的调用顺序与构造函数的调用顺序相反。
    
05复杂性:
    多继承会增加程序的复杂性,使得类层次结构更加难以理解和管理。它可能会导致代码的可维护性降低,因此在设计类层次结构时应该尽量避免使用多继承,除非确实需要。
多继承是一个强大的特性,但它的使用需要谨慎,以确保代码的清晰性和可维护性。在设计类层次结构时,应该优先考虑单一继承和组合,多继承应该只在必要时使用。
例子:
//类声明
class cow{
    int age;
    char *name;
public:
    cow();
   ~cow();
};
​
class horse{
    int age;
    char *name;
public:
    horse();
    ~horse();
};
​
class oxenAndHorses:public cow,public horse{
    int age;
    char *name;
public:
    oxenAndHorses(/* args */);
    ~oxenAndHorses();
};
//类函数实现
horse::horse(){
    std::cout << "马的构造函数" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
horse::~horse(){
    std::cout << "马的析构函数" << std::endl;
    delete [] this->name;
}
cow::cow(){
    std::cout << "牛的构造函数" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
cow::~cow(){
    std::cout << "牛的析构函数" << std::endl;
    delete [] this->name;
}
oxenAndHorses::oxenAndHorses():cow(),horse(){
    std::cout << "牛马的构造函数" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
oxenAndHorses::~oxenAndHorses(){
   std::cout << "牛马的析构函数" << std::endl;
    delete [] this->name;
}
主函数和程序结果
int main(int argc, char const *argv[]){
    oxenAndHorses NM;
    return 0;
}
s@Shuai:/mnt/e/MyCode/MultipleInheritance_Debug$ ./a.out 
    牛的构造函数
    马的构造函数
    牛马的构造函数
    牛马的析构函数
    马的析构函数
    牛的析构函数

多继承的构造函数,析构函数调用顺序

class 派生类 : 继承方式 基类名1 , 继承方式 基类命2(){}
基类1的构造函数    --->基类2的构造函数 --->派生类的构造函数
派生类的析构函数   --->基类2的析构函数 --->基类1的析构函数

1.7菱形继承


菱形继承是C++中多继承的一种特殊情况,它发生在当一个类同时继承自两个或多个类时,而这些类又共同继承自一个基类。这种继承结构在类图上形成一个菱形,因此得名。菱形继承可能导致一些问题,如对基类成员的重复访问和模糊的成员访问问题。

//类声明
class Animal{
private:
    int age;
    char *name;
public:
    Animal();
    ~Animal();
};
class cow:public Animal{
    int age;
    char *name;
public:
    cow();
   ~cow();
};
class horse:public Animal{
    int age;
    char *name;
public:
    horse();
    ~horse();
};
​
class oxenAndHorses:public cow,public horse{
    int age;
    char *name;
public:
    oxenAndHorses();
    ~oxenAndHorses();
};
//函数成员实现
horse::horse(){
    std::cout << "马的构造函数" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
horse::~horse(){
    std::cout << "马的析构函数" << std::endl;
    delete [] this->name;
}
cow::cow(){
    std::cout << "牛的构造函数" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
cow::~cow(){
    std::cout << "牛的析构函数" << std::endl;
    delete [] this->name;
}
oxenAndHorses::oxenAndHorses():cow(),horse(){
    std::cout << "牛马的构造函数" << std::endl;
    this->name = new char[128];
    this->age = 0;
}
oxenAndHorses::~oxenAndHorses(){
   std::cout << "牛马的析构函数" << std::endl;
    delete [] this->name;
}
Animal::Animal(){
    std::cout << "Animal()" << std::endl;
     this->name = new char[128];
}
Animal::~Animal(){
    std::cout << "~Animal()" << std::endl;
     delete [] this->name;
}
主程序
int main(int argc, char const *argv[]){
    oxenAndHorses NM;
    std::cout << "sizeof Animal : "     << sizeof(Animal) << std::endl ;
    std::cout << "sizeof cow : "        << sizeof(cow) << std::endl ;
    std::cout << "sizeof horse : "      << sizeof(horse) << std::endl ;
    std::cout << "sizeof oxenAndHorses : "  << sizeof(oxenAndHorses) << std::endl ;
    return 0;
}
//执行效果
s@Shuai:/mnt/e/MyCode/MultipleInheritance_Debug$ ./a.out 
    Animal()
    牛的构造函数
    Animal()
    马的构造函数
    牛马的构造函数
    sizeof Animal : 16
    sizeof cow : 32
    sizeof horse : 32
    sizeof oxenAndHorses : 80
    牛马的析构函数
    马的析构函数
    ~Animal()
    牛的析构函数
    ~Animal()
程序分析

1.7.1菱形继承的危害


菱形继承本身在C++中并不是不安全的,但它确实引入了一些复杂性和潜在的问题,这些问题可能会被认为是安全隐患,尤其是在没有正确处理的情况下。以下是一些菱形继承可能引入的问题:

  • 成员访问的二义性:在菱形继承中,如果一个类从两个不同的路径继承自同一个基类,那么派生类中将包含基类的两份成员。这会导致成员访问的二义性,因为编译器无法确定应该使用哪个基类的成员。例如:

class A {
public:
    void func() { std::cout << "Base::func" << std::endl; }
};
class B : public A {};
class C : public A {};
class D : public B, public C{
public:
    void useFunc() {
        func(); // 编译错误:二义性
    }
};
  • 重复的成员

由于菱形继承,派生类中可能会包含基类的多个副本。这意味着如果基类中有成员变量,派生类中将包含这些变量的多个副本,这可能会导致数据不一致性和意外的行为。

  • 构造和析构顺序

在菱形继承中,基类的构造函数和析构函数调用顺序可能会变得复杂。如果没有正确处理,可能会导致资源管理问题,例如文件句柄或网络连接没有被适当地释放。

  • 性能开销:虚继承通常用于解决菱形继承问题,但它会引入额外的间接性和潜在的性能开销。每次访问虚基类的成员时,都需要通过指针或引用进行间接访问,这可能会影响性能。

  • 复杂性:菱形继承会使类的继承结构变得更加复杂,这可能会导致代码更难以理解、维护和调试。

为了避免这些问题,通常建议在设计类的继承结构时尽量避免菱形继承。如果必须使用,应该使用虚继承,并确保正确地处理构造和析构顺序,以及避免成员访问的二义性。此外,清晰的文档和代码注释也可以帮助其他开发者理解复杂的继承结构。

1.8 虚基类


虚基类是面向对象编程中的一种概念,用于解决多继承时可能出现的菱形继承问题(也称为“歧义继承”)。在C++等支持多继承的编程语言中,当两个类B和C都继承自同一个类A,然后另一个类D同时继承自B和C时,类D将拥有两个类A的实例,这可能导致访问成员时的歧义。

为了解决这个问题,可以将基类A声明为虚基类。这意味着当B和C继承A时,它们不会各自包含A的一个实例。相反,只有一份A的实例被D共享,无论D是通过B还是C继承A。这样做可以确保在多继承层次结构中,基类只会存在一个实例,从而避免了歧义

在C++中,通过在基类继承列表中使用virtual关键字来声明虚基类。例如:

当有多个派生类继承虚基类时,派派生类继承多个派生类时,派派生类只包含一个基类实例,避免多次调用基类析构函数和构造函数,同时解决重复成员产生的问题;
//代码更改部分
class cow:  virtual public Animal{      //子类声明基类为虚基类
    int age;
    char *name;
public:
    cow();
   ~cow();
};
class horse:    virtual public Animal{  //子类声明基类为虚基类
    int age;
    char *name;
public:
    horse();
    ~horse();
};
//程序运行结果
    Animal()                //基类构造函数只执行一次
    牛的构造函数
    马的构造函数
    牛马的构造函数
    sizeof Animal : 16
    sizeof cow : 40
    sizeof horse : 40
    sizeof oxenAndHorses : 80
    牛马的析构函数
    马的析构函数
    牛的析构函数
    ~Animal()               //基类析构函数只执行一次

例子2:

class A {
public:
    void func() { std::cout << "Base::func" << std::endl; }
};
class B : virtual  public A {};
class C : virtual  public A {};
class D : public B, public C{
public:
    void useFunc() {
        func(); // 编译错误:二义性
    }
};
​
int main(int argc, char const *argv[]){
    D d;
    d.func();
    return 0;
}
​
s@Shuai:/mnt/e/MyCode/MultipleInheritance_Debug$ ./a.out 
    Base::func

1.8.1虚继承会导致子类的内存空间比没有使用虚继承时要大


在C++中,使用虚继承会导致子类的内存空间比没有使用虚继承时要大。这是因为虚继承机制需要额外的信息来保证只有一个基类实例存在,并且在派生类对象中可以正确地访问到这个共享的基类实例。以下是几个导致虚继承子类内存增加的因素:

  1. 虚基类指针(VBP):为了实现虚继承,每个直接或间接虚继承自虚基类的子类都会包含一个指向虚基类的指针。这个指针用于在运行时确定虚基类实例的位置。因此,每个子类都会因为虚基类指针的增加而增加内存开销。

  2. 虚基类表(VBTable):这个表包含了偏移量信息,用于在多重继承结构中定位虚基类的实例。虚基类表是编译器生成的,它为每个虚继承的层次结构提供了一个表,这也会占用额外的内存空间。

  3. 层次结构调整:虚继承可能需要对派生类的内存布局进行调整,以确保虚基类实例的唯一性和正确访问。这种调整可能会导致额外的填充(padding)或重新排列成员变量,从而增加内存使用。

  4. 构造函数和析构函数链的调整:由于虚继承的特殊性,派生类的构造函数和析构函数需要处理虚基类指针的初始化和虚基类表的管理。这可能会引入额外的代码和间接层次,从而增加内存和执行时间的开销。

综上所述,虚继承引入的额外内存开销主要是由于虚基类指针和虚基类表的存在,以及对类内存布局的调整。这些开销是为了确保在多继承场景中,无论继承层次多么复杂,都能保证对虚基类的一致访问和避免多重继承的菱形问题。在设计继承结构时,程序员需要在内存效率和设计灵活性之间做出权衡。

总结:
    /*  虚拟继承创建对象的时候,子类对象会产生一个虚基类指针。虚基类指针指向虚基类对象空间的起始地址
        虚拟继承创建对象大小 = 基类数据成员大小 + 继承类成员大小 + 虚基类指针(VBP) + 地址对齐*/
    cow = animal_data(12)+地址对齐(4)+cow_data(12)+地址对齐(4)+Vbp(8) = 40

1.9 虚函数 解决子类和基类函数同名会隐藏的问题,去掉多余版本


允许在派生类中重新定义基类中定义的函数。当我们使用指针或引用调用一个函数时,虚函数使得程序能够确定调用哪个函数版本,即基类的版本还是派生类的版本,这个过程被称为动态绑定或晚期绑定。

在C++中,通过在函数声明后加上 virtual 关键字来声明一个虚函数。这意味着这个函数可以在任何继承了这个类的派生类中被重写(override)。如果一个类有虚函数,它通常也应该有一个虚析构函数,以确保正确的析构顺序当通过基类指针删除派生类对象时。

1.9.0 override关键字

/*
    override是一个关键字,用于指示一个派生类中的函数意在重写其基类中的同名函数。这个关键字在C++11标准中被引入,用于增强代码的可读性和可维护性,同时也能够帮助编译器检测错误。
    当一个派生类的函数使用override关键字时,它告诉编译器这个函数是专门用来重写基类中的虚函数的。如果基类中没有与之对应的虚函数,或者函数签名不匹配,编译器将报错。这样可以避免无意中引入了新的函数,而不是重写基类的函数,从而保证了多态行为的一致性。*/

示例代码

class  Animal
{
private:
    int age;
    char *name;
public:
    void virtual setInfo(int age,char *name);
    Animal();
    ~Animal();
};
​
class cow:   virtual public Animal{ //声明公有虚继承基类
    int age;
    char *name;
public:
    void setInfo(int age,char *name) override; //声明为必须要重写的虚函数
    cow();
   ~cow();
};
​
class horse:    virtual public Animal{  //声明公有虚继承基类
    int age;
    char *name;
public:
    void setInfo(int age,char *name) override;//声明为必须要重写的虚函数
    horse();
    ~horse();
};
​
class oxenAndHorses:public cow,public horse{    
    int age;
    char *name;
public:
    void setInfo(int age,char *name) override;//声明为必须要重写的虚函数
    oxenAndHorses(/* args */);
    ~oxenAndHorses();
};
​
//函数实现
void cow::setInfo(int age,char *name){
    strcpy(this->name,name);
    memcpy(&(this->age),&age,sizeof(age));
}
void horse::setInfo(int age,char *name){
    strcpy(this->name,name);
    memcpy(&(this->age),&age,sizeof(age));
}
void Animal::setInfo(int age,char *name){
    strcpy(this->name,name);
    memcpy(&(this->age),&age,sizeof(age));
}
void oxenAndHorses ::setInfo(int age,char *name){
    strcpy(this->name,name);
    memcpy(&(this->age),&age,sizeof(age));
}
//结果
Animal()
牛的构造函数
马的构造函数
牛马的构造函数
sizeof Animal : 24  //Animal成员+ 虚基类指针(VBP)
sizeof cow : 48     //Animal成员空间 + cow类成员   +   虚函数指针成员 + 虚基类指针(VBP)
sizeof horse : 48   //Animal成员空间 + horse类成员 +   虚函数指针成员 + 虚基类指针(VBP)
sizeof oxenAndHorses : 88   // Animal成员空间 + cow类成员      +    horse类成员 + oxenAndHorses类成员 + 虚基类指针(VBP) +虚基类指针(VBP) + cow类 虚函数指针成员  horse类虚函数指针成员 + oxenAndHorses类虚函数指针成员
​
牛马的析构函数
马的析构函数
牛的析构函数
~Animal()
​

当一个类中有一个或多个虚拟函数,那么该类会产生一个虚函数指针

1.9.1 虚函数指针

虚函数指针(Virtual Function Pointer),通常简称为vptr,是C++中用于实现运行时多态的机制的一部分。每个包含虚函数的类的对象都会有一个隐式的指针成员,即vptr,它指向该类的虚函数表(vtable)。虚函数表是一个函数指针数组,每个指针都指向类的一个虚函数的实现。

vptr是在对象构造时由编译器自动设置的,它确保了当通过基类指针或引用调用一个虚函数时,能够动态地绑定到正确的函数实现上,即调用相应派生类中的重写版本(如果有的话)。

在C++中,vptr的设置和vtable的维护是编译器的责任,程序员通常不需要(也不应该)直接操作它们。然而,了解它们的工作原理对于理解C++多态性的底层机制是有帮助的。

#### 1.9.2虚函数表


虚函数表(Virtual Function Table,通常缩写为vtable)是C++用来实现虚函数的一个内部机制。每个包含至少一个虚函数的类都会有一个与之关联的虚函数表。虚函数表是一个函数指针数组,其中每个指针都指向类的一个虚函数的实现。

当类对象被创建时,它的内存布局中会包含一个指向其虚函数表的指针,通常称为vptr。这个指针在对象构造时由编译器自动设置,指向正确的虚函数表。当通过指针或引用调用一个虚函数时,实际上是通过vptr找到对应的vtable,然后调用vtable中相应的函数指针。

class  Animal
{
    int data;
    int data2;
    int data3;
public:
    void virtual show1(){std::cout  << "Animal show1() " << std::endl;}
    void virtual show2(){std::cout  << "Animal show2() " << std::endl;}
    Animal(){std::cout  << "Animal Class Init " << std::endl;}
    ~Animal(){std::cout << "Animal Class destroy " << std::endl;}
};
class cow:   virtual public Animal{ 
public:
    void  show1() override {std::cout  << "cow show1() " << std::endl;}
    void  show2() override {std::cout  << "cow show2() " << std::endl;}
   cow(){std::cout  << "cow Class Init " << std::endl;}
   ~cow(){std::cout  << "cow Class destroy " << std::endl;}
};

同一个类不同的实例共用一份虚函数表;
oxenAndHorses NM;
oxenAndHorses NM_1;
oxenAndHorses NM_2;

拿到虚函数表指针和找到虚函数表里面的虚函数地址

如何拿到虚函数表指针和找到虚函数表里面的虚函数地址
 typedef  void(*_Fun_t)(void);
 oxenAndHorses NM;
     std::cout << *(unsigned long long *)&NM << std::endl;
     unsigned long long *myFun_ptr = (unsigned long long *)(*(unsigned long long*)&NM);
    _Fun_t mFun1 =(_Fun_t)myFun_ptr[0];
    _Fun_t mFun2 =(_Fun_t)myFun_ptr[1];
//类设计
​
class  Animal{
public:
    int data;
    int data2;
    int data3;
    void virtual show1();
    void virtual show2();
    Animal();
    ~Animal();
};
class cow:   virtual public Animal{
public:
    void  show1() override;
    void  show2() override;
   cow();
   ~cow();
};
class horse:virtual public Animal{
public:
    void  show1() override;
    void  show2() override;
    horse();
    ~horse();
};
class oxenAndHorses:public cow,public horse{
public:
    void  show1() override;
    void  show2() override;
    oxenAndHorses();
    ~oxenAndHorses();
};
//类函数设计
Animal::Animal(){}
cow::cow(){}
horse::horse(){}
oxenAndHorses::oxenAndHorses(){}
oxenAndHorses::~oxenAndHorses(){}
horse::~horse(){}
cow::~cow(){}
Animal::~Animal(){}
void Animal::show1(){}
void cow::show1(){}
void horse::show1(){}
void Animal::show2(){}
void cow::show2(){}
void horse::show2(){}
​
void oxenAndHorses::show1(){
    std::cout << "show 1 oxenAndHorses " << std::endl;
}
void oxenAndHorses::show2(){
     std::cout << "show 2 oxenAndHorses " << std::endl;
}
​
//主函数设计
typedef  void(*_Fun_t)(void);
int main(int argc, char const *argv[]){
​
     oxenAndHorses NM;
    
     //获取类的地址
     std::cout <<   "NM address  =  0x" <<  std::hex <<  
                    (unsigned long long)(unsigned long long *)&NM << std::endl;     
    
     //获取oxenAndHorses类的第一个成员的值并将该值转换为一个8字节类型地址
     std::cout <<   "Vptr address = 0x" <<  std::hex <<  *(unsigned long long *)&NM << std::endl;
    
     //NM对象地址转换为unsigned long long指针类型,即假设vptr是第一个成员变量。
     unsigned long long *myFun_ptr = (unsigned long long *) (*(unsigned long long*)&NM);
    
    //把该地址当作虚函数表的地址,拿到第一个虚函数的地址
    _Fun_t mFun1 =(_Fun_t)myFun_ptr[0];
    
    //把该地址当作虚函数表的地址,拿到第二个虚函数的地址
    _Fun_t mFun2 =(_Fun_t)myFun_ptr[1];
​
     mFun1();   //运行第一个虚函数
     mFun2();   //运行第二个虚函数
​
    return 0;
}
//本次运行在window平台        
NM address  =  0x61fdc0
Vptr address =  0x406b08
show 1 oxenAndHorses
show 2 oxenAndHorses

以上是一个不规范的获取虚函数指针的方法,他基于一种假设,在不同的环境里面可能失败

01 vptr是对象内存布局中的第一个成员。
02 vptr的类型大小与unsigned long long相同。  unsigned long long在windowss是8个字节

这种方法是非标准的,因为它依赖于具体的编译器和内存布局,而这些可能会随着编译器版本和平台的不同而变化。在实际应用中,直接操作vtable和vptr是不推荐的,因为它是编译器的实现细节,而不是C++语言的标准特性。

使用这种方法进行传参函数,可能第一个参数调用会失败

02 多态

多态是面向对象编程中的一个核心概念,它允许对象通过共同的接口来表现出不同的行为。在C++中,多态通常是通过虚函数来实现的。当一个类声明了一个或多个虚函数时,它的派生类可以覆盖这些函数,提供自己的实现。这样,当通过基类的指针或引用调用一个函数时,实际调用的将是派生类中的相应函数,这就是动态多态或运行时多态。

2.1 实现多态的步骤

C++中实现多态的步骤如下:

01 定义基类:        基类中定义虚函数,使用virtual关键字。
02 派生类:          派生类继承自基类,并覆盖基类中的虚函数。
03 使用基类指针或引   通过基类指针或引用调用虚函数,实现多态。

多态的关键在于虚函数表(vtable)和虚函数指针(vptr)。每个包含虚函数类都有自己的vtable,vtable中存储了类的虚函数地址。每个类的对象都有一个指向其类vtable的vptr。当通过指针或引用调用虚函数时,实际上是通过vptr找到对应的vtable,然后调用正确的函数版本。

多态使得代码更加通用和可扩展,因为它允许同一组代码处理不同的对象类型,只要这些对象都继承自同一个基类并实现了相同的接口。这是面向对象设计中的一个重要原则,有助于实现代码的重用和模块化

同一操作作用于不同的对象有不同的结果,有不同的解释并且产生不同的效果,这就是多态,其实就是基类指针和引用指向子类或者间接继承类的对象,调用不同的虚函数;

2.2 在C++中,多态性可以分为两类:编译时多态和运行时多态。


  1. 编译时多态

    01 函数重载(Function Overloading):在同一个作用域内,可以有一组具有相同名字、不同参数列表的函数。编译器根据函数参数的类型和数量来决定调用哪个函数。
    ​
    02 模板(Templates):模板允许编写与类型无关的代码。模板不是多态性的直接例子,但它允许编写在不同类型上表现出多态行为的代码。
  2. 运行时多态

    -虚函数(Virtual Functions)
        通过虚函数实现的多态性允许在运行时根据对象的实际类型来调用相应的成员函数。要实现运行时多态,需要使用类的继承和虚函数。
    -动态绑定(Dynamic Binding)
        当使用指针或引用调用虚函数时,将发生动态绑定。C++编译器会根据对象的实际类型决定调用哪个函数版本。
    -纯虚函数和抽象类:
        如果一个类至少有一个纯虚函数,则它被称为抽象类。抽象类不能实例化,但它可以用来定义接口和实现多态性。

多态性是面向对象编程(OOP)的核心概念之一,它允许以统一的接口处理不同类型的对象,从而提高代码的复用性和可维护性。在C++中,通过虚函数和继承机制实现运行时多态,而函数重载和模板则提供编译时多态。

例子
类和函数设计
class A{
protected:
    int a;
public:
    void virtual setInfo(int a) ;
    void virtual showInfo();
     A();
    ~A();
};
class B: virtual public A{   
protected:
    int a; 
public:
    void virtual setInfo(int a)override;
    void virtual showInfo()override;
     B();
    ~B();
};
class C:virtual public A{
private:
   int c;
public:
    void virtual setInfo(int a)override;
    void virtual showInfo()override;
     C();
    ~C();
};
class D:virtual public B,virtual public C{
    int d;
public:
    void virtual setInfo(int a)override;
    void virtual showInfo()override;
     D();
    ~D();
};
//函数设计
void A::setInfo(int a){
    this->a = a * 10;
}
void B::setInfo(int a){
    this->a = a * 100;
}
void C::setInfo(int a){
    this->c = a *1000;
    this->a = a *1000;
}   
void D::setInfo(int a){
    this->d = a *10000;
    this->a = a *10000;
}
void A::showInfo(){
    std::cout << "A::fun::showInfo" << std::endl;
    std::cout << "A::a Value : " << this->a << std::endl;
}
void B::showInfo(){
    std::cout << "B::fun::showInfo" << std::endl;
    std::cout << "B::a Value : " << this->a << std::endl;
}
void C::showInfo(){
    std::cout << "C::fun::showInfo" << std::endl;
    std::cout << "C::a Value : " << this->a << std::endl;
    std::cout << "C::c Value : " << this->c << std::endl;
}
void D::showInfo(){
    std::cout << "D::fun::showInfo" << std::endl;
    std::cout << "D::a Value : " << this->a << std::endl;
    std::cout << "D::d Value : " << this->d << std::endl;
}
A::A(){}
B::B(){}
C::C(){}
D::D(){}
A::~A(){}
B::~B(){}
C::~C(){}
D::~D(){}
主函数与程序运行效果
void __SETINFO(A &A,int a){
    A.setInfo(a);
}
void __SHOWINFO(A &A){
    A.showInfo();
}
int main(int argc, char const *argv[]){
​
    A a; B b; C c; D d;
   
    //让父类指针指向继承类,调用不同继承类重写之后的虚函数
    __SETINFO(a,10);    //a.setInfo(10)
    __SETINFO(b,20);    //b.setInfo(20)
    __SETINFO(c,30);    //c.setInfo(30)
    __SETINFO(d,40);    //d.setInfo(40)
    
    //让父类指针指向继承类,调用不同继承类重写之后的虚函数
    __SHOWINFO(a);  //a.showInfo();
    __SHOWINFO(b);  //b.showInfo();
    __SHOWINFO(c);  //c.showInfo();
    __SHOWINFO(d);  //d.showInfo();
​
    return 0;
}
​
s@Shuai:/mnt/e/MyCode/DT_Debug$ ./debug
//a.showInfo();
A::fun::showInfo
A::a Value : 100
//b.showInfo();
B::fun::showInfo
B::a Value : 2000
//c.showInfo();
C::fun::showInfo
C::a Value : 30000
C::c Value : 30000
//d.showInfo();    
D::fun::showInfo
D::a Value : 400000
D::d Value : 400000

2.3 动态多态的条件


在C++中实现动态多态(也称为运行时多态)通常需要满足以下条件:

  1. 继承:动态多态通常涉及到基类和派生类。派生类继承自基类,并且可以添加新的成员或重写(覆盖)基类的虚函数。

  2. 虚函数:基类中需要至少有一个虚函数。虚函数是通过在函数声明前加上 virtual 关键字来指定的。派生类可以重写这些虚函数以实现特定的行为。

  3. 指针或引用:要实现动态多态,通常需要使用基类的指针或引用来调用虚函数。这样,当通过基类指针或引用调用函数时,将根据对象的实际类型(派生类的类型)来确定调用哪个版本的函数。

  4. 对象切片:当派生类对象被赋值给基类对象时,会发生对象切片(slicing)。为了避免这种情况,应该使用指针或引用来保持对象的派生类类型。

  5. 虚析构函数:如果基类中有虚析构函数,确保在删除指向派生类的基类指针时,能够正确调用派生类的析构函数。这可以通过在基类中将析构函数声明为虚函数来实现。

  6. 动态内存分配:虽然不是强制条件,但动态多态通常与动态内存分配(例如,使用 new 关键字)结合使用,因为这样可以创建基类的指针,指向在堆上分配的派生类对象。

//01 继承
class D:virtual public B,virtual public C{
    int d;
public:
    void virtual setInfo(int a)override;
    void virtual showInfo()override;
     D();
    ~D();
};
​
//02 虚函数
void virtual setInfo(int a)override;
void virtual showInfo()override;
​
//3重写
void D::setInfo(int a){
    this->d = a *10000;
    this->a = a *10000;
}
void D::showInfo(){
    std::cout << "D::fun::showInfo" << std::endl;
    std::cout << "D::a Value : " << this->a << std::endl;
    std::cout << "D::d Value : " << this->d << std::endl;
}
/*指针或引用
    当派生类对象被赋值给基类对象时,应该使用指针或引用来保持对象的派生类类型。*/
void __SETINFO(A &A,int a){
    A.setInfo(a);
} 
void __SHOWINFO(A &A){
    A.showInfo();
}
//虚析构
    /*......后面讲*/
//动态内存分配
    /*......后面讲*/

2.4 动态多态的好处


动态多态是C++面向对象编程中的一个重要特性,它提供了一些关键的好处和作用:

  1. 代码的可扩展性和灵活性:动态多态允许在不修改现有代码的情况下增加新的功能。通过继承基类并添加新的派生类,可以轻松地扩展程序的行为,而无需修改使用基类对象的代码。

  2. 接口和实现的分离:动态多态使得可以将接口(基类)与实现(派生类)分离。这有助于降低代码的耦合度,使得代码更加模块化和易于维护。

  3. 统一的处理方式:动态多态允许使用相同的接口处理不同的对象类型。这意味着可以编写更通用的代码来处理多种类型的对象,而不必为每种对象编写特定的代码。

  4. 多态函数的重写:派生类可以重写基类中的虚函数,以提供特定的实现。这允许子类以不同的方式执行某些操作,从而实现特定的行为。

  5. 资源管理:动态多态有助于更有效地管理资源,尤其是在使用智能指针和资源管理类时。例如,通过虚析构函数,可以确保正确的析构顺序,避免资源泄漏。

  6. 设计模式的基础:动态多态是许多设计模式(如工厂模式、策略模式、访问者模式等)的基础。这些模式可以提高代码的可重用性、可维护性和灵活性。

  7. 事件驱动编程:在事件驱动编程中,动态多态允许不同的对象对相同的事件做出不同的反应。例如,在图形用户界面中,不同的按钮可能对相同的点击事件有不同的响应。

  8. 对象交互的抽象:动态多态允许对象之间的交互更加抽象,因为它们不需要知道彼此的具体类型。这有助于减少对象之间的依赖关系。

总之,动态多态是C++中实现面向对象设计的关键工具之一,它提供了代码的抽象、灵活性和可扩展性,使得程序更加健壮和易于管理。

抽象类

抽象类是面向对象编程中的一个概念,它代表了一系列具有相同特征和行为的对象的抽象。在许多编程语言中,抽象类被用来为子类提供一个共同的接口和部分实现。一个抽象类不能被实例化,它主要是用来作为其他类的超类。子类继承自抽象类,必须实现抽象类中所有的抽象方法,除非子类也是抽象类。

在C++中,抽象类是一种包含至少一个纯虚函数的类纯虚函数是一种没有实现的虚函数,它在基类中被声明时赋予初值0抽象类不能被实例化,它的主要目的是作为其他类的基类,提供接口和部分实现。

//对于实例化无意义的函数我们希望他是一个抽象类
class Thread{
    pthread_t th;
protected:
    bool  isExit;
 public:
        Thread();
        ~Thread();
        void  start();
        void  stop();
        //纯虚函数,当前类变成抽象类
        void virtual run() = 0;
};
int mian(){
     Thread T; //抽象类不能实例化
}
报错:main.cpp:7:12: error: variable type 'Thread' is an abstract class
Thread_debug.hpp:20:22: note: unimplemented pure virtual method 'run' in 'Thread'

封装线程类

void *task(void *arg);
//对于实例化无意义的函数我们希望他是一个抽象类
class Thread{
    pthread_t th;
protected:
    bool  isExit;
 public:
        Thread();
        ~Thread();
        void  start();
        void  stop();
        //纯虚函数,当前类变成抽象类
        void virtual run() = 0;
};
class TimeThread:public Thread{
public:
    TimeThread();
    void  virtual run() override;
};
//方法实现
#include "Thread_debug.hpp"
​
//需要重点关注的函数
void * task(void *arg){
    //使用虚函数机制,实现多态调用
    Thread* th = (Thread*)arg;
    //调用继承或者间接继承类的虚函数
    th->run();
}
​
Thread::Thread(){
   this->isExit = false;
}
​
Thread::~Thread(){}
​
void Thread::start(){
    pthread_create(&(this->th),NULL,task,this);
    pthread_detach(this->th);
}
void Thread::stop(){
    this->isExit = true;
}
#include "timethread.h"
​
TimeThread::TimeThread():Thread(){
​
}
​
void TimeThread::run(){
    int time = 0;
    this->isExit = false;
    while(1)
    {
        sleep(1);
        std::cout << "time: " << time++ << std::endl;
        if(this->isExit == true)
        {
            break;
        }
    }
}

主函数及效果

int main(){
    TimeThread T1;
    T1.start();
    
    ::sleep(10);
    
    T1.stop();
    
    while (1);
    return 0;
}
//效果
time: 0
time: 1
time: 2
time: 3
time: 4
time: 5
time: 6
time: 7
time: 8
time: 9 //当然此程序有bug,我只是提供一个思路

虚析构函数

在C++中,虚析构函数是一个在基类中被声明为虚的析构函数。当基类指针指向派生类的对象,并且通过这个指针删除对象时,虚析构函数确保执行正确的析构函数链。

如果一个类被设计为要被继承,并且可能通过基类指针被删除,那么这个类的析构函数应该声明为虚的。这样可以防止内存泄漏和资源泄漏,确保派生类的析构函数也被调用,从而正确地释放资源。

解决问题

当一个父类指针指向其继承类的空间时,释放该指针的空间时,只会调用父类的析构函数而不会调用子类的虚构函数,造成内存泄漏;

Thread::Thread(){
    std::cout <<" theread() start ! " << std::endl;
    this->isExit = false;
}
Thread::~Thread(){
    std::cout <<" ~theread() start ! " << std::endl;
}
TimeThread::TimeThread():Thread(){
    std::cout << "Time Thread() start !" << std::endl;
}
TimeThread::~TimeThread(){
       std::cout << "~Time Thread() start !" << std::endl;
}
//主函数与实现效果
int main(){
    Thread* T1  = new  TimeThread;
    delete  T1; //无法正确调用继承类的析构函数,内存泄漏
    return  0;
}
//效果
theread() start !
Time Thread() start !
~theread() start !
//没有执行TimeThread::~TimeThread()
解决方法
class Thread{
 public:
     virtual ~Thread(); //将析构函数声明为虚析构函数
     /*....*/
};
class TimeThread:public Thread{
public:
    ~TimeThread()override; //声明为重写的虚析构函数
    /*....*/
};
//运行效果
 theread() start !
Time Thread() start !
~Time Thread() start !
 ~theread() start !

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值