Curso de C++/Tipos de dados

Tipos primitivos e compostos

editar

C++ tem basicamente 6 tipos de dados primitivos, a saber: char, int, float, double, wchar_t e bool. São, respectivamente, os tipos: caractere (ASCII), inteiro, ponto flutuante de precisão simples, ponto flutuante de dupla precisão, caractere (UTF-16) e lógico ou booleano.

A partir desses tipos de dados, podemos "formar" outros tipos que serão denominados compostos. O tipo char, por exemplo, pode ser utilizado para criar strings de caracteres e mais adiante nesta seção analizaresmos também os tipos, vetores, matrizes, strings e structs.

Tipos primitivos

editar

Caractere (ASCII)

editar
char umCaractere = 'a';

O tipo de dados caractere é utilizado para armazenar letras, números e caracteres especiais. A declaração desse tipo dá-se através da palavra reservada char.

Ao total, são 256 caracteres (se unsigned char) que podem ser armazenados, dos quais os 128 primeiros fazem parte do conjunto ASC II. Você pode atribuir um caractere de duas formas, ao declarar um caracter sem as aspas simples, você deverá saber qual é o numero decimal correspondente. Por exemplo, se você digitar char caracter = 97, o console imprimirá a letra 'a'. Logicamente poderia ter declarado também dessa forma char caracter = 'a'.

É possível armazenar números inteiros pertencentes ao intervalo de -128 até 127. Se o contexto demandar pode realizar operações matemáticas entre esses números mas tendo em mente que se você for imprimir na tela do console, o resultado será o caracter correspondente. Por exemplo:

char caracter1 = 50; // corresponde ao caracter '2'.
char caracter2 = 47; // corresponde ao caracter '/'.
caracter1 = caracter1 + caracter2; // caracter1 agora tem o valor 97, que corresponde a letra 'a'.
cout << "Imprimiu o caracter: " << caracter1 << endl; // Imprimirá a letra 'a'.

// Se você precisar imprimir o número, faça o seguinte:
int inteiro = caracter1; // Armazena o valor inteiro que está em char.
cout << "Imprimiu: " << inteiro << endl; // Imprime o número.

// ou ainda

cout << "Imprimiu: " << (int) caracter1 << endl;

A saída deste trecho de código é:

Imprimiu o caracter: a
Imprimiu: 97
Imprimiu: 97

Se um extrair o número do caractere, proceda assim:

char caracter = 'B'; // declare entre aspas simples o caracter a ser descoberto.
cout << "O código do caracter é: " << (int) caracter << endl;
// converte para inteiro antes de imprimir.

A saída é:

O código do caracter é: 66

Você poderá somar caracteres. Por exemplo: somar '2' com '/' equivale a letra 'a', pois 50 + 47 = 97.

Caracter UTF-16

editar

Existe o tipo wchar_t, que também é uma palavra reservada da linguagem C++. A declaração e o uso seguem as mesmas regras do tipo caracter, porém, o conjunto de caracteres que esse tipo suporta é muito maior (65.536 caracteres ao total). O conjunto de caracteres desse tipo é o Unicode codificado em UTF-16. Assim sendo, caracteres especiais como letras com acentuação, cedilha, e outros podem ser lidos e impressos.

Importante: Se você tentar imprimir um wchar_t na janela de console, você talvez não conseguirá (poderá aparecer um número de 0 até 65.535). Isso dependerá do conjunto de caracteres suportados pelo console. Entretanto, se você estiver realizando a leitura de dados digitados pelo usuário, esse tipo é mais adequado que char, pois o usuário poderá digitar alguns caracteres do teclado não suportados pelo tipo char.

wchar_t caracterext = 'ç';
// o caracter de cedilha faz parte do conjunto de caracteres Unicode em codificação UTF-16.
// para utilizar o tipo wchar_t a linguagem C++ conta com os mesmos comandos de entrada e saída, porém um pouco diferente

wchar_t caractere;
wcin>>caractere; // em vez do convencional "cin"
wcout << caractere<< endl; // em vez do convencional "cout"
// provavelmente com a utilização do "cout" seria exibido um número no lugar do caractere

// para trabalhar com strings wchar_t
wchar_t caracteres[50];
wcin.getline(caracteres, 50); // este sintaxe é necessária pois ignora o caractere aspaço, localiza '\0' e depois encerra
wcout << caracteres << endl;

