¿Cuándo deben verificarse los punteros para NULL en C?


18

Resumen :

¿Debería una función en C verificar siempre para asegurarse de que no está desreferenciando un NULLpuntero? Si no es así, ¿cuándo es apropiado omitir estos controles?

Detalles :

He estado leyendo algunos libros sobre programación de entrevistas y me pregunto cuál es el grado apropiado de validación de entrada para argumentos de función en C. Obviamente, cualquier función que reciba información de un usuario debe realizar la validación, incluida la comprobación de un NULLpuntero antes de desreferenciarlo. Pero, ¿qué pasa en el caso de una función dentro del mismo archivo que no espera exponer a través de su API?

Por ejemplo, lo siguiente aparece en el código fuente de git:

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Si *graphes NULLasí, un puntero nulo será desreferenciado, probablemente bloqueando el programa, pero posiblemente resultando en algún otro comportamiento impredecible. Por otro lado, la función es staticy quizás el programador ya haya validado la entrada. No lo sé, simplemente lo seleccioné al azar porque fue un breve ejemplo en un programa de aplicación escrito en C. He visto muchos otros lugares donde se usan punteros sin verificar NULL. Mi pregunta es general, no específica de este segmento de código.

Vi una pregunta similar en el contexto de la entrega de excepciones . Sin embargo, para un lenguaje inseguro como C o C ++ no hay propagación automática de errores de excepciones no controladas.

Por otro lado, he visto mucho código en proyectos de código abierto (como el ejemplo anterior) que no realiza ninguna comprobación de punteros antes de usarlos. Me pregunto si alguien tiene ideas sobre las pautas para cuándo poner cheques en una función en lugar de asumir que la función se llamó con los argumentos correctos.

Estoy interesado en esta pregunta en general para escribir código de producción. Pero también estoy interesado en el contexto de la programación de entrevistas. Por ejemplo, muchos libros de texto de algoritmos (como CLR) tienden a presentar los algoritmos en pseudocódigo sin ninguna comprobación de errores. Sin embargo, si bien esto es bueno para comprender el núcleo de un algoritmo, obviamente no es una buena práctica de programación. Por lo tanto, no quisiera decirle a un entrevistador que estaba omitiendo la comprobación de errores para simplificar mis ejemplos de código (como podría hacerlo un libro de texto). Pero tampoco quisiera parecer que produce código ineficiente con una comprobación de errores excesiva. Por ejemplo, graph_get_current_column_colorpodría haberse modificado para verificar *graphsi es nulo, pero no está claro qué haría si *graphfuera nulo, de lo contrario no debería desreferenciarlo.


77
Si está escribiendo una función para una API donde las personas que llaman no deben entender las entrañas, este es uno de esos lugares donde la documentación es importante. Si documenta que un argumento debe ser un puntero válido, no NULL, verificarlo se convierte en responsabilidad del llamante.
Blrfl


En retrospectiva del año 2017, teniendo en cuenta la pregunta y la mayoría de las respuestas escritas en 2013, ¿alguna de las respuestas aborda el problema de los comportamientos indefinidos que viajan en el tiempo debido a la optimización de los compiladores?
rwong

En el caso de llamadas API que esperan argumentos de puntero válidos, me pregunto cuál es el valor de las pruebas solo para NULL. Cualquier puntero inválido que se desreferencia sería igual de malo que NULL y por defecto igual.
PaulHK

Respuestas:


15

Los punteros nulos no válidos pueden ser causados ​​por un error del programador o por un error de tiempo de ejecución. Los errores de tiempo de ejecución son algo que un programador no puede solucionar, como una mallocfalla debido a la poca memoria o la red que deja caer un paquete o el usuario ingresa algo estúpido. Los errores del programador son causados ​​por un programador que usa la función incorrectamente.

