打印

面向对象中一些编程思想 - 继承与多态

[复制链接]
633|2
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
红蛋大叔|  楼主 | 2017-10-30 19:03 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
继承与多态

本来想把继承与多态分开来说,但是又觉得如果继承少了多态就很难体会到面向对象的编程核心所在 - 面向接口编程。

什么是继承
在已有的类的基础上建立一个新类,已有的类就是基类,新类就是派生类。基类派生子类,子类继承基类,子类除了有父类的功能以外还有自己的功能。一句话说就是子承父业,发扬光大。

/*c中基类的实现,可以理解为派生类的抽象*/
typedef struct _base_
{
   void (*fun)(void );
}base_t;

/*c中派生类的实现,除了继承基类结构体外还有自己的功能*/
typedef struct paisheng
{
   base_t  *p_base;  //继承了基类base_t  ,同时扩展出了自己的功能。
   int a;
   int b;
   xxxx
}paisheng_t;

我们知道了在c中如何定义基类以及派生类,那么如何在c中应用呢?下面来个具体的例子。

假设现在有个需求,使用MCU的uart1向模块A发送十六进制数据,使用uart2向模块B发送字符串数据。虽然面向的模块不一样,数据格式也不一样,但是要求必须使用公共的数据发送接口以向外发送数据。那么,我们该怎么实现呢?
使用两组uart发送两种不同类型的数据,从本质上来说,都是在发送数据。不同点在于两组串口的缓存、要发送数量、串口缓存的最大长度不一样。因此,这里我们可以使用继承的思路,把公共部分抽离出来做为基类,而非公共部分做为派生类,见代码。

unsigned char uart1_buf[50] ;
unsigned char uart2_buf[100] ;
unsigned char uart1_send_count = 0;
unsigned char uart2_send_count = 0;
//////////////////////////////////////////////////////////////////////
//以下是基类与派生类的类型定义
typedef struct  base  
{
   void (*tx_data)(struct  base  *p_this);
}base_t;

/*base_t 的派生类 , 用于uart1发送十六进制数据*/
typedef struct uart_tx_hex
{
   base_t                *p_base;
   unsigned char    *p_src_hex;
   unsigned int       *p_send_count;
   unsigned char     max_len;
}uart1_tx_hex_t;

/*base_t 的派生类 , 用于uart2发送字符串数据*/
typedef struct uart_tx_str
{
   base_t               *p_base;
   unsigned char    *p_src_str;
   unsigned int       *p_send_count;
   unsigned char     max_len;
}uart2_tx_str_t;

#define new_uart1_tx()  {uart1_tx_hex,&uart1_buf[0],&uart1_send_count,50}  //类似于c++中的构造函数完成对象的初始化工作
#define new_uart2_tx()  {uart2_tx_str,&uart2_buf[0],&uart2_send_count,100}

///////////////////////////////////////////////////////////////////
void uart1_tx_hex(base_t *p_this, unsigned char *buf, unsigned char len)
{
    uart1_tx_hex_t  *uart1_tx = (uart1_tx_hex_t *)p_this;  //强制转换为uart1_tx_hex_t,以便uart1_tx能引用该结构体内的数据

    if(uart1_tx == 0 || uart1_tx->p_src == 0)  return ;      
    if(len > uart1_tx->max_len)
    {
        len  = uart1_tx->len;
    }
    for(unsigned char i = 0 ; i < len; i++)
    {
        uart1_tx->p_src =  buf;
    }   

    *uart1_tx->p_send_count = i;  //要发送的数量
}

void uart2_tx_str(base_t *p_this, unsigned char *buf, unsigned char len)
{
    uart2_tx_str_t  *uart2_tx = (uart2_tx_str_t *)p_this;  //强制转换为uart1_tx_hex_t,以便uart1_tx能引用该结构体内的数据

    if(uart2_tx == 0 || uart2_tx->p_src == 0 )  return ;      
    if(len > uart2_tx->max_len)
    {
       len = uart2_tx->len;
    }

    for(unsigned char i = 0 ; i < len; i++)
    {
       uart2_tx->p_src = buf;
    }

        *uart2_tx->p_send_count = i;  //要发送的数量
}

void uart_tx_manage(void )
{
} //数据发送管理

