¿Cómo funciona la comparación de punteros en C? ¿Está bien comparar punteros que no apuntan a la misma matriz?

33

En K&R (The C Programming Language 2nd Edition) capítulo 5 leí lo siguiente:

Primero, los punteros pueden compararse bajo ciertas circunstancias. Si py qpunto a los miembros de la misma matriz, entonces, como las relaciones ==, !=, <, >=, etc trabajo correctamente.

Lo que parece implicar que solo se pueden comparar los punteros que apuntan a la misma matriz.

Sin embargo, cuando probé este código

    char t = 't';
    char *pt = &t;
    char x = 'x';
    char *px = &x;

    printf("%d\n", pt > px);

1 se imprime en la pantalla.

En primer lugar, pensé que quedaría indefinido o algún tipo o error, porque pty pxno estoy apuntando a la misma matriz (al menos en mi entendimiento).

También se pt > pxdebe a que ambos punteros apuntan a variables almacenadas en la pila, y la pila crece, por lo que la dirección de memoria de tes mayor que la de x? ¿Por qué pt > pxes verdad?

Me confundo más cuando aparece Malloc. También en K&R en el capítulo 8.7 está escrito lo siguiente:

Sin embargo, todavía existe una suposición de que los punteros a diferentes bloques devueltos por sbrkse pueden comparar significativamente. Esto no está garantizado por el estándar que permite comparaciones de punteros solo dentro de una matriz. Por lo tanto, esta versión de malloces portátil solo entre máquinas para las cuales la comparación general de punteros es significativa.

No tuve problema en comparar punteros que apuntaban al espacio mal colocado en el montón con punteros que apuntaban a variables de pila.

Por ejemplo, el siguiente código funcionó bien, al 1imprimirse:

    char t = 't';
    char *pt = &t;
    char *px = malloc(10);
    strcpy(px, pt);
    printf("%d\n", pt > px);

Basado en mis experimentos con mi compilador, 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.

Aún así, estoy confundido por lo que estoy leyendo en K&R.

La razón por la que pregunto es porque mi prof. en realidad lo hizo una pregunta de examen. Dio el siguiente código:

struct A {
    char *p0;
    char *p1;
};

int main(int argc, char **argv) {
    char a = 0;
    char *b = "W";
    char c[] = [ 'L', 'O', 'L', 0 ];

   struct A p[3];
    p[0].p0 = &a;
    p[1].p0 = b;
    p[2].p0 = c;

   for(int i = 0; i < 3; i++) {
        p[i].p1 = malloc(10);
        strcpy(p[i].p1, p[i].p0);
    }
}

¿Qué evalúan estos para:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

La respuesta es 0, 1y 0.

(Mi profesor incluye el descargo de responsabilidad en el examen de que las preguntas son para un entorno de programación Ubuntu Linux 16.04, versión de 64 bits)

(Nota del editor: si SO permitiera más etiquetas, esa última parte garantizaría , y tal vez . Si el punto de la pregunta / clase era específicamente detalles de implementación de SO de bajo nivel, en lugar de C. portátil).

Shisui
fuente
17
Tal vez usted está confundiendo lo que es válido en Ccon lo que es seguro en C. 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).
Adrian Mole
13
Como acotación al margen, usted debe no estar aprendiendo C de K & R. Para empezar, el idioma ha pasado por muchos cambios desde entonces. Y, para ser sincero, el código de ejemplo allí era de una época en que se valoraba la brevedad en lugar de la legibilidad.
paxdiablo
55
No, no se garantiza que funcione. Puede fallar en la práctica en máquinas con modelos de memoria segmentada. Ver ¿Tiene C un equivalente de std :: less de C ++? En la mayoría de las máquinas modernas, funcionará a pesar de UB.
Peter Cordes
66
@Adam: Cierra, pero en realidad es UB (a menos que el compilador que estaba usando el OP, GCC, elija definirlo. Podría). Pero UB no significa "definitivamente explota"; ¡Uno de los posibles comportamientos para UB es trabajar de la manera que esperabas! Esto es lo que hace que UB sea tan desagradable; puede funcionar correctamente en una compilación de depuración y fallar con la optimización habilitada, o viceversa, o romperse según el código circundante. La comparación de otros punteros aún le dará una respuesta, pero el lenguaje no define qué significará esa respuesta (en todo caso). No, se permite estrellarse. Es realmente UB.
Peter Cordes
3
@ Adam: Oh sí, no importa la primera parte de mi comentario, leí mal el tuyo. Pero usted afirma que la comparación de otros punteros aún le dará una respuesta . Eso no es cierto. Ese sería un resultado no especificado , no UB completo. UB es mucho peor y significa que su programa podría segfault o SIGILL si la ejecución alcanza esa declaración con esas entradas (en cualquier momento antes o después de que eso suceda realmente). (Solo plausible en x86-64 si el UB es visible en el momento de la compilación, pero en general cualquier cosa puede suceder). Parte del objetivo del UB es dejar que el compilador haga suposiciones "inseguras" mientras genera asm.
Peter Cordes

