编写面向对象的C是否不好?[关闭]


14

我似乎总是用C编写的代码大多是面向对象的,所以说我有一个源文件或某些东西,我会创建一个结构,然后将指向该结构的指针传递给该结构所拥有的函数(方法):

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

这是不好的做法吗?我如何学习以“正确的方式”编写C语言。


10
许多Linux(内核)都是以这种方式编写的,实际上,它甚至还模仿了更多类似于OO的概念,例如虚拟方法分派。我认为那很合适。
凯莉安

13
[确定程序员可以使用任何语言编写FORTRAN程序。 “- 埃德·波斯特,1983年
罗斯·帕特森

4
有什么理由为什么您不想切换到C ++?您不必使用自己不喜欢的部分。
svick '16

5
这确实引出了一个问题:“什么是面向对象的?” 我不会将此称为面向对象。我会说这是程序上的。(您没有继承,没有多态性,没有封装/隐藏状态的能力,并且可能缺少了其他OO的特征,这些特征并没有浮现在我的头上。)好的或不好的做法并不取决于这些语义。不过。
jpmc26 2016年

3
@ jpmc26:如果您是语言规定主义者,则应该听艾伦·凯(Alan Kay)的话,他发明了这个词,他说了这是什么意思,并且他说OOP就是关于Messaging的。如果您是语言描述者,则应调查该术语在软件开发社区中的用法。Cook确实做到了这一点,他分析了声称或被认为是OO的语言的功能,他发现它们有一个共同点:Messaging
约尔格W¯¯米塔格

Answers:


24

不,这不是一个坏习惯,甚至可以鼓励这样做,尽管甚至可以使用诸如 struct foo *foo_new();和的void foo_free(struct foo *foo);

当然,正如评论所言,仅在适当的地方这样做。在构造函数中使用int

前缀foo_是许多库遵循的约定,因为它可以防止与其他库的命名冲突。其他功能通常具有约定使用foo_<function>(struct foo *foo, <parameters>);。这允许您struct foo成为不透明类型。

请查看libcurl文档以了解该约定,尤其是使用“ subnamespaces”时,以便curl_multi_*当第一个参数返回时,乍一看调用函数时看起来是错误的curl_easy_init()

还有更多通用方法,请参见使用ANSI-C进行面向对象的编程


11
请始终注意“在适当情况下”。OOP不是银弹。
Deduplicator

C没有可用来声明这些函数的名称空间吗?类似于std::string,你不能foo::create吗?我不使用C。也许只在C ++中?
克里斯·西里菲斯

@ChrisCirefice C中没有名称空间,这就是为什么许多库作者将前缀用作其函数的原因。
Residuum

2

不错,很好。面向对象编程是一件好事(除非您被带走了,否则可能会拥有太多的好事)。C不是最适合OOP的语言,但是那不应该阻止您充分利用它。


4
并非我不同意,但您的意见确实应该得到一些阐述的支持。
Deduplicator

1

不算太差。它支持使用RAII来防止许多错误(内存泄漏,使用未初始化的变量,释放后使用等会导致安全问题的错误)。

因此,如果您只想使用GCC或Clang(而不是MS编译器)来编译代码,则可以使用cleanupattribute,它将适当地破坏对象。如果您这样声明对象:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

然后,my_str_destructor(ptr)当ptr超出范围时将运行。请记住,它不能与函数arguments一起使用。

另外,请记住my_str_在方法名称中使用,因为C它没有名称空间,并且很容易与其他函数名称冲突。


2
Afaik,RAII即将对C ++中的对象使用析构函数的隐式调用以确保清除,从而避免了添加显式资源释放调用的需求。因此,如果我没有记错的话,RAII和C是互斥的。
cmaster-恢复莫妮卡

@cmaster如果您#define要使用的类型名,__attribute__((cleanup(my_str_destructor)))那么您将在整个#define范围中将其获取为隐式(它将添加到所有变量声明中)。
Marqin '16

如果a)使用gcc,b)仅在函数局部变量中使用类型,并且c)仅在裸版本中使用类型(没有指向#define'd类型或它的数组的指针),则该方法有效。简而言之:它不是标准的C语言,您在使用时会遇到很多灵活性。
cmaster-恢复莫妮卡

如我的回答所述,这也适用于clang。
Marqin

啊,我没注意到。确实,这使得要求a)的严格程度大大降低,因为这__attribute__((cleanup()))几乎成为了准标准。但是,b)和c)仍然站着...
cmaster-恢复莫妮卡

-2

