我似乎总是用C编写的代码大多是面向对象的,所以说我有一个源文件或某些东西,我会创建一个结构,然后将指向该结构的指针传递给该结构所拥有的函数(方法):
struct foo {
int x;
};
struct foo* createFoo(); // mallocs foo
void destroyFoo(struct foo* foo); // frees foo and its things
这是不好的做法吗?我如何学习以“正确的方式”编写C语言。
我似乎总是用C编写的代码大多是面向对象的,所以说我有一个源文件或某些东西,我会创建一个结构,然后将指向该结构的指针传递给该结构所拥有的函数(方法):
struct foo {
int x;
};
struct foo* createFoo(); // mallocs foo
void destroyFoo(struct foo* foo); // frees foo and its things
这是不好的做法吗?我如何学习以“正确的方式”编写C语言。
Answers:
不,这不是一个坏习惯,甚至可以鼓励这样做,尽管甚至可以使用诸如 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进行面向对象的编程
std::string
,你不能foo::create
吗?我不使用C。也许只在C ++中?
不错,很好。面向对象编程是一件好事(除非您被带走了,否则可能会拥有太多的好事)。C不是最适合OOP的语言,但是那不应该阻止您充分利用它。
不算太差。它支持使用RAII来防止许多错误(内存泄漏,使用未初始化的变量,释放后使用等会导致安全问题的错误)。
因此,如果您只想使用GCC或Clang(而不是MS编译器)来编译代码,则可以使用cleanup
attribute,它将适当地破坏对象。如果您这样声明对象:
my_str __attribute__((cleanup(my_str_destructor))) ptr;
然后,my_str_destructor(ptr)
当ptr超出范围时将运行。请记住,它不能与函数arguments一起使用。
另外,请记住my_str_
在方法名称中使用,因为C
它没有名称空间,并且很容易与其他函数名称冲突。
#define
要使用的类型名,__attribute__((cleanup(my_str_destructor)))
那么您将在整个#define
范围中将其获取为隐式(它将添加到所有变量声明中)。
#define
'd类型或它的数组的指针),则该方法有效。简而言之:它不是标准的C语言,您在使用时会遇到很多灵活性。
__attribute__((cleanup()))
几乎成为了准标准。但是,b)和c)仍然站着...
这样的代码可能有很多优点,但是不幸的是,并未编写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
。现代编译器将以某种方式重新排列操作顺序,如果p
和t
识别不同的物体,但如果他们不这样做将改变行为。
这个示例当然是故意设计的,在实践中,使用一种类型的指针访问属于另一种类型的公共初始序列一部分的成员的代码通常会工作,但是不幸的是,因为无法知道此类代码何时可能失败除非禁用基于类型的别名分析,否则根本无法安全地使用它。
如果一个人想要编写一个函数来执行诸如将两个指针交换到任意类型的操作,那么就会出现一个不太那么人为的示例。在绝大多数“ 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
用作与临时指针不匹配的任何类型类型将调用未定义行为。可以肯定的是,编译器不会注意到这种情况,但是由于现代编译器原理要求程序员不惜一切代价避免未定义行为,因此在不使用分配的存储的情况下,我无法想到任何其他安全的方式来编写上述代码。
foo_function(foo*, ...)
C语言中的伪OO只是一种特殊的API样式,恰好看起来像类,但是应该更恰当地称为具有抽象数据类型的模块化编程。
下一步是隐藏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 {
...
};