Inteiros

editar

O tipo inteiro é um tipo bastante comum. Sua declaração dá-se através da palavra reservada int.

int inteiro = 500;

O tipo inteiro pode armazenar números que vão da faixa de -2.147.483.648 até 2.147.483.647 (um total de 232 números, ou 4.294.967.296). Você pode modificar esta faixa de valores usando as palavras reservadas signed, unsigned, short e long. Veremos estas palavras reservadas (chamadas de modificadores de intervalo) logo abaixo.

Importante: se você quiser utilizar um número que esteja além dessa faixa de valores, você deve utilizar o tipo double.

double inteiro = 45687921681287; // um inteiro armazenado dentro do tipo double.

Ponto flutuante (precisão simples)

editar

Representa números fracionários e números reais (o que inclui os números inteiros). A faixa de valores varia de 1,2e-38 até 3,4e+38.

A declaração é feita através da palavra reservada float:

float real = 1.15479637; // poucas casas decimais.

Ponto flutuante (dupla precisão)

editar

Igualmente ao tipo float, utilizado para números fracionários e reais, porém com precisão ainda maior. A faixa de valores varia de de 2,2e-308 até 1,8e+308.

A declaração é com a palavra reservada double:

double numero = 558749.16846516487975132156857452131274127412974812794812794812798412;
// precisão acurada.

Lógico (booleano)

editar

É o tipo mais simples, e pode armazenar apenas dois valores: verdadeiro ou falso. Para definir esses valores você deve proceder da seguinte forma:

bool logico1 = true; // True significa verdadeiro em Inglês;
bool logico2 = false; // False significa falso em Inglês;
bool logico3 = 0; // Atribui-se o inteiro 0 (zero) equivale a 'false';
bool logico4 = 189; // Qualquer valor diferente de 0 (zero) é interpretado como 'true';
//o tamanho em bits desse tipo é igual o do tipo char, ou seja, 8 bits

Sequências de escape

editar

Especialmente no caso de uma variável tipo caracter ou string, você pode querer declarar, por exemplo, a própria aspa simples (utilizando \'), ou um caracter de nova linha (\n). Para isso você deve utilizar seqüências de escape. Todas elas vêm precedidas de uma barra invertida (\).

Sequência Equivale a
 \n  Nova linha
 \t  Tabulação
 \b  Backspace
 \"  Aspa dupla
 \'  Aspa simples
 \?  Ponto de interrogação
 \\  Barra invertida


Exemplo:

cout << "Linha 1.\n\n\nLinha depois de três \'quebras de linhas\'." << endl;

A saída no console se pareceria com:

Linha 1.


Linha depois de três 'quebras de linha'.

Modificadores de faixa (ou intervalo)

editar

Em C++ existem alguns modificadores de faixa (ou intervalo). Isso altera a faixa de valores suportada pela variável (mas a capacidade de armazenamento continua a mesma). São eles: short, long, unsigned e signed.

O modificador short, que é uma palavra reservada da linguagem, fixa a faixa dos valores inteiros (int) para -32.768 até 32.767. Quando você declara um inteiro, ele inicialmente é da faixa de um long int. Se você quer reduzir a faixa, use short int.

short int numero = 12500;

No subtópico "inteiros", dissemos que a faixa de um número inteiro é de -2.147.483.648 até 2.147.483.647. Na verdade, quando você declara um int, ele já é um long int. Ao declarar um long int, estamos apenas assegurando essa faixa de valores para int.

Long também pode ser utilizado com float. Nesse caso, a faixa de valores de float será a mesma de um double.

long int numero = 45648978;
long float numero2 = 4568.0;

Unsigned e signed

editar

Estes modificadores alteram a faixa dos números inteiros (int) e caracteres (char) para valores somente positivos (unsigned) ou fixa a faixa deles em valores com sinal (signed). Por padrão, se você não declarar esses modificadores, as variáveis tipo inteiro e caracter ASCII serão signed.

Quando definimos que um inteiro terá valores sem sinal (unsigned), na verdade estamos dizendo que só estamos admitindo valores positivos. Isso altera a faixa de um inteiro para 0 até 4.294.967.295 - uma capacidade maior para números positivos). Se você declarar um valor negativo, ocorrerá um estouro de faixa.

