面向对象——OOPC

面向对象编程

面向对象的三大特性

封装

  • 所谓封装就是把一组关联的数据和函数圈起来,外部只能看见部分函数,数据则完全看不见

将类型定义放到头文件,一定程序上破坏类的"封装性",换来的是内存的灵活分配

这是OOPC一般的做法。

出于封装的考虑,任何时候都不能直接访问对象中的数据——OOPC的必须严格的一条准则

  • 设计类时提供用户可能访问的数据的相应的访问接口
  • 使用别人的类时,除非特殊说明,都不能直接访问类中的数据
#ifndef _STACK_H_
#define _STACK_H_

#define STACK_IS_EMPTY   -1

typedef struct _stack{
     
    int *stack_buf;
    int size;
    int top;
}stack_t;

int stack_init( stack_t *stack, int *stack_buffer, int size);
int stack_pop(stack_t *stack, int *data);
int stack_push(stack_t *stack,int data);

#endif

对象

万物皆可看作对象,数据与函数的组合,每个对象由一组数据(描述对象的状态)和一组函数(描述对象的行为)组成

对象是类是实例,类是对对象的抽象

一组具有相同特性(数据元素)和行为(功能)的对象,类是对一组对象共性的抽象

属性

  • 对数据(对象状态)的抽象

方法

  • 对对象行为的抽象

对象初始化(相当于C++里面的构造函数)

内存来源有三种:

  • 动态内存

  • 静态内存

  • 栈内存

    对应的对象也有三种:

面向对象——OOPC_第1张图片

三种对象的应用场合

面向对象——OOPC_第2张图片

  • 建议使用静态对象,动态对象要考虑创建失败的情况

对象初始化代码:

/************************************************************************
  * @brief:    
  * @param[in]:     None
  * @retval[out]:   None
  * @note:            
  * @author:        guangjieMVP
**************************************************************************/
int stack_init( stack_t *s, int *stack_buffer, int size )
{
     
    if(!s || !stack_buffer)
        return -1;

    s->stack_buf = stack_buffer;
    s->size      = size;
    s->top       = -1;
    return 0;
}

解初始化对象(类似C++中的析构函数)

  • 动态对象

    若对象内存来自动态分配,在应使用相应的释放内存操作(free)。

  • 静态对象

    • 局部变量

      内存开辟在系统栈中,退出当前作用于系统自动释放

    • 静态变量或全局变量

      整个程序声明周期有效,无法释放。

解初始化对象代码

int stack_deinit( stack_t *s)
{
     
    if(!s)
        return -1;

    memset(s->stack_buf, 0, s->size);
    s->size      = 0;
    s->top       = 0;
    return 0;
}

嵌入式系统中会遇到一些与硬件相关的对象,初始化往往占用一定硬件资源:I/O、系统终端、系统总线,在解初始化时,应该同时释放这些资源。

实际应用中,只需关注对象的初始化函数,查看是否分配、占用了哪些资源。若有,则在解初始化时释放操作。若无解初始化函数留空。

销毁对象的顺序

若是动态对象,销毁对象时先解初始化对象,再销毁内存,否则内存销毁了对象就不复存在。

若是静态对象,内存分配和释放完全由系统自行完成。

初始化函数和解初始化函数通常不视为类提供的一般方法

继承

苹果A(子类)水果B(父类)是一种继承关系,苹果A可以是水果B,但是水果B不一定是苹果A,苹果是水果B的一个特例,水果B苹果A的一般化。B(水果)又称为基类超类

  • 继承的优点

    可以重复使用父类中已经实现的属性和方法,无需重复编程和设计,最使代码大限度的重用

  • 如何继承

    将父类作为子类第一个程序实现继承。这样做第一个成员(父类)地址和结构体自身(子类)地址相同。子类复用父类时,子类地址也可以作为父类地址使用。
    
  • 继承的类型

    • 单继承

      只继承一个父类

    • 多继承

      继承多个父类

    为避免二义性,不建议多重继承。
    

命名栈继承示例

  • 头文件
/***********************************************************************
 * @file:     xxx.c
 * @author:   guangjieMVP
 * @github:   https://github.com/guangjieMVP
 * @version:  v1.0.0
 * @date:     2020-xx-xx
 * @brief:               
*************************************************************************/
#ifndef _STACK_NAME_H_
#define _STACK_NAME_H_

#include "stack.h"

typedef struct _stack_name
{
     
    stack_t super;
    char *stack_name;
}stack_name_t;

int stack_named_init(stack_name_t *s, int *s_buffer, int size, char *s_name);
int stack_named_deinit(stack_name_t *s);
int stack_named_push(stack_name_t *s, int val);      //入栈方法
int stack_named_pop(stack_name_t *s, int *val);      //出栈方法
int stack_named_set(stack_name_t *s, char *s_name);  //设置命名栈名字方法
const char *stack_name_get(stack_name_t *s);         //获取命名栈的名字方法

