resueltos polimorfismo metodos herencia ejercicios codigo clases c++ private-members

polimorfismo - ¿Por qué C++ permite que los miembros privados se modifiquen utilizando este enfoque?



metodos en c++ (6)

Después de ver esta pregunta hace unos minutos, me pregunté por qué los diseñadores de idiomas lo permiten, ya que permite la modificación indirecta de datos privados. Como ejemplo

class TestClass { private: int cc; public: TestClass(int i) : cc(i) {}; }; TestClass cc(5); int* pp = (int*)&cc; *pp = 70; // private member has been modified

He probado el código anterior y de hecho los datos privados han sido modificados. ¿Hay alguna explicación de por qué se permite que esto suceda o esto simplemente un descuido en el idioma? Parece socavar directamente el uso de miembros de datos privados.


Debido a la compatibilidad con versiones anteriores de C, donde puede hacer lo mismo.

Para todas las personas que se preguntan, aquí está la razón por la que esto no es UB y está permitido por el estándar

Primero, TestClass es una clase de diseño estándar ( §9 [class] p7 ):

Una clase de diseño estándar es una clase que:

  • no tiene miembros de datos no estáticos de tipo clase de diseño no estándar (o matriz de tales tipos) o referencia, // OK: el miembro de datos no estáticos es de tipo ''int''
  • no tiene funciones virtuales (10.3) ni clases de base virtual (10.1), // OK
  • tiene el mismo control de acceso (Cláusula 11) para todos los miembros de datos no estáticos, // OK, todos los miembros de datos no estáticos (1) son ''privados''
  • no tiene clases base de diseño no estándar, // OK, no hay clases base
  • o no tiene miembros de datos no estáticos en la clase más derivada y como máximo una clase base con miembros de datos no estáticos, o no tiene clases base con miembros de datos no estáticos, y // OK, no hay clases base nuevamente
  • no tiene clases base del mismo tipo que el primer miembro de datos no estáticos. // OK, no hay clases base otra vez

Y con eso, se le puede permitir reinterpret_cast la clase al tipo de su primer miembro ( §9.2 [class.mem] p20 ):

Un puntero a un objeto de estructura de diseño estándar, convertido adecuadamente usando un reinterpret_cast , apunta a su miembro inicial (o si ese miembro es un campo de bits, luego a la unidad en la que reside) y viceversa.

En su caso, el reparto de estilo C (int*) resuelve en un reinterpret_cast ( §5.4 [expr.cast] p4 ).


El compilador le habría dado un error si hubiera intentado int *pp = &cc.cc , el compilador le habría dicho que no puede acceder a un miembro privado.

En su código, está reinterpretando la dirección de cc como un puntero a un int. Usted lo escribió al estilo C, el estilo C ++ habría sido int* pp = reinterpret_cast<int*>(&cc); . El reinterpret_cast siempre es una advertencia de que está haciendo una conversión entre dos punteros que no están relacionados. En tal caso, debe asegurarse de que está haciendo lo correcto. Debes conocer la memoria subyacente (layout). El compilador no impide que lo hagas, porque esto, si es necesario, a menudo.

Cuando haces el reparto, desechas todo el conocimiento sobre la clase. De ahora en adelante el compilador solo ve un puntero int. Por supuesto que puede acceder a la memoria a la que apunta el puntero. En su caso, en su plataforma, el compilador colocó cc en los primeros n bytes de un objeto TestClass, por lo que un puntero TestClass también apunta al miembro cc.


Esto se debe a que está manipulando la memoria donde se encuentra su clase en la memoria. En su caso, solo tiene que almacenar el miembro privado en esta ubicación de memoria para que lo cambie. No es una buena idea hacerlo porque ahora sabe cómo se almacenará el objeto en la memoria.


Porque, como dice Bjarne, C ++ está diseñado para proteger a Murphy, no a Maquiavelo.

En otras palabras, se supone que te protege de los accidentes, pero si vas a cualquier trabajo para subvertirlo (como usar un yeso), ni siquiera intentará detenerte.

Cuando lo pienso, tengo una analogía algo diferente en mente: es como la cerradura de la puerta de un baño. Te da una advertencia de que probablemente no quieras entrar allí ahora mismo, pero es trivial abrir la puerta desde el exterior si decides hacerlo.

Edit: en cuanto a la pregunta que analiza @Xeo, acerca de por qué el estándar dice "tener el mismo control de acceso" en lugar de "tener todo el control de acceso público", la respuesta es larga y un poco tortuosa.

