En C, ¿es * un operador o parte de un tipo en una declaración?

9

En C, *se llama operador de indirección o operador de desreferencia. Entiendo cómo funciona cuando se usa en una declaración. Tiene sentido escribir *po * p, considerando que es un operador unario.

Sin embargo, a veces en una declaración, *se utiliza a.

void move(int *units) { ... }

o

int *p = &x;

Me parece extraño que aquí se use un operador. No puedes hacer cosas como

void move(int units++) { ... }

donde ++también es un operador unario. ¿Es este *también el operador de indirección, o *tiene un significado diferente, donde dice algo sobre el tipo de puntero? (Si este es el caso, estoy considerando usarlo, por ejemplo, int* unitsen declaraciones y, de lo int x = *p;contrario, por claridad)

En esta respuesta se dice que

El estándar C solo define dos significados para el operador *:

  • operador de indirección
  • operador de multiplicación

También he visto personas que afirman que en *realidad es parte del tipo. Esto me confunde

Torm
fuente
1
En void move(int *units) { ... }, es un operador de indirección. Considerado parte del tipo, también se puede escribir void move(int* units) { ... }, aunque prefiero el estilo anterior. Lees ambos como "puntero int". Ver también stackoverflow.com/a/8911253
Robert Harvey

Respuestas:

16

Las piezas más pequeñas de la lengua C son fichas de léxico , tales como palabras clave (por ejemplo int, if, break), los identificadores (por ejemplo move, units) y otros.

*es un elemento léxico llamado puntuador (como lo son, por ejemplo {}()+). Un signo de puntuación puede tener diferentes significados según cómo se use:

Un signo de puntuación es un símbolo que tiene un significado sintáctico y semántico independiente. Dependiendo del contexto, puede especificar una operación a realizar (...) en cuyo caso se conoce como operador

En el capítulo 6.5 estándar de C11 sobre expresiones, *se define como operador unario (sección 6.5.3.2, para indirección) y como operador multiplicativo (sección 6.5.5), exactamente como lo ha descrito. Pero *solo se interpreta como operador de acuerdo con las reglas gramaticales que hacen expresiones válidas.

Pero también hay un capítulo 6.2 sobre conceptos, que explica que los punteros a un tipo son tipos derivados. La sección 6.7.6.1 sobre los declaradores de puntero es más precisa:

si, en la declaración '' T D1 '', D1 tiene la forma
* type-qualifier-list-opt D
y el tipo especificado para ident en la declaración '' T D '' es '' derivado-declarador-tipo-lista T '', entonces el tipo especificado para ident es '' derivado-declarador-tipo-lista tipo-calificador-puntero de lista a T ''.

De hecho, esto forma *parte de un tipo derivado ( *significa que hay "puntero", pero siempre necesita otro tipo para decir a qué tipo de cosa señala algo).

Observación: No hay contradicción en esto. Hace un uso tan diferenciado del elemento del lenguaje léxico todos los días cuando habla inglés: "un identificador " designa algo y " manipular " significa hacer algo, dos usos gramaticales completamente diferentes del mismo elemento léxico.

Christophe
fuente
Eso significa que esta interpretación es solo una forma común de verla (de la otra respuesta): int * a esta declaración podría interpretarse como lectura: declare una variable, llamada a, que cuando se desreferenciada es de tipo int. -Cuando en realidad el * tiene un significado independiente en una declaración
Torm
@Torm exactamente!
Christophe
7

En C, la indirección en una declaración se lee mejor como parte de la variable que como parte del tipo. Si tengo:

int *a;

esta declaración podría interpretarse como lectura: declare una variable, llamada a, que cuando se desreferenciada es de tipo int.

Esto es importante cuando se declaran múltiples variables a la vez:

int *a, b;   (1)
int* a, b;   (2)

Ambas declaraciones son equivalentes, y en ambos ay bno son los mismos tipos - aes un puntero a un entero y bes un int. Pero la declaración (2) parece que el "tipo de puntero" es parte del tipo cuando en realidad es la declaración de *a.

