Funções

Sabemos que uma função é uma regra que associa um elemento de um conjunto a outro elemento de um conjunto. Alguns exemplos são

A função \(x \mapsto x^2\) do conjunto \(\mathbb{R}\) pro conjunto \(\mathbb{R}\).

A função \(n \mapsto \varphi(n)\) de \(\mathbb{N}\) pra \(\mathbb{N}\) que associa o número \(n\) ao \(n\)-ésimo número da sequência Fibonacci.

A função \(n \mapsto \sigma(n)\) que associa um número inteiro do conjunto intervalo \([1,26]\) à letra \(\sigma(n)\) no alfabeto português \(\Sigma = \{A, B, ..., Z\}\).

Uma motivação para funções é que elas estruturam os programas e ajudam no reuso de código. Se você precisa de um programa que calcula a raiz quadrada de um número diversas vezes, você não precisa escrever a lógica da sua função diversas vezes, mas escreve-la apenas uma vez e fazer diversas chamadas.

As funções em C então serão regras que associam tipos à outros tipos. Toda função em C retorna então um valor, e esse retorno é indicado por return(), familiar não?

A função que mapeia um double para outro double que é o seu quadrado é

double quadrado(double x) {
    return(x*x);
}

A função maximo que recebe dois int e retorna o maior deles

int maximo(int a, int b) {
    return(a > b ? a:b);
}

A função potencia que recebe um double \(a\) e um int \(n\) e retorna um double, \(a^n\) é

double potencia(double a, int n) {
    double ans = 1.0;
    for (int i = 0; i<n; i++)
        ans*=a;
    return(ans);
}

A função letra que retorna a \(n\)-ésima letra maiúscula do alfabeto do tipo char ou um caractere vazio em caso de erro é

char letra(int n) {
    if(n < 1 || n > 26)
        return('\0');
    else
        return('A' + (n - 1));
}

A função Sn que recebe um long long \(n\) e computa a soma dos primeiros \(n\) números inteiros retornando um long long portanto, é

long long Sn(long long n) {
    long long ans = 0;
    for (i = 1; i <= n; i++)
        ans+=i;
    return(ans);
}

Após declaradas, funções então podem ser chamadas. Um exemplo de programa que pode ser compilado é

#include <stdio.h>

// Fatorial
long long fat(long long n) {
    long long ans = 1;
    for (i = 1; i <= n; i++)
        ans*=i;
    return(ans);
}

int main () {
    long long n;
    scanf("%lld", &n);
    printf("O fatorial de  %lld eh %lld\n", n, fat(n));
    return(0);
}

Notamos que toda função em C então deve iniciar com o seu tipo de retorno, após isso seu nome e então uma sequência de parâmetros chamados de parâmetros formais. Um parâmetro formal é uma variável que só existe no escopo da função, quando a lógica da função é escrita o parâmetro formal é um valor arbitrário daquele tipo.
Ao realizar a chamada da função na main, como no código de fat acima, escolhemos um parâmetro n particular para ser passado como argumento da função, chamamo-lo de parâmetro real.
Por fim, note que não há problemas em o parâmetro formal n de fat e o parâmetro real n em main receberem o mesmo nome.
Haveria problema, claro, se long long n fosse uma declaração global.

Métodos

Os métodos se assemelham a funções em C, estes recebem nome e podem possuir parâmetros formais, mas métodos não possuem um tipo de retorno. Utiliza-se void para iniciar a escrita de um método.
Embora não possuam um tipo de retorno, podemos utilizar return para terminar sua execução.
A motivação para métodos é também similar, com eles podemos mover lógicas importantes ou muito utilizadas em nossos programas, estruturando melhor o código e aumentando o seu reuso.

void dfs(ref_graph g, int vertex, int vis[]) {
    vis[vertex] = 1;
    printf("%d ", vertex);
    for (int i =0; i<g->V; i++) {
        if (g->adj[vertex][i] == 1 && !vis[i])
            dfs(g, i, vis);
    }
}

Passagem de parâmetros

Cópias

A forma padrão de passar argumentos para uma função em C é a passagem por valor. Quando uma função é chamada repassando uma variável como parâmetro real, é criada uma cópia do valor para a memória do parâmetro formal. Qualquer alterção, atribuição ou operação feita no parâmetro formal dentro da função afeta apenas a cópia local, mantendo a variável original intocada.