Com signed, fixamos a faixa normal para um número inteiro.

unsigned int numero1 = 700; // inteiro sem sinal.
signed int numero2 = -700; // admite valores negativos (padrão do tipo int).
unsigned short int numero3 = 5000; // pode-se combinar modificadores.
signed short int numero4 = 22000;
signed long int numero5 = 454658;
unsigned long int numero6 = 3000000000; // aceita somente inteiros positivos.
signed char caracter1 = -128; // caracter com sinal. Faixa: -128 até 127.
unsigned char caracter2 = 255; // caracter sem sinal. Faixa: 0 até 255.

Estouro de faixa (range overflow)

editar

Se você declarar um valor maior do que o tipo suporta, ocorrerá um estouro de faixa, também chamado range overflow.

Ao chegar no fim da faixa de valores para números inteiros e caracteres (incluindo wchar_t), você volta para o início ou para o fim da faixa. Exemplo:

unsigned long int numero = 4294967295; // o valor máximo para um inteiro longo sem sinal.
cout << "Imprime: " << numero << endl;

numero = 4294967296; // estouramos a faixa de valores. Voltará ao início da faixa.
cout << "Imprime: " << numero << endl;

numero = -1; // estouramos novamente a faixa. Neste caso, retornará para o fim da faixa.
cout << "Imprime: " << numero << endl;

A saída no console se parecerá com:

Imprime: 4294967295
Imprime: 0
Imprime: 4294967295

Para o tipo double, se você estourar a faixa de valores, ocorrerá um erro de compilação (indicando constant too big ou token overflowed internal buffer). Isso ocorrerá porque para double o número deve "caber" em 64 bits. Tente o seguinte e ocorrerá erro de compilação:

double numero = 18446744073709551616; // 2 elevado a 64.
// ERRO. Não compilará.

Espaço ocupado na memória

editar

As variáveis e constantes são armazenadas na memória e, dependendo do tipo, ocupam um espaço diferente nela. Abaixo segue o tamanho em bytes ocupado por cada tipo e a faixa:

Sequência Bytes ocupados Faixa (ou intervalo)
short int
signed int
signed short int
wchat_t
signed wchar_t
2 -32.768 a 32.767
unsigned short int
unsigned wchar_t
2 0 a 65.535
int
long int
signed long int
4 -2.147.483.648 até 2.147.483.647
unsigned int
unsigned long int
4 0 a 4.294.967.295
char
signed char
1 -128 a 127
unsigned char 1 0 a 255
bool 1 true ou false
float 4 1,2e-38 a 3,4e+38
double
long float
8 2,2e-308 a 1,8e+308

Sizeof

editar

Sizeof é um operador unário de C++ que é utilizado semelhantemente a uma chamada de função. Sizeof será muito importante em tópicos posteriores, porque retorna o tamanho de memória que um tipo de dados ocupa. Para utilizá-la, a sintaxe é sizeof([tipo/variável/constante/objeto]). Exemplo:

int numero = 50;
cout << "Tamanho de um inteiro: " << sizeof(int) << endl;
cout << "Tamanho de um double: " << sizeof(double) << endl;
cout << "Tamanho da variável número: " << sizeof(numero) << endl;

A saída no console se parecerá com:

Tamanho de um inteiro: 4
Tamanho de um double: 8
Tamanho da variável número: 4

Sizeof é uma palavra reservada da linguagem C++.

Typedef

editar

Se você quiser abreviar a declaração de um tipo de dados, você pode usar typedef. Esta função também server para criar uma estrutura de dados, que veremos logo abaixo. Typedef é uma palavra reservada da linguagem.

Para abreviar a declaração de um tipo de dados, faça o seguinte:

#include <iostream>
using namespace std;

typedef unsigned short int ushort;
// a partir de agora você pode usar "ushort" para declarar inteiros curtos sem sinal.
// typedef não precisa estar no início do código.

int main() {
ushort numero = 10; // um inteiro curto sem sinal.
}

Conversões

editar

Você pode realizar conversões entre os tipos de dados. Existem dois tipos de conversão: as implícitas e as explícitas. Vejamos cada uma.

Conversões implícitas

editar

