ISCTE - IGE/ETI

Programação I/Introdução à Programação

Exame de 2ª época

1998/1999,  1º semestre


A azul (e indentadas uma vez) as respostas às questões

Questão 1

Assinale com V (Verdadeiro) as expressões que estão correctas e com F (Falso) as que estão incorrectas.

Deve preencher todos os espaços indicados por um sublinhado (___) com V ou F.  Qualquer espaço não preenchido será considerado como uma resposta errada.

Em geral as alíneas podem ter zero ou mais respostas correctas.  Cada resposta correctamente assinalada vale 0,5 valores.

Nas alíneas em que apenas uma resposta está correcta (estão assinaladas no texto), responder com mais ou menos do que um V anula a cotação.  A resposta correcta corresponde à cotação completa.  Qualquer outra resposta corresponde a zero valores.

Em todos os casos em que não é explicitamente referida a localização de uma instrução, considere que esta é dada na função main() do programa seguinte:

#include <iostream>
using namespace std;

class A {
  private:
    int x[100];
    int y;
    int f1();

  public:
    int z;
    A(int a = 1, int b = 2, int c = 3) {
        y = b;
        ...
    }
    int f2(int);
    bool f3(int);
};

int main() {
    ...
}

Questão 1.1

Após a seguinte definição:
A b(1);
que valor tem a variável y (variável membro da instância b da classe A) [apenas uma resposta correcta]?
 F  1
 V  2
A definição invoca o contrutor com um único argumento de valor 1, pelo que a variável membro y é inicializada com o valor por omissão do parâmetro b, que é 2.
[cotação: 1,0]

Questão 1.2

Assuma que está correctamente declarada uma matriz a de 10 elementos da classe A e nada mais:
A a[10];
Quais das seguintes instruções estão correctas?

 F  int b = a.z[3];

A variável a é uma matriz, pelo que a utilização directa do operador de selecção de membro . não faz sentido.
 V  int b = a[4].f2(2);
A variável a é uma matriz, sendo portanto a[4] uma expressão que se refere ao 5º elemento dessa matriz.  Esse elemento é da classe A, pelo que é lícito invocar a função membro f2(), que tem de ser invocada com um argumento inteiro.
 F  int b = a[3].x[2];
Neste caso o problema é que a variável membro x é privada, pelo que não pode ser usada directamente em código que não pertença a uma função ou procedimento membro de A (assume-se que o código existe em main())
 F  int b = a.y;
Neste caso o problema é idêntico.
[cotação: 2]

Questão 1.3

Assuma que as seguintes instruções são dadas dentro de uma função membro da classe A, que não tem quaisquer argumentos, e que não são declaradas quaisquer variáveis dentro dessa função.  Quais das seguintes instruções estão correctas quer do ponto de vista sintáctico quer do ponto de vista lógico?

 F  int b = x;

A variável x é uma matriz, pelo que a sua atribuição a um inteiro não faz sentido.
 F  int b = f2();
A função f2() exige um argumento inteiro, pelo que a sua invocação está errada.
 F  int b = x[100];
A instrução está sintacticamente correcta, mas a matriz x tem apenas 100 elementos, pelo que é um erro tentar aceder ao elemento índice 100 (os índices válidos variam de 0 a 99 inclusivé).
 F  int b = a.f1() + a.y;
Não existe qualquer variável de nome a disponível.
[cotação: 2]

Questão 2.1

Considere uma função int produtoInterno(int m1[], int m2[], int n) que devolve o produto interno de dois vectores dados pelas matrizes m1 e m2, ambas com n elementos.  Dada a pré-condição n > 0, e assumindo que as matrizes m1 e m2 têm ambas n elementos, indique a condição objectivo, condição invariante e guarda do ciclo necessário à construção desta função.  Relembra-se que o produto interno de dois vectores (m1 e m2) é dado por m1[0] m2[0] + m1[1] m2[1] + ... + m1[n-1] m2[n-1].

[cotação: 1]

