Conteúdos da cadeira

Herança e polimorfismo

Classes derivadas, funções virtuais e override, classes abstratas, slicing e destrutor virtual.

Markdown

Perguntar sobre esta página

ChatGPTClaudePerplexityGeminiCopiar e abrir

Envia o link e pede à IA para ler a página. No Gemini, cola a pergunta copiada.

Ver pergunta para copiar
Nesta página

Quando duas classes partilham dados e comportamento, copiar os membros de uma para a outra duplica código e espalha cada correção por vários sítios. A herança resolve isto: uma classe derivada herda os membros de uma classe base e acrescenta ou especializa o que precisa. O polimorfismo vai um passo além: trata objetos de classes diferentes através da mesma interface, e cada objeto responde à sua maneira.

Herdar de uma classe base

A classe derivada declara a base a seguir a dois pontos. Herdam-se os membros públicos e protegidos (protected, acessível à classe e às derivadas); o construtor da base corre sempre antes do da derivada:

#include <iostream>
#include <string>

class Pessoa {
public:
    Pessoa(const std::string& n) : nome(n) {}

    void apresentar() const {
        std::cout << "Sou " << nome << "\n";
    }

protected:
    std::string nome;
};

class Aluno : public Pessoa {
public:
    Aluno(const std::string& n, int nro) : Pessoa(n), numero(nro) {}

    void apresentar_numero() const {
        std::cout << nome << " tem o numero " << numero << "\n";
    }

private:
    int numero;
};

int main() {
    Aluno a("Ana", 123);
    a.apresentar();
    a.apresentar_numero();
}

Isto escreve Sou Ana e depois Ana tem o numero 123. O construtor de Aluno chama o da base na lista de inicialização (Pessoa(n)) antes de inicializar o seu próprio atributo. O método herdado apresentar funciona sobre o objeto derivado sem ser reescrito, e o membro protected fica visível nos métodos de Aluno mas continua escondido de main.

Funções virtuais e override

Se a derivada redefinir um método da base, qual das versões corre? Sem virtual, decide o tipo da variável em tempo de compilação. Com virtual, decide o tipo real do objeto em tempo de execução: é esta escolha tardia que se chama polimorfismo. A redefinição marca-se com override, que manda o compilador confirmar que existe mesmo um método virtual com essa assinatura na base:

#include <iostream>
#include <string>

class Pessoa {
public:
    Pessoa(const std::string& n) : nome(n) {}
    virtual ~Pessoa() = default;

    virtual void apresentar() const {
        std::cout << "Sou " << nome << "\n";
    }

protected:
    std::string nome;
};

class Aluno : public Pessoa {
public:
    Aluno(const std::string& n, int nro) : Pessoa(n), numero(nro) {}

    void apresentar() const override {
        std::cout << "Sou " << nome << ", aluno numero " << numero << "\n";
    }

private:
    int numero;
};

void cumprimentar(const Pessoa& p) {
    p.apresentar();
}

int main() {
    Aluno a("Ana", 123);
    cumprimentar(a);
}

Isto escreve Sou Ana, aluno numero 123. A função cumprimentar recebe uma referência para Pessoa, mas o objeto real é um Aluno, e é a versão de Aluno que corre. Sem o virtual, sairia Sou Ana: a função veria só a parte Pessoa do objeto. Repara que a passagem por referência é essencial aqui; por valor, nem o virtual salvava, como mostra a próxima secção.

Classes abstratas

Uma função virtual pura (= 0) não tem implementação na base e obriga cada derivada concreta a defini-la. Uma classe com pelo menos uma função virtual pura é abstrata: não se pode instanciar, serve apenas de interface comum:

#include <iostream>

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;
};

class Quadrado : public Forma {
public:
    Quadrado(double l) : lado(l) {}
    double area() const override { return lado * lado; }

private:
    double lado;
};

int main() {
    Quadrado q(3.0);
    std::cout << q.area() << "\n";
}

Isto escreve 9. Tentar declarar Forma f; nem compilava: uma forma genérica não tem área calculável, por isso a linguagem impede que exista. Cada forma concreta (quadrado, círculo, triângulo) implementa o seu area, e o código que usa formas fala só com a interface Forma. É o padrão do exemplo completo abaixo.

A armadilha do slicing

Guardar um objeto derivado numa variável do tipo base corta a parte derivada: chama-se slicing (fatiamento). A variável base só tem espaço para a parte base, e o resto perde-se, incluindo o comportamento polimórfico:

#include <iostream>

class Base {
public:
    virtual void quem() const { std::cout << "base\n"; }
    virtual ~Base() = default;
};

class Derivada : public Base {
public:
    void quem() const override { std::cout << "derivada\n"; }
};

int main() {
    Derivada d;
    Base copia = d;
    copia.quem();
    Base& ref = d;
    ref.quem();
}

Isto escreve base e depois derivada. A copia sofreu slicing: é um Base genuíno construído a partir da parte Base de d, por isso responde base. A referência ref aponta para o objeto completo, por isso o virtual escolhe derivada. A regra prática: polimorfismo usa-se com referências ou apontadores, nunca com objetos por valor. É também por isto que contentores de objetos polimórficos guardam apontadores (de preferência smart pointers, como viste nos apontadores).

A regra do destrutor virtual

Se uma classe tem pelo menos uma função virtual, o destrutor também deve ser virtual. Quando apagas um objeto derivado através de um apontador para a base, só o destrutor virtual garante que corre o destrutor da derivada antes do da base:

#include <iostream>

class Base {
public:
    virtual ~Base() { std::cout << "limpa base\n"; }
};

class Derivada : public Base {
public:
    ~Derivada() override { std::cout << "limpa derivada\n"; }
};

int main() {
    Base* p = new Derivada();
    delete p;
}

Isto escreve limpa derivada e depois limpa base. Sem o virtual, só correria o destrutor da base, e os recursos da derivada (memória pedida com new, ficheiros abertos) ficavam por libertar. Nos exemplos desta página escrevemos virtual ~Forma() = default;: destrutor virtual com implementação omissa, suficiente quando não há limpeza própria a fazer.

Exemplo completo: áreas de formas

Vamos juntar classe abstrata, duas derivadas e uma função que trata ambas pela interface comum. A área do círculo é pir2pi r^2; usamos 3.14163.1416 como valor de pipi.

#include <iostream>

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;
};

class Quadrado : public Forma {
public:
    Quadrado(double l) : lado(l) {}
    double area() const override { return lado * lado; }

private:
    double lado;
};

class Circulo : public Forma {
public:
    Circulo(double r) : raio(r) {}
    double area() const override { return 3.1416 * raio * raio; }

private:
    double raio;
};

void mostrar_area(const Forma& f) {
    std::cout << "Area: " << f.area() << "\n";
}

int main() {
    Quadrado q(3.0);
    Circulo c(1.0);
    mostrar_area(q);
    mostrar_area(c);
}

Isto escreve Area: 9 e Area: 3.1416. A mesma função mostrar_area calcula áreas de formas diferentes sem saber quais: recebe referências para a base (sem slicing) e o virtual escolhe a fórmula certa em cada chamada. Confere o círculo: 3.1416times12=3.14163.1416 times 1^2 = 3.1416. Se acrescentares um Triangulo amanhã, esta função continua igual: esse é o ganho do polimorfismo, código novo sem mexer no código que já funciona.

Ver o ficheiro no GitHub

À tua maneira

Escolhe como preferes ler.

Aparência
Ajustar cores e largura
Cor de destaque do tema FEUP
Tipo de letra

Álgebra, lógica e uma ideia de cada vez.

As tuas escolhas ficam guardadas neste navegador.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.