AULA 1 - Programação II - Graduação

De IFSC
Ir para navegação Ir para pesquisar

Origem da linguagem C++

A linguagem C foi desenvolvida pelos engenheiros Brian Kernighan e Dennis Ritchie, dos Laboratórios AT&T Bell, uma gigante das telecomunicações americana, na década de 70.

A ideia era desenvolver uma linguagem de baixo nível potente o suficiente para escrever e manter o sistema operacional UNIX. Mas a linguagem se mostrou tão poderosa, portável e robusta que seu uso acabou se disseminando no mundo inteiro.

Com a onda da programação orientada a objetos e da engenharia de software, Bjarne Stroustrup, tb dos laboratórios AT&T Bell desenvolveu a linguagem C++, no início da década de 80. A linguagem C++ pode ser vista como uma linguagem C aperfeiçoada, mais amigável ao usuário que a C, e orientada a objetos, isto é, utiliza CLASSES como elemento base de projeto.

Pacotes RAD

Sao os pacotes ou ambientes de desenvolvimento integrado (IDEs) chamados de “Rapid Application Development”.

  • Programação Orientada a Eventos e a Objetos
  • Conjunto completo de Ferramentas de Desenvolvimento - Compilador, Ligador, Executor, Depurador, etc (IDE - Integrated Development Environment)
  • Ambiente Gráfico GUI - Graphical User Interfaces
  • Acesso fácil e automático às Propriedades, Eventos e Métodos de cada classe
  • Biblioteca de classes e componentes.

Porque POO?

  • Modularidade - desenvolvimento mais rápido
  • Portabilidade - bibliotecas padrão
  • Reutilização - de componentes

A programação não é LINEAR. Não existe um único fluxo de execução.

Possibilidade de interface GRÁFICA!!!

Velha2.png

Descrição de um código OO

Em programação estruturada - fluxogramas:


Fluxogramaexemplo.png


Fluxogramaexemplo2.png




Em POO - diagramas UML:


Diagumlex1.png


Diagumlex2.png

Características da POO

Classes e Objetos

Classesobj.png


Encapsulamento

As classes, que “formatam” os objetos, encapsulam tudo aquilo que é necessário para que este objeto exista, seja particularizado e interaja adequadamente com outros objetos.

Assim, um determinado código não precisa conhecer a estrutura interna de um objeto, para relacionar-se com ele ↠ Basta conhecer sua interface!

Encapsul2.png


Encapsul.png


O objeto relaciona-se, então, com outros objetos por meio da interface (funções externas). Mas seu processamento interno (a implementação) pode ser completamente desconhecido do código usuário

Herança

Uma vez que se tenha definida uma classe mais geral (p.e. “Animal”), chamada de classe-base ou superclasse ou ainda classe-mãe, podemos gerar, a partir desta, classes mais especializadas através de Herança.

Estas sub-classes, ou classe-derivadas herdam todas as características da classe-mãe, além das suas particulares adicionais. Ex: (“Leão”, “Mosca”,”Cão”)

A Herança substitui, com vantagens, a reutilização de código tipo “Ctrl-C/Ctrl-V” pois diminui o número de LOCs, preserva o encapsulamento, etc.

Heranca.png

Funções Virtuais e Polimorfismo

Em cada subclasse que herda um método de uma superclasse, esta função-membro pode ser implementada de formas diferentes. Ex:

Funvirt.jpg


Polimorf2.png

Para que isto ocorra, o método deverá ser declarado como Virtual, na superclasse.

Sobrecarga

As linguagens POO permitem que operadores e funções tenham usos múltiplos - Sobrecarga Exs:

 Operador	Uso I			Uso II	
   <<		Stream de saída:	Desloc. à esq.:
               cout << “Alô Mundo!”	x = y<<2


A sobrecarga possibilita maior flexibilidade no uso dos objetos

Sobrecargaop2.jpg


Sobrecargaop.jpg

Modelagem de sistemas OO

ABSTRAÇÃO - PONTO CRÍTICO e parte mais difícil na construção de um software.


Modelagem bem feita: etapas seguintes consistem em simples CODIFICAÇÃO.


