O tutorial de linguagem C mais forte da história ---- compilação e pré-processamento de programas (2)

contente

3. Pré-processamento detalhado

3.1 Símbolos predefinidos

3.2 #definir

3.2.1 #define definir identificador

3.2.2 #define definir macro

3.2.3 #definir regra de substituição

3.2.4 # e ##

3.2.5 Parâmetros de macro com efeitos colaterais

3.2.6 Comparação de macros e funções

3.2.7 Convenções de nomenclatura

3.3 #undef

3.4 Definição de Linha de Comando

3.5 Compilação condicional

3.6 Inclusão de arquivo

3.6.1 Como os arquivos de cabeçalho são incluídos

3.6.2 Inclusão de arquivo aninhado


3. Pré-processamento detalhado

3.1 Símbolos predefinidos

__FILE__      //进行编译的源文件
__LINE__     //文件当前的行号
__DATE__    //文件被编译的日期
__TIME__    //文件被编译的时间
__STDC__    //如果编译器遵循ANSI C,其值为1,否则未定义,VS中是未定义的,但GCC是已经定义的

Esses símbolos predefinidos são incorporados à linguagem.

Aqui está um exemplo:

#include<stdio.h>
int main()
{
	printf("%s\n", __FILE__);
	printf("%d\n", __LINE__);
	printf("%s\n"__DATE__);
	printf("%s\n", __TIME__);
	//printf("%d", __STDC__);在vs中不遵循ANSI C,是未定义行为
	return 0;
}

Execute a captura de tela:

Aplicação: Registro

#include<stdio.h>
int main()
{
	int i = 0;
	FILE* pf = fopen("log.txt", "w");
	if (pf == NULL)
	{
		return 1;
	}
	for (i = 0; i < 10; i++)
	{
		fprintf(pf,"%s %s %s %d\n", __DATE__, __TIME__, __FILE__, __LINE__);
	}
	fclose(pf);
	pf = NULL;
	return 0;
}

Execute a captura de tela:

3.2 #definir

3.2.1 #define definir identificador

语法: 
    #define name stuff

por exemplo:

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

Descrição: Ao definir um identificador, deseja adicionar ; no final?

por exemplo:

#define MAX 1000;
#define MAX 1000

Recomenda-se não adicionar ; , o que pode facilmente levar a problemas.

Por exemplo o seguinte cenário:

if(condition) 
    max = MAX;
else 
    max = 0;

Há um erro de sintaxe aqui (caso contrário, não há correspondência if). A essência da macrodefinição deve ser esclarecida aqui.A essência da macrodefinição é a substituição.

3.2.2 #define definir macro

O mecanismo #define inclui uma provisão que permite que os parâmetros sejam substituídos em texto, uma implementação comumente chamada de macro ou define macro.

Aqui está como a macro é declarada:

#define name( parament-list ) stuff

A lista de parâmetros é uma lista de símbolos separados por vírgulas que podem aparecer em coisas.

Nota: O parêntese esquerdo da lista de parâmetros deve ser imediatamente adjacente ao nome. Se houver algum espaço em branco no meio, a lista de argumentos será interpretada como parte do material.

Tal como:

#define SQUARE( x ) x * x

Esta macro recebe um parâmetro x .

Se após a afirmação acima, você colocar

SQUARE( 5 );

Colocado em um programa, o pré-processador substitui a expressão acima por esta expressão:

5 * 5

Aviso: há um problema com esta macro:

Observe o trecho de código abaixo:

int a = 5;
printf("%d\n" ,SQUARE( a + 1) );

À primeira vista, você pode pensar que esse código imprimirá o valor 36.

Na verdade, ele imprimirá 11.

Por quê?

替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:
printf ("%d\n",a + 1 * a + 1 );

Isso deixa claro que as expressões resultantes da substituição não são avaliadas na ordem esperada.

Este problema é facilmente resolvido adicionando dois parênteses à definição da macro:

#define SQUARE(x) (x) * (x)

Este pré-processamento produz o efeito esperado:

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

Aqui está outra definição de macro:

#define DOUBLE(x) (x) + (x)

Usamos parênteses na definição para evitar o problema anterior, mas esta macro pode introduzir novos erros.

int a = 5;
printf("%d\n" ,10 * DOUBLE(a));

Qual valor será impresso?

Parece que imprime 100, mas na verdade imprime 55. Descobrimos que depois de substituir:

printf ("%d\n",10 * (5) + (5));

