Instituto Politécnico
de Beja
Escola Superior de Tecnologia e Gestão Área Departamental de Engenharia Disciplina de Linguagens de Programação I Classes contendo memória dinâmica em C++ (versão 1.0)João Paulo Barros
Índice
2. Os operadores new e delete
2.2 O operador delete 2.3 Situações geradoras de erros na utilização dos operadores new e delete 1. Variáveis e memóriaA memória ocupada pelas variáveis em C++ pode ser gerida de três formas distintasa que correspondem três tipos de variável:
No segundo caso, a gestão é muito simples: a memória é reservada no início do programa (na verdade antes de ser executada a função main) e libertada apenas no final do programa. Como tal, as variáveis existem durante toda a execução do programa. Em C++, existem duas variedades de variáveis com estas características:
#include <iostream>
2. Os operadores new e deleteOs operadores new e delete são utilizados, respectivamente para a reserva e libertação de memória durante a execução do programa. Vamos começar pelo operador new.2.1 O operador newO operador new assume várias formas. Vamos ver aqui as duas principais:
Employee* employePtr = new Employee;O novo objecto criado é manipulado a partir de um apontador para o endereço da memória reservada. Assim sendo, *employePtr é o objecto criado. Para chamar uma função membro teríamos: employePtr->getName(). Na segunda forma temos: Employee* employees = new Employee[nElements];em que nElements é a quantidade de elementos do vector criado. Neste caso employees é um vector de baixo-nível da classe Employee. 2.2 O operador deletePara cada uma das formas apresentada do operador new temos a correspondente forma para o operador delete. Este operador encarrega-se de libertar a memória previamente reservada pelo operador new.A memória criada pela primeira forma do operador new é libertada com: delete employePtr; A memória criada pela segunda forma do operador new é libertada com: delete [] employePtr; Note-se a ausência de qualquer número entre os dois parêntesis rectos. É mesmo assim! Os parêntesis rectos servem como indicação de que se está a libertar a memória correspondente a um vector, pelo que na verdade é necessário libertar a memória ocupada por cada um dos elementos do vector. 2.3 Situações geradoras de erros na utilização dos operadores new e deleteExistem várias situações geradoras de erros de execução graves quando se utilizam os operadores new e delete. Os erros de execução podem ser muito difíceis de corrigir pelo que é fundamental ter muito cuidado. As situações são as seguintes:
void f()O segundo e terceiro erros são semelhantes. Ambos correspondem a tentar a libertação de memória que nunca esteve ou já não se encontra reservada. O terceiro erro é o mais difícil de detectar pois normalmente só será detectado se o programa ficar sem memória dinâmica livre, ou seja, quando o operador new não conseguir reservar memória. Muitas vezes nessa altura pode ser difícil detectar qual ou quais os blocos de memória que não são libertados (com delete) e que se mantêm a ocupar memória. Por último, um apontador com o valor zero é, por convenção da linguagem, uma apontador que não aponta para lado nenhum. Como tal os apontadores que não estejam (ainda) a ser utilizados deverão ter o valor zero. É sempre seguro (não dá erro) fazer delete de um apontador com o valor zero, embora tal instrução não tenha nenhum efeito. Por outras palavras delete ptr em que ptr tem o valor zero não faz nada e não dá erro. 3. Classes contendo memória dinâmicaPor vezes é necessário e/ou conveniente que objectos de uma mesma classe possam ocupar diferentes quantidades de memória. É o que sucede por exemplo com uma classe string. Neste caso é claro que cada objecto deverá ocupar uma quantidade de memória proporcional ao comprimento da string. Por exemplo, a string "abcdefgh" deverá ocupar mais memória do que a string "xyz".3.1 Classes (ainda) com memória estáticaSe utilizarmos um array de baixo-nível com dimensão fixa tal não se consegue pois cada objecto irá ocupar a mesma quantidade de memória independentemente de a utilizar todas ou não. Na classe da listagem 3.3 todas as strings necessitarão de 16 (MAX_SIZE) bytes de memória independentemente de os utilizarem ou não.class StringSNota: a constante MAX_SIZE pode estar definida dentro da classe desde que declarada como static. Neste caso, o modificador static significa que só existirá uma constante MAX_SIZE, ou seja, todos os objectos da classe utilizarão a mesma constante. Note que o mesmo não sucede com as variáveis data e size. cada objecto contem a sua variável data e a sua variável size. Após a execução da instrução s1 = s2 da listagem 3.4 a distribuição da memória relativas às strings será aapresentada na figura 3.1 #include "StringS.hpp" // string estática do tipo da da listagem 3.3 3.2 Classes com memória dinâmica (finalmente...)Para que diferentes objectos, da mesma classe, possam ocupar diferentes quantidades de memória, a classe não deverá conter, directamente, toda a memória mas apenas o endereço onde a parte variável dessa memória irá residir. É o que sucede na classe da listagem 3.4.class StringDAs semelhanças entre esta classe e a da listagem 3.3 são ainda menores do que possa parecer. Após a execução da instrução s1 = s2 da listagem 3.6 a distribuição da memória relativas às strings será a apresentada na figura 3.2. #include "StringD.hpp" // string dinâmica do tipoConforme se ilustra na figura 3.2, a cópia de uma StringD para outra teve um efeito, à primeira vista surpreendentes! O objecto s2 não ficou com uma verdadeira cópia do objecto s1. Na verdade, ambos os objectos contêm agora uma referência para a mesma informação. Tal, não é, em principio, o que se pretende pelo que poderá originar vários efeitos surpreendentes. Por exemplo, se modificarmos o objecto s2 para "ABCdef" o objecto s1 também será modificado visto ambos utilizarem a mesma informação. Mais grave ainda, é o facto da destruição de um dos objectos originar a destruição dos dados do outro objecto visto serem os mesmos. A que se deve este estranho comportamento do operador=? Nada de especial, ele simplesmente fez o mais óbvio: copiou bit por bit as variáveis de um objecto para as correspondentes variáveis do outro objecto (note que ambos os objectos contêm necessariamente as mesmas variáveis visto pertencerem à mesma classe). Ora as únicas variáveis dos objectos são o inteiro size e o apontador data . Logo os apontadores irão ficar a a apontar para a mesma memória, ficando os objectos a utilizar os mesmos dados! Este e outros problemas relacionados resultam da cópia bit a bit ser o comportamento por omissão do operador de afectaçao. Felizmente existe a possibilidade de definir à nossa vontade o operador de afectação. Dessa forma podemos programá-lo de forma a que seja feita uma verdadeira cópia de dados e não apenas de referências. Na verdade quando temos uma classe que reserva memória de forma dinâmica devemos definir as seguintes três funções (note que não temos de as definir quando não utilizamos memória dinâmica):
Apresenta-se, na listagem 3.5, a definição destas cinco funções. A definição completa de uma classe StringD bem como de todas as suas funções membro também se encontra disponível. String::String(const String& s)Note que as três primeiras funções podem ter sempre a definição apresentada na listagem 3.5, independentemente da classe. O código que varia de classe para classe é o das funções copy e clear. 3.3 Outro exemplos de classes com gestão dinâmica da memória
|