Ponteiros e endereços de memória em C

Aprenda como um programa em C dispõe suas variáveis na memória e como os ponteiros permitem trabalhar diretamente com endereços de memória. Obtenha endereços com &, leia e altere valores por meio de ponteiros e passe variáveis para funções por referência. Percorra arranjos e cadeias de caracteres com aritmética de ponteiros, use ponteiros para ponteiros em argumentos de linha de comando e em funções que movem um ponteiro da função chamadora e chame funções por meio de ponteiros para funções, incluindo funções de comparação para o qsort padrão.

BásicoAula 4 de 8 do curso Curso de Programação em C

O que você vai aprender

  1. Visualize como as variáveis de um programa em C ocupam bytes na memória, armazene endereços em variáveis ponteiro e use esses ponteiros para ler e alterar as variáveis para as quais apontam.
    • Visualize como as variáveis são dispostas na memória
      Descreva a memória como bytes numerados, encontre o tamanho e o endereço de variáveis e elementos de arranjos e explique por quanto tempo cada tipo de variável existe.
    • Declare ponteiros e obtenha endereços com &
      Declare variáveis ponteiro do tipo correto, armazene o endereço de uma variável com o operador de endereço e imprima, redirecione ou redefina um ponteiro com segurança.
    • Leia e altere valores por meio de ponteiros
      Use o operador de desreferenciação para ler e escrever no objeto apontado por um ponteiro, rastreie código com diagramas de memória e evite desreferenciar ponteiros inválidos.
  2. Escreva funções que alterem variáveis da função chamadora por meio de parâmetros ponteiro, proteja dados somente de leitura com const e retorne ponteiros que permaneçam válidos.
    • Passe variáveis por referência usando parâmetros ponteiro
      Explique por que as funções em C recebem cópias de seus argumentos e use parâmetros ponteiro para alterar variáveis da função chamadora, trocar valores, retornar vários resultados e ler entradas com scanf.
    • Proteja dados com const e retorne ponteiros com segurança
      Marque dados somente de leitura com const, diferencie um ponteiro para dados constantes de um ponteiro constante e retorne ponteiros que permaneçam válidos após o término da função.
  3. Mova ponteiros por arranjos e cadeias de caracteres, explique como nomes de arranjos se relacionam com ponteiros e mantenha cada ponteiro dentro do arranjo ao qual pertence.
    • Faça operações aritméticas com ponteiros
      Some inteiros a ponteiros, subtraia e compare ponteiros para elementos do mesmo arranjo e saiba quais resultados são válidos.
    • Explique a relação entre arranjos e ponteiros
      Use a conversão de um nome de arranjo em ponteiro, a equivalência entre a[i] e *(a + i) e as diferenças reais entre um arranjo e um ponteiro.
    • Percorra arranjos e cadeias de caracteres com ponteiros
      Escreva laços que percorram arranjos e cadeias terminadas em nulo com ponteiros, use corretamente expressões idiomáticas como *p++ e respeite os limites.
  4. Acompanhe vários níveis de indireção, permita que funções atualizem um ponteiro da função chamadora e use arranjos de ponteiros, como os argumentos de linha de comando.
    • Use ponteiros para ponteiros
      Declare e desreferencie um ponteiro para ponteiro, acompanhe-o em um diagrama de memória e use-o para permitir que uma função altere um ponteiro da função chamadora.
    • Use arranjos de ponteiros e argumentos de linha de comando
      Crie arranjos de ponteiros para cadeias de caracteres, processe argc e argv como ponteiros e diferencie um arranjo de ponteiros de uma matriz bidimensional.
  5. Armazene e chame funções por meio de ponteiros, passe funções para outras funções e ordene dados de qualquer tipo com o qsort padrão e uma função de comparação.
    • Declare ponteiros para funções, atribua valores a eles e chame funções por meio deles
      Escreva e leia declarações de ponteiros para funções, chame funções por meio deles e crie tabelas de funções que escolhem o comportamento durante a execução do programa.
    • Escreva funções de retorno de chamada e ordene com qsort
      Passe funções como argumentos, use ponteiros void para manipular dados de qualquer tipo e escreva funções de comparação corretas para qsort.

Guia de estudo de Ponteiros em C

5 capítulos

1

Memória, endereços e valores apontados

Uma variável tem um valor, ocupa espaço de armazenamento e tem um tempo de vida. Um ponteiro permite acessar essa variável por meio de seu endereço, então ler código agora significa acompanhar tanto o endereço contido em um ponteiro quanto o valor encontrado nele. Comece pela disposição dos bytes e depois acompanhe os efeitos de obter um endereço, redirecionar um ponteiro e escrever por meio dele. Pequenos diagramas de memória tornam essas operações visíveis antes que elas apareçam em funções e laços.

1.1 Bytes, tamanhos e tempos de vida

A memória pode ser visualizada como uma sequência de bytes numerados. Cada número é um endereço. Um objeto como um int ocupa bytes consecutivos, e seu endereço identifica seu primeiro byte. O nome usado no código-fonte permite que o compilador encontre esse objeto; a linguagem não exige que uma etiqueta com o nome seja armazenada ao lado de seu valor.

Meça o armazenamento com sizeof

sizeof informa o tamanho em bytes e produz um valor size_t. Use %zu para imprimi-lo:

int scores[4] = {12, 18, 15, 21};
printf("%zu %zu\n", sizeof scores, sizeof scores[0]);

Com valores int de 4 bytes, isso imprime 16 4. O primeiro resultado mede os quatro elementos; o segundo mede um. sizeof(char) é sempre 1. Uma disposição comum em computadores de mesa de 64 bits usa valores int de 4 bytes, valores double de 8 bytes e ponteiros para objetos de 8 bytes. Outros sistemas podem ser diferentes. Mesmo entre sistemas de 64 bits, long costuma ocupar 8 bytes no Linux e no macOS, mas 4 no Windows.

Tamanho de armazenamento e comprimento de texto respondem a perguntas diferentes. Para char word[20] = "hi";, sizeof word é 20, enquanto strlen(word) é 2. O arranjo ainda contém 20 elementos, embora sua cadeia use apenas duas letras e um terminador. Exceto para arranjos de tamanho variável, esses tamanhos de objetos são fixados em tempo de compilação.

Acompanhe os endereços de um arranjo

Endereços hexadecimais usam os dígitos de 0 a 9 e depois de a a f. O prefixo 0x indica a base 16. Para um arranjo ilustrativo que começa em 0x1000, quatro elementos de 4 bytes começam em:

ElementoPrimeiro byteÚltimo byte
scores[0]0x10000x1003
scores[1]0x10040x1007
scores[2]0x10080x100B
scores[3]0x100C0x100F

0x100C é 4.108 em decimal, doze bytes depois de 0x1000. Os elementos de um arranjo não têm espaços entre si. Variáveis declaradas separadamente não têm ordem nem distância garantidas; endereços reais impressos também podem mudar entre execuções. Um compilador pode manter um valor em um registrador do processador quando não é necessário um local de armazenamento endereçável.

Alinhamento e ordem dos bytes

O alinhamento restringe onde os objetos de um tipo podem começar. Uma disposição comum coloca valores int de 4 bytes em múltiplos de 4 e valores double de 8 bytes em múltiplos de 8. O compilador fornece alinhamento adequado para objetos declarados.

A ordem dos bytes é a ordem dos bytes dentro de um valor com vários bytes. Para um unsigned int de 4 bytes contendo 0x12345678, o armazenamento com o byte menos significativo primeiro apresenta os bytes 78 56 34 12 dos endereços menores aos maiores; com o byte mais significativo primeiro, apresenta 12 34 56 78. Um unsigned char * pode examinar a representação de um objeto byte por byte. A ordem dos bytes não altera a ordem dos índices do arranjo.

Mantenha o objeto existente

Uma variável local comum tem duração de armazenamento automática: seu tempo de vida dura enquanto seu bloco está em execução. Implementações típicas mantêm esses objetos no registro de ativação de uma chamada e reutilizam esse armazenamento posteriormente. Variáveis globais, variáveis static e literais de cadeia de caracteres duram durante toda a execução do programa. Salvar um endereço não prolonga o tempo de vida de seu objeto.

Principais conclusões

  • sizeof mede bytes; strlen conta os caracteres antes do terminador de uma cadeia.
  • Os elementos de um arranjo ocupam armazenamento consecutivo na ordem dos índices.
  • Variáveis separadas não têm endereços relativos garantidos.
  • O alinhamento controla os endereços iniciais adequados; a ordem dos bytes controla a posição dos bytes dentro de um valor.
  • Um endereço salvo só permanece utilizável enquanto seu objeto existe.

1.2 Armazene e exiba um endereço

Um ponteiro armazena um endereço e tem um tipo que descreve o objeto que ele pode acessar. Declarar um int * fornece armazenamento para um ponteiro; isso não cria um int para esse ponteiro usar. Dê ao ponteiro um valor conhecido ao declará-lo.

Obtenha o endereço de um objeto
int count = 42;
int *p = &count;

O operador de endereço unário & obtém o endereço de count. Se count começa no endereço ilustrativo 0x1000, p contém 0x1000, enquanto count contém 42. O ponteiro também é um objeto: &p é o endereço do armazenamento separado que contém esse ponteiro.

Você pode obter o endereço de um elemento de arranjo, como &scores[2]. Uma constante numérica ou o resultado de um cálculo não é um objeto armazenado cujo endereço você possa obter dessa maneira: &42 e &(count + 1) são inválidos. A expressão binária a & b usa o mesmo símbolo para AND bit a bit; o número de operandos indica qual operação está presente.

Leia cada declarador
DeclaraçãoSignificado
int *p, q;Um ponteiro e um int comum
int *p, *q;Dois ponteiros para int
double *r;Um ponteiro para double

Os espaços em branco não alteram nenhum desses tipos. int* p e int *p são equivalentes. Declarar um ponteiro por linha torna seu asterisco mais fácil de perceber. Em computadores de mesa comuns de 64 bits, um ponteiro para objeto ocupa 8 bytes, mas seu próprio tamanho não informa o tamanho do objeto para o qual aponta.

O tipo apontado importa. Para double price = 9.5;, use double *r = &price;. Inicializar um int * com &price produz um diagnóstico de ponteiros incompatíveis. Uma conversão explícita que oculta o diagnóstico não torna válido um acesso como int a esse double: o tipo controla como os bytes armazenados são acessados.

Redirecione um ponteiro
int a = 5, b = 8;
int *p = &a;
p = &b;

A última atribuição substitui o endereço em p. Ela não move nenhum dos inteiros nem copia o valor de um inteiro para o outro. Agora p aponta para b; a continua sendo 5 e b continua sendo 8. Várias variáveis ponteiro também podem armazenar o mesmo endereço.

Use um estado vazio conhecido

Um ponteiro local não inicializado tem um valor indeterminado e às vezes é chamado de ponteiro não inicializado. Não o examine nem o desreferencie. Inicialize-o com o endereço de um objeto válido ou com NULL, disponível em cabeçalhos como <stddef.h> e <stdio.h>:

int *pending = NULL;
if (pending == NULL) {
    pending = &count;
}

Um ponteiro nulo não designa nenhum objeto. !pending é outro teste de nulidade. Esses testes só fazem sentido para ponteiros inicializados; eles não validam um endereço arbitrário nem um ponteiro cujo objeto deixou de existir.

Imprima um ponteiro para objeto
printf("%p\n", (void *)p);

%p espera um void *, então forneça a conversão explícita ao usar um ponteiro para objeto de tipo específico. Sua representação impressa é escolhida pela implementação e costuma ser hexadecimal. %d e %x esperam argumentos inteiros de tipos específicos e não podem substituir %p.

Principais conclusões

  • Inicialize um ponteiro com um endereço válido ou NULL.
  • &count é um endereço; count é o inteiro armazenado.
  • Cada nome de ponteiro precisa de seu próprio * em uma declaração.
  • Os tipos de ponteiro devem ser adequados aos objetos acessados por meio deles.
  • p = &b altera o ponteiro e deixa os inteiros inalterados.
  • Imprima um ponteiro para objeto com %p e um argumento void *.

