Tutorial de C

Olá! Aqui eu vou tentar ajudar com algumas coisas de C.

Não é muita coisa. É mais um ponto de referência p/ algumas dúvidas q eu ja tirei.

int main()

main() é o nome do "ponto de entrada" de um programa, geralmente. Então o que você quer que rode você coloca ali.

Ela na verdade é o que chamamos de função (um pouco mais avançado - leia mais em baixo), mas basicamente é um pedaço de código que você pode rodar várias vezes. Outras coisas que veremos mais na frente, como printf(), scanf() e sqrt(), são funções também, mas elas meio que... "já são definidas pelo compilador".

O main(), porém, deve ser definido por nós, e é nele onde deve ficar o nosso código.

A partir daqui, a maioria dos códigos vai omitir o int main(), mas você precisa colocar o código dentro dele.

Então isso:

int main() {
    int a = 10 + 20;
}

É a forma correta de escrever, enquanto isso:

int a = 10 + 20;

É o que vai aparecer.

mostrando coisas na tela (com printf())

O printf é uma função usada para mostrar coisas na tela.

Para usar, você precisa colocar #include <stdio.h> no início do código, antes do int main().

Um exemplo de uso do printf seria:

Olá, mundo!

Basicamente, você passa um texto (entre aspas) com uma mensagem para mostrar na tela.


Mas como mostrar, por exemplo, um número que foi calculado no programa?

Para isso, você precisa usar os %. Vejamos outro exemplo:

int x = 0;
printf("O sucessor de x é %d", x+1);

Quando você usa o %d no texto do printf, a próxima coisa que você passar depois do texto vai ser colocada no lugar dele. Nesse caso, o programa mostraria O sucessor de x é 1.

E você pode usar isso várias vezes:

int a = 1;
int b = 3;
printf("a=%d, b=%d, a+b=%d", a, b, a+b);

(Isso mostra a=1, b=3, a+b=4)


Agora cuidado: o %d é usado somente para int. Se você quiser passar outros tipos de valores, vai ter que usar outros códigos:

  • %d ou %i: int (normal)
  • %x: int (em hexadecimal - base 16)
  • %o: int (em octal - base 8)
  • %u: unsigned int (normal)
  • %c: char (em forma de caractere)
  • %f: float
  • %lf: double (o código lf pq é abreviação "long float", que é equivalente)
recebendo dados digitados (com scanf())

Além de saber como mostrar coisas na tela, é também muito útil saber sim ler coisas que o usuário digitar.

Para isso temos o scanf. Ele funciona de uma maneira parecida com o printf, mas o texto é um formato do que o usuário vai digitar, e o que você passa depois são as variáveis pra onde essas coisas digitadas vão.

Confuso? Enfim, um exemplo provavelmente ajuda mais:

int x;
scanf("%d", &x);

Aqui nós criamos uma variável x e usamos o scanf. Ao fazer isso, o programa vai parar até que o usuário digite algo e aperte enter. Ele então vai tentar pegar o texto que o usuário digitou, e, se for um número, colocar em x.

Esse programa aqui então é uma mini calculadora:

int x;
printf("Digite x: ");
scanf("%d", &x);
printf("x*5=%d", x*5);

Se eu digitar 20, o resultado vai ser x*5=100.


Lembre-se de, ao colocar a variável, colocar o & logo antes. Isso siginifica que, ao invés de você passar o que tá dentro da variável x, você está passando o endereço dela. Talvez eu explique isso algum dia direito, mas uma maneira simples de entender é: toda variável, quando o programa tá rodando, tá em algum lugar específico da memória. Passar o endereço seria indicar ao scanf onde na memória tá essa variável, para ele poder ir lá e colocar o valor que o usuário digitou ali.


Os tipos de % usados aqui são basicamente os mesmos que você usaria no printf, então pode olhar a tabela de códigos de lá mesmo.

