C语言--面向对象编程之继承

系列文章目录

C语言实现面向对象编程的第二篇,在第一篇里面我们已经了解到了何为面向对象以及实现面向对象的第一大特性–封装,对于C来说,实现继承和多态要稍微麻烦一些。


C语言实现面向对象- - 封装
C语言–面向对象编程之多态


前言

这一篇将继续探讨用C实现面向对象编程之继承,用C来实现继承还是比较麻烦的,但是也能够实现继承,用C实现继承最好不要超过两级,不然实现继承与调用很繁琐。


一、面向对象之继承

何为继承?

继承就是子类继承基类,子类拥有基类的属性和行为,而且子类也拥有自己的属性和行为。

举个例子:洛克王国宠物进化,进化和类继承是类似的。

首先有用一个基本的宠物,定义它为一个基类,姑且称为小米吧,小米拥有属性和技能。属性为姓名,血量,技能1为:老鼠小米,造成5点魔法伤害。然后小米不断打怪升级,升到10级,变为大米,大米拥有小米的原来的属性和行为,也就是大米也有姓名,血量,也有技能1:老鼠爱小米。进化之后变为大米后,拥有了新的属性,蓝量,也拥有了新的行为,技能2:老鼠爱大米会消耗蓝量。

用C语言定义就是下面这样:

/*函数行为表,C++中称为虚表*/
struct Base_skill
{
    
    
    int (*illet_skill)(Base_Pets * const Me);
};

/*小米属性*/
typedef struct 
{
    
    
    char name[10];
    int Heal_value;
	/*函数表指针,C++中称为虚指针,指向函数行为表*/
    struct Base_skill const *vptr;
}Base_Pets;

/*大米属性*/
struct Child_skill
{
    
    
    int (*rice_skill)(Child_Pets* const Me);
};

typedef struct 
{
    
    
	/*继承基类属性*/
    Base_Pets base_pet;
	/*自己的属性*/
	int Magic_value;
    struct Child_skill const *vptr;
}Child_Pets;

/*函数声明*/
int Rich_skill(Child_Pets* const Me);
int Illet_skill(Base_Pets * const Me);

看了代码,是比较明了知道继承是怎么回事了,那让我们进一步讨论内存是怎么分配的吧,顺便了解一下C++中继承的实现机制。

一个简单的Demo

#include "stdio.h"
#include <string.h>
#include <stdlib.h>

struct Base_skill_vtabl;
struct Child_skill_vtabl;

/*小米属性*/
typedef struct 
{
    
    
    char name[10];
    int Heal_value;
	/*函数表指针,C++如果使用的virtual关键词,虚化了称为虚指针,指向函数行为表*/
    struct Base_skill_vtabl const *vptr;
}Base_Pets;

/*函数行为表,C++如果使用的virtual关键词,称为虚表*/
struct Base_skill_vtabl
{
    
    
    int (*illet_skill)(Base_Pets * const Me);
};


/*大米属性*/

typedef struct 
{
    
    
	/*继承基类属性*/
    Base_Pets *base_pet;
	/*自己的属性*/
	int Magic_value;
    struct Child_skill_vtabl const *vptr;
}Child_Pets;

struct Child_skill_vtabl
{
    
    
    int (*rice_skill)(Child_Pets* const Me);
};


int Rich_skill(Child_Pets* const Me);
int Illet_skill(Base_Pets * const Me);

//构建基类
Base_Pets * Ctor_base_Pets(void)
{
    
       
    Base_Pets *ptr;

    struct Base_skill_vtabl *table;
    /*分配空间*/
    ptr=(Base_Pets *)malloc(sizeof(Base_Pets));

    table=(struct Base_skill_vtabl *)malloc(sizeof(struct Base_skill_vtabl));    
    //清零
    memset(ptr,0,sizeof(struct Base_skill_vtabl));
    
    ptr->vptr=table;

    table->illet_skill=&Illet_skill;
    
    printf("create Base_Pets class\n"); 
    return ptr;

}
//删除对象/析构对象
void Del_Base_Pets(Base_Pets * const Me)
{
    
    

    printf("Destroy Base_Pets\n");
    if(Me!=NULL) 
    {
    
    
        free((void *)Me->vptr);
        free(Me);
    }
}