La regla general que he visto es que los errores de tiempo de ejecución siempre deben verificarse, pero los errores de programador no tienen que verificarse siempre. Digamos que un programador idiota llamó directamente graph_get_current_column_color(0). Se segfault la primera vez que se llama, pero una vez que lo arreglas, la solución se compila de forma permanente. No es necesario verificar cada vez que se ejecuta.

A veces, especialmente en bibliotecas de terceros, verá una assertpara verificar los errores del programador en lugar de una ifdeclaración. Eso le permite compilar las comprobaciones durante el desarrollo y dejarlas fuera en el código de producción. También he visto ocasionalmente verificaciones gratuitas en las que la fuente del posible error del programador está muy lejos del síntoma.

Obviamente, siempre puedes encontrar a alguien más pedante, pero la mayoría de los programadores de C que conozco prefieren un código menos abarrotado sobre un código que sea marginalmente más seguro. Y "más seguro" es un término subjetivo. Una evidente falla durante el desarrollo es preferible a un sutil error de corrupción en el campo.


La pregunta es algo subjetiva, pero esta parece ser la mejor respuesta por ahora. Gracias a todos los que dieron su opinión sobre esta pregunta.
Gabriel Southern

1
En iOS, malloc nunca devolverá NULL. Si no encuentra memoria, entonces le pedirá a su aplicación que libere memoria primero, luego le pedirá al sistema operativo (que le pedirá a otras aplicaciones que libere memoria y posiblemente las mate), y si todavía no hay memoria, matará su aplicación . No se necesitan cheques.
gnasher729

11

Kernighan & Plauger, en "Herramientas de software", escribieron que verificarían todo y, en busca de condiciones que, de hecho, creían que nunca sucedería, abortarían con un mensaje de error "No puede suceder".

Informan que fueron rápidamente humillados por la cantidad de veces que vieron "No puede pasar" en sus terminales.

SIEMPRE debe verificar el puntero para NULL antes de (intentar) desreferenciarlo. SIEMPRE . La cantidad de código que duplica para verificar los NULL que no suceden, y los ciclos del procesador que "desperdicia", serán más que pagados por el número de bloqueos que no tiene que depurar de nada más que un volcado por caída: Si tienes tanta suerte.

Si el puntero es invariante dentro de un bucle, es suficiente verificarlo fuera del bucle, pero luego debe "copiarlo" en una variable local de alcance limitado, para su uso por el bucle, que agrega las decoraciones const apropiadas. En este caso, DEBE asegurarse de que cada función llamada desde el cuerpo del bucle incluya las decoraciones constantes necesarias en los prototipos, TODO EL CAMINO HACIA ABAJO. Si no lo hace, o no puede (por ejemplo, un paquete de proveedor o un compañero de trabajo obstinado), entonces usted debe comprobar NULL cada vez que se podría modificar , porque seguro como el COL Murphy era un optimista incurable, alguien SE va golpearlo cuando no estás mirando.

Si está dentro de una función y se supone que el puntero no está entrando NULL, debe verificarlo.

Si lo está recibiendo de una función y se supone que no está saliendo NULL, debe verificarlo. malloc () es particularmente conocido por esto. (Nortel Networks, ahora extinto, tenía un estándar de codificación escrito rápido y rápido sobre esto. Tuve que depurar un bloqueo en un punto, que rastreé hasta malloc () devolviendo un puntero NULL y el codificador idiota sin molestarse en verificar antes de que él le escribiera, porque él SABÍA que tenía mucha memoria ... Dije algunas cosas muy desagradables cuando finalmente lo encontré).


8
Si está en una función que requiere un puntero no NULL, pero lo verifica de todos modos y es NULL ... ¿qué sigue?
detly

1
@detly deja de hacer lo que estás haciendo y devuelve un código de error, o dispara una afirmación
James

1
@ James - no pensé en eso assert, claro. Sin NULLembargo, no me gusta la idea del código de error si estás hablando de cambiar el código existente para incluir cheques.
desviarse