Respuestas:

33

De acuerdo con el estándar C11 , los operadores relacionales <, <=, >, y >=sólo pueden ser utilizados en los punteros a los elementos de la misma matriz o un objeto struct. Esto se explica en la sección 6.5.8p5:

Cuando se comparan dos punteros, el resultado depende de las ubicaciones relativas en el espacio de direcciones de los objetos apuntados. Si dos punteros a tipos de objeto apuntan al mismo objeto, o ambos apuntan uno más allá del último elemento del mismo objeto de matriz, se comparan igual. Si los objetos apuntados son miembros del mismo objeto agregado, los punteros a los miembros de estructura declarados más tarde se comparan más que los punteros a los miembros declarados anteriormente en la estructura, y los punteros a elementos de matriz con valores de subíndice más grandes se comparan más que punteros a elementos de la misma matriz con valores de subíndice más bajos. Todos los punteros a miembros del mismo objeto de unión se comparan igual.

Tenga en cuenta que cualquier comparación que no cumpla con este requisito invoca un comportamiento indefinido , lo que significa (entre otras cosas) que no puede depender de que los resultados sean repetibles.

En su caso particular, tanto para la comparación entre las direcciones de dos variables locales como entre la dirección de una dirección local y una dinámica, la operación parecía "funcionar", sin embargo, el resultado podría cambiar al hacer un cambio aparentemente no relacionado a su código o incluso compilar el mismo código con diferentes configuraciones de optimización. Con un comportamiento indefinido, solo porque el código podría fallar o generar un error no significa que lo hará .

Como ejemplo, un procesador x86 que se ejecuta en modo real 8086 tiene un modelo de memoria segmentado que utiliza un segmento de 16 bits y un desplazamiento de 16 bits para construir una dirección de 20 bits. Entonces, en este caso, una dirección no se convierte exactamente en un número entero.

Los operadores de igualdad ==y !=sin embargo no tienen esta restricción. Se pueden usar entre dos punteros a tipos compatibles o punteros NULL. Por lo tanto, usar ==o !=en ambos ejemplos produciría un código C válido.

Sin embargo, incluso con ==y !=podría obtener algunos resultados inesperados pero bien definidos. Consulte ¿Puede una comparación de igualdad de punteros no relacionados evaluar a verdadero? para más detalles sobre esto

Con respecto a la pregunta del examen dada por su profesor, hace una serie de suposiciones erróneas:

  • Existe un modelo de memoria plana donde hay una correspondencia 1 a 1 entre una dirección y un valor entero.
  • Que los valores de puntero convertidos encajan dentro de un tipo entero.
  • Que la implementación simplemente trata los punteros como enteros cuando se realizan comparaciones sin explotar la libertad dada por el comportamiento indefinido.
  • Que se utiliza una pila y que las variables locales se almacenan allí.
  • Ese montón se utiliza para extraer la memoria asignada.
  • Que la pila (y por lo tanto las variables locales) aparece en una dirección más alta que el montón (y, por lo tanto, los objetos asignados).
  • Las constantes de cadena aparecen en una dirección más baja que el montón.

Si tuviera que ejecutar este código en una arquitectura y / o con un compilador que no satisfaga estos supuestos, podría obtener resultados muy diferentes.

Además, ambos ejemplos también exhiben un comportamiento indefinido cuando llaman strcpy, ya que el operando correcto (en algunos casos) apunta a un solo carácter y no a una cadena terminada en nulo, lo que hace que la función lea más allá de los límites de la variable dada.

