¿Cómo explicar los punteros C (declaración frente a operadores unarios) a un principiante?

141

Recientemente tuve el placer de explicarle los consejos a un principiante de programación en C y me topé con la siguiente dificultad. Puede que no parezca un problema en absoluto si ya sabe cómo usar punteros, pero intente ver el siguiente ejemplo con una mente clara:

int foo = 1;
int *bar = &foo;
printf("%p\n", (void *)&foo);
printf("%i\n", *bar);

Para el principiante absoluto, la salida puede ser sorprendente. En la línea 2 él / ella acababa de declarar * bar como be & foo, pero en la línea 4 resulta que * bar es realmente foo en lugar de & foo!

La confusión, se podría decir, proviene de la ambigüedad del símbolo *: en la línea 2 se usa para declarar un puntero. En la línea 4 se usa como operador unario que obtiene el valor al que apunta el puntero. Dos cosas diferentes, ¿verdad?

Sin embargo, esta "explicación" no ayuda en absoluto a un principiante. Introduce un nuevo concepto al señalar una discrepancia sutil. Esta no puede ser la forma correcta de enseñarlo.

Entonces, ¿cómo lo explicaron Kernighan y Ritchie?

El operador unario * es el operador de indirección o desreferenciación; cuando se aplica a un puntero, accede al objeto al que apunta el puntero. [...]

La declaración del puntero ip, int *ippretende ser un mnemotécnico; dice que la expresión *ipes un int. La sintaxis de la declaración para una variable imita la sintaxis de las expresiones en las que puede aparecer la variable .

int *ipdebe leerse como " *ipdevolverá un int"? Pero, ¿por qué la asignación después de la declaración no sigue ese patrón? ¿Qué pasa si un principiante quiere inicializar la variable? int *ip = 1(léase: *ipdevolverá un inty el intis 1) no funcionará como se esperaba. El modelo conceptual simplemente no parece coherente. ¿Me estoy perdiendo de algo?


Editar: Intentó resumir las respuestas aquí .

armin
fuente
15
La mejor explicación es dibujar cosas en un papel y conectarlas con flechas;)
Maroun
16
Cuando tenía que explicar la sintaxis de los punteros, siempre insistía en el hecho de que *en una declaración es un token que significa "declarar un puntero", en expresiones es el operador de desreferencia, y que estos dos representan cosas diferentes que tienen el mismo símbolo (igual que el operador de multiplicación - mismo símbolo, significado diferente). Es confuso, pero cualquier cosa diferente al estado actual de las cosas será aún peor.
Matteo Italia
40
tal vez escribirlo int* barhace que sea más obvio que la estrella es en realidad parte del tipo, no parte del identificador. Por supuesto, esto te lleva a diferentes problemas con cosas poco intuitivas como int* a, b.
Niklas B.
9
Siempre he pensado que la explicación de K&R es tonta e innecesaria. El lenguaje usa el mismo símbolo para dos cosas diferentes y solo tenemos que lidiar con él. *puede tener dos significados diferentes según el contexto. Al igual que la misma letra se puede pronunciar de manera diferente según la palabra en la que se encuentre, lo que dificulta aprender a hablar muchos idiomas. Si cada concepto / operación tuviera su propio símbolo, necesitaríamos teclados mucho más grandes, por lo que los símbolos se reciclan cuando tiene sentido hacerlo.
Arte
8
Me he encontrado con el mismo problema muchas veces cuando enseño C a otros y, en mi experiencia, se puede resolver de la manera en que la mayoría de las personas aquí lo han sugerido. Primero, explique el concepto de un puntero sin la sintaxis C. Luego, enseñe la sintaxis y haga hincapié en el asterisco como parte del tipo ( int* p), al tiempo que advierte a su estudiante que no use declaraciones múltiples en la misma línea cuando hay punteros involucrados. Cuando el alumno haya entendido completamente el concepto de punteros, explíquele que la int *psintaxis es equivalente y luego explique el problema con varias declaraciones.
Theodoros Chatzigiannakis

Respuestas:

43

Para que su alumno entienda el significado del *símbolo en diferentes contextos, primero debe comprender que los contextos son realmente diferentes. Una vez que comprenden que los contextos son diferentes (es decir, la diferencia entre el lado izquierdo de una tarea y una expresión general), no es un gran salto cognitivo comprender cuáles son las diferencias.

Primero explique que la declaración de una variable no puede contener operadores (demuestre esto mostrando que poner un símbolo -o +en una declaración de variable simplemente causa un error). Luego continúe para mostrar que una expresión (es decir, en el lado derecho de una asignación) puede contener operadores. Asegúrese de que el alumno comprenda que una expresión y una declaración de variable son dos contextos completamente diferentes.

Cuando entienden que los contextos son diferentes, puede continuar explicando que cuando el *símbolo está en una declaración de variable frente al identificador de variable, significa 'declarar esta variable como un puntero'. Luego puede explicar que cuando se usa en una expresión (como operador unario) el *símbolo es el 'operador de desreferencia' y significa 'el valor en la dirección de' en lugar de su significado anterior.

