terça-feira, 30 de dezembro de 2014

Apostila de Linguagem C

Atendendo a inúmeros pedidos estou disponibilizando uma versão PDF da minha apostila de C (não é C++). Este material foi utilizado por muitos anos nas minhas disciplinas de Programação no Curso de Ciência da Computação da Universidade de Passo Fundo.

Como, desde 2009, substituímos a linguagem inicial de programação por C++ não tenho investido tempo neste documento. Recentemente efetuei apenas uma breve atualização para o C99. De qualquer forma espero que seja útil para todos que procuram um material básico sobre a linguagem, seja para consulta própria ou mesmo para utilizar como recurso didático em outros cursos/instituições.




Obs: As condições da licença de uso estão apresentadas na segunda página do documento.

quarta-feira, 17 de dezembro de 2014

Faça como eu faço, não como eu digo (parte 1)

O título deste post iria ser "Coisas que te ensinamos errado", mas o fato é que não foi ensinado necessariamente errado, só não foi da melhor forma. Em função da heterogeneidade da turma, da limitação de tempo e da preocupação com que os alunos consigam resolver os problemas propostos, mesmo que não da melhor forma, os professores (eu, pelo menos) muitas vezes não codificam seus exemplos como eles próprios fariam ou recomendariam.

Primeiro caso


O que há de ruim na função abaixo? A maioria dos meus alunos devem ter visto este exemplo, ou semelhante:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// Retorna a quantidade de dias que um mes tem
int diasmes(int mes, int ano)
{
    int dias[]= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    if((ano%4==0 && ano%100!=0) || ano%400==0)  // É bissexto
        dias[1] = 29;                      // Fevereiro tem 29 dias

    return dias[mes-1];
}

Obs: Como o assunto se aplica tanto ao C como C++ não estou usando nada específico do C++11, como a sintaxe uniforme de inicialização.

Quem é da velha guarda, e como eu aprendeu a programar C com o compilador cc do Unix sabe que o problema está na linha 4. O cc que usei era um compilador pré-ANSI e daria erro de compilação com este código. Qual o problema? Inicialização de arrays automaticamente alocados.

Quando se define e inicializa uma variável automática (local não static) como n abaixo, ela será alocada em registrador ou na pilha a cada vez que a função for executada. Após isto, também a cada execução, o valor será copiado para este armazenamento.