dbush
fuente
3
@Shisui Incluso teniendo en cuenta eso, aún no debería depender de los resultados. Los compiladores pueden volverse muy agresivos cuando se trata de optimización y utilizarán comportamientos indefinidos como una oportunidad para hacerlo. Es posible que el uso de un compilador diferente y / o configuraciones de optimización diferentes puedan generar resultados diferentes.
dbush
2
@Shisui: En general, funcionará en máquinas con un modelo de memoria plana, como x86-64. Algunos compiladores para tales sistemas pueden incluso definir el comportamiento en su documentación. Pero si no, entonces el comportamiento "loco" puede ocurrir debido a UB visible en tiempo de compilación. (En la práctica, no creo que nadie quiera eso, así que no es algo que los compiladores principales buscan e "intentan romper")
Peter Cordes
1
Al igual que si un compilador ve que una ruta de ejecución conduciría <entre el mallocresultado y una variable local (almacenamiento automático, es decir, pila), podría suponer que la ruta de ejecución nunca se toma y simplemente compila toda la función a una ud2instrucción (plantea un ilegal -excepción de instrucción que manejará el núcleo al entregar una SIGILL al proceso). GCC / clang hace esto en la práctica para otros tipos de UB, como caerse del final de una no voidfunción. godbolt.org está caído en este momento parece, pero intente copiar / pegar int foo(){int x=2;}y tenga en cuenta la falta de unret
Peter Cordes
44
@Shisui: TL: DR: no es C portátil, a pesar del hecho de que funciona bien en Linux x86-64. Sin embargo, hacer suposiciones sobre los resultados de la comparación es una locura. Si no está en el subproceso principal, su pila de subprocesos se asignará dinámicamente utilizando el mismo mecanismo que mallocutiliza para obtener más memoria del sistema operativo, por lo que no hay razón para suponer que sus vars locales (pila de subprocesos) están por encima mallocde la asignación dinámica almacenamiento.
Peter Cordes
2
@PeterCordes: lo que se necesita es reconocer varios aspectos del comportamiento como "opcionalmente definidos", de modo que las implementaciones puedan definirlos o no, a su gusto, pero deben indicar de manera comprobable (por ejemplo, macro predefinida) si no lo hacen. Además, en lugar de caracterizar que cualquier situación en la que los efectos de una optimización serían observables como "Comportamiento indefinido", sería mucho más útil decir que los optimizadores pueden considerar ciertos aspectos del comportamiento como "no observables" si indican que hazlo Por ejemplo, dada int x,y;, una implementación ...
supercat
12

El problema principal al comparar punteros con dos matrices distintas del mismo tipo es que las matrices mismas no necesitan colocarse en una posición relativa particular: una podría terminar antes y después de la otra.

En primer lugar, pensé que quedaría indefinido o algún tipo o error, porque pt un px no apunta a la misma matriz (al menos en mi entendimiento).

No, el resultado depende de la implementación y otros factores impredecibles.

También es pt> px porque ambos punteros apuntan a variables almacenadas en la pila, y la pila crece hacia abajo, ¿entonces la dirección de memoria de t es mayor que la de x? ¿Por qué pt> ​​px es verdadero?

No hay necesariamente una pila . Cuando existe, no necesita crecer hacia abajo. Podría crecer Podría ser no contiguo de alguna manera extraña.

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.

Veamos la especificación C , §6.5.8 en la página 85 que analiza los operadores relacionales (es decir, los operadores de comparación que está utilizando). Tenga en cuenta que esto no se aplica a directa !=o ==comparación.

Cuando se comparan dos punteros, el resultado depende de las ubicaciones relativas en el espacio de direcciones de los objetos apuntados. ... Si los objetos apuntados son miembros del mismo objeto agregado, ... los punteros a elementos de matriz con valores de subíndice más grandes se comparan más que los punteros a elementos de la misma matriz con valores de subíndice más bajos.

En todos los demás casos, el comportamiento es indefinido.

La última oración es importante. Si bien reduzco algunos casos no relacionados para ahorrar espacio, hay un caso que es importante para nosotros: dos matrices, que no forman parte de la misma estructura / objeto agregado 1 , y estamos comparando punteros con esas dos matrices. Este es un comportamiento indefinido .