PC: n > 0
CI: r = (S k : 0 < k < i : m1[k] m2[k]) e 0 < i <= n
CO: r = (S k : 0 <= k < n : m1[k] m2[k])
G: i <> n

Questão 2.2

Defina completamente a função indicada na alínea anterior.

[cotação: 1]

int produtoInterno(int m1[], int m2[], int n) {
    int r = m1[0] * m2[0];
    for(int i = 1; i != n; ++i)
        r += m1[i] * m2[i];
    return r;
}

Questão 3

Considere o problema da criação de um pequeno programa para gerir a sua agenda pessoal de contactos.  Assuma que não vai necessitar de ter nessa agenda mais do que 100 contactos de cada vez.  Para cada contacto é necessário guardar o nome da pessoa em questão (uma sequência de caracteres) e o seu telefone (também uma sequência de caracteres).  Durante o funcionamento do programa irá necessitar de inserir novos contactos na lista, retirar contactos de pessoas e alterar o número de telefone de uma determinada pessoa que se encontra na lista.

Questão 3.1

Defina uma classe Contacto que possa guardar a informação sobre o nome de uma pessoa e o seu telefone.  Deverá ser possível criar variáveis da classe Contacto com dois argumentos (nome e número de telefone), apenas com o nome ou sem qualquer argumento.  Deve declarar também a sobrecarga do operador de igualdade (==) que compara dois contactos e devolve true caso os seus nomes sejam iguais e false no caso contrário.  Deve também declarar a sobrecarga do operador << que permite visualizar no ecrã os dados de um contacto.  Não é necessário nesta questão definir qualquer um dos métodos (funções ou procedimentos membro).

[cotação: 2]

class Contacto {
  public:
    Contacto(const std::string& nome = "",
             const std::string& telefone = "");
    std::string nome() const;
    std::string telefone() const;

  private:
    std::string nome_;
    std::string telefone_;
};

bool operator == (const Contacto& a, const Contacto& b);

std::ostream& operator << (std::ostream& saída, const Contacto& c);

Questão 3.2

Defina uma classe Contactos que deve guardar a informação sobre um conjunto de contactos (pode assumir que nunca necessitará de guardar mais do que 100 contactos).  Deve declarar os métodos necessários para inserir e retirar um contacto (dado o respectivo nome).  Deve também declarar uma função que, dada o nome de uma pessoa existente, devolva o seu número de telefone.  Não é necessário nesta questão definir qualquer um dos métodos.  Não pode utilizar a classe que implementa o conceito de lista pertencente à biblioteca padrão do C++ nem qualquer outra classe que implemente o mesmo conceito (a não ser que a defina completamente no exame).   Pode e deve usar a class string da biblioteca padrão do C++.

[cotação: 2]

class Contactos {
  public:
    Contactos();
    void insereContacto(const Contacto& contacto);
    void retiraContacto(const std::string& nome);
    std::string telefone(const string& nome);

  private:
    static const int limite = 100;
    Contacto contactos[limite];
    int número_de_contactos;
};

Questão 3.3

Defina o(s) construtor(es) da classe Contacto.
Contacto::Contacto(const std::string& nome,
                   const std::string& telefone)
    : nome_(nome), telefone_(telefone) {
}
[cotação: 1]

Questão 3.4

Defina o operador de igualdade (==) entre dois contactos sabendo que dois contactos são iguais se e só se o seu nome for igual (independentemente do seu telefone).

[cotação: 1]

bool operator == (const Contacto& a, const Contacto& b) {
    return a.nome() == b.nome();
}

Questão 3.5

Defina o operador << para a classe Contacto que permite que as seguintes instruções:
Contacto c("Manuel Maria", "1234567");

std::cout << c << std::endl;

resultem em aparecer no ecrã o seguinte texto:
O telefone do Manuel Maria é 1234567.
[cotação: 1]
std::ostream& operator << (std::ostream& saída, const Contacto& c) {
    return saída << "O telefone do " << c.nome() << " é " << c.telefone()
                 << '.';
}

Questão 3.6

