[Método de depuración] Habilidades prácticas de depuración basadas en el entorno vs.

Prefacio:

Para miles de programadores, si hay algo más doloroso que escribir programas en este mundo, debe ser encontrar los errores (lagunas) en los programas que ellos mismos escriben. Como novatos, a menudo informamos errores en nuestra escritura de código diaria (los buenos programadores simplemente tienen más errores de los que hemos visto para reducir los errores), pero cuando encontramos errores, es posible que todos no los entiendan. tiempo y finalmente se convirtió en ingeniero "C/V". En este número, basado en el entorno vs, lo guiaré para que comprenda los consejos de depuración de código.

inserte la descripción de la imagen aquí

1. ¿Qué es un error?

En primer lugar, cuando queremos superarlo, primero debemos entenderlo. Al igual que pelear una guerra, solo conociéndote a ti mismo y al enemigo puedes salir victorioso en todas las batallas.

La razón probable es que una vez que la computadora se descompuso mientras escribía el programa, después de una investigación, se encontró una pequeña polilla en el relé eléctrico de la computadora. Las fallas se denominan "errores". Este es el origen del "bicho" que nos encanta decir hoy. Su significado, consistente con el original, es realmente "una chinche".
inserte la descripción de la imagen aquí
La razón específica se puede entender:
El origen del error.


2. ¿Qué es la depuración? ¿Qué tan importante es?

Al igual que la policía que maneja un caso, razonan e investigan paso a paso de acuerdo con las pistas, y finalmente llegan a la verdad final. Quizás lo que más nos impresionó es el [Detective Conan] que hemos visto.Un buen programador es un buen detective, y cada depuración es un intento de resolver un caso *

Para la gran mayoría de los jugadores novatos, cuando escribimos código, es "tres veces, cinco divisiones y dos".
inserte la descripción de la imagen aquí
¿Y cómo solucionar el problema?
inserte la descripción de la imagen aquí
Al depurar un error, podría verse así:

inserte la descripción de la imagen aquí
Agregando, eliminando, revisando y modificando de una manera tan irreflexiva que aún puede quedarse atrapado en su lugar después de trabajar durante medio día. Por lo tanto, es muy importante dominar bien la depuración.


2.1 ¿Qué es la depuración?

La depuración (en inglés: Debugging / Debug), también conocida como depuración, es un proceso de descubrimiento y reducción de errores de programa en programas informáticos o equipos electrónicos.

2.2 Pasos básicos de la puesta en marcha

a. Descubrir la existencia de errores de programa
b. Localizar los errores mediante aislamiento y eliminación
c. Determinar la causa de los errores
d. Proponer soluciones
para corregir errores e. Corregir errores de programa y volver a probar


2.3 Introducción a la depuración y liberación

Luego, echemos un vistazo a las dos versiones en VS, a saber, -----Debug y Release

a:
La depuración generalmente se denomina versión de depuración. A través de la cooperación de una serie de opciones de compilación, el resultado compilado generalmente contiene información de depuración sin ninguna optimización, para proporcionar a los desarrolladores capacidades poderosas de depuración de aplicaciones y facilitar a los programadores la depuración de programas.
b:
El lanzamiento generalmente se denomina versión de lanzamiento, que es para usuarios. Generalmente, los clientes no pueden depurar en la versión de lanzamiento. Por lo tanto, la información de depuración no se guarda y, al mismo tiempo, a menudo realiza varias optimizaciones para lograr el código más pequeño y la mejor velocidad. Proporcionar comodidad para los usuarios.

Todavía lo mostramos a través del código:

#include<stdio.h>

int main()
{
    
    
	char* p = "hello world";
	printf("%s\n", p);

	return 0;
}

Cuando escribimos el código anterior y lo ejecutamos en la versión [Debug]
inserte la descripción de la imagen aquí

Cuando vamos al archivo para ver la información bajo [debug], vemos el resultado como se muestra en la siguiente figura:
inserte la descripción de la imagen aquí

Y cuando nuestro código se ejecuta bajo la versión [Release]:

inserte la descripción de la imagen aquí

Podemos ver que el tamaño del archivo del mismo programa es diferente en las dos versiones.
inserte la descripción de la imagen aquí

Entonces decimos que la depuración es un proceso de encontrar problemas potenciales en el código en el entorno de la versión de depuración.

¿Qué optimizaciones realiza el compilador?
Por favor vea el siguiente código:

int main()
{
    
    
    int i = 0;
    int arr[10] = {
    
     0 };
    for (i = 0; i <= 12; i++)
    {
    
    
        arr[i] = 0;
        printf("hehe\n");
    }
    return 0;
}

