Programación orientada a objetos con C++, 5ª edición.
5/5
()
Información de este libro electrónico
Relacionado con Programación orientada a objetos con C++, 5ª edición.
Libros electrónicos relacionados
Programación Orientada a Objetos con C++. 4ª Edición Calificación: 0 de 5 estrellas0 calificacionesC/C++. Curso de programación. 4ª edición: PROGRAMACIÓN INFORMÁTICA/DESARROLLO DE SOFTWARE Calificación: 4 de 5 estrellas4/5Microsoft C#. Curso de Programación. 2ª Edición Calificación: 4 de 5 estrellas4/5Visual C#. Interfaces gráficas y aplicaciones para Internet con WPF, WCF y Silverlight Calificación: 0 de 5 estrellas0 calificacionesJAVA. Interfaces gráficas y aplicaciones para Internet. 4ª Edición.: Ofimática Calificación: 4 de 5 estrellas4/5Enciclopedia de Microsoft Visual C#. Calificación: 5 de 5 estrellas5/5Java 2. Curso de Programación. 4ª Edición Calificación: 0 de 5 estrellas0 calificacionesJava 2: Lenguaje y Aplicaciones Calificación: 0 de 5 estrellas0 calificacionesMicrosoft C#. Lenguaje y Aplicaciones. 2ª Edición. Calificación: 0 de 5 estrellas0 calificacionesEnciclopedia de Microsoft Visual Basic.: Diseño de juegos de PC/ordenador Calificación: 0 de 5 estrellas0 calificacionesVisual Basic. Interfaces gráficas y aplicaciones para Internet con WPF, WCF y Silverlight: Diseño de juegos de PC/ordenador Calificación: 3 de 5 estrellas3/5HTML5, CSS3 y JQuery: Gráficos y diseño web Calificación: 5 de 5 estrellas5/5Microsoft Visual Basic .NET. Lenguaje y aplicaciones. 3ª Edición.: Diseño de juegos de PC/ordenador Calificación: 0 de 5 estrellas0 calificacionesIntroducción a la programación: Algoritmos y su implementación en vb.net, c#, java y c++ Calificación: 0 de 5 estrellas0 calificacionesC++ Soportado con Qt Calificación: 3 de 5 estrellas3/5Visual Basic.NET Curso de Programación: Diseño de juegos de PC/ordenador Calificación: 4 de 5 estrellas4/5Desarrollo de Aplicaciones IOS con SWIFT: SISTEMAS OPERATIVOS Calificación: 0 de 5 estrellas0 calificacionesJAVA 17: Fundamentos prácticos de programación Calificación: 0 de 5 estrellas0 calificacionesProgramación en C++ Calificación: 2 de 5 estrellas2/5Aprende a Programar en C++ Calificación: 5 de 5 estrellas5/5Programación Orientada a Objetos Calificación: 3 de 5 estrellas3/5Programación (GRADO SUPERIOR): PROGRAMACIÓN INFORMÁTICA/DESARROLLO DE SOFTWARE Calificación: 4 de 5 estrellas4/5Programación orientada a objetos en Java Calificación: 4 de 5 estrellas4/5Programación Orientada a Objetos en JAVA Calificación: 0 de 5 estrellas0 calificacionesUML. Aplicaciones en Java y C++ Calificación: 4 de 5 estrellas4/5Algoritmos a Fondo - Con implementaciones en c y java Calificación: 5 de 5 estrellas5/5Estructuras de datos: Fundamentación práctica Calificación: 5 de 5 estrellas5/5Desarrollo Web en Java Calificación: 3 de 5 estrellas3/5Aprende a programar en C# Calificación: 5 de 5 estrellas5/5UML. Arquitectura de aplicaciones en Java, C++ y Python. 2ª Edición Calificación: 0 de 5 estrellas0 calificaciones
Programación para usted
Ortografía para todos: La tabla periódica de la ortografía Calificación: 5 de 5 estrellas5/5Python a fondo Calificación: 5 de 5 estrellas5/5Curso básico de Python: La guía para principiantes para una introducción en la programación con Python Calificación: 0 de 5 estrellas0 calificacionesArduino. Trucos y secretos.: 120 ideas para resolver cualquier problema Calificación: 5 de 5 estrellas5/5Python Paso a paso: PROGRAMACIÓN INFORMÁTICA/DESARROLLO DE SOFTWARE Calificación: 4 de 5 estrellas4/5Python para principiantes Calificación: 5 de 5 estrellas5/5Aprender React con 100 ejercicios prácticos Calificación: 0 de 5 estrellas0 calificacionesLógica de programación Calificación: 0 de 5 estrellas0 calificacionesDiseño y construcción de algoritmos Calificación: 4 de 5 estrellas4/5Aprender PHP, MySQL y JavaScript Calificación: 5 de 5 estrellas5/5El gran libro de Python Calificación: 5 de 5 estrellas5/5HTML para novatos Calificación: 5 de 5 estrellas5/5Aprende a Programar en C++ Calificación: 5 de 5 estrellas5/5Aprendizaje automático y profundo en python: Una mirada hacia la inteligencia artificial Calificación: 0 de 5 estrellas0 calificacionesDe qué hablo cuando hablo de programar (volumen 1) Calificación: 4 de 5 estrellas4/5Aprender a programar con Excel VBA con 100 ejercicios práctico Calificación: 5 de 5 estrellas5/5Linux Essentials: una guía para principiantes del sistema operativo Linux Calificación: 5 de 5 estrellas5/5Inteligencia artificial para programadores con prisa Calificación: 5 de 5 estrellas5/5JavaScript: Guía completa Calificación: 4 de 5 estrellas4/5Aprender HTML5, CSS3 y Javascript con 100 ejerecios Calificación: 5 de 5 estrellas5/5Python para filósofos Calificación: 3 de 5 estrellas3/5Programación y Lógica Proposicional Calificación: 4 de 5 estrellas4/5Fundamentos De Programación Calificación: 5 de 5 estrellas5/5La Guía Definitiva Para Desarrolladores De Software: Trucos Y Conseños Calificación: 3 de 5 estrellas3/5Arduino. Edición 2018 Curso práctico Calificación: 4 de 5 estrellas4/5Lógica de programación: Solucionario en pseudocódigo – Ejercicios resueltos Calificación: 4 de 5 estrellas4/5Aprende a programar: Crea tu propio sitio web Calificación: 4 de 5 estrellas4/5Todo el mundo miente: Lo que internet y el big data pueden decirnos sobre nosotros mismos Calificación: 4 de 5 estrellas4/5Aplicaciones gráficas con Python 3 Calificación: 4 de 5 estrellas4/5
Comentarios para Programación orientada a objetos con C++, 5ª edición.
2 clasificaciones1 comentario
- Calificación: 5 de 5 estrellas5/5Un libro muy completo a mi parecer en cuanto a C++ y la Programación Orientada a Objetos, explicaciones muy bien detalladas y de fácil comprensión, además de ejemplos muy prácticos y completos.
A 1 persona le pareció útil
Vista previa del libro
Programación orientada a objetos con C++, 5ª edición. - Fco. Javier Ceballos Sierra
PRÓLOGO
Un programa tradicional se compone de procedimientos y de datos. Un programa orientado a objetos consiste solamente en objetos, entendiendo por objeto una entidad que tiene unos atributos particulares, los datos, y unas formas de operar sobre ellos, los métodos o procedimientos.
La programación orientada a objetos es una de las técnicas más modernas que trata de disminuir el coste del software, aumentando la eficiencia en la programación y reduciendo el tiempo necesario para el desarrollo de una aplicación. Con la programación orientada a objetos, los programas tienen menos líneas de código, menos sentencias de bifurcación y módulos que son más comprensibles porque reflejan de una forma clara la relación existente entre cada concepto a desarrollar y cada objeto que interviene en dicho desarrollo. Donde la programación orientada a objetos toma verdadera ventaja es en la compartición y reutilización del código.
Sin embargo, no debe pensarse que la programación orientada a objetos resuelve todos los problemas de una forma sencilla y rápida. Para conseguir buenos resultados, es preciso dedicar un tiempo significativamente superior al análisis y al diseño. Pero, éste no es un tiempo perdido, ya que simplificará enormemente la realización de aplicaciones futuras.
Según lo expuesto, las ventajas de la programación orientada a objetos son sustanciales. No obstante, también presenta inconvenientes; por ejemplo, la ejecución de un programa no gana en velocidad y obliga al usuario a aprenderse una amplia biblioteca de clases antes de empezar a manipular un lenguaje orientado a objetos.
Existen varios lenguajes que permiten escribir un programa orientado a objetos y entre ellos se encuentra C++. Se trata de un lenguaje de programación basado en el lenguaje C, estandarizado (véase el apéndice A), revisado y ampliamente difundido. Gracias a esta estandarización y a la biblioteca estándar, C++ se ha convertido en un lenguaje potente, eficiente y seguro, características que han hecho de C++ un lenguaje universal de propósito general ampliamente utilizado, tanto en el ámbito profesional como en el educativo, y competitivo frente a otros lenguajes como C# de Microsoft o Java de Oracle. Evidentemente, algunas nuevas características que se han incorporado a C# o a Java no están soportadas en la actualidad, como es el caso de la recolección de basura; no obstante, existen elementos suficientes en la biblioteca C++ que resuelven este problema. Otro futuro desarrollo previsto es la ampliación de la biblioteca estándar para desarrollar aplicaciones con interfaz gráfica de usuario.
El libro, en su totalidad, está dedicado al aprendizaje de la programación orientada a objetos y al desarrollo de aplicaciones. Por lo tanto, se supone que si usted ha elegido este libro es porque ya posee conocimientos del lenguaje C. Si no fuera así, quizás debiera empezar por leerse C/C++ - Curso de programación, o bien elegir uno que incluya ambas partes, lenguaje C/C++ y programación orientada a objetos, como sucede con la Enciclopedia de C++, ambos escritos también por mi. Todos los temas tratados en el libro se han documentado con abundantes problemas resueltos.
Cómo está organizado el libro
El libro se ha estructurado en 12 capítulos más algunos apéndices que a continuación se relacionan. Los capítulos 1 al 4 nos introducen en la utilización de los elementos de la biblioteca de C++ de uso común y en la programación orientada a objetos. Los capítulos 5 al 7 nos enseñan los fundamentos de la programación orientada a objetos y también, cómo reutilizar código ya existente. El capítulo 8 nos introduce en la programación genérica utilizando plantillas. El capítulo 9 nos enseña a manejar las situaciones anómalas que se puedan producir en una aplicación, conocidas como excepciones. El capítulo 10 aporta elementos de la biblioteca de C++ que hacen que la gestión de memoria dinámica pase a un segundo plano. El capítulo 11 expone las clases de la biblioteca de C++ que nos permitirán trabajar con archivos y dispositivos. Y el capítulo 12 nos introduce en la programación concurrente basada en hilos.
CAPÍTULO 1. C++ versus C
CAPÍTULO 2. PROGRAMACIÓN ORIENTADA A OBJETOS
CAPÍTULO 3. OTRAS APORTACIONES DE C++
CAPÍTULO 4. BIBLIOTECA ESTÁNDAR
CAPÍTULO 5. CLASES
CAPÍTULO 6. OPERADORES SOBRECARGADOS
CAPÍTULO 7. CLASES DERIVADAS
CAPÍTULO 8. PROGRAMACIÓN GENÉRICA
CAPÍTULO 9. EXCEPCIONES
CAPÍTULO 10. GESTIÓN DE RECURSOS
CAPÍTULO 11. FLUJOS
CAPÍTULO 12. PROGRAMACIÓN CONCURRENTE
APÉNDICE A. NOVEDADES DE C++
APÉNDICE B. LA BIBLIOTECA DE C
APÉNDICE C. ENTORNOS DE DESARROLLO
APÉNDICE D. CÓDIGOS DE CARACTERES
Qué se necesita para utilizar este libro
Esta quinta edición fue escrita utilizando indistintamente los compiladores que subyacen bajo los entornos de desarrollo Microsoft Visual Studio Community y Code::Block (véase el apéndice C). En el primer caso se trata del compilador de C/C++ de Microsoft que cumple la norma ISO/IEC 14882. En el segundo caso se trata de un compilador GCC para Win32 (un compilador de C/C++ de la colección de compiladores GNU) de libre distribución que también cumple el estándar, del cual existen versiones para prácticamente todos los sistemas operativos. Ambos entornos de desarrollo se pueden obtener de forma gratuita en Internet. Por lo tanto, los ejemplos de este libro están escritos en C++ puro, tal y como se define en el estándar C++, lo que garantizará que se ejecuten en cualquier implementación que se ajuste a este estándar, que, en la actualidad, casi con absoluta seguridad, serán la totalidad de las existentes. Por ejemplo, el autor probó la mayoría de los desarrollos bajo ambos entornos de desarrollo, y también sobre la plataforma Linux, para conseguir un código lo más portable posible.
Sobre los ejemplos del libro
El material adicional de este libro, con todos los ejemplos e indicaciones del software para reproducirlos, puede descargarlo desde http://www.ra-ma.com (en la página correspondiente al libro). La descarga consiste en un fichero ZIP con una contraseña ddd-dd-dddd-ddd-d que se corresponde con el ISBN de este libro (teclee los dígitos y los guiones).
Agradecimientos
En la preparación de esta quinta edición quiero expresar mi agradecimiento a Manuel Peinado Gallego, profesional con una amplia experiencia en desarrollos con C++, por sus aportaciones y porque revisó la cuarta y tercera edición de este libro; también, a Óscar García Población y Elena Campo Montalvo, que revisaron el capítulo sobre hilos y aportaron ideas y sugerencias que me ayudaron a realizar un mejor desarrollo; a Sebastián Sánchez Prieto, autor de varios libros sobre sistemas operativos, porque, además de supervisar el capítulo sobre hilos, siempre estuvo dispuesto a resolver las preguntas que le planteé; a Inmaculada Rodríguez Santiago, que puntualizó sobre las modificaciones que debía hacer a la segunda edición para llegar a una tercera, base para las ediciones siguientes. Todos ellos son o fueron profesores de la Universidad de Alcalá, con una amplia experiencia sobre la materia que trata el libro. Finalmente, no quiero olvidarme del resto de mis compañeros, aunque no cite sus nombres, porque todos ellos, de forma directa o indirecta, me ayudaron con la crítica constructiva que hicieron sobre las ediciones anteriores, y tampoco de mis alumnos que con su interés por aprender me hacen reflexionar sobre la forma más adecuada de transmitir estos conocimientos; a todos ellos les estoy francamente agradecido.
Francisco Javier Ceballos Sierra
http://www.fjceballos.es/
CAPÍTULO 1
© F.J.Ceballos/RA-MA
C++ versus C
El objetivo de este capítulo es dar una idea de lo que es C++ sin entrar en muchos detalles. El autor supone que el lector ya ha programado antes, por ejemplo, utilizando el lenguaje C. Si no es así, el lector deberá considerar leer alguno de los libros indicados en el prólogo.
HISTORIA DEL LENGUAJE C++
C++ es un lenguaje de programación de propósito general basado en el lenguaje de programación C y ha sido diseñado para:
Ser mucho mejor que C.
Soportar la abstracción de datos.
Soportar la programación orientada a objetos.
Y soportar la programación genérica utilizando plantillas.
El lenguaje C nació en los laboratorios Bell de AT&T (Dennis Ritchie, 1972) y ha sido estrechamente asociado con el sistema operativo UNIX, ya que su desarrollo se realizó en este sistema y debido a que tanto UNIX como el propio compilador C y la casi totalidad de los programas y herramientas de UNIX fueron escritos en C. Su eficiencia y claridad han hecho que el lenguaje ensamblador apenas haya sido utilizado en UNIX.
Este lenguaje ha evolucionado paralelamente a UNIX. Muestra de esta evolución es que en 1980 se añaden al lenguaje C características como clases (concepto tomado de Simula67), chequeo del tipo de los argumentos de una función/método y conversión, si es necesaria, de los mismos, así como otras características; el resultado fue el lenguaje denominado C con Clases.
En 1983/84, C con Clases fue rediseñado, extendido y nuevamente implementado. El resultado se denominó Lenguaje C++. Las extensiones principales fueron métodos virtuales, métodos sobrecargados (un mismo identificador da acceso a múltiples formas de un método) y operadores sobrecargados (un mismo operador puede utilizarse en distintos contextos y con distintos significados). Después de algún otro refinamiento más, C++ quedó disponible en 1985. Este lenguaje fue creado por Bjarne Stroustrup (AT&T Bell Laboratories) y documentado en varios libros suyos.
El nombre de C++ se debe a Rick Mascitti, significando el carácter evolutivo de las transformaciones de C ("++" es el operador de incremento de C).
Posteriormente, C++ ha sido ampliamente revisado y refinado, lo que ha dado lugar a añadir nuevas características, como herencia múltiple, funciones miembro static y const, miembros protected, tipos genéricos de datos o plantillas y manipulación de excepciones. Se han revisado características como sobrecarga, enlace y manejo de la memoria. Además de esto, también se han hecho pequeños cambios para incrementar la compatibilidad con C y se añadieron la identificación de tipos durante la ejecución y los espacios de nombres con el objetivo de convertir a C++ en un lenguaje más propicio para la escritura y utilización de bibliotecas.
Esta evolución requería una estandarización. Por eso, en 1989 se convocó el comité X3J16 de ANSI (American National Standards Institute), que más tarde, en 1991, entró a formar parte de la estandarización ISO. El trabajo conjunto de estos comités permitió publicar en 1998 el estándar ISO C++ (ISO/IEC - International Standardization Organization/International Electrotechnical Commission - 14882) que ha dado lugar a que este lenguaje sea estable, a que no se dependa del compilador C++ utilizado y a que el código se pueda portar entre diferentes plataformas.
Fruto de esta estandarización es la biblioteca que incorpora actualmente C++ y que fue escrita con la intención de incluir sólo aquellas clases que realmente fueran utilizadas por la mayoría de los programadores. Las facilidades proporcionadas por esta biblioteca estándar las podemos resumir en los siguientes puntos:
Soporte básico, como por ejemplo identificación del tipo de los objetos durante la ejecución y gestión de memoria.
Soporte proporcionado por la biblioteca de C (manipulación de cadenas, archivos, etc.).
La clase string para la manipulación de cadenas de caracteres.
Clases para la entrada – salida.
Clases contenedor como vectores, listas y mapas.
Algoritmos de búsqueda y de ordenación.
Clases para trabajar con números como son cmath, complex y cstdlib.
Posteriormente, en 2003, fue aprobado un documento de corrección que dio lugar al estándar ISO/IEC 14882:2003 (C++03). Y correcciones posteriores dieron lugar a los estándares ISO/IEC 14882:2011 (C++11, año 2011), ISO/IEC 14882:2014 (C++14, año 2014) e ISO/IEC 14882:2017 (C++17, año 2017). La política seguida es que cuando se da por finalizado un estándar se inician los trabajos para el siguiente; así, una vez finalizado C++17 se iniciaron los trabajos para el ISO/IEC 14882:2020 (C++20, año 2020), y así sucesivamente. Esto es, el estándar C++ se seguirá corrigiendo y modificando, lo que dará lugar a nuevos C++XX. Las modificaciones introducidas afectan tanto a la biblioteca estándar como al lenguaje. Entre las nuevas características que se han incluido, destacamos las siguientes:
Cambios en la biblioteca estándar independientes del lenguaje: por ejemplo, plantillas con un número variable de argumentos (variadic).
Facilidades para escribir código: auto, enum class, long long, nullptr, ángulos derechos (>>) en platillas o static_assert.
Ayudas para actualizar y mejorar la biblioteca estándar: constexpr, listas de iniciación generales y uniformes, referencias rvalue y una versión de la biblioteca estándar con todas estas características.
Características relacionadas con la concurrencia: modelo de memoria multitarea, thread_local o una biblioteca para realizar programación concurrente (hilos).
Características relacionadas con conceptos: concepts (mecanismo para la descripción de los requisitos sobre los tipos y las combinaciones de los mismos lo que mejorará la calidad de los mensajes de error del compilador), sentencia for para iterar sobre un conjunto de valores y conceptos en la biblioteca estándar.
Expresiones lambda.
La finalidad de todas estas nuevas características de C++ es mejorar el rendimiento de las aplicaciones durante su construcción y durante su ejecución, mejorar la usabilidad y funcionalidad del lenguaje y proporcionar una biblioteca estándar más completa y segura.
C++ es, por lo tanto, un lenguaje híbrido que, por una parte, ha adoptado todas las características de la programación orientada a objetos (POO) que no perjudiquen su efectividad; por ejemplo, métodos virtuales y la ligadura dinámica (dynamic binding), y, por otra parte, mejora sustancialmente las capacidades de C. Esto, junto con la biblioteca de clases soportada, dota a C++ de una potencia, eficacia y flexibilidad que lo convierten en un estándar dentro de los lenguajes de programación orientados a objetos.
RESUMEN DE LA BIBLIOTECA DE C++
La biblioteca estándar de C++ está definida en el espacio de nombres std y las declaraciones necesarias para su utilización son proporcionadas por un conjunto de archivos de cabecera que se exponen a continuación. Con el fin de dar una idea general de la funcionalidad aportada por esta biblioteca, hemos clasificado estos archivos, según su función, en los grupos siguientes:
Entrada/Salida.
Cadenas.
Contenedores.
Iteradores.
Algoritmos.
Números.
Diagnósticos.
Utilidades generales.
Localización.
Soporte del lenguaje.
La aportación que realizan los contenedores, iteradores y algoritmos a la biblioteca estándar a menudo se denomina STL (Standard Template Library, biblioteca estándar de plantillas).
A continuación, mostramos un listado de los diferentes archivos de cabecera de la biblioteca estándar, para hacernos una idea de lo que supone esta biblioteca. Un archivo de cabecera de la biblioteca estándar que comience por la letra c equivale a un archivo de cabecera de la biblioteca de C; esto es, un archivo
Entrada/salida
Cadenas
Contenedores
Iteradores
Algoritmos
Números
Diagnósticos
Utilidades generales
Localización
Soporte del lenguaje
Concurrencia
LENGUAJE C++ Y COMPONENTES DE LA BIBLIOTECA
El estándar C++ define dos clases de entidades:
Elementos básicos del lenguaje, como tipos predefinidos (por ejemplo, char e int) y bucles (por ejemplo, sentencias for y while).
Componentes de la biblioteca estándar, como contenedores (por ejemplo, vector y map) y operaciones de E/S (por ejemplo, << y getline).
Mientras que los elementos básicos del lenguaje no son exclusivos de C++, también disponemos de ellos en C, los componentes de la biblioteca estándar si han sido escritos para C++.
Estructura de un programa
Con C++ podemos realizar una programación combinación de varias técnicas de programación ya que las características del lenguaje C++ soportan:
La programación imperativa o por procedimientos, una programación basada en procedimientos (funciones) y estructuras de datos. Como estudiaremos más adelante, C++ proporciona soporte adicional para este tipo de programación.
La abstracción de datos, una programación centrada en el diseño de interfaces, ocultando así los detalles de implementación. Como estudiaremos más adelante, C++ apoya este tipo de programación con clases concretas y abstractas.
La programación orientada a objetos, una programación centrada en el diseño, implementación y uso de jerarquías de clases.
La programación genérica, una programación centrada en el diseño, implementación y uso de algoritmos generales. Bajo esta técnica de programación, un algoritmo es diseñado para aceptar una amplia variedad de tipos. C++ da soporte a este tipo de programación por medio de plantillas.
Precisamente, el lenguaje C fue diseñado para soportar la programación imperativa o por procedimientos. Por ejemplo, en el siguiente programa, la función main solicita un valor y muestra el cuadrado de este valor, que es calculado por medio de la función cuadrado:
#include
double cuadrado(double x) // cuadrado de x
{
return x*x;
}
int main()
{
double n;
printf(Dato =
); scanf(%lf
, &n);
printf(Cuadrado de %g = %g\n
, n, cuadrado(n));
}
El archivo que almacene el código anterior tiene que tener extensión .c.
La estructura de un programa C++, al igual que en C, también se construye a partir de una función main. Por ejemplo:
#include
using namespace std; // hacer visibles los componentes de la
// biblioteca de C++ sin std::
double cuadrado(double x) // cuadrado de x
{
return x*x;
}
int main()
{
double n;
cout << Dato =
; cin >> n;
cout << Cuadrado de
<< n << =
<< cuadrado(n) << endl;
// Por omisión el valor retornado es 0: correcto.
}
Ahora, el archivo que almacene el código anterior tiene que tener extensión .cpp.
También observamos algunos cambios con respecto a la versión C. El archivo de cabecera para la E/S es ahora iostream. La biblioteca de C++ está definida en el espacio de nombres std; un espacio de nombres define un ámbito. Esto es, puede imaginarse la biblioteca de C++ definida así:
namespace std
{
// Declaraciones y definiciones. Por ejemplo:
class istream {/* ... */};
istream cin;
class ostream {/* ... */};
ostream cout;
// ...
}
Lo anterior indica que el componente cout de C++ pertenece a ese espacio de nombres, por lo que para acceder al mismo es necesario utilizar el operador de ámbito, ::, de C++ así (ídem para cin):
std::cout << Dato =
; std::cin >> n;
a no ser que hagamos visibles los componentes de ese espacio de nombres de la biblioteca de C++ escribiendo esta sentencia:
using namespace std;
El uso de espacios de nombres hace posible que puedan existir elementos con el mismo nombre en espacios de nombres diferentes.
También observamos que C++ aporta su propia implementación para las operaciones de E/S, por ejemplo, los objetos cout y cin.
El objeto cout define un flujo hacia la salida estándar, la pantalla. Este objeto utiliza su operador << para enviar una secuencia de elementos a la salida estándar. Por ejemplo, la siguiente sentencia muestra en la consola la cadena "n =" seguida del valor de n seguido de nueva línea (en Windows, nueva línea se traduce por un retorno de carro más avance de línea - CR+LF):
cout << n =
<< n << endl;
La sentencia equivalente en el lenguaje C sería esta otra:
printf(n = %g\n
, n);
Y el objeto cin define un flujo desde la entrada estándar, el teclado. Este objeto utiliza su operador >> para obtener una secuencia de elementos desde la entrada estándar. Por ejemplo, la siguiente sentencia espera que se le proporcionen dos valores a través del teclado: el primero de tipo double y el segundo de tipo int:
double n;
int i;
cin >> n >> i;
La sentencia equivalente en el lenguaje C sería esta otra:
scanf(%lf %d
, &n, &i);
Tipos, constantes, variables y estructuras
Cada nombre de una variable (o de una constante) tiene un tipo que determina las operaciones que pueden ejecutarse con ella. Por ejemplo:
const double pi = 3.14; // constante de tipo double
int i; // variable de tipo int
complex
El código anterior declara la constante pi (alternativa a #define en C) la variable i y el objeto c. La variable i nos permite realizar operaciones con enteros y el objeto c nos permite realizar operaciones con complejos construidos a partir de valores de tipo double.
Una declaración es una sentencia que introduce un nombre en un programa; ese nombre se refiere a una entidad de un tipo concreto. Un tipo define un conjunto de valores posibles y también, para un objeto, un conjunto de operaciones. Un objeto se corresponde con una zona de la memoria que contiene un valor de algún tipo. Un valor es un conjunto de bits que son interpretados según el tipo. Una variable es el nombre del objeto. Algunos de los tipos que define C++ son:
bool // boolean, valores posibles: true o false
char // carácter ('a', 'b', ..., '2')
int // entero
float // valor con decimales (simple precisión)
double // valor con decimales (doble precisión)
Además de los tipos implícitos en el lenguaje, como bool (el tipo bool también existe en C, definido en el archivo de cabecera
#include
struct complejo
{
double real;
double imag;
};
struct complejo sumar(struct complejo c1, struct complejo c2)
{
struct complejo r = { c1.real + c2.real, c1.imag + c2.imag };
return r;
}
int main()
{
struct complejo a = { 1, 2 }, b = { 0.5, -2.8 }, c;
c = sumar(a, b);
// ...
}
Para este tipo de programación, C++ proporciona soporte adicional permitiendo añadir a la estructura, no solo datos miembro, como son real e imag, sino también, funciones miembro, como sumar, según se puede observar en el ejemplo siguiente:
#include
using namespace std;
struct complejo
{
double real;
double imag;
complejo sumar(complejo c);
};
complejo complejo::sumar(complejo c)
{
complejo r = { this->real + c.real, this->imag + c.imag };
return r;
}
int main()
{
complejo a = { 1, 2 }, b = { 0.5, -2.8 }, r;
r = a.sumar(b);
// ...
}
De esta forma, una estructura puede definir no solo la estructura del objeto sino las operaciones que se pueden realizar con el mismo. Esto es, ahora, un objeto de tipo complejo, por ejemplo, a, nos lo podemos imaginar así (ídem para b):
images/img-33-1.jpgEn este caso, la función sumar tiene un parámetro implícito, denominado this (añadido por C++), que es un puntero al objeto (a) que invoca al método (sumar), y un parámetro explícito (b) que se corresponde con el otro objeto que interviene en la suma, de ahí que la función sumar la hayamos escrito así:
complejo complejo::sumar(complejo c)
{
complejo r = { this->real + c.real, this->imag + c.imag };
return r;
}
Este código, utilizando el operador de ámbito, ::, también deja claro la pertenencia de sumar a complejo, de lo contrario sería una función externa más. Una función miembro de una estructura tiene que invocarse para un objeto de ese tipo de estructura y una función externa no participa de este requisito.
Cuando decimos que C++ añade a las funciones miembro de una estructura un parámetro implícito denominado this, puede pensar que el compilador C++ a partir del código escrito para esa función genera un código análogo a este otro:
complejo complejo::sumar(complejo* this, complejo c)
{
complejo r = { this->real + c.real, this->imag + c.imag };
return r;
}
y que la llamada a.sumar(b) la convierte en esta otra: complejo::sumar(&a, b).
A la hora de iniciar una variable, C++ ofrece varias formas de hacerlo (véase el apartado Lista de iniciación del apéndice A). Por ejemplo:
double d1 = -2.8;
double d2 = { -2.8 }; // el operador = es opcional con { ... }
double d3{ -2.8 };
double d4{}; // d4 = 0.0 (valor predeterminado)
complex
complex
complex
complex
// asignados por omisión en el constructor)
La forma que utiliza el operador = es la tradicional en C. Estas formas de iniciar una variable, evidentemente, son aplicables a todos los tipos de variables.
En C++, cuando se define una variable, no es necesario indicar su tipo explícitamente, sino que se puede utilizar en su lugar auto siempre y cuando el tipo se pueda deducir del iniciador:
auto b = true; // bool
auto car = 'a'; // char
auto i = 1234; // int
auto d1 = 1.23; // double
auto d2 = sqrt(d1); // double (valor devuelto por sqrt)
Referencias
Una referencia es un nombre alternativo (un sinónimo) para un objeto. Una referencia debe ser iniciada y, más adelante, su valor puede ser modificado. El siguiente ejemplo define las referencias x e y:
int m = 10, n = 20;
int& x = m, &y = n, z = n; // x es sinónimo de m
// y es sinónimo de n
Un operador aplicado a la referencia no opera sobre ella, sino sobre la variable referenciada:
x++; // incrementa m en una unidad
Una referencia podría ser considerada como un puntero que accede al contenido del objeto apuntado sin necesidad de utilizar el operador de indirección (*). Como ejemplo, observe la función permutar, sus dos parámetros son referencias a los argumentos respectivos pasados en la llamada a dicha función (rx es sinónimo de a y ry es sinónimo de b):
void permutar(int&, int&);
int main()
{
int a = 10, b = 20;
permutar(a, b); // llamada a la función permutar
printf(a = %d, b = %d\n
, a, b);
}
void permutar(int& rx, int& ry)
{
// rx es una referencia a a
y ry una referencia a b
int z = rx;
rx = ry;
ry = z;
}
Este programa, que utiliza la función permutar para intercambiar el valor de dos variables, escrito con el lenguaje C nos obligaría a utilizar punteros:
void permutar(int*, int*);
int main()
{
int a = 10, b = 20;
permutar(&a, &b);
printf(a = %d, b = %d\n
, a, b);
}
void permutar(int* px, int* py)
{
// px apunta a a
y py apunta a b
int z = *px;
*px = *py;
*py = z;
}
Pasar un objeto a una función por valor implica hacer una copia del objeto en el parámetro correspondiente de la función, lo cual garantiza que el objeto original no podrá ser modificado por la función. En cambio, si el objeto se pasa por referencia (utilizando una referencia o un puntero) el objeto si puede ser modificado por la función (excepto si se declara const; esto lo estudiaremos más adelante) pero se evita hacer una copia, lo que implica una ejecución más rápida.
Clases
Una clase (class) es un tipo de datos definido por el usuario. En C++, una estructura (struct) no es más que una particularización de una clase en la que todos sus miembros tienen acceso público. Según esto, la estructura struct complejo, que escribimos anteriormente, podía ser sustituida por una clase así:
class complejo
{
public:
double real;
double imag;
complejo sumar(struct complejo c);
};
En los próximos capítulos estudiaremos las clases con detalle.
Plantillas
Fijándonos en la clase complejo anterior, si quisiéramos tener libertad para definir el tipo de los datos en el momento de utilizar esa clase, podríamos, utilizando la programación genérica, escribir una plantilla. Una plantilla (de clase o de función) va precedida por template más la lista de parámetros de tipo (en nuestro caso uno, por ejemplo, T):
#include
using namespace std;
template
class complejo
{
public:
T real;
T imag;
complejo sumar(struct complejo c);
};
template
complejo
{
complejo r = { this->real + c.real, this->imag + c.imag };
return r;
}
int main()
{
complejo
r = a.sumar(b);
// ...
}
Las plantillas serán objeto de estudio detallado en un capítulo posterior.
Ahora un objeto puede ser de tipo complejo
Contenedores de la biblioteca de C++
La biblioteca estándar de C++ facilita el trabajo con matrices de cualquier tipo de datos a través de la plantilla vector y de la clase string, entre otros contenedores de datos. La plantilla vector es ideal para trabajar con matrices de cualquier tipo de datos y la clase string es ideal para trabajar con cadenas de caracteres.
Cada contenedor de la biblioteca estándar de C++ se provee a través de un archivo de cabecera. Por ejemplo:
#include
#include
Estas dos directrices ponen a disposición de un programa los contenedores vector y string del espacio de nombres std. Por ejemplo, el siguiente programa define una cadena de caracteres (s, objeto de la clase string) y una matriz de enteros de una dimensión (v, objeto de la clase vector
#include
#include
#include
using namespace std;
int main()
{
string s{ Contenido del vector:
};
vector
cout << s << endl;
for (auto i : v)
cout << i <<
;
cout << endl;
}
Ejecución del programa:
Contenido del vector:
1 2 3
Observe que es posible acceder a cada uno de los elementos de una colección utilizando la siguiente sentencia for:
for (auto var : colección)
El tipo vector
Cadenas de caracteres
La biblioteca estándar de C++ proporciona la clase string para trabajar con cadenas de caracteres de una forma sencilla. Vamos a exponer ahora las operaciones básicas, y en otro capítulo, ampliaremos este estudio.
Las cadenas de tipo string son una alternativa más flexible y potente a las cadenas tipo C como la que se muestra a continuación:
char cad[80];
Para asignar un valor, desde el teclado, a una cadena tipo C, desde C++, podemos utilizar la función getline miembro de cin. Por ejemplo:
cin.getline(cad, 80, '\n');
Esta sentencia lee una cadena de caracteres desde el teclado y la almacena en cad. Se entiende por cadena la serie de caracteres que va desde la posición actual de lectura en el buffer asociado con el flujo de entrada, hasta el final del flujo, hasta el primer carácter \n (según el ejemplo), el cual se desecha, o bien hasta que el número de caracteres leídos sea igual a n–1.
Como hemos dicho, una alternativa a las cadenas tipo C son las cadenas tipo string. Para disponer de una cadena de este tipo, basta con crear una variable de tipo string; esta variable, puede ser iniciada o no. Por ejemplo:
string s1;
string s2{ Cantabria infinita
};
La biblioteca C++, a través de la clase string, y de otras clases y funciones, proporciona muchas operaciones para trabajar con cadenas de caracteres. Por ejemplo, para asignar un valor a una cadena desde el código, utilizamos el operador de asignación (=) y para asignárselo desde el teclado, podemos utilizar el operador >> del objeto cin o la función externa getline. Utilizando cin los espacios en blanco en la entrada actúan como separadores y con getline no. Por ejemplo:
cin >> s1; // el espacio en blanco es un separador
cout << s1 << '\n'; // mostrar la cadena
Si cuando se ejecuta el operador >> de cin, introducimos como valor Cantabria infinita
sólo se almacena Cantabria
. El resto de la información queda en el buffer de entrada. Esto no ocurre si procedemos de la forma siguiente:
getline(cin, s1); // el espacio en blanco es un carácter más
Podemos concatenar un string con otro, con una cadena tipo C, con un literal de cadena, o con un carácter, utilizando los operadores + o +=. También se puede asignar un string a otro. Por ejemplo:
s2 = s1 + '.' + España.
;
s2 += '\n'; // añadir nueva línea
También se pueden comparar lexicográficamente dos cadenas de tipo string utilizando los operadores ==, !=, <, >, <= y >=. Se diferencian mayúsculas de minúsculas. Por ejemplo:
if (s1 < s2)
cout << s1 << endl;
else
cout << s2 << endl;
Otras operaciones que podemos realizar son: encontrar una subcadena en otra cadena (find), acceder a una subcadena de la cadena (substr), obtener el número de caracteres de la cadena (size), etc. Tenga presente, que estas funciones son miembro de la clase string, por lo tanto, tienen que ser invocadas para un objeto de este tipo. Veamos el siguiente ejemplo:
string s1{ Cantabria infinita
};
string s2;
s2 = s1 + '.' + España.
;
s2 += '\n'; // añadir nueva línea
s1 = España
;
int n = s2.find(s1);
if (n != string::npos)
cout << s2.substr(n, s1.size()) << endl;
Analizando el código anterior, observamos que el método find busca la cadena s1 en la cadena s2. Si la encuentra devuelve la posición del primer carácter de la cadena buscada y si no, devuelve la constante string::npos (esta constante es static, por eso se accede a ella a través del nombre de la clase). A continuación, si la cadena se encontró, s2 invoca a su función substr para extraer, en este caso, esa subcadena; el primer parámetro de substr indica la posición del primer carácter a obtener y el segundo, cuántos caracteres se quieren extraer; en este caso, el número de caracteres a extraer coincide con el número de caracteres de s1, valor devuelto por la función size.
También, al igual que en C, se pueden realizar conversiones entre caracteres (funciones externas toupper y tolower), conversiones entre cadenas de caracteres, números y viceversa (funciones externas stoi, stof, to_string, etc.), o bien, cuando sea necesario, se puede acceder al carácter que hay en una posición determinada de la cadena, por ejemplo, utilizando la indexación ([]). Para aclarar lo expuesto, veamos a continuación la función ConverMayus que convierte a mayúsculas todos los caracteres de la cadena de caracteres que se pase como argumento cuando se llame a dicha función:
string& ConverMayus(string& str)
{
for (size_t i = 0; i < str.size(); i++)
str[i] = toupper(str[i]);
return str;
}
Analizando el código anterior, observamos que la función tiene un parámetro que es una referencia a la cadena cuyos caracteres deseamos convertir a mayúsculas. Para realizar la conversión obtenemos el carácter de la posición i (desde 0 hasta el número total de caracteres) y lo almacenamos en la misma posición, pero convertido a mayúsculas mediante la función externa toupper. La función puede ser llamada de cualquiera de las dos formas siguientes:
ConverMayus(s2);
s1 = ConverMayus(s2);
Este código demuestra que la función ConverMayus no necesitaría devolver nada, pero queda más completa devolviendo una referencia a la cadena convertida, lo que nos permitirá utilizar esa función en otras operaciones, por ejemplo, de salida:
cout << ConverMayus(s2) << endl;
Matrices
Uno de los contenedores más útiles de la biblioteca estándar es vector. Un vector, al igual que un array C, es una secuencia de elementos de un tipo dado almacenados consecutivamente en memoria.
La figura siguiente muestra la estructura básica de un objeto vector<T>, donde T es el tipo de los elementos del vector. De esta estructura destacamos, entre todos sus miembros (datos y funciones) dos: un puntero, pelemento, a la secuencia de elementos que se creará dinámicamente y otro, tamaño, que especifica cuántos elementos tiene esa secuencia.
images/img-41-1.jpgIgual que dijimos para los objetos de tipo string, para crear un vector basta con definir una variable de tipo vector<T>. Esta variable, puede ser iniciada con unos valores predeterminados o no. Por ejemplo:
vector
vector
vectore1
, e2
, e3
};
vector
El código anterior crea cuatro vectores: v1 con cero elementos de tipo int, v2 con cuatro elementos de tipo double, v3 con tres elementos de tipo string y v4 con dos elementos de tipo complex (un complex es un complejo, en este caso construido a partir de valores de tipo double).
¿Y si quisiéramos construir el vector v1 con 10 elementos iniciados a 0? Pues procederíamos así:
vector
En cambio, una sentencia como la siguiente construiría un vector v1 con un elemento iniciado a 10, porque 10 es un valor válido para un elemento del vector:
vector
¿Y si quisiéramos construir el vector v1 con t elementos iniciados con un valor x? Pues podríamos proceder de esta otra forma:
#include
#include
using namespace std;
int main()
{
int t, x;
cout << Nº de elementos:
; cin >> t;
cout << Valor inicial:
; cin >> x;
// Construir v1
vector
// Mostrar v1
for (auto e : v1)
cout << e <<
;
cout << endl;
}
Ejecución del programa:
Nº de elementos: 5
Valor inicial: -1
-1 -1 -1 -1 -1
La línea sombreada del código anterior define un objeto v1 de tipo vector
También, para acceder al elemento de la posición i del vector (i tiene que ser un valor mayor o igual que 0 y menor que el valor devuelto por su función miembro size: número de elementos del vector) podemos utilizar la indexación así:
for (size_t i = 0; i < v1.size(); ++i)
cout << v1[i] <<
;
Análogamente a la clase string, el contenedor vector ha sido escrito para que proporcione muchas operaciones que faciliten el trabajo con matrices, por ejemplo, la operación de asignación:
vector
vector
// ...
v2 = v1;
Este código copia en un vector v2 existente el contenido de otro v1. El vector v2 es redimensionado al tamaño del vector v1. Evidentemente ambos vectores tienen que ser del mismo tipo. También, es posible proceder de la forma siguiente, aunque, como estudiaremos más adelante, aquí no interviene el operador de asignación, sino un constructor:
vector
Este código construye un nuevo vector v2 iniciado con otro, v1, existente.
Para añadir un elemento al final del vector, el contenedor vector proporciona la función miembro push_back:
v2.push_back(e); // añadir el elemento e al final de v2
En el caso de tener que trabajar con matrices multidimensionales, lo único que tenemos que hacer es construir un vector de vectores (en este caso, cada elemento del vector es otro vector, y así sucesivamente). Por ejemplo:
vector
vector
{01, 02, 03},
{11, 12, 13}
};
vector
El código anterior define los vectores (o matrices) v1, v2 y v3 con 0, 2 y 5 elementos, respectivamente, de tipo vector
Para acceder a los elementos de esta estructura podemos utilizar, como ya hemos visto anteriormente, el operador de indexación: v2[f] hace referencia a la fila f de v2 y v2[f][c] hace referencia al elemento que está en la posición c de la fila v2[f]. Según esto, el código que permite asignar un valor desde el teclado al elemento v2[f][c] y, después, mostrarlo, puede ser el siguiente:
cin >> v2[f][c];
cout << v2[f][c] << endl;
Aplicando lo explicado, en el siguiente ejemplo podemos ver lo sencillo que resulta construir una matriz de dos dimensiones, leer valores para cada uno de sus elementos y mostrar su contenido. Las tres operaciones, construir, leer y mostrar las vamos a implementar mediante las funciones ConstruirMatriz, LeerMatriz y MostrarMatriz, que exponemos a continuación:
#include
#include
using namespace std;
vector
{
vector
return v;
}
void LeerMatriz(vector
{
// m representa una matriz de dos dimensiones
// m.size() es el número de filas de m
// m[f] es una fila de m (matriz unidimensional)
// m[f].size() es el número de elementos (columnas) de la fila f
for (size_t f = 0; f < m.size(); ++f)
{
for (size_t c = 0; c < m[f].size(); ++c)
{
cout << m[
<< f << ][
<< c << ]:
;
cin >> m[f][c];
}
}
}
void MostrarMatriz(vector
{
for (auto v : m)
{
for (auto e : v)
cout << e <<
;
cout << endl;
}
}
int main()
{
int filas, cols;
cout << Nº de filas de la matriz:
; cin >> filas;
cout << Nº de columnas de la matriz:
; cin >> cols;
vector
LeerMatriz(m);
MostrarMatriz(m);
cout << endl;
}
Ejecución del programa:
Nº de filas de la matriz: 2
Nº de columnas de la matriz: 3
m[0][0]: 1
m[0][1]: 2
m[0][2]: 3
m[1][0]: 4
m[1][1]: 5
m[1][2]: 6
1 2 3
4 5 6
La función ConstruirMatriz tiene dos parámetros que se corresponden con el número de filas y de columnas de la matriz que se desea construir, y devuelve la matriz construida. Esta función también la podíamos haber escrito así:
vector
{
return vector
}
Analizando este código, observamos que la clase del objeto que deseamos construir es vector
La función LeerMatriz permite asignar, desde el teclado, un valor a cada elemento de la matriz y la función MostrarMatriz permite mostrar todos los elementos de la matriz. Ambas funciones tienen un parámetro que es una referencia a un objeto de la clase vector
LeerMatriz(m);
MostrarMatriz(m);
el objeto (en el ejemplo, la matriz m que se quiere leer o mostrar) será pasado por referencia, lo cual evita hacer una copia de ese objeto en el parámetro correspondiente de la función.
ASIGNACIÓN DINÁMICA DE MEMORIA
C++ cuenta fundamentalmente con dos métodos para almacenar información en la memoria. El primero utiliza variables globales y locales. En el caso de variables globales, el espacio es fijado para ser utilizado a lo largo de toda la ejecución del programa; y en el caso de variables locales, la asignación se hace a través de la pila del sistema; en este caso, el espacio es fijado temporalmente, mientras la variable existe. El segundo método utiliza los operadores new y delete de C++ (la alternativa C a estos operadores son las funciones malloc y free). Como es lógico, estos operadores utilizan el área de memoria libre para realizar las asignaciones de memoria solicitadas.
La asignación dinámica de memoria consiste en asignar la cantidad de memoria necesaria para almacenar un objeto durante la ejecución del programa, en vez de hacerlo en el momento de la compilación del mismo. Cuando se asigna memoria para un objeto de un tipo cualquiera, se devuelve un puntero a la zona de memoria asignada. Según esto, lo que tiene que hacer el compilador es asignar una cantidad fija de memoria para almacenar la dirección del objeto asignado dinámicamente, en vez de hacer una asignación para el objeto en sí. Esto implica declarar un puntero a un tipo de datos igual al tipo del objeto que se quiere asignar dinámicamente. Por ejemplo, si queremos asignar memoria dinámicamente para una matriz de enteros, el objeto apuntado será el primer entero, lo que implica declarar un puntero a un entero; esto es:
int* a = nullptr; // puntero a un int o a una matriz de enteros
int n_elementos = 0; // número de elementos de la matriz
Después, durante la ejecución del programa, en el lugar adecuado, asignaremos la memoria necesaria para la matriz. Por ejemplo:
cin >> n_elementos;
a = new (nothrow) int[n_elementos];
El operador new permite asignar un bloque de n bytes consecutivos en memoria; es el compilador quien calcula el valor de n, que en el caso del ejemplo anterior es n_elementos * sizeof(int). También se puede asignar memoria para un solo elemento del tipo que sea, por ejemplo, para un float o para una estructura de tipo complejo:
float* pf = new (nothrow) float;
struct complejo
{
float re, im;
};
complejo* pc = new (nothrow) complejo;
Si no hubiera memoria libre suficiente para satisfacer la petición, el operador new devuelve un puntero nulo, si se especificó (nothrow); en otro caso, como estudiaremos más adelante, lanzaría una excepción, así podremos verificar si se pudo, por ejemplo, construir la matriz dinámicamente, porque si no se pudo crear, no podemos continuar con el proceso que pretendíamos realizar con esa matriz:
if (a == nullptr)
{
cout << No hay memoria suficiente para construir la matriz\n
;
return -1;
}
La memoria asignada debe ser liberada cuando ya no se necesite, porque si el programa finaliza sin haber liberado toda la memoria que se asignó dinámicamente, se generan lo que se denominan lagunas de memoria. Un programa que genere lagunas de memoria, inutilizará el ordenador cuando, después de n ejecuciones, agote la memoria disponible, de aquí la importancia de vigilar que los programas que escribamos no generen lagunas de memoria. En el siguiente apartado ampliaremos este concepto.
El operador delete permite liberar un bloque de memoria asignado por el operador new, pero no pone el puntero a 0 (nullptr). Si el puntero que hace referencia al bloque de memoria que deseamos liberar es nulo, el operador delete no hace nada. El siguiente ejemplo muestra cómo liberar la memoria para las diferentes asignaciones realizadas en los ejemplos anteriores. Cuando la memoria asignada dinámicamente corresponda a una matriz de cualquier tipo hay que utilizar a continuación de delete el operador de indexación, [].
delete[] a; // a es un puntero a una matriz de enteros
delete pf; // pf es un puntero a un float
delete pc; // pc es un puntero a una estructura de tipo complejo
Anteriormente vimos cómo construir una matriz de dos dimensiones de tipo double utilizando la plantilla vector<T>. En ese ejercicio, además de la función main, implementamos las operaciones, construir, leer y mostrar la matriz mediante las funciones ConstruirMatriz, LeerMatriz y MostrarMatriz. La matriz así construida era una matriz dinámica, donde nosotros no tuvimos que escribir código para gestionar dinámicamente la memoria (asignar y liberar); como estudiaremos en un capítulo posterior, esto fue así porque el constructor de vector<T> se encargó de la construcción y el destructor de destruirla (liberar la memoria asignada). Comparativamente, vamos a realizar este mismo ejercicio, pero ahora añadiendo código que gestione dinámicamente la memoria que se necesite, por lo tanto, además de escribir la función ConstruirMatriz, habrá que escribir otra, DestruirMatriz, que permita destruir la matriz liberando la memoria asignada.
¿Cómo sería la construcción dinámica de esta estructura de datos que va a representar una matriz de dos dimensiones? Cada fila de esta supuesta matriz será una matriz dinámica de una dimensión de tipo double y las direcciones de cada una de las filas (de tipo double*) las guardaremos en otra matriz unidimensional que vamos a llamar p, esto es, su primer elemento de tipo double* va a estar apuntado por p, por lo tanto, p tiene que ser de tipo double**.
double** p; // p[f] es de tipo double* y p[f][c] es un double
int filas = 0, cols = 0;
images/img-47-1.jpgEntonces para definir y operar con una matriz de dos dimensiones necesitamos conocer la dirección de la matriz (p) y su número de filas y columnas (filas y cols). Es una buena idea encapsular estos datos en una estructura matriz2d:
struct matriz2d
{
double** p;
int filas;
int cols;
};
Según lo expuesto hasta ahora, la función main varía muy poco con respecto a la versión anterior:
int main()
{
matriz2d m; // objeto matriz2d
cout << Nº de filas de la matriz:
; cin >> m.filas;
cout << Nº de columnas de la matriz:
; cin >> m.cols;
ConstruirMatriz(m);
LeerMatriz(m);
MostrarMatriz(m);
cout << endl;
}
A continuación, vamos a escribir la función ConstruirMatriz. Esta función tiene que asignar al miembro p de la estructura (un puntero a puntero a double) la estructura dinámica que representa la matriz de dos dimensiones (figura anterior). Esto es, primero crea la matriz de punteros para después crear cada una de las filas y guardar la dirección de cada una de ellas en esa matriz de punteros:
void ConstruirMatriz(matriz2d& m)
{
m.p = (double**) new (nothrow) double*[m.filas];
// Construir las filas
for (int f = 0; f < m.filas; ++f)
m.p[f] = (double*) new (nothrow) double[m.cols];
}
La función DestruirMatriz destruirá la matriz cuando ya no se necesite, para que no se generen lagunas de memoria al finalizar el programa. Destruir la matriz significa liberar la memoria asignada por ConstruirMatriz, lógicamente, la destrucción se realiza en orden inverso a como se realizó la construcción, esto es, primero se libera la memoria asignada a cada una de las filas y finalmente se libera la memoria de la matriz de punteros. Piense que si se liberara primero la memoria asignada a la matriz de punteros las filas quedarían sin direccionar.
void DestruirMatriz(matriz2d& m)
{
for (size_t f = 0; f < m.filas; ++f)
delete[] m.p[f]; // filas
delete[] m.p; // matriz de punteros
}
Las funciones LeerMatriz y MostrarMatriz difieren muy poco de lo expuesto en el apartado anterior. Ambas funciones requieren dos bucles for anidados para recorrer la matriz por filas y, a su vez, cada fila por columnas, para así acceder a cada uno de sus elementos, m.p[f][c], para asignarle un valor o para mostrarlo.
void LeerMatriz(matriz2d& m)
{
for (size_t f = 0; f < m.filas; ++f)
{
for (size_t c = 0; c < m.cols; ++c)
{
cout << m[
<< f << ][
<< c << ]:
;
cin >> m.p[f][c];
}
}
}
void MostrarMatriz(matriz2d& m)
{
for (size_t f = 0; f < m.filas; ++f)
{
for (size_t c = 0; c < m.cols; ++c)
cout << m.p[f][c] <<
;
cout << endl;
}
}
MANIPULACIÓN DE ERRORES
En el código anterior estamos dando por supuesto que el usuario que ejecuta el programa (un usuario cualquiera) no va a realizar ninguna acción fuera de lugar que conduzca a una ejecución sin sentido del programa. Por ejemplo, el programa cuando se ejecuta solicita del usuario el número de filas y de columnas de la matriz, entonces, ¿qué sucedería si el usuario introduce un número negativo para las filas, para las columnas o para ambas? El desarrollador tiene que anticiparse a estas situaciones añadiendo el código necesario allí donde se prevea que puede darse una situación anómala por una mala actuación del usuario o porque el programa no dispone de los recursos necesarios para continuar con su ejecución, por ejemplo, porque no hay memoria suficiente para construir una estructura dinámicamente. Según lo expuesto, vamos a modificar la función main para que obligue al usuario a introducir un número de filas y de columnas mayor que cero, y para saber si la función ConstruirMatriz pudo construir la matriz, porque si no pudo construirla no tiene sentido continuar:
int main()
{
matriz2d m;
do
{
cout << Nº de filas de la matriz:
; cin >> m.filas;
}
while (m.filas < 1);
do
{
cout << Nº de columnas de la matriz:
; cin >> m.cols;
}
while (m.cols < 1);
if (!ConstruirMatriz(m))
{
cout << No se pudo construir la matriz.\n
;
return -1;
}
LeerMatriz(m);
MostrarMatriz(m);
DestruirMatriz(m);
}
Según el código anterior, la función ConstruirMatriz tiene que devolver un valor de tipo bool para indicar si construyó o no la matriz solicitada (true si la matriz se construyó satisfactoriamente y false en caso contrario). Esto sugiere modificar esta función en el sentido de que tiene que supervisar cada operación new realizada por la misma, según se indica a continuación. Si una petición de memoria (new) falla, la función tiene que liberar la memoria asignada hasta entonces, para no generar lagunas de memoria, y devolver el valor false para dar a conocer lo ocurrido. Por el contrario, si todo el proceso se desarrolla normalmente, la función devolverá el valor true.
bool ConstruirMatriz(matriz2d& m)
{
m.p = (double**) new (nothrow) double*[m.filas];
if (m.p == nullptr) return false; // error
// Iniciar la matriz de punteros
fill(m.p, m.p + m.filas, nullptr);
// Construir las filas
for (int f = 0; f < m.filas; ++f)
{
m.p[f] = (double*) new (nothrow) double[m.cols];
if (m.p[f] == nullptr) // error, liberar la memoria asignada
{
DestruirMatriz(m);
return false;
}
// Iniciar la fila a 0
fill(m.p[f], m.p[f] + m.cols, 0);
}
return true; // correcto
}
Es importante también, según se observa en el código anterior, que los elementos con los que vamos a trabajar tengan asignados unos valores iniciales conocidos, lo que nos ayudará en la gestión de errores del programa o en proponer una ejecución más óptima.
La función fill (alternativa a memset de C) declarada en
Como alternativa fill tenemos fill_n. Por ejemplo:
fill_n(m.p, m.filas, nullptr);
En este caso, el primer argumento es la dirección del primer elemento que se desea iniciar, el segundo es el número de elementos que se desea iniciar, y el tercero es el valor de tipo T empleado para iniciar cada elemento.
La ventaja que se obtiene por haber iniciado la matriz de punteros en la función ConstruirMatriz es que se puede detener el bucle for en la función DestruirMatriz cuando no haya más filas para liberar, lo que conduce a un menor tiempo de ejecución:
void DestruirMatriz(matriz2d& m)
{
for (size_t f = 0; m.p[f] != nullptr && f < m.filas; ++f)
delete[] m.p[f]; // filas
delete[] m.p; // matriz de punteros
}
Otro ejemplo, supongamos, que en la fase de pruebas de nuestras funciones escribimos una función main así:
int main()
{
matriz2d m;
MostrarMatriz(m);
}
Este código, lo normal es que genere un error durante la ejecución, simplemente porque los valores de los miembros de m son basura, por lo tanto, la ejecución de los bucles for de MostrarMatriz es impredecible. Ahora bien, si la función main anterior la escribimos así:
int main()
{
matriz2d m;
m.p = nullptr;
MostrarMatriz(m);
}
y a la función MostrarMatriz le añadimos la línea siguiente:
void MostrarMatriz(matriz2d& m)
{
if (m.p == nullptr) return;
// ...
}
ya nos hemos anticipado al error. Evidentemente, la línea añadida a la función MostrarMatriz no sería necesaria si el usuario hubiera iniciado la estructura m así (en este caso la condición del bucle for de MostrarMatriz sería false):
int main()
{
matriz2d m{ nullptr, 0, 0 };
// ...
}
Pero, debemos pensar que estas funciones bien podían ser funciones de biblioteca y el que las escribió no puede saber que código va a escribir el desarrollador que las va a utilizar, por eso es recomendable tomar las medidas necesarias para que ante cualquier código que escriba el desarrollador del programa, el comportamiento de las funciones no conduzca a abortar la ejecución del mismo. Según esto, las funciones escritas las podemos modificar en el sentido siguiente:
void LeerMatriz(matriz2d& m)
{
if (m.p == nullptr) return;
// ... asignar valores a los elementos de la matriz
}
void MostrarMatriz(matriz2d& m)
{
if (m.p == nullptr) return;
// ... mostrar los valores de los elementos de la matriz
}
void DestruirMatriz(matriz2d& m)
{
if (m.p == nullptr) return;
// ... liberar la memoria asignada a la matriz
m.p = nullptr;
}
Esto es simplemente una introducción. En los capítulos posteriores abundaremos más en el tema de cómo anticiparnos a los posibles errores que se puedan generar por una mala actuación del usuario del programa.
AÑADIR UN MENÚ DE OPCIONES
Es bastante habitual que un programa muestre en un menú las operaciones que con él se pueden realizar, con el fin de que el usuario del mismo pueda elegir en cada momento aquella que requiera ejecutar. Cada operación se corresponderá con una función (que, evidentemente, puede llamar a otras funciones) y, si estas funciones se han escrito bajo los criterios comentados en el apartado anterior, el comportamiento del programa tiene que ser siempre correcto, independientemente del orden en el que el usuario elija las operaciones que puede realizar; esto es, si una operación no procede en un instante determinado, por ejemplo, mostrar la matriz cuando aún no ha sido creada, el programa simplemente lo indicará y permitirá continuar.
Volviendo a nuestro ejemplo, las operaciones que este permite realizar son: construir dinámicamente una estructura que representa una matriz de dos dimensiones, leer datos para los elementos de esa matriz, mostrar el contenido de la matriz y destruir la matriz cuando ya no se necesite. Estas operaciones pueden ser