Pregunta

Recientemente me preguntaron en una entrevista sobre el diseño de objetos con funciones virtuales y una herencia múltiple involucrada.
Lo expliqué en contexto de cómo se implementa sin una herencia múltiple involucrada (es decir, cómo el compilador generó la tabla virtual, inserte un puntero secreto en la tabla virtual en cada objeto, etc.).
Me pareció que faltaba algo en mi explicación.
Así que aquí hay preguntas (ver el ejemplo a continuación)

  1. ¿Cuál es el diseño de memoria exacta del objeto de la clase C.
  2. Entradas de tablas virtuales para la clase C.
  3. Tamaños (como se devuelve por sizeOf) de objeto de las clases A, B y C. (8, 8, 16 ??)
  4. ¿Qué pasa si se usa la herencia virtual? ¿Seguramente los tamaños y las entradas de la tabla virtual deberían verse afectadas?

Código de ejemplo:

class A {  
  public:   
    virtual int funA();     
  private:  
    int a;  
};

class B {  
  public:  
    virtual int funB();  
  private:  
    int b;  
};  

class C : public A, public B {  
  private:  
    int c;  
};   

¡Gracias!

¿Fue útil?

Solución

El diseño de memoria y el diseño VTable dependen de su compilador. Usando mi GCC, por ejemplo, se ven así:

sizeof(int) == 4
sizeof(A) == 8
sizeof(B) == 8
sizeof(C) == 20

Tenga en cuenta que SizeOf (INT) y el espacio necesario para el puntero VTable también pueden variar de compilador a compilador y plataforma a plataforma. La razón por la cual SeaTeOf (C) == 20 y no 16 es que GCC le da 8 bytes para el subobjeto A, 8 bytes para el subobjeto B y 4 bytes para su miembro int c.

Vtable for C
C::_ZTV1C: 6u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI1C)
8     A::funA
12    (int (*)(...))-0x00000000000000008
16    (int (*)(...))(& _ZTI1C)
20    B::funB

Class C
   size=20 align=4
   base size=20 base align=4
C (0x40bd5e00) 0
    vptr=((& C::_ZTV1C) + 8u)
  A (0x40bd6080) 0
      primary-for C (0x40bd5e00)
  B (0x40bd60c0) 8
      vptr=((& C::_ZTV1C) + 20u)

Usar herencia virtual

class C : public virtual A, public virtual B

el diseño cambia a

Vtable for C
C::_ZTV1C: 12u entries
0     16u
4     8u
8     (int (*)(...))0
12    (int (*)(...))(& _ZTI1C)
16    0u
20    (int (*)(...))-0x00000000000000008
24    (int (*)(...))(& _ZTI1C)
28    A::funA
32    0u
36    (int (*)(...))-0x00000000000000010
40    (int (*)(...))(& _ZTI1C)
44    B::funB

VTT for C
C::_ZTT1C: 3u entries
0     ((& C::_ZTV1C) + 16u)
4     ((& C::_ZTV1C) + 28u)
8     ((& C::_ZTV1C) + 44u)

Class C
   size=24 align=4
   base size=8 base align=4
C (0x40bd5e00) 0
    vptridx=0u vptr=((& C::_ZTV1C) + 16u)
  A (0x40bd6080) 8 virtual
      vptridx=4u vbaseoffset=-0x0000000000000000c vptr=((& C::_ZTV1C) + 28u)
  B (0x40bd60c0) 16 virtual
      vptridx=8u vbaseoffset=-0x00000000000000010 vptr=((& C::_ZTV1C) + 44u)

Usando GCC, puede agregar -fdump-class-hierarchy para obtener esta información.

Otros consejos

1 cosa que esperar con múltiples herencias es que su puntero puede cambiar al lanzar a una subclase (por lo general no primero). Algo que debe tener en cuenta al depurar y responder preguntas de la entrevista.

Primero, una clase polimórfica tiene al menos una función virtual, por lo que tiene un VPTR:

struct A {
    virtual void foo();
};

se compila a:

struct A__vtable { // vtable for objects of declared type A
    void (*foo__ptr) (A *__this); // pointer to foo() virtual function
};

void A__foo (A *__this); // A::foo ()

// vtable for objects of real (dynamic) type A
const A__vtable A__real = { // vtable is never modified
    /*foo__ptr =*/ A__foo
};

