前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C 语言实现面向对象第一步--对象模型

C 语言实现面向对象第一步--对象模型

作者头像
帅地
发布2021-02-08 21:02:45
9680
发布2021-02-08 21:02:45
举报
文章被收录于专栏:苦逼的码农苦逼的码农

首先申明下,看完这篇文章的一些做法,你可能会觉得很傻x,但是我仅仅是抱着一种尝试和学习的态度,实际中可能也并不会这么去用。

什么是 OOP(Object-oriented Programming, OOP)?

OOP 这种编程范式大概起源于 Simula。

它依赖于:

  • 封装(encapsulation)
  • 继承(inheritance)
  • 多态(polymorphism)。

就 C++、Java 而言,OOP 的意思是利用类层级(class hierarchies)及虚函数进行编程。

从而可以通过精制的接口操作各种类型的对象,并且程序本身也可以通过派生(derivation)进行功能增量扩展。

举个 Bjarne Stroustrup FAQ 用过的栗子:

比如可能有两个(或者更多)设备驱动共用一个公共接口:

class Driver { // 公共驱动接口
  public:
  virtual int read(char* p, int n) = 0; // 从设备中读取最多 n 个字符到 p
  // 返回读到的字符总数
  virtual bool reset() = 0; // 重置设备
  virtual Status check() = 0; // 读取状态
};

Driver 仅仅是一个接口。

没有任何数据成员,而成员函数都是纯虚函数。

不同类型的驱动负责对这个接口进行相应的实现:

class Driver1 : public Driver { // 某个驱动
  public:
  Driver1(Register); // 构造函数
  int read(char*, int n);
  bool reset();
  Status check();
  // 实现细节
};
class Driver2 : public Driver { // 另一个驱动
  public:
  Driver2(Register);
  int read(char*, int n);
  bool reset();
  Status check();
  // 实现细节
};

这些驱动含有数据成员,可以通过它们创建对象。它们实现了 Driver 中定义的接口。不难想象,可以通过这种方式使用某个驱动:

  void f(Driver& d) // 使用驱动
  {
    Status old_status = d.check();
    // ...
    d.reset();
    char buf[512];
    int x = d.read(buf,512);
    // ...
  }

这里的重点是,f() 不需要知道它使用的是何种类型的驱动;

它只需知道有个 Driver 传递给了它;

也就是说,有一个接口传递给了它。

我们可以这样调用 f() :

void g() {
 Driver1 d1(Register(0xf00)); // create a Driver1 for device
 // with device register at address 0xf00
 Driver2 d2(Register(0xa00)); // create a Driver2 for device
 // with device register at address 0xa00
 // ...
 int dev;
 cin >> dev;
 if (dev==1)
 f(d1); // use d1
 else
 f(d2); // use d2
 // ...
}

当 f() 使用某个驱动时,与该驱动相对应的操作会在运行时被隐式选择。

例如,当 f() 得到 d1 时,d.read() 使用的是 Driver1::read();

而当 f() 得到 d2 时,d.read() 使用的则是 Driver2::read()。

这被称为运行时绑定,在一些动态语言中,鸭子类型(duck typing) 常用来实现这种“多态”— 不关心是什么东西,只要觉得它可以run,就给他写个叫 run的函数即可。

当然 OOP 也并非万能药。

不能简单地把 “OOP” 等同于“好”。

OOP 的优势在于类层级可以有效地表达很多问题;OOP 的主要弱点在于太多人设法强行用层级模式解决问题。

并非所有问题都应该面向对象。也可以考虑使用普通类(plain class)(也就是常说的 C With Class)、泛型编程和独立的函数(就像数学、C,以及 Fortran 中那样)作为解决问题的方案。

当然,OOP != 封装、继承、多态。

本文仅仅是想讨论下在 C 中如何实现封装、继承、多态。

封装可以借助 struct,将数据和方法都放到一个结构体内,使用者可以无需关注具体的实现。

一种很直白简单的方式,就是使用函数指针表示成员方法和数据放在一个struct 内。

比如在搜狗开源的服务端框架 Workflow 中就大量使用了这种方式:

这里可以看下 __poller_message这个结构体:

struct __poller_message
{
 int (*append)(const void *, size_t *, poller_message_t *);
 char data[0]; 
};

这里 append 函数指针就算是一个成员方法,这样会非常灵活,你可以给它赋任何一种具体实现。