这样的代码可能有很多优点,但是不幸的是,并未编写C标准来简化它。过去,编译器提供了有效的行为保证,超出了标准所要求的范围,从而使编写此类代码的可能性比标准C中的要干净得多,但是最近,编译器已开始以优化的名义撤销此类保证。

最为明显的是,许多C编译器从历史上就保证了(如果没有说明,则通过设计)如果两个结构类型包含相同的初始序列,则可以使用指向这两种类型的指针来访问该公共序列的成员,即使这些类型是不相关的,而且,为了建立共同的初始序列,所有指向结构的指针都是等效的。与不使用这种行为的代码相比,使用这种行为的代码可以更加整洁和类型安全,但是遗憾的是,即使该标准要求必须共享具有相同初始序列的结构,并且该代码必须以相同的方式进行布局,它仍然禁止代码实际使用一种类型的指针,用于访问另一种类型的初始序列。

因此,如果您想用C编写面向对象的代码,则必须决定(并且应该尽早做出此决定)跳过很多箍以遵守C的指针类型规则,并准备好拥有即使旧的编译器会生成按预期工作的代码,现代的编译器也会生成无意义的代码,或者记录要求该代码仅可用于配置为支持旧式指针行为的编译器(例如,使用(-fno-strict-aliasing))有些人认为“ -fno-strict-aliasing”是邪恶的,但我建议将“ -fno-strict-aliasing” C视为一种为某些目的提供比“标准” C更大的语义能力,但以优化为代价,而优化对于其他目的可能很重要。

通过示例,在传统编译器上,历史编译器将解释以下代码:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

依次执行以下步骤:递增的第一个成员*p,对的第一个成员的最低位进行补码*t,然后递减的第一个成员*p,并对的第一个成员的最低位进行补码*t。现代编译器将以某种方式重新排列操作顺序,如果pt识别不同的物体,但如果他们不这样做将改变行为。

这个示例当然是故意设计的,在实践中,使用一种类型的指针访问属于另一种类型的公共初始序列一部分的成员的代码通常会工作,但是不幸的是,因为无法知道此类代码何时可能失败除非禁用基于类型的别名分析,否则根本无法安全地使用它。

如果一个人想要编写一个函数来执行诸如将两个指针交换到任意类型的操作,那么就会出现一个不太那么人为的示例。在绝大多数“ 1990年代C”编译器中,可以通过以下方式实现:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

但是,在标准C中,必须使用:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

如果*p2保留在分配的存储区中,并且临时指针未保留在分配的存储区中,则有效类型*p2将成为临时指针的类型,并且代码将尝试*p2用作与临时指针不匹配的任何类型类型将调用未定义行为。可以肯定的是,编译器不会注意到这种情况,但是由于现代编译器原理要求程序员不惜一切代价避免未定义行为,因此在不使用分配的存储的情况下,我无法想到任何其他安全的方式来编写上述代码。


Downvoters:愿意发表评论吗?面向对象编程的一个关键方面是使多个类型共享公共方面的能力,并且具有指向任何此类类型的指针可用于访问那些公共方面的能力。OP的示例没有做到这一点,但是它几乎没有涉及“面向对象”的表面。历史上的C编译器将允许以比当今标准C中更简洁的方式编写多态代码。因此,在C中设计面向对象的代码需要确定一个目标语言。人们从哪个方面不同意?
超级猫

嗯...介意您展示了标准提供的保证如何不允许您干净地访问公共初始子序列的成员吗?因为我认为这就是您对在合同行为范围内敢于进行优化的弊端的怨言取决于这次吗?(这是我的猜测,这两个拒绝支持者发现令人反感。)
Deduplicator

OOP不一定需要继承,因此在实践中两个结构之间的兼容性不是什么大问题。通过将函数指针放入结构中并以特定方式调用这些函数,可以得到真正的OO。当然,foo_function(foo*, ...)C语言中的伪OO只是一种特殊的API样式,恰好看起来像类,但是应该更恰当地称为具有抽象数据类型的模块化编程。
阿蒙

@Deduplicator:请参见指示的示例。字段“ i1”是两个结构的公共初始序列的成员,但是如果代码试图使用“结构对*”来访问“结构三重奏”的初始成员,现代编译器将失败。
超级猫

哪个现代C编译器无法通过该示例?需要任何有趣的选择吗?
Deduplicator

-3

下一步是隐藏struct声明。您将其放在.h文件中:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

然后在.c文件中:

struct foo_s {
    ...
};

6
根据目标,这可能是下一步。无论是否,这甚至都不会远程尝试回答这个问题。
Deduplicator
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.