Caso contrário: muitos erros de programação, principalmente semânticos.

Exemplo de Aplicação:

Sistema que vai calcular, para uma determinada matriz de dados de entrada, parâmetros e testes estatísticos e apresentar graficamente os resultados.

Abordagem de programação procedural:

Desenvolve-se várias funções, cada uma para calcular um diferente parâmetro ou para aplicar um determinado teste, tendo a matriz de dados como argumento de entrada.

Ex:

float Aplica_Teste_T(int I, int J, float M[I,J]); 
float Calcula_Coef_Correl(int I, int J, int K, int L, float M[I,J], float N[K,L]);
float Calcula_Media(int J, float M[J]); 

etc.

Uma função ou módulo principal faz a monitoração das ações do usuário e chama as respectivas funções.

Abordagem de POO:

Identifica os diferentes classes e objetos envolvidos. Ex: Dados, Entrada_Dados, Saída_Resultados, Grafico_Saida, etc.

ModelagemOO.png

Para maiores informações:

Diferenças entre C e C++

Entrada e saída

A entrada e saída de dados em C é feita através das funções scanf() e printf(), disponíveis na sua biblioteca padrão. Apesar destas funções ainda estarem disponíveis em C++, vamos preferir utilizar o novo sistema de entrada e saída de dados por fluxo.

Uma vez incluído o arquivo iostream.h, um programa C++ dispõe de três fluxos predefinidos que são abertos automaticamente pelo sistema:

  • cin, que corresponde à entrada padrão
  • cout, que corresponde à saída padrão
  • cerr, que corresponde à saída padrão de erros

O operador << permite inserir valores em um fluxo de saída, enquanto o operador >> permite extrair valores de um fluxo de entrada.

Exemplo:

#include <iostream.h>
void main(void)
{ cout << “Olá mundo!\n“;
}


O operador << pode ser usado, de forma encadeada, para inserir diversos valores em um mesmo fluxo de saída.

Exemplo:

#include <iostream.h>
void main(void)
{
   char nome[80];
   cout << “Qual o seu nome? “;
   cin >> nome;
   cout << “Olá “ << nome << “, tudo bem? \n”;
 }

O operador >> também pode ser usado de forma encadeada.

Exemplo:

#include <iostream.h>
void main(void)
{
   float comprimento, largura;
   cout << "Informe o comprimento e a largura do retângulo: ";
   cin >> comprimento >> largura;
   cout << "Área do retângulo: " << comprimento * largura << " m2\n”;
}

Note a ausência do operador de endereço (&) na sintaxe do fluxo cin.

Ao contrário da função scanf(), não precisamos usar o operador de endereço com o fluxo cin.

Vantagens dos fluxos de entrada e saída:

  • execução mais rápida: a função printf() analisa a cadeia de formatação durante a

execução do programa, enquanto os fluxos são traduzidos durante a compilação;

  • verificação de tipos: como a tradução é feita em tempo de compilação, valores

inesperados, devido a erros de conversão, jamais são exibidos;

  • código mais compacto: apenas o código necessário é gerado; com printf(), porém, o

compilador deve gerar o código correspondente a todos os formatos de impressão;

  • uniformidade sintática: os fluxos também podem ser utilizados com tipos definidos pelo usuário (através da sobrecarga dos operadores >> e <<).

Exemplo:

#include <stdio.h>
#include <iostream.h>
void main(void)
{
  int i = 1234;
  double d = 567.89;
  printf(“\ni = %i, d = %d”, i, d); // erro de conversão!
  cout << "\ni = " << i << ", d = “ << d;
}

Observe a seguir como a função printf() exibe um valor inesperado para a variável d. Isso ocorre porque foi usado um formato de exibição errado (o correto seria %lf ), que não pode ser verificado em tempo de execução.

i = 1234, d = -1202590843
i = 1234, d = 567.89


Manipuladores

Os manipuladores são elementos que determinam o formato em que os dados serão escritos ou lidos de um fluxo.

Os principais manipuladores são:

oct leitura e escrita de um inteiro octal dec leitura e escrita de um inteiro decimal hex leitura e escrita de um inteiro hexadecimal endl insere um caracter de mudança de linha setw(int n) define campo com largura de n caracteres setprecision(int n) define total de dígitos na impressão de números reais setfill(char c) define o caracter usado no preenchimento de campos flush descarrega o buffer após a escrita Exemplo 1.5:

  1. include <iostream.h>
  2. include <iomanip.h>

void main(void) { int i=1234; float p=12.3456F;

cout << "|" << setw(8) << setfill('*') << hex << i << "|\n" << "|" << setw(6) << setprecision(4) << p << "|" << endl; } Resultado da execução: |*****4d2| |*12.35|

Conversões explícitas

Em C++, a conversão explícita de tipos pode ser feita tanto através da notação de cast quanto da notação funcional. Exemplo 1.6: int i, j; double d = 9.87; i = (int)d; // notação “cast” j = int(d); // notação funcional Entretanto, a notação funcional só pode ser usada com tipos simples ou definidos pelo usuário. Para utilizá-la com ponteiros e vetores, precisamos antes criar novos tipos. Exemplo 1.7: typedef int * ptr; int *i; double d; i = ptr(&d); // notação funcional com ponteiro


Definição de variáveis

Em C++ uma variável pode ser declarada em qualquer parte do código, sendo que seu escopo inicia-se no ponto em que foi declarada e vai até o final do bloco que a contém. Exemplo 1.8:

  1. include <iostream.h>

void main(void) { cout << “Digite os valores (negativo finaliza): “; float soma = 0; while( true ) { float valor; cin >> valor; if( valor<0 ) break; soma += valor; } cout << “\nSoma: “ << soma << endl; } Podemos até declarar um contador diretamente dentro de uma instrução for : Exemplo 1.9:

  1. include <iostream.h>

void main(void) { cout << “Contagem regressiva: “ << endl; for(int i=9; i>=0; i--) cout << i << endl; }

O operador de resolução de escopo (::) nos permite acessar uma variável global, mesmo que exista uma variável local com o mesmo nome. Exemplo 1.10:

  1. include <iostream.h>

int n=10; void main(void) { int n=20; { int n=30;

n++; // altera variável global

cout << ::n << “ “ << n << endl; } cout << ::n << “ “ << n << endl; } A saída produzida por esse programa é a seguinte: 11 30 11 20

Constantes

Programadores em C estão habituados a empregar a diretiva #define do preprocessador para definir constantes. Entretanto, a experiência tem mostrado que o uso dessa diretiva é uma fonte de erros difíceis de se detectar. Em C++, a utilização do preprocessador deve ser limitada apenas aos seguintes casos: • inclusão de arquivos; • compilação condicional. Para definir constantes, em C++, usamos a palavra reservada const. Um objeto assim especificado não poderá ser modificado durante toda a sua existência e, portanto, é imprescindível inicializar uma constante no momento da sua declaração. Exemplo 1.11: const float pi = 3.14; const int meses = 12; const char *msg = “pressione enter...”; É possível usar a palavra const também na definição de ponteiros. Nesse caso, deve estar bem claro o que será constante: o objeto que aponta ou aquele que é apontado. Exemplo 1.12: const char * ptr1 = “um”; // o objeto apontado é constante char * const ptr2 = “dois”; // o objeto que aponta é constante const char * const ptr3 = “três”; // ambos são constantes


Tipos compostos

Assim como em C, em C++ podemos definir novos tipos de dados usando as palavras reservadas struct, enum e union. Mas, ao contrário do que ocorre na linguagem C, a utilização de typedef não é mais necessária para renomear o tipo. Exemplo 1.12: struct Ficha { char *nome; char *fone; }; Ficha f, *pf; Em C++, cada enumeração enum é um tipo particular, diferente de int, e só pode armazenar aqueles valores enumerados na sua definição. Exemplo 1.13: enum Logico { falso, verdade }; Logico ok; ok = falso; ok = 0; // erro em C++, ok não é do tipo int ok = Logico(0); // conversão explícita permitida 1.8. Referências Além de ponteiros, a linguagem C++ oferece também as variáveis de referência. Esse novo recurso permite criar uma variável como sendo um sinônimo de uma outra. Assim, modificando-se uma delas a outra será também, automaticamente, atualizada. Exemplo 1.14:

  1. include <iostream.h>