1.3 Siga um ponteiro para ler ou escrever

O operador de desreferenciação unário * segue um ponteiro até seu objeto. Se p aponta para x, *p designa o próprio x. Ler *p lê esse objeto; atribuir um valor a *p o altera.

Rastreie uma escrita por meio de um endereço
int x = 10;
int *p = &x;
*p = *p + 5;
int y = *p * 2;
printf("%d %d %d\n", x, *p, y);

Primeiro, p armazena o endereço de x. No lado direito da atribuição, *p lê 10. Somar 5 resulta em 15, e o *p do lado esquerdo armazena 15 em x. A linha seguinte multiplica o valor atualizado por 2, então y passa a ser 30. A saída é 15 15 30.

O contexto distingue os asteriscos: int *p declara um ponteiro, o *p unário acessa um objeto e *p * 2 também contém uma multiplicação binária. Para um objeto válido x, *&x designa x; para um ponteiro para um objeto válido, &*p devolve o valor do ponteiro.

Separe endereços de conteúdos

Comece cada rastreamento com duas linhas de informação: o endereço contido em cada ponteiro e o valor contido em cada inteiro. Suponha que p aponte para a, que contém 3, e q aponte para b, que contém 8.

Instrução considerada a partir desse estado inicialEfeito
*p = *q;Copia 8 para a; os ponteiros mantêm seus destinos
p = q;Faz p apontar para b; os inteiros mantêm seus valores

Um diagrama de memória usa uma caixa para cada objeto. Identifique uma caixa com seu nome e endereço, coloque seu valor atual dentro dela e desenhe uma seta de um ponteiro até seu destino. Uma atribuição de endereço move uma seta; uma atribuição ao objeto apontado altera o conteúdo da caixa de destino.

Após p = q, os ponteiros são referências alternativas a b. Se você executar *p = 20; em seguida, ler *q ou b resultará em 20. Copiar um endereço cria outro caminho para o mesmo objeto, não outro inteiro.

Verifique se uma desreferenciação é válida

Um ponteiro usado para acesso deve designar um objeto existente de tipo adequado, e a escrita também exige um objeto modificável. Ponteiros nulos não designam nenhum objeto. Ponteiros não inicializados não têm um destino conhecido utilizável. Um ponteiro fica pendente quando o tempo de vida de seu objeto termina.

Acessar por meio de um ponteiro inválido é comportamento indefinido: C não impõe requisitos ao resultado. O programa pode parar com uma falha de segmentação, corromper dados ou parecer executar normalmente. Uma execução aparentemente bem-sucedida não pode estabelecer que o acesso era válido.

Para um ponteiro que pode legitimamente ser nulo, teste-o antes de acessá-lo:

if (p != NULL) {
    *p = 0;
}

Atribuir NULL a um ponteiro de que você não precisa mais pode evitar o uso acidental posterior dessa variável. Isso não redefine outras referências alternativas. Acompanhe separadamente o tempo de vida do objeto.

Use parênteses em torno do valor desejado

O ++ pós-fixado tem precedência maior que o * unário. Assim, *p++ significa *(p++): usa o valor anterior do ponteiro para acessar um valor e avança o ponteiro. (*p)++ incrementa o valor apontado e deixa o ponteiro no lugar. *p + 1 lê o valor e soma 1 sem armazenar o resultado. Essas são três ações diferentes.

Principais conclusões

  • *p designa o objeto acessado por meio de p.
  • Copiar um endereço cria referências alternativas; copiar *p copia um valor.
  • Uma desreferenciação exige um objeto existente e um tipo de acesso adequado.
  • O comportamento indefinido não garante nenhum resultado, nem mesmo um encerramento inesperado.
  • *p++ avança o ponteiro; (*p)++ incrementa o objeto apontado.
2

Permita que funções trabalhem com dados da função chamadora

Uma função recebe uma cópia de cada argumento, incluindo um argumento ponteiro. Esse endereço copiado ainda pode acessar um objeto pertencente à função chamadora, o que possibilita trocas, funções de entrada e várias saídas. A interface da função também deve mostrar se ela altera os dados. Acompanhe o endereço durante a chamada, diferencie alterações locais de escritas por meio dele e verifique quais objetos ainda existirão quando um ponteiro retornado for usado.

2.1 Altere variáveis da função chamadora por meio de parâmetros

C passa todo argumento por valor. Um parâmetro é uma variável local inicializada a partir do argumento. Passar um endereço não altera essa regra: a função recebe uma cópia do endereço, e essa cópia pode acessar o objeto da função chamadora.

Compare duas funções de redefinição
void reset_copy(int n) {
    n = 0;
}

void reset(int *n) {
    *n = 0;
}

Para int score = 12;, reset_copy(score) altera apenas seu n local. score continua sendo 12. A chamada reset(&score) copia o endereço de score para o parâmetro ponteiro n; atribuir um valor a *n altera score para 0. Essa técnica costuma ser chamada de passagem por referência, embora a passagem efetiva de argumentos da linguagem continue sendo por valor.

O endereço na chamada e a desreferenciação no corpo realizam tarefas diferentes. &score informa à função onde está o objeto. *n = 0 informa o que armazenar nele. Chamar reset(score) fornece um inteiro onde é exigido um ponteiro e gera um diagnóstico do compilador.

Acompanhe uma troca
void swap(int *a, int *b) {
    int t = *a;
    *a = *b;
    *b = t;
}

/* Inside a calling function: */
int x = 3, y = 8;
swap(&x, &y);
  1. a aponta para x e b aponta para y.
  2. t guarda o valor original de x, que é 3.
  3. *a = *b escreve 8 em x.
  4. *b = t escreve o 3 guardado em y.

As duas variáveis da função chamadora mudam. Uma versão que recebe int a, int b troca apenas cópias. Uma versão com ponteiros que executa int *t = a; a = b; b = t; também não troca os inteiros: ela redireciona apenas os parâmetros ponteiro locais.

Forneça mais de um resultado

Uma instrução de retorno fornece um valor, mas parâmetros ponteiro podem fornecer saídas adicionais:

void divide(int total, int size, int *q, int *r) {
    *q = total / size;
    *r = total % size;
}

/* size must be positive for these example inputs. */
int q, r;
divide(17, 5, &q, &r);

A divisão inteira resulta em q igual a 3 e r igual a 2. A função chamadora fornece dois objetos distintos nos quais é possível escrever. Outra interface útil retorna um status de sucesso e escreve uma saída apenas em caso de sucesso; as funções chamadoras verificam esse status antes de usar a saída.

Reconheça o mesmo padrão em chamadas de biblioteca

scanf("%d", &age) precisa do endereço de age porque armazena nele um inteiro extraído da entrada. Passar age fornece um valor em vez do int * exigido, produzindo comportamento indefinido. Verifique se scanf retorna 1 antes de usar o valor recém-lido.

Para texto, char name[20]; scanf("%19s", name); passa um ponteiro para o primeiro caractere do arranjo. A largura permite no máximo 19 caracteres de entrada, deixando espaço para '\0'. A expressão do arranjo já fornece o ponteiro para caractere exigido.

Um parâmetro de arranjo também recebe um endereço, não um bloco copiado de elementos. Em void zero(int *a, size_t n), atribuições a a[i] afetam o arranjo da função chamadora. Passar o endereço copia apenas um ponteiro, o que também permite que funções somente de leitura examinem arranjos grandes sem copiar cada elemento.

Principais conclusões

  • C copia todo argumento, incluindo endereços.
  • Uma escrita por meio de um parâmetro ponteiro pode alterar o objeto da função chamadora.
  • Uma troca de inteiros que funciona troca *a e *b usando um valor guardado.
  • Parâmetros de saída precisam de objetos válidos e modificáveis fornecidos pela função chamadora.
  • scanf recebe endereços e informa quantas conversões foram bem-sucedidas.
  • Parâmetros de arranjo compartilham o acesso aos elementos pertencentes à função chamadora.

2.2 Acesso com const e valores de retorno seguros

Uma interface com ponteiro deve deixar duas coisas claras: se a função pode escrever por meio dele e por quanto tempo o armazenamento apontado permanece válido. const responde à primeira pergunta. O tempo de vida do objeto responde à segunda.

Identifique a restrição de const
DeclaraçãoPode redirecionar p?Pode atribuir por meio de *p?
const int *p = &x;SimNão
int *const p = &x;NãoSim
const int *const p = &x;NãoNão

Um ponteiro para dados constantes restringe esse caminho de acesso. Se x foi definida como em int x = 5;, sem const, x = 6; ainda é permitido mesmo enquanto um const int * aponta para ela. Se o próprio objeto foi definido com const, tentar modificá-lo por meio de qualquer ponteiro é comportamento indefinido.

Um ponteiro constante mantém um único endereço. Ele deve ser inicializado, e o endereço não pode ser substituído posteriormente. O objeto nesse endereço ainda pode mudar quando seu tipo permite. Leia a partir do nome em direção às partes externas: int *const p torna p constante; const int *p protege o int acessado por meio de p.

Use um parâmetro somente de leitura como const char *s quando uma função apenas examinar uma cadeia de caracteres. Um char * comum pode ser passado para esse parâmetro sem conversão explícita. A função pode avançar seu s local, mas uma atribuição por meio de *s é rejeitada. Converter de volta para um tipo que permita escrita exige mais que eliminar um aviso do compilador: o objeto subjacente deve de fato ser modificável.

Retorne uma posição no armazenamento da função chamadora
int *find(int *a, size_t n, int target) {
    for (size_t i = 0; i < n; i++) {
        if (a[i] == target) return &a[i];
    }
    return NULL;
}

Para int v[] = {4, 9, 2, 9};, find(v, 4, 9) retorna &v[1]. A função termina, mas v pertence à função chamadora e continua existindo nela. Após verificar se o resultado é diferente de NULL, a função chamadora pode calcular found - v para obter o índice 1 ou executar *found = 0 para atualizar o elemento. Uma busca por 7 retorna NULL, que não deve ser desreferenciado. strchr e strstr usam a mesma convenção de retornar nulo quando não encontram o resultado.

Rejeite endereços de variáveis locais que deixaram de existir
int *bad(void) {
    int result = 42;
    return &result;
}

result tem duração de armazenamento automática. Seu tempo de vida termina quando a chamada retorna, deixando a função chamadora com um ponteiro pendente. O compilador pode emitir um aviso, e qualquer acesso posterior por meio desse ponteiro tem comportamento indefinido. Os bytes antigos estarem visíveis em uma execução não mantém o objeto existente.

O mesmo problema pode ocorrer na chave de fechamento de um bloco interno. Um ponteiro externo não pode prolongar o tempo de vida de uma variável interna cujo endereço ele salvou.

Escolha armazenamento que dure o suficiente

Retorne um valor int quando a função chamadora precisar apenas de um resultado inteiro. Para vários resultados, permita que a função chamadora forneça o armazenamento a ser preenchido. Um ponteiro retornado também pode se referir ao arranjo da função chamadora, a um literal de cadeia de caracteres exposto por meio de const char * ou a um objeto static.

Uma variável local estática é inicializada uma vez e dura durante todo o programa, mas todas as chamadas a compartilham. Se uma chamada posterior armazenar um novo valor nela, todo ponteiro retornado anteriormente verá essa alteração. O armazenamento estático resolve o problema de tempo de vida sem fornecer a cada chamada seu próprio objeto de resultado independente.

Principais conclusões

  • const int * restringe escritas por meio desse ponteiro, enquanto int *const fixa o endereço.
  • Um objeto definido com const não pode ser modificado de forma válida.
  • Ponteiros para elementos de um arranjo ainda existente da função chamadora podem permanecer válidos após o retorno de uma função.
  • Verifique um resultado de busca possivelmente nulo antes de acessá-lo ou subtraí-lo.
  • Retornar o endereço de uma variável local automática deixa um ponteiro pendente.
  • Um objeto estático de resultado é compartilhado por todas as chamadas.
3

Percorra arranjos e cadeias de caracteres

A aritmética de ponteiros relaciona o endereço de um elemento à sua posição em um arranjo. O tipo apontado fornece o tamanho do passo, enquanto o arranjo fornece a faixa permitida. Use esse modelo para ler índices, passar uma fatia para uma função e criar laços que parem antes de um acesso inválido. Cadeias de caracteres acrescentam uma segunda regra de parada: o caractere nulo de terminação marca o fim do texto, mesmo quando o armazenamento contém mais bytes.

3.1 Passos entre elementos e limites do arranjo

Somar um inteiro a um ponteiro o move por elementos do tipo apontado. Se p aponta para o elemento i de um arranjo, p + n aponta para o elemento i + n, desde que a posição resultante esteja dentro do arranjo ou imediatamente após seu último elemento.

Aplique a escala ao endereço uma vez

No endereço ilustrativo 0x1000, somar 3 fornece estes resultados:

Tipo de ponteiroTamanho de um elementoEndereço após p + 3
int *4 bytes0x100C
double *8 bytes0x1018
char *1 byte0x1003

O compilador aplica a escala. Escrever p + 3 * sizeof *p aplica a escala duas vezes e geralmente chega ao elemento errado. Use unsigned char * quando o passo desejado for realmente um byte da representação de um objeto.

Para int a[6] = {10, 20, 30, 40, 50, 60}; e int *p = &a[1];, *(p + 2) lê a[3], que é 40. A atribuição p += 3; move o próprio ponteiro para a[4], que contém 50. A partir daí, *(p - 1) lê 40. p++, p--, p += n e p -= n usam passos por elementos.

Subtraia posições para contar elementos

&a[5] - &a[1] é 4, e inverter os operandos resulta em -4. O resultado já conta elementos; não o divida por sizeof(int). Seu tipo com sinal é ptrdiff_t, declarado em <stddef.h>, e %td o imprime.

Subtraia apenas ponteiros para posições no mesmo arranjo, incluindo a posição imediatamente após seu último elemento, com um resultado representável em ptrdiff_t. Essa operação descreve posições dentro desse arranjo. Subtrair endereços de variáveis não relacionadas é comportamento indefinido, mesmo que os números impressos por acaso estejam próximos.

Use a posição após o último elemento como limite

Para seis elementos, a + 6 é um ponteiro para a posição imediatamente após o último elemento válido. Ele pode servir como limite e participar de uma subtração válida dentro do mesmo arranjo, mas não pode ser desreferenciado. a + 7 e a - 1 já são comportamento indefinido quando formados; evitar uma desreferenciação posterior não os corrige.

for (int *p = a; p < a + 6; p++) {
    printf("%d\n", *p);
}

O corpo é executado para os índices de 0 a 5. Após o último incremento, p é igual a a + 6, e o teste seguinte interrompe o laço. Usar <= permitiria que o corpo acessasse essa posição que não é um elemento.

Para as regras de aritmética, um objeto isolado funciona como um arranjo de um elemento. Você pode formar &x + 1; não pode usá-lo para ler uma suposta variável seguinte.

Compare apenas posições que façam sentido

Operadores de ordem como < comparam posições dentro de um arranjo. Não ordene ponteiros para objetos não relacionados. Igualdade e desigualdade podem comparar tipos de ponteiro compatíveis e testar se um ponteiro é nulo.

Você não pode somar dois ponteiros, multiplicar ponteiros nem fazer aritmética do C padrão com um void * ou um ponteiro para função. Para encontrar o ponto médio de um intervalo no mesmo arranjo [p, q), use p + (q - p) / 2: subtraia as posições para obter uma contagem, divida essa contagem e avance a partir de p.

Principais conclusões

  • A adição de inteiros move um ponteiro por elementos de seu tipo apontado.
  • Uma diferença entre ponteiros para posições no mesmo arranjo já conta elementos e tem tipo ptrdiff_t.
  • Ponteiros para a posição imediatamente após o último elemento podem marcar um limite, mas não podem ser desreferenciados.
  • Formar um ponteiro para antes do arranjo ou além da posição imediatamente após seu último elemento é inválido.
  • Use posições no mesmo arranjo para comparações de ordem.
  • Subtraia as posições antes de calcular um ponto médio.

3.2 Expressões de arranjo e parâmetros ponteiro

Um arranjo é um bloco de elementos. Um ponteiro é um objeto separado que contém um endereço. A relação próxima entre eles vem de uma conversão automática que ocorre na maioria das expressões, e não de serem o mesmo tipo de objeto.

Converta uma expressão de arranjo
int a[5] = {3, 1, 4, 1, 5};
int *p = a;

Aqui, a é convertido em um ponteiro para a[0]. Essa conversão de arranjo em ponteiro torna o inicializador equivalente a &a[0]. Tanto *p quanto a[0] leem 3.

C define a indexação por meio de aritmética: a[i] significa *(a + i). Portanto, p[3] também funciona e lê 1. &a[i] e a + i identificam o mesmo elemento. Até 3[a] tem o mesmo significado definido que a[3], embora a forma usual seja muito mais clara.

Saiba quando o arranjo inteiro continua sendo um arranjo

A conversão não ocorre para operandos de sizeof ou do & unário. Ela também não ocorre para typeof e typeof_unqual de C23, nem quando um literal de cadeia de caracteres inicializa um arranjo de caracteres.

Expressão, com valores int de 4 bytes e um ponteiro de 8 bytesResultado
sizeof a20 bytes para os cinco elementos
sizeof p8 bytes para o objeto ponteiro
sizeof a / sizeof a[0]5 elementos

Você não pode atribuir um novo endereço a um arranjo nem incrementar seu nome: a = p; e a++; são inválidos. Você pode redirecionar p, por exemplo, com p = a + 2;. Copie o conteúdo de um arranjo com um laço sobre os elementos ou uma chamada adequada a memcpy.

Acompanhe o tipo de parâmetro ajustado
int sum(const int *values, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += values[i];
    return total;
}

A função chamadora fornece um endereço e uma contagem. Para o arranjo acima, sum(a + 2, 3) visita os índices originais 2, 3 e 4, somando 4, 1 e 5 para retornar 10. Dentro da função, o índice 0 é relativo ao ponteiro inicial fornecido.

Em uma lista de parâmetros de função, int a[], int a[10] e int *a declaram um parâmetro ponteiro. A forma simples [10] não fornece um limite verificado nem transfere um comprimento. Dentro dessa função, sizeof a mede um ponteiro. A expressão sizeof a / sizeof a[0] resultaria em 2 em um sistema com ponteiros de 8 bytes e valores int de 4 bytes, independentemente do comprimento real do arranjo da função chamadora.

Diferencie um ponteiro para elemento de um ponteiro para arranjo

a é convertido em int *, enquanto &a tem tipo int (*)[5], um ponteiro para o arranjo inteiro de cinco elementos. Ambos identificam o início do mesmo armazenamento, mas a aritmética segue seus tipos diferentes. a + 1 avança um int, ou 4 bytes nessa disposição. &a + 1 avança um arranjo inteiro, ou 20 bytes, até a posição imediatamente após seu último elemento.

Um endereço inicial impresso não revela essa diferença de tipo. Leia a declaração e mantenha o tipo apontado junto ao ponteiro ao rastrear uma expressão.

Principais conclusões

  • A maioria das expressões de arranjo é convertida em um ponteiro para o primeiro elemento.
  • a[i] é definido como *(a + i).
  • sizeof a mede o arranjo inteiro apenas onde a realmente é um arranjo.
  • Um parâmetro de arranjo é ajustado para um ponteiro; passe seu comprimento utilizável separadamente.
  • &a aponta para um arranjo inteiro, então sua aritmética tem um passo maior.

3.3 Laços com ponteiros sobre números e texto

Um laço com ponteiro precisa de uma posição atual e de uma regra de parada. Um arranjo com quantidade conhecida usa um ponteiro para o fim; uma cadeia de caracteres usa seu '\0' de terminação. Escolha a forma que torne os elementos visitados mais fáceis de visualizar.

Percorra um intervalo com quantidade conhecida
int a[] = {3, 1, 4, 1, 5};
int total = 0;
for (const int *p = a; p < a + 5; p++) {
    total += *p;
}

Os totais acumulados são 3, 4, 8, 9 e 14. Um laço com índice que soma a[i] para índices menores que 5 fornece o mesmo resultado. Compiladores modernos com otimização costumam gerar código semelhante para essas formas; a sintaxe com ponteiros, por si só, não garante vantagem de velocidade.

Leia antes ou depois de mover

Com p no 3 de {3, 1, 4}, int v = *p++; define v como 3 e deixa p no 1. O incremento pós-fixado fornece o valor anterior do ponteiro a * e atualiza o ponteiro. *++p primeiro move para o próximo elemento e depois o lê. (*p)++ altera o elemento atual e não move o ponteiro.

Pare no terminador de uma cadeia de caracteres
size_t my_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') p++;
    return (size_t)(p - s);
}

Para "pointer", p para no terminador, sete posições a partir do início. O resultado exclui esse terminador. Uma contagem de caracteres usa o mesmo percurso: examinar "mississippi" e incrementar um contador sempre que *p == 's' conta quatro ocorrências entre onze letras.

Um ponteiro para o meio de uma cadeia de caracteres dá acesso a seu sufixo. Com const char *msg = "hello world";, puts(msg + 6) imprime "world". Os caracteres anteriores permanecem onde estavam.

Copie também o terminador
char destination[4];
const char *src = "cat";
char *dst = destination;
while ((*dst++ = *src++) != '\0') {
}

Cada iteração atribui um caractere, testa o valor atribuído e avança ambos os ponteiros. A última iteração copia '\0', então o destino é uma cadeia de caracteres completa. Ambos os ponteiros ficam imediatamente após seus terminadores. O armazenamento precisa do comprimento do texto mais um byte; omitir esse byte extra causa um estouro de buffer. Esse padrão de cópia também exige que as regiões de origem e destino não se sobreponham.

Inverta a partir das duas extremidades

Para uma cadeia de caracteres modificável, obtenha primeiro seu comprimento. Se o comprimento for zero, pare antes de formar um ponteiro para o último caractere. Caso contrário, defina left = s e right = s + length - 1. Enquanto left < right, troque os caracteres, incremente left e decremente right. Para "stressed", quatro trocas produzem "desserts"; o terminador permanece no fim original.

A verificação de cadeia vazia importa porque subtrair 1 de seu ponteiro inicial formaria um ponteiro para antes do arranjo.

Escolha armazenamento modificável para caracteres

char word[] = "hello"; cria um arranjo modificável de seis bytes, incluindo o terminador. const char *word = "hello"; aponta para um literal de cadeia de caracteres com duração de armazenamento estática. Modificar um literal de cadeia de caracteres é comportamento indefinido, mesmo que uma declaração use char * sem const. Use const char * para que escritas acidentais sejam um erro de compilação.

Um laço sobre um arranjo deve parar antes da posição imediatamente após seu último elemento, e um percurso de cadeia de caracteres deve realmente encontrar um terminador dentro do armazenamento acessível. Um ponteiro não carrega um comprimento nem uma garantia de que os bytes formam uma cadeia de caracteres.

Principais conclusões

  • Um intervalo com quantidade conhecida termina em um ponteiro para a posição imediatamente após o último elemento; uma cadeia termina em seu caractere nulo.
  • *p++ usa a posição anterior e avança o ponteiro.
  • A cópia de uma cadeia inclui o terminador e precisa do comprimento mais um byte.
  • Verifique se a cadeia está vazia antes de formar seu ponteiro para o último caractere.
  • Arranjos de caracteres inicializados a partir de texto são modificáveis; literais de cadeia de caracteres não são.
  • Um ponteiro sozinho não carrega um comprimento nem uma garantia de terminação.
4

Siga e atualize outro ponteiro

Um ponteiro pode ser, ele próprio, o objeto acessado por meio de um endereço. Seguir dois níveis permite que uma função substitua o ponteiro da função chamadora e que um programa percorra um arranjo de endereços de cadeias de caracteres. Mantenha visível o tipo de cada nível: um ponteiro para um inteiro, um ponteiro para esse ponteiro e um ponteiro para uma linha inteira representam disposições diferentes. Acompanhe o objeto alcançado por cada desreferenciação antes de decidir o que uma atribuição alterará.

4.1 Dois níveis de indireção

Um ponteiro é um objeto armazenado, então tem um endereço que outro ponteiro pode conter. Um ponteiro para ponteiro acrescenta mais um passo entre a expressão e os dados finais.

Siga as setas uma de cada vez
int x = 7;
int *p = &x;
int **pp = &p;

Use endereços ilustrativos: x em 0x2000 contém 7; p em 0x3000 contém 0x2000; pp em 0x4000 contém 0x3000. Avaliar *pp alcança p, cujo valor tem tipo int *. Avaliar **pp segue esse ponteiro até x, um int.

ExpressãoTipoObjeto ou valor alcançado
ppint **Endereço de p
*ppint *O ponteiro armazenado em p
**ppintO inteiro armazenado em x

&p tem um nível de ponteiro a mais que p; cada desreferenciação remove um nível. Inicializar int **pp com &x é incompatível porque &x tem tipo int *.

Escolha o nível da atribuição

Com outro inteiro existente y, **pp = 9; altera x. A atribuição *pp = &y; altera o ponteiro p, então usos futuros de **pp alcançam y. Ela deixa x com seu valor atual. Se q for outro int *, pp = &q; altera apenas o ponteiro externo, fazendo sua primeira seta alcançar q em vez de p.

Não conte os asteriscos sem verificar também o objeto em cada nível. Antes de avaliar **pp, tanto o caminho até o objeto ponteiro quanto o caminho desse objeto até o inteiro devem ser válidos.

Mova um ponteiro para cadeia da função chamadora
void skip_spaces(const char **s) {
    while (**s == ' ') {
        (*s)++;
    }
}

/* In the caller: */
const char *text = "   hi";
skip_spaces(&text);
puts(text);

A função chamadora passa o endereço de sua variável ponteiro. Portanto, *s designa text; **s lê o caractere atual. Cada (*s)++ avança text em um caractere. Após três iterações, ele aponta para 'h', e puts imprime "hi". A função exige um ponteiro válido para um ponteiro que aponta para uma cadeia terminada em nulo.

Uma versão que recebe const char *s e incrementa s altera apenas uma cópia local. Os parênteses em (*s)++ também são essenciais: *s++ incrementa o ponteiro local externo em vez do ponteiro para caractere da função chamadora. Em const char **s, os caracteres são somente de leitura por esse caminho de acesso, mas o ponteiro da função chamadora pode se mover.

Leia um ponteiro de saída de uma função de biblioteca

strtol de <stdlib.h> usa um parâmetro de saída do tipo char **:

char *end;
long number = strtol("42abc", &end, 10);

Ela retorna 42 e armazena em end um ponteiro para o primeiro caractere não usado, que agora alcança "abc". Se o ponteiro de fim for igual ao ponteiro inicial, nenhum dígito foi convertido. Um analisador numérico com verificação completa também trata erros de faixa; aqui, a função do ponteiro é informar a posição de parada.

Principais conclusões

  • int **pp pode conter o endereço de um objeto int *.
  • Cada desreferenciação segue um nível de ponteiro.
  • **pp altera o inteiro final; *pp pode alterar o ponteiro intermediário.
  • Atualizar um ponteiro da função chamadora exige o endereço desse ponteiro.
  • strtol usa char ** para retornar a posição do primeiro caractere não usado.

4.2 Arranjos de ponteiros para cadeias e matrizes retangulares

Um arranjo pode armazenar ponteiros como elementos. Uma matriz bidimensional, por sua vez, armazena linhas completas como elementos. Ambos aceitam dois índices, mas seu armazenamento e os tipos dos ponteiros resultantes da conversão diferem.

Armazene endereços de cadeias separadas
const char *days[] = {"Mon", "Tue", "Wed"};

days contém três ponteiros para caracteres. Cada um aponta para um literal de cadeia de caracteres separado. days[1] é o ponteiro para "Tue", e days[1][0] lê 'T'. Com ponteiros para objetos de 8 bytes, sizeof days é 24, independentemente dos comprimentos das cadeias. As próprias cadeias ocupam armazenamento em outro lugar.

Os comprimentos podem ser diferentes, formando uma coleção irregular. Reordenar a lista pode trocar elementos ponteiro sem copiar o texto. Atribuir days[0] = "Thursday"; é permitido: o elemento ponteiro pode mudar, embora os caracteres acessados por ele sejam somente de leitura.

Na maioria das expressões, days é convertido em um ponteiro para seu primeiro elemento. Como um elemento é um const char *, o tipo convertido é const char **:

for (const char **d = days; d < days + 3; d++) {
    puts(*d);
}

O laço avança um elemento ponteiro por iteração. *d recupera o ponteiro para caractere de que puts precisa.

Leia cadeias da linha de comando

int main(int argc, char *argv[]) recebe o mesmo tipo de parâmetro que char **argv. argc não é negativo e conta as cadeias disponíveis. Quando é positivo, argv[0] representa o nome do programa ou uma cadeia vazia se esse nome não estiver disponível. Os argumentos do usuário ocupam os índices de 1 até argc - 1, e argv[argc] é um ponteiro nulo.

Para uma chamada típica ./greet Ada 42, argc é 3, argv[1] aponta para "Ada" e argv[2] aponta para "42". O último argumento é texto até que uma conversão como strtol o leia. argv[1][0] é 'A'. Verifique argc > 2 antes de usar argv[2].

Um laço que imprime os argumentos do usuário pode usar for (int i = 1; i < argc; i++) puts(argv[i]);. Para percorrer a partir de argv + 1 até um ponteiro nulo, verifique primeiro argc > 0; se argc for zero, não há um intervalo de argumentos do usuário que possa ser percorrido dessa maneira. As cadeias de caracteres dos argumentos são modificáveis, ao contrário dos literais de cadeia.

Percorra linhas completas

int grid[3][4] contém doze inteiros armazenados por linhas: toda a linha 0, depois a linha 1 e depois a linha 2. Com valores int de 4 bytes, cada linha ocupa 16 bytes. grid[2][1] fica 36 bytes após grid[0][0]: duas linhas completas contribuem com 32 bytes e a coluna contribui com 4.

Declaração ou expressãoSignificado
int (*rows)[4]Ponteiro para uma linha de quatro inteiros
int *ptrs[4]Arranjo de quatro ponteiros para inteiros
grid convertidoint (*)[4]

grid + 1 avança uma linha. Uma função que o recebe pode usar int g[][4] ou int (*g)[4], com uma contagem de linhas separada. Ela não pode usar int **: a matriz não tem ponteiros para linhas armazenados para desreferenciar. Uma conversão explícita não pode criar esses ponteiros ausentes.

Da mesma forma, char names[3][6] contém dezoito posições para caracteres em linhas de tamanho fixo. const char *names[3] contém três posições para ponteiros que podem se referir a cadeias de comprimentos diferentes.

Principais conclusões

  • Um arranjo de ponteiros para cadeias armazena endereços, com o texto em outro lugar.
  • Um arranjo de ponteiros é convertido em um ponteiro para seu primeiro elemento ponteiro.
  • argv[argc] é nulo; verifique a contagem de argumentos antes de ler uma cadeia específica.
  • Uma matriz bidimensional armazena linhas contíguas e não contém objetos ponteiro para linhas.
  • int (*)[4] e int ** descrevem disposições de armazenamento diferentes.
  • Dois índices, por si só, não estabelecem que dois objetos tenham o mesmo tipo.
5

Escolha funções e forneça funções de retorno de chamada

Um ponteiro para função permite que um programa escolha uma operação durante a execução. Seu tipo registra a chamada permitida, e um parâmetro de função de retorno de chamada permite que outra função solicite essa operação quando necessário. Use essas ideias primeiro em uma pequena tabela de funções aritméticas e depois em qsort, onde a função de retorno de chamada deve interpretar corretamente os endereços dos elementos e retornar uma ordem consistente. Ordenar ponteiros para cadeias reúne ponteiros para funções e dois níveis de ponteiros para objetos.

5.1 Declarações e chamadas de ponteiros para funções

Um ponteiro para função armazena uma referência a uma função que pode ser chamada posteriormente. Seu tipo inclui o tipo de retorno e os tipos dos parâmetros. Esses tipos devem corresponder à função usada por meio do ponteiro.

Coloque parênteses em torno do nome do ponteiro
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int (*op)(int, int) = add;

Leia a partir de op: ele é um ponteiro, o elemento apontado é uma função que recebe dois valores int, e essa função retorna um int. Sem os parênteses, int *op(int, int); declara uma função que retorna um ponteiro para inteiro. Os parênteses mudam o significado da declaração.

O nome de uma função é convertido em ponteiro para função nesse inicializador. op = &add; é equivalente a op = add;. Tanto op(6, 3) quanto (*op)(6, 3) chamam add e retornam 9. Atribuir op = sub; faz a mesma chamada retornar 3. Nada na expressão de chamada mudou, exceto a função armazenada no ponteiro.

Mantenha a assinatura compatível

Um ponteiro do tipo int (*)(int, int) não pode chamar com segurança uma função que recebe e retorna double. Por exemplo, atribuir sqrt a ele produz um diagnóstico de tipo incompatível. Converter explicitamente o ponteiro para função para silenciar um diagnóstico não torna válida uma chamada incompatível; o comportamento é indefinido.

O C padrão trata separadamente ponteiros para objetos e ponteiros para funções. void * é o tipo de ponteiro genérico para objeto, não um recipiente portável para um ponteiro para função. Ponteiros para funções permitem testes de igualdade e desigualdade, mas não aritmética de ponteiros.

Um ponteiro para função nulo significa que não há função que possa ser chamada. Quando uma interface permite esse estado, verifique-o:

if (op != NULL) {
    int result = op(6, 3);
    printf("%d\n", result);
}

Chamar uma função por meio de um ponteiro para função nulo é comportamento indefinido.

Nomeie o tipo uma vez
typedef int (*binop)(int, int);
binop ops[] = {add, sub, mul};

O typedef nomeia um tipo de ponteiro. binop selected = mul; declara um ponteiro; binop pick(char symbol); declara uma função que retorna um ponteiro desse tipo. Sem o typedef, essa declaração de retorno seria int (*pick(char symbol))(int, int);.

Um arranjo de ponteiros para funções é uma tabela de despacho. ops[2](6, 3) seleciona mul e retorna 18. Para um índice de seleção com sinal i, verifique i >= 0 && i < 3 antes de avaliar ops[i]. Acessar um índice fora da tabela é inválido, mesmo que todas as funções armazenadas possam ser chamadas.

Leia declarações maiores a partir do nome

Comece pelo nome e resolva os parênteses e as partes pós-fixadas antes de avançar para as partes externas. Em int (*table[3])(int, int), table[3] cria um arranjo de três elementos, * torna cada elemento um ponteiro e (int, int) torna seu destino uma função que retorna int.

Em char *(*f)(const char *), f é um ponteiro para uma função que recebe um const char *; essa função retorna um char *. Mantenha os requisitos de tempo de vida do ponteiro retornado separados dos requisitos de chamada compatível do ponteiro para função.

Principais conclusões

  • Os parênteses distinguem um ponteiro para função de uma função que retorna um ponteiro.
  • Um ponteiro para função registra os tipos de retorno e de parâmetros das chamadas válidas.
  • O nome de uma função pode inicializar um ponteiro para função compatível sem &.
  • Um typedef pode tornar tabelas de ponteiros para funções e tipos de retorno mais fáceis de ler.
  • Valide o índice da tabela e qualquer ponteiro que possa ser nulo antes da chamada.

5.2 Funções de retorno de chamada e comparações corretas para qsort

Uma função de retorno de chamada é uma função fornecida a outra função, que a chama como parte de seu trabalho. Isso separa a operação geral de percurso ou ordenação da ação ou ordem específica que você escolhe.

Passe uma operação sem chamá-la
void twice(int *x) { *x *= 2; }

void for_each(int *a, size_t n, void (*fn)(int *)) {
    for (size_t i = 0; i < n; i++) fn(&a[i]);
}

Para int v[] = {1, 2, 3};, for_each(v, 3, twice); produz {2, 4, 6}. O nome da função fornece um ponteiro. Escrever twice() tentaria chamá-la imediatamente, sem o argumento exigido, em vez de fornecer uma operação para chamadas posteriores.

Recupere um tipo a partir de um ponteiro genérico para objeto

Um void * pode conter um ponteiro para objeto e ser convertido de volta ao tipo original. Ele não pode ser desreferenciado nem usado em aritmética de ponteiros do C padrão porque não fornece um tipo de elemento. Preserve os qualificadores: dados somente de leitura passam por const void * e depois por um ponteiro adequado para dados constantes.

qsort de <stdlib.h> recebe um endereço base, uma contagem de elementos, um tamanho de elemento em bytes e uma função de comparação. A função de retorno de chamada tem tipo int (*)(const void *, const void *). Cada argumento aponta para um elemento do arranjo, e o sinal retornado indica onde o primeiro elemento deve ficar:

Sinal retornadoOrdem
NegativoPrimeiro antes do segundo
ZeroEquivalentes para essa ordenação
PositivoPrimeiro depois do segundo
Ordene inteiros sem estouro
int cmp_int(const void *a, const void *b) {
    int x = *(const int *)a;
    int y = *(const int *)b;
    return (x > y) - (x < y);
}

/* In the caller: */
int v[] = {42, -7, 19, 0, 3};
qsort(v, 5, sizeof v[0], cmp_int);

O arranjo torna-se {-7, 0, 3, 19, 42}. As comparações produzem 0 ou 1, então a expressão de retorno produz apenas -1, 0 ou 1. Inverter a expressão de ordenação para (y > x) - (y < x) ordena em ordem decrescente.

Não use return x - y; para inteiros com sinal arbitrários. Para int de 32 bits, subtrair -2.000.000.000 de 2.000.000.000 ultrapassa INT_MAX. O estouro de inteiro com sinal tem comportamento indefinido, então não há garantia de um resultado com redução modular nem de um sinal de comparação confiável.

Ordene ponteiros para cadeias de caracteres
int cmp_str(const void *a, const void *b) {
    return strcmp(*(const char *const *)a,
                  *(const char *const *)b);
}

const char *names[] = {"Lin", "Ada", "Grace"};
qsort(names, 3, sizeof names[0], cmp_str);

Inclua <string.h> para strcmp. Um elemento do arranjo aqui é, ele próprio, um ponteiro. A função de retorno de chamada recebe um endereço desse elemento ponteiro, então desreferenciar uma vez recupera o endereço da cadeia. O const interno acrescentado impede a alteração do elemento por esse caminho de acesso. Após a ordenação, a ordem é "Ada", "Grace", "Lin".

strcmp(a, b) leria os bytes das posições dos ponteiros como texto. Use também sizeof names[0], o tamanho de um elemento ponteiro, não sizeof(char) nem o comprimento de um nome.

As funções de comparação devem deixar os elementos inalterados e responder de modo consistente, incluindo uma ordenação transitiva. qsort não promete preservar a ordem dos elementos considerados iguais na comparação. bsearch aceita o mesmo formato de função de comparação para buscar em um arranjo já ordenado segundo essa ordem.

Principais conclusões

  • Passe o nome de uma função para fornecer uma função de retorno de chamada; parênteses chamam uma função imediatamente.
  • Recupere o tipo do elemento de cada argumento genérico de comparação antes do acesso.
  • qsort precisa de uma contagem, do tamanho de um elemento e de uma função de comparação compatível.
  • Use sinais de comparação sem uma subtração que possa causar estouro.
  • Funções de comparação de ponteiros para cadeias recebem endereços de posições de ponteiros.
  • Resultados de comparação iguais não garantem uma ordem estável após qsort.

Exercícios resolvidos de Ponteiros em C

5 Lições Interativas

1

Acompanhe um endereço e o valor que ele alcança

13 passos
Construa uma representação da memória, redirecione ponteiros e preveja qual objeto cada atribuição altera.
  1. 1
    IntroduçãoMantenha três coisas separadas

    Um inteiro armazenado tem um valor, uma localização e um tempo de vida. Um ponteiro acrescenta outro objeto cujo valor é um endereço. Acompanhe um pequeno programa registrando ambos os tipos de valores: inteiros como 10 e endereços como 0x1000.

    A pergunta útil em cada linha é concreta: essa instrução altera um endereço, altera o objeto alcançado por esse endereço ou apenas lê um valor?

  2. 2
    ExplicaçãoDisponha quatro inteiros

    Imagine int scores[4] começando em 0x1000 em um sistema com inteiros de 4 bytes. O primeiro elemento usa os bytes de 0x1000 a 0x1003. O seguinte começa em 0x1004, seguido por 0x1008 e 0x100C.

    Os elementos ocupam armazenamento consecutivo. Essa regra de disposição pertence ao arranjo; ela não garante uma ordem para dois inteiros declarados separadamente.

  3. 3
    ExemploMeça o arranjo e depois seu conteúdo

    Para char word[20] = "hi";, duas medições fornecem resultados diferentes:

    • sizeof word é 20 bytes de armazenamento do arranjo.
    • strlen(word) é 2 caracteres antes de '\0'.

    O terminador ocupa outro elemento do arranjo, e os elementos restantes continuam pertencendo ao arranjo. sizeof retorna size_t, impresso com %zu; ele não conta quantos elementos o programa usa atualmente.

  4. 4
    ConexãoSepare disposição de tempo de vida

    O alinhamento controla os endereços iniciais adequados, enquanto a ordem dos bytes controla a ordem dos bytes dentro de um valor. Nenhum dos dois informa por quanto tempo um objeto existe.

    Uma variável local comum dura enquanto seu bloco é executado. Uma variável local estática ou global dura durante toda a execução do programa. Salvar um endereço registra um caminho para o armazenamento; isso não pode manter uma variável local automática existente após o término do bloco.

  5. 5
    DesafioEncontre o endereço de um elemento

    Um arranjo de inteiros de seis elementos começa em 0x1800, com 4 bytes por elemento. Onde começa o elemento 3 e qual é o tamanho do arranjo inteiro?

    ---reveal---

    O elemento 3 começa doze bytes após o início, em 0x180C. Seis elementos de 4 bytes ocupam 24 bytes. O índice fornece um deslocamento em elementos; multiplicá-lo pelo tamanho do elemento fornece o deslocamento em bytes.

  6. 6
    ExplicaçãoDê um destino ao ponteiro

    Considere int count = 42; int *p = &count;. O operador & obtém o endereço de count, e p armazena esse endereço. Se count está no endereço ilustrativo 0x2000, o valor do ponteiro é 0x2000; o valor do inteiro continua sendo 42.

    O ponteiro também ocupa seu próprio armazenamento. Seu endereço é &p, uma informação diferente do endereço contido em p.

  7. 7
    DicaLeia o asterisco ao lado de cada nome

    int *p, q; declara um ponteiro e um inteiro comum. Para declarar dois ponteiros, use int *p, *q; ou coloque as declarações em linhas separadas.

    Inicialize um ponteiro com o endereço de um objeto adequado ou com NULL. Um ponteiro nulo é um estado vazio conhecido; um ponteiro local não inicializado tem valor indeterminado e não pode ser testado com segurança como se já fosse nulo.

  8. 8
    DesafioRedirecione sem alterar os dados

    Comece com int a = 9, b = 16; int *p = &a;. Depois, execute p = &b;. Quais valores inteiros mudam e o que p contém agora?

    ---reveal---

    Nenhum dos inteiros muda: a continua sendo 9 e b continua sendo 16. Apenas p muda, armazenando o endereço de b. A atribuição de ponteiro substitui um endereço; ela não copia o conteúdo do objeto apontado anteriormente nem do novo.

  9. 9
    ExemploLeia, some e escreva por meio de p

    Continue com int x = 10; int *p = &x; e execute:

    *p = *p + 5;
    

    O *p do lado direito lê 10 de x. Somar 5 resulta em 15. O *p do lado esquerdo designa o mesmo objeto, então a atribuição escreve 15 em x. O endereço em p não mudou. Em seguida, int y = *p * 2; lê 15 e cria um inteiro separado y contendo 30.

  10. 10
    Ideia-chaveUma cópia do endereço cria outro caminho

    Para int n = 6; int *p = &n; int *q = p;, ambos os ponteiros alcançam n. A atribuição copiou um endereço, não o objeto inteiro.

    Após *q = 14;, ler n, *p ou *q resulta em 14. Desenhe uma caixa para o inteiro e duas setas apontando para ela. Criar duas caixas para inteiros sugeriria incorretamente dados independentes.

  11. 11
    DicaVerifique o objeto apontado antes do acesso

    O acesso exige um ponteiro para um objeto existente de tipo adequado. Um ponteiro nulo não designa nenhum objeto, e um ponteiro pendente perdeu seu objeto existente. Um teste de nulidade trata o estado vazio possível de um ponteiro inicializado; ele não estabelece que um objeto apontado por um ponteiro não nulo ainda exista.

    Um acesso inválido tem comportamento indefinido. C não promete um valor específico nem um encerramento inesperado, mesmo quando uma execução parece normal.

  12. 12
    DesafioSiga um endereço copiado

    Quais são os valores finais de a e b?

    int a = 6, b = 19;
    int *p = &a, *q = p;
    p = &b;
    *q = *p + 1;
    

    ---reveal---

    a passa a ser 20 e b continua sendo 19. Redirecionar p deixa q apontando para a. A última linha lê b por meio de p e escreve a soma em a por meio de q.

  13. 13
    RecapitulaçãoAcompanhe o objeto designado pelo lado esquerdo

    Leia o lado esquerdo antes de prever uma alteração. Atribuir um valor a p altera um endereço; atribuir um valor a *p altera o objeto apontado. Mantenha ponteiros copiados como setas separadas, verifique o tempo de vida do objeto apontado e use o tipo apontado para interpretar cada acesso.

2

Faça uma função alterar dados da função chamadora

11 passos
Acompanhe uma troca, use parâmetros de saída e determine se os ponteiros retornados permanecem válidos.

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

  1. 1
    IntroduçãoAcompanhe o endereço durante uma chamada
  2. 2
    ExplicaçãoUm endereço copiado ainda alcança a função chamadora
  3. 3
    ExemploGuarde o valor antes de substituí-lo
  4. 4
    ExemploEscreva o segundo valor na primeira posição
  5. 5
    ExemploConclua a troca
  6. 6
    DesafioForneça duas saídas
  7. 7
    ExplicaçãoColoque const no nível desejado
  8. 8
    ExemploRetorne uma posição válida
  9. 9
    DesafioExplique a falha de tempo de vida
  10. 10
    DesafioEscolha uma interface de busca que permita escrita
  11. 11
    RecapitulaçãoVerifique a escrita e o tempo de vida
3

Percorra um arranjo sem ultrapassar seu limite

15 passos
Relacione aritmética, conversão de arranjo e laços sobre cadeias de caracteres acompanhando cada posição visitada.

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

  1. 1
    IntroduçãoEncontre o início, o passo e a parada
  2. 2
    ExplicaçãoMova-se por elementos
  3. 3
    ExemploSubtraia duas posições
  4. 4
    Ideia-chaveUse um limite que não é um elemento
  5. 5
    DesafioLocalize o ponto médio
  6. 6
    ExplicaçãoUma expressão de arranjo fornece um ponteiro inicial
  7. 7
    ExemploUse sizeof no contexto correto
  8. 8
    DesafioPasse um intervalo menor
  9. 9
    ExemploLeia e avance, ou avance e leia
  10. 10
    ExplicaçãoEncontre o fim de uma cadeia de caracteres
  11. 11
    ExemploCopie um caractere por vez
  12. 12
    DicaReserve a posição do terminador
  13. 13
    DesafioCorrija a última visita do laço
  14. 14
    DesafioEscolha uma preparação segura para a inversão
  15. 15
    RecapitulaçãoTorne explícita cada posição visitada
4

Altere um ponteiro e leia um arranjo de ponteiros

12 passos
Acompanhe dois níveis de indireção e depois diferencie posições de ponteiros para cadeias de linhas completas de arranjos.

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

  1. 1
    IntroduçãoTorne o próprio ponteiro um objeto apontado
  2. 2
    ExemploDesenhe duas setas
  3. 3
    ExplicaçãoEscolha qual nível muda
  4. 4
    ExemploPasse por endereço a posição da função chamadora
  5. 5
    DesafioPreveja uma alteração apenas no ponteiro
  6. 6
    ConexãoLeia a posição de parada de uma conversão
  7. 7
    ExplicaçãoUm arranjo pode conter posições para ponteiros
  8. 8
    ExemploAvance pelos endereços
  9. 9
    DicaVerifique a contagem de argumentos antes de acessar cadeias
  10. 10
    DesafioLeia o tipo de uma linha inteira
  11. 11
    DesafioAjuste uma função ao armazenamento fornecido
  12. 12
    RecapitulaçãoNomeie o objeto em cada nível
5

Escolha uma operação e ordene com uma função de retorno de chamada

15 passos
Leia tipos de ponteiros para funções, selecione uma operação de uma tabela e crie funções de comparação para inteiros e ponteiros para cadeias.

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

  1. 1
    IntroduçãoSepare uma tarefa geral de sua operação escolhida
  2. 2
    ExplicaçãoLeia o tipo da função que pode ser chamada
  3. 3
    ExemploSelecione outra função
  4. 4
    DicaNomeie um tipo de ponteiro para uma tabela
  5. 5
    DesafioLeia uma declaração relacionada
  6. 6
    ExplicaçãoForneça uma função de retorno de chamada para uma ação
  7. 7
    ExemploForneça os quatro argumentos da ordenação
  8. 8
    ExplicaçãoRecupere o tipo inteiro do elemento
  9. 9
    ExemploRetorne o sinal da ordenação
  10. 10
    DesafioEvite um atalho que cause estouro
  11. 11
    ConexãoOrdene ponteiros para cadeias, não linhas de caracteres
  12. 12
    DesafioEscolha o tamanho correto para a ordenação
  13. 13
    Ideia-chaveIgualdade não garante estabilidade
  14. 14
    DesafioAcompanhe uma função de comparação selecionada
  15. 15
    RecapitulaçãoCombine chamada, disposição e ordenação

Questões de Ponteiros em C

40 Questões de Prática em 4 conjuntos

Conjunto 1: Acompanhe endereços e chamadas de funções10 Questões de Prática

Questão 1 de 10

Dado int x = 6, y = 17; const int *p = &x;, qual atribuição é permitida?
  1. *p = y;
  2. (*p)++;
  3. p = &y;
  4. *p += 1;
Mostrar respostaOcultar resposta
Resposta: C
p é um ponteiro para dados inteiros constantes, então pode ser redirecionado para y. Atribuir um valor por meio de *p é rejeitado porque esse caminho de acesso é somente de leitura. A restrição se aplica ao acesso por esse ponteiro; a própria x foi definida sem const e ainda pode ser alterada por seu nome.

Questão 2 de 10

Quais são os valores finais de a e b?

int a = 4, b = 13;
int *p = &a, *q = p;
*p += 3;
p = &b;
*q = *p - 2;
*p = *q + 5;
  1. 7, 12
  2. 11, 13
  3. 7, 18
  4. 11, 16
Mostrar respostaOcultar resposta
Resposta: D
A primeira escrita faz a ser igual a 7, e redirecionar p deixa q apontando para a. A atribuição seguinte define a como 11 usando o valor 13 de b, e a última escrita define b como 16. Manter a em 7 move incorretamente q quando apenas p foi redirecionado.

Questão 3 de 10

Um int ocupa 4 bytes. Um arranjo int levels[7] começa no endereço ilustrativo 0x2400. Em qual endereço levels[4] começa?
  1. 0x2404
  2. 0x2410
  3. 0x2414
  4. 0x241C
Mostrar respostaOcultar resposta
Resposta: B
O índice 4 está quatro elementos após o início. Cada um ocupa 4 bytes, então o deslocamento é de 16 bytes, escrito como 0x10, e o endereço é 0x2410. Somar apenas 4 ao endereço trataria o índice como uma contagem de bytes e alcançaria o segundo elemento.

Questão 4 de 10

Dado int items = 16;, qual declaração cria um ponteiro para items?
  1. int *p = items;
  2. int p = &items;
  3. int *p = *items;
  4. int *p = &items;
Mostrar respostaOcultar resposta
Resposta: D
&items obtém o endereço do inteiro, e int *p declara o tipo de ponteiro correspondente. Inicializar o ponteiro com items forneceria o inteiro armazenado 16 em vez do endereço de um objeto inteiro. Obter um endereço deixa o valor de items inalterado.

Questão 5 de 10

Qual valor n contém após este código?

int n = 14;
int *p = &n;
*p = 23;
  1. 23
  2. 14
  3. 37
  4. 0
Mostrar respostaOcultar resposta
Resposta: A
p contém o endereço de n, então *p designa esse mesmo inteiro. Atribuir 23 por meio de *p substitui o valor em n. Manter 14 confundiria uma escrita por meio do ponteiro com uma alteração do endereço armazenado no próprio ponteiro.

Questão 6 de 10

Qual é o valor de score após o retorno de clear(score)?

void clear(int value) { value = 0; }
/* In the caller: */
int score = 27;
clear(score);
  1. 0
  2. 1
  3. 27
  4. Comportamento indefinido
Mostrar respostaOcultar resposta
Resposta: C
A função recebe uma cópia local do argumento inteiro. Definir value como zero altera esse parâmetro, então score da função chamadora continua sendo 27. Para esperar zero, seria necessária uma função que recebesse &score por meio de um ponteiro para inteiro e escrevesse no objeto apontado.

Questão 7 de 10

Em um sistema com ponteiros para objetos de 8 bytes, o que isto imprime?

