广轻电气091 发表于 2019-12-2 08:48:58

C 语言面向对象编程 – 继承

上一篇文章主要讲述了 C 语言面向对象编程 – 封装的简单概念和实现。
上一篇文章的具体内容,可以查看以下链接: C 语言面向对象编程 - 封装

本篇文章继续来讨论一下,如何使用 C 语言实现面向对象编程的一个重要特性:继承。

继承就是基于一个已有的类(一般称作父类或基类),再去重新声明或创建一个新的类,这个类可以称为子类或派生类。
子类或派生类可以访问父类的数据和函数,然后子类里面又添加了自己的属性和数据。
在 C 语言里面,可以通过结构体嵌套的方式去实现类的单继承(暂不考虑多重继承),但有一点注意事项,就是在结构体嵌套时,父类对象需要放在结构体成员的第一个位置。

现在,我们基于已有的 coordinate 类作为父类,再重新定义一个 rectangle 派生类。
在上一篇文章代码的基础上,我们修改一下父类 coordinate,把操作函数通过函数指针的方式封装在结构体内,让对象的封装程度进一步提高。

修改后的父类coordinate代码,如下所示:
头文件 coordinate.h

在头文件 coordinate.h 里,声明一个位置类,类里面提供了坐标属性 x 和 y,还提供了属性的操作函数指针。
头文件对外提供 coordinate_init 和 coordinate_uninit 两个函数,用来初始化对象和解除初始化。

源文件 coordinate.c

在源文件 coordinate.c 里,属性的操作函数都使用 static 进行声明,不允许外部调用。
在函数 coordinate_init 中,主要进行了属性赋值,并注册操作函数指针,后面可以直接通过函数指针对操作函数进行调用。在函数 coordinate_uninit 中,主要是清除各个属性的赋值。
至此,整个父类 coordinate 修改完成,父类把属性和属性的操作函数都封装在结构体内,其封装程度已经比较高,外部不能直接调用父类的属性操作函数,必须通过函数指针的方式进行调用。

接下来,我们基于父类 coordinate ,重新声明一个子类 rectangle ,子类在头文件中的声明,如下所示:
头文件 rectangle.h

源文件 rectangle.c

在头文件 rectangle.h 里面,通过include包含了父类coordinate的接口,并创建了一个新的结构体,用于声明一个 rectangle 类。
这个结构体把父类 coordinate 放在了第一个成员的位置,同时新增了自己的两个属性,宽度width和高度height。

在源文件rectangle.c 里面,rectangle_create 函数用于创建一个 P_RECTANGLE_T 类型的对象,并为其分配内存空间。
分配成功后,对调用父类 coordinate_init函数,对父类的各种属性进行初始化,并同时对自身的属性 width 和 height 进行初始化,最后返回创建成功后的对象指针。
rectangle_destroy 用于父类对象属性的解除初始化,并为对象属性重新分配默认值,释放之前申请的内存空间,销毁 rectangle 对象。

从头文件 rectangle.h 和源文件 rectangle.c 可以看出,子类 rectangle 是基于其父类 coordinate 进行声明和构建的,因为矩形rectangle除了 width 和 height 属性外,还包含了坐标 x 和 y 属性。

把父类放在结构体成员的第一个位置,是由于结构体内存的连续性,可以很安全地进行强制类型转换。
举个例子:假如一个函数要求传入的参数是 COORDINATE_T 类型,但可以通过强制类型转换,传入 RECTANGLE_T 类型的参数,具体的使用方法,可以查看以下测试函数。


测试函数的运行效果,如下图所示:


通过上述代码的测试,可以总结出以下几点内容:
1、外部函数可以通过子类直接使用父类的各个成员,但只能通过子类结构体的第一个成员来访问。
2、父类放在子类结构体的第一个位置,由于结构体内存的连续性,因此可以通过强制类型转换来直接访问。
3、由于C语言结构体的特性,即使子类存在与父类同名的函数,父类的函数不会被子类的函数覆盖和重写,因此,子类与父类之间不存在函数重载。

源码下载地址:https://github.com/embediot/my_program_test

本文PDF下载:

newselect 发表于 2019-12-2 08:53:08

谢谢, 学习了

security 发表于 2019-12-2 09:11:38

我觉得啊,面向对象这事,
可以直接看李先静的这篇博文,已经写得够透彻了。
楼主可以参考一下:「再谈面向对象的三大特性」

广轻电气091 发表于 2019-12-2 09:16:23

newselect 发表于 2019-12-2 08:53
谢谢, 学习了

感谢阅读!

广轻电气091 发表于 2019-12-2 09:16:40

