【问题标题】:Providing helper functions when rolling out own structures在推出自己的结构时提供辅助功能
【发布时间】:2019-12-16 08:03:57
【问题描述】:

如果我正在开发一个 C 共享库并且我有自己的结构。为了使库使用者更容易对这些结构实例进行通用操作,我可以在结构本身内部提供指向此类函数的函数指针吗?这是一个好习惯吗?在多线程中是否存在与不同参数并行调用实用函数等的问题?

我知道它更接近 C++ 类,但我希望坚持使用 C 并学习如何用过程语言而不是 OOP 来完成它。

举个例子

typedef struct tag tag;
typedef struct my_custom_struct my_custom_struct;

struct tag
{
    // ...
};

struct my_custom_struct
{
    tag *tags;
    my_custom_struct* (*add_tag)(my_custom_struct* str, tag *tag);  
};

my_custom_struct* add_tag(my_custom_struct* str, tag *tag)
{
    // ...
}

其中 add_tag 是一个帮助器,它设法将标签添加到 *str 内的标签列表中。 我在 libjson-c 中看到了这种模式,就像这里-http://json-c.github.io/json-c/json-c-0.13.1/doc/html/structarray__list.htmlarray_list 内部提供了一个函数指针来帮助释放它。

【问题讨论】:

  • 为什么需要指向结构内函数的指针?仅仅记录函数的 API 不是更简单吗?如果同一个结构可以包含不同的指针,那么在结构中使用指向函数的指针会很有帮助。否则,它是混淆的,没有立即明显的好处。 (C 不是函数式语言;它是过程式语言。)
  • 作为一个必须维护 C 代码库并试图使用结构、不透明指针和显式 vtable 模拟继承和 C++ 类的人,请不要走下那个兔子洞。这绝对是一场噩梦。调试非常困难,因为使用函数指针会增加间接级别,因此很难找到需要放置断点的位置。
  • 旁注:C 是一种命令式语言。 函数式 语言是另一回事(规范示例:Lisp)。 Java 和 C++ 比 C 具有更多的函数式编程特性和功能。
  • 你可以,你不应该。如果函数对于结构类型中结构的每个实例都是相同的,那么指针就是浪费空间,浪费时间来初始化它,在复制结构时复制它,等等。还有其他方法可以将函数与类型关联起来,首先是简单地为其名称使用公共前缀。
  • 如果您将函数指针放在struct mystruct 中,以便您可以使用o->func(o) 而不是mystruct__func(o),因为“面向对象编程”,您'做错了。有些人还会因为代码可读性差而不鼓励类型定义。

标签: c struct


【解决方案1】:

使库对这些结构实例的通用操作更容易 消费者,我可以在里面提供指向这些函数的函数指针吗 结构本身?

可以为您的结构赋予函数指针成员,指向其参数包括指向您的结构类型的指针的函数类型,并且旨在或多或少地像 C++ 实例方法一样使用,或多或少地呈现在问题中。

这是一个好习惯吗?

TL;DR:没有。

您将遇到的第一个问题是正确初始化这些指针成员。尽管名称对应,但结构实例中的函数指针不会自动初始化为指向特定函数。除非您使结构类型不透明,否则用户可以(毫无疑问有时)声明实例而不调用您为此目的提供的任何构造函数模拟函数,然后就会出现混乱。

如果您确实使结构不透明(这毕竟不是一个坏主意),那么无论如何您都需要非成员函数,因为您的用户将无法直接访问函数指针。也许是这样的:

struct my_custom_struct *my_add_tag(struct my_custom_struct *str, tag *tag) {
    return str->add_tag(str, tag);
}

但是,如果您要提供这一点,那么额外的间接级别有什么意义呢? (答案:唯一的理由是在不同的情况下,函数指针可以指向不同的函数。)

如果你不使结构不透明,类似的情况也适用。那么你可能会假设用户会(更多)直接调用

str->add_tag(str, tag);

但究竟是什么让这变得简单

add_tag(str, tag);

?

所以总的来说,不,我一般不会认为这种方法是一种好的做法。在有限的情况下,按照这些思路做某事可能有意义,但不是作为一般的图书馆惯例。

会不会有问题 关于调用实用函数的多线程 与不同的论点并行等等?

不比以任何其他方式指定的函数更是如此,除非函数指针本身正在被修改。

