Jiraspiom

Blog Pessoal

Lição 15

Lição 15 – Exceções e Diretivas

Exceptions ou Exceções

Exceções fornecem um modo de reagir a circunstâncias excepcionais (como erros de execução) no nosso programa transferindo o controle para funções especiais chamadas manipuladores ou handlers. Para capturar exceções, devemos colocar um pedaço de código sobre uma inspeção excepcional. Isto é feito colocando aquela porção de código em um bloco try. Quando uma circunstância excepcional acontece no bloco, uma exceção é lançada, o que transfere o controle para o handler. Se nenhuma exceção é lançada, o código continua normalmente e os handlers são ignorados.

Uma exceção é lançada usando a palavra-chave throw dentro do bloco try. Tratadores de exceções, os handlers são declarados com a palavra-chave catch, que deve ser inserida imediatamente após o bloco try:

#include <iostream>
using namespace std;

int main () {
try
{
throw 20;
}
catch (int e)
{
cout << “Um erro ocorreu. Exceção Numero ” << e << endl;
}
return 0;
}


, o que mostrará na tela:

Um erro ocorreu. Exceção Numero 20


Neste exemplo, o código simplesmente lança uma exceção:

throw 20;


Uma expressão throw aceita um parâmetro (neste caso um inteiro de valor 20), que é passado como argumento para o handler.

Como podemos ver, o handler segue imediatamente as chaves {} depois do bloco try. O formato do catch é similar a uma função normal que sempre tem no mínimo um parâmetro. O tipo deste parâmetro é muito importante, já que o tipo do argumento passado pela expressão throw é comparado com ele, e apenas no caso de serem iguais a exceção é capturada.

Podemos encadear multiplos handlers (expressões catch), cada uma com um tipo de parâmetro diferente. Apenas o handler que bate seu tipo com o argumento especificado na expressão throw é executado. Se usarmos três-pontos (…) como parâmetro do catch, aquele handler irá capturar qualquer exceção, não importando o tipo de exceção que é lançada. Pode ser usado como handler padrão que captura todas as exceções não capturadas por outros handlers se for especificado por útimo:

try {
// codigo
}
catch (int param) { cout << “excecao int”; }
catch (char param) { cout << “excecao char”; }
catch (…) { cout << “excecao default”; }


No código anterior, o último handler capturaria qualquer exceção lançada com qualquer parâmetro que não seja int ou char.

OBS: Após uma exceção ter sido tratada pelo handler, a execução do programa prosegue após o bloco try-catch, não após a expressão throw.

Também é possível aninhar blocos try-catch em blocos try mais externos. Nesses casos, temos a possibilidade de que um bloco catch interno siga a exceção de seu nível externo. Isto é feito com a expressão throw; sem argumentos. Por exemplo:

try {
try {
// codigo
}
catch (int n) {
throw;
}
}
catch (…) {
cout << “Erro Ocorreu”;
}


Especificações de Exceções

Ao declarar uma função, podemos limitar o tipo da exceção que ela irá diretamente ou indiretamente lançar anexando um sufixo à declaração da função:

float minhafuncao (char param) throw (int);


Isto declara uma função chamada minhafuncao que tem um argumento do tipo char e retorna um elemento do tipo float. A punica exceção que esta função pode lançar é uma exceção do tipo int. Se lançar uma exceção com um tipo diferente, mesmo diretamente ou indiretamente, não pode ser capturada por um handler de tipo int comum.

Se este especificador throw é deixado vazio sem tipo, significa que à função não é permitida a lançar exceções. Funções sem especificador throw (funções normais) são permitidas de lançar exceções com qualquer tipo:

int minhafuncao (int param) throw(); // excecoes nao permitidas
int minhafuncao (int param); // todas as excecoes permitidas

Exceções Padrão

A biblioteca padrão de C++ fornece uma classe base especificamente projetada para declarar objetos para serem lançados como exceções. É chamado exception e é definido no arquivo de cabeçalho <exception> sobre o namespace std. Esta classe tem o default usual e construtores cópia, operadores e destrutores, e ainda uma função membro virtual adicional chamada what que retorna uma seqüência de caracteres terminadas em nulo (já explicado nas lições anteriores) e que podem ser sobrescritas em classes derivadas para conter um pouco de descrição das exceções.

