Lenguaje C: entorno de programación y preprocesamiento (no hay necesidad de preocuparse por olvidar los conocimientos de preprocesamiento)

Prefacio:

Primero, comprendamos brevemente el entorno del programa, luego resumamos en detalle la compilación y la vinculación en el entorno de traducción, y luego resumamos la compilación y el preprocesamiento.

1. Entorno del programa

En cualquier implementación de ANSI C , existen dos entornos diferentes.

  1. Entorno de traducción: este entorno es donde el código fuente se convierte en instrucciones de máquina ejecutables.
  2. Entorno de ejecución: ejecutar código binario.

¿Cómo ejecuta una computadora instrucciones binarias?

  • El código en lenguaje C que escribimos es información de texto que la computadora no puede entender directamente.
  • Entorno de traducción: código de lenguaje C -> código binario. (Precompilación, compilación, ensamblaje, vinculación)
  • Entorno de ejecución: ejecutar código binario.

2. Compilar + Enlace

2.1 Entorno de traducción

Compilación del programa + proceso de vinculación:
Insertar descripción de la imagen aquí

  • Cada archivo fuente que compone un programa se convierte en código objeto mediante el proceso de compilación.
  • El vinculador agrupa cada archivo objeto para formar un programa ejecutable único y completo .
  • El vinculador también introducirá cualquier función en la biblioteca de funciones C estándar (ANSI C) utilizada por el programa , y ​​puede buscar en la biblioteca de programas personal del programador y vincular las funciones que necesita al programa. (Biblioteca de enlaces)
    Nota:
    El entorno de desarrollo VS2019 integra:
    compilador (cl.exe) + enlazador (link.exe) + depurador

2.2 Varias etapas de compilación

Cree dos archivos fuente:
sum.c

int sum(int num1,int num2)
{
    
    
return num1+num2;
}

prueba.c

#include <stdio.h>
extern int sum(int,int);//声明外部函数
int main()
{
    
    
int n=sum(1,2;
printf("%d\n",n);
return 0}

Los entornos de desarrollo integrados como VS2019 no son convenientes para observar los detalles de la compilación, aquí uso el sistema Linux y el compilador gcc para observar estos detalles.
La compilación se divide en dos etapas, compilación + vinculación, donde la compilación se puede subdividir en precompilación + compilación + ensamblaje. (Compilar de forma aislada y vincular)
Para ver qué sucede en cada paso durante la compilación, podemos usar comandos para compilar el programa y generar diferentes archivos.
1. Preprocesamiento

gcc -E test.c -o test.i
se detiene después de que se completa el preprocesamiento y los resultados generados después del preprocesamiento se colocan en el archivo test.i.
Insertar descripción de la imagen aquí

2.compilar

gcc -s test.c (gcc -s test.i)
se detiene una vez completada la compilación y los resultados se guardan en test.s.

3.compilación

gcc -c test.c (gcc -c test.s)
se detiene una vez que se completa el ensamblaje y el resultado se guarda en test.o.

4.Enlaces

gcc test.o -o test
Deténgase después de completar el enlace y genere el programa ejecutable test.exe.

El proceso específico se muestra en la figura:
Insertar descripción de la imagen aquí

2.3 Entorno operativo

El proceso de ejecución del programa:
1. El programa debe cargarse en la memoria En un entorno con sistema operativo: Generalmente esto lo completa el sistema operativo. En un entorno independiente, la carga del programa debe organizarse manualmente o colocando el código ejecutable en la memoria de solo lectura.
2. Comienza la ejecución del programa. Entonces se llama a la función principal .
3. Comience a ejecutar el código del programa . En este momento, el programa utilizará una pila de tiempo de ejecución (también llamada marco de pila de funciones ) para almacenar las variables locales y la dirección de retorno de la función. Los programas también pueden utilizar memoria estática. Las variables almacenadas en la memoria estática conservan sus valores durante la ejecución del programa.
4. Finalice el programa . Una terminación normal de la función principal también puede resultar en una terminación inesperada .

3. Preprocesamiento

3.1 Símbolos predefinidos

__FILE__        //进行编译的源文件
__LINE__        //文件当前的行号
__DATE__        //文件被编译的日期
__TIME__        //文件被编译的时间
__STDC__        //如果编译器遵循 ANSI C,其值为1,否则未定义(当前VS是不支持ANSI C(标准c))。

Estos símbolos predefinidos están integrados en el idioma .
Como se muestra en la imagen:
Insertar descripción de la imagen aquí

3.2 Uso de #definir

3.2.1 #definir identificador de definición

gramática:

definir cosas de nombres

ejemplo:

#define MAX 1000//给1000定义一个标识符
#define REG register //为register这个关键字,创建一个简短的名字
#define DO_FOREVER for(;;)//用更形象的符号来替换一种实现
#define CASE break;case//在写case的时候,自动把break加上
#define DEBUG_PRINT printf("file:%s\tline:%d\tdatE:%s\ttime:%s\n",__FILE__,\
__LINE__,__DATE__,__TIME__);//如果定义的标识符过长,可以分成几行写,除了最后一行外,每行的后面都加一个‘\’反斜杠,代表连续符

