向结构体中添加字段而不破坏现有代码

4

我正在处理一个庞大的代码库,发现其中一个结构体缺少一个重要字段。我尽可能仔细地查看了使用该结构体的代码,并得出结论:添加额外的字段不会破坏它。

你有什么想法是我可能搞错了哪里吗?

此外:欢迎提供设计建议 - 我最好的实现方式是什么?

例如:(如果我没有表达清楚):

typedef struct foo
{
  int a;
  int b;
}
foo;

现在是:
typedef struct foo
{
  int a;
  int b;
  int c;
}
foo;

正如我所说,这是一个庞大的代码库,我无法发布。我只是想请教一下设计方面的技巧和我可能错过的东西。 - Jacob
我不知道有没有尝试过对齐数据 - 我没有看到任何的尝试,还有其他需要我查找的吗? - Jacob
当然会出问题。除非该结构仅在5个或更少的文件中使用,否则您绝对无法确定它是否仍然有效。在这种情况下,重新编译它们不是问题。起身改变领域并重新编译所有内容。 - Martin York
1
所以你添加了这个字段,重新编译了所有内容,运行它,结果它崩溃了?还是你只是在双重检查它不应该破坏任何东西? - Andrew Barrett
你为什么认为你搞砸了? - Alex Brown
显示剩余3条评论
10个回答

7

如果该结构在任何地方进行了序列化/反序列化,请确保注意代码的该部分。

仔细检查内存分配的代码区域。


这是一个很好的想法 - 幸运的是目前没有序列化。 - Jacob

4

如果你在所有地方使用sizeof(struct)来分配内存,并使用->或.操作符访问成员,我认为你不应该遇到任何问题。但是,这也取决于你尝试添加成员的位置,如果不小心可能会破坏结构对齐。


结构体对齐,谢谢!我正在寻找类似这样的东西。 - Jacob

4
从你上面写的内容来看,我看不出什么问题。 我能想到的有两个问题:
  1. 每当您更改代码并重新编译时,都会引入查找“隐藏”错误的能力。 也就是说,未初始化的指针可能会使您的新数据结构刚好足够大而被损坏。
  2. 您是否确保在使用 c 之前对其进行初始化?

后续:

既然你还没有找到错误,我建议你停止查看你的结构体。有人曾经说过,首先找马,其次找斑马。也就是说,错误可能不是一种奇异的错误。你的单元测试覆盖率有多少?我假设这是遗留代码,几乎肯定意味着0%或至少是我的经验。这准确吗?


哈哈,有一次它被初始化了,我忘记了 - 这很可能会被接受 :) - Jacob
那就不要把我标记为答案。如果还是不起作用,我们可以继续集思广益。这就是这个网站的宗旨,不是吗? - wheaties
好的,这正是重点,它完美地运行。我只是试图预见任何问题。由于现有代码不依赖于对齐,因此初始化是我忽视的唯一问题。 - Jacob

2

有没有想过可能是哪里出了问题?

没问题,但也有问题。这完全取决于使用方式、地点和原因。

假设你所说的结构是 C 风格的 POD,并且代码非常简单,那么你可以轻松应对。但是,一旦你尝试更具野心的东西,你就会遇到对齐问题(取决于如何创建对象和位置)以及至少填充问题。如果这是 C++ 并且你的 POD 包含自定义运算符/构造函数等 - 你会遇到很多麻烦。如果你依赖字节序,可能会出现跨平台问题。


2
如果这段代码有一个强大的单元测试集合,那么跟踪问题可能会容易得多(因为您要求设计建议;))
我猜您不需要在这个巨大的代码库中到处使用新的“c”变量,而是添加它以便在添加或修改某些代码时使用它? 您可以创建一个包含foo对象和c的新结构体bar,而不是将c添加到foo中。 然后在需要时使用bar。
至于实际的错误,由于信息太少,它可能是任何东西,但如果我必须猜测,我会说有人在某个地方使用了一个魔数,而不是sizeof()。

1

既然你的问题标记为C++:

未来,Pimpl/d-Pointer是一种策略,它允许您在扩展或重新设计类时拥有更大的自由度,而不会破坏兼容性。

例如,如果您最初编写了

// foo.h
class Foo {
public:
    Foo();
    Foo(const Foo &);
    ~Foo();
    int a() const;
    void a(int);
    int b() const;
    void b(int);
private:
    class FooPrivate *const d;
};

// foo.c
class FooPrivate {
public:
    FooPrivate() : a(0), b(0) {}
    FooPrivate(const FooPrivate &o) : a(o.a), b(o.b) {}
    int a;
    int b;
};
Foo::Foo() : d(new FooPrivate()) {}
Foo::Foo(const Foo &o) : d(new FooPrivate(*o->d)) {}
Foo::~Foo() { delete d; }
int Foo::a() const { return d->a; }
void Foo::a(int a) { d->a = a; }
// ...

你可以很容易地将其扩展到

// foo.h
class Foo {
public:
    // ...
    int a() const;
    void a(int);
    int b() const;
    void b(int);
    int c() const;
    void c(int);
    // ...
};

// foo.c
class FooPrivate {
    // ...
    int a;
    int b;
    int c;
};
// ...

使用Foo而不破坏任何现有(已编译的)代码。


1

查找 memcpy, memset, memcmp。这些函数不是逐成员的。如果使用以前的结构长度使用它们,可能会出现问题。

还要搜索文件中每个 struct 的实例。可能有一些函数或方法没有使用新的重要字段。正如其他人所说,如果在 #definetypedef 中找到该结构,则必须搜索这些内容。


0
如果代码用于在网络上传输数据,那么你可能会破坏一些东西。

0
如果在结构体成员中添加任何位置而不是第一个成员会导致任何问题,那么代码就具有未定义的行为,这是错误的。因此,至少你可以责怪别人(或者之前的自己)造成了破坏。但是,是的,未定义的行为包括“恰好做我们想要它做的事情”,所以像其他人说的那样,要注意内存分配、序列化(网络和文件IO)。
顺便说一下,当我看到typedef FOO ... struct FOO时,我总是感到不安,好像有人试图让C代码看起来像C++。我意识到我在这里是少数派 :)

在C++中,typedef struct FOO ... FOO同样是毫无意义的,因为struct FOO已经使得FOO可以在没有struct前缀的情况下使用。 - ephemient

0

在C结构体的末尾添加新元素总是安全的,即使该结构体被传递给不同的进程。已重新编译的代码将看到新的结构体成员,而未重新编译的代码仅知道旧的结构体大小并读取其了解的旧成员。 这里需要注意的是,新成员必须添加在结构体的末尾而不是中间。


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