Interpretaré su pregunta como dos preguntas: 1) por qué ->
existe, y 2) por qué .
no desreferencia automáticamente el puntero. Las respuestas a ambas preguntas tienen raíces históricas.
¿Por qué ->
existe?
En una de las primeras versiones de lenguaje C (que me referiré como CRM para " C Manual de referencia ", que vino con sexta edición Unix de mayo de 1975), el operador ->
tenía un significado muy exclusivo, no es sinónimo de *
y .
combinación
El lenguaje C descrito por CRM era muy diferente del C moderno en muchos aspectos. En CRM, los miembros de la estructura implementaron el concepto global de desplazamiento de bytes , que podría agregarse a cualquier valor de dirección sin restricciones de tipo. Es decir, todos los nombres de todos los miembros de la estructura tenían un significado global independiente (y, por lo tanto, tenían que ser únicos). Por ejemplo, podrías declarar
struct S {
int a;
int b;
};
y nombre a
representaría el desplazamiento 0, mientras que nombre b
representaría el desplazamiento 2 (suponiendo un int
tipo de tamaño 2 y sin relleno). El lenguaje requería que todos los miembros de todas las estructuras en la unidad de traducción tengan nombres únicos o representen el mismo valor de desplazamiento. Por ejemplo, en la misma unidad de traducción, también puede declarar
struct X {
int a;
int x;
};
y eso estaría bien, ya que el nombre a
significaría constantemente el desplazamiento 0. Pero esta declaración adicional
struct Y {
int b;
int a;
};
sería formalmente inválido, ya que intentó "redefinir" a
como desplazamiento 2 y b
como desplazamiento 0.
Y aquí es donde ->
entra el operador. Dado que cada nombre de miembro de estructura tiene su propio significado global autosuficiente, el lenguaje admite expresiones como estas
int i = 5;
i->b = 42; /* Write 42 into `int` at address 7 */
100->a = 0; /* Write 0 into `int` at address 100 */
El compilador interpretó la primera asignación como "tomar dirección 5
, agregarle desplazamiento 2
y asignarle 42
el int
valor en la dirección resultante". Es decir, lo anterior asignaría 42
al int
valor en la dirección 7
. Tenga en cuenta que este uso de ->
no le importaba el tipo de expresión en el lado izquierdo. El lado izquierdo se interpretó como una dirección numérica de valor (ya sea un puntero o un número entero).
Este tipo de engaño no era posible con *
y .
combinación. No podias hacer
(*i).b = 42;
ya que *i
es una expresión inválida. El *
operador, dado que está separado de él .
, impone requisitos de tipo más estrictos en su operando. Para proporcionar una capacidad para evitar esta limitación, CRM introdujo el ->
operador, que es independiente del tipo de operando de la izquierda.
Como señaló Keith en los comentarios, esta diferencia entre ->
y *
+ .
combinación es a lo que CRM se refiere como "relajación del requisito" en 7.1.8: Excepto por la relajación del requisito que E1
es de tipo puntero, la expresión E1−>MOS
es exactamente equivalente a(*E1).MOS
Más tarde, en K&R C, muchas de las características descritas originalmente en CRM se modificaron significativamente. La idea de "miembro de estructura como identificador de desplazamiento global" se eliminó por completo. Y la funcionalidad del ->
operador se volvió completamente idéntica a la funcionalidad *
y la .
combinación.
¿Por qué no puede .
desreferenciar el puntero automáticamente?
Una vez más, en la versión de CRM de la lengua el operando de la izquierda .
se requiere operador para ser un valor-I . Ese fue el único requisito impuesto a ese operando (y eso es lo que lo hizo diferente ->
, como se explicó anteriormente). Tenga en cuenta que CRM no requería que el operando izquierdo de .
tuviera un tipo de estructura. Solo requería que fuera un valor, cualquier valor . Esto significa que en la versión CRM de C podría escribir código como este
struct S { int a, b; };
struct T { float x, y, z; };
struct T c;
c.b = 55;
En este caso, el compilador escribiría 55
en un int
valor posicionado en byte-offset 2 en el bloque de memoria continua conocido como c
, a pesar de que el tipo struct T
no tenía un campo nombrado b
. Al compilador no le importaría el tipo real de c
nada. Lo único que le importaba c
era que fuera un valor: algún tipo de bloque de memoria grabable.
Ahora tenga en cuenta que si hiciste esto
S *s;
...
s.b = 42;
el código se consideraría válido (ya s
que también es un lvalue) y el compilador simplemente intentaría escribir datos en el puntero en s
sí , en byte-offset 2. No hace falta decir que cosas como esta podrían resultar en desbordamiento de memoria, pero el lenguaje no se preocupó por tales asuntos.
Es decir, en esa versión del lenguaje, su idea propuesta sobre la sobrecarga del operador .
para los tipos de puntero no funcionaría: el operador .
ya tenía un significado muy específico cuando se usaba con punteros (con punteros de valor o con cualquier valor). Era una funcionalidad muy extraña, sin duda. Pero estaba allí en ese momento.
Por supuesto, esta funcionalidad extraña no es una razón muy fuerte contra la introducción de un .
operador sobrecargado para punteros (como usted sugirió) en la versión reelaborada de C - K&R C. Pero no se ha hecho. Quizás en ese momento había algún código heredado escrito en la versión CRM de C que tenía que ser compatible.
(La URL para el Manual de referencia de 1975 C puede no ser estable. Otra copia, posiblemente con algunas diferencias sutiles, está aquí ).