Funções
Funções são parcelas de código “reutilizável”, escritas para que possamos dar um nome a um pedaço de código e reutilzá-lo em diversas situações, possívelmente com parâmetros diferentes.
As principais vantagens ao utilizar funções são :
- Reutilização de código
- Organização do código por meio de nomes bem definidos
- Possibilidade de parametrizar um código
- Diminuição de dependências externas
Funções são declaradas utilizando a sintaxe tipoRetorno NomeFunção(parametros) :
tipoRetornoindica o tipo do valor resultante de uma função, que pode ser atribuido a uma variável ou usado em outras expressões, podemos escrevervoidpara indicar que a função não resulta em um valor.NomeFunçãoé o nome dado a função, utilizado ao chamarparametrossão os parâmetros que devem ser repassados ao chamar a função, é uma lista com a definição de zero ou mais variaveis, separadas por virgula.
Para retornar um valor em uma função é utilizado o operador return, ele finaliza a execução da função, similar a forma como o operador break finaliza um laço de repetição.
Porém ao finalizar uma função que resulta em um valor, é necessário informar o valor ao escrever return e o uso da palavra chave return em funções assim é obrigatório (todos os caminhos devem ter um retorno).
Exemplos de funções :
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
//realiza a soma de dois números, retornando a soma
int soma(int v1, int v2)
{
return v1 + v2;
}
/* Em muitos casos, quando queremos retornar mais de um valor,
utilizamos um "parâmetro como ponteiro" para retornar valores
adicionais, neste caso temos uma função bhaskara que retorna se
a operação deu certo, e preenche x1 e x2 que são passados via ponteiro */
bool bhaskara(double a, double b, double c,
double *restrict x1, double *restrict x2)
{
const double delta = b*b - 4*a*c;
//Raiz de número negativo é um número imaginário!
if(delta < 0)
return false;
const double raizDelta = sqrt(delta);
if(x1 != NULL)
*x1 = (-b + raizDelta) / (2 * a);
if(x2 != NULL)
*x2 = (-b - raizDelta) / (2 * a);
return true;
}
//Função main testando ambas
int main()
{
int resultado1 = soma(2,3); //5
printf("A soma resultou em %d\n", resultado1);
double r1;
double r2;
if(bhaskara(2, -3, -5, &r1, &r2))
printf("Bhaskara resultou em %.2f e %.2f\n", r1, r2);
else
printf("Bhaskara deu negativo!\n");
}
Também é importante mencionar que desde o C99, é possível acessar o nome da função atual utilizando a macro __func__, que é tratado como uma variável constante e de duração estática (que claramente só será incluida na memória do programa se for utilizada).
Ponto de entrada
Em todas as aplicações feitas em C, exceto as que não usam o ambiente de execução padrão do C, precisam escrever uma função especial denominada main.
O main é o chamado “ponto de entrada” do programa, é onde o seu programa em C começa a executar.
Existem duas formas “padronizadas” de escrever o ponto de entrada.
A primeira, sem parâmetros :
int main() { /* conteúdo */ }
A segunda, que recebe :
argc, indica a quantidade de argumentos da linha de comandoargv, uma lista com os argumentos da linha de comando
int main(int argc, char *argv[]) {/* conteúdo */}
Os argumentos da linha de comando são os valores repassados a um programa quando ele inicia.
Quando iniciamos um programa pelo terminal, a linha de texto do comando utilizado para chamar o programa é enviada ao programa e cada argumento separado por espaço vira um elemento diferente de argv e a quantia de argumentos é repassada através de argc.
Historicamente o primeiro argumento da linha de comando de um programa é o caminho do arquivo utilizado para chamar o programa.
Porém, apesar dessa regra ser seguida no geral pelos terminais de qualquer sistema operacional, vale lembrar que isso não é totalmente garantido.
Qualquer programa pode diretamente chamar as funções de baixo nível para iniciar um processo diretamente como a CreateProcessW no windows, execve no linux ou posix_spawn presente no linux e implementada como chamada de sistema no macOs.
Ao chamar essas funções diretamente, é possível ignorar essa convenção histórica, o que apesar de incomum, é uma possibilidade.
Por exemplo ao executar um programa escrevendo o seguinte :
programa teste.txt -t 50
Teremos argc = 4 e os valores de argv serão:
argv[0] = "programa"
argv[1] = "teste.txt"
argv[2] = "-t"
argv[3] = "50"
Retorno da função main
A função main retorna um valor do tipo int que indica um status de finalização que é retornado ao sistema operacional.
Retornar da primeira chamada da função main é equivalente a chamar a função exit, onde o retorno do main será o código de status repassado ao sistema.
Para indicar uma execução bem sucedida, usa-se o valor 0 ou a macro EXIT_SUCCESS definida na stdlib.h.
Para indicar um erro ou falha na execução, usa-se um valor diferente de 0, geralmente positivo ou a macro EXIT_FAILURE definida também na stdlib.h.
Esses códigos de status podem ser acessados pelo terminal diretamente após executar um programa utilizando a variável %errorlevel% no Windows ou $? no bash em linux ou macOs.
Podemos fazer um programa que justamente testa isso :
#include <stdlib.h>
//Converte o texto do primeiro argumento em inteiro e retorna ele no main
//De forma que o código de status do sistema seja igual ao número passado de argumento
int main(int argc, char **argv)
{
return (argc > 1) ? (int) strtol(argv[1]) : EXIT_SUCCESS;
}
Para testar no windows :
testaerro.exe 5
echo %errorlevel%
Para testar no linux ou macOs :
./testaerro 5
echo $?
Também é importante lembrar que desde o C99, não é necessário escrever return na função main, pois a ausência de return indica que o valor retornado será 0 (só se aplica ao main).
Declaração de funções
Se uma função é definida no C, ela geralmente deve ser definida antes da função que a utiliza, sem isso, compilar o programa resulta em um erro de compilação.
Porém, existe uma forma de burlar isso, declarando a função sem definir ou implementar ela, esse tipo de declaração também é chamado como uma declaração de um “protótipo de função”.
Exemplo :
//Declaração, a forma como a função é
int soma(int v1, int v2);
int main()
{
printf("1 + 2 = %d\n", soma(1,2));
}
//Definição, escreve a implementação da função
int soma(int v1, int v2)
{
return v1 + v2;
}
Quando declaramos uma função, estamos especificando como a função deve ser chamada e quais parâmetros ela deve receber, logo toda informação relevante para chamar a função está presente.
Mas a explicação real do que realmente acontece quando declaramos uma função é um pouco mais complexa.
Ao declarar uma função, estamos basicamente dizendo ao compilador “confia em mim, essa função claramente existe”, o compilador, por sua vez, decide acreditar em você até os últimos momentos.
Mesmo se a função não existir, a etapa de compilação que compila o código de uma unidade de tradução ainda vai funcionar, e seu código é compilado indicando que ele tem uma dependência em uma função com aquele nome.
O problema real ocorre quando chegamos a última etapa, a junção de todos os códigos compilados de todas as unidades de tradução, é neste momento que o compilador checa se a função que você queria existe em algum lugar, se não existir, teremos o erro undefined reference (referência indefinida), que indica que algo que deveria estar lá, na verdade não está.
Isso significa na prática que uma declaração de função também é uma forma de importar funções externas, declarando que ela existe no seu código.
Por exemplo o seguinte código funciona apenas se compilarmos ambos arquivos juntos :
//Arquivo1.c
#include <stdio.h>
void dizerOi()
{
puts("Ola mundo!");
}
//Fim do arquivo1.c
//Arquivo2.c
void dizerOi();
int main()
{
dizerOi();
}
//Fim do arquivo2.c
No exemplo acima, o código no arquivo2.c importa a função presente no arquivo1.c e a chama, se compilarmos apenas arquivo1.c teremos um código que carece da função main e se compilarmos apenas arquivo2.c teremos um erro de undefined reference.
De forma que esse código só funcione se ambos forem compilados juntos.
Diferença entre f() e f(void)
Até antes do C23, declarar uma função com f() é diferente de declarar uma função como f(void), f() representa uma função que aceita um número qualquer de argumentos enquanto f(void) representa uma função que não aceita argumentos.
Lembrando que esses detalhes só servem para a DECLARAÇÃO da função, mas na DEFINIÇÃO, f() ainda representa uma função que não aceita argumentos.
O código abaixo exemplifica isso:
void f(); //Declaração, aceita qualquer número de argumentos
f(2); //Permitido
void f(void); //Declaração, não aceita argumentos
f(2); //Erro de compilação
//Definição, não aceita argumentos
void f() {
}
//Definição, também não aceita argumentos
void f(void) {
}
Funções variádicas
Funções variádicas são funções que tem a capacidade de receber um número variável de argumentos.
Para escrever funções variádicas, devemos colocar ... como o último argumento de uma função, antes do C23 era necessário ter ao menos um argumento além do ..., porém no C23 essa obrigatoriedade foi removida.
Exemplo de função variadica :
int printf(const char *restrict format, ...);
Para utilizar os argumentos de uma função variádica, é necessário utilizar as macros definidas na biblioteca stdarg.h junto do tipo va_list que indica a lista de argumento variádicos.
Descrições das macros, bem como dos seus argumentos (que estão descritos entre parenteses):
-
va_start(LISTA,INICIO): a macrova_startinicializa a variávelLISTAdo tipova_listque aparece logo após o argumentoINICIO, sendo necessário informar o argumentoINICIOsempre que houver outro argumento antes dos argumentos variádicos (o que é obrigatório antes doC23). -
va_arg(LISTA,TIPO): a macrova_argretorna o próximo valor do tipo informado emTIPOda variávelLISTAdo tipova_listque foi inicializada comva_start, a ideia é que a cada chamada deva_argum argumento é extraido e ava_list“avança de posição”. -
va_copy(DESTINO,FONTE): Adicionado noC99, copia a variávelFONTEdo tipova_listpara a variávelDESTINOtambém do tipova_list, sendo necessário chamarva_endpara cada uma das listas. -
va_end(LISTA): Finaliza a variávelLISTAque foi inicializada comva_start, a ideia é que normalmente essa funcionalidade é implementada usando a stack e usarva_endlimpa a stack utilizada porLISTA.
Abaixo um exemplo de uma função de argumentos variádicos que calcula uma média aritmética.
#include <stdarg.h>
#include <stdio.h>
double calculaMedia(int quantidade, ...)
{
double soma = 0;
va_list argumentos;
va_start(argumentos);
for(int i = 0; i < quantidade; i++)
soma += va_arg(argumentos, double);
va_end(argumentos);
//Evita divisão por zero ou com números negativos...
if(quantidade <= 0)
return 0;
return (soma / quantidade);
}
int main()
{
const int quant = 3;
const double prova1 = 5.7;
const double prova2 = 8.4;
const double prova3 = 9.2;
double media = calculaMedia(quant, prova1, prova2, prova3);
printf("A media das notas é %.2f\n", media);
}
Apesar do C23 ter removido a obrigatoriedade de outros argumentos, é normal que seja necessário adicionar ao menos um argumento obrigatório, para que seja possível saber quantos argumentos são.
Também é interessante saber que algumas funções do padrão do C como printf e scanf e suas variações, que normalmente são funções variádicas, também tem outras variações que começam com v como vprintf e vscanf que aceitam um parâmetro do tipo va_list.
Isso pode ser utilizado por exemplo, para implementar uma função variádica que faça algum tratamento adicional antes de chamar essas funções, por exemplo :
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
/*
A função "scanf" normalmente é extremamente problemática para
leitura de entrada do usuário no terminal, pois pode manter coisas
pendentes no buffer de leitura
A própria documentação do manpages aponta isso e relembra a dificuldade
de usar "scanf" corretamente
Utilizar "fgets" para leitura e possivelmente depois uma função separada
de conversão como "sscanf"/"strtol"/"strtod"/"strtof" é melhor para ler
entrada do usuário, porém é mais complexo
Aqui vamos mostrar como podemos fazer a combinação "fgets" + "sscanf" ficar
transparente de forma que funcione como um "scanf" porém sem o problema de
leitura pendente.
Vale lembrar, que o "scanf" é capaz de lidar com entradas enormes e as lê
sob demanda, enquanto aqui estou tentando ler a linha inteira de uma vez,
portanto "scanf" é ideal para leituras maiores (acima dos 4KB que defini)
enquanto essa função se adequa melhor para leituras simples escritas pelo
usuário.
*/
int scanf_user(const char *restrict format, ...)
{
char line[4096];
if(fgets(line, sizeof(line), stdin) == NULL)
return feof(stdin) ? EOF : 0;
va_list args;
va_start(args, format);
int result = vsscanf(line, format, args);
va_end(args);
return result;
}
int main()
{
int test1, test2;
scanf_user("%d\n", &test1);
scanf_user("%d\n", &test2);
printf("%d e %d\n", test1, test2);
}
Para testar a diferença prática do código acima, experimente escrever abacate na primeira entrada e 10 na segunda, e depois trocar as chamadas de scanf_user para scanf e testar novamente do mesmo jeito.
Modificadores
Ainda existem 2 modificadores utilizados em funções, que fornecem dicas para o compilador, sendo eles inline e _Noreturn.
Inline
O modificador inline é utilizado como uma dica para o compilador, indicando que chamar a função, deve idealmente evitar uma chamada “real” da função e apenas inserir as instruções contidas nela diretamente no local onde ela foi chamada.
Em funções com vinculação interna, o modificador inline pode ser usado normalmente (static inline).
A regra do C é que “se todas as declarações de funções especificam inline sem extern então a definição naquela unidade de tradução é uma definição inline”, logo se em algum lugar de uma unidade de tradução escrevermos extern inline void funcao1(); por exemplo, todas as definições da função funcao1 mesmo tendo inline, não serão tratadas como inline.
Na prática isso significa que o ideal é utilizarmos sempre static inline e opcionalmente, podemos fornecer em uma das unidades de traduções compiladas, uma única versão “não inline” da função que tenha o mesmo nome.
Motivação por trás do uso de inline
Chamar funções muitas vezes é custoso comparado com algumas instruções mais simples e chamar funções bilhões de vezes pode ter um custo relevante comparado a executar diretamente suas instruções.
Porém, o compilador é livre para ignorar a presença ou ausência desse modificador, fazendo o inline de funções que são pequenas e não tem o modificador inline ou decidindo mesmo numa função que o tenha que é mais eficiente chamar a função do que gerar cópias enormes de um código que é chamado em vários lugares e é grande.
Os compiladores de hoje em dia são cada vez mais inteligentes, portanto é difícil saber se inline realmente fará alguma diferença, a melhor maneira de saber isso é testando, para isso o Compiler Explorer é uma ótima alternativa que permite visualizar o assembly gerado ao compilar.
_Noreturn
O modificador _Noreturn pode ser utilizado como noreturn ao adicionar a biblioteca stdnoreturn.h, no C23 ele foi depreciado e substituito pelo atributo [[noreturn]].
Este modificador indica que a função não vai retornar sob hipótese alguma, pois provavelmente vai utilizar exit ou algum outro mecanismo como longjmp que modifique o fluxo de execução impedindo o retorno.
Se existir a remota possibilidade da função retornar, como no caso de funções como o execve do linux, que apesar de não retornar caso bem sucedida, ainda pode falhar, o ideal é não marcar a função como noreturn.
Existem dois motivos principais para marcar uma função como noreturn, a possibilidade de otimizações adicionais pelo compilador e avisos melhores por parte do compilador, que pode possivelmente indicar quando código é escrito após a chamada de uma função noreturn ou mesmo o uso da palavra chave return dentro dela.