C++ Classes Abstratas

Qual é a área de uma "forma"? A pergunta não tem resposta: você só sabe calcular a área de um círculo, de um retângulo, de um triângulo. "Forma" é uma ideia que junta todos eles, não uma coisa que você desenha. Em C++, uma classe assim se chama abstrata: ela define o que todas as filhas precisam saber fazer, sem fazer ela mesma, e o compilador proíbe criar um objeto dela. Nesta aula você vai escrever a sua primeira, entender a mensagem de erro que aparece quando alguém tenta usá-la do jeito errado e ver como o C++ faz "interfaces".

Você vai aprender
  • O que é uma função virtual pura: virtual double area() const = 0;.
  • Por que uma classe com função virtual pura é abstrata e não pode virar objeto.
  • O erro real cannot declare variable … to be of abstract type e como lê-lo.
  • O que acontece quando uma filha esquece de escrever um dos métodos.
  • Como montar o exemplo Forma → Circulo / Retangulo com area().
  • O que é uma interface em C++ (uma classe só com virtuais puras) e quando usar cada uma.

Esta aula continua Polimorfismo (virtual): você vai precisar de virtual, override e do destrutor virtual. Os programas foram compilados com o g++ 15 na máquina da Ana, na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa.


A ideia: um formulário com campos obrigatórios

A Ana vai abrir uma conta no banco e recebe um formulário. Alguns campos vêm marcados com asterisco: obrigatórios. O formulário não tem as respostas; ele só diz o que precisa ser respondido. Ninguém entrega o formulário em branco, e o banco recusa um formulário com um campo obrigatório vazio.

A classe abstrata é o formulário. As funções virtuais puras são os campos com asterisco. Cada classe filha é um formulário preenchido: só quando todos os campos obrigatórios têm resposta, o compilador aceita criar objetos dela.

FormulárioC++
O formulário em brancoClasse abstrata (Forma)
Campo obrigatório (*)Função virtual pura (area() = 0)
Formulário preenchidoClasse concreta (Circulo, Retangulo)
Banco recusa formulário incompletoCompilador recusa criar objeto de classe abstrata

1. A função virtual pura: = 0

Na aula de polimorfismo, o Animal tinha um falar() "padrão" que imprimia .... Para a Forma, não existe um area() padrão que faça sentido (devolver zero seria mentir). Então, em vez de um corpo, escrevemos = 0:

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;          // virtual pura
    virtual std::string nome() const = 0;     // virtual pura
};
virtual
Como sempre: as filhas podem (aqui, devem) substituir o método, e a chamada segue o objeto de verdade.
double area() const
A assinatura que todas as filhas vão seguir: sem parâmetros, devolve double, não altera o objeto.
= 0
"Sem corpo aqui". Esse = 0 não tem nada a ver com o número zero: é só a forma (estranha, herdada dos anos 1980) de escrever "virtual pura". Uma função virtual pura é uma obrigação: toda filha que quiser virar objeto precisa escrevê-la.
virtual ~Forma() = default;
O destrutor virtual, pelo mesmo motivo da aula anterior: as formas vão ser apagadas por ponteiros de Forma.

Uma classe com pelo menos uma função virtual pura é uma classe abstrata. Uma classe que não tem nenhuma (todas preenchidas) é uma classe concreta.


2. Não dá para criar um objeto abstrato

O que aconteceria se você criasse uma Forma e chamasse area()? Não há código para rodar. Por isso, o compilador nem deixa chegar lá:

#include <string>

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

int main() {
    Forma f;
}
$ g++ -std=c++23 -Wall -Wextra abs1.cpp -o abs1
abs1.cpp: In function ‘int main()’:
abs1.cpp:11:11: error: cannot declare variable ‘f’ to be of abstract type ‘Forma’
   11 |     Forma f;
      |           ^
