¿Qué significa hacer una "verificación nula" en C o C ++?

21

He estado aprendiendo C ++ y me está costando entender nulo. En particular, los tutoriales que he leído mencionan hacer una "verificación nula", pero no estoy seguro de lo que eso significa o por qué es necesario.

  • ¿Qué es exactamente nulo?
  • ¿Qué significa "verificar nulo"?
  • ¿Siempre necesito verificar si es nulo?

Cualquier código de ejemplo sería muy apreciado.

kdi
fuente
Cuándo: programmers.stackexchange.com/questions/186036/…
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Aconsejaría obtener algunos mejores tutoriales, si todos los que leen hablan de cheques nulos sin explicarlos y proporcionar un código de ejemplo ...
underscore_d

Respuestas:

26

En C y C ++, los punteros son intrínsecamente inseguros, es decir, cuando desreferencia un puntero, es su responsabilidad asegurarse de que apunte a algún lugar válido; esto es parte de lo que se trata la "administración de memoria manual" (a diferencia de los esquemas automáticos de administración de memoria implementados en lenguajes como Java, PHP o .NET runtime, que no le permitirán crear referencias inválidas sin un esfuerzo considerable).

Una solución común que detecta muchos errores es establecer todos los punteros que no apuntan a nada como NULL(o, en C ++ correcto 0), y verificar eso antes de acceder al puntero. Específicamente, es una práctica común inicializar todos los punteros a NULL (a menos que ya tenga algo para señalarlos cuando los declara), y establecerlos en NULL cuando usted deleteo free()ellos (a menos que salgan del alcance inmediatamente después de eso). Ejemplo (en C, pero también en C ++ válido):

void fill_foo(int* foo) {
    *foo = 23; // this will crash and burn if foo is NULL
}

Una mejor versión:

void fill_foo(int* foo) {
    if (!foo) { // this is the NULL check
        printf("This is wrong\n");
        return;
    }
    *foo = 23;
}

Sin la verificación nula, pasar un puntero NULL a esta función causará una falla por defecto, y no hay nada que pueda hacer: el sistema operativo simplemente matará su proceso y tal vez volcará el núcleo o abrirá un cuadro de diálogo de informe de bloqueo. Con la verificación nula en su lugar, puede realizar un manejo de errores adecuado y recuperarse con gracia: corrija el problema usted mismo, cancele la operación actual, escriba una entrada de registro, notifique al usuario, lo que sea apropiado.

tdammers
fuente
3
@ MrLister, ¿qué quieres decir con que las comprobaciones nulas no funcionan en C ++? Solo tiene que inicializar el puntero a nulo cuando lo declara.
TZHX
1
Lo que quiero decir es que debes recordar establecer el puntero en NULL o no funcionará. Y si recuerda, en otras palabras, si sabe que el puntero es NULL, no tendrá que llamar a fill_foo de todos modos. fill_foo comprueba si el puntero tiene un valor, no si el puntero tiene un valor válido . En C ++, no se garantiza que los punteros sean NULL o tengan un valor válido.
Sr. Lister
44
Una afirmación () sería una mejor solución aquí. No tiene sentido tratar de "estar a salvo". Si se pasó NULL, obviamente está mal, entonces, ¿por qué no simplemente bloquearse explícitamente para que el programador sea plenamente consciente? (Y en producción, no importa, porque has demostrado que nadie llamará a fill_foo () con NULL, ¿verdad? Realmente, no es tan difícil.)
Ambroz Bizjak
77
No olvide mencionar que una versión aún mejor de esta función debería usar referencias en lugar de punteros, haciendo que la verificación NULL sea obsoleta.
Doc Brown
44
Esto no es de lo que se trata la administración manual de memoria, y un programa administrado también explotará (o al menos hará una excepción, como lo hará un programa nativo en la mayoría de los idiomas) si intenta desreferenciar una referencia nula.
Mason Wheeler
7

Las otras respuestas cubrieron su pregunta exacta. Se realiza una comprobación nula para asegurarse de que el puntero que recibió en realidad apunta a una instancia válida de un tipo (objetos, primitivas, etc.).