3.2.2 #definir definir macro

El mecanismo #define incluye una disposición que permite sustituir parámetros en el texto. Esta implementación a menudo se denomina macro o macro de definición.
#define nombre de macro {lista de parámetros formales) cadena
donde la lista de parámetros formales es una tabla de símbolos separada por comas, que puede aparecer en una cadena.
Nota:
el corchete izquierdo de la lista de parámetros debe ir seguido de cerca por el nombre de la macro.
Si existe algún espacio en blanco en el medio, la lista de argumentos se interpreta como parte de la cadena.
Por ejemplo:
definir una macro

#definir CUADRADO(s) s*s

Observe el siguiente código

int a=5;
printf(“%d\n”,SQUARE(a+1));
A primera vista, puedes pensar que este código imprimirá el valor 36. De hecho, imprime 11
porque, la macro, antes Al compilar, el texto se reemplaza y el parámetro s se reemplaza con a+1, por lo que esta declaración en realidad se convierte en
printf ("%d\n", a+1*a+1);

Se puede ver que las expresiones generadas por sustitución no se evalúan en el orden esperado, agregar dos paréntesis a la definición de macro resuelve el problema.

#definir CUADRADO(s) (s)*(s)

resultado:

printf(“%d\n”,(a+1) (*a+1));

Consejo:
Todas las definiciones de macro utilizadas para evaluar expresiones numéricas deben estar entre corchetes de esta manera para evitar interacciones impredecibles entre operadores en parámetros u operadores adyacentes cuando se usan macros.

3.2.3 #definir reglas de reemplazo

Al expandir #define símbolos y macros definidos en un programa, se requieren varios pasos:
1. Al llamar a una macro, primero verifique los parámetros para ver si contienen algún símbolo definido por #definee. Si los hay, se reemplazan primero.
2. Luego, el texto de reemplazo se inserta en el programa en la ubicación del texto original. Para las macros, los nombres de los parámetros se reemplazan por sus valores.
3. Finalmente, el archivo resultante se escanea nuevamente para ver si contiene algún símbolo definido por #define. Si es así, repita el proceso anterior.
Nota:
1. Los símbolos definidos por otros #define pueden aparecer en parámetros macro y definiciones #define. Pero para las macros, la recursividad no puede ocurrir.
2. Cuando el preprocesador busca símbolos definidos por #define, no se busca el contenido de las constantes de cadena.

3.2.4 Propósito de # y ##

Propósito de "#":
Insertar descripción de la imagen aquí
Al observar esta función y este resultado, encontramos que las cadenas tienen las características de concatenación automática.
Insertar descripción de la imagen aquí
Al observar esta función y este resultado, encontramos que la cadena solo se puede colocar en la cadena cuando la cadena se usa como parámetro macro. Al
Insertar descripción de la imagen aquí
observar esta función y este resultado, encontramos que usar # puede convertir un parámetro macro en la cadena correspondiente.
código. será procesado por el preprocesador como#valor valor" .

