Funções e estrutura de programas em C

Aprenda a organizar programas em C como em projetos reais. Escreva funções com parâmetros, valores de retorno e protótipos, entenda por que C passa argumentos por valor e rastreie chamadas e recursão. Controle onde as variáveis são visíveis e por quanto tempo existem usando escopo e classes de armazenamento, incluindo 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

  1. 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 resultado
      Escreva definições de funções com tipo de retorno, parâmetros e corpo, use return e void corretamente e chame funções em expressões.
    • Declare funções com protótipos
      Declare 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 resultado
      Explique 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ão
      Acompanhe 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.
  2. 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 armazenamento
      Distinga 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 cuidado
      Defina 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.
  3. 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 macros
      Explique 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 condicionalmente
      Use #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.
  4. 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-fonte
      Descreva 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çalho
      Coloque 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 extern
      Use 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.
  5. 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 makefiles
      Escreva 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ão
      Use variáveis como CC e CFLAGS, 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.

Guia de estudo de Funções em C

5 capítulos

1

Escreva, 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.
  • return encerra a chamada; imprimir não fornece seu resultado.
  • Use void para 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çãoSignificado em C17Significado em C23
int read_sensor(void);Sem parâmetrosSem parâmetros
int read_sensor();Informações dos parâmetros não especificadasSem 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 chamadoraEfeito 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 ativaOperaçã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.
2

Controle 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
EspecificadorSignificado neste código C17
autoArmazenamento automático explícito para uma variável de bloco; geralmente omitido
registerUma indicação para acesso rápido; é proibido obter seu endereço
static dentro de uma funçãoArmazenamento mantido com um nome local
static no escopo de arquivoLigação interna, mantendo o nome dentro de sua unidade de tradução
externUma 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 static determina 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.
3

Use 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
NomeInformaçã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

  • #ifdef testa a existência; #if testa um valor.
  • Identificadores não definidos em #if são avaliados como zero.
  • -DNAME=value fornece 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.
4

Divida 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.

EtapaResultadoOpção do GCC que para nessa etapa
Pré-processamentoTexto C expandido-E
CompilaçãoTexto assembly-S
MontagemArquivo-objeto-c
LigaçãoExecutávelCompilaçã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.
  • -c produz 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çalhoMantenha em um arquivo-fonte
Protótipos de funçõesCorpos de funções com ligação externa
Declarações extern de variáveisDefinições de objetos compartilhados
Macros e tipos necessáriosFunçõ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çãoSignificado
ExternaDeclarações podem referir-se a uma entidade entre unidades de tradução
InternaDeclarações referem-se a uma entidade dentro de uma unidade de tradução
NenhumaUm 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 extern e uma definição.
  • static no 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 extern no escopo de arquivo em uma definição.
  • A persistência de static local e a ligação de static no escopo de arquivo têm funções diferentes.
5

Compile 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; reutilizar main.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ávelValor 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 .d gerados 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

5 Lições Interativas

1

Crie e rastreie uma chamada de função

19 passos
Escreva uma função, declare-a, guarde seu resultado e rastreie um cálculo recursivo.
  1. 1
    Introduçã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.

  2. 2
    ExemploDê 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;
    }
    

    int antes do nome descreve o resultado. int reading declara 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.

  3. 3
    ExemploUse uma chamada como expressão

    Em int result = corrected(9);, o argumento 9 inicializa o parâmetro reading. A expressão de retorno produz 13, e a atribuição da função chamadora armazena esse valor em result.

    A chamada também pode aparecer dentro de outra expressão: 2 * corrected(9) resulta em 26. Escrever apenas corrected(9); ainda executa a chamada, mas descarta o valor retornado.

  4. 4
    DicaEscolha 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 void pode 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 um return adequado; exibir um número não o fornece à função chamadora.

  5. 5
    DesafioVerifique 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 em 13, a divisão inteira resulta em 6 e a conversão de retorno resulta em 6.0. Use 2.0 para tornar a divisão de ponto flutuante se precisar de 6.5. O tipo de retorno não pode recuperar uma fração já descartada.

  6. 6
    Explicaçã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.

  7. 7
    Ideia-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.

  8. 8
    DesafioRastreie uma conversão permitida

    O compilador já recebeu int corrected(int reading);. O que corrected(6.8) retorna, e a chamada precisa ser rejeitada?

    ---reveal---

    A chamada pode compilar. O protótipo converte 6.8 no inteiro 6, depois a função soma 4 e retorna 10. 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.

  9. 9
    ExemploDesenhe 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, sample e o parâmetro contêm 9 em objetos separados. O corpo altera seu parâmetro para 13. Quando a chamada retorna, sample ainda contém 9: não houve nenhuma atribuição a ele.

  10. 10
    Conexã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 atualiza sample.

  11. 11
    DesafioEncontre 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 sample depois disso?

    ---reveal---

    A primeira chamada calcula 11 e o descarta, deixando sample em 7. A segunda também recebe 7, retorna 11 e, desta vez, a função chamadora o armazena. O valor final é 11, não 15; uma chamada não atualiza a variável usada como argumento apenas por retornar um valor alterado.

  12. 12
    Explicaçã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.

  13. 13
    ExemploEscreva as chamadas na descida

    Rastreie total_to(3). A primeira chamada mantém n == 3 e espera para somar 3 a total_to(2). Essa chamada mantém seu próprio n == 2 e espera por total_to(1).

    A chamada com n == 1 espera por total_to(0), que retorna 0 imediatamente. Nesse ponto mais profundo, quatro chamadas estão ativas. Não substitua os valores anteriores de n por zero; eles pertencem a chamadas diferentes.

  14. 14
    ExemploPreencha os resultados na volta

    Quando total_to(0) retorna 0, o trabalho em espera pode terminar:

    • A chamada com n == 1 retorna 1 + 0, ou 1.
    • A chamada com n == 2 retorna 2 + 1, ou 3.
    • A chamada com n == 3 retorna 3 + 3, ou 6.

    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.

  15. 15
    DesafioVerifique se um caso-base é alcançável

    Suponha que o teste do caso-base em total_to seja alterado para n == 0 e que a etapa recursiva continue usando n - 1. O que dá errado quando o argumento inicial é -2?

    ---reveal---

    As entradas tornam-se -2, -3, -4 e 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 com n <= 0 interrompe essa entrada imediatamente.

  16. 16
    DicaVerifique também o tamanho da resposta

    Um cálculo recursivo que termina ainda pode ultrapassar seu tipo de retorno. Com um int de 32 bits, factorial(12) cabe, enquanto o valor matemático de factorial(13) excede o máximo 2147483647.

    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.

  17. 17
    DicaSepare 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 altera i sem o sequenciamento necessário. A passagem por valor não torna segura uma expressão de argumento insegura.

  18. 18
    DesafioCombine declaração, conversão e chamadas

    Com total_to declarada antes do uso, uma função chamadora escreve int x = 2; total_to(x); x = total_to(3.9);. Qual é o valor final de x?

    ---reveal---

    O primeiro resultado é descartado, portanto x permanece 2. O segundo argumento é convertido em 3; o rastreamento recursivo retorna 6, que a atribuição armazena em x. A declaração controla a conversão, a recursão calcula o valor e a função chamadora decide se vai guardá-lo.

  19. 19
    Recapitulaçã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.

