Resumen :
¿Debería una función en C verificar siempre para asegurarse de que no está desreferenciando un NULL
puntero? 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 NULL
puntero 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 *graph
es NULL
así, 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 static
y 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_color
podría haberse modificado para verificar *graph
si es nulo, pero no está claro qué haría si *graph
fuera nulo, de lo contrario no debería desreferenciarlo.
fuente
Respuestas:
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
malloc
falla 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
assert
para verificar los errores del programador en lugar de unaif
declaració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.
fuente
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é).
fuente
assert
, claro. SinNULL
embargo, no me gusta la idea del código de error si estás hablando de cambiar el código existente para incluir cheques.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
argv
vector de las cadenas pasadasmain
sea 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.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 ungraph *
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, agraph *
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:
fuente
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.
fuente
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_color
es 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.fuente
Yo diría que depende de lo siguiente:
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.
fuente
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.
assert
normalmente 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:
... 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_vertex
funció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í:... 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.
fuente
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.
fuente
NULL
verificaciones adicionales vayan a hacer mucho. Piénselo: ahoraB()
buscaNULL
y ... ¿qué hace? Volver-1
? Si la persona que llama no verificaNULL
, ¿qué confianza puede tener de que de-1
todos modos se ocupará del caso del valor de retorno?