【问题标题】:Need way to alter common fields in different structs需要改变不同结构中的公共字段的方法
【发布时间】:2009-09-22 04:47:12
【问题描述】:

我在这里用 C 语言编程,用于 Windows 和各种 Unix 平台。我有一组具有公共字段的结构,但也有不同的字段。例如:

typedef struct {
    char street[10];
    char city[10];
    char lat[10];
    char long[10];
} ADDR_A;

typedef struct {
    char street[10];
    char city[10];
    char zip[10];
    char country[10];
} ADDR_B;

它们显然没有那么简单,我实际上有 6 个不同的结构,但这是基本思想。

我想要的是一个函数,我可以在其中传递指向其中一个的指针并能够更新公共字段。比如:

static void updateFields( void *myStruct ) {
    strcpy( myStruct->street, streetFromSomethingElse );
    strcpy( myStruct->city, cityFromSomethingElse );
}

显然,由于 void 指针和某种类型的强制转换,我在那里写的内容不起作用,但实际上并没有一个很好的方法来强制转换它(有吗?)。我不反对以某种方式指定结构类型的附加参数。

由于不相关的原因,我不能更改这些结构或将它们组合或用它们做任何其他事情。我必须照原样使用它们。

一如既往,提前感谢您的帮助。

编辑:正如我在下面的评论中添加的那样,不保证公共字段全部在开头或全部在结尾或其他任何内容。它们只是保证存在。

【问题讨论】:

  • 你不能有一个名为'long'的char类型数组的结构成员!

标签: c pointers function struct


【解决方案1】:

demeter 规则 - 只暴露最少的结构 - 可能适用。

static void updateStreetAndCity ( char *street, char *city ) {
    strcpy( street, streetFromSomethingElse );
    strcpy( city, cityFromSomethingElse );
}

这有一点开销,你必须用两个指针而不是一个指针来调用它,但它符合一个适用于所有结构类型的函数的账单。

如果您的结构大小不同,您可以使用宏来提供静态多态性,就像在 C99 的 tgmath.h 中所做的那样,但这需要大量工作。

【讨论】:

  • 绝对是一个可靠的想法,不幸的是,“真正的”应用程序应该是 20 多个指针而不是一个。可能仍然是我考虑的事情。
【解决方案2】:
#define ASSIGN(type,object,member,value) do { \
    ((type *)object)->member = value; \
    } while (0)

现在您可以执行以下操作:

#include <stdio.h>

struct foo {
    int x;
    char y;
};

struct bar {
    char y;
    int x;
};

int main () {
    struct foo foo;
    struct bar bar;
    ASSIGN(struct foo, &foo, x, 100);
    ASSIGN(struct bar, &bar, y, 'x');
    // ...
}

当然,您总是可以扩展它以合并一个函数调用来执行操作,因此支持 memcpystrcpy 或类似的将是微不足道的。

【讨论】:

  • 哦,看来它肯定会成为赢家。也许这就是马克的意思,但你在这里布置东西的方式确实为我打开了灯泡。
  • -1。我在这里错过了什么吗? ASSIGN(struct foo, &amp;foo, x, 100); 最好写成foo-&gt;x = 100;
【解决方案3】:

这是我插入 C++ 的时候,因为这是多态性的明显用途。或模板。或者函数重载。

但是,如果所有公共字段都在前,您应该能够将指针转换为 ADDR_A 并将其视为:

static void updateFields( void *myStruct ) {
    ADDR_A *conv = myStruct;
    strcpy( conv->street, streetFromSomethingElse );
    strcpy( conv->city, cityFromSomethingElse );
}

【讨论】:

  • 哈!你插入 C++ 就好像我实际上已经选择使用 C 一样。:-p
  • @Morinar:你一直在寻找为什么每个提议的理由都行不通的原因。它们似乎是很好的理由,但我得出的结论是,在你打破一些限制之前,你注定要为此挣扎。
  • 另外,请注意,考虑到布局不同的约束,C++ 在这方面会遇到问题。您需要一个带有一堆纯虚拟 getter 和 setter 的抽象基类,所有这些都将在每个子类中单独实例化。喜悦。
【解决方案4】:

您可以使用宏 - 如果 Adam 建议的填充不起作用

#define UPDATE_FIELDS( myStruct, aStreet, aCity ) \
  strcpy( myStruct->street, aStreet ); \
  strcpy( myStruct->city, aCity );

但是c++插件更好;)

【讨论】:

  • 这也是一个好主意,但是有大约 20 个公共字段,其中许多需要一些额外的处理来计算。我想它会起作用,但有资格成为“来自地狱的宏”。
【解决方案5】:

只要您希望更新的部分是每个结构上的公共前缀,您就可以定义一个列出这些前缀字段的新结构,并将您的参数转换为此。假设您创建的前缀结构的布局是相同的(即没有奇怪的编译器填充问题),更新将适用于所有具有该前缀的结构。