2

Acompanhe nomes e valores mantidos

13 passos
Rastreie um bloco, um contador estático e um total compartilhado sem confundir seus objetos.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoComece com uma pequena tarefa
  2. 2
    ExemploLeia a declaração visível mais próxima
  3. 3
    ExplicaçãoConsidere o escopo em que a função foi escrita
  4. 4
    DesafioCorrija um valor necessário após um bloco
  5. 5
    ExemploObserve um contador automático reiniciar
  6. 6
    ExemploMantenha o contador com static
  7. 7
    DesafioDiferencie inicialização de atribuição
  8. 8
    DicaLeia a palavra-chave em seu contexto
  9. 9
    ExemploRastreie uma atualização global
  10. 10
    DesafioEncontre a variável local que oculta a global
  11. 11
    ConexãoTorne um cálculo mais fácil de testar
  12. 12
    DesafioCombine escopo e estado mantido
  13. 13
    RecapitulaçãoAnote nomes e tempos de vida separadamente
3

Examine o código antes da compilação

13 passos
Expanda expressões, selecione um ramo de depuração e acompanhe uma proteção de cabeçalho.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoComece com uma pequena tarefa
  2. 2
    ExplicaçãoVeja o que a inclusão realmente fornece
  3. 3
    ExemploSubstitua tokens antes de usar as regras dos operadores
  4. 4
    ExemploProteja o agrupamento interno e externo
  5. 5
    DesafioDescubra o que parênteses não podem corrigir
  6. 6
    DicaSepare as regras de macros das regras de escopo
  7. 7
    ExemploTeste uma configuração pela existência e pelo valor
  8. 8
    DesafioSelecione um ramo com uma opção do compilador
  9. 9
    ExemploAcompanhe a primeira inclusão do cabeçalho
  10. 10
    Ideia-chaveInicie um novo estado da proteção em outra unidade
  11. 11
    ConexãoLocalize um diagnóstico no código-fonte
  12. 12
    DesafioCombine expansão e seleção de ramos
  13. 13
    RecapitulaçãoExamine a transformação em ordem
4

Gere um programa a partir de dois arquivos-fonte

14 passos
Crie um cabeçalho e uma implementação, ligue seus arquivos-objeto e proteja o estado privado.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoComece com uma pequena tarefa
  2. 2
    ExplicaçãoAcompanhe uma unidade de tradução até um arquivo-objeto
  3. 3
    ExemploEscreva a declaração compartilhada
  4. 4
    ExemploColoque a implementação em seu próprio arquivo-fonte
  5. 5
    DesafioPosicione corretamente uma variável compartilhada
  6. 6
    ExemploCompile separadamente e depois faça a ligação
  7. 7
    DesafioCorrija uma referência indefinida
  8. 8
    DicaNão copie a implementação para a função chamadora
  9. 9
    ExemploMantenha um contador dentro do módulo
  10. 10
    ExplicaçãoSepare três propriedades do contador
  11. 11
    DesafioExplique dois contadores independentes
  12. 12
    DicaFaça um cabeçalho funcionar por conta própria
  13. 13
    DesafioCorrija a privacidade e a interface juntas
  14. 14
    RecapitulaçãoRelacione cada necessidade à sua etapa de compilação
5

Compile apenas o que mudou

15 passos
Escreva dependências, rastreie recompilações e encurte um makefile sem perder seu significado.