#include <iostream>
#include <exception>
using namespace std;

class minhaexcecao: public exception
{
virtual const char* what() const throw()
{
return “Minha excecao aconteceu”;
}
} mex;

int main () {
try
{
throw mex;
}
catch (exception& e)
{
cout << e.what() << endl;
}
return 0;
}


, o que retorna na tela:

Minha excecao aconteceu.


Colocamos um handler que captura objetos de exceção por referência (note o & após o tipo), para que capture também classes derivadas da exceção, como nosso objeto mex da classe minhaexcecao;

Todas as exceções lançadas por componentes da Biblioteca Padrão de C++ lançam exceções derivadas desta classe std::exception. São elas:

exceção description


bad_alloc lançada por new em falha de alocação
bad_cast lançada por dynamic_cast quando acontece falha com um tipo de referência
bad_exception lançada quando um tipo de exceção não bate com nenhum catch
bad_typeid lançada por typeid
ios_base::failure lançada por funções na biblioteca iostream

Por exemplo, se usarmos o operador new e a memória não puder ser alocada, uma exceção do tipo bad_alloc é lançada:

try
{
int * vet = new int[1000];
}
catch (bad_alloc&)
{
cout << “Erro alocando memoria.” << endl;
}


É recomendado incluir todas as alocações de memória com um bloco try catch que captura este tipo de exceção para realizar uma ação limpa, ao invés de um término anormal da aplicação, que é o que acontece quqando este tipo de exceção é lançado e não tratado. Se quiser forçar uma exceção bad_alloc para vê-la em ação, você pode tentar alocar um array enorme; Talvez tentar alocar 1 bilhão de inteiros já vá resultar em uma exceção bad_alloc.

Como bad_alloc é derivada da exceção da classe base padrão, podemos tratar a mesma exceção capturando referẽncias para a classe de exceção:

#include <iostream>
#include <exception>
using namespace std;

int main () {
try
{
int* vet = new int[1000];
}
catch (exception& e)
{
cout << “Excecao padrao: ” << e.what() << endl;
}
return 0;
}

Diretivas Pré-Processamento

