Introdução ao C
Como a disciplina é introdutória, os algoritmos não utilizam recursos mais avançados da linguagem C. Muita coisa não é vista neste curso, apenas o mais elementar para se entender a execução de um programa. Como alguns sinais são bastante similares à matemática e até ao idioma escrito, basta ler com atenção um código para entender a ordem das operações.
- Palavras chave. São palavras reservadas, tem um significado fixo e inalterável. Por exemplo, uma função ou variável não pode ser chamada de 'while', porque 'while' já tem um significado fixo. A exceção é texto, podemos imprimir na tela a palavra 'while' porque neste caso, a palavra não é tratada como comando na compilação do programa, apenas como uma sequência de caracteres. O mesmo acontece com símbolos como vírgulas e ponto e vírgula, o significado é fixo e o uso é pré-determinado pela sintaxe da linguagem.
- A operação de atribuição. Bem no começo, entender a = 2 como "a é igual a dois", não tem problema porque a variável 'a' tem o valor dois depois da atribuição. Mas quando fazemos operações aritméticas, aí essa leitura é errada, precisamos ler como "o valor dois é atribuído na variável a".
Por exemplo:
a = b + c;Isso é lido da direita para a esquerda, primeiro ocorre a soma 'b + c', então o resultado é atribuído na variável 'a'.
- Declaração da variável. Seria muito chato programar tendo que se referenciar a posições de memória o tempo todo, para isso a linguagem expõe a nós uma variável, um espaço na memória que referenciamos através de um nome, muito mais fácil de entender e memorizar do que um código numérico. A variável precisa ter um tipo, afinal, seria confuso se todas as variáveis fossem de um tipo só. Uma analogia: seria confuso guardar líquidos em caixas e seria um desperdício utilizar uma caixa muito grande para guardar um objeto muito pequeno. Consulte a documentação do C para saber todos os tipos existentes.
/* Declaração de variáveis */ int var; float var2; double var3; /* Declaração de variável com atribuição de valor */ int var = 2; float var2 = 4.5; /* Perda de precisão, os dígitos da parte não inteira são perdidos */ int var = 2.5; /* Mistura de tipos. O tipo com maior precisão domina a expressão. No exemplo, float tem preferência sobre o inteiro 2. O resultado da operação é 2.5 */ float a; float b = 5; int c = 2; a = b/c; /* Caso especial de dominância: apesar da variável "a" ser float, o resultado será 2.0 pois a operação de divisão é feita primeiro e com inteiros, a operação de atribuição é feita depois. */ float a; int b = 5; int c = 2; a = b/c;
- A vírgula ','. Da mesma forma separamos exemplos numa lista com vírgulas, variáveis são separadas com vírgulas num programa. Tambem serve para separar parâmetros numa função e argumentos numa chamada de função. O compilador não distingue espaços em branco, então
int a,b,c;
declara 3 variáveis do tipo int, a ausência ou não de espaços antes ou depois das vírgulas é uma questão de legibilidade.
- O ponto e vírgula ';'. É usado para terminar uma linha, um comando.
a = c + 3 + d - 2;
Visualmente, o comando tem duas linhas, mas para o compilador, a expressão aritmética ali só termina no ponto e vírgula da linha de baixo. Um erro não incomum é colocar um ponto e vírgula onde não se deve, interrompendo um comando.
Exemplo:
if (condição); comando;
Outro exemplo:
/* é permitido escrever assim, mas a legibilidade fica horrível */ comando_1; comando_2; comando_3;
- Parêntesis '()'. Em funções e em expressões aritméticas é igual à matemática. Em comandos de decisão, como if e loops, é necessário colocar uma condição entre parêntesis. Note que o compilador não diferencia espaços em branco, coloque para deixar o código mais legível, mas não colocar espaços em branco não atrapalha na execução ou na compilação.
- Condicionais. Comandos como if e loops só são executados se uma condição for verdadeira. É natural confundir o operador '==' (igualdade lógica) com '=' (atribuição de valor) no começo. Consulte a documentação da linguagem C para saber todos os operadores disponíveis.
Uma diferença aqui é que na operação de atribuição, o que está a direita do sinal é avaliado primeiro, enquanto que na operação de comparação, o que esta a esquerda é avaliado primeiro. Isso pode causar alguma confusão, mas nada muito grave, já que para os algoritmos estudados, isso acaba não alterando o funcionamento dos mesmos. Consulte a documentação da linguagem para saber a prioridade dos operadores. Note que a condicional não tem sentido se for colocada fora de um comando, a == 2; isoladamente não produz efeito nenhum.
- Operador ternário ou condicional '?:': algumas declarações 'if ... else' podem ser reescritas como se segue:
(expressão condicional) ? (expressão 1) : (expressão 2)
Que é lida como "a condicional é verdadeira?". Se a resposta for positiva, a "expressão 1" é executada, caso contrário a "expressão 2" é executada. Os parêntesis não são necessários se a expressão for bastante simples, mas caso existiam outras operações (atribuição, relacional, lógica, aritmética) é recomendável usá-los para evitar confusões ou erros de compilação.
Observação: toda condicional é uma expressão, mas nem toda expressão é uma condicional.
- O par de abre e fecha chaves '{}'. Determinam blocos de comando. São necessários em funções e em comandos de decisão ou laços que tenham mais de um comando para executar. Por exemplo:
if (condição) { comando_1; comando_2; }
Outro exemplo:
for (contador; expressão; incremento) if (condicional) { comando_1; comando_2; }
Neste caso, veja que o 'for' não precisou de chaves porque só tem um comando subordinado a ele, o 'if'. Os dois comandos, 1 e 2, são subordinados ao 'if', não ao 'for', daí precisa de chaves no bloco do 'if'. Mantendo o uso das chaves, todo o comando poderia ser escrito numa linha só.
Mais um exemplo:
for (contador; expressão; incremento) comando;
Neste caso é até desejável omitir as chaves, pois as chaves atrapalham a leitura do único comando subordinado ao laço. Um laço escrito assim é semelhante à notação de somatória da matemática '∑'. Compare a leitura de uma expressão com o símbolo da somatória na matemática com um laço escrito numa linha só.
- O símbolo '#' (sustenido). Ficam no topo dos programas
#include <biblioteca.h>
e#define
. Funções como printf() e scanf() estão definidas nos arquivos cabeçalho, sem eles não poderemos utilizar as funções definidas nele a não ser que essas funções sejam definidas no nosso próprio programa. A função dos arquivos cabeçalho é organizar grupos de funções, principalmente as muito requisitadas. Como nem todas são utilizadas sempre, não temos um único arquivo cabeçalho para todas. Como os algoritmos tratados na introdução são simples e não são feitos programas grandes e complexos, esse tipo de gerenciamento de funções não é estudado.
#define SIM 1 #define NAO 0
Com isso, podemos substituir valores 1 e 0, respectivamente, por SIM e NAO onde houverem valores 1 ou 0 que representem uma resposta SIM ou NAO, assim fica mais fácil de ler o código.
Existem outros usos para '#' mas não são vistos na introdução.
- Código de formato. Em operações aritméticas '%' é a operação resto da divisão (o resto sempre é inteiro). Na função printf() é usado como código de formato. Exemplo:
printf("variavel tem o valor %d", variavel);
O código '%d' será substituído pelo valor da variável na impressão. É assim que se geram textos "dinâmicos", textos que variam o que será impresso na tela sem que o próprio código do programa tenha um texto fixo e inalterável. Consulte a documentação do C para saber todos os "marcadores" disponíveis. Um erro comum é a variável ter um tipo, mas na impressão o código de formato estar errado, daí o valor impresso sai errado, mesmo que a conta esteja certa; isso pode confundir e levar um bom tempo para corrigir...
- Incrementos e decrementos. Existem "atalhos", expressões que podem ser simplificadas com o uso do sinal duplo '++' ou '--'. Cuidado! Não pode haver espaço em branco entre o sinal duplo, '+ +' por exemplo, mas pode haver antes ou depois. Consulte a documentação da linguagem C para saber todas as simplificações disponíveis.
Exemplo:
/* Os pares significam a mesma coisa, versão simplificada, seguida da versão por extenso */ cont++; cont = cont + 1; cont--; cont = cont - 1; cont += a; cont = cont + a; /* cuidado com este! */ cont = cont + cont++;
Quanto ao último exemplo: suponhamos que a variável 'cont' tenha o valor 1, na leitura da expressão, a conta é 1 + 1 ou 1 + 2? Executando a operação de incremento primeiro, o resultado final é 3, mas poderia ser 2 se o compilador assim entendesse. Para evitar este tipo de ambiguidade, simplesmente dividimos a expressão em duas linhas separadas, dois comandos separados, assim teremos certeza sobre qual operação é executada primeiro. Exemplo prático para diferenciar incremento anterior e posterior:
int a = 0, b; while (a != 10) { b = ++a; printf("\n%d", b); }
Qual a diferença entre ++a e a++? No primeiro caso, este simples laço contará de 1 a 10, no segundo caso, a contagem é de 0 a 9. No primeiro caso, a variável 'a' primeiro é incrementada de um, depois atribuído o valor um à variável 'b'. No segundo caso, primeiro 'b' recebe o valor zero, depois 'a' é incrementada de um. Cuidado com a simulação disto! São duas operações em sequência e na mesma iteração do laço, não conte uma iteração e dois incrementos!
- Vetores e matrizes. Uma matriz ou vetor é declarada como variável, mas precisa de um par '[]' para cada dimensão a ser usada.
int vetor[10] = {1,2,3,4,5,6,7,8,9,10}; int matriz[2][2] = {{1,2}, {3,4}}; /* Errado! Não é permitido atribuir valores dessa forma depois da declaração do vetor, o mesmo para matriz */ vetor[10] = {1}; /* Pegadinha! O vetor tambem pode ter operadores de incremento e decremento no índice. Por quê não até um elemento de outro vetor como índice? Tome cuidado o porque o incremento pode ser anterior ou posterior à expressão em que o vetor estiver */ vetor[i++]; vetor[a[i]];
Note que o compilador não distingue espaços em branco, então não tem problema vetor []
ou matriz[] [ ]
por exemplo. O índice da dimensão deve ser inteiro e deve ser constante, não pode ser uma variável. Não faz sentido um conjunto ter meias posições ou meios elementos. Vetores e matrizes de tamanho dinâmico, variável durante a execução do programa, não são estudados numa introdução. Não tem problema se a matriz ou vetor tiver muitas posições não usadas. Na atribuição de valores na matriz, não é obrigatório dispor os elementos em linhas e colunas no próprio código, isso é só para facilitar a identificação das posições. No exemplo do vetor de 10 posições, para não atribuir nada numa posição, basta apagar um dos números dali, deixando a vírgula que sobra do jeito que esta mesmo, duas vírgulas seguidas.
Um erro bastante comum é confundir o elemento com a posição e vice-versa. Por exemplo: lista[10] = 123;
Na posição 10 do vetor 'lista' é atribuído o valor 123. Cuidado! Quando se declara um tamanho de 10 elementos por exemplo, o índice começa sempre do zero, então a última posição é a 9 e não 10.
Caracteres. Fundamentalmente são iguais a variáveis para valores simples e a vetores para sequências, a diferença é que caracteres utilizam uma tabela onde cada código numérico corresponde a um caractere.
char letra = 'a'; char palavra[4] = {"casa"};
Assim como acontece com vetores, somente na declaração da string é possível atribuir automaticamente valores a todos os índices, usando aspas e chaves. Os índices tambem começam do zero, não do um. Expressões lógicas. Em C, por convenção, verdadeiro esta associado a qualquer valor diferente de zero, enquanto o zero esta associado a falso. Assim:
/* condicional sempre verdadeira */ if (5) comando; /* condicional sempre falsa */ if (0) comando;
As operações de comparação, como (a > b)
ou (a == b)
, também são associadas a verdadeiro ou falso. Assim, a = (1 > 2)
, 'a' recebe o valor 0.
Comparações compostas são otimizadas pelo compilador, no caso do operador '||' (condição_1 || condição_2)
, se 1 for verdadeira, então 2 não é avaliada pois uma condição já foi satisfeita. No caso do operador '&&' (condição_1 && condição_2)
, se 1 for verdadeira, é preciso avaliar 2, pois se 2 não for, então a condicional não será satisfeita. Se 1 for falsa, então toda a condicional já não é satisfeita e 2 não é avaliada. Em combinações de ambos (condição_1 || condição_2 && condição_3)
o uso dos parêntesis é necessário para saber qual o par avaliado com '&&' ou com '||'.
Em condicionais, é possível trocar uma afirmação por uma negação. Por exemplo:
while !(a == 2) comando;
No lugar do laço executar enquanto 'a' for igual a 2, o laço é executado enquanto 'a' não for igual a 2. Tem o mesmo efeito de trocar a comparação '==' por '!='. Em alguns casos pode ser mais prático trocar uma condicional afirmativa por uma negativa.
- Declaração e uso de funções. Uma função, assim como uma variável, precisa ser declarada, pois ela também é um lugar na memória. A programação em C já se inicia com o uso de funções, mas a finalidade do 'return 0' e da função principal são completamente ignoradas até que se tenha o conhecimento sobre os principais comandos. Consulte a documentação do C para saber quais os tipos de função disponíveis na linguagem.
/* isto é uma definição de função. Os parâmetros não tem nenhuma regra sobre a ordem de um e de outro */ int func (int a) { /* variavel só existe nesta função, ela não é visível para outras funções */ int var_local; /* alguma operação aqui */ /* devolve algum valor */ return algum_valor; }
É entendendo a diferença entre variável local e "global" (aspas porque é no sentido de não local) que se entende por que variáveis podem ter o mesmo nome, mas cada uma numa função diferente.
/* 'a' deixou de ser um parâmetro e agora é uma variável local */ int func () { int a; } /* isto não tem sentido, confusão entre definir e chamar a função */ int func (int a = 2) { }
Pensemos no seguinte: f(x) = x + 2. Escrever f(3) = x + 2 é errado. Escrever f() = x + 2 também é sem sentido.
/* isto é uma chamada de função. Os argumentos estão na mesma ordem que os parâmetros na declaração da função. */ func (argumento1, argumento2, ...);
Uma função precisa ser declara antes de ser chamada, ao contrário, não compila. Porém, há um modo de definir a função depois da chamada, esse modo é definindo um protótipo de função.
/* protótipo da função, não precisa dar os nomes das variáveis parâmetro aqui */ int func (int, ...); /* quando o parâmetro for ponteiro ou vetor / matriz, pode omitir o nome, mas não esqueça do asterisco! */ int func2 (int *, int *); /* função principal, onde esta o corpo do seu programa */ int main() { } /* declarar função dentro de função é proibido */ int main() { int sub_func() { } } /* declaração da função */ int func (int param1, ...) { } /* vetor / matriz como parâmetro, note que o primeiro índice sempre esta vazio */ int func2 (int vetor[], int matriz[][indice_max]) { }
A função do protótipo é informar ao compilador qual o tipo da função e dos parâmetros, isso evita problemas com chamada de função com tipos incompatíveis de argumentos ou número incorreto destes. A outra função é permitir a organização do programa em diferentes arquivos, mas isso não é feito na introdução porque todos os algoritmos e programas feitos são muito simples.
Observação: se uma função devolve um valor ela pode ser usada como parte ou até mesmo ser a própria condicional. Porém, uma função que não devolve nada não pode ser usada na condicional, uma vez que "nada" não pode ser associado nem a VERDADEIRO nem a FALSO.
- Ponteiros e funções. Ponteiros são declarados como variáveis, mas tem um asterisco para indicar que é um ponteiro e não uma variável qualquer. O operador 'e comercial' indica "endereço (de memória) de".
/* sintaxe para declarar ponteiros */ int *p = &var; int *p; /* O de cima atribui o valor 10 não no ponteiro, mas aloca o valor no local de memória apontado. Cuidado! E se o ponteiro foi declarado, mas sem um endereço de memória atribuído a ele? Aí o valor 10 foi alocado num local desconhecido da memória. O de baixo está "ligando" o ponteiro a um endereço de memória. Cuidado! 'p' recebeu o endereço de 'var' e não o valor! */ *p = 10; p = &var; /* assim está errado */ *p = &var; /* se p foi declarado como ponteiro, então a sintaxe está errada. p não pode ser "transformado" numa variável qualquer */ p = 10;
Ponteiros são variáveis com propósito específico, precisam de um tipo como qualquer outra variável, mas não são utilizados como uma variável qualquer. Ponteiros só fazem sentido quando utilizados em conjunto com funções. A definição de um ponteiro está atrelada ao funcionamento da memória do computador. Para entendê-los a melhor forma é resolvendo problemas que dependam de ponteiros.
Vejamos como é a sintaxe de ponteiros, vetores, matrizes e funções quando usados em conjunto. Note que a diferenciação entre local e não local também se aplica a ponteiros. A variável não precisa ter o mesmo nome do ponteiro:
/* recebe apenas um endereço de memória de uma variável */ int func (int *p) { } /* recebe endereço de memória de uma variável num ponteiro e um valor */ int func2 (int *p, int valor) { } /* recebe um endereço de memória de uma função. No primeiro parêntesis o ponteiro, no segundo parêntesis, o(s) parâmetro(s) da função apontada. Assim como o protótipo, é permitido omitir os nomes das variáveis. Cuidado! O(s) parâmetro(s) são da função apontada, não da função func3. func3 neste exemplo tem apenas um parâmetro, o ponteiro */ double func3 ((*p)(float, float, ...)) { } int main() { /* uma declaração de variável e vetor de 10 posições */ int var, vetor[10]; /* um ponteiro com atribuição de um endereço de memória do início do vetor Quando chamamos uma função com o nome do vetor como parâmetro, tem o mesmo efeito de passar o endereço do índice zero do vetor. Isso é melhor entendido com o estudo da aritmética dos ponteiros */ int *p = &vetor[0]; /* a função "func" só pode ser chamada com endereços de memória, não com valores */ func(&var); /* isso não esta chamando a função com o valor do elemento 5, mas sim com o endereço de memória do elemento 5 do vetor */ func(&vetor[5]); /* errado! Isto está passando um endereço de memória do próprio ponteiro p para a função. Um ponteiro não pode receber um endereço de outro ponteiro, a menos que estejamos lidando com ponteiros de ponteiros, o que não é o caso */ func(&p); /* o primeiro argumento é um endereço de memória, o início do vetor, daí só ter o nome do vetor sem colchetes. O segundo não é o valor 5 e nem um endereço de memória, é o valor que está guardado no índice 5 do vetor */ func2(vetor, vetor[5]); /* a chamada de func3 é feita como uma função qualquer, mas neste caso, o nome da função apontada é o próprio endereço de memória, não use '&' como se faz com variáveis */ func3(nome); }