Salve esta aula para seguir cada Lição Interativa passo a passo.

  1. 1
    IntroduçãoComece com uma pequena tarefa
  2. 2
    ExemploRegistre as entradas do executável e dos arquivos-objeto
  3. 3
    ExemploDê uma receita a um arquivo-objeto
  4. 4
    ExemploExecute a primeira compilação e recompile um arquivo-fonte
  5. 5
    DesafioRastreie uma edição em um cabeçalho compartilhado
  6. 6
    DicaExamine o plano antes de executá-lo
  7. 7
    ExplicaçãoSubstitua configurações repetidas por variáveis
  8. 8
    ExemploExpanda a receita de ligação
  9. 9
    ExemploUse um padrão de compilação para os dois arquivos-objeto
  10. 10
    DesafioEscolha um arquivo-fonte sem compilar o cabeçalho
  11. 11
    DicaFaça da limpeza uma ação
  12. 12
    ConexãoDeixe o compilador registrar dependências de cabeçalhos
  13. 13
    DesafioMantenha correta uma recompilação paralela
  14. 14
    DesafioAplique opções alteradas a todo o programa
  15. 15
    RecapitulaçãoVerifique o grafo antes de confiar no resultado

Questões de Funções em C

50 Questões de Prática em 5 conjuntos

Conjunto 1: Rastreie nomes locais e estado mantido10 Questões de Prática

Questão 1 de 10

Por que int amount = amount + 2; dentro de um bloco não é inicializado a partir de um amount externo?
  1. O nome externo perde seu valor armazenado.
  2. Todos os inicializadores são avaliados antes do início do escopo.
  3. O novo nome está no escopo de seu inicializador.
  4. As duas declarações identificam o mesmo objeto local.
Mostrar respostaOcultar resposta
Resposta: C
O novo nome está no escopo antes de seu inicializador ser avaliado, portanto o lado direito lê a nova variável local não inicializada. O sombreamento não copia o valor do objeto externo para ela.

Questão 2 de 10

Quantas vezes static int calls = 0; é inicializada durante uma execução do programa?
  1. Uma vez, antes do início do programa.
  2. Uma vez por chamada de sua função.
  3. Uma vez em cada nível de recursão.
  4. Uma vez a cada leitura.
Mostrar respostaOcultar resposta
Resposta: A
A variável local estática é inicializada uma vez antes do início do programa. Sua declaração não a reinicia na entrada; uma atribuição separada como 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;
}
  1. 7, 7, 7
  2. 5, 7, 9
  3. 7, 9, 11
  4. 5, 5, 5
Mostrar respostaOcultar resposta
Resposta: C
O incremento precede cada retorno, e o valor mantido progride de 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);
}
  1. 9 4
  2. 4 4
  3. 9 9
  4. 4 9
Mostrar respostaOcultar resposta
Resposta: A
Dentro de 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);
  1. 6
  2. 0
  3. 12
  4. 5
Mostrar respostaOcultar resposta
Resposta: B
Cada iteração do laço declara um novo 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

Onde o nome i de for (int i = 0; i < 6; i++) { } pode ser usado?
  1. Em toda a função que contém o laço.
  2. Em todo o restante do arquivo-fonte.
  3. Em cada função chamada pelo laço.
  4. Dentro dessa instrução for.
Mostrar respostaOcultar resposta
Resposta: D
A declaração no cabeçalho limita o escopo de 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;
}
  1. 6
  2. 9
  3. 7
  4. 3
Mostrar respostaOcultar resposta
Resposta: B
As três entradas na função atualizam o mesmo 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

Um cálculo int permitted(int n) lê uma variável global limit. Por que o mesmo argumento pode produzir um resultado diferente em uma chamada posterior?
  1. Passar um argumento int altera sua variável de origem.
  2. Cada chamada reinicia em zero os objetos de escopo de arquivo.
  3. Um nome local na função chamadora substitui a variável global.
  4. Outra operação pode ter alterado limit.
Mostrar respostaOcultar resposta
Resposta: D
A função depende do valor global além de 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);
}
  1. 3 4
  2. 9 9
  3. 2 7
  4. 8 9
Mostrar respostaOcultar resposta
Resposta: D
A primeira chamada recebe o valor local 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

Uma função precisa inicializar uma variável numérica persistente com read_sensor() em tempo de execução. Por que static int first = read_sensor(); é inválido em C17?
  1. Uma variável local estática não pode ser lida após a inicialização.
  2. Um inicializador numérico estático deve ser constante.
  3. Uma função não pode retornar um valor a uma variável local.
  4. Uma variável local estática é inicializada em toda chamada.
Mostrar respostaOcultar resposta
Resposta: B
A chamada de função é um cálculo em tempo de execução, não uma expressão constante adequada para esse inicializador estático. O armazenamento estático em si é permitido dentro da função; o problema é o inicializador. Uma variável local automática comum pode ser inicializada a partir de uma chamada, enquanto um estado persistente precisa de uma atribuição em tempo de execução no código executável.
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

Qual é o parâmetro em int triple(int value) { return value * 3; }?
  1. triple
  2. int
  3. value
  4. value * 3
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

