【C语言开源库】lw_oopc:轻量级的C语言面向对象编程框架

void (*setName)(Animal* t, const char* name); // 设置动物的昵称
void (*setAge)(Animal* t, int age); // 设置动物的年龄
void (*sayHello)(Animal* t); // 动物打招呼
void (*eat)(Animal* t); // 动物都会吃(抽象方法,由子类实现)
void (*breathe)(Animal* t); // 动物都会呼吸(抽象方法,由子类实现)
void (*init)(Animal* t, const char* name, int age); // 初始化昵称和年龄
};

ABS_CLASS宏用于定义抽象类, 允许有成员属性. 代码的含义参见代码注释. 紧接着, 我们来定义Fish和Dog类, 它们都继承动物, 然后还实现了IMoveable接口:

CLASS(Fish)
{
EXTENDS(Animal); // 继承Animal抽象类
IMPLEMENTS(IMoveable); // 实现IMoveable接口

void (*init)(Fish* t, const char* name, int age); // 初始化昵称和年龄
};

CLASS(Dog)
{
EXTENDS(Animal); // 继承Animal抽象类
IMPLEMENTS(IMoveable); // 实现IMoveable接口

void(*init)(Dog* t, const char* name, int age); // 初始化昵称和年龄
};

为了让Fish对象或Dog对象在创建之后, 能够很方便地初始化昵称和年龄, Fish和Dog类均提供了init方法.下面, 我们来定义Car,车子不是动物, 但可以Move, 因此, 让Car实现IMoveable接口即可:

CLASS(Car)
{
IMPLEMENTS(IMoveable); // 实现IMoveable接口(车子不是动物,但可以Move)
};

接口, 抽象类, 具体类的定义都已经完成了. 下面, 我们开始实现它们. 接口是不需要实现的, 所以IMoveable没有对应的实现代码. Animal是抽象动物接口, 是半成品, 所以需要提供半成品的实现:

/* 设置动物的昵称*/
void Animal_setName(Animal* t, const char* name)
{
// 这里假定name不会超过128个字符, 为简化示例代码, 不做保护(产品代码中不要这样写)
strcpy(t->name, name);
}
/* 设置动物的年龄*/
void Animal_setAge(Animal* t, int age)
{
t->age = age;
}
/* 动物和我们打招呼*/
void Animal_sayHello(Animal* t)
{
printf(“Hello! 我是%s,今年%d岁了!\n”, t->name, t->age);
}
/* 初始化动物的昵称和年龄*/
void Animal_init(Animal* t, const char* name, int age)
{
t->setName(t, name);
t->setAge(t, age);
}

ABS_CTOR(Animal)
FUNCTION_SETTING(setName, Animal_setName);
FUNCTION_SETTING(setAge, Animal_setAge);
FUNCTION_SETTING(sayHello, Animal_sayHello);
FUNCTION_SETTING(init, Animal_init);
END_ABS_CTOR

这里出现了几个新的宏, 我们逐个进行讲解. ABS_CTOR表示抽象类的定义开始, ABS_CTOR(Animal)的含义是Animal抽象类的"构造函数"开始. 在C语言里边其实是没有C++中的构造函数的概念的. LW_OOPC中的CTOR系列宏(CTOR/END_CTOR, ABS_CTOR/END_ABS_CTOR)除了给对象(在C语言中是struct实例)分配内存, 然后, 紧接着要为结构体中的函数指针成员赋值, 这一过程, 也可以称为函数绑定(有点类似C++中的动态联编). 函数绑定的过程由FUNCTION_SETTING宏来完成. 对于Fish和Dog类的实现, 与Animal基本上是类似的, 除了将ABS_CTOR换成了CTOR, 直接参见代码:

/* 鱼的吃行为 */
void Fish_eat(Animal* t)
{
printf(“鱼吃水草!\n”);
}
/* 鱼的呼吸行为 */
void Fish_breathe(Animal* t)
{
printf(“鱼用鳃呼吸!\n”);
}
/* 鱼的移动行为 */
void Fish_move(IMoveable* t)
{
printf(“鱼在水里游!\n”);
}
/* 初始化鱼的昵称和年龄 */
void Fish_init(Fish* t, const char* name, int age)
{
Animal* animal = SUPER_PTR(t, Animal);
animal->setName(animal, name);
animal->setAge(animal, age);
}

CTOR(Fish)
SUPER_CTOR(Animal);
FUNCTION_SETTING(Animal.eat, Fish_eat);
FUNCTION_SETTING(Animal.breathe, Fish_breathe);
FUNCTION_SETTING(IMoveable.move, Fish_move);
FUNCTION_SETTING(init, Fish_init);
END_CTOR

上面是Fish的实现, 下面看Dog的实现:

/* 狗的吃行为 / void Dog_eat(Animal t) { printf(“狗吃骨头!\n”); } /* 狗的呼吸行为 / void Dog_breathe(Animal t) { printf(“狗用肺呼吸!\n”); } /* 狗的移动行为 / void Dog_move(IMoveable t) { printf(“狗在地上跑!\n”); } /* 初始化狗的昵称和年龄 / void Dog_init(Dog t, const char* name, int age) { Animal* animal = SUPER_PTR(t, Animal); animal->setName(animal, name); animal->setAge(animal, age); }

CTOR(Dog) SUPER_CTOR(Animal); FUNCTION_SETTING(Animal.eat, Dog_eat); FUNCTION_SETTING(Animal.breathe, Dog_breathe); FUNCTION_SETTING(IMoveable.move, Dog_move); FUNCTION_SETTING(init, Dog_init); END_CTOR

细心的朋友可能已经注意到了, 这里又有一个陌生的宏: SUPER_CTOR未介绍. 这个宏是提供给子类用的, 用于调用其直接父类的构造函数(类似Java语言中的super()调用, 在这里, 其实质是要先调用父类的函数绑定过程, 再调用自身的函数绑定过程), 类似Java那样, SUPER_CTOR如果要出现, 需要是ABS_CTOR或者CTOR下面紧跟的第一条语句. 最后, 我们把Car类也实现了:

void Car_move(IMoveable* t)
{
printf(“汽车在开动!\n”);
}

CTOR(Car)
FUNCTION_SETTING(IMoveable.move, Car_move);
END_CTOR

下面, 我们实现main方法, 以展示LW_OOPC的威力:

#include “animal.h”

int main()
{
Fish* fish = Fish_new(); // 创建鱼对象
Dog* dog = Dog_new(); // 创建狗对象
Car* car = Car_new(); // 创建车子对象

Animal* animals[2] = { 0 }; // 初始化动物容器(这里是Animal指针数组)
IMoveable* moveObjs[3] = { 0 }; // 初始化可移动物体容器(这里是IMoveable指针数组)

int i = 0; // i和j是循环变量
int j = 0;

// 初始化鱼对象的昵称为:小鲤鱼,年龄为:1岁
fish->init(fish, “小鲤鱼”, 1);

// 将fish指针转型为Animal类型指针,并赋值给animals数组的第一个成员
animals[0] = SUPER_PTR(fish, Animal);

// 初始化狗对象的昵称为:牧羊犬,年龄为:2岁
dog->init(dog, “牧羊犬”, 2);

// 将dog指针转型为Animal类型指针,并赋值给animals数组的第二个成员
animals[1] = SUPER_PTR(dog, Animal);

// 将fish指针转型为IMoveable接口类型指针,并赋值给moveOjbs数组的第一个成员
moveObjs[0] = SUPER_PTR(fish, IMoveable);

// 将dog指针转型为IMoveable接口类型指针,并赋值给moveOjbs数组的第二个成员
moveObjs[1] = SUPER_PTR(dog, IMoveable);

// 将car指针转型为IMoveable接口类型指针,并赋值给moveOjbs数组的第三个成员
moveObjs[2] = SUPER_PTR(car, IMoveable);

// 循环打印动物容器内的动物信息
for(i=0; i<2; i++)
{
Animal* animal = animals[i];
animal->eat(animal);
animal->breathe(animal);
animal->sayHello(animal);
}

// 循环打印可移动物体容器内的可移动物体移动方式的信息
for(j=0; j<3; j++)
{
IMoveable* moveObj = moveObjs[j];
moveObj->move(moveObj);
}

lw_oopc_delete(fish);
lw_oopc_delete(dog);
lw_oopc_delete(car);

return 0;
}

从上边的代码中, 我们惊喜地发现, 在C语言中, 借助LW_OOPC, 我们实现了将不同的动物(Fish和Dog对象)装入Animal容器, 然后可以用完全相同的方式调用Animal的方法(比如eat和breathe方法), 而实际调用的是具体的实现类(Fish和Dog)的对应方法. 这正是面向对象中的多态的概念. 同样, 我们可以将Fish对象, Dog对象, 以及Car对象均视为可移动物体, 均装入IMoveable容器, 然后用完全相同的方式调用IMoveable接口的move方法. 看到了吗? 借助LW_OOPC, 在C语言下我们竟然可以轻松地实现面向对象和面向接口编程!

下面, 再举一个稍微复杂的例子, 它的覆盖面是足够全面的, 足以一瞥面向对象编程的3个要素: 数据抽象, 继承和多态. 通过这个例子, 我们期望展现出LW_OOPC在遭遇问题本身比较复杂的情形下, 是如何从容应对的, 以加深读者对LW_OOPC的认识. (备注: 该问题来自<C++沉思录>第八章的例子, 有兴趣的读者可以对照参阅).

问题描述

此程序涉及的内容是用来表示算术表达式的树. 例如, 表达式(-5) * (3 + 4)对应的树为: [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jAFBcpev-1636287688598)( )]

一个表达式树包括代表常数, 一元运算符和二元运算符的节点. 这样的树结构在编译器和计算器程序中都可能用到. 我们希望能通过调用合适的函数来创建这样的树, 然后打印该树的完整括号化形式. 例如, 我们希望打印

((-5)(3+4))
(((-5)
(3+4))((-5)(3+4)))

作为输出. 此外, 我们不想为这些表达式的表示形式操心, 更不想关心有关它们内存分配和回收的事宜. 这个程序所做的事情在很多需要处理复杂输入的大型程序中是很典型的, 例如编译器, 编辑器, CAD/CAM系统等. 此类程序中通常要花费很大的精力来处理类似树, 图和类似的数据结构. 这些程序的开发者永远需要面对诸如内存分配, 灵活性和效率之类的问题. 面向对象技术可以把这些问题局部化, 从而确保今后发生的一系列变化不会要求整个程序中的其他各个部分随之做相应调整.

#include <stdio.h>
#include “expr.h”

int main()
{
Expr* expr1 = Expr_new();
Expr* expr2 = Expr_new();
Expr* expr3 = Expr_new();
Expr* expr = Expr_new();

expr1->initUnaryX(expr1, “-”, 5);
expr2->initBinaryX(expr2, “+”, 3, 4);
expr3->initBinary(expr3, “*”, expr1, expr2);
expr->initBinary(expr, “*”, expr3, expr3);

expr3->print(expr3);
printf(“\n”);
expr->print(expr);
printf(“\n”);

Expr_delete(expr);
Expr_delete(expr3);
Expr_delete(expr2);
Expr_delete(expr1);

return 0;
}

解决方案

通过考查这个树结构, 会发现这里有3种节点. 一种表示整数表达式, 包含一个整数值, 无子节点. 另外两个分别表示一元表达式和二元表达式, 包含一个操作符, 分别有一个或两个子节点. 我们希望打印各种节点, 但是具体方式需要视要打印节点的类型而定. 这就是动态绑定的用武之地了: 我们可以定义一个虚函数(print)来指明应当如何打印各种节点. 动态绑定将会负责在运行时基于打印节点的实际类型调用正确的函数. 首先, 我们抽象出"节点"的概念, 抽象类的名字定为Expr_node. 它提供了打印的抽象接口, 所有的实际节点类型均从它派生:

ABS_CLASS(Expr_node)
{
void (*print)(Expr_node* t);
};

具体类的情形怎样? 这些具体类型中最简单的一类是包含一个整数, 没有子节点的节点:

CLASS(Int_node)
{
EXTENDS(Expr_node);
int n;

void (*init)(Int_node* t, int k);
};

其他类型又如何呢? 每个类中都必须存储一个操作符(这倒简单, 本文中假定操作符最长不超过2个字符, 所以, 可以用长度为3的字符数组来保存), 但是如何存储子节点呢? 在运行时之前, 我们并不知道子节点的类型会是什么, 所以我们不能按值存储子节点, 必须存储指针. 这样, 一元和二元节点类如下所示:

CLASS(Unary_node)
{
EXTENDS(Expr_node);
char op[3]; // 假设操作符最长不超过2个字符
Expr_node* opnd;

void (*init)(Unary_node* t, const char* a, Expr_node* b);
};

CLASS(Binary_node)
{
EXTENDS(Expr_node);
char op[3]; // 假设操作符最长不超过2个字符
Expr_node* left;
Expr_node* right;

void (*init)(Binary_node* t, const char* a, Expr_node* b,
Expr_node * c);
};

这个设计方案可以用, 不过有一个问题. 用户要处理的不是值, 而是指针, 所以必须记住分配和释放对象. 例如, 我们需要这么创建表达式树:

Int_node* int_node1 = Int_node_new();
Int_node* int_node2 = Int_node_new();
Int_node* int_node3 = Int_node_new();
Unary_node* unary_node = Unary_node_new();
Binary_node* binary_node1 = Binary_node_new();
Binary_node* binary_node = Binary_node_new();

int_node1->init(int_node1, 5);
int_node2->init(int_node2, 3);
int_node3->init(int_node3, 4);
unary_node->init(unary_node, “-”, int_node1);
binary_node1->init(binary_node1, “+”, int_node2, int_node3);
binary_node->init(binary_node, “*”, unary_node, binary_node1);

lw_oopc_delete(int_node1);
…… // 删除创建的其他节点

也就是说, 我们需要去关心每一个节点的创建和释放. 我们不仅把内存管理这类烦心事推给了用户, 而且对用户来说也没有什么方便的办法来处理这些事情. 我们得好好想想办法了.

这里, 提供一种解决内存管理问题的思路: 引用计数, 这里是针对指针, 对指针的状况进行计数, 对象创建的时候, 引用计数为1, 凡是指针被赋值了, 该指针所指对象的引用计数就自增一, 每次指针要释放, 都先检查对象的引用计数, 让引用计数自减一, 如果引用计数为0, 则释放该对象.

另外, 原先的设计不够高层, 用户只能直接针对节点进行操作, 没有提供操作子树的概念(这也是用户代码之所以复杂的原因之一), 我们发现, 通过提供子树的概念, 我们不但能够隐藏Expr_node继承层次, 而且, 对于每一个节点, 我们具备了操纵左子树和右子树的能力(原来只能操作左子节点和右子节点). 而这种功能增强完全是建立在面向对象的机制之上, 我们并没有引入耦合. 在非常自然和轻松的情形下, 我们获得了更好的软件组件之间协作的能力, 这正是面向对象的魅力所在.

这里, 我们把子树的概念用类Expr来表示, 由于子树此时成了Expr_node具体类的成员, 同样, 左右子树在Expr_node中同样是以指针的方式保存, 所以, 对Expr也需要进行引用计数, 代码直接贴上来, 细节解说参见注释:

// expr.h
#ifndef EXPR_H_INCLUDED_
#define EXPR_H_INCLUDED_

#include “lw_oopc.h”

// 表达式节点
ABS_CLASS(Expr_node)
{
int use; // 引用计数

void (*print)(Expr_node* t); // 打印表达式节点
void (*finalize)(Expr_node* t); // 子类通过覆写finalize方法,实现对资源清理行为的定制
};

// 表达式(子树的概念), 其中, init*方法族提供了构建子树的高层API. 方便用户使用
CLASS(Expr)
{
int use; // 引用计数
Expr_node* p; // 子树的根节点

// 构建整数表达式(包含一个整数值,无子表达式)
void (*initInt)(Expr* t, int);

// 构建一元表达式(包含一个操作符,一个子表达式)
void (*initUnary)(Expr* t, const char*, Expr*);
// 构建一元表达式的重载形式(通过传入个整型值参数,构造一个子表达式为整数表达式的一元表达式)
void (*initUnaryX)(Expr* t, const char*, int);

// 构建二元表达式(包含一个操作符,二个子表达式)
void (*initBinary)(Expr* t, const char*, Expr*, Expr*);
// 构建二元表达式的重载形式(通过传入个整型值参数,构造两个子表达式均为整数表达式的二元表达式)
void (*initBinaryX)(Expr* t, const char*, int, int);

void (*print)(Expr* t); // 打印子树
};

// 整数表达式节点
CLASS(Int_node)
{
EXTENDS(Expr_node); // 继承Expr_node

int n; // 整数值

// 初始化整数表达式节点(传入整数值)
void (*init)(Int_node* t, int k);
};

// 一元表达式节点
CLASS(Unary_node)
{
EXTENDS(Expr_node); // 继承Expr_node

char op[3]; // 假设操作符最长不超过2个字符
Expr* opnd; // 子表达式

// 初始化一元表达式节点(传入一个操作符和一个子表达式)
void (*init)(Unary_node* t, const char* a, Expr* b);
};

// 二元表达式节点
CLASS(Binary_node)
{
EXTENDS(Expr_node); // 继承Expr_node

char op[3]; // 假设操作符最长不超过2个字符
Expr* left; // 左子表达式
Expr* right; // 右子表达式

// 初始化二元表达式节点(传入一个操作符和两个子表达式)
void (*init)(Binary_node* t, const char* a, Expr* b, Expr* c);
};

#endif

//expr.c
…… // 包含所需头文件

ABS_CTOR(Expr_node)
cthis->use = 1; // 构造函数中,将引用计数初始化为
END_ABS_CTOR

// Expr_node的析构函数(DTOR/END_DTOR用于实现析构函数语义)
DTOR(Expr_node)
if (–cthis->use == 0) // 递减引用计数,如果计数为,释放自己
{
cthis->finalize(cthis); // 释放内存之前先清理资源(其他需要释放的对象)
return lw_oopc_true; // 返回true,表示析构成功,可以释放内存
}
return lw_oopc_false; // 返回false,表示析构失败,不能释放内存
END_DTOR

// 构建整数表达式(包含一个整数值,无子表达式),n为整数值
void Expr_initInt(Expr* expr, int n)
{
Int_node* intNode = Int_node_new(lw_oopc_file_line);
intNode->init(intNode, n);

expr->p = SUPER_PTR(intNode, Expr_node);
}

…… // 因篇幅所限,构建一元表达式、二元表达式以及对应的重载形式的函数实现代码省略

// 打印表达式(子树)
void Expr_print(Expr* t)
{
Expr_node* p = t->p;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

f-1712998317375)]
[外链图片转存中…(img-5UxlIX98-1712998317376)]
[外链图片转存中…(img-z3R1F3gG-1712998317377)]
[外链图片转存中…(img-xp6q4YE5-1712998317377)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-9qYSzhos-1712998317378)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 20
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值