Original Article: Programming tips
Author: cs.engr.uky.edu

Dicas de programação

Espera-se que os alunos em disciplinas introdutórias e avançadas de Ciência da Computação conheçam algumas informações básicas sobre ferramentas de C, Unix, e ferramentas de desenvolvimento de software. Esta página detalha algumas dessas informações e sugere alguns exercicios.

C

variáveis globais, strings, buffers, alocação dinâmica, números inteiros, layout das estruturas, ponteiros, saída, parâmetros de linha de comando, recursos de idioma, vários arquivos fonte, ligação de múltiplos arquivos de objetos, depuração
  1. C permite que você declare variáveis fora de qualquer procedimento. Essas variáveis são chamadas de variáveis globais.
    1. Uma variável global é alocada uma vez quando o programa inicia e permanece na memória até o final do programa.
    2. Uma variável global é visível para todos os procedimentos no mesmo arquivo.
    3. Você pode fazer uma variável global declarada no arquivo A.c visível para todos os procedimentos em alguns outros arquivos B.c, C.c, D.c, ... declarando-o com o modificador extern nos arquivos B.c, C.c, D.c, ... como neste exemplo:
      extern int theVariable
      Se você tiver muitos arquivos compartilhando a variável, você deve declará-la extern em um arquivo de cabeçalho foo.h (descrito abaixo) e usar #include foo.h nos arquivos B.c, C.c, D.c, .... Você deve declarar a variável em exatamente um arquivo sem o modificador extern ou ela nunca será alocado.
    4. Não é uma boa idéia usar muitas variáveis globais, porque você não pode localizar os locais nos quais elas serão acessadas. Mas existem situações nas quais variáveis globais permitem evitar a passagem de muitos parâmetros para as funções.
  2. Strings são ponteiros para arrays de caracteres com terminação nula.
    1. Uma string é declarada char *variableName.
    2. Se sua string for constante, você pode simplesmente atribuir uma string literal à ela:
      char *myString = "This is a sample string"; 
    3. Uma string vazia tem apenas o terminador nulo:
      myString = ""; // empty string, length 0, containing null
    4. Um ponteiro nulo não é uma string válida:
      myString = NULL; // invalid string
      Você pode usar esse valor para indicar o fim de uma série de strings:
      argv[0] = "progName";
      argv[1] = "firstParam";
      argv[2] = "secondParam";
      argv[3] = NULL; // terminator
    5. Se sua string for computada em tempo de execução, você precisa reservar espaço suficiente para mantê-la. A sala deve ser suficiente para manter o nulo no final:
      char *myString;
      myString = (char *) malloc(strlen(someString) + 1); // allocate space
      strcpy(myString, someString); // copy someString to myString 
    6. Para evitar estouro de memória, você deveria eventualmente retornar espaço que você aloca com malloc usando free. Seu parâmetro deve ser o início do espaço retornado pelo malloc:
      free((void *) myString);
      Para manter seu código limpo e legível, você deve chamar free() no mesmo procedimento no qual você chama malloc(); Você pode chamar outros procedimentos entre esses dois pontos para manipular sua string.
    7. Se você estiver copiando strings, você deve ter muito cuidado para nunca copiar mais bytes do que a estrutura de dados de destino pode conter. Os transbordamentos do buffer são a causa mais comum de falhas de segurança nos programas. Em particular, considere usar strncpy() e strncat() ao invés de strcpy() e strcat().
    8. Se você estiver usando C ++, você precisa converter seus objetos de string em strings no estilo C antes de passá-las em uma chamada de sistema.
      string myString // Esta declaração funciona apenas em C++
      ...
      someCall(myString.c_str())
      Infelizmente, c_str() retorna uma string imutável. Se você precisar de uma string mutável, você pode copiar os dados usando strcpy() (como acima) ou você pode lançar o tipo:
      someCall(const_cast<char *>(myString.c_str()))
      Casting não é tão segura quanto a cópia, porque someCall() pode realmente modificar a string, o que confundirá qualquer parte do programa que pressupõe que myString seja constante, o que é o comportamento usual das strings C++.
  3. Buffer é uma região de memória que atua como um contêiner para dados. Mesmo que os dados possam ter uma interpretação (como uma série de estruturas com vários campos), os programas que lêem e armazenam buffers normalmente os tratam como matrizes de bytes. Uma matriz de bytes não é o mesmo que uma string, mesmo que ambos sejam declarados como char * ou char [].
    1. Eles não podem conter caracteres ASCII e não podem ter terminação nula.
    2. Você não pode usar strlen() para encontrar o comprimento de dados em um buffer (porque o buffer pode conter bytes nulos). Em vez disso, você precisa descobrir o comprimento dos dados pelo valor de retorno da chamada do sistema (geralmente read) que gerou os dados.
    3. Você não pode usar strcpy(), strcat(), ou rotinas relacionadas em buffers de bytes; Em vez disso, você precisa usar memcpy() ou bcopy().
    4. Você escreve um buffer de 123 bytes para um arquivo usando um código como este:
      char *fileName = "/tmp/foo"
      #define BUFSIZE 4096
      char buf[BUFSIZE]; // buffer containing at most BUFSIZE bytes
      ...
      int outFile; // file descriptor, a small integer
      int bytesToWrite; // number of bytes still to be written
      char *outPtr = buf;
      ...
      if ((outFile = creat(fileName, 0660)) < 0) { // failure
      	// see permissões de arquivo to understand 0660
      	perror(fileName); // print cause
      	exit(1); // and exit
      }
      bytesToWrite = 123; // initialization; 123 is just an example
      while ((bytesWritten = write(outFile, outPtr, bytesToWrite)) < bytesToWrite) {
      	// not all bytes have been written yet
      	if (bytesWritten < 0) { // failure
      		perror("write");
      		exit(1);
      	}
      	outPtr += bytesWritten;
      	bytesToWrite -= bytesWritten;
      }
    5. Para fazer com que o compilador aloque espaço para buffers, você deve declarar o buffer com um tamanho que o compilador possa calcular, como em
      #define BUFSIZE 1024
      char buf[BUFSIZE];
      Se você apenas declarar o buffer sem tamanho:
      char buf[];
      então ele tem tamanho desconhecido e C não alocará nenhum espaço. Isso é aceitável se buf for um parâmetro formal (isto é, aparece em um cabeçalho de procedimento); o parâmetro real (fornecido pela chamada) tem um tamanho. Mas não é aceitável que buf seja uma variável. Se você não conhece o tamanho do buffer no tempo de compilação, você deve usar o código como este:
      char *buf = (char *) malloc(bufferSize);
      onde bufferSize é o resultado do tempo de execução de alguma computação.
  4. Você pode alocar e desalocar dinamicamente a memória.
    1. Instâncias individuais de qualquer tipo:
      typedef ... myType;
      myType *myVariable = (myType *) malloc(sizeof(myType));
      // you can now access *myVariable.
      ...
      free((void *) myVariable);
      Novamente, é uma boa prática de programação invocar free() na mesma rotina na qual você chama malloc().
    2. Arrays unidimensionais de qualquer tipo:
      myType *myArray = (myType *) malloc(arrayLength * sizeof(myType));
      // myArray[0] .. myArray[arrayLength - 1] are now allocated.
      ...
      free((void *) myArray);
    3. Arrays bidimensionais são representados por uma matriz de ponteiros, cada um aponta para uma matriz:
      myType **myArray = (myType **) malloc(numRows * sizeof(myType *));
      int rowIndex;
      for (rowIndex = 0; rowIndex < numRows; rowIndex += 1) {
      	myArray[rowIndex] = (myType *) malloc(numColumns * sizeof(myType));
      }
      // myArray[0][0] .. myArray[0][numColumns-1] .. myArray[numRows-1][numColumns-1]
      // are now allocated.  You might want to initialize them.
      ...
      for (rowIndex = 0; rowIndex < numRows; rowIndex += 1) {
      	free((void *) myArray[rowIndex]);
      }
      free((void *) myArray);
      
    4. e você estiver usando C ++, não mistur new/delete com malloc/free para a mesma estrutura de dados. A vantagem de new/delete para instâncias de classe é que elas chamam automaticamente construtores, que podem inicializar dados, e destrutores, que podem finalizar os dados. Quando você usa malloc/free, você deve inicializar e finalizar explicitamente.
  5. Inteiros
    1. C geralmente representa números inteiros em 4 bytes. Por exemplo, o número 254235 é representado como o número binário 00000000,00000011,11100001,00011011.
    2. Por outro lado, o texto ASCII representa números como qualquer outro caractere, com um byte por dígito usando uma codificação padrão. Em ASCII, o número 254235 é representado como 00110010, 00110101, 00110110, 00110010, 00110011, 00110101.
    3. Se você precisa escrever um arquivo de números inteiros, geralmente é mais eficiente tanto no espaço quanto no tempo escrever as versões de 4 bytes do que convertê-las em cadeias ASCII e gravá-las. Veja como escrever um único inteiro para um arquivo aberto:
      write(outFile, &myInteger, sizeof(myInteger))
    4. Você pode observar os bytes individuais de um número inteiro, lançando-o como uma estrutura de quatro bytes:
      int IPAddress; // stored as an integer, understood as 4 bytes
      typedef struct {
      	char byte1, byte2, byte3, byte4;
      } IPDetails_t;
      IPDetails_t *details = (IPDetails_t *) (&IPAddress);
      printf("byte 1 is %o, byte 2 is %o, byte 3 is %o, byte 4 is %o\n",
      	details->byte1, details->byte2, details->byte3, details->byte4);
    5. Os inteiros de vários bytes podem ser representados de forma diferente em máquinas diferentes. Algumas (como o Sun SparcStation) colocam o byte mais significativo primeiro; outras (como Intel i80x86 e seus descendentes) colocam primeiro o byte menos significativo. Se você estiver escrevendo dados inteiros que possam ser lidos em outras máquinas, converta os dados para a ordem de byte de “rede” por htons() ou htonl(). Se você estiver lendo dados inteiros que possam ter sido escritos em outras máquinas, converta os dados da ordem de "rede" para o sua ordem de bytes local por ntohs() ou ntohl().
  6. Você pode prever o layout da memória das estruturas e o valor que sizeof() retornará. Por exemplo,
    struct foo {
    	char a; // uses 1 byte
    		// C inserts a 3-byte pad here so b can start on a 4-byte boundary
    	int b; // uses 4 bytes
    	unsigned short c; // uses 2 bytes
    	unsigned char d[2]; // uses 2 bytes
    };
    Portanto, sizeof(struct foo) retorna 12. Esta previsibilidade (para uma determinada arquitetura) é o porquê de alguns chamam C de "linguagem de montagem portável". Você precisa prever o layout da estrutura ao gerar dados que devem seguir um formato específico, como um cabeçalho em um pacote de rede.
  7. Você pode declarar ponteiros para qualquer tipo e atribuir valores que apontem para objetos desse tipo.
    1. Em particular, C permite que você crie ponteiros para números inteiros:
      int someInteger;
      int *intPtr = &someInteger; // declares a pointer-valued variable and assigns an appropriate pointer value
      someCall(intPtr); // passes a pointer as an actual parameter
      someCall(&someInteger); // has the same effect as above
    2. Um procedimento de biblioteca C que leva um ponteiro para um valor provavelmente modifica esse valor (torna-se um parâmetro "out" ou "in out"). No exemplo acima, é muito provável que someCall modifique o valor do inteiro do someInteger.
    3. Você pode criar um ponteiro para uma matriz de inteiros e usá-lo para passar por esse array.
      #define ARRAY_LENGTH 100
      int intArray[ARRAY_LENGTH];
      int *intArrayPtr;
      ...
      int sum = 0;
      for (intArrayPtr = intArray; intArrayPtr < intArray+ARRAY_LENGTH; intArrayPtr += 1) {
      	sum += *intArrayPtr;
      } 
    4. Você pode criar um ponteiro para uma matriz de estruturas e usá-lo para percorrer esse array.
      #define ARRAY_LENGTH 100
      typedef struct {int foo, bar;} pair_t; // pair_t is a new type
      pair_t structArray[ARRAY_LENGTH]; // structArray is an array of ARRAY_LENGTH pair_t elements
      pair_t *structArrayPtr; // structArrayPtr points to a pair_t element
      ...
      int sum = 0;
      for (structArrayPtr = structArray; structArrayPtr < structArray+ARRAY_LENGTH; structArrayPtr += 1) {
      	sum += structArrayPtr->foo + structArrayPtr->bar;
      } 
    5. Quando você adiciona um número inteiro a um ponteiro, o ponteiro é avançado por muitos elementos, não importa quão grandes sejam os elementos. O compilador conhece o tamanho e faz o certo.
  8. Saída
    1. Você formata a saída com printf ou sua variante, fprintf.
    2. O formato string usa %d, %s, %f para indicar que um inteiro, uma string ou um real deve ser colocado na saída.
    3. O formato string usa \t e \n para indicar a guia e a nova linha
    4. Exemplo:
      printf("I think that the number %d is %s\n", 13, "lucky");
    5. Misturar printf(), fprintf(), e cout pode não imprimir elementos na ordem que você espera. Eles usam áreas de teste independentes ("buffers") que imprimem quando estão cheios.
  9. A rotina main() assume parâmetros de função que representam parâmetros de linha de comando.
    1. Uma maneira comum de escrever a rotina main é esta:
      int main(int argc; char *argv[]);
      Aqui, argc é o número de parâmetros, e argv é uma array de strings, ou seja, uma array de ponteiros para array de caracteres com terminação nula.
    2. Por convenção, o primeiro elemento de argv é o nome do próprio programa.
      int main(int argc; char *argv[]);
      printf("I have %d parameters; my name is %s, and my first parameter is %s\n", 
      	argc, argv[0], argv[1]); 
  10. Funções úteis da linguagem
    1. Você pode incrementar um número inteiro ou ter um ponteiro apontando para o próximo objeto usando o operador ++. Geralmente, é melhor colocar este operador após a variável: myInt++. Se você colocar o ++ antes da variável, a variável é incrementada antes de ser avaliada, o que raramente é o que você deseja.
    2. Você pode criar uma tarefa onde a variável do lado esquerdo participa como a primeira parte da expressão no lado direito:
      myInt -= 3; // equivalent to myInt = myInt - 3
      myInt *= 42; // equivalent to myInt = myInt * 42
      myInt += 1;  // equivalent to and maybe preferable to myInt++
    3. Você pode expressar números em decimal, octal (utilizando o digito 0, como prefixo, como em 0453), ou hexadecimal (utilizando 0x, como prefixo, como em 0xffaa).
    4. Você pode tratar um inteiro como um conjunto de bits e executar operações bit a bit:
      myInt = myInt | 0444; // bitwise OR; 0444 is in octal
      myInt &= 0444; // bitwise AND with an assignment shorthand
      myInt = something ^ whatever; // bitwise XOR
    5. C e C ++ têm expressões condicionais. Em vez de escrever
      if (a < 7)
      	a = someValue
      else
      	a = someOtherValue;
      voce pode escrever
      a = a < 7 ? someValue : someOtherValue;
    6. As atribuições retornam o valor do lado esquerdo, para que você possa incluir uma atribuição em expressões maiores, como condicionais. Mas você deve seguir a convenção de que tais atribuições estão sempre cercadas por parênteses para indicar tanto a alguém que lê seu código quanto ao compilador que você realmente quer dizer uma tarefa, e não um teste de igualdade. Por exemplo, escreva
      if ((s = socket(...)) == -1)
      não
      if (s = socket(...) == -1)
      A segunda versão é mais difícil de ler e, neste caso, incorreta, porque o operador de igualdade == tem maior precedência do que o operador de atribuição =.
  11. Os programas que não são trivialmente curtos geralmente devem ser decompostos em vários arquivos fonte, cada um com um nome que termina em .c (para programas em C) ou .cpp (para programas C++).
    1. Tente agrupar funções que manipulem as mesmas estruturas de dados ou tenham propósitos relacionados no mesmo arquivo.
    2. Todos os tipos, funções, variáveis globais e constantes de manifesto que são necessários por mais de um arquivo fonte também devem ser declarados em um header file, com um nome que termina em .h.
    3. Exceto para funções na linha, não declare o corpo das funções (ou qualquer coisa que faça com que o compilador gere código ou aloque espaço) no arquivo de cabeçalho.
    4. Cada arquivo fonte deve se referir aos arquivos de cabeçalho necessários com uma linha #include.
    5. Nunca #include um arquivo .c.
  12. Quando você tem vários arquivos fonte, você precisa linkar todos os arquivos de objetos compilados junto com as bibliotecas que seu programa precisa.
    1. O método mais fácil é usar o compilador C, que conhece as bibliotecas C:
      gcc *.o -o myProgram
      Este comando pede ao compilador que vincule todos os arquivos de objeto com a biblioteca C (o que está incluído implicitamente) e coloque o resultado no arquivo myProgram, que se torna executável.
    2. Se o seu programa precisar de outras bibliotecas, você deve especificá-las após seus arquivos de objeto, porque o vinculador apenas coleta rotinas de bibliotecas que já conhece e requer links e liga os arquivos na ordem que você especificou. Então, se você precisa de uma biblioteca, como libxml2, seu comando de chamada deve ser assim:
      gcc *.o -lxml2 -o myProgram
      O compilador sabe como pesquisar em vários diretórios padrão pela versão atual de libxml2.
  13. Depurando programas em C
    1. Se você tiver uma falha de segmentação, provavelmente você tem um índice fora do alcance, um ponteiro não inicializado ou um ponteiro nulo.
    2. Você pode colocar declarações impressas no seu programa para ajudá-lo a localizar um erro.
    3. A depuração provavelmente será mais bem sucedida se você usar o gdb (descritos abaixo) para descobrir onde seu erro está.
    4. Os programas que funcionam por um longo período de tempo devem ter o cuidado de liberar toda a memória que alocam ou, eventualmente, ficam sem memória. Para depurar vazamentos de memória, você pode considerar estes artigos sobre depuração de vazamentos de memória em C e C++.

