C 中的高级类






4.87/5 (13投票s)
在 C 中实现类的干净示例
引言
本文讨论如何在 C 语言中干净地编写高级类。
这是一种我提出的方法,可以让你非常干净、轻松地在 C 语言中创建高级类。它对于需要大量结构的大型项目非常有用,尤其是在没有 C++ 运行时的嵌入式系统上工作时(或者如果你只是普遍不喜欢 C++)。
背景
我过去曾多次尝试过这个方法,但充其量都非常业余。然而,我在我的博客中写了一篇关于我在 C 语言中多年来学到的编程技巧的文章,但页面格式无法很好地呈现它。此外,CodeProject 过去曾帮助我克服了无数障碍,我觉得应该回馈一下,你懂的?
Using the Code
首先要做的是定义一些定义和宏,以便更容易地定义类。这些宏使用 memcpy 和 malloc / free,因此你需要包含以下头文件
#include <string.h> // for memcpy
#include <malloc.h> // for malloc / free
我将首先介绍的定义用于定义类类型。
#define class typedef struct
#define class_t void *
#define ref_class static struct
#define partial_class struct
第一个定义用于定义普通类。不幸的是,由于 C 语言中 struct
的工作方式,名称必须在类定义之后。这很烦人,但也没办法。
第二个定义定义了由类的构造函数、析构函数和运算符返回的类类型。
第三个定义定义了一个引用类。引用类有两个目的。首先,每当创建新类时,它都会复制引用类以初始化函数指针。第二个目的是提供对类本身不包含的 protected
成员的访问,其方式与你在 C++ 类中使用 static
成员函数的方式大致相同。这样做的原因是,你不会真的希望每次创建类时都复制 20-30 个函数指针。你会将它们设置为 protected
(引用类的成员),这样它们仍然可以访问,因为它们永远不会改变。
第四个定义定义了局部类,它几乎只用于定义引用类的 private
成员。
接下来的定义是访问修饰符。
#define public {
#define private }, {
#define protected },
你可能想知道这里发生了什么,但稍后它将不言自明。唯一需要说明的是,所有引用类中都应该至少有一个 public
和 protected
部分,并且顺序应该始终是 public
、private
,然后是 protected
。
接下来的宏用于声明类的构造函数、析构函数、运算符和成员。
#define constructor(...) class_t (*_constructor)(class_t, ##__VA_ARGS__)
#define destructor(...) class_t (*_destructor)(class_t, ##__VA_ARGS__)
#define operator(o) class_t (*op_##o)(class_t, ...)
#define member(type, name, ...) type (*name)(class_t, ##__VA_ARGS__)
第一个和第二个宏用于声明类具有构造函数或析构函数,参数是传递给函数的参数。
第三个宏用于声明一个运算符,参数是操作的名称,例如 'add
'、'sub
'、'mul
' 等。它会在名称前添加 'op_
' 前缀,并将其声明为具有 1 个或多个参数。
第四个宏用于声明一个成员函数,参数是函数类型、名称和函数接受的参数。
接下来的宏用于定义类的构造函数、析构函数、运算符和成员。
#define constructor_set(...)
._constructor = (class_t(*)(class_t, ##__VA_ARGS__))
#define destructor_set(...)
._destructor = (class_t(*)(class_t, ##__VA_ARGS__))
#define operator_set(o)
.op_##o = (class_t(*)(class_t, ...))
#define member_set(type, name, ...)
.name = (type (*)(class_t, ##__VA_ARGS__))
第一个和第二个宏用于设置类的构造函数和析构函数,参数是函数的参数。紧随此宏之后应该是函数的名称,例如
constructor_set() MyClass_constructor
这也适用于其他两个宏。第三个宏用于定义一个运算符,参数是操作名称。
第四个宏用于定义一个成员函数,参数是函数类型、名称和函数的参数。
接下来的宏用于创建新类并可选地调用构造函数。
#define new(type, ...)
(type *)((type *)&_##type)->_constructor(memcpy(malloc
(sizeof(type)), &_##type, sizeof(type)), ##__VA_ARGS__)
#define new_(type) (type *)
memcpy(malloc(sizeof(type)), &_##type, sizeof(type))
#define _new(type, name, ...)
(type *)memcpy(&(name), &_##type, sizeof(type));
name._constructor(&name, ##__VA_ARGS__)
#define _new_(type, name)
(type *)memcpy(&(name), &_##type, sizeof(type))
带有 '_
' 后缀的宏在创建时不调用构造函数,无论它是否存在。
带有 '_
' 前缀的宏不为类使用 malloc 分配成员,而是将引用类直接复制到现有类中。这些宏的参数是类类型、类对象(不是指针)和传递给构造函数的参数。
对于没有 '_
' 前缀的宏,参数只是类类型和传递给构造函数的参数。
接下来的宏用于删除类并可选地调用析构函数
#define delete(name, ...)
free(name->_destructor(name, ##__VA_ARGS__));
#define delete_(name, ...) free(name);
#define _delete(name, ...) name._destructor(&(name), ##__VA_ARGS__);
#define _delete_(name, ...)
概念与新宏相同;带 '_
' 前缀的宏用于不需要释放的对象,带 '_
' 后缀的宏不调用析构函数。
现在我终于介绍了所有宏的用法,我将解释类的创建过程。
- 声明类
- 声明函数
- 定义类
- 定义函数
类创建必须始终以这个确切的顺序完成,否则你会得到未定义引用错误。下面的代码片段展示了如何声明一个类
class {
constructor();
destructor();
const char *helloText;
const char *worldText;
member(int, print, char *);
member(int, println, char *);
} MyClass;
你可以看到,尽管宏看起来很复杂,但实际上很容易理解。接下来是第 2 步,声明函数。
class_t MyClass_constructor(MyClass *this);
class_t MyClass_destructor(MyClass *this);
class_t MyClass_op_add(int lop, int rop);
void MyClass_print(MyClass *this, char *text);
void MyClass_println(MyClass *this, char *text);
请注意,构造函数、析构函数和运算符都具有 class_t
返回类型。此外,重要的是要注意,为了本例的目的,运算符函数使用整数参数,这可能会输出编译器警告。通常,它们也应该是 class_t
。
接下来是第 3 步,定义类
ref_class {
MyClass MyClass;
partial_class {
int izSecret;
} _MyClass;
operator(add);
} _MyClass = {
public
constructor_set() MyClass_constructor,
destructor_set() MyClass_destructor,
.helloText = "Hello ",
.worldText = "World",
member_set(int, print, char *) MyClass_print,
member_set(int, println, char *) MyClass_println,
private
.izSecret = 5,
protected
operator_set(add) MyClass_op_add,
};
如果这还不性感,我不知道什么才是。
首先,引用类的名称应与类的名称相同,但带有单个 '_
' 前缀,并且第一个成员应始终与类具有相同的类型。这是因为引用类通过 private
和 protected
成员扩展了类。
这是可选的,但如果存在,private
成员必须在局部类中,并且它们必须是引用类的第二个成员。为了连贯性,你可以看到我将第一个成员命名为 MyClass
,以说明该部分是 public
并被 MyClass
类可见,并将第二个成员命名为 _MyClass
,以说明该部分是 private
并且仅被 _MyClass
引用类可见。
在声明第一个和(可选的)第二个成员之后,其余的就像声明任何普通类成员一样,然后结束并进入实际定义。
在这里你可以看到 public
、private
和 protect
定义的魔力,它们被转换为括号来定义 public
和 private
类;同时呈现出与 C++ 访问修饰符几乎相同的外观。
除了宏完成的所有事情的常规定义之外,你可以看到我使用 '.helloText
' 和 '.worldText
' 来定义它们。如果你以声明的相同顺序定义所有内容,这不是必需的,但我仍然强烈建议这样做,以防你以后需要进行任何更改。
然后最后我们进行第 4 步,定义函数。
class_t MyClass_constructor(MyClass *this) {
printf("constructor was called\n\n");
return this;
}
class_t MyClass_destructor(MyClass *this) {
printf("\ndestructor was called\n");
return this;
}
class_t MyClass_op_add(int lop, int rop) {
printf("%i\n", lop + rop);
return NULL;
}
void MyClass_print(MyClass *this, char *text) {
printf("%s", text);
}
void MyClass_println(MyClass *this, char *text) {
printf("%s\n", text);
}
这里需要注意的是,构造函数和析构函数必须返回 'this
'。
现在,一个使用示例
int main(int argc, char *argv[]) {
MyClass _mc, *mc = _new(MyClass, _mc); // create the
// class and call the constructor
mc->print(mc, (char *)mc->helloText);// call member function
_delete_(_mc) // delete the class
mc = new_(MyClass); // create allocated class
mc->println(mc, (char *)mc->worldText); // call member function
_MyClass.op_add(4, 5); // perform operation
delete(mc); // delete and free class, call destructor
return 0;
}
就是这样!
关注点
当我编写代码时,运算符宏最初接受一个实际的运算符,如 'operator(+)',这是通过检查运算符的一系列条件并确定要调用哪个函数来数学完成的。但问题是它需要定义所有运算符,并且只会减慢处理速度,所以它被丢弃了。