Defina o método da classe Contactos que devolve o número de telefone de uma pessoa dado como argumento o seu nome.  Caso o nome não exista deverá devolver uma sequência de caracteres (cadeia) vazia.

[cotação: 2]

std::string Contactos::telefone(const string& nome) {
    for(int i = 0; i != número_de_contactos; ++i)
        if(contactos[i].nome() == nome)
            return contactos[i].telefone();
    return "";
}

Questão 3.7

Faça um pequeno programa que use uma variável da classe Contactos e peça para inserir nesta lista três contactos cujos dados devem ser introduzidos pelo utilizador.  Não é necessário repetir a declaração ou definição da classe Contactos.  Admita que os nomes e telefones a inserir não contêm nenhum espaço (' ').

[cotação: 1]

int main()
{
    Contactos c;

    for(int i = 0; i != 3; ++i) {
        cout << "Introduza o nome: " << endl;
        string n;
        cin >> n;
        cout << "Introduza o telefone: " << endl;
        string t;
        cin >> t;
        c.insereContacto(Contacto(n, t));
    }
}

Questão 4

Defina completamente uma classe Polígono que contenha uma sequência de n vértices (cada vértice é um par ordenado de valores decimais, i.e., uma coordenada no plano).  Assuma que qualquer polígono tem no máximo 20 vértices.  A classe Polígono deve suportar as seguintes operações:
Construtores:
Deve existir um construtor por omissão (que cria um polígono degenerado com zero vértices) e um construtor que receba a sequência de vértices (na forma de uma matriz bidimensional com n × 2 elementos, sendo n dado por um segundo parâmetro).

 
Operador de igualdade (==):
Dois polígonos são iguais se são constituídos pelos mesmos vértices pela mesma ordem.  Tenha em atenção que a ordem dos vértices é importante mas que o primeiro vértice considerado na sequência é irrelevante (e.g., os triângulos ((1,1), (2,2), (3,3)) e ((1,1), (3,3), (2,2)) devem ser considerados diferentes e os triângulos ((1,1), (2,2), (3,3)) e ((2,2), (3,3), (1,1)) devem ser considerados iguais).  Admita que os polígonos não contêm vértices repetidos.

 
void adicionaVértice(double x, double y):
Adiciona o vértice (x, y) ao polígono.

 
void retiraVértice (double x, double y):
Retira o primeiro vértice de coordenadas (x, y), caso exista.  Se não existir não faz nada.
[cotação: 2]
class Polígono {
  public:
    Polígono();
    Polígono(double v[][2], int n);

    void adicionaVértice(double x, double y);
    void retiraVértice(double x, double y);

  private:
    friend bool operator == (const Polígono& a, const Polígono& b);
    friend std::ostream& operator << (std::ostream& o, const Polígono& p);

    static const int limite = 20;
    int número_de_vértices;
    double vértices[limite][2];
};

Polígono::Polígono(): número_de_vértices(0) {
}

Polígono::Polígono(double v[][2], int n) : número_de_vértices(n) {
    // Número de vértices não pode exceder o limite:
    if(n > limite)
        exit(1);

    for(int i = 0; i != n; ++i) {
        vértices[i][0] = v[i][0];
        vértices[i][1] = v[i][1];
    }
}

void Polígono::adicionaVértice(double x, double y) {
    // Não se pode exceder o limite de vértices:
    if(número_de_vértices == limite)
        return;
    vértices[número_de_vértices][0] = x;
    vértices[número_de_vértices++][1] = y;
}

void Polígono::retiraVértice(double x, double y) {
    for(int i = 0; i != número_de_vértices; ++i)
        if(vértices[i][0] == x && vértices[i][1] == y) {
            // Retira vértice compactando matriz:
            for(; i != número_de_vértices - 1; i++) {
                vértices[i][0] = vértices[i + 1][0];
                vértices[i][1] = vértices[i + 1][1];
            }
            --número_de_vértices;
        }
}

