Go最全【C语言开源库】lw_oopc:轻量级的C语言面向对象编程框架(1),Golang面试知识点总结宝典助你通关

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

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

// 循环打印动物容器内的动物信息
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)( )]](https://bbs.csdn.net/topics/618658159)


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



((-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;
p->print§;
}

CTOR(Expr)
FUNCTION_SETTING(initInt, Expr_initInt);
FUNCTION_SETTING(initUnary, Expr_initUnary);
FUNCTION_SETTING(initUnaryX, Expr_initUnaryX);
FUNCTION_SETTING(initBinary, Expr_initBinary);
FUNCTION_SETTING(initBinaryX, Expr_initBinaryX);
FUNCTION_SETTING(print, Expr_print);
cthis->use = 1; // 构造函数中,将引用计数初始化为
END_CTOR

// Expr的析构函数(DTOR/END_DTOR用于实现析构函数语义)
DTOR(Expr)
if (–cthis->use == 0) // 递减引用计数,如果计数为,释放自己
{
Expr_node_delete(cthis->p);
return lw_oopc_true;
}
return lw_oopc_false;
END_DTOR

// 整数表达式节点的初始化
void Int_node_init(Int_node* t, int k)
{
t->n = k;
}

// 整数表达式节点的打印
void Int_node_print(Expr_node* t)
{
Int_node* cthis = SUB_PTR(t, Expr_node, Int_node);
printf(“%d”, cthis->n);
}

// 整数表达式节点的资源清理
void Int_node_finalize(Expr_node* t)
{
// 什么都不需要做
}

CTOR(Int_node)
SUPER_CTOR(Expr_node);
FUNCTION_SETTING(init, Int_node_init);
FUNCTION_SETTING(Expr_node.print, Int_node_print);
FUNCTION_SETTING(Expr_node.finalize, Int_node_finalize);
END_CTOR

…… // 因篇幅所限,一(二)元表达式节点的初始化, 打印, 资源清理, 构造等函数的实现代码省略

//main.c
#include “stdio.h”
#include “Expr.h”

int main()
{
Expr* expr = Expr_new();
…… // 创建expr1, expr2, expr3的代码

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);
……	// 删除expr3、expr2、expr1的代码

return 0;

}


程序运行效果:



liuboyf1@ipc:~/data/custom/aklw_oopc/demo/expr$ ./a.out
((-5)*(3+4))
(((-5)*(3+4))*((-5)*(3+4)))


怎么样? 效果还不错吧, 最重要的是, 我们的C语言代码现在已经完全是面向对象的.


#### 方案的可扩展性如何?


假设我们希望添加一种Ternary\_node类型来表示三元操作符, 如?: (也就是if-then-else操作符), 看看, 难度有多大? 事实上, 正是因为前面的设计是面向对象的, 要增加一种节点类型易如反掌:



// 三元表达式节点
CLASS(Ternary_node)
{
EXTENDS(Expr_node);

char op[3];		        // 假设操作符最长不超过2个字符
Expr\* left;
Expr\* middle;
Expr\* right;

// 初始化三元表达式节点(传入一个操作符和三个子表达式)
void (\*init)(Ternary_node\* t, const char\* op, Expr\* left, Expr\* middle, Expr\* right);

};


在Expr中添加创建三元表达式的方法:



// 表达式(子树的概念), 其中, init*方法族提供了构建子树的高层API, 方便用户使用
CLASS(Expr)
{
int use; // 引用计数
Expr_node* p; // 子树的根节点
…… // 既有实现
// 构建三元表达式(包含一个操作符,三个子表达式)
void (*initTernary)(Expr* t, const char*, Expr*, Expr*, Expr*);
// 构建三元表达式的重载形式(通过传入一个整型值参数,构造三个子表达式均为整数表达式的三元表达式)
void (*initTernaryX)(Expr* t, const char*, int, int, int);
…… // 既有实现
};


请读者参照Binary\_node的现有实现, 实现出Ternary\_node, 这里不再赘述. 一旦实现出Ternary\_node, 我们就可以这样创建表达式树并打印:



…… // 创建expr1, expr2, expr3, expr对象(指针)

expr1->initUnaryX(expr1, “-”, 0);
expr2->initUnaryX(expr2, “-”, 5);
expr3->initBinaryX(expr3, “+”, 3, 4);
expr->initTernary(expr, “?:”, expr1, expr2, expr3);

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


为了支持新的节点类型, 对原有代码的更动很少(仅对Expr类有增加方法), 而且只有新增操作(新增类,新增方法), 但没有修改操作(指修改原有方法), 面向对象的设计赋予了系统极大的弹性, 让程序在应对变化时, 更加从容. 在这个例子中, LW\_OOPC帮助我们在C语言的世界里营造出OO的天地, 带领我们再一次领略了面向对象的风采.


#### LW\_OOPC最佳实践


说得简单一点, 要想使用好LW\_OOPC这套宏, 还得首先懂面向对象, 要遵循面向对象设计的那些大原则, 比如开闭原则等. 在C语言中使用面向对象, 根据实际使用的情况, 给出如下建议:


1. 继承层次不宜过深, 建议最多三层(接口, 抽象类, 具体类, 参见图1和图2) 继承层次过深, 在Java/C#/C++中均不推崇, 在C语言中实践面向对象的时候, 尤其要遵循这一点, 只有这样, 代码才能简单清爽.
2. 尽量避免多重继承 尽可能使用单线继承, 但可实现多个接口(与Java中的单根继承类似).
3. 尽量避免具体类继承具体类 具体类继承具体类, 不符合抽象的原则, 要尽量避免.
4. 各继承层次分别维护好自己的数据 子类尽量不要直接访问祖先类的数据, 如果确实需要访问, 应当通过祖先类提供的函数, 以函数调用的方式间接访问.


[[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BQHO1aqY-1636287688600)( )]](https://bbs.csdn.net/topics/618658159)


#### LW\_OOPC的优点


1. 轻量级.
2. 广泛的适应性, 能够适应各种平台, 各种编译器(能支持C的地方, 基本上都能支持).
3. 帮助懂OO的Java/C++程序员写出面向对象的C程序.
4. 使用C, 也能引入OO的设计思想和方法, 在团队的C/C++分歧严重时可能非常有用.


#### LW\_OOPC的缺点


1. 无法支持重载(C语言不支持所致)


![img](https://img-blog.csdnimg.cn/img_convert/26777b21657968a5f237339e1d611ff0.png)
![img](https://img-blog.csdnimg.cn/img_convert/81068dc5d7229b0c9fd34d59372afc7d.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**


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

适应各种平台, 各种编译器(能支持C的地方, 基本上都能支持).
3. 帮助懂OO的Java/C++程序员写出面向对象的C程序.
4. 使用C, 也能引入OO的设计思想和方法, 在团队的C/C++分歧严重时可能非常有用.


#### LW\_OOPC的缺点


1. 无法支持重载(C语言不支持所致)


[外链图片转存中...(img-udJLQGDd-1715508808610)]
[外链图片转存中...(img-p6ZACs7W-1715508808610)]

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值