Tipos em C++ | As 2 categorias principais: tipos aritméticos e Void

Pirâmide escrita fundamentos dos tipos em C++

Introdução

Os tipos são partes fundamentais em qualquer programa. Quando nos referimos aos tipos em C++, estamos falando do tipo dos objetos (ou variáveis) que criaremos nos nossos programas, objetos esses que constituem os elementos primordiais da nossa aplicação. Para entendê-los e compreender as funcionalidades mais avançadas da linguagem, portanto, é preciso partir das bases – dos tipos. Ao contrário do que acontece em outras linguagens cujos objetos têm seus tipos atribuídos dinamicamente (como Python, por exemplo), os tipos em C++ são atribuídos às variáveis de modo estático, isto é, durante sua declaração: eles devem ser definidos de modo explícito ou por inferência através de mecanismos como auto ou decltype. Mas espere um segundo… Do que você está falando? Que são esses tais tipos? Para que servem? Como eles se relacionam entre si?

Para responder às perguntas acima, começarei abordando brevemente o que são os tipos primitivos em C++ e em seguida falarei dos principais tipos aritméticos da linguagem; tratarei, então, dos conceitos signed e unsigned que se aplicam a estes tipos; falarei do tipo void, e concluirei dando algumas dicas de quais tipos usar em diferentes situações.

P.S.: Este artigo pretende tratar dos principais pontos importantes acerca dos tipos primitivos em C++, sem aprofundar-se muito em alguns deles: isto será feito em artigos dedicados para os pontos que precisarem ser tratados com mais detalhes.

Tipos primitivos em C++

Nesta categoria estão os tipos fundamentais da linguagem, os quais servem de base para os tipos definidos pelo usuário (tratarei disso em um artigo posterior). Aqui temos os tipos aritméticos e o tipo especial void. Falemos primeiro daquele, e depois retornemos a este.

Tipos aritméticos

Inteiros, shorts, longs e long longs constituem a primeira sub-categoria dos tipos aritméticos: os tipos inteiros; além disso, nestes também estão incluídos os tipos char e bool. A segunda categoria, a dos tipos de ponto flutuante, é composta pelos floats, doubles e long doubles. A tabela a seguir resume a categoria em que se encontram cada um desses tipos, assim como o tamanho mínimo que eles possuem de acordo com o padrão C++, definido pelo Comitê de Padronização de C++ (tradução livre do inglês C++ Standard Comittee).

Categoria do tipoNome do tipoSignificadoTamanho mínimo
InteiroboolBooleanoND
charCaractere8 bits
shortInteiro curto16 bits
intInteiro16 bits
longInteiro longo32 bits
long longInteiro longo duplo (long long int)64 bits
Ponto flutuantefloatPonto flutuante de precisão simples6 dígitos significativos
doublePonto flutuante de precisão dupla10 dígitos significativos
long doublePonto flutuante longo de precisão dupla10 dígitos significativos
Tabela 1 – Tamanhos do tipos aritméticos por categoria segundo o padrão C++ vigente.

Para entender todos esses tipos, é preciso compreender o tamanho que cada um deles toma na memória da máquina que executa o programa.


Um byte é a menor unidade de memória endereçável, e possui 8 bits na maioria das máquinas atuais. Uma palavra (word) é a unidade básica de armazenamento de uma máquina, e é constituída de uma pequena quantidade de bytes (normalmente 4 ou 8).


Bool

O tipo mais simples entre os tipos aritméticos é o bool: ele serve apenas para representar dois valores: verdadeiro ou falso (ou true e false, em inglês). Em teoria, ele pode ser representado com apenas um bit, tendo o valor 1 deste único bit o sentido de verdadeiro, e o valor zero o significado oposto (falso). Todavia, como o byte é a menor unidade de memória endereçável – à qual podemos dar um nome através de uma variável, por exemplo -, não se pode criar booleanos que usem apenas 1 bit de memória (apesar de se poder utilizar truques para que cada bit de um byte represente um valor booleano diferente).

A principal funcionalidade dos bools é auxiliar na tomada de decisões durante a execução do programa. Suponha que você está escrevendo um programa de compras de supermercado: a decisão de comprar ou não um item da sua lista de compras poderia ser feita através de um bool chamado aindaTenhoEmCasa ; se o valor deste bool for true (ou verdadeiro), não comprarei o tal artigo da lista, pois ainda o possuo em casa; se o valor for false, contudo, porei o item no carrinho e o comprarei.

Char

Em seguida há o char, que é utilizado para representar caracteres individuais (como a letra A, o número 2, ou o ponto de interrogação, por exemplo). Esse tipo tem um tamanho mínimo de 8 bits, que é exatamente o tamanho de 1 byte na maioria dos sistemas atuais; assim sendo, o tamanho de um char é algumas vezes utilizado como um sinônimo para o byte. Para explicar os valores assumidos pelo char, preciso fazer um detour e falar das palavras-chaves (me referirei a elas pelo termo em inglês de agora em diante: keyword) signed e unsigned.

Signed e Unsigned

Os tipos aritméticos em C++ podem assumir valores negativos ou positivos, cujos limites são determinados pelo tipo em questão e pelo tamanho que tal tipo ocupa na memória do computador de acordo com o modelo de dados utilizado (p.e.: um inteiro, na maioria dos sistemas modernos, utiliza ao menos 32 bits, podendo conter valores na faixa de -2,147,483,648 a 2,147,483,647 – ainda que o padrão exija somente 16 bits de resolução mínima para um inteiro). Tais tipos, que suportam valores negativos, são chamados de signed: todos os tipos aritméticos mencionados até aqui são signed por padrão (com exceção de bool, a quem esse conceito não se aplica, e do char, cujo comportamento padrão depende do compilador utilizado). Em contrapartida, é possível modificar os tipos normais para obter suas versões unsigned, que aceitam apenas valores positivos. Para tornar um tipo normal em unsigned, basta adicionar-lhe como prefixo a keyword como mostrado a seguir.

// Tipos em C++ - exemplo de definição de um tipo unsigned

// temperaturaEmCel é apenas int e aceita valores negativos;
int temperaturaEmCel = -10;

// temperaturaEmKel é unsigned double, portanto espera apenas valores positivos
unsigned double temperaturaEmKel = 263.15;

Como se vê acima, não é necessário adicionar a keyword signed em frente ao tipo normal para que ele aceite valores negativos (apesar de ser permitido escrever signed int valorNegativo para declarar uma variável), apenas é preciso fazê-lo para os tipos unsigned.

Signeds, unsigneds… há alguma outra diferença entre eles? Como é de se esperar, sim. Por suportarem também valores negativos, os tipos signed possuem uma faixa de valores aceitáveis menor, pois utilizam o seu bit mais significativo (o primeiro da esquerda para a direita) para determinar o sinal do valor em questão. A diferença é resumida na tabela abaixo para inteiros de 32 bits.

TiposFaixa de valoresValores limite
int-232-1 a +232-1 – 1-2,147,483,648 e +2,147,483,647
unsigned int0 a +232-10 e 4,294,967,295
Tabela 2 – Diferenças nas faixas de valores suportados pelo tipo int em suas versões signed e unsigned.

Observando a tabela acima, pode-se perguntar: o que acontece se atribuirmos um valor negativo a uma variável de tipo unsigned? Neste caso, a conversão do valor original é feita através do complemento de 2: o complemento de 2 do valor negativo é usado para inicializar a variável unsigned. Em outras palavras: se o valor -10 for atribuido a um unsigned int, o valor que ele armazenará será (para inteiros de 32 bits) 232 – 10, ou 4,294,967,286. Este exemplo é mostrado abaixo.

// Tipos em C++ - exemplo de uso de um tipo unsigned para armazenar valores negativos
#include <iostream>

using namespace std;

int main()
{
    unsigned int temperaturaEmCelsius = -10;
    // Resultado: O valor resultante da conversão é: 4294967286
    cout << "O valor resultante da conversão é: " << temperaturaEmCelsius << endl;
    return 0;
}

De voltar ao char

Terminado nosso detour, retomarei de onde paramos na discussão dos chars. Esse tipo de dados armazena valores numéricos, ainda que seja utilizado para representar caracteres. Isso funciona da seguinte maneira: o valor armazenado em um char, 67 por exemplo, se impresso na tela será exibido como o caractere correspondente a si no padrão ASCII (Código padrão americano para intercâmbio de informações, ou American Standard Code for Information Interchange, em inglês), isto é, a letra “C”. Conhecer esse mecanismo de interpretação permite manipular chars como se fossem números para se obter caracteres desejados facilmente. Veja o exemplo abaixo para se passar do caractere “C” para o “M” apenas com uma manipulação simples.

// Tipos em C++ - exemplo de manipulaçao de chars
#include <iostream>

using namespace std;

int main()
{
    unsigned char meuChar = 67;
    // Resultado: O meu char é: C
    cout << "O meu char é: " << meuChar << endl;

    unsigned int modificador = 10;
    meuChar = meuChar + modificador; // ou meuChar += modificador;

    // Resultado: O meu novo char é: M
    cout << "O meu novo char é: " << meuChar << endl;

    return 0;
}