我知道它更接近 C++ 类,但我希望坚持使用 C 并学习如何用程序语言来完成,而不是 哎呀。

如果您想学习 C 习语和约定,那么一定要这样做。你所描述的不是一个。 C代码和库绝对可以使用封装等OO原则设计,在某种程度上甚至可以使用多态性,但通常不是通过您描述的机制来实现的。这个答案涉及到一些 用于此目的的方法。

【讨论】:

  • 感谢您的详细解答和指导。我是 C 编程范式和模式的新手。这无疑帮助我理解了设计选择是如何做出的。
【解决方案2】:

这是一个好习惯吗?

TLDR;没有。

背景:

在过去的一年半里,我几乎只在 STM32 微控制器上使用嵌入式 C 进行编程(与使用 C++ 或“C+”相反,如下所述)。像我一样,必须在 架构级别 学习 C,这对我来说非常有见地。我已经非常努力地研究了 C 架构,以达到我可以说我“知道 C”的地方。事实证明,众所周知,C 和 C++ 不是同一种语言。在语法级别,C 几乎完全是 C++ 的一个子集(在 C 支持 C++ 不支持的东西的地方有一些关键差异),因此为什么人们(包括我自己在内)经常认为/认为它们几乎是相同的语言,但在建筑水平他们是非常不同的动物。


旁白:

请注意,我最喜欢的嵌入式方法是使用一些俗称的“C+”。它基本上是使用 C++ 编译器来编写 C 风格的嵌入式代码。您基本上只是按照您希望编写 C 的方式编写 C,除了您使用 C++ 类来极大地简化(否则为纯 C)体系结构。换句话说,“C+”是一个笔名,用于描述使用 C++ 编译器编写类 C 代码,使用类而不是“基于对象的 C”架构(如下所述)。您有时也可能使用一些高级 C++ 概念,例如运算符重载或模板,但在大多数情况下避免使用 STL,以免在初始化后意外使用动态分配(例如,在幕后和自动,如 C++ 向量所做的那样), 因为在正常运行时动态内存分配/释放会很快耗尽稀缺的 RAM 资源,并使原本确定性的代码变得不确定。所谓的“C+”还可能包括混合使用 C(使用 C 编译器编译)和 C++(使用 C++ 编译器编译),根据需要链接在一起(不要忘记您在包含的 C 头文件中使用 extern "C"在您的 C++ 代码中,根据需要)。

核心 Arduino 源代码(同样,core,不一定是他们的示例“草图”或初学者示例代码)在这方面做得非常好,可以用作优秀“C+”的模型“ 设计。

[旁观]


架构 C 笔记:

所以,关于 C 架构(即:实际的 C,而不是“C+”/C 风格的 C++):

如您所知,C 不是一种面向对象的语言,但它可以以“基于对象”的风格编写。请注意,我说的是“基于对象”,而不是“面向对象”,因为这就是我听到其他迂腐 C 程序员提到它的方式。我可以说我编写了基于对象的 C 架构,它实际上非常有趣。

要创建基于对象的 C 架构,需要记住以下几点:

  1. 命名空间可以在 C 中通过在命名空间名称前面加上下划线来完成。毕竟,这就是一个名称空间。例如:mylibraryname_foo()mylibraryname_bar() 等。例如,将其应用于枚举,因为 C 没有像 C++ 那样的“枚举类”。也将它应用于所有 C 类“方法”,因为 C 没有类。适用于与特定库相关的所有全局变量或定义。
  2. 在创建 C“类”时,您有 2 个主要的体系结构选项,它们都非常有效且被广泛使用:
    1. 使用public structs(可能隐藏在名为“myheader_private.h”的标头中,给他们一种伪隐私感)
    2. 使用不透明结构(通常称为“不透明指针”,因为它们是指向不透明结构的指针)
  3. 在创建 C“类”时,您可以选择将指向函数的指针封装在上面的结构中,以使其具有更“C++”的感觉。这有点常见,但在我看来,这是一个可怕的想法,它使代码几乎无法遵循并且非常难以阅读、理解和维护。

第一个选项,公共结构:

制作一个包含所有“类数据”的结构定义的头文件。我建议您不要包含指向函数的指针(稍后将讨论)。 这实质上为您提供了一个“所有成员都是公共的 C++ 类”的等价物。 缺点是您不会隐藏数据。好处是您可以使用所有 C“类对象”的静态内存分配,因为包含这些库头文件的用户代码知道结构的完整规范和大小。

