Existem diferentes formas de se lidar com alocação de objetos e sua passagem para métodos e funções. A escolha pela maneira correta pode evitar ter que lidar com diversos problemas e, consequentemente, com bugs e dificuldades de depuração.
Vamos analisar alguns casos e ver como é possível ter implementações diferentes para um mesmo tipo de necessidade, e como determinado tipo de implementação pode interferir no resultado final, em aspectos de confiabilidade, segurança e performance.
Funções que recebem objetos por valor
Suponha que haja necessidade da criação de uma função qualquer que tome um objeto como parâmetro à fim de obter alguns de seus valores. Chamarei esta função de FuncaoXYZ:
void FuncaoXYZ(...);
Para a passagem de um objeto de uma classe qualquer (que chamarei de TClasseQualquer) por valor, temos algumas possibilidades para esta simples função:
(1) void FuncaoXYZ(TClasseQualquer objeto);
(2) void FuncaoXYZ(const TClasseQualquer objeto);
(3) void FuncaoXYZ(const TClasseQualquer *objeto);
(4) void FuncaoXYZ(const TClasseQualquer &objeto);
Neste caso, vemos como é importante o conhecimento da linguagem de programação na qual se está implementando. Este conhecimento influencia diretamente na escolha de implementação ótima e outra passível de problemas.
Nas funções acima, tanto (1) como (2) fazem uma cópia do objeto e trabalham com esta cópia dentro da função. Ou seja, a cada vez que a função é chamada ela precisa criar uma nova cópia do objeto. Isso pode consumir tempo e recursos, se o objeto for "grande" e esta operação for executada repetidas vezes, como dentro de um laço de repetição, por exemplo.
Em (1) também não há garantia de que a função não fará modificações no objeto, pois o mesmo não foi passado como uma constante. Mesmo que neste tipo de implementação ela esteja criando uma cópia do objeto original e por isso não irá modificá-lo, esse é um tipo de comportamento indesejado, que deveria ser evitado.
Em (3) não há certeza de que o objeto passado não é nulo (NULL). Logo, a função sempre deverá fazer o teste antes de acessar o objeto recebido, sob pena de acessar um endereço de memória inválido. Esse tipo de situação deve ser evitado, sendo portanto desaconselhável a passagem de objetos por ponteiro quando só desejamos acessar seus valores, sem alterá-lo.
Assim, em (4), chegamos a uma implementação que nos livra de ter que checar se o objeto recebido é nulo, por não ser um ponteiro, que garante que ele não poderá ser modificado dentro da função, por ser uma constante, e que não criará uma cópia do objeto recebido, já que foi criado um apelido (uma referência, ou seja &) para o objeto original.
Logo, utilizando a notação "const T&" (onde T é um tipo qualquer) para funções que recebem um objeto por valor, garantimos um determinado comportamento e assim saberemos, se algo der errado, que diversos tipos de condições (como as descritas acima) não irão ocorrer.
Se tomarmos esse tipo de implementação como regra, livramos o software desses problemas.
Métodos que recebem de objetos por valor
Como métodos são funções dentro de classes, podemos levar em consideração a mesma regra adotada para funções vista acima (notação "const T&").
Porém, há ainda um aspecto importante que devemos analisar: O que garante que o método, em sua implementação, não irá modificar os atributos da classe, caso desejado ?
Em C++ você dá esta garantia adicionando "const" após a declaração do método:
void FuncaoXYZ(
const TClasseQualquer &objeto) const;
Assim, o compilador emite um erro se método tentar alterar qualquer atributo da classe.
Funções que recebem objetos por referência
Quando há a necessidade de alterar um objeto recebido por parâmetro, dentro de uma função, podemos fazê-lo declarando de uma das seguintes formas:
(1) void FuncaoXYZ(TClasseQualquer *objeto);
(2) void FuncaoXYZ(TClasseQualquer &objeto);
Como foi visto, fica claro a escolha de (2) pois não temos que testar se o objeto recebido é nulo. E também fica mais difícil de pensar em deletar o objeto da memória, já que não é um ponteiro.
Métodos que recebem objetos por referência
A mesma notação para funções pode ser aplicada, podendo ser adicionado o uso de const quando aplicável (quando não se desejar mudar o valor de algum atributo da classe dentro da implementação do método).
Outros usos
Como a notação de referência é útil para nos poupar dos aborrecimentos trazidos pelos ponteiros, podemos utilizá-la sempre que possível. Mesmo utilizando bibliotecas de terceiros que possuam a notação de ponteiro (*), podemos adaptar seu uso.
Por exemplo:
void TMinhaClasse::ReimplementacaoDoMetodoASDFG(
const TAlgumaClasse *ponteiro)
{
#ifdef _DEBUG // PRE-CONDICAO
assert( ponteiro != NULL );
#endif
const TAlgumaClasse &referencia = ponteiro;
// trabalha com "referencia" ao inves
// de "ponteiro"
// ...
}
Ao invés de utilizar o ponteiro real, criamos uma referência para ele.
Vale lembrar que se a função aceitasse nulo (NULL) como um valor válido, não deveríamos substituir o ponteiro pela notação de referência. (Ou até poderíamos fazê-lo se o valor nulo pudesse ser tratado em um bloco de código separado (com um if, por exemplo), sendo um caso a parte, e o resto da função utilizasse o objeto como sendo não-nulo.)
Conclusão
A utilização correta dos recursos da linguagem pode trazer diversos benefícios ao software escrito, desde melhoria de performance até diminuição dos índices de erro e tempo de depuração.
No caso do uso de objetos em funções e métodos, evita problemas indesejáveis relacionados à alocação memória e acesso irrestrito à informações.
Sempre que aplicável, devemos utilizar parâmetros constantes e referências ao invés de ponteiros.
Um comentário:
Aprendi muito
Postar um comentário