你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

C语言面向对象思想的运用

[复制链接]
gaosmile 发布时间:2020-8-26 22:59
目录
前言1、继承2、封装伪构造函数3、多态
前言
我们都知道C语言是一门过程性语言,所谓过程性就是在解决问题时,将问题按步骤分解。
例如,做菜的时候,先点火,再倒油,接着下菜翻炒,最后加盐和酱油。但有时候借鉴面向对象的思想来组织代码,逻辑层次会更加清晰。
C和C++的最大区别便是,C++有类,C没有类的概念。单单这一个类使得C缺失很多的东西。好在C有结构体,勉强可以当0.1个类来使用。
众所周知,类有三大特性:封装、继承、多态。我们来看看C语言如何借鉴类的三大特性来更好的组织代码。
1、继承
C语言没有严格意义上的继承,可以借助结构体嵌套实现类似于继承的形式,但始终不尽人意。
struct parent
{
    int a;
};
struct son
{
    struct parent p;//儿子继承父亲
    int b;
};
C++的类可以实现成员的访问控制,例如将变量b声明成private,那么外部就无法访问。但C的结构体做不到。
在C++里头,父亲的私有成员,儿子是无法访问的。结构体嵌套也做不到。因为结构体根本就没有访问控制的概念。
对于C++而言,访问控制实质上是在编译层做的,我们仍旧可以通过指针来间接访问。
例如:
class Base{
public:
   int a;
private:
   int b;
};
尽管b被声明成私有,但我们仍旧有办法访问它(借助指针绕过语法检查):
Base t;
int *p = &t.a;
cout << p[1] << endl;
2、封装
封装就是把数据和方法打包到一个类里面。C++的实现大致如下:
class 类名
{
public:
    公有方法1
    公有方法2
    ……
    公有数据1
    ……
private:
    私有方法1
    私有方法2
    ……
    私有数据1
    ……
};
这样做的好处是显而易见的。一个类实现了一个小模块,使得代码结构比较清晰。对外接口和数据定义成public,允许调用者直接访问。内部接口和数据定义成private,外部不可见。
在 QT 中,为了更好的隐藏一个类的具体实现,一般是一个公开头文件、一个私有头文件,私有头文件中定义实现的内部细节,公开头文件中定义开放给客户程序员的接口和公共数据。看看QObject (qobject.h),对应有一QObjectPrivate(qobject_p.h ) ,其他的也类似。
QObject{
public:
    xxx
    xxx
private:
    QObjectPrivate * priv;
};
我们可以借助C语言的指针和结构体来实现方法和数据的封装。基本框架如下:
struct 结构体名{
    数据1;
    数据2;
    ……
    方法1:
    方法2;
    ……
}
在结构体里定义成员变量很容易,直接int a;
在结构体里定义成员函数要使用函数指针,比如:
int (*func)(void *)
所以,我们把上面的框架具体化就是:
struct manager{
    int data1;
    int data2;
    int (*operation1)(struct manager*);
    int (*operation2)(struct manager*);
};
实际上,C++的成员函数也是通过函数指针的形式来实现,本质上是一致的。我们都知道类的成员函数和类的成员变量是分开存储的,同一个类的所有对象,成员函数只需要占据一份地址空间。
在定义结构体之后,函数指针并没有赋值,一般我们会定义一个结构体初始化函数来初始化结构体成员,这有点类似于类的构造函数,但类的构造函数在创建对象时自动调用,而我们这个结构体初始化函数只能自己手动调用了。
同样的,对标C++的析构函数,我们在C语言里头有一个去初始化的函数来完成模块的去初始化,这种思想不就是一样的吗?
static int operation1(struct manager *manager_ptr)
{
    ……
}
static int operation2(struct manager *manager_ptr)
{
    ……
}
伪构造函数
注意,我们把两个operation函数定义成了static,这样子文件之外的函数就不能调用它,只能通过manager结构体来调用。是不是感觉有点封装的意味。
void init_manager(struct manager* manager_ptr)
{
    manager_ptr->operation1 = operation1;
    manager_ptr->operation2 = operation2;
    manager_ptr->data1 = 0;
    manager_ptr->data2 = 0;
}
去初始化函数我就不写了。
如果operation函数在外面的文件定义,则可以作为init_manager函数的参数传入,这种场景也非常常见。我实现了模块A,该模块的operation1函数处理数据并输出一些结果。但是我并不知道使用该模块的人想要什么格式的结果,比如有一些人想要json格式的结果,有些人想要xml格式的结果。我不能帮他们一一实现一个方法,我干脆叫你们统一按照我指定的函数模板,实现一个处理函数,完了你们调用结构体初始化函数注册下,我会在operation1函数处理完数据后,调用你们的处理函数,给你们一个满意的结果。
为了达到上面的目的,简单修改下,我们把函数operation2定义成一种类型,
typedef int  (*FUN_CBK)(struct manager *manager_ptr);
结构体定义稍作修改:
struct manager{
    int data1;
    int data2;
    int (*operation1)(struct manager*);
    FUN_CBK call_back;
};
结构体初始化函数也要做相应的修改,增加了一个函数指针形参:
void init_manager(struct manager* manager_ptr, \
FUN_CBK fun)
{
    manager_ptr->operation1 = operation1;
    manager_ptr->call_back = fun;//用外部传入的回调函数进行初始化
    manager_ptr->data1 = 0;
    manager_ptr->data2 = 0;
}
通过上面的操作,我们用结构体和函数指针完成了模块化封装。
我看了网上的博客,有些人为了特意模仿类,还用以下方式实现了类似于类的构造函数:
struct manager *manager_create(int m, int n)
{
    struct manager *m = (struct manager *)\
    malloc(sizeof(struct manager *));
    m->data1 = m;
    m->data2 = n;
    m->operation1 = operation1;
    m->operation2 = operation2;
}
以及类似于类的析构函数:
void manager_delete(struct manager *m_ptr)
{
    free(m_ptr);
    m_ptr = NULL;
}
使用示例:
struct manager *m_ptr = manager_create (1,2);
manager_delete(m_ptr);
个人不是很喜欢这种做法,万一忘记调用manager_delete还有内存泄露的风险。
结构体归根到底还是结构体,不能实现成员对外不可见。而C++中将成员声明成private之后,外部就无法访问了。C语言里想这么做,只能将该成员移出结构体,定义为static形式。因为C不支持在结构体内部定义static变量(不信,你可以自己去试下)。
为何不能在结构体内定义static变量,想想就知道了,static变量的地址在编译链接之后是唯一且确定的,而结构体只有在实例化时才能确定其地址,并且每个结构体实例都有自己的地址空间。
3、多态
多态在上面的例子也有体现。C语言实现的多态并非是严格意义上的多态,但是这种思想的应用很广泛,我们姑且叫它多态吧。你不解C++的多态也没关系,丝毫不影响你理解下文。
linux的VFS便借鉴了这种思想。VFS(Virtual File System)是内核提供的文件系统抽象层,其提供了文件系统的操作接口,可以隐藏底层不同文件系统的实现。
一个文件系统无非就是实现对文件、目录的管理。针对文件VFS定义了统一的结构体:
struct file {
    union {
        struct llist_nodefu_llist;
        struct rcu_head fu_rcuhead;
    } f_u;
    struct pathf_path;
    struct inode*f_inode;/* cached value */
    const struct file_operations*f_op;
    ……
};
strcut file代表一个文件,每种文件系统(比如ext3,vfat)实现读写等操作的方式都不一样,所以将这些方法封装成函数指针,统一定义在结构体struct file_operations内。
struct file_operations {
    struct module *owner;
    loff_t (*llseek) (struct file *, loff_t, int);
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ……
};
每个文件系统各自完成自己的实现。

