,*************, | Manual de C | '*************' Autor: Dimitri Vashnov a.k.a xf86config Email: palwaaktaar@bol.com.br Thanks Unsekurity Team! 0. Introducao Bem, se vc eh elite, nao tem muito o que fazer aqui, este manual eh para que nao sabe programar em C ou para aqueles que acham que nao sabem. Se tiverem duvidas, utilizem-se da tentativa-erro, ou entao pesquisem outros tuts. Atrevo-me a escrever este tutorial pensando naqueles que como eu nao dispuseram de um manual de C em portugues de facil leitura e acesso.. naqueles que querem aprender mas nao querem perder tempo catando tutoriais ou pedacos.. ao que cansaram de procurar ajuda em canais de irc inuteis. Este tutorial foi feito para aqueles que querem programar em C preferencialmente em Linux Slackware ou qualquer outro que nao seja RHL (Red Hat Like) como o Conectiva, pq o Conectiva nao foi feito para voce, amigo fussador. Eles nao instalam nenhum compilador c. Nada de perder tempo, vamos logo ao que interessa! 1. Criando e Compilando seu programa. 1.1 Criando Crie seu programa em um editor de textos qualquer que voce tenha a sua disposicao. Para o Linux existe o joe, pico e jed, tem tambem o vi, e o vim, mas nao os recomendo caso voce seja iniciante. Comece assim: 'joe programa.c'. No joe, para salvar, digite ctrl+k+d, e para salvar e sair, digite ctrl+k+x, e para abandonar as mudancas e sair, digite ctrl+c. A extensao do arquivo deve ser .c caso seu programa seja feito em C, e .cpp caso ele seja feito em c++. 1.2 Compilando Existem diversos compiladores disponiveis. O compilador cc feito pela SUN existe na maioria das plataformas UNIX, e o gcc, feito pela GNU, existe em todos os linux e SO's de codigo aberto. Os compiladores c++ mais comuns sao o CC (feito pela SUN, observe que neste caso eh tudo maiusculo) e o g++, feito pela GNU. A linha de comando basica para compilar um programa em C eh a seguinte: $ gcc arquivo.c Serah gerado ai um arquivo executavel chamado a.out! Execute-o digitando ./a.out. Caso voce queira dar um nome ao arquivo executavel em vez de a.out, a linha de comando eh $ gcc arquivo.c -o arquivo Ex.: gcc pcrack.c -o pcrack Se voce esta usando a ferramenta cc, o procedimento eh o mesmo: $ cc arquivo.c -o arquivo 1.3 Entendendo as etapas de compilacao A compilacao de um programa eh feita pelo gcc em algumas etapas das quais iremos falar logo adiante: Etapa 1) O pre-processador O pre-processador remove comentarios do programa e interpreta diretivas especiais do pre-processador. Estas diretivas sao iniciadas por #, veja: #include Com certeza voce jah deve ter visto isso sempre no comeco de um programa em c. Essa eh uma diretiva interpretada pelo pre-processador. A diretiva #include inclui naquele local um dado arquivo texto. No caso acima, no lugar da linha ele coloca o conteudo do arquivo stdio.h, que eh um header file (arquivo de cabecalho). Quando este arquivo estah envolvido por < > eh porque o arquivo estah no diretorio padrao, no linux este diretorio eh /usr/include, mas isso pode mudar de versao pra versao. Existe tambem a diretiva #define que eh assim: #define MAX_SIZE 100 Faz uma substituicao completa por todo arquivo trocando MAX_SIZE por 100. Iremos discutir mais detalhes sobre o preprocessador mais adiante ! Etapa 2) Compilador C O Compilador C recebe o codigo jah peneirado do pre-processador e converte para assembly. Etapa 3) Assembler O Assembler cria um arquivo objeto, de extensao .o em Unix'es e .obj no DOS. Este arquivo jah estah em binario. Etapa 4) Linkador Se o programa faz referencia a funcoes que estejam definidas em outros arquivos objetos (como as bibliotecas padroes libc5 e glibc2) entao o linka- dor une tudo fazendo um executavel! Nosso programa estah pronto. 1.4 Alguns parametros uteis dos compiladores cc e gcc. Com certeza voce irah precisar de parametros adicionais nas suas aventuras. -lbiblioteca --> esta eh a mais interessante, permite voce linkar seu programa com uma biblioteca pre-compilada, de extensao .so, geralmente dentro do seu /usr/lib. Apos o -l vem o nome da biblioteca. Ex.: gcc pcrack.c -o pcrack -lcrypt --> aqui crypt se refere a biblioteca que encontramos em /usr/lib/libcrypt.so. Para saber o nome da biblioteca, re- mova o 'lib' do inicio do nome do arquivo e a extensao '.so'! -Ldiretorio --> estah eh muito util, voce pode indicar ao linkador o diretorio por onde ele deve procurar as bibliotecas que voce quer linkar. O padrao eh /lib e /usr/lib, se voce acrescentar varias opcoes, ele vai procurar nos diretorios padroes e nos que voce especificou. Ex.: gcc pcrack.c -o pcrack -L/home/username/mylibs -L. -Idiretorio --> tb muito legal, voce informa onde o pre-processador deve procurar os headers files, aqueles arquivos que voce usou no #include. Ex.: gcc pcrack.c -o pcrack -I/home/username/myheaders -I. Vejam ai nos dois exemplos anteriores o que fizemos: -L. e -I. Este . eh o diretorio atual do arquivo que estah sendo compilado. -D --> Quando vc usa -Dmacro ou -Dmacro=valor eh o mesmo que colocar dentro do arquivo a linha #define macro ou #define macro valor ! Isso eh bom pra portabilidade do seu codigo. 1.5 Usando as bibliotecas. Praticamente todos os linux vem com bibliotecas padroes p/ C instaladas. Estas bibliotecas contem funcoes variadas que fazem diversas coisas, isso lhe pou- pa o trabalho de ter que refazer funcoes! Caso voce tenha duvida de como usar alguma funcao, digite man 3 nome-da-funcao ou man nome-da-funcao -S 3. Ex.: Se voce digitar man free nao vai cair no help que deseja. Digite entao man 3 free. Quando voce inclui um header no seu programa, todas as funcoes e declaracoes definidas naquele header passam a fazer parte de seu programa, podendo usar mais funcoes. O processo basico eh o seguinte: Passo 1) Voce inclui o header que contem a familia de funcoes que voce quer usar Passo 2) Usa a funcao de acordo com a sintaxe. Passo 3) P/ os headers que nao fazem parte da biblioteca padrao C voce terah que especificar a biblioteca que tem q linkar por meio de -l, quando for compilar. Por exemplo, quando fizemos, quando fizemos #include nao precisamos especificar a biblioteca por meio de -l, mas quando fizemos #include tivemos que colocar -lcrypt na nossa linha de comando p/ compilar corretamente. Eh que a biblioteca crypt nao eh padrao. 2. Nocoes basicas de C 2.1 A estrutura basica de um programa em C Todo programa em C tem que ter a seguinte estrutura: * Diretivas do Preprocessador * Definicao de tipos * Prototipos das funcoes * Variaveis * Funcoes Devemos ter obrigatoriamente uma funcao main() pois esta eh a primeira funcao a ser executada quando executamos o programa. Aqui vai um exemplo para voce se familiarizar: Nome do arquivo: file1.c --cut-- #include /* Os comentarios podem ficar em qlq lugar. Observe que toda linha fora do comentario terminha em ponto e virgula ; (isso nao vale pra diretivas do precompilador como a acima). */ int i; void main(int argc, char *argv[]) { /* este void que voce ve eh o tipo de dado que a funcao main() retorna */ i = 0; /* inicializacao da variavel inteira i */ printf("O primeiro argumento eh %s \n",argv[1]); /* imprime na tela o primeiro argumento */ i += 10; /* soma 10 ao valor de i */ printf("O valor de i eh %d \n",i); /* imprime na tela o valor de i*/ } /* os { e } delimitam o bloco de codigo da funcao main() */ --cut-- O programinha acima pode ser compilado com o comando 'gcc file1.c -o file1' Vamos com mais calma. Uma funcao tem a seguinte forma: tipo nome_da_funcao(parametros) { declaracao das variaveis locais. comandos em c. } Esse 'tipo' indica que tipo de valor a funcao irah retornar, se eh inteiro, se eh um caractere, um texto, um numero racional, etc! Os 'parametros' indicam que variaveis de entrada o programa deve receber, separados por virgula. Entre as chaves { e } ficam as instrucoes (em C) que serao executadas quando a funcao for chamada. Sempre nessas instrucoes, as declaracoes de variaveis sao feitas antes. Vejamos aqui um exemplo de funcao: int soma(int a, int b) { int resultado; resultado = a + b; return resultado; } Vemos acima que a funcao soma() retorna um valor inteiro (eh por isso que ha um int antes da palavra soma), recebe dois parametros inteiros, a e b, e no seu bloco de codigo, declara uma terceira variavel inteira chamada resultado, soma os valores de a e b e joga a resposta na variavel resultado, e com o comando 'return resultado;' ele faz a funcao retornar aquele valor armazenado em return. Apos retornar uma vez, a funcao termina sua execucao. 2.2 Operadores Se voce jah programou em alguma linguagem qualquer, deve saber o que chamamos de operadores. Um operador eh um simbolo que quando utilizado quer indicar que uma tarefa, uma operacao deve ser realizada. Vejamos aqui alguns operadores da linguagem C: 2.2.1 Operador de Atribuicao. Um operador de atribuicao serve exatamente para atribuir valores a variaveis. Na linguagem C, este operador eh o simbolo = . Este eh um operador binario que atribui o valor encontrado a direita do simbolo a variavel encontrada a esquerda do mesmo. Ex.: int funcao1(void) { int valor1, valor2; x = 1; y = 3; valor1 = x + y; valor2 = valor1 + 18; return valor2; } Vimos acima a atribuicao do valor 1 a variavel x, e assim por diante. Uma operacao de atribuicao sempre retorna o valor atribuido. Assim, a expressao (a = b) retornara o valor de a apos a operacao. 2.2.2 Operador de Igualdade. Um operador de igualdade serve para comparar dois valores, se sao ou nao iguais. Na linguagem C, temos dois operadores de igualdade: == e != . No caso == , o valor retornado pela expressao (a == b) eh 1 se a e b sao iguais ou 0 se forem diferentes, enquanto que com !=, o valor retornado pela expressao (a != b) eh 0 se forem iguais e 1 se forem diferentes. Veja a tabela verdade: Tabela 1. ,-------------------------, | a | b | a == b | a != b | |-------------------------| | 0 | 0 | 1 | 0 | | 0 | 1 | 0 | 1 | | 1 | 0 | 0 | 1 | | 1 | 1 | 1 | 0 | '-------------------------' Assim, a operacao (y == 3) retorna 1 se o valor de y eh 3, e retorna 0 caso contrario. Isso eh especialmente importante mais adiante quando formos ver controladores de fluxo. 2.2.3 Operador de Negacao. Se voce quer negar uma expressao (ou seja, trocar 0 para 1 e 1 para 0), basta colocar o sinal ! antes da expressao. Eh importante que voce delimite a area da expressao a ser negada. Ex.: A expressao !(x == 1) nega a expressao (x == 1), ou seja, se x for 1, !(x == 1) retorna 0, e caso x seja diferente de 1, retornara 1. Podemos perceber claramente que !(a==b) tem a mesma tabela verdade de (a!=b) e !(a!=b) tem a mesma tabela verdade de (a==b). Uma propriedade importante eh que !(!a) tem a mesma tabela verdade de a, qualquer que seja a expressao a. Podemos tambem omitir os parentesis caso esteja claro a expressao !x pode ser feito para negar x, caso x seja um valor ou variavel. Em C, tudo que eh nao nulo eh considerado, para fins logicos, como 1, e tudo que eh nulo (como 0, NULL ou '' ou "") eh considerado 0. Assim, se tivermos a expressao !x e a variavel x tiver o valor 2, !x serah 0, e se x tiver o valor 0, !x terah o valor 1. 2.2.4 Operadores Aritmeticos. Os operadores aritmeticos servem para realizar operacoes aritmeticas, ou seja, aquelas que envolvem as operacoes basicas que aprendemos logo nos primeiros anos da escola: soma, subtracao, multiplicacao e divisao. As expressoes sao do tipo (membro1 operador membro2) e o valor retornado pela expressao eh exatamente o resultado da operacao.. Tabela 2. ,---------,--------------,-----------------,-----------, | Soma: | Subtracao: | Multiplicacao: | Divisao: | |------------------------------------------------------| | (a + b) | (a - b) | (a * b) | (a / b) | '---------'--------------'-----------------'-----------' Ex.: int metade(int x) { x = x / 2; return x; } No caso acima, temos 2 operadores numa unica linha: x = x / 2; Mas eles nao tem parentesis.. Como saberemos qual operacao foi realizada primeiro? Eh aih que nos deparamos com a precedencia dos operadores. Algumas operacoes tem precedencia sobre as outras na sua valoracao. No caso acima, a divisao eh feita antes e a atribuicao depois. No topico 2.3 iremos analisar minusciosamente as precedencias. Atentemos agora para um recurso muito importante em C que eh a abreviacao desses operadores: Podemos trocar (x = x + z) simplesmente por (x += z) quando queremos somar z a x, e identicamente para as outras operacoes: Soma: x += y Subtracao: x -= y Multiplicacao: x *= y Divisao: x /= y Estes sao os operadores aritmeticos mais basicos. Temos tambem o operador de resto, que eh o %, que dada a expressao (a % b) nos retorna o resto da divisao dos inteiros a e b. Este operador tambem pode ser abreviado como os anteriores: Resto: x %= y 2.2.5 Operadores de Incremento e Decremento. Estes operadores servem para somar 1 ou subtrair 1. Eh um tipo de abreviacao para os operadores de adicao e subtracao: ++x significa x += 1 ou x = x + 1 --y significa y -= 1 ou y = y - 1 No caso destes operadores de incremento, vemos um pequeno detalhe muito importante. Eles podem ser usados das seguintes maneiras: ++x, x++, --x, x-- O que difere entre ++x e x++ eh o valor retornado. Se colocamos ++ antes da expressao, o valor eh incrementado antes e soh entao eh retornado. Se colocarmos ++ depois da expressao, o valor eh retornado, e soh entao eh incrementado. Identicamente para o --. Vejamos: int x; x = 1; ++x; /* retorna 2 */ x++; /* retorna 2 */ x == 3; /* retorna 1 */ Outro exemplo: int x, y; x = 1; y = 3; x = y++; Aqui, o valor de x eh 3 e o valor de y eh 4. Continuando.... y = ++x; Agora o valor de y eh igual a 4, e o de x tambem. 2.2.6 Operadores Relacionais Estes servem para examinar as relacoes de ordem entre valores, como maior, menor, maior ou igual, menor ou igual. MAIOR: > MENOR: < MAIOR OU IGUAL: >= MENOR OU IGUAL: <= O valor retornado pela expressao eh 1 caso a expressao seja verdadeira, e 0 caso contrario. Ex.: int x,y,z; y = 2; z = 3; x = y < z; O valor de x eh 1. 2.2.7 Operadores Logicos AND e OR (E e OU) Em C, o operador logico E eh o && e o operador logico OU eh o ||. O operador E eh caracterizado pela seguinte tabela verdade: Tabela 3. ,---,---,--------, | a | b | a && b | |---|---|--------| | 0 | 0 | 0 | | 0 | 1 | 0 | | 1 | 0 | 0 | | 1 | 1 | 1 | '---'---'--------' Assim, a expressao (x < y) && (z >= x) sera 1 se e somente se x for menor que y e, simultaneamente, z for maior ou igual a x. Ou seja, se ambas as expressoes forem verdadeiras. O operador OU eh caracterizado pela seguinte tabela verdade: Tabela 4. ,---,---,--------, | a | b | a || b | |---|---|--------| | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 1 | '---'---'--------' Assim, a expressao (x < y) || (x < z) serah 1 se e somente se pelo menos uma das expressoes for verdadeira. Sera 0 se e somente se ambas forem falsas. 2.2.8 Operador sizeof Este operador serve para determinar o tamanho de uma variavel, tipo ou constante. O valor retornado eh em bytes. Ex.1: long int x; x = sizeof(x); Acima, o valor de x serah 4, ou 4 bytes, que dah 32bits, ou seja, x eh um inteiro de 32 bits, considerando que voce esteja usando um sistema semelhante ao nosso. Ex.2: int x; x = sizeof(int); Aqui x assume o valor que as variaveis do tipo int (inteiro) assumem por padrao. Neste caso, x serah 4. Ex.3: int x; x = sizeof("Oi"); Isso fara x igual a 3 (3 bytes) jah que cada caractere eh 1 byte e o 3o byte eh o caractere finalizador: '\0' cujo valor eh 0. Ex.4: int x; x = sizeof("Unsekurity Team!"); Neste caso, x terah o valor 17. 2.2.9 Operador unario & Este operador serve-nos para obter o endereco do 1o byte de memoria onde uma dada variavel estah armazenada. Este endereco eh em hexadecimal. Ex.1: int x; printf("%x",&x); O exemplo acima imprime na tela o endereco da variavel x. Veremos logo a seguir mais sobre este tipo de operador. 2.3 Precedencia e Associatividade dos Operadores. Esse lance de precedencia de operadores eh muito legal e serve para voce poupar parentesis e o tempo que voce usaria para digitar parentesis desnecessarios. A associatividade tambem evita que voce tenha que ficar associando desnecessariamente e com isso voce pode enxugar ainda mais seu codigo.. ,-------------------------------------,-------------------------, | Operadores | Associatividade | |-------------------------------------'-------------------------| | () [] -> Esquerda para a direita | | | | ! ~ ++ -- * & (tipo) sizeof Direita para a esquerda | | | | obs.: * e & sao os simbolos que | | usamos com ponteiros.. | | (tipo) significa int, float, e | | essas declaracoes.. | | | | * / % Esquerda para a direita | | | | + - Esquerda para a direita | | | | << >> Esquerda para a direita | | | | < <= > >= Esquerda para a direita | | | | == != Esquerda para a direita | | | | & Esquerda para a direita | | | | ^ Esquerda para a direita | | | | | Esquerda para a direita | | | | && Esquerda para a direita | | | | || Esquerda para a direita | | | | ?: Direita para a esquerda | | | | = += -= *= /= %= &= ^= |= <<= >>= Direita para a esquerda | | | '---------------------------------------------------------------' Na mesma linha estao operadores de igual precedencia, e de cima pra baixo, a precedencia diminui (os de cima sao mais prioritarios do que os de baixo, assim como na vida real, hehe) Vamos explicar um pouco sobre a lista acima. A expressao x = 1 + 2; equivale a (x = (1 + 2)); Veja que no caso do = nos associamos parentesis da direita para a esquerda.. Vejamos outro caso.. A expressao agora eh.. *y = 1 + 2 * 3 + 5 * 4; /* y eh um ponteiro para um inteiro ok? */ e equivale a ((*y) = ((1 + (2 * 3)) + (5 * 4))); A precendencia diz que operacoes tem precedencia sobre as outras.. No nosso caso, 1 + 2 * 3 + 5 * 4, a multiplicacao tem mais prioridade, entao calculamos antes das somas. A associatividade eh usada quando temos 2 operacoes de igual prioridade. Ou seja, se vc ver 2 * 3 * 4 voce estarah na verdade vendo ((2 * 3) * 4), pois temos duas multiplicacoes de igual precedencia, e a multiplicacao tem associatividade da esquerda para a direita, ou seja, associamos primeiro a multiplicacao que estava a esquerda, e depois a seguinte. Outro bom exemplo eh 2 / 3 * 4, que eh ((2 / 3) * 4), jah que a divisao e a multiplicacao tem a mesma precedencia. Um exemplo de associatividade classico eh o seguinte: x = y += 4 Seria equivalente a (x = (y += 4)) jah que como = e += tem a mesma precendencia, a prioridade eh dada entao a quem estiver mais a direita, e assim por diante. 2.4 Constantes As constantes em C podem ser basicamente de 4 tipos: a) inteiro b) caractere c) racional d) string 2.4.1 Constantes inteiras Uma constante inteira pode estar na base octal, decimal ou hexadecimal. Uma constante na base octal eh representada por um numero no qual o primeiro eh 0 e os seguintes sao os digitos de 0 a 7. Ex.1: 017262 eh uma constante octal valida. 09267 nao eh uma constante octal valida. Uma constante na base decimal ou eh 0 ou eh comecada com um digito nao nulo, de 0 a 9. Ex.2: 0 e 180289 sao constantes decimais validas. 01928 nao eh uma constante decimal. Uma constante na base hexadecimal eh comecada com 0x ou 0X e seguido de digitos de 0 a 9 e caracteres a, b, c, d, e, f. Ex.3: 0xffffff eh uma constante hexadecimal valida. 0xg ou 0x01 sao constantes hexadecimais invalidas. Uma constante unsigned termina em u ou U e uma constante long termina em l ou L. Ex.4: 8273L representa uma constante long int valida. 2.4.2 Constantes caracteres Um caractere pode ser representado por um valor inteiro definido pela tabela ascii, ou atraves da representacao entre '' (aspas simples). Ex.1: 'a' ou 'A' ou '8' sao caracteres ascii validos. Caracteres ocupam 1 byte de memoria, e tem mais 2 maneiras de serem representados, soh que dessa vez tendo em conhecimento apenas seus valores em ascii. '\ooo' onde ooo eh um numero em octal. Ex.3: '\013' ou '\005' sao caracteres ascii validos. '\19' nao eh um caractere ascii valido. '\51' representa o fecha parentesis ). Observe que '\051' eh o mesmo que '\51'. '\xhh' onde hh eh de um a dois digitos hexadecimais (de 0 a 9, a .. f, A .. F). Ex.4: '\xb' ou '\xx5' sao caracteres ascii validos. Alguns caracteres devem ser representados com um \ na frente, para que nao haja confusao no programa: '\\' -> eh uma \ '\?' -> eh uma ? '\'' -> eh um ' (apostrofo ou aspas simples) '\"' -> eh um " (aspas) 2.4.2.1 Sequencias de escape Sequencias de escape produzem caracteres nao graficos, que realizam determinadas acoes especiais. Sao geralmente usados como marcadores para indicar uma nova linha, o fim de um arquivo, etc. \a (alerta) -> Produz um alerta visivel ou audivel. \b (backspace) -> Muda a posicao atual 1 caractere antes. \f (form feed) -> Move a posicao atual para o inicio da proxima pagina. \n (new linha) -> Move a posicao atual p/ posicao inicial da proxima linha. \r (carriage return) -> Move a posicao atual p/ a posicao inicial da linha. \t (horizontal tab) -> Move a posicao atual p/ a proxima posicao de tabulacao definida na linha. \v (vertical tab) -> Move a posicao atual p/ a proxima posicao de tabulacao vertical definida. Ex.1: '\n' eh um caractere de nova linha. Quando impresso, uma nova linha eh criada e a escrita eh iniciada dela. 2.4.2.2 Sequencias de trigrama Todas as ocorrencias num arquivo fonte das seguintes sequencias de tres caracteres (trigrama) sao substituidas pelo caractere correspondente. ,----------,-----------, | Trigrama | Caractere | |----------|-----------| | ??= | * | | ??( | [ | | ??/ | \ | | ??) | ] | | ??' | ^ | | ??< | { | | ??! | | | | ??> | } | | ??- | ~ | '----------'-----------' Substituicao eh feita no momento da compilacao, e as sequencias trigramas foram criadas porque alguns conjuntos de caracteres nao continham os caracteres aqui representados. Se voce utiliza um sistema novo, provavelmente isso nao passarah apenas de curiosidade. Eh apenas para fins de compatibilidade. 2.4.3 Constante racional Uma constante pode ser tambem racional. Para isso, representamos com um . (ponto) no meio do numero. Ex.1: float x; x = 1.0; Veja que 1.0 eh um float valido. Dependendo da precisao do float, podemos colocar mais ou menos decimais, e tambem podemos colocar l ou L para especificar que a constante eh long double, ou f ou F para especificar que a constante eh float. Sem sufixo, a constante eh considerada double. Ex.2: 1.0L eh um long double, 1.0 eh double e 1.0f eh float. Podemos tambem usar expoentes, com (aeb), onde a eh um float, e b eh um inteiro que indica a potencia de 10 que deve ser multiplicada a a. Ex.: Ex.3: 1e-2 significa 1 x 10^-2 = 1 x 0.01 = 0.01 Ex.4: 1.0e-1 significa 1.0 x 10^-1 = 1.0 x 0.01 = 0.01 2.4.4 Constantes string Existem constantes que representam textos, em C. Sao textos entre aspas "". Ex.1: "Unsekurity Team!" eh uma string constante. Ex.2: "" eh uma string constante nula. 3. Controle de Fluxo Chegamos finalmente a um otimo capitulo. Sem dominar controle de fluxo, voce nao programara bem em C. Controle de fluxo eh o termo que usamos para designar os meios que temos para alterar o fluxo de um programa. Fluxo eh a ordem em que o seu programa eh executado. Bem, suponhamos que voce queira fazer uma tarefa repetitiva.. Digamos, criar um arquivo texto com cada linha e seu respectivo numero. Para fins didaticos, vamos supor que tenhamos 10 linhas e queiramos que a 1a linha seja 0, a 2a seja 1, a 3a seja 2, e assim por diante ate que a ultima seja 9. Sem controle de fluxo, voce teria de escrever isto: --corte aqui-- #include #include int main(int argc, char **argv, char **env) { FILE *exemplo; exemplo = fopen("exemplo1.txt","w"); /* funcao que abre um arquivo chamado exemplo1.txt para escrita (w), mas se o arquivo nao existir, ele cria */ fprintf(exemplo,"%d\n",0); fprintf(exemplo,"%d\n",1); fprintf(exemplo,"%d\n",2); fprintf(exemplo,"%d\n",3); fprintf(exemplo,"%d\n",4); fprintf(exemplo,"%d\n",5); fprintf(exemplo,"%d\n",6); fprintf(exemplo,"%d\n",7); fprintf(exemplo,"%d\n",8); fprintf(exemplo,"%d\n",9); /* funcao que escreve para o arquivo. */ fclose(exemplo); /* funcao que fecha o arquivo */ exit; } --corte aqui-- Bem, deu para fazer, afinal, para nossos fins, que eh nos entrosar logo no mundo C e Hacking, o que importa eh que funcione! Agora imagina ai se voce quisesse escrever 10000 numeros num arquivo.. Seria sacal, pra nao dizer impossivel, fazer do jeito que fizemos acima: escrever numero por numero. Ai entram os controles de fluxos para realizarmos tarefas repetitivas e tambem para tomadas de decisoes sobre que caminhos o fluxo deve seguir. Voce pode estar pensando: bah, mas que coisa idiota seria escrever 10000 numeros num arquivo, eu nao me interesso nisso.. Entao seria a hora para parar e pensar: Como vc scanearia 10000 portas de um servidor numa dada rede ? Faria um por um? Com certeza nao mano, ai vc se ve em apuros se nao souber controle de fluxo. Para mostrar a vantagem do controle de fluxo, veja como eu faria o programa anterior agora usando controles de fluxo: --corte aqui-- #include #include int main(int argc, char **argv, char **env) { FILE *exemplo; int i; exemplo = fopen("exemplo1.txt","w"); /* funcao que abre um arquivo chamado exemplo1.txt para escrita (w), mas se o arquivo nao existir, ele cria */ for(i=0;i<10;i++) { fprintf(exemplo,"%d\n",i); /* funcao que escreve para o arquivo. */ } fclose(exemplo); /* funcao que fecha o arquivo */ exit; } --corte aqui-- Poxa, nos livramos de muitos bytes!! Voce deve estar pensando: bah, sao apenas alguns bytes, prefiro do jeito anterior.. Ai vc tem que se lembrar do seu tempo: tempo eh dinheiro.. tempo eh precioso. Agora se vc disser: bah, tenho tempo e dinheiro de sobra.. Entao tah bem mano, faca como quiser! 3.1 Bloco de comandos. Voce pode agrupar um conjunto de tarefas, comandos, etc.. usando chaves { }. O seguinte conjunto de comandos: x = y = 0; x++; ++y; podem ser agrupadas em { x = y = 0; x++; ++y; } 3.2 IF-ELSE Bem, if-else eh um controlador do fluxo com o qual podemos fazer com quem um bloco de comandos seja executado se uma dada condicao for obedecida, e tambem pode fazer com que um bloco de comandos alternativo seja executado caso a condicao seja desobedecida. A sintaxe eh a seguinte: if (expressao) comando1 else comando2 Ex.1: if (x == 2) x += 1; else x -= 1; Ex.2: if (!x) { ++x; y = 1; } else { x = y = 0; } Ex.3: if (x=y) ; Ex.4: if (x>1) { x = 0; } No exemplo 1, se x for 2 (ou seja, se a expressao for verdadeira, ou >0) entao soma-se 1 a x. Caso contrario, diminui-se 1 de x. No exemplo 2, se !x for verdadeiro (ou seja, se x for falso, ou =0) entao o bloco de comandos { ++x; y = 1; } eh executado. Caso contrario (ou seja, se !x for falso) entao o segundo bloco de comandos eh executado: { x = y = 0; } No exemplo 3, se o valor retornado pela atribuicao x = y for nao nulo, entao nada eh feito. Caso contrario, nada eh feito tb! O comando ; que nao tem nada simplesmente nada faz. 3.3 IF-ELSE-IF Esta eh uma tecnica para vc encadear varios if-else. As condicoes (expressoes) vao sendo verificadas ateh q uma das condicoes seja verdadeira, entao um bloco de comandos particular serah executado: if (expressao1) bloco1 else if (expressao2) bloco2 else if (expressao3) bloco3 ... else if (expressaon) blocon else blocofinal Vamos exemplificar: Ex.1: if (x == -1) { ++x; } else if (x == 0) ; else if (x == 1) { --x; } else { x = 0; } O Exemplo acima faz com que x seja 0 no final. 3.4 switch Esse controlador de fluxo eh uma estrutura de decisao multipla, que verifica se uma dada expressao combina com qualquer um dos possiveis resultados constantes, e desvia o fluxo do programa de acordo. A sintaxe: switch (expressao) { case constante1: comandos1 case constante2: comandos2 default: comandos } Se a expressao for igual a constante1, entao todos os comandos apartir de comandos1 sao executados, a nao ser q vc saia com o uso de break; Se a expressao nao for igual a contante1 mas for igual a constante2, entao comandos2 em diante eh executado. Ex.1: No exemplo 1 do topico 3.3 podemos fazer assim: switch (x) { case -1: ++x; break; case 0: break; case 1: --x; break; default: x = 0; } No exemplo acima, se x for -1, ele incrementa 1 e sai logo do switch com um break; de forma que os comandos logo adiante nao sao executados. Se x for 0 ele apenas sai com um break; e se for 1, ele decrementa e sai. Se nenhum destes casos bater com a expressao, entao x eh zerado. Muita atencao com o break! switch (y) { case 'a': case 'b': case 'c': y = 'd'; break; case 'e': case 'f': case 'g': y = 'h'; break; } No caso acima, se o caractere y for a b ou c, entao atribuimos 'd' a ele e com o break saimos do switch. Se vc tiver com duvidas nos topicos discutidos agora, recomendo que voce use da tentativa-erro: tente, erre, ateh aprender. Tambem pergunte a alguem que saiba mais que voce e possa ajuda-lo (nem sempre quem sabe quer ajudar). 3.5 while while eh uma estrutura de laco (loop) que permite que um bloco de comandos seja executado enquanto uma expressao for verdadeira. while (expressao) blocodecomandos Ex.1: --corte aqui-- #include #include int main(int argc, char **argv, char **env) { FILE *exemplo; int i; exemplo = fopen("exemplo1.txt","w"); /* funcao que abre um arquivo chamado exemplo1.txt para escrita (w), mas se o arquivo nao existir, ele cria */ i = 0 while (i<10) { fprintf(exemplo,"%d\n",i); /* funcao que escreve no arquivo */ i++; } fclose(exemplo); /* funcao que fecha o arquivo */ exit; } --corte aqui-- No programa acima, enquando a expressao i<10 for verdadeira, o bloco de comandos { fprintf(exemplo,"%d\n",i); /* funcao que escreve no arquivo */ i++; } eh executado. Podemos substituir o while anterio pelo seguinte: while (i++<10) { fprintf(exemplo,"%d\n",i); /* funcao que escreve no arquivo */ } Entendeu ai? Ele primeiro compara i com 10 e depois de ter comparado, e a expressao ter sido valorada, entao ele incrementa i. Elimina a necessidade de colocar i++; dentro do bloco de comandos. Com a estrutura while jah eh possivel fazer muitas coisas! Quando voce quiser deixar um loop causado pelo while, vc usa break; Geralmente voce fara isso caso uma condicao excepcional ocorra.. while (1) { i++; if (i == 20) break; } A estrutura acima faz com que o bloco de comandos seja executado ateh que i seja 20, e entao o fluxo escapara do loop. Veja que a estrutura acima nao eh recomendavel pois caso i comece com um valor 20 ou maior antes de entrar no loop, entao o break nunca ocorrera e o loop serah infinito! Pelo menos teoricamente, pq ateh la seu computador terah queimado ou vc tera morrido. 3.6 for Esta eh uma estrutura de controle essencial para o newbie. Com ela, vc pode executar um bloco de comandos enquanto uma condicao for satisfeita, e quando nao for satisfeita, o fluxo pula fora do loop. for (inicializacao;condicao;incremento) blocodecomandos A inicializacao consiste de um comando que eh executado antes de comecar o loop. A condicao eh aquela na qual o loop se mantem e o incremento eh o comando realizado sempre no termino da execucao de cada ciclo do loop. Ex.1: for (i=0;i<10;i++) { printf("%d\n",i); } Uma estrutura for para o loop infinito eh Ex.2: for (;;) { /* comandos vem aqui */ } pode ser tb assim: Ex.3: for (;;) ; Outra estrutura for: Ex.4: for(i=10000;i==0;--i) { /* comandos vem aqui */ } Observe tambem que voce pode aninhar estruturas de loop como while e for, para obter um loop de varias dimensoes. Ex.5: for (x=0;x<5;x++) { for (y=0;y<7;y++) { /* comandos vem aqui */ } } Podemos combinar while, if, for, etc.. um dentro do outro, sem limitacoes logicas (apenas fisicas, q eh o seu computador). Dispomos tambem de um recurso mais avancado mas que muitos nao conhecem. Podemos, ao declarar uma estrutura for no seu codigo, colocar multiplas inicializacoes, incrementos e condicoes. Para multiplas inicializacoes, separamos por virgula as inicialicacoes. O mesmo faz-se para o incremento, e para colocar multiplas condicoes, basta fazermos uso do operador && ou ||, sinta-se a vontade. Um exemplo do que falo eh o seguinte: for (i=0,j=10;(i<10)&&(j==4);i++,++j) { /* comandos vem aqui */ } 3.7 do-while Bem, esta estrutura de loop (repeticao ou laco) chamada do-while eh equivale ao while, ou seja, executa um bloco de comandos enquanto uma condicao for satisfeita, mas no while, a condicao eh testada antes do loop, e no do-while a condicao eh testada sempre no final do loop. A sintaxe eh do bloco while (condicao); Ex.1: do { i++; } while (i<5); No do-while, o bloco de comandos sempre eh executado no 1o loop, e os demais loops soh haverao dependendo da condicao no final. 3.8 break e continue Faz-se necessario a existencia de comandos que permitam-nos sair de um loop ou pular um loop sem executar o codigo. Para isso existem os comandos break e continue. Quando o fluxo encontra um comando break; no bloco de comandos da estrutura de loop, entao o fluxo desvia para fora da estrutura de loop, e nenhum codigo mais eh executado dakela estrutura nem nenhuma condicao eh verificada. Jah quando o fluxo do programa encontra um comando continue; a acao tomada eh abandonar a execucao e pular para o proximo caso, ou seja, pular para o fim do bloco de comandos. O fluxo, porem, continua na estrutura de loop. O break pode ser usado em for, while, do e switch. Jah o comando continue nao se aplica a switch. Ex.1: for (x=0;x10) { goto ponto1; } else { x++; } ponto1: printf("%d",x); Espero que este exemplo tenha sido bastante esclarecedor. Nao podemos usar goto sem usar pelo menos um label, e o que chamo label eh uma palavra qualquer terminada em dois pontos. este_eh_um_label: Tendo um label, para fazermos o fluxo do programa pular para onde ele aponta, basta usar o seguinte goto este_eh_um_label; O fluxo eh desviado imediatamente e qualquer loop serah abandonado. A desvantagens do uso de goto's eh a dificuldade que voce tera em ler depois o seu codigo. Muitos labels sao de dificil localizacao, e assim voce levara mais tempo. Por isso, recomenda-se o uso das estruturas anteriormente explanadas. Pode-se fazer praticamente tudo sem o uso de goto's. Bem, este eh o fim deste capitulo, tente treinar muito usando todas estes controladores de fluxo. Mais uma vez, gostaria de lembra-lo que nem sempre todos estao com vontade de ajuda-lo nesta caminhada, entao eh bom ser realista! Nao se iluda atras de ajuda no IRC. Recomendo voce procurar amigos de verdade que possam ajuda-lo. Mas tambem nao seja amigo de alguem soh pq ela sabe das coisas. 4. Variaveis Este capitulo visa expor a voce os diversos tipos de variaveis que podemos declarar em C e como declara-las. Mais uma vez tento abrir-lhe os olhos para que nao soh leia mas tambem pratique! Force os valores que as variaveis possam receber e tente entender como a coisa funciona. 4.1 Variaveis e Escopo As variaveis em C sao declaradas sempre antes de comecar as linhas de comando propriamente ditas. Podemos declarar uma variavel dentro ou fora de uma funcao. Se, por exemplo, voce declarar uma variavel dentro de uma funcao (entre as chaves dela) entao voce devera fazer isso sempre logo apos o "abre chaves", ou seja, sempre no comeco da funcao. Estas variaveis declaradas dentro de funcoes sao chamadas variaveis locais. O Escopo de uma variavel local (Escopo de uma variavel eh a area do codigo em que ela pode ser usada..) eh a regiao que vai desde sua declaracao ateh o fim da funcao, que termina com um }. Voce pode tambem declarar variaveis fora das funcoes, e estas declaracoes devem ser feitas antes de todas as funcoes, logo no comeco do arquivo. Estas sao as variaveis globais, e pelo nome, o escopo destas variaveis eh a regiao desde sua declaracao ateh o fim do arquivo, ou mesmo no fim de outros arquivos. Uma variavel global pode ser usada dentro das demais funcoes do programa, mas nao pode ser inicializada fora de uma funcao. Assim, recomenda-se inicializar sua variavel na funcao main() que eh a primeira funcao a ser executada quando seu programa eh rodado. Duas variaveis tem o mesmo escopo se e somente se seus escopos terminam no mesmo ponto. Podemos tambem declarar variaveis dentro de blocos de comandos, como em estruturas if e for, e o escopo de tais variaveis serah apenas este bloco. 4.2 Tipos de variaveis Os tipos mais comuns de variaveis sao os seguintes: a) inteiro b) racional c) caractere Existe tambem um outro tipo, que eh apenas uma variacao usada para certos fins: d) ponteiro (apontador) Toda variavel eh declarada assim: (especificador-de-tipo) (tipo) nome_da_variavel; E pode ser declarada e inicializada assim: (especificador-de-tipo) (tipo) nome_da_variavel = (constante); Onde (constante) eh uma constante do tipo (tipo). Ex.1: int x = 1; int x, y=2, z; char m = 'a'; 4.2.1 Inteiro Uma variavel inteira eh aquela que assume valores inteiros. O tipo basico para um inteiro eh int, ou seja, declaramos um inteiro assim: int x; ou long int x; O valor de um inteiro assim declarado eh um inteiro de 32bits (estou considerando aqui os Linuxes cujo kernel eh 2.2.* num Pentium II) ou seja, pode ir de -2147483648 a 2147483647. Inteiros de 16 bits ainda podem ser declarados, bastando anexar a palavra short antes de int: short int x; ou short x; Isso faz com que x varie de -32768 a 32767. Um inteiro pode ser declarado para ser positivo apenas, bastando colocar a palavra unsigned (sem sinal). Portanto unsigned shor int m; seria uma declaracao no qual os valores de m podem ir de 0 a 65535 (2^16 - 1) e .. unsigned long int n; seria uma declaracao no qual os valores de n podem ir de 0 a 4294967296 (2^32 - 1). Segue aqui uma tabela com os principais tipos de declaradores para inteiros: ,---------------------,--------------------, | 16 bit | 32 bits | |---------------------|--------------------| | short | long | | short int | long int | | unsigned short int | unsigned long int | | signed short int | signed long int | | | unsigned int | | | signed int | '---------------------'--------------------' 4.2.2 Racional Uma variavel racional (ou ponto flutuante) eh aquela que assume valores racionais. Podemos declarar assim: float x1; ou double x2; Na primeira declaracao, temos x1 um ponto flutuante de precisao simples, e x2 um ponto flutuante de precisao dupla, ou seja, x2 pode ter mais casas decimais do que x1. Voce tambem pode usar unsigned, long e short para float: unsigned float x3; ,----------------,-----------------, | float | double | | short float | short double | | long float | long double | | unsigned float | unsigned double | | signed float | signed double | '----------------'-----------------' 4.2.3 Caractere Um caractere em ASCII - American Standart Code for Information Interchange, tem um valor que varia de 0 a 255. Ou seja, quando voce declara um caractere, voce estah na verdade declarando um inteiro de 0 a 255. char letra; Eh importante voce saber boa parte da tabela ASCII, principalmente se voce quer lidar com baixo nivel. O seguinte programa em C exibe a tabela para voce: -- corte aqui -- #include #include void main(int argc, char **argv) { unsigned char i; for (i=0;i<255;++i) { printf("%d -> %c\n",i,i); } exit(0); } -- corte aqui -- Para compilar o programa acima, no linux, digite gcc arquivo.c -o arquivo. (voce pode trocar o gcc por cc, que existe em mais ambientes UNIX). A linha de execucao preferida eh ./arquivo > ascii.txt, pois assim basta ler o conteudo de ascii.txt para analisar a tabela. Por exemplo, a letra A (maiusculo) tem o valor 65. Assim, apos declarado a variavel unsigned char c; voce poderia ter feito.. c = 'A'; ou c = 65; pois o resultado seria o mesmo. 4.2.4 Ponteiro Um ponteiro, ou apontador (sao as notacoes que mais uso), eh uma variavel que aponta para uma determinada area da memoria. Seu valor eh um endereco da memoria, que eh um inteiro em hexadecimal. Por ser inteiro, pode-se realizar algumas operacoes aritmeticas, como se fosse realmente um inteiro (mas mantenha em mente que eh um endereco de memoria). Chamamos de apontador porque podemos utilizar o operador unario * para acessar ao valor da memoria que o nosso ponteiro armazena. Como a memoria eh enderecada em bytes, este operador * retorna do byte para o qual o ponteiro 'aponta'. A declaracao eh simples, basta colocar um asterisco * antes do nome da variavel: int *x; float *y; char *s; Podemos tambem colocar o * no final do declarador. Ex.: int* x; float* y; char* s; Feito a declaracao, teremos que x conterah o endereco da memoria do 1o byte no qual estah um inteiro. Como um inteiro ocupa 4 bytes (32 bits), entao x conterah o endereco apenas do 1o byte. Para acessar, *x conterah o valor dos 4 bytes. O mesmo para y e s. Para um inteiro, o valor nulo eh 0, para um float, eh 0.0, para um char, eh '\0', mas e para um ponteiro? Nesse caso eh NULL, que eh definido em /usr/include/malloc.h como (char *)0. Assim, se vc comparar um ponteiro qualquer e ele apontar para NULL, entao ele nao apontarah para nada. 4.2.4.1 Operador unario & Apenas recordando, & eh um operador unario utilizado junto a variaveis para determinar o endereco de memoria onde estao armazenadas. Sua sintaxe eh a seguinte: Ex.1: &x eh o endereco de memoria de uma variavel declarada com int x; ou char x; 4.2.4.2 Operador unario * Este operador aqui eh novo neste tutorial. Eh utilizado para acessar ao valor da memoria para o qual um ponteiro aponta. Ex.1: int *pointerx; pointerx armazena o endereco de memoria *pointerx armazena o valor contido nakele endereco de memoria (nao apenas o valor daquele byte e sim o daquele byte e os seguintes ateh alcancar um int) Ex.2: arquivo.c ---corte aqui--- #include #include int main(int argc, char *argv[]) { int *pointerx, *pointery; int i; for(i=0;i<10;i++) { printf("%x -> %d\n",pointerx++,*pointerx); printf("%x -> %d\n",pointery++,*pointery); } exit; } ---corte aqui--- Compilando com gcc arquivo.c -o arquivo Temos: unsekurity:~# ./arquivo 80494c4 -> 134517992 80494b0 -> 0 80494c8 -> 1073818640 80494b4 -> -1 80494cc -> 1073781264 80494b8 -> 0 80494d0 -> 1074716332 80494bc -> -1 80494d4 -> 134513426 80494c0 -> 0 80494d8 -> 1073943016 80494c4 -> 134517992 80494dc -> 1074153896 80494c8 -> 1073818640 80494e0 -> 134513474 80494cc -> 1073781264 80494e4 -> 0 80494d0 -> 1074716332 80494e8 -> 1 80494d4 -> 134513426 unsekurity:~# Preste atencao que o valor inicial de pointerx eh um endereco de memoria cujo valor eh 134517992, ou seja, jah existe algo armazenado la. Isso pq o apontador para inteiro nao foi inicializado (foi somente declarado). Para isso voce tem q atribuir a ele um endereco de memoria livre. Existem basicamente 2 metodos para isso. 1o metodo -> declarando um inteiro e fazendo o apontador apontar para este inteiro. Ex.3: int x, *y; /* sim, podemos declarar um inteiro e um apontador para inteiro na mesma linha */ y = &x; /* faz y apontar para x */ Isso eh especiamente util pois assim as outras funcoes e programas nao vao mecher nakela area de memoria, jah que estah sendo oficialmente ocupada por uma variavel (isso se voce nao alterar a tal variavel criada). Tambem vale para outros tipos de variaveis: Ex.4: char a, *b; b = &a; 2o metodo -> usar uma funcao especial para retornar um endereco de memoria que caiba este inteiro (ou qlq tipo que desejar). Esta funcao eh um alocador de memoria. Os alocadores de memoria mais comuns sao malloc e calloc, que iremos explicar mais adiante, mas por enquanto, voce pode utilizar o seguinte: Ex.5: int *x; x = malloc(4); A funcao malloc (Memory ALLOCation) separa um espacozim na memoria suficiente para 4 bytes, e retorna o endereco do primeiro desses bytes. Voce pode utilizar assim tambem: Ex.6: char *y; y = malloc(sizeof(char)); ou Ex.7: float *f; f = malloc(sizeof(float)); Apos ter feito uso do apontador, voce pode liberar a memoria com a funcao free(): free(f); free(y); free(x); O unico parametro de free() eh o apontador cuja memoria foi separada com malloc. 4.3 Variaveis multidimensionais Acabamos de ver anteriormente os 4 tipos basicos de variaveis. Mas os tipos que aprendemos sao simples. Soh armazenam um unico valor. Se, por exemplo, queremos guardar informacoes sobre as portas de um servidor. Supondo que tenhamos scaneado 1024 portas, e queiramos guardar tudo o que sabemos em variaveis. Voce declararia 1024 variaveis uma por uma? Com certeza nao, entao voce teria que precisar das arrays ou listas de variaveis. Muitos chamam de vetor ou matriz, mas aqui preferimos chamar de array. 4.3.1 Unidimensionais Uma variavel unidimensional eh mais conhecida por lista ordenada. Uma lista de inteiros pode ser declarada da seguinte forma: Ex.1: int nome[10]; Ex.2: int variavel[1024]; Assim sao criadas, numa soh declaracao, multiplas variaveis. Para acessa-las, usamos o nome da variavel e o indice da variavel que queremos manipular. Os indices comecam a ser contados do 0. No exemplo 1, a primeira variavel da lista eh referenciada como nome[0], a segunda eh nome[1], a terceira eh nome[2], a quarta eh nome[3], e assim por diante, ateh a decima, que eh nome[9]. A variavel nome[10] nao existe. No exemplo 2, o mesmo se verifica, e assim, a ultima variavel eh variavel[1023]. Podemos utilizar estas variaveis como se fossem simples inteiros: nome[0] = 1; nome[1] = nome[0] + nome[2]; Em lugar do indice podemos usar uma variavel inteira, como a seguir: for (i=0;i<10;i++) { nome[i] = nome[i] + i; } Assim como declaramos e usamos uma lista de inteiros e seus membros, o mesmo podemos fazer para racionais e caracteres. Para os racionais, a semelhanca eh muito forte, entao nao iremos perder tempo discutindo o sexo dos anjos. 4.3.2 Multidimensionais Quando falamos anteriormente de variaveis unidimensionais, nos referimos a variaveis cuja listagem cresce numa soh dimensao. Agora nos referimos a variaveis multidimensionais a variaveis com multiplos indices, que se expandem em multiplas diferentes direcoes independentes. Declaramos uma variavel multidimensional colocando quantos [numero] a direita quantos forem as dimensoes: Ex.1: char var[18]; Aqui, var tem 1 dimensao, com 18 itens. Ex.2: char var[14][15]; Aqui, var tem 2 dimensoes, e tem 14 x 15 = 210 itens. Ex.3: float f[10][10][10]; Aqui, f tem 3 dimensoes, e tem 10 x 10 x 10 = 1000 itens. 4.3.3 Apontadores e Arrays Quando declaramos um array, digamos: int x[3]; No referenciamos a x[0], x[1] e x[2] aos 3 inteiros criados. O nome x referencia-se ao endereco de memoria do 1o byte do 1o item do array. Ele eh que armazena o endereco da memoria onde comeca o array. Ex.1: ---corte aqui--- #include #include int main(int argc, char *argv[], char *envp[]) { int x[3]; printf("%p %p\n",x,&x[0]); exit; } ---corte aqui--- O programinha acima mostra que x contem o endereco de memoria do primeiro byte do primeiro item da lista x. O mesmo obviamente ocorre com listas de outros tipos. Ex.2: ---corte aqui--- #include #include int main(int argc, char argv[][], char envp[][]) { char str[] = "Unsekurity Team"; printf("%s %s\n",str,&str[0]); exit; } ---corte aqui--- Ex.3: ---corte aqui--- #include #include int main(int argc, char *argv[], char *envp[]) { char str[] = "Unsekurity Team"; if (str == &str[0]) { printf("Yeah\n"); } else { printf("Oh shit\n"); } exit; } ---corte aqui--- Verifique o programa acima! Ele mostra que str eh igual ao endereco do 1o byte do 1o item da lista str[]. 4.3.4 O operador sizeof O operador sizeof tambem pode ser utilizado para saber o tamanho em bytes de um array. O valor que eh retornado eh o total de itens multiplicado pelo tamanho de cada item. Ex.1: int x[10], y; y = sizeof(x); O valor de y serah 40, que eh 4 (tamanho de um inteiro) vezes 10 (tamanho do array). Ex.2: int x[14][15]; Neste caso, o sizeof(x) sera 14 x 15 x 4 = 210 x 4 = 840 bytes. 4.4 Strings Strings sao arrays de caracteres. Simples, nao? Qualquer texto pode ser representado como uma lista ordenada de caracteres. Ex.1: char titulo[20]; A declaracao acima cria uma lista de 20 caracteres. Com essa array, voce podera representar todas as palavras que tenham ateh 20 digitos. A atribuicao de valores a strings podem ser feitos assim: Ex.2: titulo[0] = 'U'; titulo[1] = 'n'; titulo[2] = 's'; titulo[3] = 'e'; titulo[4] = 'k'; titulo[5] = 'u'; titulo[6] = 'r'; titulo[7] = 'i'; titulo[8] = 't'; titulo[9] = 'y'; titulo[10] = ' '; titulo[11] = 't'; titulo[12] = 'e'; titulo[13] = 'a'; titulo[14] = 'm'; titulo[15] = '!'; titulo[16] = '\0'; Mas este tipo de atribuicao nao compensa. Muito desgastante e obviamente desperdica nosso tempo. Qual seria entao o outro meio mais facil de atribuir valores a uma string? Para fazer a atribuicao corretamente, temos que fazer uso da funcao strcpy ou strncpy, que logo mais discutiremos mais a fundo e dos perigos que voce corre! Estas funcoes tem defeitos que permitem a voce acesso root.. Mas se acalme e continue lendo pq nao eh assim facil como voce tah pensando. 4.4.1 strcpy e strncpy A funcao strcpy copia uma string para uma string. Vejamos sua sintaxe: Digite 'man strcpy' no seu terminal unix. ------------- NAME strcpy, strncpy - copy a string SYNOPSIS #include char *strcpy(char *dest, const char *src); char *strncpy(char *dest, const char *src, size_t n); DESCRIPTION The strcpy() function copies the string pointed to be src (including the terminating 0' character) to the array pointed to by dest. The strings may not overlap, and the destination string dest must be large enough to receive the copy. The strncpy() function is similar, except that not more than n bytes of src are copied. Thus, if there is no null byte among the first n bytes of src, the result wil not be null-terminated. In the case where the length of src is less than that of n, the remainder of dest will be padded with nulls. RETURN VALUE The strcpy() and strncpy() functions return a pointer to the destination string dest. ------------- A sintaxe ai eh char *strcpy(char *dest, const char *src); Isso significa que o primeiro parametro eh uma string e o segundo eh uma string constante. Uma string constante eh o que vemos assim: "Unsekurity Team!" ou "Joaquim" Portanto, para fazer o que fizemos no exemplo 2 anteriormente, bastaria o seguinte: strcpy(titulo,"Unsekurity team!"); Esse comando resumiria todos os outros. O valor retornado pela funcao eh um ponteiro (jah jah iremos explicar) para a string de destino. A funcao strncpy eh identica, mas ela copia n caracteres apenas. Agora voce deve ter se perguntado: o que acontece se voce tentar encher uma string com uma maior que ela? Ai que vem muitos conceitos que cercam o hacking, como o buffer overflow. Nosso objetivo aqui nao eh enfatizar essa tecnica, mas outros textos do Unsekurity Team falam muito melhor do que falo aqui sobre o assunto. Mas o problema que vemos aqui com o strcpy eh que esta funcao vai copiando para uma dada regiao da memoria os caracteres ateh que ela encontre um caractere finalizador, conhecido como '\0' ou cujo valor eh 0. Podemos com isso sobrescrever areas da memoria importantissimas, e com isso, obter algum tipo de poder sobre o sistema. Portanto, tenha muito cuidado no uso desta funcao. Verifique sempre o tamanho do buffer e do texto que iremos passar para o buffer. 4.4.2 Declarando e Inicializando strings A declarao de uma string consiste na sintaxe char nome_da_string[numero]; Mas tambem podemos declarar uma string e atribuir-lhe logo seu valor inicial (que chamamos de inicializar uma variavel). Para isso, usamos a seguinte sintaxe: char nome_da_string[numero] = { 'U', 'n', 's', 'e', 'k', 'u', 'r', 'i', 't', 'y', ' ', 'T', 'e', 'a', 'm', '!' }; ou mais simplesmente char nome_da_string[numero] = "Unsekurity Team!"; 4.4.3 Nao ha meio mais simples que strcpy ou strncpy ? Nao podemos simplesmente fazer nome_da_string = "Unsekurity Team!"; Pois nome_da_string, como vimos antes, eh o endereco de memoria em que comeca a string (cujo final eh '\0' ou 0, e dai precisamos apenas saber onde comeca uma string), e qualquer texto entre aspas "" eh uma string constante cujo endereco na memoria eh outro local. Assim, como nome_da_string eh o endereco de memoria que nao muda, nao podemos atribuir-lhe outro endereco de memoria distinto do seu (e que eh temporario). Alias, nao podemos mudar o endereco da memoria em q um inteiro, um caractere ou um racional reside! Eh para isso que existem ponteiros. Agora vc deve estar se perguntando como funciona a funcao strcpy. Ela funciona copiando caractere por caractere apenas os seus valores (e nao o endereco de memoria de cada caractere) para cada item da string, que eh um array de caracteres. 4.5 Aplicacoes especiais dos apontadores 4.5.1 Apontadores para caracteres Existe algo especial no conceito de apontadores que ainda nao visualizamos no texto. Ja podemos criar variaveis multidimensionais, mas que tem tamanho fixos. Mas quando precisarmos de variaveis multidimensionais de tamanhos variaveis? Para isso usamos apontadores para caracteres. Podemos com apontadores simular variaveis multidimensionais de tamanhos variaveis, cujo tamanho pode alterar no decorrer da execucao do programa. Sabemos que a memoria de um computador da arquitetura Von Neumann eh uma sequencia de bytes ordenados, ou seja, temos um 1o byte, temos um 2o byte, e assim por diante. Sabemos que um caractere ocupa o tamanho de 1 byte. Assim, podemos armazenar uma 'string' que eh uma sequencia de caracteres com uma sequencia de bytes consecutivos, bastando apenas saber onde comeca a string e onde termina. Para saber onde comecaremos a ler a string, basta saber o endereco do byte no qual estah armanzenado o 1o caractere. Para saber onde termina, nao precisamos do endereco do ultimo byte, basta apenas que o ultimo byte da sequencia armazene o valor 0 ou '\0' em ascii, que eh o delimitador do final de arquivos, de strings, etc. Jah sabendo como uma string eh armazenada na memoria, o apontador de caractere nos prove um meio muito pratico (porem as vezes complicado) para acessar a string localizada naquela area da memoria. Para isso, basta termos acesso ao endereco do byte do 1o caractere. Nosso apontador para caracteres vai apontar inicialmente para este byte, e se formos somando 1 unidade, o apontador irah passar a apontar para o proximo byte, e se subtrairmos 1 unidade, o apontador ira passar a apontar o byte anterior. Ou seja, apenas usando o operador ++ e -- iremos lendo os caracteres que desejamos. Vamos assim usando ++ pra ir lendo o string ateh encontrarmos um caractere nulo '\0' ou 0. O uso de apontadores para caracteres a fim de substituir o uso de array de caracteres soh deve ser feito se voce dominar plenamente esta tecnica, que as vezes se torna essencial para realizar coisas mais avancadas. No topico 4.3.3, vimos que se declararmos um array, o nome do array servirah para nos referirmos ao endereco do 1o item da lista. Com isso, quando declaramos uma string via array de caracteres, o nome do array conterah o endereco da memoria onde a string estarah armazenada, e esse endereco pode ser atribuido a um ponteiro. Veja: char nome[] = "Unsekurity Team"; char *nomep; A atribuicao nomep = nome; eh valida, jah que a variavel nome contem o endereco da string declarada, e nomep eh um apontador que, logicamente, passarah a apontar (ou a se referir) a string nome. Mas a atribuicao nome = nomep; eh invalida, jah que nome eh o endereco onde estah armazenado o array nome[], e assim, nao pode mudar. Ou seja, a basica diferenca entre um array e um apontador eh que o endereco armazenado numa eh constante (nao muda) enquanto que o endereco na outra eh alteravel. Muitas funcoes recebem como parametro um apontador para caracteres, e isso jah eh suficiente para que a funcao tenha acesso a string armazenada na memoria. Vejamos alguns exemplos que podem ajuda-lo a consolidar mais as ideias: apontador1.c ---corte aqui--- #include #include int main(int argc, char *argv[], char *envp[]) { int i; char *var1; char var2[] = "Unsekurity Team"; printf("String var2 eh:\n"); for(i=0;;i++) { if (!var2[i]) /* se var2[i] eh 0 */ { printf("\nFim da string var2\n"); break; } else /* se var2[i] nao eh 0 */ { printf("%c ",var2[i]); } } var1 = var2; /* aqui var1 passarah a apontar p/ a string var2 */ printf("String apontada por var1 eh:\n"); for(i=0;;i++) { if (!*(var1+i)) /* se *(var1+i) eh 0 */ { printf("\nFim da string var1\n"); break; } else { printf("%c ",*(var1+i)); } } } ---corte aqui--- Compilando e executando, temos: unsekurity:~# gcc apontador1.c -o apontador1 unsekurity:~# ./apontador1 String var2 eh: U n s e k u r i t y T e a m Fim da string var2 String apontada por var1 eh: U n s e k u r i t y T e a m Fim da string var1 unsekurity:~# Verifique que quando fazemos var1 = var2; Entao var1+0 serah o endereco de memoria do item var2[0], que eh &var2[0], var1+1 serah o endereco de memoria do item var2[1], que eh &var2[1], e de uma forma geral, var1+i eh o endereco de memoria do item var2[i], que eh &var2[i]. Isso pq o tamanho do item que tratamos eh unitario (um caractere mede um byte). Se estivessemos tratando com inteiros de 32 bits, em vez de somar de 1 em 1 para ir lendo os valores, teriamos que somar de 4 em 4. Assim, *(var1+0) serah var2[0], *(var1+1) serah var2[1], *(var1+2) serah var2[2], *(var1+3) serah var2[3] e assim por diante. *(1) eh o valor que estah no primeiro byte de memoria, *(2) eh o valor que estah no segundo, e assim por diante. Para ir lendo a memoria atraves de apontadores, recomenda-se manter inalterado o valor do local da memoria em que voce comecou a analisar, sob risco de voce nao saber mais voltar ao inicio. Assim, evite construcoes como *(var1++); ou *(++var1); ou alterar o valor do apontador em si. Para ir lendo, eh melhor que faca: *(var1+i); e ir incrementado o valor de i. Assim, o valor do apontador se mantem inalterado, e a expressao var1+i retorna um endereco, que atraves do operador unario *, pode-se acessar o valor. Como aqui nao estamos alterando o valor do apontador, pode-se utilizar var2, jah que *(var2+i) eh o mesmo que var2[i]. Quando voce faz *(var1++); O apontador passarah a apontar o 2 caractere, enquanto que em *(var1+(i++)); O apontador continuarah a apontar sempre o 1 caractere, apesar da expressao acima retornar o i-esimo caractere. Para finalizar, vejamos o seguinte esquema: _________________ | | | V _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ |_|_|_|_|_|_|_|_|_|_|_|_|_|U|n|s|e|k|u|r|... A B ,---> este eh um apontador para char. _ _ |_| ---> |_| -> cada um dessas casinhas eh um char. |_| |_| |_| |_| |_| 4.5.2 Apontadores para apontadores. Voce deve ter visto acima a semelhanca entre um apontador e um array (qualquer que seja o tipo). Um apontador para um tipo simples de variavel (apontador para um tipo nao apontador) simula muito bem uma lista de variaveis, ou seja, um array, um vetor. Isso possibilita arrays de tamanhos variaveis, que podem mudar no percurso do programa. Mas e as variaveis multidimensionais, aquelas que tem 2 ou mais dimensoes, como podemos simula-las? Para isso, usamos uma tecnica muito importante chamada apontadores para apontadores. Sim. Vejamos o seguinte esquema: ,--> apontador para apontadores de caracteres. | ,-> apontadores de caracteres. | | _ _ _ _ _ _ _ _ |_| ---> |_| -------------> |_|_|_|_|_|_| ... -> caracteres. |_| ------------, _ _ _ _ _ |_| ----------, '--> |_|_|_|_|_| ... |_| | _ _ _ _ _ |_| '------> |_|_|_|_|_| ... |_| |_| ... Isso simula muito bem uma estrutura multidimensional, fazendo-se necessario apenas o uso de marcadores de fim de lista (como o 0 para caracteres). char **nomes; Acima voce ve a declaracao de um apontador para apontadores de caracteres. Devido a flexibilidade de se usar um apontador, voce pode inserir quantos itens desejar, e estes itens nao obedecem um limite de tamanho fixo. Ex.1: apontador2.c ---corte aqui--- #include #include int main(int argc, char **argv) { char **x; x = malloc(16); *(x+0) = "Joao"; *(x+1) = "Maria"; *(x+2) = "Joe"; printf("%s %s %s\n",*(x+0),*(x+1),*(x+2)); printf("%c %c %c %c\n",*(*(x+0)+0),*(*(x+0)+1),*(*(x+0)+2),*(*(x+0)+3)); printf("%c %c %c %c %c\n",*(*(x+1)+0),*(*(x+1)+1),*(*(x+1)+2),*(*(x+1)+3),*(*(x+1)+4)); printf("%c %c %c\n",*(*(x+2)+0),*(*(x+2)+1),*(*(x+2)+2)); free(x); /* esta funcao libera a memoria reservada via malloc */ exit; } ---corte aqui--- No exemplo acima, com um apontador para apontador de caracteres, declarado como char **x; Usamos *(x+i) para acessar a i-esima string e *(*(x+i)+j) para acessar o j-esimo caractere da i-esima string. Podemos tambem utilizar um array de apontadores para caracteres (este recurso, porem, eh mais limitado do que usar um apontador para apontador). Ex.2: char *nomes[]; Que pode ser inicializado assim: char *nomes[] = { "Joao", "Maria", "Joe" }; No caso acima, teriamos um numero fixo (3) de strings cujos tamanhos podem variar. Os recursos e vantagens deste topico serao discutidos mais a fundo no tutorial de algoritmos em C, que eh um manual mais avancado para aqueles que concluirem este manual. 4.6 Classes de memoria A classe de memoria determina o tempo de vida da memoria associada ao objeto, que pode ser uma variavel ou uma funcao. Existe 2 classes de memoria: a automatica e a estatica. As variaveis automaticas (aquelas cuja classe de memoria eh automatica) sao locais ao bloco em que foram definidas, e sao logo esquecidas no termino da execucao do bloco. As variaveis estaticas (aquelas cuja classe de memoria eh estatica) podem ser locais a um dado bloco ou externa a todos (nesse caso, eh uma variavel global), e por serem ditas estaticas, seus valores sao mantidos quando blocos e funcoes terminam ou recomecam sua execucao. Ou seja, se voce usar uma variavel estatica dentro de uma funcao, da proxima vez que esta funcao for executada, o valor da variavel nao terah mudado (mas a variavel continua local, ou seja, seu escopo eh limitado). Uma variavel estatica pode ter ligacao interna ou externa. Quando a ligacao eh interna, ela nao pode ser acessada de fora do seu escopo. Ou seja, seu escopo eh apenas o bloco no qual foi definida, e ela nao existirah fora de tal bloco. Quando a ligacao eh externa, ela pode ser consultada de fora de seu escopo normal, que seria o bloco em que ela fosse definida. Existem 5 especificadores de classe de memoria, que sao palavras que devemos colocar no comeco da declaracao de uma variavel. Soh podemos usar no maximo uma por vez (nao eh possivel fazer combinacoes). 4.6.1 auto O especificador de classe de memoria auto define quando uma variavel eh automatica, ou seja, quando a execucao foge seu escopo, ela deixa de existir. Por definicao, toda variavel declarada dentro de um bloco de comando sem a utilizacao da palavra-chave auto eh do tipo auto. Este especificador soh pode ser utilizado dentro de uma funcao. Ex.1: int nome_da_funcao(int parametro) { auto int x; /* comandos vem aqui */ } Apos a execucao da funcao, a variavel x eh perdida. 4.6.2 extern Jah sabemos o que sao variaveis externas: sao variaveis com ligacao externa. Bem, mas como fazer para acessa-las? Eh para isso que serve o especificador extern. O extern serve para aumentar o escopo de uma variavel externa. A classe de memoria de uma variavel declarada com extern eh automatica, ou seja, terminado seu escopo, ela deixa de existir. O especificador extern pode ser usado dentro de uma funcao, para se referir a variaveis definidas fora da tal funcao, mas tambem pode ser usado fora de qualquer funcao, para se referir a variaveis definidas em outros arquivos fontes. Ex.1: arquivo1.c: ---corte aqui--- #include #include extern float x; extern float metade(void); int main(int argc, char argv[]) { x = 1.0; x = metade(); printf("x eh %f\n",x); exit; } ---corte aqui--- arquivo2.c ---corte aqui--- #include #include float x; float metade(void) { return x/2; } ---corte aqui--- Compile os arquivos acima com a linha de comando gcc arquivo1.c arquivo2.c -o exemplo_extern E execute com ./exemplo_extern unsekurity:~# ./exemplo_extern x eh 0.500000 unsekurity:~# O exemplo acima serve para ilustrar o uso do extern. A variavel x definida em arquivo2.c tem inicialmente como escopo todo o arquivo2.c, mas por ser variavel externa (com ligacao externa) pode ser acessado pelo arquivo1.c, bastando para isso a declaracao de x usando extern em arquivo1.c. Apos o uso do extern, o escopo da variavel x deixou de ser apenas o arquivo2.c para ser ambos os arquivos. Ex.2: Voce poderah ver exemplos praticos do uso de extern em arquivos de cabecalho (que tem extensoes .h) tais como o stdio.h, cujos trechos disponibilizamos logo abaixo (verifique o /usr/include/stdio.h): --- corte aqui --- ... /* Read a character from STREAM. */ extern int fgetc __P ((FILE *__stream)); extern int getc __P ((FILE *__stream)); /* Read a character from stdin. */ extern int getchar __P ((void)); ... --- corte aqui --- Estas funcoes estao definidas nas bibliotecas glibc2 ou libc5, dependendo do seu linux (se for novo, deverah ser glibc2).. Eh importante observar que uma declaracao de variavel usando extern nao separa memoria nem inicializa os dados. Apenas amplia o escopo de uma variavel externa jah existente. Tambem eh bom frizar que voce soh pode usar isso para ampliar o escopo de variaveis externas apenas, e nunca o escopo das locais. 4.6.3 static Uma variavel externa declarada com static tem seu escopo limitada ao arquivo fonte onde a variavel eh definida. Isso faz com que as variaveis externas declaradas com static nao possam ser acessadas com o uso de extern apartir de outros arquivos fonte. Ex.1: arquivo3.c ---corte aqui--- #include #include int x, y; extern int funcao(void); int main(int argc, char argv[]) { y = funcao(); printf("x eh %d, y eh %d\n",x,y); exit; } ---corte aqui--- arquivo4.c ---corte aqui--- #include #include static x; int funcao(void) { x = 3; return x; } ---corte aqui--- Isto eh especialmente util se, por exemplo, num arquivo fonte voce precisa de funcoes que usam variaveis cujos nomes devem permanecer livres para o uso em outros arquivos fonte. Ou seja, com static em variaveis externas, as variaveis tornam-se invisiveis para livre uso do programador (isto nao seria possivel sem o static, pois haveria conflito entre nomes). Uma variavel local (nao externa, cujo escopo eh apenas uma funcao) declarada com static obtem classe de memoria estatica, ou seja, elas nao perdem seus valores quando do termino da execucao da funcao. Ex.2: arquivo5.c ---corte aqui--- #include #include int retornar(int y) { static int x; x += y; printf("Valor de x: %d\n",x); return x; } int main(int argc, char argv[]) { retornar(1); retornar(4); retornar(-2); exit; } ---corte aqui--- Compile com a seguinte linha de comando: gcc arquivo5.c -o arquivo5 e execute com ./arquivo5 unsekurity:~# ./arquivo5 Valor de x: 1 Valor de x: 5 Valor de x: 3 unsekurity:~# Voce pode verificar que o valor da variavel estatica x preservou-se mesmo tendo a funcao ter sido iniciado e terminado sua execucao 3 vezes. 4.6.4 register Bem, algumas variaveis que sao acessadas frequentemente, como os contadores de estruturas for, podem ser armazenadas no processador, mais especificamente em um registrador, local cujo acesso eh mais veloz que a memoria convencional (RAM ou SWAP). A sintaxe eh: register int x; ou register char y; Ou seja, basta colocar a palavra-chave 'register' antes da declaracao. Este tipo de declaracao obedece a limites de hardware, ou seja, depende do processador que rodarah o programa. Processadores mais novos sempre tem registradores maiores, e por isso, sao mais potentes. Nao eh possivel, no entanto, retornar o endereco de memoria de uma variavel declarada com register, exatamente porque a variavel nao estah na memoria, e sim num registrador. Ou seja, mais adiante voce aprenderah o operador unario & e verah que ele nao poderah ser utilizado com variaveis em registradores. Voce nao pode usar declaracoes register para variaveis globais, ou seja, akelas cujo escopo eh o arquivo inteiro, e deve, portanto, usar register apenas para variaveis locais e para os parametros de uma funcao. Ex.1: int funcao(register int x) { register char y; /* codigo vem aqui */ } 4.6.6 typedef Em declaracoes com typedef, nenhuma variavel eh criada. Em compensacao, criam-se novos identificadores que resumem outros tipos de variaveis. Ex.1: typedef long int novo_tipo; Assim, verificaremos que long int x; e novo_tipo x; sao sinonimos. Ou seja, typedef eh apenas um recurso para criar novos nomes de tipo mais complexos apartir do que jah dispomos. Ex.2: Com typedef char caractere; verificaremos que caractere a, b[10]; eh sinonimo de char a, b[10]; Ex.3: Com typedef float racional; teremos que racional metade(racional x) { return x/2; } e float metade(float x) { return x/2; } sao a mesma coisa. 4.7 Molde Molde eh um operador unario para fins de conversao entre tipos de valores. Podemos converter caracteres em inteiros, inteiros em caracteres, float em inteiros, etc.. Basta para isso a seguinte expressao: (nome-do-tipo) expressao Ex.1: int x; /* x eh inteiro */ double y; y = sqrt((double) x) Se voce verificar bem, verah que a funcao sqrt recebe um valor do tipo double e retorna outro double, correto? Mas x eh inteiro.. Observe entao que (double)x eh uma expressao que retorna o valor inteiro de x como se fosse double. Ex.2: int valor_caractere(char a) { return (int)a; } A funcao acima retorna o valor inteiro referente a um dado caractere a. Ex.3: int x; unsigned int y; y = (unsigned int) x - 1000; Aqui, do valor de x eh retirado 1000 unidades, e o valor inteiro eh convertido para o tipo unsigned int. Molde tambem pode ser chamado de 'Cast'. Assim, quando vc ver a palavra 'without cast' numa mensagem de erro do seu compilador, eh pq o erro provem de uma atribuicao de tipos inaceitavel. 5. Funcoes 5.1 Conceitos basicos Bem, jah vimos no capitulo 2 algo sobre Funcoes. Apenas relembrando, sabemos que uma funcao tem a seguinte forma: declaradores_de_tipo nome_da_funcao(parametros) { /* comandos vem aqui */ } Os declaradores de tipo definem certas propriedades de uma funcao. Eh praticamente igual ao que vimos nos declaradores de variaveis. Uma funcao que retornarah um numero inteiro eh da seguinte forma: int nome_da_funcao(parametros) { /* comandos vem aqui */ } Quando retorna um caractere eh da seguinte forma: char nome_da_funcao(parametros) { /* comandos vem aqui */ } 5.1.1 A funcao main() Sabemos que um programa eh um conjunto de funcoes e variaveis. Ao ser executado, existe uma funcao que eh executada primeiro, e por padrao, esta eh a funcao main, e sem esta, o programa nao eh executavel. Voce deve assim declarar sempre uma funcao main() em pelo menos um de seus arquivos fontes, e o especificador de tipo pode ser qualquer um que voce queira, desde int, char, float, void, .. Ex.: int main(int argc, char *argv[], char *env[]) { /* comandos vem aqui */ } ou char main(int argc, char *argv[], char *env[]) { /* comandos vem aqui */ } O valor retornado pela funcao main eh o valor retornado pelo programa. 5.1.2 Parametros Com certeza voce deverah ter visto alguns programas que recebem parametros na linha de comando. Para isso, voce tem que declarar os parametros da funcao main como a seguir: (tipo) main(int argc, char *argv[], char *env[]) { /* comandos vem aqui */ } Onde (tipo) eh qualquer um do tipos de valores que podem ser retornados. Veja que quando colocamos esta como sendo a sintaxe da funcao main, o inteiro argc assume o numero total de parametros recebidos. Um array de strings eh definido por argv, de forma que argv[0] eh o primeiro parametro, argv[1] eh o segundo, e assim por diante. Deve-se frisar que o primeiro parametro eh o nome do executavel. O array de strings definido com env fornece-nos os valores das variaveis de sistema, aquelas que voce pode acessar do seu console digitando 'env': unsekurity:~# env PWD=/root HZ=100 HOSTNAME=unsekurity.virtualave.net MOZILLA_HOME=/usr/lib/netscape ignoreeof=10 LS_OPTIONS= --color=auto -F -b -T 0 QTDIR=/usr/lib/qt OPENWINHOME=/usr/openwin MANPATH=/usr/local/man:/usr/man/preformat:/usr/man:/usr/X11R6/man:/usr/openwin/man LESSOPEN=|lesspipe.sh %s PS1=\h:\w\$ PS2=> KDEDIR=/opt/kde LESS=-M USER=root LS_COLORS=no=00:fi=00:di=01;34:ln=01;36:pi=40;33:so=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:ex=01;32:*.cmd=01;32:*.exe=01;32:*.com=01;32:*.btm=01;32:*.bat=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.bz2=01;31:*.rpm=01;31:*.deb=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.jpg=01;35:*.gif=01;35:*.bmp=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.mpg=01;37:*.avi=01;37:*.mov=01;37: CPLUS_INCLUDE_PATH=/usr/lib/qt/include: MACHTYPE=i386-slackware-linux-gnu MAIL=/var/spool/mail/root LOGNAME=root SHLVL=1 HUSHLOGIN=FALSE MINICOM=-c on SHELL=/bin/bash HOSTTYPE=i386 OSTYPE=linux-gnu TERM=linux HOME=/root PATH=/usr/local/sbin:/usr/sbin:/sbin:/usr/local/bin:/usr/bin:/bin:/usr/X11R6/bin:/usr/openwin/bin:/usr/games:/opt/kde/bin _=/usr/bin/env unsekurity:~# O seguinte programa mostra como eh feito essa atribuicao aos parametros da funcao main: parametros.c ---corte aqui--- #include #include main(int argc, char *argv[], char *env[]) { printf("Existem %d parametros.\n",argc); if (argc) { int i; for (i=0;i #include int (*soma) (int a, int b) { return a+b; } int (*diferenca) (int a, int b) { return a-b; } int operacao(int a, int b, int(*funcao)(int x, int y)) { return (*funcao)(a, b); } int main(int argc, char **argv) { int x, y, a, b; if(argc<3) { return 0; } a = atoi(argv[1]), b = atoi(argv[2]); x = operacao(a,b,(int(*)(int,int))soma); y = operacao(a,b,(int(*)(int,int))diferenca); printf("%d + %d = %d e %d - %d = %d\n",a,b,x,a,b,y); } ---corte aqui--- Veja que a funcao operacao() tem 3 parametos: dois inteiros a,b e um apontador para uma funcao. No nosso caso, int(*funcao)(int x, int y) nos diz que funcao eh um apontador para uma funcao de 2 parametros x e y. O molde, no nosso caso, eh (int(*)(int,int)). isso converte o nome de qualquer funcao para um apontador de funcao de 2 parametros inteiros. Assim, o molde geral eh (declaradores_de_tipo(*)(parametros)). Isso convertera qualquer funcao para um apontador de funcao. A declaracao basica de um apontador de funcao eh: int (*ptfuncao)(parametros); E a atribuicao adequada seria ptfuncao = (int(*)(parametros))nomedafuncao; Assim, uma chamada (*ptfuncao)(parametros); seria equivalente a nomedafuncao(parametros); Ex.: int (*cmp)(void*,void*); cmp = (int(*)(void*,void*))strcmp; Assim, cmp eh um apontador para strcmp. 5.7 Funcoes retornando vetores Ateh a pouco tempo me deparava com duvidas quando desejava criar uma funcao retornando um vetor. Para que a funcao retornasse um apontador, bastaria acrescer * no fim do seus declaradores de tipo: char* funcao(parametros) ou char *funcao(parametros) sao declaracoes identicas. Declaracoes literalmente retornando vetores nao sao permitidas (se eu estiver errado, corrija-me) O que se permite eh retornar um apontador para o elemento inicial de um vetor. Tente compilar uma funcao com a seguinte declaracao: int funcao()[] { ... } E obterah um erro como o seguinte: "`funcao' declared as function returning an array". 5.8 Funcoes com numero variavel de parametros Esse eh um topico muito interessante e que eh a causa do format bug. Assim, para entender essa especie de bug que tanto falam, eh importante que voce dobre sua atencao neste topico, jah que aqui mostraremos como escrever uma funcao que aceite uma lista variavel de argumentos, assim como a funcao printf. A declaracao de uma funcao com varios argumentos eh declarado_de_tipo funcao(alguns_parametros, ...) onde esses tres pontos ... significa que a funcao espera receber um numero variavel de parametros. Esse ... soh pode aparecer no final de uma lista de parametros. O funcionamento de tal funcao serah de percorrer uma lista de argumentos que nem tem nome. O header ajuda a funcao a percorrer tal lista de argumentos. Assim, voce deve incluir tal arquivo da seguinte maneira: #include no arquivo que tem a funcao anterior. Existe um tipo chamado va_list, declarado em stdarg.h, que eh usado para declarar uma variavel que referir-se-a a cada um dos parametros. Eh como poderiamos chamar de um apontador de parametro. Vejamos um exemplo: #include int myprintf(char *format, ...) { va_list ap; } No nosso caso, ap (apontador de parametro) eh do tipo va_list. Usamos entao a macro va_start para atribuir a ap o valor do primeiro parametro. #include int myprintf(char *format, ...) { va_list ap; va_start(ap,format); } Assim ap aponta para o primeiro parametro. A chamada va_arg retorna um parametro e faz ap apontar para o proximo, e deve conter o tipo de valor a ser retornado. Assim, #include int myprintf(char *format, ...) { va_list ap; int x; va_start(ap,format); x = va_arg(ap,int); } Cada chamada va_arg retorna um parametro (convertendo-o ao tipo especificado) e faz o apontador de parametros apontar para o proximo. O perigo reside no fato que os parametros estao todos armazenados numa pilha. Vao sendo retirados a cada chamada a va_arg. Se retirarmos parametros demais, estaremos retirando na verdade valores da Stack, ou Pilha, que armazena os parametros de todas as funcoes sendo chamadas no computador. O pequeno grande defeito das funcoes printf, sprintf, etc.. eh que se voce passar como primeiro parametro adicional uma string que contem um formato (ou seja, se voce passar "%s blabla %d mais blabla") a substituicao eh feita e podemos ler mais valores alem do permitido pela funcao, e inclusive altera-los. Nosso objetivo, no entanto, eh o estudo da linguagem C, e tal assunto foge um pouco ao objetivo deste texto. Recomendo que leiam um texto do Unsekurity sobre o assunto, http://unsekurity.virtualave.net. 6. Estruturas 6.1 Declarando e Acessando estruturas Estruturas sao nada mais que tipos complexos de dados, que podem conter variaveis de diferentes tipos, inclusive apontadores. A definicao eh da seguinte forma: struct estrutura { ... }; Veja que cada struct termina em ponto-e-virgula. Um exemplo pratico eh: struct pessoa { float altura; float peso; char* nome; int idade; }; Isso define um novo tipo de variavel, chamada pessoa. Podemos 'criar' uma pessoa da seguinte forma: struct pessoa dv; E podemos modificar os subvalores de dv: dv.altura = 1.70; dv.peso = 60; dv.nome = "Dimitri Vashnov"; dv.idade = 18; Obviamente podemos criar estruturas que contenham outras estruturas: struct grupo { struct pessoa chefe; int tamanho; }; Assim, poderiamos declarar dv o chefe o grupo assim: struct grupo grupo_do_dv; grupo_do_dv.chefe = dv; grupo_do_dv.tamanho = 1; Assim, a idade do chefe seria grupo_do_dv.chefe.idade 6.2 Apontadores para estruturas Podemos tambem ter apontadores para estruturas diversas, bastando adicionar * logo apos todos os declaradores de tipo. Ex.: struct ponto { int x, y; }; struct ponto *mouse; Assim mouse eh um apontador para uma estrutura ponto. Podemos acessar os subvalores da estrutura apontada por mouse de 2 formas: (*mouse).x ou mouse->x Ambas as formas sao validas, mas eh recomendavel o uso da ultima. Assim, mouse->x e mouse->y sao inteiros. Podemos ter uma sequencia de . e -> se alternando, bastando prestarmos atencao se a estrutura da qual queremos acessar o objeto eh nao-apontador ou apontador, respectivamente. 6.3 Lista Encadeada (Linked Lists) Esta eh a parte mais importante deste topico. Eh possivel declarar dentro de uma estrutura um apontador para outra estrutura do mesmo tipo. Dificil de compreender? Vejamos com um exemplo: struct item { int valor; struct item *proximo; }; Uma declaracao recursiva, voce diria. Isso nos fornece uma lista de itens no qual o primeiro tem um apontador para o segundo, o segundo tem um apontador para o terceiro, e assim por diante. O ultimo item tem apontador NULL. Vejamos isso na pratica: struct item a, b, c, d; a.valor = 12; a.proximo = &b; b.valor = 28; b.proximo = &c; c.valor = -4; c.proximo = &d; d.valor = 2000; d.proximo = NULL; Assim, podemos acessar o valor do primeiro item assim: a.valor, e o valor do segundo item assim: a.proximo->valor; e o valor do terceiro item assim: a.proximo->proximo->valor; e o valor do quarto assim: a.proximo->proximo->proximo->valor; Obviamente, podemos percorrer toda a lista ateh encontrar um elemento cujo campo proximo seja NULL. Um exemplo ilustrativo seria: ,-------, | | |A|_|_|B|_|D|_|C| | | | | '-----' '---' A seguir segue o codigo fonte de uma implementacao de lista encadeada. ---corte aqui--- #include #include #define LEDSIZE 100 struct item { int valor; /*valor armazenado pela estrutura*/ struct item *prox; /*ponteiro para proximo item*/ }; struct nocabeca { int valor; int nocabeca; struct item *prox; }; struct nocabeca *vago; struct item *ocupar(void) { struct item *pt; if(vago->prox != NULL) { pt = vago->prox; vago->prox = vago->prox->prox; pt->prox = NULL; } else { printf("Overflow\n"); exit; } return pt; } void desocupar(struct item *pt) { pt->prox = vago->prox; vago->prox = pt; } int main(int argc, char *argv[]) { struct item temp[LEDSIZE]; int i; vago = malloc(sizeof(struct nocabeca)); /*inicializa LED de LEDSIZE itens*/ (*vago).valor = 0; (*vago).nocabeca = 1; vago->prox = &temp[0]; for(i=0;ivalorchar ou (*item2).valorchar Deve-se frisar que uma uniao contem apenas um dos valores listados, de forma que nao poderemos acessar 2 ou + tipos de dados ao mesmo tempo. Uma uniao soh pode ser inicializada com o valor do tipo do seu primeiro mesmo. Ou seja, union item relogio = 4; eh uma inicializacao valida, mas union item pente = 5.2; nao eh. Assim, se voce fizer relogio.valorchar = "Unsekurity"; entao o valor que estava em relogio.valorint serah perdido. O tipo certo do dado armazenado eh de conhecimento do programador e nao da maquina. Obviamente, voce pode criar estruturas que contenham unioes e unioes que contenham estruturas: O exemplo mais comum eh === struct in_addr { union { struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b; struct { u_short s_w1,s_w2; } S_un_w; u_long S_addr; } S_un; #define s_addr S_un.S_addr #define s_host S_un.S_un_b.s_b2 #define s_net S_un.S_un_b.s_b1 #define s_imp S_un.S_un_w.s_w2 #define s_impno S_un.S_un_b.s_b4 #define s_lh S_un.S_un_b.s_b3 }; === *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-* Este tutorial estah incompleto, porem, jah contem mais informa- coes do que o anteriormente pu- blicado. Possiveis erros, con- tactem o Unsekurity Team ou a mim via E-mail. Se houver algum trecho mal explicado, por favor, contactem-nos. Este texto nao eh traducao de nenhum livro ou tu- torial e foi feito na integra por Dimitri Vashnov a.k.a xf86config. Email: palwaaktaar@bol.com.br Visite: http://unsekurity.virtualave.net *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*