10
@detly no vas a llegar muy lejos como desarrollador de C si no te gustan los códigos de error
James

55
@ JohnR.Strohm - esto es C, son afirmaciones o nada: P
detly

5

Puede omitir la comprobación cuando pueda convencerse de alguna manera de que el puntero no puede ser nulo.

Por lo general, las comprobaciones de puntero nulo se implementan en código en el que se espera que aparezca nulo como un indicador de que un objeto no está disponible actualmente. Nulo se utiliza como valor centinela, por ejemplo, para terminar listas vinculadas, o incluso conjuntos de punteros. Se requiere que el argvvector de las cadenas pasadas mainsea ​​anulado por un puntero, de forma similar a cómo una cadena termina por un carácter nulo: argv[argc]es un puntero nulo, y puede confiar en esto cuando analiza la línea de comando.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

Entonces, las situaciones para verificar nulo son aquellas en las que es un valor esperado. Las comprobaciones nulas implementan el significado del puntero nulo, como detener la búsqueda de una lista vinculada. Evitan que el código haga referencia al puntero.

En una situación en la que el diseño no espera un valor de puntero nulo, no tiene sentido comprobarlo. Si surge un valor de puntero no válido, es muy probable que parezca no nulo, lo que no se puede distinguir de los valores válidos de ninguna manera portátil. Por ejemplo, un valor de puntero obtenido de la lectura de almacenamiento no inicializado interpretado como un tipo de puntero, un puntero obtenido mediante alguna conversión sombreada o un puntero incrementado fuera de los límites.

Acerca de un tipo de datos como graph *: esto podría diseñarse de modo que un valor nulo sea un gráfico válido: algo sin bordes ni nodos. En este caso, todas las funciones que toman un graph *puntero tendrán que tratar con ese valor, ya que es un valor de dominio correcto en la representación de gráficos. Por otro lado, a graph *podría ser un puntero a un objeto tipo contenedor que nunca es nulo si tenemos un gráfico; un puntero nulo podría decirnos que "el objeto gráfico no está presente; todavía no lo asignamos, o lo liberamos; o esto actualmente no tiene un gráfico asociado". Este último uso de punteros es un booleano / satélite combinado: el puntero no es nulo indica "Tengo este objeto hermano", y proporciona ese objeto.

Podríamos establecer un puntero en nulo incluso si no estamos liberando un objeto, simplemente para disociar un objeto de otro:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

El argumento más convincente que sé que un puntero no puede ser nulo en cierto punto es envolver ese punto en "if (ptr! = NULL) {" y su correspondiente "}". Más allá de eso, estás en territorio de verificación formal.
John R. Strohm

4

Permítanme agregar una voz más a la fuga.

Como muchas de las otras respuestas, digo: no te molestes en comprobar en este punto; Es responsabilidad de quien llama. Pero tengo una base para construir en lugar de una simple conveniencia (y arrogancia de programación en C).

Intento seguir el principio de Donald Knuth de hacer que los programas sean lo más frágiles posible. Si algo sale mal, haga que se bloquee grandemente , y hacer referencia a un puntero nulo suele ser una buena manera de hacerlo. La idea general es un bloqueo o un bucle infinito es mucho mejor que crear datos incorrectos. ¡Y llama la atención de los programadores!

Pero hacer referencia a punteros nulos (especialmente para estructuras de datos grandes) no siempre causa un bloqueo. Suspiro. Es verdad. Y ahí es donde caen los Activos. Son simples, pueden bloquear instantáneamente su programa (que responde a la pregunta, "¿Qué debería hacer el método si encuentra un valor nulo?"), Y se puede activar / desactivar para varias situaciones (recomiendo NO los apaga, ya que es mejor para los clientes tener un bloqueo y ver un mensaje críptico que tener datos incorrectos).

Esos son mis dos centavos.


1