Si bien su compilador acaba de insertar algún tipo de instrucción de máquina CMP (comparar) que compara numéricamente los punteros, y tuvo suerte aquí, UB es una bestia bastante peligrosa. Literalmente, puede suceder cualquier cosa: su compilador podría optimizar toda la función, incluidos los efectos secundarios visibles. Podría engendrar demonios nasales.

1 Se pueden comparar los punteros en dos matrices diferentes que forman parte de la misma estructura, ya que esto se incluye en la cláusula donde las dos matrices son parte del mismo objeto agregado (la estructura).

nanofaradio
fuente
1
Más importante aún, con ty xsiendo definido en la misma función, no hay razón para suponer nada acerca de cómo un compilador dirigido a x86-64 presentará locales en el marco de la pila para esta función. La pila que crece hacia abajo no tiene nada que ver con el orden de declaración de variables en una función. Incluso en funciones separadas, si una podría alinearse con la otra, los locales de la función "secundaria" aún podrían mezclarse con los padres.
Peter Cordes
1
el compilador podría optimizar cabo toda la función, incluyendo efectos secundarios visibles No es una exageración: para otros tipos de UB (como caerse al final de un no- voidfunción) g ++ y sonido metálico ++ realmente hacer eso en la práctica: godbolt.org/z/g5vesB que suponga que el camino de ejecución no se toma porque conduce a UB, y compile dichos bloques básicos para una instrucción ilegal. O sin instrucciones, simplemente cayendo silenciosamente a cualquier asm que viene después si esa función alguna vez fue llamada. (Por alguna razón gccno hace esto, solo g++).
Peter Cordes
6

Entonces pregunté qué

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Evaluar a. La respuesta es 0, 1 y 0.

Estas preguntas se reducen a:

  1. Es el montón encima o debajo de la pila.
  2. Es el montón arriba o debajo de la sección literal de cadena del programa.
  3. igual que [1].

Y la respuesta a las tres es "implementación definida". Las preguntas de tu profesor son falsas; Lo han basado en el diseño tradicional de Unix:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

pero varias unidades modernas (y sistemas alternativos) no se ajustan a esas tradiciones. A menos que hayan precedido la pregunta con "a partir de 1992"; asegúrese de dar un -1 en la evaluación.

mevets
fuente
3
¡Implementación no definida, indefinida! Piénselo de esta manera, el primero puede variar entre implementaciones, pero las implementaciones deben documentar cómo se decide el comportamiento. El último comportamiento medio puede variar de cualquier manera y la implementación no tener que decirle cuclillas :-)
paxdiablo
1
@paxdiablo: Según los fundamentos de los autores de la Norma, "El comportamiento indefinido ... también identifica áreas de posible extensión de lenguaje conforme: el implementador puede aumentar el lenguaje al proporcionar una definición del comportamiento oficialmente indefinido". La justificación dice además: "El objetivo es dar al programador una oportunidad de luchar para hacer poderosos programas en C que también sean altamente portátiles, sin que parezcan degradar programas en C perfectamente útiles que no sean portátiles, por lo tanto, el adverbio estrictamente". Los escritores de compiladores comerciales entienden esto, pero algunos otros escritores de compiladores no.
supercat
Hay otro aspecto definido de implementación; la comparación del puntero está firmada , por lo que dependiendo de la máquina / os / compilador, algunas direcciones pueden interpretarse como negativas. Por ejemplo, una máquina de 32 bits que colocó la pila a 0xc << 28, probablemente mostraría las variables automáticas en una dirección menor que el montón o la rodata.
Mevets
1
@mevets: ¿Especifica el Estándar alguna situación en la cual la firma de los punteros en las comparaciones sería observable? Esperaría que si una plataforma de 16 bits permite objetos mayores de 32768 bytes, y arr[]es un objeto así, el Estándar exigiría una arr+32768comparación mayor que arrincluso si una comparación de puntero firmada informara lo contrario.
supercat
No lo sé; El estándar C está orbitando en el noveno círculo de Dante, rezando por la eutanasia. El OP hizo referencia específica a K&R y una pregunta de examen. #UB es escombros de un grupo de trabajo perezoso.
Mevets
1