A operação de multiplicação precede a adição definida pela macro, então não há

55

A solução para este problema é adicionar um par de parênteses em torno da expressão de definição de macro.

#define DOUBLE( x)   ( ( x ) + ( x ) )

dica:

Portanto, as definições de macro usadas para avaliar expressões numéricas devem ser colocadas entre parênteses dessa maneira para evitar interações imprevisíveis entre operadores em argumentos ou operadores adjacentes ao usar a macro.

3.2.3 #definir regra de substituição

Existem várias etapas envolvidas ao expandir #define para definir símbolos e macros em um programa.

  1. Ao chamar a macro, os argumentos são verificados primeiro para ver se contêm algum símbolo definido por #define. Em caso afirmativo, eles são substituídos primeiro.
  2. O texto de substituição é então inserido no programa no lugar do texto original. Para macros, os nomes dos parâmetros são substituídos por seus valores.
  3. Finalmente, o arquivo resultante é verificado novamente para ver se contém algum símbolo definido por #define. Se sim, repita o processo acima.

Perceber:

  1. Outros símbolos definidos por #define podem aparecer em parâmetros de macro e definições de #define. Mas para macros, a recursão não pode ocorrer.
  2. Quando o pré-processador procura por símbolos definidos por #define, o conteúdo das constantes de string não é procurado. (A definição de macro na string não será substituída)

3.2.4 # e ##

Como inserir parâmetros em strings?

Primeiro vamos ver este código:

char* p = "hello ""bit\n";
printf("hello"," bit\n");
printf("%s", p);

A saída aqui é olá bit?

A resposta é definitiva: sim.

Descobrimos que as strings têm as características de concatenação automática.

1. Podemos escrever código assim? :

#define PRINT(FORMAT, VALUE)\
 printf("the value is "FORMAT"\n", VALUE);
...
PRINT("%d", 10);
//上述宏定义替换之后的结果如下所示:
printf("the value is " "%d" "\n",10);

resultado da operação:

Aqui você pode colocar uma string em uma string somente quando a string é usada como um parâmetro de macro.

1. Outro truque é usar # para transformar um parâmetro de macro em uma string correspondente.

por exemplo:

int i = 10;
#define PRINT(VALUE)\
 printf("the value of " #VALUE "is %d \n", VALUE);
...
PRINT( i + 3);//产生了什么效果?
//上面的这段代码会替换为:
printf("the value of " "value" "is %d \n",VALUE);

O #VALUE no código é processado pelo pré-processador como:

"VALOR"

A saída final deve ser:

the value of i+3 is 13

O papel de ##

## pode combinar os símbolos de ambos os lados em um símbolo.

Ele permite que as definições de macro criem identificadores de partes separadas de texto.

#define ADD_TO_SUM(sumnum, value) \
 sum##num += value;
...
ADD_TO_SUM(5, 10);//作用是:给sum5增加10

NOTA: Essas conexões devem gerar um identificador válido. Caso contrário, o resultado é indefinido.

3.2.5 Parâmetros de macro com efeitos colaterais

Quando um parâmetro de macro aparece mais de uma vez na definição de macro, se o parâmetro tiver efeitos colaterais, você poderá ser perigoso ao usar a macro, levando a consequências imprevisíveis. Um efeito colateral é um efeito permanente que ocorre quando uma expressão é avaliada.

Por exemplo:

x+1;//不带副作用
x++;//带有副作用

A macro MAX pode provar problemas causados ​​por argumentos com efeitos colaterais.

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?

Aqui temos que saber qual é o resultado do processamento do pré-processador:

z = ( (x++) > (y++) ? (x++) : (y++));

Então a saída é:

x=6 y=10 z=9

3.2.6 Comparação de macros e funções

As macros geralmente são usadas para realizar operações simples.

Por exemplo, encontre o maior de dois números.

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

Então, por que não usar funções para realizar essa tarefa?

Existem duas razões:

  1. O código usado para chamar a função e retornar da função pode levar mais tempo do que realmente realizar esse pequeno trabalho computacional (as funções incluem chamadas de função, operações lógicas e função retornam três partes que levam tempo. Macros só precisam de operações lógicas). Portanto, as macros superam as funções em termos de tamanho e velocidade do programa.
  2. Mais importante, os parâmetros da função devem ser declarados como tipos específicos. Portanto, as funções só podem ser usadas em expressões do tipo apropriado. Por outro lado, como essa macro pode ser aplicada a inteiros, inteiros longos, tipos de ponto flutuante, etc. que podem ser usados ​​para > comparar tipos. As macros são independentes de tipo.

Desvantagens das macros: É claro que as macros também apresentam desvantagens em relação às funções:

  1. Cada vez que uma macro é usada, uma cópia do código de definição da macro é inserida no programa. A menos que a macro seja relativamente curta, ela pode aumentar significativamente a duração do programa.
  2. As macros não podem ser depuradas. (A macro foi substituída no estágio de pré-compilação e o código após a macro ter sido substituído durante a depuração, ou seja, o código depurado e o código depurado real não são o mesmo código)
  3. As macros não são suficientemente rigorosas porque são independentes de tipo.
  4. As macros podem causar problemas com a precedência do operador, tornando os procedimentos propensos a erros.

As macros às vezes podem fazer coisas que as funções não podem. Por exemplo: os parâmetros de macro podem ter tipos, mas as funções não.

#define MALLOC(num, type) (type *)malloc(num * sizeof(type))
...
//使用
MALLOC(10, int);//类型作为参数
//预处理器替换之后:
(int*)malloc(10 * sizeof(int));

Uma comparação de macros e funções

Atributos

#defindedefine macro

função

comprimento do código

O código de macro é inserido no programa cada vez que é usado. Exceto para macros muito pequenas, a duração do programa pode crescer substancialmente

O código da função aparece em apenas um lugar; toda vez que a função é usada, o mesmo código naquele lugar é chamado

velocidade de execução

mais rápido

Há sobrecarga adicional de chamada e retorno de função, por isso é relativamente lento

operador precedente

Os parâmetros de macro são avaliados no contexto de todas as expressões circundantes. A menos que sejam adicionados parênteses, a precedência de operadores adjacentes pode ter consequências imprevisíveis, por isso é recomendável que as macros sejam escritas com mais parênteses.

Um parâmetro de função é avaliado apenas uma vez quando a função é chamada e seu valor de resultado é passado para a função. Os resultados da avaliação de expressão são mais previsíveis.

Parâmetros com efeitos colaterais

Os parâmetros podem ser substituídos em vários locais do macrocorpo, de modo que a avaliação de parâmetros com efeitos colaterais pode produzir resultados imprevisíveis.

Os parâmetros da função são avaliados apenas uma vez quando os parâmetros são passados ​​e o resultado é mais fácil de controlar.

Tipo de parâmetro

Os parâmetros de uma macro são independentes de tipo e podem ser usados ​​com qualquer tipo de parâmetro, desde que a operação no parâmetro seja legal.

Os parâmetros da função estão relacionados com o tipo, se os tipos dos parâmetros forem diferentes, são necessárias funções diferentes, mesmo que as tarefas que executam sejam diferentes.

depuração

Macros são inconvenientes para depurar

As funções podem ser depuradas declaração por declaração

recursão

Macros não podem ser recursivas

As funções podem ser recursivas

3.2.7 Convenções de nomenclatura

Em geral, a sintaxe para usar macros para funções é semelhante. Assim, a própria linguagem não pode nos ajudar a distinguir entre os dois.

Então nosso hábito habitual é:

capitalizar nomes de macros

Não use todos os nomes de funções em maiúsculas

3.3 #undef

Esta diretiva é usada para remover uma definição de macro.

#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

3.4 Definição de Linha de Comando

Muitos compiladores C fornecem a capacidade de definir símbolos na linha de comando. Usado para iniciar o processo de compilação.

Por exemplo, esse recurso é útil quando queremos compilar diferentes versões de um programa com base no mesmo arquivo de origem. (Suponha que um programa declare um array de um certo tamanho. Se a memória da máquina for limitada, precisamos de um array pequeno, mas outra memória da máquina é maiúscula e precisamos de um array que possa ser maiúsculo.)

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

Instruções de compilação:

gcc -D ARRAY_SIZE=10 test.c(假设文件名为test.c)

3.5 Compilação condicional

Ao compilar um programa, é conveniente se quisermos compilar ou descartar uma instrução (um grupo de instruções). Porque temos diretivas de compilação condicional.

Por exemplo:

Código de depuração, é uma pena excluí-lo, e é uma pena mantê-lo no caminho, para que possamos compilá-lo seletivamente.

#include<stdio.h>
int main()
{
#if 0
	for (int i = 0; i < 10; i++)
	{
		printf("hello world\n");
	}
#endif
	return 0;
}

Execute a captura de tela:

Suponha que o código seja modificado da seguinte forma:

#include<stdio.h>
int main()
{
#if 1
	for (int i = 0; i < 10; i++)
	{
		printf("hello world\n");
	}
#endif
	return 0;
}

Execute a captura de tela:

Nota: Se o #if for seguido de uma variável, o resultado será semelhante ao #if seguido de 0. Por que isso acontece? Porque as variáveis ​​são geradas após a execução, ou seja, as variáveis ​​têm significados correspondentes após a execução. Depois de executá-lo, as instruções de pré-processamento desapareceram.

Diretivas de compilação condicional comuns:

1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。常量表达式也可以是逻辑表达式,比如0>2为假,返回值为0,就相当于0
如:
#define __DEBUG__ 1
#if __DEBUG__//此时的效果与1完全相同
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
//这两种是来判断是否被定义 
#if !defined(symbol)
#ifndef symbol
//这两种是来判断是否没有被定义
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif

Nota: Na compilação condicional, se o caractere após nosso if não for predefinido ou for predefinido após a compilação condicional, esse caractere será equivalente a 0. Mas como geralmente o usamos em instruções de compilação condicional, a frente geralmente deve aparecer. Aqui também devemos prestar atenção a um ponto, embora os caracteres que não são predefinidos serão padronizados para 0 quando o julgamento condicional da compilação condicional for realizado, mas ao usar a função printf para saída, se não houver um predefinido antes, não pode saída, uma vez que não existe uma diretiva de pré-processamento correspondente para substituí-la durante o pré-processamento.

3.6 Inclusão de arquivo

Já sabemos que a diretiva #include pode fazer com que outro arquivo seja compilado. como se realmente aparecesse no lugar da diretiva #include.

A maneira como essa substituição funciona é simples: o pré-processador primeiro remove essa diretiva e a substitui pelo conteúdo do arquivo de inclusão. Esse arquivo de origem é incluído 10 vezes, na verdade é compilado 10 vezes.

3.6.1 Como os arquivos de cabeçalho são incluídos

  • arquivo local contém
#include "filename"

Estratégia de busca: primeiro procure no diretório onde o arquivo fonte está localizado, caso o arquivo de cabeçalho não seja encontrado, o compilador irá procurar o arquivo de cabeçalho no local padrão assim como o arquivo de cabeçalho da função de biblioteca.

Se não for encontrado, ele solicitará um erro de compilação.

Caminho para arquivos de cabeçalho padrão para ambiente Linux:

/usr/include

Caminho para arquivos de cabeçalho padrão para ambiente VS: (VS2013)

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include

Preste atenção para encontrá-lo de acordo com seu próprio caminho de instalação.

  • arquivo de biblioteca contém
#include <filename.h>

Localize o arquivo de cabeçalho e vá diretamente para o caminho padrão para encontrá-lo, caso não seja encontrado, será exibido um erro de compilação.

É possível dizer que os arquivos de biblioteca também podem ser incluídos na forma de ""?

A resposta é sim, sim. No entanto, a eficiência da pesquisa é menor dessa maneira e, é claro, não é fácil distinguir se é um arquivo de biblioteca ou um arquivo local.

3.6.2 Inclusão de arquivo aninhado

Se tal cenário ocorrer:

comm.h e comm.c são módulos comuns.

test1.he test1.c usam módulos comuns.

test2.he test2.c usam módulos comuns.

test.he test.c usam o módulo test1 e o módulo test2.

Desta forma, haverá duas cópias de comm.h no programa final. Isso resulta na duplicação do conteúdo do arquivo.

Como resolver este problema?

Resposta: Compilação condicional.

No início de cada arquivo de cabeçalho, escreva:

#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif   //__TEST_H__

Motivo: Porque quando o arquivo é incluído pela segunda vez, uma compilação condicional foi realizada anteriormente, ou seja, __TEST_H__ foi definido uma vez, então desta vez, o conteúdo do arquivo em test.h não será incluído duas vezes, isso is Ignore o conteúdo entre a compilação condicional em test.h.

Nota: Não é necessário usar __TEST_H__, mas geralmente é usado o nome do arquivo incluído, principalmente para melhor identificação e distinção.

ou:

#pragma once

Isso evita a duplicação de arquivos de cabeçalho. Essa maneira de escrever geralmente é suportada em novos compiladores.

Acho que você gosta

Origin blog.csdn.net/m0_57304511/article/details/123211919
Recomendado
Clasificación