编辑:正如 sztomi 正确指出的那样,只要字段的相对偏移量相同,那么您就可以访问非前缀元素。但是,我仍然更喜欢创建一个通用结构来充当此功能的“模板”;它可以防止在您可能选择的任何其他结构中更改字段名称。

【讨论】:

  • 我要补充一点,您实际上并不需要一个公共前缀,只是您要更改的相互字段的偏移量是相同的。而且您不需要一个通用的“基础”结构,您可以选择其中一个,然后转换为它。
  • 这是个好主意,但是,它们没有相同的偏移量。在我的例子中,这只是一个巧合。
【解决方案6】:

考虑到结构至少没有类似的布局,实现这一点的方法有限,既不短也不优雅:

  1. 使用哈希表而不是结构来存储数据。这样做可以访问以它们的名称作为哈希表中的键的字段,而不管实际类型如何。这在 C 语言中需要很多麻烦,而且只有当你需要访问一百个字段时,我才不推荐它。

  2. 坚持强类型并明确更新字段。是的,这不是你想要的,但对于中等数量的字段,这可能没问题。

  3. 按照其他人的建议使用宏。但是我认为最好尽可能避免使用宏。不值得。

在 C 中一起破解这样的东西既不容易也不好(没有鸭子打字)。这是一个典型的例子,动态语言会更舒服(也许你可以从你的代码中调用 python?:))

【讨论】:

  • 我会接受这一点,因为它最准确地总结了各种选项。我的实际解决方案是结合不同的字段更新和一些宏帮助。正如 David Thornley 在另一条评论中指出的那样,我似乎总是受到可笑的限制,这确实阻碍了我使用大多数这些解决方案的能力。我确实要感谢大家的反馈……如果不出意外,我确实学到了很多东西。
【解决方案7】:

只是一个未经验证的想法:

struct ADDR_X_offset {
  int street;
  int city;
} ADDR_A_offset = { offsetof(ADDR_A,street), offsetof(ADDR_A,city) },
  ADDR_B_offset = { offsetof(ADDR_B,street), offsetof(ADDR_B,city) };

static void updateFields( void *myStruct, struct ADDR_X_offset *offset ) {

    strcpy( myStruct+offset->street, streetFromSomethingElse );

    strcpy( myStruct+offset->city,   cityFromSomethingElse );

}

【讨论】:

  • 我个人会使用指针而不是偏移量。不过,我喜欢这个主意。 +1
【解决方案8】:

尚未提及的一个选项是使用来自&lt;stddef.h&gt;offsetof() 宏为字段创建描述符。然后,您可以将适当的描述符传递给修改函数。

typedef struct string_descriptor
{
    size_t offset;
    size_t length;
} string_descriptor;

在 C99 中,您可以执行以下操作:

int update_string(void *structure, const string_descriptor d, const char *new_val)
{
    char *buffer = (char *)structure + d.offset;
    size_t len = strlen(new_val);
    size_t nbytes = MIN(len, d.length);
    // Optionally validate lengths, failing if the new value won't fit, etc
    memcpy(buffer, new_val, nbytes);
    buffer[nbytes] = '\0';
    return(0);  // Success
}

void some_function(void)
{
    ADDR_A a;
    ADDR_B b;
    static const string_descriptor a_des =
        { .offset = offsetof(ADDR_A, street), .length = sizeof(a.street) };
    static const string_descriptor b_des =
        { .offset = offsetof(ADDR_B, street), .length = sizeof(b.street) };

    update_string(&a, a_des, "Regent St."); // Check return status for error!
    update_string(&b, b_des, "Oxford St."); // Check return status for error!
}

显然,对于单个类型,这看起来很笨拙,但是通过仔细概括,您可以使用多个描述符,并且描述符可以更大(更复杂)但通过引用传递,并且只需要定义一次,等等。描述符,相同的update_string() 函数可用于更新两个结构中的任何一个中的任何字段。棘手的一点是长度初始化器sizeof(b.street);我认为这需要一个正确类型的变量来提供正确的信息,尽管我很高兴接受(标准 C99)修订以消除该限制。非最小通用描述符可以包含类型指示符和成员名称以及其他可能相关的信息 - 尽管有详细说明,但我坚持使用非常简单的结构。您可以使用函数指针为不同的字符串提供不同的验证 - 纬度和经度的验证与街道名称的验证非常不同。

您还可以编写函数来获取描述符数组并进行多次更新。例如,您可以为 ADDR_A 中的字段设置一个描述符数组,为 ADDR_B 设置另一个描述符数组,然后为新字符串数组中的字段传递新值,等等。

您有责任获取正确的描述符并使用正确的描述符。


另请参阅:What is the purpose and return type of the __builtin_offsetof operator?