El propósito de "##":
puede combinar los símbolos de ambos lados en un solo símbolo.
Permite definiciones de macros para crear identificadores a partir de fragmentos de texto separados.
Insertar descripción de la imagen aquí
Insertar descripción de la imagen aquí
Al observar estas dos funciones y resultados, encontramos que cuando se define la macro, la conexión "##" debe generar un identificador legal; de lo contrario, el resultado no está definido.

3.2.5 Parámetros macro con efectos secundarios

Cuando los parámetros de macro aparecen más de una vez en la definición de una macro , si los parámetros tienen efectos secundarios, puede correr peligro al utilizar esta macro, lo que tendrá consecuencias impredecibles. Los efectos secundarios son efectos permanentes que ocurren cuando se evalúa una expresión .
ejemplo:

#include <stdio.h>
#define MAX(a,b) ((a)>(b)?(a):(b))
int main()
{
    
    
    int x = 5;
    int y = 8;
    int z = MAX(x++, y++);
    printf("x=%d\ty=%d\tz=%d\n", x, y, z);
    return 0;
}

Resultados de impresión:
Insertar descripción de la imagen aquí
la macro MAX aquí tiene efectos secundarios y los parámetros se utilizan varias veces en la definición.

El resultado después del procesamiento por el procesador:
z=((x++)>(y++)?(x++):(y++));
primero compare x=5, y=8, y luego agregue 1 a ambos; en este momento, x =6, y =9, debido a que el resultado de la comparación es falso, entonces y se asigna a z y luego se suma uno, entonces x=6, y=10, z=9,

3.2.6 Comparación de macros y funciones

Las macros suelen realizar operaciones sencillas.
Por ejemplo, encuentre el mayor de dos números.

#definir MAX(a, b) ((a)>(b)?(a):(b))

Aquí, ¿por qué no utilizamos funciones para realizar esta tarea?
Ventajas de las macros
1. El código utilizado para llamar funciones y regresar de funciones puede llevar más tiempo que realizar este pequeño trabajo de cálculo. Por lo tanto, las macros son mejores que las funciones en términos de tamaño y velocidad del programa.
2. Más importante aún, los parámetros de la función deben declararse como un tipo específico ,
por lo que la función solo se puede usar en expresiones del tipo apropiado . Por el contrario, las macros se pueden aplicar a números enteros, enteros largos, punto flotante y otros tipos que se pueden comparar.
Las macros son independientes del tipo.
<font color=red size=>Desventajas de las macros
Por supuesto, las macros también tienen desventajas en comparación con las funciones:
1. Cada vez que se utiliza una macro, se insertará una copia del código de definición de la macro en el programa. Si la macro es larga , puede aumentar significativamente la duración del programa .
2. Las macros no se pueden depurar.
3. Las macros no son lo suficientemente rigurosas porque no tienen nada que ver con los tipos .
4. Las macros pueden causar problemas con la prioridad de las operaciones, haciendo que el programa sea propenso a errores.

Por ejemplo: las macros pueden hacer cosas que las funciones no pueden hacer. Los parámetros de las macros pueden ser tipos, pero las funciones no pueden hacer eso.

#define MALLOC(num,type) (type*)malloc(num*sizeof(type));
//使用
MALLOC(20, int);//类型做参数
//经过预处理器替换后
(int*)malloc(10 * sizeof(int));

Comparación entre macros y funciones:
Insertar descripción de la imagen aquí
Suplemento:
Marco de pila de funciones:

  1. Funciones de llamada
  2. Crear un marco de pila de funciones
  3. Cálculo
  4. función de retorno

inline (función en línea):
Tiene las ventajas de macros y funciones. (No hay dos procesos: llamar a la función y devolver la función )

3.2.7 Convención de nomenclatura

La sintaxis de uso de funciones y macros es similar, por lo que el lenguaje en sí no tiene forma de distinguir entre las dos.
Valor predeterminado del programador:
los nombres de las macros deben estar en mayúsculas
y los nombres de las funciones no deben estar en mayúsculas.