void main(void) { int n=5; int &nr = n; // nr é uma referência a n int *ptr = &nr; // ptr aponta nr (e n também!) cout << “n = “ << n << “ nr = “ << nr << endl; n += 2; cout << “n = “ << n << “ nr = “ << nr << endl;

  • ptr = 3;

cout << “n = “ << n << “ nr = “ << nr << endl; } A saída produzida por esse programa é a seguinte: n = 5 nr = 5 n = 7 nr = 7 n = 3 nr = 3 Uma variável de referência deve ser obrigatoriamente inicializada e o tipo do objeto referenciado deve ser o mesmo do objeto que referencia.


Alocação de memória

C++ oferece dois novos operadores, new e delete, em substituição respectivamente às funções malloc() e free(), embora estas funções continuem disponíveis. O operador new aloca um espaço de memória, inicializa-o e retorna seu endereço. Caso a quantidade de memória solicitada não esteja disponível, o valor NULL é devolvido. Exemplo 1.15: int *p1 = new int; // aloca espaço para um int int *p2 = new int(5); // aloca um int com valor inicial igual a 5 int *p3 = new int[5]; // aloca espaço para um vetor com 5 elementos int (*p4)[3] = new int[2][3]; // aloca uma matriz de int 2x3 Cuidado para não confundir a notação: int *p = new int(5); // aloca espaço para um int e armazena nele o valor 5 int *q = new int[5]; // aloca espaço para um vetor com 5 elementos do tipo int O operador delete libera um espaço de memória previamente alocado por new, para um objeto simples. Para liberar espaço alocado a um vetor, devemos usar o operador delete[]. A aplicação do operador delete a um ponteiro nulo é legal e não causa qualquer tipo de erro; na verdade, a operação é simplesmente ignorada. Exemplo 1.16: delete p; // libera um objeto delete[] q; // libera um vetor de objetos É preciso observar que: • a cada operação new deve corresponder uma operação delete; • é importante liberar memória assim que ela não seja mais necessária; • a memória alocada é liberada automaticamente no final da execução do programa.

Protótipos

Um protótipo é uma declaração que especifica a interface de uma função. Nessa declaração devem constar o tipo da função, seu nome e o tipo de cada um de seus parâmetros. Ao contrário da norma C-ANSI, que estabelece que uma função declarada com uma lista de argumentos vazia – f( ) – pode receber qualquer número de parâmetros, de quaisquer tipos, em C++, uma tal declaração equivale a f(void). Uma função cujo tipo não seja void deve, obrigatoriamente, devolver um valor através do comando return. Exemplo 2.1: int f(int,int); // a função f recebe dois parâmetros int e retorna int double g(char,int); // a função g recebe um char e um int e retorna double A presença do nome de um parâmetro no protótipo é opcional; entretanto, recomendase que ele conste da declaração sempre que isso aumentar a legibilidade do programa. Exemplo 2.2: int pesquisa(char *lista[],int tam, char *nome); void cursor(int coluna, int linha); 2.2. Passagem por referência Além da passagem por valor, C++ também permite passar parâmetros por referência. Quando usamos passagem por referência, o parâmetro formal declarado na função é na verdade um sinônimo do parâmetro real que foi passado a ela. Assim, qualquer alteração realizada no parâmetro formal ocorre também no parâmetro real. Para indicar que um parâmetro esta sendo passado por referência, devemos usar o operador & prefixado ao seu nome. Exemplo 2.3:

  1. include <iostream.h>

void troca(int &, int &); // parâmetros serão passados por referência void main(void) { int x=5, y=7; cout << “Antes : x=“ << x << “, y=” << y << endl; troca(x,y); cout << “Depois: x=“ << x << “, y=” << y << endl; }


