sirven que punteros puntero para los declaracion arreglos aritmetica apuntadores c pointers

c - que - Punteros a punteros versus punteros normales



punteros en c (12)

El propósito de un puntero es guardar la dirección de una variable específica. Entonces la estructura de memoria del siguiente código debería verse así:

int a = 5; int *b = &a;

...... dirección de memoria ...... valor
a ... 0x000002 ................... 5
b ... 0x000010 ................... 0x000002

Bien vale. Luego suponga que ahora quiero guardar la dirección del puntero * b. Luego, generalmente definimos un puntero doble, ** c, como

int a = 5; int *b = &a; int **c = &b;

Entonces la estructura de la memoria se ve así:

...... dirección de memoria ...... valor
a ... 0x000002 ................... 5
b ... 0x000010 ................... 0x000002
c ... 0x000020 ................... 0x000010

Entonces ** c se refiere a la dirección de * b.

Ahora mi pregunta es, ¿por qué este tipo de código,

int a = 5; int *b = &a; int *c = &b;

generar una advertencia?

Si el propósito del puntero es solo guardar la dirección de memoria, creo que no debería haber jerarquía si la dirección que vamos a guardar se refiere a una variable, un puntero, un doble puntero, etc., por lo que el siguiente tipo de código debería Sé valido.

int a = 5; int *b = &a; int *c = &b; int *d = &c; int *e = &d; int *f = &e;


¿Por qué este tipo de código genera una advertencia?

int a = 5; int *b = &a; int *c = &b;

El operador & produce un puntero al objeto, es decir, &a es de tipo int * por lo que es válido (mediante inicialización) a b que también es de tipo int * . &b produce un puntero al objeto b , es decir, &b es de tipo puntero a int * , i .e., int ** .

C dice en las restricciones del operador de asignación (que son válidas para la inicialización) que (C11, 6.5.16.1p1): "ambos operandos son punteros a versiones calificadas o no calificadas de tipos compatibles" . Pero en la definición C de lo que es un tipo compatible, int ** e int * no son tipos compatibles .

Entonces hay una violación de restricción en int *c = &b; inicialización, lo que significa que el compilador requiere un diagnóstico.

Una de las razones de la regla aquí es que el Estándar no garantiza que los dos tipos de puntero diferentes sean del mismo tamaño (excepto para los tipos de puntero void * y void * ), es decir, sizeof (int *) y sizeof (int **) pueden ser valores diferentes.


Ahora mi pregunta es, ¿por qué este tipo de código,

int a = 5; int *b = &a; int *c = &b;

generar una advertencia?

Necesitas volver a los fundamentos.

  • las variables tienen tipos
  • las variables tienen valores
  • un puntero es un valor
  • un puntero se refiere a una variable
  • si p es un valor de puntero, entonces *p es una variable
  • si v es una variable, entonces &v es un puntero

Y ahora podemos encontrar todos los errores en su publicación.

Luego suponga que ahora quiero guardar la dirección del puntero *b

No. *b es una variable de tipo int. No es un puntero. b es una variable cuyo valor es un puntero. *b es una variable cuyo valor es un entero.

**c refiere a la dirección de *b .

NO NO NO. Absolutamente no. Debe comprender esto correctamente si va a comprender los punteros.

*b es una variable; Es un alias para la variable a . La dirección de la variable a es el valor de la variable b . **c no se refiere a la dirección de a . Más bien, es una variable que es un alias para la variable a . (Y también lo es *b .)

La declaración correcta es: el valor de la variable c es la dirección de b . O, de manera equivalente: el valor de c es un puntero que se refiere a b .

Cómo sabemos esto? Regrese a los fundamentos. Dijiste que c = &b . Entonces, ¿cuál es el valor de c ? Un puntero ¿A qué? b .

Asegúrese de comprender completamente las reglas fundamentales.

Ahora que con suerte comprende la relación correcta entre variables y punteros, debería poder responder a su pregunta sobre por qué su código da un error.


Creo que no debería haber jerarquía si la dirección que vamos a guardar se refiere a variable, puntero, doble puntero

Sin la "jerarquía" sería muy fácil generar UB por todas partes sin ninguna advertencia, eso sería horrible.

Considera esto:

char c = ''a''; char* pc = &c; char** ppc = &pc; printf("%c/n", **ppc); // compiles ok and is valid printf("%c/n", **pc); // error: invalid type argument of unary ‘*’

El compilador me da un error y, por lo tanto, me ayuda a saber que he hecho algo mal y puedo corregir el error.

Pero sin "jerarquía", como:

char c = ''a''; char* pc = &c; char* ppc = &pc; printf("%c/n", **ppc); // compiles ok and is valid printf("%c/n", **pc); // compiles ok but is invalid

El compilador no puede dar ningún error ya que no hay "jerarquía".

Pero cuando la línea:

printf("%c/n", **pc);

se ejecuta, es UB (comportamiento indefinido).

Primero *pc lee el char como si fuera un puntero, es decir, probablemente lee 4 u 8 bytes aunque solo hayamos reservado 1 byte. Eso es UB.

Si el programa no se bloqueó debido a la UB anterior pero solo devolvió un valor de guarnición, el segundo paso sería desreferenciar el valor de guarnición. Una vez más UB.

Conclusión

El sistema de tipos nos ayuda a detectar errores al ver int *, int **, int ***, etc. como diferentes tipos.


Si el propósito del puntero es solo guardar la dirección de memoria, creo que no debería haber jerarquía si la dirección que vamos a guardar se refiere a variables, punteros, punteros dobles, ... etc.

En tiempo de ejecución, sí, un puntero solo contiene una dirección. Pero en tiempo de compilación también hay un tipo asociado con cada variable. Como han dicho los demás, int* e int** son dos tipos diferentes e incompatibles.

Hay un tipo, void* , que hace lo que desea: almacena solo una dirección, puede asignarle cualquier dirección:

int a = 5; int *b = &a; void *c = &b;

Pero cuando desee desreferenciar un void* , debe proporcionar la información de tipo ''faltante'' usted mismo:

int a2 = **((int**)c);


Si el propósito del puntero es solo guardar la dirección de memoria, creo que no debería haber jerarquía si la dirección que vamos a guardar se refiere a variables, punteros, punteros dobles, etc., por lo que el siguiente tipo de código debería ser válido.

Bueno, eso es cierto para la máquina (después de todo, casi todo es un número). Pero en muchos idiomas se escriben variables, lo que significa que el compilador puede asegurarse de que las use correctamente (los tipos imponen un contexto correcto en las variables)

Es cierto que un puntero a puntero y un puntero (probablemente) usan la misma cantidad de memoria para almacenar su valor (cuidado, esto no es cierto para int y puntero a int, el tamaño de una dirección no está relacionado con el tamaño de un casa).

Entonces, si tiene una dirección de una dirección, debe usarla como está y no como una dirección simple porque si accede al puntero para señalar como un puntero simple, entonces podrá manipular una dirección de int como si fuera un int , que no lo es (reemplace int sin otra cosa y debería ver el peligro). Puede estar confundido porque todo esto son números, pero en la vida cotidiana no: yo personalmente hago una gran diferencia en $ 1 y 1 perro. dog y $ son tipos, sabes lo que puedes hacer con ellos.

Puedes programar en ensamblaje y hacer lo que quieras, pero observarás lo peligroso que es, porque puedes hacer casi lo que quieras, especialmente cosas extrañas. Sí, modificar un valor de dirección es peligroso, suponga que tiene un automóvil autónomo que debe entregar algo en una dirección expresada en distancia: 1200 calle de memoria (dirección) y suponga que las casas de la calle están separadas por 100 pies (1221 es una dirección no válida), si puede manipular direcciones como desee como número entero, podría intentar entregar a las 1223 y dejar el paquete en el medio del pavimento.

Otro ejemplo podría ser, casa, dirección de la casa, número de entrada en una libreta de direcciones de esa dirección. Todos estos tres son conceptos diferentes, tipos diferentes ...


Si el propósito del puntero es solo guardar la dirección de memoria, creo que no debería haber jerarquía si la dirección que vamos a guardar se refiere a variables, punteros, punteros dobles, etc., por lo que el siguiente tipo de código debería ser válido.

Creo que este es su malentendido: el propósito del puntero en sí mismo es almacenar la dirección de memoria, pero un puntero generalmente también tiene un tipo para que sepamos qué esperar en el lugar al que apunta.

Especialmente, a diferencia de usted, otras personas realmente quieren tener este tipo de jerarquía para saber qué hacer con el contenido de la memoria que señala el puntero.

El objetivo principal del sistema de puntero de C es tener información de tipo adjunta.

Si lo haces

int a = 5;

&a implica que lo que obtienes es un int * modo que si desreferencias, es un int nuevamente.

Llevando eso a los siguientes niveles,

int *b = &a; int **c = &b;

&b es un puntero. Pero sin saber qué se esconde detrás, resp. a lo que apunta, es inútil. Es importante saber que desreferenciar un puntero revela el tipo del tipo original, de modo que *(&b) es un int * , y **(&b) es el valor int original con el que trabajamos.