comentários (// e /* */)

Às vezes é bom poder anotar algo no código. Para isso, usamos os comentários.

Um comentário é um texto que não vira código - ele é essencialmente ignorado pelo compilador.

Em C, a maneira "clássica" de comentar é com o /* */. Você começa o comentário com /* e termina ele com */.

int x; /* olá! isso aqui é um comentário. */
scanf("%d", &x); /* um comentário
de múltiplas linhas
*/

O compilador ignora os comentários, então isso é equivalente a:

int x;
scanf("%d", &x);

Desde 1990(?) você pode também usar os comentários de uma-linha. Ao colocar o //, tudo até o fim daquela linha é ignorado.

int x; // olá! isso aqui é um comentário.
scanf("%d", &x);
// como eu quero colocar
// várias linhas, eu tenho
// que usar o // no início de cada linha.

Mais uma vez, o compilador ignora os comentários, e resulta em:

int x;
scanf("%d", &x);

Além de anotar coisas, comentários são úteis para ignorar um código por um tempo, sendo que você quer usar ele depois.

Alguns editores até tem telas específicas para comentar o texto selecionado, o que deixa isso bem prático (no VSCode é Ctrl+K, eu acho?)

funções: introdução

Os exemplos desse capítulo são completos! As coisas aqui ficam fora do main().

Funções são "blocos de código" que podem ser rodados quando você quiser.

Vamos começar com um exemplo:

float triplo(float x) {
    return 3.0 * x;
}

Aqui, temos uma função, de nome triplo, que recebe um número x, multiplica ele por 3.0 e retorna o resultado, que também é float.

Quando você quer usar ela, você pode fazer triplo(num), e o resultado vai ser o triplo de num. Um exemplo:

#include <stdio.h>

float triplo(float x) {
    return 3.0 * x;
}

int main() {
    int x = 10;
    printf("O triplo de 10 é %d\n", triplo(x));
}

Esse programa vai mostrar O triplo de 10 é 30.


Uma forma mais genérica de definir uma função seria:

tipo_retorno nome_funcao(tipo_parametro parametro...) {
    codigo...
}
  • tipo_retorno é o tipo que a função retorna;
  • nome_funcao é o nome da função;
  • parametro é um parâmetro da função (algo que você passa pra ela, entre os parênteses - o x em f(x));
  • tipo_parametro é o tipo do parametro parametro;
funções: por que usar? (modularização)

Os exemplos desse capítulo são completos porque a gente vai mexer com coisas "fora" do main().

Como mencionado anteriormente, uma função é um pedaço de código que pode ser rodado várias vezes.

Vamos explicar a importância delas com um exemplo - um programa que mostra um quadrado vazado, 5x5:

#include <stdio.h>

int main() {
    int i, j;

    // mostrar o topo do quadrado (1 linha)
    for (i = 0; i < 5; i++)
        printf("*");
    printf("\n");

    // mostrar o meio do quadrado (3 linhas)
    for (i = 0; i < 3; i++) {
        printf("*");
        for (j = 0; j < 3; j++)
            printf(" ");
        printf("*\n");
    }

    // mostrar o topo do quadrado (1 linha)
    for (i = 0; i < 5; i++)
        printf("*");
    printf("\n");
}

Ao rodar esse programa, a saída é:

*****
*   *
*   *
*   *
*****

Esse código tem cara de meio confuso, né? É difícil ler de cara o que ele faz. A gente pode adicionar comentários e tal, mas se começarmos a criar funções, conseguimos chegar a um nível ainda maior de "código compreensível", e conseguimos também diminuir a repetição de código.

Vamos criar uma função para mostrar uma linha de asteriscos, chamada mostrarLinha:

void mostrarLinha(int tamanho) {
    int i;
    for (i = 0; i < tamanho; i++) {
        printf("*");
    }
    printf("\n");
}

Para usar ela, basta escrever, por exemplo, mostrarLinha(5), e ele vai mostrar uma linha com tamanho=5.

Ao fazer isso, o código fica muito mais simples:

#include <stdio.h>

void mostrarLinha(int tamanho) {
    int i;
    for (i = 0; i < tamanho; i++) {
        printf("*");
    }
    printf("\n");
}

int main() {
    int i, j;

    mostrarLinha(5);

    // mostrar o meio do quadrado (3 linhas)
    for (i = 0; i < 3; i++) {
        printf("*");
        for (j = 0; j < 3; j++)
            printf(" ");
        printf("*\n");
    }

    mostrarLinha(5);
}

E dá pra melhorar! Se a gente criar uma para mostrar as linhas vazadas também:

#include <stdio.h>

void mostrarLinha(int tamanho) {
    int i;
    for (i = 0; i < tamanho; i++) {
        printf("*");
    }
    printf("\n");
}

void mostrarLinhaVazada(int tamanho) {
    int i;
    printf("*");
    for (i = 0; i < tamanho - 2; i++) {
        printf(" ");
    }
    printf("*");
    printf("\n");
}

int main() {
    int i;

    mostrarLinha(5);
    for (i = 0; i < 3; i++) mostrarLinhaVazada(5);
    mostrarLinha(5);
}

Eu diria que um dos melhores benefícios de criar funções é que você modulariza o programa. Você consegue separar o programa em pedaços pequenos que você consegue utilizar quando quiser, quantas vezes quiser, na ordem que quiser. E ainda você acaba não misturando as variáveis, porque o i em mostrarLinha não é o mesmo i de mostrarLinhaVazada.

Inclusive, talvez a explicação de argumentos não tenha sido muito boa até agora. As coisas que você passa entre os parênteses, ao usar a função, vai para as variáveis na função, entre os parênteses também. Então, quando você faz mostrarLinha(5), ele roda o código dentro de mostrarLinha, com tamanho sendo igual a 5.

estudo de caso: expressões mais complexas

Tenhamos um pequeno exemplo - vamos tentar analisar ele para ver o que está acontecendo:

#include <stdio.h>

int dobro(int x) {
    return x * 2;
}

int metade(int x) {
    return x / 2;
}

int main() {
    int x = 4;
    printf("%d\n", dobro(5) + metade(x + 2));
}

Se você rodar esse código, o resultado é 13.

O printf está sendo chamado com os argumentos: "%d\n", dobro(5) + metade(x + 2)

O argumento dobro(5) + metade(x + 2) precisa ser calculado - então (o computador) vai calcular dobro(5) e metade(x + 2), e depois somar:

dobro(5) tem como resultado 10;

metade(x+2) primeiro tem o x+2 calculado, que dá 6, e depois é calculado: metade(6) = 3;

No fim das contas, temos 10 + 3, que dá 13.

O printf então foi chamado com os argumentos: "%d\n", 13

E então ele mostra um 13 na tela.

Uma maneira de analisar expressões complexas assim é começar pelo que está mais dentro de parênteses (nesse caso o x + 2) e ir seguindo a partir daí até chegar nos valores mais de fora.

constantes

Muitas vezes, ao escrever código, temos valores que não queremos que sejam alterados. Geralmente, podemos fazer que isso seja uma realidade usando as constantes.

Como exemplo, vejamos o código:

int quant_elementos = 5;
int elementos[quant_elementos];

// vamos preencher o array `elementos` com números
for (int i = 0; i < quant_elementos; i++) {
    elementos[i] = i;
}

// whoops!
quant_elementos = 20;

// vamos agora mostrar os elementos
for (int i = 0; i < quant_elementos; i++) {
    printf("%d ", elementos[i]);
}
printf("\n");

Eu devo admitir que esse exemplo não é muito prático, já que não teve motivo para mudar o quant_elementos (eu mudei só pra mostrar o problema mesmo), mas ainda assim...

O resultado vai ser algo como isso aqui:

0 1 2 3 4 32676 0 0 0 0 0 5 12 20 4 0 736875264 32766 -761248256 -543001068

Isso aconteceu porque criamos um array de 5 elementos, mas depois mudamos o tamanho para 20 elementos. Isso é um problema que devemos perceber, mas uma maneira de fazer o compilador ajudar a gente a não cometer erros como esse é utilizando constantes.

Temos dois métodos para constantes em C: utilizando a palavra-chave const ou a diretiva #define.


Com o const nós fazemos uma variável não poder ser alterada, tendo o mesmo valor desde o início.

const int x = 5;
x = 8; // isso aqui dá erro!

Aplicando isso ao código original:

const int quant_elementos = 5;
int elementos[quant_elementos];

// vamos preencher o array `elementos` com números
for (int i = 0; i < quant_elementos; i++) {
    elementos[i] = i;
}

// isso aqui vai dar erro, forçando a gente a não fazer isso
quant_elementos = 20;

// vamos agora mostrar os elementos
for (int i = 0; i < quant_elementos; i++) {
    printf("%d ", elementos[i]);
}
printf("\n");

Com o #define nós não estamos nem criando uma variável - o que é criado é na verdade um macro, que é um nome especial que é substituído pelo seu valor em qualquer lugar que você usar. Faríamos, então:

#define X 5

int main() {
    X = 8; // isso aqui dá erro! mas o erro é mais críptico
    return 0;
}

No original, ficaria:

#define QUANT_ELEMENTOS 5

int elementos[QUANT_ELEMENTOS];

// vamos preencher o array `elementos` com números
for (int i = 0; i < quant_elementos; i++) {
    elementos[i] = i;
}

// isso aqui vai dar um erro críptico que previne que o código compile
QUANT_ELEMENTOS = 20;

// vamos agora mostrar os elementos
for (int i = 0; i < quant_elementos; i++) {
    printf("%d ", elementos[i]);
}
printf("\n");

Lembre-se que macros sempre podem ser usados em qualquer função, e que eles não são variáveis propriamente ditas e por isso podem causar alguns erros de sintaxe confusos.


Qual se deve usar então?

Eu não tenho uma opinião muito definida sobre isso, mas em geral eu recomendaria usar const, e só usar o define caso for necessário - e costumam haver várias situações onde o define pode ser necessário ou pelo menos mais útil, mas se você quer só um valor que não possa ser alterado eu acho que vale mais a pena usar o const.

obtendo o tempo atual (com time())

Por agora essa explicação de tempo aqui é só algo simples p/ poder usar com números aleatórios.

Uma das maneiras mais básicas de obter informação sobre o tempo em C é usar a função time(), disponível no header time.h.

Eu peguei um protótipo dessa função (encontrei em /usr/include/time.h no meu computador) e simplifiquei:

/* Retorna o tempo atual (em segundos) e coloca em *x se x não for NULL. */
time_t time(time_t *x);

Basicamente, ela é uma função que, quando chamada, retorna um time_t indicando o tempo, em forma de número (a quantidade de segundos passada desde ... 1 de janeiro de 1970, se eu não me engano).

Esse time_t é um alias("apelido") de um número inteiro, então se tiver tendo dificuldade pra entender basta trocar time_t por int na cabeça.

Um programa simples que mostraria a quantidade de segundos atual é:

time_t s;
s = time(NULL);
printf("Segundos: %d\n", s);

Se você parar pra ver, aqui eu estou passando um NULL. Ele basicamente indica que não é para o time() fazer nada de "colocar em *x" (como tem no texto lá em cima) e é só para retornar o valor. Mas, se você fizer:

time_t s;
time(&s);
printf("Segundos: %d\n", s);

Aqui você vai estar passando o & (endereço) da variável s e isso vai fazer com que o time() coloque o valor nela, mesmo sem você ter usado o =. É tipo o scanf().

Curiosidade: você poder usar tanto x = time(NULL) quanto time(&x) é na verdade um legado de versões muito, muito velhas de C. Não vou explicar aqui mas consulte <a href="https://stackoverflow.com/questions/61432103/why-does-stdtime-have-an-unnecessary-parameter">essa pergunta no StackOverflow</a> se tiver com curiosidade.

geração de números aleatórios (com rand())

Alguns tipos de programa precisam fazer coisas aleatórias acontecerem (como sortear cartas em um baralho, ou rolar um dado). Em computadores, podemos fazer algo parecido através da geração de números aleatórios.

Para gerar números aleatórios podemos utilizar a função rand(), disponível em stdlib.h

Essa função gera um número entre 0 e RAND_MAX, que é uma constante que também fica em stdlib.h. O valor dela é geralmente... 36727? alguma coisa assim.

Enfim. Vejamos esse código:

printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());