O que void report(int code) permite que uma função chamadora faça?
  1. Armazenar report(4) em um int.
  2. Chamar report(4); como instrução.
  3. Somar report(4) a um inteiro.
  4. Retornar report(4) de uma função int.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Qual valor double portion(int total) { return total / 4; } retorna para portion(11)?
  1. 2.0
  2. 2.75
  3. 3.0
  4. 2.5
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Uma definição de int capacity(void) aparece abaixo de main. Qual declaração acima de main descreve completamente uma chamada sem entrada em C17?
  1. int capacity();
  2. void capacity(int);
  3. int capacity(void) { }
  4. int capacity(void);
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Com int fifth(int n) { return n / 5; } declarada antes do uso, qual é o valor de fifth(-14.8)?
  1. -3
  2. -2
  3. -2.96
  4. -14
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Um cabeçalho declara double stretch(int n);. Qual definição é compatível com essa declaração?
  1. int stretch(int count) { return count * 1.5; }
  2. double stretch(double n) { return n * 1.5; }
  3. double stretch(void) { return 1.5; }
  4. double stretch(int count) { return count * 1.5; }
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);
}
  1. 17
  2. 13
  3. 4
  4. 0
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);
  1. 22
  2. 28
  3. 16
  4. 14
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);
}
  1. 3
  2. 4
  3. 5
  4. 6
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);
}
  1. 7
  2. 8
  3. 6
  4. 5
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

Com #define TRACE 0, o que #ifdef TRACE faz?
  1. Ignora seu ramo porque TRACE é expandido para zero.
  2. Mantém seu ramo porque TRACE está definido.
  3. Rejeita a definição porque zero não é um nome.
  4. Lê uma variável de tempo de execução chamada TRACE.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Qual texto #define LIMIT 18 substitui no código-fonte posterior?
  1. Os caracteres LIMIT dentro de "LIMIT".
  2. O token isolado LIMIT.
  3. O sufixo do nome LOCAL_LIMIT.
  4. A instrução inteira que contém LIMIT.
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
  1. 16
  2. 20
  3. 19
  4. 15
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Por que #define LIMIT 12; torna int count = LIMIT * 2; inválido?
  1. A macro não pode substituir um token após =.
  2. Uma macro numérica precisa de um tipo de retorno declarado.
  3. O ponto e vírgula torna-se parte da expressão.
  4. A macro transforma count em um nome de constante.
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);
  1. a = 5, b = 5
  2. a = 10, b = 5
  3. a = 19, b = 10
  4. a = 19, b = 5
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))
  1. Avalia o argumento uma vez e eleva esse valor ao cubo.
  2. Incrementa três vezes em uma ordem especificada da esquerda para a direita.
  3. É rejeitada porque uma macro não pode receber expressões.
  4. Tem comportamento indefinido por alterações repetidas não sequenciadas.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Por que #if VERBOSE pode ignorar um ramo silenciosamente quando a compilação define apenas VERB0SE?
  1. O identificador não definido VERBOSE conta como zero.
  2. Os dois nomes compartilham uma definição porque são parecidos.
  3. Todo nome não definido no pré-processador é um erro fatal de compilação.
  4. A diretiva espera um valor de VERBOSE em tempo de execução.
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
  1. 6
  2. 7
  3. 8
  4. 2
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));
}
  1. item2=7
  2. JOIN(item, 2)=7
  3. JOIN(item, 2)=2
  4. item2=2
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Um cabeçalho com proteção contra inclusão múltipla contém 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?
  1. O segundo arquivo-fonte reutiliza o estado da proteção do primeiro.
  2. A proteção transforma as duas definições em declarações compartilhadas.
  3. Cada unidade de tradução inclui sua própria definição externa.
  4. O ligador remove os nomes das proteções antes de verificar duplicatas.
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

O que gcc -std=c17 -c driver.c normalmente produz em um sistema do tipo Unix?
  1. Um arquivo-objeto chamado driver.o.
  2. Um executável chamado driver.
  3. Um texto assembly chamado driver.s.
  4. Um arquivo de cabeçalho chamado driver.h.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma função chamadora inclui um protótipo correto para reading, mas ligar apenas seu arquivo-objeto informa uma referência indefinida a reading. O que deve ser adicionado?
  1. Outro protótipo compatível na função chamadora.
  2. Uma proteção em torno da declaração existente da função.
  3. O arquivo-objeto que define reading nas entradas da ligação.
  4. O mesmo cabeçalho como uma entrada compilada separadamente.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um cabeçalho declara 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?
  1. Adicionar o arquivo que contém a definição a um comando de ligação posterior.
  2. Colocar um segundo protótipo apenas no arquivo-fonte da chamada.
  3. Adicionar uma proteção contra inclusão múltipla apenas ao código da chamada.
  4. Incluir o mesmo cabeçalho no arquivo que contém a definição.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Qual ligação static int cached = 3; tem quando declarado fora de todas as funções?
  1. Ligação externa.
  2. Ligação interna.
  3. Ausência de ligação.
  4. Ligação automática.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um cabeçalho protegido contém 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ê?
  1. 2, porque as duas inclusões nomeiam o mesmo contador compartilhado.
  2. 1, porque a proteção permite apenas uma chamada bem-sucedida.
  3. 0, porque os dois arquivos-fonte têm contadores independentes.
  4. Um erro de ligação, porque os dois contadores estáticos são externos.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

