Somente esta páginaTodas as páginas
Fornecido por GitBook
Não foi possível gerar o PDF para 107 páginas, generation stopped at 100.
Extender com mais 50 páginas.
1 de 100

Manual da Linguagem GNU C

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Pilha e Estouro de Pilha

The Stack, And Stack Overflow

Recursão tem uma desvantagem: há um limite no número de níveis aninhados de chamadas de função que um programa pode fazer. Em C, cada chamada de função aloca um bloco de memória que é utilizado até que a chamada retorne. A linguagem C aloca estes blocos consecutivamente numa grande área de memória conhecida como pilha (stack), portanto chamamos este blocos de quadros de pilha (stack frames).

O tamanho da pilha é limitado; se um programa tenta usar muito dela, isso causa uma falha porque a pilha estará cheia. Isso é chamado de estouro de pilha (stack overflow).

Estouros de pilha no GNU/Linux se manifestam tipicamente como o sinal chamado de SIGSEGV, também conhecido como "falha de segmentação" ("segmentation fault"). Por padrão, este sinal encerra a execução do programa imediatamente ao invés de permitir que o programa se recupere ou atinja seu fim esperado. (Nestes casos, nós normalmente falamos que o programa "crashou"). Veja .

Aqui me rendi ao neologismo "crashou", oriundo do inglês "crash". Até pensei em usar "travou", mas este termo nós brasileiros normalmente utilizamos quando o programa para de responder ("congela"), mas numa situação de estouro de pilha, o programa é encerrado e não fica "congelado" ou "travado".

Não é conveniente tentar observar um estouro de pilha passando um número grande como argumento para uma função recursiva que implemente Fibonacci porque o programa rodaria por muito tempo antes de dar erro. O algoritmo é simples, mas lentíssimo: ao calcular fib (n), o número de chamadas (recursivas) fib (1) ou fib (2) que ele vai fazer é igual ao resultado final.

Todavia, você pode rapidamente observar um estouro de pilha usando seguinte função:

int
fill_stack (int n)
{
  if (n <= 1)  /* Limita a profundeza da recursão  */
    return 1;
  else
    return fill_stack (n - 1);
}

Com um laptop Lemote Yeeloong rodando o sistema operacional gNewSense GNU/Linux com configuração padrão, um experimento demonstrou que há espaço suficiente na pilha para realizar 261906 chamadas aninhadas para essa função. Uma mais e a pilha estoura e o programa é encerrado. Em outra plataforma com uma configuração diferente ou com uma função diferente, o limite pode ser maior ou menor.

Em meus testes com o Windows 11 e Visual Studio 2022, a pilha estourou a partir de 4023 chamadas, ou seja, com fill_stack(4024).

Corpo da Função

O resto da definição da função é chamado de corpo da função. Como qualquer corpo de função, este inicia com um { , termina com um } e contém zero ou mais comandos e declarações. Comandos especificam ações que o programa deve tomar. Declarações definem nomes de variáveis, funções, etc. Cada comando e cada declaração termina com um ponto-e-vírgula (;).

Comandos e declarações frequentemente contém expressões; uma expressão é uma construção cuja execução produz um valor de algum tipo, mas também pode causar ações por meio de "efeitos colaterais" que alteram a execução subsequente. Diferentemente, um comando, não tem um valor; ele afeta a execução do programa somente através das ações que ele gera.

O corpo desta função fib não contém declarações e contém somente um comando, mas este é um comando complexo uma vez que ele contém comandos aninhados. Essa função utiliza dois tipos de comandos:

return

O comando return faz a função retornar imediatamente. Ele normalmente aparece assim:

return valor;

Seu objetivo é computar o valor da expressão e sair da função, fazendo-a retornar o valor da expressão produzida. Por exemplo:

return 1;

faz a função retornar o inteiro 1 e

return fib (n - 1) + fib (n - 2);

faz a função retornar um valor computado ao fazer a soma dos resultados e duas chamadas de função, como especificado.

if…else

O comando if…else é um condicional. Sempre que ele executa, ele escolhe um dos seus dois sub-comandos para executar e ignora o outro. Veja:

if (condicional)
  comando-se-verdadeiro
else
  comando-se-falso

O que ele faz é computar a expressão condicional e, se der "verdadeiro", ele executa o comando-se-verdadeiro. Do contrário, executa o comando-se-falso. Veja .

