Pasos siguientes

Introducción a la programación y C++

Este instructivo en línea continúa con conceptos más avanzados. Lee la parte III. En este módulo, nos enfocaremos en usar punteros y comenzar a usar objetos.

Aprende del ejemplo 2

Nuestro enfoque en este módulo es adquirir más práctica con la descomposición, comprender los punteros y comenzar con objetos y clases. Revisa los siguientes ejemplos. Escribe los programas tú mismo cuando se te solicite o realiza los experimentos. No podemos enfatizar lo suficiente que la clave para convertirse en un buen programador es la práctica, la práctica y la práctica.

Ejemplo 1: Más práctica de descomposición

Considera el siguiente resultado de un juego simple:

Welcome to Artillery.
You are in the middle of a war and being charged by thousands of enemies.
You have one cannon, which you can shoot at any angle.
You only have 10 cannonballs for this target..
Let's begin...

The enemy is 507 feet away!!!
What angle? 25<
You over shot by 445
What angle? 15
You over shot by 114
What angle? 10
You under shot by 82
What angle? 12
You under shot by 2
What angle? 12.01
You hit him!!!
It took you 4 shots.
You have killed 1 enemy.
I see another one, are you ready? (Y/N) n

You killed 1 of the enemy.

La primera observación es el texto de introducción que se muestra una vez por ejecución del programa. Necesitamos un generador de números al azar para definir la distancia del enemigo para cada ronda. Necesitamos un mecanismo para obtener la entrada del ángulo por parte del jugador y, obviamente, está en una estructura de bucle, ya que se repite hasta golpear al enemigo. También necesitamos una función para calcular la distancia y el ángulo. Por último, debemos hacer un seguimiento de cuántos disparos hicieron para golpear al enemigo, así como también cuántos enemigos hemos alcanzado durante la ejecución del programa. A continuación, se muestra un posible esquema para el programa principal.

StartUp(); // This displays the introductory script.
killed = 0;
do {
  killed = Fire(); // Fire() contains the main loop of each round.
  cout << "I see another one, care to shoot again? (Y/N) " << endl;
  cin >> done;
} while (done != 'n');
cout << "You killed " << killed << " of the enemy." << endl;

El procedimiento de Incendio controla la forma en que se juega el juego. En esa función, llamamos a un generador de números al azar para obtener la distancia del enemigo y, luego, configuramos el bucle para obtener la entrada del jugador y calcular si golpeó o no al enemigo. La condición de la guardia en el bucle es qué tan cerca estamos de golpear al enemigo.

In case you are a little rusty on physics, here are the calculations:

Velocity = 200.0; // initial velocity of 200 ft/sec Gravity = 32.2; // gravity for distance calculation // in_angle is the angle the player has entered, converted to radians. time_in_air = (2.0 * Velocity * sin(in_angle)) / Gravity; distance = round((Velocity * cos(in_angle)) * time_in_air);

Debido a las llamadas a cos() y sin(), deberás incluir math.h. Intenta escribir este programa, ya que es una buena práctica para la descomposición de problemas y una buena revisión de C++ básico. Recuerda realizar solo una tarea en cada función. Este es el programa más sofisticado que escribimos hasta ahora, por lo que puede llevar un poco de tiempo.Esta es nuestra solución. 

Ejemplo 2: Practica con punteros

Cuando trabajes con punteros, debes tener en cuenta cuatro aspectos:
  1. Los punteros son variables que contienen direcciones de memoria. Mientras se ejecuta un programa, todas las variables se almacenan en la memoria, cada una en su propia dirección o ubicación única. Un puntero es un tipo especial de variable que contiene una dirección de memoria en lugar de un valor de datos. Así como se modifican los datos cuando se usa una variable normal, el valor de la dirección almacenada en un puntero se modifica a medida que se manipula una variable de puntero. Por ejemplo:
    int *intptr; // Declare a pointer that holds the address
                 // of a memory location that can store an integer.
                 // Note the use of * to indicate this is a pointer variable.
    
    intptr = new int; // Allocate memory for the integer.
    *intptr = 5; // Store 5 in the memory address stored in intptr.
          
  2. Por lo general, decimos que un puntero "apunta" a la ubicación que almacena (el "puntero"). Por lo tanto, en el ejemplo anterior, intptr hace referencia al Pointee 5.

    Observa el uso del operador "new" para asignar memoria a nuestro Pointee entero. Esto es algo que debemos hacer antes de intentar acceder al punto.

    int *ptr; // Declare integer pointer.
    ptr = new int; // Allocate some memory for the integer.
    *ptr = 5; // Dereference to initialize the pointee.
    *ptr = *ptr + 1; // We are dereferencing ptr in order
                     // to add one to the value stored
                     // at the ptr address.
          

    El operador * se usa para la desreferencia en C. Uno de los errores más comunes que cometen los programadores de C/C++ al trabajar con punteros es olvidar inicializar el punto. En ocasiones, esto puede causar una falla en el tiempo de ejecución porque estamos accediendo a una ubicación en la memoria que contiene datos desconocidos. Si intentamos modificar estos datos, podemos causar una corrupción sutil de memoria, lo que dificulta la búsqueda de errores. 

  3. La asignación de punteros entre dos punteros hace que apunten al mismo puntero. Entonces, la asignación y = x; hace y apunte al mismo puntero que x. La asignación del puntero no toca al punto. Solo cambia un puntero para que tenga la misma ubicación que otro. Después de la asignación del puntero, los dos punteros "comparten" el punto. 
  4. void main() {
     int* x; // Allocate the pointers x and y
     int* y; // (but not the pointees).
    
     x = new int; // Allocate an int pointee and set x to point to it.
    
     *x = 42; // Dereference x and store 42 in its pointee
    
     *y = 13; // CRASH -- y does not have a pointee yet
    
     y = x; // Pointer assignment sets y to point to x's pointee
    
     *y = 13; // Dereference y to store 13 in its (shared) pointee
    }
      