(PS: char[0] 数组是一种 C 语言中常用技巧,通常放在结构体的最后,常用来构成缓冲区。

使用这样的写法最适合制作动态 buffer,可以这样分配空间:malloc(sizeof(struct XXX)+ buff_len); 这样就直接把 buffer 的结构体和缓冲区一块分配了**。**

用起来也非常方便,因为现在空数组其实变成了buff_len长度的数组了。

感兴趣的可以去看下源码(学习分支):https://github.com/sogou/workflow/tree/study

当然了,这里我选择了模仿 C++ 对象模型,在《Inside the C++ Object Model》中提到了三种对象模型设计思路:

  • 简单对象模型: 对象中只存储每个成员(包括函数和数据)的指针
  • 表格驱动对象模型: 对象中存储两个指针,一个指向存储数据的表,一个指向存储函数指针的表(虚函数的解决方案)
  • C++ 实际对象模型: 对象存储 non-static 数据,static成员(数据和函数) 和 non-static 函数都单独存放(注意,并没有指针指向它们,这可以在编译时自动确定地址), 还有一个虚表指针指向存储虚函数指针的表格(这个表第一个元素可能存放的是 type_info object 以支持RTTI)

那这里选择对象只存储数据本身和函数指针。

我们需要一个创建对象和回收资源的方法,可以抄抄 C++ 的作业,C++ 中构造对象使用的是new运算符,new运算符完成了 内存分配 + 调用类构造函数两件事。

delete则回收资源,主要是调用类的析构函数 + 释放内存。

new()方法必须知道当前正在创建的是什么类型的对象,在 C++ 中,编译器会自动识别,并生成对应的汇编。

但是在 C 中我们只能手动将类型相关的信息作为参数。

然后在 new 方法内使用一系列的 if 去分别处理每种类型?

这种方法显然不合适,每个对象应该知道怎么构造自己以及如何析构,也就是类型信息应该自带构造和析构函数。

所以设计了一个 Class 类,Class 类包含类的元信息,比如类的大小(分配内存时会用)、构造、析构函数等。

其它所有的类都继承自这个类。

所谓的继承实际上就是将一个Class类型指针放在第一字段。

很简单,因为只有统一放在对象开头,new 方法内才能识别出这个 Class 类型指针。

所以整个对象模型大概是这个样子:

struct Class {
    size_t size;    /* size of an object */
    void * (* ctor) (void * this, va_list * vl);
    void * (* dtor) (void * this);
    //.... clone 等
};

我们来实现以下newdelete:

// 要将参数透传给对象的构造函数,所以使用 C 语言变长参数
// type 是具体的类类型参数
void * new (const void * type, ...) {
  // 因为 Class 放在第一个字段,所以可以直接做截断,转为 Class
    const struct Class *class = type;
    // 分配对象内存
    void *this = calloc(1, class->size);
    *(struct Class**)this = class;      // 这一步实际上是将每一个类构造出的对象,填充上指向类类型的指针
    // 执行构造函数
    if(class->ctor) {
      // 变长参数,C 语法
        va_list vl;
        va_start(vl, type);
        this = class->ctor(this, &vl);
        va_end(vl);
    }
    return this;
}
// 传入待析构的对象指针
void delete (void * self) {
  // 获取 Class 类型指针
    const struct Class **this = self;
    // 如果有析构函数, 就执行析构
    if(self && *this && (*this)->dtor) {
        self = (*this)->dtor(self);
    }
    // 释放内存
    free(self);
}

接着,我们基于这个Class来实现一个 String。

// string.h

// 这就是需要传入 new 函数的第一个参数,类型指针
extern const void * StringNew;

struct String {
    const void *class;       /* 父类, 都是 Class */
    char * content;            /* 字符串内容 */
    char *(*get_content)(struct String*);     // 获取
    void (*set_content)(struct String*, const char *); // 设置
};

这是String的实现:

// string.c

// getter
static char *get_content(struct String *str) {
    return str->content;
}

// setter
static void set_content(struct String *str, const char *newcontent) {
    if(str->content) {
        free(str->content);
    }
    str->content = strdup(newcontent);
}

// 构造函数
static void*  string_ctor(void *_this, va_list *args) {
    struct String * this = _this;
    // 初始化内容
    const char *content = va_arg(*args, const  char*);
    this->content = strdup(content);
    // 设置成员函数指针
    this->get_content = get_content;
    this->set_content = set_content;
    return this;
}

// 析构函数
static void* string_dtor(void *_this) {
    struct String* this = _this;
    // 释放字符串内存
    if(this->content) {
        free(this->content);
        this->content = NULL;
    }
    return this;
}

// 定义一个 Class 变量,即 String 类型的 Class
static const struct Class _String = {
        sizeof(struct String),
        string_ctor,
        string_dtor
};
// 然后将 _String 变量取地址赋值给定义在 string.h 的 StringNew
// StringNew 就相当于构造字符串的类模板了,以后需要将这个指针传递给 new 函数
const void *StringNew = &_String;

来看下怎么用吧:

void test_str() {
    // 构造
    struct String *str = new(StringNew, "test");


    printf("%s\n", str->get_content(str));

    str->set_content(str, "newtest");

    printf("%s\n", str->get_content(str));


    // 析构
    delete(str);
}

是不是有点那味了?

就是每次都得显示的传 this参数,这个没办法,语法不支持。

不过应该是可以用宏包一下。

好了,整体的框架已经搭好了,可以基于这种模式去实现继承、多态了。

这部分我就放在第二篇写了,可以自己先去试下,达到大概这种效果:

Circle 继承自Graph,然后可以将 Circle 对象向上转型为 Graph,但是Graph去调用具体 draw方法的时候,还是执行的 Circledraw方法。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 帅地玩编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是 OOP(Object-oriented Programming, OOP)?
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档