第二个选项:不透明结构:

在您的库头文件中,对结构进行前向声明:

/// Opaque pointer (handle) to C-style "object" of "class" type mylibrarymodule:
typedef struct mylibrarymodule_s *mylibrarymodule_h;

在您的库 .c 源文件中,提供 struct mylibrarymodule_s 的完整定义。由于这个库的用户只包含头文件,他们看不到这个不透明结构的完整实现或大小。这就是“不透明”的意思:“隐藏”。它被混淆或隐藏起来。 这基本上为您提供了“所有成员都是私有的 C++ 类”的等价物。 好处是您可以获得真正的数据隐藏。缺点是你不能使用这个库在你的用户代码中为你的任何C“类对象”使用静态内存分配,因为包括这个库的任何用户代码甚至都不知道结构有多大,所以它不能是静态的分配。相反,库必须在程序初始化时进行一次动态内存分配,即使对于嵌入式确定性实时安全关键系统也是安全的,因为您在正常程序执行期间不会分配或释放内存。

有关选项 2 的详细和完整示例(不要混淆:我在此处链接到的答案中将其称为“选项 1.5”)请参阅此处关于不透明结构/指针的其他答案:Opaque C structs: how should they be declared?

就个人而言,我认为具有静态内存分配和“所有公共成员”的选项 1 可能是我的首选方法,但我最熟悉 opaque struct 选项 2 方法,因为这就是我工作的 C 代码库用途最多。

上面的第 3 条:在结构中包含指向函数的指针。

这是可以做到的,有些人做到了,但我真的很讨厌。不要这样做。它只会让你的代码很难理解。例如,在具有出色索引器的 Eclipse 中,我可以 Ctrl + 单击任何内容,它会跳转到它的定义。如果我想查看我在 C“对象”上调用的函数的实现怎么办?我 Ctrl + 单击它,它会跳转到 pointer 函数的声明。但是功能在哪里???我不知道!我可能需要 10 分钟 grepping 并使用查找或搜索工具,在代码库中挖掘所有内容,才能找到臭名昭著的函数定义。一旦找到它,我就会忘记我在哪里,并且每次使用这种方法编辑库模块时,我都必须为每个函数重新重复一遍。这很糟糕。上面的不透明指针方法效果很好,公共指针方法也很容易。

现在,直接回答您的问题:

为了使库使用者更容易对这些结构实例进行通用操作,我可以在结构本身内提供指向这些函数的函数指针吗?

是的,你可以,但它只会让调用更容易。不要这样做。找到函数来查看它的实现变得非常困难。

这是一个好习惯吗?

不,请改用上面的选项 1 或选项 2,您现在只需在每个 C“对象”上调用 C“命名空间”“方法”。您必须简单地将“C 类的成员”作为每个调用的第一个参数传递给函数。这意味着您可以在 C++ 中执行以下操作:

myclass.dosomething(int a, int b);

你只需要在基于对象的 C 中做:

// Notice that you must pass the "guts", or member data
// (`mylibrarymodule` here), of each C "class" into the namespaced
// "methods" to operate on said C "class object"!
// - Essentially you're passing around the guts (member variables)
//  of the C "class" (which guts are frequently referred to as
// "private data", or just `priv` in C lingo) to each function that
// needs to operate on a C object
mylibrarymodule_dosomething(mylibrarymodule_h mylibrarymodule, int a, int b); 

在多线程中是否存在与不同参数并行调用实用函数等的问题?

是的,与任何多线程尝试访问相同数据的多线程情况相同。只需将互斥锁添加到每个基于 C 结构的“对象”,并确保作用于 C“对象”的每个“方法”正确锁定(获取)和解锁(提供)互斥锁,然后再对C“对象”。

相关:

  1. Opaque C structs: how should they be declared? [使用“基于对象”的 C 架构]

【讨论】:

    【解决方案3】:

    我建议你阅读 com 规范,你会收获很多。所有这些 com、ole 和 dcom 技术都基于一个简单的结构,该结构包含了自己的数据和方法。

    https://www.scribd.com/document/45643943/Com-Spec

    在这里更简单 http://www.voidcn.com/article/p-fixbymia-beu.html

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-01-23
      • 1970-01-01
      • 2021-08-06
      • 2016-06-03
      • 1970-01-01
      相关资源
      最近更新 更多