Si cree que en sus circunstancias no debería haber una jerarquía de tipos, siempre puede trabajar con void * , aunque la usabilidad directa es bastante limitada.


El lenguaje C está fuertemente tipado. Esto significa que, para cada dirección, hay un tipo , que le dice al compilador cómo interpretar el valor en esa dirección.

En tu ejemplo:

int a = 5; int *b = &a;

El tipo de a es int , y el tipo de b es int * (leído como "puntero a int "). Usando su ejemplo, la memoria contendría:

..... memory address ...... value ........ type a ... 0x00000002 .......... 5 ............ int b ... 0x00000010 .......... 0x00000002 ... int*

El tipo no está realmente almacenado en la memoria, es solo que el compilador sabe que, cuando lees a , encontrarás un int , y cuando leas b encontrarás la dirección de un lugar donde puedes encontrar un int .

En tu segundo ejemplo:

int a = 5; int *b = &a; int **c = &b;

El tipo de c es int ** , leído como "puntero a puntero a int ". Significa que, para el compilador:

  • c es un puntero;
  • cuando lees c , obtienes la dirección de otro puntero;
  • cuando lees ese otro puntero, obtienes la dirección de un int .

Es decir,

  • c es un puntero ( int ** );
  • *c también es un puntero ( int * );
  • **c es un int .

Y la memoria contendría:

..... memory address ...... value ........ type a ... 0x00000002 .......... 5 ............ int b ... 0x00000010 .......... 0x00000002 ... int* c ... 0x00000020 .......... 0x00000010 ... int**

Dado que el "tipo" no se almacena junto con el valor, y un puntero puede apuntar a cualquier dirección de memoria, la forma en que el compilador conoce el tipo del valor en una dirección es básicamente tomando el tipo del puntero y eliminando el más a la derecha * .

Por cierto, eso es para una arquitectura común de 32 bits. Para la mayoría de las arquitecturas de 64 bits, tendrá:

..... memory address .............. value ................ type a ... 0x0000000000000002 .......... 5 .................... int b ... 0x0000000000000010 .......... 0x0000000000000002 ... int* c ... 0x0000000000000020 .......... 0x0000000000000010 ... int**

Las direcciones ahora son de 8 bytes cada una, mientras que un int todavía solo tiene 4 bytes. Como el compilador conoce el tipo de cada variable, puede manejar fácilmente esta diferencia y leer 8 bytes para un puntero y 4 bytes para int .


El sistema de tipos de C requiere esto, si desea obtener una advertencia correcta y si desea que el código se compile en absoluto. Con solo un nivel de profundidad de punteros, no sabría si el puntero apunta a un puntero o a un entero real.

Si desreferencia un tipo int** , sabe que el tipo que obtiene es int* y, de manera similar, si desreferencia int* el tipo es int . Con su propuesta, el tipo sería ambiguo.

Tomando de su ejemplo, es imposible saber si c apunta a int o int* :

c = rand() % 2 == 0 ? &a : &b;

¿A qué tipo apunta c? El compilador no lo sabe, por lo que esta siguiente línea es imposible de realizar:

*c;

En C, toda la información de tipo se pierde después de la compilación, ya que cada tipo se verifica en tiempo de compilación y ya no es necesario. Su propuesta realmente desperdiciaría memoria y tiempo, ya que cada puntero tendría que tener información de tiempo de ejecución adicional sobre los tipos contenidos en los punteros.


En

int a = 5; int *b = &a; int *c = &b;

Recibe una advertencia porque &b es de tipo int ** , e intenta inicializar una variable de tipo int * . No hay conversiones implícitas entre esos dos tipos, lo que lleva a la advertencia.

Para tomar el ejemplo más largo que desea trabajar, si tratamos de desreferenciar el compilador nos dará un int , no un puntero que podamos desreferenciar aún más.

También tenga en cuenta que en muchos sistemas int e int* no tienen el mismo tamaño (por ejemplo, un puntero puede tener 64 bits de largo y un int 32 bits de largo). Si cancela la referencia de f y obtiene un int , pierde la mitad del valor y ni siquiera puede convertirlo en un puntero válido.


Eso sería porque cualquier puntero T* es en realidad de tipo pointer to a T (o address of a T ), donde T es el tipo apuntado. En este caso, * se puede leer como pointer to a(n) , y T es el tipo apuntado.