Para convencer verdaderamente a su estudiante, explique que los creadores de C podrían haber usado cualquier símbolo para referirse al operador de desreferenciación (es decir, podrían haberlo usado en su @lugar) pero por cualquier razón tomaron la decisión de diseño *.

Con todo, no hay forma de explicar que los contextos son diferentes. Si el estudiante no entiende que los contextos son diferentes, no puede entender por qué el *símbolo puede significar cosas diferentes.

Pharap
fuente
80

La razón por la taquigrafía:

int *bar = &foo;

en su ejemplo puede ser confuso es que es fácil interpretarlo mal como equivalente a:

int *bar;
*bar = &foo;    // error: use of uninitialized pointer bar!

cuando en realidad significa:

int *bar;
bar = &foo;

Escrito de esta manera, con la declaración variable y la asignación separadas, no existe tal posibilidad de confusión, y el uso del paralelismo de declaración ↔ descrito en su cita de K&R funciona perfectamente:

  • La primera línea declara una variable bar, tal que *bares un int.

  • La segunda línea asigna la dirección de fooa bar, haciendo que *bar(an int) sea un alias para foo(también un int).

Al presentar la sintaxis del puntero C a los principiantes, puede ser útil mantener inicialmente este estilo de separar las declaraciones del puntero de las asignaciones, y solo introducir la sintaxis abreviada combinada (con advertencias apropiadas sobre su potencial de confusión) una vez que los conceptos básicos del puntero se utilizan en C se ha internalizado adecuadamente.

Ilmari Karonen
fuente
44
Estaría tentado a hacerlo typedef. typedef int *p_int;significa que una variable de tipop_int tiene la propiedad que *p_intes un int. Entonces tenemos p_int bar = &foo;. Animar a cualquiera a crear datos no inicializados y luego asignarlos como un hábito predeterminado parece ... una mala idea.
Yakk - Adam Nevraumont
66
Este es solo el estilo de las declaraciones en C con daño cerebral; No es específico de los punteros. considere int a[2] = {47,11};, eso no es una inicialización del elemento (inexistente) a[2]eiher.
Marc van Leeuwen
55
@MarcvanLeeuwen De acuerdo con el daño cerebral. Idealmente,* debería ser parte del tipo, no estar vinculado a la variable, y luego podría escribir int* foo_ptr, bar_ptrpara declarar dos punteros. Pero en realidad declara un puntero y un número entero.
Barmar
1
No se trata solo de declaraciones / asignaciones "abreviadas". Todo el problema vuelve a aparecer en el momento en que desea utilizar punteros como argumentos de función.
armin
30

Corto en declaraciones

Es bueno saber la diferencia entre declaración e inicialización. Declaramos variables como tipos y las inicializamos con valores. Si hacemos ambas cosas al mismo tiempo, a menudo lo llamamos definición.

1. int a; a = 42;

int a;
a = 42;

Nosotros declaramos una intllamada de una . Luego lo inicializamos dándole un valor 42.

2. int a = 42;

nos declaramos y intnombramos una y le damos el valor 42. Se inicializa con 42. Una definicion.

3. a = 43;

Cuando usamos las variables, decimos que las operamos . a = 43Es una operación de asignación. Asignamos el número 43 a la variable a.

Diciendo

int *bar;

declaramos que bar es un puntero a un int. Diciendo

int *bar = &foo;

declaramos bar e inicializamos con la dirección de foo .

Después de haber inicializado barra , podemos usar el mismo operador, el asterisco, para acceder y operar con el valor de foo . Sin el operador, accedemos y operamos en la dirección a la que apunta el puntero.

Además de eso, dejé que la imagen hablara.

Qué

Una ASCIIMACIÓN simplificada sobre lo que está sucediendo. (Y aquí una versión del reproductor si quieres pausar, etc.)

          ASCIIMACIÓN

Morpfh
fuente
22

La segunda declaración int *bar = &foo;se puede ver gráficamente en la memoria como,

   bar           foo
  +-----+      +-----+
  |0x100| ---> |  1  |
  +-----+      +-----+ 
   0x200        0x100

Ahora bares un puntero de tipo que intcontiene la dirección &de foo. Usando el operador unario *, deferencia para recuperar el valor contenido en 'foo' usando el puntero bar.

EDITAR : Mi enfoque con los principiantes es explicar el memory addressde una variable, es decir

Memory Address:Cada variable tiene una dirección asociada proporcionada por el sistema operativo. En int a;, &aes la dirección de la variable a.

Continúe explicando los tipos básicos de variables en Cas,

Types of variables: Las variables pueden contener valores de tipos respectivos pero no direcciones.

int a = 10; float b = 10.8; char ch = 'c'; `a, b, c` are variables. 

Introducing pointers: Como se dijo anteriormente, las variables, por ejemplo

 int a = 10; // a contains value 10
 int b; 
 b = &a;      // ERROR