Es el modo [depuración] para compilar, el resultado del programa es un
inserte la descripción de la imagen aquí
modo [lanzamiento] de bucle infinito para compilar, el programa no tiene un bucle infinito.

inserte la descripción de la imagen aquí

La diferencia entre ellos se debe a la optimización.


3. Introducción a la depuración en entorno Windows

Nota: La herramienta de depuración para el entorno de desarrollo de Linux es gdb, que se presentará más adelante en el curso.

3.1 Preparación para el entorno de depuración

inserte la descripción de la imagen aquí
Solo seleccionando la opción de depuración en el entorno se puede depurar el código con normalidad.

3.2 Aprender teclas de acceso directo

inserte la descripción de la imagen aquí

En la imagen de arriba, marqué algunas teclas de método abreviado que se usan a menudo en la vida diaria. Recordar las teclas de método abreviado mejorará en gran medida nuestra eficiencia de depuración. A continuación, presentaré en detalle:

F5

Comenzar a depurar, a menudo utilizado para saltar directamente al siguiente punto de interrupción.

F9

Crear puntos de interrupción y cancelar puntos
de interrupción El papel importante de los puntos de interrupción, puede establecer puntos de interrupción en cualquier parte del programa.
De esta manera, el programa puede detenerse en cualquier posición deseada y luego ejecutarse paso a paso.

F10

Proceso por proceso, generalmente utilizado para procesar un proceso, un proceso puede ser una llamada de función o una declaración.

F11

Sentencia por sentencia es ejecutar una sentencia cada vez, pero esta tecla de atajo puede hacer que nuestra lógica de ejecución entre en la función (este es el uso más largo).

CTRL + F5

Comience la ejecución sin depurar, si desea que el programa se ejecute directamente sin depurar, puede usarlo directamente.

Se pueden ver más teclas de acceso directo de la siguiente manera:
https://blog.csdn.net/mrlisky/article/details/72622009


4. Ejemplo de demostración

Habiendo dicho tanto, después de todo, todo está en papel, a continuación, te lo mostraremos a través de ejemplos específicos.

4.1 Ejemplo 1: Suma de factoriales

Pensamiento de código:

Antes de comenzar a escribir código, debemos pensar en cuál es la suma factorial de n.
Cuando tenemos una idea en mente, será muy rápido escribirla, en lugar de simplemente comenzar: la lógica es muy simple, primero ingrese n para representar la suma de los factoriales de n, y finalmente realice la operación de suma

A continuación, piense en el conocimiento que necesita usar en cada paso, de la siguiente manera:

1. Factorial: 1x2x3...xn usa una declaración de bucle

2. Sumar: todavía usando un bucle

¡Finalmente, solo imprímelo! !

el código se muestra a continuación:

int main()
{
    
    
	int i = 0;
	int sum = 0;//保存最终结果
	int n = 0;
	int ret = 1;//保存n的阶乘(乘法的话一定要初始化为1)
	scanf("%d", &n);

	for (i = 1; i <= n; i++)
	{
    
    
		int j = 0;
		for (j = 1; j <= i; j++)
		{
    
    
			ret *= j;
		}
		sum += ret;
	}

	printf("%d\n", sum);

	return 0;
}

Bueno, después de tener el código anterior, tenemos que verificar si el código escrito en este momento es correcto. Al principio, demos un ejemplo simple, tomemos la entrada [3] como ejemplo. En nuestra imaginación, después del factorial de [3] queda:

El primer paso: el factorial de 1, o sea, 1,
el segundo paso: el factorial de 2, o sea, 1 2=2,
el tercer paso: el factorial de 3, o sea, 1
2*3=6;
el cuarto paso: sumar tres números, es decir, 1+2+6=9 (es decir, el resultado final es 9)

Entonces, ¿es ese realmente el caso? A continuación, realizaremos una depuración paso a paso para ver los resultados.

En primer lugar, podemos ver que hemos entrado en el bucle de memoria. En este momento, i = 1, j = 0, y el bucle comenzará a ejecutarse una vez, por lo que el factorial de [1] en este momento se puede calcular como : 1! = 1

inserte la descripción de la imagen aquí
Después de salir del bucle interno por primera vez, podemos encontrar [1! ], para que pueda ver que [sum] es 1 en este momento

inserte la descripción de la imagen aquí
Luego vamos a calcular [2! ], en este momento, dentro de nuestra función, la operación de bucle se realizará dos veces, después de la ejecución, cada valor en este momento se muestra en la siguiente tabla:

inserte la descripción de la imagen aquí
En este punto nosotros [2! ] Después de completar el cálculo, se puede obtener el valor de [ret], y finalmente se realiza la operación de acumulación, es decir, la operación de [1+2], por lo que la [suma] en este momento debe ser 3, y El resultado es el siguiente:

inserte la descripción de la imagen aquí
Después de las dos primeras ejecuciones, regrese y ejecute [3! ], se realizan internamente tres operaciones de bucle, como ya sabemos, [3! ] El resultado es 6, seguimos con [F10] para ver el resultado:

Comienza el primer bucle interno, en este momento [j=1], [ret=2]

inserte la descripción de la imagen aquí

Después de completar el segundo ciclo, en este momento [j=2], [ret=4]

inserte la descripción de la imagen aquí

Después del tercer ciclo, podemos encontrar que [j=3], [ret=12]

inserte la descripción de la imagen aquí

Después del último paso del ciclo, necesitamos calcular la suma acumulada. En este momento, podemos encontrar que cuando [j=4] saltamos fuera de la operación del ciclo, y el resultado final muestra [15], que debería ser [9] (Oye... ¿cómo podría ser [15]? En este momento, las cabecitas curiosas de todos comienzan a agitarse)

inserte la descripción de la imagen aquí

No tenga miedo si encuentra un error de programa, analicémoslo cuidadosamente. Vamos a ordenarlo y escribirlo así:

a:
Cuando n=1, ingresamos al primer ciclo, y luego no pasa nada, ingresamos al segundo ciclo [ret=1*1], [sum=0+1], no hay problema en este momento;
b:
Cuando n=2, ingresamos al primer ciclo, no pasa nada, e ingresamos al segundo ciclo, [ret=1 * 1=1], pero tenga en cuenta que después de esto, [sum=sum no se calculará +ret], pero continúe sin saltar en el segundo bucle, porque la condición del segundo bucle es [i<2], lo cual sigue siendo cierto en este momento, por lo que el segundo bucle continúa, [ret=1 *2=2]; saltando del segundo ciclo, [sum=0+2], pero tenga en cuenta que el primer ciclo no ha terminado en este momento, para el primer ciclo, [n=1] en este momento, pero también Continúe el caso de [n= 2], por lo que el resultado final es [4], por lo que se produjo un error en este paso.

Entonces podemos cambiarlo así (restablecer el valor de [ret] cada vez), y el resultado de la ejecución será correcto en este momento:
inserte la descripción de la imagen aquí
Otra forma es que no podemos usar dos capas de anidamiento para la encapsulación, solo definimos una capa de bucle, el código específico es el siguiente:


int main()
{
    
    
	
	int n = 1;
	scanf("%d", &n);

	int ret = 1;//保存n的阶乘(乘法的话一定要初始化为1)
	int i = 1;
	int sum = 0;//保存最终结果

	for (i = 1; i <= n; i++)
	{
    
    
		ret *= i;
		sum += ret;
	}

	printf("%d\n", sum);
	return 0;
}

5.2 Ejemplo 2: Problema de bucle infinito

Primero, le daré un fragmento de código, y puede adivinar qué generará el programa al final:

int main()
{
    
    
    int i = 0;
    int arr[10] = {
    
     1,2,3,4,5,6,7,8,9,10 };
    for (i = 0; i <= 12; i++)
    {
    
    
        arr[i] = 0;
        printf("hehe\n");
    }
    return 0;
}

Creo que la primera impresión de la mayoría de los amigos cuando ven este programa es que el subíndice de la matriz es [0-9], pero aquí es [<=12]. El problema obvio es que el acceso a la matriz está fuera de los límites.

Pero, ¿realmente es así?, las viejas reglas ejecutan el programa directamente para ver si es lo que pensamos al final.

inserte la descripción de la imagen aquí

Oye... ¿Descubrimos por qué el resultado es un ciclo interminable de impresión? A continuación, si desea resolver este problema, ¿puede analizar el motivo a simple vista? Entonces necesitas usar la depuración.

Al principio, ingresamos al ciclo, inicializamos la matriz y obtenemos los siguientes resultados.
inserte la descripción de la imagen aquí
Luego ingresamos al ciclo, cambiamos los elementos de la matriz a [0] y continuamos hasta la operación de [arr[9]]. Está dentro de nuestra rango normal.

inserte la descripción de la imagen aquí

Entonces, ¿qué pasa con el próximo [arr[10]]? ¿Qué pasa con [arr[11]] y [arr[12]]? ¿Cómo son? Sigamos con la depuración
inserte la descripción de la imagen aquí

De la figura anterior, podemos encontrar que todavía opera en él, entonces, ¿por qué todavía podemos acceder a la posición [arr[10]]? A continuación te explico

