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".
- 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 typee como lê-lo. - O que acontece quando uma filha esquece de escrever um dos métodos.
- Como montar o exemplo
Forma→Circulo/Retangulocomarea(). - 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ário | C++ |
|---|---|
| O formulário em branco | Classe abstrata (Forma) |
| Campo obrigatório (*) | Função virtual pura (area() = 0) |
| Formulário preenchido | Classe concreta (Circulo, Retangulo) |
| Banco recusa formulário incompleto | Compilador 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
= 0nã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
fcom o tipo abstratoForma". - 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').
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';
}
| Trecho | O que faz |
|---|---|
void descrever() const na Forma | Uma 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::pi | O π que vem no cabeçalho <numbers> C++20 (aula Matemática). |
double area() const override | Preenche 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 abstrata | Interface | |
|---|---|---|
| Virtuais puras | Pelo menos uma | Todas (menos o destrutor) |
| Métodos com corpo | Pode ter (como descrever()) | Não tem |
| Atributos | Pode ter | Não tem |
| Ideia | "Uma família de coisas parecidas" (Forma) | "Uma habilidade" (Imprimivel, Comparavel) |
| Quando usar | As filhas dividem dados e parte do comportamento | Classes sem parentesco precisam de um contrato comum |
É 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
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>.
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.
$ 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ódigo | O que faz |
|---|---|
virtual double area() const = 0; | Função virtual pura: obrigação para as filhas |
| Classe com ≥ 1 virtual pura | Abstrata: não pode virar objeto |
double area() const override { … } | A filha preenche a obrigação |
| Filha que não preencheu todas | També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 |
| Interface | Classe só com virtuais puras e destrutor virtual |
Exercícios
1. Virtual pura
Complete para perimetro() ser uma função virtual pura da Forma:
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:
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;.