Por lo general, solo verifico cuando se asigna un puntero, que generalmente es el único momento en que puedo hacer algo al respecto y posiblemente recuperarme si no es válido.

Si obtengo un identificador de una ventana, por ejemplo, comprobaré si es nulo correctamente y de vez en cuando, y haré algo sobre la condición nula, pero no voy a verificar si es nulo cada vez. Uso el puntero, en todas y cada una de las funciones a las que se pasa el puntero, de lo contrario tendría montañas de código de manejo de errores duplicado.

Funciones como graph_get_current_column_colores probable que no pueda hacer nada útil para su situación si encuentra un puntero incorrecto, por lo que dejaría la comprobación de NULL para sus llamantes.


1

Yo diría que depende de lo siguiente:

  1. ¿Es crítica la utilización de la CPU? Cada cheque para NULL lleva cierto tiempo.
  2. ¿Cuáles son las probabilidades de que el puntero sea NULL? ¿Se acaba de usar en una función anterior? ¿Podría haber cambiado el valor del puntero?
  3. ¿Es el sistema preventivo? ¿Significado podría suceder un cambio de tarea y cambiar el valor? ¿Podría entrar un ISR y cambiar el valor?
  4. ¿Qué tan estrechamente acoplado está el código?
  5. ¿Existe algún tipo de mecanismo automático que verifique los punteros NULL automáticamente?

La utilización de la CPU / el puntero de probabilidades es NULL Cada vez que verifica NULL, lleva tiempo. Por esta razón, trato de limitar mis cheques a donde el puntero podría haber cambiado su valor.

Sistema preventivo Si su código se está ejecutando y otra tarea podría interrumpirlo y potencialmente cambiar el valor, sería bueno tener una verificación.

Módulos estrechamente acoplados Si el sistema está estrechamente acoplado, tendría sentido que tenga más comprobaciones. Lo que quiero decir con esto es que si hay estructuras de datos que se comparten entre varios módulos, un módulo podría cambiar algo de otro módulo. En estas situaciones, tiene sentido verificar más a menudo.

Comprobaciones automáticas / Asistencia de hardware Lo último que se debe tener en cuenta es si el hardware en el que se está ejecutando tiene algún tipo de mecanismo que puede verificar NULL. Específicamente me refiero a la detección de fallas de página. Si su sistema tiene detección de fallas de página, la CPU misma puede verificar los accesos NULL. Personalmente, considero que este es el mejor mecanismo, ya que siempre se ejecuta y no depende del programador para realizar comprobaciones explícitas. También tiene el beneficio de prácticamente cero gastos generales. Si está disponible, lo recomiendo, la depuración es un poco más difícil pero no demasiado.

Para probar si está disponible, cree un programa con un puntero. Establezca el puntero en 0 y luego intente leerlo / escribirlo.


No sé si clasificaría un segfault como realizar una verificación NULL automática. Estoy de acuerdo en que tener una protección de memoria de la CPU ayuda para que un proceso no pueda hacer tanto daño al resto del sistema, pero no lo llamaría protección automática.
Gabriel Southern

1

En mi opinión, validar las entradas (pre / post-condiciones, es decir) es una buena cosa para detectar errores de programación, pero solo si da como resultado un fuerte y desagradable error que no se puede ignorar. assertnormalmente tiene ese efecto.

Cualquier cosa que no llegue a esto puede convertirse en una pesadilla sin equipos muy cuidadosamente coordinados. Y, por supuesto, lo ideal es que todos los equipos estén muy cuidadosamente coordinados y unificados bajo estándares estrictos, pero la mayoría de los entornos en los que he trabajado no llegaron a eso.

Solo como ejemplo, trabajé con algunos colegas que creían que uno debería verificar religiosamente la presencia de punteros nulos, por lo que rociaron mucho código como este:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... y a veces solo así sin siquiera devolver / configurar un código de error. Y esto estaba en una base de código que tenía varias décadas de antigüedad con muchos complementos de terceros adquiridos. También era una base de código plagada de muchos errores, y a menudo errores que eran muy difíciles de rastrear hasta las causas raíz, ya que tenían una tendencia a bloquearse en sitios muy alejados de la fuente inmediata del problema.