3.3 El papel de #undef

La función de #undef es eliminar una definición de macro.
Si se va a redefinir el nombre existente, primero se debe eliminar su nombre anterior.

#include <stdio.h>
#define MAX 10
int main()
{
    
    
    printf("%d\n", MAX);
#undef MAX
#define MAX 20
    printf("%d\n", MAX);
    return 0;
}

Resultado de impresión:
Insertar descripción de la imagen aquí

3.4 Definición de línea de comando

Muchos compiladores de C ofrecen la posibilidad de definir símbolos en la línea de comando. Se utiliza para iniciar el proceso de compilación.
Por ejemplo: esta característica es útil cuando queremos compilar diferentes versiones de un programa basadas en el mismo archivo fuente. (Supongamos que en un programa se declara una matriz de cierta longitud. Si la memoria de la máquina es limitada, necesitamos una matriz muy pequeña, pero si la memoria de otra máquina es más grande, necesitamos una matriz que pueda ser más grande).

El entorno VS no se puede demostrar, puede utilizar el compilador gcc para demostrarlo.

#include <stdio.h>
int main()
{
    
    
int arr[arr1];
int i =0 ;
for(i = 0; i < arr1; i++)
{
    
    
arr[i] = i;
}
for( i = 0; i < arr1; i++)
{
    
    
printf("%d ",arr[i]);
}
printf("\n");
    return 0;
}

Insertar descripción de la imagen aquí

  1. ls abre el menú de la carpeta actual
  2. gcc test.c -D arr1=15 -o test -D especifica la variable y le asigna un valor -o compila el archivo test.c (use el compilador gcc para compilar el archivo test.c y especifica el valor de arr1 como 15 ) (gcc -D arr1= 10 prueba.c)
  3. ./test .Directorio actual/archivo de prueba (compila archivos en el directorio actual)

3.5 Compilación condicional

El propósito y la función de la compilación condicional:
1. La compilación condicional se refiere al uso de condiciones para controlar si un determinado programa en el programa fuente está incluido en la compilación.
2. Seleccione el código requerido y compílelo de acuerdo con las condiciones, de modo que genere diferentes aplicaciones para diferentes usuarios (diferentes plataformas de software y hardware).
3. La compilación condicional también puede facilitar la depuración del programa pieza por pieza y simplificar el proceso de depuración.

Al compilar un programa, es muy conveniente si queremos compilar o abandonar una declaración (un grupo de declaraciones). Porque tenemos directivas de compilación condicional.
Por ejemplo:
es una pena eliminar el código de depuración, pero mantenerlo es un obstáculo, por lo que podemos compilarlo selectivamente.

#include <stdio.h>
//#define __DEBUG__
int main()
{
    
    
    int i = 0;
    int arr[10] = {
    
     0 };
    for (i = 0; i < 10; i++)
    {
    
    
        arr[i] = i;
#ifdef __DEBUG__
        printf("%d\n", arr[i]);
#endif
    }
    for (i = 0; i < 10; i++)
    {
    
    
        printf("%d ", arr[i]);
    }
    return 0;
}

Insertar descripción de la imagen aquí
Directivas de compilación condicional comunes:

1. Compilación condicional

#if expresión constante
//…………
#endif
//La expresión constante es evaluada por el preprocesador
Por ejemplo:
#define DEBUG 1
#if DEBUG //Establecido
//…………Ejecución #endif // Función
de marca final : If
el resultado de la expresión constante es verdadero (distinto de cero), entonces se realizará la compilación.

2. Compilación condicional de múltiples ramas

#if expresión constante 1
sección 1//…
#elif expresión constante 2
sección 2//…
#else
sección 3//… #función
endif
:
si el valor de la expresión 1 es verdadero (no 0), compila el segmento 1 del programa Si el valor de la expresión 2 es verdadero (no 0), compile el segmento 2 del programa; de lo contrario, compile el segmento 3 del programa, lo que puede permitir que el programa complete diferentes funciones en diferentes condiciones.