struct A {
    A__vtable const *__vptr; // ptr to const not const ptr
                             // vptr is modified at runtime
};

// default constructor for class A (implicitly declared)
void A__ctor (A *__that) { 
    __that->__vptr = &A__real;
}

Observación: C ++ se puede compilar a otro lenguaje de alto nivel como C (como lo hizo Cfront) o incluso a un subconjunto de C ++ (aquí C ++ sin virtual). puse __ En los nombres generados por el compilador.

Tenga en cuenta que este es un simplista modelo donde RTTI no es compatible; Los compiladores reales agregarán datos en el VTable para admitir typeid.

Ahora, una clase derivada simple:

struct Der : A {
    override void foo();
    virtual void bar();
};

Los subobjetos de clase base no virtuales (*) son subobjetos como subobjetos de miembros, pero mientras que los subobjetos miembros son objetos completos, es decir. Su tipo real (dinámico) es su tipo declarado, los subobjetos de clase base no están completos y su cambio de tipo real durante la construcción.

(*) Las bases virtuales son muy diferentes, como las funciones de miembros virtuales son diferentes de los miembros no virtuales

struct Der__vtable { // vtable for objects of declared type Der
    A__vtable __primary_base; // first position
    void (*bar__ptr) (Der *__this); 
};

// overriding of a virtual function in A:
void Der__foo (A *__this); // Der::foo ()

// new virtual function in Der:
void Der__bar (Der *__this); // Der::bar ()

// vtable for objects of real (dynamic) type Der
const Der__vtable Der__real = { 
    { /*foo__ptr =*/ Der__foo },
    /*foo__ptr =*/ Der__bar
};

struct Der { // no additional vptr
    A __primary_base; // first position
};

Aquí "Primera posición" significa que el miembro debe ser primero (otros miembros podrían ser reordenados): están ubicados en Offset Zero para que podamos reinterpret_cast punteros, los tipos son compatibles; En el desplazamiento no cero, tendríamos que hacer ajustes de puntero con aritmética en char*.

La falta de ajuste puede no parecer un gran problema en términos de código generado (solo algunos agregan instrucciones de ASM inmediatas), pero significa mucho más que eso, significa que tales punteros pueden considerarse que tienen diferentes tipos: un objeto de tipo A__vtable* puede contener un puntero a Der__vtable y ser tratado como un Der__vtable* o A__vtable*. El mismo objeto de puntero sirve como puntero a un A__vtable en funciones que se ocupan de objetos de tipo A y como un puntero a un Der__vtable en funciones que se ocupan de objetos de tipo Der.

// default constructor for class Der (implicitly declared)
void Der__ctor (Der *__this) { 
    A__ctor (reinterpret_cast<A*> (__this));
    __this->__vptr = reinterpret_cast<A__vtable const*> (&Der__real);
}