Unix

arquivos padrão, comandos, chamadas de sistema, permissões de arquivo
  1. Por convenção, cada processo começa com três arquivos padrão abertos: entrada padrão, saída padrão e erro padrão, associados aos descritores de arquivos 0, 1 e 2.
    1. A entrada padrão geralmente está conectada ao seu teclado. Tudo o que você digita vai para o programa.
    2. A saída padrão geralmente está conectada à sua tela. Independentemente das saídas do programa, tornam-se visíveis.
    3. O erro padrão também está geralmente conectado à sua tela.
    4. Você pode usar o shell para invocar programas e fazer com que a saída padrão de um programa esteja diretamente vinculada à entrada padrão de outro programa:
      ls | wc
    5. Você pode usar o shell para invocar programas para que a entrada e/ou saída padrão esteja vinculada a um arquivo:
      ls > lsOutFile
      wc < lsOutFile
      sort -u < largeFile > sortedFile 
    6. Em geral, os programas não sabem ou se importam se o shell tiver rearranjado o significado de seus arquivos padrão.
  2. Comandos Unix
    1. Os comandos são apenas os nomes dos arquivos executáveis. A variável de ambiente PATH diz ao shell onde procurá-los. Normalmente, esta variável tem um valor como /bin:/usr/bin:/usr/local/bin:..
    2. Para ver onde o shell encontra um programa específico, por exemplo, vim, diz where vim.
  3. As Chamadas do sistema e as chamadas da biblioteca seguem algumas convenções importantes.
    1. O valor de retorno da chamada geralmente indica se a chamada conseguiu (geralmente o valor é 0 ou positivo) ou falhou (geralmente o valor é -1).
    2. Verifique sempre o valor de retorno das chamadas da biblioteca. Quando uma chamada do sistema falha, a função perror() pode imprimir o erro (para erro padrão):
      int fd;
      char *filename = "myfile";
      if ((fd = open(filename, O_RDONLY)) < 0) {
      	perror(filename); // might print "myfile: No such file or directory"
      }
    3. Uma página de manual para uma chamada de sistema ou uma rotina de biblioteca pode listar um tipo de dados que não define, como size_t ou time_t ou O_RDONLY. Esses tipos geralmente são definidos nos arquivos de cabeçalho mencionados na página do manual; Você precisa incluir todos esses arquivos de cabeçalho no seu programa C.
  4. As Permissões de arquivo Unix geralmente são expressas com números octais.
    1. No exemplo de creat() acima, 0660 é um número octal (é o que significa o 0 principal), representando 110,110,000 binário. Este número octal concede permissões de leitura e gravação, mas não executa permissões, ao proprietário do arquivo e ao grupo do arquivo, mas sem permissões para outros usuários.
    2. Você define permissões quando você cria um arquivo pelo parâmetro para a chamada creat().
    3. O comando ls -l mostra as permissões dos arquivos.
    4. Você pode alterar as permissões de um arquivo que possui usando o programa chmod.
    5. Todos os seus processos possuem uma característica chamada umask, geralmente representada como um número octal. Quando um processo cria um arquivo, os bits no umask são removidos das permissões especificadas na chamada creat(). Então, se o seu umask é 066, outros não podem ler ou escrever arquivos que você cria, porque 066 representa permissões de leitura e gravação para o seu grupo e para outras pessoas. Você pode inspecionar e modificar seu umask usando o programa umask, que você normalmente invoca no seu script de inicialização de shell (dependendo do seu shell, ~/.login ou ~/.profile).