#endif
  • 源文件

    • 初始化命名栈

      先初始化基类成员,再初始化派生类成员,这和面向对象编程语言调用构造函数的顺序一致:

      先调用基类的构造函数,再调用派生类的构造函数初始化派生类。

    #include "stack_name.h"
    
    
    int stack_named_init(stack_name_t *s, int *s_buffer, int size, char *s_name)
    {
           
        if(!s || !s_buffer || !s_name)
            return -1;
    
        stack_init(&s->super, s_buffer, size);        // 初始化基类
        s->stack_name = s_name;
    
        return 0;
    }
    
    • 入栈

      int stack_named_push(stack_name_t *s, int val)
      {
               
          if(!s)
              return -1;
      
          return stack_push(&s->super, val);   //命名栈(子类)入栈函数复用普通栈(父类)入栈函数            
      }
      
      • 入栈
      int stack_named_pop(stack_name_t *s, int *val)
      {
               
          if(!s || !val)
              return -1;
          
          return stack_pop(&s->super, val);        //命名栈(子类)入栈函数复用普通栈(父类)出栈函数          
      }c
      
      • 派生类的特有方法
    
    int stack_named_set(stack_name_t *s, char *s_name)
    {
           
        if(!s || !s_name)
            return -1;
    
        s->stack_name = s_name;
        return 0;
    }
    
    const char *stack_name_get(stack_name_t *s)
    {
           
        if(!s)
            return -1;
    
        return s->stack_name;
    }
    
    • 派生类析构函数

      解初始化过程中应先解初始化派生类成员,再解初始化基类成员

      
      int stack_named_deinit(stack_name_t *s)
      {
               
          if(!s)
              return -1;
      	s->stack_name = NULL;                          //先解初始化派生类成员
          return  stack_deinit(&s->super);               //解初始化超类(基类)
      }
      

    命名栈和普通栈继承关系的UML图表示

面向对象——OOPC_第3张图片

最少知识原则

类使用者不需要知道类内部具体细节,只需要使用类提供的公有方法

基础这个原则,派生类也要提供出入栈的方法

int stack_named_push(stack_name_t *s, int val)
{
     
    if(!s)
        return -1;

    return stack_push(&s->super, val);   //命名栈(子类)入栈函数复用普通栈(父类)入栈函数            
}

int stack_named_pop(stack_name_t *s, int *val)
{
     
    if(!s || !val)
        return -1;
    
    return stack_pop(&s->super, val);        //命名栈(子类)入栈函数复用普通栈(父类)出栈函数          
}

多态

  • 多态是面向对象编程非常重要的特性

  • **多态的核心:**对于上层调用者,不同的对象可以使用完全相同的操作方法

  • OOPC依赖函数指针实现多态

函数指针同放在结构体类型定义中,但函数指针不能视为普通属性,应当把它当做方法。
在OOP中,类中的函数指针称为抽象方法(定义类时没有实现,故称曰"抽象")
往往将函数指针封装到为一个特殊类(抽象类,抽象类里都是未实现的抽象方法)

在普通栈基础上实现一个带检查功能的栈即入栈钱必须进行特性的检查,检查通过才能入栈。入栈方式多种,假定暂时有如下三种:

  • 范围检查 :必须在特定范围之内才能通过
  • 奇偶检查:必须是奇数(或者偶数),才能检查通过
  • 变化检查:值必须是增加或者减少才能通过

(1)继承实现"带范围检查功能的"栈

  • 头文件

    
    
    #ifndef _STACK_WITH_RANGE_WITH_CHECK_H_
    #define _STACK_WITH_RANGE_WITH_CHECK_H_
    
    #include "stack.h"
    
    typedef struct _stack_range_check
    {
           
        stack_t super;
        int max;
        int min;
    }stack_rang_check_t;
    
    int stack_range_check_init(stack_rang_check_t *s, int *s_buffer, int size, int max, int min);
    int stack_range_check_deinit(stack_rang_check_t *s);
    int stack_range_check_push(stack_rang_check_t *s, int val);
    int stack_range_check_pop(stack_rang_check_t *s, int *val);
    
    
    #endif
    
  • 源文件


#include "stack_with_range_check.h"


int stack_range_check_init(stack_rang_check_t *s, int *s_buffer, int size, int max, int min)
{
     
    if(!s || !s_buffer || size <= 0)
        return -1;

    stack_init(&s->super, s_buffer, size);
    s->max = max;
    s->min = min;
    return 0;
}


int stack_range_check_deinit(stack_rang_check_t *s)
{
     
    s->max = 0;
    s->min = 0;
    return stack_deinit(&s->super);    
}