Es posible asignar b = apero no b = &a, ya que la variable bpuede contener valor pero no dirección, por lo tanto, necesitamos punteros .

Pointer or Pointer variables :Si una variable contiene una dirección, se conoce como variable puntero. Use *en la declaración para informar que es un puntero.

 Pointer can hold address but not value
 Pointer contains the address of an existing variable.
 Pointer points to an existing variable
Sunil Bojanapally
fuente
3
El problema es que leer int *ipcomo "ip es un puntero (*) de tipo int" se mete en problemas al leer algo así x = (int) *ip.
armin
2
@abw Eso es algo completamente diferente, de ahí los paréntesis. No creo que las personas tengan dificultades para comprender la diferencia entre declaraciones y casting.
bzeaman
@abw In x = (int) *ip;, obtenga el valor desreferenciando el puntero ipy convierta el valor en intcualquier tipo ip.
Sunil Bojanapally
1
@BennoZeeman Tienes razón: el casting y las declaraciones son dos cosas diferentes. Traté de insinuar el papel diferente del asterisco: primero "esto no es un int, sino un puntero a int" 2nd "esto le dará el int, pero no el puntero a int".
armin
2
@abw: ¿Cuál es la razón por la enseñanza int* bar = &foo;hace que las cargas más sentido. Sí, sé que causa problemas cuando declaras múltiples punteros en una sola declaración. No, no creo que eso importe en absoluto.
Carreras de ligereza en órbita
17

Mirando las respuestas y los comentarios aquí, parece haber un acuerdo general de que la sintaxis en cuestión puede ser confusa para un principiante. La mayoría de ellos proponen algo en este sentido:

  • Antes de mostrar cualquier código, use diagramas, bocetos o animaciones para ilustrar cómo funcionan los punteros.
  • Al presentar la sintaxis, explique los dos roles diferentes del símbolo de asterisco . Faltan muchos tutoriales o están evadiendo esa parte. Se produce la confusión ("Cuando divide una declaración de puntero inicializada en una declaración y una asignación posterior, debe recordar eliminar el *" - comp.lang.c FAQ ) Esperaba encontrar un enfoque alternativo, pero supongo que esto es el camino a seguir.

Puede escribir en int* barlugar de int *barresaltar la diferencia. Esto significa que no seguirá el enfoque de K&R "declaración imita el uso", pero el enfoque Stroustrup C ++ :

No declaramos *barser un número entero. Declaramos barser un int*. Si queremos inicializar una variable recién creada en la misma línea, está claro que estamos tratando bar, no *bar.int* bar = &foo;

Los inconvenientes:

  • Debe advertir a su estudiante sobre el problema de la declaración de puntero múltiple ( int* foo, barvs int *foo, *bar).
  • Tienes que prepararlos para un mundo de dolor . Muchos programadores quieren ver el asterisco adyacente al nombre de la variable, y se esforzarán mucho para justificar su estilo. Y muchas guías de estilo aplican esta notación explícitamente (estilo de codificación del kernel de Linux, Guía de estilo de la NASA C, etc.).

Editar: Un enfoque diferente que se ha sugerido es ir a la forma de "imitación" de K&R, pero sin la sintaxis de "taquigrafía" (ver aquí ). Tan pronto como omita hacer una declaración y una asignación en la misma línea , todo se verá mucho más coherente.

Sin embargo, tarde o temprano el alumno tendrá que lidiar con punteros como argumentos de función. Y punteros como tipos de retorno. Y punteros a funciones. Tendrás que explicar la diferencia entre int *func();y int (*func)();. Creo que tarde o temprano las cosas se vendrán abajo. Y quizás más pronto es mejor que más tarde.

armin
fuente
16

Hay una razón por la cual los favores de estilo K&R int *py los favores de estilo Stroustrup int* p; ambos son válidos (y significan lo mismo) en cada idioma, pero como dijo Stroustrup:

La elección entre "int * p;" y "int * p;" no se trata de lo correcto y lo incorrecto, sino del estilo y el énfasis. C enfatizó expresiones; Las declaraciones a menudo se consideraban poco más que un mal necesario. C ++, por otro lado, tiene un fuerte énfasis en los tipos.

Ahora, ya que está tratando de enseñar C aquí, eso sugeriría que debería enfatizar expresiones más que tipos, pero algunas personas pueden asimilar más fácilmente un énfasis más rápido que el otro, y eso se trata de ellos en lugar del lenguaje.

Por lo tanto, a algunas personas les resultará más fácil comenzar con la idea de que un int*es algo diferente de unint y partir de allí.

Si alguien asimila rápidamente la forma de mirarlo que usa int* bar que tienen barcomo una cosa que no es un entero, sino un puntero para int, a continuación, van a ver rápidamente que *barestá haciendo algo a bar, y el resto seguirá. Una vez que hayas hecho eso, puedes explicar por qué los codificadores C tienden a preferirint *bar .

