O que é importante: 1. Perceber o que são tipos da memória. 2. Perceber o que são construtores de cópia (copy construtors). 3. Perceber construção e destruição em classes derivadas. 4. Perceber o que é destrutor virtual. 5. Perceber as respectivas regras da linguagem C++. 6. Estar apto a construir programas que utilizem as respectivas construções de POO. 3. Construtores e destrutores 3.1. Tipos da memória 3.2. Construtores de cópia (copy construtor) 3.3. Chamada de construtores para classes bases e derivadas (construção e destruição em classes derivadas) 3.4. Destrutor virtual 3.1. Tipos da memória. Em C++ podem ser construídos quatro tipos de objectos: 1. Objectos automáticos são objectos que são construídos quando são declarados e destruídos quando se sai do bloco (scope) dentro de qual foram declarados. Os objectos automáticos são colocados no stack. 2. Objectos estáticos são objectos que são criados quando o programa começa e são destruídos quando o programa termina. 3. Objectos criados na zona “free store” são objectos que são criados através de comandos de reserva dinâmica de memória (o operador new). O espaço de memória é reservado na zona denominada “free store” (ou “heap”) e só pode ser libertado com o operador delete. 4. Objectos membros são objectos que são criados como membros de uma classe ou como elementos de um array. De seguida vou mostrar quando são chamados os construtores e destrutores para cada um destes tipos de objectos que surgem ao longo de um programa. O construtor para uma variável local é executado cada vez que a linha de controlo passa pela declaração da variável local. O destrutor para uma variável local é executado cada vez o programa sair do bloco onde foi declarada essa variável local. cout << "--------------\n"; { expressao block1; cout << "--------------\n"; { expressao block2; cout << "--------------\n"; Os resultados: } } -------------construtor destrutor -------------construtor destrutor -------------- Os construtores para “global static objects” num ficheiro são executados pela ordem em que cada uma das declarações ocorre; os destrutores são chamados pela ordem inversa. Os construtores para objectos locais estáticos são chamados na primeira vez que a linha de controlo passa pela definição do objecto. O operador new para o objecto invoca o respectivo construtor. O operador delete para o objecto chama o respectivo destrutor. Para declarar um array de objectos de uma classe com um construtor, essa classe tem de ter um construtor por defeito. Não há maneira de especificar argumentos para um construtor numa declaração de um array. O destrutor tem de ser chamado para cada elemento de um array quando esse array é destruído. Isto é feito implicitamente para os arrays que não foram reservados em memória através da utilização do operador new. Construtores A função de construção tem de ter o mesmo nome que a classe para a qual a função é declarada. Isto é, class A {...} tem de ter uma função de construção A(). A função de construção não pode ter um tipo de retorno. Uma função de construção pode ter argumentos por defeito. Os valores dos argumentos por defeito aparecem na declaração da função de construção, não na definição. A sintaxe tem o aspecto do seguinte exemplo: Vamos declarar um array de objectos. class A { Neste caso temos de usar um //................. construtor por defeito (porque não A(int, int=2,char=‘q’); podemos especificar uma lista de //................ argumentos para cada objecto). Por defeito pode significar tanto um } construtor sem argumentos como A::A(int i1, int i2,char c1) um construtor com todos os {................................} argumentos por defeito. Não é possível obter o endereço de uma função de construção. Uma função de construção não pode ser declarada como static, const, ou volatile. Uma função de construção tem de ser uma função membro não estática porque as funções de construção não podem existir separadamente dos objectos particulares. Uma função de construção não pode ser herdada. É única para a sua classe. Quando um objecto de uma classe derivada é inicializado, o construtor da classe base é invocado primeiro, e só depois o construtor da classe derivada é invocado também. Um “copy construtor”, tal como um construtor por defeito, pode ser gerado pelo compilador. Funções membro da classe podem ser chamadas de dentro de uma função de construção, e objectos membros da classe podem ser referenciados dentro de uma função de construção. 3.2. Construtores de cópia (copy construtor) O “copy construtor” para uma classe é um caso especial em que o argumento é uma referência para um objecto da mesma classe. Uma maneira diferente de construir um objecto pode ser a seguinte: pessoa p1(21,”Paulo”); pessoa p2 = p1; O primeiro objecto (p1) é construído através do construtor implementado para a classe pessoa. O segundo objecto (p2) é construído de outra forma. O que se pretende ao declarar p2 desta forma é criar uma réplica de p1. Para isso existe outro tipo de construtor, o construtor de cópia (copy construtor). O que o construtor de cópia por defeito (default copy construtor) do compilador faz é reservar espaço no stack para p2 (tal como faria o construtor por defeito do compilador) e copiar para esse espaço p1. O objecto a copiar pertence à classe pessoa. O objecto possui portanto um ponteiro com o mesmo valor. memória memória char* Nome Objecto p1 char* Nome Objecto p2 P a u l o NULL Quando a função onde os objectos foram definidos acabar a sua execução os objectos vão ser destruídos. O primeiro a ser destruído é o objecto p2. A ordem de destruição dos objectos é a inversa da ordem de construção. Quando o objecto p2 for destruído é chamado o destrutor do objecto. O destrutor liberta a zona de memória onde está o Nome. Em seguida vai ser destruído o objecto p1. Quando o destrutor de p1 tenta libertar a zona da memória ocupada pelo Nome esta já não lhe pertence, porque foi libertada por p2. Mais uma vez, a melhor maneira de resolver este problema passa pela implementação de um construtor próprio para o objecto. Assim, o construtor de cópia para a classe pessoa será: class pessoa { unsigned short Idade; char* Nome; public: pessoa(unsigned short, char*); pessoa(pessoa&); pessoa(const pessoa&); //......................... }; pessoa::pessoa(const pessoa &p) { Nome = new char[strlen(p.Nome)+1]; strcpy(Nome,p.Nome); Idade = p.Idade; } O construtor de cópia é declarado como um construtor normal, só que como parâmetro de entrada recebe a referência de um objecto da mesma classe (o objecto a copiar). Na implementação segue os mesmos passos do construtor normal, só que inicializa os objectos membros com os valores do objecto a copiar. Aos objectos que são ponteiros para zonas de memória conseguidas com o operador new, é dado o valor de novas zonas de memória reservadas para o novo objecto, de forma a evitar o problema referido. A outra situação onde se usa o construtor de cópia é na chamada de funções que recebem objectos da classe como parâmetros. void enviar_dados(pessoa); void main(void) { pessoa p(25,”Paulo”); enviar_dados(p); // ............................. } A função enviar_dados recebe como parâmetro o objecto p. Para o usar vai ter que criar na sua região do stack uma cópia do objecto original. Como a cópia do objecto é destruída quando a função terminar, se o construtor de cópia não for o adequado pode surgir o mesmo problema do caso anterior. 3.3. Chamada de construtores para classes bases e derivadas (construção e destruição em classes derivadas) Podemos construir uma classe aluno derivada da classe pessoa. class aluno : public pessoa { int grupo; public: //............... }; Tem interesse definir também para esta classe um construtor, um destrutor e um construtor de cópia. Como a classe aluno é uma classe derivada, contém todos os elementos da classe base. Por isso, dentro dos construtores e do destrutor da classe aluno é feita a chamada aos construtores e ao destrutor (respectivamente) da classe base (pessoa). class aluno : public pessoa { int grupo; public: aluno(unsigned short Id, char* No, int gr); aluno(const aluno&); // o construtor de cópia ~aluno(); //............... }; aluno::aluno(unsigned short Id, char* No, int gr) : pessoa(Id,No) { grupo = gr; } aluno::aluno(const aluno& p) : pessoa(p) { grupo = p.grupo; } pessoa::pessoa(const pessoa &p) copy constructor for pessoa //................... { Nome = new char[strlen(p.Nome)+1]; strcpy(Nome,p.Nome); Idade = p.Idade; cout << "copy constructor for pessoa\n"; } class pessoa int main(int argc, char* argv[]) { { protected: aluno a(21, "Ricardo", 6251); unsigned short Idade; copy_demo(a); char* Nome; a.print_me(); public: copy_demo_ref(a); pessoa(const pessoa&); //......................................... //...................................... void copy_demo(aluno ALUNO) class aluno : public pessoa {} { void copy_demo_ref(aluno& ALUNO) public: // aluno(const aluno&); {} Erro: No copy constructor available for class 'aluno' class pessoa int main(int argc, char* argv[]) { { protected: aluno a(21, "Ricardo", 6251); unsigned short Idade; copy_demo(a); char* Nome; a.print_me(); private: copy_demo_ref(a); pessoa(const pessoa&); //......................................... //...................................... void copy_demo(aluno ALUNO) class aluno : public pessoa {} { void copy_demo_ref(aluno& ALUNO) public: // aluno(const aluno&); {} aluno::aluno(const aluno &p) : pessoa(p) { grupo = p.grupo; cout << "copy constructor for aluno\n"; pessoa::~pessoa() { delete[] Nome; cout << "destructor for pessoa\n"; } class aluno : public pessoa { public: aluno(const aluno&); copy constructor for pessoa copy constructor for aluno //................... destructor for aluno destructor for pessoa } aluno::~aluno() { cout << "destructor for aluno\n"; } int main(int argc, char* argv[]) { aluno a(21, "Ricardo", 6251); copy_demo(a); a.print_me(); copy_demo_ref(a); //......................................... Destrutores A função de destruição tem o mesmo nome que a sua classe, excepto que o seu nome é precedido por um til (~). Podemos então declarar um destrutor da seguinte maneira: class A { //...................... ~A(); // destrutor para A }; A função de destruição pode estar “inline”. Se estiver “out-of-line”, a sua definição será escrita do seguinte modo: A::~A() { //.............. } A finalidade da função de destruição é oposta à de construção. O destrutor “des-inicializa” um objecto. Uma função de destruição não pode ter um tipo de retorno. Tal como para um construtor, o compilador gera o seu próprio e interno tipo para o destrutor. Não nos é permitido escrever um código que tenha como retorno um tipo. Ao contrário da função de construção uma função de destruição não pode ter argumentos. Como um destrutor não tem argumentos não se pode utilizar os mecanismos de “overloading”. A questão dos argumentos por defeito, evidentemente, também não é aplicada às funções de destruição. Não podemos obter o endereço de uma função de destruição. Não temos necessidade de aceder a uma função de destruição através de um ponteiro porque a função é utilizada apenas para destruir um objecto da classe. Uma função de destruição tem de ser uma função membro não estática da sua classe. Isto é, um destrutor não pode ser declarado como const, volatile, ou static. Uma função de destruição não pode ser herdada. Neste caso temos o mesmo problema que acontece para os construtores. Se um destrutor pudesse ser herdado, o correcto destrutor poderia não ser executado. Quando uma declaração de uma classe contém objectos membros que são eles próprios objectos de outras classes, o corpo do destrutor da classe (que contêm esses) é executado antes dos destrutores para esses objectos contidos.