//继承基类对象
//构建基类
Child_Pets *Ctor_Child_Pets(void)
{
    
      
 
    Child_Pets *ptr;

    struct Child_skill_vtabl *table;
    /*分配空间*/
    ptr=(Child_Pets *)malloc(sizeof(Child_Pets));
    table=(struct Child_skill_vtabl *)malloc(sizeof(struct Child_skill_vtabl)); 

    //清零
    memset(ptr,0,sizeof(struct Child_skill_vtabl));
    
    ptr->base_pet=Ctor_base_Pets();
  
    ptr->vptr=table;

    table->rice_skill=&Rich_skill;
    printf("create Child_Pets class\n");   
    return ptr;
}
//删除对象/析构对象
void Del_Child_Pets(Child_Pets * const Me)
{
    
    

    printf("Destroy Child_Pets\n");

    if(Me!=NULL) 
    {
    
    
        Del_Base_Pets(Me->base_pet);
        free((void *)Me->vptr);
        free(Me);
    }
}

int Rich_skill(Child_Pets* const Me)
{
    
    
    printf("use the rich skill\n");
    return 1;
}

int Illet_skill(Base_Pets * const Me)
{
    
    
    printf("use the illet skill\n");
    return 1;
}


int main()
{
    
    
    //创建基类指针
    Child_Pets *test;
    //创建基类指针
    test=Ctor_Child_Pets();
    
    test->base_pet->vptr->illet_skill(test->base_pet);
    test->vptr->rice_skill(test);
    
    Del_Child_Pets(test);
    system("pause");

}

运行结果:

create Base_Pets class
create Child_Pets class
use the illet skill
use the rich skill
Destroy Child_Pets
Destroy Base_Pets

由于使用的是堆的方式分配内存(malloc),需要自己分配内存,基类包含子类的指针是比较合适的。

内存解析

内存解析依旧使用我们的小米和大米的demo作为例子,探讨程序运行中是怎么分配的,看下图。
在这里插入图片描述
在继承中,子类函数指针包含了基类的指针,可以通过指针访问基类函数。在main中创建了子类指针,通过调用创建子类的函数,在堆区中分类了基类和子类的属性内存,创建的时候是先分配了子类的内存,子类中由于拥有了基类的指针,就可调用创建基类函数,创建基类的对象。

这样子类就拥有了自己的属性和行为以及基类的属性和行为,函数表和属性都分配在堆中。实际上子类所占的空间==基类的空间(属性和行为)+子类的空间(属性和行为)。这些都是分配在堆区。

二、C++中继承内存分配

简要介绍C++中内存分配

demo代码

// NOTE: compile with g++ filename.cpp -std=c++11
#include <iostream>
#include <cmath>
#include <cstdlib>
using namespace std;
class Base_Pets
{
    
    
  public:

  Base_Pets()
  {
    
    
    cout<<"Create Base_pets"<<endl;
  }

  ~Base_Pets()
  {
    
    
    cout<<"Del Base_pets"<<endl;
  }  
  //行为
  int Illet_skill(void)
  {
    
    
    cout<<"use the illet skill"<<endl;
    return 1;
  }
  //属性
  char name[10];
  int Heal_value;
};

class Child_Pets:public Base_Pets
{
    
    
  public:
  //行为
  int Rich_skill(void)
  {
    
    
    cout<<"use the rich skill"<<endl;
        return 1;
  }
  Child_Pets()
  {
    
    
    cout<<"Create Child_Pets"<<endl;   
  }

  ~Child_Pets()
  {
    
    
    cout<<"del Child_Pets"<<endl;   
  } 
  //属性
	int Magic_value;
};

void test()
{
    
    
  Child_Pets test;
  test.Rich_skill();
  test.Illet_skill();

}

int main()
{
    
    
  test();
  system("pause");
}

运行结果:

Create Base_pets
Create Child_Pets
use the rich skill
use the illet skill
del Child_Pets
Del Base_pets

简要分析

在C++中,内存分配是先分配是由编译器完成的,先分配基类的内存,后分配子类内存,也就是调用析构函数进行内存分配,我们也在其中使用malloc将一些属性和行为分配到堆中。

释放是C++编译器主动调用解析函数释放C++中的内存,是编译器来完成的,变量声明周期内完成。

创建和销毁的顺序是相反的,这里与我们写的C中面向对象的方式一致,不同的是,内存分配是我们先分配子类,后分配基类,但赋值操作是一样的,先基类赋值。销毁的顺序和我们创建对象的方式也是一致的。

三、总结

自己用C使用堆分配内存的方式与C++中创建对象的方式还是有一些区别的。

区别:

  • C实现继承更为繁琐,使用堆方式容易导致内存泄漏
  • C中的继承一般是继承父类中的所有属性与行为,而且都是可以访问的,不够安全
  • C中创建对象与继承对象,以及销毁对象都是需要显示调用的,C++创建对象由编译器隐式调用
  • C中通过函数指针访问子类对象比较麻烦,C++可以直接访问

猜你喜欢

转载自blog.csdn.net/weixin_46185705/article/details/123737979