// Apresentam-se duas versões.  A primeira admite que os vértices de uma polígono
// são todos diferentes, como sugerido no enunciado.  A segunda é mais genérica.
bool operator == (const Polígono& a, const Polígono& b) {
    if(a.número_de_vértices != b.número_de_vértices)
        return false;

    int n = a.número_de_vértices;

    // Se os polígonos não têm vértices então são iguais:
    if(n == 0)
        return true;

    // Procurar em b o vértice igual ao primeiro de a:
    for(int i = 0; i != n; ++i)
        if(a.vértices[0][0] == b.vértices[i][0] &&
           a.vértices[0][1] == b.vértices[i][1]) {

            // Verificar se os restantes são iguais:
            ++i;
            for(int j = 1; j != n; ++j, ++i) {
                if(a.vértices[j][0] != b.vértices[i % n][0] ||
                   a.vértices[j][1] != b.vértices[i % n][1])
                    return false;

            // Se aqui se chega é porque são todos iguais:
            return true;
        }

    // Só se chega aqui se não se encontrou:
    return false;
}

bool operator == (const Polígono& a, const Polígono& b) {
    if(a.número_de_vértices != b.número_de_vértices)
        return false;

    int n = a.número_de_vértices;

    // Se os polígonos não têm vértices então são iguais:
    if(n == 0)
        return true;

    for(int i = 0; i != n; ++i) {
        // Verificar se são iguais começando em zero para a e em i para b:
        bool iguais = true;
        for(int j = 0; j != n && iguais; ++j)
            if(a.vértices[j][0] != b.vértices[(i + j) % n][0] ||
               a.vértices[j][1] != b.vértices[(i + j) % n][1])
                iguais = false;
        if(iguais)
            return true;

        // Caso contrário procura-se mais à frente.
    }

    // Só se chega aqui se não são iguais:
    return false;
}

// Este operador não era pedido no enunciado:
std::ostream& operator << (std::ostream& saída, const Polígono& p) {
    saída << '(';
    for(int i = 0; i != p.número_de_vértices; ++i) {
        if(i != 0)
            saída << ", ";
        saída << '(' << p.vértices[i][0] << ", "
              << p.vértices[i][1] << ')';
    }
    return saída << ')';
}

// Note-se que tudo se simplificaria se se definisse uma classe Coordenada para as coordenadas 2D.

Questão 5

Explique (sucintamente) o quais as diferenças entre membros públicos e privados de uma classe e quais as vantagens da utilização de membros privados.

[cotação: 1]

A diferença entre membros públicos e privados de uma classe está em quem tem direito de lhes aceder.  Só têm acesso aos membros privados de uma classe outros membros ou amigos da mesma classe.  A membros públicos pode-se aceder sem restrições.  A vantagem principal da utilização de membros privados é a de esconder pormenores de implementação da classe de quem a vai usar.  Para compreender bem esta vantagem é importante compreender que uma classe é implementada para posteriores utilizações, ou seja, para uma classe existe o "programador fabricante", que implementa a classe e se preocupa com a sua implementação, e o "programador utilizador", que usa a classe sem se preocupar com o "como ela faz", preocupando-se apenas com "o que ela faz".  Claro está que este "programador utilizador " da classe pode estar por sua vez a fabricar outra classe, pelo que assume os dois papeis simultaneamente, tal como o marceneiro fabrica móveis usando um maço, que até pode ter sido ele a fabricar.  À capacidade de olhar para uma entidade duma forma mais global sem preocupações com os pormenores chama-se capacidade de abstracção e é fundamental em qualquer programador.

O esconder dos pormenores de implementação não é apenas uma vantagem para o "utilizador programador".  O fabricante, ao ter a garantia de que o utilizador bem intencionado não acederá aos membros privados da classe (tipicamente variáveis membro), pode fazer assumpções acerca dos seus valores que não poderia de outra forma.  Por exemplo, para a classe dos racionais falada nas folhas da cadeira, o programador da classe pode assumir que o denominador é positivo no início de cada função membro, o que lhe simplifica bastante a vida...

(Nota: a sua resposta poderia ser bem mais sucinta...)