char label[12] = "oak";
char *p = label;
printf("%zu %zu
", sizeof label, sizeof p);
  1. 4 8
  2. 12 8
  3. 12 12
  4. 3 8
Mostrar respostaOcultar resposta
Resposta: B
label é um arranjo de doze caracteres, então sizeof label é 12, embora o texto armazenado tenha apenas três letras. p é um objeto ponteiro separado e ocupa 8 bytes nesse sistema. Contar o terminador fornece o prefixo ocupado pela cadeia de caracteres, não o tamanho declarado do arranjo.

Questão 8 de 10

Quais são os valores de left e right após a chamada?

void exchange(int *a, int *b) {
    int *t = a;
    a = b;
    b = t;
}
/* In the caller: */
int left = 11, right = 29;
exchange(&left, &right);
  1. 29, 11
  2. 29, 29
  3. 11, 11
  4. 11, 29
Mostrar respostaOcultar resposta
Resposta: D
A função troca suas cópias locais de ponteiros sem atribuir valores a *a ou *b. Nenhum objeto inteiro é alterado, então left continua sendo 11 e right continua sendo 29. Esperar que os valores sejam trocados confunde mover valores de endereços locais com copiar os inteiros alcançados por esses endereços.

Questão 9 de 10

Uma função é declarada como int read_count(int *out);. Ela retorna 1 em caso de sucesso e escreve a contagem apenas nesse caso. Qual código da função chamadora imprime com segurança uma contagem recém-lida?
  1. int n; if (read_count(&n)) printf("%d", n);
  2. int n; read_count(&n); printf("%d", n);
  3. int n; if (read_count(n)) printf("%d", n);
  4. int *n; if (read_count(n)) printf("%d", *n);
Mostrar respostaOcultar resposta
Resposta: A
A função chamadora fornece um objeto inteiro por endereço e usa seu valor somente após o status indicar sucesso. Uma impressão incondicional poderia ler um n não inicializado quando a função falhasse sem escrever. Um ponteiro não inicializado não fornece um objeto de saída válido para a função preencher.

Questão 10 de 10

Qual par a última linha imprime?

int *next_value(void) {
    static int n = 20;
    n += 3;
    return &n;
}
/* In the caller: */
int *first = next_value();
int saved = *first;
int *second = next_value();
printf("%d %d
", saved, *first + *second);
  1. 23 49
  2. 23 52
  3. 26 52
  4. 23 46
Mostrar respostaOcultar resposta
Resposta: B
A primeira chamada armazena 23, que é copiado para saved. A segunda chamada altera o mesmo inteiro estático para 26, e ambos os ponteiros retornados o alcançam, então seus valores somam 52. O resultado 49 trata o primeiro ponteiro como um registro preservado do valor anterior, mas apenas saved contém um inteiro copiado e separado.
Conjunto 2: Raciocine sobre intervalos de arranjos e cadeias de caracteres10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

O que total(data + 1, 3) retorna?

int total(const int *p, size_t n) {
    int s = 0;
    for (size_t i = 0; i < n; i++) s += p[i];
    return s;
}
/* In the caller: */
int data[] = {9, 4, 7, 2, 8};
  1. 20
  2. 13
  3. 17
  4. 21
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Em um sistema com ponteiros de 8 bytes e valores int de 4 bytes, o que count(values) retorna?

size_t count(int a[9]) {
    return sizeof a / sizeof a[0];
}
/* In the caller: */
int values[9] = {0};
  1. 2
  2. 9
  3. 8
  4. 4
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Quais são os valores de taken e a[0] depois?

int a[] = {8, 15, 22};
int *p = a;
int taken = *p++;
(*p)++;
  1. 15, 9
  2. 8, 9
  3. 8, 8
  4. 9, 8
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

O que puts(message + 5) imprime para const char *message = "blue sky";?
  1. sky
  2. sky
  3. blue
  4. ky
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Qual é o valor de *(p - 2) após estas instruções?

int bins[] = {6, 12, 18, 24, 30, 36, 42};
int *p = bins + 1;
p += 4;
  1. 18
  2. 36
  3. 12
  4. 24
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Para double values[8];, quanto vale &values[6] - &values[2]?
  1. 32
  2. 8
  3. 4
  4. 2
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Após este código, qual é o maior inteiro não negativo k para o qual p + k pode ser formado dentro do intervalo permitido para ponteiros?

int a[8];
int *p = a + 1, *end = a + 8;
p += (end - p) / 2;
end -= 2;
p += end - p;
  1. 3
  2. 2
  3. 1
  4. 4
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Uma cadeia de caracteres modificável terminada em nulo é invertida com esta preparação. Qual entrada revela um cálculo inválido de ponteiro antes do início do laço?

size_t n = strlen(s);
char *left = s;
char *right = s + n - 1;
while (left < right) {
    char t = *left; *left++ = *right; *right-- = t;
}
  1. Uma cadeia de dois caracteres
  2. Uma cadeia de um caractere
  3. Uma cadeia de comprimento ímpar
  4. Uma cadeia de caracteres vazia
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Um arranjo int a[6] começa no endereço ilustrativo 0x3000, e cada inteiro ocupa 4 bytes. Quais endereços a + 1 e &a + 1 representam?
  1. 0x3004 e 0x3018
  2. 0x3004 e 0x3004
  3. 0x3018 e 0x3018
  4. 0x3001 e 0x3006
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Quais são os valores de count e p - text após este laço?

char text[] = "abaca";
char *p = text;
int count = 0;
while (*p != '\0') {
    if (*p++ == 'a') count++;
}
  1. 3, 6
  2. 2, 5
  3. 3, 5
  4. 2, 6
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 3: Acompanhe níveis de ponteiros e disposições de armazenamento10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

Após int n = 31; int *p = &n; int **pp = &p;, qual é o tipo de *pp?
  1. int
  2. int **
  3. int ***
  4. int *
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Com ponteiros para objetos de 8 bytes, quanto vale sizeof words?

const char *words[] = {"red", "violet", "gold", "teal"};
  1. 20
  2. 32
  3. 24
  4. 8
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Quanto vale *p depois?

int a = 18, b = 25;
int *p = &a;
int **pp = &p;
*pp = &b;
**pp -= 4;
  1. 21
  2. 14
  3. 25
  4. 18
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Para char *end; long n = strtol("57kg", &end, 10);, para onde end aponta após a conversão?
  1. O primeiro caractere de "57kg"
  2. O caractere 7 em "57kg"
  3. O caractere k em "57kg"
  4. O terminador após "57kg"
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Um programa recebe a linha de comando ./paint amber 6 na disposição usual de um ambiente hospedado. Quais são os valores de argc e argv[2][0]?
  1. 2 e 6
  2. 3 e 6
  3. 3 e '6'
  4. 2 e '6'
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Qual tipo de parâmetro corresponde a int board[2][5] da função chamadora após sua conversão usual de arranjo?
  1. int **board
  2. int (*board)[5]
  3. int *board[5]
  4. int (*board)[2]
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Quais são os valores de a, b e *p após a chamada?

void shift(int **slot, int *next) {
    **slot += 2;
    *slot = next;
    **slot += 3;
}
/* In the caller: */
int a = 5, b = 12;
int *p = &a;
shift(&p, &b);
  1. 7, 15, 15
  2. 10, 12, 12
  3. 7, 15, 7
  4. 5, 15, 15
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Qual é o deslocamento em bytes de map[2][3] em relação a map[0][0] e qual é o tipo de map + 1, para int map[4][6] com inteiros de 4 bytes?
  1. 36 bytes e int **
  2. 60 bytes e int **
  3. 36 bytes e int (*)[6]
  4. 60 bytes e int (*)[6]
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Quais são word e o índice de p depois?

char word[] = "abcd";
char *p = word;
char saved = *p++;
*p = saved;
p += 2;
*p = 'z';
  1. "aacz", índice 3
  2. "bbcz", índice 3
  3. "aazd", índice 2
  4. "aacd", índice 4
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma função auxiliar deve retornar um ponteiro para o maior elemento do arranjo não vazio da função chamadora, permitindo que ela o modifique. Qual projeto atende aos dois requisitos?
  1. Copiar o maior valor para um int local e retornar seu endereço.
  2. Armazenar o maior valor em um int estático e retornar seu endereço.
  3. Acompanhar o índice do maior elemento e retornar &a[index].
  4. Acompanhar o maior valor e retorná-lo por meio de um const int *.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Conjunto 4: Escolha operações e crie funções de retorno de chamada para ordenação10 Questões de Prática

Salve esta aula para ver todas as respostas e praticar cada conjunto.

Questão 1 de 10

O que int (*filter)(int); declara?
  1. Uma função que recebe int e retorna int *.
  2. Um ponteiro para função que recebe int e retorna int.
  3. Uma função que recebe int * e retorna int.
  4. Um ponteiro para função que recebe int * e retorna int.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 2 de 10

Uma função tem a assinatura void visit(int *a, size_t n, void (*action)(int *));, e void mark(int *x); está disponível. Como a função chamadora fornece mark para um arranjo data de cinco elementos?
  1. visit(data, 5, mark());
  2. visit(data, 5, *mark(data));
  3. visit(data, 5, mark(data));
  4. visit(data, 5, mark);
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 3 de 10

Qual valor é atribuído a result?

int difference(int a, int b) { return a - b; }
int product(int a, int b) { return a * b; }
int (*op)(int, int) = difference;
op = product;
int result = (*op)(7, 4);
  1. 28
  2. 3
  3. 11
  4. -3
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 4 de 10

Para int readings[8];, qual chamada ordena todos os elementos usando uma função de retorno de chamada compare compatível?
  1. qsort(readings, sizeof readings, 8, compare);
  2. qsort(readings, 8, sizeof readings, compare);
  3. qsort(readings, 8, sizeof readings[0], compare);
  4. qsort(readings, 8, sizeof readings[0], compare());
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 5 de 10

Quais são os valores de result e calls após este código?

int calls = 0;
int add_one(int x) { calls++; return x + 1; }
int twice(int x) { calls++; return x * 2; }
/* In a function: */
int (*ops[])(int) = {twice, add_one};
int result = ops[1](ops[0](5));
  1. 12, 2
  2. 11, 1
  3. 12, 1
  4. 11, 2
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 6 de 10

Uma função de comparação de inteiros deve funcionar para todo int representável em ordem crescente. Qual expressão de retorno atende a esse requisito após ler os elementos em x e y?
  1. (x > y) - (x < y)
  2. (x < y) - (x > y)
  3. x - y
  4. x > y
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 7 de 10

Para um arranjo const char *labels[], o que recupera a cadeia de caracteres passada como parâmetro const void *a da função de comparação antes de chamar strcmp?
  1. (const char *)a
  2. (const char *const *)a
  3. *(const char *const *)a
  4. **(const char *const *)a
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 8 de 10

Uma tabela int (*ops[3])(int, int) tem funções válidas nas posições 0 e 2 e NULL na posição 1. Para um inteiro com sinal choice, qual condição permite ops[choice](8, 2) com segurança?
  1. choice < 3 && ops[choice] != NULL
  2. choice >= 0 && choice < 3 && ops[choice] != NULL
  3. ops[choice] != NULL && choice >= 0 && choice < 3
  4. choice >= 0 && choice < 3
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 9 de 10

Uma função de comparação ordena inteiros por valor absoluto, retornando zero para valores opostos de mesma magnitude. Ela ordena {4, -4, 2, -2} com qsort. Qual resultado é garantido?
  1. {-2, 2, -4, 4} exatamente nessa ordem.
  2. {2, -2, 4, -4} exatamente nessa ordem.
  3. {-4, 4, -2, 2} exatamente nessa ordem.
  4. Os dois valores de magnitude 2 precedem os valores de magnitude 4.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Questão 10 de 10

Uma lista é ordenada por comprimento crescente das cadeias de caracteres usando este corpo de função de retorno de chamada. Qual é o defeito?

const char *x = *(const char *const *)a;
const char *y = *(const char *const *)b;
size_t nx = strlen(x), ny = strlen(y);
return nx < ny;
  1. Retorna um valor positivo para texto mais curto e zero para texto mais longo.
  2. Lê os comprimentos das cadeias dos bytes das posições de ponteiros.
  3. Altera as cadeias de caracteres enquanto mede seus comprimentos.
  4. Compara endereços de caracteres em vez de contagens de caracteres.
Mostrar respostaOcultar resposta

Salve esta aula para ver esta resposta.

Travou em uma questão sua? Ajuda com Tarefas

Termos-Chave

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

A

Ajuste de parâmetro de arranjo para ponteiro
O ajuste de parâmetro de arranjo para ponteiro transforma uma declaração de arranjo em uma lista de parâmetros de função em um parâmetro ponteiro, sem copiar o comprimento do arranjo.
Um parâmetro escrito como int a[10] é um ponteiro dentro da função, então sizeof a mede esse ponteiro.
Alinhamento
Alinhamento é a restrição de um tipo sobre os endereços iniciais adequados para seus objetos, tratada automaticamente nas declarações comuns.
Uma disposição comum de inteiros alinhados a quatro bytes coloca elementos sucessivos de um arranjo em endereços divisíveis por quatro.
argc
argc é a contagem não negativa de argumentos passada à main em ambiente hospedado, contando as cadeias fornecidas por argv, incluindo o nome do programa quando presente.
Antes de ler argv[2], teste se argc > 2 para garantir que a cadeia de argumento solicitada existe.
argv
argv é a lista de ponteiros para caracteres passada à main em ambiente hospedado, com um ponteiro nulo no índice argc e cadeias de argumentos do usuário a partir do índice 1.
Para uma chamada usual ./sum 8 13, argv[2] alcança "13"; o argumento é texto antes da conversão numérica.
Aritmética de ponteiros
A aritmética de ponteiros move um ponteiro por contagens de elementos de seu tipo, com resultados restritos ao seu arranjo e à posição imediatamente após o último elemento.
Somar 2 a um ponteiro para inteiro move duas posições de inteiros; a função chamadora não precisa multiplicar o deslocamento por sizeof.
Armazenamento por linhas
O armazenamento por linhas armazena todos os elementos de uma linha antes da linha seguinte em um arranjo retangular, com a largura fixa da linha determinando os passos entre linhas.
Para int a[3][4], somar 1 ao a convertido avança uma linha completa de quatro inteiros, não um inteiro.
Arranjo de ponteiros
Um arranjo de ponteiros armazena valores de endereço separados como seus elementos, com os objetos apontados mantidos em outros locais de armazenamento.
Uma lista de quatro ponteiros para cadeias de caracteres tem quatro posições de endereço, mesmo quando as cadeias têm comprimentos muito diferentes.
Assinatura de função
Uma assinatura de função descreve os tipos de retorno e de parâmetros que devem ser compatíveis com o tipo de ponteiro usado para chamar a função.
Um ponteiro para dois parâmetros inteiros não pode chamar com segurança uma função que espera um double, mesmo com uma conversão explícita.

B

Byte
Um byte é a unidade de armazenamento medida por sizeof; um char em C ocupa sempre um byte, embora os tamanhos de outros tipos dependam do sistema.
Em um sistema com inteiros de quatro bytes, um arranjo de oito inteiros ocupa 32 bytes antes de qualquer ponteiro ser declarado.

C

Caractere nulo
O caractere nulo é o caractere de valor zero, escrito '\0', que termina uma cadeia de caracteres em C dentro de seu armazenamento acessível.
Um laço de comprimento para no terminador, enquanto um laço de cópia completo também deve escrever esse caractere final.
Comportamento indefinido
Comportamento indefinido significa que o padrão C não impõe requisitos ao resultado de uma operação, inclusive quanto à parada ou continuação do programa.
Desreferenciar um ponteiro nulo tem comportamento indefinido, mesmo que uma máquina específica relate uma falha de segmentação.
Conversão de arranjo em ponteiro
A conversão de arranjo em ponteiro transforma uma expressão de arranjo em um ponteiro para seu primeiro elemento na maioria dos contextos de expressão, sem copiar seus elementos.
int *p = values; inicializa um ponteiro para values[0]; todos os elementos continuam pertencendo ao arranjo original.

D

Declarador de ponteiro
Um declarador de ponteiro usa * com um nome declarado individual para indicar que o objeto nomeado tem tipo de ponteiro.
Em int *left, right;, apenas left é um ponteiro; declarar right como ponteiro exige outro asterisco.
Desreferenciação
Desreferenciação é a aplicação do * unário a um ponteiro para designar o objeto apontado, permitindo uma leitura ou escrita adequada por esse caminho.
Após int *p = &score;, a atribuição *p = 28; altera o mesmo inteiro lido posteriormente por meio de score.
Diferença entre ponteiros
Uma diferença entre ponteiros é a contagem de elementos com sinal obtida ao subtrair duas posições em um arranjo, incluindo uma posição válida imediatamente após o último elemento.
&a[5] - &a[2] resulta em 3 elementos, independentemente de quantos bytes um elemento ocupa nesse sistema.
Duração de armazenamento
Duração de armazenamento descreve por quanto tempo o armazenamento de um objeto dura, como uma execução de bloco para uma variável local automática comum.
Um ponteiro salvo de um bloco interno não pode manter existente o inteiro local comum desse bloco após seu término.
Duração de armazenamento automática
A duração de armazenamento automática faz um objeto local comum existir durante a execução de seu bloco, com o armazenamento terminando quando a execução sai dele.
O resultado local comum de uma função auxiliar deixa de existir no retorno, mesmo que ela tenha copiado seu endereço para um ponteiro.
Duração de armazenamento estática
A duração de armazenamento estática faz o armazenamento de um objeto durar durante toda a execução do programa, e uma variável local estática mantém seu valor entre chamadas.
Dois ponteiros retornados para um mesmo objeto estático de resultado veem o mesmo valor atualizado após outra chamada escrever nele.

E

Endereço de memória
Um endereço de memória identifica um byte de armazenamento; o endereço de um objeto identifica o primeiro byte do armazenamento que ele ocupa.
Se um inteiro de quatro bytes começa no endereço ilustrativo 0x2000, seus bytes restantes seguem até 0x2003.
Estouro de buffer
Um estouro de buffer escreve além dos limites de um objeto de destino, como quando uma cópia de cadeia de caracteres não tem espaço para seu caractere nulo final.
Uma palavra de três letras exige quatro posições para caracteres; um destino de três bytes não comporta seu terminador.
Estouro de inteiro com sinal
Um estouro de inteiro com sinal ocorre quando um resultado aritmético com sinal está fora da faixa representável por seu tipo, causando comportamento indefinido em C.
Uma função de comparação de inteiros que usa x - y pode causar estouro mesmo quando cada entrada separadamente cabe em um int.

F

Falha de segmentação
Uma falha de segmentação é um acesso inválido à memória relatado pelo sistema que muitas vezes interrompe um programa, embora um acesso inválido em C não precise produzi-la.
Uma desreferenciação nula pode parar com uma falha de segmentação; outra escrita por ponteiro inválido pode corromper dados silenciosamente.
Fatia de arranjo
Uma fatia de arranjo pode ser fornecida em C como um ponteiro inicial mais uma contagem que descreve uma parte consecutiva selecionada de um arranjo existente.
Passar data + 2 com uma contagem de 3 seleciona os índices originais de 2 a 4 sem copiá-los.
Função de comparação
Uma função de comparação ordena um par de elementos usando um resultado negativo, zero ou positivo para indicar, respectivamente, primeiro antes, equivalentes ou primeiro depois.
Retornar (x > y) - (x < y) ordena inteiros em ordem crescente sem subtrair os próprios valores.
Função de retorno de chamada
Uma função de retorno de chamada é uma função fornecida a outra para que ela a execute quando seu trabalho geral exigir uma ação ou decisão escolhida.
Um percurso recebe um ponteiro para ação e o chama com o endereço de cada elemento para atualizar esse elemento.

I

Incremento pós-fixado
O incremento pós-fixado fornece o valor anterior de um objeto à expressão que o contém e incrementa esse objeto, como no avanço do ponteiro em *p++.
int x = *p++; lê o elemento na posição anterior e deixa p apontando para a posição seguinte.

L

Limites do arranjo
Os limites do arranjo determinam quais índices de elementos existem e para onde um ponteiro pode se mover, com apenas uma posição adicional permitida como marcador de fim.
Formar a - 1 para um ponteiro no início de um arranjo é inválido, mesmo que o programa nunca leia por meio dele.
Literal de cadeia de caracteres
Um literal de cadeia de caracteres é um texto entre aspas em C que fornece um arranjo com duração de armazenamento estática cujos caracteres não devem ser modificados.
const char *p = "oak"; expõe o literal por um caminho somente de leitura, enquanto char a[] = "oak"; cria uma cópia modificável.

M

Matriz bidimensional
Uma matriz bidimensional é um arranjo cujos elementos são arranjos de tamanho fixo, armazenando linhas completas e contíguas em vez de ponteiros separados para linhas.
int grid[2][5] é convertido em int (*)[5]; passá-lo como int ** descreveria uma disposição diferente.
Matriz irregular
Uma matriz irregular é uma coleção cujas linhas podem ter comprimentos diferentes, comumente representada em C usando ponteiros para linhas armazenadas separadamente.
Ponteiros para cadeias de caracteres podem se referir a "red" e "violet" sem reservar linhas de caracteres de mesmo tamanho para as duas palavras.

N

Nível de indireção
Um nível de indireção é um passo de ponteiro em uma cadeia de endereços, percorrido por uma desreferenciação e acrescentado ao obter o endereço de um objeto.
Para int **pp, a primeira desreferenciação fornece um int *, e a segunda alcança um objeto inteiro.

O

Operador de endereço
O operador de endereço unário & obtém o endereço de um objeto endereçável ou de uma função, produzindo um tipo de ponteiro correspondente.
Para um arranjo de inteiros, &values[2] fornece o endereço do terceiro elemento sem alterar seu conteúdo.
Operador de indexação
O operador de indexação acessa um elemento ao definir a[i] como *(a + i), usando o tipo de elemento do ponteiro para aplicar a escala à posição.
Se p aponta para o elemento original 2, então p[1] acessa o elemento original 3 do mesmo arranjo.
Ordem dos bytes
Ordem dos bytes é a ordem dos bytes dentro de um valor com vários bytes; sistemas que usam o byte menos significativo primeiro o armazenam no menor endereço.
Um 0x12345678 de quatro bytes tem os bytes 78 56 34 12 em ordem crescente de endereços em um sistema que armazena o byte menos significativo primeiro.
Ordenação estável
Uma ordenação estável preserva a ordem relativa original dos elementos que sua função de comparação considera iguais; qsort não oferece essa garantia.
Ordenar nomes apenas por comprimento pode reordenar dois nomes igualmente longos, mesmo que seu resultado de comparação seja zero.

P

Parâmetro de saída
Um parâmetro de saída é um parâmetro ponteiro usado para armazenar um resultado em um local modificável fornecido pela função chamadora, além de qualquer valor de retorno.
Uma função auxiliar de divisão pode preencher inteiros separados para quociente e resto enquanto retorna um status de sucesso.
Parâmetro ponteiro
Um parâmetro ponteiro é um ponteiro local de uma função inicializado a partir de um argumento, permitindo que a função acesse um objeto por endereço.
Uma função auxiliar de redefinição recebe &score por meio de int *out e escreve zero no inteiro da função chamadora usando *out.
Passagem por referência
Em C, passagem por referência costuma designar a técnica de passar um endereço e modificar o objeto apontado, enquanto o próprio endereço é copiado.
Uma troca chamada com &x e &y altera variáveis da função chamadora ao escrever por meio dos dois parâmetros ponteiro locais.
Passagem por valor
A passagem por valor fornece a um parâmetro de função uma cópia do valor de seu argumento, inclusive quando esse valor é um ponteiro para armazenamento da função chamadora.
Atribuir um valor a um parâmetro local int deixa o inteiro da função chamadora inalterado; escrever por meio de um endereço copiado pode alterá-lo.
Ponteiro
Um ponteiro é um valor tipado que pode designar um objeto ou uma função; um ponteiro para objeto também pode conter um valor nulo ou apontar para a posição imediatamente após o último elemento.
Em int *p = &count;, a variável ponteiro armazena um endereço enquanto o inteiro mantém seu próprio valor.
Ponteiro constante
Um ponteiro constante é um objeto ponteiro cujo endereço armazenado não pode ser alterado após a inicialização, mesmo quando o objeto apontado permanece modificável.
Com int *const p = &x;, *p = 3; é permitido, mas p = &y; não pode redirecionar o ponteiro.
Ponteiro de fim da conversão
Um ponteiro de fim da conversão registra onde uma conversão de texto parou, permitindo que a função chamadora examine o sufixo não consumido independentemente do resultado numérico.
Converter "73ms" com strtol pode retornar 73 e definir o ponteiro de fim da função chamadora para o m.
Ponteiro genérico para objeto
Um ponteiro genérico para objeto usa void *, ou uma forma qualificada, para conter um endereço de objeto que deve ser convertido para um tipo adequado antes do acesso.
Uma função de comparação de inteiros converte const void * em const int * antes de ler o elemento do arranjo endereçado.
Ponteiro nulo
Um ponteiro nulo é um valor de ponteiro que não designa nenhum objeto nem função e pode representar um resultado opcional vazio ou uma função de retorno de chamada ausente.
Uma busca de elemento retorna NULL quando não encontra correspondência, então a função chamadora testa o resultado antes de desreferenciá-lo.
Ponteiro para a posição imediatamente após o último elemento
Um ponteiro para a posição imediatamente após o último elemento marca a posição logo após o elemento final de um arranjo e pode servir como limite, mas não pode ser desreferenciado.
Para int a[5], um percurso pode parar quando seu ponteiro atual for igual a a + 5, antes de acessar outro elemento.
Ponteiro para arranjo
Um ponteiro para arranjo designa um arranjo inteiro como seu tipo de elemento, então somar 1 avança pelo tamanho de armazenamento desse arranjo completo.
Para int a[4], &a tem tipo int (*)[4]; seu passo aritmético difere do passo de &a[0].
Ponteiro para dados constantes
Um ponteiro para dados constantes permite ler o objeto apontado, mas rejeita escritas por meio desse ponteiro; o próprio ponteiro ainda pode ser redirecionado.
const int *p = &x; permite p = &y;, enquanto uma atribuição como *p = 3; é rejeitada.
Ponteiro para função
Um ponteiro para função designa uma função que pode ser chamada e registra os tipos de retorno e de parâmetros compatíveis exigidos para chamá-la por meio dele.
int (*op)(int, int) = add; armazena uma função de adição que pode ser chamada posteriormente como op(4, 7).
Ponteiro para ponteiro
Um ponteiro para ponteiro designa um objeto ponteiro, acrescentando um nível de indireção antes que o acesso alcance os dados finais apontados.
Para int **pp = &p;, a atribuição *pp = &y; redireciona p, enquanto **pp = 8; altera o objeto apontado por ele.
Ponteiro pendente
Um ponteiro pendente é um ponteiro cujo objeto anteriormente apontado teve seu tempo de vida encerrado, tornando-o inutilizável para acesso posterior a esse objeto.
Retornar o endereço de uma variável local automática deixa um ponteiro pendente quando a chamada da função termina.
Precedência de operadores
A precedência de operadores determina como os operadores são agrupados quando os parênteses não especificam o agrupamento, antes da avaliação da expressão resultante.
O ++ pós-fixado tem precedência maior que o * unário, então *p++ é agrupado como *(p++), não como (*p)++.
ptrdiff_t
ptrdiff_t é o tipo inteiro com sinal usado para subtração de ponteiros; uma subtração definida deve ter resultado representável nesse tipo.
Inclua <stddef.h> para ptrdiff_t e use %td para imprimir uma diferença entre posições válidas no mesmo arranjo.

Q

qsort
qsort é a função da biblioteca padrão que rearranja um arranjo usando sua contagem de elementos, o tamanho de cada elemento e uma função de comparação fornecida.
Ordenar elementos que são ponteiros para cadeias exige sizeof names[0] para o tamanho da posição, não a contagem de caracteres de uma cadeia.

R

Referência ao mesmo objeto por caminhos distintos
A referência ao mesmo objeto por caminhos distintos ocorre quando expressões ou ponteiros diferentes designam o mesmo objeto, de modo que uma escrita por um caminho é visível por outro.
Após int *q = p;, ambos os ponteiros alcançam o mesmo inteiro até que uma atribuição posterior redirecione um deles.
Registro de ativação
Um registro de ativação é o armazenamento que uma implementação típica usa para os dados de trabalho de uma chamada de função, incluindo muitos objetos locais automáticos.
O retorno de uma função permite que o armazenamento de seu registro antigo seja reutilizado; um endereço local salvo não pode preservá-lo.

S

sizeof
O operador sizeof informa o tamanho de armazenamento de um tipo ou objeto em bytes e retorna um valor do tipo inteiro sem sinal size_t.
Para char code[10] = "go";, sizeof code informa 10, embora o texto tenha apenas dois caracteres.
strtol
strtol é a função da biblioteca padrão que converte o texto numérico inicial em long e pode informar sua posição de parada por meio de char **.
Passar &end permite que strtol substitua o ponteiro para caractere da função chamadora pelo endereço do primeiro caractere não usado.

T

Tabela de despacho
Uma tabela de despacho é um arranjo de ponteiros para funções que seleciona uma operação por um índice escolhido durante a execução do programa.
Um menu de operações verifica seu índice de seleção antes de usá-lo para recuperar e chamar uma entrada da tabela.
typedef
Uma declaração typedef introduz um nome para um tipo existente, facilitando a reutilização de um tipo complexo de ponteiro para função em declarações.
Após typedef int (*operation)(int);, operation chosen; declara um ponteiro para função, não um corpo de função.

V

Valor indeterminado de ponteiro
Um valor indeterminado de ponteiro é o estado não inicializado de um ponteiro local comum; ele não fornece um objeto apontado conhecido e utilizável.
A declaração int *p; sozinha não permite que o programa teste, siga ou escreva por meio desse ponteiro com segurança.

Sobre esta aula

Por StudeliRelatar um erro

Crie seus próprios Materiais de Estudo

Transforme suas anotações, slides e documentos em Guias de Estudo, Questões de Prática, Flashcards e mais 6 ferramentas.

Começar