在C语言中的结构体继承

85

在C语言中可以继承一个结构体吗?如果可以,怎么做?


7
C语言中不存在继承。 - Ed S.
48
除非你实施它。 - Philip
12个回答

111

你能够接近的最近方法是比较常见的习语:

typedef struct
{
    // base members

} Base;

typedef struct
{
    Base base;

    // derived members

} Derived;

由于DerivedBase复制而来,您可以这样做:

Base *b = (Base *)d;

假设 d 是一个 Derived 类的实例,它们可以算是多态的一种。但要使用虚函数又需要解决另一个问题:你需要在 Base 中有一个等效于 vtable 指针的东西,其中包含函数指针,这些函数接受 Base 作为其第一个参数(你可以将其命名为 this)。

那时,你可能还不如使用 C++!


13
好的,假设你的平台上有可用的C++编译器! - anon
6
如果有C编译器的话,就一定会有C++编译器 - 只需使用生成C语言输出的编译器即可。 - Daniel Earwicker
2
哎呀,你救了我的命。我通常使用Java编码,当面对类似于你发布的代码时,我以为它是一个组合体,当他们进行强制转换时,我感到非常困惑。 - Surya Wijaya Madjid
2
你可以在复杂的实际项目(如git)中看到它的用途。请参见log-tree.cstruct tag“继承”自struct object - albfan
1
@entonio C语言是如此底层,以至于指针只是一个指针。它没有类型。 这是完全不正确的。而且 [共同的初始序列被限制为属于同一 union 的成员] (https://port70.net/~nsz/c/c11/n1570.html#6.5.2.3p6)。这个答案是错误的。"由于Derived以Base的副本开始,因此您可以这样做:`Base *b = (Base*)d;`" 不,您不能安全地这样做。请参见 https://dev59.com/s1sW5IYBdhLWcg3wwZcP - Andrew Henle
显示剩余3条评论

49

C语言没有显示地定义继承的概念,不像C++。但是,您可以在一个结构体中重复使用另一个结构体:

typedef struct {
    char name[NAMESIZE];
    char sex;
} Person;

typedef struct {
    Person person;
    char job[JOBSIZE];
} Employee;

typedef struct {
    Person person;
    char booktitle[TITLESIZE];
} LiteraryCharacter;

1
据我所知,您在C++中也可以在另一个结构/类成员内拥有一个结构/类成员。 - Tyler Millican
73
C 语言规定在结构体的第一个成员之前不得添加填充。因此,您实际上可以(并且被允许)将 LiteraryCharacter* 强制转换为 Person*,并将其视为 person。+1 - Johannes Schaub - litb
5
@JohannesSchaub-litb,您的评论比答案本身更好地解释了问题 :) - Greg
2
重要的是要注意,您只能通过引用传递这些类型。您不能将它们复制到Person对象中,否则它会被切割。 - Kevin Cox

43

我喜欢并使用了在C语言中实现类型安全继承的想法,来自于此处

例如:

struct Animal
{
    int weight;
};

struct Felidae
{
    union {
      struct Animal animal;
    } base;
    int furLength;
};

struct Leopard
{
    union {
      struct Animal animal;
      struct Felidae felidae;
    } base;

    int dotCounter;
};

使用方法:

struct Leopard leopard;
leopard.base.animal.weight = 44;
leopard.base.felidae.furLength = 2;
leopard.dotCounter = 99;

我从未想过这一点。如果您使联合匿名,它会变得非常整洁。但是缺点是您需要列出所有父项,以避免嵌套变量。 - Kevin Cox
2
有趣的方法。然而,一旦您键入“leopard.base”,继承/多态的整个意义就被消除了。 - Powerslave
4
不会的。leopard.base.felidae.base.animal.weight只是leopard.base.animal.weight的另一个名称 - 它在内存中的位置相同,意思也相同。 - Martin
我认为在某些情况下,您不需要多态行为,只需要继承的能力。但是,您仍然可以获得多态性,因为基于Animal的所有内容都应该有一个.base.animal命名空间,尽管这有点牵强。 - Kurt E. Clothier
1
如果有人决定将结构体转换为其初始成员的指针,那么您可能会遇到问题,尽管C声称这是明确定义的。如果我们对初始成员使用正确的类型,它就会起作用。但是 *(struct Animal*)&leopard...糟糕了,第一个成员不是 struct Animal 而是一个联合体。现在我们陷入了深深的森林中,语言律师和危险的野兽潜伏其中,例如严格别名、公共初始序列规则或组合类型规则。那不是一个安全的地方。也许可以使用 _Generic 或 typeof 来解决问题。 - Lundin
显示剩余2条评论

10

如果你的编译器支持匿名结构体,你可以这样做:

typedef struct Base
{
    // base members
} Base_t;

typedef struct
{
   struct Base;  //anonymous struct

   // derived members

} Derived_t;

这种方式可以直接访问基本结构成员,更加方便。


6
在 POSIX 中,后缀 "_t" 被保留。你可以随意使用它,但请注意,如果你的代码是为 POSIX 系统(例如 Linux)编写的,或者有人最终想要将你的代码移植到 POSIX 系统上,可能会遇到命名冲突的问题。 - L0j1k
14
这实际上在标准C中不起作用(甚至不是C11)。 - Chase
1
@Chase 它确实有效,至少在GCC v12.2.0中是这样。我用gcc -std=c11 -pedantic -Wall -Wextra main.c -o test尝试了一下,它可以正常工作。 - Zakk
1
@Zakk它确实可以工作,但不符合标准。参见这个答案。简而言之,这是微软的扩展功能,你应该在编译器中传递-fms-extensions,但现代的GCC似乎会自动启用它。 - Sun of A beach

9

如果您想使用一些gcc魔法(我想应该也适用于微软的C编译器),您可以这样做:


struct A
{
   int member1;
};

struct B
{
   struct A;
   int member2;
}

使用gcc编译时,您可以使用-fms-extensions(允许像Microsoft编译器一样使用未命名结构成员)进行编译。这与Daniel Earwicker提供的解决方案类似,但它允许您在结构体B实例上访问member1。即B.member1而不是B.A.member1。
这可能不是最可移植的方法,并且如果使用C ++编译器,则无法工作(不同的语义意味着重新声明/定义结构体A而不是实例化它)。
然而,如果您只在gcc / C领域中使用它,它将起作用并完全满足您的需求。

这不是组合吗? - Sumit Gera
1
不,这是正确的继承。假设您有一个名为b的struct B类型的结构体,b.member1将编译并按预期工作。组合将类似于b.base.member1。GCC会为您执行此操作。在这种情况下,它实际上将struct B的定义作为两个整数。 - Matt
这只能在C中实现,而不能在C++中实现吗?如果不是,请访问此链接 - Sumit Gera
只是C语言。在C++中,这种语法是不合法的。虽然C++有像类一样的结构体继承。 - Matt
现在我感觉好多了! - Sumit Gera
显示剩余4条评论

4
您可以执行上述提到的操作。
typedef struct
{
    // base members

} Base;

typedef struct
{
    Base base;

    // derived members

} Derived;

但是如果你想避免指针转换,可以使用指向 BaseDerivedunion 指针。


4

对于anon(和其他人类似)答案的微小变化。对于一级深度继承,可以执行以下操作:

#define BASEFIELDS              \
    char name[NAMESIZE];        \
    char sex

typedef struct {
    BASEFIELDS;
} Person;

typedef struct {
    BASEFIELDS;
    char job[JOBSIZE];
} Employee;

typedef struct {
    BASEFIELDS;
    Employee *subordinate;
} Manager;

这样,接受指向Person的函数将会接受指向Employee或Manager的指针(通过类型转换),就像其他答案中的方法一样,但在这种情况下,初始化也会变得更加自然:
Employee e = {
    .name = "...";
    ...
};

vs

# as in anon's answer
Employee e = {
    .person.name = "...";
    ...
};

我相信一些流行的项目是这样做的(例如libuv)
更新:在libsdl事件实现中,也有一些类似(但不相同)概念的良好示例,使用结构体和联合体。