security 发表于 2019-12-2 09:11
我觉得啊,面向对象这事,
可以直接看李先静的这篇博文,已经写得够透彻了。
楼主可以参考一下:「再谈面向 ...

感谢建议!学习了

gushuailove 发表于 2019-12-2 09:17:57

找到“强制类型转换”就明白什么回事了

sipure 发表于 2019-12-2 09:56:47

C不是为面向对象而生的,C++才是.

onepower 发表于 2019-12-2 10:27:28

还在那句: 这么折腾赶啥?? 直接上C++不就好了!!

广轻电气091 发表于 2019-12-2 10:27:51

gushuailove 发表于 2019-12-2 09:17
找到“强制类型转换”就明白什么回事了

烦请指导{:lol:} {:handshake:}

广轻电气091 发表于 2019-12-2 10:29:46

sipure 发表于 2019-12-2 09:56
C不是为面向对象而生的,C++才是.

是的,C++ 是为面向对象而生的。C 语言大多数应用都是以面向过程为主。But,在使用 C 语言进行大中型程序开发的时候,面向对象的思想还是有值得借鉴的地方。比如,操作系统开发

广轻电气091 发表于 2019-12-2 10:30:14

onepower 发表于 2019-12-2 10:27
还在那句: 这么折腾赶啥?? 直接上C++不就好了!!

折腾的过程可以收获不少知识{:lol:}

johnlj 发表于 2019-12-2 11:19:57

lz干的工作是让c也能做到C++差不多功能,从而强化对OOP的理解吧

广轻电气091 发表于 2019-12-2 11:33:15

johnlj 发表于 2019-12-2 11:19
lz干的工作是让c也能做到C++差不多功能,从而强化对OOP的理解吧

我在学习怎样让 C 代码更加模块化,耦合度更低{:lol:}

takashiki 发表于 2019-12-2 14:44:30

广轻电气091 发表于 2019-12-2 11:33
我在学习怎样让 C 代码更加模块化,耦合度更低

恰恰相反,您这样封装之后,不是更加模块化了,而是更加耦合了。所以,我其实极力反对这种挂羊头卖狗肉的行为。
您这个坐标结构中,x、y是坐标的数据,而moveby、get_x、get_y则无论如何也不能是坐标的数据。但是您再看看您的结构体定义,这就是个函数指针型的数据吧。
我再举个例子,就以小动物为例吧。他可以有头有身子有脚,派生的可能有尾巴或者有翅膀。但是,您不能把能走能飞作为它的一个组成部分,而只是具备某种行为。

广轻电气091 发表于 2019-12-2 15:09:26

takashiki 发表于 2019-12-2 14:44
恰恰相反,您这样封装之后,不是更加模块化了,而是更加耦合了。所以,我其实极力反对这种挂羊头卖狗肉的 ...

感谢指导!你说得有一定道理,对于 x 和 y,它俩是属性,正确来说,如果把函数指针(具体操作)都封装进去的话,结构体就不能命名为 coordinate 了。文中的结构体命名可能会引起歧义

cat_li 发表于 2019-12-2 15:10:56

为啥不直接用C++呢

广轻电气091 发表于 2019-12-2 15:12:14

cat_li 发表于 2019-12-2 15:10
为啥不直接用C++呢

学习中,学习中

slzm40 发表于 2019-12-2 15:18:09

lz要不看一下golang??当然,它不支持单片机,你会发现,相见恨晚,
当然你要是只是搞单片机的,当我没说.

广轻电气091 发表于 2019-12-2 15:53:48

slzm40 发表于 2019-12-2 15:18
lz要不看一下golang??当然,它不支持单片机,你会发现,相见恨晚,
当然你要是只是搞单片机的,当我没说. ...

感谢建议!我目前工作的技术栈是嵌入式相关的,主要编程语言是 C/C++,所以才如此折腾 C 语言面向对象编程

samo110 发表于 2019-12-2 16:46:01

之前看RT-Thread的代码也是大量使用这样的思想

广轻电气091 发表于 2019-12-2 16:55:04

samo110 发表于 2019-12-2 16:46
之前看RT-Thread的代码也是大量使用这样的思想

操作系统底层一般都是 C 语言面向对象编程

rifjft 发表于 2019-12-2 17:45:15

{:lol:}这资料整理得越来越专业了,可以考虑出教程、在线培训,向C专家迈进{:lol:} 是时候考虑一个响亮的名号了{:titter:}

bhwyg 发表于 2019-12-2 18:15:12

本帖最后由 bhwyg 于 2019-12-2 18:17 编辑