Esse código mostra três números aleatórios. Legal? Mas tem um problema.

Ao rodar esse código de novo... os números são os mesmos. Isso é porque o rand(), como quase tudo em um computador, é um pedaço de código que sempre roda a mesma coisa.

Eu não sei dizer a maneira específica como o rand() calcula os números (talvez não seja igual em todos os computadores), mas ele usa como base uma seed. Uma maneira que eu gosto de usar pra explicar isso é com Minecraft. Quando você cria um mundo, você pode especificar uma seed e, se você criar vários mundos com a mesma seed, todos eles vão ser iguais (a início, pelo menos).

Então: dois programas com a mesma seed geram a mesma sequência de números aleatórios.

Para setar a seed de um programa, você pode usar o srand(), também disponível no stdlib.h.

O problema é... o que você coloca na seed? Porque se você colocar uma seed fixa, vai ser a mesma sequência ainda.

Pra isso, podemos usar o <a href="#s-time">time()</a>. Como expliquei anteriormente, ele retorna a quantidade de segundos, que é um valor que sempre tá aumentando, e por isso podemos usar isso como seed - toda vez que o programa rodar, a seed vai ser diferente, e então a sequência de números aleatórios vai ser diferente.

srand(time(0));
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());

Agora funciona.

o stack e seus limites