Usted ve que el tipo dinámico, según lo definido por el VPTR, cambia durante la construcción, ya que asignamos un nuevo valor al VPTR (en este caso particular, la llamada al constructor de clase base no hace nada útil y puede optimizarse, pero no es ' t El caso con constructores no triviales).

Con herencia múltiple:

struct C : A, B {};

A C La instancia contendrá un A y un B, como eso:

struct C {
    A base__A; // primary base
    B base__B;
};

Tenga en cuenta que solo uno de estos subobjetos de clase base puede tener el privilegio de sentarse en la compensación cero; Esto es importante en muchos sentidos:

  • La conversión de punteros a otras clases base (upcasts) necesitará un ajuste; Por el contrario, los upcasts necesitan los ajustes opuestos;

  • Esto implica que al hacer una llamada virtual con un puntero de clase base, el this tiene el valor correcto para la entrada en el exaseño de clase derivado.

Entonces el siguiente código:

void B::printaddr() {
    printf ("%p", this);
}

void C::printaddr () { // overrides B::printaddr()
    printf ("%p", this);
}

se puede compilar a

void B__printaddr (B *__this) {
    printf ("%p", __this);
}

// proper C::printaddr taking a this of type C* (new vtable entry in C)
void C__printaddr (C *__this) {
    printf ("%p", __this);
}

// C::printaddr overrider for B::printaddr
// needed for compatibility in vtable
void C__B__printaddr (B *__this) {
    C__printaddr (reinterpret_cast<C*>(reinterpret_cast<char*> (__this) - offset__C__B));
}

Vemos el C__B__printaddr El tipo declarado y la semántica son compatibles con B__printaddr, para que podamos usar &C__B__printaddr en la vtable de B; C__printaddr no es compatible pero se puede usar para llamadas que involucran un C objetos o clases derivadas de C.

Una función de miembro no virtual es como una función gratuita que tiene acceso a cosas internas. Una función de miembro virtual es el "punto de flexibilidad" que se puede personalizar anulando. La declaración de función de los miembros virtuales juega un papel especial en la definición de una clase: al igual que otros miembros, son parte del contrato con el mundo externo, pero al mismo tiempo son parte de un contrato con la clase derivada.

Una clase base no virtual es como un objeto miembro donde podemos refinar el comportamiento a través de la anulación (también podemos acceder a miembros protegidos). Para el mundo externo, la herencia para A en Der implica que existirán conversiones implícitas derivadas de base para punteros, que un A& puede estar atado a un Der lvalue, etc. para más clases derivadas (derivadas de Der), también significa que las funciones virtuales de A son heredados en el Der: Funciones virtuales en A Se puede anular en clases derivadas adicionales.

Cuando se deriva una clase, digamos Der2 se deriva de Der, Conversiones implícitas Un consejo de tipo Der2* a A* se realiza semánticamente en paso: primero, una conversión a Der* se valida (el control de acceso a la relación de herencia de Der2 de Der se verifica con las reglas habituales de público/protegido/privado/amigo), luego el control de acceso de Der a A. Una relación de herencia no virtual no puede ser refinada o anulada en clases derivadas.

Las funciones de los miembros no virtuales se pueden llamar directamente y los miembros virtuales deben llamarse indirectamente a través del VTable (a menos que el tipo de objeto real sea conocido por el compilador), por lo que el compilador), por lo que el compilador), por lo que el compilador), por lo que el compilador) virtual La palabra clave agrega una indirección al acceso a las funciones de los miembros. Al igual que para los miembros de la función, el virtual La palabra clave agrega una indirección al acceso de objeto base; Al igual que para las funciones, las clases de base virtual agregan un punto de flexibilidad en la herencia.

Al hacer una herencia múltiple no virtual, repetida y múltiple:

struct Top { int i; };
struct Left : Top { };
struct Right : Top { };
struct Bottom : Left, Right { };

Solo hay dos Top::i subobjetos en Bottom (Left::i y Right::i), como con los objetos de los miembros:

struct Top { int i; };
struct mLeft { Top t; };
struct mRight { mTop t; };
struct mBottom { mLeft l; mRight r; }

Nadie se sorprende de que haya dos int submembersos (l.t.i y r.t.i).

Con funciones virtuales:

struct Top { virtual void foo(); };
struct Left : Top { }; // could override foo
struct Right : Top { }; // could override foo
struct Bottom : Left, Right { }; // could override foo (both)

Significa que hay dos funciones virtuales diferentes (no relacionadas) llamadas foo, con distintas entradas VTAB (ambas, ya que tienen la misma firma, pueden tener un excesivo común).

El semántico de las clases de base no virtuales se deduce del hecho de que la herencia básica, no virtual no virtual es una relación exclusiva: la relación de herencia establecida entre la izquierda y la parte superior no puede modificarse mediante una derivación adicional, por lo que el hecho de que existe una relación similar entre Right y Top no puede afectar esta relación. En particular, significa que Left::Top::foo() se puede anular en Left y en Bottom, pero Right, que no tiene relación de herencia con Left::Top, no puede establecer este punto de personalización.

Las clases base virtuales son diferentes: una herencia virtual es una relación compartida que se puede personalizar en clases derivadas:

struct Top { int i; virtual void foo(); };
struct vLeft : virtual Top { }; 
struct vRight : virtual Top { };
struct vBottom : vLeft, vRight { }; 

Aquí, este es solo un subobjetado de clase base Top, sólo uno int miembro.

Implementación:

El espacio para las clases de base no virtuales se asigna en función de un diseño estático con compensaciones fijas en la clase derivada. Tenga en cuenta que el diseño de una clase derivada está incluido en el diseño de la clase más derivada, por lo que la posición exacta de los subocjetos no depende del tipo real (dinámico) de objeto (al igual que la dirección de una función no virtual es constante ). OTOH, la posición de los subobjetos en una clase con herencia virtual está determinada por el tipo dinámico (al igual que la dirección de la implementación de una función virtual se conoce solo cuando se conoce el tipo dinámico).

La ubicación de Subobject se determinará en tiempo de ejecución con el VPTR y el VTable (la reutilización del VPTR existente implica menos sobrecarga de espacio), o un puntero interno directo al subobjeto (más sobrecarga, menos indirecciones necesarias).

Porque el desplazamiento de una clase base virtual se determina solo para un objeto completo, y no se puede conocer por un tipo declarado dado, No se puede asignar una base virtual a la compensación cero y nunca es una base primaria. Una clase derivada nunca reutilizará el VPTR de una base virtual como su propia VPTR.

En términos de posible traducción:

struct vLeft__vtable { 
    int Top__offset; // relative vLeft-Top offset
    void (*foo__ptr) (vLeft *__this); 
    // additional virtual member function go here
};

// this is what a subobject of type vLeft looks like
struct vLeft__subobject { 
    vLeft__vtable const *__vptr;
    // data members go here
};

void vLeft__subobject__ctor (vLeft__subobject *__this) { 
    // initialise data members
}

// this is a complete object of type vLeft 
struct vLeft__complete {
    vLeft__subobject __sub;
    Top Top__base;
}; 

// non virtual calls to vLeft::foo
void vLeft__real__foo (vLeft__complete *__this);

// virtual function implementation: call via base class
// layout is vLeft__complete 
void Top__in__vLeft__foo (Top *__this) {
    // inverse .Top__base member access 
    char *cp = reinterpret_cast<char*> (__this);
    cp -= offsetof (vLeft__complete,Top__base);
    vLeft__complete *__real = reinterpret_cast<vLeft__complete*> (cp);
    vLeft__real__foo (__real);
}

void vLeft__foo (vLeft *__this) {
    vLeft__real__foo (reinterpret_cast<vLeft__complete*> (__this));
}

// Top vtable for objects of real type vLeft
const Top__vtable Top__in__vLeft__real = { 
    /*foo__ptr =*/ Top__in__vLeft__foo 
};