Y esta práctica fue uno de los motivos. Es una violación de una precondición establecida de la move_vertexfunción anterior pasarle un vértice nulo, pero tal función simplemente la aceptó en silencio y no hizo nada en respuesta. Entonces, lo que solía suceder es que un complemento puede tener un error de programador que hace que pase nulo a dicha función, solo para no detectarlo, solo para hacer muchas cosas después, y eventualmente el sistema comenzará a fallar o bloquearse.

Pero el verdadero problema aquí fue la incapacidad de detectar fácilmente este problema. Así que una vez traté de ver qué pasaría si convirtiera el código analógico anterior en un assert, así:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... y para mi horror, encontré que esa afirmación fallaba de izquierda a derecha incluso al iniciar la aplicación. Después de arreglar los primeros sitios de llamadas, hice algunas cosas más y luego obtuve un montón de errores de afirmación. Seguí adelante hasta que modifiqué tanto código que terminé revirtiendo mis cambios porque se habían vuelto demasiado intrusivos y a regañadientes mantuvieron esa verificación de puntero nulo, en lugar de documentar que la función permite aceptar un vértice nulo.

Pero ese es el peligro, aunque sea el peor de los casos, de no hacer que las violaciones de las condiciones previas / posteriores sean fácilmente detectables. Luego, a lo largo de los años, puede acumular silenciosamente una carga de código de barco que viola tales condiciones previas / posteriores mientras vuela bajo el radar de las pruebas. En mi opinión, estas comprobaciones de puntero nulo fuera de una afirmación flagrante y desagradable pueden hacer mucho, mucho más daño que bien.

En cuanto a la pregunta esencial de cuándo debe verificar los punteros nulos, creo en afirmar generosamente si está diseñado para detectar un error del programador, y no dejar que eso se silencie y sea difícil de detectar. Si no se trata de un error de programación y de algo que está fuera del control del programador, como un fallo de falta de memoria, entonces tiene sentido verificar la nula y utilizar el manejo de errores. Más allá de eso, es una pregunta de diseño y se basa en lo que sus funciones consideran condiciones previas / posteriores válidas.


0

Una práctica es realizar siempre la verificación nula a menos que ya la haya verificado; así que si la entrada se pasa de la función A () a B (), y A () ya ha validado el puntero y está seguro de que B () no se llama a ningún otro lugar, entonces B () puede confiar en que A () desinfecta los datos.


1
... hasta que dentro de 6 meses alguien venga y agregue más código que llame a B () (posiblemente suponiendo que quien escribió B () seguramente verificó los NULL correctamente). Entonces estás jodido, ¿verdad? Regla básica: si existe una condición no válida para la entrada a una función, verifíquela porque la entrada está fuera del control de la función.
Maximus Minimus

@ mh01 Si solo está rompiendo código aleatorio (es decir, haciendo suposiciones y no leyendo la documentación), entonces no creo que las NULLverificaciones adicionales vayan a hacer mucho. Piénselo: ahora B()busca NULLy ... ¿qué hace? Volver -1? Si la persona que llama no verifica NULL, ¿qué confianza puede tener de que de -1todos modos se ocupará del caso del valor de retorno?
detly

1
Esa es la responsabilidad de las personas que llaman. Usted asume su propia responsabilidad, lo que incluye no confiar en ninguna entrada arbitraria / incognoscible / potencialmente no verificada que le den. De lo contrario, estás en una ciudad sin salida. Si la persona que llama no verifica, entonces la persona que llamó se equivocó; revisaste, tu propio trasero está cubierto, puedes decirle a quien escribió la persona que llamó que al menos hiciste las cosas bien.
Maximus Minimus
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.