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.

Você vai aprender
  • O que é um índice e por que a primeira posição é 0.
  • Como pegar um caractere com s[0] e com s.at(0), e a diferença entre os dois.
  • O que acontece com s.at(10) (a exceção std::out_of_range) e com s[10] (comportamento indefinido).
  • Os atalhos .front() e .back().
  • Como trocar um caractere: s[0] = 'p';.
  • Como percorrer a string com for e com for (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:

Índice01234
CaractereBruno

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 nome na posição 0". Os colchetes [ ] recebem o índice. O resultado é um char (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. at quer dizer "em" ("o caractere em 2"). Faz o mesmo que nome[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.

Acentos ocupam duas caixinhas

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 queEm "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.

Nunca numa string vazia

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'
"terminate chamado depois de lançar um std::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++ chamou terminate ("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 depois nunca 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álidoDevolve o caractereDevolve o caractere
Índice foraComportamento indefinido: lixo, travamento ou nadaLança std::out_of_range, sempre
VelocidadeA mais rápidaUm tiquinho mais lenta (faz uma comparação)
Quando usarQuando o índice com certeza é válido, como num laço de 0 a size() - 1Quando o índice vem de fora (o usuário digitou, veio de um arquivo, de uma conta)
Curiosidade: 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) por p. O valor é um char, então vai entre aspas simples.
palavra.back() = 'a';
Troca o último (o) por a.
palavra.at(1) = 'i';
Troca a posição 1 (a) por i, 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 i valendo 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 i for menor que o tamanho. Com tamanho 3, i passa 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.
E se o contador for 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, c vale 'b'; na segunda, 'a'; e assim até o fim. Não tem índice, não tem size(), não tem como sair do intervalo.
if (c == 'a')
"Se c for igual a 'a'" (veja if e else). Compare um char com outro char: 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çoQuando 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 laço "muda" os caracteres, mas a string continua igual

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

EscritaO que fazFora do intervalo
s[i]Caractere da posição i (0 = primeiro)Comportamento indefinido
s.at(i)O mesmo, conferindo o índiceLanç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 iComo s[i]
for (std::size_t i = 0; i < s.size(); i++)Percorre com a posiçãoSeguro com <
for (char c : s)Percorre lendo cópiasSempre seguro
for (char& c : s)Percorre podendo mudarSempre seguro

Exercícios

1. O primeiro índice

Complete para mostrar a primeira letra de nome:

std::cout << nome[];

2. O último, com atalho

Complete para mostrar a última letra de nome:

std::cout << nome.;

3. Percorrer mudando

Complete o laço para poder mudar cada caractere de s:

for ( c : s) { c = '*'; }

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] e s.at(i) pegam (e trocam) o caractere da posição i. O resultado é um char: compare com aspas simples.
  • .at() fora do intervalo lança std::out_of_range. [ ] fora do intervalo é comportamento indefinido: o g++ 15 sem otimização aborta com Assertion, mas com -O2 sai 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 e for (std::size_t i = 0; i < s.size(); i++) quando precisar da posição.