inserte la descripción de la imagen aquí
Porque cuando nuestra matriz se almacena en la memoria, hay un espacio de almacenamiento continuo, y también hay un cierto espacio después de la matriz, y este espacio de direcciones continuo existe debajo del espacio de marco de pila de la función [principal]. vista, los espacios de almacenamiento son todos continuos, por lo que también se puede acceder al espacio después de la matriz.

Además de este problema, hay otro problema evidente, no sé si te habrás dado cuenta, es que cuando lo ejecutamos hasta el final, los dos valores de [i] y [arr[12]] inesperadamente se convierte en 0 al mismo tiempo. ¿Por qué es esto? ¿Paño de lana?
inserte la descripción de la imagen aquí
A continuación, tomamos la dirección de los dos respectivamente, y podemos encontrar que los dos en realidad apuntan a la misma parte del espacio de direcciones.
inserte la descripción de la imagen aquí

En este punto, podemos pensar que para la variable [i], debería estar ubicada en los dos últimos dígitos de la posición final de todo el arreglo, solo así se cambiará el valor de [i] cuando el arreglo esté accedido fuera de los límites, y finalmente modificar este bloque Al cambiar el valor en el espacio del bloque, el valor de la variable de bucle [i] se modifica, por lo que el valor de [i] nunca puede llegar a 13, por lo que habrá un infinito bucle de impresión.

A continuación, echemos un vistazo más de cerca al diseño de la memoria.

En primer lugar, sabemos que [i] y [arr] son ​​elementos de variables locales, y las variables locales se colocan en el área de la pila en la memoria, y el hábito de usar el área de la pila es usar primero el espacio de direcciones alto y luego el poco espacio de direcciones (esto es muy importante), se puede encontrar que la dirección de la variable a es más grande que la dirección de la variable b.
inserte la descripción de la imagen aquí

Entonces, ¿qué es exactamente el área de pila en la memoria? Tomemos la siguiente imagen como ejemplo.
inserte la descripción de la imagen aquí
Tan pronto como el programa ingrese al marco de la pila de funciones de la función [principal], primero abrirá un espacio para la variable [i], y luego algunas posiciones pueden quedar vacantes para abrir espacio para diez elementos en el [arr ] array, de acuerdo con lo anterior, podemos encontrar que varios puestos han sido vacantes, entonces, ¿por qué deberían estar vacantes? (Hay preguntas de la universidad aquí)

Esto no está estipulado por mí ni por nadie más, el tamaño en el medio depende del compilador
1. Bajo el compilador VC6.0, no hay espacio adicional en el medio
2. En gcc, el compilador bajo el entorno Linux, cree Habrá un entero entre las variables locales, es decir, 4 bytes
3. En editores como VS 2013/2019/2022, habrá dos enteros en el medio, es decir, 8 bytes.

Por lo tanto, aunque este código se ejecuta en diferentes compiladores, aunque se obtiene el fenómeno de bucle infinito, la implementación subyacente es algo diferente.

Entonces podemos saber que la matriz es de menor a mayor durante el uso normal, pero ¿la dirección de la matriz también es así? Vamos a probarlo.

inserte la descripción de la imagen aquí

De la figura anterior, podemos encontrar que la dirección de cada elemento de la matriz cambia de menor a mayor.

Con estas reservas de conocimiento, podemos mirar hacia atrás a la pregunta original y responderla bien:

Cuando se inicia el programa, primero se crea la variable [i] y primero se abre el espacio de direcciones en la memoria, mientras que el espacio de direcciones de la matriz [arr] se abre más tarde. Sin embargo, recién ahora sabemos que el subíndice de la matriz y el orden de cambio de dirección de los elementos de la matriz son de menor a mayor, mientras que la pila en la memoria usa primero la dirección alta y luego la dirección baja, por lo que cuando la matriz se accede al revés, es posible encontrar la variable [i] y sobrescribirla, por lo que es posible cambiar el valor de la variable de ciclo a otra cosa, lo que hará que la condición de finalización del ciclo no se cumpla, lo que resultará en el fenómeno de la impresión de bucle infinito. (Eso es todo por ahora)

Por lo tanto, la solución correcta es cambiar nuestra condición de fin de bucle.


Resumir:

A través del estudio de este período, creo que cuando encuentra un error en el programa, no irá directamente al programa para agregar, eliminar, verificar y modificar. Puede decir que esto es difícil, ¡pero el dicho va bien! (La mejor manera de tener miedo al miedo es conquistarlo)

¡Eso es todo por este tema! ¡Gracias por mirar, si te es útil, recuerda apoyarlo tres veces seguidas!

Supongo que te gusta

Origin blog.csdn.net/m0_56069910/article/details/129260050
Recomendado
Clasificación