Aquí hay un seguimiento de este código:

1. Asigna dos punteros x e y. La asignación de punteros no asigna ningún puntero.
2. Asigna un punto y configura x para que apunte a él.
3. Anula la referencia de x para almacenar 42 en su puntero. Este es un ejemplo básico de una operación de desreferencia. Comienza en x y sigue la flecha para acceder al puntero.
4. Intenta desviar y para guardar 13 en su puntero. Esta falla porque no tienes un puntero. Nunca se le asignó uno.
5. Asigna y = x; de modo que y se dirija al puntero de x. Ahora, x e y indican al mismo puntero (es decir, "compartiendo").
6. Intenta desviar y para guardar 13 en su puntero. Esta vez funciona porque la tarea anterior le dio un puntero.

Como puedes ver, las imágenes son muy útiles para comprender el uso del puntero. Aquí hay otro ejemplo.

int my_int = 46; // Declare a normal integer variable.
                 // Set it to equal 46.

// Declare a pointer and make it point to the variable my_int
// by using the address-of operator.
int *my_pointer = &my_int;

cout << my_int << endl; // Displays 46.

*my_pointer = 107; // Derefence and modify the variable.

cout << my_int << endl; // Displays 107.
cout << *my_pointer << endl; // Also 107.

Observa en este ejemplo que nunca asignamos memoria con el operador "new". Declaramos una variable de número entero normal y la manipulamos mediante punteros.

En este ejemplo, se ilustra el uso del operador de eliminación, que desasigna la memoria del montón, y cómo podemos realizar la asignación para estructuras más complejas. Abordaremos la organización de la memoria (montón y pila del entorno de ejecución) en otra lección. Por ahora, solo piensa en el montón como un almacenamiento gratuito de memoria disponible para los programas en ejecución.

int *ptr1; // Declare a pointer to int.
ptr1 = new int; // Reserve storage and point to it.

float *ptr2 = new float; // Do it all in one statement.

delete ptr1; // Free the storage.
delete ptr2;

En este último ejemplo, mostramos cómo se usan los punteros para pasar valores por referencia a una función. Así es como modificamos los valores de las variables dentro de una función.

// Passing parameters by reference.
#include <iostream>
using namespace std;

void Duplicate(int& a, int& b, int& c) {
  a *= 2;
  b *= 2;
  c *= 2;
}

int main() {
  int x = 1, y = 3, z = 7;
  Duplicate(x, y, z);
  // The following outputs: x=2, y=6, z=14.
  cout << "x="<< x << ", y="<< y << ", z="<< z;
  return 0;
}

Si dejamos los argumentos &'s fuera de la definición de la función duplicada, pasamos las variables "por valor", es decir, se realiza una copia del valor de la variable. Cualquier cambio realizado en la variable de la función modificará la copia. No modifican la variable original.

Cuando se pasa una variable por referencia, no pasamos una copia de su valor, sino que se pasa la dirección de la variable a la función. Cualquier modificación que hagamos a la variable local modifica en realidad la variable original que se pasó. 

Si eres un programador en C, este es un giro nuevo. Podríamos hacer lo mismo en C declarando Duplicate() como Duplicate(int *x), en cuyo caso x es un puntero a un int, luego llama a Duplicate() con el argumento &x (dirección de x) y usa la desreferencia de x en Duplicate() (ver a continuación). Sin embargo, C++ proporciona una manera más simple de pasar valores a las funciones por referencia, aunque la forma anterior de hacerlo aún funciona.

void Duplicate(int *a, int *b, int *c) {
  *a *= 2;
  *b *= 2;
  *c *= 2;
}

int main() {
  int x = 1, y = 3, z = 7;
  Duplicate(&x, &y, &z);
  // The following outputs: x=2, y=6, z=14.
  cout << "x=" << x << ", y=" << y << ", z=" << z;
  return 0;
}

Ten en cuenta que, con las referencias de C++, no es necesario pasar la dirección de una variable ni anular la referencia de la variable dentro de la función llamada.

¿Cuál es el resultado del siguiente programa? Haz un dibujo de la memoria para averiguarlo.

void DoIt(int &foo, int goo);

int main() {
  int *foo, *goo;
  foo = new int;
  *foo = 1;
  goo = new int;
  *goo = 3;
  *foo = *goo + 3;
  foo = goo;
  *goo = 5;
  *foo = *goo + *foo;
  DoIt(*foo, *goo);
  cout << (*foo) << endl;
}

void DoIt(int &foo, int goo) {
  foo = goo + 3;
  goo = foo + 4;
  foo = goo + 3;
  goo = foo;
} 

Ejecuta el programa para ver si obtuviste la respuesta correcta.

Ejemplo 3: Cómo pasar valores por referencia

Escribe una función llamada acelerado() que tome como entrada la velocidad de un vehículo y una cantidad. La función agrega la cantidad a la velocidad para acelerar el vehículo. El parámetro de velocidad se debe pasar por referencia y cantidad por valor. Aquí está nuestra solución.

Ejemplo 4: Clases y objetos

Considera la siguiente clase:

// time.cpp, Maggie Johnson
// Description: A simple time class.

#include <iostream>
using namespace std;

class Time {
 private:
  int hours_;
  int minutes_;
  int seconds_;
 public:
  void set(int h, int m, int s) {hours_ = h; minutes_ = m; seconds_ = s; return;}
  void increment();
  void display();
};

