Função em C++: uma explicação descomplicada e sem rodeios

Função em C++: explicação detalhada e sem rodeios

Uma função em C++ nada mais é que um bloco de código que possui um nome. Simple, não? Mas é isso mesmo: uma função é um bloco de código com nome, que permite o uso desse código dentro do bloco em lugares diferentes através da invocação da função usando o seu nome.

Exemplo 1 – Definição de função em C++

#include <iostream>

using namespace std;

void contagemRegressiva(const int tempo) {
    cout << "Contagem regressiva: ";
    for (int i = tempo; i >= 0; --i) {
        cout << i << " ";
    }
}

int main()
{
    contagemRegressiva(10);
    return 0;
}

Contagem regressiva: 10 9 8 7 6 5 4 3 2 1 0

No exemplo acima, defini a função contagemRegressiva na linha 5 e a utilizei na linha 14, escrevendo o seu nome seguido de parênteses contendo o argumento da função. Por falar nisso, o que são os argumentos das funções? Irei falar disso na próxima seção, junto com os outros elementos que constituem uma função em C++.


Elementos de uma função em C++

As funções em C++ possuem 4 elementos que as definem (veja a figura abaixo):

  1. Retorno da função: a primeira coisa que aparece na definição de uma função é o seu tipo de retorno. Este é o tipo do valor que será retornado pela função, e para as funções que não retornam nada o tipo de retorno é void.
  2. Nome da função: este é o nome que será utilizado quando se quiser chamar a função no código. Basta utilizar parêntesis após o seu nome para invocar uma função.
  3. Parâmetros de entrada: uma lista de variáveis (com o tipo seguido do nome) separada por vírgulas, que são utilizadas no corpo da função. Os parâmetros de entrada são inicializados a partir dos valores fornecidos pelo usuário ao invocar a função (no exemplo 1, linha 14, tempo é inicializado com o valor 10). Esse valores são chamados de argumentos da função.
  4. Corpo da função: é o bloco de código representado pelo nome da função. Toda vez que a função é invocada, é como se o corpo da função fosse inserido no lugar da sua chamada. O corpo de uma função pode ser vazio.
Elementos de uma função em C++

Declaração e protótipo de uma função em C++

Uma função criada sem o seu corpo, como no exemplo 2, é dita declarada, mas não definida. Esse tipo de função não pode ser invocado no código antes de ser definida em alguma outro lugar.

Se removemos da declaração da função o seu tipo de retorno, obtemos o que se chama seu protótipo ou assinatura. Assim sendo, a assinatura da função contagemRegressiva é contageRegressiva(const int tempo) (o nome do parâmetro pode ser omitido). Se você quiser uma lista mais abrangente do que pode aparecer em uma declaração de função em C++, veja esse artigo do CppReference.

Note que no caso das funções template, o protótipo da função pode conter o tipo de retorno.

Uma função só pode aparecer no código após a sua definição, caso contrário a compilação falhará.


Exemplo 2 – Declaração de funções em C++

void contagemRegressiva(const int tempo);

double raizQuadrada(const double x);

// Uso da função antes de sua definição
erro();

void erro() {
    cout << "Erro!" << endl;;
};

Definição de uma função em C++

Uma função, assim como uma variável, pode ser declarada em um lugar e definida em outro. Felizmente, assim como para as variáveis, definir uma função é muito simples: basta reescrever a declaração da função sem o ponto-e-vírgula (;) e adicionar-lhe o seu corpo, como vemos na linha 5 do exemplo 1 e na linha 8 do exemplo 2.


O que acontece quando uma função é invocada?

Quando chamamos uma função em algum lugar do código, ao chegar na linha em que a função é invocada, o programa transfere o controle de si para a função.

A primeira coisa que é feita quando a função recebe o controle do programa é a inicialização dos seus parâmetros de entrada. Tomando o bloco a seguir como exemplo, na linha 4 o programa passa o controle da execução para a função, e o parâmetro da função x é inicializado com o valor 22.4.