O que deve ficar em um cabeçalho para permitir que vários arquivos-fonte usem um único int failures definido em um deles?
  1. int failures = 0;
  2. extern int failures;
  3. extern int failures = 0;
  4. static int failures = 0;
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?
  1. Incluir calc.h em main.c, mantendo os dois arquivos-fonte na compilação.
  2. Remover calc.c da compilação e manter sua inclusão em main.c.
  3. Manter os dois arquivos-fonte e adicionar outra proteção a calc.h.
  4. Incluir main.c dentro de calc.c para que as duas definições concordem.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um arquivo-fonte define 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?
  1. Remover static da variável e manter todos os usos de quota.
  2. Adicionar static int quota; ao cabeçalho e manter as leituras diretas.
  3. Repetir extern int quota; na função chamadora antes de cada uso.
  4. Remover a declaração da variável externa e usar quota_value() para lê-la.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um cabeçalho declara size_t packet_count(void);. O que torna o cabeçalho autossuficiente?
  1. Pedir que cada função chamadora inclua <stddef.h> após o cabeçalho.
  2. Mover a declaração para cada arquivo-fonte com uma cópia do nome do tipo.
  3. Incluir o arquivo-fonte da implementação antes do cabeçalho nas funções chamadoras.
  4. Incluir <stddef.h> antes dessa declaração no cabeçalho.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Dois arquivos-fonte incluem um cabeçalho com 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?
  1. Usar extern int jobs; no cabeçalho e uma definição em um arquivo-fonte.
  2. Usar static int jobs = 1; no cabeçalho e nenhuma definição em arquivo-fonte.
  3. Manter o cabeçalho inalterado e protegê-lo com um novo nome de macro.
  4. Remover o cabeçalho e inicializar jobs nos dois arquivos-fonte.
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

O que faz um alvo comum existente do make precisar ser gerado novamente após a atualização de seus pré-requisitos?
  1. Seu arquivo-fonte tem mais linhas do que antes.
  2. Um pré-requisito tem uma data de modificação mais recente.
  3. Sua receita contém mais de um comando.
  4. O mesmo comando foi executado em um dia anterior.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

O executável 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?
  1. Compilar main.c e depois ligar viewer.
  2. Compilar os dois arquivos-fonte e depois ligar viewer.
  3. Ligar viewer sem compilar nenhum arquivo-fonte.
  4. Compilar screen.c e depois ligar viewer.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Um arquivo-fonte inclui 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?
  1. O make não tem um caminho declarado do cabeçalho ao arquivo-objeto.
  2. Cabeçalhos são sempre processados apenas durante a execução do programa.
  3. O ligador restaura automaticamente o conteúdo anterior do cabeçalho.
  4. Uma inclusão no arquivo-fonte impede que seu arquivo-objeto dependa de cabeçalhos.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Para a regra de ligação tool: main.o engine.o, em que $@ e $^ são expandidos?
  1. main.o e engine.o
  2. tool e main.o
  3. tool e main.o engine.o
  4. main.o engine.o e tool
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 $@
  1. Fonte sensor.c, saída sensor.o, com -DTRACE=1 -std=c17.
  2. Fontes sensor.c shared.h, saída sensor.o, com -DTRACE=1 -std=c17.
  3. Fonte sensor.o, saída sensor.c, com -DTRACE=1 -std=c17.
  4. Fonte sensor.c, saída shared.h, com -DTRACE=1 -std=c17.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma compilação recente tem a ordem de datas de modificação 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?
  1. Os dois arquivos-objeto são gerados novamente antes da ligação; o grafo de dependências está completo.
  2. Apenas helper.o é gerado novamente antes da ligação; falta uma dependência de main.o.
  3. Nenhum arquivo-objeto é gerado novamente; cabeçalhos não podem provocar receitas de arquivos-objeto.
  4. Apenas main.o é gerado novamente antes da ligação; falta uma dependência de helper.o.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Por que um arquivo chamado clean pode impedir uma regra clean: sem pré-requisitos e sem marcação de remover arquivos-objeto?
  1. O make reserva todo alvo chamado clean para compilação interna.
  2. O shell recusa comandos de alvos que têm arquivos correspondentes.
  3. O make pode tratar esse alvo existente como já atualizado.
  4. O compilador remove receitas de limpeza quando a otimização está ativada.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Um projeto carrega arquivos .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?
  1. Compilar apenas main.c e depois ligar; não executar nenhuma dessas receitas.
  2. Compilar os dois arquivos-fonte e depois ligar; não executar nenhuma dessas receitas.
  3. Compilar os dois arquivos-fonte e depois ligar; executar as três receitas.
  4. Apenas ligar; não executar receitas porque os arquivos de dependências já existem.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um projeto tem 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?
  1. Gera novamente os dois arquivos-objeto e liga; não é necessária outra recompilação para as novas opções.
  2. Gera novamente main.o e liga; force uma recompilação completa para atualizar work.o também.
  3. Refaz apenas a ligação e mantém os dois arquivos-objeto; altere o cabeçalho novamente para aplicar as opções.
  4. Gera novamente work.o e liga; force uma recompilação completa para atualizar main.o também.
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?
  1. A ligação pode começar depois que um dos arquivos-objeto terminar e reutilizar o outro arquivo antigo.
  2. Cada arquivo-objeto espera pelo outro, depois a ligação começa com os dois arquivos antigos.
  3. As duas receitas de arquivos-objeto podem ser executadas juntas; a ligação espera ambas terminarem.
  4. A ligação é executada primeiro para reservar app, depois as duas receitas de arquivos-objeto o substituem.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Travou em uma questão sua? Ajuda com Tarefas

