Baixe o app para aproveitar ainda mais
Prévia do material em texto
Ponteiros Ponteiro é uma variável que guarda (aponta para) um endereço de memória onde é possível o armazenamento de um valor de acordo com o tipo especificado na sua declaração. A memória de um computador é identificada por células que são regiões onde valores podem ser armazenados. Células de memória – Porções da memória principal do computador. Estas células são endereçadas. OBS: Os endereços representados são apenas exemplos, não refletem uma situação real. Com o software DDD você pode ver o endereço real de uma variável utilizada em seu programa, basta colocar o cursor do mouse sobre a variável e observar seu endereço no rodapé da ferramenta. Operadores utilizados em ponteiros A linguagem C++ utiliza dois operadores básicos para manipulação de ponteiros, * e &. * É utilizado para declaração de variável ponteiro e também para acesso ao conteúdo de um endereço de memória apontado por um ponteiro. & É utilizado para representar um endereço de memória de uma variável OBS..: Em C++, o operador * (asterisco) é também utilizado como operador aritmético de multiplicação, portanto tenha muito cuidado ao utilizá-lo. A forma correta de declaração de variável ponteiro é: <tipo-de-dado> * <nome-da-variável> int * a; float * b; char * str; Veja que é necessário informar de qual o tipo será o ponteiro. De acordo com esta informação é possível declarar um ponteiro a partir de qualquer tipo pré-definido da linguagem e de qualquer tipo de dados criado pelo programador. Quando ocorre a declaração destas duas variáveis, elas apontam para endereços de memória. Exemplo: #include<iostream> using namespace std; int main(void) { int * a, *b; a = new int; b = new int; *a = 10; *b = 50; return(0); } Observe: - As instruções a = new int; e b = new int; reference a alocação de memória para os ponteiros a e b antes que eles recebam valores. - As instruções *a = 10 e *b = 50, significam que no endereço apontado pelo ponteiro a o valor 10 será inserido e no endereço apontado por b o valor 50. *a=10; - Conteúdo de a é 10 (posição de memória apontada por a recebe o valor 10) *b=50; - Conteúdo de b é 50 (posição de memória apontada por b recebe o valor 50) Exemplo: #include<iostream> using namespace std; int main(void) { int * a, b; a = new int; *a = 10; b = *a; return(0); } A variável a é um ponteiro que aponta para um endereço de memória qualquer e b não aponta por não se tratar de um ponteiro, e sim uma variável comum. A linha de instrução *a =10, diz que o conteúdo do endereço apontado por a recebe o valor 10. A linha b = * 10, diz que o conteúdo do endereço apontado por a (valor 10) será copiado para a variável b, que não é ponteiro. #include<iostream> using namespace std; int main(void) { int * a, *b; a = new int; b = new int; *a = 10; *b = *a; return(0); } Neste exemplo, o conteúdo do endereço apontado por a (valor 10), é copiado para o endereço apontado pelo ponteiro b. Neste caso, as duas também permanecem com o mesmo valor. #include<iostream> using namespace std; int main(void) { int * a, *b; a = new int; b = new int; *a = 10; b = a; *b = 50; return(0); } Para entender melhor o que ocorre com este programa, vamos analisá-lo passo a passo: 1) int *a, *b; - Declaração de dois ponteiro tipo inteiro. Ambos apontam para endereços de memória. 2) a = new int; b = new int; Alocação de memória para os ponteiros a e b 3) *a = 10; - O endereço apontado pelo ponteiro a será preenchido com um valor inteiro 10. 4) b = a; - O ponteiro b deixa de apontar para seu endereço especificado na figura acima e passa a apontar para o endereço de a. 5) *b = 50; - O endereço apontado pelo ponteiro b recebe o valor 50. Observe que o valor 10 que existe no endereço apontado por a foi substituído por 50. Isso ocorreu porque o ponteiro b está apontando para o mesmo endereço que o ponteiro a. #include<iostream> using namespace std; int main(void) { int * a, b; b = 10; a = &b; *a = 50; return(0); } OBS: Observe que neste caso não foi necessário alocar memória para o ponteiro a, pois ele foi utilizado para apontar para a variável b. O ponteiro a não recebeu diretamente um valor, por isso não foi necessário a alocação de memória para ele. 1) int * a, b; - Declaração de uma variável ponteiro a e uma variável (não ponteiro) b; Observe que a variável b não é ponteiro, portanto não aponta para nenhum endereço na memória. Mesmo não apontando, toda variável é representada por um endereço. 2) b = 10; - A variável b recebe um valor inteiro 10. 3) a = &b; - O ponteiro a aponta para o endereço da variável b O & é utilizado para passar ao ponteiro a o endereço da variável b. Ele é necessário porque b não é ponteiro. A partir desta linha de código, tudo que for feito através do ponteiro a, afetará a variável b. 4) *a = 50; - Endereço apontado pelo ponteiro a receberá o valor 50. #include<iostream> using namespace std; struct dados { char nome[30]; int idade; }; int main(void) { dados * pessoa; //Declaração do ponteiro pessoa do tipo dados pessoa = new dados; //Alocação de memória para o ponteiro pessoa return(0); } A variável pessoa é um ponteiro do tipo dados (struct). Observe como fica a representação na memória: É possível também a utilização de ponteiros a partir de tipos de dados que não são primitivos (int, float, char, double e void). Utilizamos neste exemplo um tipo de dado criado pelo programador com o nome “dado”. Para fazer isso foi utilizado o struct. Passagem de parâmetros para função por valor e por ponteiro Passagem de parâmetros por valor #include<iostream> using namespace std; void soma(int, int); int main(void) { int a = 10, b = 20; soma(a,b); return(0); } void soma(int x, int y) { cout<<“\nSoma = “<< x+y; } 1) int a=10, b= 20; - Declaração e inicialização de duas variáveis inteiras: a e b As variáveis a e b são locais, ou seja, só são válidas apenas dentro do escopo do programa principal, onde foram declaradas. 2) soma(a,b); - Chamada da função soma, passando como parâmetro dois valores inteiros através das variáveis a e b. As variáveis x e y recebem os valores de a e b. Qualquer alteração realizada com as variáveis x e y não afetarão as variáveis a e b. Cópias dos valores de a e b são passadas para as variáveis x e y. #include<iostream> using namespace std; void dados(int , int ); int main(void) { int a = 10, b = 20; dados(a,b); return(0); } void dados(int x, int y) { x = 50; y = 70; } 1) int a=10, b=20; Declaração de duas variáveis no programa principal e também inicialização com valores inteiros 10 e 20. Estas variáveis são válidas dentro desta função, caso seja necessário utilizar seus valores dentro de uma outra função, é preciso passá-las como parâmetro. 3) dados(a,b); - Chamada da função dados, passando os valores de a e b por valor. Os valores de a e b são passados para x e y. Qualquer alteração em x e y não afetarão a e b. 4) void dados(int x, int y) { x = 50; y = 70; } Corpo da função dados e alteração dos valores 10 e 20 existentes em x e y, conforme figura do item 3. Quando a execução dos códigos da função terminar e retornar ao programa principal, os valores de a e b permanecerão os mesmos, ou seja, 10 e 20. Isto acontecedevido a passagem de parâmetro ser por valor. Passagem de parâmetros por ponteiro #include<iostream> using namespace std; void dados(int *, int *); int main(void) { int a = 10, b = 20; dados(&a,&b); } void dados(int * x, int * y) { *x = 50; *y = 70; } 1) int a = 10, b = 20; - Declaração de duas variáveis inteiras e atribuição dos valores 10 e 20 2) dados(&a, &b); - Chamada da função dados passando o endereço de a e b (&a, &b). Isto ocorre porque a função foi construída utilizando ponteiros em seus argumentos: dados(int *x, int *y) Os endereços de a e b (&a , &b) foram passados para os ponteiros x e y que a partir deste momento apontam eles. 3) void dados(int * x, int * y) { *x = 50; *y = 70; } Observe que ao se tratar do conteúdo dos ponteiros x e y, na verdade as variáveis a e b são afetadas, pois x e y apontam para elas. Ao término da função dados e retorno ao programa principal os valores de a e b estarão modificados. Programação modular A modularização de programas é um método utilizado para facilitar a construção de grandes programas, através de sua divisão em pequenas partes, ou seja, módulos. Modularizar um programa significa dividi-lo em partes, ou seja, em arquivos distintos. Cada arquivo conterá parte do código que será compilado e ligado, formando um único código executável. Se todos os módulos serão ligados novamente, então surge a pergunta: - Por que dividi-los??? A resposta para isso é simples: São várias as razões para dividir um código fonte em partes: 1ª – Organização: Cada módulo de programa tem como conteúdo códigos referentes a um conjunto de ações ou atividades; 2ª – Clareza na implementação: A escrita e a leitura de códigos em módulos é muito mais fácil, principalmente no processo de correção de defeitos; 3ª – Facilidade na manutenção dos programas: Com módulos, encontrar erros em programas torna-se um processo menos trabalhoso; 4ª – Tempo de compilação: O processo de compilação pode ser algo que demande muito tempo em programas grandes. Quando erros são encontrados e arrumados, apenas os módulos alterados sofrerão nova compilação, diminuindo bastante o tempo gasto na compilação. No caso de um programa que não é dividido em módulos, qualquer alteração realizada no código fonte exigirá toda a compilação do sistema, ocasionando esperas desnecessárias. Ex: Arquivo: DADOS.H struct dados { char nome[50]; char endereco[50]; float salario; }; void cadastrar(void); void imprimir(void); Observe que neste arquivo encontram-se apenas as definições de um tipo de dado chamado dados e dois protótipos de funções que serão utilizadas para manipular os dados. NÃO HÁ IMPLEMENTAÇÃO DE CÓDIGO NO ARQUIVO DADOS.H, APENAS DEFINIÇÕES Arquivo: DADOS.CPP #include”dados.h” #include<iostream> using namespace std; extern dados pessoa; void cadastrar(void) { cout<<”\nInforme o nome:”; cin>>pessoa.nome; cout<<”\nInforme o endereco:”; cin>>pessoa.endereco; cout<<”\nInforme o salario:”; cin>>pessoa.salario; } void imprimir(void) { cout<<”\nNome :”<<pessoa.nome; cout<<”\nEndereco:”<<pessoa.endereco; cout<<”\nSalario:”<<pessoa.salario; } Observe que neste arquivo encontram-se os códigos das funções definidas no arquivo DADOS.H. ESTE ARQUIVO É UTILIZADO PARA O DESENVOLVIMENTO DAS FUNÇÕES DEFINIDAS NO ARQUIVO DADOS.H E UTILIZAÇÃO DO STRUC Arquivo: PRINCIPAL.CPP #include”dados.h” #include<iostream> using namespace std; dados pessoa; int main(void) { cadastrar(); imprimir(); return(0); } Neste arquivo desenvolvemos o programa principal que será utilizado para a chamada das funções definidas no arquivo DADOS.H e implementadas no arquivo DADOS.CPP. Para compilar: g++ -g –o executável dados.cpp principal.cpp OBS: -g : Opção utilizada na compilação para geração do binário com opção de depuração -o : Opção utilizada para geração dos códigos objetos e executável Executável: Nome dado ao código executável que será gerado dados.cpp: Arquivo fonte principal.cpp: Arquivo fonte Estruturas de dados Introdução Quando um software é desenvolvido, duas coisas importantes devem ser levadas em consideração, os dados e os procedimentos utilizados para manipulá-los. - Dados: Informações que o software utilizará para resolver um determinado problema - Procedimentos: Como estes dados serão manipulados. Quanto mais um projeto de software evolui, maior será a preocupação com estes dois itens. Tipos de dados Para que o computador possa trabalhar com dados, ele deve ser informado desse fato. Isso que dizer que antes de qualquer participação do usuário na iteratividade homem- máquina, ela deve ser preparada para isso. As declarações de variáveis dentro de um programa, são utilizadas com o intuito de preparar a memória da máquina para o armazenamento de informações. O tipo de valor a ser inserido na memória equivale ao tipo de dado permitido. Portanto, tipo de dado é o formato da informação a ser inserida na memória do computador. Exemplo: int a; - Declaração de uma variável do tipo inteiro. A memória está sendo informada que uma variável será utilizada para armazenamento de valores inteiros. Valores inteiros..: 1, 10, -100, -65, 1100, etc... float a; - Declaração de uma variável do tipo real. A memória está sendo informada que uma variável será utilizada para armazenamento de valores reais. Valores reais..: -12.34, 12.00, 34.00, 89.68, 100.00, -234.56, etc.... char c; - Declaração de uma variável do tipo caractere. A memória está sendo informada que uma variável será utilizada para armazenamento de caracteres. Caracteres..: a, A, b, 1, 3, #, +, -, /, etc.... Tipos de dados primitivos São tipos de dados já existentes em uma linguagem de programação, como int, float, char, dentre outros. A partir dos tipos primitivos podemos criar outros tipos de dados. São considerados tipos primitivos: - números inteiros - números reais - caracteres A cada um destes tipos primitivos, um conjunto de valores pode ser atribuído e operações realizadas. Exemplos de valores já foram apresentados anteriormente, observe agora as operações que podem ser realizadas com os tipos primitivos: - Inteiros: soma, subtração, multiplicação, divisão e resto. - Real: soma, subtração, multiplicação, divisão. - Caractere: Igualdade, diferença e concatenação. - Ponteiro: Representação de endereços de memória Tipos de dados abstratos Conforme apresentado anteriormente, os dados manipulados em um programa são informações processadas com o propósito de apresentar a solução de um problema, o que vai depender de como o software foi projetado. Os tipos de dados primitivos dizem como os dados serão armazenados e não como serão manipulados. Para fazer isso, é necessário que o programador crie tipos de dados novos (abstratos), baseando-se nos tipos já existente e que ofereça funções para sua utilização. Portanto, um TAD possui duas partes: - Especificação - Implementação Exemplo de um TAD: struct dados { char nome[30]; int idade; }pessoa; void cadastrar(void) { cout<<“\nDigite nome..:”; cin>>pessoa.nome; cout<<“\nDigite idade..:”; cin>>pessoa.idade; } void imprime(void) { cout<<“\nNome..:”<<pessoa.nome; cout<<“\nDigite idade..:”<<pessoa.idade; } Todo este código representa um TAD. Você pode observar que foi criado um registro contendo informações relacionadas,com o propósito de guardar dados de uma pessoa. Em seguida foram implementadas funções para manipulação dos dados abstraídos do problema e inseridos no registro (struct dados). Estrutura de dados O computador é uma máquina que foi projetada para processamento de informações, com o propósito de facilitar a vida das pessoas. Com a necessidade de todos pela computação, os programas existentes são os mais diversos e também os mais complexos. Já imaginou se no desenvolvimento destes programas não existissem técnicas de organização, manipulação e utilização dos dados? Teríamos grandes problemas, pois os programas não seriam eficientes e tudo seria um caos. Portanto: Estruturas de dados são conceitos e técnicas desenvolvidas para organização e manipulação de dados. Pilha utilizando vetores Apesar de ser uma forma bastante simples de organização e manipulação de dados, esta estrutura tem papel fundamental em diversas áreas da computação, como a construção de linguagens de programação, compiladores, sistemas operacionais, etc. O processo de ordenação e acesso aos dados utilizado em pilha segue o princípio onde o caminho de entrada de dados na estrutura é o mesmo utilizado para retirada, ou seja, LIFO – Last In First Out – Último que entra é o primeiro a sair. Para entender melhor pense em uma pilha de pratos ou de livros. Eles foram colocados uns sobre os outros, seguindo sempre o mesmo procedimento, de baixo para cima, formando uma pilha. A retirada deve ser feita pelo mesmo caminho, você sempre deve retirar o último elemento (topo). Caso a retirada seja feita com um elemento aleatório, a pilha de pratos pode cair e você terá um prejuízo. Baseando-se neste princípio, pense em um conjunto de valores inteiros sendo inseridos e removidos da memória. Devemos utilizar um vetor para fazer esta simulação e também outras duas variáveis para representar a base da pilha (início) e o topo da pilha (fim), local onde os dados serão inseridos e removidos. Observe que à medida que os dados são inseridos, o a variável topo deve ser incrementada para que o próximo elemento a ser inserido fique após o último que existia, assim ele passa a ser o último elemento da estrutura. O mesmo deve acontecer até que o topo represente o valor final do vetor, caracterizando pilha cheia. Antes que qualquer elemento seja inserido na estrutura pilha indicará que a pilha está vazia, portanto as variáveis topo e base estarão armazenando o mesmo valor de índice para acesso ao vetor de dados, ou seja, valor 0 (zero). Programa Pilha // Arquivo PILHA.H - Descrição da estrutura pilha #include<iostream> using namespace std; #define MAX 5 extern pilha p; struct pilha { int dados[MAX]; int base; int topo; }; void empilha(int); void desempilha(void); void inicializa(void); void imprime(void); Este trecho de código representa um TAD pilha. Utilizamos para representação da pilha uma estrutura que contém um vetor que será utilizado para armazenamento dos valores e duas variáveis inteiras que representarão o topo e a base da pilha. As funções definidas serão utilizadas para realização das atividades de inicialização, inserção, remoção e impressão da pilha void inicializa(void) { int i; p.topo=p.base=0; for(i=0;i<MAX;i++) { p.dados[i]=0; } } Função utilizada para inicialização da pilha. Após a execução da função inicializa, o vetor que representa os dados da pilha será preenchido com 0 (zero) e também as variáveis topo e base receberão zero para indicar que a pilha está vazia e pronta para entrada de dados. void empilha(int x) { if(p.topo == MAX ) { cout<<“\nPilha cheia...”; } else { p.dados[p.topo]=x; p.topo++; } } Esta função quando chamada receberá um valor (através de x) que indica o valor a ser inserido na pilha. Observe que o vetor de dados receberá o valor de x e em seguida o topo se desloca para a próxima posição, indicando onde poderá ser inserido o próximo valor. Para visualizar melhor siga os passos: 1) Imagine que a função receba o valor inteiro 10 através da variável x. int a=10; empilha(a); Neste momento, o valor da variável a será passado para a variável x da função insere. O valor de x será inserido na pilha através dos comandos existentes na função: p.dados[p.topo]=x; Após a inserção, o topo da pilha deverá ser deslocado para a próxima posição do vetor, onde deverá ser inserido o próximo valor. O comando que executa o deslocamento do topo é: p.topo++; Neste momento o topo passa a armazenar o valor 1. Para melhor fixação vamos chamar novamente a função insere, passando outro valor para ser inserido na pilha: a = 8; empilha(a); Neste momento o valor 8 é passado para x que será inserido na pilha, onde o topo estiver: p.dados[p.topo]=x; Veja que o topo andou novamente. Esses procedimentos irão ocorrer até que o topo chegue à quantidade máxima permitida pelo vetor de dados. Por essa razão é necessário o teste de verificação se a pilha está cheia: if(p.topo == MAX ) { cout<<“\nPilha cheia...”; } Exercícios 1 – Implemente o programa pilha apresentado como exemplo nesta apostila 2 – Utilizando o programa pilha proposto no exercício 1, escreva um programa que simule o funcionamento de uma calculadora onde os valores são inseridos na pilha e quando for solicitado uma operação, dois valores sejam removidos retornando a ela o resultado desta operação. Este processo deve acontecer até que a pilha fique com apenas um valor 3 – Faça um programa utilizando pilha para solução do seguinte problema: 10 + ( ( x * ( y *z ) / (a * b) ) – 50 ) Fila utilizando vetores Esta estrutura de dados é bem parecida com a estrutura pilha, pelo fato de apresentar atividades formais de inserção e remoção de dados, ou seja, a inserção é realizada utilizando-se um caminho e a saída outro, sempre desta maneira. O nome FILA, dado a esta estrutura, segue o princípio das filas que conhecemos como fila de banco, fila de carros, dentre outros, onde o primeiro elemento que entra na fila é o primeiro a sair – FIFO – Fist In First Out. O primeiro carro da fila será o primeiro a entrar na garagem. Se outro veículo chegar ele ficará por último. Baseando-se neste princípio, pense em um conjunto de valores inteiros sendo inseridos e removidos da memória. Devemos utilizar um vetor para fazer esta simulação e também outras duas variáveis para representar o início e o fim da fila. A entrada de dados acontece sempre no fim da fila e a remoção do início. A estrutura básica da fila pode ser representada da seguinte maneira em programação: struct fila { int dados[4]; int inicio, fim; }; Logicamente, quando utilizamos vetor, a possibilidade de inserir mais elementos do que foi determinado pelo tamanho (4 neste caso), é impossível. Para resolver esse problema é necessário verificar no processo de inserção de um novo elemento se o limite máximo suportado não foi atingido. Na tentativa de inserir novo elemento o programa deve informar que a fila está cheia, sendo impossível a inserção. void enfila(int x) { if(p.fim == 4 ) { cout<<“\nFila cheia...”; } else { p.dados[p.fim]=x; p.fim++; } } Se o fim atingir o limite máximo de elementos do vetor, o programa informará que a fila está cheia, caso contrário o elemento será inserido. Observe a figura e veja o que poderia acontecer no uso de fila com vetor. A) Neste caso,a fila está vazia sendo possível a inserção de elementos. B) Neste item ocorre a entrada de 3 elementos, observe que no final ainda existe a possibilidade de inserção de mais um elemento. C) Como a teoria de fila diz que a remoção deve ser feita no início, dois elementos foram removidos, portanto o início deixou de ser a posição que guarda os elementos 10 e 20 e passou a ser a posição que guarda o valor 30. D) Este item representa a inserção de mais um elemento, que foi possível por existir uma posição ainda vazia. Imagine agora se for necessário inserir outro elemento? Qual seria a resposta do programa? FILA CHEIA!!!!!! Isso não é verdade, pois, ainda existem duas posições vazias onde poderia ser inserido novo elemento. Uma solução para este problema seria a reorganização do vetor, trazendo para o início todos os elementos logo atrás daquele que sair da fila. A) Neste momento seria impossível a inserção de um novo elemento porque a fila está cheia. B) Com a remoção de um elemento, a posição vaga passaria a ser a última com a chegada de todos os elementos uma posição à frente. C) Se um novo elemento chegar para ser inserido será possível por existir uma posição vaga. for(i=0;i<p.fim;i++) { p.dados[i] = p.dados[i+1]; } Este laço de repetição reorganiza o vetor trazendo todos os números uma posição à frente. Portanto, a função desenfila ficará assim: void desenfila(void) { if(p.fim == p.inicio) { cout<<“\nFila vazia!!!”; } else { for(i=0;i<p.fim;i++) { p.dados[i] = p.dados[i+1]; } } } Esta solução resolverá o problema, mas pode não ser tão eficiente pelo tempo gasto para realizar a reorganização. Imagine uma fila com 10000 elementos e a cada elemento removido este processo deve ocorrer, o tempo gasto seria muito grande. Outra solução que existe é a criação de uma fila circular. Neste caso o vetor é tratado como se fosse um círculo, ao invés de um conjunto em linha reta. O primeiro elemento (posição 0 do vetor) segue imediatamente o último, isto indica que mesmo se o último elemento do vetor possuir algum valor não que dizer a fila está vazia e um novo elemento poderá ser inserido no início. Para entender melhor, observe a figura: A) Como demonstrado anteriormente, não seria possível inserir um novo elemento, já que a última posição está sendo utilizada pelo valor 30. B) Utilizando esta técnica de fila circular, o primeiro elemento que segue o último passa a ser a posição que receberá um novo elemento se esta também não estiver ocupada. Referências Bibliográficas TENENBAUM, A. M., LANGSAM Y. & AUGENSTEIN M. J. Estruturas de dados usando C. Ed. Makron Books, São Paulo 1995. SCHILDT H. C Completo e Total, 3ª edição. Editora Makron Books, São Paulo, 1996. SCHILDT H. C++ Guia para iniciantes. Ed. Ciência Moderna, Rio de Janeiro, 2002. DEITEL H.M. C++ como programar, 3ª edição. Editora Bookman, Porto Alegre, 2001. DROZDEK A. Estrutura de dados e Algoritmos em C++, Editora Cengage Learning, 2008
Compartilhar