Os parâmetros de uma função em C++ são como variáveis locais à função, e sua inicialização é idêntica à das variáveis locais ordinárias (visite nosso artigo sobre o assunto se quiser relembrar como acontece a inicialização de variáveis).

double raizQuadrada(const double x) {...}

int main() {
    raizQuadrada(22.4);
}

Inicialização dos parâmetros da função por cópia

Por padrão, os parâmetros de uma função são inicializados com uma cópia dos argumentos fornecidos durante a chamada da função, como quando criamos uma variável fazendo simplesmente int x = y – o valor em y é copiado para x. Em inglês isso é chamado de pass-by-value (ou “passagem por valor”).

Vejamos um exempo de pass-by-value a seguir.


Exemplo 3 – Passando argumentos por cópia

#include <iostream>
#include <cmath>

using namespace std;

void quadrado(int x) {
    x = pow(x, 2);
    cout << "Valor de x na funcao: " << x << endl; 
}

int main()
{
    int x = 10;
    quadrado(x);
    cout << "Valor de x na main: " << x << endl;
}

Valor de x na funcao: 100
Valor de x na main: 10

Como vemos no exemplo acima, após chamar a função quadrado que recebe um valor inteiro por cópia usando x como argumento, mesmo modificando o valor de x dentro da função, o valor de x na main permanece 10.

Isso se dá porque dentro da função nós modificamos o x que é uma cópia do x da main, ou seja, elas são duas variáveis diferentes que possuem o mesmo valor. Logo, se mudamos uma a outra permanece inalterada.


Inicialização dos parâmetros da função por referência

Outro tipo possível de parâmetro de uma função em C++ é a referência. Assim como uma variável pode ser uma referência para outra variável, um parâmetro de função também pode ser uma referência para o argumento da função.

Assim, quando chamamos uma função cujos parâmetros são referências, esses parâmetros tornam-se referências à variáveis que fornecemos na entrada da função, e portanto modificá-los no corpo da função também alterará o valor das variáveis que passamos como argumento até mesmo fora da função.

Uma vantagem das referências como parâmetros das funções é que as referências evitam a criação de uma cópia dos argumentos de entrada, o que pode resultar em um aumento importante da performance do programa quando tais argumentos são objetos grandes.

Vejamos como usar parâmetros de tipo referência no exemplo 4.

Best practices

Sempre que não for modificar o valor de um parâmetro de uma função, utilize referências constantes ao invés de passar o argumento por cópia.

Exemplo:
`void random(string str) {…}` → `void random(const string& str) {…}`


Exemplo 4 – Passando argumentos por referência

#include <iostream>
#include <cmath>

using namespace std;

void quadrado(int& x) {
    x = pow(x, 2);
    cout << "Valor de x na funcao: " << x << endl; 
}

int main()
{
    int x = 10;
    quadrado(x);
    cout << "Valor de x na main: " << x << endl;
}

Valor de x na funcao: 100
Valor de x na main: 100

No bloco de saída anterior, vemos que x passado como argumento da função quadrado teve seu valor alterado dentro da função (de 10 para 100), pois que o parâmetro da função quadrado foi alterada para int&, ou seja, referência para int.


Parâmetros de tipo ponteiro em uma função em C++

Além dos parâmetros comuns e das referências, as funções em C++ também podem ter parâmetros de tipo ponteiro. Um parâmetro ponteiro é declarado da mesma forma que uma variável comum de tipo ponteiro: basta adicionar * na frente do tipo – const char* c, por exemplo.

Para se inicializar um parâmetro de tipo ponteiro, é preciso passar para a função o endereço de uma variável do tipo correto. No exemplo 5 o parâmetro x da função quadrado é do tipo ponteiro para int, e é inicializado com o endereço da variável x da main através do operador de endereçamento & (linha 15).


Exemplo 5 – Passando argumentos para parâmetros ponteiro

#include <iostream>
#include <cmath>

using namespace std;