3. Determine si está definido
// símbolo significa símbolo Una vez que se define el símbolo, se ejecutará el segmento del programa.

1. #si segmento de programa
definido(símbolo) #endif 2. #ifdef(símbolo) segmento de programa #endif 3. #if ! segmento de programa definido(símbolo) #endif 4. #ifndef(símbolo) segmento de programa #endif//Fin Función de bandera : si el identificador ha sido definido por #define, compila el segmento del programa.















4. Instrucciones anidadas

#if definido(os_UNIX)
#ifdef OPCIÓN1
Unix_opción();
#endif
#ifdef OPCIÓN2
Unix_opción2();
#endif
#elif definido(os_MISDOS)
#ifdef OPCIÓN2
Misdos_opción2();
#endif
#endif
Problema de anidamiento, preste atención al emparejamiento endif

ejemplo:

#include <stdio.h>
#define MAX 100
#define MIN 1
int main()
{
    
    
#if defined(MAX)
    #ifdef MAX
      printf("1\n");
    #endif
    #ifdef MIN
      printf("2\n");
    #endif
#elif defined(MIN)
    #ifdef OPTION2
    printf("3\n");
    #endif
#endif
    return 0;
}

Resultado de impresión:
Insertar descripción de la imagen aquí

Nota:
1. La lógica implementada por la compilación condicional se puede implementar usando declaraciones condicionales, pero las declaraciones condicionales compilarán todo el programa fuente, y el programa de código de destino generado será más largo y el tiempo de ejecución será más largo.
2. Al utilizar la compilación condicional, de acuerdo con las condiciones solo se pueden compilar el segmento de programa 1 o el segmento de programa 2. El programa de destino generado será más corto y el tiempo de ejecución será más corto.

3.6 Inclusión de archivos

La directiva #include puede hacer que se compile otro elemento. Su propósito es reemplazar todos los archivos incluidos en la ubicación de la directiva #include. (Durante el preprocesamiento, primero elimine esta instrucción y luego reemplace todo el contenido del archivo incluido)

3.6.1 Cómo se incluyen los archivos de encabezado

  • El archivo local contiene:

#incluir “miarchivo”

Estrategia de búsqueda: primero busque en el directorio del archivo fuente. Si no se encuentra el archivo de encabezado, el compilador buscará el archivo de encabezado en la ubicación de la ruta estándar al igual que el archivo de encabezado de la función de biblioteca; si no lo encuentra, solicitará un error de compilación.

  • Los archivos de la biblioteca incluyen:

#incluir <stdio.h>

Estrategia de búsqueda: vaya directamente a la ruta estándar para buscar el archivo de encabezado, si no lo encuentra, se generará un error de compilación.

  • Resumen:
    Podemos usar el formulario "" para incluir archivos de biblioteca, pero la eficiencia de búsqueda es menor y no es fácil distinguir si se trata de un archivo de biblioteca o un archivo local.

La ruta a los archivos de encabezado estándar para el entorno Linux:

/usr/incluir

Ruta al archivo de encabezado estándar del entorno VS:
ruta predeterminada de vs2013

c:\Archivos de programa (x86)\microsoft Visual Studio 12.0\vc\include

3.6.2 Inclusión de archivos anidados

Diagrama de inclusión de archivos anidados:
Insertar descripción de la imagen aquí
puede ver que los módulos comunes de test.h y test.c contienen dos módulos comunes de common.h y common.c, por lo que el contenido del archivo se repite.

Cómo manejar la inclusión de archivos anidados:
para resolver el problema, utilizamos la compilación condicional
de dos maneras:

#ifndef __TEST_H//Escribe según el nombre del archivo
#define __TEST_H
//El contenido del archivo de encabezado
#endif//Marca de fin

o:

#pragma once // Aviso de compilación, el archivo de encabezado solo se incluye una vez

4. Otras instrucciones de preprocesamiento

#error
#pragma
#linea
……

Supongo que te gusta

Origin blog.csdn.net/plj521/article/details/132458181
Recomendado
Clasificación