Funções e estrutura de programas em C
static e extern. Use o pré-processador para cabeçalhos, macros e compilação condicional, divida um programa em arquivos-fonte e arquivos de cabeçalho e compile-o com make para que apenas os arquivos alterados sejam recompilados.BásicoAula 2 de 8 do curso Curso de Programação em C
O que você vai aprender
- Divida um programa em funções que recebem parâmetros e retornam resultados, declare-as com protótipos, entenda que C passa argumentos por valor e rastreie como as chamadas, inclusive as recursivas, são executadas e retornam.
- Defina funções que recebam entradas e retornem um resultadoEscreva definições de funções com tipo de retorno, parâmetros e corpo, use
returnevoidcorretamente e chame funções em expressões. - Declare funções com protótiposDeclare uma função com um protótipo antes de sua primeira chamada, saiba o que o compilador verifica e converte e evite parênteses vazios e declarações implícitas.
- Passe argumentos por valor e retorne o resultadoExplique que cada argumento em C é copiado para seu parâmetro, preveja o que uma função pode ou não alterar e use valores de retorno para devolver resultados.
- Rastreie chamadas de função e recursãoAcompanhe como cada chamada recebe seus próprios parâmetros e variáveis locais e retorna à função chamadora, e escreva e rastreie funções recursivas com um caso-base.
- Defina funções que recebam entradas e retornem um resultado
- Diferencie o escopo de um nome do tempo de vida de uma variável, use variáveis locais estáticas para manter estado entre chamadas e avalie os custos das variáveis globais.
- Determine onde um nome pode ser usado (escopo)Aplique os escopos de bloco, arquivo e protótipo, preveja a qual variável um nome se refere quando há sombreamento e evite erros de escopo.
- Escolha o tempo de vida de uma variável com classes de armazenamentoDistinga a duração de armazenamento automática da estática, use variáveis locais estáticas para manter valores entre chamadas e saiba o que significam auto, register, static e extern.
- Compartilhe dados com variáveis globais com cuidadoDefina e use variáveis de escopo de arquivo (globais), explique sua visibilidade e inicialização e prefira parâmetros e valores de retorno quando uma variável global acrescentaria um acoplamento oculto.
- Determine onde um nome pode ser usado (escopo)
- Use #include, #define e macros com segurança e selecione código em tempo de compilação com diretivas condicionais, macros predefinidas e opções do compilador.
- Inclua arquivos e defina macrosExplique o que o pré-processador faz antes da compilação, inclua cabeçalhos, defina macros constantes e semelhantes a funções e evite as armadilhas clássicas das macros.
- Compile código condicionalmenteUse #if, #ifdef, #ifndef, #elif, #else e #endif com opções -D e macros predefinidas para incluir ou excluir código em tempo de compilação, incluindo proteções contra inclusão múltipla.
- Inclua arquivos e defina macros
- Compile vários arquivos-fonte separadamente e ligue-os, escreva arquivos de cabeçalho que compartilhem declarações e controle quais nomes outros arquivos podem ver com static e extern.
- Compile e ligue vários arquivos-fonteDescreva as etapas do arquivo-fonte ao executável, compile arquivos separadamente em arquivos-objeto, ligue-os e diferencie erros de compilação de erros de ligação.
- Escreva arquivos de cabeçalhoColoque declarações compartilhadas em um cabeçalho com proteção contra inclusão múltipla, inclua-o onde for necessário, inclusive em seu próprio arquivo-fonte, e mantenha definições fora dele.
- Controle a ligação com static e externUse extern para compartilhar uma variável entre arquivos e static para manter funções e variáveis privadas ao seu arquivo, e corrija os erros de ligação causados pelo uso incorreto desses recursos.
- Compile e ligue vários arquivos-fonte
- Escreva makefiles cujas regras e dependências recompilam apenas o que mudou e mantenha-os concisos com variáveis, variáveis automáticas do make, regras de padrão e alvos fictícios.
- Escreva regras e dependências em makefilesEscreva regras com alvos, pré-requisitos e receitas indentadas com tabulação, preveja quais comandos o make executa após uma alteração e liste corretamente as dependências de cabeçalhos.
- Simplifique makefiles com variáveis e regras de padrãoUse variáveis como
CCeCFLAGS, as variáveis automáticas do make$@,$<e$^, regras de padrão, regras internas e alvos fictícios para manter um makefile conciso e correto.
- Escreva regras e dependências em makefiles
Guia de estudo de Funções em C
1Escreva, declare e chame funções
Uma função dá a uma tarefa um nome, um conjunto de entradas e uma maneira clara de produzir um resultado. Você criará definições e protótipos e depois acompanhará os valores nas chamadas e no retorno às funções chamadoras. Rastreamentos curtos tornam as regras visíveis: um parâmetro recebe uma cópia, um valor de retorno precisa ter um destino e uma chamada recursiva deve terminar em algum momento. Os exemplos usam C17; listas explícitas de parâmetros também tornam o código claro ao compilar como C23.
1.1 Defina funções que recebam entradas e retornem um resultado
Uma função reúne uma tarefa para que outra parte do programa possa chamá-la. Dê a ela um nome que descreva essa tarefa e escolha suas entradas e seu resultado antes de escrever seu corpo. Em um programa C comum executado em um ambiente hospedeiro, a execução começa em main.
Crie uma definição
int square(int n)
{
return n * n;
}
A definição tem um tipo de retorno (int), um nome (square), uma lista de parâmetros (int n) e um corpo entre chaves. O parâmetro é a variável local que recebe uma entrada. Um argumento é o valor fornecido por uma chamada: em square(6), esse valor é 6. A chamada produz 36. Defina cada função no nível do arquivo, fora de outras funções; o C padrão não permite definições de funções aninhadas.
Cada parâmetro precisa ter seu próprio tipo. Escreva int max(int a, int b), com uma vírgula separando as declarações. Em uma chamada, forneça valores sem seus tipos: max(8, 3).
Retorne um valor à função chamadora
int max(int a, int b)
{
if (a > b)
return a;
return b;
}
Um return encerra imediatamente a chamada atual. Para max(8, 3), o primeiro retorno é executado, portanto o segundo nunca é alcançado. Uma expressão retornada é convertida para o tipo de retorno da função. Escolha esse tipo de modo a preservar o tipo de resultado necessário.
Para um resultado fracionário, use aritmética de ponto flutuante em todo o cálculo:
double c_to_f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
A chamada c_to_f(37.0) retorna 98.6. Uma versão inteira que usa c * 9 / 5 + 32 retorna 98, porque a divisão descarta a fração. Reordenar para 9 / 5 * c + 32 faz 9 / 5 ser avaliado primeiro como 1, resultando em 69 para 37. Um tipo de retorno de ponto flutuante, por si só, não pode desfazer um cálculo inteiro já realizado dentro do corpo.
Use o resultado
Uma chamada que retorna valor é uma expressão. Com a definição acima, square(3) + square(4) resulta em 25. Em square(square(2)), a chamada interna retorna 4, que se torna o argumento da chamada externa, resultando em 16. Uma instrução como square(5); é válida, mas descarta o valor retornado.
Imprimir e retornar cumprem tarefas diferentes. printf exibe texto; return fornece um resultado que a função chamadora pode armazenar ou usar em outra expressão. Se uma função comum não void alcançar sua chave de fechamento sem retornar e a função chamadora usar seu resultado, o comportamento será indefinido. Compile com -Wall -Wextra para ajudar a encontrar retornos ausentes.
Funções que não produzem valor
Uma função void executa uma tarefa sem retornar um valor. Ela pode terminar antecipadamente com return;. Por exemplo, void print_line(int width) pode imprimir uma linha de traços, mas int x = print_line(5); é inválido porque a chamada não fornece valor. Para não receber entradas, escreva void na lista de parâmetros, como em int read_sensor(void).
int main(void) retorna um código de saída ao ambiente hospedeiro. Use return 0; para indicar sucesso; <stdlib.h> também fornece EXIT_SUCCESS e EXIT_FAILURE. Alcançar o fim de main retorna zero automaticamente. Outras funções que retornam valor não têm essa exceção.
Principais conclusões
- Uma definição informa tipo de retorno, nome, parâmetros tipados e corpo.
- Um parâmetro recebe um argumento fornecido por uma chamada.
returnencerra a chamada; imprimir não fornece seu resultado.- Use
voidpara ausência de valor de retorno e(void)para ausência de parâmetros. - Escolha operações aritméticas e tipos de retorno que preservem o resultado necessário.
1.2 Declare funções com protótipos
Uma chamada de função precisa de uma declaração que o compilador já tenha visto. Se a definição vier depois, coloque um protótipo antes da primeira chamada. O protótipo informa ao compilador como verificar a chamada sem fornecer o corpo da função.
Separe a declaração do corpo
#include <stdio.h>
double c_to_f(double c);
int main(void)
{
printf("%.1f\n", c_to_f(25.0));
return 0;
}
double c_to_f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
O protótipo termina com ponto e vírgula. A definição tem um corpo e não tem ponto e vírgula entre sua lista de parâmetros e a chave de abertura. Uma organização útil é: cabeçalhos, protótipos, main e depois outras definições. A saída aqui é 77.0.
Os nomes dos parâmetros são opcionais em protótipos: double c_to_f(double); declara a mesma função. Nomes como c podem documentar uma entrada, mas não precisam coincidir com os nomes da definição. Todas as declarações devem concordar quanto ao tipo de retorno e aos tipos dos parâmetros. Uma função usada precisa de uma definição; repetir protótipos compatíveis não cria mais funções.
O que o compilador verifica
Com um protótipo, o compilador verifica a quantidade de argumentos e converte cada valor fornecido para o tipo declarado do parâmetro. Dado double c_to_f(double c);, tanto c_to_f() quanto c_to_f(20.0, 5.0) têm a quantidade incorreta e exigem um diagnóstico.
Uma diferença de tipo numérico pode, por outro lado, ser uma conversão permitida:
int half(int n)
{
return n / 2;
}
Para half(7.9), a conversão primeiro dá ao parâmetro o valor 7. A divisão inteira produz então 3. Para half(-7.9), a conversão fornece -7, e a divisão produz -3. Ambas descartam as partes fracionárias em direção a zero. A chamada não preserva o valor double original apenas porque o argumento foi escrito com um ponto decimal. Adicione -Wconversion quando quiser avisos sobre essas conversões.
Indique explicitamente quando não há parâmetros
| Declaração | Significado em C17 | Significado em C23 |
|---|---|---|
int read_sensor(void); | Sem parâmetros | Sem parâmetros |
int read_sensor(); | Informações dos parâmetros não especificadas | Sem parâmetros |
Em C17, uma lista de parâmetros vazia em uma declaração impede a verificação completa dos argumentos; ela não autoriza argumentos arbitrários para a função definida posteriormente. Use (void) de forma consistente. O GCC 15 mudou seu dialeto C padrão para gnu23, portanto -std=c17 torna explícito o padrão pretendido ao acompanhar estes exemplos.
Corrija um problema de declaração
Chamar uma função não declarada viola o C moderno. Inclua o cabeçalho padrão que a declara, como <stdio.h> para printf, ou adicione seu próprio protótipo correspondente. As versões atuais de GCC e Clang normalmente rejeitam declarações implícitas.
Um protótipo como double area(double r); não pode corresponder a uma definição que retorna float e recebe float. Inclua o cabeçalho de um módulo no arquivo que define suas funções para que o compilador possa compará-los. Não tente corrigir uma incompatibilidade de tipos adicionando uma conversão explícita de tipo (cast) na chamada: corrija a declaração e a definição para que descrevam a mesma função.
Principais conclusões
- Coloque uma declaração antes da primeira chamada.
- Um protótipo termina com ponto e vírgula; uma definição fornece o corpo.
- Protótipos verificam a quantidade de argumentos e especificam suas conversões.
- Use
(void)para indicar ausência de parâmetros tanto em C17 quanto em C23. - Inclua o cabeçalho que contém a declaração e mantenha todas as declarações compatíveis.
- Uma conversão numérica permitida ainda pode perder uma fração.
1.3 Passe argumentos por valor e retorne o resultado
C passa todos os argumentos por valor: cada parâmetro começa com uma cópia do valor de seu argumento. Atribuir um valor a um parâmetro inteiro comum altera essa cópia, mesmo que ele tenha o mesmo nome de uma variável da função chamadora.
Rastreie a cópia
Com <stdio.h> incluído e esta definição antes da chamada:
void double_it(int n)
{
n *= 2;
printf("inside: %d\n", n);
}
A função chamadora executa:
int score = 21;
double_it(score);
printf("after: %d\n", score);
A saída é inside: 42 e depois after: 21. O parâmetro muda de 21 para 42; nenhuma instrução atribui um valor à variável score da função chamadora.
Guarde um resultado retornado
int doubled(int n)
{
return n * 2;
}
| Instrução da função chamadora | Efeito em um score inicialmente igual a 21 |
|---|---|
doubled(score); | Descarta 42; score permanece 21 |
score = doubled(score); | Armazena 42 em score |
A função calcula um valor; a atribuição na função chamadora atualiza sua variável. Retornar o valor de uma variável local é seguro: int total = a + b; return total; copia o valor para fora antes que a variável local automática deixe de existir. Cálculos que usam apenas parâmetros e resultados retornados podem ser testados sem preparar um estado compartilhado oculto.
Veja por que uma troca apenas por valor falha
void swap(int a, int b)
{
int t = a;
a = b;
b = t;
}
Após int p = 3, q = 7; swap(p, q);, a função chamadora ainda tem p == 3 e q == 7. O corpo trocou suas cópias. Alterar objetos da função chamadora por meio de parâmetros exige seus endereços, passados como valores de ponteiro.
Sequencie os argumentos quando a ordem for importante
Argumentos podem ser expressões como a + 1 ou square(b). Todos terminam de ser avaliados antes que o corpo da função chamada comece, mas sua ordem de avaliação não é especificada. Duas chamadas a next_ticket(), que altera o estado, em uma única impressão podem fornecer senhas em qualquer ordem. Use instruções separadas:
int first = next_ticket();
int second = next_ticket();
printf("%d %d\n", first, second);
Evite f(i, i++): sua leitura e modificação não sequenciadas de i produzem comportamento indefinido.
Principais conclusões
- Parâmetros recebem cópias dos valores dos argumentos.
- Armazene um resultado retornado com uma atribuição quando a função chamadora precisar de uma atualização.
- Uma troca apenas por valor altera seus parâmetros, não as variáveis da função chamadora.
- Use instruções separadas quando a ordem de avaliação dos argumentos for importante.
1.4 Rastreie chamadas de função e recursão
Uma chamada pausa a função chamadora até que um resultado esteja pronto. Rastrear as chamadas ativas explica como um programa retorna ao lugar certo e por que a recursão precisa de uma regra de parada.
Mantenha um registro por chamada ativa
A maioria das implementações usa uma pilha de chamadas. Cada chamada ativa tem um registro de ativação que guarda os valores de seus parâmetros, as variáveis locais automáticas e o ponto em que a função chamadora deve continuar. O padrão especifica o comportamento sem exigir essa organização específica da memória.
Se main chama print_table e print_table chama c_to_f, a função de conversão retorna primeiro. A execução continua dentro de print_table, que depois retorna a main. Variáveis locais automáticas comuns pertencem a uma chamada. Uma variável local estática é diferente: todas as chamadas compartilham seu único valor armazenado.
Acompanhe a descida de um cálculo recursivo
Uma função recursiva chama a si mesma para um problema menor. Um caso-base retorna diretamente; um caso recursivo avança em direção a ele.
int factorial(int n)
{
if (n <= 1)
return 1;
return n * factorial(n - 1);
}
Para factorial(4), a primeira chamada não pode terminar sua multiplicação até que factorial(3) retorne. Essa chamada espera por factorial(2), que espera por factorial(1).
| Chamada ativa | Operação à espera de um resultado |
|---|---|
factorial(4) | 4 * factorial(3) |
factorial(3) | 3 * factorial(2) |
factorial(2) | 2 * factorial(1) |
factorial(1) | Caso-base: retornar 1 |
Há quatro chamadas ativas de fatorial no ponto mais profundo. Cada uma tem seu próprio n; chegar a n == 1 não altera os valores das chamadas anteriores.
Acompanhe a volta dos retornos
A chamada do caso-base retorna 1. A chamada com n == 2 agora calcula 2 * 1 e retorna 2. A seguinte retorna 3 * 2, ou 6; a primeira retorna 4 * 6, ou 24. Preencha os valores de retorno no rastreamento durante a volta, depois que o resultado da função chamada for conhecido.
A condição de parada deve ser alcançável para as entradas permitidas. Se o teste fosse n == 1, começar em zero continuaria a gerar chamadas com -1, -2 e valores menores em vez de alcançar um. A continuação da recursão esgota recursos e pode encerrar o programa inesperadamente. O teste mais amplo n <= 1 interrompe essas entradas, embora o fatorial matemático seja usado para inteiros não negativos.
Verifique a profundidade e o tipo do resultado
Parar corretamente não garante que o resultado caiba no tipo. Em uma plataforma com int de 32 bits, o maior valor é 2147483647. factorial(12) resulta em 479001600, que cabe; factorial(13) precisaria de 6227020800, que não cabe. O estouro de inteiro com sinal tem comportamento indefinido.
Um laço pode calcular o fatorial mantendo um produto acumulado em uma única chamada de função. A recursão é útil quando o próximo problema tem a mesma estrutura em tamanho menor. Para inteiros positivos, somar algarismos é outro exemplo: sum_digits(472) precisa de 2 + sum_digits(47), depois 2 + 7 + sum_digits(4), resultando em 13. Um rastreamento deve mostrar tanto a redução da entrada quanto a combinação posterior dos valores retornados.
Principais conclusões
- As chamadas terminam na ordem inversa à de seu aninhamento.
- Cada chamada tem seus próprios parâmetros e variáveis locais automáticas.
- A recursão precisa de um caso-base alcançável e de progresso em direção a ele.
- Rastreie as chamadas na descida e os valores retornados na volta.
- Verifique se os resultados recursivos cabem em seu tipo numérico.
2Controle onde as variáveis são visíveis e por quanto tempo existem
Um nome pode ficar visível em uma pequena parte do código-fonte enquanto sua variável existe durante todo o programa. Manter essas duas propriedades separadas facilita muito a compreensão de variáveis locais, locais estáticas e globais. Você acompanhará nomes em blocos aninhados, comparará valores novos com valores mantidos e escolherá como uma função recebe os dados de que precisa. Essa escolha afeta a facilidade de rastrear um resultado incorreto e testar uma função isoladamente.
2.1 Determine onde um nome pode ser usado (escopo)
Escopo é a parte do código-fonte em que um nome pode ser usado. Leia as chaves e declarações para determinar a qual variável uma expressão se refere; a ordem das chamadas não altera essas regras.
Encontre o limite do escopo
Uma variável declarada dentro de um bloco tem escopo de bloco, que começa logo após seu declarador e termina na chave de fechamento desse bloco. Parâmetros são visíveis dentro dos corpos de suas funções. Uma declaração no cabeçalho de um for, como for (int i = 0; i < 10; i++), torna i disponível durante esse laço, mas não depois dele.
Um nome declarado fora de todas as funções tem escopo de arquivo, desde sua declaração até o fim da unidade de tradução. Funções acima dessa declaração precisam de uma declaração anterior para usá-lo. O nome de um parâmetro em um protótipo tem escopo de protótipo, que termina com o protótipo. Assim, double c_to_f(double c); pode ser seguido de uma definição que chama o parâmetro de degrees.
Rastreie o sombreamento de dentro para fora
#include <stdio.h>
int x = 10;
void show(void)
{
printf("%d\n", x);
}
int main(void)
{
int x = 5;
{
int x = 1;
printf("%d\n", x);
}
printf("%d\n", x);
show();
return 0;
}
A impressão interna usa 1, a seguinte usa 5 e show imprime 10. Prevalece a declaração visível mais próxima de x. Sair do bloco interno revela novamente a variável local externa. Chamar show não torna a variável local de main visível dentro dela: show foi definida onde o x de escopo de arquivo é visível.
Funções diferentes podem declarar, cada uma, uma variável local i, count ou result. Elas são variáveis diferentes porque suas declarações pertencem a blocos diferentes.
Declare um valor onde seus usos posteriores possam vê-lo
Este código não compila:
if (temp > 30.0) {
int warning = 1;
}
printf("%d", warning);
A impressão está fora do escopo de warning. Se o valor precisar ser usado depois, declare-o e inicialize-o antes da condicional:
int warning = 0;
if (temp > 30.0) {
warning = 1;
}
printf("%d", warning);
A inicialização também fornece o valor para o caminho em que a condição é falsa.
Evite criar uma nova variável por acidente
Declarar int total = 0; dentro de um laço cria uma nova variável local cada vez que se entra em seu bloco. Se você pretendia acumular em um total externo, use uma atribuição como total += value;. Ative -Wshadow para ajudar a encontrar nomes ocultados.
O escopo começa antes de o inicializador ser avaliado. Dentro de um bloco, int x = x + 1; lê o novo x, não inicializado, mesmo que exista um x externo. Use outro nome se o inicializador precisar do valor externo.
O escopo descreve onde um nome é visível no código-fonte. A duração de armazenamento descreve quanto tempo seu objeto existe durante a execução do programa. Uma variável local estática tem escopo de bloco, enquanto seu valor armazenado existe durante toda a execução.
Principais conclusões
- O escopo de bloco termina na chave de fechamento correspondente.
- Nomes de escopo de arquivo ficam visíveis abaixo de suas declarações.
- Uma declaração interna sombreia uma declaração externa visível.
- Uma função chamada não pode ver as variáveis locais automáticas da função chamadora.
- O escopo começa antes de um inicializador ser avaliado.
- Escopo e duração de armazenamento respondem a perguntas diferentes.
2.2 Escolha o tempo de vida de uma variável com classes de armazenamento
A duração de armazenamento descreve quanto tempo o espaço de armazenamento de uma variável existe. Use variáveis automáticas para trabalho pertencente a uma chamada e armazenamento estático quando um valor precisar persistir entre chamadas.
Compare um valor novo com um valor mantido
Parâmetros e variáveis comuns de escopo de bloco têm duração de armazenamento automática. Cada chamada de função recebe suas próprias instâncias. Uma declaração sem inicializador, como int sum;, não fornece um valor inicial utilizável. Inicialize um acumulador antes de lê-lo: int sum = 0;.
Variáveis de escopo de arquivo e variáveis de escopo de bloco declaradas static têm duração de armazenamento estática. Elas existem durante toda a execução do programa. A inicialização ocorre uma vez antes do início do programa; sem um inicializador explícito, objetos numéricos começam em zero.
int next_ticket(void)
{
static int n = 0;
n++;
return n;
}
Três chamadas em instruções separadas retornam 1, 2 e 3. O inicializador não é executado novamente na entrada da função. Remover static cria um novo n = 0 automático para cada chamada, portanto as três chamadas retornam 1.
O nome n continua tendo escopo de bloco. Outras funções não podem referir-se a ele diretamente pelo nome. Chamadas recursivas também compartilham a mesma variável local estática; elas não recebem cópias estáticas separadas como recebem com variáveis locais automáticas.
Inicialize uma vez, atribua quando necessário
Um inicializador numérico estático deve ser uma expressão constante em C. static int limit = 12; é válido; static int limit = read_sensor(); é inválido. Faça uma atribuição em tempo de execução no código executável quando um valor precisar vir de uma chamada. Uma instrução como n = 0; é executada sempre que a execução a alcança, portanto colocá-la no início de next_ticket reiniciaria o contador a cada chamada.
Leia os especificadores de classe de armazenamento
| Especificador | Significado neste código C17 |
|---|---|
auto | Armazenamento automático explícito para uma variável de bloco; geralmente omitido |
register | Uma indicação para acesso rápido; é proibido obter seu endereço |
static dentro de uma função | Armazenamento mantido com um nome local |
static no escopo de arquivo | Ligação interna, mantendo o nome dentro de sua unidade de tradução |
extern | Uma declaração que se refere a um objeto ou função existente |
Compiladores modernos escolhem a alocação de registradores por conta própria, portanto escrever register raramente melhora o código. auto int x; significa o mesmo que uma variável local comum int x; em C17. C23 também permite inferência de tipo com auto x = 3.5;, que declara um double.
C17 também tem _Thread_local, escrito thread_local em C23, para armazenamento por thread. A gramática agrupa typedef com os especificadores de classe de armazenamento, embora ele dê nome a um tipo em vez de escolher o armazenamento. Em C17, normalmente apenas um especificador de classe de armazenamento aparece em uma declaração; _Thread_local também pode acompanhar static ou extern.
Um extern int count; de escopo de bloco não cria uma cópia local automática. Ele declara um objeto com ligação e duração de armazenamento estática. Sempre considere onde a declaração aparece antes de decidir o que uma palavra-chave de classe de armazenamento faz.
Principais conclusões
- Inicialize variáveis automáticas antes de lê-las.
- O armazenamento estático dura toda a execução do programa e é inicializado em zero por padrão.
- Uma variável local estática mantém seu valor enquanto seu nome permanece local.
- Um inicializador estático é aplicado uma vez; uma atribuição é executada quando alcançada.
- A localização de
staticdetermina se sua função principal é alterar o tempo de vida ou a ligação.
2.3 Compartilhe dados com variáveis globais com cuidado
Uma variável global é declarada no escopo de arquivo, fora de todas as funções. Ela mantém um valor durante toda a execução e pode dar a várias funções acesso a um estado compartilhado.
Acompanhe o objeto compartilhado
#include <stdio.h>
double total_km = 0.0;
void add_trip(double km)
{
total_km += km;
}
int main(void)
{
add_trip(12.5);
add_trip(7.25);
printf("%.2f\n", total_km);
return 0;
}
As duas chamadas atualizam o mesmo objeto, produzindo 19.75. O parâmetro km continua sendo uma cópia; a função atribui um valor separadamente a uma variável global à qual pode se referir pelo nome. A passagem por valor não impede essa atribuição.
Variáveis globais têm duração de armazenamento estática. Variáveis globais numéricas são inicializadas em zero por padrão e exigem inicializadores que sejam expressões constantes: int error_count; começa em 0, e double limit = 2.5 * 4; começa em 10.0.
Verifique a visibilidade e o sombreamento
Um nome global só fica visível abaixo de sua declaração. Uma função acima da definição precisa de uma declaração anterior, como extern double total_km;.
Se, em vez disso, add_trip contiver:
double total_km = 0.0;
total_km += km;
ela cria e atualiza um objeto local. A variável global permanece inalterada. Remova a declaração extra para atualizar o total compartilhado; -Wshadow ajuda a identificar ocultações acidentais.
Torne as entradas explícitas quando o compartilhamento for desnecessário
Um contador de erros compartilhado pode ser útil quando várias funções relatam falhas. Mas um cálculo que lê uma variável global tem uma entrada ausente de sua lista de parâmetros. Os testes precisam preparar esse estado, e rastrear um valor incorreto exige encontrar quem pode alterá-lo.
Uma alternativa para o cálculo das viagens é:
double with_trip(double total, double km)
{
return total + km;
}
A função chamadora usa total = with_trip(total, 12.5);. Um teste fornece as duas entradas diretamente, sem preparar variáveis globais.
Restrinja os nomes compartilhados
Variáveis comuns de escopo de arquivo têm ligação externa. Compartilhe uma delas por meio de declarações e uma única definição; definições repetidas em arquivos diferentes quebram essa organização. Por sua vez, static no escopo de arquivo confere ligação interna, permitindo que outro arquivo tenha seu próprio objeto independente com o mesmo nome. Ambos os objetos continuam existindo durante toda a execução: aqui a palavra-chave limita a ligação, não o tempo de vida.
Principais conclusões
- Uma variável global mantém um valor entre chamadas e tem duração de armazenamento estática.
- A passagem por valor não impede que uma função altere uma variável global visível.
- O estado compartilhado acrescenta entradas e efeitos que uma lista de parâmetros não mostra.
- Prefira parâmetros explícitos e resultados retornados quando o estado compartilhado for desnecessário.
3Use o pré-processador de C
Antes de o código C ser compilado, o pré-processador inclui cabeçalhos, expande macros e seleciona ramos condicionais. Essas operações transformam o código recebido pelo compilador, portanto uma macro curta pode alterar uma expressão de maneiras surpreendentes. Você examinará expansões, agrupará expressões com segurança e usará opções e proteções contra inclusão múltipla para controlar qual texto é mantido. A distinção principal é entre escolher código durante a compilação e escolher um caminho durante a execução do programa.
3.1 Inclua arquivos e defina macros
O pré-processador transforma o texto-fonte antes de o compilador verificar expressões e tipos de C. Suas diretivas começam com #. Examine sua saída com gcc -E file.c quando uma inclusão ou expansão de macro for difícil de explicar.
Inclua declarações e dê nomes a constantes
#include <stdio.h> insere um cabeçalho padrão. Use #include "convert.h" para um cabeçalho do projeto: o GCC procura primeiro no diretório do arquivo que faz a inclusão e depois nos caminhos de cabeçalhos configurados. A opção -Iinclude adiciona um diretório de busca. A inclusão copia o texto do cabeçalho; ela não liga funções compiladas.
Uma macro semelhante a objeto substitui tokens:
#define MAX_STUDENTS 30
Usos posteriores do token MAX_STUDENTS tornam-se 30. O pré-processador deixa a cadeia "MAX_STUDENTS" e o nome diferente MAX_STUDENTS_2 inalterados. Não escreva = nem ponto e vírgula no final. Com #define PI 3.14159;, return PI * r * r; é expandido para o código incorreto return 3.14159; * r * r;. Uma diretiva normalmente termina na quebra de linha; uma barra invertida no final faz com que ela continue.
Expanda antes de calcular
Uma macro semelhante a função não tem espaço entre seu nome e o parêntese de abertura:
#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))
SQUARE_BAD(2 + 3) torna-se 2 + 3 * 2 + 3, que resulta em 11. 100 / SQUARE_BAD(5) torna-se 100 / 5 * 5, resultando em 100. A macro com parênteses fornece 25 e 4, respectivamente. Coloque parênteses em torno de cada parâmetro e de toda a expressão de substituição para que os operadores ao redor não alterem seu agrupamento.
Escrever #define SQUARE (x) ((x) * (x)), por outro lado, cria uma macro semelhante a objeto cuja substituição começa com (x). O espaço altera o significado da definição.
Parênteses não impedem avaliações repetidas
SQUARE(i++) é expandido para ((i++) * (i++)). Isso modifica i duas vezes sem sequenciamento, portanto o comportamento é indefinido. Uma função como square(i++) avalia o argumento uma vez antes de entrar no corpo. Prefira funções para cálculos, especialmente quando uma expressão de entrada altera o estado.
Uma macro semelhante a função não tem tipos declarados para parâmetros ou retorno. Seu código C expandido ainda é verificado quanto aos tipos, mas ela não tem um protótipo que especifique conversões de argumentos. Macros continuam úteis para proteções contra inclusão múltipla, compilação condicional e expressões constantes inteiras. Em C17, uma variável const int não é uma expressão constante inteira; uma macro ou uma constante enum pode fornecer um valor para case. C23 também introduz constexpr.
Transforme argumentos em tokens ou texto
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define CAT(a, b) a##b
SHOW(3 + 4); usa # para transformar o argumento em texto e imprime 3 + 4 = 7. CAT(temp, 1) usa ## para juntar tokens no nome temp1. A conversão em literal de cadeia (stringification) usa os tokens do argumento fornecido sem expandir macros nessa ocorrência; uma ocorrência comum do argumento é expandida.
Macros não seguem o escopo de bloco. Uma definição permanece ativa até #undef NAME ou o fim da unidade de tradução, mesmo que a diretiva tenha sido escrita entre chaves. Remova uma definição antiga com #undef antes de fornecer outro texto de substituição.
Principais conclusões
- O pré-processamento inclui texto e expande tokens antes da compilação de C.
- Uma definição de macro não tem
=nem ponto e vírgula no final. - Coloque parênteses em torno dos parâmetros da macro e de toda a expressão de substituição.
- Argumentos repetidos em macros podem repetir efeitos colaterais; use uma função para cálculos.
#converte um argumento em literal de cadeia e##junta tokens.- Uma macro permanece definida além dos limites de blocos até que sua definição seja removida.
3.2 Compile código condicionalmente
A compilação condicional seleciona quais linhas do código-fonte chegam ao compilador. Ela pode ativar saída de depuração, escolher código de uma plataforma ou proteger um cabeçalho contra inclusão repetida.
Teste a existência ou teste um valor
#define DEBUG 0
#ifdef DEBUG
/* Kept: DEBUG exists. */
#endif
#if DEBUG
/* Removed: DEBUG expands to zero. */
#endif
#ifdef DEBUG verifica se a macro está definida. #if DEBUG avalia seu valor de substituição como uma expressão constante inteira. Dentro de #if, um identificador não definido conta como zero; uma macro escrita incorretamente pode remover um ramo silenciosamente. As expressões defined DEBUG e defined(DEBUG) testam a existência.
Use #ifndef NAME para o teste oposto de existência. Feche cada condicional com #endif. #elif acrescenta outra condição e #else fornece a alternativa final. C23 acrescenta #elifdef e #elifndef; C17 pode expressar esses testes usando #elif defined(NAME) ou #elif !defined(NAME).
Escolha a configuração em tempo de compilação
#ifdef DEBUG
printf("debug: c = %.1f\n", c);
#endif
gcc -std=c17 -DDEBUG main.c define DEBUG como 1 e mantém a impressão. Sem essa definição, a linha é removida antes da compilação. -DLEVEL=2 fornece um valor como se #define LEVEL 2 aparecesse antes do texto do arquivo.
Isso é diferente de um if comum, cuja condição é avaliada durante a execução do programa. Condições do pré-processador não podem ler uma variável comum de tempo de execução. #if 0 remove um bloco, inclusive código com comentários /* */ existentes que tornariam inseguro envolvê-lo em outro comentário desse tipo.
Proteja um cabeçalho
#ifndef CONVERT_H
#define CONVERT_H
double c_to_f(double c);
#endif
A primeira inclusão define CONVERT_H e mantém o protótipo. Inclusões posteriores na mesma unidade de tradução ignoram o conteúdo. Use um nome de proteção distinto baseado no nome do cabeçalho; evite sublinhados iniciais reservados à implementação. Outra unidade de tradução inicia seu próprio pré-processamento e inclui as declarações para si. Uma proteção não impede que arquivos-objeto diferentes definam o mesmo símbolo externo.
Muitos compiladores aceitam #pragma once, mas ela não é uma diretiva ISO C. Proteções funcionam com diretivas condicionais padrão.
Leia as informações predefinidas
| Nome | Informação |
|---|---|
__FILE__ | Nome do arquivo-fonte atual como cadeia de caracteres |
__LINE__ | Número da linha atual do código-fonte |
__DATE__, __TIME__ | Data e hora da tradução |
__STDC_VERSION__ | Versão selecionada do padrão |
Os valores de versão incluem 199901L para C99, 201112L para C11, 201710L para C17 e 202311L para C23. __func__ contém o nome da função atual, mas é um identificador predefinido, não uma macro.
Um ramo específico de plataforma pode testar defined(_WIN32) ou defined(__linux__) e depois usar #else como alternativa. Para rejeitar explicitamente uma compilação não aceita, escreva uma condição seguida de #error "This program needs C11 or later" e #endif. A omissão de #endif, por outro lado, produz um erro de diretiva como “unterminated conditional directive”.
Principais conclusões
#ifdeftesta a existência;#iftesta um valor.- Identificadores não definidos em
#ifsão avaliados como zero. -DNAME=valuefornece uma definição de macro em tempo de compilação.- Proteções contra inclusão múltipla impedem o processamento repetido dentro de uma unidade de tradução.
- Macros predefinidas fornecem informações sobre arquivo, linha, momento da compilação e padrão.
4Divida um programa entre arquivos-fonte e arquivos de cabeçalho
Um programa maior pode manter funções relacionadas em arquivos-fonte separados e compartilhar suas declarações por meio de cabeçalhos. Cada arquivo-fonte é compilado independentemente, e o ligador conecta os arquivos-objeto resultantes. Você criará um pequeno módulo, decidirá de quais nomes outros arquivos precisam e diagnosticará definições ausentes ou duplicadas. Incluir o mesmo cabeçalho nas funções chamadoras e no arquivo que contém as definições ajuda o compilador a detectar incompatibilidades antes da execução do programa.
4.1 Compile e ligue vários arquivos-fonte
A compilação separada transforma cada arquivo-fonte em um arquivo-objeto antes de ligar o programa. Isso permite reutilizar arquivos-objeto inalterados e dá às funções relacionadas um lugar bem definido.
Acompanhe as etapas de compilação
Uma unidade de tradução é um arquivo .c após o pré-processamento, incluindo o conteúdo dos cabeçalhos incluídos. Cada unidade é compilada independentemente.
| Etapa | Resultado | Opção do GCC que para nessa etapa |
|---|---|---|
| Pré-processamento | Texto C expandido | -E |
| Compilação | Texto assembly | -S |
| Montagem | Arquivo-objeto | -c |
| Ligação | Executável | Compilação final normal |
Um arquivo-objeto contém código de máquina e informações sobre os símbolos que define ou dos quais precisa. Em sistemas do tipo Unix, geralmente tem o sufixo .o. O ligador conecta usos de nomes externos a definições fornecidas por arquivos-objeto e bibliotecas.
Compile dois arquivos e ligue-os
Suponha que convert.h declare double c_to_f(double c);, que convert.c inclua esse cabeçalho e defina a função e que main.c inclua o cabeçalho e a chame.
gcc -Wall -Wextra -std=c17 -c main.c
gcc -Wall -Wextra -std=c17 -c convert.c
gcc main.o convert.o -o temp
O primeiro comando produz main.o sem fazer a ligação. O segundo produz convert.o. O último cria o executável temp a partir dos dois arquivos-objeto. Você também pode compilar e ligar em um único comando:
gcc -Wall -Wextra -std=c17 main.c convert.c -o temp
Forneça os arquivos-fonte para compilação e os arquivos-objeto para ligação. Cabeçalhos entram por meio de #include; não os liste como fontes do programa compiladas separadamente. Se apenas convert.c mudar, reutilize main.o, gere novamente convert.o e faça a ligação outra vez. Se um cabeçalho compartilhado mudar, gere novamente cada arquivo-objeto cujo arquivo-fonte o inclua.
Diagnostique a etapa que falhou
Um erro de compilação diz respeito a uma unidade de tradução: sintaxe inválida, quantidade incorreta de argumentos ou tipos visíveis conflitantes. O ligador não pode corrigi-los.
Um erro de ligação diz respeito ao programa montado. Se você compilar apenas main.c, o protótipo pode permitir que a compilação seja bem-sucedida, enquanto a ligação informa “undefined reference to c_to_f” ou “Undefined symbols”. A declaração informa a existência de uma função; ela não fornece seu código de máquina. Adicione convert.c a uma compilação combinada ou convert.o à ligação.
Duas definições externas ligadas da mesma função causam um erro de símbolo duplicado. Um programa comum tem uma main; ligar dois arquivos-fonte que a definem falha. Funções static privadas ao arquivo podem ter nomes iguais em unidades de tradução diferentes porque têm ligação interna.
Bibliotecas também fornecem definições. Em muitos sistemas Linux, código que usa funções declaradas por <math.h> precisa de uma opção de ligação da biblioteca matemática: gcc main.c -o app -lm. Mantenha essa opção de biblioteca depois do código que precisa dela.
Inclua a interface, compile a implementação
Evite #include "convert.c". Isso copia corpos de funções para outra unidade de tradução. Se convert.c também for compilado e ligado, suas definições externas aparecerão duas vezes. O arquivo .h de um módulo compartilha suas declarações; seu arquivo .c é compilado uma vez como arquivo-fonte.
Principais conclusões
- Uma unidade de tradução é um arquivo-fonte pré-processado com o texto incluído.
-cproduz um arquivo-objeto sem fazer a ligação.- Ligue todos os arquivos-objeto necessários ao executável.
- Definições ausentes e duplicadas são problemas de ligação.
- Inclua cabeçalhos; compile arquivos-fonte separadamente.
4.2 Escreva arquivos de cabeçalho
O cabeçalho de um módulo compartilha sua interface; seu arquivo-fonte fornece a implementação. Tanto as funções chamadoras quanto a implementação incluem o cabeçalho para que suas declarações sejam compatíveis.
Declare a interface uma vez
Um convert.h com proteção contra inclusão múltipla contém:
#ifndef CONVERT_H
#define CONVERT_H
double c_to_f(double c);
double f_to_c(double f);
#endif
A implementação em convert.c o inclui antes dos corpos:
#include "convert.h"
double c_to_f(double c)
{
return c * 9.0 / 5.0 + 32.0;
}
double f_to_c(double f)
{
return (f - 32.0) * 5.0 / 9.0;
}
Uma função chamadora inclui o mesmo cabeçalho e <stdio.h>. A chamada printf("%.1f %.1f\n", c_to_f(25.0), f_to_c(212.0)); imprime 77.0 100.0.
Se a implementação definir float c_to_f(float c), incluir seu cabeçalho expõe os tipos conflitantes ao compilador. A compilação independente sem essa declaração pode não detectar a incompatibilidade.
Escolha o conteúdo correto
| Compartilhe em um cabeçalho | Mantenha em um arquivo-fonte |
|---|---|
| Protótipos de funções | Corpos de funções com ligação externa |
Declarações extern de variáveis | Definições de objetos compartilhados |
| Macros e tipos necessários | Funções auxiliares e estado privados |
Um cabeçalho que contém int count = 0; define um objeto em cada unidade de tradução que o inclui, causando definições externas duplicadas. Mesmo int count; é uma definição tentativa. A configuração padrão do GCC desde a versão 10 diagnostica cópias repetidas no momento da ligação, enquanto algumas ferramentas mesclam símbolos comuns e ocultam o defeito. Use extern int count; no cabeçalho e uma definição em um arquivo-fonte.
Torne a inclusão confiável
Uma proteção impede o processamento repetido dentro de uma unidade de tradução, inclusive a inclusão indireta por meio de outros cabeçalhos. Protótipos compatíveis podem se repetir, mas definições de tipos como um enum podem causar erros de redefinição.
Um cabeçalho autossuficiente inclui o que suas declarações precisam. Se um protótipo usa size_t, inclua <stddef.h> nele. As funções chamadoras não devem precisar de uma ordem especial de inclusão.
Para cabeçalhos do projeto, as aspas fazem o GCC procurar primeiro no diretório do arquivo que faz a inclusão. Adicione um diretório separado de cabeçalhos com -Iinclude, como em gcc -Iinclude -std=c17 -c src/main.c. O arquivo-objeto da implementação ainda precisa ser ligado.
Principais conclusões
- Cabeçalhos compartilham protótipos, declarações
extern, macros e tipos. - Mantenha definições de objetos compartilhados e corpos de funções com ligação externa em arquivos-fonte.
- Inclua o cabeçalho de um módulo em sua própria implementação.
- Use proteções contra inclusão múltipla e inclua os cabeçalhos necessários às suas declarações.
4.3 Controle a ligação com static e extern
A ligação determina se declarações em lugares diferentes se referem à mesma entidade. Ela é separada do escopo, que descreve onde um nome pode ser usado, e da duração de armazenamento, que descreve quanto tempo um objeto existe.
Conecte um nome a um objeto
| Ligação | Significado |
|---|---|
| Externa | Declarações podem referir-se a uma entidade entre unidades de tradução |
| Interna | Declarações referem-se a uma entidade dentro de uma unidade de tradução |
| Nenhuma | Um nome local, como um parâmetro comum, identifica sua própria entidade |
Funções e variáveis comuns de escopo de arquivo têm ligação externa por padrão. static no escopo de arquivo confere ligação interna. Variáveis locais automáticas comuns e parâmetros não têm ligação.
Para compartilhar um contador de erros, coloque estas declarações em um cabeçalho com proteção contra inclusão múltipla:
extern int error_count;
void log_error(int code);
Um arquivo-fonte inclui esse cabeçalho e fornece:
int error_count = 0;
void log_error(int code)
{
printf("error: %d\n", code);
error_count++;
}
Esse arquivo-fonte também inclui <stdio.h> para printf. Outros arquivos-fonte incluem o cabeçalho compartilhado, não uma segunda definição. Após três chamadas, toda função chamadora que lê error_count refere-se ao mesmo objeto e vê 3.
Diferencie declaração de definição
extern int error_count; declara um objeto existente e não aloca novo armazenamento. int error_count = 0; o define. Um inicializador muda a situação: no escopo de arquivo, extern int error_count = 0; é uma definição apesar da palavra-chave. Mantenha o inicializador no único arquivo que contém a definição.
Em declarações comuns de funções públicas, extern é opcional: double c_to_f(double c); já declara uma função com ligação externa. Inclua a declaração pública no arquivo que a define para que o compilador possa verificar a compatibilidade.
Oculte o estado por trás de funções
static int count = 0;
void counter_increment(void)
{
count++;
}
int counter_value(void)
{
return count;
}
O valor armazenado existe durante toda a execução, mas apenas esta unidade de tradução pode referir-se a ele pelo nome count. Coloque os protótipos das funções públicas em um cabeçalho. Outro arquivo pode incrementar e ler o contador por meio dessas funções sem alterar diretamente a variável privada. Uma função auxiliar também pode ser static quando outros arquivos não precisarem dela.
Diagnostique uma escolha incorreta de ligação
Uma definição static int count; em um cabeçalho cria um contador privado independente em cada unidade de tradução que o inclui. Atualizar um não atualiza os outros. Proteções contra inclusão múltipla não mesclam esses objetos: cada unidade de tradução pré-processa sua própria cópia do cabeçalho.
Uma função definida como static em uma unidade de tradução não pode atender a uma chamada externa de outra; a ligação falha para esse nome externo. Dê ligação externa apenas às funções necessárias a outros arquivos e declare-as no cabeçalho.
Dentro de uma função, static dá a uma variável local armazenamento persistente sem ampliar seu escopo. Fora das funções, confere ligação interna a um objeto ou função. Variáveis de escopo de arquivo têm duração de armazenamento estática com ou sem essa palavra-chave.
Principais conclusões
- A ligação externa conecta declarações entre unidades de tradução.
- Compartilhe uma variável com declarações
externe uma definição. staticno escopo de arquivo torna um objeto ou função privado à sua unidade de tradução.- Um cabeçalho que contém uma variável estática fornece objetos separados às unidades que o incluem.
- Um inicializador transforma uma declaração de objeto
externno escopo de arquivo em uma definição. - A persistência de
staticlocal e a ligação destaticno escopo de arquivo têm funções diferentes.
5Compile programas com vários arquivos usando make
Uma compilação correta precisa de mais do que uma lista de comandos do compilador: ela precisa saber quais arquivos dependem de quais outros arquivos. Um makefile registra essas relações para que uma edição no código-fonte recompile os arquivos-objeto afetados e depois ligue o programa novamente. Você rastreará essas decisões de recompilação, incluirá dependências de cabeçalhos e encurtará comandos repetidos com variáveis e regras de padrão. Execuções simuladas (dry runs) tornam o plano visível antes da execução de qualquer receita.
5.1 Escreva regras e dependências em makefiles
make lê Makefile ou makefile no diretório do projeto e atualiza o alvo solicitado. Sem um argumento de alvo, normalmente gera o primeiro alvo comum.
Registre saídas e entradas
Uma regra começa com target: prerequisites. Sua receita fica nas linhas seguintes, indentadas com tabulação, ou após um ponto e vírgula na mesma linha:
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
temp: main.o convert.o; $(CC) $(CFLAGS) -o temp main.o convert.o
main.o: main.c convert.h; $(CC) $(CFLAGS) -c main.c
convert.o: convert.c convert.h; $(CC) $(CFLAGS) -c convert.c
.PHONY: clean
clean:; rm -f temp main.o convert.o
Aqui temp é o alvo padrão. make clean solicita a ação de limpeza. Se escrever uma receita na linha seguinte, use uma tabulação literal: espaços podem causar “missing separator”. A configuração de exibição de caracteres de espaço em branco de um editor ajuda a perceber a diferença.
Rastreie as decisões de recompilação
O make atualiza primeiro os pré-requisitos de um alvo. Em seguida, gera novamente o alvo se ele estiver ausente ou se algum pré-requisito tiver uma data de modificação mais recente.
- Primeira compilação: compilar os dois arquivos-fonte e depois ligar
temp: três comandos. - Sem alterações: nenhuma receita do programa precisa ser executada.
- Editar apenas
convert.c: recompilá-lo e ligar novamente: dois comandos; reutilizarmain.o. - Editar
convert.h: recompilar os dois arquivos-fonte e ligar novamente: três comandos.
O cabeçalho afeta as duas unidades de tradução, embora nenhum dos arquivos-fonte tenha sido editado. Imagine os arquivos de entrada gerando arquivos-objeto, que por sua vez geram o executável. As alterações se propagam por essas relações registradas.
Inclua dependências de cabeçalhos
A regra incompleta main.o: main.c não detecta edições em convert.h. O make não lê o código-fonte C para descobrir inclusões; ele segue os pré-requisitos declarados. Liste os cabeçalhos incluídos ou carregue arquivos de dependências gerados pelo compilador para evitar ligar arquivos-objeto desatualizados.
Mantenha juntas as operações dependentes do shell
Linhas de receita separadas normalmente usam shells separados. Uma linha que contém apenas cd build não altera o diretório da linha seguinte. Use um único comando como cd build && gcc -c ../main.c quando a compilação depender dessa mudança de diretório. Visualize as receitas planejadas com make -n.
Principais conclusões
- Regras indicam alvos, pré-requisitos e receitas indentadas com tabulação.
- O make atualiza os pré-requisitos antes de decidir se gera novamente seu alvo.
- Um alvo ausente ou um pré-requisito mais recente provoca uma nova geração.
- Edições em cabeçalhos devem provocar a geração de cada arquivo-objeto afetado.
- Cada linha de receita normalmente é executada em seu próprio shell.
5.2 Simplifique makefiles com variáveis e regras de padrão
Variáveis e regras de padrão eliminam comandos repetidos do compilador enquanto preservam o grafo de dependências. Os exemplos colocam cada receita após um ponto e vírgula na linha de sua regra. Encurte um makefile somente depois que suas relações de compilação estiverem corretas.
Dê nomes a configurações compartilhadas
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
OBJS = main.o convert.o
temp: $(OBJS); $(CC) $(CFLAGS) -o $@ $^
%.o: %.c convert.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:; rm -f temp $(OBJS)
$(CC) expande a variável do compilador; $(CFLAGS) expande suas opções. CPPFLAGS convencionalmente contém opções do pré-processador como -DDEBUG ou -Iinclude. As opções -Wall -Wextra ativam avisos, -g adiciona informações de depuração e -O2 ativa a otimização. Variáveis do make não definidas são expandidas para texto vazio. Escreva $(CC), não $CC: o segundo significa a variável de uma letra C, seguida de um C literal.
Leia variáveis automáticas do make dentro de uma regra
| Variável | Valor na regra atual |
|---|---|
$@ | Nome do alvo |
$< | Primeiro pré-requisito |
$^ | Todos os pré-requisitos, removendo nomes duplicados |
$? | Pré-requisitos mais recentes que o alvo |
Para main.o, a parte correspondente ao padrão é main, portanto %.c torna-se main.c. A receita de compilação usa $< para passar esse arquivo-fonte ao compilador e $@ para nomear main.o. O cabeçalho é uma dependência, mas não é passado como outra entrada de compilação.
Na regra de ligação, todos os pré-requisitos são arquivos-objeto. $^ é expandido para main.o convert.o, e $@ torna-se temp. Usar $< ali passaria apenas main.o, perdendo as definições de convert.o.
O GNU make tem regras internas que podem compilar x.c em x.o usando CC, CPPFLAGS e CFLAGS. Uma regra de dependência explícita sem receita pode usar essa receita interna. O GNU make pode até gerar um programa simples com make hello a partir de hello.c sem um makefile.
Marque alvos de ação como fictícios
clean nomeia uma ação, não um arquivo a criar. .PHONY: clean garante que a ação seja executada mesmo que exista um arquivo chamado clean. rm -f não falha quando os arquivos já estão ausentes. Um alvo all que agrupa vários programas geralmente também é fictício.
Gere listas de cabeçalhos e examine as recompilações
Adicione -MMD -MP às opções de compilação para gravar um arquivo .d ao lado de cada arquivo-objeto. -MMD registra dependências de cabeçalhos do usuário; -MP adiciona regras vazias que ajudam quando um cabeçalho registrado é removido. Após o alvo padrão comum, carregue as listas com:
-include $(OBJS:.o=.d)
A substituição transforma nomes de arquivos-objeto em nomes de arquivos de dependências. O sinal de menos inicial permite que arquivos .d estejam ausentes na primeira compilação. Remova esses arquivos gerados em clean junto com os arquivos-objeto.
make -n mostra os comandos antecipadamente, make -B força a recompilação e make -j4 permite até quatro receitas de uma vez. Dependências corretas também são importantes para compilações paralelas. Alterar apenas o texto de CFLAGS não torna os arquivos-objeto existentes mais antigos que um pré-requisito. Após alterar opções, use make -B ou uma recompilação limpa para que todos os arquivos-objeto usem as novas configurações.
Principais conclusões
- Use
$(NAME)para expandir uma variável nomeada do make. $@,$<e$^indicam o alvo, o primeiro pré-requisito e todos os pré-requisitos.- Regras de padrão reutilizam uma receita para arquivos com partes de nome correspondentes.
- Alvos fictícios executam ações mesmo quando existem arquivos com os mesmos nomes.
- Arquivos
.dgerados registram dependências de cabeçalhos do usuário. - Force uma recompilação ou faça uma recompilação limpa após alterar opções do compilador.
Exercícios resolvidos de Funções em C
1Crie e rastreie uma chamada de função
19 passos
- 1IntroduçãoComece com uma pequena tarefa
Acompanhe um valor até uma função e depois descubra para onde vai seu resultado. Vamos criar uma definição e um protótipo, diferenciar um parâmetro copiado da variável da função chamadora e rastrear chamadas recursivas até um caso-base e na volta pelos valores retornados.
- 2ExemploDê uma entrada tipada a uma tarefa
Queremos uma função que acrescente um ajuste fixo a uma leitura inteira.
int corrected(int reading) { return reading + 4; }intantes do nome descreve o resultado.int readingdeclara um parâmetro, e o corpo calcula o valor retornado. Defina essa função fora dos corpos de todas as outras funções; o C padrão não permite definições aninhadas. - 3ExemploUse uma chamada como expressão
Em
int result = corrected(9);, o argumento9inicializa o parâmetroreading. A expressão de retorno produz13, e a atribuição da função chamadora armazena esse valor emresult.A chamada também pode aparecer dentro de outra expressão:
2 * corrected(9)resulta em26. Escrever apenascorrected(9);ainda executa a chamada, mas descarta o valor retornado. - 4DicaEscolha entre retornar e imprimir
Um cálculo pode retornar um resultado sem imprimi-lo. Isso permite que uma função chamadora o armazene, compare ou imprima mais tarde.
Uma função declarada
voidpode executar uma tarefa de saída, mas não fornece valor para uma atribuição. Dentro dela,return;encerra a função sem um valor. Em uma função que retorna valor, assegure-se de que cada caminho que produz um resultado alcance umreturnadequado; exibir um número não o fornece à função chamadora. - 5DesafioVerifique a aritmética antes do retorno
O que esta função retorna para
average_pair(5, 8)?double average_pair(int a, int b) { return (a + b) / 2; }---reveal---
Ela retorna
6.0: a adição inteira resulta em13, a divisão inteira resulta em6e a conversão de retorno resulta em6.0. Use2.0para tornar a divisão de ponto flutuante se precisar de6.5. O tipo de retorno não pode recuperar uma fração já descartada. - 6ExplicaçãoInforme o compilador antes da chamada
Quando o corpo vier depois, coloque
int corrected(int reading);antes da chamada. Esse protótipo permite ao compilador verificar a quantidade de argumentos e as conversões. Seu ponto e vírgula indica uma declaração; a definição fornece o corpo. - 7Ideia-chaveEspecifique uma lista de entradas vazia
Use
int read_sensor(void);para indicar ausência de parâmetros tanto em C17 quanto em C23. Em C17,int read_sensor();deixa as informações dos parâmetros não especificadas; C23 altera essa forma para significar ausência de parâmetros. Selecione C17 explicitamente com-std=c17. - 8DesafioRastreie uma conversão permitida
O compilador já recebeu
int corrected(int reading);. O quecorrected(6.8)retorna, e a chamada precisa ser rejeitada?---reveal---
A chamada pode compilar. O protótipo converte
6.8no inteiro6, depois a função soma4e retorna10. Um aviso de conversão pode ajudar a revelar a perda de informação; use-Wconversion. Um protótipo verifica uma chamada usando seus tipos declarados, mas nem todo argumento numérico de tipo diferente é um erro. - 9ExemploDesenhe dois valores separados
Considere outra função:
void change(int reading) { reading += 4; }A função chamadora executa
int sample = 9; change(sample);. Na entrada,samplee o parâmetro contêm9em objetos separados. O corpo altera seu parâmetro para13. Quando a chamada retorna,sampleainda contém9: não houve nenhuma atribuição a ele. - 10ConexãoDeixe a função chamadora guardar um resultado
Use
sample = corrected(sample);para manter o resultado. A função calcula a partir de uma cópia; a função chamadora então armazena o valor retornado. Nomes iguais não conectariam seus objetos: a atribuição atualizasample. - 11DesafioEncontre a chamada cujo resultado é descartado
Começando com
int sample = 7;, a função chamadora executa:corrected(sample); sample = corrected(sample);Qual é o valor de
sampledepois disso?---reveal---
A primeira chamada calcula
11e o descarta, deixandosampleem7. A segunda também recebe7, retorna11e, desta vez, a função chamadora o armazena. O valor final é11, não15; uma chamada não atualiza a variável usada como argumento apenas por retornar um valor alterado. - 12ExplicaçãoAcrescente uma regra de parada para a recursão
Uma função recursiva chama a si mesma para um problema menor:
int total_to(int n) { if (n <= 0) return 0; return n + total_to(n - 1); }Para uma entrada positiva, subtrair um aproxima do caso-base. O caso-base responde diretamente. O caso recursivo não pode terminar sua adição até que a chamada menor retorne.
- 13ExemploEscreva as chamadas na descida
Rastreie
total_to(3). A primeira chamada mantémn == 3e espera para somar3atotal_to(2). Essa chamada mantém seu próprion == 2e espera portotal_to(1).A chamada com
n == 1espera portotal_to(0), que retorna0imediatamente. Nesse ponto mais profundo, quatro chamadas estão ativas. Não substitua os valores anteriores denpor zero; eles pertencem a chamadas diferentes. - 14ExemploPreencha os resultados na volta
Quando
total_to(0)retorna0, o trabalho em espera pode terminar:- A chamada com
n == 1retorna1 + 0, ou1. - A chamada com
n == 2retorna2 + 1, ou3. - A chamada com
n == 3retorna3 + 3, ou6.
A maioria das implementações mantém essas chamadas ativas em registros de ativação. Cada retorno retoma a função chamadora após sua chamada e depois libera o estado automático dessa chamada.
- A chamada com
- 15DesafioVerifique se um caso-base é alcançável
Suponha que o teste do caso-base em
total_toseja alterado paran == 0e que a etapa recursiva continue usandon - 1. O que dá errado quando o argumento inicial é-2?---reveal---
As entradas tornam-se
-2,-3,-4e continuam se afastando de zero. O teste de parada existe, mas é inalcançável nesse caminho. A recursão contínua pode esgotar o espaço da pilha. A versão comn <= 0interrompe essa entrada imediatamente. - 16DicaVerifique também o tamanho da resposta
Um cálculo recursivo que termina ainda pode ultrapassar seu tipo de retorno. Com um
intde 32 bits,factorial(12)cabe, enquanto o valor matemático defactorial(13)excede o máximo2147483647.O estouro com sinal é comportamento indefinido. Antes de escolher entradas recursivas, verifique tanto se as chamadas param quanto se os cálculos intermediários e finais cabem nos tipos usados. Um laço também precisa da verificação aritmética.
- 17DicaSepare chamadas quando a ordem for importante
Todas as expressões dos argumentos terminam antes de o corpo da função chamada começar, mas C não especifica sua ordem de avaliação. Se duas chamadas alterarem o estado compartilhado, calcule seus resultados em instruções separadas antes de passá-los adiante.
Evite
f(i, i++): ela lê e alteraisem o sequenciamento necessário. A passagem por valor não torna segura uma expressão de argumento insegura. - 18DesafioCombine declaração, conversão e chamadas
Com
total_todeclarada antes do uso, uma função chamadora escreveint x = 2; total_to(x); x = total_to(3.9);. Qual é o valor final dex?---reveal---
O primeiro resultado é descartado, portanto
xpermanece2. O segundo argumento é convertido em3; o rastreamento recursivo retorna6, que a atribuição armazena emx. A declaração controla a conversão, a recursão calcula o valor e a função chamadora decide se vai guardá-lo. - 19RecapitulaçãoUse três pontos de verificação para uma chamada
Para uma chamada, verifique sua declaração, rastreie suas entradas copiadas e localize o uso de seu valor de retorno. Para recursão, acompanhe cada chamada ativa separadamente e combine os resultados na volta a partir do caso-base.
2Acompanhe nomes e valores mantidos
13 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoComece com uma pequena tarefa
- 2ExemploLeia a declaração visível mais próxima
- 3ExplicaçãoConsidere o escopo em que a função foi escrita
- 4DesafioCorrija um valor necessário após um bloco
- 5ExemploObserve um contador automático reiniciar
- 6ExemploMantenha o contador com static
- 7DesafioDiferencie inicialização de atribuição
- 8DicaLeia a palavra-chave em seu contexto
- 9ExemploRastreie uma atualização global
- 10DesafioEncontre a variável local que oculta a global
- 11ConexãoTorne um cálculo mais fácil de testar
- 12DesafioCombine escopo e estado mantido
- 13RecapitulaçãoAnote nomes e tempos de vida separadamente
3Examine o código antes da compilação
13 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoComece com uma pequena tarefa
- 2ExplicaçãoVeja o que a inclusão realmente fornece
- 3ExemploSubstitua tokens antes de usar as regras dos operadores
- 4ExemploProteja o agrupamento interno e externo
- 5DesafioDescubra o que parênteses não podem corrigir
- 6DicaSepare as regras de macros das regras de escopo
- 7ExemploTeste uma configuração pela existência e pelo valor
- 8DesafioSelecione um ramo com uma opção do compilador
- 9ExemploAcompanhe a primeira inclusão do cabeçalho
- 10Ideia-chaveInicie um novo estado da proteção em outra unidade
- 11ConexãoLocalize um diagnóstico no código-fonte
- 12DesafioCombine expansão e seleção de ramos
- 13RecapitulaçãoExamine a transformação em ordem
4Gere um programa a partir de dois arquivos-fonte
14 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoComece com uma pequena tarefa
- 2ExplicaçãoAcompanhe uma unidade de tradução até um arquivo-objeto
- 3ExemploEscreva a declaração compartilhada
- 4ExemploColoque a implementação em seu próprio arquivo-fonte
- 5DesafioPosicione corretamente uma variável compartilhada
- 6ExemploCompile separadamente e depois faça a ligação
- 7DesafioCorrija uma referência indefinida
- 8DicaNão copie a implementação para a função chamadora
- 9ExemploMantenha um contador dentro do módulo
- 10ExplicaçãoSepare três propriedades do contador
- 11DesafioExplique dois contadores independentes
- 12DicaFaça um cabeçalho funcionar por conta própria
- 13DesafioCorrija a privacidade e a interface juntas
- 14RecapitulaçãoRelacione cada necessidade à sua etapa de compilação
5Compile apenas o que mudou
15 passos
Salve esta aula para seguir cada Lição Interativa passo a passo.
- 1IntroduçãoComece com uma pequena tarefa
- 2ExemploRegistre as entradas do executável e dos arquivos-objeto
- 3ExemploDê uma receita a um arquivo-objeto
- 4ExemploExecute a primeira compilação e recompile um arquivo-fonte
- 5DesafioRastreie uma edição em um cabeçalho compartilhado
- 6DicaExamine o plano antes de executá-lo
- 7ExplicaçãoSubstitua configurações repetidas por variáveis
- 8ExemploExpanda a receita de ligação
- 9ExemploUse um padrão de compilação para os dois arquivos-objeto
- 10DesafioEscolha um arquivo-fonte sem compilar o cabeçalho
- 11DicaFaça da limpeza uma ação
- 12ConexãoDeixe o compilador registrar dependências de cabeçalhos
- 13DesafioMantenha correta uma recompilação paralela
- 14DesafioAplique opções alteradas a todo o programa
- 15RecapitulaçãoVerifique o grafo antes de confiar no resultado
Questões de Funções em C
Conjunto 1: Rastreie nomes locais e estado mantido10 Questões de Prática
Questão 1 de 10
int amount = amount + 2; dentro de um bloco não é inicializado a partir de um amount externo?Mostrar respostaOcultar resposta
Questão 2 de 10
static int calls = 0; é inicializada durante uma execução do programa?Mostrar respostaOcultar resposta
calls = 0; a reiniciaria sempre que fosse alcançada.Questão 3 de 10
Quais valores três chamadas separadas a tick retornam?
int tick(void) {
static int n = 5;
n += 2;
return n;
}
Mostrar respostaOcultar resposta
5 para 7, 9 e 11. Retornar 7 todas as vezes descreveria uma nova variável local automática, não esse objeto estático.Questão 4 de 10
Quais valores este programa imprime?
#include <stdio.h>
int size = 9;
void show(void) { printf("%d ", size); }
int main(void) {
int size = 4;
show();
printf("%d", size);
}
Mostrar respostaOcultar resposta
show, a declaração visível é size de escopo de arquivo, cujo valor é 9. A impressão dentro de main usa seu valor local 4. Chamar show a partir de main não expõe a ela o escopo local da função chamadora, portanto a primeira impressão não pode usar essa variável local apenas porque tem o mesmo nome.Questão 5 de 10
O que é impresso?
int total = 0;
for (int i = 1; i <= 3; i++) {
int total = i;
total += 2;
}
printf("%d", total);
Mostrar respostaOcultar resposta
total interno e altera esse objeto. O total externo permanece em zero e é o nome visível após o laço. Somar os valores das iterações ao total externo exigiria uma atribuição a ele dentro do laço, sem a declaração que causa sombreamento.Questão 6 de 10
i de for (int i = 0; i < 6; i++) { } pode ser usado?Mostrar respostaOcultar resposta
i a essa instrução for, incluindo seu corpo. Ela não se comporta como uma declaração anterior ao laço, que permaneceria visível depois dele.Questão 7 de 10
Qual é o valor de result após int result = visit(2);?
int visit(int n) {
static int calls = 0;
calls++;
if (n == 0) return calls;
int inner = visit(n - 1);
return inner + calls;
}
Mostrar respostaOcultar resposta
calls estático, que chega a 3 na chamada do caso-base. Essa chamada retorna 3; as duas chamadas em espera somam, cada uma, o valor compartilhado atual 3, produzindo 6 e 9. Obter 6 trata calls como valores mantidos separados de 1, 2 e 3 para os três registros de ativação, mas uma variável local estática é compartilhada.Questão 8 de 10
int permitted(int n) lê uma variável global limit. Por que o mesmo argumento pode produzir um resultado diferente em uma chamada posterior?Mostrar respostaOcultar resposta
n, portanto uma atualização de limit pode alterar um resultado posterior. Passar o mesmo argumento não reinicia essa entrada compartilhada. A inicialização no escopo de arquivo ocorre uma vez, e a passagem por valor não faz as variáveis locais da função chamadora se tornarem entradas ocultas dessa função.Questão 9 de 10
Quais dois inteiros são impressos?
#include <stdio.h>
int count = 6;
int advance(int n) { count += n; return count; }
int main(void) {
int count = 2;
count = advance(count);
int next = advance(1);
printf("%d %d", count, next);
}
Mostrar respostaOcultar resposta
2, mas atualiza a variável global de 6 para 8 e depois retorna 8 para a variável local. A chamada seguinte atualiza apenas a variável global para 9 e retorna esse valor para next. Imprimir 9 9 trata incorretamente o count local que recebeu a atribuição como uma referência alternativa ao objeto global após a primeira chamada.Questão 10 de 10
read_sensor() em tempo de execução. Por que static int first = read_sensor(); é inválido em C17?Mostrar respostaOcultar resposta
Conjunto 2: Acompanhe valores em chamadas de função10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
int triple(int value) { return value * 3; }?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
void report(int code) permite que uma função chamadora faça?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
double portion(int total) { return total / 4; } retorna para portion(11)?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
int capacity(void) aparece abaixo de main. Qual declaração acima de main descreve completamente uma chamada sem entrada em C17?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
int fifth(int n) { return n / 5; } declarada antes do uso, qual é o valor de fifth(-14.8)?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
double stretch(int n);. Qual definição é compatível com essa declaração?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
O que é impresso pelo seguinte programa completo?
#include <stdio.h>
void lower(int n) { n -= 4; }
int main(void) {
int level = 17;
lower(level);
printf("%d\n", level);
}
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Qual é o valor final de x?
int adjust(int x) { x += 3; return x * 2; }
/* In main: */
int x = 4;
adjust(x);
x = adjust(x + 1);
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
O que depth(3) retorna?
int depth(int n) {
if (n == 0) return 2;
return 1 + depth(n - 1);
}
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
Qual valor int answer = collect(4.9); armazena?
int collect(int n) {
if (n <= 1) return 1;
return n + collect(n - 2);
}
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 3: Expanda macros e escolha ramos em tempo de compilação10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
#define TRACE 0, o que #ifdef TRACE faz?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
#define LIMIT 18 substitui no código-fonte posterior?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
Qual é o valor de 5 * INC(3)?
#define INC(x) x + 1
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
#define LIMIT 12; torna int count = LIMIT * 2; inválido?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Após este trecho, quais são os valores de a e b?
#define TWICE(x) x + x
#define GROUP(x) ((x) + (x))
int a = 30 / TWICE(2 + 1);
int b = 30 / GROUP(2 + 1);
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
Qual é o comportamento de CUBE(i++) com esta definição?
#define CUBE(x) ((x) * (x) * (x))
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
#if VERBOSE pode ignorar um ramo silenciosamente quando a compilação define apenas VERB0SE?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
Com -DLEVEL=2, qual valor chega ao compilador para result?
#define STEP(x) ((x) + 1)
#if defined(LEVEL) && LEVEL > 2
int result = STEP(5);
#elif LEVEL == 2
int result = 2 * STEP(3);
#else
int result = STEP(1);
#endif
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
Qual texto é impresso por este programa completo?
#include <stdio.h>
#define JOIN(a, b) a##b
#define PRINT(x) printf(#x "=%d", x)
int main(void) {
int item2 = 7;
PRINT(JOIN(item, 2));
}
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
int mode = 2;. Dois arquivos-fonte separados o incluem, são compilados e depois ligados. Por que a proteção não impede um erro de definição duplicada?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 4: Diagnostique cabeçalhos, arquivos-objeto e ligação10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
gcc -std=c17 -c driver.c normalmente produz em um sistema do tipo Unix?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
reading, mas ligar apenas seu arquivo-objeto informa uma referência indefinida a reading. O que deve ser adicionado?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
int classify(double x);, mas seu arquivo-fonte define double classify(double x). O que torna a incompatibilidade visível durante a compilação do arquivo que contém a definição?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
static int cached = 3; tem quando declarado fora de todas as funções?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
static int errors; e void record(void);. Um arquivo-fonte o inclui e define record para incrementar errors; outro o inclui e chama record duas vezes, depois imprime seu próprio errors. O que é impresso e por quê?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
int failures definido em um deles?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
main.c inclui "calc.c"; calc.c inclui um calc.h protegido e define a função pública int calc(int n). A compilação gcc main.c calc.c -o run informa duas definições de calc. Qual alteração preserva a compilação separada e verifica as chamadas corretamente?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
static int quota = 9; e int quota_value(void) { return quota; }. Outro inclui um cabeçalho que declara tanto extern int quota; quanto int quota_value(void); e depois usa os dois nomes. Qual é a correção adequada da interface para manter a cota armazenada privada?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
size_t packet_count(void);. O que torna o cabeçalho autossuficiente?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
extern int jobs = 1;. Um incrementa jobs e o outro o imprime. Qual correção cria um objeto compartilhado e mantém a verificação de tipos nos dois arquivos?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Conjunto 5: Planeje compilações incrementais confiáveis10 Questões de Prática
Salve esta aula para ver todas as respostas e praticar cada conjunto.
Questão 1 de 10
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 2 de 10
viewer depende de main.o e screen.o; cada arquivo-objeto depende do arquivo-fonte correspondente. Apenas screen.c torna-se mais recente que seu arquivo-objeto, e todas as outras entradas permanecem inalteradas. Quais receitas são necessárias?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 3 de 10
settings.h, mas a regra de seu arquivo-objeto lista apenas o arquivo-fonte. Por que editar o cabeçalho pode deixar o programa inalterado após o make?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 4 de 10
tool: main.o engine.o, em que $@ e $^ são expandidos?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 5 de 10
Para a regra abaixo, quais entradas e saída do compilador são usadas quando o make gera sensor.o? Considere CPPFLAGS = -DTRACE=1 e CFLAGS = -std=c17.
%.o: %.c shared.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 6 de 10
shared.h < main.o < helper.o < app. As regras são app: main.o helper.o, main.o: main.c shared.h e helper.o: helper.c. Em seguida, apenas shared.h é editado. Os dois arquivos-fonte o incluem. O que acontece e o que está errado?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 7 de 10
clean pode impedir uma regra clean: sem pré-requisitos e sem marcação de remover arquivos-objeto?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 8 de 10
.d gerados após sua regra do executável. main.d lista main.o: main.c a.h; worker.d lista worker.o: worker.c b.h a.h. Todas as saídas estão atualizadas e então apenas a.h é editado. O que make -n deve mostrar?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 9 de 10
app: main.o work.o, main.o: main.c shared.h e work.o: work.c. Todas as saídas estão atualizadas. CFLAGS muda de -O0 para -O2 e depois apenas shared.h é editado. As duas receitas usam CFLAGS. O que o make comum faz e o que garante que as novas opções sejam aplicadas de forma consistente?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Questão 10 de 10
app depende de main.o e codec.o. Cada arquivo-objeto tem uma regra completa de dependências de arquivo-fonte e cabeçalhos; apenas codec.h é compartilhado pelos dois arquivos-fonte. Após editar esse cabeçalho, por que make -j4 app pode compilar simultaneamente e ainda fazer a ligação corretamente?Mostrar respostaOcultar resposta
Salve esta aula para ver esta resposta.
Travou em uma questão sua? Ajuda com Tarefas
Termos-Chave
A
Alvo
Alvo fictício
.PHONY, de modo que um arquivo existente com esse nome não possa impedir sua receita.clean como fictício garante que a limpeza continue sendo executada se um arquivo chamado clean aparecer no diretório.Alvo padrão
all ou o executável final primeiro impede que uma execução comum do make pare em um único arquivo-objeto.Argumento
cube(5 + 1), o argumento é avaliado como 6 antes de o corpo de cube começar.Arquivo de cabeçalho
Arquivo de dependências gerado
-MMD pode gravar sensor.d, que faz uma edição posterior em um cabeçalho provocar a geração de um novo sensor.o.Arquivo-objeto
sensor.c, a compilação pode substituir sensor.o e reutilizar um main.o inalterado.Ausência de ligação
auto
auto indica explicitamente armazenamento automático em C17; C23 também a usa para inferir um tipo a partir de um inicializador.auto int level; tem o mesmo comportamento de armazenamento que uma variável local comum int level;.C
Cabeçalho autossuficiente
size_t inclui <stddef.h> antes dessa declaração.Caso recursivo
Caso-base
1, permitindo que as multiplicações em espera nas chamadas anteriores terminem.Compilação condicional
Concatenação de tokens
## em uma lista de substituição de macro para combinar tokens vizinhos em um único token de pré-processamento.temp e 2 pode formar o identificador temp2 no código-fonte expandido.Conversão de argumento
5.8 a um parâmetro int converte o valor em 5; não o arredonda para o inteiro mais próximo.Conversão em literal de cadeia
# antes de um parâmetro de macro no texto de substituição para transformar os tokens do argumento fornecido em um literal de cadeia de caracteres.3 + 4 em literal de cadeia para exibir a expressão junto com seu valor calculado.D
Declaração
main permite verificar uma chamada mesmo quando o corpo da função vem depois.Definição de função
double scale(double factor) deve concordar com o protótipo incluído tanto na função chamadora quanto na implementação.Definição múltipla
main causam um erro de definição múltipla ou de símbolo duplicado.Definição tentativa
extern que pode fornecer uma definição.int count; em um cabeçalho compartilhado não é uma substituição portável para uma declaração extern.Dependência oculta
Diretiva de pré-processamento
# e controla a transformação do texto-fonte.#include "sensor.h" insere o texto do cabeçalho no pré-processamento atual.Duração de armazenamento
Duração de armazenamento automática
Duração de armazenamento estática
E
Efeito colateral
Escopo
Escopo de arquivo
Escopo de bloco
if não pode ser usada pelo nome em uma impressão após o fechamento desse bloco.Escopo de protótipo
value, enquanto a definição chama o mesmo parâmetro de n.Estado compartilhado
Execução simulada
make -n imprime receitas comuns de compilação sem executá-las, mostrando os comandos planejados do compilador e do ligador.extern
extern pode declarar uma entidade com ligação sem definir um objeto, desde que nenhum inicializador torne a declaração uma definição.extern int errors;, enquanto um arquivo-fonte fornece a definição inicializada do objeto.F
Função
Função chamadora
main chama read_sensor, main é a função chamadora que espera receber a leitura retornada.L
Ligação
static restringe sua ligação a uma unidade de tradução.Ligação externa
Ligação interna
check sem conflito no momento da ligação.Ligador
M
Macro predefinida
__FILE__ e __LINE__ pode identificar a origem de sua mensagem.Macro semelhante a função
Macro semelhante a objeto
#define LIMIT 16 substitui o token LIMIT, mas deixa "LIMIT" inalterado.Módulo
O
Operador defined
defined de pré-processamento resulta em um se seu operando, um nome de macro, estiver definido e em zero se não estiver.#if defined(DEBUG) para testar a existência mesmo quando DEBUG tem o texto de substituição 0.Ordem não especificada de avaliação dos argumentos
P
Parâmetro
int cube(int n), o parâmetro n recebe um valor de entrada separado para cada chamada.Passagem por valor
n dentro de uma função deixa a variável inteira comum da função chamadora inalterada.Pilha de chamadas
main.Pré-processador
gcc -E sample.c para examinar o código expandido antes de o compilador verificar expressões C.Pré-requisito
Proteção contra inclusão múltipla
Protótipo de função
double scale(double factor); informa ao compilador que deve converter seu argumento para double.R
Receita
Recursão
Referência indefinida
sensor.o da ligação pode deixar uma referência indefinida a uma função pública de leitura que somente sensor.o define.register
register marca uma variável local com uma indicação de otimização que compiladores podem ignorar e proíbe obter seu endereço.register a um contador de laço não obriga um compilador moderno a usar um registrador do processador.Registro de ativação
factorial(4), quatro chamadas de fatorial têm registros de ativação separados.Regra
main.o lista tanto main.c quanto os cabeçalhos do projeto incluídos durante sua compilação.Regra de padrão
% para corresponder a uma parte compartilhada do nome de arquivo, permitindo que um padrão de dependências e uma receita gerem vários alvos.%.o: %.c relaciona sensor.o a sensor.c usando sensor como parte compartilhada do nome.S
Sombreamento
total em um laço pode deixar inalterado o acumulador externo pretendido.U
Unidade de tradução
V
Valor de retorno
double result sem ser impresso.Variável automática do make
$@ nomeia sensor.o enquanto sua receita de arquivo-objeto é executada.Variável do make
$(CFLAGS).CC muda o texto do compilador usado pelas receitas que fazem referência a $(CC).Variável global
main.Variável local estática
static, que mantém um valor armazenado entre chamadas repetidas.void
void especifica ausência de valor de retorno quando usada como tipo de retorno, ou ausência de parâmetros quando usada sozinha em uma lista de parâmetros.void executa sua tarefa de saída, mas não pode fornecer o lado direito de uma atribuição.#
#error
#error exige um diagnóstico quando é alcançada durante o pré-processamento, permitindo rejeitar uma compilação não aceita.#error quando o padrão C escolhido for mais antigo do que o programa exige.Sobre esta aula
Crie seus próprios Materiais de Estudo
Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.