Termos-Chave

68 Termos-Chave, cada um com sua definição

A

Alvo
Um alvo é o arquivo de saída ou a ação que se pede ao make para atualizar.
O alvo do executável geralmente é colocado primeiro para que executar make sem argumentos gere todo o programa.
Alvo fictício
Um alvo fictício nomeia uma ação declarada com .PHONY, de modo que um arquivo existente com esse nome não possa impedir sua receita.
Declarar clean como fictício garante que a limpeza continue sendo executada se um arquivo chamado clean aparecer no diretório.
Alvo padrão
O alvo padrão é o alvo que o make escolhe sem um argumento de alvo explícito, normalmente o primeiro alvo comum do arquivo.
Colocar all ou o executável final primeiro impede que uma execução comum do make pare em um único arquivo-objeto.
Argumento
Um argumento é uma expressão fornecida por uma chamada para dar o valor inicial de um parâmetro.
Em cube(5 + 1), o argumento é avaliado como 6 antes de o corpo de cube começar.
Arquivo de cabeçalho
Um arquivo de cabeçalho compartilha declarações, macros ou tipos por meio de inclusão textual.
Um cabeçalho de conversão declara os dois sentidos da conversão sem fornecer seus corpos compilados.
Arquivo de dependências gerado
Um arquivo de dependências gerado registra pré-requisitos de arquivos-fonte e cabeçalhos descobertos pelo compilador para o make carregar em compilações posteriores.
Compilar com -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
Um arquivo-objeto contém código de máquina compilado e informações de símbolos que o ligador pode combinar com outros arquivos-objeto.
Após editar apenas sensor.c, a compilação pode substituir sensor.o e reutilizar um main.o inalterado.
Ausência de ligação
A ausência de ligação significa que um nome identifica sua própria entidade em vez de conectar declarações por ligação interna ou externa.
Variáveis locais automáticas comuns e parâmetros não têm ligação, mesmo quando outra função usa os mesmos nomes.
auto
A palavra-chave auto indica explicitamente armazenamento automático em C17; C23 também a usa para inferir um tipo a partir de um inicializador.
Em C17, auto int level; tem o mesmo comportamento de armazenamento que uma variável local comum int level;.

C

Cabeçalho autossuficiente
Um cabeçalho autossuficiente inclui os cabeçalhos exigidos por suas próprias declarações, tornando desnecessária uma ordem especial de inclusão.
Um cabeçalho que declara uma função com size_t inclui <stddef.h> antes dessa declaração.
Caso recursivo
Um caso recursivo é um ramo que resolve parte de um problema chamando a mesma função com uma entrada mais próxima de um caso-base.
Subtrair um de uma entrada positiva para o fatorial aproxima o caso recursivo de seu teste de parada.
Caso-base
Um caso-base responde diretamente a uma entrada sem fazer outra chamada recursiva.
O caso-base do fatorial retorna 1, permitindo que as multiplicações em espera nas chamadas anteriores terminem.
Compilação condicional
A compilação condicional mantém ou ignora ramos do código-fonte de acordo com testes de pré-processamento antes de o código resultante ser compilado.
Uma impressão de depuração removida pela compilação condicional está ausente do executável resultante.
Concatenação de tokens
A concatenação de tokens usa ## em uma lista de substituição de macro para combinar tokens vizinhos em um único token de pré-processamento.
Uma chamada de macro que junta temp e 2 pode formar o identificador temp2 no código-fonte expandido.
Conversão de argumento
Uma conversão de argumento altera o valor de um argumento fornecido para o tipo de parâmetro especificado por um protótipo de função visível.
Passar 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
A conversão em literal de cadeia usa # 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.
Uma macro de diagnóstico pode converter 3 + 4 em literal de cadeia para exibir a expressão junto com seu valor calculado.

D

Declaração
Uma declaração introduz um nome e as informações de tipo necessárias para verificar seus usos.
Uma declaração acima de main permite verificar uma chamada mesmo quando o corpo da função vem depois.
Definição de função
Uma definição de função fornece o corpo executável de uma função junto com seu nome, tipo de retorno e lista de parâmetros.
A definição de 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
Um erro de definição múltipla ocorre quando uma ligação recebe definições conflitantes do mesmo nome com ligação externa.
Dois arquivos-fonte ligados que definem main causam um erro de definição múltipla ou de símbolo duplicado.
Definição tentativa
Uma definição tentativa é uma declaração de objeto no escopo de arquivo sem inicializador nem extern que pode fornecer uma definição.
Colocar int count; em um cabeçalho compartilhado não é uma substituição portável para uma declaração extern.
Dependência oculta
Uma dependência oculta é uma entrada ou necessidade de estado que afeta uma função sem aparecer em sua lista de parâmetros.
Um cálculo que lê um limite global precisa que esse limite seja preparado antes que a função possa ser testada.
Diretiva de pré-processamento
Uma diretiva de pré-processamento começa com # e controla a transformação do texto-fonte.
A diretiva #include "sensor.h" insere o texto do cabeçalho no pré-processamento atual.
Duração de armazenamento
A duração de armazenamento descreve quanto tempo o armazenamento de um objeto existe durante a execução, independentemente de onde seu nome está visível.
Uma variável local estática combina escopo de bloco com duração de armazenamento por toda a execução do programa.
Duração de armazenamento automática
A duração de armazenamento automática dá a variáveis locais comuns e parâmetros armazenamento vinculado à execução de seu bloco ou à chamada de função.
Duas chamadas recursivas ativas têm parâmetros automáticos distintos mesmo que seus nomes sejam iguais.
Duração de armazenamento estática
A duração de armazenamento estática significa que um objeto existe durante toda a execução do programa e é inicializado uma vez antes de seu início.
Um objeto numérico de escopo de arquivo sem inicializador explícito começa em zero e mantém atribuições posteriores.