int stack_range_check_push(stack_rang_check_t *s, int val)
{
     
    if(!s)
        return -1;

    if(val >= s->min && val < s->max)
    {
     
        return stack_push(&s->super, val);     //复用父类的入栈函数
    }
    return -1;
}


int stack_range_check_pop(stack_rang_check_t *s, int *val)
{
     
    if(!s || !val)
        return -1;
     
    return stack_pop(&s->super, val);      //复用父类的出栈函数
}
这种继承的方式实现由很严重的缺点,当检查的方式有好几种,那就得使用继承的方式实现好几个类以及相应的方法。

(3)多态实现通用的"带检查功能的栈"

不同的检查方式就实现不同的check抽象方法。

  • 头文件

#ifndef _STACK_WITH_WITH_CHECK_H_
#define _STACK_WITH_WITH_CHECK_H_

#include "stack.h"

typedef struct _stack_check
{
     
    stack_t super;                                  //父类
    int (*check)(struct _stack_check *s, int vol);   //抽象方法
}stack_check_t;

int stack_check_init(stack_check_t *s, int *s_buffer, int size, int (*check)(struct _stack_check *s,int vol));
int stack_check_deinit(stack_check_t *s);
int stack_check_push(stack_check_t *s, int val);
int stack_check_pop(stack_check_t *s, int *val);


#endif
  • 源文件

    
    #include "stack_with_check.h"
    #include "string.h"
    
    
    int stack_check_init(stack_check_t *s, int *s_buffer, int size, int (*check)(struct _stack_check *s,int vol))
    {
           
        if(!s || !s_buffer || size <= 0 || !check)     //参数检查
            return -1;
    
        stack_init(&s->super, s_buffer, size);         //先初始化父类成员
        s->check = check;                              //再初始化派生类成员
        return 0;
    }
    
    
    int stack_check_deinit(stack_check_t *s)
    {
           
        if(!s)
            return -1;
    
        s->check = NULL;                    //先解初始化派生类成员
        return stack_deinit(&s->super);     //再解初始化父类成员
    }
    
    
    int stack_check_push(stack_check_t *s, int val)
    {
           
        if(!s)
            return -1;
    
        if( s->check == NULL || s->check(s, val) )       //当没有检查方式时为普通栈或者入栈的值通过检查方式
        {
           
            return stack_push(&s->super, val);           //复用父类的入栈函数
        }
            
        return -1;
    }
    
    
    int stack_check_pop(stack_check_t *s, int *val)
    {
           
        if(!s || !val)
            return -1;
         
        return stack_pop(&s->super, val);      //复用父类的出栈函数
    }
    

面向对象——OOPC_第4张图片

UML图中抽象类名和抽象方法使用斜体表示

OOP中,包含抽象方法(函数指针)的类称为抽象类抽象类不能直接实例化(还有方法没有实现)。抽象类只能被继承,在子类中实现抽象类的抽象方法。

上面使用多态实现带检查功能的栈,检查功能作为栈一种扩展,检查逻辑直接在相应的派生类中实现,检查功能与栈绑定在一起,检查功能无法独立复用。

若要实现带检查功能的队列,希望能复用上面的检查逻辑的代码

struct validator{
     
	struct stack super;
    int (*validate)(struct validator *s, int val);
}

基类super来自普通栈,validate表示抽象的数据检查方法。

检查方法是该类的一个方法(抽象方法),检查逻辑与栈绑定,无法复用检查逻辑代码
分离检查逻辑
//抽象接口
struct validator{
     
    int (*validate)(struct validator *s, int val);
}

//带检查的栈
struct stack_with_validator{
     
    struct stack super;
    struct validator;
}

抽象方法使用斜体表示**

OOP中,包含抽象方法(函数指针)的类称为抽象类抽象类不能直接实例化(还有方法没有实现)。抽象类只能被继承,在子类中实现抽象类的抽象方法。

上面使用多态实现带检查功能的栈,检查功能作为栈一种扩展,检查逻辑直接在相应的派生类中实现,检查功能与栈绑定在一起,检查功能无法独立复用。

若要实现带检查功能的队列,希望能复用上面的检查逻辑的代码

struct validator{
     
	struct stack super;
    int (*validate)(struct validator *s, int val);
}

基类super来自普通栈,validate表示抽象的数据检查方法。

检查方法是该类的一个方法(抽象方法),检查逻辑与栈绑定,无法复用检查逻辑代码
分离检查逻辑
//抽象接口
struct validator{
     
    int (*validate)(struct validator *s, int val);
}

//带检查的栈
struct stack_with_validator{
     
    struct stack super;
    struct validator;
}

你可能感兴趣的:(C语言)