//客户端调用
{
        unsigned char test_hex[10] = {0x01,0x02,0x03,0x05,0x06};
        unsigned char test_str = "test";

        uart1_tx_hex_t uart1 = new_uart1_tx();  //初始化uart1
        uart2_tx_str_t uart2 = new_uart2_tx();

        base_t *base = &uart1;
  (1) base->tx_data(base, test_hex, 6);  

        base_t *base = &uart2;
  (2) base->tx_data(base, test_str, strlen(test_str));  
}

我们来看看客户端代码
从客户端代码来看我们实现了使用相同发送接口发送不同类型的数据。也看到了继承在这个例子中的应用-把他们要做的共同事情给剥离抽象为基类结构体,把他们不同的地方做为派生类派生出两个子类结构体。对于客户端来说,他的调用是针对接口调用而不是直接调用实现(比如uart2_tx_str)。这样做的好处就在于除了隔离变化以外还可以灵活的扩展程序。比如我现在需要在uar1中扩展一组发送字符串类型的功能,我们只需要让该功能继承于uart1的基类,然后模仿uart1发送十六进制的方法来设计实现  ,这样做也符合了面向对象编程中二条重要的原则:
(1)对修改关闭,扩展开放。
(2)针对接口编程而非实现(细节)以应对不断变化的软件需求

注意看客户端代码的(1),(2)标识的代码段,调用相同的接口却做着不同的事这就是面向对象中一个很重要的概念-多态。下面的描述可能比较c++化,如果没有学过c++的话可以无视。
在c++中,实现多态需要借助于虚函数,而在c中使用结构体。在c++中如果派生类与基类同名,如果不在基类中指明该同名函数是虚函数,那么即使基类指针指向派生类也只能调用基类的同名函数而非派生类中的同名函数。而c中使用结构体就天然避免了这个问题,因为在派生类结构体继承于基类结构体即使名字相同的函数也是两个不一样的地址,不会产生冲突。

好了,该总结了。
对于长期使用c编程而面向对象编程不是很有经验的程序员来说既是学习过c++或者其他面向对象语言,甚至看过很多设计模式,却还是不容易使用c去应用c++中的封装,继承,多态等等这些思想与原则。我觉得知道继承的理念要比我们在c中如何去套用c++中的一些设计原则而显得更为重要。那么,继承的理念是什么?继承的理念是抽象出共同点做为客户端的调用接口,把不同点隔离,使代码更具有重用性的同时也能比较好的隔离变化。这句话可能还是有点抽象,再来个具体的例子解析一下。

IIC是MCU与外部设备通信的一个常用协议,有时候一个MCU需要用上多个硬件IIC以满足MCU与外部硬件通信。那么,从本质上来说,对于一个MCU上的所有硬件IIC它的数据发送,数据接收等操作都是一样的,不一样的是MCU上的那组IIC与哪个外部硬件通信。于是,我们可以设计一组复用接口以满足对IIC通信的需求。如下例子
hand = iic1_init();  //初始化iic1,并把iic1的句柄给hand,通过hand区分不同IIC。当我想用iic2时,我只需要hand = iic2_init(), 而读写调用可以不变。当你觉得2组iic不够用时,自己也还可以再定义一个iic3_init(),把iic3的句柄给hand,这样,对于iic的读写调用依然不变。
/*虽然硬件IIC不一样,但是数据的读写操作时一致的,所以有下面两个可重用的读写接口*/
iic_write_byte(hand, addr,0x01);
iic_read_byte(hand ,addr,&test);
当然,这个例子是不一定能调的通的,这里只是借助iic的例子来说明一下继承思想。

最后要注意的是,抽象一个缺点就是导致程序的可读性下降,毕竟一个项目的维护时间远远多于研发时间。所以抽象与重用性之间哪个更为重要则需要根据具体项目来定夺了。

后面再写几篇我们项目中可能会用上的设计模式
(1)适配器模式 - 隔离硬件
(2)中介者模式 - 集中操作
(3)观察者模式 - MVC架构

如果你们对面向接口编程有兴趣,给你们推荐几本书,具体要自己去买或者下载了。
《程序员成长计划》 - 李先静
《程序设计与数据结构》 - 周立功
《面向Ametal框架与接口编程(上)》- 周立功

多多交流!











相关帖子

沙发
红蛋大叔|  楼主 | 2017-10-30 19:04 | 只看该作者

使用特权

评论回复
板凳
舍恩| | 2017-10-31 08:27 | 只看该作者
学习学习!

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

25

主题

69

帖子

3

粉丝