E

Efeito colateral
Um efeito colateral é uma alteração no estado do programa ou uma interação, como uma saída, além do simples cálculo do valor de uma expressão.
Incrementar um contador compartilhado é um efeito colateral, portanto a ordem de chamadas separadas ao contador pode ser importante.
Escopo
O escopo de um nome é a região do código-fonte em que sua declaração está visível.
Uma variável pode continuar existindo enquanto seu nome está oculto por outra declaração em um bloco aninhado.
Escopo de arquivo
O escopo de arquivo estende-se de uma declaração fora de função até o fim da unidade de tradução.
Uma função auxiliar definida acima de uma variável global precisa de uma declaração anterior para poder usá-la.
Escopo de bloco
O escopo de bloco limita um nome local à região que vai de logo após seu declarador até o fim do bloco que o contém.
Uma variável declarada dentro de um bloco if não pode ser usada pelo nome em uma impressão após o fechamento desse bloco.
Escopo de protótipo
O escopo de protótipo limita o nome de um parâmetro ao protótipo de função que o declara.
Um protótipo pode chamar seu parâmetro de value, enquanto a definição chama o mesmo parâmetro de n.
Estado compartilhado
O estado compartilhado são dados persistentes que várias operações podem ler ou alterar.
Uma contagem de erros compartilhada faz um relatório posterior depender das funções anteriores que a atualizaram.
Execução simulada
Uma execução simulada com make -n imprime receitas comuns de compilação sem executá-las, mostrando os comandos planejados do compilador e do ligador.
Examine uma execução simulada após editar um cabeçalho para ver quais arquivos-objeto o make planeja gerar novamente.
extern
A palavra-chave extern pode declarar uma entidade com ligação sem definir um objeto, desde que nenhum inicializador torne a declaração uma definição.
Um cabeçalho compartilhado declara extern int errors;, enquanto um arquivo-fonte fornece a definição inicializada do objeto.

F

Função
Uma função é um bloco nomeado de código executável que realiza uma tarefa quando chamado.
Uma função de conversão de temperatura permite que várias partes de um programa reutilizem um cálculo testado.
Função chamadora
Uma função chamadora é a função que chama outra função e retoma a execução após seu retorno.
Quando main chama read_sensor, main é a função chamadora que espera receber a leitura retornada.

L

Ligação
A ligação determina se declarações de um nome em lugares diferentes se referem ao mesmo objeto ou função.
Uma variável global pode ter duração de armazenamento estática enquanto static restringe sua ligação a uma unidade de tradução.
Ligação externa
A ligação externa permite que declarações em unidades de tradução diferentes identifiquem o mesmo objeto ou função.
Uma função pública de contador tem ligação externa para que funções chamadoras em outro arquivo-fonte possam ligar-se à sua definição.
Ligação interna
A ligação interna restringe a identidade compartilhada de um nome a uma unidade de tradução, mantendo sua entidade privada em relação a outras unidades.
Dois arquivos podem definir, cada um, uma função auxiliar estática chamada check sem conflito no momento da ligação.
Ligador
Um ligador combina arquivos-objeto e bibliotecas e resolve nomes externos para suas definições.
O ligador precisa do arquivo-objeto que define uma função de conversão mesmo quando a função chamadora incluiu seu protótipo.

M

Macro predefinida
Uma macro predefinida é fornecida pela implementação e fornece informações como a linha atual do código-fonte ou a versão da linguagem.
Um diagnóstico que usa __FILE__ e __LINE__ pode identificar a origem de sua mensagem.
Macro semelhante a função
Uma macro semelhante a função recebe argumentos como tokens e os substitui em um texto de substituição antes da compilação comum de C.
Parênteses em uma macro semelhante a função preservam o agrupamento, mas não impedem a avaliação repetida de argumentos.
Macro semelhante a objeto
Uma macro semelhante a objeto associa um nome a tokens de substituição que são inseridos quando esse nome aparece posteriormente.
A definição #define LIMIT 16 substitui o token LIMIT, mas deixa "LIMIT" inalterado.
Módulo
Um módulo agrupa código de implementação C relacionado em um arquivo-fonte com um cabeçalho correspondente.
Um módulo de contador pode expor duas funções públicas enquanto mantém privada sua contagem armazenada.

O

Operador defined
O operador defined de pré-processamento resulta em um se seu operando, um nome de macro, estiver definido e em zero se não estiver.
Use #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
A ordem não especificada de avaliação dos argumentos significa que C não determina uma ordem obrigatória de avaliação das expressões dos argumentos de uma chamada.
Duas chamadas que geram senhas em uma impressão podem fornecer seus números em qualquer ordem.

P