void Time::increment() {
  seconds_++;
  minutes_ += seconds_/60;
  hours_ += minutes_/60;
  seconds_ %= 60;
  minutes_ %= 60;
  hours_ %= 24;
  return;
}

void Time::display() {
  cout << (hours_ % 12 ? hours_ % 12:12) << ':'
       << (minutes_ < 10 ? "0" :"") << minutes_ << ':'
       << (seconds_ < 10 ? "0" :"") << seconds_
       << (hours_ < 12 ? " AM" : " PM") << endl;
}

int main() {
  Time timer;
  timer.set(23,59,58);
  for (int i = 0; i < 5; i++) {
    timer.increment();
    timer.display();
    cout << endl;
  }
}

Observa que las variables de miembro de la clase tienen un guion bajo al final. Esto se hace para diferenciar entre las variables locales y las variables de clase.

Agrega un método de disminución a esta clase. Aquí está nuestra solución.

Las maravillas de la ciencia: Informática

Ejercicios

Al igual que en el primer módulo de este curso, no proporcionamos soluciones para los ejercicios y proyectos.

Recuerde que es un buen programa...

... se descomponen de manera lógica en funciones en las que cualquier función realiza una sola tarea.

... tiene un programa principal que funciona como un esquema de lo que hará el programa.

... tiene nombres descriptivos de funciones, constantes y variables.

Usa constantes para evitar cualquier número "mágico" en el programa.

... tiene una interfaz de usuario sencilla.

Ejercicios de calentamiento

  • Ejercicio 1

    El número entero 36 tiene una propiedad peculiar: es un cuadrado perfecto y también es la suma de los números enteros del 1 al 8. El siguiente número es 1225, que es 352, y la suma de los números enteros del 1 al 49. Encuentra el siguiente número que sea un cuadrado perfecto y también la suma de una serie 1...n. Puede ser mayor que 32,767. Puedes usar funciones de biblioteca que conozcas (o fórmulas matemáticas) para que tu programa se ejecute más rápido. También es posible escribir este programa con bucles for para determinar si un número es un cuadrado perfecto o una suma de una serie. Nota: Según tu máquina y tu programa, encontrar este número puede tardar bastante.

  • Ejercicio 2

    Tu librería universitaria necesita tu ayuda para calcular los negocios para el próximo año. La experiencia demuestra que las ventas dependen en gran medida de si se requiere un libro para un curso o solo es opcional, y de si se usó o no en la clase. Un nuevo libro de texto obligatorio se venderá al 90% de los posibles inscritos, pero si ya se usó en la clase con anterioridad, solo el 65% lo comprará. Del mismo modo, el 40% de los posibles inscritos comprará un nuevo libro de texto opcional, pero si se utilizó en la clase antes de que solo el 20% lo compre. (Ten en cuenta que, en este caso, "usado" no significa "libros de segunda mano").

  • Escribe un programa que acepte como entrada una serie de libros (hasta que el usuario ingrese un centinela). En cada libro, solicita lo siguiente: un código para el libro, el costo de la copia única, la cantidad actual de libros disponibles, la inscripción a la clase potencial y datos que indiquen si el libro es obligatorio/opcional, nuevo o usado en el pasado. Como resultado, muestra toda la información de entrada en una pantalla con un buen formato junto con la cantidad de libros que se deben pedir (si hay alguno, ten en cuenta que solo se piden libros nuevos), el costo total de cada pedido.

    Luego, una vez completada toda la entrada, muestra el costo total de todos los pedidos de libros y la ganancia esperada si la tienda paga el 80% del precio de lista. Debido a que aún no hemos hablado de ninguna manera de tratar con un gran conjunto de datos que ingresan a un programa (¡no te pierdas las novedades!), solo debes procesar un libro a la vez y mostrar la pantalla de salida de ese libro. Luego, cuando el usuario haya terminado de ingresar todos los datos, tu programa debería mostrar los valores totales y de ganancias.

    Antes de comenzar a escribir código, tómate un tiempo para pensar en el diseño de este programa. Dividir en un conjunto de funciones y crear una función main() que se lea como un esquema para tu solución al problema. Asegurarte de que cada función realice una tarea.

    Este es un resultado de muestra:

    Please enter the book code: 1221
     single copy price: 69.95
     number on hand: 30
     prospective enrollment: 150
     1 for reqd/0 for optional: 1
     1 for new/0 for used: 0
    ***************************************************
    Book: 1221
    Price: $69.95
    Inventory: 30
    Enrollment: 150
    
    This book is required and used.
    ***************************************************
    Need to order: 67
    Total Cost: $4686.65
    ***************************************************
    
    Enter 1 to do another book, 0 to stop. 0
    ***************************************************
    Total for all orders: $4686.65
    Profit: $937.33
    ***************************************************

Proyecto de base de datos

En este proyecto, creamos un programa de C++ completamente funcional que implementa una aplicación de base de datos simple.

Nuestro programa nos permitirá administrar una base de datos de compositores y su información relevante. Estas son algunas de las funciones del programa:

  • La capacidad de agregar un nuevo compositor
  • La capacidad de clasificar a un compositor (es decir, indicar cuánto nos gusta o no su música)
  • La capacidad de ver todos los compositores de la base de datos
  • La capacidad de ver a todos los compositores según su clasificación

“Hay dos formas de construir un diseño de software: una es hacerlo tan simple que obviamente no haya deficiencias y la otra es hacerlo tan complicado que no haya deficiencias evidentes. El primer método es mucho más difícil". - C.A.R. Hoare

