Operações com ponteiros em C++ – 5 operações que você precisa conhecer

Dedo apontando para o texto 5 Operações com ponteiros em C++

Vimos no artigo O que é um ponteiro em C++? Que bicho é esse? o que são os ponteiros e como utilizá-los, mas não pudemos falar de um assunto importante acerca deles: as operações com ponteiros. Em C++, assim como é possível realizar operações com os tipos padrões da linguagem (inteiros, doubles etc.), também é possível realizar operações com ponteiros; os símbolos usados são os mesmos (por exemplo: +, -, ++, –), mas seus efeitos são diferentes quando aplicados ao ponteiros. Iremos começar pelo uso de ifs com ponteiros (#1), e então veremos como usar os operadores + (#2), ++ (#3), – (#4) e — (#5). Vejamos, então, que história é essa de operações com ponteiros em C++.

Operações com ponteiros em C++ #1 – If com ponteiros

Quando tratamos dos ponteiros em C++ anteriormente, eu disse que tentar acessar o conteúdo de um ponteiro nulo (para refrescar a memória sobre o assunto, consulte o artigo sobre ponteiros) resulta em um erro de segmentation fault, que pode ser difícil de se debugar até mesmo para programadores experientes. Logo, para evitar esse problema e alguns outros que lhe são semelhantes, é útil verificar que não temos um ponteiro nulo antes de tentar utilizar o operador *. Mas como fazemos isso? O modo mais simples de se verificar que um ponteiro é válido é utilizar o bom e velho if: basta passar o ponteiro como argumento do if, ou então compará-lo com o valor nullptr. Veja a seguir um exemplo desse tipo de verificação realizado dos dois modos.

#include <iostream>

using namespace std;

char* charPtr = nullptr;

if (charPtr) { // ponteiro como argumento direto do if
    cout << "Ponteiro não-nulo." << endl;
}
else if (charPtr != nullptr) {
    cout << "Ponteiro não é um nullptr." << endl;
}
else {
    cout << "O ponteiro em questão é nulo." << endl;
}

/*********************************************************
* Texto mostrado na saída: "O ponteiro em questão é nulo"
*********************************************************/

Aritmética de ponteiros em C++: operadores -, +, — e ++

Assim como os utilizamos com os tipos aritméticos padrões, também podemos utilizar os operadores de adição e subtração (+ e -), e os operadores de incremento (++) e decremento (–) com ponteiros. Todavia, como se pode imaginar, o efeito que esses operadores têm sobre os ponteiros não são os mesmo que eles possuem sobre os outros tipos primitivos. Vejamos, então, como funcionam os operadores aritméticos quando aplicados aos ponteiros.

Para facilitar as coisas, usarei o exemplo de um ponteiro que aponta para um array estático, ou array de tamanho fixo (ambos não são correspondências diretas do inglês built-in arrays, mas foram as melhores que eu encontrei).


#include <iostream>

int main() {
    const char arrayDeChars[] = {'a', 'b', 'c', 'd', 'e'};
    // charPtr é um ponteiro para const chars, e não um ponteiro constante para chars
    const char* charPtr = arrayDeChars;
    
    // Operações para mover o ponteiro para a "frente" em direção ao fim do array
    std::cout << "Primeiro caractere do array: " << *charPtr << std::endl;

    ++charPtr; // Operador de incremento
    std::cout << "Segundo caractere do array: " << *charPtr << std::endl;

    charPtr = charPtr + 1; // Deslocamento do ponteiro 1 posição para a frente
    std::cout << "Terceiro caractere do array: " << *charPtr << std::endl;

    charPtr += 1; // Versão simplificada da expressão da linha 13
    std::cout << "Quarto caractere do array: " << *charPtr << std::endl;

    ++charPtr;
    std::cout << "Quinto caractere do array: " << *charPtr << std::endl;

    ++charPtr;
    std::cout << "Último caractere do array - null terminator \\0: " << *charPtr << std::endl;

    ++charPtr;
    std::cout << "Uma posição além do último caractere do array: " << *charPtr << std::endl;
    
    // Operações para mover o ponteiro em direção ao início do array
    std::cout << "-------------------------------------------------" << std::endl;
    --charPtr; // Operador de decremento

    std::cout << "Último caractere do array - null terminator \\0: " << *charPtr << std::endl;
    charPtr -= 1; // Versão simplificada da expressão da linha 14

    std::cout << "Quinto caractere do array: " << *charPtr << std::endl;
    charPtr = charPtr - 1;

    std::cout << "Quarto caractere do array: " << *charPtr << std::endl;
    charPtr -= 3;

    std::cout << "Primeiro caractere do array: " << *charPtr << std::endl;
    --charPtr; // Operador de decremento

    std::cout << "Conteúdo da posição anterior ao primeiro char do array: " << *charPtr << std::endl;

    return 0;
}
Resultado da execução do código exemplo de operações aritméticas com ponteiros em C++.
Resultado da execução do código exemplo de operações aritméticas com ponteiros – executado em Programiz.

No exemplo acima, começamos criando um array estático que contém as letras ‘a’, ‘b’, ‘c’, ‘d’ e ‘e’ (note que ao utilizar a inicialização por lista de valores, com { }, não é preciso fornecer o tamanho do array entre colchetes). Dados os valores do array, o seu tamanho é de 5 elementos, sim? Na verdade, não. Como veremos no artigo dedicado aos arrais estáticos em C++, aos arrays de caracteres é adicionado como último elemento o caractere nulo ‘\0’, que indica o fim da cadeira de caracteres no array. Logo, o tamanho verdadeiro da variável arrayDeChars é 6, e não 5.

char array estático usado para se inicializar um ponteiro para char - o null caracter é adicionado ao fim do array.
Conteúdos do array de caracteres arrayDeChars

Ponteiros em C++ para arrays estáticos

Logo após definir a variável arrayDeChars criamos o ponteiro para const char chamado charPtr (isso significa que o caractere para o qual ele aponta não pode ser modificado), e ele é inicializado utilizando arrayDeChars (linha 6). Até aqui quase tudo bem, mas há algo estranho na inicialização de charPtr: não se deve inicializar ponteiros usando o endereço dos objetos para o qual o ponteiro apontará (por exemplo, int* intPtr = &variavelInt)? Onde está então o operador de endereçamento & na inicialização de charPtr? Bem notado! O que acontece na linha 6 é o seguinte: por padrão, quando se utiliza um array estático para se inicializar um ponteiro, o ponteiro aponta para o primeiro elemento do array; portanto, para simplificar a notação no código, o compilador em C++ trata (na maioria dos casos) as duas linhas em ênfase a seguir como se fossem a mesma:

// Notação simplificada
const char* charPtr = arrayDeChars;

// Notação "completa"
const char* charPtr = &arrayDeChars[0];

Operações com ponteiros em C++ #2 e #3 – Operador + e operador de incremento (++)

Após a inicialização da variável charPtr, utilizamos o operador * para acessar o objeto para o qual ela aponta (linha 9), e vemos na saída do programa a letra ‘a’. Como vimos acima, isto se dá porque o ponteiro aponta para o primeiro elemento do array. Em seguida, na linha 11, usamos o operador de incremento ++ para avançar o ponteiro “de uma posição” e fazê-lo apontar para o próximo caractere no array: a letra ‘b’.

Operadores de incremento ++ e decremento —

O operador de incremento, quando usado em tipos aritméticos normais (inteiros, doubles etc.), incrementa em uma unidade o valor da variável; por exemplo, se a variável temperatura é um inteiro cujo valor é 31, ao fazermos ++temperatura o seu valor passa a ser 32.

Todavia, quando se trata de ponteiros em C++, o operador de incremento faz com que o ponteiro aponte para o endereço de memória seguinte àquele ao qual ele aponta atualmente. Logo, supondo que o ponteiro para int intPtr aponte para o primeiro elemento do array [1, 2, 3, 4] (o número 1), ao fazer ++intPtr o ponteiro passará a apontar para o segundo elemento – o número 2.

O mesmo comportamento, mas no sentido contrário (de deslocar o ponteiro “para trás”), é obtido ao se utilizar o operador de decremento –. Tomando o exemplo do parágrafo anterior, no qual intPtr aponta para o número 2 (segundo elemento do array), se fizermos –intPtr retornaremos o ponteiro uma posição no array, fazendo-o apontar novamente para o número 1. Para os tipos aritméticos normais, o operador — reduz o valor da variável em 1 unidade.

Em seguida, na linha 14 utilizamos o operador + para somar ao endereço que está atualmente armazenado no nosso ponteiro charPtr o valor 1: isto resulta no deslocamento do ponteiro “para a frente” de 1 posição; assim, o ponteiro passa a apontar para a letra ‘c’ no array. Note que nessa linha há duas operaçoes: uma operação de soma (operador +) de 1 unidade ao endereço atual contido em charPtr (fazendo-o apontar para a letra ‘c’ ao invés da letra ‘b’), e uma operação de atribuição (com o opereador =) desse novo valor de endereço de volta à variável charPtr – essas duas etapas acontecem de forma explícita.

Esse procedimento que acabei de descrever acontece também na linha 17. Contudo, ele acontece de forma implícita: o operador += tem exatamente o mesmo efeito de se utilizar os operadores + e = combinados, como é feito na linha 14. Primeiro adiciona-se o valor 1 ao conteúdo de charPtr, que passa a ter o endereço da letra ‘d’ do array, e em seguida se armazena o endereço atualizado de volta em charPtr.

Dica – operador += e a combinação = e +

Por via de regra, o uso do operador de atribuição = com o operador + como em var = var + 1 tem o mesmo efeito de se utilizar o operador +=, como em var += 1. O efeito pode ser diferente apenas para o uso desses operadores em objetos de classes, para os quais pode haver sobrecarga dos operadores em questão.

Acesso a uma posição de memória além do fim do array usando ponteiros em C++

Nas linhas 26 e 27 do exemplo, deslocamos o ponteiro para uma posição além do final do array – imediatamente antes havíamos imprimido na tela o caractere \0 (veja o ponto vermelho na sexta linha do resultado da execução do código) -, e vemos algo curioso acontecer: o caractere ? dentro de um losango preto é mostrado na saída; o que isso significa? Isso quer dizer que acessamos uma área de memória na qual o valor contido não podia ser representado com um caractere comum, e portanto tal valor foi mostrado na saída como essa interrogação “mascarada”.

Portanto, se executássemos o código outra vez, o valor de saída seria talvez diferente, visto que a região de memória que estamos acessando está fora do array e seu valor pode ser absolutamente aleatório (podendo até mesmo ser um valor válido, como o número 5 ou a letra x, por exemplo). Em resumo: o comportamento de tal operação de acesso é indefinido e o programador deve se certificar de sempre acessar apenas regiões de memória “conhecidas” e com valores válidos.

Operações com ponteiros em C++ #4 e #5 – Operador – e operador de decremento (–)

Tendo chegado além do fim do array, na linha 31, começamos a fazer o caminho reverso. Nessa linha utiliza-se o operador de decremento —, que funciona de maneira análoga ao operador de incremento, mas na direção reversa (para saber mais sobre ambos, consulte o artigo do Cpp Reference sobre o assunto). Logo, retornamos o ponteiro charPtr ao caractere de terminação ao fim do array. Em seguida, utilizamos o operador -=, e adivinhem o que acontece? Isso mesmo! Ele decrementa a variável à sua esquerda do valor à sua direita, e em seguida armazena o resultado de volta na variável à esquerda. Por exemplo, se a é uma variável inteira de valor 4, fazer a -= 4; fará com a passe a ter o valor 0 (algo semelhante a isto acontece na linha 40 do exemplo).

Com o mesmo efeito, na linha 37, retornamos o ponteiro uma posição no array ao fazer charPtr = charPtr - 1;. Até aqui, tudo bem.

Então, continuamos decrementando o valor do endereço armazenado no ponteiro até chegarmos a uma posição anterior ao início do array. Que acontece, nesse caso, se tentarmos usar o operador * para acessar o conteúdo do endereço para o qual o ponteiro aponta? Como podemos ver na última linha da figura com o resultado da execução do código, o valor impresso na tela é o mesmo que aquele que fora impresso para o null character \0: o pontinho vermelho.

Contudo, esse comportamento na verdade é indefinido, pois estamos acessando um elemento que não faz parte do array, e portanto pode ter qualquer valor (a depender do conteúdo do seu endereço de memória, que pode muito bem nem mesmo ter sido inicializado, ou pode conter “lixo”, como se diz na programação).

Dica – nunca acesse regiões de memória desconhecidas

Sempre que for trabalhar com ponteiros, certifique-se de que o ponteiro aponta para uma região “conhecida” e inicializada de memória, para evitar comportamentos indefinidos no programa e possíveis bugs difíceis de se identificar e resolver.

Conclusão

Neste artigo vimos como se realizar operações que já conhecemos e utilizamos com outros tipos (inteiros, double etc.), mas dessa vez com ponteiros em C++. Começamos com o uso dos ifs com ponteiros, para verificar a sua validade, e em seguida passamos ao uso dos operadores aritméticos +, -, ++ e — com os ponteiros. Além disso, ainda vimos que podemos aplicar os operadores += e -= com ponteiros, e o que acontece quando tentamos acessar elementos além dos limites de um array estático.

Foto de perfil de Emanoel

Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.

2 thoughts on “Operações com ponteiros em C++ – 5 operações que você precisa conhecer

Leave a Reply

Your email address will not be published. Required fields are marked *