abs1.cpp:3:7: note:   because the following virtual functions are pure within ‘Forma’:
    3 | class Forma {
      |       ^~~~~
abs1.cpp:6:20: note:     ‘virtual double Forma::area() const’
    6 |     virtual double area() const = 0;
      |                    ^~~~
abs1.cpp:7:25: note:     ‘virtual std::string Forma::nome() const’
    7 |     virtual std::string nome() const = 0;
      |                         ^~~~
error: cannot declare variable ‘f’ to be of abstract type ‘Forma’
"Não é possível declarar a variável f com o tipo abstrato Forma".
note: because the following virtual functions are pure within ‘Forma’:
"Porque as seguintes funções virtuais são puras dentro de Forma:". O g++ lista, uma por nota, os campos obrigatórios que ninguém preencheu.
‘virtual double Forma::area() const’ / ‘virtual std::string Forma::nome() const’
As duas que faltam, com a linha de cada uma.

O Clang do editor diz a mesma coisa assim: variable type 'Forma' is an abstract class, com notas unimplemented pure virtual method 'area' in 'Forma'. Com new Forma, a mensagem muda um pouco: invalid new-expression of abstract class type ‘Forma’ (Clang: allocating an object of abstract class type 'Forma').

O que pode ser do tipo Forma

Objetos, não. Mas referências e ponteiros para Forma, sim: const Forma&, Forma*, std::unique_ptr<Forma>. Eles apontam para um círculo ou retângulo de verdade, e é assim que a classe abstrata é usada: como o tipo "qualquer forma" nas funções e nas listas.

Missão (provoque o erro): o programa compila e roda. Acrescente no main a linha Forma f; e rode: a compilação deve falhar dizendo que Forma é abstrata. Leia as notas: quais funções o compilador lista? Depois apague o = 0 de uma delas (e dê um corpo, como { return 0; }) e veja a lista diminuir.

#include <iostream>
#include <string>

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

int main() {
    std::cout << "Compilou!\n";
}
#include <iostream>
#include <string>

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

int main() {
    Forma f;
    std::cout << "Compilou!\n";
}

3. As filhas preenchem o formulário: Circulo e Retangulo

#include <iostream>
#include <memory>
#include <numbers>
#include <string>
#include <vector>

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;          // virtual pura
    virtual std::string nome() const = 0;     // virtual pura
    void descrever() const {
        std::cout << nome() << " com área " << area() << '\n';
    }
};

class Circulo : public Forma {
public:
    Circulo(double raio) : raio_{raio} {}
    double area() const override { return std::numbers::pi * raio_ * raio_; }
    std::string nome() const override { return "Círculo"; }
private:
    double raio_;
};

class Retangulo : public Forma {
public:
    Retangulo(double largura, double altura) : largura_{largura}, altura_{altura} {}
    double area() const override { return largura_ * altura_; }
    std::string nome() const override { return "Retângulo"; }
private:
    double largura_;
    double altura_;
};

int main() {
    std::vector<std::unique_ptr<Forma>> desenho;
    desenho.push_back(std::make_unique<Circulo>(1.0));
    desenho.push_back(std::make_unique<Retangulo>(3.0, 2.0));
    desenho.push_back(std::make_unique<Circulo>(2.0));

    double total = 0;
    for (const auto& f : desenho) {
        f->descrever();
        total += f->area();
    }
    std::cout << "Área total: " << total << '\n';
}
TrechoO que faz
void descrever() const na FormaUma classe abstrata pode ter métodos comuns, com corpo. Este usa nome() e area() sem saber como eles serão feitos: na hora de rodar, cada objeto entrega a sua versão.
std::numbers::piO π que vem no cabeçalho <numbers> C++20 (aula Matemática).
double area() const overridePreenche o campo obrigatório. O override confere se a assinatura bate com a da Forma.
std::vector<std::unique_ptr<Forma>>Uma lista de "qualquer forma", como o pet shop da aula anterior.
$ g++ -std=c++23 -Wall -Wextra formas.cpp -o formas
$ ./formas
Círculo com área 3.14159          « π × 1 × 1
Retângulo com área 6              « 3 × 2
Círculo com área 12.5664          « π × 2 × 2
Área total: 21.708

O descrever() foi escrito uma vez, na Forma, e funcionou para todas as formas. É um padrão muito comum: a classe abstrata escreve o "roteiro" e deixa os detalhes para as filhas.

Quando a filha esquece um campo

Se uma classe filha não escrever todas as virtuais puras, ela herda o "buraco" e continua abstrata. Veja um Quadrado que só escreveu area():

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

int main() {
    Quadrado q{2.0};
    return q.area() > 0 ? 0 : 1;
}
$ g++ -std=c++23 -Wall -Wextra abs2.cpp -o abs2
abs2.cpp: In function ‘int main()’:
abs2.cpp:19:14: error: cannot declare variable ‘q’ to be of abstract type ‘Quadrado’
   19 |     Quadrado q{2.0};
      |              ^