Quando um programa está rodando, ele tem uma região na memória chamada de "stack", o que traduz para português como "pilha".

E esse nome é uma boa analogia mesmo - o jeito que isso é usado é que nele ficam salvos as variáveis atuais e, quando alguma função é chamada, é colocado no stack o lugar para onde o processador deve voltar depois de executar a função, e também é colocada as variáveis da nova função que vai ser criada.

O problema do stack é que ele não é muito flexível - como ele está sendo modificado constantemente, e sendo usado do jeito que ele é usado, ter valores muito grandes ou que mudam de tamanho não é muito eficiente, e portanto é comum a memória do stack ser bem pequena.

Por "pequena" eu quero dizer "pequena comparada à memória total", porque na minha máquina (12GB RAM) cada programa parece ter em média 8MB de memória reservada para o stack.

Por causa disso, quando um programa precisa de muita memória, ou até mesmo só de uma memória que mude de tamanho, o heap é uma região muito mais útil.

o heap e alocação de memória

O heap é o nome dado à região de um programa que é controlada por alocadores.

Dando uma pesquisada eu também descobri que o heap é o nome de uma estrutura de dados! Não vou falar sobre ela porque eu honestamente não sei como ela funciona...

Um alocador, por sua vez, é o responsável por "achar" partes livres da memória que podem ser usadas para o programa fazer algo. Por causa disso, o heap é muito mais dinâmico que o stack, e tem uma flexibilidade que permite um programa usar muito mais memória que outro, e que isso mude ao decorrer desses programas.