Sin embargo, voy a agregar mi propio consejo aquí. Evitar cheques nulos. :) Las comprobaciones nulas (y otras formas de programación defensiva) desordenan el código, y en realidad lo hacen más propenso a errores que otras técnicas de manejo de errores.

Mi técnica favorita cuando se trata de punteros de objetos es usar el patrón Objeto nulo . Eso significa devolver una (puntero, o incluso mejor, una referencia a una) matriz o lista vacía en lugar de nula, o devolver una cadena vacía ("") en lugar de nula, o incluso la cadena "0" (o algo equivalente a "nada "en el contexto) donde espera que se analice a un entero.

Como beneficio adicional, aquí hay algo que quizás no sabías sobre el puntero nulo, que fue implementado (por primera vez) por CAR Hoare para el lenguaje Algol W en 1965.

Lo llamo mi error de mil millones de dólares. Fue la invención de la referencia nula en 1965. En ese momento, estaba diseñando el primer sistema de tipo integral para referencias en un lenguaje orientado a objetos (ALGOL W). Mi objetivo era asegurar que todo uso de referencias debería ser absolutamente seguro, con una verificación realizada automáticamente por el compilador. Pero no pude resistir la tentación de poner una referencia nula, simplemente porque era muy fácil de implementar. Esto ha llevado a innumerables errores, vulnerabilidades y fallas en el sistema, lo que probablemente ha causado miles de millones de dólares de dolor y daños en los últimos cuarenta años.

Ñame Marcovic
fuente
66
Objeto nulo es incluso peor que tener un puntero nulo. Si un algoritmo X requiere datos Y que usted no tiene, entonces eso es un error en su programa , que simplemente está ocultando fingiendo que sí.
DeadMG
Depende del contexto, y de cualquier manera la prueba de "presencia de datos" supera a la prueba de nulo en mi libro. Según mi experiencia, si un algoritmo funciona, por ejemplo, en una lista, y la lista está vacía, entonces el algoritmo simplemente no tiene nada que hacer, y lo logra simplemente usando declaraciones de control estándar como for / foreach.
Yam Marcovic
Si el algoritmo no tiene nada que ver, ¿por qué lo llamas? Y la razón por la que podría haber querido llamarlo en primer lugar es porque hace algo importante .
DeadMG
@DeadMG Debido a que los programas tienen que ver con la entrada, y en el mundo real, a diferencia de las tareas asignadas, la entrada puede ser irrelevante (por ejemplo, vacía). El código todavía se llama de cualquier manera. Tiene dos opciones: o verifica la relevancia (o el vacío), o diseña sus algoritmos para que lean y funcionen bien sin verificar explícitamente la relevancia utilizando declaraciones condicionales.
Yam Marcovic
Vine aquí para hacer casi el mismo comentario, así que le di mi voto. Sin embargo, también agregaría que esto es representativo de un problema mayor de los objetos zombie : cada vez que tenga objetos con inicialización (o destrucción) en varias etapas que no están completamente vivos pero no están completamente muertos. Cuando ve un código "seguro" en lenguajes sin finalización determinista que ha agregado comprobaciones en cada función para ver si el objeto ha sido eliminado, es este problema general lo que le preocupa. Nunca debe ser nulo, debe trabajar con estados que tengan los objetos que necesitan para toda su vida.
ex0du5
4

El valor del puntero nulo representa un "ningún lugar" bien definido; es un valor de puntero inválido que se garantiza que no se compara con ningún otro valor de puntero. Intentar desreferenciar un puntero nulo da como resultado un comportamiento indefinido y generalmente generará un error de tiempo de ejecución, por lo que debe asegurarse de que un puntero no sea NULL antes de intentar desreferenciarlo. Varias funciones de la biblioteca C y C ++ devolverán un puntero nulo para indicar una condición de error. Por ejemplo, la función de biblioteca mallocdevolverá un valor de puntero nulo si no puede asignar el número de bytes que se han solicitado, e intentar acceder a la memoria a través de ese puntero (generalmente) provocará un error de tiempo de ejecución:

int *p = malloc(sizeof *p * N);
p[0] = ...; // this will (usually) blow up if malloc returned NULL

Por lo tanto, debemos asegurarnos de que la mallocllamada se realizó correctamente comprobando el valor de pcontra NULL:

