5

我一直在研究我正在从事的项目的 C++ 和结构;目前我正在使用“链式”模板结构将数据字段添加为伪特征。

虽然它有效,但我认为我更喜欢多重继承,如下例所示:

struct a {
    int a_data;
}; // 'Trait' A

struct b {
    int b_data;
}; // 'Trait' B

struct c : public a, public b {
    int c_data;
}; // A composite structure with 'traits' A and B.

struct d : public b {
    int d_data;
}; // A composite structure with 'trait' B.

我的实验代码示例表明它们工作正常,但是当事情变得复杂时,我对它的实际工作方式有点困惑。

例如:

b * basePtr = new c;
cout << basePtr->b_data << endl;
b * basePtr = new d;
cout << basePtr->b_data << endl;

每次都可以正常工作,即使通过使用指针作为参数的函数调用也是如此。

我的问题是代码如何知道 b_data 存储在派生结构之一中的位置?据我所知,结构仍然使用没有额外数据的压缩结构(即 3 个 int 结构仅占用 12 个字节,2 个 int 8 个字节等)。当然,它需要某种额外的数据字段来说明 a_data 和 b_data 存储在给定结构中的位置吗?

这更像是一个好奇的问题,因为无论如何它似乎都可以工作,如果有多个实现在使用中,我会很乐意接受一个例子。虽然我确实有点担心,因为我想通过进程间消息队列传输这些结构后面的字节,并想知道它们是否会在另一端被解码(所有使用队列的程序都是由同一编译器编译并在单个平台上运行)。

4

7 回答 7

3

在这两种情况下,basePtr真的指向类型对象的指针b,所以没有问题。这个对象不是一个完整的对象,而是一个更衍生对象的子对象(这实际上是技术术语),这一事实并不重要。

d *从tob *和 from c *to的(静态、隐式)转换b *负责调整指针值,使其真正指向b子对象。所有信息都是静态已知的,因此编译器会自动进行所有这些计算。

于 2013-01-16T20:49:11.567 回答
2

您应该在内存管理类继承内容下阅读关于 C++ 类的维基百科值。

基本上,编译器创建类结构,因此在编译时它知道类的每个部分的偏移量。

当你调用一个变量时,编译器知道它的类型和它的结构,如果你把它转换成一个基类,它只需要跳转到右边的偏移量。

于 2013-01-16T20:49:16.150 回答
2

在大多数实现中,指针转换,比如 from c*to b*,将在必要时自动调整地址。在声明中

b * basePtr = new c;

new 表达式分配一个c对象,该对象包含一个a基类子对象、一个b基类子对象和一个c_data成员子对象。在原始内存中,这可能看起来只有三个整数。new 表达式返回创建的完整c对象的地址,该地址(在大多数实现中)与a基类子对象的地址和a_data成员子对象的地址相同。

但是new c,带有 type的表达式c*用于初始化b*指针,这会导致隐式转换。编译器设置为完整对象中基类子对象basePtr的地址。不难,因为编译器知道从对象到其唯一子对象的偏移量。bccb

之后,like 表达式basePtr->b_data不需要知道完整的对象类型是什么。它只知道它b_data位于 的开头b,因此它可以简单地取消引用b*指针。

于 2013-01-16T20:49:32.923 回答
1

细节取决于 C++ 实现,但在这种情况下,使用非虚拟继承,您可以这样想:

c有两个子对象,一个带有 type a,一个带有 type b

当您将指向 的指针强制转换c为指向 的指针时b,编译器足够智能,因此强制转换的结果是指向原始指针所引用对象的b子对象的指针。c这可能涉及更改返回指针的数值。

通常,单继承时,子对象指针将具有与原始指针相同的数值。对于多重继承,它可能不会。

于 2013-01-16T20:49:34.510 回答
1

是的,有一些额外的字段定义了每个子组件在聚合中的偏移量。但是它们并不存储在聚合本身中,而是最有可能(尽管关于如何做到这一点的最终选择留给编译器设计人员)存储在位于数据段隐藏侧的辅助结构中。

你的对象不是多态的(你错误地使用了它们,但我稍后会谈到),而只是像这样的化合物:

c[a[a_data],b[b_data],c_data];
            ^
            b* points here 

d[b[b_data],d_data]
  ^
  b* points here

(请注意,实际布局可能取决于特定的编译器甚至使用的优化标志)

b开头相对于开头的偏移量cd不依赖于特定对象实例,因此它不是保留在对象中所需的值,而只是在编译器已知但不一定可用的一般描述dc你。

编译器知道,给定 ac或 a db组件从哪里开始。但是给定 ab无法知道它是在 ad还是 a内c

您错误地使用对象的原因是您不关心它们的破坏。您将它们分配给new,但之后从未delete-ed 它们。

而且您不能只是调用delete baseptr,因为b子组件中没有任何内容可以告诉聚合它实际上是(在运行时)的一部分。

有两种编程风格可以解决它:

  • 经典的 OOP,假设实际类型在运行时是已知的,并假装所有类都有一个virtual析构函数:它为所有结构提供了一个额外的“ghost”字段(v-table 指针,指向“辅助描述符”,包含所有虚函数的地址),使发起的析构函数调用delete实际上被分派到最派生的那个(因此 deletepbase将实际调用c::~cd::~d取决于实际对象)

  • 泛型编程风格,假设您以其他方式(很可能从模板参数)知道实际的派生类型,所以您不会delete pbase,而是static_cast<actual_derived_class*>(pbase)

于 2013-01-16T21:08:30.187 回答
0

继承是一种方法的抽象,用于重用其下另一个类的函数。如果该方法位于其下方的类中,则可以从该类中调用该方法。结构使您能够像在数据结构中一样拥有变量,类似于使用变量或函数的类。

class trait
{
  //variable definition 
  //variable declaration

  function function_name(variable_type variable_name, and more)
  {
    //operation on variables in function call
  }

  variable_name = function_name(variable_name);

  struct struct_name
  { 
    //variable definition
  }

  struct_name = {value_1, value_2, and more}

  operation on struct_name.value_1
} 
于 2013-01-16T21:27:55.907 回答
0

编译时知识和运行时知识是有区别的。编译器的部分工作是尽可能多地使用编译时信息,以避免必须在运行时执行操作。

在这种情况下,每条数据在给定类型中的确切位置的所有细节在编译时都是已知的。所以编译器不需要在运行时知道它。每当您访问特定成员时,它只会使用其编译时知识来计算您需要的数据的适当偏移量。

指针转换也是如此。它将在转换指针值时调整指针值,以确保该点位于适当的子部分。

这样做的部分原因是来自单个类或结构的数据值永远不会与类定义中未提及的任何其他数据值交错,即使该结构是另一个结构的子组件,无论是通过组合还是继承. 因此,任何单个结构的相对布局总是相同的,无论它在内存中的哪个位置找到。

于 2013-01-17T03:46:11.973 回答