O método abaixo tem o propósito de trocar o valor de duas variáveis, o que você acha que será impresso ?

 #include <stdio.h>

 void swap (int a, int b) {
    int aux = a;
    a = b;
    b = aux;
 }

 int main() {
    int a = 1, b = 2;
    swap(a,b);
    printf("%d %d\n", a, b);
 }

Apontadores

Toda variável declarada em um programa ocupa um espaço físico na memória. Esse espaço possui um número que o identifica, o endereço de memória. Um apontador é um tipo que armazena um endereço de memória de outra variável.

Dois operadores serão fundamentais, considere char c.

Operador Operação
& &c indica o endereço de memória virtual o qual c está armazenada.
* char* a = &c indica que a é um apontador que aponta para o endereço de memória de c. Após isso, *a indica uma dereferência, o contéudo de c é acessado a partir de seu apontador.

Considere o trecho de programa

int x = 42;
int* p = &x;
int** q = &p;

x é um variável do tipo int que tem o valor 42 em seu endereço, o apontador p armazena o endereço no qual x está armazenada e por fim o apontador q armazena o endereço no qual o apontador p está armazenado.

Possíveis formas de imprimir o contéudo de x, ou seja 42 são

printf("%d\n", x);  // variavel
printf("%d\n", *p); // dereferencia
printf("%d\n", **q); // dereferencia duas vezes

Possíveis formas de imprimir o endereço de x são

printf("%p\n", (void*)&x); // operador &
printf("%p\n", (void*)p); // conteudo de p
printf("%p\n", (void*)*q); // derefencia

Passagem por referência

Em C, podemos simular uma passagem por referência passando não o valor de uma variável, mas sim o seu endereço. Uma função que tem por parâmetro formal variáveis do tipo apontador de um outro tipo, modificações no conteúdo do endereço modificam as variáveis originais que estão no endereço.

O novo método de troca agora pode modificar o conteúdo das variáveis corretamente

#include <stdio.h>

void swap (int* a, int* b) {
    int aux = *a;
    *a = *b;
    *b = aux;
}

int main() {
    int a = 1, b = 2;
    swap(&a, &b);
    printf("%d %d\n", a, b);
}

Parâmetros e retorno na main

Parâmetros

A função main é uma função como qualquer outra, ela é chamada pelo sistema operacional no momento da execução. A função main pode ser declarada com dois parâmetros formais, o argc e o argv e ela deve retornar um int, novamente, para o sistema operacional.

int main(int argc, char* argv[]) {
    return(0);
}

Utilizamos esses parâmetros normalmente para fazer chamadas do programa com a linha de comando, ou seja, é possível passar argumentos para a main quando vamos chamar a execução do nosso programa já compilado.
argc é o argument count, indica quantos argumentos a função main foi chamada. Ele é no mínimo um, o nome do programa.
argvé o argument value ele é um apontador para a matriz de strings dos outros argumentos.

Retorno

O valor retornado pela main serv como um código de status de saída. Ele informa ao sistma operacional se o programa rodou com sucesso ou se ele teve algum erro.
return(0) indica ao sistema operacional que tudo correu bem.
return(1) ou qualquer outro número diferente de zero indica erro. Cada erro tem um código diferente utilizado na saída do programa, o sistema operacional ao receber esse erro decide o que vai fazer.

#include <stdio.h>
int main(int argc, char* argv[]) {
    if (argc < 2) {
        printf("Erro, espera o nome do jogador");
        return(1);
    }
    printf("Bem-vindo, %s!\n", argv[1]);
    return(0);
}

Recursão

O \(n\)-ésimo termo da sequência de Fibonacci \(\varphi(n)\) é obtido pela soma dos \((n-1)\)-ésimo e do \((n-2)\)-ésimo termo da sequência de Fibonacci, \(\varphi(n) = \varphi(n-1) + \varphi(n-2)\).

Para fazer um programa que compute \(\varphi(n)\) é muito simples: Peça para o seu minion calcular \(\varphi(n-1)\) e peça para o seu outro minion calcular \(\varphi(n-2)\), agora você só soma os dois e tem a resposta.

Minions ?! Sim! É exatamente assim que funciona construir um código recursivo.

long long Fibonacci(long long n) {
    return(Fibonacci(n-1) + Fibonacci(n-2));
}

Mas o primeiro minion chamaria o terceiro, e o segundo o quarto e assim por diante, não saberiamos a hora de parar. A recursão depende de duas coisas, a regra de recursão e o caso base da recursão. Para a sequência de Fibonacci os casos bases são \(\varphi(0) = 0\) e \(\varphi(1) = 1\).

long long Fibonacci(long long n) {
    if (n == 0) return(0);
    if (n == 1) return(1);
    return(Fibonacci(n-1) + Fibonacci(n-2));
}

Toda função recursiva então só precisa de dizer o que pedir para os minions e dizer aos minions quando parar.

Por exemplo, se eu quisesse computar o fatorial de \(n\) que vimos, eu pediria para o meu minon computar apenas \((n-1)!\) e multiplicaria isso por \(n\), é claro que eu teria que pedir pra ele contar aos minions que eles devem parar em \(0!\).

Ou quem sabe, computar \(a^n\) com \(n \in \mathbb{N}\), bem, eu pediria a um dos meus minions para computar \(a^{n-1}\) e só multiplicaria o resultado por \(a\), e é claro que \(a^0 = 1\).

Exercícios

1: Escreva uma função prime que recebe um número int e retorna 1 se o número é primo e 0 caso contrário. Lembre que \(1\) não é primo. Chame essa função na main.

2: Crie um método formato_relogio que recebe um int representando um total de segundos e três apontadores int* sendo as horas, minutos e segundos. O método deve converter o tempo total em horas, minutos e segundos salvando-os nos apontadores e imprimindo no formato h:m:s. Faça a chamada na main.

3: Faça o mesmo que o exercício 2:, mas receba o int dos segundos totais como um argumento da main. Retorne 1 na main se o argumento esperado não for passado na linha de comando. Dica: utilize atoi() e inclua <stdlib.h>.

4: Escreva a versão iterativa e a versão recursiva das funções que computam \(n!\) e \(a^n\) com \(n \in \mathbb{N}\), leia a e n e chame as quatro funções na main.

5: Escreva uma função iterativa ou recursiva (você escolhe a que achar melhor) mdc para calcular o máximo divisor comum entre dois números \(n,m \in \mathbb{N}\). Dica: Algoritmo de Euclides.

6: Crie um método void ord3 (int* a, int* b, int* c) que recebe três apontadores para inteiros e ordena os três valores de forma que a tenha o menor valor, b o valor intermediário e c o maior valor. Leia a, b e c e chame ord3 na main, depois imprima a, b e c nessa ordem.

7: Uma empresa com um modelo de negócios suspeito promete te deixar rico. Funciona assim: você entra no nível 0. Para o negócio funcionar, você precisa recrutar 3 pessoas (que formam o nível 1). Cada uma dessas 3 pessoas precisa recrutar mais 3 (formando o nível 2) e assim por diante.
Escreva uma função recursiva int nrecrutados(int nivel) que calcula quantas pessoas novas entram em um nível específico.

8: Programe um caixa eletrônico. O cliente chega no terminal e pede um saque passando o valor int na linha de comando, ou seja, o valor é um argumento da main. Crie um método void sacar(int valorint* n200, int* n100, int* n50, int* n20, int* n10, int* n5, int* n2, int* n1) que computa a menor quantidade de cédulas possíveis e salva nos endereços de memória dos apontadores. Retorne erro se o cliente não passar o argumento esperado. Imprima quantas cédulas de cada o caixa deve entregar. (Sim, você pode supor que o caixa tem infinitas cédulas de cada).

9: Imagine que você investe em duas ações PETR4 e ITUB4. Na main, leia os preços atuais dessas duas ações digitados pelo usuário. Em seguida, crie um método void rebalancear(double* preco_petr, dobule* preco_itub, double aporte) que recebe os apontadores para os preços das duas ações eum valor de dinheiro extra, aporte, que você deseja investir. A função (i) identifica qual das duas ações está com o menor preço no momento; (ii) aplica 70% do valor de aporte na ação mais barata e 30% restantes na ação mais cara; (iii) atualize diretamente o valor das variáveis na main somando o impacto proporcional no aporte ao preço base de cada ação; (iv) imprimir dentro do método qual ação recebeu o maior aporte. Na main, exiba os valores finais atualizados após a execução do método.

10: Crie uma função raiz que compute \(\sqrt{n}\) para \(n \in \mathbb{N}\).

📄 Gabarito