¿Por qué existe el operador de flecha (->) en C?

264

El .operador dot ( ) 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 al 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 desreferenciar automáticamente el puntero si se usa en un puntero (una información que el compilador conoce en tiempo de compilación afaik).

Entonces, ¿por qué los creadores del lenguaje han decidido complicar las cosas agregando este operador aparentemente innecesario? ¿Cuál es la gran decisión de diseño?

Askaga
fuente
1
Relacionado: stackoverflow.com/questions/221346/… - también, puede anular ->
Krease
16
@ Chris Ese es sobre C ++, lo que por supuesto hace una gran diferencia. Pero ya que estamos hablando de por qué C fue diseñado de esta manera, supongamos que estamos de vuelta en la década de 1970, antes de que C ++ existiera.
Mysticial
55
Mi mejor conjetura es que el operador de flecha existe para expresar visualmente "¡Mira! Estás tratando con un puntero aquí"
Chris
44
De un vistazo, siento que esta pregunta es muy extraña. No todas las cosas están cuidadosamente diseñadas. Si mantienes este estilo en toda tu vida, tu mundo estaría lleno de preguntas. La respuesta que obtuvo más votos es realmente informativa y clara. Pero no llega al punto clave de su pregunta. Siga el estilo de su pregunta, puedo hacer muchas preguntas. Por ejemplo, la palabra clave 'int' es la abreviatura de 'integer'; ¿Por qué la palabra clave 'doble' tampoco es más corta?
junwanghe
1
@junwanghe Esta pregunta en realidad representa una preocupación válida: ¿por qué el .operador tiene mayor prioridad que el *operador? Si no fuera así, podríamos tener * ptr.member y var.member.
milleniumbug

Respuestas:

358

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 arepresentaría el desplazamiento 0, mientras que nombre brepresentaría el desplazamiento 2 (suponiendo un inttipo 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 asignificarí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" acomo desplazamiento 2 y bcomo 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 2y asignarle 42el intvalor en la dirección resultante". Es decir, lo anterior asignaría 42al intvalor 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 *ies 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 E1es de tipo puntero, la expresión E1−>MOSes 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 55en un intvalor posicionado en byte-offset 2 en el bloque de memoria continua conocido como c, a pesar de que el tipo struct Tno tenía un campo nombrado b. Al compilador no le importaría el tipo real de cnada. Lo único que le importaba cera 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 sque también es un lvalue) y el compilador simplemente intentaría escribir datos en el puntero en 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í ).

AnT
fuente
10
Y la sección 7.1.8 del Manual de referencia C citado dice "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 ' "."
Keith Thompson el
1
¿Por qué no tuvo que *iser un valor de algún tipo predeterminado (int?) En la dirección 5? Entonces (* i) .b habría funcionado de la misma manera.
Random832
55
@Leo: Bueno, a algunas personas les gusta el lenguaje C como ensamblador de nivel superior. En ese período en la historia de C, el lenguaje en realidad era un ensamblador de alto nivel.
ANT
29
Huh Esto explica por qué muchas estructuras en UNIX (por ejemplo, struct stat) prefijan sus campos (por ejemplo, st_mode).
icktoofay
55
@ perfectionm1ng: Parece que bell-labs.com ha sido tomada por Alcatel-Lucent y las páginas originales se han ido. Actualicé el enlace a otro sitio, aunque no puedo decir cuánto tiempo permanecerá activo. De todos modos, buscar en Google el "manual de referencia de Ritchie C" generalmente encuentra el documento.
AnT
46

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 punto tiene mayor prioridad que el operador estrella, por lo que si tiene una estructura que contiene un puntero a una estructura que contiene un puntero a una 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 máxima prioridad (igual que el punto) y se asocia de izquierda a derecha. Creo que esto es más claro que usar el operador de punto tanto para los punteros como para estructurar y estructurar, porque conocemos el tipo de la expresión sin tener que mirar la declaración, que incluso podría estar en otro archivo.

effeffe
fuente
2
Con los tipos de datos anidados que contienen tanto estructuras como punteros a estructuras, esto puede dificultar las cosas, ya que debe pensar en elegir el operador adecuado para cada acceso de submiembro. Es posible que termine con ab-> c-> do a-> bc-> d (tuve este problema al usar la biblioteca de tipos libres; necesitaba buscar su código fuente todo el tiempo). Además, esto no explica por qué no sería posible dejar que el compilador desreferenciara el puntero automáticamente al tratar con punteros.
Askaga
3
Si bien los hechos que está diciendo son correctos, no responden mi pregunta original de ninguna manera. Explicas la igualdad de a-> y * (a). anotaciones (que ya se han explicado varias veces en otras preguntas), así como dar una declaración vaga acerca de que el diseño del lenguaje es algo arbitrario. No encontré su respuesta muy útil, por lo tanto, el voto negativo.
Askaga
16
@effeffe, el PO está diciendo que el lenguaje podría haber interpretado fácilmente a.b.c.dcomo (*(*(*a).b).c).d, haciendo que el ->operador inútil. Entonces, la versión del OP ( a.b.c.d) es igualmente legible (en comparación con a->b->c->d). Es por eso que su respuesta no responde la pregunta del OP.
Shahbaz
44
@Shahbaz Ese puede ser el caso de un programador de Java, un programador de C / C ++ lo entenderá a.b.c.dy a->b->c->dcomo dos cosas muy diferentes: la primera es un acceso de memoria único a un subobjeto anidado (en este caso, solo hay un único objeto de memoria) ), el segundo es tres accesos a la memoria, persiguiendo punteros a través de cuatro objetos distintos. Esa es una gran diferencia en el diseño de la memoria, y creo que C tiene razón al distinguir estos dos casos de manera muy visible.
cmaster - reinstalar monica
2
@Shahbaz No quise decir que, como insulto a los programadores de Java, simplemente estén acostumbrados a un lenguaje con punteros totalmente implícitos. Si hubiera sido educado como programador de Java, probablemente pensaría lo mismo ... De todos modos, en realidad creo que la sobrecarga del operador que vemos en C es menos que óptima. Sin embargo, reconozco que todos hemos sido mimados por los matemáticos que liberalmente sobrecargan a sus operadores para casi todo. También entiendo su motivación, ya que el conjunto de símbolos disponibles es bastante limitado. Supongo que, al final, es solo la cuestión de dónde trazas la línea ...
cmaster - restablece monica
19

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 con un puntero, al igual que cuando el compilador no le permite mezclar dos tipos incompatibles.

mukunda
fuente
44
Esta es la respuesta simple y correcta. La mayoría de las
veces,
10
Muchas cosas en C son ambiguas y confusas. Hay conversiones de tipo implícitas, los operadores matemáticos están sobrecargados, la indexación encadenada hace algo completamente diferente dependiendo de si está indexando una matriz multidimensional o una matriz de puntero y cualquier cosa podría ser una macro que oculta cualquier cosa (la convención de nomenclatura mayúscula ayuda allí, pero C no lo hace) t)
PSkocik