C++ Acessar Caracteres
Uma string é um "colar" de caracteres em ordem (aula Strings). Muitas vezes você precisa de uma conta só desse colar: a primeira letra do nome para fazer as iniciais, a última letra para saber se a palavra termina em "s", cada letra da senha para trocá-la por *. Nesta aula você vai aprender a pegar um caractere pela posição, a trocá-lo, a passar por todos eles, um por um, e o que acontece quando você pede uma posição que não existe.
- O que é um índice e por que a primeira posição é 0.
- Como pegar um caractere com
s[0]e coms.at(0), e a diferença entre os dois. - O que acontece com
s.at(10)(a exceçãostd::out_of_range) e coms[10](comportamento indefinido). - Os atalhos
.front()e.back(). - Como trocar um caractere:
s[0] = 'p';. - Como percorrer a string com
fore comfor (char c : s).
Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana, na pasta ~/cpp, com o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.
A ideia: casas numeradas a partir do zero
Imagine os caracteres de "Bruno" em caixinhas lado a lado, cada uma com um número na frente. Esse número é o índice (a "posição") do caractere. Em C++, como em quase todas as linguagens, a contagem começa do 0, não do 1:
| Índice | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Caractere | B | r | u | n | o |
Pense no índice como "quantos passos andar a partir do começo". Para chegar no B, você não anda nada: índice 0. Para o r, anda um passo: índice 1. Daí vêm as duas regras que você vai usar o tempo todo:
- o primeiro caractere está no índice
0; - o último está no índice
size() - 1."Bruno"tem 5 caracteres, então o último é o 4. Não existe índice 5.
1. Pegar um caractere com colchetes: s[i]
#include <iostream>
#include <string>
int main() {
std::string nome = "Bruno";
std::cout << nome[0] << '\n';
std::cout << nome[1] << '\n';
std::cout << nome[4] << '\n';
std::cout << nome.at(2) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra indice.cpp -o indice $ ./indice B r o u
- nome[0]
- "O caractere de
nomena posição 0". Os colchetes[ ]recebem o índice. O resultado é umchar(um caractere só, veja Caracteres), não uma string. - nome[4]
- O último caractere de
"Bruno": índice 4 =size() - 1. - nome.at(2)
- Outra forma de pegar o caractere da posição 2.
atquer dizer "em" ("o caractere em 2"). Faz o mesmo quenome[2], com uma diferença importante que você vai ver na seção 3.
O índice pode ser qualquer conta que dê um número inteiro: nome[nome.size() - 1] é o último caractere, seja qual for o tamanho da string.
Os índices contam bytes, como o .size(). Em "maçã", o ç ocupa as posições 2 e 3, e s[2] pega só a metade dele. A aula Acentos e UTF-8 mostra o que aparece na tela nesse caso. Nesta aula, os exemplos usam palavras sem acento.
2. O primeiro e o último: .front() e .back()
Pegar a primeira e a última letra é tão comum que existem dois atalhos: front ("frente") e back ("fundo", "costas").
#include <iostream>
#include <string>
int main() {
std::string fruta = "banana";
std::cout << "primeira: " << fruta.front() << '\n';
std::cout << "última: " << fruta.back() << '\n';
std::cout << "última (com índice): " << fruta[fruta.size() - 1] << '\n';
}
$ g++ -std=c++23 -Wall -Wextra ultimo.cpp -o ultimo $ ./ultimo primeira: b última: a última (com índice): a
| Atalho | É o mesmo que | Em "banana" |
|---|---|---|
s.front() | s[0] | b |
s.back() | s[s.size() - 1] | a |
Prefira .back() a s[s.size() - 1]: é mais curto e não dá chance de esquecer o - 1.
Uma string vazia não tem primeiro nem último caractere. .front(), .back() e s[0] numa string vazia são comportamento indefinido (seção 3). Antes, pergunte com if (!s.empty()) (veja Strings e if e else).
3. Fora do intervalo: at × colchetes
E se você pedir uma posição que não existe? É aqui que [ ] e .at() se separam.
.at() confere e "lança uma exceção"
#include <iostream>
#include <string>
int main() {
std::string nome = "Ana";
std::cout << "antes\n";
std::cout << nome.at(10) << '\n';
std::cout << "depois\n";
}
$ g++ -std=c++23 -Wall -Wextra at.cpp -o at $ ./at antes terminate called after throwing an instance of 'std::out_of_range' what(): basic_string::at: __n (which is 10) >= this->size() (which is 3) Abortado (imagem do núcleo gravada) ./at $ echo $? 134
Vamos ler essa mensagem linha por linha:
- antes
- A primeira linha rodou normalmente.
- terminate called after throwing an instance of 'std::out_of_range'
- "
terminatechamado depois de lançar umstd::out_of_range". O.at()percebeu que 10 está fora e lançou uma exceção: um "alarme" do C++ que diz "deu errado, alguém precisa tratar isso".out_of_range= "fora do intervalo". Como ninguém tratou o alarme, o C++ chamouterminate("encerrar") e o programa morreu. - what(): basic_string::at: __n (which is 10) >= this->size() (which is 3)
- O
what()("o quê") é a explicação: o índice pedido (__n) foi 10, e ele é maior ou igual ao tamanho, que é 3. É exatamente o seu erro, com os números. - Abortado (imagem do núcleo gravada)
- Esta linha é do Bash, não do programa: ele avisa que o programa foi abortado (o sinal
SIGABRT). "Imagem do núcleo" (core dump) é uma fotografia da memória que o sistema pode guardar para depuração. - 134
- O código de saída de um programa abortado (128 + 6, o número do sinal). O
depoisnunca apareceu.
Parece ruim, mas é o comportamento bom: o programa parou no lugar certo, com uma mensagem que diz o que aconteceu. Uma exceção também pode ser capturada e tratada, para o programa continuar ("índice inválido, tente de novo"); isso é assunto de Exceções (try e catch).
Colchetes não prometem nada: comportamento indefinido
O mesmo programa com colchetes:
#include <iostream>
#include <string>
int main() {
std::string nome = "Ana";
std::cout << "antes\n";
std::cout << "[" << nome[10] << "]\n";
std::cout << "depois\n";
}
Os colchetes não conferem o índice. Eles foram feitos para ser rápidos: calculam "começo + 10" e leem o que estiver lá. Ler fora da string é comportamento indefinido (em inglês, undefined behavior, ou UB): a regra do C++ diz que, a partir daí, o programa pode fazer qualquer coisa. Mostrar lixo, mostrar nada, travar, ou parecer funcionar e falhar meses depois. Veja o mesmo código, compilado de dois jeitos:
$ g++ -std=c++23 -Wall -Wextra colchete.cpp -o colchete $ ./colchete antes /usr/include/c++/15/bits/basic_string.h:1369: constexpr std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::operator[](size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; reference = char&; size_type = long unsigned int]: Assertion '__pos <= size()' failed. Abortado (imagem do núcleo gravada) ./colchete
Sem otimização, a biblioteca do g++ 15 liga umas conferências extras (as assertions) e parou o programa: "Assertion '__pos <= size()' failed" = "a conferência posição ≤ tamanho falhou". Agora com -O2, a opção que deixa o programa mais rápido (veja Compilar com g++):
$ g++ -std=c++23 -Wall -Wextra -O2 colchete.cpp -o colchete
colchete.cpp: In function ‘int main()’:
colchete.cpp:7:37: warning: ‘((__gnu_cxx::__alloc_traits<std::allocator<char>, char>::value_type*)((char*)&nome + offsetof(std::__cxx11::string, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::<unnamed>)))[10]’ is used uninitialized [-Wuninitialized]
7 | std::cout << "[" << nome[10] << "]\n";
| ^~~~~
colchete.cpp:5:17: note: ‘nome’ declared here
5 | std::string nome = "Ana";
| ^~~~
$ ./colchete | od -c
0000000 a n t e s \n [ 260 ] \n d e p o i s
0000020 \n
0000021
Desta vez o programa não parou: entre os colchetes saiu um byte de lixo (o 260 que o od -c mostra; na tela ele aparece como nada ou como �), e o depois foi impresso como se tudo estivesse bem. O aviso do compilador, apesar de assustador, diz o essencial: "o caractere 10 de nome é usado sem ter sido inicializado". As conferências extras somem com -O2, e em outros compiladores elas nem existem. Não conte com elas.
s[i] | s.at(i) | |
|---|---|---|
| Índice válido | Devolve o caractere | Devolve o caractere |
| Índice fora | Comportamento indefinido: lixo, travamento ou nada | Lança std::out_of_range, sempre |
| Velocidade | A mais rápida | Um tiquinho mais lenta (faz uma comparação) |
| Quando usar | Quando o índice com certeza é válido, como num laço de 0 a size() - 1 | Quando o índice vem de fora (o usuário digitou, veio de um arquivo, de uma conta) |
s[s.size()] é permitido
Uma única posição "fora" é válida com colchetes: s[s.size()] devolve o caractere nulo '\0', que todo std::string guarda escondido no fim, por compatibilidade com a linguagem C (aula Strings). Por isso a conferência da biblioteca diz __pos <= size(), com "menor ou igual". Mas não há motivo para ler esse '\0'; e com .at(s.size()), é exceção.
Comportamento indefinido é um dos assuntos mais importantes do C++, e tem uma aula só para ele: Comportamento Indefinido. Lá você também vai ver os sanitizers, que pegam esse tipo de erro na hora.
4. Mudar um caractere
Os colchetes (e o .at(), o .front() e o .back()) também servem para escrever. Basta colocá-los do lado esquerdo do =:
#include <iostream>
#include <string>
int main() {
std::string palavra = "gato";
palavra[0] = 'p';
std::cout << palavra << '\n';
palavra.back() = 'a';
std::cout << palavra << '\n';
palavra.at(1) = 'i';
std::cout << palavra << '\n';
}
$ g++ -std=c++23 -Wall -Wextra mudar.cpp -o mudar $ ./mudar pato pata pita
- palavra[0] = 'p';
- Troca o caractere da posição 0 (
g) porp. O valor é umchar, então vai entre aspas simples. - palavra.back() = 'a';
- Troca o último (
o) pora. - palavra.at(1) = 'i';
- Troca a posição 1 (
a) pori, com conferência de índice.
Assim você troca um caractere por um caractere; o tamanho da string não muda. Para trocar um pedaço por outro de tamanho diferente (por exemplo, "gato" por "cachorro" no meio de uma frase), use .replace(), em Funções de String.
Missão: alguém digitou o nome todo em minúsculas. Sem reescrever as strings, troque a primeira letra de cada uma por maiúscula ('A' e 'S'), usando [0] ou .front(), para o programa mostrar Ana Souza.
#include <iostream>
#include <string>
int main() {
std::string nome = "ana";
std::string sobrenome = "souza";
// troque as primeiras letras aqui
std::cout << nome << " " << sobrenome << '\n';
}
#include <iostream>
#include <string>
int main() {
std::string nome = "ana";
std::string sobrenome = "souza";
nome[0] = 'A';
sobrenome[0] = 'S';
std::cout << nome << " " << sobrenome << '\n';
}
5. Percorrer a string com for
Para fazer algo com cada caractere (mostrar, contar, trocar), você precisa de uma repetição: "para cada posição de 0 até o fim, faça isto". O for faz exatamente isso. Ele tem uma aula própria mais adiante (for); aqui basta entender este modelo, que você vai usar muito:
#include <iostream>
#include <string>
int main() {
std::string nome = "Bia";
for (std::size_t i = 0; i < nome.size(); i++) {
std::cout << i << ": " << nome[i] << '\n';
}
}
$ g++ -std=c++23 -Wall -Wextra percorrer.cpp -o percorrer $ ./percorrer 0: B 1: i 2: a
- for ( … ; … ; … )
- "Para": repete o bloco entre
{ }. Os parênteses têm três partes separadas por;. - std::size_t i = 0
- Começo: cria o contador
ivalendo 0, o primeiro índice.std::size_té o tipo de número que o.size()devolve: um inteiro sem sinal (nunca negativo), feito para tamanhos e posições (veja Inteiros). - i < nome.size()
- Condição: repete enquanto
ifor menor que o tamanho. Com tamanho 3,ipassa por 0, 1 e 2, e para antes do 3, que não existe. É<, nunca<=. - i++
- Depois de cada volta, soma 1 em
i(veja ++ e --). - nome[i]
- O caractere da posição atual. Aqui os colchetes são seguros: a condição garante que
ié sempre um índice válido.
int?
Muita gente escreve int i. Funciona, mas o -Wall avisa:
$ g++ -std=c++23 -Wall -Wextra percorrer2.cpp -o percorrer2
percorrer2.cpp: In function ‘int main()’:
percorrer2.cpp:6:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::__cxx11::basic_string<char>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
6 | for (int i = 0; i < nome.size(); i++) {
| ~~^~~~~~~~~~~~~
$ ./percorrer2
0: B
1: i
2: a
"Comparação de inteiros com sinais diferentes: int (com sinal, pode ser negativo) e size_type (sem sinal)". Comparar os dois pode dar resultados errados com números negativos (veja Avisos). A solução é usar std::size_t no contador, como no exemplo acima.
6. O jeito mais curto: for (char c : s)
Quando você só precisa de cada caractere, e não da posição dele, existe uma forma mais simples, o for de intervalo (range-for, aula for de Intervalo). Leia como: "para cada char c em palavra".
#include <iostream>
#include <string>
int main() {
std::string palavra = "banana";
int quantos = 0;
for (char c : palavra) {
std::cout << c << ' ';
if (c == 'a') {
quantos++;
}
}
std::cout << '\n' << "letras a: " << quantos << '\n';
}
$ g++ -std=c++23 -Wall -Wextra rangefor.cpp -o rangefor $ ./rangefor b a n a n a letras a: 3
- for (char c : palavra)
- Repete o bloco uma vez para cada caractere. Na primeira volta,
cvale'b'; na segunda,'a'; e assim até o fim. Não tem índice, não temsize(), não tem como sair do intervalo. - if (c == 'a')
- "Se
cfor igual a'a'" (veja if e else). Compare umcharcom outrochar: aspas simples. - quantos++
- Conta mais um.
Para mudar os caracteres: char&
No for (char c : s), o c é uma cópia de cada caractere. Mudar a cópia não muda a string. Veja:
#include <iostream>
#include <string>
int main() {
std::string senha = "segredo";
for (char c : senha) {
c = '*';
}
std::cout << senha << '\n';
for (char& c : senha) {
c = '*';
}
std::cout << senha << '\n';
}
$ g++ -std=c++23 -Wall -Wextra refor.cpp -o refor
refor.cpp: In function ‘int main()’:
refor.cpp:6:15: warning: variable ‘c’ set but not used [-Wunused-but-set-variable]
6 | for (char c : senha) {
| ^
$ ./refor
segredo « o primeiro laço só mudou cópias
******* « o segundo, com char&, mudou a string
O compilador até avisou: "a variável c recebe valor mas nunca é usada", porque mudar uma cópia que ninguém lê não serve para nada. O & em char& c transforma o c numa referência: um "outro nome" para o próprio caractere dentro da string, e não uma cópia. Referências têm aula própria no Nível 2 (Referências); por enquanto, guarde: para ler, char c; para mudar, char& c.
Qual laço usar?
| Laço | Quando usar |
|---|---|
for (char c : s) | Só ler cada caractere (contar, procurar, mostrar). O mais simples e seguro. |
for (char& c : s) | Mudar cada caractere (esconder senha, passar para maiúsculas). |
for (std::size_t i = 0; i < s.size(); i++) | Quando você precisa da posição (mostrar o índice, olhar o vizinho s[i + 1], pular de dois em dois). |
Quando dá errado
terminate called after throwing an instance of 'std::out_of_range'
O que significa: um .at(i) recebeu um índice maior ou igual ao tamanho. A linha what(): mostra os dois números: __n (which is 10) >= this->size() (which is 3).
Como consertar: lembre que o último índice é size() - 1. Se o índice vem do usuário, confira antes com if (i < s.size()), ou trate a exceção (Exceções).
Assertion '__pos <= size()' failed.
O que significa: um s[i] com índice fora da string. A biblioteca do g++ 15 pegou o erro porque o programa foi compilado sem otimização. Com -O2, ou em outro compilador, o mesmo erro passaria em silêncio, mostrando lixo.
Como consertar: o mesmo que acima. Num laço, confira se a condição é i < s.size() (e não <=). Quando o índice não é garantido, use .at().
error: ISO C++ forbids comparison between pointer and integer
#include <iostream>
#include <string>
int main() {
std::string nome = "Ana";
if (nome[0] == "A") {
std::cout << "Começa com A\n";
}
}
$ g++ -std=c++23 -Wall -Wextra aspasdupla.cpp -o aspasdupla
aspasdupla.cpp: In function ‘int main()’:
aspasdupla.cpp:6:17: warning: comparison with string literal results in unspecified behavior [-Waddress]
6 | if (nome[0] == "A") {
aspasdupla.cpp:6:17: error: ISO C++ forbids comparison between pointer and integer [-fpermissive]
O que significa: nome[0] é um char (que, por dentro, é um número inteiro) e "A", com aspas duplas, é um literal de texto (que vira um endereço, um "ponteiro"). O C++ proíbe comparar os dois.
Como consertar: um caractere se compara com um caractere: nome[0] == 'A', com aspas simples.
warning: comparison of integer expressions of different signedness
O que significa: o contador do laço é int e o .size() é sem sinal.
Como consertar: use std::size_t i, ou troque o laço por for (char c : s), que não tem contador.
O que significa: você escreveu for (char c : s) e mudou o c, que é uma cópia. O sinal é o aviso variable ‘c’ set but not used.
Como consertar: for (char& c : s).
Referência rápida
| Escrita | O que faz | Fora do intervalo |
|---|---|---|
s[i] | Caractere da posição i (0 = primeiro) | Comportamento indefinido |
s.at(i) | O mesmo, conferindo o índice | Lança std::out_of_range |
s.front() | Primeiro caractere (s[0]) | String vazia: indefinido |
s.back() | Último caractere (s[s.size() - 1]) | String vazia: indefinido |
s[i] = 'x'; | Troca o caractere da posição i | Como s[i] |
for (std::size_t i = 0; i < s.size(); i++) | Percorre com a posição | Seguro com < |
for (char c : s) | Percorre lendo cópias | Sempre seguro |
for (char& c : s) | Percorre podendo mudar | Sempre seguro |
Exercícios
1. O primeiro índice
Complete para mostrar a primeira letra de nome:
2. O último, com atalho
Complete para mostrar a última letra de nome:
3. Percorrer mudando
Complete o laço para poder mudar cada caractere de s:
4. Em std::string s = "sol";, qual é o índice do l?
5. O que acontece com std::string s = "sol"; std::cout << s.at(3);?
6. E com s[10] na mesma string?
Missão final: o raio-x da palavra
Missão: para a palavra "paralelepipedo", mostre a primeira letra, a última e quantas letras e ela tem, contando com um laço for (nada de contar com o dedo e escrever o número):
Primeira: p Última: o Letras e: 3
#include <iostream>
#include <string>
int main() {
std::string palavra = "paralelepipedo";
int quantos = 0;
// seu código aqui
}
#include <iostream>
#include <string>
int main() {
std::string palavra = "paralelepipedo";
int quantos = 0;
for (char c : palavra) {
if (c == 'e') {
quantos++;
}
}
std::cout << "Primeira: " << palavra.front() << '\n';
std::cout << "Última: " << palavra.back() << '\n';
std::cout << "Letras e: " << quantos << '\n';
}
Resumo
- Cada caractere tem um índice: o primeiro é
0, o último ésize() - 1. Índices contam bytes (acentos ocupam dois). s[i]es.at(i)pegam (e trocam) o caractere da posiçãoi. O resultado é umchar: compare com aspas simples..at()fora do intervalo lançastd::out_of_range.[ ]fora do intervalo é comportamento indefinido: o g++ 15 sem otimização aborta comAssertion, mas com-O2sai lixo..front()e.back()são o primeiro e o último; nunca numa string vazia.- Percorra com
for (char c : s)para ler,for (char& c : s)para mudar efor (std::size_t i = 0; i < s.size(); i++)quando precisar da posição.