Muchos de nosotros aprendimos a diseñar y programar usando un enfoque de "procedimiento". La pregunta central con la que empezamos es “¿Qué debe hacer el programa?”. Dividimos la solución a un problema en tareas, cada una de las cuales resuelve una parte del problema. Estas tareas se asignan a funciones en nuestro programa a las que se llama secuencialmente desde main() o otras funciones. Este enfoque paso a paso es ideal para algunos problemas que debemos resolver. Sin embargo, la mayoría de las veces, nuestros programas no son solo secuencias lineales de tareas o eventos.

Con un enfoque orientado a objetos (OO), empezamos con la pregunta “¿Qué objetos del mundo real estoy modelando?”. En lugar de dividir un programa en tareas como se describió anteriormente, lo dividimos en modelos de objetos físicos. Estos objetos físicos tienen un estado definido por un conjunto de atributos y un conjunto de comportamientos o acciones que pueden realizar. Las acciones pueden cambiar el estado del objeto o invocar acciones de otros objetos. La premisa básica es que un objeto "sabe" cómo hacer las cosas por sí mismo. 

En el diseño fuera de la oficina, definimos los objetos físicos en términos de clases y objetos, atributos y comportamientos. Por lo general, un programa OO tiene una gran cantidad de objetos. Sin embargo, muchos de estos objetos son esencialmente los mismos. Ten en cuenta lo siguiente.

Una clase es un conjunto de atributos y comportamientos generales para un objeto, que puede existir físicamente en el mundo real. En la ilustración anterior, tenemos una clase de Apple. Todas las manzanas, independientemente de su tipo, tienen atributos de color y sabor. También definimos un comportamiento en el que la manzana muestra sus atributos.

En este diagrama, definimos dos objetos que pertenecen a la clase Apple. Cada objeto tiene los mismos atributos y acciones que la clase, pero define los atributos de un tipo específico de manzana. Además, la acción Display muestra los atributos de ese objeto en particular, p.ej., "Verde" y "Agrio".

Un diseño OO consiste en un conjunto de clases, los datos asociados con estas clases y el conjunto de acciones que estas pueden realizar. También debemos identificar las formas en que interactúan las diferentes clases. Esta interacción la pueden realizar los objetos de una clase que invocan las acciones de objetos de otras clases. Por ejemplo, podríamos tener una clase AppleOutputer que genera el color y el sabor de un array de objetos de Apple llamando al método Display() de cada objeto de Apple.

Estos son los pasos que realizamos en el diseño OO:

  1. Identifica las clases y define en general qué almacena como datos un objeto de cada clase y qué puede hacer un objeto.
  2. Define los elementos de datos de cada clase
  3. Define las acciones de cada clase y cómo se pueden implementar algunas acciones de una clase usando acciones de otras clases relacionadas.

Para un sistema grande, estos pasos ocurren de forma iterativa en diferentes niveles de detalle.

Para el sistema de base de datos de Composer, necesitamos una clase de Composer que encapsule todos los datos que queremos almacenar en un compositor individual. Un objeto de esta clase puede ascender o descender de nivel (cambiar su clasificación) y mostrar sus atributos.

También necesitamos una colección de objetos Composer. Para ello, definimos una clase Database que administra los registros individuales. Un objeto de esta clase puede agregar o recuperar objetos de Composer y mostrar objetos individuales invocando la acción de visualización de un objeto de Composer.

Por último, necesitamos algún tipo de interfaz de usuario para proporcionar operaciones interactivas en la base de datos. Esta es una clase de marcador de posición, es decir, aún no sabemos cómo será la interfaz de usuario, pero sabemos que la necesitaremos. Puede ser gráfica o basada en texto. Por ahora, definimos un marcador de posición que podemos completar más adelante.

Ahora que identificamos las clases para la aplicación de base de datos de Composer, el siguiente paso es definir los atributos y las acciones de las clases. En una aplicación más compleja, usaríamos lápiz y papel o UML, tarjetas CRC o OOD para asignar la jerarquía de clases y la forma en que interactúan los objetos.

Para nuestra base de datos de Composer, definimos una clase de Composer que contiene los datos relevantes que queremos almacenar en cada compositor. Además, incluye métodos para manipular las clasificaciones y mostrar los datos.

La clase Database necesita algún tipo de estructura para contener objetos de Composer. Necesitamos poder agregar un objeto Composer nuevo a la estructura y recuperar un objeto de Composer específico. También nos gustaría mostrar todos los objetos por orden de entrada o por clasificación.

La clase Interfaz de usuario implementa una interfaz basada en menús con controladores que llaman a acciones en la clase Database. 

Si las clases se entienden con facilidad y sus atributos y acciones son claros, como en la aplicación de Composer, es relativamente fácil diseñarlas. Sin embargo, si tienes alguna pregunta en mente sobre cómo las clases se relacionan e interactúan, lo mejor es sacarla primero y analizar los detalles antes de comenzar a programar.

Una vez que tenemos un panorama claro del diseño y lo evaluamos (pronto hablaremos más sobre esto), definimos la interfaz para cada clase. En este punto, no nos preocupan los detalles de la implementación; cuáles son los atributos y las acciones, y qué partes del estado y las acciones de una clase están disponibles para otras clases.

En C++, por lo general, lo hacemos definiendo un archivo de encabezado para cada clase. La clase Composer tiene miembros de datos privados para todos los datos que queremos almacenar en un compositor. Necesitamos descriptores de acceso (métodos get) y mutadores (métodos set), así como las acciones principales para la clase.

// composer.h, Maggie Johnson
// Description: The class for a Composer record.
// The default ranking is 10 which is the lowest possible.
// Notice we use const in C++ instead of #define.
const int kDefaultRanking = 10;

class Composer {
 public:
  // Constructor
  Composer();
  // Here is the destructor which has the same name as the class
  // and is preceded by ~. It is called when an object is destroyed
  // either by deletion, or when the object is on the stack and
  // the method ends.
  ~Composer();

  // Accessors and Mutators
  void set_first_name(string in_first_name);
  string first_name();
  void set_last_name(string in_last_name);
  string last_name();
  void set_composer_yob(int in_composer_yob);
  int composer_yob();
  void set_composer_genre(string in_composer_genre);
  string composer_genre();
  void set_ranking(int in_ranking);
  int ranking();
  void set_fact(string in_fact);
  string fact();

  // Methods
  // This method increases a composer's rank by increment.
  void Promote(int increment);
  // This method decreases a composer's rank by decrement.
  void Demote(int decrement);
  // This method displays all the attributes of a composer.
  void Display();

 private:
  string first_name_;
  string last_name_;
  int composer_yob_; // year of birth
  string composer_genre_; // baroque, classical, romantic, etc.
  string fact_;
  int ranking_;
};

La clase Database también es sencilla.

// database.h, Maggie Johnson
// Description: Class for a database of Composer records.
#include  <iostream>
#include "Composer.h"

// Our database holds 100 composers, and no more.
const int kMaxComposers = 100;

class Database {
 public:
  Database();
  ~Database();

  // Add a new composer using operations in the Composer class.
  // For convenience, we return a reference (pointer) to the new record.
  Composer& AddComposer(string in_first_name, string in_last_name,
                        string in_genre, int in_yob, string in_fact);
  // Search for a composer based on last name. Return a reference to the
  // found record.
  Composer& GetComposer(string in_last_name);
  // Display all composers in the database.
  void DisplayAll();
  // Sort database records by rank and then display all.
  void DisplayByRank();

 private:
  // Store the individual records in an array.
  Composer composers_[kMaxComposers];
  // Track the next slot in the array to place a new record.
  int next_slot_;
};

Observa cómo encapsulamos cuidadosamente los datos específicos del compositor en una clase separada. Podríamos haber colocado una struct o una clase en la clase Database para representar el registro de Composer y haber accedido a él directamente desde allí. Sin embargo, eso sería “sub-cosificación”, es decir, no modelamos con objetos tanto como podríamos hacerlo.

Cuando comiences a trabajar en la implementación de las clases Composer y Database, notarás que es mucho más sencillo tener una clase de Composer independiente. En particular, tener operaciones atómicas separadas en un objeto Composer simplifica en gran medida la implementación de los métodos Display() en la clase Database.

Por supuesto, también existe la "excesividad" en la que intentamos convertir todo en una clase o tener más clases de las que necesitamos. Se necesita práctica para encontrar el equilibrio adecuado, y descubrirás que cada programador tiene opiniones diferentes. 

Para determinar si estás objetando de forma excesiva o insuficiente, tus clases se pueden ordenar con cuidado. Como se mencionó antes, es importante desarrollar un diseño de clase antes de comenzar a codificar, y esto puede ayudarte a analizar tu enfoque. Una notación común que se usa con este propósito es UML (lenguaje de modelado unificado). Ahora que tenemos las clases definidas para los objetos Composer y Database, necesitamos una interfaz que permita al usuario interactuar con la base de datos. Un menú sencillo hará el truco:

Composer Database
---------------------------------------------
1) Add a new composer
2) Retrieve a composer's data
3) Promote/demote a composer's rank
4) List all composers
5) List all composers by rank
0) Quit

Podríamos implementar la interfaz de usuario como una clase o como un programa de procedimiento. No todo en un programa de C++ tiene que ser una clase. De hecho, si el procesamiento es secuencial o está orientado a tareas, como en este programa de menú, está bien implementarlo mediante procedimientos. Es importante implementarla de manera que siga siendo un "marcador de posición", es decir, si en algún momento queremos crear una interfaz gráfica de usuario, no deberíamos cambiar nada en el sistema más que en la interfaz de usuario.

Lo último que necesitamos para completar la aplicación es un programa para probar las clases. Para la clase de Composer, queremos un programa main() que reciba entradas, propague un objeto Composer y lo muestre para asegurarse de que la clase funcione correctamente. También queremos llamar a todos los métodos de la clase Composer.

// test_composer.cpp, Maggie Johnson
//
// This program tests the Composer class.

#include <iostream>
#include "Composer.h"
using namespace std;

int main()
{
  cout << endl << "Testing the Composer class." << endl << endl;

  Composer composer;

  composer.set_first_name("Ludwig van");
  composer.set_last_name("Beethoven");
  composer.set_composer_yob(1770);
  composer.set_composer_genre("Romantic");
  composer.set_fact("Beethoven was completely deaf during the latter part of "
    "his life - he never heard a performance of his 9th symphony.");
  composer.Promote(2);
  composer.Demote(1);
  composer.Display();
}

Necesitamos un programa de prueba similar para la clase Database.

// test_database.cpp, Maggie Johnson
//
// Description: Test driver for a database of Composer records.
#include <iostream>
#include "Database.h"
using namespace std;

int main() {
  Database myDB;

  // Remember that AddComposer returns a reference to the new record.
  Composer& comp1 = myDB.AddComposer("Ludwig van", "Beethoven", "Romantic", 1770,
    "Beethoven was completely deaf during the latter part of his life - he never "
    "heard a performance of his 9th symphony.");
  comp1.Promote(7);

  Composer& comp2 = myDB.AddComposer("Johann Sebastian", "Bach", "Baroque", 1685,
    "Bach had 20 children, several of whom became famous musicians as well.");
  comp2.Promote(5);

  Composer& comp3 = myDB.AddComposer("Wolfgang Amadeus", "Mozart", "Classical", 1756,
    "Mozart feared for his life during his last year - there is some evidence "
    "that he was poisoned.");
  comp3.Promote(2);

  cout << endl << "all Composers: " << endl << endl;
  myDB.DisplayAll();
}