Uma característica importante dos chars é a de poderem ser signed ou unsigned de acordo com o compilador utilizado; ou seja: para evitar surpresas ao se trabalhar com chars, é sempre bom definir qual tipo se deseja, signed ou unsigned. Como escolher, então? Bem, vamos lá.

Por ter 8 bits na maioria das máquinas (lembre-se: este é o tamanho mínimo definido pelo padrão internacional), o char pode assumir valores que variam de -127 a 127 (ou de -128 a 127 na maioria sistemas modernos), e isto lhe permite representar todos os caracteres presentes no conjunto básico das máquinas (veja a Tabela ASCII para as correspondências entre os valores decimais de 0 a 127 e os caracteres). Na versão unsigned, o char com seus 8 bits pode conter valores de 0 a 255 (28– 1), o que permite representar um conjunto extendido de caracteres que contém o conjuto básico. Assim sendo, do ponto de vista prático, não há necessidade de usar unsigned chars para armazenar os caracteres padrões e mais comuns que utilizamos no cotidiano, mas fazê-lo traz mais clareza ao código, porque deixa clara a intenção da variável, através de seu tipo, de receber apenas valores positivos que representam caracteres, e não números pequenos sem significado evidente entre -128 e 127. Um uso comum dos unsigned chars, além do ordinário para caracteres normais e do conjunto extendido, é a representação de valores de intensidade de cores no padrão RGB, no qual cada componente de cor R, G e B tem valores na faixa de 0 a 255 (utilizar um inteiro para a mesma finalidade custaria 16 ou 32 bits, ao invés de 8, e não deixaria claro ao usuário qual é a faixa de valores desejada).

A título de informação, há ainda outros tipos de char que permitem utilizar uma gama muito maior de caracteres (mas que o usuário normal raramente precisará utilizar). Eles são os seguintes: wchar_t, char16_t e char32_t.

Inteiros

Esta é uma grande família, mas na grande maioria dos casos, apenas nos interessa o irmão do meio: o tipo int. Abaixo dele, como mostrado na Tabela 1, há o short (que freqüentemente é pequeno demais); acima, há o long (que na maioria das máquinas tem o mesmo tamanho do int), e o long long (que quase sempre é grande demais e não vale a pena em função do seu custo adicional de manipulação e memória). Dito isto, para que servem esses tipos? Todos eles servem para armazenar valores numéricos inteiros. Os valores máximos que cada tipo pode armazenar seguem a mesma regra aplicada aos chars (depende da quantidade de bits usada e se o tipo é signed ou unsigned).

Ainda que os ints sirvam para armazenar apenas valores inteiros, é possível inicializá-los com valores contendo parte fracionária (valores em ponto flutuante), e o resultado é simples: a parte à direita da vírgula é truncada, e mantêm-se apenas a porção inteira do número original. O exemplo abaixo ajuda a ilustrar esse caso.

// Tipos em C++ - exemplo de inicialização de inteiros
#include <iostream>

using namespace std;

int main()
{
    int valorInteiro = 45.9;
    // Resultado: O valor inteiro inicializado a partir de 45.9 é: 45
    cout << "O valor inteiro inicializado a partir de 45.9 é: "
            << valorInteiro << endl;
    return 0;
}

Floats

Os tipos de ponto-flutuante, do inglês floats, possuem valores com parte fracionária, ou seja, após a vígula; assim, é possível representar valores “quebrados” (como os 45.9 do exempo anterior) utilizando-se deles. Os tipos incluídos nesta categoria são o float, o double e o long double. Como se vê na Tabela 1, o float é o menor dos tipos, seguido pelo double e pelo long double. Todos esses tipos podem também ser signed e unsigned, e esses keywords tem o mesmo efeito que nos demais tipos aritméticos: um unsigned double terá maior faixa de valores positivos (tanto maior quanto mais bits houver em sua representação no sistema em questão) do que um double normal, mas não suportará valores negativos. O mesmo vale para os outros dois. A diferença entre esses tipos não é muito clara, no entanto: 6 bits significativos, 10 bits significativos…No final das contas, qual devo utilizar?

