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)
:
tipoRetorno
indica o tipo do valor resultante de uma função, que pode ser atribuido a uma variável ou usado em outras expressões, podemos escrevervoid
para indicar que a função não resulta em um valor.NomeFunção
é o nome dado a função, utilizado ao chamarparametros
sã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.
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_start
inicializa a variávelLISTA
do tipova_list
que aparece logo após o argumentoINICIO
, sendo necessário informar o argumentoINICIO
sempre que houver outro argumento antes dos argumentos variádicos (o que é obrigatório antes doC23
). -
va_arg(LISTA,TIPO)
: a macrova_arg
retorna o próximo valor do tipo informado emTIPO
da variávelLISTA
do tipova_list
que foi inicializada comva_start
, a ideia é que a cada chamada deva_arg
um argumento é extraido e ava_list
"avança de posição". -
va_copy(DESTINO,FONTE)
: Adicionado noC99
, copia a variávelFONTE
do tipova_list
para a variávelDESTINO
também do tipova_list
, sendo necessário chamarva_end
para cada uma das listas. -
va_end(LISTA)
: Finaliza a variávelLISTA
que foi inicializada comva_start
, a ideia é que normalmente essa funcionalidade é implementada usando a stack e usarva_end
limpa 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.