C Ponteiros e Strings

Uma string em C é um array de char que termina num '\0' (Strings). E o nome de um array vira um ponteiro para o primeiro item (Ponteiros e Arrays). Juntando as duas aulas: quase sempre que um programa em C mexe com texto, ele está segurando um char *, o endereço da primeira letra. Nesta aula você vai andar por uma string com um ponteiro, escrever funções que recebem texto, entender de vez a diferença entre char s[] e char *p, e conhecer os erros que derrubam programas que lidam com texto.

Você vai aprender
  • Por que uma string "é" um char * e o que printf("%s", p + 4) mostra.
  • Como percorrer uma string com p++ até o '\0', e medir o tamanho com p - s.
  • Como escrever funções que recebem uma string e até a mudam.
  • A diferença entre char cidade[] = "Recife" e char *apelido = "Recife".
  • Como uma função devolve um ponteiro para dentro de uma string.
  • Os erros: alterar um literal, usar um char * sem valor e devolver um array local.

Os programas foram compilados na máquina da Ana, na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o dedo na fila de cadeiras

Na aula Strings, uma string era uma fila de cadeiras, uma letra em cada, com a placa "FIM" (o '\0') na última. Para ler o texto, você não precisa saber quantas cadeiras são: basta pôr o dedo na primeira e ir andando, uma cadeira por vez, até encontrar a placa.

O ponteiro é esse dedo. Um char * aponta para uma cadeira (uma letra). *p é a letra embaixo do dedo; p++ anda o dedo uma cadeira. E todas as funções de texto do C (printf com %s, strlen, strcpy…) recebem só o dedo na primeira cadeira e andam sozinhas até a placa.


1. A string vira um ponteiro para a primeira letra

#include <stdio.h>

int main(void) {
    char nome[] = "Ana Lima";
    char *p = nome;

    printf("*p       = %c\n", *p);
    printf("p[1]     = %c\n", p[1]);
    printf("*(p + 2) = %c\n", *(p + 2));
    printf("p        = %s\n", p);
    printf("p + 4    = %s\n", p + 4);
    return 0;
}
char *p = nome;
O nome do array vira o endereço da primeira letra, &nome[0], e p guarda esse endereço. O tipo é char *: "ponteiro para char".
*p
A letra para onde p aponta: 'A'. Mostrada com %c, um caractere só.
p[1] e *(p + 2)
As mesmas formas de Ponteiros e Arrays: p[1] é a segunda letra; *(p + 2) é a terceira.
printf("%s", p)
O %s recebe um char *: ele mostra a letra apontada e todas as seguintes, até o '\0'.
printf("%s", p + 4)
p + 4 aponta para a quinta cadeira, a do L. O %s começa dali: mostra só Lima. Nada foi copiado nem cortado; o texto inteiro continua no array.
$ gcc -std=c23 -Wall -Wextra primeira.c -o primeira
$ ./primeira
*p       = A
p[1]     = n
*(p + 2) = a
p        = Ana Lima
p + 4    = Lima
nomeAna' 'Lima\0pp + 4
Um texto só, na memória. p aponta para o A e p + 4 para o L. O %s lê de onde o ponteiro aponta até o '\0': por isso p + 4 mostra Lima.

Experimente: troque o nome pelo seu e mude o p + 4 para mostrar só o sobrenome. Depois tente p + 1 e p + 7. O que aparece com p + 8, a posição do '\0'?

#include <stdio.h>

int main(void) {
    char nome[] = "Ana Lima";
    char *p = nome;

    printf("*p       = %c\n", *p);
    printf("p        = %s\n", p);
    printf("p + 4    = %s\n", p + 4);
    return 0;
}

2. Percorrer a string com um ponteiro

Na aula Strings, você percorreu o texto com um índice: for (int i = 0; s[i] != '\0'; i++). Com um ponteiro, o próprio "dedo" anda:

#include <stdio.h>

int main(void) {
    char palavra[] = "gato";

    for (char *p = palavra; *p != '\0'; p++) {
        printf("[%c] ", *p);
    }
    printf("\n");
    return 0;
}
char *p = palavra;
O dedo começa na primeira letra.
*p != '\0'
"Enquanto a letra embaixo do dedo não for a placa de fim". Muita gente escreve só *p, que dá no mesmo: o '\0' vale 0, que é falso.
p++
Anda uma letra (um char é 1 byte, então o endereço sobe 1).
$ gcc -std=c23 -Wall -Wextra percorrer.c -o percorrer
$ ./percorrer
[g] [a] [t] [o]

Medir o tamanho: p - s

Se o dedo começou na primeira letra e parou no '\0', a distância que ele andou é o número de letras. Subtrair dois ponteiros dá exatamente isso (Ponteiros e Arrays). É assim que o strlen funciona por dentro:

#include <stdio.h>

int tamanho(char *s) {
    char *p = s;
    while (*p != '\0') {
        p++;
    }
    return p - s;
}

int main(void) {
    char nome[] = "Ana";
    char frase[] = "Bom dia, turma!";

    printf("\"%s\" tem %d letras\n", nome, tamanho(nome));
    printf("\"%s\" tem %d caracteres\n", frase, tamanho(frase));
    printf("\"\" tem %d\n", tamanho(""));
    return 0;
}
int tamanho(char *s)
A função recebe a string como char *: o endereço da primeira letra. É a forma comum de passar texto para uma função.
char *p = s; while (*p != '\0') p++;
Um segundo dedo, p, anda até o '\0'. O s fica parado no começo.
return p - s;
A distância entre os dois dedos: quantas letras p andou. O '\0' não entra na conta.
tamanho("")
A string vazia: o primeiro caractere já é o '\0', o laço não anda nada, e a distância é 0.
$ gcc -std=c23 -Wall -Wextra tamanho.c -o tamanho
$ ./tamanho
"Ana" tem 3 letras
"Bom dia, turma!" tem 15 caracteres
"" tem 0

A subtração de dois ponteiros tem um tipo próprio, o ptrdiff_t (de <stddef.h>, mostrado com %td). Aqui ele foi convertido para int no return, o que basta para textos de tamanho normal. O strlen de verdade devolve size_t (Funções de String).

Missão: complete a função contar, que conta quantas vezes uma letra aparece no texto. O laço com o ponteiro p já está pronto; falta, dentro dele, comparar a letra apontada (*p) com letra e somar 1 em total.

a em banana: 3
a em abacaxi: 3
r em arara: 2
#include <stdio.h>

int contar(char *s, char letra) {
    int total = 0;
    for (char *p = s; *p != '\0'; p++) {
        // se a letra apontada por p for igual a letra, some 1 em total
    }
    return total;
}

int main(void) {
    char fruta1[] = "banana";
    char fruta2[] = "abacaxi";
    char ave[] = "arara";

    printf("a em %s: %d\n", fruta1, contar(fruta1, 'a'));
    printf("a em %s: %d\n", fruta2, contar(fruta2, 'a'));
    printf("r em %s: %d\n", ave, contar(ave, 'r'));
    return 0;
}
#include <stdio.h>

int contar(char *s, char letra) {
    int total = 0;
    for (char *p = s; *p != '\0'; p++) {
        if (*p == letra) {
            total++;
        }
    }
    return total;
}

int main(void) {
    char fruta1[] = "banana";
    char fruta2[] = "abacaxi";
    char ave[] = "arara";

    printf("a em %s: %d\n", fruta1, contar(fruta1, 'a'));
    printf("a em %s: %d\n", fruta2, contar(fruta2, 'a'));
    printf("r em %s: %d\n", ave, contar(ave, 'r'));
    return 0;
}

3. Uma função que muda a string

Como a função recebe o endereço das letras, e não uma cópia, ela pode mudar o texto de quem a chamou (o mesmo motivo de Passar por Ponteiro). A Ana quer transformar um aviso em letras maiúsculas, para chamar a atenção:

#include <stdio.h>
#include <ctype.h>

void gritar(char *s) {
    for (char *p = s; *p != '\0'; p++) {
        *p = toupper(*p);
    }
}

int main(void) {
    char aviso[] = "a prova foi adiada";

    gritar(aviso);
    printf("%s\n", aviso);
    return 0;
}
#include <ctype.h>
Traz o toupper ("to upper", para maiúscula), que devolve a versão maiúscula de uma letra; o que não é letra volta igual (Classificar Caracteres).
*p = toupper(*p);
Lê a letra apontada, troca pela maiúscula e escreve de volta na mesma cadeira. É o array aviso do main que muda.
gritar(aviso);
Sem &: o nome do array já é o endereço da primeira letra.
$ gcc -std=c23 -Wall -Wextra gritar.c -o gritar
$ ./gritar
A PROVA FOI ADIADA

O cabeçalho void gritar(char s[]), com colchetes, quer dizer exatamente a mesma coisa que char *s: num parâmetro, o C troca o array por um ponteiro. Muita gente prefere o char *s justamente para deixar claro que a função recebe um endereço.

E quando a função só lê?

A tamanho e a contar não mudam nenhuma letra. Para elas, o certo é const char *s: o const promete (e o compilador garante) que a função não vai escrever no texto. Isso é tão importante que tem aula própria, a próxima: const e Ponteiros.


4. char s[] × char *p: duas coisas diferentes

Estas duas linhas parecem iguais, mas criam coisas diferentes:

#include <stdio.h>