2
这会带来棘手的别名后果。据我所知,只有在结构体位于动态内存中时,才能沿继承链进行重新转换。 (请参见https://gustedt.wordpress.com/2016/08/17/effective-types-and-aliasing/)没有动态内存,联合方法可能更好。 - Petr Skocik
@PSkocik 显然,在处理这类事情时应该小心,因为很多事情可能会出错。我相信这个例子也适用于指向堆栈变量的指针。话虽如此,我同意在像这样的场景中进行强制转换时,联合体始终是更好的解决方案。 - Alex
无论对象是如何分配的(自动、静态、动态),都没有关系。潜在问题的一个例子是不同的对象可能在“名称”和“性别”字段之间具有不同数量的填充,但还有其他可能性。主要问题是没有保证这样做会起作用,使得这段代码不可靠,因此无法使用。 - user694733
1
@user694733 我不同意填充语句。我们只关心BASEFIELDS,对于这些填充在每个给定平台上都是确定性的,前提是它们始终放置在结构的开头。显然,在实际情况下,BASEFIELDS需要包含类型标识符以确保正确处理字段,并且很快会变得混乱。但是我同意这种hackery并不好,如果需要C++样式的继承 - 应该使用C++。 - Alex
CPython也使用这样的语法构造Python类型,参见https://docs.python.org/2/extending/newtypes.html,这里PyObject_HEAD定义了Python类型所需的公共字段。 - Glen Fletcher

4

这段代码需要使用-fms-extensions编译才能正常工作。

流程图

main.c

#include "AbstractProduct.h"
#include "Book.h"
#include "Product.h"
#include "TravelGuide.h"

/***********************/

int main() {

    Product p = Product_new();  
    p.set_id(&p, 2);
    p.set_name(&p, "name2");
    p.set_description(&p, "description2");
    p.set_price(&p, 2000);  
    p.display(&p);

    TravelGuide tg = TravelGuide_new(); 
    tg.set_id(&tg, 1);
    tg.set_name(&tg, "name1");
    tg.set_description(&tg, "description1");        
    tg.set_price(&tg, 1000);
    tg.set_isbn(&tg, "isbn1");
    tg.set_author(&tg, "author1");
    tg.set_title(&tg, "title1");
    tg.set_country(&tg, "country1");
    tg.display(&tg);

}

AbstractProduct.c

#include "AbstractProduct.h"

/*-------------------------------*/

static void set_id(AbstractProduct *this, int id) {
    this->id = id;
}

/*-------------------------------*/

static void set_name(AbstractProduct *this, char *name) {
    strcpy(this->name, name);
}

/*-------------------------------*/

static void set_description(AbstractProduct *this, char *description) {
    strcpy(this->description, description);
}

/*-------------------------------*/

static int get_id(AbstractProduct *this) {
    return this->id;    
}

/*-------------------------------*/

static char *get_name(AbstractProduct *this) {
    return this->name;  
}

/*-------------------------------*/

static char *get_description(AbstractProduct *this) {
    return this->description;   
}

/*-------------------------------*/

static void display(AbstractProduct *this) {

    printf("-AbstractProduct- \n"); 
    printf("id: %d\n", this->get_id(this)); 
    printf("name: %s\n", this->get_name(this)); 
    printf("description: %s\n", this->get_description(this));   
    printf("\n");
}

/*-------------------------------*/

void AbstractProduct_init(AbstractProduct *obj) {

    obj->set_id = set_id;
    obj->set_name = set_name;
    obj->set_description = set_description; 
    obj->get_id = get_id;
    obj->get_name = get_name;
    obj->get_description = get_description;
    obj->display = display;

}

/*-------------------------------*/

AbstractProduct AbstractProduct_new() {

    AbstractProduct aux;
    AbstractProduct_init(&aux);
    return aux;
}

AbstractProduct.h

#ifndef AbstractProduct_H
#define AbstractProduct_H

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

/***********************/

typedef struct AbstractProduct{

    int id;
    char name[1000];
    char description[1000];

    void (*set_id)();
    void (*set_name)();
    void (*set_description)();  
    int (*get_id)();    
    char *(*get_name)();    
    char *(*get_description)(); 
    void (*display)();  

} AbstractProduct;

AbstractProduct AbstractProduct_new();
void AbstractProduct_init(AbstractProduct *obj);

#endif

Book.c

#include "Book.h"

/*-------------------------------*/

static void set_isbn(Book *this, char *isbn) {
    strcpy(this->isbn, isbn);
}

/*-------------------------------*/

static void set_author(Book *this, char *author) {
    strcpy(this->author, author);
}

/*-------------------------------*/

static void set_title(Book *this, char *title) {
    strcpy(this->title, title);
}

/*-------------------------------*/

static char *get_isbn(Book *this) {
    return this->isbn;  
}

/*-------------------------------*/

static char *get_author(Book *this) {
    return this->author;    
}

/*-------------------------------*/

static char *get_title(Book *this) {
    return this->title; 
}

/*-------------------------------*/

static void display(Book *this) {

    Product p = Product_new();
    p.display(this);    

    printf("-Book- \n");
    printf("isbn: %s\n", this->get_isbn(this)); 
    printf("author: %s\n", this->get_author(this)); 
    printf("title: %s\n", this->get_title(this));   
    printf("\n");
}

/*-------------------------------*/

void Book_init(Book *obj) {

    Product_init((Product*)obj);

    obj->set_isbn = set_isbn;
    obj->set_author = set_author;
    obj->set_title = set_title; 
    obj->get_isbn = get_isbn;
    obj->get_author = get_author;
    obj->get_title = get_title; 
    obj->display = display;
}
/*-------------------------------*/

Book Book_new() {

    Book aux;   
    Book_init(&aux);
    return aux;
}

Book.h

#ifndef Book_H
#define Book_H

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

#include "Product.h"

/***********************/

typedef struct Book{

    Product;
    char isbn[1000];
    char author[1000];
    char title[1000];

    void (*set_isbn)();
    void (*set_author)();
    void (*set_title)();    

    char *(*get_isbn)();
    char *(*get_author)();
    char *(*get_title)();   
    // void (*display)();   


} Book;

Book Book_new();
void Book_init(Book *obj);

#endif

Product.c

#include "Product.h"

/*-------------------------------*/

static void set_price(Product *this, double price) {
    this->price = price;
}

/*-------------------------------*/

static double get_price(Product *this) {
    return this->price; 
}

/*-------------------------------*/

static void display(Product *this) {

    AbstractProduct p = AbstractProduct_new();
    p.display(this);    

    printf("-Product- \n"); 
    printf("price: %f\n", this->get_price(this));   
    printf("\n");
}

/*-------------------------------*/

void Product_init(Product *obj) {

    AbstractProduct_init((AbstractProduct*)obj);

    obj->set_price = set_price;
    obj->get_price = get_price; 
    obj->display = display;

}

/*-------------------------------*/

Product Product_new() {

    Product aux;    
    Product_init(&aux);
    return aux;
}

Product.h

#ifndef Product_H
#define Product_H

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "AbstractProduct.h"

/***********************/

typedef struct Product{

    AbstractProduct;
    double price;

    void (*set_price)();
    double (*get_price)();  
    // void (*display)();   

} Product;

Product Product_new();
void Product_init(Product *obj);

#endif

TravelGuide.c

#include "TravelGuide.h"

/*-------------------------------*/

static void set_country(TravelGuide *this, char *country) {
    strcpy(this->country, country);
}

/*-------------------------------*/

static char *get_country(TravelGuide *this) {
    return this->country;   
}

/*-------------------------------*/

static void display(TravelGuide *this) {

    Book b = Book_new();
    b.display(this);

    printf("-TravelGuide- \n"); 
    printf("country: %s\n", this->get_country(this));   
    printf("\n");
}

/*-------------------------------*/

void TravelGuide_init(TravelGuide *obj) {

    Book_init((Book*)obj);
    obj->set_country = set_country;
    obj->get_country = get_country;
    obj->f = obj->display;
    obj->display = display;

}

/*-------------------------------*/

TravelGuide TravelGuide_new() {

    TravelGuide aux;
    TravelGuide_init(&aux);
    return aux;
}

TravelGuide.h

#ifndef TravelGuide_H
#define TravelGuide_H

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

#include "Book.h"

/***********************/

typedef struct TravelGuide{

    Book;
    char country[1000];
    void (*f)();

    void (*set_country)();
    char *(*get_country)();
    // void *(*display)();

} TravelGuide;

TravelGuide TravelGuide_new();
void TravelGuide_init(TravelGuide *obj);

#endif

Makefile

.PHONY: clean
define ANNOUNCE_BODY

    ***********************************************
    ************          start make **************
    ***********************************************
endef

all:
    $(info $(ANNOUNCE_BODY))    

    clear;
    if [ -f binary/main ]; then rm binary/main; fi;

# compiler 

    gcc $(INC) -c -fms-extensions main.c -o binary/main.o
    gcc $(INC) -c -fms-extensions AbstractProduct.c -o binary/AbstractProduct.o
    gcc $(INC) -c -fms-extensions Product.c -o binary/Product.o
    gcc $(INC) -c -fms-extensions Book.c -o binary/Book.o
    gcc $(INC) -c -fms-extensions TravelGuide.c -o binary/TravelGuide.o

# linker    

    gcc binary/main.o \
        binary/AbstractProduct.o \
        binary/Product.o \
        binary/Book.o \
        binary/TravelGuide.o \
        -o \
        binary/main

这对于概念验证很不错,但如果您必须走这么远才能在C中实现OOP,最好使用C ++。在我看来,C并不是为OOP设计的,如果它真的需要纯OOP(需要花费大量精力才能实现),那么您正在使用错误的语言。就我个人而言,我不想维护这样的代码。 - Alex
此外,我认为您采用的这种方法会浪费一些内存。与C++不同,每个实例将需要额外的sizeof(function pointer) * number of methods的空间来存储方法指针。在C++中,sizeof(class)不包括方法指针,并且如果存在虚方法,则包含1个指向虚函数表的附加指针。 - Alex
使用“.”调用方法更容易,而且不会污染命名空间。将new和init分开是有很多道理的。这是最好的答案,我不知道为什么得分如此之低。 - Vélimir
请你能否把回答简短一些? - MAChitgarha

-3

C语言不是面向对象的语言,因此没有继承。


-3
你可以模拟它,但你不能真正继承。

4
现实是什么?C ++只是一个非常简单的运行时库,用于调度和很多编译器语法,在需要时调用它。毕竟最初的C ++编译器生成的是C代码(实际上非常易读的C代码)。 - Javier

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接