Só pra lembrar que a categoria Extras contém assuntos variados que complementam a categoria Obrigações.
A categoria Obrigações aborda os trabalhos passados em aula.
Só pra lembrar que a categoria Extras contém assuntos variados que complementam a categoria Obrigações.
A categoria Obrigações aborda os trabalhos passados em aula.
MRAM: Um novo tipo de memória
As tecnologias de memória RAM que existem atualmente no mercado, podem ser classificadas basicamente em memórias voláteis e memórias não voláteis.
As memórias SDRAM, EDO e FPM que utilizamos nos micros domésticos são memórias voláteis, que permitem um acesso a dados relativamente rápido, são baratas, mas em compensação perdem todos os dados quando o micro é desligado. Outro problema é que este tipo de memória consome muita eletricidade, um módulo de 64 MB de memória PC-100 por exemplo, consome mais de 5 Watts. Estes dois fatores somados impedem que este tipo de memória seja usada um micros de mão, celulares, etc., apenas em micros de mesa e notebooks.
Em seguida, temos as memórias SRAM, ou Static RAM, as memórias que encontramos na forma de memória cache. A SRAM é um tipo de memória ultra-rápida, que traz a vantagem adicional de não consumir tanta energia, sendo muito utilizada em micros de mão (os Palms por exemplo), agendas eletrônicas, celulares, etc. O grande problema das SRAM é que elas são incrivelmente caras.
Como terceira opção, temos as memórias Flash. Elas não são tão rápidas quanto as SRAM, e também não são nem um pouco baratas, muito pelo contrário, também são caríssimas, mas em compensação trazem a vantagem de armazenar os dados por tempo indeterminado, sem a necessidade de serem alimentadas eletricamente. Isso explica o seu uso em cartões de memória por exemplo.
Porém, a IBM anunciou a criação de mais uma tecnologia de memória, a MRAM, ou Magnetc RAM. A idéia fundamental é ler e gravar dados de forma magnética e não usando eletricidade como nas tecnologias atuais. As vantagens, segundo a IBM, seriam um consumo elétrico muito menor, uma velocidade de acesso mais alta, e principalmente, o fato das memórias conservarem os dados gravados, assim como nas memórias Flash.
Até o momento, a IBM tem apenas protótipos, mas planeja lançar os novos módulos no mercado nos próximos 4 anos.
Caso a IBM consiga cumprir suas promessas, as memórias MRAM trarão uma verdadeira revolução no ramo de memórias, principalmente no ramo de portáteis, que poderão substituir as caríssimas memórias SRAM e Flash por um tipo de memória muito mais barato. Teremos então aparelhos com muito mais memória, com uma autonomia de baterias bem superior, e principalmente, bem mais baratos. São justamente os preços exorbitantes das memórias SRAM e Flash que fazem os Palms, celulares e agendas eletrônicas virem com tão pouca memória.
Nos desktops, as memórias MRAM também podem ser muito úteis, pois por não perderem os dados, permitem que tanto a inicialização quanto o desligamento dos micros seja instantâneo, já que não é preciso ler os salvar dados no disco rígido cada vez que o micro for ligado ou desligado, eles sempre estarão na memória. Isso fora o fato de economizarmos bastante na conta de luz.
Veja que iniciativas como a MRAM e o Crusoé, que comentei no artigo de ontem, estão pavimentando o caminho para micros portáteis e celulares cada vez mais poderosos. Ao invés de costurar baterias na roupa, criar mini reatores nucleares e outras loucuras que andam divulgando por aí, estes dispositivos seguem um caminho muito mais lógico, que é simplesmente gastar menos eletricidade, combinando poder de processamento com baixo consumo.
RAID 10
Um dos grandes atrativos do RAID é a possibilidade de escolher entre diferentes modos de operação, de acordo com a relação capacidade/desempenho/confiabilidade que você pretende atingir.
O RAID 10 (Mirror/Strip) é um modo que pode ser usado apenas caso você tenha a partir de 4 discos rígidos e o módulo total seja um número par (6, 8, etc.). Neste modo, metade dos HDs serão usados em modo striping (RAID 0), enquanto a segunda metade armazena uma cópia dos dados dos primeiros, assegurando a segurança.
Este modo é na verdade uma combinação do RAID 0 e RAID 1, daí o nome. O ponto fraco é que você sacrifica metade da capacidade total. Usando 4 HDs de 500 GB, por exemplo, você fica com apenas 1 TB de espaço disponível. No RAID 10 você obtém o dobro de desempenho que em um HD sozinho, mas sem abrir mão da segurança.
Memória bolha
Memória Bolha é um tipo de memória de computador de aramazenamento não-volátil que usa um filme de material magnético de pequena espessura para prender pequenas áreas magnetizadas reconhecidas como bolhas, que armazenam um bit de dados. Memória bolha surgiu no início dos anos 1970 como uma promessa de tecnologia, mas falhou comercialmente devido à rápida queda dos preços dos discos rígidos no início dos anos 1980.
Mémoria GDDR
Memórias RAM também são usadas em placas de vídeo, para formar o circuito de memória de vídeo. Até muito recentemente, a memória de vídeo usava exatamente a mesma tecnologia da memória RAMque é instalada na placa-mãe.
Placas de vídeo de alto desempenho, no entanto, estavam precisando de memórias mais rápidas do que as usadas convencionalmente no PC.
Com isso optou-se por usar memórias com as tecnologias DDR2 e DDR3.
Só que as memórias DDR2 e DDR3 usadas em placas de vídeo têm características diferentes das memórias DDR2 e DDR3 usadas no PC – especialmente a tensão de alimentação.
Por este motivo é que elas são chamadas GDDR2 e GDDR3 (o “G” vem de “Gráfica”)
maiores detalhes: http://pt.wikipedia.org/wiki/GDDR
Memórias Regulares
As memórias regulares são o tipo mais primitivo de memória RAM. Nelas, o acesso é feito da forma tradicional, enviando o endereço RAS, depois o CAS e aguardando a leitura dos dados para cada ciclo de leitura.
Isto funcionava bem nos micros XT e 286, onde o clock do processador era muito baixo, de forma que a memória RAM era capaz de funcionar de forma sincronizada com ele. Em um 286 de 8 MHz, eram,usados chips com tempo de acesso de 125 ns (nanossegundos) e em um de 12 MHz eram usados chips de 83 ns.
O problema era que a partir daí as memórias da época atingiram seu limite e passou a ser necessário fazer com que a memória trabalhasse de forma assíncrona, onde o processador trabalha a uma freqüência mais alta que a memória RAM.
A partir do 386, a diferença passou a ser muito grande, de forma que as placas mãe passaram a trazer chips de memória cache, dando início à corrida que conhecemos.
Intel Expande Oferta de Memória Flash
Boa matéria que fala sobre o soluções que a Intel implementa nos telefones celulares, como o uso da memória PSRAM.
link: http://www.intel.com/portugues/technology/magazine/archive/2006/jan/revista0106_6.pdf
Os próximos tópicos são complementos para o conhecimento da memória NVRAM.
Benefícios da NVRAM
Bad NVRAM
Quando NVRAM está a falhar, é geralmente significa que o seu computador hardware não está retendo os ajustes necessários especializados que deveria embora o padrão permanecer configurações de BIOS. Uma vez que o BIOS conta com as configurações armazenadas no NVRAM, a fim de lidar com o hardware que você tem particular, o desempenho pode faltar na estabilidade. O conteúdo da NVRAM chip pode tornar-se corrompido por uma variedade de razões:
Quando você receber uma mensagem de erro sobre o seu NVRAM
A memória pode receber e armazenar informação, assim como fornecer a mesma. A memória tem diminutos condensadores que conseguem reter as informações recebidas e reenvia-las. A memória é capaz de conservar, acessar de forma rápida e aleatória, qualquer informação. Qualquer microcomputador contém um mínimo de memória para seu funcionamento, seja ela principal ou secundária.
Em um microcomputador existem vários tipos de memória principal. Há duas classes principais quanto ao poder de gravação dos dados:
Memória ROM – ROM é a sigla para Read Only Memory (memória somente de leitura). Já pelo nome, é possível perceber que esse tipo de memória só permite leitura, ou seja, suas informações são gravadas pelo fabricante uma única vez e após isso não podem ser alteradas ou apagadas, somente acessadas. Em outras palavras, são memórias cujo conteúdo é gravado permanentemente.
Memória RAM – RAM é a sigla para Random Access Memory (memória de acesso aleatório). Este tipo de memória permite tanto a leitura como a gravação e regravação de dados. No entanto, assim que elas deixam de ser alimentadas eletricamente, ou seja, quando o usuário desliga o computador, a memória RAM perde todos os seus dados.
A seguir, listaremos alguns tipos de memórias ROM e RAM com algumas definições e características inerentes a cada uma.
EPROM
No processo de evolução das memórias, passaram a ser necessárias memórias que permitissem a regravação e continuassem sendo ROMs depois disto. Surgiram então as memórias EPROM (Erasable programmable read only memory). Significa que pode gravar dados, apagar dados e depois disto ela é utilizada como memória somente para leitura.
As memórias EPROM utilizam a tecnologia opto – elétrica. Os dados são gravados por meio de tensão e corrente elétrica e apagados por meio de luz ultravioleta.
As pastilhas de memórias EPROM vêm com uma janela de cristal que permite visualizar a matriz de elementos que compõem a memória. Aplicando radiação ultravioleta pela janela, ela atinge todas as locações de memória, apagando seus conteúdos.
As memórias EPROM têm que ser apagadas totalmente, não sendo possível apagar apenas um bit ou endereço e deixar todo o resto como estava.
Após a gravação de uma EPROM é aconselhável cobrir a janela de cristal com uma etiqueta adesiva metalizada, para evitar que radiação ultravioleta entre acidentalmente e danifique as informações nela contidas.
EEPROM
Com as memórias EEPROM (Electrically erasable programmable read only memory) é possível gravar e apagar dados com sinais elétricos, não sendo mais necessária a radiação ultravioleta. Podemos apagar somente 1 bit, se for o caso, e deixar o resto como estava.
Também conhecida como E2PROM, E2PROM e E2PROM.
Vantagens: É possível gravar, apagar e regravar utilizando apenas sinais elétricos.
Desvantagens: Consome muita energia e tem poucos endereços de memória, comparado com a EPROM.
São aplicáveis em sistemas eletrônicos que precisam de setup, agendas eletrônicas, câmeras fotográficas, etc.
OTP-ROM (One-time programmable read only memory)
• As conexões são programadas pelo usuário
– Usuário define o arquivo especificando o conteúdo da ROM
– Arquivo é inserido na máquina denominada ROM programmer
– Cada conexão programável corresponde a um ponto fundido
– ROM programmer rompe os pontos fundidos onde a conexão não deve existir
• Habilidade de escrita muito baixa
– Escrita uma única vez; necessita do dispositivo de programação da ROM
• Retenção de informações muito alta
– Os bits armazenados permanecem, a menos que o programador seja usado para romper mais conexões
• Comumente utilizado em produtos finais
– Barato, dificilmente é modificado acidentalmente
SRAM = Static Random Access Memory
É uma memória na qual as “células” que armazenam os bits são compostas de arranjos de 4 a 6 transistores, numa estrutura chamada “flip-flop”.
Esse tipo de arranjo é extremamente rápido e mantém a informação armazenada enquanto houver energia alimentando a memória.
A desvantagem desse tipo de memória é que ela consome de 4 a 6 transistores, ou seja, demanda uma área de silício considerável; por isso, a capacidade de armazenamento é comparativamente menor em relação às memórias DRAM (Dynamic Random Access Memory).
As DRAMs usam apenas um transistor para armazenar os bits, permitindo um aproveitamento excelente do silício. No entanto, sua estrutura simples requer o uso de complicados procedimentos de “refresh”, destinados a não deixar que as células percam a informação, que de outra forma, em poucos milissegundos se perderia.
As memórias SRAM são geralmente usadas na função de memória cache, para armazenar as informações mais frequentemente utilizadas de forma a poderem ser “chamadas” mais rapidamente. Também por não precisarem de sistemas de refresh, as SRAMs consomem muito pouca energia, sendo especialmente viáveis em aparelhos portáteis alimentados a bateria.
DRAM
A Memória DRAM (Dinamic Ram, Ram dinâmica), é o tipo de memória mais empregada atualmente, pelo fato de guardar uma grande quantidade de bits, e serem relativamente baratas. A memória DRAM é a principal memória do Pc, ela retém a informação através diminutos condutores. Uma desvantagem das memórias DRAM é precisar que os diminutos sejam recarregados constantemente, para que não se perca seus dados, e com isso consome vários ciclos do processador.
PSRAM
A especificação PSRAM (pseudo static random access memory), que é similar a memória SRAM (static RAM) e tem um estrutura semelhante ao DRAM (dynamic RAM), trabalha mais rápida e consome menos energia que outros tipos de memória em aparelhos móveis.
A PSRAM também permite velocidades mais altas para a transferência de dados e ajuda os desenvolvedores a incluir mais funcionalidades nos telefones 3G.
NVRAM
NVRAM é um acrônimo para a não-volátil Random Access Memory. NVRAM é um tipo de Random Access Memory (RAM) que mantém a sua informação quando o poder está desligado. A NVRAM é um pequeno 24 pinos DIP (Dual Inline Package) chip circuito integrado, sendo, portanto, capaz de obter o poder necessário para mantê-lo correr a partir do CMOS bateria instalada em sua placa mãe. Ela mantém controle de diversos parâmetros de sistema, tais como número de série, Ethernet MAC (Media Access Control) endereço, HOSTID, data de fabricação, etc NVRAM é, portanto, um tipo de memória não-volátil que oferece acesso aleatório.
Mutable, O legado
Continuando nosso artigo mutable, que raios é isso? agora irei demonstrar um segunda maneira de “burlar” o mesmo método const sem usar atributos mutáveis (mutable).
Temos a seguinte interface de exemplo, Entity (pure abstract)
class Entity
{
public:
virtual void logic() = 0;
virtual void draw() const = 0;
};
Note que classes como a acima são muito comuns em vários projetos
Agora a classe Player que implementa Entity
class Player : public Entity
{
public:
void logic()
{
}
void draw() const
{
mDrawCount++;
// Erro! pois é um método const
}
private:
std::size_t mDrawCount;
};
Mais skhaz porque não controlar isso no método logic que não é const? eu tenho bons motivos para isso, um deles é se você implementar um sistema de ticks como descrito no artigo Controle de velocidade de jogo com ticks onde quase sempre o método draw não será chamado no mesmo ciclo que logic.
ponteiro e const_cast os “vilões”
Com o uso do cast const_cast podemos burlar o atributo declarado como mutable (assim como para const e volatile) para não constantes.
void Player::draw() const
{
const_cast<player *>(this)->mDrawCount++;
// work fine =D
}
Existe coisa mais prazerosa do que admitir um erro que foi cometido na mesma semana? Existe: quando você sabia que estava certo, mas resolveu usar o senso comum por falta de provas.
Pois bem. O mesmo amigo que me recomendou que escrevesse sobre o assunto do ponteiro nulo achou um livro sobre armadilhas em C com um exemplo que demonstra exatamente o contrário: dependendo da plataforma, ponteiros nulos são sim válidos.
Nesse caso, se tratava de um programa que iria rodar em um microprocessador, daqueles que o DQ costuma programar. Pois bem. Quando o dito cujo ligava era necessário chamar uma rotina que estava localizada exatamente no endereço 0. Para fazer isso, o código era o seguinte:
( * (void(*)()) 0 ) ();
Nada mais simples: um cast do endereço 0 (apesar de normalmente inválido, 0 pode ser convertido para endereço) para ponteiro de função que não recebe parâmetros e não retorna nada, seguido de deferência (“o apontado de”) e chamada (a dupla final de parênteses).
(* (void(*)()) 0 ) ();
É bem o que o autor diz depois de jogar esta expressão: “expressions like these strike terror into the hearts of C programmers”. É lógico que isso não é bem verdade para as pessoas que acompanham este blogue =)
Bom, parece que o “mother-fucker” wordpress ferrou com meu artigo sobre o Houaiss. Enquanto eu choro as pitangas aqui vai um outro artigo um pouco mais simples, mas igualmente interessante.
“Wanderley, tenho umas sugestões para teu blog.
A primeira:
Que tal analisar o código abaixo e dizer se compila ou não. Se não compilar, explicar porquê não compila. Se compilar, o que acontecerá e por quê.”
O código é o que veremos abaixo:
#include <stdio.h> #include <stdlib.h> void func() { *(int *)0 = 0; return 0; } int main(int argc, char **argv) { func(); return 0; }
Bem, para testar a compilação basta compilar. Porém, se estivermos em uma entrevista, geralmente não existe nenhum compilador em um raio de uma sala de reunião senão seu próprio cérebro.
E é nessas horas que os entrevistadores testam se você tem um bom cérebro ou um bom currículo.
Por isso, vamos analisar passo a passo cada bloco de código e entender o que pode estar errado. Se não encontrarmos, iremos supor que está tudo certo.
#include <stdio.h> #include <stdlib.h>
Dois includes padrões, ultranormal, nada de errado aqui.
void func() { *(int *)0 = 0; return 0; }
Duas ressalvas aqui: a primeira quanto ao retorno da função é void, porém a função retorna um inteiro. Na linguagem C, isso funciona, no máximo um warning do compilador. Em C++, isso é erro brabo de tipagem.
A segunda ressalva diz respeito à linha obscura, sintaticamente correta, mas cuja semântica iremos guardar para o final, já que ainda falta o main para analisar.
int main(int argc, char **argv)
{
func();
return 0;
}
A clássica função inicial, nada de mais aqui. Retorna um int, e de fato retorn. Chama a função func, definida acima.
A linha que guardamos para analisar contém uma operação de casting, atribuição e deferência, sendo o casting executado primeiro, operador unário que é, seguido pelo segundo operador unário, a deferência. Como sempre, a atribuição é uma das últimas. Descomprimida a expressão dessa linha, ficamos com algo parecido com as duas linhas abaixo:
int* p = (int*) 0; *p = 0;
Não tem nada de errado em atribuir o valor 0 a um ponteiro, que é equivalente ao define NULL da biblioteca C (e C++). De acordo com a referência GNU, é recomendado o uso do define, mas nada impede utilizar o 0 “hardcoded”.
Porém, estamos escrevendo em um ponteiro nulo, o que com certeza é um comportamento não-definido de conseqüências provavelmente funestas. O ponteiro nulo é um ponteiro inválido que serve apenas para marcar um ponteiro como inválido. Se escrevermos em um endereço inválido, bem, não é preciso ler o padrão para saber o que vai acontecer =)
Alguns amigos me avisaram sobre algo muito pertinente: dizer que acessar um ponteiro nulo, portanto inválido, é errado e nunca deve ser feito. Como um ponteiro nulo aponta para um endereço de memória inválido, acessá-lo irá gerar uma exceção no seu sistema operacional e fazer seu programa capotar. Um ponteiro nulo é uma maneira padrão e confiável de marcar o ponteiro como inválido, e testar isso facilmente através de um if. Mais uma vez: ponteiros nulos apontando para um endereço de memória inválido (o endereço 0) nunca devem ser acessados, apenas atribuído a ponteiros.
Em código. Isso pode:
int* p = 0; // atribuindo nulo a um ponteiro int* p2 = p; // isso também podeIsso não pode:
*p = 15; // NUNCA acessar ponteiros nulos int x = *p; // isso também não pode, ler de um ponteiro nuloDito isso, me sinto melhor =)
Os diferentes erros na linguagem C
Uma coisa que me espanta de vez em quando é o total desconhecimento por programadores mais ou menos experientes dos níveis de erros que podem ocorrer em um fonte escrito em C ou C++. Desconheço o motivo, mas desconfio que o fato de outras linguagens não terem essa divisão de processos pode causar alguma nivelação entre as linguagens e fazer pensar que o processo de compilação em C é como em qualquer outra linguagem.
Porém, para começar, só de falarmos em compilação já estamos pegando apenas um pedaço do todo, que é a geração de um programa executável em C. Tradicionalmente, dividimos esse processo em três passos:
- Preprocessamento
- Compilação
- Linkedição
Vamos dar uma olhada mais de perto em cada um deles e descobrir erros típicos de cada processo.
Preprocessamento
O preprocessamento é especificado pelos padrões C e C++, mas, tecnicamente, não faz parte da linguagem. Ou seja, antes que qualquer regra de sintaxe seja verificada no código-fonte, o preprocessamento já terá terminado.
Essa parte do processo lida com substituição de texto e diretivas baseadas em arquivos e símbolos. Por exemplo, a diretiva de preprocessamento mais conhecida
#include <stdio.h>faz com que todo o conteúdo do arquivo especificado seja incluído exatamente no ponto onde for colocada essa diretiva. Isso quer dizer que, antes sequer do código-fonte ser compilado, todo o conteúdo desse header padrão estará no corpo do arquivo C.
Para evitar que o mesmo header seja incluído inúmeras vezes dentro da mesma unidade em C, causando assim erros de redefinição, existe outra diretiva muito usada para cercar esses arquivos públicos:
#ifndef __MEUHEADER__ // se já estiver definido, caio fora até endif #define __MEUHEADER__ // conteúdo do header #endif // __MEUHEADER__Esse conjunto de duas diretivas, por si só, é capaz de gerar os mais criativos e bizarros erros de compilação em C. E estamos falando de erros que ocorrem antes que sequer seja iniciado o processo de compilação propriamente dito. Obviamente que os erros serão capturados durante a compilação, mas o motivo deles terem ocorrido foi um erro decorrente do processo de preprocessamento. Por exemplo, vamos supor que um determinado fonte necessita de uma declaração de função contida em meuheader.h:
#include "header-do-mal.h" #include "meuheader.h" int func() { meuheaderFunc(); }Porém, num daqueles acasos da natureza, o header-do-mal.h define justamente o que não poderia definir jamais (obs.: e isso pode muito bem acontecer na vida real, se usamos definições muito comuns):
#ifndef __HEADERDOMAL__ #define __HEADERDOMAL__ // tirei header da jogada, huahuahua (risos maléficos) #define __MEUHEADER__ #endif // __HEADERDOMAL__Na hora do preprocessamento, o preprocessador não irá mais incluir o conteúdo dentro de header.h:
#ifndef __MEUHEADER__ // se já estiver definido, caio fora até endif #define __MEUHEADER__ int meuheaderFunc(); // talvez alguém precise disso #endif // __MEUHEADER__Conseqüentemente, durante a compilação do código-fonte já preprocessado, sem a declaração da função meuheaderFunc, irá ocorrer o seguinte erro:
error C3861: 'meuheaderFunc': identifier not foundIsso em fontes pequenos é facilmente identificável. Em fontes maiores, é preciso ter um pouco mais de cuidado.
Após o processo de preprocessamento, de todos os arquivos indicados terem sido incluídos, de todas as macros terem sido substituídas, todas as constantes colocadas literalmente no código-fonte, temos o que é chamado unidade de compilação, que será entregue ao compilador, que, por sua vez, irá começar a análise sintática de fato, descobrindo novos erros que podem ou não (como vimos) ter a ver com a fase anterior. A figura abaixo ilustra esse processo, com algumas trocas conhecidas:
Dica: quando o bicho estiver pegando, e tudo o que você sabe sobre linguagem C não estiver te ajudando a resolver um problema, tente gerar uma unidade de compilação em C e analisar sua saída. Às vezes o que é claro no código pode se tornar obscuro após o preprocessamento. Para fazer isso no VC++ em linha de comando, use o parâmetro /E.
Compilação
Se você conseguir passar ileso para a fase de compilação, pode se considerar um mestre do preprocessamento. Por experiência própria, posso afirmar que a maior parte do tempo gasto corrigindo erros de compilação, por ironia do destino, não terá origem na compilação em si, mas no preprocessamento e linkedição. Isso porque o preprocessamento confunde muito o que vimos no nosso editor preferido, e a linkedição ocorre em uma fase onde não importa mais o que está dentro das funções, mas sim o escopo de nomes, um assunto um pouco mais vago do que a linguagem C.
Aqui você irá encontrar geralmente erros bem comportados, como conversão entre tipos, else sem if e esquecimento de pontuação ou parênteses.
int cannotConvertError(const char* message) { int ret = message[0]; return ret; } int ret = cannotConvertError(3); error C2664: 'cannotConvertError' : cannot convert parameter 1 from 'int' to 'const char *'if( test() ) something; something-else; else else-something;error C2181: illegal else without matching ifwhile( (x < z) && func(x, func2(y) != 2 ) { something; }error C2143: syntax error : missing ')' before '{'Claro, não estamos falando de erros relacionados a templates, que são um pesadelo à parte.
Dica: nunca subestime o poder de informação do compilador e da sua documentação. Se o erro tem um código (geralmente tem), procure a documentação sobre o código de erro específico, para ter uma idéia de por que esse erro costuma ocorrer, exemplos de código com esse erro e possíveis soluções. Ficar batendo a cabeça não vai ajudar em nada, e com o tempo, você irá ficar sabendo rapidamente o que aconteceu.
Linkedição
Chegando aqui, onde a esperança reside, tudo pode vir por água abaixo. Isso porque você já espera confiante que tudo dê certo, quando, na verdade, um erro bem colocado pode fazer você desistir pra sempre desse negócio de programar em C.
As características mais desejadas para corrigir erros nessa fase são:
- Total conhecimento da fase do preprocessamento
- Total conhecimento da fase da compilação
- Total conhecimento de níveis de escopo e assinatura de funções
Os dois primeiros itens são uma maldição previsível que deve-se carregar para todo o sempre. Se você não consegue entender o que aconteceu nas duas primeiras fases, dificilmente irá conseguir seguir adiante com essa empreitada. O terceiro item significa que deve-se levar em conta as bibliotecas que estamos usando, headers externos (com dependências externas), conflitos entre nomes, etc.
Alguns erros mais encontrados aqui são as funções não encontradas por falta da LIB certa ou por LIBs desatualizadas que não se encaixam mais com o projeto, fruto de muitas dores de cabeça de manutenção de código. Essa é a parte em que mais vale a pena saber organizar e definir uma interface clara entre os componentes de um projeto.
Do ponto de vista técnico, é a fase onde o linker junta todos os arquivos-objeto especificados, encontra as funções, métodos e classes necessárias e monta uma unidade executável, como ilustrado pela figura abaixo.
Dica: uma LIB, ou biblioteca, nada mais é que uma coleção de arquivos-objeto que já foram compilados, ou seja, já passaram pelas duas primeiras fases, mas ainda não foram linkeditados. Muitas vezes é importante manter compatibilidade entre LIBs e os projetos que as usam, de forma que o processo de linkedição ocorra da maneira menos dolorosa possível.
Erros além da imaginação
É óbvio que, por ter passado pelas três fases de transformação de um código-fonte em um programa executável, não quer dizer que este programa está livre de erros. Os famigerados erros de lógica podem se disfarçar até o último momento da compilação e só se mostrarem quando o código estiver rodando (de preferência, no cliente).
Entre esses erros, os mais comuns costumam se aproveitar de macros, como max, que usa mais de uma vez o parâmetro, que pode ser uma chamada com uma função. A função será chamada duas vezes, mesmo que aparentemente no código a chamada seja feita uma única vez:
#define max(a, b) ( a > b ? a : b ) int z = max( func(10), 30 );Um outro erro que já encontrei algumas vezes é quando a definição de uma classe tem um sizeof diferente do compilado em sua LIB, pela exclusão ou adição de novos membros. Isso pode (vai) fazer com que, durante a execução, a pilha seja corrompida, membros diferentes sejam acessados, entre outras traquinagens. Esses erros costumam acusar a falta de sincronismo entre os headers usados e suas reais implementações.
Enfim, na vida real, é impossível catalogar todos os erros que podem ocorrer em um fonte em C. Se isso fosse possível, ou não existiriam bugs, ou pelo menos existiria uma ferramenta para automaticamente procurar por esses erros e corrigi-los.
Um projeto cheio de erros
Como burlar orientação a objetos em C++.
Em uma entrevista, foi perguntado ao criador da linguagem C++, Bjarne Stroustrup, por que as pessoas têm uma ligação tão profunda com as linguagens de programação, a ponto de se formarem comunidades de fanáticos por linguagens. A resposta foi típica de um profissional experiente da área de computação: “Meu palpite é que as linguagens que usamos para expressar nossas idéias tornam-se parte de nós, tanto que, se você só conhece uma linguagem, os partidários de outras linguagens podem representar uma ameaça pessoal. Nesse caso, a solução parece ser conhecer também outras linguagens. Não acredito que seja possível atuar profissionalmente na área de software conhecendo apenas uma linguagem de programação. Também pode haver um motivo econômico: embora os conhecimentos básicos transcendam os limites das linguagens de programação, o mesmo não se aplica a várias habilidades práticas. Então, se eu conheço apenas a linguagem X e seus conjuntos de ferramentas, mas você quer discutir a linguagem Y e seus conjuntos de ferramentas, você está ameaçando o meu meio de vida. Mais uma vez, parece que a solução é conhecer várias linguagens e conjuntos de ferramentas (e ter uma base sólida nos fundamentos da área). Infelizmente, as soluções que sugiro não levam em conta o fato de que a maioria das pessoas tem muito pouco tempo livre, depois de dar conta de todos os afazeres. Mas isso não é desculpa para o fanatismo.”
Esse famigerado fanatismo a respeito de uma ou outra linguagem de programação pode ser decorrente de diversos fatores como: aceitação da linguagem no mercado de trabalho (se o mercado carece e aprova uma linguagem, é natural que exista muitos profissionais trabalhando com essa linguagem), utilização da mesma em trabalhos acadêmicos ou, simplesmente, pelo destaque temporário que a linguagem se encontra – “moda” de uma linguagem de programação.
Entre diversas linguagens aceitas pelo mercado, as mais populares são C,C++ e Java. No decorrer deste artigo, será apresentado uma comparação entre elas, usando a linguagem C++ como referência.
Ao visualizar as três linguagens descritas anteriormente, a linguagem C++ pode ser considerada como a linguagem mediana, e as outras duas, como formadoras da extremidade, onde C está no início e Java no fim. A linguagem C foi criada com o seguinte objetivo principal: facilitar a criação de programas extensos com menos erros, recorrendo ao paradigma da programação algorítmica ou procedimental, mas sobrecarregando menos o autor do compilador, cujo trabalho complica-se ao ter de realizar as características complexas da linguagem. Já a linguagem Java, partiu das seguintes premissas: orientação a objeto, portabilidade (independência de plataforma), recursos de rede e segurança (aplicações em rede mais seguras).
Dessa forma, C++ representa o elo entre C e Java, ou seja, ela é uma linguagem de programação de alto nível com facilidades para o uso em baixo nível. Por conta disso, alguns fanáticos ou estudiosos afirmam que C++ apresenta o lado inseguro de C, principalmente ao trabalhar com ponteiros e memória; em contrapartida, apresenta o lado seguro de Java, por automatizar todo o processo de alocação de memória e impossibilitar a utilização de ponteiros.
Assim, C++ assemelha-se a uma faca de dois gumes, pois, se de um lado há liberdade demais, a ponto de se tornar inseguro a utilização de alguns recursos oferecidos por uma linguagem, por outro, há liberdade de menos, ao ponto de perder desempenho por falta deles.
Um recurso utilizado em linguagens de programação discutido neste artigo é o uso de ponteiros na linguagem C++. Ponteiros caracterizam, basicamente, em endereçar um valor na memória. Por trabalhar diretamente com a memória, seu uso inescrupuloso pode ser perigoso. Com outras palavras, o uso indiscriminado de ponteiros pode corromper o conceito de orientação a objetos em C++.
1. Comparação de linguagens: C, C++ e Java
A seguir, são detalhadas a principais características positivas de cada linguagem para efeito de comparação.
C:
l Eficiência
l Portabilidade de Código
l Habilidade para acessar endereços específicos de hardware.
l Demanda poucos recursos do sistema.
C++:
l Trata-se de uma linguagem ao mesmo tempo de alto e baixo nível. Tem-se todo o poder de uma linguagem de baixo nível, e pode-se usar a orientação a objetos para aumentar indefinidamente o nível de programação.
l Há uma quantidade gigantesca de bibliotecas e componentes aproveitáveis para aplicações em C++.
l É uma linguagem de sólida tradição. Destaca-se o fato de que o Unix foi feito em C. Portanto trata-se de uma linguagem que pode ser aplicada em projetos de alta responsabilidade.
l Quem tem interesse em eventualmente mudar de área dentro de TI, aproveita muito a bagagem de uso de C++. Por exemplo: Seja o caso de quem programou para web por algum tempo usando C++, e depois se interessou por outra área, digamos: reconhecimento de voz. Esse profissional aproveita bem a bagagem anterior. Quem programou para web com uma tecnologia específica para web (por exemplo asp, php) não terá aplicação da bagagem quanto estiver atuando para a nova área. O profissional retornará a condição de principiante.
Java:
l Java implementa orientação a objetos muito bem, sendo mais fácil de se aprender que C++ e mais segura (não possui comandos tão poderosos).
l Java é multiplataforma a nível de bytecode. Portanto, vale o slogan da linguagem amplamente repetido: “escreva uma vez e execute em qualquer lugar”.
l O mercado já comprou a idéia de usar Java. Isso é um fato muito importante. A massa crítica de empresas e profissionais que adotam essa tecnologia é substancioso.
l Supostamente, quem adota Java está protegido contra variações que afetam o mercado de tecnologia ao longo do tempo. Isso porque é sempre possível (teoricamente) fazer uma máquina virtual Java que torna os bytecodes executáveis num computador que ainda nem se inventou ainda.
2. Ponteiros
No campo da programação, um ponteiro ou apontador é um tipo de dado de uma linguagem de programação cujo valor se refere diretamente a um outro valor alocado em outra área da memória, através de seu endereço. Um ponteiro é uma simples implementação do tipo referência da Ciência da Computação.
Ponteiros também são utilizados para simular a passagem de parâmetros por referência em linguagens que não oferecem essa construção (em C, por exemplo). Isso é útil se desejamos que uma modificação em um valor feito pela função chamada seja visível pela função que a chamou, ou também para que uma função possa retornar múltiplos valores.
Linguagens como C e C++ permitem que ponteiros possam ser utilizados para apontar para funções, de forma que possam ser invocados como uma função qualquer. Essa abordagem é essencial para a implementação de modelos de re-chamada (callback), muito utilizados atualmente em bibliotecas de rotinas para manipulação de interfaces gráficas. Tais ponteiros devem ser tipados de acordo com o tipo de retorno da função o qual apontam.[7]
No ambiente C++, existe um tipo de ponteiro mais complexo denominado de ponteiro inteligente. Basicamente, ele simula um ponteiro comum. A diferença está nas funcionalidades disponíveis: coletor de lixo ou verificação de limites do tipo de dado, para adicionar segurança e reduzir erros de programação, ainda que maximizando a eficiência; e, evitar vazamento de memória, desalocando espaços não mais utilizados e destruindo os ponteiros defasados.
Além dos ponteiros inteligentes, tem também os ponteiros nulos e salvagens. Ponteiros nulos, possuem um valor reservado, geralmente zero, indicando que ele não se refere a um objeto. São usados freqüentemente para representar condições especiais como a falta de um sucessor no último elemento de uma lista ligada, mantendo uma estrutura consistente para os nós da lista. Ponteiros Selvagens não possuem endereço associado. Qualquer tentativa em usá-los causa comportamento indefinido, ou porque seu valor não é um endereço válido ou porque sua utilização pode danificar partes diferentes do sistema.
Ao observar as características dos ponteiros e também da linguagem C++, percebe-se uma ligação poderosa e bastante perigosa, capaz de “burlar” o paradigma de orientação a objetos proposto na linguagem.
3. Como burlar orientação a objetos em C++ com ponteiros
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
class P_exemplo{
private: int num;
public:
void fixa_num(int val) {num=val;}
int* retornaEnd(){return &(this -> num);};
void mostra_num();
};
void P_exemplo::mostra_num()
{
cout << num << “\n”;
}
int main()
{
P_exemplo ob; // declara um objeto
ob.fixa_num(1); // acessa ob diretamente
ob.mostra_num();
int* p = ob.retornaEnd(); // atribui a p o endereco de ob
*p = *p + 1;
printf(“%u\n”, *p);
ob.mostra_num();
return 0;
}
O código acima mostra o uso de ponteiros para ter acesso a um atributo privado de uma classe. Em outras palavras, “quebrando” a orientação a objetos.
A classe P_exemplo possui um atributo privado, denominado ‘num’. Ademais, foram criadas três funções: ‘fixa_num’, ‘retornaEnd’ e ‘mostra_num’. A primeira função atribui um valor ao atributo. A segunda retorna o endereço do atributo já fixado. E a última função simplesmente exibe o valor do atributo fixado com algum valor. No método principal do programa, criou-se um objeto por definição e uso do paradigma orientação a objetos. Depois atribuimos o valor 1 ao atributo ‘num’ e mostramos o seu valor. Como resultado, será exibido em tela o valor 1. Logo em seguida, criamos um ponteiro ‘p’ e atribuímos a ele, o retorno da função ‘retornaEnd’ (o retorno será o endereço do atributo ‘num’ fixado com valor 1). A partir daí, trabalhamos com o ponteiro, efetuando uma operação aritmética.
Por fim, imprimimos o resultado contido no ponteiro e o valor contido no atributo ‘num’. O que se observa é que ambos impressões apresentará o resultado igual a 2. Concluindo assim, que a orientação a objetos foi burlada, pois não é possível trabalhar diretamente com um atributo privado fora da sua classe geradora.
Projeto de Multiplicador para ponto flutuante
A multiplicação em ponto flutuante, para fim prático, é realizada pelo processador através de cinco etapas. Estas etapas estão dispostas conforme o fluxograma abaixo.
Geralmente, os processadores dedicam hardware para realizar operação em ponto flutuante. Neste artigo, foi implementada uma solução em hardware para realizar a operação descrita acima.
1. Esquema em hardware para multiplicação em Ponto Flutuante
O esquema em hardware assume como entrada dois registradores, e retorna um registro com o resultado da multiplicação. Em alto nível, esse esquema pode ser representado pela figura.
O sinal do resultado da multiplicação é dado através da porta lógica XOR da figura. No esquema, o considera-se 1 como valor negativo e 0 valor positivo. A porta lógica XOR retorna valor 0 (positivo) quando as entradas são iguais, por exemplo 1 XOR 1(sinais iguais), e retorna valor 1 (negativo) para entradas diferentes, por exemplo XOR 0(sinais contrários).
Resolução das etapas:
Etapa 1 – Os expoentes são somados. Esse operação é realizada pela ALU 1(Unidade Lógica Aritmética), que tem em como entrada os expoentes. A soma pode ser sem bias[3] ou através dos números deslocados. O resultado é passado para a unidade de controle
Etapa 2 – Os significando são multiplicados. Essa operação é realizada pela ALU 2. O resultado dessa multiplicação é passado para a unidade de controle.
Com os resultados das etapas 1 e 2, a unidade de controle decide quais sinais de controle serão ativados (CMux1, CMux2, CInc …). Para os sinais de controle foi adotado 0 como desativado e 1 como ativado.
Etapa 3 – A unidade de controle avalia se o produto está normalizado. No caso afirmativo, os sinais de controle ficam: CMux1=1, recebe o resultado da soma dos expoentes. CInc=0, e o valor dessa soma não tem valor incrementado. CMux2=0, recebe o valor do produto da ALU 2. CDesl=0, e não é necessário deslocar o produto para direita. Caso negativo, os sinais CMux1 e CMux2 permanecem inalterados, porém os sinais CInc e CDesl têm seus valores modificados, sinalizando que o produto precisa ser normalizado. Assim, a unidade de incremento e a unidade de deslocamento são ativadas.
Etapa 4 – A unidade de controle ativa o sinal CArred se o produto precisar ser arredondado. Caso, após o arredondamento, o produto não permaneça normalizado, a unidade de controle será “informada” para tomar novas decisões. Para isso os sinais de controle ficam: CMux1=0, CMux2=1, recebendo os valores após o arredondamento e CInc=1 e CDesl=1, sinalizando que o produto precisa ser normalizado.
Etapa 5 – Após o arredondamento, os valores do produto, expoente e sinal, são armazenados em um registrador.
Nova parceria: RicZignal
Mesmo conteúdo do blog antigo.
Compiladores
O compilador é visto como um facilitador, ou ainda, como um programa de sistema que traduz um programa descrito em uma linguagem de alto nível para um programa equivalente em código de máquina para um processador. Em geral, um compilador não produz diretamente o código de máquina mas sim um programa em linguagem simbólica (assembly) semanticamente equivalente ao programa em linguagem de alto nível. O programa em linguagem simbólica é então traduzido para o programa em linguagem de máquina através de montadores.
Assembly
A linguagem simbólica ou assembly, ou ainda, linguagem de montagem é uma notação legível por humanos para o código de máquina que uma arquitetura de computador específica usa. É de longe, a mais poderosa, mais rápida, mais flexível e que permite tirar partido de todas as características de um microprocessador. No entanto, todo este poder de controle sobre o processador tem um preço: para a maioria das aplicações, programar em assembly seria uma tarefa árdua, razão pela qual foram criadas linguagens designadas de alto nível (porque estão mais próximas do raciocínio do programador), que facilitam muito a tarefa ao programador para a maioria das tarefas. De qualquer forma, existem sempre casos em que não podemos prescindir da linguagem de montagem, como por exemplo, o software que controla diretamente o hardware e o software para automação e controle.
Linkers
Quando um programa é criado, ele é constituído por métodos e instruções. Ademais, ele pode referenciar bibliotecas externas que complementam o funcionamento do programa. A função do linker é justamente evitar uma “redundância de compilação” , já que o novo programa, ao fazer uso de bibliotecas externas, repetiria o processo de compilação para cada biblioteca. Esse processo levaria um desperdício para os recursos computacionais.
Basicamente, o linker aglutina todas as bibliotecas referenciadas por um programa com os procedimentos deste e transforma tudo em um arquivo executável.
Loaders
Normalmente é de responsabilidade do sistema operacional carregar e executar arquivos. A parte do sistema operacional que executa essa função é chamada de Loader.
O loader pode ser definido como o inverso dos linkers. Ele consiste em um programa de sistema que coloca o programa-objeto na memória principal, para que esteja pronto para ser executado.
O carregamento na memória principal varia de acordo com o código gerado pelo linker. Em função disto, o loader pode ser denominado de absoluto ou relocável.
Vantagens e desvantagens em utilizar Big-Endian e Little-Endian
Endianness é a forma de ordenar dados pela sua ordem de magnitude ou ordem de grandeza, ou ainda, é o jeito que uma CPU lê/escreve palavras da/na memória principal. As principais formas de endianness são big-endian e little-endian. Na forma little-endian, os bytes são guardados por ordem crescente do seu “peso numérico” em endereços sucessivos da memória. Na notação big-endian, os bytes são guardados por ordem decrescente do seu “peso numérico” em endereços sucessivos da memória.
Nós estamos acostumados com os números representados na ordem big-endian(Extremidade-maior primeiro), em que os primeiros dígitos a serem representados são os de maior peso. Por exemplo, o número 1234 significa 1 milhar (103), 2 centenas (102) 3 dezenas (101) e 4 unidade (100). Datas escritas no formato big-endian devem ser escritas como AAAA-MM-DD. O formato DD-MM-AAAA é a representação da data em little-endian, e finalmente o formato MM-DD-AAAA é a representação em middle-endian, porque o campo com a ordem de grandeza média, está escrito primeiro. No entanto, os números que representam os dias, meses e anos estão todos no formato big-endian.
Como a notação big-endian trabalha com números exatamente como lemos, poderíamos pensar que essa notação seria altamente vantajosa. No entanto, a forma little-endian tem a propriedade de que, na falta de alinhamento restrições, o mesmo valor pode ser lido em vários comprimentos de memória sem utilizar diferentes endereços. Ademais, a notação little-endian é muito mais barata.
De certa forma, as vantagens e desvantagens são relativas. Ao escolhermos uma notação, precisamos levar em conta como iremos trabalhar e qual o resultado esperado.
Início das pesquisas para o conteúdo do blog de Arquitetura de Computadores da Universidade Federal de Sergipe. 2008/1