【问题标题】:Making two objects in C more object-oriented使 C 中的两个对象更加面向对象
【发布时间】:2021-03-03 03:21:49
【问题描述】:

首先,这只是一个学术问题。我知道 C 不是做 OOP 编程的工作,但这更像是一个学习练习,让初学者了解什么是可能的,什么是不可能的(或者,什么是可能的,但不是一个好主意)。

让我们以以下为起点,我有两个不同的对象,但我想为每个对象提供相同的两种方法:createprint。为了简化问题,我省略了任何错误检查、释放等:

#include <stdio.h>
#include <stdlib.h>

struct Person {
    char* name;
    int age;
};
struct Car {
    char* make;
    char* model;
    int year;
};
struct Person* create_person(void)
{
    struct Person *new = malloc(sizeof (struct Person));
    return new;
}
void print_person(struct Person* person)
{
    printf("<Person: %s (%d)>\n", person->name, person->age);
}
struct Car* create_car(void)
{
    struct Car *new = malloc(sizeof (struct Car));
    return new;
}
void print_car(struct Car* car)
{
    printf("<Car: %s - %s (%d)>\n", car->make, car->model, car->year);
}
int main(void)
{
    struct Car *car = create_car();
    *car = (struct Car) {.make="Chevy", .model="Eldorado", .year=2015};
    print_car(car);

    struct Person *person = create_person();
    *person = (struct Person) {.name="Tom", .age=30};
    print_person(person);
}

我认为第一部分是将“方法”分组到结构本身中。那么我们会有:

#include <stdio.h>
#include <stdlib.h>

struct Person {
    char* name;
    int age;
    void (*print)(struct Person*);
};
struct Car {
    char* make;
    char* model;
    int year;
    void (*print)(struct Car*);
};
void print_car(struct Car* car);
void print_person(struct Person* person);
struct Person* create_person(void)
{
    struct Person *new = malloc(sizeof (struct Person));
    return new;
}
void print_person(struct Person* person)
{
    printf("<Person: %s (%d)>\n", person->name, person->age);
}
struct Car* create_car(void)
{
    struct Car *new = malloc(sizeof (struct Car));
    return new;
}
void print_car(struct Car* car)
{
    printf("<Car: %s - %s (%d)>\n", car->make, car->model, car->year);
}
int main(void)
{
    struct Car *car = create_car();
    *car = (struct Car) {.make="Chevy", .model="Eldorado", .year=2015, .print=print_car};
    car->print(car);

    struct Person *person = create_person();
    *person = (struct Person) {.name="Tom", .age=30, .print=print_person};
    person->print(person);
}

下一步如何让它“更像 OOP”?也许使用预处理器胶水和泛型?这两个对象可能成为最像 OOP 的例子是什么?再说一次,我知道这不是 C 的意义所在,但它更像是一种学习体验。

【问题讨论】:

  • David,如果你还没有找到这篇小论文,它有一个创造性的方法来使用 C 进行 OOP。恰当地标题为 Object-oriented Programming with ANSI C (Chapter 2) 我注意到第 2 章,因为那是具体细节讨论了使用的方法。有合理的继承和其他 OPP 特征。非常值得一读,看看它是否包含一些您可以使用的方法。该方法是合理的且功能强大,但实施起来有点乏味——这就是为什么 C 语言中的 OOP 不是什么大事......
  • @DavidC.Rankin 我明白了,谢谢你的链接。你读过 C 的接口 + 实现吗?我听说那本书推荐了很多 C 语言中的 OOP 概念。
  • 不,我还没有看到那个,但我会查一下。我在几年前(大约 5 到 6 年)完成了所有我能找到的 OOP。而且,虽然我喜欢解决问题的简洁方法,但我总是回到这样一个现实,即对于每一个新问题,重新实现所有好的方法,通常花费的时间与编写一个量身定制的解决方案一样多。大部分都是因为 C 是强类型的。即使引入了_Generic,似乎我最终还是会为某个问题编写另一种量身定制的方法。 (你可能会发现我错过的突破:)

标签: c oop


【解决方案1】:

您可以使用 Linux 内核应用的方法。 OOP 的实现基于使用组合进行继承,并将接口嵌入到新类中。

container_of 可以轻松地在指向类的指针和指向其成员之一的指针之间切换。通常嵌入对象将是类的接口。要查找有关 container_of 宏的更多详细信息,请参阅我对其他相关问题的回答。

https://stackoverflow.com/a/66429587/4989451

这种方法被用来创建一个巨大的复杂的面向对象的软件,即 Linux 内核。

在问题的示例中,Car 和 Person 类使用我们可以称为 struct Printable 的打印接口。我强烈建议在create_... 函数中生成完全初始化的对象。让它复制所有字符串。此外,你应该添加destroy_...方法来释放create_...分配的资源。

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>

#define container_of(ptr, type, member) \
  (type*)(void*)((char*)ptr - offsetof(type, member))

struct Printable {
    void (*print)(struct Printable*);
};

struct Person {
    char* name;
    int age;
    struct Printable printable;
};

void print_person(struct Printable *printable) {
    struct Person *p = container_of(printable, struct Person, printable);
    printf("<Person: %s (%d)>\n", p->name, p->age);
}

struct Person *create_person(char *name, int age) {
    struct Person *p = malloc(sizeof *p);
    p->name = strdup(name);
    p->age = age;
    p->printable.print = print_person;
    return p;
}

struct Car {
    char* make;
    char* model;
    int year;
    struct Printable printable;
};

void print_car(struct Printable *printable) {
    struct Car *c = container_of(printable, struct Car, printable);
    printf("<Car: %s - %s (%d)>\n", c->make, c->model, c->year);
}

struct Car *create_car(char *make, char *model, int year) {
    struct Car *c = malloc(sizeof *c);
    c->make = strdup(make);
    c->model = strdup(model);
    c->year = year;
    c->printable.print = print_car;
    return c;
}

void print(struct Printable *printable) {
    printable->print(printable);
}

int main() {
    struct Car *car = create_car("Chevy", "Eldorado", 2015);
    struct Person *person = create_person("Tom", 30);
    print(&car->printable);
    print(&person->printable);
    return 0;
}

产生输出:

<Car: Chevy - Eldorado (2015)>
<Person: Tom (30)>

请注意,函数print() 需要一个指向Printable 接口的指针。函数不需要了解原始类的任何信息。没有使用预处理器。所有演员都是在“图书馆”方面完成的,而不是在客户端。该库初始化 Printable 接口,因此不能被滥用。

您可以轻松添加其他基类或接口,例如 RefCounted 来解决内存管理问题。 i-face 将包含指向析构函数和引用计数本身的指针。其他示例是侵入式链表或二叉树。

【讨论】:

  • 问题,如果函数完全定制为person 类型,为什么你传递print_person(struct Printable *printable) 而不是只传递print_person(struct Person *person)
  • 另外,还有一件事:为什么要使用 c-&gt;make = srdup(make) 而不仅仅是 c-&gt;make = make ?指针不会指向现有的字符串文字,或者复制字符串有什么好处?
  • @David542,关于strdup()。它适用于文字。但如果名称不是文字,即从文件加载,它将不起作用。您每次都必须分配新名称。如果其他一些 Person 对象使用文字怎么办。唯一可扩展的解决方案是create_person() 中的name = strdup()destroy_name() 中的free(name)
  • @David542,因为print_person专用于Printable接口。将print_person 替换为print_person_ops 可能是个好主意。世界其他地方将使用print(&amp;person.printable),可以包装为print_person(&amp;person)
  • 没有。只需使用void destory_person(struct Person *p) { free(p-&gt;name); free(p); }。 :) RefCounted 值得一玩,因为它极大地简化了对象生命周期的管理。如果将类似于 Lunux 内核中的kref 接口。见kernel.org/doc/Documentation/kref.txt
猜你喜欢
  • 1970-01-01
  • 2023-04-05
  • 2014-04-17
  • 2012-02-24
  • 1970-01-01
  • 2011-04-16
  • 1970-01-01
  • 1970-01-01
  • 2015-02-12
相关资源
最近更新 更多