Erik
fuente
Es un muy buen punto para plantear esto, porque a menudo hay una confusión entre 1 y 2. Sin embargo, el tipo de a es "puntero a int", por lo que incluso si el * solo se aplica a a, definitivamente es parte del tipo . Podría, por ejemplo, usarlo en un typedef (que no podría hacer para un inicializador).
Christophe el
He estado buscando un buen argumento para decidir entre las dos declaraciones, esto lo logró. Sin embargo, rara vez declaro la variable como una lista, pero aún así, a partir de ahora adoptaré la primera forma como la más clara en todos los casos. Gracias !
Newtopian
4

Para agregar a las otras respuestas correctas:

Es solo una expresión de declaración que refleja posibles expresiones de uso. ¡IIRC, creo que Kernigan o Ritchie lo llamaron un experimento! Aquí hay un texto de apoyo marginal:

En los años 70, cuando Kernighan y Ritchie escribían El lenguaje de programación C, eran bastante honestos acerca de cómo las declaraciones pueden volverse ilegibles rápidamente a simple vista:

C a veces se castiga por la sintaxis de sus declaraciones, particularmente las que involucran punteros a funciones. La sintaxis es un intento de hacer la declaración y el uso de acuerdo ; funciona bien para los casos simples, pero puede ser confuso para los más difíciles, porque las declaraciones no se pueden leer de izquierda a derecha, y porque los paréntesis se usan en exceso. [...]

(Mi énfasis)

http://codinghighway.com/2013/12/29/the-absolute-definitive-guide-to-decipher-c-declarations/

Entonces, sí, tiene razón, estos son de hecho los mismos operadores aplicados a las expresiones de declaración, aunque como ha observado, solo algunos de los operadores tienen sentido (por ejemplo, ++ no).

Tal estrella es de hecho parte del tipo de la variable individual que se declara; sin embargo, como han señalado otros carteles, no (más precisamente, no puede) transferirse a otras variables que se declaran al mismo tiempo (es decir, en la misma declaración de declaración): cada variable comienza de nuevo con el mismo tipo base que el objetivo (eventual), y luego obtiene sus propios operadores de puntero, matriz y función (oportunidad de aplicar o no) para completar su tipo.


Esta es la razón por la cual los programadores experimentados tienden a preferir (1) int *p;vs. int* p;y (2) declarar solo una variable por declaración, aunque el lenguaje permita más.

Para agregar a eso int (*p);es una declaración legal, estos padres simplemente se agrupan y en este caso simple, y no hacen nada diferente int *p. Esto es lo mismo que decir que una expresión (no declarativa) (i) >= (0)es la misma i >= 0, ya que siempre se pueden agregar legalmente ().

He mencionado que como otro ejemplo, sin embargo, de por qué los programadores prefieren una forma sobre la otra, por lo que considerar int (*p);vs int(* p);. Quizás pueda ver cómo el paréntesis, la estrella, etc. se aplican a la variable, no al tipo base (es decir, agregando todos los paréntesis permitidos).

Erik Eidt
fuente
1

En esta declaración de función:

void move(int *units);

el *es parte de tipo, es decir int*. No es un operador. Es por eso que mucha gente lo escribiría como:

void move(int* units);
BЈовић
fuente
1

Sólo el *, []y ()los operadores tienen ningún significado en las declaraciones (C ++ añade &, pero no vamos a entrar en eso aquí).

En la declaración

int *p;       

la int-ness de pestá especificado por el especificador de tipo int, mientras que el puntero-dad de pes especificado por el declarador *p .

El tipo de pes "puntero a int"; Este tipo está completamente especificado por la combinación del especificador de tipo intmás el declarador *p.

En una declaración, el declarador introduce el nombre de la cosa que se declara ( p) junto con información de tipo adicional no dada por el especificador de tipo ("puntero a"):

T v;     // v is a single object of type T, for any type T
T *p;    // p is a pointer to T, for any type T
T a[N];  // a is an N-element array of T, for any type T
T f();   // f is a function returning T, for any type T

Esto es importante: puntero-ness, array-ness y function-ness se especifican como parte del declarador, no como el especificador de tipo 1 . Si tú escribes

int* a, b, c;

se analizará como

int (*a), b, c;

así que solo ase declarará como un puntero a int; by cse declaran como regulares ints.