Ferramentas de desenvolvimento de Software

editor de texto, depurador, compilador, páginas de manual, make, pesquisar,
  1. Use um editor de texto para criar, modificar e inspecionar seu programa. Existem vários editores de texto razoáveis disponíveis.
    1. O editor vim e sua interface gráfica, gvim, se esforçam para aprender, mas fornecem um conjunto de ferramentas de alta qualidade para edição de arquivos de programas, incluindo destaque de sintaxe, parênteses, conclusão de palavras, recuo automático, pesquisa por tag (que se move você rapidamente a partir de um local onde o programa chama uma função para o local onde a função está definida) e a busca integrada de páginas manuais. O Vim foi projetado para uso do teclado; você nunca precisa usar o mouse se não quiser. Está disponível gratuitamente para sistemas operacionais Unix, Win32 e Microsoft. É a versão mais desenvolvida da série editora que inclui ed, ex, vi, e elvis. Você pode ler a documentação on-line para vim para o vim e obter assistência imediata através do comando vim e :help.
    2. O editor emacs é, se qualquer coisa, mais carregado de recursos do que vim. Isso também requer um esforço significativo para aprender. Também está disponível gratuitamente para sistemas operacionais Unix e Microsoft. Você pode encontrar documentação aqui.
    3. Há muitos outros editores de texto disponíveis, mas geralmente eles não oferecem os dois recursos mais úteis que você precisa para criar programas: recuo automático e destaque de sintaxe. No entanto, esses editores de texto muitas vezes têm a vantagem de ser mais fáceis de aprender, de acordo com suas habilidades limitadas. Entre esses editores de texto de menor qualidade são (para Unix) pico, gedit, e joe e (para Microsoft) notepad e word.
    4. Você pode estar familiarizado com um ambiente de desenvolvimento integrado (IDE), como Eclipse, Code Warrior ou .NET. Esses ambientes geralmente possuem editores de texto que estão integrados com depuradores e compiladores. Se você estiver usando tal IDE, faz sentido usar os editores de texto associados.
  2. gdb é um depurador que entende suas variáveis e a estrutura do programa.
    1. Você pode encontrar documentação aqui.
    2. Para usar o gdb efetivamente, você precisa passar o sinalizador -g para o compilador C ou C++.
    3. Se o seu programa myProgram falhou, deixando um arquivo chamado core, tente core, gdb myProgram core.
    4. Você também pode executar seu programa desde o início sob o controle de gdb: gdb myProgram.
    5. Todos os comandos para gdb podem ser abreviados para um prefixo exclusivo.
    6. O comando help é muito útil.
    7. O comando where mostra a pilha de chamadas, incluindo números de linha mostrando onde cada rotina é. Esse é o primeiro comando que você deve tentar ao depurar um arquivo principal.
    8. Para imprimir o valor de alguma expressão (você pode incluir suas variáveis ​​e os operadores C habituais), digite print expression, como em
      print (myInt + 59) & 0444;
    9. Para ver o seu programa, tente list myFunction ou list myFile.c:38.
    10. Para configurar um registro de ativação diferente como atual, use o comando up (para mais recente) ou down (para menos recente).
    11. Você pode definir um ponto de interrupção em qualquer linha de qualquer arquivo. Por exemplo, você pode usar break foo.p:38 para definir um ponto de interrupção na linha 38 no arquivo foo.p. Toda vez que seu programa atinge essa linha enquanto executa, ele irá parar e gdb irá pedir-lhe comandos. Você pode observar variáveis, por exemplo, ou avançar através do programa.
    12. O comando next avança uma declaração (chamando e retornando de qualquer procedimento, se necessário).
    13. O comando step avança uma declaração, mas se a declaração envolve uma chamada de procedimento, ele entra no procedimento e pára na primeira declaração lá
    14. Se você inserir o comando set follow-fork-mode child, então, quando seu programa executa a chamada fork(), gdb continuará a depurar o filho, e não o pai.
    15. Deixe o gdb usando o comando quit.
    16. Você pode preferir usar a interface gráfica ddd para o gdb.
  3. Dê sempre aos programas de compilação gcc ou g++ a flag -Wall para ativar um alto nível de advertências. Da mesma forma, dê a javac o -Xlint:all. Não rode um programa que gera quaisquer avisos de compilação.
  4. Você pode ler o manual para obter detalhes sobre programas, rotinas da biblioteca C e chamadas do sistema Unix usando o programa man, como em man printf ou man gcc.
    1. Às vezes, a função que você deseja está localizada em uma seção específica do manual do Unix e você deve solicitar explicitamente: man 2 open ou man 3 printf. A seção 1 abrange os programas, a seção 2 cobre as chamadas do sistema e a seção 3 cobre a biblioteca C e a seção 8 cobre a administração do sistema. Você provavelmente não precisa das outras seções.
    2. Você pode encontrar se algum programa, a rotina da biblioteca C ou a chamada do sistema Unix forem relevantes para algum assunto usando a flag -k, como em man -k print.
  5. Use o programa make para organizar receitas para recompilar e vincular novamente seu programa quando você altera um arquivo de origem.
    1. Veja este tutorial ou este manual para mais detalhes.
    2. Se o seu programa é composto de vários arquivos, você pode compilá-los separadamente e, em seguida, vinculá-los. Você compila com a flag -c, e usa o -o para indicar o arquivo de saída. Um makefile razoável pode parecer assim:
      SOURCES = driver.c input.c output.c
      OBJECTS = driver.o input.o output.o
      HEADERS = common.h
      CFLAGS = -g -Wall
      
      program: $(OBJECTS)
      	$(CC) $(CFLAGS) $(OBJECTS) -o program
      
      $(OBJECTS): $(HEADERS)
      	
      testRun: program
      	program < testData
      Este makefile usa uma definição interna de CC e uma regra interna para converter arquivos de origem C como driver.c em seu arquivo de objeto. Se você modificar apenas input.c, então make testRun fará com que o compilador reconstrua o input.o, e então fazer com que o compilador vincule os objetos, crie o program, e depois execute program com a entrada padrão redirecionada do arquivo testData.
    3. Se você tiver muitos arquivos de origem e muitos arquivos de cabeçalho, você pode querer usar o programa makedepend para criar automaticamente as regras Makefile que especificam como os arquivos de origem dependem de arquivos de cabeçalho. O exemplo acima pressupõe que todos os arquivos de origem dependem de todos os arquivos de cabeçalho, o que geralmente não é o caso.
  6. O programa grep pode procurar rapidamente uma definição ou variável, particularmente nos arquivos de inclusão:
    grep "struct timeval {" /usr/include/*/*.h

Exercícios

Faça estes exercícios em C.
  1. Escreva um programa chamado atoi que abra um arquivo de dados nomeado na linha de comando e lê dele uma única linha de entrada, que deve conter um número inteiro representado em caracteres ASCII. O programa converte essa string em um inteiro, multiplica o número inteiro em 3 e imprime o resultado como padrão. O programa não deve usar a função atoi(). Você deve usar o programa make. Seu Makefile deve ter três regras: atoi, run (que executa seu programa em seus dados de teste padrão e redireciona a saída para um novo arquivo) e clean (que remove arquivos temporários). Verifique se o seu programa roda corretamente em dados ruins e termiana com uma mensagem útil se o arquivo de dados estiver ausente ou ilegível. Percorra seu programa iniciando-o com o gdb, colocando um ponto de interrupção no main(), e usando o comando step repetidamente.
  2. Procure a página do manual para o programa cat. Codifique sua própria versão do cat. Sua versão deve aceitar vários (ou nenhum) parâmetro de nome de arquivo. Não é necessário aceitar nenhum parâmetro de opção.
  3. Escreva um programa removeSuffix que leve um único parâmetro: o nome de um arquivo de sufixo. O arquivo suffix tem uma linha por entrada. Uma entry é uma sequência não vazia, que chamamos de suffix, seguido pelo sinal >, seguido de outra string (possivelmente vazia), que chamamos de substituição. Seu programa deve armazenar todos os sufixos e suas substituições em uma tabela hash. Use encadeamento externo. Seu programa deve então ler a entrada padrão. Para cada palavraw delimitada no espaço na entrada, encontre o sufixo mais longo s que aparece em w, e modifique w removendo s e inserindo a substituição do s, criando w'. Produza uma linha por palavra modificada, na forma w>w'. Não imprima nenhuma palavra que não seja modificada.