我一直在研究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个内存8个字节,等等)。当然,它需要某种额外的数据字段来说明a_data和b_data存储在给定结构中的位置?
这更像是一个好奇心问题,因为它似乎都可以正常工作,如果有多个实现在使用中,我会很乐意接受一个例子。虽然我确实有一点担心,因为我想通过进程间消息队列传输这些结构后面的字节,并想知道它们是否会在另一端被解码好(所有使用该队列的程序都将是由相同的编译器编译并在单个平台上运行。)
答案 0 :(得分:3)
在这两种情况下,basePtr
真正是指向b
类型对象的指针,因此没有问题。事实上,这个对象不是一个完整的对象,而是一个更加派生的对象的子对象(这实际上是技术术语),并不重要。
从d *
到b *
以及从c *
到b *
的(静态,隐式)转换负责调整指针值,以便真正实现指向b
子对象。所有信息都是静态知道的,因此编译器会自动进行所有这些计算。
答案 1 :(得分:2)
您应该在 memory management 和 class inheritance 内容下阅读C ++类的维基百科值。
基本上,编译器会创建类结构,因此在编译时它会知道类的每个部分的偏移量。
当你调用一个变量时,编译器会知道它的类型及其结构,如果你把它转换为基类,它只需要跳到右边的集合。
答案 2 :(得分:2)
在大多数实现中,指针转换(例如从c*
到b*
)将在必要时自动调整地址。在声明中
b * basePtr = new c;
新表达式分配一个c
对象,该对象包含a
基类子对象,b
基类子对象和c_data
成员子对象。在原始内存中,这可能看起来只有三个整数。 new表达式返回创建的完整c
对象的地址,该对象(在大多数实现中)与a
基类子对象的地址和a_data
成员的地址相同子对象。
但是,类型new c
的表达式c*
用于初始化b*
指针,这会导致隐式转换。编译器将basePtr
设置为完整b
对象中c
基类子对象的地址。不难,因为编译器知道从c
对象到其唯一b
子对象的偏移量。
之后,像basePtr->b_data
这样的表达式不需要知道完整的对象类型是什么。它只知道b_data
位于b
的最开头,因此它可以简单地取消引用b*
指针。
答案 3 :(得分:1)
这个细节取决于C ++实现,但在这种情况下,使用非虚拟继承,您可以这样想:
c有两个子对象,一个是a类型,另一个是b类型。
当你将指向c的指针转换为指向b的指针时,编译器足够聪明,因此强制转换的结果是指向原始指针引用的c对象的b子对象的指针。这可能涉及更改返回指针的数值。
通常,对于单继承,子对象指针将具有与原始指针相同的数值。对于多继承,它可能不会。
答案 4 :(得分:1)
是的,有额外的字段定义每个子组件进入聚合的偏移量。但是不存储在聚合本身中,但最有可能(尽管最终选择如何做到这一点留给编译器设计者)在辅助结构中驻留在数据段的隐藏一侧。
你的对象不是多态的(并且你错误地使用了它们,但我稍后会谈到它),但只是像以下那样的化合物:
c[a[a_data],b[b_data],c_data];
^
b* points here
d[b[b_data],d_data]
^
b* points here
(注意,实际布局可能取决于特定的编译器甚至使用的优化标志)
b
开头对c
或d
开头的偏移不依赖于特定的对象nstance,因此它不是保持在对象,但只是在编译器已知的一般d
和c
描述中,但不一定适用于您。
在给定c
或d
的情况下,编译器知道b
组件的开始位置。但是,如果b
无法知道它是否位于d
或c
内。
tou之所以错误地使用了这个对象,你不关心它们的破坏。您使用new
进行分配,但绝不使用delete
。
并且您不能只调用delete baseptr
,因为b
子组件中没有任何内容可以告诉它实际上(在运行时)的聚合内容。
有两种编程风格可供选择:
经典的OOP,假设实际类型在运行时是已知的,并假装所有类都有一个virtual
析构函数:它为所有结构提供了一个额外的“ghost”字段(v-表指针,指向“辅助描述符”中的一个表,其中包含所有虚函数地址),使得由delete
发起的析构函数调用实际上被调度到最派生的一个(因此删除{{1}实际上会根据实际对象调用pbase
或c::~c
通用编程风格,假设您以其他方式(很可能来自模板参数)知道实际的派生类型,因此您不会d::~d
,而是delete pbase
答案 5 :(得分: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
}
答案 6 :(得分:0)
编译时知识和运行时知识之间存在区别。编译器的部分工作是尽可能多地使用编译时信息,以避免在运行时做任何事情。
在这种情况下,编译时已知每个数据在给定类型中的确切位置的所有细节。因此编译器不需要在运行时知道它。无论何时访问特定成员,它只使用其编译时知识来计算所需数据的适当偏移量。
指针转换同样如此。它将在转换时调整指针值,以确保该点位于相应的子部分。
部分原因是来自单个类或结构的数据值永远不会与类定义中未提及的任何其他数据值交错,即使该结构是另一个结构的子组件也是如此。通过组合或继承。所以任何单个结构的相对布局总是相同的,无论它在哪里找到它。