Muito se discute a respeito das facilidades criadas com a utilização de linguagens orientadas a objetos (OO), principalmente quando a comparação é realizada com a linguagem C. De fato, a linguagem C não possui as características de uma linguagem OO, contudo alguns desses aspectos podem ser criados em C. É claro que isto exige empenho por parte do programador. Nesse contexto, este artigo tem o objetivo de apresentar uma forma de implementar os principais conceitos de OO: Herança, Encapsulamento e Polimorfismo. O aprofundamento dos tópicos não será apresentado neste artigo, pois o objetivo é demonstrar a utilização dos ponteiros e suas aplicações!
Aspectos de Orientação a Objeto
Como apresentado na introdução, Herança, Encapsulamento e Polimorfismo são conceitos básicos de OO. O encapsulamento já foi discutido no artigo anterior em que os tipos de dados abstratos (TDA) eram criados de forma que sua implementação não era conhecida. Diante disso, um TDA é considerado como uma classe em OO, pois tem como função representar uma estrutura geral e as suas operações. Esse aspecto permite modularizar um código, isto é, organizá-lo em unidades funcionais distintas. Neste ponto, dois tópicos são essenciais: Herança e Polimorfismo.
- Herança – Uma característica importante da POO é permitir a criação de novas classes a partir de uma classe já existente. Essa característica pode ser entendida como uma especialização, isto é, uma forma de ampliar a funcionalidade de uma classe;
- Polimorfismo – A palavra polimorfismo significa ‘de muitas formas’. Na POO isso significa que a partir de um mesmo nome, que representa um objeto ou método, podemos ter várias formas, isto é, comportamentos diferentes.
Quando a herança é utilizada temos uma relação hierárquica. A classe que foi herdada é chamada de classe base, já a outra classe é chamada de derivada. Nesse contexto, a classe derivada herda de uma classe base todos os métodos, campos, propriedades e eventos da classe base. Dependendo da forma como isso foi especificado, é possível definir alguns elementos que podem ser alterados pela classe derivada! Considere o exemplo clássico [2] de formas geométricas mostrado na Figura 1.
A classe Shape define um conjunto de atributos e um método chamado Draw. Note que na declaração do método Draw aparece a keyword ‘virtual’. Essa keyword especifica que o método pode ser substituído em uma classe derivada. Na implementação mostrada abaixo optou-se pela linguagem C#.
public class Shape
{
// A few example members
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
// Virtual method
public virtual void Draw()
{
Console.WriteLine("Performing base class drawing tasks");
}
}
Abaixo são mostradas as classes derivadas da classe Shape. Observe que nas classes Circle, Rectangle e Triangle o método Draw aparece com a keyword ‘override’, indicando a substituição do método da classe base pelo método da classe derivada. Além disso, todas essas classes podem acessar os atributos de Shape: X, Y, Height e Width.
class Circle : Shape
{
public override void Draw()
{
// Code to draw a circle...
Console.WriteLine("Drawing a circle");
base.Draw();
}
}
class Rectangle : Shape
{
public override void Draw()
{
// Code to draw a rectangle...
Console.WriteLine("Drawing a rectangle");
base.Draw();
}
}
class Triangle : Shape
{
public override void Draw()
{
// Code to draw a triangle...
Console.WriteLine("Drawing a triangle");
base.Draw();
}
}
O potencial deste tipo de implementação pode ser observado no código abaixo. Considere que existe uma estrutura de dados do tipo Shape, ou seja, da classe base. Essa estrutura de dados é populada com objetos do tipo Rectangle, Triangle e Circle. Esse procedimento é possível, pois essas classes também são do tipo Shape. No processo iterativo ‘foreach’ essa estrutura de dados é percorrida e, para cada iteração, o método Draw definido em Shape é invocado.
shapes.Add(new Rectangle());
shapes.Add(new Triangle());
shapes.Add(new Circle());
foreach (Shape s in shapes)
{
s.Draw();
}
À primeira vista, o método Draw em Shape será executado, contudo esses métodos foram estendidos. Portanto, ao chamar o método Draw a partir de Shape, o método será executado na classe derivada! Assim, cada classe derivada apresentou um comportamento diferente, porém a chamada pôde ser realizada a partir de um nome comum.
Polimorfismo e Funções Virtuais em C
A forma mais simples de criar um mecanismo de herança e polimorfismo em C é a partir das tabelas de funções virtuais, também chamada de VTable. Uma VTable consiste de um conjunto de ponteiros para funções que são comuns nas classes derivadas e que são agrupados em uma struct. Considere, por exemplo, a classe Shape e o método virtual Draw na linguagem C.
typedef struct tShape Shape; //Define a estrutura Shape
typedef struct tShape_VTable Shape_VTable; //Define a tabela de funções virtuais
struct tShape_VTable
{
void (*Draw)(Shape * const me); //Função virtual Draw que dever ser substituída
};
struct tShape
{
//atributos
int X;
int Y;
int Height;
int Width;
//tabela de funções virtuais – Ponteiro para o tipo Shape_VTable
Shape_VTable * VTable;
};
Diante disso, as classes derivadas tem o dever de atribuir aos ponteiros de funções conforme suas implementações. A definição de Rectangle, Triangle e Circle são mostradas abaixo.
typedef struct tRectangle Rectangle;
typedef struct tTriangle Triangle;
typedef struct tCircle Circle;
struct tRectangle
{
Shape base;
//outros membros da estrutura....
};
struct tTriangle
{
Shape base;
//outros membros da estrutura....
};
struct tCircle
{
Shape base;
//outros membros da estrutura....
};
Note que na definição dessas estruturas a base Shape é sempre o primeiro membro. Organizando desse modo fica fácil observar a herança e que o endereço base é o endereço da estrutura shape. Dito de outra maneira, essas estruturas apresentam a relação de extensão. A Figura 2 ilustra essa situação.
Cabe ressaltar que esse tipo de construção é possível somente porque a descrição da estrutura Shape é conhecida, caso contrário seria necessário utilizar um ponteiro para estrutura Shape. Um breve exemplo, sem considerar os módulos e construtores para cada classe derivada, ilustrando a utilização das estruturas é mostrado abaixo.
void Circle_Draw(Shape const * const me);
void Rectangle_Draw(Shape const * const me);
void Triangle_Draw(Shape const * const me);
int main()
{
Circle circle;
Triangle triangle;
Rectangle rectangle;
//tabelas de funções virtuais para cada tipo de estrutura
Shape_VTable VTable_circle;
Shape_VTable VTable_triangle;
Shape_VTable VTable_rectangle;
//inicialização das tabelas de acordo com sua função de desenho
VTable_circle.Draw = Circle_Draw;
VTable_rectangle.Draw = Rectangle_Draw;
VTable_triangle.Draw = Triangle_Draw;
//inicialização das estruturas com a tabela de funções virtuais
circle.base.VTable = &VTable_circle;
triangle.base.VTable = &VTable_rectangle;
rectangle.base.VTable = &VTable_triangle;
//Vetor com ponteiros para dados do tipo Shape, ou seja, o tipo base
Shape * shapes[3] = {
(Shape *)&circle,
(Shape *)&triangle,
(Shape *)&rectangle
};
int i;
for(i = 0; i < 3; i++)
{
//chamada da função virtual
//shapes[i]-> É um ponteiro para uma das estruturas
//shapes[i]->VTable-> Indica o acesso ao membro VTable que também é um ponteiro
//shapes[i]->VTable->Draw Acessa o ponteiro de função Draw definido em VTable
shapes[i]->VTable->Draw((Shape const * const)&shapes[i]);
}
return 0;
}
void Circle_Draw(Shape const * const me)
{
printf("Circle!\r\n");
}
void Rectangle_Draw(Shape const * const me)
{
printf("Rectangle!\r\n");
}
void Triangle_Draw(Shape const * const me)
{
printf("Triangle!\r\n");
}
Conclusão
Esse artigo apresentou, de maneira introdutória, alguns conceitos de orientação a objetos que podem ser aplicados em projetos utilizando linguagem C. Com base nesse e no artigo anterior você, leitor, poderá explorar novas formas de estruturar um projeto. Os conceitos envolvidos e demonstrados são muito importantes para reutilização de código. Outro ponto que pode ser observado é em relação à extensão e manutenção do projeto, pois a herança e o polimorfismo podem tornar o design flexível. Durante o projeto, vale a pena considerar os seguintes tópicos:
- Programe para uma interface;
- Encapsule o que varia;
- Para reduzir o acoplamento das estruturas dê prioridade à composição em relação à herança.
Para saber mais
Embora o objetivo seja a implementação das técnicas de OO em C, é extremamente importante conhecer as características de orientação a objetos. Vale a pena conferir o artigo do Felipe Lavratti sobre polimorfismo em que o tópico é demonstrado com mais detalhes de modularização.
Referências
[1] – Livro: C, completo e total – 3ª edição revista e atualizada. Herbert Schildt. [2] – https://msdn.microsoft.com/pt-br/library/ms173152.aspx









Parabéns pelo artigo! Muito bom.
Obrigado, Cesar.
Excelente artigo Fernando! Nota 10!
Apenas gostaria de acrescentar aos demais leitores que o Shape * shapes[3] não precisa ser de tamanho fixo. Se implementar em forma de lista encadeada (ao invés de array) é possível alocar quantos objetos quiserem até acabar a memória. A base da minha gfx é exatame isso.
Quem sabe fica de sugestão para mais um artigo de ponteiros, hein Fernando?
Olá, Rafael.
Um artigo sobre listas encadeadas? Pode ser uma série sobre estrutura de dados. É um assunto interessante!
Obrigado pelo retorno!