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.
- Por que uma string "é" um
char *e o queprintf("%s", p + 4)mostra. - Como percorrer uma string com
p++até o'\0', e medir o tamanho comp - s. - Como escrever funções que recebem uma string e até a mudam.
- A diferença entre
char cidade[] = "Recife"echar *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], epguarda esse endereço. O tipo échar *: "ponteiro parachar". - *p
- A letra para onde
paponta:'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
%srecebe umchar *: ele mostra a letra apontada e todas as seguintes, até o'\0'. - printf("%s", p + 4)
p + 4aponta para a quinta cadeira, a doL. O%scomeç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
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'. Osfica parado no começo. - return p - s;
- A distância entre os dois dedos: quantas letras
pandou. 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
avisodomainque 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.
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.
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 texto | Um ponteiro para o literal |
sizeof | 7 (o texto inteiro, com o '\0') | 8 no Linux, 4 no editor (o ponteiro) |
| Mudar uma letra | Pode: 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 strcpy | Pode: apelido = "Olinda"; aponta para outro literal |
| Melhor declaração | char 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 ocestá. - return p;
- Achou: devolve o dedo parado no
@. O endereço aponta para dentro do arrayemaildomain, 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 doptrdiff_t. - arroba + 1
- A cadeira logo depois do
@: o%smostra o domínio. - *arroba = '\0';
- Escreve a placa de fim em cima do
@. Agora, para quem lê a partir deemail, 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
constdiz que não vamos mudar as letras. - dias[hoje]
- O item 5: um
const char *que aponta para"sexta". O%so 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 ler | const char *msg = "…"; |
| Uma função que lê um texto | Parâmetro const char *s |
| Uma função que muda um texto | Parâ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 fixos | const 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ódigo | O 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 - s | Quantas 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:
2. Andar uma letra
Complete o laço que percorre a string até o fim:
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ânciap - sé o número de letras. - Uma função com parâmetro
char *spode mudar o texto de quem chama; se só lê, useconst 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). Prefiraconst 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.