void troca(int &a, int &b) // parâmetros recebidos por referência { int x = a; a = b; b = x; } A execução do programa acima causa a seguinte saída: Antes : x=5, y=7 Depois: x=7, y=5 A chamada de uma função com parâmetros de referência é muito simples. Essa facilidade aumenta a legibilidade e a potência da linguagem; mas deve ser usada com cautela, já que não há proteção ao valor do parâmetro real que é transmitido à função. O uso da palavra reservada const na declaração dos parâmetros de referência permite anular o risco de alteração do parâmetro real, ao mesmo tempo em que evita que seu valor tenha que ser duplicado na memória, o que ocorreria numa passagem por valor. Exemplo 2.4:

  1. include <iostream.h>

struct Ficha { char nome[20]; char email[30]; }; void exibe(const Ficha &f) { cout << f.nome << “: “ << f.email << endl; } void main(void) { Ficha usr = { “Silvio”, “slago@ime.usp.br” }; exibe(usr); } Referências e ponteiros podem ser combinados, sem nenhum problema: Exemplo 2.4: bool abrir(FILE *&arquivo, char *nome) { if( (arquivo=fopen(nome,”r”))==NULL ) // se arquivo não existe arquivo=fopen(nome,”w”); // cria arquivo vazio return arquivo!=NULL; // informa se o arquivo foi aberto } Uma alternativa seria passar o parâmetro arquivo à função abrir() como um ponteiro de ponteiro, mas isso tornaria o código bem menos legível. Funções 10 2.3. Parâmetros com valores default Certos argumentos de uma função têm sempre os mesmos valores. Para não ter que especificar esses valores cada vez que a função é chamada, a linguagem C++ permite declará-los como default. Exemplo 2.5:

  1. include <iostream.h>
  2. include <stdlib.h>

void exibe(int num, int base=10); // base tem valor default igual a 10 void main(void) { exibe(13); // decimal, por default exibe(13,2); // binário exibe(13,16); // hexadecimal } void exibe(int num, int base) { char str[100]; itoa(num,str,base); cout << str << endl; } Parâmetros com valores default devem necessariamente ser os últimos da lista e podem ser declarados tanto no protótipo quanto no cabeçalho da função, desde que tal declaração apareça antes de qualquer uso da função.

Funções inline

A palavra-chave inline substitui vantajosamente a utilização da diretiva #define do preprocessador para definir pseudo-funções, ou seja, macros. Cada chamada de uma função inline é substituída por uma cópia do seu código, o que aumenta bastante a velocidade de execução do programa. Note porém que, por motivos de economia de memória, apenas funções muito curtas devem ser declaradas inline. A vantagem é que essas funções se comportam como funções normais e, portanto, permitem a consistência de tipo de seus parâmetros, o que não é possível com #define. Exemplo 2.6:

  1. include <iostream.h>

inline double sqr(double n) { return n * n; } void main(void) { cout << sqr(10) << endl; } Ao contrário das funções normais, as funções inline somente são visíveis dentro do arquivo no qual são definidas.


Sobrecarga de funções

A assinatura de uma função se define por:

  • o tipo de valor que ela devolve;
  • seu nome;
  • a lista de tipos dos parâmetros formais.

Entretanto, apenas os dois últimos desses atributos são discriminantes, isto é, servem para identificar que função está sendo chamada num ponto qualquer do código.

Podemos usar essa propriedade para dar um mesmo nome a duas ou mais funções diferentes, desde que elas tenham listas de parâmetros distintas.

O compilador selecionará a função a ser chamada tomando como base o número e o tipo dos parâmetros reais especificados na sua chamada.

Como essa escolha é feita em tempo de compilação, as funções sobrecarregadas têm desempenho idêntico às funções clássicas.

Dizemos que as chamadas de funções sobrecarregadas são resolvidas de maneira estática.

Exemplo 2.7:

#include <iostream.h>
int soma(int a, int b)
{
  return a+b;
}
int soma(int a, int b, int c)
{
 return a+b+c;
}
double soma(double a, double b)
{
 return a+b;
}
void main(void)
{
  cout << soma(1,2) << endl;
  cout << soma(3,4,5) << endl;
  cout << soma(6.7,8.9) << endl;
}


AULA 1 - Introdução Revisão de linguagem C >>

<<= Página do curso