再写一个实际的例子。
定义一个人的标准接口和数据如下:
strcut man{
    int head;
    int body;
    ……
    void (*say_hello)(void);//见面问候的方式
};
中国人见面时,说你好:
void china_say_hello(void)
{
    printf(“你好”);
}
英国人见面时,说hello:
void english_say_hello(void)
{
    printf(“hello”);
}
现在来初始化它们各自的问候方式:
struct man china{
    .say_hello = china_say_hello;
}
struct man english{
    .say_hello = English_say_hello;
}
英国人和中国人对外呈现都是struct man,其见面问候的接口都是man.say_hello,但其底层实现却可以不一样。
并且我们可以在程序运行时,随意的更改中国人的问候方式。比如婴儿时期,只会“哇哇”叫,长大了才会说“你好”,我们可以改变成员say_hello的值,让其在不同时期指向不同的函数,从而达到运行时多态的目的。
其实呢,C++的多态,也是通过函数指针来实现的,学习过C++的同学就会知道,含有虚函数的类,会维护一个虚函数表,里面存放了虚函数的地址。所以说啊,C语言是C++的母语,万变不离指针,指针是C语言的一大法宝。

收藏 评论0 发布时间:2020-8-26 22:59

举报

0个回答

所属标签

STM32团队

意法半导体微控制器和微处理器拥有广泛的产品线,包含低成本的8位单片机和基于ARM® Cortex®-M0、M0+、M3、M4、M33、M7及A7内核并具备丰富外设选择的32位微控制器及微处理器


最新内容

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
STM32N6 AI生态系统
STM32MCU,MPU高性能GUI
ST ACEPACK电源模块
意法半导体生物传感器
STM32Cube扩展软件包
关注我们
微信公众号二维码 微信公众号
手机版二维码 手机版