1
2
3
void fn(){
   int n=0;
   // ...

Nada errado com uma variável de tipo fundamental como esta aí. Mas e com um array, como aquele do exemplo anterior? Arrays não podem ser armazenados em registrador. Irão ficar na memória, no segmento de pilha (stack). Mas o problema não é este, pois alocar espaço na pilha para um vetor usa uma única operação que subtrai o tamanho do vetor do Stack Pointer. Mas como ele é inicializado? Normalmente o compilador cria um vetor idêntico estático, e a cada entrada na função copia os dados daquele array para a área alocada.

Então, quando você escreve aquela função acima, é como se tivesse feito isto:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int __s129ax334[]= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

int diasmes(int mes, int ano)
{
    int dias[12], i;
    for(i=0; i<12; ++i)
        dias[i] = __s129ax334[i];

    if((ano%4==0 && ano%100!=0) || ano%400==0)  // É bissexto
        dias[1] = 29;                      // Fevereiro tem 29 dias

    return dias[mes-1];
}

Veja o for nas linhas 6 e 7. E pense que esta função pode estar sendo chamada N vezes durante a execução do programa. Está aí o problema!

A prova


Para os que só acreditam vendo, segue o assembly x86 gerado pelo GCC 4.7.1 32 bits, compilando com flag de otimização -O2. Algumas diretivas e instruções não relacionadas foram omitidas para facilitar a visualização. Se você não está afim de encarar código assembly, então jmp solucao.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 .data
LC0:
 .long 31
 .long 28
 .long 31
 .long 30
 .long 31
 .long 30
 .long 31
 .long 31
 .long 30
 .long 31
 .long 30
 .long 31

 .text
_diasmes:
 ; ...
 sub esp, 48               ; Aloca espaco na pilha (12*4 bytes)
 mov edi, esp              ; Endereço destino (dias[])
 mov esi, OFFSET FLAT:LC0  ; Endereço origem (LC0)
 mov ecx, 12               ; Quantidade de palavras a serem copiadas
 rep movsd                 ; Repete 12 copias de inteiros 

Lá está o array estático LC0  no segmento de dados (linhas 2 a 14), a alocação do espaço na pilha (linha 19) e a cópia do array (linhas 20 a 23) com a instrução movsd que copia uma sequencia de inteiros do endereço apontado por esi (source index) para o endereço apontado por edi (destination index). O laço está implícito no prefixo rep, que repete a próxima instrução a quantidade de vezes especificada no ecx.

Solução


Antes de alguém aí pensar "Então é melhor eu mesmo definir o array global e dispensar a cópia" eu já adianto: O melhor, neste caso é deixá-lo local mas estático (static). Neste caso você mantém a visibilidade local, o que muito bom, mas o array é alocado estaticamente no segmento de dados, já com seus valores. Ou seja, quando o loader do SO carregar o binário para a memória, já estará pronto.

Aproveito para mais uma otimização secundária: como o maior valor do array é 31, não existe necessidade de ser int, que normalmente teria quatro bytes por valor. Pode muito bem ser um unsigned char, que ocupará apenas um byte por valor, na maioria das arquiteturas, e suporta um intervalo de valores de 0 a 255.

1
2
3
4
5
6
7
8
9
int diasmes(int mes, int ano)
{
    static unsigned char dias[]= {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
    dias[1] = ((ano%4==0 && ano%100!=0) || ano%400==0)? 29: 28;             
    return dias[mes-1];
}


Outras situações equivalentes


Agora lembre que uma string C também é um array (de char), então aqui temos novamente a mesma questão. Se for uma string C++ também terá um objeto sendo construído e a sequencia de caracteres copiados a cada execução.

1
2
char vogais[]="aeiou";
string nome="fulano";

Obs: Aí já aparece para outro episódio desta série que é a inicialização de uma std::string com o sinal de atribuição (=).

Quando não usar esta otimização?


Você pode ter problemas com isto quando estiver mudando os valores do vetor no corpo da função ou em outra função chamada que recebe o array por referência não constante e você quer descartar estas alteração a cada execução, garantindo que os valores originais sempre sejam encontrados, pois os valores estáticos são preservados entre chamadas da função. No exemplo acima, a única modificação é no segundo elemento do array, correspondente ao mês de fevereiro, então vale a pena. Melhor uma atribuição do que 12. Mas perceba que precisei (re)atribuir 28 para os anos não bissextos.

Se o array local está sendo inicializado e não é alterado, além de static defina-o constante (const).

Perceba que o problema está na inicialização, não na alocação. Então se o array não estiver sendo inicializado provavelmente é melhor que não seja estático.

Mas vale a pena?


Aqui nem é o caso de "tanto esforço por tão pouco ganho" pois o esforço é tão pequeno que deve valer a pena. Quanto mais a função for executada, maior o ganho de desempenho.

Eu tenho a opinião de que, com o atual desempenho e disponibilidade de memória nos computadores, podemos abdicar de algumas estratégias que levariam a um programa mais eficiente se isto resultar em código mais legível e manutenível. Não acho que este seja o caso.

terça-feira, 16 de dezembro de 2014

E este tal de C++11?


Muito do conteúdo que vou postar aqui baseia-se nos recursos do C++11, então é natural começar falando um pouco desta nova versão da linguagem. O C++ é uma linguagem cuja evolução não é decidida por uma empresa, mas sim de forma democrática por um comitê internacional. Por estes e outros motivos, a evolução da linguagem é lenta.

Quando o C++ foi criado, no início dos anos 80 por Bjarne Stroustrup, o padrão de facto da linguagem era o seu livro "The C++ Programming Language". A partir dos anos 90 houve a preocupação em padronizar formalmente e internacionalmente a linguagem. Assim surgiram os padrões ISO/IEC para as versões C++98 (1998), C++03 (2003) e mais recentemente o C++11 (foi chamado por muito tempo de C++0x pois se esperava estar concluído ainda nos anos 200x). Este é atual padrão da linguagem C++, aprovado pelo comitê em agosto de 2011 e publicado pela ISO em set/2011 (ISO/IEC 14882:2011). Agora em 2014 foi feita uma revisão (informalmente C++14), já aprovada pelo comitê mas ainda não publicada até a presente data. A próxima grande atualização prevista é o C++17.

Onde conhecer C++11?

O documento oficial da versão atual da linguagem é o ISO/IEC 14882:2011. Como acontece com normas ISO, o documento é pago. A ABNT vende no Brasil por R$ 666,40. O draft final redigido pelo comitê está gratuitamente disponível em http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3376.pdf.
Deve ser o mesmo conteúdo da versão paga (não a garantia disto), mas sem formatação. De qualquer forma, não é um documento para estudar ou aprender (a não ser como estudo de caso para uma cadeira de Linguagens Formais ou Compiladores!). Também não é um documento que apresente apenas as diferenças do C++11 para a versão anterior. Este é o atual standard, então toda a linguagem está descrita.

Principais fontes de pesquisa que eu usei/uso para pesquisa (random_shuffle'd):
  • A FAQ do Stroustrup;
  • O site cplusplus.com, aliás, minha principal referência online para a biblioteca padrão;
  • A edição atual do The C++ Programming Language. Ganhei um exemplar do meu aluno Natan Streppel. Valeu Natan! O livro é D+;
  • O Google, como todo mundo, né!

Quais são as diferenças do C++03 para o C++11?

Um único post não é espaço adequado para relacionar, sem ser muito superficial, a evolução que ocorreu. Vamos fazer isto em doses homeopáticas. Quem está ansioso olha na FAQ do Stroustrup.
Mas aí vai um pequeno exemplo, que disponibilizei no Ideone.com, e que ilustra algumas poucas novidades :


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <algorithm>
#include <iterator>

using namespace std;

int main()
{
    int a[] {10, 48, 57, 23, -1, 12};

    cout << "Is sorted? "
         << boolalpha
         << is_sorted(begin(a), end(a))
         << endl;

    sort(begin(a), end(a));
    for(auto v : a)
        cout << v << '\n';

    return 0;
}

O que aparece de novo aí?

Vale a pena usar c++11?

Certamente. O principal ganho é em produtividade. Algumas coisas simples,  como o range-for, que a maioria das linguagens já tinha e faltava no C++; a inferência de tipo; as expressões lambda; o alinhamento com o C99 e as novas funções e classes da biblioteca padrão (Lista simplesmente encadeada, tabela hash, classes mais aprimoradas para medir tempo  do que a velha <time.h>, regex, geradores de números pseudo-aleatórios, para citar alguns exemplos) são muito úteis mesmo para um programador C++ básico/intermediário. Parte significativa da mudança será mais interessante para um programador que faça uso da Programação Orientada a Objetos e/ou Meta-programação por Templates mais a fundo.

Vale a pena migrar meu código para c++11?

Quem aí tem tempo para isto???? Não vejo necessidade em sair convertendo código legado.  Só a (re)compilação, em alguns casos específicos, já pode dar algum ganho de desempenho. Principalmente onde a nova Move semantic, pode permitir ao RVO (Return value optimization) eliminar cópias desnecessárias de grandes objetos como em uma função que retorna um container (um list<T>, por exemplo).

A única quebra de compatibilidade que identifiquei foi a mudança de semântica da palavra reservada auto. Antes era um modificador de tipo que indicava que a variável teria alocação automática, isto é deveria ser armazenada na pilha. Agora serve para inferência de tipo (o compilador irá detectar o tipo para a variável com base no valor usado para inicializá-la). Então este código, que antes era válido, agora gera erro de compilação.


1
auto int x=0;

Como compilar usando C++11?

Na linha de comando, basta incluir a flag -std=c++11 na chamada ao compilador g++

g++ -std=c++11 programa.cpp -o programa

No Code::Blocks, basta ligar esta opção no menu Settings/Compiler/Compiler Settings:



Experimentem, explorem, divirtam-se. É assim que se deve (re)aprender programação!

sábado, 13 de dezembro de 2014

Hello world!

Bem vindos!
Depois de mais de duas décadas de experiência em desenvolvimento de software em inúmeras linguagens de programação e muitos anos de docência no ensino superior em disciplinas que envolvem programação de computadores, resolvi criar um  blog.

Os motivo são diversos:
  • Adoro programar e aprender programação. Gostaria de incentivar aos outros o mesmo interesse.
  • O alcance de um post na Web é muito maior do que de uma sala de aula. A grande maioria daquilo que eu conheço hoje aprendi pesquisando na Web!
  • Acredito que todo o conhecimento que adquirimos na vida só é útil em duas situações: 
    • Quando utilizamos na prática para melhorar nossa vida e a das outras pessoas;
    • Quando repassamos para que os outros possam fazer o mesmo.
O perfil do conteúdo deste blog será do intermediário ao avançado do C++. Não vou ficar postando aquilo que qualquer um já aprendeu, ou pode aprender, em alguma disciplina ou apostila introdutória. A linguagem C++ é muito poderosa, mas repleta de "esconderijos" e "meandros". São nestes recursos que eu quero me aventurar.

Quero dar espaço também à outras tecnologias que domino, como C# e javascript. Mas o foco é C++ mesmo!

Sejam bem-vindos. Espero que meus post sejam úteis!