paulo1205
(usa Ubuntu)
Enviado em 14/07/2022 - 23:44h
As sugestões acima estão OK, mas a invocação que você fez ao
g++ não está errada, só não é muito eficiente, porque a ideia de separar a programação em múltiplos arquivos diferentes é justamente poder compilar as partes que dependam de alterações no código fonte desde a última compilação.
O problema mesmo no que você fez foi o fato de que dentro de
carro.hpp e
moto.hpp você menciona
Automovel, mas não a apresenta ao compilador. A declaração dessa classe reside em
automovel.hpp, então você vai ter de incluir (ou importar, agora com o C++-20) tal declaração antes de usá-la, quer como classe base dessas suas declarações de classes derivadas, quer para instanciar objetos.
O simples fato de você colocar todos os arquivos
.cpp na linha de comando do
g++ não faz automaticamente com que cada arquivo compilado informe ao próximo arquivo todos os símbolos que ele reconheceu ao longo da compilação. Cada novo arquivo
.cpp inicia uma compilação nova, de modo que todos os símbolos que forem necessários em cada uma dessas compilações mas não sejam diretamente declarados nesses arquivos requererão um
#include (ou
import) do arquivo onde as respectivas declarações ocorrerem.
Dito isso, notei nos seus arquivos que você não usou a técnica de
include guarding para se proteger contra a múltipla inclusão do mesmo arquivo várias vezes na mesma compilação. Recordando brevemente como fazer, todos os seus arquivos de cabeçalhos, tanto em C quanto em C++, devem ter a seguinte forma geral.
// Exemplo: arquivo “exemplo.h”
#ifndef EXEMPLO_H_GUARD__ // Testa se o símbolo de guarda está indefinido.
#define EXEMPLO_H_GUARD__ // Define o símbolo de guarda, de modo a evitar que novas inclusões do mesmo arquivo dentro da mesma compilação provoque o aparecimento de declarações repetidas.
/* Suas declarações e definições de tipos vão aqui. */
#endif // Fecha o bloco de compilação condicional baseada no símbolo de guarda.
Você deve aplicar isso a todos os seus cabeçalhos (no caso, a
automovel.hpp,
carro.hpp e
moto.hpp).
Ainda dois outros problemas. O primeiro, mais estético do que funcional, é que você incluiu <iostream> dentro de
automovel.hpp, mas aparentemente não usou nada que justificasse tal inclusão, de modo que provavelmente seria melhor não a fazer.
O segundo, um pouco mais grave, é que, provavelmente por causa da inclusão de <iostream>, você colocou uma diretiva
using namespace std; dentro do cabeçalho, o que geralmente
não é uma boa ideia, porque pode acabar poluindo o restante da compilação com símbolos que não são necessários e podem até causar problemas em determinadas situações. Normalmente, quando você usa objetos ou tipos de dados declarados em outro cabeçalho dentro de um arquivo de cabeçalhos seu, emprega sempre seus nomes completamente qualificados, como mostra o exemplo abaixo, de um arquivo
x.h que declara uma classe que depende do tipo
std::string, declarado dentro de <string>.
#ifndef X_H__
#define X_H__
#include <string>
class X {
private:
std::string value;
public:
X(const std::string &);
inline std::string get(){ return value; }
};
#endif // !defined(X_H__)
A observação acima vale para o arquivo de cabeçalho; a implementação, possivelmente num arquivo
x.cc, poderia usar a diretiva
using namespace sem problemas (desde que alguém não resolva chamar
x.cc dentro de um
#include, o que é tecnicamente possível, mas provavelmente errado).
// Arquivo x.cc: implementação da classe X.
#include <string>
#include "x.h"
using namespace std;
X::X(const string &s): value(s) { }
Ainda mais uma observação: você falou em polimorfismo, mas suas classes não tem funções-membros declaradas como
virtual.
É verdade que o termo “polimorfismo” tem mais de um sentido possível, dependendo do contexto. Entretanto, quando se fala de OO, e de C++ em particular, o efeito desejado é permitir que uma referência ou ponteiro para um objeto de uma classe base adquira comportamentos diferenciados quando fizer referência a um objeto de uma classe derivada. Com essa finalidade, o C++ requer que as funções-membros sujeitas a comportamento polimórfico sejam especificadas como virtuais.
Veja o seguinte exemplo.
#include <iostream>
struct automovel {
void barulho() const { std::cout << "[barulho genérico]\n"; }
};
struct carro: public automovel {
void barulho() const { std::cout << "vruum, vruum\n"; }
};
struct motoca: public automovel {
void barulho() const { std::cout << "pó-ró-pó-pó-pó-pó\n"; }
};
int main(){
automovel *autos[]={ new automovel, new carro, new motoca };
for(auto x: autos)
x->barulho();
}
O programa acima vai imprimir três vezes “[barulho genérico]”. Não vai haver polimorfismo, embora os ponteiros tenham sido criados para classes derivadas, porque a função barulho não foi declarada como virtual na classe base.
Se você mudar apenas a declaração de
automovel::barulho para incluir a especificação de que a função-membro é virtual, o polimorfismo vai aparecer.
struct automovel {
virtual void barulho() const { std::cout << "[barulho genérico]\n"; }
};
$ g++ x.cc
$ ./a.out
[barulho genérico]
vruum, vruum
pó-ró-pó-pó-pó-pó
Muitas vezes (provavelmente a maioria esmagadora das vezes), porém, quando você tem uma classe base polimórfica, você quer obrigar todas as instâncias a serem de uma classe derivada, com a classe base servindo apenas como uma referência genérica para objetos de tipos correlatos mas não necessariamente idênticos. Nesse caso, uma ou mais funções virtuais da classe base são definidas não com um corpo, mas com valor igual a zero, chamando-se “funções-membros virtuais puras” (
pure virtual member functions), e a classe passa a ser classificada como classe abstrata, pois não se podem criar objetos dessa classe, mas tão-somente de classes derivadas que possuam suas próprias reimplementações de todas as funções virtuais puras da classe base.
No nosso exemplo, para transformar a classe
automovel em abstrata, eis como se faria.
struct automovel {
virtual void barulho() const = 0;
};
Obviamente, tal alteração faria com que o primeiro elemento usado nas versões anteriores do programa no vetor
autos, dentro de
main(), não pudesse mais ser declarado, pois
new automovel passa a ser uma tentativa inválida de instanciar uma classe abstrata.
Funções virtuais explícitas do C++ podem causar alguma confusão inicial na cabeça de quem vem de experiência anterior com Java ou similares. Isso porque em Java todos os objetos de qualquer classe são necessariamente referências (i.e. ponteiros), e todas as funções-membros são implicitamente polimórficas (aquilo que o C++ chama de funções-membros virtuais).
Para implementar funções virtuais (no caso do Java ou similares, todas as funções-membro de qualquer classe), é necessário fazer uso interno de ponteiros, com uma tabela de ponteiros com uma entrada para cada função virtual. Esse uso de ponteiros pode acarretar algum impacto de desempenho, ao se comparar uma função virtual com uma que não seja virtual (cuja chamada ao endereço de destino pode ser embutida diretamente no código executável, em vez de se consultar a tabela primeiro, e só depois executar a chamada). Por esse motivo, o C++ permite distinguir funções-membros não-virtuais e virtuais, e é recomendável usar não-virtuais sempre em que o polimorfismo não for estritamente necessário para tais funções.
... Então Jesus afirmou de novo: “(...) eu vim para que tenham vida, e a tenham plenamente.” (João 10:7-10)