punteros - ¿Por qué existe el operador de flecha(->) en C?
punteros a cadenas (3)
C también hace un buen trabajo al no hacer nada ambiguo.
Seguro que el punto podría estar sobrecargado para significar ambas cosas, pero la flecha se asegura de que el programador sepa que está operando en un puntero, como cuando el compilador no te permite mezclar dos tipos incompatibles.
El operador de punto ( .
) Se usa para acceder a un miembro de una estructura, mientras que el operador de flecha ( ->
) en C se usa para acceder a un miembro de una estructura a la que hace referencia el puntero en cuestión.
El puntero en sí no tiene ningún miembro al que se pueda acceder con el operador de punto (en realidad es solo un número que describe una ubicación en la memoria virtual, por lo que no tiene ningún miembro). Por lo tanto, no habría ambigüedad si solo definiéramos el operador de punto para eliminar la referencia automática del puntero si se usa en un puntero (una información que el compilador conoce en el momento de la compilación en el enlace).
Entonces, ¿por qué los creadores del lenguaje decidieron hacer las cosas más complicadas al agregar este operador aparentemente innecesario? ¿Cuál es la gran decisión de diseño?
Interpretaré su pregunta como dos preguntas: 1) por qué ->
incluso existe, y 2) por qué .
No elimina automáticamente la referencia al puntero. Las respuestas a ambas preguntas tienen raíces históricas.
¿Por qué ->
incluso existe?
En una de las primeras versiones del lenguaje C (a la que me referiré como CRM para el " Manual de referencia de C ", que vino con la 6a edición de Unix en 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 compensación 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 usted podría declarar
struct S {
int a;
int b;
};
y el nombre a
significaría el desplazamiento 0, mientras que el nombre b
significaría el desplazamiento 2 (suponiendo que sea del tipo 2 de tamaño y sin relleno). El idioma requiere que todos los miembros de todas las estructuras en la unidad de traducción tengan nombres únicos o representen el mismo valor de compensación. Por ejemplo, en la misma unidad de traducción también se podría declarar.
struct X {
int a;
int x;
};
y eso estaría bien, ya que el nombre a
consistiría consistentemente en compensación 0. Pero esta declaración adicional
struct Y {
int b;
int a;
};
sería formalmente inválido, ya que intentó "redefinir" a
como offset 2 b
como offset 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 */
La primera asignación fue interpretada por el compilador como "tomar la dirección 5
, agregarle un desplazamiento 2
y asignar 42
al valor int
en la dirección resultante". Es decir, lo anterior asignaría 42
al valor int
en la dirección 7
. Tenga en cuenta que este uso de ->
no se preocupó por el tipo de expresión en el lado izquierdo. El lado izquierdo se interpretó como una dirección numérica rvalue (ya sea un puntero o un entero).
Este tipo de trucos no era posible con *
y .
combinación. No podias hacer
(*i).b = 42;
ya que *i
ya es una expresión no válida. El operador *
, ya que está separado de .
, impone requisitos de tipo más estrictos a su operando. Para proporcionar una capacidad para evitar esta limitación, CRM introdujo el operador ->
, que es independiente del tipo del operando de la izquierda.
Como Keith observó en los comentarios, esta diferencia entre ->
y *
+ .
combinación es a lo que se refiere CRM como "relajación del requisito" en 7.1.8: Excepto por la relajación del requisito de que E1
sea 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 ->
volvió completamente idéntica a la funcionalidad de *
y .
combinación.
Por que no puedo desreferir el puntero automáticamente?
Nuevamente, en la versión CRM del lenguaje el operando izquierdo de .
Se requería que el operador fuera un lvalor . Ese fue el único requisito impuesto a ese operando (y eso es lo que lo hizo diferente de ->
, como se explicó anteriormente). Tenga en cuenta que CRM no requiere el operando izquierdo de .
tener 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 valor int
posicionado en byte-offset 2 en el bloque de memoria continua conocido como c
, incluso aunque el tipo de struct T
no tuviera un campo llamado b
. El compilador no se preocuparía por el tipo real de c
en absoluto. Todo lo que le importaba es que c
era un lvalue: una especie de bloque de memoria grabable.
Ahora nota que si hiciste esto
S *s;
...
s.b = 42;
el código se consideraría válido (ya que s
también es un valor límico) y el compilador simplemente intentaría escribir datos en el puntero s
, en el byte-offset 2. No hace falta decir que cosas como estas podrían fácilmente provocar un exceso de memoria, pero El lenguaje no se ocupaba de tales asuntos.
Es decir, en esa versión del lenguaje, su idea propuesta sobre el operador de sobrecarga .
para los tipos de puntero no funcionaría: operador .
Ya tenía un significado muy específico cuando se usaba con punteros (con punteros de valor de l o con cualquier valor de l). Era una funcionalidad muy rara, sin duda. Pero estaba allí en ese momento.
Por supuesto, esta extraña funcionalidad no es una razón muy fuerte contra la introducción de sobrecargas .
operador para los punteros (como sugirió) en la versión modificada de C - K&R C. Pero no se ha hecho. Tal vez 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 del Manual de referencia de 1975 C puede no ser estable. here hay otra copia, posiblemente con algunas diferencias sutiles).
Más allá de las razones históricas (buenas y ya informadas), también hay un pequeño problema con la precedencia de los operadores: el operador de punto tiene mayor prioridad que el operador estrella, por lo que si tiene la estructura que contiene el puntero a la estructura que contiene el puntero a la estructura ... Estos dos son equivalentes:
(*(*(*a).b).c).d
a->b->c->d
Pero el segundo es claramente más legible. El operador de flecha tiene la prioridad más alta (solo como un punto) y se asocia de izquierda a derecha. Creo que esto es más claro que usar el operador de punto para los punteros a estructura y estructura, porque sabemos el tipo de la expresión sin tener que mirar la declaración, que incluso podría estar en otro archivo.