【讨论】:

    【解决方案9】:

    如果你不能以任何方式改变结构,我认为传入类型的想法是一个很好的想法。制作一堆对地址做一件事的函数,并让它接受地址的类型。

    int updateStreetAndCity(void *addr, int type);
    double getLatitude(void *addr, int type);
    

    等等。您可以有一个枚举或几个宏定义来创建类型。然后在每个函数中,根据你传入的类型,调用另一个特定于该类型的函数来为你完成工作(以避免一个怪物 switch 语句,它会做一千种不同的事情)。

    【讨论】:

      【解决方案10】:

      如果你没有被传递的结构的类型(就像你的 void* 建议的那样),基本上你注定要失败。它们只是一堆有起始地址的内存。

      可以做的是有几个函数,比如你建议的 update_fields,并根据结构的类型以某种方式调用正确的函数(那个很简单,也可以相当隐藏在宏中以避免语法噪音)。

      这样做你会得到如下代码:

      #include <stdio.h>
      #include <string.h>
      
      typedef struct {
          char street[10];
          char city[10];
          char lat[10];
          char lon[10];
      } ADDR_A;
      
      typedef struct {
          char street[10];
          char city[10];
          char zip[10];
          char country[10];
      } ADDR_B;
      
      #define UPDATEFIELDS(type, value) updateFields_##type(value)
      
      static void updateFields_ADDR_A(ADDR_A *myStruct ) {
              strcpy(myStruct->street, "abc" );
              strcpy(myStruct->city, "def" );
          }
      
      static void updateFields_ADDR_B(ADDR_B *myStruct ) {
              strcpy(myStruct->street, "abc" );
              strcpy(myStruct->city, "def" );
          }
      
      int main(){
          ADDR_A a;
          ADDR_B b;
      
          updateFields_ADDR_A(&a);
          updateFields_ADDR_B(&b);
          UPDATEFIELDS(ADDR_A, &a);
      
          printf("%s\n", a.city);
          printf("%s\n", b.city);
      }
      

      请注意,您显然不应该在 updateFields_XXX 函数中编写实际的字段更改代码,而只需使用该代码作为包装器来获取内部字段的正确偏移量。

      我你也想有代码局部性,你可以再次使用宏,

      #define COMMONBODY strcpy(myStruct->street, "abc" );\
              strcpy(myStruct->city, "def" );
      
      
      static void updateFields_ADDR_A(ADDR_A *myStruct ) {
          COMMON_BODY
          }
      
      static void updateFields_ADDR_B(ADDR_B *myStruct ) {
          COMMON_BODY
          }
      

      甚至将通用代码放在一个单独的文件中并包含两次(非常不寻常,但可以工作的文件)

      static void updateFields_ADDR_A(ADDR_A *myStruct ) {
          #include "commonbody.c"
          }
      
      static void updateFields_ADDR_B(ADDR_B *myStruct ) {
          #include "commonbody.c"
          }
      

      除了名称修饰部分之外,使用 typeof 运算符也几乎有可能(但据我所知并不完全)。

      如果您的结构大小不同,我们甚至可以只使用一个 updateFields 函数(带有 void*),它将结构的大小作为第二个参数,并使用它自动转换为正确的类型。这很脏但可能[如果有人想看到开发该解决方案,请发表评论]。

      您也可以使用联合。如果有一个公共前缀,则可以使用任何联合成员访问公共字段(但我知道不是这样)。您必须了解,定义这样的联合不会改变现有数据结构的任何内容。这基本上定义了一个 ADDR_A 或 ADDR_B 类型,而不是可以使用指针引用。

      typedef union {
          ADDR_A a;
          ADDR_B b;
      } ADDR_A_or_B;
      

      如果有公共前缀,您可以使用任何访问器字段设置这些变量:

      static void updateFields(ADDR_A_or_B *myStruct ) {
          strcpy( myStruct->a.street, someStreeValueFromSomewhereElse);
          strcpy( myStruct->a.city, someCityValueFromSomewhereElse);
      }
      

      你只需要在调用时进行投射:

      ADDR_A a;
      updateFields((ADDR_A_or_B *)&a);
      
      ADDR_B b;
      updateFields((ADDR_A_or_B *)&b);
      

      【讨论】:

      • 我认为他使用 void* 来模拟多态,并且他知道实际类型是 ADDR_AADDR_B
      • 是的,如果是这样,建议的通用前缀技巧就可以了。我的意思是 void * 删除所有关于类型的显式信息,甚至绕过编译器类型检查。但是你给了我一个想法,我会在我的答案中添加,因为还没有人这样做:定义一个联合类型。
      【解决方案11】:
      typedef struct {
          char street[10];
          char city[10];
      } ADDR_BASE;
      
      typedef struct {
          struct ADDR_BASE addr;
          char lat[10];
          char long[10];
      } ADDR_A;
      
      typedef struct {
          struct ADDR_BASE addr;
          char zip[10];
          char country[10];
      } ADDR_B;
      

      这样您就可以将 ADDR_BASE* 传递给您的函数。

      【讨论】:

      • 问题说明结构不能被修改。
      猜你喜欢
      • 2015-12-17
      • 2013-08-10
      • 2014-09-09
      • 1970-01-01
      • 2020-12-04
      • 2014-08-04
      • 2022-06-13
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多