abs2.cpp:10:7: note:   because the following virtual functions are pure within ‘Quadrado’:
   10 | class Quadrado : public Forma {
      |       ^~~~~~~~
abs2.cpp:7:25: note:     ‘virtual std::string Forma::nome() const’
    7 |     virtual std::string nome() const = 0;
      |                         ^~~~

Repare que o erro aparece na linha que cria o Quadrado (19), não na classe. E a nota diz exatamente o que falta: nome(). Esse é o grande serviço da classe abstrata: é impossível esquecer um método obrigatório e só descobrir quando o programa estiver rodando.


4. Interfaces: classes só com virtuais puras

Em linguagens como Java e C#, existe a palavra interface. O C++ não tem essa palavra, mas tem a mesma ideia: uma classe que tem apenas funções virtuais puras (mais o destrutor virtual), sem atributos. Ela é um contrato puro: "quem herdar de mim sabe fazer isto".

A vantagem aparece quando classes que não têm nada a ver umas com as outras precisam de uma habilidade em comum. Na lojinha da Ana, produtos e clientes não são parentes, mas os dois sabem se imprimir:

#include <iostream>
#include <string>
#include <vector>

// Interface: só funções virtuais puras (e o destrutor virtual)
class Imprimivel {
public:
    virtual ~Imprimivel() = default;
    virtual void imprimir() const = 0;
};

class Produto : public Imprimivel {
public:
    Produto(std::string nome, double preco) : nome_{nome}, preco_{preco} {}
    void imprimir() const override {
        std::cout << "Produto: " << nome_ << " (R$ " << preco_ << ")\n";
    }
private:
    std::string nome_;
    double preco_;
};

class Cliente : public Imprimivel {
public:
    Cliente(std::string nome) : nome_{nome} {}
    void imprimir() const override {
        std::cout << "Cliente: " << nome_ << '\n';
    }
private:
    std::string nome_;
};

void imprimir_tudo(const std::vector<const Imprimivel*>& itens) {
    for (const Imprimivel* item : itens) {
        item->imprimir();
    }
}

int main() {
    Produto caderno{"Caderno", 12.9};
    Cliente bia{"Bia"};
    Produto lapis{"Lápis", 1.5};

    imprimir_tudo({&caderno, &bia, &lapis});
}
$ g++ -std=c++23 -Wall -Wextra interface.cpp -o interface
$ ./interface
Produto: Caderno (R$ 12.9)
Cliente: Bia
Produto: Lápis (R$ 1.5)

A função imprimir_tudo não sabe o que é um produto nem um cliente: só sabe que todos são Imprimivel. Aqui a lista guarda ponteiros comuns (const Imprimivel*), porque os objetos já vivem no main e a lista só os empresta: ninguém precisa apagá-los. O &caderno é o endereço do caderno (aula Ponteiros).

Uma classe pode herdar de uma classe base e de várias interfaces ao mesmo tempo: class Livro : public Produto, public Imprimivel, public Comparavel. Esse é o uso mais tranquilo da herança múltipla.

Classe abstrataInterface
Virtuais purasPelo menos umaTodas (menos o destrutor)
Métodos com corpoPode ter (como descrever())Não tem
AtributosPode terNão tem
Ideia"Uma família de coisas parecidas" (Forma)"Uma habilidade" (Imprimivel, Comparavel)
Quando usarAs filhas dividem dados e parte do comportamentoClasses sem parentesco precisam de um contrato comum
Nomes de interface

É costume dar a uma interface um nome de habilidade, terminando em "-ável"/"-ível": Imprimivel, Desenhavel, Comparavel, Salvavel. Ajuda a ler class Produto : public Imprimivel como "produto é imprimível".


Quando dá errado

"cannot declare variable ‘f’ to be of abstract type ‘Forma’"

O que significa: você tentou criar um objeto de uma classe abstrata. Também aparece em lugares menos óbvios: um parâmetro por valor (void f(Forma x)) ou um std::vector<Forma>, que precisariam criar objetos Forma.

Como consertar: crie um objeto de uma classe filha concreta (Circulo c{1.0};). Para "qualquer forma", use const Forma&, Forma* ou std::unique_ptr<Forma>.

A filha também é abstrata: "pure within ‘Quadrado’"

O que significa: a filha não escreveu todas as virtuais puras. As notas listam as que faltam, com o nome da classe onde foram declaradas (Forma::nome()).

Como consertar: escreva cada método listado na filha, com a mesma assinatura e override. Se a assinatura estiver diferente (um const a menos), o override vai apontar o erro.

"invalid new-expression of abstract class type ‘Forma’"
$ g++ -std=c++23 -Wall -Wextra abs3.cpp -o abs3
abs3.cpp: In function ‘int main()’:
abs3.cpp:8:20: error: invalid new-expression of abstract class type ‘Forma’
    8 |     Forma* p = new Forma;
      |                    ^~~~~
abs3.cpp:1:7: note:   because the following virtual functions are pure within ‘Forma’:
    1 | class Forma {
      |       ^~~~~
abs3.cpp:4:20: note:     ‘virtual double Forma::area() const’
    4 |     virtual double area() const = 0;
      |                    ^~~~

O que significa: "expressão new inválida para o tipo abstrato Forma". O ponteiro Forma* está certo; o new Forma, não.

Como consertar: Forma* p = new Circulo{1.0};, ou, melhor, std::unique_ptr<Forma> p = std::make_unique<Circulo>(1.0);.


Missão

Missão: crie a classe Triangulo, filha de Forma, com base e altura. A área é base * altura / 2 e o nome é Triângulo. Depois tire as barras // da linha que acrescenta um triângulo de base 4 e altura 3 ao desenho. A saída deve ter Triângulo com área 6.

#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;
    virtual std::string nome() const = 0;
    void descrever() const {
        std::cout << nome() << " com área " << area() << '\n';
    }
};

class Retangulo : public Forma {
public:
    Retangulo(double largura, double altura) : largura_{largura}, altura_{altura} {}
    double area() const override { return largura_ * altura_; }
    std::string nome() const override { return "Retângulo"; }
private:
    double largura_;
    double altura_;
};

// escreva a classe Triangulo aqui

int main() {
    std::vector<std::unique_ptr<Forma>> desenho;
    desenho.push_back(std::make_unique<Retangulo>(3.0, 2.0));
    // desenho.push_back(std::make_unique<Triangulo>(4.0, 3.0));

    for (const auto& f : desenho) {
        f->descrever();
    }
}
#include <iostream>
#include <memory>
#include <string>
#include <vector>

class Forma {
public:
    virtual ~Forma() = default;
    virtual double area() const = 0;
    virtual std::string nome() const = 0;
    void descrever() const {
        std::cout << nome() << " com área " << area() << '\n';
    }
};

class Retangulo : public Forma {
public:
    Retangulo(double largura, double altura) : largura_{largura}, altura_{altura} {}
    double area() const override { return largura_ * altura_; }
    std::string nome() const override { return "Retângulo"; }
private:
    double largura_;
    double altura_;
};

class Triangulo : public Forma {
public:
    Triangulo(double base, double altura) : base_{base}, altura_{altura} {}
    double area() const override { return base_ * altura_ / 2; }
    std::string nome() const override { return "Triângulo"; }
private:
    double base_;
    double altura_;
};

int main() {
    std::vector<std::unique_ptr<Forma>> desenho;
    desenho.push_back(std::make_unique<Retangulo>(3.0, 2.0));
    desenho.push_back(std::make_unique<Triangulo>(4.0, 3.0));

    for (const auto& f : desenho) {
        f->descrever();
    }
}

A saída da solução no laboratório:

$ ./triangulo
Retângulo com área 6
Triângulo com área 6

Referência

CódigoO que faz
virtual double area() const = 0;Função virtual pura: obrigação para as filhas
Classe com ≥ 1 virtual puraAbstrata: não pode virar objeto
double area() const override { … }A filha preenche a obrigação
Filha que não preencheu todasTambém é abstrata
const Forma&, Forma*, std::unique_ptr<Forma>Permitidos: apontam para objetos das filhas
Forma f;, new Forma, void g(Forma f), std::vector<Forma>Proibidos: criariam objetos abstratos
virtual ~Forma() = default;Sempre, em classe abstrata e em interface
InterfaceClasse só com virtuais puras e destrutor virtual

Exercícios

1. Virtual pura

Complete para perimetro() ser uma função virtual pura da Forma:

virtual double perimetro() const

2. Concreta ou abstrata?

A Forma tem area() e nome() virtuais puras. A classe Losango herda de Forma e só escreveu area(). Ela é concreta ou abstrata?

3. O tipo da lista

Complete o tipo de um vetor que guarda formas diferentes e apaga cada uma sozinho:

std::vector<> desenho;

4. Qual destas linhas compila, sendo Forma abstrata e Circulo concreta?

5. O que é uma interface em C++?

6. Em virtual double area() const = 0;, o que significa o = 0?


Resumo

  • virtual … = 0; é uma função virtual pura: não tem corpo na base e é obrigatória nas filhas.
  • Uma classe com ao menos uma virtual pura é abstrata: Forma f; dá cannot declare variable ‘f’ to be of abstract type ‘Forma’, e as notas listam o que falta.
  • A filha só vira concreta quando escreve todas as virtuais puras; senão, continua abstrata.
  • Use a classe abstrata por referência, ponteiro ou std::unique_ptr; nunca por valor.
  • Classes abstratas podem ter atributos e métodos comuns (como descrever()), que usam as virtuais puras.
  • Interface = classe só com virtuais puras; serve para dar uma habilidade comum a classes sem parentesco.
  • Sempre virtual ~Base() = default;.