Em C, isso é primariamente feito pelas funções malloc(), realloc() e free(), que estão disponíveis no <stdlib.h>.

Um exemplo:

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // criando o array usando malloc()
    const int tamanho = 64;
    int *arr = malloc(tamanho * sizeof(int));

    // colocando vários elementos no array (e mostrando eles)
    for (int i = 0; i < tamanho; i++) {
        arr[i] = i*2;
        printf("Elemento %d: %d\n", i, arr[i]);
    }

    // liberando o array com free()
    free(arr);
}

Aqui estamos criando um array de tamanho (64) elementos, utilizando o malloc.

A função malloc() recebe um único argumento, que é a quantidade de bytes a serem alocados.

O sizeof(int) retorna o tamanho do tipo int, que é 4 bytes (na minha máquina, pelo menos).

Como estamos multiplicando o tamanho por sizeof(int), estamos alocando 4 bytes para cada elemento.

O resultado é retornado em forma de um ponteiro, que colocamos na variável arr.

Logo depois, estamos passando pelo array e preenchendo seus elementos (o dobro do valor do índice), e mostrando na tela.

Por último, nós usamos o free() para sinalizar que a memória para onde arr aponta não vai ser mais usada por nós (e que outro programa pode usar).

structs

Structs são uma maneira de criar tipos de dados mais estruturados em C. Um exemplo pode ajudar:

struct Pessoa {
    int codigo;
    char *nome;
};

Acima criamos um struct Pessoa, que podemos usar como tipo de variável. Esse struct possui campos - neste caso "codigo" e "nome", que são "sub-variáveis" de toda variável do tipo Pessoa.

Segue um exemplo de uso desse struct:

#include <stdio.h>

struct Pessoa {
    int codigo;
    char *nome;
};

int main() {
    // inicializando a variável
    struct Pessoa p;
    p.codigo = 23;
    p.nome = "Ana Hita";

    // mostrando as informações na tela
    printf("Nome = %s, Código = %d\n", p.nome, p.codigo);
}

DETALHE: para acessar um campo de uma variável-struct, usa-se o ., como em p.codigo = 23, que deu o valor 23 ao campo "codigo" de p.