O no. Si hubiera una manera de que todos entendieran por primera vez el concepto, no habría tenido ningún problema en primer lugar, y la mejor manera de explicárselo a una persona no será necesariamente la mejor manera de explicárselo a otra.

Jon Hanna
fuente
1
Me gusta el argumento de Stroustrup, pero me pregunto por qué eligió el símbolo & para denotar referencias, otra posible trampa.
armin
1
@abw Creo que vio simetría en si podemos hacer, int* p = &aentonces podemos hacer int* r = *p. Estoy bastante seguro de que lo cubrió en The Design and Evolution of C ++ , pero ha pasado mucho tiempo desde que lo leí, y tontamente le presté mi copia a alguien.
Jon Hanna
3
Supongo que te refieres int& r = *p. Y apuesto a que el prestatario todavía está tratando de digerir el libro.
armin
@abw, sí, eso fue exactamente lo que quise decir. Los errores tipográficos en los comentarios no generan errores de compilación. El libro es en realidad una lectura bastante rápida.
Jon Hanna
44
Una de las razones por las que estoy a favor de la sintaxis de Pascal (como se extendió popularmente) sobre las C es que Var A, B: ^Integer;deja en claro que el tipo "puntero al entero" se aplica a ambos Ay B. Usar un K&Restilo int *a, *btambién es viable; sino una declaración como int* a,b;, sin embargo, parece como si ay bestán ambas están declaradas como int*, pero en realidad se declara acomo una int*y bcomo int.
supercat
9

tl; dr:

P: ¿Cómo explicar los punteros C (declaración frente a operadores unarios) a un principiante?

A: no lo hagas. Explique los punteros al principiante y muéstreles cómo representar sus conceptos de puntero en la sintaxis C después.


Recientemente tuve el placer de explicarle los consejos a un principiante de programación en C y me topé con la siguiente dificultad.

OMI, la sintaxis de C no es horrible, pero tampoco es maravillosa: no es un gran obstáculo si ya entiendes los punteros, ni ninguna ayuda para aprenderlos.

Por lo tanto: comience explicando los punteros y asegúrese de que realmente los entiendan:

  • Explíquelos con diagramas de caja y flecha. Puede hacerlo sin direcciones hexadecimales, si no son relevantes, solo muestre las flechas que apuntan a otro cuadro o a algún símbolo nulo.

  • Explique con pseudocódigo: simplemente escriba la dirección de foo y el valor almacenado en la barra .

  • Luego, cuando su novato comprenda qué son los punteros, por qué y cómo usarlos; luego muestre el mapeo en la sintaxis de C.

Sospecho que la razón por la cual el texto de K&R no proporciona un modelo conceptual es que ya entendieron los punteros , y probablemente asumieron que cualquier otro programador competente en ese momento también lo hizo. El mnemotécnico es solo un recordatorio del mapeo desde el concepto bien entendido hasta la sintaxis.

Inútil
fuente
En efecto; Comience con la teoría primero, la sintaxis viene después (y no es importante). Tenga en cuenta que la teoría del uso de la memoria no depende del idioma. Este modelo de caja y flechas lo ayudará con las tareas en cualquier lenguaje de programación.
2015
Vea aquí algunos ejemplos (aunque google también lo ayudará) eskimo.com/~scs/cclass/notes/sx10a.html
oɔɯǝɹ
7

Este problema es algo confuso al comenzar a aprender C.