当年入门python时,有本书里介绍类、继承的概述,觉得很好。以下是印象中的内容,不一定全对:
以动物、牛、羊为例,
父类与子类:牛羊是动物的一种特例,所以动物是父类,是子类。
属性与方法:动物呼吸、发声叫方法,动物外表颜色、大小叫属性;
继承:所有动物都会发声,所以动物拥有发声这个方法,牛羊继承动物类后就拥有了发声的方法,但是牛羊发声是不同的,所以这两者可以重写动物的发声方法;
类与实例的区别:以上说的都是类,口中说的“动物”是类,草场上真实存在的牛A、牛B、羊A、羊B才是实例。类就相当于结构体,实例就相当于结构体定义的变量。
除此之外,还有一些初始化方法之类的,就是在类中定义了一些初始化方法(封装在内部的子程序或函数),实例时这些方法会自动执行。
以上这些高大上的东西,在C语言中也可以用结构体模拟出来。
---------
我是学了这个之后在C# LabVIEW中又遇到了类似的,发现形式不同,但原理都一样。

广轻电气091 发表于 2019-12-2 18:16:29

rifjft 发表于 2019-12-2 17:45
这资料整理得越来越专业了,可以考虑出教程、在线培训,向C专家迈进 是时候考虑一个响亮的名 ...

言重了,小弟才疏浅薄,还在努力学习中

广轻电气091 发表于 2019-12-2 18:17:10

bhwyg 发表于 2019-12-2 18:15
当年入门python时,有本书里介绍类、继承的概述,觉得很好。以下是印象中的内容,不一定全对:
以动物、牛 ...

是的,原理和概念都差不多,学习这些基础知识,是一个艰难的过程

bhwyg 发表于 2019-12-2 18:18:44

广轻电气091 发表于 2019-12-2 18:17
是的,原理和概念都差不多,学习这些基础知识,是一个艰难的过程

如您签名所言,思想很重要。思想转变过来,这玩意很容易懂

takashiki 发表于 2019-12-2 19:13:14

广轻电气091 发表于 2019-12-2 18:17
是的,原理和概念都差不多,学习这些基础知识,是一个艰难的过程

LZ,因为我从不知道C语言如何面向对象,就拿C++的概念来类比吧。那么,我觉得您可能对这个面向对象是不是理解有偏差,主要是数据和代码没有分离,形成耦合,这样做出来的对象事实上是基于对象,而不是面向对象。
首先,封装。没什么说的,直接就是结构体,里面装满数据。typedef struct coordinate {
    short x, y;
} COORDINATE_T, *P_COORDINATE_T;
然后是建立销毁对象。你那些init、uninit没有问题。但是从形式上来看,init不能用作创建一个xxx对象而是对已有的对象进行初始化,这里描述是有问题的。
非虚成员函数/属性:直接就用void coordinate_set_x(P_COORDINATE_T obj, short x); 不是更好吗。
虚函数:C++弄这玩意用上了虚表,因为C++中的虚方法都是属于类本身的,是代码而不是数据。对象通过自身的虚表去访问虚方法,改变虚表指针就形成了多态。
对象也可以拥有自己的函数指针(数据),但这不是用来继承父类的,而是用来实现回调,大家习惯上把这个东西叫做事件,封装后改个名字叫委托。

RAMILE 发表于 2019-12-2 20:43:51

最近正在研究 openocd

里面是标准的C面向对象写法,openocd是开源MCU调试软件,支持各种CPU体系,对于每一种支持对象,都有一个标准的struct做接口,类似下面的代码
struct target_type riscv011_target = {
      .name = "riscv",

      .init_target = init_target,
      .deinit_target = deinit_target,
      .examine = examine,

      /* poll current target status */
      .poll = riscv011_poll,

      .halt = halt,
      .resume = riscv011_resume,
      .step = step,

      .assert_reset = assert_reset,
      .deassert_reset = deassert_reset,

      .read_memory = read_memory,
      .write_memory = write_memory,

      .arch_state = arch_state,
};

广轻电气091 发表于 2019-12-3 08:55:00

takashiki 发表于 2019-12-2 19:13
LZ,因为我从不知道C语言如何面向对象,就拿C++的概念来类比吧。那么,我觉得您可能对这个面向对象是不是 ...

感谢指导!看来我还需要继续努力学习C++。请问您对“数据和代码”的分离,有何高见?{:handshake:}

广轻电气091 发表于 2019-12-3 08:55:30

RAMILE 发表于 2019-12-2 20:43
最近正在研究 openocd

里面是标准的C面向对象写法,openocd是开源MCU调试软件,支持各种CPU体系,对于每一 ...

Linux Kernel里面大量使用了这种方式

takashiki 发表于 2019-12-5 09:37:31