Em boa parte dos sistemas atuais, as operações com doubles são tão eficientes quanto (ou mais eficientes que) aquelas com floats, o que torna estes últimos desinteressantes – ainda porque eles frequentemente não possuem a precisão necessária para as operacões em muitos domínios práticos, como geolocalização, por exemplo. Assim sendo, é comum se ver o uso do double como tipo padrão entre aqueles de ponto-flutuante; utilizá-lo é, a menos que seu projeto possua exigências muito rígidas de uso de memória ou performance, a melhor opção para o usuário comum. E o long double, onde entra nessa história? Bom, este aqui normalmente é demais: a precisão adicional que ele fornece é comumente desnecessária para a maioria das aplicações, e o seu custo computacional extra torna-lhe caro às aplicações comuns. Todavia, em aplicações onde a precisão deve ser altíssima, talvez em bibliotecas de ferramentas matemáticas ou de cálculos de astronomia, por exemplo, o long double pode ser a única opção viável (ainda que o tamanho do long double possa variar muito de implementação em implementação, podendo até ser igual ao do tipo double normal).

// Tipos em C++ - exemplo de uso de tipos de ponto flutuante com diferentes precisões numéricas

#include <iostream>
#include <limits>

// Aqui crio um "apelido" para um um tamanho limite para o tipo long double
using ldbl = std::numeric_limits< long double >;

using namespace std;

int main()
{
    float floatVar = 1.000111222333444555666777888L;
    double doubleVar = 1.000111222333444555666777888L;
    long double longDoubleVar = 1.000111222333444555666777888L;

    cout.precision(ldbl::max_digits10);
    // Resultado: Valor armazenado em um float: 1.00011122226715087891
    cout << "Valor armazenado em um float: " << floatVar << endl;
    // Resultado: Valor armazenado em um double: 1.00011122233344451615
    cout << "Valor armazenado em um double: " << doubleVar << endl;
    // Resultado: Valor armazenado em um long double: 1.00011122233344455562
    cout << "Valor armazenado em um double: " << longDoubleVar << endl;

    return 0;
}

Como é possível ver no exemplo acima, um mesmo valor armazenado em um float, um double e um long double perde seu significado real após um número de casa decimais diferente para um dos três tipos. No exemplo, é utilizada a precisão do long double para o sistema em questão (20 dígitos) e vemos que o float tem 10 dígitos de precisão (da parte decimal 1 ao último 2 do trio que segue o trio de 1s); o double tem 17 dígitos de precisão e o long double 20. Após esses limites, a informação é perdida (veja o número 2 errado que aparece ao fim do long double quando lhe imprimimos na tela).

Void

Este tipo tem uma utilidade muito restrita se usado sozinho. O void ganha terreno quando utilizado em conjunto com ponteiros, pois permite criar ponteiros “genéricos” que podem ser transformados em ponteiros de outros tipos (tratarei disso em um artigo dedicado ao assunto). Por si só, void representa a ausência de valores, o vazio – sua principal função é servir de retorno para funções das quais não se deseja retornar nada, mas apenas produzir efeitos secundários ou colaterais (side effects), como mostrado no exemplo abaixo.

/*******************************************************
* // Tipos em C++
* Exemplo de utilização do tipo void em uma função cujo objetivo é 
* imprimir um texto na tela e não retornar nada.
********************************************************/
#include <iostream>

using namespace std;

void cadeMeusLeitores() {
    cout << "Você não tem nenhum." << endl;
    // Este return é permitido, mas opcional
    return;
}

int main()
{
    // Resultado na tela: Você não tem nenhum.
    cadeMeusLeitores();

    return 0;
}

Conclusão – Que tipo usar?

Chegamos ao fim deste artigo sobre tipos em C++; espero que tenha sido claro, apesar da natureza um pouco confusa do assunto; se não, por favor me diga quais pontos carecem de melhores explicações e eu criarei artigos dedicados para cada um deles. Para ajudá-lo, contudo, irei relembrar quais tipos usar nos cenários mais comuns.

  • Sempre que os valores que você for utilizar devam ser positivos, utilize tipos unsigned para evitar confusões, deixando mais clara a intenção daquele objeto;
  • Por via de regra, utilize double como tipo padrão para operações com tipos de ponto-flutuante, pois ele oferece o melhor custo-benefício para a maioria das aplicações.
  • Se precisar de um valor inteiro muito pequeno, utilize unsigned char para economizar memória e limitar os valores tolerados. Short normalmente é pequeno demais quando se precisa dele, portanto a opção padrão para inteiros é o int.
  • Utilize void como valor de retorno de funções que não precisam retornar nada.
  • O tipo char, sem signed ou unsigned, pode ser tanto signed quanto unsigned, a depender da implementação. Portanto, sempre especifique a natureza do char que se deseja utilizar e mantenha consistência no seu emprego ao longo do programa.
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.

4 thoughts on “Tipos em C++ | As 2 categorias principais: tipos aritméticos e Void

  1. Gostei muito da explicação. Ficou bem mais fácil de entender os tipos e suas diferenças. Além de não demandar muito tempo de leitura 🙂

Leave a Reply

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