Volvamos al principio y consideremos una estructura como:

struct X { int a; int b; };

C siempre tenía algunas reglas para una estructura como esta. Una es que, en una instancia de la estructura, la dirección de la estructura en sí misma debe ser igual a la dirección de a , por lo que puede convertir un puntero a la estructura en un puntero a int , y acceder a a con resultados bien definidos. Otra es que los miembros tienen que estar dispuestos en el mismo orden en la memoria que están definidos en la estructura (aunque el compilador es libre de insertar el relleno entre ellos).

Para C ++, hubo una intención de mantener eso, especialmente para las estructuras C existentes. Al mismo tiempo, existía la intención aparente de que si el compilador deseaba imponer la private (y la protected ) en tiempo de ejecución, debería ser fácil hacerlo (razonablemente eficiente).

Por lo tanto, dado algo como:

struct Y { int a; int b; private: int c; int d; public: int e; // code to use `c` and `d` goes here. };

Se debe exigir al compilador que mantenga las mismas reglas que C con respecto a Ya e Yb . Al mismo tiempo, si va a imponer el acceso en el tiempo de ejecución, puede querer mover todas las variables públicas juntas en la memoria, por lo que el diseño sería más como:

struct Z { int a; int b; int e; private: int c; int d; // code to use `c` and `d` goes here. };

Entonces, cuando se implementan las cosas en tiempo de ejecución, básicamente puede hacer algo como if (offset > 3 * sizeof(int)) access_violation();

Que yo sepa, nadie lo ha hecho nunca, y no estoy seguro de que el resto del estándar realmente lo permita, pero parece haber al menos el germen medio formado de una idea en esa línea.

Para hacer cumplir ambos, el C ++ 98 dijo que Y::a y Y::b tenían que estar en ese orden en la memoria, y Y::a tenía que estar al principio de la estructura (es decir, C-like reglas). Pero, debido a los especificadores de acceso intermedios, Y::c Y::e ya no tenían que estar en orden unos con otros. En otras palabras, todas las variables consecutivas definidas sin un especificador de acceso entre ellas se agruparon, el compilador fue libre de reorganizar esos grupos (pero aún así tuvo que mantener la primera al principio).

Eso estuvo bien hasta que un imbécil (es decir, yo) señaló que la forma en que se escribieron las reglas tuvo otro pequeño problema. Si escribiera código como:

struct A { int a; public: int b; public: int c; public: int d; };

... terminaste con un poco de auto contradición. Por un lado, esta aún era oficialmente una estructura POD, por lo que se suponía que las reglas tipo C debían aplicarse, pero como tenía (especificamente sin sentido) identificadores de acceso entre los miembros, también le dio permiso al compilador para reorganizar los miembros, rompiendo las reglas tipo C que pretendían.

Para solucionarlo, rediseñaron un poco el estándar para que hablara de que todos los miembros tienen el mismo acceso, en lugar de si hubo o no un especificador de acceso entre ellos. Sí, podrían haber decretado que las reglas solo se aplicarían a los miembros públicos, pero parece que nadie vio nada que ganar con eso. Dado que esto estaba modificando un estándar existente con una gran cantidad de código que había estado en uso durante bastante tiempo, optaron por el cambio más pequeño que podrían hacer para solucionar el problema.


Todo el propósito de reinterpret_cast (y un modelo de estilo C es incluso más poderoso que un reinterpret_cast ) es proporcionar una ruta de escape en torno a las medidas de seguridad.


Una buena razón es permitir la compatibilidad con C, pero la seguridad de acceso adicional en la capa C ++.

Considerar:

struct S { #ifdef __cplusplus private: #endif // __cplusplus int i, j; #ifdef __cplusplus public: int get_i() const { return i; } int get_j() const { return j; } #endif // __cplusplus };

Al exigir que la C visible S y la C ++ visible S sean compatibles con el diseño , S se puede usar a través del límite del idioma, mientras que la parte C ++ tiene mayor seguridad de acceso. La subversión de seguridad de acceso reinterpret_cast es un corolario desafortunado pero necesario.

Además, la restricción de tener a todos los miembros con el mismo control de acceso se debe a que la implementación puede reorganizar los miembros en relación con los miembros con un control de acceso diferente. Presumiblemente, algunas implementaciones ponen a los miembros con el mismo control de acceso juntos, en aras de la limpieza; También podría usarse para reducir el relleno, aunque no conozco ningún compilador que haga eso.