Dentro do comando if…else, condicional é simplesmente uma expressão. Ela é considerada "verdadeira" se seu valor for diferente de zero. (Uma operação de comparação como em n <= 2, produz o valor 1 se for "verdadeiro" e 0 se for "falso.” Veja .) Portanto,

if (n <= 2)
  return 1;
else
  return fib (n - 1) + fib (n - 2);

primeiro testa se o valor de n é menor ou igual a 2. Se sim, a expressão n <= 2 tem valor 1. Daí a execução continua com o comando

return 1;

Do contrário, a execução continua com o comando:

return fib (n - 1) + fib (n - 2);

Cada um desses comandos encerra a execução da função e provê um valor para que ela retorne. Veja Comando return.

Calcular fib(n), que utiliza inteiros, funciona apenas quando n < 47 porque o resultado de fib (47) é muito grande para caber num tipo int. A operação de adição ao tentar somar fib (46) e fib (45) não consegue produzir o resultado correto. Isto é chamado de estouro de inteiro (integer overflow).

Estouros podem se manifestar de várias maneiras, mas uma coisa que eles não fazem é produzir o resultado correto já que este não cabe no espaço reservado para o valor. Veja .

Veja para uma explicação completa sobre funções.

Prefácio

Este manual explica a linguagem C para uso com o GNU Compiler Collection (GCC) em sistema operacional GNU/Linux e em outros sistemas. Nós nos referimos a este dialeto como GNU C. Se você já sabe C, pode usar este manual como referência.

Se você sabe conceitos básicos de programação mas não sabe nada sobre C, você pode ler este manual sequencialmente desde o início para aprender a linguagem C.

Se você é iniciante em programação, recomendamos que você primeiro aprenda uma linguagem com coleta de lixo automática e sem ponteiros explícitos ao invés de começar com C. Boas opções incluem Lisp, Scheme, Python e Java. Por conta dos ponteiros explícitos da linguagem C, quem programa precisa ter cuidado para evitar certos tipos de erro ao utilizar a memória.

C é uma linguagem venerável; ela foi usada pela primeira vez em 1973. O GNU C Compiler, posteriormente ampliado a GNU Compiler Collection, foi lançado pela primeira vez em 1987. Outras importantes linguagens de programação foram desenhadas com base no C: uma vez que você saiba C, ele te dá uma base útil para aprender C++, C#, Java, Scala, D, Go e outras.

A vantagem especial de C é que ele ao mesmo tempo é simples e te dá um acesso próximo ao hardware do computador, o que anteriormente requeria escrever código em linguagem assembler para descrever instruções de máquina individuais. Algumas pessoas chamaram o C de "uma linguagem assembler de alto nível" por conta de seus ponteiros explícitos e a falta de gerenciamento automático de memória. Como disse alguém brincando, "C combina o poder da linguagem assembler com a conveniência da linguagem assembler". Todavia, C é muito mais portável e muito mais fácil de ler e de escrever que a linguagem assembler.

O livro chama de linguagem assembler o que normalmente no Brasil nos referimos como linguagem Assembly. Há pessoas que defendem que assembler é o compilador de Assembly, enquanto Assembly é a linguagem em si, mas no livro o autor chama a linguagem de assembler mesmo.

Este manual descreve a linguagem GNU C suportada pelo GNU Compiler Collection (GCC) desde 2017 aproximadamente. Por favor, nos informe se qualquer mudança for necessária para refletir a versão atual do GNU C.

Se uma determinada construção não estiver disponível ou funcionar de forma diferente em outros compiladores, nós avisaremos. Se uma construção não for parte do padrão ISO C, nós diremos que é uma "extensão GNU C", porque é útil que você saiba isso. Todavia, padrões e outros dialetos são tópicos secundários para este manual. Em razão da simplicidade, manteremos tais notas curtas, a menos que seja vital dizer mais sobre.

Alguns aspectos do significado de programas em C dependem da plataforma alvo: em qual computador e em qual o sistema operacional o código compilado vai rodar. Quando for este o caso, avisaremos também.

Raramente mencionamos C++ ou outras linguagens que o GCC suporta. Esperamos que este manual sirva de base para escrever manuais para estas linguagens, mas linguagens tão diferentes não podem compartilhar um manual comum.

A linguagem C não provê facilidades built-in (não traz facilidades nativas) para realizar operações como entrada e saída, gerenciamento de memória, manipulação de strings e similares. Ao invés disso, essas facilidades são providas pelas funções definidas na biblioteca padrão, que está automaticamente disponível em qualquer programa em C. Veja A Biblioteca C Padrão no Manual de Referência da Biblioteca GNU C.

A maioria dos sistemas GNU/Linux usa a biblioteca GNU C (glibc) para prover tais funcionalidades. Ela mesma é escrita em C, então uma vez que você saiba C, pode ler o código-fonte dela e ver como as funções da biblioteca fazem seu trabalho. Uma fração das funções é implementada como chamadas de sistema (system calls), o que significa que elas contêm uma instrução especial que pede ao kernel (do Linux) para realizar uma tarefa específica. Para entender como elas são implementadas, você precisaria ler o código-fonte do Linux. Se uma função da biblioteca é uma chamada de sistema ou não, é um detalhe interno de implementação e não faz diferença para quem vai chamar a função.

Este manual incorpora o antigo Manual do Pré-processador GNU C, que foi um dos primeiros manuais GNU. Ele também usa textos do antigo Manual do GNU C que foi escrito por Trevis Rothwell e James Youngman.

O GNU C tem vários recursos obscuros, cada um disponível seja por retrocompatibilidade ou para atender situações muito específicas. Nós os colocamos num manual complementar, o Manual de Obscuridades do GNU C, que será publicado digitalmente no futuro.

Por favor, reporte erros e sugestões (em inglês) para c-manual@gnu.org. Para erros de tradução, reporte aqui.

Fibonacci Recursiva

Para introduzir os conceitos mais básicos de C, vamos ver um código que implementa uma função matemática simples que faz cálculos com números inteiros. Esta função calcula o enésimo número na série de Fibonnaci, na qual cada número é a soma dos dois números anteriores: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ….

int
fib (int n)
{
  if (n <= 2)  /* Isto evita recursividade infinita. */
    return 1;
  else
    return fib (n - 1) + fib (n - 2);
}

Este simples programa ilustra vários recursos da linguagem C:

  • A definição de uma função cujas primeiras duas linhas constituem no cabeçalho da função. Veja Definição de Funções.

  • Um parâmetro de função n, referenciado com a variável n dentro do corpo da função. Veja Variáveis de Parâmetros de Função. Uma definição de função usa parâmetros para se referir aos valores dos argumentos passados para ela numa chamada de função.

  • Aritmética. Programas em C somam com + e subtraem com -. Veja .

  • Comparações numéricas. O operador <= testa se é "menor ou igual". Veja .

  • Constantes inteiras escritas na base 10. Veja Constantes Inteiras.

  • Uma chamada de função. A chamada de função fib (n - 1) chama a função fib passando o valor n - 1 como seu argumento. Veja Chamadas de Função.

  • Um comentário que começa com /* e termina com */. O comentário não tem nenhum efeito na execução do programa. Seu propósito é prover explicações para pessoas lendo o código-fonte. Incluir comentários no código é tremendamente importante - eles adicionam contexto de forma que outras pessoas possam entender o código mais rapidamente. Veja .

  • Dois tipos de comandos, o return e o if...else. Veja Comandos.

  • Recursão. A função fib chama a si mesma; isto é chamado de chamada recursiva. Isto é válido em C e bastante comum.

A função fib não seria útil se ela não retornasse. Portanto, funções recursivas, para serem úteis, precisam evitar recursão infinita.

Esta definição de função previne a recursão infinita ao tratar o caso especial onde n é dois ou menos. Portanto a máxima profundeza de chamadas recursivas é menor que n.

  • Cabeçalho da Função O nome da função e como ela é chamada.

  • Corpo da Função Declarações que implementam a função.

1. O Primeiro Exemplo

Este capítulo apresenta o código-fonte para um programa em C muito simples e o utiliza para explicar alguns recursos da linguagem. Se você já sabe os pontos básicos de C apresentados neste capítulo, você pode só dar uma passada por ele ou pulá-lo.

Apresentamos exemplos de código C usando uma fonte de largura fixa já que esta é a maneira que eles aparecem quando você os edita num editor de textos como o GNU Emacs.

Cabeçalho da Função

Em nosso exemplo, as primeiras duas linhas da definição da função consistem no cabeçalho. Seu propósito é estabelecer o nome da função e dizer como esta deve ser chamada:

int
fib (int n)

O cabeçalho acima diz que a função retorna um inteiro (tipo int), que seu nome é fib e que ela recebe um argumento chamado n que também é um inteiro. (Tipos de dados serão explicados mais tarde, em .)

Linguagem GNU C

Introdução e Manual de Referência por Richard Stallman, Trevis Rothwell e Nelson Beebe

Introdução à Edição em Português

Este livro é uma tradução comentada do GNU C Language Manual, publicado online gratuitamente pelo projeto GNU. Aqui na Mente Binária, pensamos que nossos conterrâneos no Brasil (e possivelmente outros falantes de língua portuguesa mundo afora) merecem o acesso a um material deste calibre também em seu idioma materno e por isso iniciamos este processo de tradução.

Traduzir nunca é fácil, por isso ressalto aqui algumas decisões que tomei e também algumas especificidades desta tradução:

  • Procurei ao máximo utilizar o gênero neutro.

  • Algumas traduções são simplesmente muito incomuns (pense em stack frame por exemplo). Portanto, em alguns casos optei por manter o termo em inglês.

  • As referências cruzadas não estão todas linkadas. Como os capítulos estão traduzidos um por um, você verá no texto referências a capítulos ainda não traduzidos. Trabalharei nelas ao adicionar novos capítulos.

  • Fiz vários comentários ao longo do texto. Você vai encontrá-los destacados em caixas como essa:

Isto é um comentário do tradutor.

  • Preferi manter os nomes de variáveis e funções em inglês porque normalmente os compiladores de C não suportam caracteres latinos para definir estes nomes.

  • Para o trabalho pesado de tradução, usamos o modelo GPT-4o. Depois vem o refinamento e adaptação manual.

Qualquer erro, crítica ou sugestão, é só entrar em contato conosco.

Espero que você aprenda tanto quanto eu aprendi ao ler este livro. Boa leitura!

Grande abraço,

Fernando Mercês Diretor Executivo - Mente Binária

4. Além dos Inteiros

Até agora, apresentamos programas que operam com inteiros. Neste capítulo, apresentaremos exemplos de manipulação de números não inteiros e arrays de números.

Explicação do Programa Completo

Aqui está a explicação do código do exemplo na seção anterior.

Este programa de exemplo imprime uma mensagem que mostra o valor de fib (20) e termina com o código 0 (que indica uma execução bem-sucedida).

Todo programa em C inicia com a execução da função chamada main. Portanto, o programa de exemplo define uma função chamada main para fornecer uma maneira de iniciá-lo. O que essa função faz é o que o programa faz. Veja

A função main é a primeira a ser chamada quando o programa é executado, mas ela não é a primeira a ser definida no código de exemplo. A ordem das definições de funções no código-fonte não faz diferença para o significado do programa.

A chamada inicial para main sempre passa certos argumentos, mas a main não precisa se preocupar com eles. Para ignorar esses argumentos, defina a main com void como na lista de parâmetros. (void como na lista de parâmetros de função normalmente significa "chamar sem argumentos", mas a main é um caso especial.)

A função main retorna 0 porque essa é a maneira convencional de ela indicar uma execução bem-sucedida. Ela poderia, ao invés disso, retornar um número inteiro positivo para indicar erro, e alguns programas utilitários têm convenções específicas para o significado de certos códigos numéricos de erro. Veja .

A maneira mais simples de imprimir texto em C é chamando a função printf, então aqui explicamos brevemente o que essa função faz. Para uma explicação completa de printf e das outras funções padrão de entrada e sída (E/S), veja a seção "I/O on Streams" no The GNU C Library Reference Manual (disponível somente em inglês).

O primeiro argumento da printf é uma constante string () que é um modelo para a saída. A função printf copia a maior parte dessa string diretamente para a saída, incluindo o caractere de nova linha no final da string, que é escrito como '\n'. A saída vai para a saída padrão do programa, que comumente é o terminal.

'%' no modelo introduz um código que substitui outro texto na saída. Especificamente, '%d' significa pegar o próximo argumento da printf e substituí-lo no texto como um número decimal. (O argumento para '%d' deve ser do tipo int; se não for, printf funcionará mal.) Então, a saída será uma linha que se parece com isso:

O item 20 da série Fibonacci é 6765

Este programa não contém uma definição para a printf porque ela é definida pela biblioteca C, o que a torna disponível em todos os programas em C. No entanto, cada programa precisaria declarar a printf para que ela seja chamada corretamente. A linha #include cuida disso; ela inclui um arquivo de cabeçalho chamado stdio.h no código do programa. Esse arquivo é fornecido pelo sistema operacional e contém declarações para as muitas funções padrão de entrada/saída da biblioteca C, dentre elas a printf.

Não se preocupe com arquivos de cabeçalho por agora; explicaremos eles mais tarde em .

O primeiro argumento da printf não precisa ser uma constante string; pode ser qualquer string (veja Strings). No entanto, usar uma constante é o caso mais comum.

2. Um Programa Completo

É muito bom escrever uma função Fibonacci, mas você não pode executá-la por si só. Ela é um código útil, mas não é um programa completo. Neste capítulo, apresentamos um programa completo que contém a função fib. Este exemplo mostra como fazer o programa começar, como fazê-lo terminar, como realizar cálculos e como imprimir um resultado na tela.

Exemplo de um Programa Completo

Aqui está um exemplo de um programa completo que usa uma versão simples, recursiva, da função fib (ver Fibonacci Recursiva):

#include <stdio.h>

int
fib (int n)
{
  if (n <= 2)  /* Isto evita recursividade infinita. */
    return 1;
  else
    return fib (n - 1) + fib (n - 2);
}

int
main (void)
{
  printf ("O item %d da série de Fibonacci é %d\n",
          20, fib (20));
  return 0;
}

Este programa imprime uma mensagem que exibe o valor de fib (20).

Agora vamos à uma explicação sobre este código.

Fibonacci Iterativa

Aqui vai um algoritmo muito mais rápido para computar a mesma sequência de Fibonacci. Ele é mais rápido por duas razões. Primeiro, ele usa iteração (isto é, repetição) ao invés de recursão, então ele não consome o tempo de um número alto de chamadas de função, mas principalmente, ele é mais rápido porque o número de repetições é pequeno - somente n.

int
fib (int n)
{
  int ultimo = 1;   /* O valor inicial é fib (1).  */
  int anterior = 0;   /* O valor inicial controla fib (2).  */
  int i;

  for (i = 1; i < n; ++i)
    /* Se n é 1 ou menos, a repetição executa zero vezes
    /* já que i < n é falso logo na primeira vez. */
    {
      /* Agora ultimo é fib (i)
         e anterior é fib (i - 1).  */
      /* Calcula fib (i + 1).  */
      int proximo = anterior + ultimo;
      /* Troca os valores.  */
      anterior = ultimo;
      ultimo = proximo;
      /* Agora ultimo é fib (i + 1)
         e anterior é fib (i).
         Mas isso não vai ficar assim por muito tempo
         porque estamos prestes a incrementar i.  */
    }

  return ultimo;
}

Essa definição calcula fib (n) num tempo proporcional à n. Os comentários na definição explicam como ela funciona: ela avança através da série sempre mantendo os últimos dois valores em ultimo e anterior, e os soma para obter o próximo valor.

Aqui vão os recursos adicionais da linguagem C que essa definição utiliza:

Blocos internos

Dentro de uma função, onde um comando é chamado, você pode escrever um bloco. Seu formato é { ... } e ele contém zero ou mais comandos e declarações. (Você também pode utilizar blocos adicionais como comandos dentro de um bloco.)

O corpo da função também contém um bloco, que é a razão deste conter comandos e declarações.

Veja .

Declarações de variáveis locais

O corpo da função contém tanto declarações quanto comandos. Há três declarações diretamente no corpo da função, assim como uma quarta declaração num bloco interno. Cada declaração começa com int porque isso declara uma variável cujo tipo é inteiro. Uma declaração pode declarar várias variáveis, mas cada uma dessas declara somente uma variável.

Variáveis declaradas dentro de um bloco (seja ele o que define o corpo de uma função ou um bloco interno) são variáveis locais. Estas variáveis existem somente dentro daquele bloco; seus nomes não estão definidos fora daquele bloco e sair do bloco desaloca a memória ocupada por elas. Este exemplo declara quatro variáveis locais: ultimo, anterior, i e proximo.

A declaração de variável local mais básica é assim:

tipo nomedavariavel;

Por exemplo,

int i;

declara a variável local i como um inteiro. Veja .

Inicializadores

Quando você declara uma variável, você também pode especificar seu valor inicial, assim:

tipo nomedavariavel = valor;

Por exemplo,

int ultimo = 1;

declara a variável local ultimo como inteiro (tipo int) e a inicializa com o valor 1. Veja Inicializadores.

Atribuição

Atribuição: um tipo de expressão específica, escrita com o operador =, que armazena um novo valor numa variável ou em outro lugar. Portanto,

variavel = valor

é uma expressão que computa valor e armazena o valor na variável. Veja .

Comandos de expressão

Um comando de expressão é uma expressão seguida de um ponto-e-vírgula. Isso computa o valor da expressão e na sequência ignora este valor.

Um comando de expressão é útil quando a expressão muda algum dado ou tem outros efeitos colaterais — por exemplo, com chamadas de função ou com atribuições como neste exemplo. Veja .

Não faz sentido usar uma expresão sem efeitos colaterais num comando de expressão, exceto em casos muito especiais. Por exemplo, o comando de expressão x; examinaria o valor de x e o ignoraria.

Operator de incremento

O operador de incremento é o ++. A expressão ++i é uma forma abreviada de i = i + 1. Veja .

Comando for

O comando for é uma maneira clara de executar um comando repetidamente—um laço (ver Comandos de Laço). Especificamente,

for (i = 1; i < n; ++i)
  corpo

começa fazendo i = 1 (põe o um em i ) para preparar o laço. O laço em si consiste em

  • Testar i < n e sair do laço se isso for falso.

  • Executar corpo.

  • Avançar o laço (executar ++i, que incrementa i).

O resultado é executar corpo com 1 em i, então com 2 em i e assim sucessivamente, parando imediatamente antes da repetição quando i for igual a n. Se n é menor que 1, o laço vai executar o corpo zero vezes.

O corpo do laço for precisa ser um e somente um comando. Você não pode escrever dois comandos numa mesma linha nele; se você tentar, somente o primeiro deles será tratado como parte do laço.

A forma de colocar múltiplos comandos no laço for é agrupá-los como um bloco e é isso que fazemos neste exemplo.

Compilando o Programa de Exemplo

Rodar um programa em C requer converter seu código-fonte num arquivo executável. Isto é chamado de compilar o programa, e o comando para fazer isso usando GNU C é o gcc.

O programa de exemplo consiste num único arquivo de código-fonte. Se o chamarmos de fib1.c, o comando completo para compilá-lo será:

gcc -g -O -o fib1 fib1.c

No comando acima, -g instrui que sejam geradas informações de depuração, -O diz para otimizar em nível básico e -o fib1 diz para criar o programa executável num arquivo fib1.

Para rodar o programa, use o nome do arquivo como um comando do shell. Por exemplo,

./fib1

Todavia, a menos que você tenha certeza de que o programa está correto, você deve esperar que precise depurá-lo. Portanto, use o comando

gdb fib1

que inicia o debugger (ou depurador) GDB (veja A Sample GDB Session em Debugging with GDB) e você pode rodar e depurar o programa executável fib1.

Um conselho do Richard Stallman, a partir da sua experiência pessoal, é ir para o debugger assim que você conseguir reproduzir o problema. Não tente evitar isso usando outros métodos—ocasionalmente eles funcionam como atalhos, mas normalmente eles levam muito tempo. Com o debugger você certamente vai encontrar o problema num tempo razoável; em geral, você terminará seu trabalho mais rapidamente. Quanto mais cedo você levar a sério e iniciar o debugger, mais cedo provavelmente encontrará o problema.

Veja para uma introdução sobre compilação de programais mais complexos que consistem em mais de um arquivo de código-fonte.

Escreva Programas em Inglês!

Em princípio, você pode escrever os nomes de funções e variáveis em um programa, assim como os comentários, em qualquer idioma. C permite qualquer tipo de caractere Unicode em comentários, e você pode colocá-los em identificadores com um prefixo especial (veja ). No entanto, para permitir que programadores de todos os países compreendam e desenvolvam o programa, é melhor, nas circunstâncias atuais, escrever todos os identificadores e comentários em inglês.

O inglês é a língua comum dos programadores; em todos os países, os programadores geralmente aprendem inglês. Se os nomes e comentários em um programa forem escritos em inglês, a maioria dos programadores em Bangladesh, Bélgica, Bolívia, Brasil, Bulgária e Burundi poderá entendê-los. Em todos esses países, a maioria dos programadores fala inglês, ou pelo menos o lê, mas eles não leem os idiomas uns dos outros. Na Índia, com tantos idiomas, dois programadores podem não ter um idioma comum além do inglês.

Se você não se sente confiante em escrever em inglês, faça o melhor que puder e siga cada comentário em inglês com uma versão em um idioma que você escreve melhor; adicione uma nota no comentário pedindo a outros para traduzirem isso para o inglês. Alguém eventualmente fará isso.

A interface do usuário do programa é uma questão diferente. Não precisamos escolher um idioma para isso; é fácil suportar múltiplos idiomas e deixar cada usuário escolher o idioma para exibição do texto. Isso requer escrever o programa para suportar a localização de sua interface. (O pacote gettext existe para dar suporte a isso; veja The GNU C Library no The GNU C Library Reference Manual.) Dessa forma, um esforço de tradução baseado em comunidade pode fornecer suporte para todos os idiomas que os usuários desejam usar.

5. Sintaxe Lexical

Para começar a descrição completa da linguagem C, explicamos a sintaxe lexical e as unidades lexicais de código em C. As unidades lexicais de uma linguagem de programação são conhecidas como tokens. Este capítulo cobre todos os tokens de C, exceto constantes, que serão abordadas em um capítulo posterior (veja ). Um tipo essencial de token é o identificador (veja ), que é usado para nomes de qualquer tipo.

3. Armazenamento e Dados

O armazenamento em programas C é composto por unidades chamadas bytes. Um byte é a menor unidade de armazenamento que pode ser usada primariamente.

Em quase todos os computadores, um byte é composto por 8 bits. Existem alguns computadores peculiares (principalmente “controladores embarcados” para sistemas muito pequenos) onde um byte é maior do que isso, mas este manual não tenta explicar a peculiaridade desses computadores; assumimos que um byte tem 8 bits.

Cada tipo de dado em C é composto por um certo número de bytes; esse número é o tamanho do tipo de dado. Veja , para mais detalhes. Os tipos signed char e unsigned char têm um byte de comprimento; use esses tipos para operar com dados byte a byte. Veja . Você pode se referir a uma série de bytes consecutivos como um array de elementos char; é assim que uma string de caracteres se parece na memória. Veja .

Variações para o Exemplo com Array

O código para chamar a função avg_of_double tem duas declarações que começam com o mesmo tipo de dado:

Em C, você pode combinar as duas, assim:

Isso declara nums_to_average para que cada um de seus elementos seja um double, e average para que seja simplesmente um double.

No entanto, embora você possa combiná-las, isso não significa que deva. Se for útil escrever comentários sobre as variáveis, e geralmente é, então é melhor manter as declarações em linhas separadas para que você possa colocar um comentário em cada uma. Isso também ajuda ao usar editores de texto para encontrar ocorrências de uma variável nos arquivos de código-fonte.

Em arquivos grandes de código-fonte, pode ser que você queira encontrar o momento da declaração de uma variável utilizando um editor de texto como vim, Visual Studio Code, Notepad++, etc. Por exemplo, você pode buscar por int num para buscar a declaração de uma variável chamada num, que é tipo int. No entanto, se você declarar mais de uma variável na mesma linha como em int i, num, j;, a busca por int num não encontraria esta ocorrência. Além da clareza, este é o motivo pelo qual os autores desencorajam este tipo de declaração.

Definimos então todos os elementos do array nums_to_average com atribuições, mas é mais conveniente usar um inicializador na declaração:

O inicializador do array é uma lista de valores separados por vírgulas, delimitados por chaves. Veja .

Note que a declaração não especifica um tamanho para nums_to_average, então o tamanho é determinado a partir do inicializador. Há cinco valores no inicializador, então nums_to_average recebe o comprimento 5. Se adicionarmos outro elemento ao inicializador, nums_to_average terá seis elementos.

Como o código calcula o número de elementos a partir do tamanho do array, usando sizeof, o programa operará em todos os elementos no inicializador, independentemente de quantos sejam.

No código de exemplo, uma certa aritimética é utilizada para pegar o tamanho do array, que é 5, já que este é inicializado com 5 elementos. A técnica consiste em utilizar o operador sizeof, que retorna o tamanho de um tipo em tempo de compilação, diretamente no array e dividir esse resultado pelo tamanho de qualquer um dos elementos do array, já que todos são do mesmo tipo (normalmente o primeiro elemento, de índice 0, é o escolhido).

Para entender como isto functiona, suponha que uma variável do tipo double ocupe 8 bytes de memória. Isso significa que um array de double com 5 elementos vai ocupar 40 bytes. Ao dividir 40 pelo tamanho de um dos elementos, que é 8, você obtém o número de elementos do array, que é 5 neste caso.

Fazer isso faz com que você possa adicionar ou remover elementos na inicialização do array sem se preocupar em atualizar o valor do tamanho do array, já que as operações com o sizeof irão calcular o tamanho do array novamente.

Só lembre-se que isto é em tempo de compilação, ou seja, é preciso recompilar o programa para alterar os elementos com os quais o array é inicializado. Só assim sizeof retornará o valor atualizado do tamanho do array.

Espaços em Branco

O espaço em branco refere-se a caracteres que existem em um arquivo, mas aparecem em branco em uma impressão deste arquivo (ou tradicionalmente apareciam em branco, várias décadas atrás). A linguagem C exige espaço em branco para separar dois identificadores consecutivos ou para separar um identificador de uma constante numérica. Além disso, em algumas situações especiais descritas mais adiante, o espaço em branco é opcional; você pode usá-lo quando quiser, para tornar o código mais fácil de ler.

Espaço e tabulação (caractere "tab") no código C são tratados como caracteres de espaço em branco. Quebras de linha também são. Você pode representar uma quebra de linha com o caractere de nova linha (também chamado de linefeed ou LF), CR (carriage return), ou a sequência CRLF (dois caracteres: carriage return seguido de um caractere de nova linha).

O caractere de avanço de formulário (formfeed), Control-L, era tradicionalmente usado para dividir um arquivo em páginas. Ele ainda é usado dessa forma no código fonte, e as ferramentas que geram impressões agradáveis do código fonte ainda iniciam uma nova página após cada caractere de "formfeed". Dividir o código em páginas separadas por caracteres de avanço de formulário é uma boa maneira de dividi-lo em partes compreensíveis e mostrar a outros programadores onde começam e terminam.

O caractere de tabulação vertical, Control-K, era tradicionalmente usado para fazer a impressão avançar para a próxima seção de uma página. Não conhecemos nenhum motivo particular para usá-lo no código fonte, mas ele ainda é aceito como espaço em branco em C.

Comentários também são sintaticamente equivalentes ao espaço em branco.

Um Exemplo com Números Não Inteiros

Aqui está uma função que opera e retorna números de ponto flutuante que não precisam ser, necessariamente, inteiros. Ponto flutuante é uma maneira de representar um número como uma fração juntamente com uma potência de 2. (Para mais detalhes, veja .) Este exemplo calcula a média de três números de ponto flutuante que são passados para a função como argumentos:

double
average_of_three (double a, double b, double c)
{
  return (a + b + c) / 3;
}

Os valores dos parâmetros a, b e c não precisam ser inteiros e, mesmo quando são inteiros, provavelmente a média deles não o é.

double é o tipo de dado usual em C para cálculos com números de ponto flutuante.

Para imprimir um double com a função printf, devemos usar '%f' em vez de '%d':

printf ("A média é %f\n",
        average_of_three (1.1, 9.8, 3.62));

O código que chama a printf deve passar um double para impressão com '%f' e um int para impressão com '%d'. Se o argumento tiver o tipo errado, a printf produzirá uma saída sem sentido.

Aqui está um programa completo que calcula a média de três números específicos e imprime o resultado:

double
average_of_three (double a, double b, double c)
{
  return (a + b + c) / 3;
}

int
main (void)
{
    printf ("A média é %f\n",
            average_of_three (1.1, 9.8, 3.62));
    return 0;
}

A partir de agora, não mostraremos mais exemplos de chamadas às funções que escrevemos na função main. Em vez disso, encorajamos você a escrevê-las por conta própria quando quiser testar a execução de algum código.

Aritmética Básica

A aritmética básica em C é feita com os operadores binários usuais da álgebra: adição (‘+’), subtração (‘-’), multiplicação (‘*’) e divisão (‘/’). O operador unário ‘-’ é usado para mudar o sinal de um número. O operador unário + também existe; ele retorna seu operando inalterado.

‘/’ é o operador de divisão, mas dividir inteiros pode não dar o resultado que você espera. Seu valor é um inteiro, que não é igual ao quociente matemático quando este é uma fração. Use ‘%’ para obter o resto inteiro correspondente, quando necessário. Veja . A divisão de ponto flutuante produz um valor o mais próximo possível do quociente matemático.

Esses operadores usam a sintaxe algébrica com a regra de precedência algébrica usual (veja ) de que multiplicação e divisão são feitas antes de adição e subtração, mas você pode usar parênteses para especificar explicitamente como os operadores devem se aninhar. Eles são associativos à esquerda (veja). Assim,

-a + b - c + d * e / f

é equivalente a

(((-a) + b) - c) + ((d * e) / f)

Identificadores

Um identificador (nome) em C é uma sequência de letras, dígitos ou '_' (caractere de sublinha ou underscore), que não começa com um dígito. A maioria dos compiladores também permite '$'. Um identificador pode ter o comprimento que você desejar; por exemplo,

int anti_dis_establishment_arian_ism;

Letras em identificadores são sensíveis a maiúsculas e minúsculas em C; assim, a e A são dois identificadores diferentes.

Identificadores em C são usados como nomes de variáveis, nomes de funções, nomes de typedef, constantes de enumeração, tags de tipo, nomes de campo e rótulos. Certos identificadores em C são palavras-chave, o que significa que têm significados sintáticos específicos. Palavras-chave em C são palavras reservadas, o que significa que você não pode usá-las de outra forma. Por exemplo, você não pode definir uma variável ou função chamada return ou if.

Você também pode incluir outros caracteres, até mesmo caracteres não ASCII, em identificadores escrevendo seus nomes de caracteres Unicode, que começam com '\u' ou '\U', no nome do identificador. Veja . No entanto, geralmente é uma má ideia usar caracteres não ASCII em identificadores, e quando os nomes são escritos em inglês, nunca precisam de caracteres não ASCII. Veja Escreva Programas em Inglês!.

Como mencionado acima, é necessário (pelo menos um) espaço em branco para separar dois identificadores consecutivos ou para separar um identificador de uma constante numérica anterior ou seguinte.

Comentários

Um comentário encapsula texto que não tem efeito na execução ou no significado do programa.

O propósito dos comentários é explicar o código para as pessoas que o leem. Escrever bons comentários para seu código é extremamente importante – eles devem fornecer informações sobre o contexto, que ajudem os programadores a entender as razões pelas quais o código foi escrito da maneira que está. Você, retornando ao código seis meses depois, precisará da ajuda desses comentários para lembrar por que o escreveu dessa forma.

Comentários desatualizados que se tornam incorretos são contraproducentes, então parte da responsabilidade do desenvolvedor de software é atualizar os comentários conforme necessário para corresponder às mudanças no código do programa.

C permite dois tipos de sintaxe de comentário, o estilo tradicional e o estilo C++. Um comentário tradicional em C começa com '/*' e termina com '*/'. Por exemplo,

/* Este é um comentário na sintaxe C tradicional. */

Um comentário tradicional pode conter '/*', mas esses delimitadores não se aninham como pares. O primeiro '/*' termina o comentário independentemente de ele conter sequências '/*' ou não.

/* Isto /* é um comentário */ Mas isso não! */

Um comentário de linha começa com '//' e termina no final da linha. Por exemplo,

// Isto é um comentário no estilo C++

Comentários de linha se aninham, na prática, porque o '//' dentro de um comentário de linha é parte desse comentário:

// a linha inteira é // um comentário
Isto é código, não comentário.

É seguro colocar comentários de linha dentro de comentários de bloco, ou vice-versa.

/* comentário tradicional
   // contém comentário de linha
   mais um comentário tradicional
 */ o texto aqui não é um comentário

// comentário de linha /* que contém um comentário tradicional */

Mas tenha cuidado ao comentar uma extremidade de um comentário tradicional com um comentário de linha. O delimitador '/*' não inicia um comentário se ocorrer dentro de um comentário já iniciado.

 // comentário de linha  /* e aqui normalmente começaria um comentário de bloco
    Oops! O comentário de linha acabou;
    isto não é mais um comentário.  */

Comentários não são reconhecidos dentro de strings constantes. "/* blah */" é a string constante ‘/* blah */’, não uma string vazia.

Um comentário é sintaticamente equivalente ao espaço em branco, então ele sempre separa tokens. Assim,

  int/* comment */foo;

é equivalente a

  int foo;

mas um código limpo sempre usa espaço em branco real para separar o comentário visualmente do código ao redor.

Um Exemplo com Arrays

Uma função para calcular a média de três números é muito específica e limitada. Uma função mais geral calcularia a média de qualquer quantidade de números. Isso requer passar os números em um array. Um array é um objeto na memória que contém uma série de valores do mesmo tipo de dado. Este capítulo apresenta os conceitos básicos e o uso de arrays através de um exemplo; para a explicação completa, veja .

Aqui está a definição de uma função para calcular a média de vários números de ponto flutuante, passados como tipo double. O primeiro parâmetro, length, especifica quantos números são passados. O segundo parâmetro, input_data, é um array que contém esses números.

double
avg_of_double (int length, double input_data[])
{
  double sum = 0;
  int i;

  for (i = 0; i < length; i++)
    sum = sum + input_data[i];

  return sum / length;
}

O código anterior introduz a expressão para se referir a um elemento de um array: input_data[i] significa o elemento de índice i no array input_data. O índice do elemento pode ser qualquer expressão com um valor inteiro; neste caso, a expressão é i. Veja .

O menor índice válido em um array é 0, não 1, e o maior índice válido é um a menos que o número de elementos. (Isso é conhecido como indexação com origem zero.)

Este exemplo também introduz a maneira de declarar que um parâmetro de função é um array. Tal declaração é feita após o nome do parâmetro. Assim como double foo declara que foo é do tipo double, double input_data[] declara que cada elemento de input_data é do tipo double. Portanto, input_data em si tem o tipo "array de double".

É muito comum em textos técnicos em inglês você encontrar as palavras foo, bar ou foobar. Elas são utilizadas para passar uma ideia de "qualquer coisa", ou "qualquer nome", ou seja, a ideia de que o nome não importa. Em português, seria equivalente ao modo como nos referenciamos a pessoas desconhecidas com "fulano" ou "cicrano".

Ao declarar um parâmetro que é um array, não é necessário dizer qual é o comprimento do array. Neste caso, o parâmetro input_data não tem informação de comprimento. É por isso que a função precisa de outro parâmetro, length, para que o chamador forneça essa informação para a função avg_of_double.

O nome avg_of_double é uma forma abrevidada de average of double. Em inglês, average é média. Já length, traduz para tamanho (no caso do código de exemplo, tamanho do array -em outras palavras, o número de elementos do array). Temos também input_data (dados de entrada) e sum (soma).

6. Aritmética

Os operadores aritméticos em C tentam ser o mais semelhante possível às operações aritméticas abstratas, mas é impossível fazer isso de forma perfeita. Números em um computador têm um intervalo finito de valores possíveis, e valores não inteiros têm um limite em sua precisão possível. No entanto, exceto quando os resultados estão fora do intervalo, você não terá surpresas ao usar ‘+’ para adição, ‘-’ para subtração e ‘*’ para multiplicação.

Cada operador em C tem uma precedência, que é sua posição na ordem gramatical dos vários operadores. Os operadores com a maior precedência capturam os operandos adjacentes primeiro; essas expressões então se tornam operandos para operadores de menor precedência. Damos algumas informações sobre a precedência dos operadores neste capítulo onde descrevemos os operadores; para a explicação completa, veja .

Os operadores aritméticos sempre promovem seus operandos antes de operar sobre eles. Isso significa converter tipos de dados inteiros estreitos para um tipo de dado mais amplo (veja ). Se você está apenas começando a aprender C, não se preocupe com isso ainda.

Dado dois operandos que têm tipos diferentes, a maioria das operações aritméticas os converte para um tipo comum. Por exemplo, se um dado é int e o outro é double, o tipo comum é double. (Isso ocorre porque double pode representar todos os valores que um int pode conter, mas o contrário não é verdadeiro.) Para mais detalhes, veja .

Operadores e Pontuação

Aqui descrevemos a sintaxe lexical de operadores e de pontuação em C. Os operadores específicos de C e seus significados são apresentados em capítulos subsequentes.

A maioria dos operadores em C consiste em um ou dois caracteres que não podem ser usados em identificadores. Os caracteres usados para operadores em C são ‘!~^&|*/%+-=<>,.?:’.

Alguns operadores são de um único caractere. Por exemplo, ‘-’ é o operador de negação (com um operando) e o operador de subtração (com dois operandos).

Alguns operadores são de dois caracteres. Por exemplo, ‘++’ é o operador de incremento. O reconhecimento de operadores de múltiplos caracteres funciona agrupando tantos caracteres consecutivos quanto possível para constituir um operador.

Por exemplo, a sequência de caracteres ‘++’ é sempre interpretada como o operador de incremento; portanto, se quisermos escrever duas instâncias consecutivas do operador ‘+’, devemos separá-las com um espaço para que não se combinem como um único token. Aplicando a mesma regra, a+++++b é sempre tokenizado como a++ ++ + b, e não como a++ + ++b, mesmo que a última forma pudesse fazer parte de um programa C válido, e a primeira não (já que a++ não é um lvalue e, portanto, não pode ser o operando de ++).

Alguns operadores de C são palavras-chave em vez de caracteres especiais. Eles incluem sizeof (veja ) e _Alignof (veja ).

Os caracteres ‘;{}’ são usados para pontuação e agrupamento. O ponto e vírgula (‘;’) termina uma declaração. As chaves (‘{’ e ‘}’) iniciam e terminam um bloco no nível da declaração (veja ), e cercam o inicializador (veja ) para uma variável com múltiplos elementos ou campos (como arrays ou structs).

Colchetes (‘[’ e ‘]’) são usados para indexação de arrays, como em array[5].

Parênteses são usados em expressões para o aninhamento explícito de expressões (veja ), ao redor das declarações de parâmetros em uma declaração ou definição de função, e ao redor dos argumentos em uma chamada de função, como em printf("Foo %d\n", i) (veja ). Vários tipos de declarações também usam parênteses como parte de sua sintaxe — por exemplo, declarações if, declarações for, declarações while e declarações switch. Veja e seções seguintes.

Parênteses também são necessários ao redor do operando das palavras-chave dos operadores sizeof e _Alignof quando o operando é um tipo de dado em vez de um valor. Veja .

  /* O array de valores para calcular a média.  */
  double nums_to_average[5];
  /* A média, uma vez que a calculamos.  */
  double average;
  double nums_to_average[5], average;
{
  /* O array de valores para calcular a média.  */
  double nums_to_average[]
    = { 58.7, 5.1, 7.7, 105.2, -3.14159 };

  /* A média, uma vez que a calculamos.  */
  double average = avg_of_double ((sizeof (nums_to_average)
                                   / sizeof (nums_to_average[0])),
                                  nums_to_average);

  /* …agora faça uso da média… */
}

Continuação de Linha

A sequência formada por uma barra invertida e um caractere de nova linha é completamente ignorada em qualquer lugar em um programa C. Isso torna possível dividir uma única linha de código-fonte em várias linhas no arquivo fonte. O GNU C tolera e ignora outros espaços em branco entre a barra invertida e a nova linha. Em particular, ele sempre ignora um caractere CR (carriage return) ali, caso algum editor de texto decida terminar a linha com a sequência CRLF.

O principal uso da continuação de linha em C é para definições de macro que seriam inconvenientemente longas para uma única linha (veja ).

É possível continuar um comentário de linha em outra linha com a sequência barra invertida-nova linha. Você pode colocar tal sequência no meio de um identificador, até mesmo numa palavra-chave, ou num operador. Você pode até dividir ‘/*’, ‘*/’, e ‘//’ em várias linhas com a sequência barra invertida-nova linha. Aqui está um exemplo feio:

/\
*
*/ fo\
o +\
= 1\
0;

Isso é equivalente a ‘/* */ foo += 10;’.

Não faça essas coisas em programas reais, pois elas tornam o código difícil de ler.

Nota: Por questão de uso de certas ferramentas no código-fonte, é sensato terminar cada arquivo fonte com um caractere de nova linha que não seja precedido por uma barra invertida, para que ele realmente termine a última linha.

Estouro de Inteiros com Sinal

Para inteiros com sinal, o resultado de um estouro em C é, em princípio, indefinido, o que significa que qualquer coisa pode acontecer. Portanto, compiladores C podem fazer otimizações que tratam o caso de estouro sem qualquer preocupação. (Como o resultado do estouro é indefinido em princípio, não se pode alegar que essas otimizações são errôneas.)

Cuidado: Essas otimizações podem fazer coisas surpreendentes. Por exemplo,

int i;
…
if (i < i + 1)
  x = 5;

poderia ser otimizado para fazer a atribuição incondicionalmente, porque a condição if é sempre verdadeira se i + 1 não causar estouro.

O GCC oferece opções do compilador para controlar o tratamento de estouro de inteiros com sinal. Essas opções operam por módulo; ou seja, cada módulo se comporta de acordo com as opções com as quais foi compilado.

Essas duas opções especificam maneiras particulares de lidar com estouro de inteiros com sinal, além da maneira padrão:

-fwrapv Faz com que operações de inteiros com sinal sejam bem definidas, como operações de inteiros sem sinal: elas produzem os n bits menos significativos do resultado real. O mais alto desses n bits é o bit de sinal do resultado. Com -fwrapv, essas operações fora do intervalo não são consideradas estouro, então (estritamente falando) o estouro de inteiros nunca acontece.

A opção -fwrapv habilita algumas otimizações com base nos valores definidos de resultados fora do intervalo. No GCC 8, ela desabilita otimizações que se baseiam em assumir que operações de inteiros com sinal não causarão estouro.

-ftrapv Gera um sinal SIGFPE quando ocorre estouro de inteiros com sinal. Isso termina o programa, a menos que o programa lide com o sinal. Veja .

Outra opção útil para encontrar onde ocorre estouro é:

-fsanitize=signed-integer-overflow Exibe uma mensagem de aviso em tempo de execução quando ocorre estouro de inteiros com sinal. Isso verifica os operadores ‘+’, ‘*’ e ‘-’. Isso tem prioridade sobre -ftrapv.

Aritmética em Modo Misto

Misturar inteiros e números de ponto flutuante em uma operação aritmética básica converte automaticamente os inteiros para ponto flutuante. Na maioria dos casos, isso dá exatamente os resultados desejados. Mas, às vezes, importa exatamente onde a conversão ocorre.

Se i e j são inteiros, (i + j) * 2.0 os adiciona como inteiros e, em seguida, converte a soma para ponto flutuante para a multiplicação. Se a adição causar um estouro, isso não é equivalente a converter cada inteiro para ponto flutuante e, em seguida, somar os dois números de ponto flutuante. Você pode obter este último resultado convertendo explicitamente os inteiros, como em ((double) i + (double) j) * 2.0. Veja .

Somar ou multiplicar vários valores, incluindo alguns inteiros e alguns de ponto flutuante, realiza as operações da esquerda para a direita. Assim, 3.0 + i + j converte i para ponto flutuante, depois adiciona 3.0, depois converte j para ponto flutuante e adiciona isso. Você pode especificar uma ordem diferente usando parênteses: 3.0 + (i + j) soma i e j primeiro e depois adiciona essa soma (convertida para ponto flutuante) a 3.0. Nesse aspecto, C difere de outras linguagens, como Fortran.

Aritmética de Inteiros

Cada uma das operações aritméticas básicas em C tem duas variantes para inteiros: com e sem sinal. A escolha é determinada pelos tipos de dados de seus operandos.

Cada tipo de dado inteiro em C é ou signed (com sinal) ou unsigned (sem sinal). Um tipo com sinal pode conter um intervalo de números positivos e negativos, com o zero próximo ao meio do intervalo. Um tipo sem sinal pode conter apenas números não negativos; seu intervalo começa em zero e vai para cima.

Os tipos inteiros mais básicos são int, que normalmente pode conter números de -2.147.483.648 a 2.147.483.647, e o unsigned int, que normalmente pode conter números de 0 a 4.294.967.295. (Isso assume que int tem 32 bits de largura, o que é sempre verdadeiro para GNU C em computadores reais, mas nem sempre em controladores embarcados.) Veja , para informações completas sobre tipos inteiros.

Quando uma operação aritmética básica recebe dois operandos com sinal, ela realiza aritmética com sinal. Quando recebe dois operandos sem sinal, realiza aritmética sem sinal.

Se um operando for unsigned int e o outro int, o operador trata ambos como sem sinal. De maneira mais geral, o tipo comum dos operandos determina se a operação é sinalizada ou não. .

Imprimir os resultados de uma aritmética sem sinal com printf usando ‘%d’ pode produzir resultados surpreendentes para valores distantes de zero. Mesmo que as regras acima digam que a computação foi feita com aritmética sem sinal, o resultado impresso na tela pode parecer com sinal!

A explicação é que o padrão de bits resultante de uma adição, subtração ou multiplicação é, na verdade, o mesmo para operações com e sem sinal. A diferença está apenas no tipo de dado do resultado, que afeta a interpretação do padrão de bits resultante e se a operação aritmética pode ou não causar um estouro (veja a próxima seção).

Mas ‘%d’ não sabe o tipo de dado de seu argumento. Ele vê apenas o padrão de bits do valor e está definido para interpretá-lo como signed int. Para imprimi-lo como sem sinal, é necessário usar ‘%u’ em vez de ‘%d’. Veja The GNU C Library no The GNU C Library Reference Manual (disponível somente em inglês).

A aritmética em C nunca opera diretamente em tipos inteiros estreitos (aqueles com menos bits que int; veja ). Em vez disso, ela os "promove" para int. Veja .

Caracteres

Arquivos de código-fonte em GNU C são geralmente escritos utilizando-se o conjunto de caracteres ASCII, que foi definido na década de 1960 para o inglês. No entanto, eles também podem incluir caracteres Unicode representados na codificação de múltiplos bytes UTF-8. Isso possibilita a representação de letras acentuadas como ‘á’, assim como outros scripts, tais como árabe, chinês, cirílico, hebraico, japonês e coreano.

No código fonte C, caracteres não ASCII são válidos em comentários, em constantes de caracteres largos (veja ) e em strings constantes (veja ).

Outra maneira de especificar caracteres não ASCII em constantes (caracteres ou strings) e identificadores é com uma sequência de escape começando com barra invertida, especificando o caractere Unicode pretendido. (Veja .) Isso especifica caracteres não ASCII sem colocar um caractere não ASCII real no próprio arquivo de código-fonte.

C também aceita pares de caracteres chamados dígrafos para certos caracteres. Veja .

Em alguns sistemas obscuros, GNU C utiliza UTF-EBCDIC ao invés de UTF-8, mas não vale a pena explicar isso neste manual.

Estouro de Inteiros

Quando o valor matemático de uma operação aritmética não cabe no intervalo do tipo de dado em uso, isso é chamado de estouro (overflow, em inglês). Quando isso ocorre na aritmética de inteiros, é chamado de estouro de inteiros.

O estouro de inteiros acontece apenas em operações aritméticas. Operações de conversão de tipo, por definição, não causam estouro, mesmo quando o resultado não cabe em seu novo tipo. .

Números com sinal usam a representação em complemento de dois, na qual o número mais negativo não tem um equivalente positivo (veja ). Assim, o operador unário ‘-’ em um inteiro com sinal pode causar estouro.

Estouro de Inteiros sem Sinal

A aritmética sem sinal em C ignora o estouro; ela produz o resultado real módulo a enésima potência de 2, onde n é o número de bits no tipo de dado. Dizemos que ela “trunca” o resultado verdadeiro para os n bits mais baixos.

No parágrafo acima, a palavra módulo significa o operador matemático de cálculo do resto, que em C é representado pelo caractere %. A expressão neste caso poderia ser representada por resultado_real % pow(2, 32), para um inteiro de 32 bits.

Um resultado real que é negativo, quando tomado módulo a enésima potência de 2, gera um número positivo. Por exemplo,

unsigned int x = 1;
unsigned int y;

y = -x;

causa estouro porque o número negativo -1 não pode ser armazenado em um tipo sem sinal. O resultado real, que é -1 módulo a enésima potência de 2, é um a menos que a enésima potência de 2. Esse é o maior valor que o tipo de dado sem sinal pode armazenar. Para um unsigned int de 32 bits, o valor é 4.294.967.295. Veja .

Adicionar esse número a si mesmo, como aqui,

unsigned int z;

z = y + y;

deveria gerar 8.489.934.590; no entanto, isso novamente é grande demais para caber, então o estouro trunca o valor para 4.294.967.294. Se fosse um inteiro com sinal, isso significaria -2, o que (não por coincidência) é igual a -1 + -1.

Operações de Deslocamento

Deslocar um inteiro significa mover os valores dos bits para a esquerda ou para a direita dentro dos bits do tipo de dado. O deslocamento é definido apenas para inteiros. Aqui está a forma de escrever:

O operando à esquerda é o valor a ser deslocado, e o operando à direita indica quantos bits deslocá-lo (a contagem de deslocamento). O operando à esquerda é promovido (veja ), então o deslocamento nunca opera em um tipo de inteiro estreito; é sempre int ou mais amplo. O resultado da operação de deslocamento tem o mesmo tipo que o operando esquerdo promovido.

Comparações Numéricas

Existem dois tipos de operadores de comparação: os de igualdade e os de ordenação. Comparações de igualdade testam se duas expressões têm o mesmo valor. O resultado é um valor verdade: um número que é 1 para "verdadeiro" ou 0 para "falso".

A comparação de igualdade é escrita como == porque = simples é o operador de atribuição.

Comparações de ordenação testam qual operando é maior ou menor. Seus resultados são valores verdade. Estas são as comparações de ordenação em C:

Para quaisquer inteiros a e b, exatamente uma das comparações a < b, a == b e a > b é verdadeira, assim como na matemática. No entanto, se a e b são valores especiais de ponto flutuante (não números ordinários), todas as três podem ser falsas. Veja e .

/* Deslocamento à esquerda.  */
5 << 2 ⇒ 20

/* Deslocamento à direita.  */
5 >> 2 ⇒ 1
a == b   /* Testa se é igual.  */
a != b   /* Testa se não é igual.  */
a < b   /* Testa se é menor-que.  */
a > b   /* Testa se é maior-que.  */
a <= b  /* Testa se é menor-ou-igual.  */
a >= b  /* Testa se é maior-ou-igual.  */

Hacks com Deslocamento

Você pode usar os operadores de deslocamento para diversos hacks (soluções engenhosas) úteis. Por exemplo, dada uma data especificada pelo dia do mês d, mês m e ano y, você pode armazenar toda a data em um único inteiro date:

unsigned int d = 12;        /* 12 em binário é 0b1100.  */
unsigned int m = 6;         /* 6 em binário é 0b110.  */
unsigned int y = 1983;      /* 1983 em binário é 0b11110111111.  */
unsigned int date = (((y << 4) + m) << 5) + d;
/* Adiciona 0b11110111111000000000
   e 0b11000000 e 0b1100.
   A soma é 0b11110111111011001100.  */

Para extrair o dia, mês e ano de date, use uma combinação de deslocamento e resto:

/* 32 em binário é 0b100000.  */
/* O resto da divisão por 32 dá os 5 bits menos significativos, 0b1100.  */
d = date % 32;
/* Deslocar 5 bits para a direita descarta o dia, restando 0b111101111110110.
   O resto da divisão por 16 dá os 4 bits menos significativos restantes, 0b110.  */
m = (date >> 5) % 16;
/* Deslocar 9 bits para a direita descarta dia e mês,
   restando 0b111101111110.  */
y = date >> 9;

-1 << LOWBITS é uma maneira inteligente de criar um inteiro cujos LOWBITS bits menos significativos são todos 0 e o restante são todos 1. -(1 << LOWBITS) é equivalente a isso, devido à associatividade da multiplicação, já que negar um valor é equivalente a multiplicá-lo por -1.

Advertências em Operações de Deslocamento

Aviso: Se a contagem de deslocamento for maior ou igual à largura em bits do primeiro operando promovido, os resultados dependem da máquina. Logicamente falando, o valor "correto" seria -1 (para deslocamento à direita de um número negativo) ou 0 (em todos os outros casos), mas o resultado real é o que a instrução de deslocamento da máquina faz nesse caso. Portanto, a menos que você possa provar que o segundo operando não é grande demais, escreva código para verificá-lo em tempo de execução.

Aviso: Nunca confie na relação entre os operadores de deslocamento e outros operadores binários aritméticos em termos de precedência. Programadores não lembram dessas precedências e não entenderão o código. Sempre use parênteses para especificar explicitamente o aninhamento, assim:

a + (b << 5)   /* Desloca primeiro, depois soma.  */
(a + b) << 5   /* Soma primeiro, depois desloca.  */

Nota: De acordo com o padrão C, o deslocamento de valores com sinal não é garantido para funcionar corretamente quando o valor deslocado é negativo ou se torna negativo durante a operação de deslocamento à esquerda. No entanto, apenas as pessoas mais rigorosas teriam motivo para se preocupar com isso; apenas computadores com instruções de deslocamento estranhas poderiam, plausivelmente, fazer isso de forma incorreta. No GNU C, a operação sempre funciona como esperado.

Atribuição Modificadora

Você pode abreviar a construção comum

lvalue = lvalue + expressão

como

lvalue += expressão

Isso é conhecido como uma atribuição modificadora. Por exemplo,

i = i + 5;
i += 5;

mostra duas instruções equivalentes. A primeira usa atribuição simples; a segunda usa atribuição modificadora.

A atribuição modificadora funciona com qualquer operador aritmético binário. Por exemplo, você pode subtrair algo de um lvalue assim:

lvalue -= expression

ou multiplicá-lo por uma certa quantia assim:

lvalue *= expression

ou deslocá-lo por uma certa quantia assim:

lvalue <<= expression
lvalue >>= expression

Na maioria dos casos, esse recurso não adiciona mais poder à linguagem, mas fornece uma conveniência substancial. Além disso, quando o lvalue contém código que possui efeitos colaterais, a atribuição simples executa esses efeitos colaterais duas vezes, enquanto a atribuição modificadora os executa apenas uma vez. Por exemplo,

x[foo ()] = x[foo ()] + 5;

chama foo duas vezes, e ele pode retornar valores diferentes a cada vez. Se foo () retornar 1 na primeira vez e 3 na segunda vez, o efeito pode ser somar x[3] e 5 e armazenar o resultado em x[1], ou somar x[1] e 5 e armazenar o resultado em x[3]. Não sabemos qual dos dois ocorrerá, porque C não especifica qual chamada a foo é computada primeiro.

Tal instrução não é bem definida e não deve ser usada.

Por outro lado,

x[foo ()] += 5;

é bem definida: chama foo apenas uma vez para determinar qual elemento de x ajustar, e ajusta esse elemento somando 5 a ele.

Divisão e Resto

A divisão de inteiros em C arredonda o resultado para um número inteiro. O resultado é sempre arredondado em direção a zero.

 16 / 3  ⇒ 5
-16 / 3  ⇒ -5
 16 / -3 ⇒ -5
-16 / -3 ⇒ 5

Para obter o resto correspondente, use o operador ‘%’:

 16 % 3  ⇒ 1
-16 % 3  ⇒ -1
 16 % -3 ⇒ 1
-16 % -3 ⇒ -1

‘%’ tem a mesma precedência de operador que ‘/’ e ‘*’.

A partir do quociente arredondado e do resto, você pode reconstruir o dividendo, assim:

int
original_dividend (int divisor, int quotient, int remainder)
{
  return divisor * quotient + remainder;
}

Para fazer uma divisão não arredondada, use ponto flutuante. Se apenas um operando for de ponto flutuante, ‘/’ converte o outro operando para ponto flutuante.

16.0 / 3   ⇒ 5.333333333333333
16   / 3.0 ⇒ 5.333333333333333
16.0 / 3.0 ⇒ 5.333333333333333
16   / 3   ⇒ 5

O operador de resto ‘%’ não é permitido para operandos de ponto flutuante, porque não é necessário. O conceito de resto faz sentido para inteiros porque o resultado da divisão de inteiros deve ser um número inteiro. Para ponto flutuante, o resultado da divisão é um número de ponto flutuante, ou seja, uma fração, que diferirá do resultado exato apenas por uma quantidade muito pequena.

Existem funções na biblioteca padrão C para calcular restos a partir da divisão de números de ponto flutuante por valores inteiros. Veja The GNU C Library no The GNU C Library Reference Manual (disponível somente em inglês).

A divisão de inteiros causa estouro em um caso específico: dividir o menor valor negativo para o tipo de dado (veja ) por -1. Isso ocorre porque o resultado correto, que é o número positivo correspondente, não cabe (veja Estouro de Inteiros) no mesmo número de bits. Em alguns computadores atualmente em uso, isso sempre causa um sinal SIGFPE (veja ), o mesmo comportamento que a opção -ftrapv especifica (veja Estouro de Inteiros com Sinal).

A divisão por zero leva a resultados imprevisíveis—dependendo do tipo de computador, pode causar um sinal SIGFPE ou pode produzir um resultado numérico.

Atenção: Certifique-se de que o programa não divide por zero. Se você não puder garantir que o divisor não é zero, teste se ele é zero e pule a divisão, se for o caso.

Atribuição Simples

Uma expressão de atribuição simples calcula o valor do operando à direita e o armazena no lvalue à esquerda. Aqui está uma expressão de atribuição simples que armazena 5 em i:

Dizemos que isso é uma atribuição para a variável i e que atribui a i o valor 5. Não tem ponto e vírgula porque é uma expressão (portanto, tem um valor). Adicionar um ponto e vírgula no final a tornaria uma instrução (veja ).

Aqui está outro exemplo de uma expressão de atribuição simples. Seus operandos não são simples, mas o tipo de atribuição feita aqui é uma atribuição simples.

Uma atribuição simples com dois tipos de dados numéricos diferentes converte o valor do operando à direita para o tipo do lvalue, se possível. Ela pode converter qualquer tipo numérico para qualquer outro tipo numérico.

A atribuição simples também é permitida em alguns tipos não numéricos: ponteiros (veja ), estruturas (veja ) e uniões (veja).

Aviso: A atribuição não é permitida em arrays porque em C não existem valores de array; variáveis em C podem ser arrays, mas esses arrays não podem ser manipulados como um todo. Veja .

Veja para as regras completas sobre tipos de dados usados em atribuições.

Armadilha: Atribuição em Subexpressões

Em C, a ordem de cálculo das partes de uma expressão não é fixa. Com exceção de alguns poucos casos especiais, as operações podem ser calculadas em qualquer ordem. Se uma parte da expressão tem uma atribuição para x e outra parte da expressão usa x, o resultado é imprevisível, pois esse uso pode ser calculado antes ou depois da atribuição.

Aqui está um exemplo de código ambíguo:

Se o segundo argumento, x, for calculado antes do terceiro argumento, x = 4, o valor do segundo argumento será 20. Se eles forem calculados na outra ordem, o valor do segundo argumento será 4.

Aqui está uma maneira de tornar esse código não ambíguo:

Aqui está outra maneira, com o outro significado:

Esse problema se aplica a todos os tipos de atribuições, e aos operadores de incremento e decremento, que são equivalentes a atribuições. Veja para mais informações sobre isso.

No entanto, pode ser útil escrever atribuições dentro de uma condição if ou um teste while, junto com operadores lógicos. Veja .

Operações Bit-a-bit

Operadores bit-a-bit (bitwise, em inglês) operam em inteiros, tratando cada bit de forma independente. Eles não são permitidos para tipos de ponto flutuante.

Os exemplos nesta seção usam constantes binárias, começando com ‘0b’ (veja ). Elas representam inteiros de 32 bits do tipo int. Vamos aos operadores:

Negação

Operador unário para negação bitwise; este operador altera cada bit de a de 1 para 0 ou de 0 para 1.

É útil lembrar que ~x + 1 é igual a -x, para inteiros, e ~x é igual a -x - 1. O último exemplo acima mostra isso com -1 como x.

Conjunção (E / AND)

Operador binário para “and” bit-a-bit ou “conjunção”. Cada bit no resultado é 1 se esse bit for 1 em ambos a e b.

Disjunção (OU / OR)

Operador binário para “ou” bit-a-bit (“ou inclusivo” ou “disjunção”). Cada bit no resultado é 1 se esse bit for 1 em a ou b.

Ou Exclusivo (XOR)

Operador binário para “xor” bit-a-bit (“ou exclusivo”). Cada bit no resultado é 1 se esse bit for 1 em exatamente um de a e b.

Para entender o efeito desses operadores em inteiros com sinal, lembre-se de que todos os computadores modernos usam representação em complemento de dois (veja ) para inteiros negativos. Isso significa que o bit mais alto do número indica o sinal; ele é 1 para um número negativo e 0 para um número positivo. Em um número negativo, o valor nos outros bits aumenta à medida que o número se aproxima de zero, de modo que 0b111…111 é -1 e 0b100…000 é o inteiro negativo mais baixo possível.

Aviso: C define uma ordem de precedência para os operadores binários bit-a-bit, mas você nunca deve confiar nisso. Você nunca deve confiar em como os operadores binários bit-a-bit se relacionam em precedência com os operadores binários aritméticos e de deslocamento. Outros programadores não lembram dessa ordem de precedência, então sempre use parênteses para especificar explicitamente o aninhamento.

Por exemplo, suponha que offset seja um inteiro que especifica o deslocamento dentro da memória compartilhada de uma tabela, exceto que seus poucos bits inferiores (LOWBITS indica quantos) são flags especiais. Veja como obter apenas esse deslocamento e adicioná-lo ao endereço base.

Graças ao conjunto externo de parênteses, não precisamos saber se ‘&’ tem precedência mais alta que ‘+’. Graças ao conjunto interno, não precisamos saber se ‘&’ tem precedência mais alta que ‘<<’. Mas podemos confiar que todos os operadores unários têm precedência mais alta que qualquer operador binário, então não precisamos de parênteses ao redor do operando esquerdo de ‘<<’.

Operadores de Incremento e Decremento

Os operadores ‘++’ e ‘--’ são os operadores de incremento e decremento. Quando usados em um valor numérico, eles adicionam ou subtraem 1. Não os consideramos como atribuições, mas eles são equivalentes a atribuições.

Usar ‘++’ ou ‘--’ como prefixo, antes de um lvalue, é chamado de pré-incremento ou pré-decremento. Isso adiciona ou subtrai 1, e o resultado se torna o valor da expressão. Por exemplo,

imprime linhas contendo 5, 6 e novamente 6. A expressão ++i incrementa i de 5 para 6 e tem o valor 6, então a saída do printf nessa linha exibe ‘6’.

Usando ‘--’ em vez disso, para pré-decremento,

imprime três linhas que contêm (respectivamente) ‘5’, ‘4’ e novamente ‘4’.

Deslocar Gera Novos Bits

Uma operação de deslocamento move os bits para uma extremidade do número e precisa gerar novos bits na outra extremidade.

Deslocar para a esquerda um bit deve gerar um novo bit menos significativo. Ele sempre insere um zero ali. Isso é equivalente a multiplicar pela potência de 2 correspondente. Por exemplo,

O significado de deslocar para a direita depende se o tipo de dado é com sinal ou sem sinal (veja ). Para um tipo de dado com sinal, ele realiza um “deslocamento aritmético,” que mantém o sinal do número inalterado ao duplicar o bit de sinal. Para um tipo de dado sem sinal, ele realiza um “deslocamento lógico,” que sempre insere zeros no bit mais significativo.

Em ambos os casos, deslocar para a direita um bit é uma divisão por dois, arredondando em direção ao infinito negativo. Por exemplo,

Para um operando esquerdo negativo a, a >> 1 não é equivalente a a / 2. Ambos dividem por 2, mas ‘/’ arredonda em direção a zero.

A contagem de deslocamento deve ser zero ou maior. Deslocar por um número negativo de bits produz resultados dependentes da máquina.

i = 5
x[foo ()] = y + 6
x = 20;
printf ("%d %d\n", x, x = 4);
y = 20;
printf ("%d %d\n", y, x = 4);
x = 4;
printf ("%d %d\n", x, x);
#include <stdio.h>   /* Declara printf. */

int
main (void)
{
  int i = 5;
  printf ("%d\n", i);
  printf ("%d\n", ++i);
  printf ("%d\n", i);
  return 0;
}
#include <stdio.h>   /* Declara printf. */

int
main (void)
{
  int i = 5;
  printf ("%d\n", i);
  printf ("%d\n", --i);
  printf ("%d\n", i);
  return 0;
}
5 << 3     é equivalente a   5 * 2*2*2
-10 << 4   é equivalente a   -10 * 2*2*2*2
(unsigned) 19 >> 2 ⇒ 4
(unsigned) 20 >> 2 ⇒ 5
(unsigned) 21 >> 2 ⇒ 5

7. Expressões de Atribuição

Como conceito geral em programação, uma atribuição é uma construção que armazena um novo valor em um local onde valores podem ser armazenados — por exemplo, em uma variável. Esses locais são chamados de lvalues (veja ) porque são locais (por isso o "l") que armazenam um valor.

Uma atribuição em C é uma expressão porque possui um valor; chamamos isso de expressão de atribuição. Uma atribuição simples se parece com:

lvalue = valor-para-armazenar

Dizemos que ela atribui o valor da expressão valor-para-armazenar ao local lvalue, ou que armazena valor-para-armazenar lá. Você também pode pensar no "l" de "lvalue" como significando "esquerda" (left), já que é o que você coloca no lado esquerdo do operador de atribuição.

No entanto, essa não é a única forma de usar um lvalue, e nem todos os lvalues podem ser atribuídos. Para usar o lvalue no lado esquerdo de uma atribuição, ele precisa ser modificável. Em C, isso significa que ele não foi declarado com o qualificador de tipo const (veja ).

O valor da expressão de atribuição é o de lvalue após o novo valor ser armazenado nele. Isso significa que você pode usar uma atribuição dentro de outras expressões. Os operadores de atribuição são associativos à direita, de modo que:

x = y = z = 0;

é equivalente a:

x = (y = (z = 0));

Essa é a única maneira útil para associá-los; a outra maneira,

((x = y) = z) = 0;

seria inválida, pois uma expressão de atribuição como x = y não é válida como um lvalue.

Quando o texto diz que o valor de expressão de atribuição é o de lvalue, quer dizer que a expressão "retorna" o lvalue. Por exemplo, i = 10 "retorna" 10. Ou seja, se essa expressão de atribuição inteira for utilizada à direita de um operador de atribuição, o valor dela será 10.

Aviso: Coloque parênteses ao redor de uma atribuição se você a aninhar dentro de outra expressão, a menos que seja uma expressão condicional, uma série separada por vírgulas ou outra atribuição.

Lvalues

Uma expressão que identifica um espaço de memória que armazena um valor é chamada de lvalue, porque é uma localização que pode armazenar um valor.

Os tipos padrão de lvalues são:

  • Uma variável.

  • Uma expressão de desreferenciamento de ponteiro (veja ) usando o unário ‘*’.

  • Uma referência a um campo de estrutura (veja ) usando ‘.’, se o valor da estrutura for um lvalue.

  • Uma referência a um campo de estrutura usando ‘->’. Isso é sempre um lvalue já que ‘->’ implica desreferenciamento de ponteiro.

  • Uma referência a uma alternativa de união (veja ), nas mesmas condições que para campos de estruturas.

  • Uma referência a um elemento de um array usando ‘[…]’, se o array for um lvalue.

Se a operação mais externa de uma expressão for qualquer outro operador, essa expressão não é um lvalue. Assim, a variável x é um lvalue, mas x + 0 não é, mesmo que essas duas expressões calculem o mesmo valor (supondo que x seja um número).

Um array pode ser um lvalue (as regras acima determinam se ele é um), mas usar o array em uma expressão o converte automaticamente em um ponteiro para o elemento zero. O resultado dessa conversão não é um lvalue. Portanto, se a variável a for um array, você não pode usá-la sozinha como o operando esquerdo de uma atribuição. Mas você pode atribuir a um elemento de a, como a[0]. Isso é um lvalue, já que a é um lvalue.

Operadores Lógicos

Os operadores lógicos combinam valores verdade (verdadeiro ou falso), que normalmente são representados em C como números. Qualquer expressão com um valor numérico é um valor verdade válido: zero significa falso, e qualquer outro valor significa verdadeiro. Um tipo de ponteiro também é significativo como valor verdade; um ponteiro nulo (que é zero) significa falso, e um ponteiro não nulo significa verdadeiro (veja ). O valor de um operador lógico é sempre 1 ou 0 e tem o tipo int (veja ).

Os operadores lógicos são usados principalmente na condição de uma instrução if, ou no teste final de uma instrução for ou while (veja ). No entanto, eles são válidos em qualquer contexto onde uma expressão com valor inteiro seja permitida.

! exp

Operador unário para "não" lógico. O valor é 1 (verdadeiro) se exp for 0 (falso), e 0 (falso) se exp for diferente de zero (verdadeiro).

Aviso: se exp for qualquer coisa além de um lvalue ou uma chamada de função, você deve escrever parênteses ao redor dela.

left && right

O operador binário "e" lógico computa left e, se necessário, right. Se ambos os operandos forem verdadeiros, a expressão && retorna o valor 1 (verdadeiro). Caso contrário, a expressão && retorna o valor 0 (falso). Se left resultar em um valor falso, isso determina o resultado geral, então right nem é computado.

left || right

O operador binário "ou" lógico computa left e, se necessário, right. Se pelo menos um dos operandos for verdadeiro, a expressão || retorna o valor 1 (verdadeiro). Caso contrário, a expressão || retorna o valor 0 (falso). Se left resultar em um valor verdadeiro, isso determina o resultado geral, então right nem é computado.

Aviso: nunca confie na precedência relativa de && e ||. Quando você usá-los juntos, sempre use parênteses para especificar explicitamente como eles se aninham, como mostrado aqui:

if ((r != 0 && x % r == 0)
    ||
    (s != 0 && x % s == 0))

8. Expressões de Controle de Execução

Este capítulo descreve os operadores de C que combinam expressões para controlar quais dessas expressões serão executadas, ou seja, em qual ordem.

Pós-incremento e Pós-decremento

Usar ‘++’ ou ‘--’ após um lvalue faz algo peculiar: obtém o valor diretamente do lvalue e, em seguida, o incrementa ou decrementa. Assim, o valor de i++ é o mesmo que o valor de i, mas i++ também incrementa i “um pouco depois”. Isso é chamado de pós-incremento ou pós-decremento.

Por exemplo,

#include <stdio.h>   /* Declara printf. */

int
main (void)
{
  int i = 5;
  printf ("%d\n", i);
  printf ("%d\n", i++);
  printf ("%d\n", i);
  return 0;
}

imprime linhas contendo 5, novamente 5, e 6. A expressão i++ tem o valor 5, que é o valor de i naquele momento, mas incrementa i de 5 para 6 logo em seguida.

O quanto "logo em seguida" acontece? O compilador tem certa flexibilidade para decidir isso. A regra é que o incremento deve ocorrer até o próximo sequence point; em casos simples, isso significa até o final da instrução. Veja .

Independentemente de onde exatamente o código compilado incrementa o valor de i, o ponto crucial é que o valor de i++ é o valor que i tinha antes de ser incrementado.

Se um operador unário precede uma expressão de pós-incremento ou pós-decremento, o incremento é aninhado internamente:

-a++   é equivalente a   -(a++)

Essa é a única ordem que faz sentido; -a não é um lvalue, então não pode ser incrementado.

O uso mais comum de pós-incremento é com arrays. Aqui está um exemplo de uso de pós-incremento para acessar um elemento de um array e avançar o índice para o próximo acesso. Compare com o exemplo avg_of_double (veja Um Exemplo com Arrays), que é quase o mesmo, mas não usa pós-incremento.

double
avg_of_double_alt (int length, double input_data[])
{
  double sum = 0;
  int i;

  /* Pega cada elemento e adiciona à soma.  */
  for (i = 0; i < length;)
    /* Usa o índice i, depois o incrementa.  */
    sum += input_data[i++];

  return sum / length;
}

Expressão Condicional

C possui uma expressão condicional que seleciona uma de duas expressões para calcular e obter o valor. Ela se parece com isto:

condição ? se_verdadeiro : se_falso

Regras para o Operador Condicional

O primeiro operando, condição, deve ser um valor que possa ser comparado com zero — um número ou um ponteiro. Se for verdadeiro (diferente de zero), a expressão condicional calcula se_verdadeiro e seu valor se torna o valor da expressão condicional. Caso contrário, a expressão condicional calcula se_falso e seu valor se torna o valor da expressão condicional. A expressão condicional sempre calcula apenas uma das duas, se_verdadeiro ou se_falso, nunca ambas.

Aqui está um exemplo: o valor absoluto de um número x pode ser escrito como:

(x >= 0 ? x : -x)

Aviso: Os operadores de expressão condicional têm uma precedência sintática relativamente baixa. Exceto quando a expressão condicional é usada como argumento em uma chamada de função, escreva parênteses ao redor dela. Para maior clareza, sempre escreva parênteses ao redor se ela se estender por mais de uma linha.

Os operadores de atribuição e o operador vírgula (veja Operador Vírgula) têm precedência mais baixa do que os operadores de expressão condicional, então coloque parênteses ao redor deles quando aparecerem dentro de uma expressão condicional. Veja .

Ramos do Operador Condicional

Chamamos os ramos (branches) da expressão condicional de se_verdadeiro e se_falso.

Os dois ramos normalmente devem ter o mesmo tipo, mas algumas exceções são permitidas. Se ambos forem tipos numéricos, a expressão condicional converte ambos para o tipo comum (veja ).

Com ponteiros (veja ), os dois valores podem ser ponteiros para tipos minimamente compatíveis (veja ). Nesse caso, o tipo de resultado é um ponteiro semelhante, cujo tipo de destino combina todos os qualificadores de tipo (veja ) de ambos os ramos.

Se um dos ramos tiver o tipo void * e o outro for um ponteiro para um objeto (não para uma função), a expressão condicional converte o ramo void * para o tipo do outro.

Se um dos ramos for uma constante inteira com valor zero e o outro for um ponteiro, a expressão condicional converte zero para o tipo do ponteiro.

No GNU C, você pode omitir se_verdadeiro em uma expressão condicional. Nesse caso, se a condição for diferente de zero, seu valor se torna o valor da expressão condicional, após a conversão para o tipo comum. Assim,

x ? : y

tem o valor de x se for diferente de zero; caso contrário, o valor de y.

Omitir se_verdadeiro é útil quando a condição tem efeitos colaterais. Nesse caso, escrever a expressão duas vezes executaria os efeitos colaterais duas vezes, mas escrevê-la uma vez os executa apenas uma vez. Por exemplo, supondo que a função next_element avance uma variável ponteiro para apontar para o próximo elemento em uma lista e retorne o novo ponteiro,

next_element () ? : default_pointer

é uma maneira de avançar o ponteiro e usar seu novo valor, se ele não for nulo, mas usar default_pointer se for nulo. Não podemos fazer da seguinte forma:

next_element () ? next_element () : default_pointer

porque isso avançaria o ponteiro uma segunda vez.

Operadores Lógicos e Atribuições

Existem casos em que atribuições aninhadas dentro de uma condição podem realmente tornar um programa mais fácil de ler. Aqui está um exemplo usando um tipo hipotético list que representa uma lista; ele testa se a lista tem pelo menos dois elementos, utilizando funções hipotéticas, nonempty, que retorna verdadeiro se o argumento for uma lista não vazia, e list_next, que avança de um elemento da lista para o próximo. Assumimos que uma lista nunca é um ponteiro nulo, de modo que as expressões de atribuição são sempre “verdadeiras.”

if (nonempty (list)
    && (temp1 = list_next (list))
    && nonempty (temp1)
    && (temp2 = list_next (temp1)))
  …  /* usa temp1 e temp2 */

Aqui aproveitamos o operador ‘&&’ para evitar a execução do restante do código se uma chamada a nonempty retornar “falso.” O único lugar natural para colocar as atribuições é entre essas chamadas.

Seria possível reescrever isso como várias instruções, mas isso poderia tornar o código muito mais pesado. Por outro lado, quando o teste é ainda mais complexo do que este, dividi-lo em várias instruções pode ser necessário para manter a clareza.

Se uma lista vazia for um ponteiro nulo, podemos dispensar a chamada a nonempty:

if ((temp1 = list_next (list))
    && (temp2 = list_next (temp1)))
  …

Operador Vírgula

O operador vírgula representa a execução sequencial de expressões. O valor da expressão com vírgula vem da última expressão da sequência; as expressões anteriores são calculadas apenas por seus efeitos colaterais. Ele se parece com isto:

exp1, exp2 …

Você pode agrupar qualquer número de expressões dessa forma, colocando vírgulas entre elas.

Os Usos do Operador Vírgula

Com vírgulas, você pode colocar várias expressões em um lugar que exige apenas uma expressão — por exemplo, no cabeçalho de uma instrução for. Esta instrução

for (i = 0, j = 10, k = 20; i < n; i++)

contém três expressões de atribuição para inicializar i, j e k. A sintaxe de for exige apenas uma expressão para inicialização; para incluir três atribuições, usamos vírgulas para agrupá-las em uma única expressão maior: i = 0, j = 10, k = 20. Essa técnica também é útil na expressão de avanço do laço, a última das três dentro dos parênteses do for.

Na instrução for e na instrução while (veja Instruções de Laço), uma vírgula fornece uma maneira de realizar algum efeito colateral antes do teste de saída do laço. Por exemplo,

while (printf ("No teste, x = %d\n", x), x != 0)

Uso Limpo do Operador Vírgula

Sempre escreva parênteses ao redor de uma série de operadores vírgula, exceto quando estiverem no nível superior em uma instrução de expressão, ou dentro dos parênteses de uma instrução if, for, while ou switch (veja ). Por exemplo, em

for (i = 0, j = 10, k = 20; i < n; i++)

as vírgulas entre as atribuições são claras porque estão entre um parêntese e um ponto e vírgula.

Os argumentos em uma chamada de função também são separados por vírgulas, mas isso não é um caso do operador vírgula. Note a diferença entre

foo (4, 5, 6)

que passa três argumentos para foo e

foo ((4, 5, 6))

que usa o operador vírgula e passa apenas um argumento (com valor 6).

Aviso: não use o operador vírgula ao redor de um argumento de função, a menos que isso torne o código mais legível. Quando o fizer, não coloque parte de outro argumento na mesma linha. Em vez disso, adicione uma quebra de linha para tornar os parênteses ao redor do operador vírgula mais fáceis de ver, como neste exemplo:

foo ((mumble (x, y), frob (z)),
     *p)

Quando Não Usar o Operador Vírgula

Você pode usar uma vírgula em qualquer subexpressão, mas, na maioria dos casos, isso apenas torna o código confuso, e é mais claro elevar todas as expressões separadas por vírgula, exceto a última, para um nível mais alto. Assim, em vez disso:

x = (y += 4, 8);

é muito mais claro escrever assim:

y += 4, x = 8;

ou assim:

y += 4;
x = 8;

Use vírgulas apenas nos casos em que não haja alternativa mais clara envolvendo múltiplas instruções.

Por outro lado, não hesite em usar vírgulas na expansão de uma definição de macro. As compensações em termos de clareza de código são diferentes nesse caso, porque o uso da macro pode melhorar tanto a clareza geral que a "feiúra" da definição da macro é um pequeno preço a pagar. Veja .

Reordenação de Operandos

A linguagem C não garante que as operações dentro de uma expressão sejam realizadas na ordem em que aparecem no código. Por exemplo, na expressão:

foo () + bar ()

foo pode ser chamada primeiro ou bar pode ser chamada primeiro. Se foo atualiza um dado e bar usa esse dado, os resultados podem ser imprevisíveis.

A ordem imprevisível de cálculo de subexpressões também faz diferença quando uma delas contém uma atribuição. Já vimos este exemplo de código problemático:

x = 20;
printf ("%d %d\n", x, x = 4);

Nesse caso, o segundo argumento, x, pode ter um valor diferente dependendo se ele for computado antes ou depois da atribuição no terceiro argumento.

Otimização e Ordenação

Os pontos de sequência limitam a liberdade do compilador para reordenar operações arbitrariamente, mas as otimizações ainda podem reordená-las se o compilador concluir que isso não alterará os resultados. Assim, neste código:

x++;
y = z;
x++;

há um ponto de sequência após cada instrução, então o código deve incrementar x uma vez antes da atribuição a y e outra vez depois. No entanto, o incremento de x não afeta y ou z, e a definição de y não pode afetar x. Por isso, o código pode ser otimizado para:

y = z;
x += 2;

Normalmente, isso não tem nenhum efeito além de tornar o programa mais rápido. Mas existem situações especiais em que isso pode causar problemas devido a coisas que o compilador não pode saber, como memória compartilhada. Para limitar a otimização nesses casos, use o qualificador de tipo volatile (veja ).

Associatividade e Ordenação

Um operador binário associativo, como +, quando usado repetidamente, pode combinar qualquer número de operandos. Os valores dos operandos podem ser calculados em qualquer ordem.

Se os valores forem inteiros e o estouro puder ser ignorado, eles podem ser combinados em qualquer ordem. Assim, dadas quatro funções que retornam unsigned int, chamá-las e somar seus resultados, como no exemplo:

(foo () + bar ()) + (baz () + quux ())

pode somar os resultados em qualquer ordem.

Por outro lado, a aritmética com inteiros com sinal, em que o estouro é significativo, nem sempre é associativa (veja Estouro de Inteiros). Portanto, as somas devem ser realizadas na ordem especificada, obedecendo os parênteses e a associação à esquerda. Isso significa calcular (foo () + bar ()) e (baz () + quux ()) primeiro (em qualquer ordem), depois somar os dois.

O mesmo se aplica à aritmética com valores de ponto flutuante, já que ela também não é realmente associativa. No entanto, a opção do GCC -funsafe-math-optimizations permite que o compilador altere a ordem de cálculo quando uma operação associativa (associativa na matemática exata) combina vários operandos. Essa opção entra em vigor ao compilar um módulo (veja ). Alterar a ordem de associação pode permitir que o programa otimize a execução das operações de ponto flutuante.

Em todos esses casos, as quatro chamadas de função podem ser realizadas em qualquer ordem. Não há certo ou errado nisso.

10. Ordem de Execução

A ordem de execução de um programa em C nem sempre é óbvia e não é necessariamente previsível. Este capítulo descreve no que você pode confiar.

Pós-incremento e Ordenação

Os requisitos de ordenação para as operações de pós-incremento e pós-decremento (veja Pós-incremento e Pós-decremento) são flexíveis: esses efeitos colaterais devem ocorrer "um pouco depois," antes do próximo ponto de sequência. Isso ainda permite várias ordens que podem produzir resultados diferentes. Nesta expressão:

z = x++ - foo ()

não é previsível se x será incrementado antes ou depois de chamar a função foo. Se foo referir-se a x, ela pode ver o valor antigo ou o valor incrementado.

A solução para estes e outros problemas de ordem de execução é simples: definir bem os pontos de sequência ao usar instruções separadas. Por exemplo:

x++;

z = x - foo ();

Nesta expressão em particular:

x = x++

x certamente será incrementado, mas o valor incrementado pode ser substituído pelo valor antigo. Isso acontece porque a incrementação e a atribuição podem ocorrer em qualquer ordem. Se a incrementação de x ocorrer depois da atribuição para x, o valor incrementado permanecerá. Mas, se a incrementação ocorrer primeiro, a atribuição colocará o valor não incrementado de volta em x, deixando a expressão como um todo sem alterar o valor de x.

Conclusão: evite tais expressões. Certifique-se, ao usar pós-incremento e pós-decremento, de que a expressão específica que você utiliza não seja ambígua quanto à ordenação de execução.

Pontos de Sequência

Existem alguns pontos no código onde C faz garantias limitadas sobre a ordem das operações. Esses pontos são chamados de pontos de sequência. Aqui estão os casos em que eles ocorrem:

  • No final de uma expressão completa: ou seja, uma expressão que não faz parte de uma expressão maior. Todos os efeitos colaterais gerados por essa expressão são realizados antes que a execução passe para o código subsequente.

  • No final do primeiro operando de certos operadores: ,, &&, || e ?:. Todos os efeitos colaterais especificados por esse operando são realizados antes de qualquer execução do próximo operando.

    As vírgulas que separam argumentos em uma chamada de função não são operadores vírgula e não criam pontos de sequência. A regra para argumentos de função e a regra para operandos são diferentes (veja Ordenação de Operandos).

  • Logo antes de chamar uma função: todos os efeitos colaterais gerados pelas expressões dos argumentos são realizados antes de chamar a função.

    Se a função a ser chamada não for constante — isto é, se for computada por uma expressão — todos os efeitos colaterais nessa expressão são realizados antes de chamar a função.

A ordenação imposta por um ponto de sequência se aplica localmente a um intervalo limitado de código, conforme indicado em cada caso acima. Por exemplo, a ordenação imposta pelo operador vírgula não se aplica ao código fora dos operandos desse operador. Assim, neste código:

(x = 5, foo (x)) + x * x

o ponto de sequência do operador vírgula ordena x = 5 antes de foo (x), mas x * x pode ser calculado antes ou depois deles.

Ordenação de Operandos

Os operandos e argumentos podem ser computados em qualquer ordem, mas há limites para esse embaralhamento no GNU C:

  • Operandos de um operador aritmético binário podem ser computados em qualquer ordem, mas não podem ser misturados: um deles precisa ser completamente calculado antes do outro. Quaisquer efeitos colaterais no operando que é computado primeiro são executados antes que o outro operando seja computado.

  • Isso também se aplica aos operadores de atribuição, exceto na atribuição simples, onde o valor anterior do operando à esquerda não é utilizado.

  • Os argumentos em uma chamada de função podem ser computados em qualquer ordem, mas também não podem ser misturados. Assim, um argumento é totalmente calculado, depois outro, e assim por diante, até que todos sejam concluídos. Quaisquer efeitos colaterais em um argumento são executados antes que a computação de outro argumento comece.

Essas regras não cobrem os efeitos colaterais causados pelos operadores de pós-incremento e pós-decremento — esses podem ser adiados até o próximo ponto de sequência.

Se quisermos ser rigorosos, o fato é que o GCC pode reordenar os cálculos de muitas outras maneiras, desde que isso não altere o resultado da execução do programa. No entanto, porque isso não altera o resultado do programa, é algo desprezível, a menos que você esteja preocupado com os valores de certas variáveis em determinados momentos vistos por outros processos. Nesses casos, você deve usar volatile para evitar otimizações que poderiam fazer com que se comportassem de maneira estranha. Veja .

Tipos Com ou Sem Sinal

Um tipo inteiro sem sinal pode representar apenas números positivos e zero. Um tipo com sinal pode representar tanto números positivos quanto negativos, em um intervalo distribuído quase igualmente em ambos os lados do zero. Por exemplo, unsigned char armazena números de 0 a 255 (na maioria dos computadores), enquanto signed char armazena números de -128 a 127. Cada um desses tipos possui 256 valores possíveis, já que ambos ocupam 8 bits.

Escreva signed ou unsigned antes da palavra-chave do tipo para especificar se o tipo é com ou sem sinal. No entanto, os tipos inteiros diferentes de char são, por padrão, com sinal; com eles, signed é um termo redundante.

O char simples pode ser com sinal ou sem sinal; isso depende do compilador, da máquina em uso e do sistema operacional.

Em muitos programas, não faz diferença se char é com ou sem sinal. Quando isso for relevante, não deixe ao acaso; escreva explicitamente signed char ou unsigned char.


Nota pessoal de Richard Stallman: Uma vez comendo com hackers em um restaurante de peixes, pedi um prato chamado Arctic Char. Quando minha refeição chegou, notei que o chef não a havia assinado. Então reclamei: "Este char está sem sinal—eu queria um char com sinal!" Ou, melhor, teria dito isso se tivesse pensado rápido o suficiente.

A piada acima funciona em inglês porque, em inglês, "signed" também significa "assinado" e no mundo da gastronomia existem os pratos assinados, de chefs famosos/as, etc.

11. Tipos Primitivos

Este capítulo descreve todos os tipos de dados primitivos de C — ou seja, todos os tipos de dados que não são compostos a partir de outros tipos. Eles incluem os tipos int e double, que já abordamos.

Esses tipos são todos compostos por bytes (veja ).

Tipos de Dados Inteiros

Aqui descrevemos todos os tipos inteiros e suas características básicas. Veja para mais informações sobre as representações de dados inteiros em nível de bits e aritmética.

~a
~0b10101000 ⇒ 0b11111111111111111111111101010111
~0 ⇒ 0b11111111111111111111111111111111
~0b11111111111111111111111111111111 ⇒ 0
~ (-1) ⇒ 0
a & b
0b10101010 & 0b11001100 ⇒ 0b10001000
a | b
0b10101010 | 0b11001100 ⇒ 0b11101110
a ^ b
0b10101010 ^ 0b11001100 ⇒ 0b01100110
shared_mem_base + (offset & (-1 << LOWBITS))

Escreva Atribuições em Instruções Separadas

É frequentemente conveniente escrever uma atribuição dentro de uma condição if, mas isso pode reduzir a legibilidade do programa. Aqui está um exemplo do que evitar:

if (x = advance (x))
  …

A ideia aqui é avançar x e testar se o valor é diferente de zero. No entanto, os leitores podem não perceber que está sendo usado ‘=’ e não ‘==’. De fato, escrever ‘=’ onde ‘==’ era pretendido dentro de uma condição é um erro comum, por isso o GNU C pode emitir avisos quando ‘=’ aparece de uma forma que sugere ser um erro.

É muito mais claro escrever a atribuição como uma instrução separada, assim:

x = advance (x);
if (x != 0)
  …

Isso torna inconfundivelmente claro que x está recebendo um novo valor.

Outro método é usar o operador vírgula (veja ), assim:

if (x = advance (x), x != 0)
  …

No entanto, colocar a atribuição em uma instrução separada geralmente é mais claro, a menos que a atribuição seja muito curta, pois isso reduz o aninhamento.

Conversão entre Tipos Inteiros

C converte tipos inteiros implicitamente em muitas situações. Os tipos inteiros estreitos, como char e short, são convertidos para int sempre que usados em operações aritméticas. A atribuição de um novo valor a uma variável inteira (ou outro lvalue) converte o valor para o tipo da variável.

Você também pode converter explicitamente um tipo inteiro para outro usando um operador de cast. Veja .

O processo de conversão para um tipo mais largo é direto: o valor permanece inalterado. A única exceção ocorre ao converter um valor negativo (obviamente em um tipo com sinal) para um tipo sem sinal mais largo. Nesse caso, o resultado é um valor positivo com os mesmos bits (veja ).

A conversão para um tipo mais estreito, também chamada de truncamento, envolve descartar alguns bits do valor. Isso não é considerado estouro (veja ), pois a perda de bits significativos é uma consequência normal do truncamento. O mesmo se aplica à conversão entre tipos com e sem sinal da mesma largura.

Mais informações sobre conversões em atribuições estão em . Para conversões em operações aritméticas, veja .

Operadores Lógicos e Comparações

A coisa mais comum a se usar dentro dos operadores lógicos é uma comparação. Convenientemente, ‘&&’ e ‘||’ têm precedência mais baixa do que os operadores de comparação e operadores aritméticos, então podemos escrever expressões assim, sem parênteses, e obter o aninhamento que é natural: duas operações de comparação que devem ser ambas verdadeiras.

Este exemplo também mostra como é útil que ‘&&’ garanta pular o operando à direita se o operando à esquerda for falso. Por causa disso, esse código nunca tenta dividir por zero.

Isto é equivalente:

Um valor verdade é simplesmente um número, então usar r como valor verdade testa se ele é diferente de zero. Mas o significado de r como uma expressão não é um valor verdade — é um número a ser usado na divisão. Portanto, é mais estiloso escrever explicitamente != 0.

Aqui está outra maneira equivalente de escrever isso:

Isso ilustra o operador unário ‘!’, e a necessidade de escrever parênteses ao redor de seu operando.

9. Gramática dos Operadores Binários

Operadores binários são aqueles que recebem dois operandos, um à esquerda e outro à direita.

Todos os operadores binários em C são sintaticamente associativos à esquerda. Isso significa que a op b op c significa (a op b) op c. No entanto, os únicos operadores que você deve repetir dessa forma sem parênteses são +, -, * e /, pois esses casos são claros na álgebra. Então, é aceitável escrever a + b + c ou a - b - c, mas nunca a == b == c ou a % b % c. Para esses operadores, use parênteses explícitos para mostrar como as operações se aninham.

Cada operador em C possui uma precedência, que é sua posição na ordem gramatical entre os vários operadores. Os operadores com a maior precedência capturam operandos adjacentes primeiro; essas expressões, então, se tornam operandos para operadores de menor precedência.

A ordem de precedência dos operadores em C é totalmente especificada, portanto, qualquer combinação de operações leva a um aninhamento bem definido. Declaramos apenas uma parte da ordem completa de precedência aqui, pois é uma má prática para o código C depender dos outros casos. Para casos não especificados neste capítulo, sempre use parênteses para tornar o aninhamento explícito.

Você pode depender desta subsequência da ordem de precedência (de maior para menor):

  • Operações pós-fixadas: acesso a um campo ou alternativa (‘.’ e ‘->’), indexação de array, chamadas de função e operadores unários pós-fixados.

  • Operadores unários prefixados.

  • Multiplicação, divisão e resto (eles têm a mesma precedência).

  • Adição e subtração (eles têm a mesma precedência).

  • Comparações — mas atenção!

  • Operadores lógicos && e || — mas atenção!

  • Expressão condicional com ? e :.

  • Atribuições.

  • Execução sequencial (o operador vírgula, ,).

Duas das linhas na lista acima dizem "mas atenção!" Isso significa que a linha cobre operadores com precedência sutilmente diferente. Nunca dependa da gramática de C para decidir como duas comparações se aninham; em vez disso, sempre use parênteses para especificar o aninhamento.

Você pode deixar vários operadores && ou || se associarem, mas sempre use parênteses para mostrar como && e || se aninham entre si. Veja .

Há uma outra ordem de precedência da qual o código pode depender:

  • Operadores unários pós-fixados.

  • Operadores bit-a-bit e de deslocamento — mas atenção!

  • Expressão condicional com ? e :.

A advertência para operadores bit-a-bit e de deslocamento é similar à dos operadores lógicos: você pode deixar múltiplos usos de um operador bit-a-bit se associarem, mas sempre use parênteses para controlar o aninhamento de operadores diferentes.

Essas listas não especificam qualquer ordem de precedência entre os operadores bit-a-bit e de deslocamento da segunda lista e os operadores binários acima das expressões condicionais na primeira lista. Quando eles aparecem juntos, coloque parênteses. Veja .

Nota pessoal de Richard Stallman: Eu escrevi o GCC sem lembrar nada sobre a ordem de precedência em C além do que está declarado aqui. Estudei a tabela completa de precedência para escrever o parser do GCC e prontamente a esqueci novamente. Se você precisar consultar a ordem completa de precedência para entender algum código em C, adicione parênteses suficientes para que ninguém mais precise fazer isso.

Inteiros Básicos

Os tipos de dados inteiros em C podem ser com sinal ou sem sinal. Um tipo sem sinal pode representar apenas números positivos e zero. Um tipo com sinal pode representar números positivos e negativos, em um intervalo distribuído quase igualmente em ambos os lados do zero.

Além da característica de sinalização, os tipos de dados inteiros variam em tamanho, ou seja, no número de bytes que ocupam. O tamanho determina o intervalo de valores inteiros que o tipo pode armazenar.

Aqui está uma lista dos tipos de dados inteiros com sinal, com os tamanhos que possuem na maioria dos computadores. Cada um tem um tipo correspondente sem sinal; veja .

  • signed char Um byte (8 bits). Este tipo inteiro é usado principalmente para inteiros que representam caracteres, geralmente como elementos de arrays ou campos de outras estruturas de dados.

  • short ou short int Dois bytes (16 bits).

  • int Quatro bytes (32 bits).

  • long ou long int Quatro bytes (32 bits) ou oito bytes (64 bits), dependendo da plataforma. Tipicamente, é de 32 bits em computadores de 32 bits e 64 bits em computadores de 64 bits, mas há exceções.

  • long long ou long long int Oito bytes (64 bits). Suportado no GNU C desde os anos 1980 e incorporado ao padrão C a partir do ISO C99.

Você pode omitir int ao usar long ou short. Isso é inofensivo e uma prática comum.

Tipos de Dados Complexos

Números complexos podem incluir uma parte real e uma parte imaginária. As constantes numéricas abordadas anteriormente possuem valores reais. Uma constante com valor imaginário é uma constante numérica real comum seguida de ‘i’.

Para declarar variáveis numéricas como complexas, use a palavra-chave _Complex. Os tipos de dados complexos padrão em C são de ponto flutuante:

No entanto, o GNU C também oferece suporte a tipos inteiros complexos.

Como _Complex é uma palavra-chave assim como float, double e long, essas palavras-chave podem aparecer em qualquer ordem, mas a ordem mostrada acima parece ser a mais lógica.

O GNU C suporta constantes para valores complexos. Por exemplo, 4.0 + 3.0i tem o valor 4 + 3i com o tipo _Complex double. Veja Constantes Imaginárias.

Para extrair as partes real e imaginária de um número complexo, o GNU C fornece as palavras-chave __real__ e __imag__:

O padrão C não inclui essas palavras-chave e, em vez disso, depende de funções definidas em complex.h para acessar as partes real e imaginária de um número complexo: crealf, creal e creall extraem a parte real de um número complexo de tipo float, double ou long double, respectivamente; cimagf, cimag e cimagl extraem a parte imaginária.

O GNU C também define ‘~’ como um operador para a conjugação complexa, o que significa negar a parte imaginária de um número complexo:

Para compatibilidade com o padrão C, você pode usar a função de biblioteca apropriada: conjf, conj ou conjl.


Nota de compatibilidade: Para compatibilidade com versões mais antigas do GNU C, a palavra-chave __complex__ também é aceita. No entanto, para usar um recurso mais novo, use a nova palavra-chave _Complex, conforme definido na ISO C11.

Tipo Booleano

O tipo inteiro sem sinal bool armazena valores verdade: seus possíveis valores são 0 e 1. Converter qualquer valor diferente de zero para bool resulta em 1. Por exemplo:

Diferentemente de int, bool não é uma palavra-chave. Ele é definido no arquivo de cabeçalho stdbool.h.

Na história das especificações de C, a palavra-chave _Bool foi introduzida na C99, junto com o arquivo de cabeçalho mencionado no texto. Isso significa que você pode usar uma variável do tipo _Bool e atribuir a ela os valores 0 ou 1. Alternativamente, você pode incluir o cabeçalho stdbool.h e usar bool junto a true ou false. No entanto, a partir da C23, bool é uma palavra-chave e não necessita de arquivos de cabeçalho adicionais, mas o suporte a esta especificação ainda pode ser parcial em alguns compiladores.

Constantes do Tipo Inteiro

Uma constante do tipo inteiro consiste em um número que especifica o valor, seguido opcionalmente por sufixos para especificar o tipo de dado.

As constantes mais simples do tipo inteiro são números escritos na base 10 (decimal), como 5, 77 e 403. Uma constante decimal não pode começar com o caractere ‘0’ (zero), pois isso a tornaria uma constante octal.

Você pode obter o efeito de uma constante do tipo inteiro negativa colocando um sinal de menos no início. Em termos gramaticais, isso é uma expressão aritmética, e não uma constante, mas se comporta como uma constante verdadeira.

As constantes do tipo inteiro também podem ser escritas em octal (base 8), hexadecimal (base 16) ou binário (base 2).

  • Uma constante octal começa com o caractere ‘0’ (zero), seguido por qualquer número de dígitos octais (‘0’ a ‘7’):

Tecnicamente, a constante 0 é uma constante octal, mas podemos considerá-la decimal, pois o valor é o mesmo.

  • Uma constante hexadecimal começa com ‘0x’ (maiúsculo ou minúsculo), seguido por dígitos hexadecimais (‘0’ a ‘9’, assim como ‘a’ até ‘f’ em maiúsculo ou minúsculo):

  • Uma constante binária começa com ‘0b’ (maiúsculo ou minúsculo), seguida por bits (cada um representado pelos caracteres ‘0’ ou ‘1’):

As constantes binárias são uma extensão do GNU C e não fazem parte do padrão C.

A partir do C23, as constantes binárias fazem parte do padrão.

Às vezes, é necessário um espaço após uma constante do tipo inteiro para evitar confusão léxica com os tokens seguintes. Veja Números Inválidos.

Inteiros Estreitos

Os tipos que são mais estreitos que int raramente são usados para variáveis comuns — em vez disso, usamos int. Isso ocorre porque C converte esses tipos mais estreitos para int em qualquer operação aritmética. Literalmente, não há razão para declarar uma variável local como char, por exemplo.

Particularmente, se o valor for realmente um caractere, você deve declarar a variável como int. Não como char! Usar esse tipo estreito pode forçar o compilador a truncar valores durante a conversão, o que é um desperdício. Além disso, algumas funções retornam um valor de caractere ou -1 para indicar "nenhum caractere". Usar int permite distinguir -1 de um caractere pelo sinal.

Os tipos inteiros estreitos são úteis como partes de outros objetos, como arrays e estruturas. Compare estas declarações de arrays, cujos tamanhos em processadores de 32 bits são mostrados:

Além disso, cadeias de caracteres (strings) devem ser compostas por char, porque é isso que todas as funções padrão da biblioteca esperam. Assim, o array ac poderia ser usado como uma cadeia de caracteres, mas os outros não.

O Tipo Void

O tipo de dado void é um tipo fictício — ele não permite operações. Ele literalmente significa “nenhum valor”. Quando uma função não deve retornar nenhum valor, usamos void como seu tipo de retorno. Nesse caso, as instruções return nessa função não devem especificar nenhum valor (veja ). Aqui está um exemplo:

Uma função que retorna void é comparável ao que outras linguagens (por exemplo, Fortran e Pascal) chamam de "procedimento" ao invés vez de "função".

Variações de Inteiros

Os tipos inteiros em C têm nomes padrão, mas seu significado pode variar dependendo do tipo de plataforma usada: o tipo de computador, o sistema operacional e o compilador. Pode até mesmo depender das opções usadas no compilador.

O char simples pode ser com sinal ou sem sinal; isso também depende da plataforma. Mesmo no GNU C, não há uma regra geral.

Em teoria, os tamanhos de todos os tipos inteiros podem variar. char é sempre considerado um "byte" em C, mas não necessariamente um byte de 8 bits; em algumas plataformas, ele pode ter mais de 8 bits. O padrão ISO C especifica apenas que nenhum desses tipos pode ser mais estreito do que os que estão acima dele na lista de e que short tem pelo menos 16 bits.

É possível que no futuro o GNU C suporte plataformas onde int tenha 64 bits. Na prática, no entanto, nos computadores reais de hoje, há pouca variação; você pode confiar na tabela apresentada anteriormente (veja ).

Para ter certeza absoluta do tamanho de um tipo inteiro, use os tipos int16_t, int32_t e int64_t. Seus tipos sem sinal correspondentes adicionam ‘u’ no início: uint16_t, uint32_t e uint64_t. Para definir todos esses tipos, inclua o arquivo de cabeçalho stdint.h.

O GNU C Compiler compila para alguns controladores embarcados que usam dois bytes para int. Em alguns, int é apenas um "byte", e o mesmo vale para short int — mas esse "byte" pode conter 16 bits ou até 32 bits. Esses processadores não suportam sistemas operacionais comuns (eles têm seus próprios sistemas operacionais especializados), e a maioria dos programas em C não tenta oferecer suporte a tais processadores.

if (r != 0 && x % r == 0)
if (r && x % r == 0)
if (!(r == 0) && x % r == 0)
_Complex float foo;
_Complex double bar;
_Complex long double quux;
_Complex double foo = 4.0 + 3.0i;

double a = __real__ foo; /* a agora é 4.0. */
double b = __imag__ foo; /* b agora é 3.0. */
_Complex double foo = 4.0 + 3.0i;
_Complex double bar = ~foo; /* bar agora é 4 - 3i. */
bool a = 0;
bool b = 1;
bool c = 4; /* Armazena o valor 1 em c. */
0      // zero
077    // 63
0403   // 259
0xff   // 255
0XA0   // 160
0xffFF // 65535
0b101  // 5
signed char ac[1000];   /* 1000 bytes */
short as[1000];         /* 2000 bytes */
int ai[1000];           /* 4000 bytes */
long long all[1000];    /* 8000 bytes */
void
print_if_positive (double x, double y)
{
  if (x <= 0)
    return;
  if (y <= 0)
    return;
  printf ("Next point is (%f,%f)\n", x, y);
}
Estouro de Inteiros
Operadores Lógicos
Operações Bit-a-bit
Tipos Com ou Sem Sinal
Inteiros Básicos
Inteiros Básicos

Designadores de Tipos

Algumas construções em C exigem uma forma de designar um tipo de dado específico, independente de qualquer variável ou expressão que tenha esse tipo. Isso é feito com um designador de tipo. Construções que precisam de um designador incluem casts (veja ) e sizeof (veja ).

Também usamos designadores de tipos para nos referirmos ao tipo de um valor em C, então você verá muitos designadores de tipo neste manual. Quando dizemos, "O valor tem tipo int," int é um designador de tipo.

Para criar o designador de qualquer tipo, imagine uma declaração de variável para uma variável desse tipo e exclua o nome da variável e o ponto e vírgula final.

Por exemplo, para designar o tipo de inteiros de palavra completa, começamos com a declaração de uma variável foo com esse tipo:

int foo;

Então, excluímos o nome da variável foo e o ponto e vírgula, deixando apenas int. Portanto, o designador de tipo para este tipo é int.

E quanto a inteiros longos sem sinal? A partir da declaração:

unsigned long int foo;

determinamos que o designador é unsigned long int.

Seguindo este procedimento, o designador para qualquer tipo primitivo é simplesmente o conjunto de palavras-chave que especifica esse tipo em uma declaração. O mesmo é válido para tipos compostos como estruturas, uniões e enumerações.

Os designadores para tipos de ponteiros seguem a regra de excluir o nome da variável e o ponto e vírgula, mas o resultado não é tão simples. Veja , como parte do capítulo sobre ponteiros. Veja , para designadores de tipos de arrays.

Para entender que tipo um designador representa, imagine um nome de variável inserido no lugar certo no designador para fazer uma declaração válida. Qual seria o tipo com o qual essa variável teria sido declarada? Esse é o tipo que o designador designa.

Constantes de Números Imaginários

Um número complexo consiste em uma parte real mais uma parte imaginária. (Você pode omitir uma das partes se ela for zero.) Esta seção explica como escrever constantes numéricas com valores imaginários. Ao adicionar essas constantes a constantes numéricas de valor real ordinário, podemos criar constantes com valores complexos.

A forma simples de escrever uma constante numérica imaginária é anexar o sufixo ‘i’ ou ‘I’, ou ‘j’ ou ‘J’, a uma constante inteira ou de ponto flutuante. Por exemplo, 2.5fi tem o tipo _Complex float e 3i tem o tipo _Complex int. As quatro letras de sufixo alternativas são todas equivalentes.

A outra maneira de escrever uma constante imaginária é multiplicar uma constante real por _Complex_I, que representa o número imaginário i. O padrão C não suporta sufixos com ‘i’ ou ‘j’, então este método mais trabalhoso é necessário.

Para escrever uma constante complexa com uma parte real não nula e uma parte imaginária não nula, escreva as duas separadamente e as some, assim:

4.0 + 3.0i

Isso dá o valor 4 + 3i, com o tipo _Complex double.

Essa soma pode incluir várias constantes reais, ou nenhuma. Da mesma forma, pode incluir várias constantes imaginárias, ou nenhuma. Por exemplo:

_Complex double foo, bar, quux;

foo = 2.0i + 4.0 + 3.0i; /* A parte imaginária é 5.0. */
bar = 4.0 + 12.0; /* A parte imaginária é 0.0. */
quux = 3.0i + 15.0i; /* A parte real é 0.0. */

Veja Tipos de Dados Complexos.

Tipos de Dados de Ponto Flutuante

Ponto flutuante é o análogo binário da notação científica: internamente, representa um número como uma fração e um expoente binário; o valor é essa fração multiplicada pela potência de 2 especificada. (O padrão C nominalmente permite outras bases, mas no GNU C a base é sempre 2.)

Por exemplo, para representar o número 6, a fração seria 0,75 e o expoente seria 3; juntos representam o valor 0,75 * 2^3, ou seja, 0,75 * 8. O valor 1,5 usaria 0,75 como fração e 1 como expoente. O valor 0,75 usaria 0,75 como fração e 0 como expoente. O valor 0,375 usaria 0,75 como fração e -1 como expoente.

Esses expoentes binários são usados por instruções da máquina. Você pode escrever uma constante de ponto flutuante dessa forma, usando hexadecimal, se desejar; mas normalmente escrevemos números de ponto flutuante em decimal (base 10). Veja .

C tem três tipos de dados de ponto flutuante:

double Ponto flutuante de "dupla precisão", que usa 64 bits. Este é o tipo de ponto flutuante normal, e computadores modernos geralmente fazem seus cálculos de ponto flutuante nesse tipo ou em algum tipo mais amplo. Exceto quando há um motivo especial para fazer diferente, este é o tipo a ser usado para valores de ponto flutuante.

float Ponto flutuante de "precisão simples", que usa 32 bits. É útil para valores de ponto flutuante armazenados em estruturas e arrays, para economizar espaço quando a precisão total de double não é necessária. Além disso, a aritmética de precisão simples é mais rápida em alguns computadores, o que pode ser útil ocasionalmente. Mas não com frequência — a maioria dos programas não usa o tipo float.

Seria mais claro se float fosse o nome do tipo que usamos para a maioria dos valores de ponto flutuante; no entanto, por razões históricas, não é assim.

long double Ponto flutuante de "precisão estendida", com precisão de 80 bits ou 128 bits, dependendo da máquina em uso. Em algumas máquinas, que não possuem formato de ponto flutuante mais amplo do que double, este é equivalente a double.

A aritmética de ponto flutuante levanta muitas questões sutis. Veja para mais informações.

Outros Tipos de Dados

Além dos tipos primitivos, C oferece várias formas de construir novos tipos de dados. Por exemplo:

  • Você pode definir ponteiros, valores que representam os endereços de outros dados (veja ).

  • Pode definir estruturas, como em muitas outras linguagens (veja ).

  • Pode criar uniões, que definem múltiplas formas de interpretar o conteúdo do mesmo espaço de memória (veja ).

  • Enumerações são coleções de códigos inteiros nomeados (veja ).

Os tipos de arrays em C são usados para alocar espaço para objetos, mas C não permite operar sobre um valor de array como um todo. Veja .

O último parágrafo explica que não podemos fazer coisas como int array[] = 10 em C (não conheço linguagem que permita operar arrays assim). Por serem conjunto de dados, é preciso operar sobre seus elementos, por exemplo, array[0] = 10.

Constantes de Ponto Flutuante

Uma constante de ponto flutuante deve ter um ponto decimal, um expoente de dez, ou ambos; isso a distingue de uma constante do tipo inteiro.

Para indicar um expoente, use ‘e’ ou ‘E’. O valor do expoente segue em seguida, sempre como um número decimal; ele pode opcionalmente começar com um sinal. O expoente n significa multiplicar o valor da constante por dez elevado à potência n.

Assim, ‘1500.0’, ‘15e2’, ‘15e+2’, ‘15.0e2’, ‘1.5e+3’, ‘.15e4’ e ‘15000e-1’ são seis maneiras de escrever um número de ponto flutuante cujo valor é 1500. Todas são equivalentes em princípio.

Exemplos com Pontos Decimais:

1.0
1000.
3.14159
.05
.0005

Constantes Equivalentes com Expoentes:

1e0, 1.0000e0
100e1, 100e+1, 100E+1, 1e3, 10000e-1
3.14159e0
5e-2, .0005e+2, 5E-2, .0005E2
.05e-2

Uma constante de ponto flutuante normalmente tem o tipo double. Você pode forçá-la a ser do tipo float adicionando ‘f’ ou ‘F’ no final. Por exemplo:

3.14159f
3.14159e0f
1000.f
100E1F
.0005f
.05e-2f

Da mesma forma, adicionar ‘l’ ou ‘L’ no final força a constante a ser do tipo long double.

Você também pode usar expoentes em constantes de ponto flutuante em hexadecimal, mas como ‘e’ seria interpretado como um dígito hexadecimal, o caractere ‘p’ ou ‘P’ (de "potência") indica um expoente.

O expoente em uma constante de ponto flutuante hexadecimal é um inteiro decimal com sinal opcional que especifica uma potência de 2 (não 10 ou 16) a ser multiplicada no número.

Exemplos:

0xAp2        // 40 em decimal
0xAp-1       // 5 em decimal
0x2.0Bp4     // 32.6875 em decimal
0xE.2p3      // 113 em decimal
0x123.ABCp0  // 291.6708984375 em decimal
0x123.ABCp4  // 4666.734375 em decimal
0x100p-8     // 1
0x10p-4      // 1
0x1p+4       // 16
0x1p+8       // 256

Veja Tipos de Dados de Ponto Flutuante.

Constantes do Tipo String

Uma constante do tipo string representa uma série de caracteres. Ela começa com ‘"’ e termina com ‘"’; entre esses delimitadores estão os conteúdos da string. Caracteres especiais como ‘"’, ‘\’ e nova linha podem ser escapados nas constantes do tipo string da mesma forma que nas constantes de caracteres. Em uma constante do tipo string, ‘'’ não precisa ser escapado.

Uma constante do tipo string define um array de caracteres que contém os caracteres especificados seguidos pelo caractere nulo (código 0). Usar a constante do tipo string é equivalente a usar o nome de um array com esses conteúdos. Em casos simples, onde não há sequências de escape com barra invertida, o comprimento em bytes da constante do tipo string é igual ao número de caracteres escritos nela mais um (para o caractere nulo).

Como acontece com qualquer array em C, usar a constante do tipo string em uma expressão converte o array em um ponteiro (veja ) para o elemento zero do array (veja ). Esse ponteiro terá o tipo char * porque aponta para um elemento do tipo char. char * é um exemplo de designador de tipo para um tipo de ponteiro (veja ). Esse tipo é usado para strings em geral, não apenas para strings expressas como constantes em um programa.

Assim, a constante do tipo string "Foo!" é quase equivalente a declarar um array como este:

char string_array_1[] = {'F', 'o', 'o', '!', '\0' };

E então usar string_array_1 no programa. No entanto, existem duas diferenças:

  1. A constante do tipo string não define um nome para o array.

  2. A constante do tipo string provavelmente será armazenada em uma área de memória somente leitura.

Novas linhas não são permitidas no texto de uma constante do tipo string. O motivo para essa proibição é detectar o erro de omitir o ‘"’ de fechamento. Para inserir uma nova linha em uma constante do tipo string, escreva-a como ‘\n’ na constante do tipo string.

Um caractere nulo real no código fonte dentro de uma constante do tipo string gera um aviso (warning) do compilador. Para colocar um caractere nulo no meio de uma constante do tipo string, escreva ‘\0’ ou ‘\000’.

Constantes do tipo string consecutivas são efetivamente concatenadas. Assim,

"Fo" "o!"   é equivalente a   "Foo!"

Isso é útil para escrever uma string que contém várias linhas, como esta:

"Esta mensagem é tão longa que precisa de mais de\n"
"uma única linha de texto. C não permite que uma\n"
"nova linha se represente diretamente em uma\n"
"constante do tipo string, então precisamos escrever\n"
"\\n para colocá-la na string. Para melhorar a\n"
"legibilidade do código-fonte, é recomendável\n"
"inserir quebras de linha no código onde elas\n"
"ocorrem no conteúdo da constante.\n"

A sequência de uma barra invertida e uma nova linha é ignorada em qualquer lugar de um programa C, inclusive dentro de uma constante do tipo string. Assim, você pode escrever constantes do tipo string em várias linhas desta forma:

"Esta é outra maneira de inserir novas linhas em uma\n\
constante do tipo string e quebrar a linha depois delas\n\
no código-fonte."

No entanto, a concatenação é a forma recomendada de fazer isso.

Você também pode escrever constantes do tipo string "estranhas" como esta:

"Fo\
o!"

Mas não faça isso—escreva assim:

"Foo!"

Tome cuidado para não passar uma constante do tipo string para uma função que modifica a string recebida. A memória onde a constante do tipo string está armazenada pode ser somente leitura, o que causaria um sinal fatal SIGSEGV, normalmente terminando a função (veja ). Ainda pior, a memória pode não ser somente leitura. Nesse caso, a função poderia modificar a constante do tipo string, prejudicando o conteúdo de outras constantes do tipo string que deveriam conter o mesmo valor e foram unificadas pelo compilador.

12. Constantes

Uma constante é uma expressão que representa um valor específico, representando explicitamente o valor desejado. C permite constantes para números, caracteres e strings. Já vimos constantes numéricas e de strings nos exemplos.

Constantes do Tipo Caractere Largo

Uma constante do tipo caractere largo (wide char) representa caracteres com mais de 8 bits no código do caractere. Esta é uma funcionalidade pouco comum que precisamos documentar, mas que provavelmente você nunca usará. Se você está apenas aprendendo C, pode ignorar esta seção.

A constante original do tipo caractere largo em C é escrita como ‘L’ (maiúscula!) seguida imediatamente por uma constante de caractere comum (sem espaço intermediário). Seu tipo de dado é wchar_t, que é um alias definido em stddef.h para um dos tipos de inteiro padrão. Dependendo da plataforma, pode ser de 16 bits ou 32 bits. Se for de 16 bits, essas constantes de caractere usam a forma UTF-16 do Unicode; se forem de 32 bits, usam o UTF-32.

Também existem constantes Unicode do tipo caractere largo que especificam explicitamente a largura. Essas constantes começam com ‘u’ ou ‘U’ em vez de ‘L’. O prefixo ‘u’ especifica uma constante Unicode de 16 bits, e o prefixo ‘U’ especifica uma constante Unicode de 32 bits. Seus tipos são, respectivamente, char16_t e char32_t; esses tipos são declarados no arquivo de cabeçalho uchar.h. Essas constantes de caractere são válidas mesmo se uchar.h não for incluído, mas alguns usos podem ser inconvenientes sem incluir o cabeçalho para declarar esses nomes de tipo.

O caractere representado em uma constante do tipo caractere largo pode ser um caractere ASCII comum. L'a', u'a' e U'a' são todos válidos e todos iguais a 'a'.

Em todos os três tipos de constantes do tipo caractere largo, você pode escrever diretamente um caractere Unicode não ASCII na constante; o valor da constante será o código Unicode do caractere. Ou você pode especificar o caractere Unicode com uma sequência de escape (veja Códigos de Caracteres Unicode).

Programa Completo Linha por Linha

Aqui está o mesmo exemplo explicado linha por linha. Iniciantes, vocês acham que esta seção ajuda ou não? Vocês gostariam de um layout diferente por exemplo? Por favor, escreva para rms@gnu.org (em inglês).

#include <stdio.h>      /* Inclui as declarações de funções */
                        /*   de E/S comuns como a printf.  */
                        /* A maiora dos programas precisa delas.  */

int                     /* Essa função retorna um int.  */
fib (int n)             /* O nome dela é fib;  */
                        /*   seu argumento é o n.  */
{                       /* Início do corpo da função.  */
  /* Evita que a recursão seja inifinta.  */
  if (n <= 2)           /* Se n é 1 ou 2,  */
    return 1;           /*   faça com que fib retorne 1.  */
  else                  /* do contrário, some os dois números  */
                        /* Fibonacci anteriores.  */
    return fib (n - 1) + fib (n - 2);
}

int                     /* Essa função retorna um int.  */
main (void)             /* Comece aqui; ignore os argumentos.  */
{                       /* Imprima a mensagem com os números.  */
  printf ("Fibonacci series item %d is %d\n",
          20, fib (20));
  return 0;             /* Termine programa, reporte successo.  */
}

13. Tamanho de Tipo

Cada tipo de dado possui um tamanho, que é o número de bytes (veja Armazenamento e Dados) que ele ocupa na memória. Para referenciar esse tamanho em um programa C, usa-se sizeof. Existem duas formas de utilizá-lo:

sizeof expressão Essa forma retorna o tamanho da expressão, com base em seu tipo de dado. Ela não calcula o valor da expressão — apenas seu tamanho —, então, se a expressão contiver efeitos colaterais ou chamadas de função, eles não serão executados. Portanto, sizeof é sempre uma operação em tempo de compilação e não tem custo em tempo de execução.

Não é permitido usar como operando de sizeof um valor que seja um campo de bits (veja ).

Por exemplo:

double a;
i = sizeof a + 10;

inicializa i com 18 na maioria dos computadores, porque a ocupa 8 bytes.

Veja como determinar o número de elementos de um array array:

(sizeof array / sizeof array[0])

A expressão sizeof array retorna o tamanho do array, e não o tamanho de um ponteiro para um elemento. No entanto, se a expressão for um parâmetro de função declarado como array, essa variável na verdade tem tipo de ponteiro (veja ), portanto o resultado será o tamanho desse ponteiro.

sizeof (tipo) Essa forma retorna o tamanho de tipo. Por exemplo:

i = sizeof (double) + 10;

é equivalente ao exemplo anterior.

Não é permitido aplicar sizeof a um tipo incompleto (veja ), nem a void. Aplicá-lo a um tipo de função retorna 1 no GNU C, o que permite que a adição de um inteiro a um ponteiro de função funcione como desejado (veja ).


Aviso: Ao usar sizeof com um tipo em vez de uma expressão, é obrigatório escrever parênteses em torno do tipo.

Aviso: Ao aplicar sizeof ao resultado de um cast (veja ), também é necessário colocar parênteses ao redor da expressão de cast para evitar ambiguidade na gramática da linguagem C. Especificamente:

sizeof (int) -x

é interpretado como:

(sizeof (int)) - x

Se o que se deseja é:

sizeof ((int) -x)

deve-se escrever exatamente assim, com os parênteses adicionais.


O tipo do valor retornado por sizeof é sempre um tipo inteiro sem sinal; qual exatamente depende da máquina. O cabeçalho stddef.h define o nome size_t como um alias (typedef) para esse tipo. Veja .

Códigos de Caracteres Unicode

Você pode especificar caracteres Unicode, para constantes do tipo caractere individuais ou como parte de constantes do tipo string (veja Constantes do Tipo String), usando sequências de escape; e até mesmo em identificadores C. Use a sequência de escape ‘\u’ com um código Unicode hexadecimal de 16 bits. Se o valor do código for grande demais para 16 bits, use a sequência de escape ‘\U’ com um código Unicode hexadecimal de 32 bits. (Esses códigos são chamados de nomes universais de caracteres.) Por exemplo:

\u6C34      /* Código de 16 bits (UTF-16) */
\U0010ABCD  /* Código de 32 bits (UTF-32) */

Uma forma de utilizá-los é em constantes do tipo string UTF-8 (veja Constantes do Tipo String UTF-8). Por exemplo:

u8"fóó \u6C34 \U0010ABCD"

Você também pode usá-los em constantes do tipo caractere largo (veja Constantes do Tipo Caractere Largo), como neste exemplo:

u'\u6C34'      /* Código de 16 bits */
U'\U0010ABCD'  /* Código de 32 bits */

E em constantes do tipo string larga (veja Constantes do Tipo String Larga), como neste exemplo:

u"\u6C34\u6C33"  /* Código de 16 bits */
U"\U0010ABCD"    /* Código de 32 bits */

Além disso, você pode usar esses códigos em um identificador:

int foo\u6C34bar = 0;

Os códigos no intervalo de D800 a DFFF não são válidos em Unicode. Códigos menores que 00A0 também são proibidos, exceto pelos códigos 0024, 0040 e 0060; esses caracteres são, na verdade, caracteres de controle ASCII, que você pode especificar com outras sequências de escape (veja Constantes de Caracteres).

Constantes de Caracteres

Uma constante de caractere é escrita com aspas simples, como em 'c'. No caso mais simples, c é um único caractere ASCII que a constante deve representar. A constante tem o tipo int, e seu valor é o código do caractere correspondente. Por exemplo, 'a' representa o código do caractere para a letra ‘a’: 97, no caso.

Para colocar o caractere ‘'’ (aspas simples) na constante de caractere, use a barra invertida (‘\’) como escape. Essa constante de caractere fica assim: '\''. A barra invertida aqui funciona como um caractere de escape, e tal sequência, começando com ‘\’, é chamada de sequência de escape.

Para colocar o caractere ‘\’ (barra invertida) na constante de caractere, use outra barra invertida como escape. Essa constante de caractere fica assim: '\\'.

Aqui estão todas as sequências de escape que representam caracteres específicos em uma constante de caractere. Os valores numéricos mostrados são os códigos ASCII correspondentes, como números decimais:

'\a' ⇒ 7       /* alarme, CTRL-g */
'\b' ⇒ 8       /* backspace, BS, CTRL-h */
'\t' ⇒ 9       /* tabulação, TAB, CTRL-i */
'\n' ⇒ 10      /* nova linha, CTRL-j */
'\v' ⇒ 11      /* tabulação vertical, CTRL-k */
'\f' ⇒ 12      /* avanço de formulário, CTRL-l */
'\r' ⇒ 13      /* retorno de carro, RET, CTRL-m */
'\e' ⇒ 27      /* caractere de escape, ESC, CTRL-[ */
'\\' ⇒ 92      /* caractere de barra invertida, \ */
'\'' ⇒ 39      /* caractere de aspas simples, ' */
'\"' ⇒ 34      /* caractere de aspas duplas, " */
'\?' ⇒ 63      /* ponto de interrogação, ? */

‘\e’ é uma extensão do GNU C; para seguir o padrão C, escreva ‘\33’. (O número após a barra invertida é octal.) Para especificar uma constante de caractere usando decimal, use um cast; por exemplo, (unsigned char) 27.

Você também pode escrever códigos de caracteres em octal e hexadecimal como ‘\octalcode’ ou ‘\xhexcode’. Decimal não é uma opção aqui, então códigos octais não precisam começar com ‘0’.

O valor da constante de caractere tem o tipo int. No entanto, o código do caractere é tratado inicialmente como um valor do tipo char, que é então convertido para int. Se o código do caractere for maior que 127 (0177 em octal), o int resultante pode ser negativo em uma plataforma onde o tipo char tem 8 bits e é com sinal.

Tipos de Dados de Constantes do Tipo Inteiro

O tipo de uma constante do tipo inteiro é normalmente int, se o valor couber nesse tipo, mas aqui estão as regras completas. O tipo de uma constante do tipo inteiro é o primeiro nesta sequência que pode representar corretamente o valor:

  • int

  • unsigned int

  • long int

  • unsigned long int

  • long long int

  • unsigned long long int

E que não seja excluído pelas regras a seguir:

  • Se a constante tiver o sufixo ‘l’ ou ‘L’, isso exclui os dois primeiros tipos (não longos).

  • Se a constante tiver o sufixo ‘ll’ ou ‘LL’, isso exclui os primeiros quatro tipos (não long long).

  • Se a constante tiver o sufixo ‘u’ ou ‘U’, isso exclui os tipos com sinal.

  • Caso contrário, se a constante for decimal (não binária, octal ou hexadecimal), isso exclui os tipos sem sinal.

Exemplos de Sufixos:

3000000000u      // três bilhões como unsigned int.
0LL              // zero como long long int.
0403l            // 259 como long int.

Os sufixos em constantes do tipo inteiro são raramente usados. Quando o tipo preciso é importante, é mais claro converter explicitamente (veja Conversão de Tipo Explícita).

Veja Tipos de Dados Inteiros.

Desreferenciando Ponteiros

O principal uso de um valor de ponteiro é desreferenciá-lo (acessar os dados para os quais ele aponta) com o operador unário ‘*’. Por exemplo, *&i é o valor no endereço de i — que é justamente i. As duas expressões são equivalentes, desde que &i seja válido.

Uma expressão de desreferência de ponteiro cujo tipo é um dado (e não uma função) é um lvalue.

Os ponteiros se tornam realmente úteis quando os armazenamos em algum lugar e os usamos depois. Aqui está um exemplo simples para ilustrar essa prática:

{
  int i;
  int *ptr;

  ptr = &i;

  i = 5;

  …

  return *ptr;   /* Retorna 5, obtido de i. */
}

Isso mostra como declarar a variável ptr com o tipo int * (ponteiro para int), armazenar nela um valor de ponteiro (apontando para i) e usá-lo depois para obter o valor do objeto para o qual ele aponta (o valor em i).

Aqui está outro exemplo de uso de um ponteiro para uma variável:

/* Define a variável global i. */
int i = 2;

int
foo (void)
{
  /* Armazena o endereço da variável global i. */
  int *global_i = &i;

  /* Declara uma i local, ocultando a i global. */
  int i = 5;

  /* Imprime o valor da i global e da i local. */
  printf ("global i: %d\nlocal i: %d\n", *global_i, i);
  return i;
}

É claro que, em um programa real, seria muito mais limpo usar nomes diferentes para essas duas variáveis, em vez de chamar ambas de i. Mas é difícil ilustrar esse ponto sintático com um código “limpo”. Se alguém puder fornecer um exemplo útil para ilustrar isso com mais clareza, ele será bem-vindo.

Tipos de Ponteiros

Para cada tipo de dado t, existe um tipo para ponteiros para o tipo t. Para estas variáveis:

int i;
double a[5];
  • i tem tipo int; dizemos que &i é um “ponteiro para int”.

  • a tem tipo double[5]; dizemos que &a é um “ponteiro para um array de cinco doubles”.

  • a[3] tem tipo double; dizemos que &a[3] é um “ponteiro para double”.

Designadores de Tipo de Ponteiro

Todo tipo em C possui um designador; você o obtém removendo o nome da variável e o ponto e vírgula de uma declaração (veja Designadores de Tipos). Aqui estão os designadores para os tipos de ponteiros das declarações de exemplo da seção anterior:

int *           /* Ponteiro para int. */
double *        /* Ponteiro para double. */
double (*)[5]   /* Ponteiro para double[5]. */

Lembre-se: para entender que tipo um designador representa, imagine a declaração correspondente com um nome de variável inserido, e determine qual seria o tipo dessa variável. Assim, o designador de tipo double (*)[5] corresponde à declaração de variável:

double (*variavel)[5];

Isso declara uma variável ponteiro que, ao ser desreferenciada, fornece um array de 5 doubles. Portanto, o designador de tipo significa: “ponteiro para um array de 5 doubles”.

Endereço dos Dados

A forma mais básica de criar um ponteiro é com o operador “de endereço”, ‘&’. Suponha que temos as seguintes variáveis disponíveis:

int i;
double a[5];

Agora, &i fornece o endereço da variável i — um valor de ponteiro que aponta para a localização de i — e &a[3] fornece o endereço do elemento 3 de a. (Pela convenção usual de numeração em inglês com origem no 1, esse é na verdade o quarto elemento do array, já que o elemento inicial tem índice 0.)

O operador de endereço é incomum porque opera sobre um local para armazenar um valor (um lvalue, veja Lvalues), e não sobre o valor atualmente armazenado ali. (O argumento à esquerda de uma atribuição simples também é incomum por esse mesmo motivo.) Você pode usá-lo sobre qualquer lvalue, exceto um campo de bits (veja Campos de Bits) ou um construtor (veja ).

Desreferenciando Ponteiros Nulos ou Inválidos

Tentar desreferenciar um ponteiro nulo é um erro. Na maioria das plataformas, isso geralmente causa um sinal, normalmente SIGSEGV (veja ).

char *foo = NULL;
c = *foo;    /* Isso causa um sinal e termina o programa. */

O mesmo ocorre com um ponteiro que possui alinhamento incorreto para o tipo de dado apontado (em muitos tipos de computadores), ou que aponta para uma parte da memória não alocada no espaço de endereçamento do processo.

Esse sinal termina o programa, a menos que ele tenha sido configurado para lidar com o sinal (veja The GNU C Library em The GNU C Library Reference Manual).

No entanto, o sinal pode não ocorrer se a desreferência for eliminada pela otimização. No exemplo acima, se você não usar posteriormente o valor de c, o GCC pode otimizar e remover o código de *foo. Para evitar esse tipo de otimização, você pode usar o qualificador volatile, como neste exemplo:

volatile char *p;
volatile char c;
c = *p;

Você pode usar isso para testar se p aponta para memória não alocada. Basta configurar primeiro um tratador de sinal, para que o sinal não termine o programa.

Constantes do Tipo String Larga

Uma constante do tipo string larga (wide string) representa um array de caracteres de 16 bits ou 32 bits. Elas são raramente usadas; se você está apenas aprendendo C, pode ignorar esta seção.

Existem três tipos de constantes do tipo string larga, que diferem no tipo de dado usado para cada caractere na string. Cada constante desse tipo é equivalente a um array de números inteiros, mas o tipo desses inteiros depende do tipo específico da constante. Quando utilizada em uma expressão, a constante será convertida em um ponteiro para o primeiro elemento do array, como ocorre normalmente com arrays em C (veja ). Para cada tipo de constante do tipo string larga, indicamos aqui o tipo desse ponteiro.

char16_t Este é um tipo de constante do tipo string Unicode de 16 bits: cada elemento é um código de caractere Unicode de 16 bits com o tipo char16_t, de forma que a string possui o tipo de ponteiro char16_t *. (Este é um designador de tipo; veja .) A constante é escrita como ‘u’ (em letra minúscula) seguida (sem espaço intermediário) de uma constante do tipo string com a sintaxe usual.

char32_t Este é um tipo de constante do tipo string Unicode de 32 bits: cada elemento é um código de caractere Unicode de 32 bits, e a string possui o tipo char32_t *. A constante é escrita como ‘U’ (em letra maiúscula) seguida (sem espaço intermediário) de uma constante do tipo string com a sintaxe usual.

wchar_t Este é o tipo original de constante do tipo string larga. A constante é escrita como ‘L’ (em letra maiúscula) seguida (sem espaço intermediário) de uma constante do tipo string com a sintaxe usual, e a string possui o tipo wchar_t *.

A largura do tipo de dado wchar_t depende da plataforma de destino, o que torna esse tipo de string larga um pouco menos útil em comparação com os tipos mais novos.

No Windows, por padrão, as funções da API que operam com strings requerem strings largas. Por exemplo:

MessageBox(NULL, L"Olá", L"Mundo", MB_OK) .

Os tipos char16_t e char32_t são declarados no cabeçalho uchar.h. O tipo wchar_t é declarado no cabeçalho stddef.h.

Constantes consecutivas do tipo string larga do mesmo tipo se concatenam, assim como constantes do tipo string comuns. Uma constante do tipo string larga concatenada com uma constante do tipo string comum resulta em uma constante do tipo string larga. No entanto, não é possível concatenar duas constantes do tipo string larga de tipos diferentes. Além disso, não é possível concatenar uma constante do tipo string larga (de qualquer tipo) com uma constante do tipo string UTF-8.

14. Ponteiros

Entre as linguagens de alto nível, C é relativamente de baixo nível, próxima da máquina. Isso se deve principalmente ao fato de possuir ponteiros explícitos. Um valor de um ponteiro é um endereço numérico de dados na memória. O tipo de dado a ser encontrado nesse endereço é especificado pelo tipo de dado do próprio ponteiro. Nada na linguage C pode determinar o tipo de dado “correto” da informação na memória; ela segue cegamente o tipo de dado do ponteiro usado para acessar os dados.

O operador unário ‘*’ obtém os dados para os quais um ponteiro aponta — isso é chamado de desreferenciar o ponteiro. Seu valor sempre possui o tipo que o ponteiro aponta.

C também permite ponteiros para funções, mas como há algumas diferenças em seu funcionamento, trataremos deles mais adiante. .

Constantes do Tipo String UTF-8

Escrever ‘u8’ imediatamente antes de uma constante do tipo string, sem espaço intermediário, significa representar essa string na codificação UTF-8 como uma sequência de bytes. O UTF-8 representa caracteres ASCII com um único byte e representa caracteres Unicode não ASCII (códigos 128 e acima) como sequências de múltiplos bytes. Aqui está um exemplo de uma constante do tipo string UTF-8:

u8"A cónstàñt"

Essa constante ocupa 13 bytes mais o caractere nulo de terminação, porque cada uma das letras acentuadas é uma sequência de dois bytes.

Concatenar uma string comum com uma string UTF-8 conceitualmente produz outra string UTF-8. No entanto, se a string comum contiver códigos de caracteres 128 ou superiores, os resultados não podem ser considerados confiáveis.

Ponteiros Nulos

Um valor de ponteiro pode ser nulo, o que significa que ele não aponta para nenhum objeto. A forma mais limpa de obter um ponteiro nulo é escrevendo NULL, uma macro padrão definida em <stddef.h>. Também é possível fazer isso convertendo o valor 0 para o tipo de ponteiro desejado, como em:

(char *) 0

(O operador de cast realiza a conversão explícita de tipo; veja .)

Você pode armazenar um ponteiro nulo em qualquer lvalue cujo tipo de dado seja um tipo de ponteiro:

char *foo;
foo = NULL;

Essas duas linhas, se forem consecutivas, podem ser combinadas em uma declaração com inicializador:

char *foo = NULL;

Também é possível fazer o cast explícito de NULL para o tipo de ponteiro específico desejado — isso não faz diferença:

char *foo;
foo = (char *) NULL;

Para testar se um ponteiro é nulo, compare-o com zero ou com NULL, como no exemplo abaixo:

if (p != NULL)
  /* p não é nulo. */
  operate(p);

Como testar se um ponteiro não é nulo é algo básico e frequente, todos, exceto iniciantes em C, entenderão a forma abreviada sem != NULL:

if (p)
  /* p não é nulo. */
  operate(p);

Declarações de Variáveis Ponteiro

A forma de declarar que uma variável foo aponta para o tipo t é:

t *foo;

Para lembrar essa sintaxe, pense: “se você desreferenciar foo, usando o operador ‘*’, o que você obtém é do tipo t. Logo, foo aponta para o tipo t.”

Assim, podemos declarar variáveis que armazenam ponteiros para esses três tipos, da seguinte maneira:

int *ptri;            /* Ponteiro para int. */
double *ptrd;         /* Ponteiro para double. */
double (*ptrda)[5];   /* Ponteiro para double[5]. */

A declaração int *ptri; significa: “se você desreferenciar ptri, obtém um int.” Já double (*ptrda)[5]; significa: “se você desreferenciar ptrda, e então indexar por um inteiro menor que 5, você obtém um double.” Os parênteses expressam o fato de que você deve desreferenciar primeiro, depois indexar.

Compare isso com a seguinte declaração:

double *aptrd[5];     /* Array de cinco ponteiros para double. */

Como o operador ‘*’ tem precedência sintática menor que a indexação, double *aptrd[5] significa: “se você indexar aptrd por um inteiro menor que 5, e então desreferenciar, obtém um double.” Portanto, *aptrd[5] declara um array de ponteiros, não um ponteiro para array.

Aritmética com Ponteiros

Somar um inteiro (positivo ou negativo) a um ponteiro é válido em C. Isso assume que o ponteiro aponta para um elemento de um array, e avança ou recua o ponteiro o número de elementos indicado pelo inteiro. Aqui está um exemplo em que somar um inteiro positivo avança o ponteiro para elementos posteriores do mesmo array:

void
incrementing_pointers()
{
  int array[5] = { 45, 29, 104, -3, 123456 };
  int elt0, elt1, elt4;

  int *p = &array[0];
  /* Agora p aponta para o elemento 0. Busque o valor. */
  elt0 = *p;

  ++p;
  /* Agora p aponta para o elemento 1. Busque o valor. */
  elt1 = *p;

  p += 3;
  /* Agora p aponta para o elemento 4 (o último). Busque o valor. */
  elt4 = *p;

  printf("elt0 %d  elt1 %d  elt4 %d.\n", elt0, elt1, elt4);
  /* Imprime: elt0 45  elt1 29  elt4 123456. */
}

Abaixo, um exemplo em que somar um inteiro negativo recua o ponteiro para elementos anteriores do array:

void
decrementing_pointers()
{
  int array[5] = { 45, 29, 104, -3, 123456 };
  int elt0, elt3, elt4;

  int *p = &array[4];
  /* Agora p aponta para o elemento 4 (o último). Busque o valor. */
  elt4 = *p;

  --p;
  /* Agora p aponta para o elemento 3. Busque o valor. */
  elt3 = *p;

  p -= 3;
  /* Agora p aponta para o elemento 0. Busque o valor. */
  elt0 = *p;

  printf("elt0 %d  elt3 %d  elt4 %d.\n", elt0, elt3, elt4);
  /* Imprime: elt0 45  elt3 -3  elt4 123456. */
}

Se um valor de ponteiro foi criado somando um inteiro a outro ponteiro, deve ser possível subtrair esses dois ponteiros e obter novamente o inteiro original. Isso também funciona em C:

void
subtract_pointers()
{
  int array[5] = { 45, 29, 104, -3, 123456 };
  int *p0, *p3, *p4;

  int *p = &array[4];
  /* Agora p aponta para o elemento 4 (último). Guarde o valor. */
  p4 = p;

  --p;
  /* Agora p aponta para o elemento 3. Guarde o valor. */
  p3 = p;

  p -= 3;
  /* Agora p aponta para o elemento 0. Guarde o valor. */
  p0 = p;

  printf("%d, %d, %d, %d\n",
         p4 - p0, p0 - p0, p3 - p0, p0 - p3);
  /* Imprime: 4, 0, 3, -3. */
}

A operação de soma não sabe onde começam ou terminam os arrays na memória. Tudo o que ela faz é somar o inteiro (multiplicado pelo tamanho do tipo apontado) ao valor numérico do ponteiro. Quando o ponteiro original e o resultado apontam para dentro do mesmo array, o resultado é bem definido.

Somente programadores/as experientes devem fazer aritmética com ponteiros envolvendo objetos de memória diferentes.

O resultado da subtração entre dois ponteiros tem tipo int ou long se necessário (veja Tipos de Dados Inteiros). A forma limpa de declarar esse tipo é usando o tipo ptrdiff_t, definido no arquivo <stddef.h>.

O C define a subtração de ponteiros de forma a ser consistente com a adição ponteiro + inteiro, de modo que (p3 - p1) + p1 seja igual a p3, como em álgebra comum. A subtração entre ponteiros funciona subtraindo o valor numérico de p1 de p3, e dividindo pelo tamanho do tipo apontado. Os dois ponteiros devem apontar para dentro do mesmo array.

No C padrão, adições e subtrações não são permitidas com void *, já que o tamanho do tipo apontado não está definido nesse caso. O mesmo vale para ponteiros para funções. No entanto, essas operações funcionam no GNU C, e o "tamanho do tipo apontado" é considerado como 1 byte.

Ponteiros para void

O tipo peculiar void *, um ponteiro cujo tipo apontado é void, é usado com frequência em C. Ele representa um ponteiro para “não dizemos o quê”. Por exemplo:

void *numbered_slot_pointer(int);

declara uma função numbered_slot_pointer que recebe um parâmetro inteiro e retorna um ponteiro — mas não dizemos para que tipo de dado ele aponta.

As funções de alocação dinâmica de memória (veja ) usam o tipo void * para se referir a blocos de memória, independentemente do tipo de dado que o programa armazenará nesses blocos.

Com o tipo void *, é possível passar o ponteiro adiante e testar se ele é nulo. No entanto, desreferenciá-lo fornece um valor void, que não pode ser usado (veja O Tipo void). Para desreferenciar o ponteiro, é necessário convertê-lo para algum outro tipo de ponteiro primeiro.

Atribuições convertem automaticamente void * para qualquer outro tipo de ponteiro, desde que o operando à esquerda tenha um tipo de ponteiro. Por exemplo:

{
  int *p;
  /* Converte o valor de retorno para int *. */
  p = numbered_slot_pointer(5);
  …
}

Ao passar um argumento do tipo void * para um parâmetro que exige um tipo de ponteiro específico, também ocorre a conversão. Por exemplo, suponha que a função hack tenha sido declarada com um parâmetro do tipo float *:

/* Declaramos hack assim.
   Assumimos que está definida em outro lugar. */
void hack(float *);
…
/* Agora chamamos hack. */
{
  /* Converte o valor de retorno de numbered_slot_pointer
     para float * para passá-lo para hack. */
  hack(numbered_slot_pointer(5));
  …
}

Também é possível converter para outro tipo de ponteiro usando um cast explícito (veja ), como neste exemplo:

(int *) numbered_slot_pointer(5)

Aqui está um exemplo que decide em tempo de execução para qual tipo de ponteiro converter:

void
extract_int_or_double(void *ptr, bool its_an_int)
{
  if (its_an_int)
    handle_an_int(*(int *)ptr);
  else
    handle_a_double(*(double *)ptr);
}

A expressão *(int *)ptr significa: converter ptr para o tipo int *, depois desreferenciá-lo.

Comparação de Ponteiros

Dois valores de ponteiro são considerados iguais se apontam para a mesma localização, ou se ambos são nulos. Você pode testar isso com os operadores == e !=. Aqui está um exemplo simples:

{
  int i;
  int *p, *q;

  p = &i;
  q = &i;

  if (p == q)
    printf("Isso será impresso.\n");

  if (p != q)
    printf("Isso não será impresso.\n");
}

Comparações de ordenação, como > e >=, operam sobre ponteiros convertendo-os para inteiros sem sinal. O padrão da linguagem C diz que os dois ponteiros devem apontar para dentro do mesmo objeto na memória. No entanto, em sistemas GNU/Linux, essas operações simplesmente comparam os valores numéricos dos ponteiros.

Em princípio, os ponteiros comparados devem ter o mesmo tipo, mas diferenças são permitidas em alguns casos limitados:

  • Se os tipos apontados pelos dois ponteiros forem quase compatíveis (veja ), a comparação é permitida.

  • Se um dos operandos for do tipo void * (veja Ponteiros para void) e o outro for um ponteiro de outro tipo, o operador de comparação converte o ponteiro void * para o tipo do outro operando, para que possam ser comparados.

    • Obs.: Em C padrão, isso não é permitido se o outro tipo for ponteiro para função, mas no GNU C isso funciona.

Os operadores de comparação também permitem comparar o número inteiro 0 com um valor de ponteiro. Nesse caso, o 0 é convertido para um ponteiro nulo do mesmo tipo do outro operando.

Aritmética de Ponteiros em Baixo Nível

O comportamento da aritmética de ponteiros é, teoricamente, definido apenas quando os ponteiros envolvidos apontam para dentro de um mesmo objeto alocado na memória. Mas os operadores de adição e subtração não têm como saber se os ponteiros estão todos dentro de um único objeto — eles não sabem onde os objetos começam ou terminam. Então, o que esses operadores fazem de fato?

Ao somar um ponteiro p com um inteiro i, o compilador trata p como um endereço de memória, que na prática é um valor inteiro — vamos chamá-lo de pint. O valor i é tratado como uma quantidade de elementos do tipo para o qual p aponta. O total de bytes corresponde a i * sizeof(*p). O resultado da soma, como inteiro, é:

Esse valor é então reinterpretado como um ponteiro do mesmo tipo de p.

Se o ponteiro inicial p e o resultado da operação não apontarem para partes do mesmo objeto, a operação não é oficialmente legítima, e programas em C não deveriam fazer isso. Mas você pode fazer mesmo assim — e ela produzirá exatamente o resultado descrito acima.

Em certas situações especiais, isso pode ser útil — mas quem não for experiente deve evitar esse tipo de prática.

Exemplo: função para somar ponteiros em baixo nível

Aqui está uma função que desloca um valor de ponteiro como se ele apontasse para um objeto de tamanho arbitrário, realizando explicitamente esse cálculo:

Essa função realiza o mesmo trabalho que p + i, desde que p tenha o tipo de ponteiro adequado. Ela usa o tipo intptr_t, definido no cabeçalho <stdint.h>. (Na prática, long long funcionaria, mas é mais limpo e seguro usar intptr_t.)

Ponteiros e Arrays

A forma mais limpa de se referir a um elemento de um array é usando array[indice]. Outra forma, mais complicada, de fazer a mesma coisa é obter o endereço desse elemento como um ponteiro, e então desreferenciá-lo:

ou, de forma equivalente:

Isso obtém primeiro um ponteiro para o elemento zero, depois o incrementa com + para apontar para o elemento desejado, e então obtém o valor de lá.

Essa construção com aritmética de ponteiros é, na verdade, a definição dos colchetes em C: a[b] significa, por definição, *(a + b).

Essa definição trata a e b de forma simétrica: um deles deve ser um ponteiro e o outro, um inteiro — não importa qual vem primeiro.

Assim, como a indexação com colchetes é definida em termos de adição e desreferência, ela também é simétrica. Por isso, você pode escrever 3[array], que é equivalente a array[3]. No entanto, seria tolice escrever 3[array], já que isso não tem nenhuma vantagem e pode confundir quem lê o código.

Pode parecer contraditório o fato de que *(a + b) exige um ponteiro, enquanto array[3] usa um valor do tipo array. Por que isso é válido?

Porque o nome de um array, quando usado sozinho em uma expressão (exceto em sizeof), representa um ponteiro para o elemento zero do array. Assim, array + 3 converte implicitamente array para &array[0], e o resultado é um ponteiro para o elemento 3 — equivalente a &array[3].

Como os colchetes são definidos com base nessa adição, array[3] primeiro converte array para um ponteiro. É por isso que funciona usar um array diretamente nessa construção.

pint + i * sizeof(*p)
#include <stdint.h>

void *
ptr_add(void *p, int i, int objsize)
{
  intptr_t p_address = (long) p;
  intptr_t totalsize = i * objsize;
  intptr_t new_address = p_address + totalsize;
  return (void *) new_address;
}
*(&array[0] + indice)
*(array + indice)