int *p = malloc(sizeof *p * N);
if (p != NULL) // or just if (p)
  p[0] = ...;

Ahora, agárrate a tus calcetines un minuto, esto se pondrá un poco irregular.

Hay un puntero nulo valor y un puntero nulo constante , y los dos no son necesariamente los mismos. El puntero nulo valor es cualquier valor que los usos arquitectura subyacente para representar "la nada". Este valor puede ser 0x00000000, 0xFFFFFFFF, 0xDEADBEEF o algo completamente diferente. No asuma que el puntero nulo valor es siempre 0.

La constante de puntero nulo , OTOH, es siempre una expresión integral con valor 0. En lo que respecta a su código fuente , 0 (o cualquier expresión integral que evalúe a 0) representa un puntero nulo. Tanto C como C ++ definen la macro NULL como la constante de puntero nulo. Cuando se compila su código, la constante de puntero nulo se reemplazará con el valor de puntero nulo apropiado en el código de máquina generado.

Además, tenga en cuenta que NULL es solo uno de los muchos valores de puntero inválidos posibles ; si declara una variable de puntero automático sin inicializarla explícitamente, como

int *p;

El valor inicialmente almacenado en la variable es indeterminado y puede no corresponder a una dirección de memoria válida o accesible. Desafortunadamente, no hay forma (portátil) de saber si un valor de puntero no NULL es válido o no antes de intentar usarlo. Entonces, si está tratando con punteros, generalmente es una buena idea inicializarlos explícitamente a NULL cuando los declara, y establecerlos en NULL cuando no apuntan activamente a nada.

Tenga en cuenta que esto es más un problema en C que en C ++; C ++ idiomático no debería usar punteros tanto.

John Bode
fuente
3

Hay un par de métodos, todos esencialmente hacen lo mismo.

int * foo = NULL; // a veces se establece en 0x00 o 0 o 0L en lugar de NULL

comprobación nula (comprobar si el puntero es nulo), versión A

if (foo == NULL)

cheque nulo, versión B

if (! foo) // ya que NULL se define como 0,! foo devolverá un valor de un puntero nulo

cheque nulo, versión C

si (foo == 0)

De los tres, prefiero usar la primera verificación, ya que explícitamente le dice a los futuros desarrolladores lo que estaba tratando de verificar Y deja en claro que esperaba que foo fuera un puntero.


fuente
2

Usted no La única razón para usar un puntero en C ++ es porque desea explícitamente la presencia de punteros nulos; de lo contrario, puede tomar una referencia, que es semánticamente más fácil de usar y garantiza que no sea nulo.

DeadMG
fuente
1
@James: 'nuevo' en modo kernel?
Nemanja Trifunovic
1
@James: una implementación de C ++ que representa las capacidades que disfruta una mayoría significativa de codificadores de C ++. Eso incluye todas las características del lenguaje C ++ 03 (excepto export) y todas las características de la biblioteca C ++ 03 y TR1 y una buena parte de C ++ 11.
DeadMG
55
Desearía que la gente no dijera que "las referencias garantizan que no son nulas". Ellos no. Es tan fácil generar una referencia nula como un puntero nulo, y se propagan de la misma manera.
mjfgates
2
@Stargazer: La pregunta es 100% redundante cuando solo usas las herramientas de la forma en que los diseñadores de idiomas y las buenas prácticas sugieren que deberías hacerlo.
DeadMG
2
@DeadMG, no importa si es redundante. No respondiste la pregunta . Lo diré de nuevo: -1.
Riwalk
-1

Si no marca el valor NULL, especialmente, si eso es un puntero a una estructura, tal vez haya encontrado una vulnerabilidad de seguridad: la desreferencia de puntero NULL. La desreferencia de puntero NULO puede conducir a otras vulnerabilidades de seguridad graves, como desbordamiento del búfer, condición de carrera ... que pueden permitir que el atacante tome el control de su computadora.

Muchos proveedores de software como Microsoft, Oracle, Adobe, Apple ... lanzan parches de software para corregir estas vulnerabilidades de seguridad. Creo que deberías comprobar el valor NULL de cada puntero :)

oDisPo
fuente