广轻电气091 发表于 2019-12-3 08:55
感谢指导!看来我还需要继续努力学习C++。请问您对“数据和代码”的分离,有何高见?...

最近都没多少时间没有及时回复,见谅。
我本人其实并不怎么推崇使用C去模拟C++的面向对象,更加不推崇过度设计的设计模式。上帝给你一把水果刀,是用来削水果的,而不是用来杀牛的。同样,用杀牛刀来杀鸡,同样也不可取。C语言可能为了效率的考虑会嵌入汇编,但是C#嵌入汇编似乎并不可取(虽然可以做到)。本回复中可能有一些设计模式,但是我不推崇过度设计,合适即可。

因为C语言本身的限制,数据是数据,代码是代码。C的结构体中是无法直接放入代码的,要放代码,必须是函数指针指向代码,这样效率就差了。本来就是生死看淡不服就干的性子,结果封装后要经过一次二传手。

现在进行封装,比如我要进行压缩文件类的封装。假如我就在单片机上压缩解压缩,那么着重于速度,于是我选用了QuickLZ、LZ4、LZO、Snappy等等快速压缩算法。
那么压缩文件有哪些数据呢?有大家共同的,比如文件名啊,文件位置啊,大小啊啥啥的一堆,搞成CompressFile类。然后也有自己特有的,比如LZ4需要超大的缓冲区(12~13MB,快的代价),LZO解压完全不需要缓冲区,还有一些杂七杂八的,那就从CompressFile派生出来,比如QuickLZCompressFile、LZ4CompressFile、LZOCompressFile类。
到这里为止,都是数据。

那么代码呢?我重新设计一个类,只封装操作行为。比如打开文件、关闭文件、压缩、解压缩、获取文件大小,获取解压后的大小等等。
typedef struct {
        CompressFile* (*Open)(const char* Name, const char* Mode);
        void (*Close)(CompressFile* File);
        int (*Compress)(CompressFile* File);
        int (*Decompress)(CompressFile* File);
        ...
} ICompressFileOperator; 这个类中,设计的操作都是关于CompressFile的,是在操作数据。
QuickLZ、LZ4、LZO、Snappy等等这些实际的文件操作都是从ICompressFileOperator派生出来并各自实现(多态),比如QuickLZCompressFileOperator等等。

封装、继承、多态现在都有了,那么我们对外接口,要高内聚低耦合,那么只公布ICompressFileOperator,并且公布CompressFile*的声明typedef struct CompressFile CompressFile; 而定义则是内部使用。
那么外部只有一个ICompressFileOperator,怎么用啊?还得有个产生它的实例的方法,这里使用工厂模式(不管是简单工厂模式还是抽象工厂模式),总之通过ICompressFileOperator* CompressFactory.CreateFileOperator(enum CompressType); 或者没封装的ICompressFileOperator* CreateCompressFileOperator(enum CompressType);产生这个实例。