// vLeft vtable for objects of real type vLeft
const vLeft__vtable vLeft__real = { 
    /*Top__offset=*/ offsetof(vLeft__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

void vLeft__complete__ctor (vLeft__complete *__this) { 
    // construct virtual bases first
    Top__ctor (&__this->Top__base); 

    // construct non virtual bases: 
    // change dynamic type to vLeft
    // adjust both virtual base class vptr and current vptr
    __this->Top__base.__vptr = &Top__in__vLeft__real;
    __this->__vptr = &vLeft__real;

    vLeft__subobject__ctor (&__this->__sub);
}

Para un objeto de tipo conocido, el acceso a la clase base es a través de vLeft__complete:

struct a_vLeft {
    vLeft m;
};

void f(a_vLeft &r) {
    Top &t = r.m; // upcast
    printf ("%p", &t);
}

se traduce a:

struct a_vLeft {
    vLeft__complete m;
};

void f(a_vLeft &r) {
    Top &t = r.m.Top__base;
    printf ("%p", &t);
}

Aquí el tipo real (dinámico) de r.m se conoce y también se conoce la posición relativa del subobjeto en el momento de la compilación. Pero aquí:

void f(vLeft &r) {
    Top &t = r; // upcast
    printf ("%p", &t);
}

el tipo real (dinámico) de r no se sabe, por lo que el acceso es a través del VPTR:

void f(vLeft &r) {
    int off = r.__vptr->Top__offset;
    char *p = reinterpret_cast<char*> (&r) + off;
    printf ("%p", p);
}

Esta función puede aceptar cualquier clase derivada con un diseño diferente:

// this is what a subobject of type vBottom looks like
struct vBottom__subobject { 
    vLeft__subobject vLeft__base; // primary base
    vRight__subobject vRight__base; 
    // data members go here
};

// this is a complete object of type vBottom 
struct vBottom__complete {
    vBottom__subobject __sub; 
    // virtual base classes follow:
    Top Top__base;
}; 

Tenga en cuenta que el vLeft La clase base está en una ubicación fija en un vBottom__subobject, asi que vBottom__subobject.__ptr se usa como VPTR para todo vBottom.

Semántica:

La relación de herencia es compartida por todas las clases derivadas; Esto significa que el derecho a anular se comparte, por lo que vRight puede anular vLeft::foo. Esto crea un intercambio de responsabilidades: vLeft y vRight debe estar de acuerdo en cómo personalizan Top:

struct Top { virtual void foo(); };
struct vLeft : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vRight : virtual Top { 
    override void foo(); // I want to customise Top
}; 
struct vBottom : vLeft, vRight { };  // error

Aquí vemos un conflicto: vLeft y vRight busca definir el comportamiento de la única función virtual FOO, y vBottom La definición es un error por falta de un exceso de exageración común.

struct vBottom : vLeft, vRight  { 
    override void foo(); // reconcile vLeft and vRight 
                         // with a common overrider
};

Implementación:

La construcción de la clase con clases base no virtuales con clases base no virtuales implica llamar a constructores de clase base en el mismo orden que se hace para las variables de miembros, cambiando el tipo dinámico cada vez que ingresamos un CTOR. Durante la construcción, los subobjetos de clase base realmente actúan como si fueran objetos completos (esto es incluso cierto con los subobjetos de clase base abstractos imposibles: son objetos con funciones virtuales indefinidas (puras)). Las funciones virtuales y RTTI pueden llamarse durante la construcción (excepto, por supuesto, las funciones virtuales puras).

La construcción de una clase con clases base no virtuales con bases virtuales es más complicada: Durante la construcción, el tipo dinámico es el tipo de clase base, pero el diseño de la base virtual sigue siendo el diseño del tipo más derivado que aún no está construido, por lo que necesitamos más Vtables para describir este estado:

// vtable for construction of vLeft subobject of future type vBottom
const vLeft__vtable vLeft__ctor__vBottom = { 
    /*Top__offset=*/ offsetof(vBottom__complete, Top__base),
    /*foo__ptr =*/ vLeft__foo 
};

Las funciones virtuales son las de vLeft (Durante la construcción, la vida útil del objeto Vbottom no ha comenzado), mientras que las ubicaciones de base virtual son las de un vBottom (como se define en el vBottom__complete traducido objetado).

Semántica:

Durante la inicialización, es obvio que debemos tener cuidado de no usar un objeto antes de que se inicialice. Debido a que C ++ nos da un nombre antes de que un objeto esté completamente inicializado, es fácil hacerlo:

int foo (int *p) { return *pi; }
int i = foo(&i); 

o con este puntero en el constructor:

struct silly { 
    int i;
    std::string s;
    static int foo (bad *p) { 
        p->s.empty(); // s is not even constructed!
        return p->i; // i is not set!
    }
    silly () : i(foo(this)) { }
};

Es bastante obvio que cualquier uso de this En la lista de la In-In-Init debe verificarse cuidadosamente. Después de la inicialización de todos los miembros, this se puede pasar a otras funciones y registrarse en algún conjunto (hasta que comience la destrucción).

Lo que es menos obvio es que cuando la construcción de una clase que involucra bases virtuales compartidas, los subobjetos dejan de construir: durante la construcción de un vBottom:

  • Primero se construyen las bases virtuales: cuando Top se construye, se construye como un sujeto normal (Top ni siquiera sabe que es una base virtual)

  • entonces las clases base se construyen en orden de izquierda a derecha: el vLeft Se construye el subobjeto y se vuelve funcional como un vLeft (Pero con un vBottom diseño), entonces el Top Subobject de clase base ahora tiene un vLeft tipo dinámico;

  • la vRight Comienza la construcción del suboblex y el tipo dinámico de la clase base cambia a Vright; pero vRight no se deriva de vLeft, no sabe nada de vLeft, entonces el vLeft la base ahora está rota;

  • Cuando el cuerpo del Bottom Comienza el constructor, los tipos de todos los subobjetos se han estabilizado y vLeft es funcional de nuevo.

No estoy seguro de cómo se puede tomar esta respuesta como una respuesta completa sin la mención de la alineación o los bits de relleno.

Déjame dar un poco de antecedentes de alineación:

"Se dice que una dirección de memoria A está alineada N-byte cuando a es un múltiplo de n bytes (donde n es un poder de 2). En este contexto, un byte es la unidad más pequeña de acceso a la memoria, es decir, cada dirección de memoria especifica Un byte diferente. Una dirección alineada con N bytes tendría ceros menos significativos log2 (n) cuando se expresa en binario.

La redacción alternativa de red alineada Bit Bit designada AB/8 Dirección alineada de bytes (Ex. 64 bits alineados es 8 bytes alineados).

Se dice que un acceso a la memoria se alinea cuando se accede al dato es de n bytes de largo y la dirección de dato está alineada N-byte. Cuando no está alineado un acceso a la memoria, se dice que está desalineado. Tenga en cuenta que, por definición, los accesos de memoria de bytes siempre están alineados.

Se dice que un puntero de memoria que se refiere a datos primitivos que es de n bytes de largo está alineado si solo se le permite contener direcciones que están alineadas N-byte, de lo contrario se dice que no está alineado. Un puntero de memoria que se refiere a un agregado de datos (una estructura de datos o una matriz) está alineada si (y solo si) se alinee cada dato primitivo en el agregado.

Tenga en cuenta que las definiciones anteriores suponen que cada dato primitivo es un poder de dos bytes de largo. Cuando este no es el caso (como con el punto flotante de 80 bits en x86), el contexto influye en las condiciones en las que el dato se considera alineado o no.

Las estructuras de datos se pueden almacenar en la memoria en la pila con un tamaño estático conocido como limitado o en el montón con un tamaño dinámico conocido como ilimitado ".

Para mantener la alineación, el compilador inserta bits de relleno en el código compilado de un objeto de estructura/clase. "Aunque el compilador (o intérprete) normalmente asigna elementos de datos individuales en los límites alineados, las estructuras de datos a menudo tienen miembros con diferentes requisitos de alineación. Para mantener una alineación adecuada, el traductor normalmente inserta miembros de datos adicionales no identificados para que cada miembro esté correctamente alineado. Además, además del La estructura de datos en su conjunto puede estar acolchada con un miembro final sin nombre. Esto permite que cada miembro de una matriz de estructuras se alinee correctamente. .... ....

El relleno solo se inserta cuando un miembro de la estructura es seguido por un miembro con un requisito de alineación más grande o al final de la estructura " - Wiki

Para obtener más información sobre cómo lo hace GCC, mira

http://www.delorie.com/gnu/docs/gcc/gccint_111.html

y busque el texto "Basic-Align"

Ahora vengamos a este problema:

Usando la clase de ejemplo, he creado este programa para un compilador GCC que se ejecuta en un Ubuntu de 64 bits.

int main() {
    cout << "!!!Hello World!!!" << endl; // prints !!!Hello World!!!
    A objA;
    C objC;
    cout<<__alignof__(objA.a)<<endl;
    cout<<sizeof(void*)<<endl;
    cout<<sizeof(int)<<endl;
    cout<<sizeof(A)<<endl;
    cout<<sizeof(B)<<endl;
    cout<<sizeof(C)<<endl;
    cout<<__alignof__(objC.a)<<endl;
    cout<<__alignof__(A)<<endl;
    cout<<__alignof__(C)<<endl;
    return 0;
}

Y el resultado para este programa es como el siguiente:

4
8
4
16
16
32
4
8
8

Ahora déjame explicarlo. Como A y B tienen funciones virtuales, crearán VTables separados y VPTR se agregará al comienzo de sus objetos, respectivamente.

Por lo tanto, el objeto de la Clase A tendrá un VPTR (apuntando al VTable de A) y un Int. El puntero tendrá 8 bytes de largo y el int tendrá 4 bytes de largo. Por lo tanto, antes de compilar el tamaño es de 12 bytes. Pero el compilador agregará 4 bytes adicionales al final de Int A como bits de relleno. Por lo tanto, después de la compilación, el tamaño de los objetos de A será 12+4 = 16.

Del mismo modo para los objetos de clase B.

Ahora el objeto de C tendrá dos VPTR (uno para cada clase A y Clase B) y 3 INTS (A, B, C). Entonces, el tamaño debería haber sido 8 (VPTR A) + 4 (int a) + 4 (bytes de relleno) + 8 (VPTR B) + 4 (int B) + 4 (int c) = 32 bytes. Entonces, el tamaño total de C será de 32 bytes.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top