Parâmetro
Um parâmetro é uma variável de função inicializada com uma cópia do valor de um argumento na entrada da função.
Em int cube(int n), o parâmetro n recebe um valor de entrada separado para cada chamada.
Passagem por valor
A passagem por valor é a regra de passagem de argumentos de C: cada parâmetro recebe uma cópia do valor calculado por sua expressão de argumento.
Uma atribuição ao parâmetro n dentro de uma função deixa a variável inteira comum da função chamadora inalterada.
Pilha de chamadas
Uma pilha de chamadas é a estrutura usual de implementação que acompanha chamadas de função ativas e permite que retornem na ordem inversa à de seu aninhamento.
Uma função de conversão chamada por uma função que imprime tabelas retorna antes que esta retorne a main.
Pré-processador
O pré-processador transforma o texto-fonte por meio de diretivas como inclusão, substituição de macros e seleção condicional.
Execute gcc -E sample.c para examinar o código expandido antes de o compilador verificar expressões C.
Pré-requisito
Um pré-requisito é uma entrada ou dependência declarada que o make verifica e atualiza antes de decidir se um alvo precisa ser gerado novamente.
Um cabeçalho compartilhado é pré-requisito de todo arquivo-objeto cuja unidade de tradução inclui esse cabeçalho.
Proteção contra inclusão múltipla
Uma proteção contra inclusão múltipla impede o processamento repetido de um cabeçalho dentro de uma unidade de tradução.
Dois cabeçalhos podem incluir um cabeçalho comum protegido sem repetir seu conteúdo na mesma unidade.
Protótipo de função
Um protótipo de função é uma declaração que especifica o tipo de retorno e os tipos dos parâmetros de uma função para verificar chamadas.
O protótipo double scale(double factor); informa ao compilador que deve converter seu argumento para double.

R

Receita
Uma receita é a sequência de comandos do shell associada a uma regra do make, com linhas comuns de receita começando com tabulações literais.
A receita de um arquivo-objeto compila seu arquivo-fonte, enquanto a receita do executável liga os arquivos-objeto necessários.
Recursão
A recursão ocorre quando uma função chama a si mesma para resolver outra instância de um problema.
Uma função que soma algarismos pode processar o último algarismo e somar recursivamente os que restam.
Referência indefinida
Uma referência indefinida é uma falha de ligação em que um nome externo necessário não tem uma definição disponível nas entradas da ligação.
Omitir 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
A palavra-chave register marca uma variável local com uma indicação de otimização que compiladores podem ignorar e proíbe obter seu endereço.
Adicionar register a um contador de laço não obriga um compilador moderno a usar um registrador do processador.
Registro de ativação
Um registro de ativação é um registro de uma chamada ativa, que normalmente contém seus parâmetros, variáveis locais automáticas e local de retorno.
No ponto mais profundo de factorial(4), quatro chamadas de fatorial têm registros de ativação separados.
Regra
Uma regra do make indica um alvo, seus pré-requisitos e quaisquer comandos de receita necessários para atualizar esse alvo a partir dessas entradas.
A regra de main.o lista tanto main.c quanto os cabeçalhos do projeto incluídos durante sua compilação.
Regra de padrão
Uma regra de padrão usa % 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.
A regra %.o: %.c relaciona sensor.o a sensor.c usando sensor como parte compartilhada do nome.

S

Sombreamento
O sombreamento ocorre quando uma declaração interna oculta um nome externo visível, fazendo os usos no escopo interno selecionarem a nova entidade.
Declarar um segundo total em um laço pode deixar inalterado o acumulador externo pretendido.

U

Unidade de tradução
Uma unidade de tradução é um arquivo-fonte após o pré-processamento, junto com seu texto incluído, traduzido independentemente.
Incluir o mesmo cabeçalho em dois arquivos-fonte fornece declarações a duas unidades de tradução separadas.

V

Valor de retorno
Um valor de retorno é o resultado que uma função fornece à função chamadora para uso em uma expressão.
O valor de retorno de uma conversão pode ser atribuído a double result sem ser impresso.
Variável automática do make
Uma variável automática do make é definida pelo make para a regra atual, fornecendo valores como os nomes de seu alvo ou de seus pré-requisitos.
A variável automática $@ nomeia sensor.o enquanto sua receita de arquivo-objeto é executada.
Variável do make
Uma variável do make nomeia texto reutilizável, como configurações do compilador ou listas de arquivos-objeto, expandido com uma referência como $(CFLAGS).
Alterar uma atribuição de CC muda o texto do compilador usado pelas receitas que fazem referência a $(CC).
Variável global
Uma variável global é um objeto declarado no escopo de arquivo, com duração de armazenamento estática e visibilidade abaixo de sua declaração.
Duas funções que adicionam viagens podem atualizar um total global de distância que permanece disponível para main.
Variável local estática
Uma variável local estática é um objeto de escopo de bloco declarado static, que mantém um valor armazenado entre chamadas repetidas.
Um contador local estático de senhas avança a cada chamada em vez de recomeçar em seu inicializador.
void
A palavra-chave 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.
Uma função de impressão void executa sua tarefa de saída, mas não pode fornecer o lado direito de uma atribuição.

#

#error
A diretiva #error exige um diagnóstico quando é alcançada durante o pré-processamento, permitindo rejeitar uma compilação não aceita.
Um teste de versão pode selecionar #error quando o padrão C escolhido for mais antigo do que o programa exige.

Sobre esta aula

Por StudeliRelatar um erro

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.

Começar