Estos son los principios básicos que pueden ayudarlo a comenzar:

  1. Solo hay unos pocos tipos básicos en C:

    • char: un valor entero con el tamaño de 1 byte.

    • short: un valor entero con el tamaño de 2 bytes.

    • long: un valor entero con un tamaño de 4 bytes.

    • long long: un valor entero con un tamaño de 8 bytes.

    • float: un valor no entero con un tamaño de 4 bytes.

    • double: un valor no entero con un tamaño de 8 bytes.

    Tenga en cuenta que el tamaño de cada tipo generalmente está definido por el compilador y no por el estándar.

    Los tipos enteros short, longy long longgeneralmente son seguidos por int.

    Sin embargo, no es imprescindible y puede usarlos sin el int.

    Alternativamente, puede simplemente indicar int, pero eso podría ser interpretado de manera diferente por diferentes compiladores.

    Para resumir esto:

    • shortes lo mismo short intpero no necesariamente lo mismo que int.

    • longes lo mismo long intpero no necesariamente lo mismo que int.

    • long longes lo mismo long long intpero no necesariamente lo mismo que int.

    • En un compilador dado, intes uno short into long into long long int.

  2. Si declara una variable de algún tipo, también puede declarar otra variable apuntando a ella.

    Por ejemplo:

    int a;

    int* b = &a;

    Entonces, en esencia, para cada tipo básico, también tenemos un tipo de puntero correspondiente.

    Por ejemplo: shorty short*.

    Hay dos formas de "mirar" la variable b (eso es lo que probablemente confunde a la mayoría de los principiantes) :

    • Se puede considerar bcomo una variable de tipo int*.

    • Se puede considerar *bcomo una variable de tipo int.

    Por lo tanto, algunas personas declararían int* b, mientras que otras declararían int *b.

    Pero el hecho es que estas dos declaraciones son idénticas (los espacios no tienen sentido).

    Puede usarlo bcomo puntero a un valor entero o*b como el valor entero puntiagudo real.

    Puede obtener (leer) el valor señalado: int c = *b .

    Y se puede establecer (escribir) el valor en punta: *b = 5.

  3. Un puntero puede apuntar a cualquier dirección de memoria, y no solo a la dirección de alguna variable que haya declarado previamente. Sin embargo, debe tener cuidado al usar punteros para obtener o establecer el valor ubicado en la dirección de memoria puntiaguda.

    Por ejemplo:

    int* a = (int*)0x8000000;

    Aquí, tenemos una variable que aapunta a la dirección de memoria 0x8000000.

    Si esta dirección de memoria no está asignada dentro del espacio de memoria de su programa, entonces cualquier operación de lectura o escritura que use *aprobablemente hará que su programa se bloquee, debido a una violación del acceso a la memoria.

    Puede cambiar el valor de forma segura a, pero debe tener mucho cuidado al cambiar el valor de *a.

  4. El tipo void*es excepcional en el hecho de que no tiene un "tipo de valor" correspondiente que se pueda usar (es decir, no se puede declarar void a). Este tipo se usa solo como un puntero general a una dirección de memoria, sin especificar el tipo de datos que reside en esa dirección.

barak manos
fuente
7

Quizás recorrerlo un poco más lo hace más fácil:

#include <stdio.h>

int main()
{
    int foo = 1;
    int *bar = &foo;
    printf("%i\n", foo);
    printf("%p\n", &foo);
    printf("%p\n", (void *)&foo);
    printf("%p\n", &bar);
    printf("%p\n", bar);
    printf("%i\n", *bar);
    return 0;
}

Pídales que le digan qué esperan que sea la salida en cada línea, luego pídales que ejecuten el programa y vean qué aparece. Explique sus preguntas (la versión desnuda allí seguramente provocará algunas, pero puede preocuparse por el estilo, la rigidez y la portabilidad más adelante). Luego, antes de que su mente se vuelva loca por pensar demasiado o se conviertan en un zombi después del almuerzo, escriba una función que tome un valor y la misma que tome un puntero.

En mi experiencia, está superando ese "¿por qué esto se imprime de esa manera?" joroba, y luego muestra de inmediato por qué esto es útil en los parámetros de la función mediante el juego práctico (como preludio de algún material básico de K&R como el análisis de cadenas / procesamiento de matriz) que hace que la lección no solo tenga sentido sino que se mantenga.

El siguiente paso es conseguir que explicar a usted cómo i[0]se relaciona con &i. Si pueden hacer eso, no lo olvidarán y puede comenzar a hablar sobre estructuras, incluso un poco antes de tiempo, solo para que se asimile.

Las recomendaciones anteriores sobre cuadros y flechas también son buenas, pero también pueden terminar divagando en una discusión completa sobre cómo funciona la memoria, que es una charla que debe suceder en algún momento, pero que puede distraerlo inmediatamente. : cómo interpretar la notación de puntero en C.

zxq9
fuente
Este es un buen ejercicio. Pero la cuestión que quería plantear es una sintaxis específica que podría tener un impacto en el modelo mental que construyen los estudiantes. Considere esto: int foo = 1;. Ahora bien, esto está bien: int *bar; *bar = foo;. Esto no está bien:int *bar = foo;
armin
1
@abw Lo único que tiene sentido es lo que los estudiantes terminen diciéndose a sí mismos. Eso significa "ver uno, hacer uno, enseñar uno". No puede proteger ni predecir qué sintaxis o estilo verán en la jungla (¡incluso sus viejos repositorios!), Por lo que debe mostrar suficientes permutaciones para que los conceptos básicos se entiendan independientemente del estilo, y luego comience a enseñarles por qué se han establecido ciertos estilos. Como enseñar inglés: expresión básica, modismos, estilos, estilos particulares en un determinado contexto. No es fácil, desafortunadamente. ¡En cualquier caso, buena suerte!
zxq9
6

El tipo de la expresión *bar es int; por lo tanto, el tipo de la variable (y expresión) bares int *. Como la variable tiene un tipo de puntero, su inicializador también debe tener un tipo de puntero.

Existe una inconsistencia entre la inicialización y asignación de la variable de puntero; Eso es algo que hay que aprender por las malas.

John Bode
fuente
3
Mirando las respuestas aquí tengo la sensación de que muchos programadores experimentados ya ni siquiera pueden ver el problema . Supongo que es un subproducto de "aprender a vivir con inconsistencias".
armin
3
@abw: las reglas para la inicialización son diferentes de las reglas para la asignación; para los tipos aritméticos escalares, las diferencias son insignificantes, pero son importantes para los tipos de puntero y agregados. Eso es algo que deberá explicar junto con todo lo demás.
John Bode
5

Prefiero leerlo como el primero se *aplica a intmás de bar.

int  foo = 1;           // foo is an integer (int) with the value 1
int* bar = &foo;        // bar is a pointer on an integer (int*). it points on foo. 
                        // bar value is foo address
                        // *bar value is foo value = 1

printf("%p\n", &foo);   // print the address of foo
printf("%p\n", bar);    // print the address of foo
printf("%i\n", foo);    // print foo value
printf("%i\n", *bar);   // print foo value
grorel
fuente
2
Luego tienes que explicar por qué int* a, bno hace lo que ellos piensan que hace.
Pharap
44
Es cierto, pero no creo que int* a,bdeba usarse en absoluto. Para una mejor lisibilidad, actualización, etc., solo debe haber una declaración de variable por línea y nunca más. También es algo para explicar a los principiantes, incluso si el compilador puede manejarlo.
grorel
Sin embargo, esa es la opinión de un hombre. Hay millones de programadores que están completamente de acuerdo con declarar más de una variable por línea y hacerlo diariamente como parte de sus trabajos. No puede ocultar a los estudiantes de formas alternativas de hacer las cosas, es mejor mostrarles todas las alternativas y dejar que decidan de qué manera quieren hacer las cosas porque si alguna vez se vuelven empleados, se espera que sigan un cierto estilo que pueden o no sentirse cómodos con ellos. Para un programador, la versatilidad es un rasgo muy bueno.
Pharap
1
Estoy de acuerdo con @grorel. Es más fácil pensar *como parte del tipo y simplemente desalentarlo int* a, b. A menos que prefiera decir que *aes de tipo intmás que aun puntero a int...
Kevin Ushey
@grorel tiene razón: int *a, b;no debe usarse. Declarar dos variables con diferentes tipos en la misma declaración es una práctica bastante pobre y un buen candidato para problemas de mantenimiento en el futuro. Quizás sea diferente para aquellos de nosotros que trabajamos en el campo incrustado, donde an int*y a intmenudo son de diferentes tamaños y, a veces, se almacenan en ubicaciones de memoria completamente diferentes. Es uno de los muchos aspectos del lenguaje C que se enseñaría mejor como 'está permitido, pero no lo hagas'.
Pastel de perro malvado
5
int *bar = &foo;

Question 1: ¿Qué es bar?

Ans: Es una variable de puntero (para escribir int). Un puntero debe apuntar a una ubicación de memoria válida y luego debe desreferenciarse (* barra) utilizando un operador unario *para leer el valor almacenado en esa ubicación.

Question 2: ¿Qué es &foo?

Ans: foo es una variable de tipo int.que se almacena en una ubicación de memoria válida y esa ubicación la obtenemos del operador, &por lo que ahora tenemos una ubicación de memoria válida &foo.

Entonces, ambos juntos, es decir, lo que el puntero necesitaba era una ubicación de memoria válida y eso se consigue &foopara que la inicialización sea buena.

Ahora el puntero barapunta a una ubicación de memoria válida y el valor almacenado en él se puede obtener haciendo referencia a ella, es decir*bar

Gopi
fuente
5

Debe señalar a un principiante que * tiene un significado diferente en la declaración y la expresión. Como sabe, * en la expresión es un operador unario, y * En la declaración no es un operador y solo un tipo de sintaxis que se combina con el tipo para que el compilador sepa que es un tipo de puntero. es mejor decir un principiante, "* tiene un significado diferente. Para comprender el significado de *, debe encontrar dónde se usa *"

Yongkil Kwon
fuente
4

Creo que el diablo está en el espacio.

Escribiría (no solo para el principiante, sino también para mí): int * bar = & foo; en lugar de int * bar = & foo;

Esto debería hacer evidente cuál es la relación entre la sintaxis y la semántica

rpaulin56
fuente
4

Ya se señaló que * tiene múltiples roles.

Hay otra idea simple que puede ayudar a un principiante a comprender las cosas:

Piensa que "=" también tiene múltiples roles.

Cuando la asignación se usa en la misma línea con la declaración, piense en ella como una llamada de constructor, no como una asignación arbitraria.

Cuando veas:

int *bar = &foo;

Piensa que es casi equivalente a:

int *bar(&foo);

Los paréntesis tienen prioridad sobre el asterisco, por lo que "& foo" se atribuye intuitivamente mucho más fácilmente a "bar" que a "* bar".

morfizm
fuente
4

Vi esta pregunta hace unos días, y luego leí la explicación de la declaración de tipo de Go en el Blog de Go . Comienza dando una cuenta de las declaraciones de tipo C, lo que parece un recurso útil para agregar a este hilo, aunque creo que ya hay respuestas más completas.

C adoptó un enfoque inusual e inteligente para la sintaxis de declaración. En lugar de describir los tipos con sintaxis especial, uno escribe una expresión que involucra el elemento que se declara y establece qué tipo tendrá esa expresión. Así

int x;

declara que x es un int: la expresión 'x' tendrá el tipo int. En general, para descubrir cómo escribir el tipo de una nueva variable, escriba una expresión que involucre esa variable que se evalúe como un tipo básico, luego coloque el tipo básico a la izquierda y la expresión a la derecha.

Así, las declaraciones

int *p;
int a[3];

declare que p es un puntero a int porque '* p' tiene el tipo int, y que a es una matriz de int porque a [3] (ignorando el valor de índice particular, que se castiga como el tamaño de la matriz) tiene tipo En t.

(Continúa describiendo cómo extender esta comprensión a los punteros de función, etc.)

Esta es una forma en que no lo había pensado antes, pero parece una forma bastante directa de explicar la sobrecarga de la sintaxis.

Andy Turner
fuente
3

Si el problema es la sintaxis, puede ser útil mostrar un código equivalente con plantilla / uso.

template<typename T>
using ptr = T*;

Esto se puede usar como

ptr<int> bar = &foo;

Después de eso, compare la sintaxis normal / C con este enfoque de C ++ solamente. Esto también es útil para explicar los punteros constantes.

MI3Guy
fuente
2
Para los principiantes será mucho más confuso.
Karsten
Pensé que no mostrarías la definición de ptr. Solo úselo para declaraciones de puntero.
MI3Guy
3

La fuente de confusión surge del hecho de que el *símbolo puede tener diferentes significados en C, dependiendo del hecho en el que se usa. Para explicar el puntero a un principiante, se *debe explicar el significado del símbolo en un contexto diferente.

En la declaración

int *bar = &foo;  

El *símbolo no es el operador de indirección . En cambio, ayuda a especificar el tipo de barinformación al compilador que bares un puntero a unint . Por otro lado, cuando aparece en una declaración, el *símbolo (cuando se usa como operador unario ) realiza una indirecta. Por lo tanto, la declaración

*bar = &foo;

estaría mal ya que asigna la dirección de fooal objeto que barapunta, no a barsí mismo.

hacks
fuente
3

"tal vez escribirlo como int * bar hace que sea más obvio que la estrella es en realidad parte del tipo, no parte del identificador". Así que hago. Y digo que es algo así como Type, pero solo para un nombre de puntero.

"Por supuesto, esto te lleva a diferentes problemas con cosas poco intuitivas como int * a, b".

Павел Бивойно
fuente
2

Aquí debe usar, comprender y explicar la lógica del compilador, no la lógica humana (lo sé, usted es un humano, pero aquí debe imitar la computadora ...).

Cuando escribes

int *bar = &foo;

el compilador agrupa eso como

{ int * } bar = &foo;

Es decir: aquí hay una nueva variable, su nombre es bar, su tipo apunta a int y su valor inicial es &foo.

Y debe agregar: los =denota anteriores no una inicialización una afectación, mientras que en las expresiones siguientes *bar = 2;que es una afectación

Editar por comentario:

Cuidado: en caso de declaración múltiple, *solo está relacionado con la siguiente variable:

int *bar = &foo, b = 2;

bar es un puntero a int inicializado por la dirección de foo, b es un int inicializado a 2, y en

int *bar=&foo, **p = &bar;

barra en puntero fijo a int, y p es un puntero a un puntero a un int inicializado a la dirección o barra.

Serge Ballesta
fuente
2
En realidad, el compilador no lo agrupa así: int* a, b;declara que a es un puntero a an int, pero b es un an int. El *símbolo solo tiene dos significados distintos: en una declaración, indica un tipo de puntero, y en una expresión es el operador de desreferencia unario.
tmlen
@tmlen: Lo que quise decir es que en la inicialización, se *ajusta al tipo, de modo que el puntero se inicializa mientras que en una afectación se ve afectado el valor en punta. Pero al menos me diste un buen sombrero :-)
Serge Ballesta
0

Básicamente, el puntero no es una indicación de matriz. El principiante piensa fácilmente que el puntero parece una matriz. la mayoría de los ejemplos de cadenas que usan el

"char * pstr" es similar parece

"char str [80]"

Pero, cosas importantes, el puntero se trata como un número entero en el nivel inferior del compilador.

Veamos ejemplos ::

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char **argv, char **env)
{
    char str[] = "This is Pointer examples!"; // if we assume str[] is located in 0x80001000 address

    char *pstr0 = str;   // or this will be using with
    // or
    char *pstr1 = &str[0];

    unsigned int straddr = (unsigned int)pstr0;

    printf("Pointer examples: pstr0 = %08x\n", pstr0);
    printf("Pointer examples: &str[0] = %08x\n", &str[0]);
    printf("Pointer examples: str = %08x\n", str);
    printf("Pointer examples: straddr = %08x\n", straddr);
    printf("Pointer examples: str[0] = %c\n", str[0]);

    return 0;
}

A los resultados les gustará esto 0x2a6b7ed0 es la dirección de str []

~/work/test_c_code$ ./testptr
Pointer examples: pstr0 = 2a6b7ed0
Pointer examples: &str[0] = 2a6b7ed0
Pointer examples: str = 2a6b7ed0
Pointer examples: straddr = 2a6b7ed0
Pointer examples: str[0] = T

Entonces, básicamente, tenga en cuenta que Pointer es una especie de número entero. presentando la dirección.

cpplover - Slw Essencial
fuente
-1

Explicaría que los ints son objetos, como flotadores, etc. Un puntero es un tipo de objeto cuyo valor representa una dirección en la memoria (de ahí que el puntero por defecto sea NULL).

Cuando declara un puntero por primera vez, usa la sintaxis de tipo-puntero-nombre. Se lee como un "nombre llamado puntero entero que puede apuntar a la dirección de cualquier objeto entero". Solo usamos esta sintaxis durante la declinación, de forma similar a cómo declaramos un int como 'int num1' pero solo usamos 'num1' cuando queremos usar esa variable, no 'int num1'.

int x = 5; // un objeto entero con un valor de 5

int * ptr; // un entero con un valor de NULL por defecto

Para hacer que un puntero apunte a la dirección de un objeto, usamos el símbolo '&' que se puede leer como "la dirección de".

ptr = & x; // ahora el valor es la dirección de 'x'

Como el puntero es solo la dirección del objeto, para obtener el valor real contenido en esa dirección debemos usar el símbolo '*' que cuando se usa antes de un puntero significa "el valor en la dirección señalada por".

std :: cout << * ptr; // imprime el valor en la dirección

Puede explicar brevemente que ' ' es un 'operador' que devuelve resultados diferentes con diferentes tipos de objetos. Cuando se usa con un puntero, el operador ' ' ya no significa "multiplicado por".

Ayuda a dibujar un diagrama que muestre cómo una variable tiene un nombre y un valor y un puntero tiene una dirección (el nombre) y un valor y muestra que el valor del puntero será la dirección del int.

usuario2796283
fuente
-1

Un puntero es solo una variable utilizada para almacenar direcciones.

La memoria en una computadora está compuesta de bytes (un byte consta de 8 bits) dispuestos de forma secuencial. Cada byte tiene un número asociado, al igual que el índice o el subíndice en una matriz, que se llama la dirección del byte. La dirección del byte comienza de 0 a uno menos que el tamaño de la memoria. Por ejemplo, digamos en 64 MB de RAM, hay 64 * 2 ^ 20 = 67108864 bytes. Por lo tanto, la dirección de estos bytes comenzará de 0 a 67108863.

ingrese la descripción de la imagen aquí

Veamos qué sucede cuando declaras una variable.

marcas int;

Como sabemos, un int ocupa 4 bytes de datos (suponiendo que estamos usando un compilador de 32 bits), por lo que el compilador reserva 4 bytes consecutivos de la memoria para almacenar un valor entero. La dirección del primer byte de los 4 bytes asignados se conoce como la dirección de las marcas variables. Digamos que la dirección de 4 bytes consecutivos son 5004, 5005, 5006 y 5007, entonces la dirección de las marcas variables será 5004. ingrese la descripción de la imagen aquí

Declarar variables de puntero

Como ya se dijo, un puntero es una variable que almacena una dirección de memoria. Al igual que cualquier otra variable, primero debe declarar una variable de puntero antes de poder usarla. Así es como puede declarar una variable de puntero.

Sintaxis: data_type *pointer_name;

data_type es el tipo de puntero (también conocido como el tipo base del puntero). pointer_name es el nombre de la variable, que puede ser cualquier identificador C válido.

Tomemos algunos ejemplos:

int *ip;

float *fp;

int * ip significa que ip es una variable de puntero capaz de apuntar a variables de tipo int. En otras palabras, una variable de puntero ip puede almacenar la dirección de variables de tipo int solamente. Del mismo modo, la variable de puntero fp solo puede almacenar la dirección de una variable de tipo flotante. El tipo de variable (también conocido como tipo base) ip es un puntero a int y el tipo de fp es un puntero a flotante. Una variable de puntero de tipo puntero a int puede representarse simbólicamente como (int *). Del mismo modo, una variable de puntero de tipo puntero para flotar se puede representar como (float *)

Después de declarar una variable de puntero, el siguiente paso es asignarle una dirección de memoria válida. Nunca debe usar una variable de puntero sin asignarle una dirección de memoria válida, porque justo después de la declaración contiene un valor de basura y puede estar apuntando a cualquier parte de la memoria. El uso de un puntero no asignado puede dar un resultado impredecible. Incluso puede hacer que el programa se bloquee.

int *ip, i = 10;
float *fp, f = 12.2;

ip = &i;
fp = &f;

Fuente: thecguru es, con mucho, la explicación más simple pero detallada que he encontrado.

Cody
fuente