Ten en cuenta que estos programas de prueba simples son un buen primer paso, pero requieren que inspeccionemos manualmente el resultado para asegurarnos de que el programa funcione correctamente. A medida que se expande un sistema, la inspección manual de los resultados rápidamente se vuelve poco práctica. En una lección posterior, presentaremos programas de pruebas de autoverificación en forma de pruebas de unidades.

El diseño de nuestra aplicación ya está completo. El siguiente paso es implementar los archivos .cpp para las clases y la interfaz de usuario.Para comenzar, copia y pega en archivos los códigos .h y del controlador de pruebas, y compílalos.Usa los controladores de prueba para probar tus clases. Luego, implementa la siguiente interfaz:

Composer Database
---------------------------------------------
1) Add a new composer
2) Retrieve a composer's data
3) Promote/demote a composer's rank
4) List all composers
5) List all composers by rank
0) Quit

Usa los métodos que definiste en la clase Database para implementar la interfaz de usuario. Haz que tus métodos sean a prueba de errores. Por ejemplo, una clasificación siempre debe estar en el rango de 1 a 10. Tampoco permitas que nadie agregue 101 compositores, a menos que planees cambiar la estructura de datos en la clase Database.

Recuerda que todo tu código debe seguir nuestras convenciones de programación, que se repiten aquí para tu conveniencia:

  • Cada programa que escribimos comienza con un comentario de encabezado, que proporciona el nombre del autor, su información de contacto, una breve descripción y el uso (si corresponde). Cada función o método comienza con un comentario sobre la operación y el uso.
  • Agregamos comentarios explicativos con oraciones completas, siempre que el código no se documente a sí mismo, por ejemplo, si el procesamiento es complicado, no evidente, interesante o importante.
  • Usa siempre nombres descriptivos: las variables son palabras en minúscula separadas por un _, como en my_variable. Los nombres de funciones o métodos usan letras mayúsculas para marcar palabras, como en MyExcitingFunction(). Las constantes comienzan con una "k" y usan letras mayúsculas para marcar palabras, como en kDaysInWeek.
  • La sangría está en múltiplos de dos. El primer nivel consiste en dos espacios. Si se necesita más sangría, se usan cuatro espacios, seis espacios, etcétera.

¡Bienvenido al mundo real!

En este módulo, presentamos dos herramientas muy importantes que se usan en la mayoría de las organizaciones de ingeniería de software. La primera es una herramienta de compilación y la segunda es un sistema de administración de configuración. Ambas herramientas son esenciales en la ingeniería de software industrial, en la que muchos ingenieros trabajan a menudo en un sistema grande. Estas herramientas ayudan a coordinar y controlar los cambios en la base de código, y proporcionan un medio eficiente para compilar y vincular un sistema a partir de muchos archivos de encabezado y de programa.

Archivos makefile

El proceso de compilación de un programa suele administrarse con una herramienta de compilación, que compila y vincula los archivos requeridos en el orden correcto. Muy a menudo, los archivos C++ tienen dependencias; por ejemplo, una función llamada en un programa reside en otro programa. O tal vez, varios archivos .cpp diferentes necesitan un archivo de encabezado. Una herramienta de compilación descifra el orden de compilación correcto de estas dependencias. Además, solo compilará los archivos que cambiaron desde la última compilación. Esto puede ahorrar mucho tiempo en sistemas con varios cientos o miles de archivos.

Por lo general, se usa una herramienta de compilación de código abierto llamada make. Para obtener más información, lee este artículo. Observa si puedes crear un gráfico de dependencias para la aplicación de la base de datos de Composer y, luego, tradúcelo a un archivo makefile.Esta es nuestra solución.

Sistemas de administración de configuración

La segunda herramienta que se usa en la ingeniería de software industrial es Configuration Management (CM). Se usa para administrar el cambio. Supongamos que Roberto y Susan son escritores de tecnología y trabajan en la actualización de un manual técnico. Durante una reunión, su gerente le asigna a cada una una sección del mismo documento para actualizar.

El manual técnico está almacenado en una computadora a la que pueden acceder Bob y Susan. Sin una herramienta o proceso de CM en marcha, podrían surgir varios problemas. Una situación posible es que la computadora que almacena el documento esté configurada para que Bob y Susan no puedan trabajar en el manual al mismo tiempo. Esto los ralentizaría considerablemente.

Surge una situación más peligrosa cuando la computadora de almacenamiento permite que Bob y Susan abran el documento al mismo tiempo. Esto es lo que podría suceder:

  1. Roberto abre el documento en su computadora y trabaja en su sección.
  2. Susan abre el documento en su computadora y trabaja en su sección.
  3. Roberto completa sus cambios y guarda el documento en la computadora de almacenamiento.
  4. Susan completa sus cambios y guarda el documento en la computadora de almacenamiento.

En esta ilustración, se muestra el problema que puede ocurrir si no hay controles en la copia única del manual técnico. Cuando Susan guarda los cambios, reemplaza los que hizo Roberto.

Este es exactamente el tipo de situación que puede controlar un sistema de CM. Con un sistema de CM, Bob y Susan "consultan" su propia copia del manual técnico y trabajan en ella. Cuando Bob vuelve a revisar sus cambios, el sistema sabe que Susan tiene su propia copia revisada. Cuando Susan revisa su copia, el sistema analiza los cambios que realizaron Bob y Susan, y crea una versión nueva que combina los dos conjuntos de cambios.