下面是内部耦合的简单工厂模式,要继续解耦就改用抽象工厂模式,代码太多不写了。里面就简单点的单例模式返回:
ICompressFileOperator* CreateCompressFileOperator(enum CompressType type) {
        switch(type) {
        case CompressType_QuickLZ:
                {
                        static QuickLZCompressFileOperator quicklz_FileOp;
                        return &quicklz_FileOp;
                }
        case CompressType_LZ4:
                ...
        ...
        default:
                return NULL;
};

这样搞出来的代码和数据是完全分离的,而且代码只有接口暴露,实现都不暴露。数据只有一个不完全类型,或者干脆搞成HCOMPRESSFILE,细节全部隐藏。据说,这是面向对象的一个应用方向:面向接口编程。

进行到现在,忽然领导要求,你解压到一定程度你得告诉我一声,我得根据你们每个人单独安排具体事务。这时得把函数指针当做CompressFile的数据了,每个人都拥有不同的处理方法,这个处理方法是在解压这个过程中调用的,C语言就叫回调,很多高级语言改个名字,叫事件。

canspider 发表于 2019-12-5 09:55:05

本帖最后由 canspider 于 2019-12-5 09:56 编辑

takashiki 发表于 2019-12-5 09:37
最近都没多少时间没有及时回复,见谅。
我本人其实并不怎么推崇使用C去模拟C++的面向对象,更加不推崇过 ...

看完你的这个回复,终于解决了我很久以来的一个困惑。
我一直在关注一个名叫TeenyUSB的轻量级USB协议栈(https://github.com/xtoolbox/TeenyUSB),里面的设备和主机代码中大量用到了xxx_backend这个东西
它里面创建一个设备时,需要这样
tusb_cdc_device_t cdc_dev = {
.backend = &cdc_device_backend,
.ep_in = 1,
.ep_out = 1,
.ep_int = 8,
.on_recv_data = cdc_recv_data,
.on_send_done = cdc_send_done,
.on_line_coding_change = cdc_line_coding_change,
.rx_buf = cdc_buf,
.rx_size = sizeof(cdc_buf),
};

创建一个主机类时,需要这样
static const tusbh_boot_key_class_t cls_boot_key = {
    .backend = &tusbh_boot_keyboard_backend,
    .on_key = process_key
};
static const tusbh_msc_class_t cls_msc_bot = {
    .backend = &tusbh_msc_bot_backend,
    .mount = msc_ff_mount,
    .unmount = msc_ff_unmount,
};

我之前一直不明白为什么可以直接操作的东西非加一个backend绕一下。
现在知道了, 这就是为了将不同的USB接口抽象成一个通用的操作,把这些操作都放在backend里面,而这些backend又放在了结构体前面的固定位置,模拟出了你说的Ixxx接口的效果。
核心部分只通过Ixxx接口来操作这些不同的设备类型,这样要增加或是删掉一个接口就不用去动核心部分的代码了。

当看别人的代码云里雾里的时候真的很需要像您和楼主这样的人从大的架构上讲一些设计思路

广轻电气091 发表于 2019-12-5 10:30:28

本帖最后由 广轻电气091 于 2019-12-5 10:34 编辑

takashiki 发表于 2019-12-5 09:37
最近都没多少时间没有及时回复,见谅。
我本人其实并不怎么推崇使用C去模拟C++的面向对象,更加不推崇过 ...

感谢指导!你的回复值得仔细斟酌几遍。最近也一直在思考,如何培养自己的面向对象思维,毕竟,世间万物皆对象。

------------------------------------------------------------
以下内容,摘自网络。

上课的时候,老师说:什么是对象? 然后回答:万物皆对象。要用面向对象的思想去看待一切事物,平时生活中就要用这种思路。

1.见过一个经典的问题,就是在你为你的类创建方法的时候,问自己一句“是狗在摇尾巴,还是你在摇狗尾巴”(是类本身的行为,还是你操作类作出的行为);

2. 多看代码,多自己写代码;

3. 如果不看设计模式你只能学会面向对象是如何实现的(语法),看了才知道如何设计。当然也有看了设计模式只知道设计模式是怎么实现的,但是还是不会设计;

4.面向对象分析眼中只有“存在”,而不是什么“所有”。所谓“所有x都做了a这件事”,是指你用实际行动真正否定了“存在一个x它没有做a这件事”。

5.面向对象是对个体建模,然后将个体的类型展现出来。比如我们要建立狗的模型,想表达狗是一种动物,同时也是一种宠物,那么我们就从两个类型来研究狗。
当你研究了许多具体的个体,才能将类型确定下来,并为同一类型的对象(的各个方面)写出文档。如果不仔细研究个体,空洞地从抽象出发,往往滥用概念。

6.面向对象是从实际应用领域出发,为真实的对象建模。而不是简单地把真实对象用一些计算机领域的数据(什么整数、数组、字符串之类的术语)来代替。避免脱离实际的术语,避免使用计算机领域的术语,而应该用自然而然的应用领域的术语来描述对象、对象类、属性、继承、行为、用例、状态、事件、规则等等。而使用计算机领域的术语进行系统设计、(与硬件等的)接口设计,则是在清晰完整对业务建模之后的事。

7.查询是一个动词,动词一般是类的一个方法,类一般是名词

8.书生气是做不了面向对象设计的。完全不需要套用书本上的所谓模式,你先去搞清楚现实中有哪些东西是独立存在自我管理和自我发展的,把他们的实际行为逻辑用大白话说清楚,然后再来看看跟面向对象理论有什么契合之处吧。不要想着用什么编程的技巧堆砌起来就成了面向对象分析和设计技术了。

slzm40 发表于 2019-12-5 10:31:52

takashiki 发表于 2019-12-5 09:37
最近都没多少时间没有及时回复,见谅。
我本人其实并不怎么推崇使用C去模拟C++的面向对象,更加不推崇过 ...

完全Linux哲学,一切皆文件, 所有获得的File视为文件, 或者叫句柄, 所有相关在持有文件或句柄中, 所有的实现细节都在api内部封装掉.我只要fd.
不过我还是不太建议在c中搞面向对象,特别是单片机这类的, 比如读写一类API的引起内存方面的拷贝,还是太消耗了.

广轻电气091 发表于 2019-12-5 10:33:40

canspider 发表于 2019-12-5 09:55
看完你的这个回复,终于解决了我很久以来的一个困惑。
我一直在关注一个名叫TeenyUSB的轻量级USB协议栈( ...

是的,如果接触过系统内核或者驱动代码,当面对这类结构的代码,就会不自而然地想从另一个思维角度去考虑问题。
所以,我觉得用 C 语言去模拟面向对象,最关键的一点是培养自己的面向对象思维以及锻炼 C 语言的高级用法。
并不是以偏概全地比较哪种语言孰优孰劣。每种语言针对不同的场合都有其优势和劣势,合适选用即可。

广轻电气091 发表于 2019-12-5 10:37:24

slzm40 发表于 2019-12-5 10:31
完全Linux哲学,一切皆文件, 所有获得的File视为文件, 或者叫句柄, 所有相关在持有文件或句柄中, 所有的实 ...

感谢指导!合适的就是最好的,针对某些特定场合,确实不适宜用 C 语言面向对象编程。

slzm40 发表于 2019-12-5 10:39:53

本帖最后由 slzm40 于 2019-12-5 11:02 编辑

广轻电气091 发表于 2019-12-5 10:30
感谢指导!你的回复值得仔细斟酌几遍。最近也一直在思考,如何培养自己的面向对象思维,毕竟,世间万物皆 ...

面向对象的编程思想还是不太建议在c中去培养,很浪费时间,特别是用c去实现面向对象,会在面向对象而面向对象,在c的内部实现过度挣扎.
各种哲学,
1. Linux 一切皆文件
2. golang 大道至简,皆struct,皆interface
3. java 一切皆对象
4. restful 一切皆资源,所有的操作皆面向资源操作
5. c 面向接口编程,所有细节都在接口内封装, 这个是我瞎编的

广轻电气091 发表于 2019-12-5 10:45:36

slzm40 发表于 2019-12-5 10:39
面向对象的编程思想还是不太建议在c中去培养,很浪费时间,特别是用c去实现面向对象,会在面向对象而面向对 ...

感谢提供建议!对于 C 语言的高级用法,我发现如果项目中需要进行模块解耦,大多数都是面向接口编程。而我发现之前的两篇文章,更多的是使用面向对象里面的面向接口编程,而非完全面向对象。

有一段话,摘自网络,感觉很有启发意义:

上课的时候,老师说:什么是对象? 然后回答:万物皆对象。要用面向对象的思想去看待一切事物,平时生活中就要用这种思路。

1.见过一个经典的问题,就是在你为你的类创建方法的时候,问自己一句“是狗在摇尾巴,还是你在摇狗尾巴”(是类本身的行为,还是你操作类作出的行为);

2.书生气是做不了面向对象设计的。完全不需要套用书本上的所谓模式,你先去搞清楚现实中有哪些东西是独立存在自我管理和自我发展的,把他们的实际行为逻辑用大白话说清楚,然后再来看看跟面向对象理论有什么契合之处吧。不要想着用什么编程的技巧堆砌起来就成了面向对象分析和设计技术了。

slzm40 发表于 2019-12-5 10:58:53

本帖最后由 slzm40 于 2019-12-5 11:01 编辑

广轻电气091 发表于 2019-12-5 10:45
感谢提供建议!对于 C 语言的高级用法,我发现如果项目中需要进行模块解耦,大多数都是面向接口编程。而 ...

面向对象的思想建立起来,建议是再学一门简单的面向对象语言,如Python. 无非是数据,行为,多态,继承一类的.
非完全体的面向对象和面向接口揉合,最终你暴露给用户的肯定只有接口. c语言特性使你不得不这么做,这个才不会导致哪个神级程序员改了你非完全体的面向对象的数据.
当然,也不是说面向对象不适合c,只是不适合完全实现, 语言特性使得这个struct不得不被暴露, 我倒是经常用于底层数据抽象封装,这样只有自己知道怎么改. 而调用的人无需关注我的数据. 他们能看到的只有接口,而接口是看不到数据的.
这也是我很喜欢linux内核和api层的实现.

security 发表于 2019-12-5 11:00:09

广轻电气091 发表于 2019-12-5 10:45
感谢提供建议!对于 C 语言的高级用法,我发现如果项目中需要进行模块解耦,大多数都是面向接口编程。而 ...

推荐看看李先静的「系统程序员成长计划」。
还有「C程序设计的抽象思维」的第三部分:数据抽象。
还有 「C语言程序设计:现代方法(第 2 版)」的第 19 章,这本书,可以戳这边,在 107 楼,那边有网盘地址。
还有 「C语言接口与实现」。

广轻电气091 发表于 2019-12-5 11:07:07

security 发表于 2019-12-5 11:00
推荐看看李先静的「系统程序员成长计划」。
还有「C程序设计的抽象思维」的第三部分:数据抽象。
还有 「 ...

感谢建议!已记录。

广轻电气091 发表于 2019-12-5 11:07:48

slzm40 发表于 2019-12-5 10:58
面向对象的思想建立起来,建议是再学一门简单的面向对象语言,如Python. 无非是数据,行为,多态,继承一类的. ...

是的,C 语言的语法特性并不能让其真正面向对象,最多就是面向接口。

xuwuhan 发表于 2019-12-6 09:55:40

感谢分享

didadida 发表于 2019-12-6 10:40:49

大神们的讨论收下了,谢谢{:lol:}{:lol:}

eddia2012 发表于 2019-12-6 11:46:17

好文,学习了!{:lol:}

广轻电气091 发表于 2019-12-6 13:32:27

xuwuhan 发表于 2019-12-6 09:55
感谢分享

感谢阅读

广轻电气091 发表于 2019-12-6 13:32:44

didadida 发表于 2019-12-6 10:40
大神们的讨论收下了,谢谢

感谢关注

广轻电气091 发表于 2019-12-6 13:33:03

eddia2012 发表于 2019-12-6 11:46
好文,学习了!

感谢支持

qiaogang2006 发表于 2019-12-8 23:48:30

本帖最后由 qiaogang2006 于 2019-12-9 00:20 编辑

takashiki 发表于 2019-12-5 09:37
最近都没多少时间没有及时回复,见谅。
我本人其实并不怎么推崇使用C去模拟C++的面向对象,更加不推崇过 ...

大神,小白请教“对外接口”使用的问题,假如单片机中有个类X,其中需要压缩抽象类,大体代码如下(两种方法主要区别是构造函数不同),写单片机时忍不住写成A了,但书上好像都是方法B,好处是隐藏了压缩类接口吗?

方法A:实例化X类对象时,外部传入一个已经实例化的压缩对象y,实例化:Xx(&y);
class X
{
    X( ICompressFileOperator * c ): _comp(c) { ... }

    ICompressFileOperator * _comp;
}
class lz4Compress: public ICompressFileOperator
{
}
实例化:
    lz4Compressy;
    Xx(&y);

方法B:压缩类CPP文件再建了一个函数,X类构造函数调用这个函数,实例化:Xx();
ICompressFileOperator& CreatCompressFileOperator()
{//为什么要这个函数?
    return * new lz4Compress;
}
class X
{
    X( )
    {
      _comp = CreatCompressFileOperator();
    }

    ICompressFileOperator * _comp;
}
实例化:
    Xx();

takashiki 发表于 2019-12-9 06:27:17

qiaogang2006 发表于 2019-12-8 23:48
大神,小白请教“对外接口”使用的问题,假如单片机中有个类X,其中需要压缩抽象类,大体代码如下(两种 ...

方式A的抽象程度不够,对外仍然要用到具体的类lz4Compressy; 方式B这个具体的类只是对内使用的,对外则被隐藏(解耦)了。
从实际的大型软件协作开发来讲,比如压缩类是在一个文件中实现的(比如dll),第一版它只实现了lz4Compress ,第二版又实现了LzoCompress,后来老板觉得用户反馈说就没有压缩率高的又塞了个LzmaCompress进去。而实例化则是另一个文件中实现的(比如exe),第一版就搞好了。那么后续打补丁的话,方式A必须连同exe文件一起打补丁,方式B则只需要对dll打补丁就行了。
方式B这种简单的工厂模式仍然存在内部耦合,继续解耦就是注册模式,又多一层。这个模式不在Java设计模式的23个模式之内,由于语言特性Java没有全局变量不好实现,但是C++就很简单了。

qiaogang2006 发表于 2019-12-9 09:54:01

本帖最后由 qiaogang2006 于 2019-12-9 09:55 编辑

takashiki 发表于 2019-12-9 06:27
方式A的抽象程度不够,对外仍然要用到具体的类lz4Compressy; 方式B这个具体的类只是对内使用的,对外则 ...

多谢指教,但就是这个函数没有类名,看程序时很别扭,还要想一想它是属于哪里的,有没有别的好的方式呢?

还有,3楼 @security 说的李先静的这篇博文「再谈面向对象的三大特性」,最后面有个例子(如下),感觉跟我的方法A是一个套路,为什么他不写成方法B,把norflash、nandflash、内存/磁盘 再做成工厂模式,在构造时选择是norflash还是nandflash、内存/磁盘呢?

在『多态』的帮助下,情况就会大不相同了。A这个类依赖于B这个类,我们可以把B定义成一个接口,让使用A这个类的使用者传入进来,也就是所谓的依赖注入。如果你想重用A这个类,你可以为它定制一个B接口的实现。比如,我最近在一个只有8K内存的硬件上,为一块norflash写了一个简单的文件系统(且看作是A类),如果我直接去调用norflash的API(且看作是B类),就会让文件系统(A类)与norflash的API(B类)紧密耦合到一起,这就会让文件系统的重用性大打折扣。
我的做法是定义了一个块设备的接口(即B接口):
typedef unsigned short block_num_t;

struct _block_dev_t;
typedef struct _block_dev_t block_dev_t;

typedef block_num_t (*block_dev_get_block_nr_t)(block_dev_t* dev);
typedef bool_t (*block_dev_read_block_t)(block_dev_t* dev, block_num_t block_num, void* buff);
typedef bool_t (*block_dev_write_block_t)(block_dev_t* dev, block_num_t block_num, const void* buff);
typedef void   (*block_dev_destroy_t)(block_dev_t* dev);

struct _block_dev_t {
    block_dev_get_block_nr_t   get_block_nr;
    block_dev_write_block_t    write_block;
    block_dev_read_block_t   read_block;
    block_dev_destroy_t      destroy;
};
在初始化文件系统时,把块设备注入进来:
bool_t sfs_init(sfs_t* fs, block_dev_t* dev);
这样,文件系统只与块设备接口交互,不需要关心实现是norflash、nandflash、内存还是磁盘。而且带来几个附加好处:
可以在PC上做文件系统的单元测试。在PC上,用内存模拟一个块设备,文件系统可以正常工作了。
可以通过装饰模式为块设备添加磨损均衡算法和坏块管理算法。这些算法和文件系统都可以独立重用。
————————————————
版权声明:本文为CSDN博主「李先静」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/absurd/article/details/53585009

takashiki 发表于 2019-12-9 10:18:50

qiaogang2006 发表于 2019-12-9 09:54
多谢指教,但就是这个函数没有类名,看程序时很别扭,还要想一想它是属于哪里的,有没有别的好的方式呢? ...

没有类名看程序时很别扭,这,这,这...
您用类的静态方法包装一下,或者塞到命名空间中去,形式上就有了。要我说,都是Java给带坏了。

抽象是高内聚低耦合必然要面对的,然而教科书上的面向对象编程似乎弱化了它的地位(有的算作封装,有的干脆不抽象全是实例),反正符合三大原则就是面向对象,所以就要涉及到设计模式。但是设计模式带来的一个副作用就是代码冗长占资源,所以由您自己取舍了。
比如你说的这个,假如他只用了一种块设备,其他的通通不用,那么用B方法的话,其他没用的类会全部被编译器优化掉。而用A方法的话,则都参与编译了,生成的hex一下子长胖很多。

jaky80000 发表于 2019-12-11 11:40:20

楼主位的代码我的理解还是你说的父类被当做了子类的一个结构体成员来用的,因为在使用的时候还是调用了你说的父类的成员变量。
我的愚见。

广轻电气091 发表于 2019-12-11 12:46:41

jaky80000 发表于 2019-12-11 11:40
楼主位的代码我的理解还是你说的父类被当做了子类的一个结构体成员来用的,因为在使用的时候还是调用了你说 ...

是的,因为 C 语言的语法特性,限制了只能这样使用

雨醉江南 发表于 2019-12-11 20:50:32

学习了,谢谢

hanshiruo 发表于 2019-12-11 21:57:34

学习了,谢谢

广轻电气091 发表于 2019-12-12 08:49:15

雨醉江南 发表于 2019-12-11 20:50
学习了,谢谢

感谢关注!

广轻电气091 发表于 2019-12-12 08:49:31

hanshiruo 发表于 2019-12-11 21:57
学习了,谢谢

感谢关注!

亦言567 发表于 2020-5-28 21:43:55

向楼主学习

enwa 发表于 2020-6-13 22:38:25


向楼主学习{:handshake:}

广轻电气091 发表于 2020-6-29 14:44:16

enwa 发表于 2020-6-13 22:38
向楼主学习

感谢关注!

广轻电气091 发表于 2020-6-29 14:44:32

亦言567 发表于 2020-5-28 21:43
向楼主学习

感谢支持!

ALyang 发表于 2020-8-11 17:50:49

谢谢.....学习了

lushanlq 发表于 2020-10-26 12:41:48

学习了,谢谢大家的精彩讨论。

清新怡人 发表于 2022-6-10 09:04:04

期待楼主出一期多态
页: [1]
查看完整版本: C 语言面向对象编程 – 继承