Conversão implícita é aquela realizada pela linguagem sem que o programador tenha explicitamente solicitado que ela a fizesse (entretanto, fica subentendido que você deseja fazer a conversão, por isso ela é chamada de implícita). C++ converte implicitamente a maioria dos tipos de dados. Os principais são:

  • De inteiros para caracteres e ponto flutuante.
  • De ponto flutuante para inteiro e caracter.
  • De caracter para inteiro e ponto flutuante.

Exemplo (de caracter para inteiro e ponto flutuante):

char c = 'a';
int i;
float f;
double d;

i = c; // conversão implícita. i recebe o valor 97 (código de 'a');
f = c; // f recebe 97;
d = c; // d recebe 97;

Importante: Quando convertemos ponto flutuante para inteiros e caracteres, a parte fracionária é desprezada. Ao realizar a conversão poderá ocorrer um estouro de faixa, alterando o valor.

Conversões explícitas

editar

Na conversão implícita você solicita que a linguagem realize a conversão, de forma explícita. C++ converte implicitamente a maioria dos tipos de dados mas, para certificar-se de que fará a conversão correta, você pode fazer conversões explícitas (cast), conforme o exemplo que segue:

char c = 'a';
cout << "Conversão explícita de uma caracter para inteiro: " << (int) c << endl;

float f = (float) c; // conversão explícita para ponto flutuante, precisão simples.
cout << "Conversão explícita de um caracter para ponto flutuante: " << f << endl;

Tipos de dados compostos

editar

Os tipos de dados compostos derivam dos tipos primitivos e são úteis para resolver uma grande quantidade de problemas. Se quisermos, por exemplo, armazenar uma frase inteira? Ou, por exemplo, os tempos dos atletas que disputaram uma corrida?

Para resolver esses problemas podemos criar tipos compostos, que atendem a uma função específica no seu programa. Veremos a seguir 4 principais tipos compostos de dados que podemos criar em C++ e que lhe ajudará a solucionar a maioria dos problemas.

Vetores

editar
float registro[10]; // declaração de um vetor de 10 números reais;

registro[0] = 3.50;
registro[1] = 3.59;
registro[2] = 3.72;

registro[9] = 4.84;

No código acima criamos um registro que armazena 10 números de ponto flutuante na memória. Para criar um vetor, declaramos o tipo de dados, o nome do registro e, entre colchetes, a quantidade de registros que queremos armazenar.

Para acessar, utilizamos o nome do registro com o índice do registro entre colchetes. É muito importante ter em mente que os índices começam em 0 (zero)! Sendo assim, um registro de, por exemplo, N coisas, tem índices de 0 até N-1.

A sintaxe para declarar um vetor é:

[tipo] [nome][quantidade de registros (entre colchetes)];

Ao declarar a quantidade de registros, você pode fazê-lo digitando um número inteiro ou colocando ali o nome de uma constante inteira. Por exemplo:

#include <iostream>
using namespace std;
#define TAM 100

const int TAM2 = 200;

int main() {
char a[TAM]; // um vetor de caracteres com 100 registros.
float b[TAM2]; // um vetor de números reais com 200 registros.
cout << "Tamanho do vetor A: " << sizeof(a) << endl;
cout << "Tamanho do vetor B: " << sizeof(b) << endl;
}

Isso é útil quando criamos vários vetores, todos com o mesmo tamanho. Se, porventura, for necessário modificar o tamanho do vetor, você pode fazê-lo simplesmente alterando o valor da constante.

Você pode utilizar o operador sizeof para obter o tamanho de um registro. A saída no console do código acima geraria a seguinte saída no console:

Tamanho do vetor A: 100
Tamanho do vetor B: 200

Matrizes

editar

Matrizes são, conceitualmente, um vetor de vetores. A declaração de uma matriz se dá semelhantemente a de um vetor. Ex:

int matriz[10][20]; // uma matriz 10 colunas e 20 linhas.

matriz[0][0] = 1; // primeira célula da primeira coluna.
matriz[0][1] = 2; // segunda célula da primeira coluna.

matriz[9][19]; // última célula da última coluna.

Note que os índices, tanto das colunas quanto das linhas, começam em zero também. O primeiro colchete indica a coluna e o segundo a linha.

Podemos criar matrizes com mais dimensões. Exemplo:

int matriz[5][6][7];
// uma matriz com 210 campos, formada por 5 colunas, cada coluna com 6 linhas,
// e cada linha contendo 7 campos.

Todas as regras válidas para vetores valem para matrizes.

Strings

editar

Strings são vetores de caracteres. Ainda que sejam vetores, as strings em C++ possuem algumas particularidades.

#include <iostream>
using namespace std;
#include <string>

int main() {

char frase[50]; // uma string com tamanho fixo: 50 caracteres.
strcpy(frase,"Tipos de dados"); // copia "Tipos de dados" para frase.

char *frase1; // uma string de tamanho indefinido.
frase1 = "Curso de C++";

string frase2; // uma string construída usando a biblioteca <string>
frase2 = "Testes com strings";

cout << frase << "\n" << frase1 << "\n" << frase2 << endl;
system ("pause");
}

A saída no console se parecerá com:

Tipos de dados
Curso de C++
Testes com strings

Aprenderemos a utilizar strings e manipulá-las no tópico Entrada e saída, Biblioteca <string>.

Structs

editar

Structs, do inglês structure, são utilizadas para criar estruturas de dados: tipos compostos de dados que, em conjunto, formam uma única estrutura.

A declaração e o uso de structs é bastante simples.

#include <iostream>
using namespace std;

typedef struct Aluno {
 int matricula;
 int idade;
 char* nome;
}; // o ponto e vírgula é obrigatório no fim das chaves ao criar uma struct.

int main() {

 Aluno individuo; // cria 1 (uma) variável chamada indivíduo, que tem matrícula, idade e nome.
 Aluno turma[30]; // cria um vetor de 30 alunos. Um conjunto de alunos é uma turma.

 individuo.nome = "Fulano"; // altera o atributo nome de 'individuo' para Fulano.
 individuo.matricula = 12587; // altera o atributo matrícula de 'individuo' para 12587.
 individuo.idade = 20; // altera o atributo idade de 'individuo' para 20.

 turma[0].nome = "Primeiro aluno";
 turma[0].matricula = 6716;
 turma[0].idade = 18;

 cout << "Dados de \'individuo\':\nNome: " << individuo.nome << "\nMatrícula: "
      << individuo.matricula << "\nIdade: " << individuo.idade << endl;
 cout << "\nDados do primeiro aluno da turma:\nNome: " << turma[0].nome << "\nMatrícula: "
      << turma[0].matricula << "\nIdade: " << turma[0].idade << endl;
 // não havíamos comentado mas, se o código não couber em uma linha,
 // você pode quebrar para a próxima. O comando termina apenas no ';'.

 system("pause");

}

A saída no console se parecerá com:

Dados de 'indivíduo':
Nome: Fulano
Matrícula: 12587
Idade: 20

Dados do primeiro aluno da turma:
Nome: Primeiro aluno
Matrícula: 6716
Idade: 18

A declaração da struct não precisa estar, necessariamente, no início do código (antes da main). Poderá estar dentro de um bloco de chaves ({}), mas nesse caso seria como uma struct local: só poderia ser acessada dentro deste bloco. Pelo código podemos notar a sintaxe da declaração de uma struct:

typedef struct [NOME] {
 <variáveis>
}; - o ponto-e-vírgula depois da chave é fundamental.

Para acessar os atributos, a sintaxe é a seguinte:

[nome da estrutura].[nome do atributo] = [valor] (para modificar os valores)

[nome da estrutura].[nome do atributo] (para acessar os valores)

Note que podem ser criados vetores e matrizes de structs. Isso é útil, quando, como no exemplo dado, queremos criar o registro de alunos (de uma turma), por exemplo.

Podemos ainda "formatar" as estruturas de acordo com necessidades especificas, vejamos os exemplos:

struct Estrutura_Basica
{
	int indice;
	char nome[50];
};

struct Estrutura_Aninhada
{
	struct Estrutura_Interna
	{
		int idade;
	};
	Estrutura_Interna idade;
	int indice;
	char nome[50];
};

struct EstruturaData
{
	short dia, mes, ano;
};

struct Estrutura_Com_Tipo_Derivado
{
	EstruturaData data;
	int indice;
	char nome[50];
};

struct Estrutura_Com_Objeto_Definido
{
	int indice;
	char nome[50];
} ObjetoDefinido;