int main(void) {
    char cidade[] = "Recife";
    char *apelido = "Recife";

    printf("sizeof cidade  = %zu\n", sizeof cidade);
    printf("sizeof apelido = %zu\n", sizeof apelido);

    cidade[0] = 'r';
    printf("cidade  = %s\n", cidade);

    apelido = "Veneza brasileira";
    printf("apelido = %s\n", apelido);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra duas.c -o duas
$ ./duas
sizeof cidade  = 7
sizeof apelido = 8
cidade  = recife
apelido = Veneza brasileira
char cidade[] = "Recife";
Um array de 7 bytes (6 letras + '\0') que é seu. As letras do literal são copiadas para ele. Você pode mudar as letras (cidade[0] = 'r').
char *apelido = "Recife";
Um ponteiro (8 bytes no Linux de 64 bits, 4 no editor) que aponta para o próprio literal "Recife", guardado numa área só de leitura do programa. Nada é copiado.
sizeof cidade × sizeof apelido
O primeiro mede o array inteiro (7); o segundo mede só o ponteiro (8), não importa o tamanho do texto.
apelido = "Veneza brasileira";
O ponteiro pode passar a apontar para outro texto. O primeiro literal continua lá, intocado.
cidader e c i f e \07 bytes, seusapelido•R e c i f e \0literal na área só de leitura
cidade tem as próprias letras (uma cópia, que pode mudar). apelido só guarda um endereço: as letras são as do literal, que não podem ser alteradas.
char cidade[] = "Recife";char *apelido = "Recife";
O que éUm array com uma cópia do textoUm ponteiro para o literal
sizeof7 (o texto inteiro, com o '\0')8 no Linux, 4 no editor (o ponteiro)
Mudar uma letraPode: cidade[0] = 'r';Não: compila, mas o programa morre (Strings, seção 10)
Trocar o texto inteiro com =Não compila: use snprintf ou strcpyPode: apelido = "Olinda"; aponta para outro literal
Melhor declaraçãochar cidade[] = …const char *apelido = … (const e Ponteiros)

Tentar trocar o texto do array com = é erro de compilação:

#include <stdio.h>

int main(void) {
    char cidade[20] = "Recife";
    cidade = "Olinda";
    printf("%s\n", cidade);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra atribuir.c -o atribuir
atribuir.c: In function ‘main’:
atribuir.c:5:12: error: assignment to expression with array type
    5 |     cidade = "Olinda";
      |            ^

"Atribuição a uma expressão de tipo array". Um array não pode ser trocado de lugar: ele é as suas cadeiras. Para pôr outro texto nelas, copie letra por letra com snprintf(cidade, sizeof cidade, "%s", "Olinda"); (Funções de String). No editor, o Clang diz array type 'char[20]' is not assignable ("o tipo array char[20] não aceita atribuição").

Alterar o literal pelo ponteiro

Juntando as seções 3 e 4: se você passar para gritar um ponteiro para literal, em vez de um array, a função tenta escrever na área só de leitura:

#include <stdio.h>
#include <ctype.h>

void gritar(char *s) {
    for (char *p = s; *p != '\0'; p++) {
        *p = toupper(*p);
    }
}

int main(void) {
    char *aviso = "a prova foi adiada";

    gritar(aviso);
    printf("%s\n", aviso);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra gritarlit.c -o gritarlit     « nenhum aviso
$ ./gritarlit
Falha de segmentação     (imagem do núcleo gravada) ./gritarlit
$ echo $?
139

Só uma linha mudou (char *aviso em vez de char aviso[]), o gcc não avisou nada, e o programa morreu na primeira letra. No editor, o mesmo programa mostra A PROVA FOI ADIADA e termina normalmente, porque no WebAssembly não existe memória só de leitura. Não se engane: num computador de verdade ele cai. A defesa é declarar const char *aviso: aí o compilador recusa passar o texto para uma função que escreve nele (const e Ponteiros).


5. Devolver um ponteiro para dentro da string

Uma função pode devolver um char * que aponta para o meio de um texto que já existe. A Ana quer separar o usuário e o domínio de um e-mail. Primeiro, uma função que acha a primeira vez que um caractere aparece:

#include <stdio.h>

char *achar(char *s, char c) {
    for (char *p = s; *p != '\0'; p++) {
        if (*p == c) {
            return p;
        }
    }
    return NULL;
}

int main(void) {
    char email[] = "ana.lima@escola.edu.br";

    char *arroba = achar(email, '@');
    if (arroba == NULL) {
        printf("e-mail sem @\n");
        return 1;
    }
    printf("O @ está na posição %td\n", arroba - email);
    printf("Domínio: %s\n", arroba + 1);

    *arroba = '\0';
    printf("Usuário: %s\n", email);
    return 0;
}
char *achar(char *s, char c)
Devolve um char *: o endereço da cadeira onde o c está.
return p;
Achou: devolve o dedo parado no @. O endereço aponta para dentro do array email do main, que continua existindo depois que a função termina (diferente do erro da seção "Quando dá errado").
return NULL;
Não achou: devolve o ponteiro nulo, o "não aponta para nada" (Ponteiro Nulo). Quem chama tem de conferir antes de usar.
arroba - email
A distância desde o começo: a posição do @ (contando do 0). O %td é o formato do ptrdiff_t.
arroba + 1
A cadeira logo depois do @: o %s mostra o domínio.
*arroba = '\0';
Escreve a placa de fim em cima do @. Agora, para quem lê a partir de email, o texto acaba ali: sobra só o usuário (o truque de Strings, seção 5).
$ gcc -std=c23 -Wall -Wextra dominio.c -o dominio
$ ./dominio
O @ está na posição 8
Domínio: escola.edu.br
Usuário: ana.lima

A biblioteca já tem essa função pronta: strchr(email, '@'), do <string.h>, faz o mesmo que a achar (Funções de String). Escrevê-la uma vez mostra que não há mágica: é um dedo andando até achar a letra.


6. Uma lista de textos

Um array de char * guarda vários textos: cada item é um ponteiro para uma string. É o jeito comum de ter uma tabela de nomes:

#include <stdio.h>

int main(void) {
    const char *dias[] = {"domingo", "segunda", "terça", "quarta",
                          "quinta", "sexta", "sábado"};

    int hoje = 5;
    printf("Hoje é %s.\n", dias[hoje]);
    printf("Amanhã é %s.\n", dias[(hoje + 1) % 7]);
    printf("A lista tem %zu itens.\n", sizeof dias / sizeof dias[0]);
    return 0;
}
const char *dias[]
Um array de 7 ponteiros. Cada um aponta para um literal; o const diz que não vamos mudar as letras.
dias[hoje]
O item 5: um const char * que aponta para "sexta". O %s o mostra.
(hoje + 1) % 7
O dia seguinte, voltando ao domingo depois do sábado.
$ gcc -std=c23 -Wall -Wextra dias.c -o dias
$ ./dias
Hoje é sexta.
Amanhã é sábado.
A lista tem 7 itens.

Os textos podem ter tamanhos diferentes, porque cada um mora no seu lugar e o array só guarda os endereços. A aula Ponteiro para Ponteiro mostra como passar uma lista assim para uma função (é o caso do argv do main).


Formas diferentes: qual usar

Você quer…Use
Um texto que vai mudar (ler do teclado, montar, converter)Um array: char nome[50]; ou char aviso[] = "…";
Um texto fixo, só para lerconst char *msg = "…";
Uma função que lê um textoParâmetro const char *s
Uma função que muda um textoParâmetro char *s (e, se ela escreve mais letras, também o tamanho: char *s, size_t tam)
Percorrer letra por letraÍndice (s[i]) ou ponteiro (*p, p++): os dois são C comum; escolha o que achar mais claro
Uma lista de textos fixosconst char *lista[] = {…};

Quando dá errado

Falha de segmentação ao mudar uma letra

O que significa: o ponteiro aponta para um literal (char *s = "…";), que fica numa área só de leitura. O gcc não avisa; no editor o programa nem cai.

Como consertar: se o texto vai mudar, use um array (char s[] = "…";). Se não vai, declare const char *s, e o compilador barra qualquer tentativa de escrita.

warning: ‘nome’ is used uninitialized: um char * sem lugar para as letras
#include <stdio.h>

int main(void) {
    char *nome;
    printf("Seu nome: ");
    scanf("%19s", nome);
    printf("Olá, %s!\n", nome);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra seminicio.c -o seminicio
seminicio.c: In function ‘main’:
seminicio.c:6:5: warning: ‘nome’ is used uninitialized [-Wuninitialized]
    6 |     scanf("%19s", nome);
      |     ^~~~~~~~~~~~~~~~~~~
seminicio.c:4:11: note: ‘nome’ was declared here
    4 |     char *nome;
      |           ^~~~
$ echo Ana | ./seminicio
Seu nome: Olá, (null)!

O que significa: "nome é usado sem valor inicial". Um char * é só o dedo, não as cadeiras: ele não reserva espaço nenhum para as letras. Aqui o lixo dentro de nome calhou de ser 0 (o ponteiro nulo), o scanf não guardou nada e o printf mostrou (null). Com outro lixo, o scanf escreveria num lugar qualquer da memória, e o programa poderia morrer ou estragar outras variáveis. No editor, o Clang avisa variable 'nome' is uninitialized when used here.

Como consertar: reserve as cadeiras com um array: char nome[20]; (o %19s deixa um lugar para o '\0'). Ou aponte para memória reservada com malloc.

error: assignment to expression with array type

O que significa: você tentou trocar o texto de um array com =.

Como consertar: snprintf(destino, sizeof destino, "%s", "novo texto");, ou, se o texto é fixo, use um const char *, que pode apontar para outro literal.

warning: function returns address of local variable: devolver um array local
#include <stdio.h>

char *saudacao(void) {
    char texto[] = "Bom dia!";
    return texto;
}

int main(void) {
    char *s = saudacao();
    printf("%s\n", s);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra local.c -o local
local.c: In function ‘saudacao’:
local.c:5:12: warning: function returns address of local variable [-Wreturn-local-addr]
    5 |     return texto;
      |            ^~~~~
$ ./local
Falha de segmentação     (imagem do núcleo gravada) ./local

O que significa: o array texto é local: some quando a função termina, e o endereço devolvido aponta para cadeiras que não existem mais (Valor de Retorno). O gcc troca esse endereço por NULL de propósito, e o printf morre. No editor, o Clang avisa address of stack memory associated with local variable 'texto' returned e o programa mostra Bom dia! por sorte. A achar da seção 5 não tem esse problema porque devolve um endereço dentro do array de quem chamou.

Como consertar: para um texto fixo, devolva o literal: const char *saudacao(void) { return "Bom dia!"; } (literais existem durante o programa todo). Para um texto montado na hora, receba o array de quem chama como parâmetro (void saudacao(char *destino, size_t tam)) ou use Strings Dinâmicas.

warning: comparison with string literal results in unspecified behavior

O que significa: você escreveu if (resposta == "sim"). O == compara os endereços (onde cada texto mora), não as letras, e dá falso (Strings, seção 8).

Como consertar: if (strcmp(resposta, "sim") == 0), com #include <string.h>.


Referência rápida

CódigoO que faz
char *p = nome;p aponta para a primeira letra de nome
*p, p[i], *(p + i)A letra apontada; a letra i posições depois
printf("%s", p + 4)Mostra o texto a partir da quinta letra
for (char *p = s; *p != '\0'; p++)Percorre a string letra por letra
p - sQuantas letras há entre s e p (tipo ptrdiff_t, formato %td)
void f(char *s)Função que recebe (e pode mudar) uma string; igual a char s[]
void f(const char *s)Função que só lê a string
char s[] = "Ana";Array com uma cópia: as letras podem mudar; s = … não compila
const char *p = "Ana";Ponteiro para o literal: as letras não podem mudar; p = "Bia"; pode
*p = '\0';Corta o texto no ponto em que p aponta
const char *lista[] = {"a", "b"};Uma lista de textos

Exercícios

1. A letra apontada

Com char *p = "gato";, complete para mostrar a letra g:

printf("%c\n", );

2. Andar uma letra

Complete o laço que percorre a string até o fim:

for (char *p = s; *p != '\0'; )

3. O resto do texto

Com char nome[] = "Ana Lima";, o que printf("%s\n", nome + 4); mostra?

4. No Linux de 64 bits, quanto vale sizeof p com char *p = "Recife";?

5. Qual destas linhas não compila?

6. char *s = "aviso"; gritar(s);, em que gritar troca as letras por maiúsculas. O que acontece no Linux?

Missão final: o espelho de palavras

Missão: complete a função inverter, que vira uma palavra de trás para a frente no próprio array ("roma" vira "amor"). Ela já tem dois dedos: inicio na primeira letra e fim na última. Enquanto inicio < fim: troque as letras *inicio e *fim (com uma variável char temp), ande inicio uma letra para a frente e fim uma para trás.

amor
marrocos
A
#include <stdio.h>
#include <string.h>

void inverter(char *s) {
    char *inicio = s;
    char *fim = s + strlen(s) - 1;
    // enquanto inicio < fim: troque *inicio e *fim, avance inicio e recue fim
}

int main(void) {
    char a[] = "roma";
    char b[] = "socorram";
    char c[] = "A";
    inverter(a);
    inverter(b);
    inverter(c);
    printf("%s\n%s\n%s\n", a, b, c);
    return 0;
}
#include <stdio.h>
#include <string.h>

void inverter(char *s) {
    char *inicio = s;
    char *fim = s + strlen(s) - 1;
    while (inicio < fim) {
        char temp = *inicio;
        *inicio = *fim;
        *fim = temp;
        inicio++;
        fim--;
    }
}

int main(void) {
    char a[] = "roma";
    char b[] = "socorram";
    char c[] = "A";
    inverter(a);
    inverter(b);
    inverter(c);
    printf("%s\n%s\n%s\n", a, b, c);
    return 0;
}

Com "A", inicio e fim já começam na mesma letra, o laço não roda e nada muda. Repare que os três textos são arrays: com char *a = "roma";, a função tentaria escrever num literal (seção 4). E cuidado com a string vazia: com "", fim ficaria uma posição antes do começo; uma versão para uso real confere if (*s == '\0') return; no início.


Resumo

  • O nome de uma string vira um char * para a primeira letra. Funções de texto recebem esse ponteiro e andam até o '\0'; printf("%s", p + 4) começa na quinta letra.
  • Percorrer: for (char *p = s; *p != '\0'; p++). A distância p - s é o número de letras.
  • Uma função com parâmetro char *s pode mudar o texto de quem chama; se só lê, use const char *s.
  • char s[] = "…" é um array com cópia (letras mudam; s = … não compila). char *p = "…" aponta para um literal só de leitura (mudar letra derruba o programa; p = … pode). Prefira const char *p.
  • Uma função pode devolver um ponteiro para dentro de uma string que já existe (como o strchr), mas nunca o endereço de um array local.
  • Um char * sem valor não tem cadeiras para as letras: reserve um array antes de ler texto.
  • Próxima aula: const e Ponteiros.