Los sistemas de CM tienen varias funciones además de la administración de cambios simultáneos, como se describió anteriormente. Muchos sistemas almacenan archivos de todas las versiones de un documento desde la primera vez que se crea. En el caso de un manual técnico, esto puede ser muy útil cuando un usuario tiene una versión antigua del manual y le hace preguntas a un escritor técnico. Un sistema de CM permitiría al escritor técnico acceder a la versión anterior y poder ver lo que ve el usuario.

Los sistemas de CM son especialmente útiles para controlar los cambios que se realizan en el software. Esos sistemas se denominan sistemas de administración de configuración de software (SCM). Si tienes en cuenta la gran cantidad de archivos de código fuente individuales en una gran organización de ingeniería de software y la gran cantidad de ingenieros que deben realizar cambios, queda claro que un sistema SCM es fundamental.

Administración de configuración de software

Los sistemas SCM se basan en una idea simple: las copias definitivas de tus archivos se guardan en un repositorio central. Las personas revisan copias de los archivos del repositorio, trabajan en esas copias y, luego, las vuelven a revisar cuando terminan. Los sistemas de SCM administran las revisiones de varias personas y hacen un seguimiento de ellas en un solo conjunto principal. 

Todos los sistemas de SCM proporcionan las siguientes funciones esenciales:

  • Administración de simultaneidad
  • Control de versiones
  • Sincronización

Veamos cada una de estas funciones con más detalle.

Administración de simultaneidad

La simultaneidad se refiere a la edición simultánea de un archivo por parte de más de una persona. Con un repositorio grande, queremos que las personas puedan hacerlo, pero puede generar algunos problemas.

Considera un ejemplo simple en el dominio de ingeniería: supongamos que permitimos que los ingenieros modifiquen el mismo archivo de forma simultánea en un repositorio central de código fuente. Client1 y Client2 deben realizar cambios en un archivo al mismo tiempo:

  1. El Cliente 1 abre bar.cpp.
  2. El cliente 2 abre bar.cpp.
  3. Client1 cambia el archivo y lo guarda.
  4. Client2 cambia el archivo y lo guarda reemplazando los cambios de Client1.

Obviamente, no queremos que esto suceda. Incluso si controlamos la situación haciendo que los dos ingenieros trabajen en copias separadas en lugar de hacerlo directamente en un conjunto principal (como en la siguiente ilustración), las copias deben conciliarse de alguna manera. La mayoría de los sistemas SCM solucionan este problema permitiendo que varios ingenieros revisen un archivo ("sincronización" o "actualización") y realicen los cambios según sea necesario. Luego, el sistema de SCM ejecuta algoritmos para combinar los cambios a medida que se vuelven a verificar los archivos (“enviar” o “confirmar”) en el repositorio.

Estos algoritmos pueden ser simples (pedir a los ingenieros que resuelvan cambios conflictivos) o no tan simples (determinar cómo combinar los cambios conflictivos de forma inteligente y solo preguntarle a un ingeniero si el sistema realmente se atasca). 

Control de versiones

El control de versiones hace referencia a hacer un seguimiento de las revisiones de los archivos que permiten volver a crear (o revertir) una versión anterior del archivo. Para ello, debes crear una copia de cada archivo cuando se registra en el repositorio o guardar todos los cambios realizados en un archivo. En cualquier momento, podemos usar los archivos o cambiar la información para crear una versión anterior. Los sistemas de control de versiones también pueden crear informes de registro de quiénes registraron los cambios, cuándo se registraron y cuáles fueron los cambios.

Sincronización

Con algunos sistemas SCM, se registran y se quitan archivos individuales del repositorio. Los sistemas más potentes te permiten extraer más de un archivo a la vez. Los ingenieros revisan su propia copia completa del repositorio (o parte de este) y trabajan en los archivos según sea necesario. Luego, confirman sus cambios en el repositorio principal de forma periódica y actualizan sus propias copias personales para mantenerse al día con los cambios que realizaron otras personas. Este proceso se denomina sincronización o actualización.

Subversión

Subversion (SVN) es un sistema de control de versión de código abierto. Tiene todas las funciones descritas anteriormente.

La SVN adopta una metodología simple cuando se producen conflictos. Un conflicto ocurre cuando dos o más ingenieros realizan cambios diferentes en la misma área de la base de código y, luego, ambos envían los cambios. El SVN solo alerta a los ingenieros que hay un conflicto: depende de los ingenieros resolverlo.

Usaremos SVN a lo largo de este curso para que te familiarices con la administración de configuraciones. Esos sistemas son muy comunes en la industria.

El primer paso es instalar SVN en tu sistema. Haz clic aquí para obtener instrucciones. Busca tu sistema operativo y descarga el objeto binario correspondiente.

Terminología de SVN

  • Revisión: Es un cambio en un archivo o conjunto de archivos. Una revisión es una “instantánea” en un proyecto que cambia constantemente.
  • Repositorio: Es la copia principal en la que SVN almacena el historial de revisión completo de un proyecto. Cada proyecto tiene un repositorio.
  • Copia de trabajo: Es la copia en la que un ingeniero realiza cambios en un proyecto. Puede haber muchas copias de trabajo de un proyecto determinado, cada una de las cuales es propiedad de un ingeniero individual.
  • Echa un vistazo: Para solicitar una copia de trabajo desde el repositorio. Una copia de trabajo equivale al estado del proyecto cuando se procesó.
  • Confirmación: Envía cambios de tu copia de trabajo al repositorio central. También se denomina registro o envío.
  • Actualización: Para incorporar los cambios de otras personas del repositorio a tu copia de trabajo o indicar si la copia de trabajo tiene cambios sin confirmar. Esto equivale a una sincronización, como se describió anteriormente. Por lo tanto, la actualización/sincronización actualiza tu copia de trabajo con la copia del repositorio.
  • Conflicto: La situación en la que dos ingenieros intentan confirmar cambios en la misma área de un archivo. La SVN indica conflictos, pero los ingenieros deben resolverlos.
  • Mensaje de registro: Un comentario que adjuntas a una revisión cuando lo confirmas y que describe tus cambios. El registro proporciona un resumen de lo que sucede en un proyecto.