int x; // Holds an integer. // Is type "int". // Not a pointer; T is nonexistent. int *px; // Holds the address of an integer. // Is type "pointer to an int". // T is: int int **pxx; // Holds the address of a pointer to an integer. // Is type "pointer to a pointer to an int". // T is: int*

Esto se usa para propósitos de desreferenciación, donde el operador de desreferencia tomará una T* y devolverá un valor cuyo tipo es T El tipo de retorno puede verse como truncando el "puntero a la izquierda a la (n)" más a la izquierda, y como lo que sobra.

*x; // Invalid: x isn''t a pointer. // Even if a compiler allows it, this is a bad idea. *px; // Valid: px is "pointer to int". // Return type is: int // Truncates leftmost "pointer to" part, and returns an "int". *pxx; // Valid: pxx is "pointer to pointer to int". // Return type is: int* // Truncates leftmost "pointer to" part, and returns a "pointer to int".

Observe cómo para cada una de las operaciones anteriores, el tipo de retorno del operador de desreferencia coincide con el tipo T* declaración T* original.

Esto ayuda enormemente tanto a los compiladores primitivos como a los programadores a analizar el tipo de un puntero: para un compilador, la dirección del operador agrega un * al tipo, el operador de eliminación de referencias elimina un * del tipo y cualquier falta de coincidencia es un error. Para un programador, el número de * s es una indicación directa de cuántos niveles de indirección está tratando ( int* siempre apunta a int , float** siempre apunta a float* que a su vez siempre apunta a float , etc. )

Ahora, teniendo esto en cuenta, hay dos problemas principales con solo usar un solo * independientemente del número de niveles de indirección:

  1. El puntero es mucho más difícil de desreferenciar para el compilador, ya que tiene que volver a la asignación más reciente para determinar el nivel de indirección y determinar el tipo de retorno de manera adecuada.
  2. El puntero es más difícil de entender para el programador, porque es fácil perder la noción de cuántas capas de indirección hay.

En ambos casos, la única forma de determinar el tipo real del valor sería dar marcha atrás, obligándote a buscar en otro lugar para encontrarlo.

void f(int* pi); int main() { int x; int *px = &x; int *ppx = &px; int *pppx = &ppx; f(pppx); } // Ten million lines later... void f(int* pi) { int i = *pi; // Well, we''re boned. // To see what''s wrong, see main(). }

Este ... es un problema muy peligroso, y uno que se resuelve fácilmente haciendo que el número de * s represente directamente el nivel de indirección.


Hay diferentes tipos Y hay una buena razón para ello:

Teniendo …

int a = 5; int *b = &a; int **c = &b;

… la expresion …

*b * 5

... es válido, mientras que la expresión ...

*c * 5

no tiene sentido.

El gran problema no es cómo se almacenan los punteros o punteros a punteros, sino a qué se refieren.


Los punteros son abstracciones de direcciones de memoria con semántica de tipo adicional, y en un lenguaje como el tipo C importa.

En primer lugar, no hay garantía de que int * e int ** tengan el mismo tamaño o representación (en las arquitecturas de escritorio modernas que tienen, pero no se puede confiar en que sea universalmente cierto).

En segundo lugar, el tipo es importante para la aritmética del puntero. Dado un puntero p de tipo T * , la expresión p + 1 produce la dirección del siguiente objeto de tipo T Entonces, asuma las siguientes declaraciones:

char *cp = 0x1000; short *sp = 0x1000; // assume 16-bit short int *ip = 0x1000; // assume 32-bit int long *lp = 0x1000; // assume 64-bit long

La expresión cp + 1 nos da la dirección del siguiente objeto char , que sería 0x1001 . La expresión sp + 1 nos da la dirección del siguiente objeto short , que sería 0x1002 . ip + 1 nos da 0x1004 , y lp + 1 nos da 0x1008 .

Entonces, dado

int a = 5; int *b = &a; int **c = &b;

b + 1 nos da la dirección del siguiente int , y c + 1 nos da la dirección del siguiente puntero a int .

Se requieren punteros a punteros si desea que una función escriba en un parámetro de tipo puntero. Toma el siguiente código:

void foo( T *p ) { *p = new_value(); // write new value to whatever p points to } void bar( void ) { T val; foo( &val ); // update contents of val }

Esto es cierto para cualquier tipo T Si reemplazamos T con un puntero tipo P * , el código se convierte en

void foo( P **p ) { *p = new_value(); // write new value to whatever p points to } void bar( void ) { P *val; foo( &val ); // update contents of val }

La semántica es exactamente la misma, solo los tipos son diferentes; el parámetro formal p es siempre un nivel más de indirección que la variable val .