En casi cualquier plataforma remotamente moderna, los punteros y los enteros tienen una relación de ordenamiento isomorfo, y los punteros a objetos disjuntos no están intercalados. La mayoría de los compiladores exponen este orden a los programadores cuando las optimizaciones están deshabilitadas, pero el Estándar no hace distinción entre las plataformas que tienen ese orden y las que no lo hacen y no requiere que ninguna implementación exponga tal orden al programador incluso en plataformas que lo harían. definirlo En consecuencia, algunos escritores de compiladores realizan varios tipos de optimizaciones y "optimizaciones" basadas en la suposición de que el código nunca comparará el uso de operadores relacionales en punteros con diferentes objetos.

Según la justificación publicada, los autores del Estándar pretendían que las implementaciones extiendan el lenguaje al especificar cómo se comportarán en situaciones que el Estándar caracteriza como "Comportamiento indefinido" (es decir, donde el Estándar no impone requisitos ) cuando hacerlo sería útil y práctico. , pero algunos escritores de compiladores preferirían asumir que los programas nunca intentarán beneficiarse de algo más allá de lo que exige el Estándar, que permitir que los programas exploten de manera útil los comportamientos que las plataformas podrían soportar sin costo adicional.

No conozco ningún compilador diseñado comercialmente que haga algo extraño con las comparaciones de punteros, pero a medida que los compiladores se trasladan al LLVM no comercial para su back-end, es cada vez más probable que procesen código sin sentido cuyo comportamiento había sido especificado anteriormente compiladores para sus plataformas. Tal comportamiento no se limita a operadores relacionales, sino que incluso puede afectar la igualdad / desigualdad. Por ejemplo, a pesar de que el Estándar especifica que una comparación entre un puntero a un objeto y un puntero "justo pasado" a un objeto inmediatamente anterior comparará los compiladores basados ​​en gcc y LLVM que son propensos a generar código sin sentido si los programas realizan tal comparaciones

Como ejemplo de una situación en la que incluso la comparación de igualdad se comporta sin sentido en gcc y clang, considere:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Tanto clang como gcc generarán código que siempre devolverá 4 incluso si xson diez elementos, yinmediatamente lo sigue y ies cero, lo que da como resultado que la comparación sea verdadera y p[0]se escriba con el valor 1. Creo que lo que sucede es que una pasada de optimización se reescribe la función como si *p = 1;fuera reemplazada por x[10] = 1;. El último código sería equivalente si el compilador lo interpretara *(x+10)como equivalente *(y+i), pero desafortunadamente una etapa de optimización x[10]posterior reconoce que un acceso a solo se definiría si xtuviera al menos 11 elementos, lo que haría imposible que ese acceso se vea afectado y.

Si los compiladores pueden obtener esa "creatividad" con el escenario de igualdad de puntero que describe el Estándar, no confiaría en que se abstengan de ser aún más creativos en los casos en que el Estándar no imponga requisitos.

Super gato
fuente
0

Es simple: comparar punteros no tiene sentido ya que nunca se garantiza que las ubicaciones de memoria para los objetos estén en el mismo orden en que las declaró. La excepción son las matrices. & array [0] es menor que & array [1]. Eso es lo que K&R señala. En la práctica, las direcciones de los miembros de struct también están en el orden en que las declara en mi experiencia. No hay garantías sobre eso ... Otra excepción es si compara un puntero por igual. Cuando un puntero es igual a otro, sabes que está apuntando al mismo objeto. Lo que sea que es. Mala pregunta de examen si me preguntas. Dependiendo de Ubuntu Linux 16.04, entorno de programación de la versión de 64 bits para una pregunta de examen? De Verdad ?

Hans Lepoeter
fuente
Técnicamente, las matrices no son realmente una excepción, ya que no se declara arr[0], arr[1]etc por separado. Usted declara arrcomo un todo, por lo que el orden de los elementos de la matriz individual es un problema diferente al descrito en esta pregunta.
paxdiablo
1
Los elementos de la estructura están garantizados para estar en orden, lo que garantiza que uno puede usar memcpypara copiar una parte contigua de una estructura y afectar a todos los elementos de la misma y no afectar a nada más. El estándar es descuidado sobre la terminología en cuanto a qué tipos de aritmética de puntero se pueden hacer con estructuras o malloc()almacenamiento asignado. La offsetofmacro sería bastante inútil si no se pudiera utilizar el mismo tipo de aritmética de puntero con los bytes de una estructura que con a char[], pero el Estándar no dice expresamente que los bytes de una estructura son (o pueden usarse como) Un objeto de matriz.
supercat
-4