Ahora que instalaste SVN, ejecutaremos algunos comandos básicos. Lo primero que debes hacer es configurar un repositorio en un directorio especificado. Estos son los comandos:

$ svnadmin create /usr/local/svn/newrepos
$ svn import mytree file:///usr/local/svn/newrepos/project -m "Initial import"
Adding         mytree/foo.c
Adding         mytree/bar.c
Adding         mytree/subdir
Adding         mytree/subdir/foobar.h

Committed revision 1.

El comando import copia el contenido del directorio mytree en el proyecto del directorio del repositorio. Podemos ver el directorio en el repositorio con el comando list.

$ svn list file:///usr/local/svn/newrepos/project
bar.c
foo.c
subdir/

La importación no crea una copia de trabajo. Para ello, debes utilizar el comando svn checkout. Esto crea una copia de trabajo del árbol de directorios. Hagámoslo ahora:

$ svn checkout file:///usr/local/svn/newrepos/project
A    foo.c
A    bar.c
A    subdir
A    subdir/foobar.h
…
Checked out revision 215.

Ahora que tienes una copia de trabajo, puedes realizar cambios en los archivos y directorios allí. Tu copia de trabajo es como cualquier otra colección de archivos y directorios. Puedes agregar nuevos, editarlos o moverlos, y hasta borrar toda la copia de trabajo. Ten en cuenta que, si copias y mueves archivos en tu copia de trabajo, es importante usar svn copy y svn move en lugar de los comandos de tu sistema operativo. Para agregar un archivo nuevo, usa svn add y, para borrar un archivo, usa svn delete. Si lo único que quieres hacer es editar, abre el archivo con el editor y edítalo.

Hay algunos nombres de directorios estándar que suelen usarse con Subversion. El directorio “troncal” contiene la línea de desarrollo principal del proyecto. Un directorio de "ramas" contiene cualquier versión de la rama en la que estés trabajando.

$ svn list file:///usr/local/svn/repos
/trunk
/branches

Por lo tanto, supongamos que realizaste todos los cambios necesarios en tu copia de trabajo y deseas sincronizarla con el repositorio. Si muchos otros ingenieros trabajan en esta área del repositorio, es importante que mantengas actualizada tu copia de trabajo. Puedes usar el comando svn status para ver los cambios que realizaste.

A       subdir/new.h      # file is scheduled for addition
D       subdir/old.c        # file is scheduled for deletion
M       bar.c                  # the content in bar.c has local modifications

Tenga en cuenta que hay muchas marcas en el comando de estado para controlar este resultado. Si deseas ver los cambios específicos en un archivo modificado, usa svn diff.

$ svn diff bar.c
Index: bar.c
===================================================================
--- bar.c	(revision 5)
+++ bar.c	(working copy)
## -1,18 +1,19 ##
+#include
+#include

 int main(void) {
-  int temp_var;
+ int new_var;
...

Por último, para actualizar la copia de trabajo desde el repositorio, usa el comando svn update.

$ svn update
U  foo.c
U  bar.c
G  subdir/foobar.h
C  subdir/new.h
Updated to revision 2.

Este es un lugar en el que podría ocurrir un conflicto. En el resultado anterior, la “U” indica que no se realizaron cambios en las versiones del repositorio de estos archivos y que se realizó una actualización. La "G" significa que se produjo una combinación. Se cambió la versión del repositorio, pero los cambios no entraron en conflicto con los tuyos. La "C" indica un conflicto. Esto significa que los cambios del repositorio se superpusieron con los tuyos y ahora debes elegir entre ellos.

Para cada archivo que tiene un conflicto, Subversion coloca tres archivos en la copia de trabajo:

  • file.mine: Este es el archivo tal como existía en tu copia de trabajo antes de que actualizaras la copia de trabajo.
  • file.rOLDREV: Este es el archivo que quitaste del repositorio antes de realizar los cambios.
  • file.rNEWREV: Este archivo es la versión actual del repositorio.

Para resolver el conflicto, puedes realizar una de las siguientes tres acciones:

  • Revisa los archivos y realiza la combinación manualmente.
  • Copia uno de los archivos temporales que creó SVN en la versión de copia de trabajo.
  • Ejecuta svn revert para descartar todos los cambios.

Una vez que hayas resuelto el conflicto, ejecuta el comando svn resolve para informar a SVN. Esta acción quita los tres archivos temporales y SVN ya no verá el archivo en un estado de conflicto.

Lo último que debes hacer es confirmar tu versión final en el repositorio. Para ello, usa el comando svn commit. Cuando confirmas un cambio, debes proporcionar un mensaje de registro que describa los cambios. Este mensaje de registro se adjunta a la revisión que creaste.

svn commit -m "Update files to include new headers."  

Hay mucho más para aprender sobre SVN y cómo puede respaldar grandes proyectos de ingeniería de software. Hay una gran cantidad de recursos disponibles en la Web. Solo haz una búsqueda en Google en "Subversión".

A modo de práctica, crea un repositorio para tu sistema de base de datos de Composer y, luego, importa todos tus archivos. Luego, consulta una copia funcional y usa los comandos descritos anteriormente.

Referencias

Libro de subversión en línea

Artículo de Wikipedia sobre SVN

Sitio web de subversión

Aplicación: A Study in Anatomy

Visita eSkeletons de The University of Texas en Austin