void quadrado(int* x) {
    *x = pow(*x, 2);
    cout << "Valor de x na funcao: " << x << endl;
    cout << "Oops! quis dizer: " << *x << endl;
}

int main()
{
    int x = 10;
    quadrado(&x);
    cout << "Valor de x na main: " << x << endl;
}

Valor de x na funcao: 0x7ffe952ef774
Oops! quis dizer: 100
Valor de x na main: 100


Retorno de uma função em C++

As funções em C++, como já sabemos a essa altura do artigo, podem retornar valores após sua execução. Para retornar algo de uma função, basta escrever no corpo da função return seguido do que se deseja retornar.

Contudo, o que não falei ainda é que o retorno das funções, assim como os parâmetros de entrada, podem ser feitos por cópia, por referência, ou ainda podem ser ponteiros.

Vejamos, portanto, como funcionam esses diferentes tipos de retornos de função em C++.

Retorno por cópia

Esse é o mais simples dos modos de retorno de uma função: o valor retornado pela função é copiado e passado para o local onde a função foi chamada. Se tomamos o exemplo 6, na linha 16 o retorno da chamada da função multiplicarString é uma copia do string original com duas linhas adicionais que contém “Oi, meu chapa”.


Exemplo 6 – Retorno de uma cópia de variáveis locais à função

#include <iostream>

using namespace std;

string multiplicarString (string str, const unsigned int times) {
    const string originalStr = str;
    for (unsigned int i = 0; i < times - 1; ++i) {
        str += '\n' + originalStr;
    }
    return str;
}

int main()
{
    const string oi {"Oi, meu chapa"};
    string oiModificado = multiplicarString(oi, 3);
    cout << oi << " (original)" << endl;
    cout << oiModificado << endl;
}

Oi, meu chapa (original)
Oi, meu chapa
Oi, meu chapa
Oi, meu chapa


Retorno por referência e ponteiros

Quando desejamos retornar valores por referência em uma função em C++, utilizamos o símbolo & após o tipo do retorno, como quando criamos variáveis de tipo referência para algum outro tipo. O mesmo vale para ponteiros, com a diferença de que é preciso usar * após o tipo de retorno, ao invés de &.

É importanto saber que não se deve, entretanto, retornar referências para variáveis locais de uma função, pois as variáveis locais a uma função são destruidas após o término da execução da função, e quaisquer referências para tais variáveis tornam-se referências inválidas (ou dangling references, em inglês). No caso dos ponteiros, o mesmo acontece, e tais ponteiros inválidos são chamados de dangling pointers em inglês.

// Exemplo do uso errado do retorno de referências em uma função
string& multiplicarString (string str, const unsigned int times) {
    const string originalStr = str;
    for (unsigned int i = 0; i < times - 1; ++i) {
        str += '\n' + originalStr;
    }
    return str;
}

// Exemplo do uso errado do retorno de ponteiros em uma função
string* multiplicarString (string str, const unsigned int times) {
    const string originalStr = str;
    for (unsigned int i = 0; i < times - 1; ++i) {
        str += '\n' + originalStr;
    }
    return &str;
}

Nunca retorne uma referência a uma variável local de uma função em C++, porque após o fim da execução da função, o valor da variável se torna indefinido.


Próximos passos

Antes de tudo: se você chegou até aqui no artigo, parabéns! Esse foi bem longo, mas espero que tenha valido a pena lê-lo. Se quiser saber ainda mais a fundo como funcionam as funções, veja o Capítulo 1, página 28, do livro C++ Profissional, de Marc Gregoire (um ex-funcionário da Microsoft e expert em C++) ou o Capítulo 6 do livro C++ Primer, de Stanley B. Lipman.

Agora que sabemos como funcionam as funções, seus parâmetros de entrada e seus valores de retorno, vale a pena entender mais sobre um tipo mais avançado de função em C++, que dá mais flexibilidade ao usuário e previne ainda mais a repetição de código: as funções template.

Foto de perfil de Emanoel

Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.

Leave a Reply

Your email address will not be published. Required fields are marked *