¡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 malloctiene 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 mallocpuede 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, 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.

strcpycomenzará 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 p1puntero se ha inicializado en un bloque de exactamente 10 bytes.

  • Si ase 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 strcpyse 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, strcpycontinuará 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.

Ghii Velte
fuente
3
Desafortunadamente, esta respuesta es inherentemente inválida. No se puede razonar nada sobre el comportamiento indefinido. La comparación no necesita hacerse a nivel de máquina.
Antti Haapala
66
Ghii, en realidad no. Si observa C11 Anexo J y 6.5.8, el acto de comparación en sí mismo es UB. La desreferenciación es un tema aparte.
paxdiablo
66
No, UB puede ser dañino incluso antes de desreferenciar un puntero. Un compilador es libre de optimizar completamente una función con UB en un solo NOP, aunque esto obviamente cambia el comportamiento visible.
nanofarad
2
@Ghii, el Anexo J (el bit que mencioné) es la lista de cosas que son comportamientos indefinidos , por lo que no estoy seguro de cómo eso respalda su argumento :-) 6.5.8 explícitamente llama la comparación como UB. Para su comentario a supercat, no hay comparación cuando imprime un puntero, por lo que probablemente tenga razón de que no se bloqueará. Pero eso no es lo que preguntaba el OP. 3.4.3también es una sección que debe mirar: define a UB como el comportamiento "para el cual esta Norma Internacional no impone requisitos".
paxdiablo
3
@GhiiVelte, sigues diciendo cosas que simplemente están mal, a pesar de que te lo señalamos. Sí, el fragmento que publicó debe compilarse, pero su afirmación de que se ejecuta sin problemas es incorrecta. Le sugiero que lea el estándar, particularmente (en este caso) C11 6.5.6/9, teniendo en cuenta que la palabra "deberá" indica un requisito L "Cuando se restan dos punteros, ambos apuntarán a elementos del mismo objeto de matriz, o uno más allá del último elemento del objeto de matriz ".
paxdiablo
-5

Los punteros son enteros, como todo lo demás en una computadora. Absolutamente puede compararlos con <y >producir resultados sin hacer que un programa se bloquee. Dicho esto, el estándar no garantiza que esos resultados tengan ningún significado fuera de las comparaciones de matrices.

En su ejemplo de variables asignadas de pila, el compilador es libre de asignar esas variables a registros o direcciones de memoria de pila, y en cualquier orden que elija. Las comparaciones como <y, >por lo tanto, no serán consistentes entre los compiladores o arquitecturas. Sin embargo, ==y !=no están tan restringidos, comparar la igualdad de punteros es una operación válida y útil.

nickelpro
fuente
2
La pila de palabras aparece exactamente cero veces en el estándar C11. Y el comportamiento indefinido significa que puede pasar cualquier cosa (incluido el bloqueo del programa).
paxdiablo
1
@paxdiablo ¿Dije que sí?
nickelpro
2
Usted mencionó las variables asignadas a la pila. No hay una pila en el estándar, eso es solo un detalle de implementación. El problema más serio con esta respuesta es la afirmación de que puede comparar los punteros sin posibilidad de un bloqueo, eso es simplemente incorrecto.
paxdiablo
1
@nickelpro: Si se desea escribir código que sea compatible con los optimizadores en gcc y clang, es necesario saltar a través de muchos aros tontos. Ambos optimizadores buscarán agresivamente oportunidades para hacer inferencias sobre las cosas a las que accederán los punteros cada vez que haya alguna forma en que el Estándar se pueda torcer para justificarlos (e incluso a veces cuando no lo hay). Dado int x[10],y[10],*p;, si el código evalúa y[0], luego evalúa p>(x+5)y escribe *psin modificar pmientras tanto, y finalmente evalúa y[0]nuevamente, ...
supercat
1
nickelpro, acepta no estar de acuerdo, pero tu respuesta sigue siendo fundamentalmente incorrecta. Comparo su enfoque con el de las personas que usan en (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')lugar de isalpha()porque ¿qué implementación sensata tendría esos caracteres discontinuos? La conclusión es que, incluso si ninguna implementación que conoce tiene un problema, debe codificar el estándar tanto como sea posible si valora la portabilidad. Sin embargo, agradezco la etiqueta "estándares maven", gracias por eso. Puedo poner en mi CV :-)
paxdiablo