typedef struct Estrutura_A_Ser_Redefinida
{
	int indice;
	char nome[50];
} Estrutura_Redefinida;

// ou pode ser também

struct Estrutura_A_Ser_Redefinida2
{
	int indice;
	char nome[50];
};

typedef Estrutura_A_Ser_Redefinida2 Estrutura_Redefinida2;

//e ainda uma estrutura anônima

typedef struct
{
	int indice;
	char nome[50];
} Estrutura_Redefinida3;

typedef struct lista list_t; //lista interessante
struct list {
	list_t*		proximo;
	list_t*		anterior;
	void*		dado;
};

//herado do C, que permite a inicialização do objeto
struct Estrutura_Basica_Inicilaizada
{
	int indice;
	char nome[50];
} objeto =
{
	1, "Tiago Silva"
};

struct Estrutura_Como_Mais_De_Um_objeto1
{
	int indice;
	char nome[50];
} Objeto1, objeto2; // e por ai vai

// alem disso podemos também ralizar
struct Estrutura_Como_Mais_De_Um_objeto2
{
	int indice;
	char nome[50];
} ObjetoComum, *objetoPonteiro; // e por ai vai

struct Estrutura_Como_Mais_De_Um_objeto3
{
	int indice;
	char nome[50];
} objetoArray[5], *objetoPonteiro1, **objetoPonteiro_para_Ponteiro;

//no caso de C++
struct Estrutura_Como_Construtor
{
	int indice;
	char nome[50];
	Estrutura_Como_Construtor()
	{
		indice = 0;
		//nome = {'T', 'I', 'A', 'G', 'O', '\0'};
		//nome = "Desconhecido";
		strcpy(nome, "Desconhecido");
	}
	~Estrutura_Como_Construtor() {} // mais tarde o veremos
};

struct Estrutura_Como_Sobrecarga_de_Metodos
{
	int indice;
	char nome[50];
	void setDados(int parametro_indice)
	{
		indice = parametro_indice;
	}
	void setDados(int parametro_indice, char* parametro_nome)
	{
		indice = parametro_indice;
		strcpy(nome, parametro_nome);// detalhes
	}
};

struct Estrutura_Como_Sobrecarga_de_Operador
{
	int indice;
	char nome[50];
	void operator=(Estrutura_Basica strt_basec)
	{
		this->indice = strt_basec.indice;
		strcpy(this->nome, strt_basec.nome);
	}
};

struct Estrutura_Sem_Dados
{
	bool metodo_qualquer()
	{
		cout << "deseja escolher[S/N]" << endl;
		char resp;
		cin >> resp;
		if(resp == 'S' || resp == 's')
		{
			cout << "Sim" << endl;
			return true;
		}
		else
		{
			cout << "Não" << endl;
			return false;
		}
	}
	// também 
	bool metodo_qualquer2()
	{
		bool retorno;
		cout << "deseja escolher[S/N]" << endl;
		char resp;
		cin >> resp;
		if(resp == 'S' || resp == 's')
		{
			cout << "Sim" << endl;
			retorno = true;
		}
		else
		{
			cout << "Não" << endl;
			retorno = false;
		}
		return retorno;
	}
};

//para saber o tamaho;

int tam = sizeof(Estrutura_Basica); // nem sempre o tamanho é igual a soma do tamanho dos tipos internos

Revisão

editar

Neste tópico discutimos os tipos de dados possíveis em C++: os tipos primitivos e os compostos. Entretanto, podemos criar tipos e estruturas de dados através de classes, mas este assunto será abordado inteiramente em um tópico posterior.

Antes de passar para o próximo tópico, certifique-se de ter compreendido e memorizado o que segue:

  • A sintaxe da declaração e acesso aos tipos primitivos de dados.
  • Os modificadores de acesso: o que eles fazem e a faixa para qual alteram cada tipo de dados.
  • O que é estouro de faixa (range overflow), e o que acontece quando isso ocorre.
  • A função do operador unário sizeof.
  • Os erros comuns que geram erro de compilação, citados nos comentários dos códigos.
  • O que é conversão implícita e explícita e como fazê-las.
  • O que são tipos compostos de dados, e a sintaxe da declaração e acesso a vetores, matrizes e strings.
  • Structs: o que são, a declaração e o acesso aos atributos.