São linhas projetadas incluídas no código que não de nossos programas que não são expressões do programa mas diretivas para o pré-processamento. Estas linhas são sempre precedidas por um (#). O pré-processamento é executado antes da compilação começar.

As diretivas se extendem apenas através de uma única linha de código. Assim que um caracter de nova linha é encontrado, o pré-processamento é considerado finalizado. Ponto-e-vírgula não são necessários no final de uma diretiva. A única maneira que uma diretiva pré-processamento pode se extender por mais de uma linha é precedendo o caracter de nova linha no fim da linha por uma barra invertida (\).

Definições macro (#define, #undef)

Para definir macros pré-processamento podemos usar #define, cujo formato é:

#define identificador troca

Quando o pré-procesamento encontra esta diretiva, ele substitui qualquer ocorrência do identificador no resto do código por troca. Esta substituição pode ser uma expressão, um bloco, ou simplesmente nada. O pré-processamento não entende C++, simplesmente troca qualquer ocorrência de identificador por troca.
Exemplo:

#define TAMANHO_TABELA 100
int tabela1[TAMANHO_TABELA];
int tabela2[TAMANHO_TABELA];

Após o pré-processamento ter trocado TAMANHO_TABELA, o código é equivalente a:

int tabela1[100];
int tabela2[100];

Este uso de #define é comum, mas ele também pode funcionar com parâmetros para definir funções macros:

#define tMaior(a,b) a>b?a:b

Fazzendo isto, poderíamos substituir qualquer ocorrência de tMaior com dois argumentos pela expressão de troca, mas também substituíndo cada argumento por seu identificador exatamente como experaríamos se fosse a função:

// funcao macro
#include <iostream>
using namespace std;

#define tMaior(a,b) ((a)>(b)?(a): (b))

int main()
{
int x=5, y;
y= tMaior(x,2);
cout << y << endl;
cout << tMaior(7,x) << endl;
return 0;
}

, o que retornaria na tela:

5
7

macros definidas não são afetadas pela estrutura do bloco. Uma macro dura até sua indefinição com a diretiva #undef:

#define TAMANHO_TABELA 100
int tabela1[TAMANHO_TABELA];
#undef TAMANHO_TABELA
#define TAMANHO_TABELA 200
int tabela2[TAMANHO_TABELA];

Isto geraria o mesmo que:

int tabela1[100];
int tabela2[200];

Definições de funções macro aceitam dois operadores especiais (# e ##) na seqüência de substituição:

Se o operador # for usado antes que um parâmetro é usado na seqüência de troca, aquele parâmetro é substituído por uma string literal (como se ela estivesse entre aspas duplas).

#define str(x) #x
cout << str(teste);

Isto seria traduzido para:

cout << “teste”;

O operador ## concatena dois argumentos, não deixando espaço em branco entre eles:

#define func(a,b) a ## b
func(c,out) << “teste”;

Isto também seria traduzido para:

cout << “teste”;

Como as substituições pré-processamento acontecem antes da qualquer checagem sintática em C++, definições macro podem ser uma funcionalidade ótima, mas deve ser usada com cuidado: códigos que dependem muito de macros complicadas podem gerar um resultado obscuro para outros programadores, já que a sintaxe que eles experam é, na maioria dos casos, diferente de expressões comuns que programadores esperam em C++.

Inclusões condicionais (#ifdef, #ifndef, #if, #endif, #else and #elif)

São diretivas que permitem incluir ou descartar parte do código de um programa se uma certa condição for encontrada.

#ifdef permite que uma seção de um programa seja compilada apenas se a macro que é especificada como parâmetro tiver sido definida, não importando qual é o seu valor. Exemplo:

#ifdef TAMANHO_TABELA
int tabela[TAMANHO_TABELA];
#endif


neste caso, a linha de código int tabela[TAMANHO_TABELA]; é compilada apenas se TAMANHO_TABELA foi definido previamente com #define, independentemente de seu valor. Se não foi definida, aquela linha não será incluída na compilação do programa.

#ifndef serve para o oposto: o código entre as diretivas #ifndef e #endif apenas é compilado se o identificador especificado não foi definido previamente. Exemplo:

#ifndef TAMANHO_TABELA
#define TAMANHO_TABELA 100
#endif
int tabela[TAMANHO_TABELA];


Neste caso, se quando chegarmos neste pedaço de código, a macro TAMANHO_TABELA ainda não foi definida, seria definida com um valor de 100. Se já existia, manteria o valor anterior já que a diretiva #define não seria executada.

As diretivas #if, #else e #elif (ou seja, else if) servem para especificar alguma condição a ser encontrada para que a porção de código que elas cercam seja compilada.A condição que segue #if ou #elif pode apenas avaliar expressões constantes, inluíndo expressões macro. Exemplo:

#if TAMANHO_TABELA>200
#undef TAMANHO_TABELA
#define TAMANHO_TABELA 200

#elif TAMANHO_TABELA<50
#undef TAMANHO_TABELA
#define TAMANHO_TABELA 50

#else
#undef TAMANHO_TABELA
#define TAMANHO_TABELA 100
#endif

int tabela[TAMANHO_TABELA];


Note como a estrutura inteira de diretivas #if, #elif e #else encadeadas terminam com #endif.

o comportamento de #ifdef e #ifndef também podem ser alcançados usando os operadores especiais definided e !defined respectivamente em qualquer uma das diretivas #if ou #elif:

#if !defined TAMANHO_TABELA
#define TAMANHO_TABELA 100
#elif defined TAMANHO_VETOR
#define TAMANHO_TABELA TAMANHO_VETOR
int tabela[TAMANHO_TABELA];

Controle de Linha (#line)

Quando compilamos um programa e algum erro acontece durante este processo, o compilador mostra uma mensagem de erro com referências ao nome do arquivo onde o erro aconteceu e o número da linha, o que facilita a localização do código que gerou o erro.

A diretiva #line nos permite controlar ambas as coisas, os números das linhas nos arquivos de código assim como o nome do arquivo que queremos que apareça quando um erro acontece. Seu formato é:

#line numero “nomedoarquivo”


, onde numero é o novo número da linha que será atribuído à próxima linha de código. Os números das linhas de sucessivas linhas será incrementado um a um a partir deste ponto. Já nomedoarquivo é um parâmetro opcional que permite redefinir o nome do arquivo que será mostrado. Exemplo:

#line 20 “atribuindo variavel”
int a?;


Este código irá gerar um erro que será mostrado como erro no arquivo atribuindo variavel, linha 20.

Diretiva Error (#error)

Esta diretiva aborta o processo de compilação quando é encontrada, gerando uma compilação do erro que pode ser especificado como seu parâmetro:

#ifndef __cplusplus
#error Um compilador C++ se faz necessario!
#endif


Este exemplo aborta o processo de compilação se o nome da macro __cplusplus não está definido (o nome desta macro está definido por padrão em todos os compiladores de C++).

Inclusão de arquivo fonte (#include)

Esta diretiva também foi usada assiduamente em códigos anteriores do curso. Quando o pré-processamento encontra uma diretiva #include, ele a substitui por um conteúdo inteiro de um arquivo especificado. Existem duas maneiras de especificar um arquivo a ser incluído:

#include “arquivo”
#include <arquivo>


A única diferença entre ambas as expressões é o local (diretórios) onde o compilador irá procurar pelo arquivo. No primeiro caso, onde o nome do arquivo é especificado entre aspas duplas, o arquivo é procurado primeiramente no mesmo diretório que inclui o arquivo contendo a diretiva. No caso em que não esteja lá, o compilador procura o arquivo nos diretórios onde é configurado para procurar pelos arquivos de cabeçalhos padrão. Se o nome do arquivo está entre < e > o arquivo é procurado diretamente nestes locais. Assim, arquivos de cabeçalho padrão são normalmente incluídos entre < e >, enquanto outros arquivos de cabeçalhos específicos são incluídos usando aspas simples.

Diretiva (#pragma)

Esta diretiva é usada para especificar diversas opções ao compilador. Estas opções são específicas para a plataforma e compilador que você estiver usando. Consulte o manual ou a referência de seu compilador para mais informações dos possíveis parâmetros que você pode definir com #pragma.

Se o compilador não suporta um argumento específico para #pragma, ele é ignorado – não é gerado erro.

Nomes de Macros Pré-Definidos

Os seguintes nomes de macros são definidos a qualquer momento:

macro valor
__LINE__ Valor inteiro representando a linha atual no arquivo fonte sendo compilado.
__FILE__ Uma string contendo o nome presumido do arquivo fonte sendo compilado.
__DATE__ Uma string na forma “Mm dd aaaa” contendo a data em que o processo de compilação se iniciou.
__TIME__ Uma string na forma “hh:mm:ss” contendo a hora em que o processo de compilação se iniciou.
__cplusplus Um valor inteiro. Todos os compiladores C++ têm esta constante definida com algum valor. Se o compilador está totalmente de acordo com o padrão C++, seu valor é igual ou maior que 199711L dependendo da versão do padrão a que eles estão de acordo.

Exemplo:

#include <iostream>
using namespace std;

int main()
{
cout << “Este é o número da linha ” << __LINE__;
cout << ” do arquivo ” << __FILE__ << “.\n”;
cout << “A compilacao comecou em ” << __DATE__;
cout << ” às ” << __TIME__ << “.\n”;
cout << “O compilador dá um valor __cplusplus de ” << __cplusplus;
return 0;
}


, o que retornaria:

Este é o número da linha 7 do arquivo /home/jay/stdmacronames.cpp.
A compilacao comecou em Nov 1 2005 às 10:12:29.
O compilador dá um valor __cplusplus de 1


Anúncios
%d blogueiros gostam disto: