¡Qué pregunta tan provocativa!
Incluso el escaneo superficial de las respuestas y comentarios en este hilo revelará cuán emotiva resulta ser su consulta aparentemente simple y directa.
No debería ser sorprendente.
Indiscutiblemente, los malentendidos sobre el concepto y el uso de punteros representan una causa predominante de fallas graves en la programación en general.
El reconocimiento de esta realidad es evidente en la ubicuidad de los idiomas diseñados específicamente para abordar, y preferiblemente para evitar los desafíos que los punteros presentan por completo. Piense en C ++ y otros derivados de C, Java y sus relaciones, Python y otros scripts, simplemente como los más prominentes y prevalentes, y más o menos ordenados en severidad al tratar el problema.
Desarrollar una comprensión más profunda de los principios subyacentes, por lo tanto, debe ser pertinente para cada individuo que aspira a la excelencia en la programación, especialmente a nivel de sistemas .
Me imagino que esto es precisamente lo que tu maestro quiere demostrar.
Y la naturaleza de C lo convierte en un vehículo conveniente para esta exploración. Menos claro que el ensamblaje, aunque quizás más fácilmente comprensible, y aún mucho más explícitamente que los lenguajes basados en una abstracción más profunda del entorno de ejecución.
Diseñado para facilitar la traducción determinista de la intención del programador en instrucciones que las máquinas pueden comprender, C es un lenguaje de nivel de sistema . Si bien se clasifica como de alto nivel, realmente pertenece a una categoría 'mediana'; pero como no existe ninguno, la designación de 'sistema' tiene que ser suficiente.
Esta característica es en gran parte responsable de convertirlo en un idioma de elección para los controladores de dispositivos , el código del sistema operativo y las implementaciones integradas . Además, una alternativa merecidamente favorecida en aplicaciones donde la eficiencia óptima es primordial; donde eso significa la diferencia entre supervivencia y extinción, y por lo tanto es una necesidad en lugar de un lujo. En tales casos, la conveniencia atractiva de la portabilidad pierde todo su atractivo, y optar por el rendimiento sin brillo del mínimo común denominador se convierte en una opción impensablemente perjudicial .
Lo que hace que C, y algunos de sus derivados, sean bastante especiales, es que permite a sus usuarios un control total , cuando eso es lo que desean, sin imponerles las responsabilidades relacionadas cuando no lo hacen. Sin embargo, nunca ofrece más que los aislamientos más delgados de la máquina , por lo que el uso adecuado exige una comprensión precisa del concepto de punteros .
En esencia, la respuesta a su pregunta es sublimemente simple y satisfactoriamente dulce, en confirmación de sus sospechas. Siempre que , sin embargo, se atribuya la importancia necesaria a cada concepto en esta declaración:
- Los actos de examinar, comparar y manipular punteros son siempre y necesariamente válidos, mientras que las conclusiones derivadas del resultado dependen de la validez de los valores contenidos, y por lo tanto no es necesario.
El primero es tanto siempre segura y potencialmente adecuado , mientras que el segundo tan sólo puede ser adecuada cuando ha sido establecida como segura . Sorprendentemente , para algunos, entonces establecer la validez de este último depende y exige lo primero.
Por supuesto, parte de la confusión surge del efecto de la recursividad inherentemente presente dentro del principio de un puntero, y los desafíos que se presentan al diferenciar el contenido de la dirección.
Has supuesto correctamente ,
Me llevan a pensar que cualquier puntero se puede comparar con cualquier otro puntero, independientemente de dónde apunten individualmente. Además, creo que la aritmética de puntero entre dos punteros está bien, sin importar dónde apunten individualmente porque la aritmética solo usa las direcciones de memoria que almacenan los punteros.
Y varios contribuyentes han afirmado: los punteros son solo números. A veces algo más cercano a los números complejos , pero todavía no más que los números.
La acritud divertida en la que se ha recibido esta afirmación aquí revela más sobre la naturaleza humana que la programación, pero sigue siendo digna de mención y elaboración. Quizás lo hagamos más tarde ...
Como un comentario comienza a insinuar; Toda esta confusión y consternación deriva de la necesidad de discernir lo que es válido de lo que es seguro , pero eso es una simplificación excesiva. También debemos distinguir qué es funcional y qué es confiable , qué es práctico y qué puede ser apropiado , y aún más: lo que es apropiado en una circunstancia particular de lo que puede ser apropiado en un sentido más general . Por no mencionar; La diferencia entre conformidad y propiedad .
Con ese fin, en primer lugar hay que apreciar precisamente lo que un puntero es .
- Usted ha demostrado un firme control sobre el concepto, y como algunos otros pueden encontrar estas ilustraciones condescendientemente simplistas, pero el nivel de confusión evidente aquí exige tal simplicidad en la aclaración.
Como varios han señalado: el término puntero es simplemente un nombre especial para lo que es simplemente un índice y, por lo tanto, nada más que cualquier otro número .
Esto ya debería ser evidente teniendo en cuenta el hecho de que todas las computadoras convencionales contemporáneas son máquinas binarias que necesariamente funcionan exclusivamente con y sobre números . La computación cuántica puede cambiar eso, pero es muy poco probable y no ha alcanzado la mayoría de edad.
Técnicamente, como ha notado, los punteros son direcciones más precisas ; Una idea obvia que introduce naturalmente la gratificante analogía de correlacionarlos con las 'direcciones' de casas o parcelas en una calle.
En un modelo de memoria plana : toda la memoria del sistema está organizada en una sola secuencia lineal: todas las casas de la ciudad se encuentran en la misma carretera, y cada casa se identifica de manera única por su número. Deliciosamente simple.
En esquemas segmentados : se introduce una organización jerárquica de carreteras numeradas por encima de las casas numeradas para que se requieran direcciones compuestas.
- Algunas implementaciones son aún más complicadas, y la totalidad de 'caminos' distintos no necesita sumar una secuencia contigua, pero nada de eso cambia nada sobre el subyacente.
- Estamos necesariamente en condiciones de descomponer cada enlace jerárquico en una organización plana. Cuanto más compleja sea la organización, más obstáculos tendremos que superar para hacerlo, pero debe ser posible. De hecho, esto también se aplica al 'modo real' en x86.
- De lo contrario, la asignación de enlaces a ubicaciones no sería biyectiva , ya que una ejecución confiable, a nivel del sistema, exige que DEBE serlo.
- múltiples direcciones no deben mapearse a ubicaciones de memoria singulares, y
- las direcciones singulares nunca deben asignarse a múltiples ubicaciones de memoria.
Llevándonos al giro adicional que convierte el enigma en una maraña tan fascinantemente complicada . Arriba, era conveniente sugerir que los punteros son direcciones, en aras de la simplicidad y la claridad. Por supuesto, esto no es correcto. Un puntero no es una dirección; un puntero es una referencia a una dirección , contiene una dirección . Al igual que el sobre tiene una referencia a la casa. Contemplar esto puede llevarlo a vislumbrar lo que se entiende con la sugerencia de recursión contenida en el concepto. Todavía; tenemos pocas palabras y hablamos de las direcciones de referencias a direccionesy tal, pronto detiene la mayoría de los cerebros en una excepción de código de operación no válida . Y en su mayor parte, la intención se obtiene fácilmente del contexto, así que volvamos a la calle.
Los trabajadores postales en esta ciudad imaginaria nuestra son muy parecidos a los que encontramos en el mundo "real". Es probable que nadie sufra un derrame cerebral cuando habla o pregunta acerca de una dirección no válida , pero cada uno de ellos se negará cuando les pida que actúen sobre esa información.
Supongamos que solo hay 20 casas en nuestra singular calle. Además, finja que un alma disléxica o equivocada ha dirigido una carta, una muy importante, al número 71. Ahora, podemos preguntarle a nuestro transportista Frank, si existe esa dirección, y él informará de manera simple y tranquila: no . Incluso podemos esperar que él para estimar hasta qué punto fuera de la calle esta ubicación sería mentir si lo hizo existe: aproximadamente 2,5 veces mayor que el final. Nada de esto le causará exasperación. Sin embargo, si tuviéramos que pedirle que entregue esta carta, o que recoja un artículo de ese lugar, es probable que sea bastante franco sobre su descontento y su negativa a cumplir.
Los punteros son solo direcciones, y las direcciones son solo números.
Verifique el resultado de lo siguiente:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
Llámalo a todos los punteros que quieras, válidos o no. Por favor, no publicar sus hallazgos si falla en su plataforma, o su (contemporánea) compilador se queja.
Ahora, debido a que los punteros son simplemente números, es inevitablemente válido compararlos. En cierto sentido, esto es precisamente lo que su maestro está demostrando. Todas las siguientes afirmaciones son perfectamente válidas , ¡y adecuadas! - C, y cuando se compila se ejecutará sin encontrar problemas , aunque ninguno de los punteros necesita inicializarse y los valores que contienen pueden ser indefinidos :
- Solo estamos calculando
result
explícitamente en aras de la claridad , e imprimiéndolo para obligar al compilador a calcular lo que de otro modo sería un código muerto redundante.
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
Por supuesto, el programa está mal formado cuando aob no está definido (léase: no se inicializó correctamente ) en el punto de prueba, pero eso es completamente irrelevante para esta parte de nuestra discusión. Estos fragmentos, al igual que las siguientes afirmaciones, están garantizados , por el 'estándar', para compilar y ejecutarse sin problemas, a pesar de la validez IN de cualquier puntero involucrado.
Los problemas solo surgen cuando se desreferencia un puntero no válido . Cuando le pedimos a Frank que recoja o entregue en la dirección no válida e inexistente.
Dado cualquier puntero arbitrario:
int *p;
Si bien esta declaración debe compilar y ejecutar:
printf(“%p”, p);
... como debe ser esto:
size_t foo( int *p ) { return (size_t)p; }
... los dos siguientes, en marcado contraste, aún se compilarán fácilmente, pero fallarán en la ejecución a menos que el puntero sea válido , con lo que aquí solo queremos decir que hace referencia a una dirección a la que se ha otorgado acceso a la presente aplicación :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
¿Qué tan sutil es el cambio? La distinción radica en la diferencia entre el valor del puntero, que es la dirección, y el valor de los contenidos: de la casa en ese número. No surge ningún problema hasta que se desreferencia el puntero ; hasta que se intente acceder a la dirección a la que se vincula. Al tratar de entregar o recoger el paquete más allá del tramo de la carretera ...
Por extensión, el mismo principio se aplica necesariamente a ejemplos más complejos, incluida la necesidad antes mencionada de establecer la validez requerida:
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
La comparación relacional y la aritmética ofrecen una utilidad idéntica a la equivalencia de prueba, y son igualmente válidas, en principio. Sin embargo , lo que significarían los resultados de tal cálculo es un asunto completamente diferente, y precisamente el problema abordado por las citas que incluyó.
En C, una matriz es un búfer contiguo, una serie lineal ininterrumpida de ubicaciones de memoria. La comparación y la aritmética aplicada a los punteros de que las ubicaciones de referencia dentro de una serie tan singular son naturalmente, y obviamente significativas en relación tanto entre sí como con esta 'matriz' (que simplemente se identifica por la base). Precisamente, lo mismo se aplica a cada bloque asignado a través de malloc
, o sbrk
. Debido a que estas relaciones son implícitas , el compilador puede establecer relaciones válidas entre ellas y, por lo tanto, puede estar seguro de que los cálculos proporcionarán las respuestas anticipadas.
Realizar gimnasia similar en punteros que hacen referencia a bloques o matrices distintos no ofrece ninguna utilidad inherente y aparente . Más aún, ya que cualquier relación que exista en un momento puede ser invalidada por una reasignación que sigue, en la que es muy probable que cambie, incluso se invierta. En tales casos, el compilador no puede obtener la información necesaria para establecer la confianza que tenía en la situación anterior.
¡Usted , sin embargo, como programador, puede tener tal conocimiento! Y en algunos casos están obligados a explotar eso.
Hay SON , por lo tanto, las circunstancias en las que incluso esto es totalmente VÁLIDO y perfectamente ADECUADO.
De hecho, eso es exactamente lo que malloc
tiene que hacer internamente cuando llega el momento de intentar fusionar bloques recuperados, en la gran mayoría de las arquitecturas. Lo mismo es cierto para el asignador del sistema operativo, como eso detrás sbrk
; si es más obvio , con frecuencia , en entidades más dispares , más críticamente , y relevante también en plataformas donde esto malloc
puede no ser. ¿Y cuántos de esos no están escritos en C?
La validez, seguridad y éxito de una acción es inevitablemente la consecuencia del nivel de conocimiento sobre el cual se basa y aplica.
En las citas que ha ofrecido, Kernighan y Ritchie están abordando un tema estrechamente relacionado, pero no obstante separado. Están definiendo las limitaciones del lenguaje y explicando cómo puede explotar las capacidades del compilador para protegerlo al menos al detectar construcciones potencialmente erróneas. Describen las longitudes que puede alcanzar el mecanismo , está diseñado, para ayudarlo en su tarea de programación. El compilador es tu servidor, tú eres el maestro. Sin embargo, un maestro sabio es uno que está íntimamente familiarizado con las capacidades de sus diversos sirvientes.
Dentro de este contexto, el comportamiento indefinido sirve para indicar peligro potencial y la posibilidad de daño; para no implicar una condena inminente e irreversible, o el fin del mundo tal como lo conocemos. Simplemente significa que nosotros - 'es decir, el compilador' - no podemos hacer ninguna conjetura sobre lo que esto puede ser, o representar, y por esta razón elegimos lavarnos las manos al respecto. No seremos responsables por cualquier desventura que pueda resultar del uso o mal uso de esta instalación .
En efecto, simplemente dice: "Más allá de este punto, vaquero : estás solo ..."
Su profesor está tratando de demostrarle los mejores matices .
Observe el gran cuidado que han tomado al elaborar su ejemplo; y cómo quebradizo que todavía es. Al tomar la dirección de a
, en
p[0].p0 = &a;
el compilador se ve obligado a asignar almacenamiento real para la variable, en lugar de colocarlo en un registro. Sin embargo, al ser una variable automática, el programador no tiene control sobre dónde está asignado y, por lo tanto, no puede hacer ninguna conjetura válida sobre lo que le seguiría. Es por eso que a
debe establecerse igual a cero para que el código funcione como se espera.
Simplemente cambiando esta línea:
char a = 0;
a esto:
char a = 1; // or ANY other value than 0
hace que el comportamiento del programa se vuelva indefinido . Como mínimo, la primera respuesta ahora será 1; Pero el problema es mucho más siniestro.
Ahora el código invita al desastre.
Aunque sigue siendo perfectamente válido e incluso se ajusta al estándar , ahora está mal formado y, aunque es seguro que se compila, puede fallar en la ejecución por varios motivos. Por ahora existen múltiples problemas - ninguno de los cuales el compilador es capaz de reconocer.
strcpy
comenzará en la dirección de a
, y continuará más allá de esto para consumir - y transferir - byte tras byte, hasta que encuentre un valor nulo.
El p1
puntero se ha inicializado en un bloque de exactamente 10 bytes.
Si a
se coloca al final de un bloque y el proceso no tiene acceso a lo que sigue, la siguiente lectura, de p0 [1], provocará una segfault. Este escenario es poco probable en la arquitectura x86, pero es posible.
Si a
se puede acceder al área más allá de la dirección de , no se producirá ningún error de lectura, pero el programa aún no se salva de la desgracia.
Si ocurre un byte cero dentro de los diez que comienzan en la dirección de a
, aún puede sobrevivir, ya que entonces strcpy
se detendrá y al menos no sufriremos una violación de escritura.
Si se no criticada por leer mal, pero no hay byte cero se produce en este lapso de 10, strcpy
continuará e intentar escribir más allá del bloque asignado por malloc
.
Si esta área no es propiedad del proceso, la segfault debe activarse inmediatamente.
La situación aún más desastrosa, y sutil , surge cuando el siguiente bloque es propiedad del proceso, ya que entonces el error no se puede detectar, no se puede generar ninguna señal y, por lo tanto, puede "parecer" que todavía "funciona" , mientras que en realidad sobrescribirá otros datos, las estructuras de administración de su asignador o incluso el código (en ciertos entornos operativos).
Esta es la razón por la cual los errores relacionados con el puntero pueden ser tan difíciles de rastrear . Imagine estas líneas enterradas en lo profundo de miles de líneas de código intrincadamente relacionado, que alguien más ha escrito, y se le indica que profundice.
Sin embargo , el programa debe todavía compilar, ya que sigue siendo perfectamente válido y conformes estándar C.
Este tipo de errores, ningún estándar y ningún compilador pueden proteger a los incautos. Me imagino que eso es exactamente lo que pretenden enseñarte.
Las personas paranoicas constantemente buscan cambiar la naturaleza de C para deshacerse de estas posibilidades problemáticas y así salvarnos de nosotros mismos; Pero eso es falso . Esta es la responsabilidad que estamos obligados a aceptar cuando elegimos perseguir el poder y obtener la libertad que nos ofrece un control más directo e integral de la máquina. Los promotores y perseguidores de la perfección en el rendimiento nunca aceptarán nada menos.
La portabilidad y la generalidad que representa es una consideración fundamentalmente separada y todo lo que el estándar busca abordar:
Este documento especifica la forma y establece la interpretación de los programas expresados en el lenguaje de programación C. Su propósito es promover la portabilidad , confiabilidad, mantenibilidad y ejecución eficiente de programas en lenguaje C en una variedad de sistemas informáticos .
Es por eso que es perfectamente apropiado mantenerlo distinto de la definición y especificación técnica del lenguaje en sí. Contrariamente a lo que muchos creen que la generalidad es antitética a excepcional y ejemplar .
Para concluir:
- Examinar y manipular los punteros mismos es invariablemente válido y, a menudo, fructífero . La interpretación de los resultados puede o no ser significativa, pero nunca se invita a la calamidad hasta que se desreferencia el puntero ; hasta que se intente acceder a la dirección vinculada.
Si esto no fuera cierto, la programación tal como la conocemos , y nos encanta, no hubiera sido posible.
C
con lo que es seguro enC
. Sin embargo, siempre se puede comparar dos punteros con el mismo tipo (verificando la igualdad, por ejemplo), utilizando la aritmética de punteros y la comparación,>
y<
solo es seguro cuando se usa dentro de una matriz dada (o bloque de memoria).