目录
  • 前言
  • 一、面向对象基本概念
    • 什么是对象?
    • 对象与类
    • 面向对象的编程方式
  • 二、C语言实现面向对象
    • 面向对象的三大特征
    • 面向对象之封装
      • 简介
      • 代码实现–基础版
      • 代码实现-进阶版
  • 总结

    前言

    面向对象是一种思维方式,基本上用什么语言都是可以实现的。C语言的编程方式一般是面向过程的,但是也是可以实现面向对象的。对象是什么?什么又是面向对象?面向对象的三大特性又怎么实现,且听我细细道来。

    一、面向对象基本概念

    什么是对象?

    此对象非彼对象,虽然有时候此对象又可以是你脑袋中的对象,那让我们从我们误解的对象开始了解吧,虽然我没有,但是用一下自己的直男思维,想想一个对象也是可以滴。那我就进入一下我这个直男脑袋中的对象吧!我有一个对象,这个对象呢,肤白貌美大长腿。用诗中的话就是“皓腕凝霜雪,垆边人似月”,美丽的江南女子,谁不喜欢呢。既然是想象,对象不只是仅仅是肤白貌美大长腿,还得爱你,在你寂寞的时候能陪你,在你难过时能在你身边,在你打游戏的时候不会无理取闹。这样的对象多好哇,简直就是梦中情人,可惜只存在想想中(叹气)。

    通过上面的例子,想象中的对象,它具有了对象的特征,是不算非常符合人类的特性,易懂。那让我们从上面的例子提取出来对象的普遍特征。

    特征一:属性

    让我们回到我想象中的对象,对象是怎样的,皓腕凝霜雪,垆边人似月,这个是对象的属性,也就是样子。当然属性不只是样子啦,你可以添加更多的属性,比如声音好听,年龄20岁等等。

    特征二:行为

    对象具有的动作就是行为。在上面的例子就是,对象非常爱你,难过的时候能陪你,寂寞的时候也能陪你等等,就是这个对象具有的动作,对象能干什么。

    对象与类

    我们知道了对象是什么,但是你有没有发现这个对象是很广泛的,也就是我想象中的对象不知有一个,符合我想象中对象的特征可以多个,也就是我可以想象又很多个对象。我可以想象有”后宫佳丽三千人“,这三个都符合我对象的特征。这些特征就是类,也就是符合我想象的人不只是只有一个,可以有多个,只要这个人符合我想象的特征,她就是在这个类下面的。

    那么类与对象的关系又是怎样的?对象就是符合这个类的特例,怎么理解呢?

    在我的想象中,符合大美女的属性有很多,但是我不知道具体是谁,但是有一天我在动漫中看见了这个人,她叫小A。小A就是在大美女类下面的一个对象。又有一天,我又碰到一个人,也符合我定义的大美女,这个对象,她叫小B。小A和小B都是符合定义的,也就是在这个类下面的,而小A和小B是一个特例,也就是这个类下面的人,是独一无二的。

    面向对象的编程方式

    老生常谈,先简单看看面向过程的编程方式是怎样的?设想一个场景,刺激点的。有一天杰哥想你了,打算和你回家一起打电动,那他该怎么做才能邀请到你回家一起打电动?

    面向过程的解决方式:简单点的方式简化一下

    1:他首先西装革履,打扮的人模狗样,看起来十分帅气,小姐姐看了表示很赞

    2:打车到你家

    3:盛情邀约

    面向对象的解决方式又是怎样的呢:我们看他邀请你涉及几个对象,打车对象,邀请对象,打电动对象,回家对象。那对象是怎么做的呢?那让我们看一下执行顺序:杰哥首先调用了打扮的对象进行了打扮,然后调用打车对象去了你家。到了你家后调用了邀请对象的行为发出了邀请,然后你调用了邀请对象的行为拒绝了杰哥,杰哥调用了情绪的对象的行为,发出了很难过的感觉。

    打扮对象:

    • 行为:打扮

    打车对象:

    • 属性:打车人
    • 属性:打车地点
    • 行为:上车

    邀请对象:

    • 属性:邀请人
    • 属性:邀请结果
    • 行为1:接受
    • 行为2:拒绝
    • 行为3:发出邀请

    回家对象:

    • 属性:回家的路
    • 属性:回家的时间
    • 行为:回家

    情绪:

    • 属性:程度
    • 行为1:伤心
    • 行为2:难过
    • 行为3:非常难过

    通过上面的例子,大概了解到了与面向过程的区别了,面向对象的编程方式的单元是对象,做了什么事情也是以对象执行动作。对象可以被很多对象调用,杰哥可以调用邀请对象中的邀请行为,你也可以调用邀请对象发出拒绝邀请的动作。对象的属性是怎样的,怎样定义是灵活的。

    看到上面的例子:面向对象的编程方式=面向过程+面向对象。对象将某一些行为高度封装,然后由指挥官也就是我们自己按照自己的想法按照某个顺序调用(面向过程),在过程中,对象之间会进行一定的数据交互与一定的对象之间的行为调用。

    再举个例子:实现一个循迹小车

    构建对象:传感器 控制器 小车

    小车对象:

    • 属性:当前偏移值
    • 行为:前进 ,后退 ,左转 ,右转

    控制器:

    • 属性:输入值,输出值
    • 行为:计算控制值

    传感器:

    • 属性:传感器测量值 传感器数量 传感器
    • 行为:测量
    //伪代码
    void Follow_mark(void)
    {
    	调用传感器对象进行测量,将测量值保存到器测量值
    	调用控制器对象,将传感器测量值作为输入参数,计算得到结果进行保存
    	调用小车对象,根据控制传入的控制值,计算当前偏移量,然后根据偏移量调用左转/右转行为
    }
    

    二、C语言实现面向对象

    面向对象的三大特征

    了解了面向对象的思想,思想是最重要的,特征是次要的。面向对象具有三大特征,我们或多或少都可以实现,java,python,C++都有,但是C也是可以实现的,只是会比较麻烦,三大特征分别是封装,继承,多态。这三大特征能够帮助实现面向对象的编程,使得面向对象变得更优雅。我们先了解三大特征之三大特征之封装。

    面向对象之封装

    简介

    封装就是将对象的特征进行封装,使之对象的属性和行为只能通过对象进行访问。在上面的例子中,邀请的对象,它的属性与行为是被封装好的,我们只能调用邀请这个对象才能调用邀请对象的行为。

    优势:

    1、隐藏内部细节,类似函数,只需要调用这个邀请对象的行为发出邀请,而不需要知道里面的底层实现

    2、更安全,复用性更好。对象的值都是被封装好的,隐藏掉的,一般是程序员只会提供相应的接口来访问,不能直接修改。复用性,从上面的例子,谁都可以调用对象的行为。

    代码实现–基础版

    基础版不涉及函数指针与函数表,先学习这个基础版的,理解好面向对象的最简单的封装的实现。

    在实现前我们先想一想C到底有什么结构可以实现封装属性,各种属性。这个很简单,结构体嘛,能放各种类型的属性。

    行为又怎么体现呢,可以实现各种行为,函数嘛。后面的多态会涉及函数指针,使用函数指针可以实现多态,这都是后面的事情,后面的文章会有简介。

    那让我们做一个PID控制器的对象吧,如果不懂的小伙伴也没关系,这个只是控制器,有输入,输出,调试参数,了解这些就行了。具体实现过程,内部细节不懂也没关系,这个不重要,我代码会标出来的。

    那我们直接阅读代码,进入困难模式:代码会有比较详细的注释,很容易看懂!

    //开始构造对象,既然是控制器,对象必须具有输入,输出,调试参数
    //属性就是:参数值,输入值,输出值
    //行为就是:设置参数,查看参数,根据输入计算输出,构造对象,删除对象
    //属性:用结构体实现
    #include "stdio.h"
    #include <string.h>
    #include <stdlib.h>
    //控制器对象
    //控制器对象属性
    typedef struct 
    {
        int input;/*控制器输入*/
        int ouput;/*控制器输出*/
        int P_parameter,I_parameter,D_parameter;/*控制调试参数*/
        int Sum_error;/*总偏差,位置式PID积分相关的参数*/
        int Last_error;/*上次偏差,位置式PID积分相关的参数*/
    }controller;
    //构造对象,初始化
    controller *Ctor_controller(void)
    {  
        controller *temp;
        temp=(controller *)malloc(sizeof(controller));
        //清零
        memset(temp,0,sizeof(controller));
        return temp;
    }
    //删除对象
    void Del_ontroller(controller * const Me)
    {
        if(Me!=NULL) free(Me);
    }
    //设置控制器参数
    void Write_controller(controller * const Me,int P,int I,int D)
    {
        Me->P_parameter=P;
        Me->I_parameter=I;
        Me->D_parameter=D;
    }
    //读取控制器参数的值
    controller Read_controller(const controller * const Me,int P,int I,int D)
    {
        return (*Me);
    }
    //计算控制器输出,细节看不懂没关系,只需要知道传入的是偏差,就会有输出一个计算结果就行,这个结果能够帮助控制
    //至于偏差怎么定义什么时候需要用到PID控制器就知道了
    int Out_controller(controller * const Me,int input)
    {
        float	iIncpid=0;
        int now_error=input;//当前偏差
    	Me->Sum_error+=input; 
        //积分量限幅,方式积分饱和过深
    	if(Me->Sum_error >500)
    	{
    		Me->Sum_error = 500 ;
    	}
    	if(Me->Sum_error < -500)
    	{
    		Me->Sum_error = -500 ;
    	}
        Me->ouput=Me->P_parameter * input                  // P
             +Me->I_parameter * Me->Sum_error                // I
             +Me->D_parameter * (now_error-Me->Last_error); // D
        Me->Last_error=now_error;					// 存储误差,用于下次计算		
        return(Me->ouput);           // 返回计算值    
    }
    int main()
    {
        controller *test;
        controller read_val;
        //构造,创建一个对象
        test=Ctor_controller();
        //设置对象的值
        Write_controller(test,1,1,1);
        //查看对象的值
        read_val=Read_controller(test,1,1,1);
        printf("对象 P= %d I=%d D=%d \r\n",read_val.P_parameter,read_val.I_parameter,read_val.D_parameter);
        //调用控制器一次:
        printf("控制器输出=%d \r\n",Out_controller(test,100));
        //删除/销毁一个对象
        Del_ontroller(test);
    }
    

    输出结果:

    对象 P= 1 I=1 D=1 
    控制器输出=300 

    从上面的例子可以看出来,我直接调用对象,就可以实现封装,设置,查看等,注意使用了需要手动调用删除,不然容易出现内存泄漏,对象的生存时间就是我们程序员自己释放前的时间。

    这里是使用堆的方式,容易出现内存溢出的情况,如果是单片机等其他资源较小的单元,可以使用其他方式构造对象,比如下面:对象的生存时间就是主函数的结束时间,编译器替我们释放了对象的资源,不需要我们主动进行释放。

    #include "stdio.h"
    #include <string.h>
    #include <stdlib.h>
    //控制器对象
    //控制器对象属性
    typedef struct 
    {
        int input;/*控制器输入*/
        int ouput;/*控制器输出*/
        int P_parameter,I_parameter,D_parameter;/*控制调试参数*/
        int Sum_error;/*总偏差,位置式PID积分相关的参数*/
        int Last_error;/*上次偏差,位置式PID积分相关的参数*/
    }controller;
    //构造对象,初始化
    void Ctor_controller(controller * const Me)
    {  
        //清零
        memset(Me,0,sizeof(controller));
    }
    //删除对象
    void Del_ontroller(controller * const Me)
    {
        ;
    }
    //设置控制器参数
    void Write_controller(controller * const Me,int P,int I,int D)
    {
        Me->P_parameter=P;
        Me->I_parameter=I;
        Me->D_parameter=D;
    }
    //读取控制器参数的值
    controller Read_controller(const controller * const Me,int P,int I,int D)
    {
        return (*Me);
    }
    //计算控制器输出,细节看不懂没关系,只需要知道传入的是偏差,就会有输出一个计算结果就行,这个结果能够帮助控制
    //至于偏差怎么定义什么时候需要用到PID控制器就知道了
    int Out_controller(controller * const Me,int input)
    {
        float	iIncpid=0;
        int now_error=input;//当前偏差
    	Me->Sum_error+=input; 
        //积分量限幅,方式积分饱和过深
    	if(Me->Sum_error >500)
    	{
    		Me->Sum_error = 500 ;
    	}
    	if(Me->Sum_error < -500)
    	{
    		Me->Sum_error = -500 ;
    	}
        Me->ouput=Me->P_parameter * input                  // P
             +Me->I_parameter * Me->Sum_error                // I
             +Me->D_parameter * (now_error-Me->Last_error); // D
        Me->Last_error=now_error;					// 存储误差,用于下次计算		
        return(Me->ouput);           // 返回计算值    
    }
    int main()
    {
        controller test;
        controller read_val;
        //构造,创建一个对象
        Ctor_controller(&test);
        //设置对象的值
        Write_controller(&test,1,1,1);
        //查看对象的值
        read_val=Read_controller(&test,1,1,1);
        printf("对象 P= %d I=%d D=%d \r\n",read_val.P_parameter,read_val.I_parameter,read_val.D_parameter);
        //调用控制器一次:
        printf("控制器输出=%d \r\n",Out_controller(&test,100));
    }
    

    代码实现-进阶版

    到进阶版,才能够完整的看到封装的实现,封装里面就具有了对象的属性与行为。这里我们通过函数指针访问对象的行为,我们可以通过函数指针访问对象的行为。

    那具体行为是怎么实现的呢?实现是通过函数表中的函数指针来访问函数,以此来实现不同函数的调用,从而实现对象的行为。

    那让我们看一下代码实现,然后分析指针指向就知道函数是怎么实现的。

    头文件 :定义了对象的属性与行为

    #ifndef __OOP_H
    #define __OOP_H
    //控制器对象
    struct controller_vtbl;
    typedef struct 
    {
        //对象属性    
        int input;/*控制器输入*/
        int ouput;/*控制器输出*/
        int P_parameter,I_parameter,D_parameter;/*控制调试参数*/
        int Sum_error;/*总偏差,位置式PID积分相关的参数*/
        int Last_error;/*上次偏差,位置式PID积分相关的参数*/
        //对象行为指针,通过指针访问函数
        struct controller_vtbl *vptr;
    }controller;
    //对象的行为所在表,定义对象的行为在这里,通过定义函数指针指向需要实现对象行为的指针
    struct controller_vtbl
    {
        controller * (*Ctor_controller)(void);
        void         (*Del_controller)(controller * const Me);
        controller   (*Read_controller)(const controller * const Me);
        void         (*Write_controller)(controller * const Me,int P,int I,int D);
        int          (*Out_controller)(controller * const Me,int input);
    };
    //对象行为函数
    controller * Ctor_controller(void);
    void Del_controller(controller * const Me);
    controller Read_controller(const controller * const Me);
    void Write_controller(controller * const Me,int P,int I,int D);
    int Out_controller(controller * const Me,int input);
    #endif
    

    源文件::具体函数的行为属性的实现就在这里

    //开始构造对象,既然是控制器,对象必须具有输入,输出,调试参数
    //属性就是:参数值,输入值,输出值
    //行为就是:设置参数,查看参数,根据输入计算输出,构造对象,删除对象
    //属性:用结构体实现
    #include "stdio.h"
    #include <string.h>
    #include <stdlib.h>
    #include "temp.h"
    //构造对象,初始化
    controller * Ctor_controller(void)
    {  
        controller *ptr;
        struct controller_vtbl *table;
        ptr=(controller *)malloc(sizeof(controller));
        table=(struct controller_vtbl *)malloc(sizeof(struct controller_vtbl));    
        //清零
        memset(ptr,0,sizeof(controller));
        table->Ctor_controller=&Ctor_controller;
        table->Del_controller=&Del_controller;
        table->Out_controller=&Out_controller;
        table->Write_controller=&Write_controller;
        table->Read_controller=&Read_controller;
        ptr->vptr=table;
        return ptr;
    }
    //删除对象/析构对象
    void Del_controller(controller * const Me)
    {
        if(Me!=NULL) 
        {
            free(Me->vptr);
            free(Me);
        }
    }
    //设置控制器参数
    void Write_controller(controller * const Me,int P,int I,int D)
    {
        Me->P_parameter=P;
        Me->I_parameter=I;
        Me->D_parameter=D;
    }
    //读取控制器参数的值
    controller Read_controller(const controller * const Me)
    {
        return (*Me);
    }
    //计算控制器输出,细节看不懂没关系,只需要知道传入的是偏差,就会有输出一个计算结果就行,这个结果能够帮助控制
    //至于偏差怎么定义什么时候需要用到PID控制器就知道了
    int Out_controller(controller * const Me,int input)
    {
        float	iIncpid=0;
        int now_error=input;//当前偏差
    	Me->Sum_error+=input; 
        //积分量限幅,方式积分饱和过深
    	if(Me->Sum_error >500)
    	{
    		Me->Sum_error = 500 ;
    	}
    	if(Me->Sum_error < -500)
    	{
    		Me->Sum_error = -500 ;
    	}
        Me->ouput=Me->P_parameter * input                  // P
             +Me->I_parameter * Me->Sum_error                // I
             +Me->D_parameter * (now_error-Me->Last_error); // D
        Me->Last_error=now_error;					// 存储误差,用于下次计算		
        return(Me->ouput);                          // 返回计算值    
    }
    int main()
    {
        controller *test;
        controller read_val;
        //构造,创建一个对象,返回对象指针
        test=Ctor_controller();
        //设置对象的值 
        test->vptr->Write_controller(test,1,1,1);
        //查看对象的值
        read_val=test->vptr->Read_controller(test);
        printf("对象 P= %d I=%d D=%d \r\n",read_val.P_parameter,read_val.I_parameter,read_val.D_parameter);
        //调用控制器一次:
        printf("控制器输出=%d \r\n",test->vptr->Out_controller(test,100));
        //删除/销毁一个对象
        test->vptr->Del_controller(test);
    }
    

    运行结果:

    对象 P= 1 I=1 D=1 
    控制器输出=300 

    可以看到,我们这次操作对象并不是直接调用函数,而是通过指针的方式来访问具体的哪个函数,而指针是在创建的对象里面的,这样就可以直接通过对象来访问它的行为。

    后面实现多态也是使用了函数指针的方式,在多态里面这里的指针与行为表有了它自己的名字,就是虚指针与虚表。

    总结

    知道了什么是对象,对象与类的关系,以及如何实现面向对象的封装,下一篇将了解到面向对象的继承,如何用C语言来实现,例子依然是PID控制器,不过会对控制器进行继承,更高级的实现。

    本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注的更多内容!  

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。