El *, []y ()los operadores se pueden combinar para crear tipos arbitrariamente complejas:

T *a[N];      // a is an N-element array of pointers to T
T (*a)[N];    // a is a pointer to an N-element array of T
T *(*f[N])(); // f is an N-element array of pointers to functions 
              // returning pointer to T

T *(*(*(*f)[N])())[M]  // f is a pointer to an N-element array of pointers
                       // to functions returning pointers to M-element
                       // arrays of pointers to T

Observe que *, []y ()obedezca las mismas reglas de precedencia en las declaraciones que hacen en las expresiones. *a[N]se analiza como *(a[N])en las declaraciones y expresiones.

Lo realmente importante a tener en cuenta en esto es que la forma de una declaración coincide con la forma de la expresión en el código. Volviendo a nuestro ejemplo original, tenemos un puntero a un entero llamado p. Si queremos recuperar ese valor entero, usamos el *operador para desreferenciar p, así:

x = *p;

El tipo de expresión *p es int, que se deduce de la declaración

int *p;

Del mismo modo, si tenemos una matriz de punteros doubley queremos recuperar un valor específico, indexamos en la matriz y desreferenciamos el resultado:

y = *ap[i];

Nuevamente, el tipo de expresión *ap[i] es double, que se deduce de la declaración

double *ap[N];

Entonces ¿por qué no ++jugar un papel en una declaración como *, []o ()? ¿O cualquier otro operador como +o ->o &&?

Bueno, básicamente, porque la definición del lenguaje lo dice. Solo se reserva *, []y ()para desempeñar cualquier papel en una declaración, ya que debe poder especificar punteros, matrices y tipos de funciones. No hay un tipo "increment-this" separado, por lo que no es necesario ++ser parte de una declaración. No hay "-este bit a bit" tipo, así que no hay necesidad de unario &, |, ^, o ~ser parte de una declaración tampoco. Para los tipos que usan el .operador de selección de miembros, usamos las etiquetas structy unionen la declaración. Para los tipos que usan el ->operador, usamos las etiquetas structy unionjunto con el *operador en el declarador.


  1. Por supuesto, puede crear nombres typedef para punteros, matrices y tipos de funciones, como
    typedef int *iptr;
    iptr a,b,c; // all three of a, b, and c are pointers to int
    pero de nuevo, es el declarador el *iptr que especifica el puntero del nombre typedef.

John Bode
fuente
1

El estándar C solo define dos significados para el *operador:

  • operador de indirección
  • operador de multiplicación

Es cierto que hay dos significados para el * operador . No es cierto que esos sean los únicos significados de la * ficha .

En una declaración como

int *p;

el *no es un operador; Es parte de la sintaxis de una declaración.

La sintaxis de la declaración de C está pensada para reflejar su sintaxis de expresión, por lo que la declaración anterior se puede leer como " *pes de tipo int", de lo que podemos inferir que pes de tipo int*. Sin embargo, esta no es una regla firme, y no se menciona en ninguna parte del estándar. En cambio, la sintaxis de las declaraciones se define por sí sola, por separado de la sintaxis de las expresiones. Es por eso que, por ejemplo, la mayoría de los operadores que pueden aparecer en expresiones no pueden aparecer con los mismos significados en las declaraciones (a menos que sean parte de una expresión que es parte de la declaración, como el +operador en int array[2+2];).

Un caso en el que el principio del "uso de los espejos de declaración" no funciona del todo es:

int arr[10];

donde arr[10]sería de tipo int si existiera .

Y, de hecho, hay otro uso del *token. Se puede declarar un parámetro de matriz [*]para indicar una matriz de longitud variable. (Esto se agregó en C99.) No *es una multiplicación ni una desreferencia. Sospecho que se inspiró en la sintaxis comodín de shell.

Keith Thompson
fuente
Un lugar más grande en el que se rompe el concepto de "declaración después del uso" es con la inicialización del puntero. El efecto de una declaración int *p = 0;es similar al uso * p = 0; `pero los significados son muy diferentes.
supercat
@ Supercat: Claro. la inicialización siempre coincide con el tipo de objeto (en este caso int*).
Keith Thompson