Sunteți pe pagina 1din 238

INTRODUCCION A LA PROGRAMACION EN

C/C++

i
CAPITULO 1

Proceso de creación del software.

Se define como proceso al conjunto ordenado de pasos a seguir para llegar a la


solución de un problema u obtención de un producto, en este caso particular, para
lograr un producto software que resuelva un problema específico.
El proceso de creación de software puede llegar a ser muy complejo,
dependiendo de su porte, características y criticidad del mismo. Por ejemplo la
creación de un sistema operativo es una tarea que requiere proyecto, gestión,
numerosos recursos y todo un equipo disciplinado de trabajo. En el otro extremo, si
se trata de un sencillo programa, éste puede ser realizado por un solo programador
fácilmente. Es así que normalmente se dividen en tres categorías según su tamaño
(líneas de código) o costo.
Considerando los de gran porte, es necesario realizar complejas tareas, tanto
técnicas como de gerencia, una fuerte gestión y análisis diversos, la complejidad de
ello ha llevado a que desarrolle una ingeniería específica para tratar su estudio y
realización: es conocida como Ingeniería de Software.
En tanto que en los de mediano porte, pequeños equipos de trabajo (incluso un
avezado analista-programador solitario) pueden realizar la tarea. Aunque, siempre en
casos de mediano y gran porte (y a veces también en algunos de pequeño porte, según
su complejidad), se deben seguir ciertas etapas que son necesarias para la
construcción del software. Tales etapas, si bien deben existir, son flexibles en su
forma de aplicación, de acuerdo a la metodología o proceso de desarrollo escogido y
utilizado por el equipo de desarrollo o por el analista-programador solitario (si fuere
el caso).
Los procesos de desarrollo de software poseen reglas preestablecidas, y deben
ser aplicados en la creación del software de mediano y gran porte, ya que en caso
contrario lo más seguro es que el proyecto o no logre concluir o termine sin cumplir
los objetivos previstos, y con variedad de fallos inaceptables. Entre tales «procesos»
los hay ágiles o livianos (ejemplo XP), pesados y lentos (ejemplo RUP), y variantes
intermedias. Normalmente se aplican de acuerdo al tipo y porte del software a
desarrollar, a criterio del líder (si lo hay) del equipo de desarrollo. Algunos de esos
procesos son Programación Extrema (en inglés eXtreme Programming o XP),
Proceso Unificado de Rational (en inglés Rational Unified Process o RUP), Feature
Driven Development (FDD), etc.
Cualquiera sea el proceso utilizado y aplicado al desarrollo del software, y casi
independientemente de él, siempre se debe aplicar un modelo de ciclo de vida.
Cuando un proyecto fracasa, rara vez es debido a fallas técnicas, la principal
causa de fallos y fracasos es la falta de aplicación de una buena metodología o
proceso de desarrollo. Entre otras, una fuerte tendencia, desde hace pocas décadas, es
mejorar las metodologías o procesos de desarrollo, o crear nuevas y concientizar a los
profesionales de la informática a su utilización adecuada. Normalmente los
especialistas en el estudio y desarrollo de estas áreas y afines son los ingenieros en
software, es su orientación. Los especialistas en cualquier otra área de desarrollo
informático normalmente aplican sus conocimientos especializados pero utilizando
modelos, paradigmas y procesos ya elaborados.
Es común para el desarrollo de software de mediano porte que los equipos
humanos involucrados apliquen metodologías propias, normalmente un híbrido de los
procesos anteriores y a veces con criterios propios.
El proceso de desarrollo puede involucrar numerosas y variadas tareas , desde
lo administrativo, pasando por lo técnico y hasta la gestión y el gerenciamiento. Pero,
casi rigurosamente, siempre se cumplen ciertas etapas mínimas; las que se pueden
resumir como sigue:
 Captura, elicitación, especificación y análisis de requisitos (ERS)
 Diseño
 Codificación
 Pruebas (unitarias y de integración)
 Instalación y paso a producción
 Mantenimiento
En las anteriores etapas pueden variar ligeramente sus nombres, o ser más
globales, o contrariamente, ser más refinadas; por ejemplo indicar como una única
fase (a los fines documentales e interpretativos) de análisis y diseño; o indicar como
implementación lo que está dicho como codificación; pero en rigor, todas existen e
incluyen, básicamente, las mismas tareas específicas.

Lenguajes de programación.

Definición.

Un lenguaje de programación es un conjunto de símbolos y reglas sintácticas


y semánticas que definen su estructura y el significado de sus elementos y
expresiones. Es utilizado para controlar el comportamiento físico y lógico de una
máquina.
Un lenguaje de programación es un lenguaje diseñado para describir el
conjunto de acciones que un equipo debe ejecutar. Por lo tanto, un lenguaje de
programación es un modo práctico para que los seres humanos puedan dar
instrucciones a un equipo.
Los primeros lenguajes de programación surgieron de la idea de Charles
Babagge, la cual se le ocurrió a este hombre a mediados del siglo XIX, predijo
muchas de las teorías en que se basan los actuales ordenadores. Consistía en lo que él
denominaba la maquina analítica, pero que por motivos técnicos no pudo construirse
hasta mediados del siglo XX. Con él colaboro Ada Lovedby, la cual es considerada
como la primera programadora de la historia, pues realizo programas para aquélla
supuesta maquina de Babagge, en tarjetas perforadas.
Los lenguajes de programación son los medios de comunicación entre los
programadores o usuarios y la computadora. Con ellos se construyen los programas
que después serán ejecutados por la computadora.

Clasificacion de los lenguajes de programación.

Un lenguaje de programación es un conjunto limitado de palabras y de


símbolos que representan procedimientos, cálculos, decisiones y otras operaciones
que pueden ejecutar una computadora.

Lenguajes de Programacion según su nivel.

Calsificamos los lenguajes según su nivel de programación en:


 Lenguajes de máquina
El lenguaje máquina de una computadora consta de cadenas de números
binarios (ceros y unos) y es el único que "entienden" directamente los
procesadores. Todas las instrucciones preparadas en cualquier lenguaje de
máquina tienen por lo menos dos partes. La primera es el comando u
operación, que dice a la computadora cuál es la función que va a realizar.
Todas las computadoras tienen un código de operación para cada una de
sus funciones. La segunda parte de la instrucción es el operando, que
indica a la computadora dónde hallar o almacenar los datos y otras
instrucciones que se van a manipular; el número de operandos de una
instrucción varía en las distintas computadoras.
Según los estándares actuales, las primeras computadoras eran poco
tolerantes. Los programadores tenían que traducir las instrucciones de
manera directa a la forma de lenguaje de máquina que comprendían las
computadoras. Por ejemplo, un programador que escribiera la instrucción
"SUMAR 0814" para una de las primeras máquinas IBM hubiera escrito:
000100000000000000000000000010111000
Además de recordar las docenas de códigos numéricos para los comandos
del conjunto de instrucciones de la máquina, el programador tenía que
conocer las posiciones donde se almacenan los datos y las instrucciones.
La codificación inicial muchas veces requería meses, por lo que era
costosa y era frecuente que originara errores.
 Lenguajes ensambladores
A principios de la década de 1950, y con el fin de facilitar la labor de los
programadores, se desarrollaron códigos nemotécnicos para las
operaciones y direcciones simbólicas. La palabra nemotécnico se refiere a
una ayuda para la memorización. Uno de los primeros pasos para mejorar
el proceso de preparación de programas fue sustituir los códigos de
operaciones numéricos del lenguaje de máquina por símbolos alfabéticos,
que son los códigos nemotécnicos. Todas las computadoras actuales tienen
códigos nemotécnicos aunque, naturalmente, los símbolos que se usan
varían en las diferentes marcas y modelos. La computadora sigue
utilizando el lenguaje de máquina para procesar los datos, pero los
programas ensambladores traducen antes los símbolos de código de
operación especificados a sus equivalentes en lenguaje de máquina. Este
procedimiento preparó avances posteriores. La técnica de
direccionamiento simbólico permite expresar una dirección no en términos
de su localización numérica absoluta, sino en términos de símbolos
convenientes para el programador.
Durante las primeras etapas del direccionamiento simbólico, el
programador asigna un nombre simbólico y una dirección real a un dato.
Así, durante el resto del programa, el programador se referirá a los
nombres simbólicos, más que a las direcciones, cuando fuera preciso
procesar estos datos. Más adelante se hizo otra mejora. Se dejó a la
computadora la tarea de asignar y recordar las direcciones de las
instrucciones. Lo único que tenía que hacer el programador era indicar a la
computadora la dirección de la primera instrucción, y el programa
ensamblador se encargaba de almacenar, de manera automática, todas las
demás en forma secuencial a partir de ese punto. Así, si se agregaba más
tarde otra instrucción al programa, no era necesario modificar las
direcciones de todas las instrucciones que seguían al punto de inserción.
En vez de ello, el procesador ajustaba automáticamente las localidades de
memoria la próxima vez que se ejecutaba el programa.En la actualidad, los
programadores no asignan números de dirección reales a los datos
simbólicos, simplemente especifican dónde quieren que se coloque la
primera localidad del programa, y el programa ensamblador se encarga de
lo demás: asigna localidades tanto para las instrucciones como para los
datos.Estos programas de ensamble, o ensamblador, también permite a la
computadora convertir las instrucciones en lenguaje ensamblador del
programador en su propio código de máquina. Un programa de
instrucciones escrito en lenguaje ensamblador por un programador se
llama programa fuente. Después de que el ensamblador convierte el
programa fuente en código de máquina a éste se le denomina programa
objeto. Para los programadores es más fácil escribir instrucciones en un
lenguaje ensamblador que en códigos de lenguajes de máquina, pero es
posible que se requieran dos corridas de computadora antes de que se
puedan utilizar las instrucciones del programa fuente para producir las
salidas deseadas.
Los lenguajes ensambladores tienen ventajas sobre los lenguajes de
máquina. Ahorran tiempo y requieren menos atención a detalles. Se
incurren en menos errores y los que se cometen son más fáciles de
localizar.
 Lenguajes de alto nivel
Los primeros programas ensambladores producían sólo una instrucción en
lenguaje de máquina por cada instrucción del programa fuente. Para
agilizar la codificación, se desarrollaron programas ensambladores que
podían producir una cantidad variable de instrucciones en lenguaje de
máquina por cada instrucción del programa fuente. Dicho de otra manera,
un sola macroinstrucción podía producir varias líneas de código en
lenguaje de máquina. Por ejemplo, el programador podría escribir "LEER
ARCHIVO", y el programa traductor produciría una serie detallada de
instrucciones al lenguaje de máquina previamente preparada, con lo que se
copiaría un registro del archivo que estuviera leyendo el dispositivo de
entrada a la memoria principal. Así, el programador no se tenía que ocupar
de escribir una instrucción por cada operación de máquina realizada.
El desarrollo de las técnicas nemotécnicas y las macroinstrucciones
condujo, a su vez, al desarrollo de lenguajes de alto nivel que a menudo
están orientados hacia una clase determinada de problemas de proceso. A
diferencia de los programas de ensamble, los programas en lenguaje de
alto nivel se pueden utilizar con diferentes marcas de computadores sin
tener que hacer modificaciones considerables. Esto permite reducir
sustancialmente el costo de la reprogramación cuando se adquiere equipo
nuevo. Otras ventajas de los lenguajes de alto nivel son:
o Son más fáciles de aprender que los lenguajes ensambladores.
o Se pueden escribir más rápidamente.
o Permiten tener mejor documentación.
o Son más fáciles de mantener.
o Un programador que sepa escribir programas en uno de estos
lenguajes no está limitado a utilizar un solo tipo de máquina.

Lenguajes compilados.

Naturalmente, un programa que se escribe en un lenguaje de alto nivel también


tiene que traducirse a un código que pueda utilizar la máquina. Los programas
traductores que pueden realizar esta operación se llaman compiladores. Éstos, como
los programas ensambladores avanzados, pueden generar muchas líneas de código de
máquina por cada proposición del programa fuente. Se requiere una corrida de
compilación antes de procesar los datos de un problema.
Los compiladores son aquellos cuya función es traducir un programa escrito en
un determinado lenguaje a un idioma que la computadora entienda (lenguaje máquina
con código binario).
Al usar un lenguaje compilado, el programa desarrollado nunca se ejecuta
mientras haya errores, sino hasta que luego de haber compilado el programa, ya no
aparecen errores en el código.

Lenguajes interpretados.

Se puede también utilizar una alternativa diferente de los compiladores para


traducir lenguajes de alto nivel. En vez de traducir el programa fuente y grabar en
forma permanente el código objeto que se produce durante la corrida de compilación
para utilizarlo en una corrida de producción futura, el programador sólo carga el
programa fuente en la computadora junto con los datos que se van a procesar. A
continuación, un programa intérprete, almacenado en el sistema operativo del disco, o
incluido de manera permanente dentro de la máquina, convierte cada proposición del
programa fuente en lenguaje de máquina conforme vaya siendo necesario durante el
proceso de los datos. No se graba el código objeto para utilizarlo posteriormente.
La siguiente vez que se utilice una instrucción, se le debe interpretar otra vez y
traducir a lenguaje máquina. Por ejemplo, durante el procesamiento repetitivo de los
pasos de un ciclo, cada instrucción del ciclo tendrá que volver a ser interpretado cada
vez que se ejecute el ciclo, lo cual hace que el programa sea más lento en tiempo de
ejecución (porque se va revisando el código en tiempo de ejecución) pero más rápido
en tiempo de diseño (porque no se tiene que estar compilando a cada momento el
código completo). El intérprete elimina la necesidad de realizar una corrida de
compilación después de cada modificación del programa cuando se quiere agregar
funciones o corregir errores; pero es obvio que un programa objeto compilado con
antelación deberá ejecutarse con mucha mayor rapidez que uno que se debe
interpretar a cada paso durante una corrida de producción.

Lenguajes de programación declarativos.

Se les conoce como lenguajes declarativos en ciencias computacionales a


aquellos lenguajes de programación en los cuales se le indica a la computadora qué es
lo que se desea obtener o qué es lo que se esta buscando.
La programación declarativa es una forma de programación que implica la
descripción de un problema dado en lugar de proveer una solución para dicho
problema, dejando la interpretación de los pasos específicos para llegar a dicha
solución a un intérprete no especificado. La programación declarativa adopta, por lo
tanto, un enfoque diferente al de la programación imperativa tradicional.
En otras palabras, la programación declarativa provee el "qué", pero deja el
"cómo" liberado a la implementación particular del intérprete. Por lo tanto se puede
ver que la programación declarativa tiene dos fases bien diferenciadas, la declaración
y la interpretación.
Los lenguajes declarativos están orientados a buscar la solución del problema,
sin preocuparse por la forma de llegar a ello; es decir, el programador debe
concentrarse en la lógica del algoritmo, más que en el control de la secuencia. Los
programas están formados por un conjunto de definiciones o ecuaciones, las cuales
describen lo que debe ser calculado, no en sí la forma de hacerlo. Las variables sólo
pueden tener asignado un solo valor a lo largo de la ejecución del programa, lo cual
implica que no puede existir asignación destructiva. Debido a esto, cobra especial
importancia el uso del anidamiento y la recursividad.
Las listas representan la estructura fundamental de datos.
El orden de la ejecución no resulta importante debido a que no existen efectos
colaterales; es decir, que al calcular un valor, resulta imposible afectar el cálculo de
otros y con esto se puede afirmar que cualquier secuencia de ejecución deberá
conducir al mismo resultado.
Las expresiones o definiciones pueden ser usadas como valores y por lo tanto se
pueden tratar como argumentos de otras definiciones.El control de la ejecución no es
responsabilidad del programador.
Entre los lenguajes declarativos podemos clasificar:
 Programación lógica
La idea fundamental de la programación lógica consiste en emplear la lógica
como lenguaje de programación. La lógica no es imperativa porque no sirve
para indicar cómo resolver un problema. La lógica es declarativa porque sirve
para especificar qué problema resolver.
En la programación lógica, se especifican las condiciones que satisfacen las
soluciones, se deducen las soluciones a partir de las condiciones y el énfasis
de todo está en qué problema resolver. El problema se describe especificando
qué caracteriza a sus posibles soluciones.
La programación lógica, junto con la funcional, forma parte de lo que se
conoce como programación declarativa. En los lenguajes tradicionales, la
programación consiste en indicar cómo resolver un problema mediante
sentencias; en la programación lógica, se trabaja de forma descriptiva,
estableciendo relaciones entre entidades, indicando no cómo, sino qué hacer.
Se establece entonces que la idea esencial de la programación lógica es:
algoritmos = lógica + control. Es decir, un algoritmo se construye
especificando conocimiento en un lenguaje formal y el problema se resuelve
mediante un mecanismo de inferencia que actúa sobre aquél.
Al hacer un recorrido por la programación lógica, aparece como uno de sus
lenguajes más representativos, Prolog, que es un clásico de la inteligencia
artificial y que se aplica de múltiples formas en el desarrollo de software
comercial.
 Programación funcional
La programación funcional es un paradigma de programación declarativa
basado en la utilización de funciones matemáticas. El objetivo de la
programación funcional es conseguir lenguajes expresivos y matemáticamente
elegantes, en los que no sea necesario bajar al nivel de la máquina para
describir el proceso llevado a cabo por el programa.
Los programas escritos en un lenguaje funcional están constituidos
únicamente por definiciones de funciones, entendiendo éstas no como
subprogramas clásicos de un lenguaje imperativo, sino como funciones
puramente matemáticas, en las que se verifican ciertas propiedades como la
transparencia referencial, y por tanto, la carencia total de efectos laterales.
Otras características propias de estos lenguajes son la no existencia de
asignaciones de variables y la falta de construcciones estructuradas como la
secuencia o la iteración. Existen dos grandes categorías de lenguajes
funcionales: los funcionales puros y los híbridos. La diferencia entre ambos
estriba en que los lenguajes funcionales híbridos son menos dogmáticos que
los puros, al permitir conceptos tomados de los lenguajes imperativos, como
las secuencias de instrucciones o la asignación de variables. En contraste, los
lenguajes funcionales puros tienen una mayor potencia expresiva,
conservando a la vez su transparencia referencial, algo que no se cumple
siempre con un lenguaje híbrido.
 Programación orientada a bases de datos
Las bases de datos son programas que administran información y hacen más
ordenada la información, aparte de hacer la fácil de buscar y por supuesto de
encontrar.
Las características de las bases de datos pueden ser ventajosas o
desventajosas: pueden ayudar a almacenar, organizar, recuperar, comunicar y
manejar información en formas que serían imposibles sin las computadoras,
pero también afecta de alguna manera ya que existen enormes cantidades de
información en bases de datos de las que no se tiene control del acceso.
Las bases de datos tienen muchos usos: facilitan el almacenamiento de
grandes cantidades de información; permiten la recuperación rápida y flexible
de información, con ellas se puede organizar y reorganizar la información, así
como imprimirla o distribuirla en formas diversas.
Es claro que los lenguajes orientados a bases de datos son declarativos y no
imperativos, pues el problema es "qué" se quiere hacer o "qué" se necesita
buscar y encontrar en la base de datos, y no se enfatiza el "cómo".
Una base de datos también se puede definir como un banco de datos o
conjunto de datos que pertenecen al mismo contexto, almacenados
sistemáticamente para su posterior uso. Los sistemas gestores de bases de
datos (SGBD) permiten almacenar y posteriormente acceder a los datos de
forma rápida y estructurada.

Lenguajes de programación imperativos.


Se llama lenguajes imperativos a aquellos en los cuales se le ordena a la
computadora cómo realizar una tarea siguiendo una serie de pasos o instrucciones. El
proceso anterior se puede realizar con un lenguaje imperativo como por ejemplo
BASIC, C, C++, Java, Clipper, Dbase, C#, PHP, Perl, etc.
Dentro de la programación imperativa, se tiene un conjunto de instrucciones
que le indican al computador cómo realizar una tarea.
Los lenguajes imperativos se basan en comandos u órdenes que se le dan a la
computadora para que haga algo, con el fin de organizar o cambiar valores en ciertas
partes de la memoria.
La ejecución de estos comandos se realiza, en la mayor parte de ellos,
secuencialmente, es decir, hasta que un comando no ha sido ejecutado no se lee el
siguiente. Según el dominio, o mejor dicho con el propósito que se utiliza el
programa, se puede hablar de lenguajes de dominio específico y de dominio general.
Lenguajes imperativos procedurales es la aplicación quien controla qué
porciones de código se ejecuta, y la secuencia en que este se ejecuta. La ejecución de
la aplicación se inicia con la primera línea de código, y sigue una ruta predefinida a
través de la aplicación, llamando procedimientos según sea necesario. Los lenguajes
procedurales están fundamentados en la utilización de variables para almacenar
valores y en la realización de operaciones con los datos almacenados.
En este tipo de lenguajes, la arquitectura consta de una secuencia de celdas,
llamadas memoria, en las cuales se pueden guardar en forma codificada, lo mismo
datos que instrucciones; y de un procesador, el cual es capaz de ejecutar de manera
secuencial una serie de operaciones, principalmente aritméticas y booleanas, llamadas
comandos. En general, un lenguaje procedural ofrece al programador conceptos que
se traducen de forma natural al modelo de la máquina. El programador tiene que
traducir la solución abstracta del problema a términos muy primitivos, cercanos a la
máquina.
Con un lenguaje procedural el usuario (normalmente será un programador)
especifica qué datos se necesitan y cómo obtenerlos. Esto quiere decir que el usuario
debe especificar todas las operaciones de acceso a datos llamando a los
procedimientos necesarios para obtener la información requerida. Estos lenguajes
acceden a un registro, lo procesan y basándose en los resultados obtenidos, acceden a
otro registro, que también deben procesar. Así se va accediendo a registros y se van
procesando hasta que se obtienen los datos deseados. Las sentencias de un lenguaje
procedural deben estar embebidas en un lenguaje de alto nivel, ya que se necesitan
sus estructuras (bucles, condicionales, etc.) para obtener y procesar cada registro
individual.

Lenguajes de programación orientados a objetos

En la Programación Orientada a Objetos se definen los programas en términos


de "clases de objetos", objetos que son entidades que combinan estado (es decir,
datos) comportamiento (esto es, procedimientos o métodos) e identidad (propiedad
del objeto que lo diferencia del resto). La programación orientada a objetos expresa
un programa como un conjunto de estos objetos, que colaboran entre ellos para
realizar tareas. Esto permite hacer los programas módulos más fáciles de escribir,
mantener y reutilizar.
De esta forma, un objeto contiene toda la información, (los denominados
atributos) que permite definirlo e identificarlo frente a otros objetos pertenecientes a
otras clases (e incluso entre objetos de la misma clase, al poder tener valores bien
diferenciados en sus atributos). A su vez, dispone de mecanismos de interacción (los
llamados métodos) que favorecen la comunicación entre objetos (de una misma clase
o de distintas), y en consecuencia, el cambio de estado en los propios objetos. Esta
característica lleva a tratarlos como unidades indivisibles, en las que no se separan (ni
deben separarse) información (datos) y procesamiento (métodos).

Las principales diferencias entre la programación imperativa y la programación


orientada a objetos:
 La programación orientada a objetos es más moderna, es una evolución de la
programación imperativa plasmada en el diseño de una familia de lenguajes
conceptos que existían previamente, con algunos nuevos.
 La programación orientada a objetos se basa en lenguajes que soportan
sintáctica y semánticamente la unión entre los tipos abstractos de datos y sus
operaciones (a esta unión se la suele llamar clase).
 La programación orientada a objetos incorpora en su entorno de ejecución
mecanismos tales como el polimorfismo y el envío de mensajes entre objetos.

Traductores.

Definición.

Un traductor (compilador o intérprete) es un software que lee un programa


escrito en un lenguaje (lenguaje fuente) y lo traduce a un programa equivalente en
otro lenguaje (lenguaje objeto). Como parte importante de este proceso de
traducción, el traductor informa a su usuario de la presencia de errores en el programa
fuente. (Fig. 1.4)

Figura 1. 1. Traductor.

Etapas del proceso de traducción.

El proceso de traducción se divide en dos fases o etapas:


 Fase de análisis.- La parte del análisis divide al programa fuente en sus
elementos componentes y crea una representación intermedia del programa
fuente.
 Fase de síntesis.- La parte de la síntesis construye el programa objeto deseado
a partir de la representación intermedia.

De las dos partes, la síntesis es la que requiere las técnicas más especializadas.
Además de un traductor, se pueden necesitar otros programas para crear un
programa objeto ejecutable. Un programa fuente se puede dividir en módulos
almacenados en archivos distintos. La tarea de reunir el programa fuente a menudo se
confía a un programa distinto, llamado preprocesador. El preprocesador también
puede expandir abreviaturas, llamadas a macros, a proposiciones del lenguaje fuente.
Es útil pensar en estas fases como en piezas separadas dentro del traductor, y
pueden en realidad escribirse como operaciones codificadas separadamente aunque en
la práctica a menudo se integren juntas.

Fase de Análisis

Análisis léxico

El análisis léxico constituye la primera fase, aquí se lee el programa fuente de


izquierda a derecha y se agrupa en componentes léxicos (tokens), que son secuencias
de caracteres que tienen un significado. Además, todos los espacios en blanco, líneas
en blanco, comentarios y demás información innecesaria se elimina del programa
fuente. También se comprueba que los símbolos del lenguaje (palabras clave,
operadores,...) se han escrito correctamente.
Como la tarea que realiza el analizador léxico es un caso especial de
coincidencia de patrones, se necesitan los métodos de especificación y
reconocimiento de patrones, y estos métodos son principalmente las expresiones
regulares y los autómatas finitos. Sin embargo, un analizador léxico también es la
parte del traductor que maneja la entrada del código fuente, y puesto que esta entrada
a menudo involucra un importante gasto de tiempo, el analizador léxico debe
funcionar de manera tan eficiente como sea posible.

Análisis sintáctico

En esta fase los caracteres o componentes léxicos se agrupan jerárquicamente


en frases gramaticales que el compilador utiliza para sintetizar la salida. Se
comprueba si lo obtenido de la fase anterior es sintácticamente correcto (obedece a la
gramática del lenguaje). Por lo general, las frases gramaticales del programa fuente se
representan mediante un árbol de análisis sintáctico.
La estructura jerárquica de un programa normalmente se expresa utilizando
reglas recursivas. Por ejemplo, se pueden dar las siguientes reglas como parte de la
definición de expresiones:
1. Cualquier identificador es una expresión.
2. Cualquier número es una expresión.
3. Si expresión1 y expresión2 son expresiones, entonces también lo son:
 expresión1 + expresión2
 expresión1 * expresión2
 ( expresión1 )

Las reglas 1 y 2 son reglas básicas (no recursivas), en tanto que la regla 3 define
expresiones en función de operadores aplicados a otras expresiones.
La división entre análisis léxico y análisis sintáctico es algo arbitraria. Un factor
para determinar la división es si una construcción del lenguaje fuente es
inherentemente recursiva o no. Las construcciones léxicas no requieren recursión,
mientras que las construcciones sintácticas suelen requerirla. No se requiere recursión
para reconocer los identificadores, que suelen ser cadenas de letras y dígitos que
comienzan con una letra. Normalmente, se reconocen los identificadores por el
simple examen del flujo de entrada, esperando hasta encontrar un carácter que no sea
ni letra ni dígito, y agrupando después todas las letras y dígitos encontrados hasta ese
punto en un componente léxico llamado identificador. Por otra parte, esta clase de
análisis no es suficientemente poderoso para analizar expresiones o proposiciones.
Por ejemplo, no podemos emparejar de manera apropiada los paréntesis de las
expresiones, o las palabras begin y end en proposiciones sin imponer alguna clase de
estructura jerárquica o de anidamiento a la entrada.

Análisis semántico

La fase de análisis semántico revisa el programa fuente para tratar de encontrar


errores semánticos y reúne la información sobre los tipos para la fase posterior de
generación de código. En ella se utiliza la estructura jerárquica determinada por la
fase de análisis sintáctico para identificar los operadores y operandos de expresiones
y proposiciones.
Un componente importante del análisis semántico es la verificación de tipos.
Aquí, el compilador verifica si cada operador tiene operandos permitidos por la
especificación del lenguaje fuente. Por ejemplo, las definiciones de muchos lenguajes
de programación requieren que el compilador indique un error cada vez que se use un
número real como índice de una matriz. Sin embargo, la especificación del lenguaje
puede imponer restricciones a los operandos, por ejemplo, cuando un operador
aritmético binario se aplica a un número entero y a un número real.

Fase de síntesis
Consiste en generar el código objeto equivalente al programa fuente. Sólo se
genera código objeto cuando el programa fuente está libre de errores de análisis, lo
cual no quiere decir que el programa se ejecute correctamente, ya que un programa
puede tener errores de concepto o expresiones mal calculadas. Por lo general el
código objeto es código de máquina relocalizable o código ensamblador. Las
posiciones de memoria se seleccionan para cada una de las variables usadas por el
programa. Después, cada una de las instrucciones intermedias se traduce a una
secuencia de instrucciones de máquina que ejecuta la misma tarea. Un aspecto
decisivo es la asignación de variables a registros.

Generación de código intermedio

Después de los análisis sintáctico y semántico, algunos compiladores generan


una representación intermedia explícita del programa fuente. Se puede considerar esta
representación intermedia como un programa para una máquina abstracta. Esta
representación intermedia debe tener dos propiedades importantes; debe ser fácil de
producir y fácil de traducir al programa objeto.
La representación intermedia puede tener diversas formas. Existe una forma
intermedia llamada "código de tres direcciones" que es como el lenguaje ensamblador
de una máquina en la que cada posición de memoria puede actuar como un registro.
El código de tres direcciones consiste en una secuencia de instrucciones, cada una de
las cuales tiene como máximo tres operandos. Esta representación intermedia tiene
varias propiedades:
 Primera.- Cada instrucción de tres direcciones tiene a lo sumo un
operador, además de la asignación, por tanto, cuando se generan estas
instrucciones, el traductor tiene que decidir el orden en que deben
efectuarse las operaciones.
 Segunda.- El traductor debe generar un nombre temporal para guardar los
valores calculados por cada instrucción.
 Tercera.- Algunas instrucciones de "tres direcciones" tienen menos de tres
operandos, por ejemplo, la asignación.

Optimización de código

La fase de optimización de código consiste en mejorar el código intermedio, de


modo que resulte un código máquina más rápido de ejecutar. Esta fase de la etapa de
síntesis es posible sobre todo si el traductor es un compilador (difícilmente un
interprete puede optimizar el código objeto). Hay mucha variación en la cantidad de
optimización de código que ejecutan los distintos compiladores. En los que hacen
mucha optimización, llamados "compiladores optimizadores", una parte significativa
del tiempo del compilador se ocupa en esta fase. Sin embargo, hay optimizaciones
sencillas que mejoran sensiblemente el tiempo de ejecución del programa objeto sin
retardar demasiado la compilación.

Tipos de traductores

Existen dos tipos importantes de traductores:


 Los que van adaptando las instrucciones conforme son encontradas. A este
proceso se lo llama interpretar y a los programas que lo hacen se los conoce
como intérpretes. La traducción es simultánea y se produce de forma
dialogada con el programador.
 Los que convierten el conjunto de instrucciones en lenguaje de programación,
al programa equivalente escrito en lenguaje de máquina. A ese proceso se lo
llama compilar y al programa traductor se le denomina compilador. No se
realiza simultáneamente y no hay un dialogo con el programador durante la
programación.

Intérprete

Un intérprete es un programa informático capaz de analizar y ejecutar otros


programas, escritos en un lenguaje de alto nivel. Los intérpretes se diferencian de los
compiladores en que mientras estos traducen un programa desde su descripción en un
lenguaje de programación al código máquina del sistema destino, los primeros (los
intérpretes) sólo realizan la traducción a medida que sea necesario, típicamente,
instrucción por instrucción, y normalmente no guardan el resultado de dicha
traducción.
Los programas interpretados suelen ser más lentos que los compilados debido a
la necesidad de traducir el programa mientras se ejecuta, pero a cambio son más
flexibles como entornos de programación y depuración (lo que se traduce, por
ejemplo, en una mayor facilidad para reemplazar partes enteras del programa o añadir
módulos completamente nuevos), y permiten ofrecer al programa interpretado un
entorno no dependiente de la máquina donde se ejecuta el intérprete, sino del propio
intérprete (lo que se conoce comúnmente como máquina virtual).
Comparando su actuación con la de un ser humano, un compilador equivale a
un traductor profesional que, a partir de un texto, prepara otro independiente
traducido a otra lengua, mientras que un intérprete corresponde al intérprete humano,
que traduce de viva voz las palabras que oye, sin dejar constancia por escrito.
En la actualidad, uno de los entornos más comunes de uso de los intérpretes
informáticos es Internet, debido a la posibilidad que estos tienen de ejecutarse
independientemente de la plataforma.

Compilador
Un compilador es un programa informático que traduce un programa escrito en
un lenguaje de programación a otro lenguaje de programación, generando un
programa equivalente que la máquina será capaz de interpretar. Usualmente el
segundo lenguaje es código máquina, pero también puede ser simplemente texto. Este
proceso de traducción se conoce como compilación.
Un compilador es un programa que permite traducir el código fuente de un
programa en lenguaje de alto nivel, a otro lenguaje de nivel inferior (típicamente
lenguaje máquina). De esta manera un programador puede diseñar un programa en un
lenguaje mucho más cercano a cómo piensa un ser humano, para luego compilarlo a
un programa más manejable por una computadora.
Normalmente los compiladores están divididos en dos partes (Fig. 1.5):
 Front End: es la parte que analiza el código fuente, comprueba su validez,
genera el árbol de derivación y rellena los valores de la tabla de símbolos.
Esta parte suele ser independiente de la plataforma o sistema para el cual
se vaya a compilar.
 Back End: es la parte que genera el código máquina, específico de una
plataforma, a partir de los resultados de la fase de análisis, realizada por el
Front End.
Figura 1. 2. Partes de un compilador

Esta división permite que el mismo Back End se utilice para generar el código
máquina de varios lenguajes de programación distintos y que el mismo Front End que
sirve para analizar el código fuente de un lenguaje de programación concreto sirva
para generar código máquina en varias plataformas distintas.
El código que genera el Back End normalmente no se puede ejecutar
directamente, sino que necesita ser enlazado por un programa enlazador (linker).
CAPITULO 2

INTRODUCCIÓN A LA PROGRAMACIÓN ORIENTADA A


OBJETOS

2.1. Introducción a la programación orientada a objetos.

Desde principios de la era computacional se crearon diversas técnicas de


programación que a medida han evolucionado para poder adaptarse a nuevos retos y
poder crear soluciones más realistas que se amolden a el entorno real.
Podemos notar que a inicios la programación era no estructurada. Este estilo de
Programación No Estructurada, consistía en un solo programa principal, el cual se
establece como una secuencia de comandos o instrucciones que modifican datos que
son a su vez globales en el transcurso de todo el programa.
Esta técnica de programación no estructurada ofrece tremendas desventajas una
vez que el programa se hace suficientemente grande. Por ejemplo, si la misma
secuencia de instrucciones se necesita en diferentes situaciones dentro del programa,
la secuencia debe ser repetida. Esto ha llevado a la idea de extraer estas secuencias,
dando origen a nuevas técnicas como lo son la programación procedimental y
modular, conduciéndonos a un estilo de programación estructurada.
La Programación Estructurada es un método de programación basado sobre
el concepto de la unidad y del alcance. La programación estructurada ofrece muchas
ventajas sobre la programación secuencial, es más fácil de leer y más conservable;
siendo muy flexible, facilitando el buen diseño de programas.
La programación estructurada, es un estilo de programación con el cual el
programador elabora programas, cuya estructura es la más clara posible, mediante el
uso de tres estructuras básicas de control lógico: secuencia, selección e iteración.
Los programas son más fáciles de entender. Un programa estructurado puede
ser leído en secuencia, de arriba hacia abajo, sin necesidad de estar saltando de un
sitio a otro en la lógica, lo cual es típico de otros estilos de programación. La
estructura del programa es más clara puesto que las instrucciones están más ligadas o
relacionadas entre si, por lo que es más fácil comprender lo que hace cada función.
Reducción del esfuerzo en las pruebas. El programa se puede tener listo para
producción normal en un tiempo menor del tradicional; por otro lado, el seguimiento
de las fallas o depuración se facilita debido a la lógica más visible, de tal forma que
los errores se pueden detectar y corregir más fácilmente. Los programas quedan
mejor documentados internamente.
A pesar de las múltiples ventajas que ofrecen la programación estructurada,
surge necesidades y problemas complejos los cuales necesitan recrear o representar
mundos reales, lo cual, se dificulta con la técnicas de programación estructurada.
La Programación Orientada a Objetos (POO) aporta un nuevo enfoque a los
retos que se plantean en la programación estructurada cuando los problemas a
resolver son complejos. Al contrario que la programación procedimental que enfatiza
en los algoritmos, la POO enfatiza en los datos. En lugar de intentar ajustar un
problema al enfoque procedimental de un lenguaje, POO intenta ajustar el lenguaje al
problema. La idea es diseñar formatos de datos que se correspondan con las
características esenciales de un problema. Los lenguajes orientados combinan en una
única unidad o módulo, tanto los datos como las funciones que operan sobre esos
datos. Tal unidad se llama objeto. Si se desea modificar los datos de un objeto, hay
que realizarlo mediante las funciones miembro del objeto. Ninguna otra función
puede acceder a los datos. Esto simplifica la escritura, depuración y mantenimiento
del programa.
La tecnología orientada a objetos se define como una metodología de diseño de
software que modela las características de objetos reales o abstractos por medio del
uso de clases y objetos. Hoy en día, la orientación a objetos es fundamental en el
desarrollo de software, sin embargo, esta tecnología no es nueva, sus orígenes se
remontan a la década de los años sesenta (Simula, uno de los lenguajes de
programación orientados a objetos más antiguos, fue desarrollado en 1967).
La Programación Orientada a Objetos, la podemos definir como una técnica
o estilo de programación que utiliza objetos como bloque esencial de construcción.
ASCUI
El objeto, es el concepto principal sobre el cual se fundamenta la programación
orientada a objetos, el cual puede ser visto como una entidad que posee atributos y
efectúa acciones. En el mundo real podemos encontrar cientos de ejemplos que
cumplen con ésta definición, algunos de ellos son: una bicicleta, un automóvil, una
persona, una computadora, etc.
Los programas creados con la POO, se organizan como un conjunto finito de
objetos que contienen datos y operaciones (funciones miembro en C++) que llaman a
esos datos y que se comunican entre sí mediante mensajes.(Figura 2.1).

Figura 2. 1. Representación POO.

Un programa orientado a objetos es una colección de clases.


Necesita de una función principal que cree objetos y comience la ejecución
mediante la invocación de sus funciones o métodos.
En primer lugar, se crean los objetos. Segundo, los mensajes se envían desde
unos objetos y se reciben en otros a medida que el programa se ejecuta. Y tercero, se
borran los objetos cuando ya no son necesarios y se recupera la memoria ocupada por
ellos.
Los objetos son tipos de datos abstractos definidos por el programador. En
realidad son unidades que contienen datos y funciones que operan sobre esos datos.
A los objetos también se les conoce como instancias de clase. A los elementos
de un objeto se les conoce como miembros (datos miembros y funciones miembro).

2.1.1. Características de la POO

Las características más importantes que debe soportar un lenguaje que lo


definen como "orientado a objetos", son:
 Abstracción.
 Encapsulamiento.
 Principio de ocultación.
 Polimorfismo.
 Herencia.
 Recolección de basura.

2.1.1.1. Abstracción.

Denota las características esenciales de un objeto, donde se capturan sus


comportamientos. Cada objeto en el sistema sirve como modelo de un "agente"
abstracto que puede realizar trabajo, informar y cambiar su estado, y "comunicarse"
con otros objetos en el sistema sin revelar cómo se implementan estas características.
Los procesos, las funciones o los métodos pueden también ser abstraídos y cuando lo
están, una variedad de técnicas son requeridas para ampliar una abstracción.
Un ejemplo de esto puede identificarse en un automóvil. Aunque se observa un
automóvil como un único ente, este estará compuesto por: características y métodos o
funciones. (Figura 2.2).
Figura 2. 2. Objeto abstracto: automóvil.

2.1.1.2. Encapsulamiento.

Significa reunir a todos los elementos que pueden considerarse pertenecientes a


una misma entidad, al mismo nivel de abstracción. Esto permite aumentar la cohesión
de los componentes del sistema.
La encapsulación es un mecanismo que consiste en organizar datos y métodos
de una estructura, conciliando el modo en que el objeto se implementa, es decir,
evitando el acceso a datos por cualquier otro medio distinto a los especificados. Por lo
tanto, la encapsulación garantiza la integridad de los datos que contiene un objeto.
El encapsulamiento nos permite considerar a los objetos como cajas negras:
como objetos que podemos utilizar sin enfocarnos en la forma en que trabajan.

Ejemplo: en un automóvil, un mecánico debe saber cómo trabaja el motor, la


transmisión, etc., pero un conductor, puede usar estos componentes sin preocuparse
por estos detalles, el automóvil encapsula todos los detalles de las partes que lo
constituyen, por lo que un conductor tan solo necesita conocer su interfaz: el
acelerador, el freno y el volante.

2.1.1.3. Principio de ocultación.


Cada objeto está aislado del exterior, es un módulo natural, y cada tipo de
objeto expone una interfaz a otros objetos que específica cómo pueden interactuar
con los objetos de la clase. El aislamiento protege a las propiedades de un objeto
contra su modificación por quien no tenga derecho a acceder a ellas, solamente los
propios métodos internos del objeto pueden acceder a su estado. Esto asegura que
otros objetos no pueden cambiar el estado interno de un objeto de maneras
inesperadas, eliminando efectos secundarios e interacciones inesperadas. Algunos
lenguajes relajan esto, permitiendo un acceso directo a los datos internos del objeto
de una manera controlada y limitando el grado de abstracción. La aplicación entera se
reduce a un agregado o rompecabezas de objetos.
El usuario de una clase en particular no necesita saber cómo están estructurados
los datos dentro de ese objeto, es decir, un usuario no necesita conocer la
implementación Al evitar que el usuario modifique los atributos directamente y
forzándolo a utilizar funciones definidas para modificarlos (llamadas interfaces), se
garantiza la integridad de los datos (por ejemplo, uno puede asegurarse de que el tipo
de datos suministrados cumple con nuestras expectativas bien que los se encuentran
dentro del periodo de tiempo esperado).
La encapsulación define los niveles de acceso para elementos de esa clase.
Estos niveles de acceso definen los derechos de acceso para los datos, permitiéndonos
el acceso a datos a través de un método de esa clase en particular, desde una clase
heredada o incluso desde cualquier otra clase. Existen tres niveles de acceso:
 Público: funciones de toda clase pueden acceder a los datos o métodos de una
clase que se define con el nivel de acceso público. Este es el nivel de
protección de datos más bajo
 Privado: el acceso a los datos está restringido a los métodos de esa clase en
particular. Este es nivel más alto de protección de datos
 Protegido: el acceso a los datos está restringido a las funciones de clases
heredadas, es decir, las funciones miembro de esa clase y todas las subclases

2.1.1.4. Polimorfismo

Comportamientos diferentes, asociados a objetos distintos, pueden compartir el


mismo nombre, al llamarlos por ese nombre se utilizará el comportamiento
correspondiente al objeto que se esté usando. O dicho de otro modo, las referencias y
las colecciones de objetos pueden contener objetos de diferentes tipos, y la
invocación de un comportamiento en una referencia producirá el comportamiento
correcto para el tipo real del objeto referenciado. Cuando esto ocurre en "tiempo de
ejecución", esta última característica se llama asignación tardía o asignación
dinámica. Algunos lenguajes proporcionan medios más estáticos (en "tiempo de
compilación") de polimorfismo, tales como las plantillas y la sobrecarga de
operadores de C++.
Polimorfismo de sobrecarga. El polimorfismo de sobrecarga ocurre cuando
las funciones del mismo nombre existen, con funcionalidad similar, en clases que son
completamente independientes una de otra (éstas no tienen que ser clases secundarias
de la clase objeto). Por ejemplo, la clase complex, la clase image y la clase link
pueden todas tener la función "display". Esto significa que no necesitamos
preocuparnos sobre el tipo de objeto con el que estamos trabajando si todo lo que
deseamos es verlo en la pantalla. Por lo tanto, el polimorfismo de sobrecarga nos
permite definir operadores cuyos comportamientos varían de acuerdo a los
parámetros que se les aplican. Así es posible, por ejemplo, agregar el operador + y
hacer que se comporte de manera distinta cuando está haciendo referencia a una
operación entre dos números enteros (suma) o bien cuando se encuentra entre dos
cadenas de caracteres (concatenación).
Polimorfismo paramétrico (también llamado polimorfismo de plantillas). El
polimorfismo paramétrico es la capacidad para definir varias funciones utilizando el
mismo nombre, pero usando parámetros diferentes (nombre y/o tipo). El
polimorfismo paramétrico selecciona automáticamente el método correcto a aplicar
en función del tipo de datos pasados en el parámetro. Por lo tanto, podemos por
ejemplo, definir varios métodos homónimos de addition() efectuando una suma de
valores.
 El método int addition(int, int) devolvería la suma de dos enteros.
 float addition(float, float) devolvería la suma de dos flotantes.
 char addition(char, char) daría por resultado la suma de dos caracteres.

Polimorfismo de inclusión (también llamado redefinición o subtipado). La


habilidad para redefinir un método en clases que se hereda de una clase base se llama
especialización. Por lo tanto, se puede llamar un método de objeto sin tener que
conocer su tipo intrínseco: esto es polimorfismo de subtipado. Permite no tomar en
cuenta detalles de las clases especializadas de una familia de objetos,
enmascarándolos con una interfaz común (siendo esta la clase básica). Imagine un
juego de ajedrez con los objetos rey, reina, alfil, caballo, torre y peón, cada uno
heredando el objeto pieza. El método movimiento podría, usando polimorfismo de
subtipado, hacer el movimiento correspondiente de acuerdo a la clase objeto que se
llama. Esto permite al programa realizar el movimiento de pieza sin tener que verse
conectado con cada tipo de pieza en particular.

2.1.1.5. Herencia.

Por herencia se entiende la capacidad de poder crear nuevas clases a partir de


alguna anterior, de forma que las nuevas "heredan" las características de sus ancestros
(propiedades y métodos). Se trata por tanto de la capacidad de crear nuevos tipos de
datos a partir de los anteriores. Una característica especial de la herencia es que si se
cambia el comportamiento de la clase antecesora (también llamada padre, base o
super), también cambiará el comportamiento de las clases derivadas de ella
(descendientes). Como puede deducirse fácilmente, la herencia establece lo que se
llama una jerarquía de clases del mismo aspecto que el árbol genealógico de una
familia. Se entiende también que estos conceptos representan niveles de abstracción
que permiten acercar la programación a la realidad del mundo físico tal como lo
concebimos. Por ejemplo, todas las guitarras, ya sean Eléctricas, ElectroAcústicas o
normales, cumplen con características similares como número de cuerdas (6), método
de afinación, cambio de tono, cambio de postura y rasgueo entre otras. Si no se
siguiera con una metodología de herencia, por cada clase (Guitarra Electrica y
ElectroAcústica) se tendría que repetir la definición de todos los atributos y métodos
que pertenecen a la clase padre Guitarra, que corresponde a la abstracción más amplia
del concepto "Guitarra". Mediante el uso de la herencia, se puede definir una clase
"Guitarra" que cumpla con todas las características generales del concepto guitarra y
sus evoluciones, con el fin de acotar el número de especificaciones de las clases
"GuitarraElectrica" y "ElectroAcústica", y permitir la herencia a éstas últimas clases
con las características del objeto padre. (Figura 2.3).
Figura 2. 3. Herencia.

2.1.1.6. Recolección de basura.

La Recolección de basura o Garbage Collection es la técnica por la cual el


ambiente de Objetos se encarga de destruir automáticamente, y por tanto desasignar
de la memoria, los Objetos que hayan quedado sin ninguna referencia a ellos. Esto
significa que el programador no debe preocuparse por la asignación o liberación de
memoria, ya que el entorno la asignará al crear un nuevo Objeto y la liberará cuando
nadie lo esté usando. En la mayoría de los lenguajes híbridos que se extendieron para
soportar el Paradigma de Programación Orientada a Objetos como C++ u Object
Pascal, esta característica no existe y la memoria debe desasignarse manualmente.

2.1.2. Uso de los lenguajes de POO

Actualmente una de las áreas más candentes en la industria y en el ámbito


académico es la orientación a objetos. La orientación a objetos promete mejoras de
amplio alcance en la forma de diseño, desarrollo y mantenimiento del software
ofreciendo una solución a largo plazo a los problemas y preocupaciones que han
existido desde el comienzo en el desarrollo de software: la falta de portabilidad del
código y reusabilidad, código que es difícil de modificar, ciclos de desarrollo largos y
técnicas de codificación no intuitivas.
La introducción de tecnología de objetos como una herramienta conceptual para
analizar, diseñar e implementar aplicaciones permite obtener aplicaciones más
modificables, fácilmente extensibles y a partir de componentes reusables. Esta
reusabilidad del código disminuye el tiempo que se utiliza en el desarrollo y hace que
el desarrollo del software sea más intuitivo porque se piensa naturalmente en
términos de objetos más que en términos de algoritmos de software.
La Programación Orientada a Objetos ofrece algunas de ventajas respecto a
otros paradigmas de la programación:
 Fomenta la reutilización y extensión del código.
 Facilita el mantenimiento del software.
 Permite crear sistemas más complejos.
 Agiliza el desarrollo de software.
 Facilita la creación de programas visuales.
 Facilita el trabajo en equipo relacionar el sistema al mundo real.

2.2. Introducción a las Clases y Objetos.

2.2.1. Conceptos Clases y Objetos

Un objeto se define como la unidad que en tiempo de ejecución realiza las


tareas de un programa. También a un nivel más básico se define como la instancia de
una clase.
Estos objetos interactúan unos con otros, en contraposición a la visión
tradicional en la cual un programa es una colección de subrutinas (funciones o
procedimientos), o simplemente una lista de instrucciones para el computador. Cada
objeto es capaz de recibir mensajes, procesar datos y enviar mensajes a otros objetos
de manera similar a un servicio. (Figura 2.4).
Un objeto es una abstracción encapsulada que tiene un estado interno como
dado por una lista de atributos cuyos valores son únicos al objeto. El objeto también
conoce la lista de mensajes al que puede responder y cómo responderá en cada uno.
Un objeto está constituido por:
 Atributos: estos son los datos que caracterizan al objeto. Son variables que
almacenan datos relacionados al estado de un objeto.
 Métodos (usualmente llamados funciones miembro): Los métodos de un
objeto caracterizan su comportamiento, es decir, son todas las acciones
(denominadas operaciones) que el objeto puede realizar por sí mismo. Estas
operaciones hacen posible que el objeto responda a las solicitudes externas (o
que actúe sobre otros objetos). Además, las operaciones están estrechamente
ligadas a los atributos, ya que sus acciones pueden depender de, o modificar,
los valores de un atributo.

Figura 2. 4. Constitución de objetos.

Una clase es la estructura de un objeto, es decir, la definición de todos los


elementos de que está hecho un objeto. Un objeto es, por lo tanto, el "resultado" de
una clase. En realidad, un objeto es una instancia de una clase, por lo que se pueden
intercambiar los términos objeto o instancia (o incluso evento).
Los objetos son miembros o pertenecen a una clase. Una clase es un tipo
definido por el usuario que determina las estructuras de datos y las operaciones
asociadas con ese tipo. Las clases son como plantillas o modelos que describen cómo
se construyen ciertos tipos de objetos. Incluye en su descripción un nombre para el
tipo de objeto, una lista de atributos (y sus tipos), y una lista de mensajes con
métodos correspondientes al que un objeto de la clase puede responder. Cada vez que
se construye un objeto de una clase, se crea una instancia de esa clase.
Una clase se compone de dos partes:
 Atributos (denominados, por lo general, datos miembros): esto es, los datos
que se refieren al estado del objeto
 Métodos (denominados, por lo general, funciones miembros): son funciones
que pueden aplicarse a objetos

2.2.2. Sintaxis de la definición de clases

Una clase define la estructura que tendrán los objetos que se creen a partir de
esta, la cual contiene las características de los objetos o atributos y los métodos o
funciones miembros necesarios para modificar y evaluar los atributos. Se describen
también el tipo de ocultamiento que posee cada atributo y método.
Si tenemos una clase llamada auto, los objetos Peugeot y Renault serán
instancias de esa clase. También puede haber otros objetos Peugeot 406,
diferenciados por su número de modelo. Asimismo, dos instancias de una clase
pueden tener los mismos atributos, pero considerarse objetos distintos independientes.
En un contexto real: dos camisas pueden ser idénticas, pero no obstante, también ser
diferentes de alguna manera. Sin embargo, si las mezclamos es imposible distinguir
una de la otra.
La sintaxis típica de una clase es:

clase Nombre {
// Variables miembro (habitualmente privadas)
miembro_1; //lista de miembros
miembro_2;
miembro_3;
...
// Funciones o métodos (habitualmente públicas)
funcion_miembro_1( ); //funciones miembro conocidas
funcion_miembro_2 ( ); // funciones como métodos
...
// Propiedades (habitualmente públicas)
propiedad_1;
propiedad_2;
propiedad_3;
...
}

2.2.3. Mensajes y métodos

Los objetos, las clases y sus instancias se comunican a través del paso de
mensajes. Esto elimina la duplicación de datos y garantiza que no se propaguen los
efectos de los cambios en las estructuras de datos encapsuladas dentro de objetos
sobre otras partes del sistema. A menudo los mensajes se realizan como llamadas a
funciones.
El mensaje es la acción que realiza un objeto sobre otro, la petición de servicio.
Se considera al mensaje como una llamada, posiblemente comprendido en el
tiempo de ejecución, al objeto o clase que pide un servicio o la notificación de un
suceso específico.
Un mensaje es representado por un identificador, o combinación de
identificadores que implican una acción para ser tomada por un objeto. Los mensajes
pueden ser simples o pueden incluir parámetros que afectan como el objeto receptor
responde. La manera en que un objeto responde al mensaje puede también ser
afectado por los valores de sus atributos.
El método es la respuesta al mensaje, la forma de proporcionar el servicio. Un
método consiste en la implementación en una clase de un protocolo de respuesta a los
mensajes dirigidos a los objetos de la misma. La respuesta a tales mensajes puede
incluir el envío por el método de mensajes al propio objeto y aun a otros, también
como el cambio del estado interno del objeto.(Figura 2.5).

Figura 2. 5. Comunicación entre objetos.

2.2.4. Métodos constructores y destructores

Un método constructor es una función miembro especial que lleva a cabo la


inicialización automática de cada objeto de la clase en el momento en que se declara.
Un constructor es una función miembro pública con el mismo nombre de la clase, sin
indicación de tipo devuelto y se ejecuta automáticamente al crearse un objeto de la
clase.
Cuando un objeto no va a ser utilizado, el espacio de memoria de dinámica que
utiliza ha de ser liberado, así como los recursos que poseía, permitiendo al programa
disponer de todos los recursos posibles. A esta acción se la da el nombre de
destrucción del objeto.
El sistema de recogida de basura se ejecuta periódicamente, buscando objetos
que ya no estén referenciados.
Todas las clases tienen uno y sólo un destructor. Si no se indica lo contrario, el
compilador genera un destructor por defecto cuya única función es liberar el espacio
que ocupan los miembros estáticos (no reservados dinámicamente) del objeto que se
destruye. El destructor es una función miembro que realiza la tarea de “limpieza”. Un
destructor es una función miembro pública con el mismo nombre de la clase pero
precedido por el símbolo (~), sin indicación de tipo devuelto, que se ejecuta
automáticamente cuando se destruye un objeto.

2.2.5. Definición de datos miembro

Un dato miembro representan los datos internos del objeto, se define fuera de
cualquier método pero dentro de la clase, es una buena costumbre definirlos
inmediatamente después de la llave que abre la clase, es decir antes de la definición
de cualquier método.
Para definir un dato miembro:

Modificador_de_visibilidad tipo nombre;

El tipo de un dato miembro puede ser un tipo de datos primitivo o un objeto.


Los modificadores de visibilidad controlan el acceso al dato miembro de la clase.
Pueden ser aplicados tanto a métodos como a datos miembro y son:
 Público
 Privado
 Protegidos
Si un método o dato tiene visibilidad pública entonces puede ser referenciado
directamente desde fuera del objeto y si tiene visibilidad privada no puede ser
referenciada externamente solo puede ser usada dentro de la clase, si la visibilidad es
protegida solo podrá ser referenciada a través de métodos dentro de la misma clase
únicamente. En general, si un método ofrece servicios de una clase, puede ser
declarado con visibilidad pública. La encapsulación evita que los valores sean
cambiados por cualquiera. Por defecto la visibilidad es privada. Los datos miembros
de una clase tienen que ser privados, no públicos ni protegidos. De esta manera
aseguramos la encapsulación de la clase.

2.2.6. Definición de funciones miembro.

Son funciones que pueden aplicarse a los objetos, son las acciones o
operaciones que puede realizar un objeto. Las funciones pueden ser: estáticas, no
estáticas, constructoras, etc. Para definir una función:

Modificador_Visibilidad modificador_tipoRegreso nombre (parámetros)


{ //cuerpo
}

El modificador de visibilidad puede ser público o privado. El tipo de regreso


puede ser: un tipo primitivo, una clase o bien sin retorno. Los nombres de las
funciones miembro (métodos) de una clase tiene que ser autoexplicativos de la
finalidad de dichos métodos. Como ocurría con las funciones.
Las funciones miembros de una clase que no modifiquen los datos miembros ni
llamen a funciones que modifiquen los datos miembros de la misma deberían
declararse como constantes. De otra forma los objetos creados como constantes no
tendrían acceso a las funciones miembro de la clase.

2.3. Estructura de un Programa Orientado a Objetos.

La programación orientada a objetos (POO) es una de las técnicas más


modernas de desarrollo que trata de disminuir el coste del software, aumentando la
eficiencia y reduciendo el tiempo de espera para la puesta en escena de una nueva
aplicación. Por eso, donde la POO toma verdadera ventaja es en poder compartir y
reutilizar el código. 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, compilado, estandarizado, ampliamente
difundido, y con una biblioteca estándar C++ que lo ha convertido en un lenguaje
universal, de propósito general, y ampliamente utilizado tanto en el ámbito
profesional como en el educativo.
El lenguaje C++ es tomado como lenguaje de aprendizaje en este material de
apoyo por todas las características que nos brinda
C++ es un lenguaje de programación diseñado a mediados de los años 1980 por
Bjarne Stroustrup. La intención de su creación fue el extender al exitoso lenguaje de
programación C con mecanismos que permitan la manipulación de objetos. En ese
sentido, desde el punto de vista de los lenguajes orientados a objetos, el C++ es un
lenguaje híbrido. Posteriormente se añadieron facilidades de programación genérica,
que se sumó a los otros dos paradigmas que ya estaban admitidos (programación
estructurada y la programación orientada a objetos). Por esto se suele decir que el C+
+ es un lenguaje de programación multiparadigma.
Una particularidad del C++ es la posibilidad de redefinir los operadores
(sobrecarga de operadores), y de poder crear nuevos tipos que se comporten como
tipos fundamentales.
La meta de C++ es mejorar la productividad. Ésta viene por muchos caminos,
pero el lenguaje está diseñado para ayudarle todo lo posible, y al mismo tiempo
dificultarle lo menos posible con reglas arbitrarias o algún requisito que use un
conjunto particular de características. C++ está diseñado para ser práctico; las
decisiones de diseño del lenguaje C++ estaban basadas en proveer los beneficios
máximos al programador.

2.3.1. Estructura de un programa en C ++.

El lenguaje de programación C++ nos expresa un conjunto le reglas semánticas,


sintácticas, léxico, necesarias para que la computadora pueda entender lo que se pide
o quiere, de no ser adecuada una expresión introducida no podrá ser entendida.
Existen variedad de errores sintácticos que la maquina es capaz de discernir y
denunciar, para mayor facilidad del usuario.
Un programa en el lenguaje C++ estará compuesto por:
 Comentarios: Muestra información en cuanto a que realiza el programa, la
utilidad de las funciones, variables y objetos, estos no serán procesados por el
compilador.
 Encabezados: se realizan las llamadas y accesos a archivos de Biblioteca de
funciones estándar y diseñadas por los usuarios.
 Definiciones de prototipos: se definen las estructuras de nuevos tipos de
datos como lo son las clases, estructuras, etc., se implementan las funciones
que cumplirán un objetivo determinado.
 Declaraciones globales: Se manifiestan el uso de variables, objetos,
constantes, etc., el ámbito de estas se extiende desde el punto en el que se
definen hasta el final del programa, proporcionan un mecanismo de
intercambio de información entre funciones sin necesidad de utilizar
argumentos. Además se declaran las existencias de funciones si no han sido
implementadas.
 Declaraciones locales: Se manifiesta la existencia de variables, objetos,
constantes, etc., que serán utilizadas en una función determinada o cuerpo, la
cual no podrá extender su ámbito de existencia fuera de la función.
 Declaración de la función principal main( ): La función main es
imprescindible en cualquier programa C/C++ representa el punto de inicio de
su ejecución. Es la función de control principal de un programa, reflejándose
como el cuerpo del mismo programa.
 Implementación y cuerpo de funciones: Se define la forma o cuerpo de una
función para resolver determinada tarea.
Ejemplo:
#include <iostream.h> // Encabezados permite inclusión de biblioteca
class Punto { // Definición de prototipo
public: int x, y;
public:
Punto(int x, int y) {x=0; y=0;}
int extraer_x( )
{ return x; }
int extraer_y( )
{ return y; }
};
Punto aux; //Objeto aux declarado global
void main( ) //Cuerpo de Programa
{
Punto punto; // Objeto punto declarado local
cout << "Coordenada X:" << punto.extraer_x( ) << endl;
cout << "Coordenada Y:" << punto.extraer_y( )<< endl;
}

Como observamos en el ejemplo, un programa será una secuencia de líneas que


contendrán instrucciones, expresiones, sentencias, directivas de compilación,
comentarios, símbolos, etc.

2.3.1.1. Operadores de línea

En el lenguaje se utilizaran conjuntos de símbolos denominados operadores de


línea que permiten dar orden y control en las líneas de código. Entre estos tenemos:
operador de separación, operador de fin de instrucción, operadores de comentarios,
operadores de agrupación o bloque.
2.3.1.1.1. Operadores de comentarios ( //, /* */)

Estos símbolos nos permiten indicar la presencia de un comentario, el cual no


es más que la documentación de algún propósito a cumplir en el programa, solo será
de carácter informativo a los usuarios. Es decir, lo encontrado dentro de estos no será
procesado por el compilador, por lo tanto la maquina no notara la existencia del
contenido en la ejecución del programa.
Los comentarios se introducirán en el programa separados por /* y */
(comentario de bloque) o comenzándolos con // (comentario de línea). Los
comentarios entre /* y */ pueden tener la longitud que queramos, pero no se anidan,
es decir, si escribimos /* hola /* amigo */ mío */, el compilador interpretará que el
comentario termina antes de mío, y dará un error. Los comentarios que comienzan por
// sólo son válidos hasta el final de la línea en la que aparecen. El uso de los
comentarios son opcionales ya que no intervienen directamente sobre el programa.

2.3.1.1.2. Operador de fin de instrucción (;)

El punto y coma es uno de los operadores más usados en C++; este se usa con
el fin de indicar el final de una línea de instrucción. El punto y coma es de uso
obligatorio, toda instrucción deberá ser finalizada por este operador sino se generara
un error, excepto en el uso de directivas y definición de funciones.
También es conocido como sentencia nula al ser controlada por una sentencia
cíclica o al usarse innecesariamente de forma repetida.

instrucción1; // el operador ; indica el fin de la instrucción1


instruccion2;;; // el primer operador ; indica el fin de la instrucción2
// los siguiente operadores ; indica sentencias nulas
El punto y coma se usa también separador de contadores, condicionales e
incrementadores dentro de un sentencia for.

2.3.1.1.3. Operadores de agrupación o bloque ({ })

Un bloque es un grupo de instrucciones contenidas entre los símbolos de llave


izquierda '{' (inicio de bloque) y llave derecha '}' (fin de bloque). Las sentencias
compuestas se agrupan en bloques que pueden contener combinaciones de sentencias
elementales (denominadas sentencias de expresión) y otras sentencias compuestas.
Así las sentencias compuestas pueden estar anidadas, una dentro de otra. Cada inicio
de bloque debe tener un fin de bloque.
Su uso es obligatorio en la definición de funciones, prototipos, sentencias que
controles más de una instrucción y opcionalmente pueden aparecer en cualquier otra
parte del programa.

void main()
{ Instrucción_a;
{ Instrucción_b;
Instrucción_c;
}
}

2.3.1.1.4. Operador separación (,)

La coma tiene una doble función, por una parte separa elementos de una lista de
argumentos de una función. Por otra, puede ser usado como separador en expresiones
"de coma". Ambas funciones pueden ser mezcladas, pero hay que añadir paréntesis
para resolver las ambigüedades.
E1, E2, ..., En ;

En una expresión "de coma", cada operando es evaluado como una expresión,
pero los resultados obtenidos anteriormente se tienen en cuenta en las subsiguientes
evaluaciones. Por ejemplo:

func(i, (j = 1, j + 4), k);

Llamará a la función con tres argumentos: (i, 5, k). La expresión de coma (j = 1,


j+4), se evalúa de izquierda a derecha, y el resultado se pasará como argumento a la
función.

2.3.1.2. Directivas

El preprocesador analiza el fichero fuente antes de la fase de compilación real,


y realiza las sustituciones de macros y procesa las directivas de compilación (C++ es
un lenguaje compilado).
Una directiva de compilación o preprocesador es una línea cuyo primer carácter
es un #. Las directivas serán instrucciones que le daremos al compilador para
indicarle que realice alguna operación antes de compilar nuestro programa, como el
incluir funciones de alguna biblioteca como son los encabezados.
Algunas de las directivas tenemos:

 #define, sirve para definir macros. Las macros suministran un sistema para la
sustitución de palabras, con y sin parámetros.

#define identificador_de_macro <secuencia>


El preprocesador sustituirá cada ocurrencia del identificador_de_macro
en el fichero fuente, por la secuencia. Cada sustitución se conoce como una
expansión de la macro. La secuencia es llamada a menudo cuerpo de la macro.
 #include, permite insertar ficheros externos dentro de nuestro fichero de
código fuente. Estos ficheros son conocidos como ficheros incluidos, ficheros
de cabecera o "headers".

#include <nombre de fichero cabecera>


#include "nombre de fichero de cabecera"
#include identificador_de_macro

El preprocesador elimina la línea #include y, conceptualmente, la


sustituye por el fichero especificado. El tercer caso haya el nombre del fichero
como resultado de aplicar la macro.El código fuente en si no cambia, pero el
compilador observa el fichero incluido.
La diferencia entre escribir el nombre del fichero entre < > o " ", está en
el algoritmo usado para encontrar los ficheros a incluir. En el primer caso el
preprocesador buscará en los directorios "include" definidos en el compilador
(librerías estándar). En el segundo, se buscará primero en el directorio actual, es
decir, en el que se encuentre el fichero fuente, si no existe en ese directorio, se
trabajará como el primer caso (librerías creadas por el usuario).

2.3.1.3. Palabras reservadas


El lenguaje C++ está compuesto por un conjunto de palabras reservadas que
tiene un significado determinado para el compilador, de manera tal que cuando las
encuentra en el programa sabe que hay que llevar a cabo una determinada acción.
Las variables que se declaren a lo largo de todo el programa, así como las
funciones y demás no pueden llevar el nombre de estas palabras reservadas, ya que
son de uso por parte del compilador, ya que si se hace esto, se producirían errores, por
lo que es importante tener una buena familiarización con las mismas, a fin de no
cometer errores.
Algunas de las palabras reservadas del lenguaje se muestran en la tabla
siguiente (Figura 2.6.):

Palabras reservadas lenguaje C++


asm auto bool break case
catch char class const continue
default delete do double else
enum explicit extern false float
for friend goto if inline
int long mutable namespace new
operator private protected public register
return short signed sizeof static
struct switch template this throw
true try typedef typename union
unsigned using virtual void volatile
while
Figura 2. 6. Tabla de palabras reservadas en C++.

 asm. Medio definido por la puesta en práctica de utilización de lenguaje


de ensamblaje a lo largo de C++ .
 break. La declaración de pausa o descanso manda pasar el argumento al
partidario de la declaración, es utilizada en las llamadas a do, while, for o
switch.
 case. Se define dentro de una estructura switch, case, y default.
 const. Variable contante cuyo valor no puede ser alterado.
 continue. Envío de los pasos a seguir sin detenerse como es caso siguiente.
 catch. Maneja una excepción generado por un throw.
 class. Define una nueva clase. Pueden crearse objetos de esta clase.
 delete. Destruye un objeto de memoria creado con new.
 new. Asigna dinámicamente un objeto de memoria libre. Determina
automáticamente el tamaño del objeto.
 friend. Declara una función o una clase que sea un “friend (amigo)” de otra
clase. Los amigos pueden tener acceso a todos los miembros de datos y
a todas las funciones miembro de una clase.
 operador. Declara un operador “homónimo”.
 private. Un miembro de clase accesible a funciones miembro y a funciones
friend de la
 clase de miembros private.
 protected. Una forma extendida de acceso private; tambièn se puede tener
acceso a los miembros protected por funciones miembros de clases derivadas
y amigos de clases derivadas.
 public. Un miembro de clase accesible a cualquier función.
 template. Declara como construir una clase o una función, usando una
variedad de tipos.
 this. Un apuntador declarado en forma implícita en toda función de miembro
no static de una clase. Señales al objeto al cual esta función miembro ha sido
invocada.
 throw. Transfiere control a un manejador de excepción o termina la ejecución
del programa si no puede ser localizado un manejador apropiado.
 virtual. Declara una función virtual.

2.3.1.4. Identificadores.

Los identificadores son palabras no reservadas correspondientes a nombres de


declaraciones de variables, constantes, prototipos, funciones, etc.
Debemos seguir ciertas reglas al nombrar tipos de datos, variables, funciones,
etc. Los identificadores válidos del C++ son los formados a partir de los caracteres
del alfabeto (el inglés, no podemos usar ni la ñ ni palabras acentuadas), los dígitos
(0...9) y el subrayado (_), la única restricción es que no podemos comenzar un
identificador con un dígito (es así porque se podrían confundir con literales
numéricos).
Hay que señalar que el C++ distingue entre mayúsculas y minúsculas, por lo
que Hola y hola representan dos cosas diferentes. Hay que evitar el uso de
identificadores que sólo difieran en letras mayúsculas y minúsculas, porque inducen a
error.

2.3.1.5. Ejemplo de un programa en C++.

Un programa simple puede ser el siguiente:

/*
Este es un programa simple en C++, escribe una frase
en la pantalla
*/
#include <iostream.h>
int main( )
{
cout << "Hola mundo\n"; // imprime en la pantalla la frase "hola mundo"
return 0;
}

Analizando el código del ejemplo observamos:


La primera parte separada entre /* y */ es un comentario. Es recomendable que
se comenten los programas, explicando que es lo que estamos haciendo en cada caso,
para que cuando se lean sean más comprensibles.
La línea que empieza por # es una directiva. En este caso indica que se incluya
el fichero "iostream.h", que contiene las definiciones para entrada/salida de datos en
C++.
En la declaración de main ( ) hemos incluido la palabra reservada int, que
indica que la función devuelve un entero al finalizar su propósito.
La llave izquierda nos indica el inicio del cuerpo de la función main ( ).
La sentencia separada entre llaves indica que se escriba la frase "Hola mundo".
El operador << escribe (inserta) el segundo argumento en el primero. En este caso la
cadena "Hola mundo\n" se escribe en la salida estándar (cout). El carácter \ seguido
de otro carácter indica un solo carácter especial, en este caso un salto de línea (\n).
La palabra reservada return nos indica retornar un valor de la mismo tipo que
el definido en la función. En este caso retornamos el valor numérico entero 0.
La llave derecha nos indica el fin del cuerpo de la función main ( ).

CAPITULO 3

TIPOS DE DATOS

3.1. Tipos de datos.

C++ no soporta un gran número de tipos de datos predefinidos, pero tiene la


capacidad para crear sus propios tipos de datos. Posee un conjunto de tipos simples
correspondientes a las unidades de almacenamiento típicas de un computador y a las
distintas maneras de utilizarlos. Todos los tipos de datos simples o básicos de C/C++
son, esencialmente, números. Los tres tipos de datos básicos son:
 Enteros: int
 Números flotantes (reales): float, double
 Caracteres: char

Los tipos simples de datos admitidos se muestran en la siguiente tabla de rango


de valores. (Figura 3.1)

Denominación Tipo de datos Tamaño en bits Rango de valores


char Carácter 8 de 0 a 255
int Número entero 16 de –32768 a 32767
Número real de
float 32 de 3.4 x 10-38 a 3.4 x 1038
precisión simple
Número real de
double 64 de 1.7 x 10-308 a 1.7 x 10308
precisión doble
void Tipo vacío 0 sin valor
Figura 3. 1. Tabla de los tipos de datos simples en C++.

Los tamaños en bits pueden variar dependiendo del compilador empleado. Por
ejemplo, gcc interpreta que el entero es de 32 bits, y para usar enteros de 16 bits hay
que indicarlo expresamente. Por tanto, no debe usted presuponer ningún tamaño
concreto para los tipos si quiere escribir programas portables.
El tipo char se usa normalmente para variables que guardan un único carácter,
aunque lo que en realidad guardan es un código ASCII, es decir, un número entero de
8 bits sin signo (de 0 a 255). Los caracteres se escriben siempre entre comillas
simples (‘…’). Por lo tanto, si suponemos que x es una variable de tipo char, estas dos
asignaciones tienen exactamente el mismo efecto, ya que 65 es el código ASCII de la
letra A:
x = 'A';
x = 65;
A diferencia de las comillas simples de los caracteres sueltos cadenas de
caracteres se escriben con comillas dobles (”…”).
El tipo int se usa para números enteros, mientras que los tipos float y double
sirven para números reales. El segundo permite representar números mayores, a costa
de consumir más espacio en memoria.
Un tipo especial del C++ es el denominado void (vacío). Este tipo tiene
características muy peculiares, ya que es sintácticamente igual a los tipos elementales
pero sólo se emplea junto a los derivados, es decir, no hay objetos del tipo void. Se
emplea para especificar que una función no devuelve nada, para declarar funciones
sin argumentos; o como base para punteros a objetos de tipo desconocido. Por
ejemplo:

void BoraPrantalla (void);

indica que la función BorraPantalla no tiene parámetros y no retorna nada.

3.1.1. Modificadores de tipo.

Existen, además, unos modificadores de tipo que pueden preceder a los tipos de
datos. Dichos modificadores son:
 signed: obliga a que los datos se almacenen con signo.
 unsigned: los datos se almacenan sin signo.
 long: los datos ocuparán el doble de espacio en bits del habitual, y, por lo
tanto, aumentará su rango de valores.
 short: los datos ocuparán la mitad del espacio habitual, y, por lo tanto,
disminuirá su rango de valores..

De este modo, nos podemos encontrar, por ejemplo, con estos tipos de datos:

 unsigned int: Número entero de 16 bits sin signo. Rango: de 0 a 65535.


 signed int: Número entero de 16 bits con signo. No tiene sentido, porque el
tipo int ya es con signo por definición, pero es sintácticamente correcto.
 signed char: Carácter (8 bits) con signo. Rango: de –128 a 127
 long int: Número entero de 32 bits. Rango: de –2147483648 a 2147483647
Incluso podemos encontrar combinaciones de varios modificadores. Por
ejemplo:
 unsigned long int: Número entero de 32 bits sin signo. Rango: de 0 a
4294967295

3.1.2. Tipos enumerados.

Un tipo especial de tipos enteros son los tipos enumerados. Estos tipos sirven
para definir un tipo que sólo puede tomar valores dentro de un conjunto limitado de
valores. Estos valores tienen nombre, luego se da una lista de constantes asociadas a
este tipo. La sintaxis es:

enum booleano {FALSE, TRUE}; // definimos el tipo booleano

Aquí hemos definido el tipo booleano que puede tomar los valores FALSE o
TRUE. En realidad hemos asociado la constante FALSE con el número 0, la
constante TRUE con 1, y si hubiera más constantes seguiríamos con 2, 3, etc.
Si por alguna razón nos interesa dar un número concreto a cada valor podemos
hacerlo en la declaración:

enum colores {rojo = 4, azul, verde = 3, negro = 1};

El azul tomará el valor 5 (4+1), ya que no hemos puesto nada. También se


pueden usar números negativos o constantes ya definidas.
Si al definir un tipo enumerado no se le da nombre al tipo declaramos una serie
de constantes:

enum { CERO, UNO, DOS };

Hemos definido las constantes CERO, UNO y DOS con los valores 0, 1 y 2.
3.1.3. Tipos derivados

De los tipos fundamentales podemos derivar otros mediante el uso de los


siguientes operadores de declaración:

* Puntero: Para cualquier tipo T, el puntero a ese tipo es T*. Una variable de
tipo T* contendrá la dirección de un valor de tipo T.
& Referencia: Una referencia es un nombre alternativo a un objeto, se emplea
para el paso de argumentos y el retorno de funciones por referencia. T& significa
referencia a tipo T.
[] Arreglos: Para un tipo T, T[n] indica un tipo arreglo con n elementos. Los
índices del arreglo empiezan en 0, luego llegan hasta n-1. Podemos definir arreglos
multidimensionales como arreglos de arreglos.
() Función: La declaración de una función nos da el nombre de la función, el
tipo del valor que retorna y el número y tipo de parámetros que deben pasársele.
Ejemplos:

int *n; // puntero a un entero


int v[20]; // arreglo de 20 enteros
int *c[20]; // arreglo de 20 punteros a entero
void f(int j); // función con un parámetro entero

3.1.4. Tipos compuestos

Los tipos de datos compuestos en C++ son:


 Estructuras. Las estructuras son el tipo equivalente a los registros de otros
lenguajes, se definen poniendo la palabra struct delante del nombre del tipo y
colocando entre llaves los tipos y nombres de sus campos. Si después de
cerrar la llave ponemos una lista de variables las declaramos a la vez que
definimos la estructura.
 Uniones. Las uniones son idénticas a las estructuras en su declaración, con la
particularidad de que todos sus campos comparten la misma memoria (el
tamaño de la unión será el del campo con un tipo mayor).
 Clases. Las clases son estructuras con una serie de características especiales.

3.1.5. Datos estáticos y datos dinámicos

Otra forma de clasificar los tipos de datos:

 Datos estáticos: su tamaño y forma es constante durante la ejecución de un


programa y, por tanto, se determinan en tiempo de compilación. El ejemplo
típico son los arreglos. Tienen el problema de que hay que dimensionar la
estructura de antemano, lo que puede conllevar desperdicio o falta de
memoria.

 Datos dinámicos: su tamaño y forma es variable (o puede serlo) a lo largo de


un programa, por lo que se crean y destruyen en tiempo de ejecución. Esto
permite dimensionar la estructura de datos de una forma precisa: se va
asignando memoria en tiempo de ejecución según se va necesitando.

3.2. Variables y constantes.

3.2.1. Variable.
En C/C++ una variable es una posición con nombre en memoria donde se
almacena un valor de un cierto tipo de dato y puede ser modificado. Las variables
pueden almacenar todo tipo de datos: Caracteres, números, estructuras.
Las variables están compuestas por dos partes importantes que son:
 El contenido, que es el valor que se almacena o está almacenado en la
variable y está directamente relacionada al mismo nombre de la variable.
 La dirección de memoria, que nos indica la posición o dirección donde se
almacena el contenido de la variable en la memoria de ejecución. Esta se
representa mediante el operador de dirección & acompañada del nombre de la
variable.

Sea una variable con el nombre A. Se representa de esta: su contenido es A, y su


dirección es &A. (Figura 3.2)

Figura 3. 2. Variable.

3.2.1.1. Declaración de las variables

Una variable típicamente tiene un nombre (un identificador) que describe su


propósito. Toda variable utilizada en un programa debe ser declarada previamente. La
definición utilizada en un programa en cualquier parte del programa. Una definición
reserva un espacio de almacenamiento en memoria. El procedimiento para definir
(crear) una variable es escribir el tipo de dato, el identificador o nombre de la variable
y, en ocasiones, el valor inicial que tomará.
La declaración de variables puede considerarse como una sentencia. Desde este
punto de vista, la declaración terminará con un ";".
Sintaxis de declaración de una variable:

tipodato nombrevariable;

Ejemplos:

int a;
float d;
char e;

Sintaxis de declaración de n variables:

tipodato nomvar1, nomvar2, nomvar3,…, nomvarn;

Ejemplo:

int a, b, c;

Cuando declaramos más de una variable usando el separador (,), estas reservan
posiciones de memorias consecutivas del tamaño indicado por el tipo de dato.
Observando el ejemplo anterior se notara que las variables a, b y c reservan
posiciones de memoria del tamaño del tipo de dato int que es de 2 bytes. Es decir que
si la dirección de la variable a (&a) es 0x1000, entonces la dirección de la variable b
(&b) será la igual a la siguiente posición de memoria del tamaño del tipo int, 0x1002
(las posiciones de memoria del computador se representan en notación hexadecimal y
su tamaño será de 1 byte). (Figura 3.3)

Figura 3. 3. Declaración de variables consecutivas.

También es posible inicializar las variables dentro de la misma declaración.


Sintaxis de declaración e inicialización de variables:

tipodato nombrevariable = inicialización;


ó
tipodato nomvar1=inicial1, nomvar2=inicial2, … , nomvarn= inicialn;

Ejemplos:

int a = 5;
int a = 2, b = 4, c = 6;

Las variables no inicializadas tienen un valor, este valor lo conocemos como


"basura", ya que el valor es desconocido que puede interferir en un programa.
Las variables no pueden tener el mismo nombre que una “palabra reservada”
del lenguaje. No se les pueden colocar espacios en blanco. Los nombres de variables
solo pueden tener letras, dígitos numéricos y el guión bajo ó subguión (_).

Los nombres de variables no pueden llevar caracteres especiales, ejemplo:


caracteres acentuados, ñ, *, /, -, etc.
Deben comenzar por un carácter (letra no numero) o también pueden comenzar
con un guión bajo (_), ejemplo: _costo.

3.2.1.2. Ámbito de las variables

Dependiendo de dónde se declaren las variables, podrán o no ser accesibles


desde distintas partes del programa. Las variables declaradas dentro de un bucle,
serán accesibles sólo desde el propio bucle, serán de ámbito local del bucle.
Las variables declaradas dentro de una función, sólo serán accesibles desde esa
función. Esas variables son variables locales o de ámbito local de esa función.
Las variables declaradas fuera de las funciones, normalmente antes de definir
las funciones, en la zona donde se declaran los prototipos, serán accesibles desde
todas las funciones. Estas variables serán globales o de ámbito global.

3.2.1.2.1. Variables Locales

Las variables locales son aquellas definidas en el interior de una función y son
visibles sólo en esta función específica. Las reglas por las que se rigen las variables
locales son:
 En el interior de una función, una variable local no puede ser modificada por
ninguna sentencia externa a la función.
 Los nombres de las variables locales no han de ser únicos. Dos, tres o más
funciones - por ejemplo: pueden definir variables de nombre Interruptor. Cada
variable es distinta y pertenece a la función en que está declarada.
 Las variables locales de las funciones no existen en memoria hasta que se
ejecuta la función. Esta propiedad permite ahorrar memoria, ya que permite
que varias funciones compartan la misma memoria para sus variables locales
(pero no a la vez).

Ejemplo:

#include <iostream.h>
#include <conio.h>
void main()
{ //declaración de variables locales de la función main()
int x, y, a; //variables locales x, y, a

}

void suma()
{ //declaración de variables locales de la función suma()
int a, b; //variable locales a, b

}

3.2.1.2.2. Variables Globales

Las Variables Globales son variables que se declaran fuera de la función y por
defecto (omisión) son visibles a cualquier función incluyendo main ().
Ejemplo:

#include <iostream.h>
#include <conio.h>
int a, c, b, x; //declaración de variables globales
main()
{
//declaración de variables locales
}

3.2.1.3. Tipos de Almacenamientos

Las variables por su parte pueden tener distinto tipo de almacenamiento,


dependiendo éste de las partes del código en el que van a ser utilizadas.
 Static
Una variable estática existe desde que el programa comienza su ejecución y
dura hasta que el programa termina. Esta característica permite retener el
valor de una variable incluso aunque la ejecución del programa salga fuera del
ámbito en el que ha sido declarada. Se declara anteponiendo la palabra
reservada static a la declaración de la variable. Considere el siguiente
fragmento de código:

ejemplo( )
{
static int x=0;
x++;
...
}

En este caso, static modifica la declaración de la variable x, que por defecto


es local a la función, de manera que si bien el ámbito de x sigue siendo la
función (no se puede utilizar fuera de ella), ésta no se destruye una vez
ejecutada la función, sino que sigue en memoria conservando su valor, y si
la función es llamada por segunda vez, el valor de la variable x ya no será 0
sino 1. Las variables globales que se declaran en cada archivo son por
defecto static.
 Extern
La palabra reservada extern sirve para declarar un nombre de función o
variable como externa, y permite referencia una declaración que se encuentra
en otro archivo. Esta característica fue diseñada originalmente para facilitar la
compilación separada de archivos, en este curso, no la utilizaremos de
momento.
 Auto
Es la declaración de una variable local. Se usa para definir el ámbito temporal
de una variable local, es utilizada por defecto en las funciones.
 Register
Cuando a la declaración de una variable le antecede la palabra reservada
register se indica al compilador que la variable se almacenará en uno de los
registros del hardware del microprocesador. La palabra clave register, en una
sugerencia, no un mandato, al compilador. Una variable register debe ser local
a una función. La razón de utilizar variables register reside en que las
operaciones sobre los valores situadas en los registros son normalmente más
rápidas que las realizadas sobre valores situados en memoria, por lo que se
aumente la eficacia y velocidad del programa.
Una aplicación típica es el uso de una variable register como variable de
control de un bucle; de este modo se reduce el tiempo en el la CPU requiere
para buscar el valor de la variable en memoria. Por ejemplo:

register int i;
for (i=1; i<10000; i++)
{
...
}
3.2.1.4. Conversión explícita de tipos de datos

En C++ está permitida una conversión explícita del tipo de una expresión
mediante una construcción que tiene la forma nombre_del_tipo (expresión). La
expresión es convertida al tipo especificado. Por ejemplo la función raíz cuadrada
(sqrt) devuelve un resultado de tipo double. Para poder asignar este resultado a una
variable de otro tipo, por ejemplo de tipo int, tendremos que escribir:

int a;
a = int (sqrt ( 2 ));

Una variable de un determinado tipo no siempre puede ser convertida


explícitamente a otro tipo.

3.2.2. Constantes

Las constantes son tipos de datos (con valores numéricos o de cadena) que
permanecen invariables, sin posibilidad de cambiar el valor que tienen durante el
curso del programa. Una constante corresponde a una longitud fija de un área
reservada en la memoria principal del ordenador, donde el programa almacena
valores fijos. Una constante también puede ser definida, como una variable cuyo
valor o contenido no puede ser modificado.
Por ejemplo: El valor de pi = 3.141592

3.2.2.1. Constantes definidas.

Las constantes pueden recibir nombres simbólicos mediante la directiva


#define, esto significa que esa constante tendrá el mismo valor a lo largo de todo el
programa. El identificador de una constante así definida será una cadena de caracteres
que deberá cumplir los mismos requisitos que el de una variable (sin espacios en
blanco, no empezar por un dígito numérico, etc.).
Ejemplo:

#include <stdio.h>
#define PI 3.1415926 // constante definida PI
int main()
{
printf("Pi vale %f", PI);
return 0;
}

Lo cual mostrará por pantalla:

Pi vale 3.1415926

Es decir, PI es una constante a la que le hemos asignado el valor 3.1415926


mediante la directiva #define. La directiva #define también se puede utilizar para
definir expresiones más elaboradas con operadores (suma, resta, multiplicación, etc.)
y otras constantes que hayan sido definidas previamente, por ejemplo:

#define X 2.4
#define Y 9.2
#define Z X + Y

Otro ejemplo del uso de las constantes definidas:

#include <stdio.h>
#define escribe printf
main()
{
int r;
escribe("Ingrese un numero: ");
scanf("%d",&r);
escribe("El cuadrado del numero es: %d",r*r);
}

3.2.2.2. Constantes de enumeración.

Se caracterizan por poder adoptar valores entre una selección de constantes


enteras denominadas enumeradores; estos valores son establecidos en el momento
de la declaración del nuevo tipo. Como se ha señalado, son enteros y (una vez
establecidos) de valor constante.
Ejemplo:

enum { MALO, BUENO, REGULAR };

Hemos definido las constantes MALO, BUENO y REGULAR con los valores
0, 1 y 2.

enum { PRIMERO=1, SEGUNDO, TERCERO= 5 };

Hemos definido las constantes PRIMERO, SEGUNDO y TERCERO con los


valores 1, 2 (si no se establece asume el siguiente valor numérico) y 5.

3.2.2.3. Constantes declaradas


El especificador constante (const), permite crear o declarar entidades cuyo
valor no se puede modificar. Una vez que una constante se declara no se puede
modificar dentro del programa. Las constantes deben ser inicializadas cuando se
declaran.

La palabra clave const se utiliza para hacer que un objeto-dato, señalado por un
identificador, no pueda ser modificado a lo largo del programa (sea constante). El
especificador const se puede aplicar a cualquier objeto de cualquier tipo, dando lugar
a un nuevo tipo con idénticas propiedades que el original pero que no puede ser
cambiado después de su inicialización (se trata pues de un verdadero especificador de
tipo).
Cuando se utiliza en la definición de parámetros de funciones o con miembros
de clases, tiene significados adicionales especiales.
Sintaxis:

const [<tipo-de-variable>] <nombre-de-variable> [ = <valor> ];

Ejemplo:

const int longitud = 20;


char array[longitud];

La palabra clave const declara que un valor no es modificable por el programa.


Puesto que no puede hacerse ninguna asignación posterior a un identificador
especificado como const, esta debe hacerse inevitablemente en el momento de la
declaración, a menos que se trate de una declaración extern.
Las constantes se pueden utilizar para sustituir a #define.
Ejemplo:
const PI = 3.141592 // sustituye a #define PI 3.141592
const long = 128 // sustituye a #define long 128
3.3. Operadores y operaciones.

3.3.1. Operadores

Un operador es un carácter o grupo de caracteres que actúa sobre una, dos o


más variables u operandos o expresiones que se encuentran en una operación para
obtener un resultado.
Ejemplo típicos de operadores son la suma (+), la diferencia (-), el producto
(*), etc. Los operadores pueden ser unarios, binarios y terciarios, según actúen sobre
uno, dos o tres operandos, respectivamente. Hay varios tipos de operadores,
clasificados según el tipo de objetos sobre los que actúan.

3.3.1.1. Operadores aritméticos

Son usados para crear expresiones matemáticas. Existen dos operadores


aritméticos unitarios, '+' y '-' que tienen la siguiente sintaxis:

+ <expresión>
- <expresión>

Asignan valores positivos o negativos a la expresión a la que se aplican.


En cuanto a los operadores binarios existen varios. '+', '-', '*' y '/', tienen un
comportamiento análogo, en cuanto a los operandos, ya que admiten enteros y de
coma flotante. Se trata de las conocidísimas operaciones aritméticas de suma, resta,
multiplicación y división.
Sintaxis:
<expresión> + <expresión>
<expresión> - <expresión>
<expresión> * <expresión>
<expresión> / <expresión>
<expresión> % <expresión>

El operador de módulo '%', devuelve el resto de la división entera del primer


operando entre el segundo. Por esta razón no puede ser aplicado a operandos en coma
flotante.
Cuando las expresiones que intervienen en una de estas operaciones sean
enteras, el resultado también será entero. Por otro lado si las expresiones son en punto
flotantes, con decimales, el resultado será en punto flotante.

3.3.1.2. Operadores incrementales

Son dos operadores unitarios, se trata de operadores un tanto especiales, ya que


sólo pueden trabajar sobre variables, pues implican una asignación. Se trata de los
operadores '++' y '--'. El primero incrementa el valor del operando y el segundo lo
decrementa, ambos en una unidad. Existen dos modalidades, dependiendo de que se
use el operador en la forma de prefijo o de sufijo.
Sintaxis:

<variable> ++ // (post-incremento)
++ <variable> // (pre-incremento)
<variable>-- // (post-decremento)
-- <variable> // (pre-decremento)

En su forma de prefijo, el operador es aplicado antes de que se evalúe el resto


de la expresión; en la forma de sufijo, se aplica después de que se evalúe el resto de la
expresión. Veamos un ejemplo, en las siguientes expresiones "a" vale 100 y "b" vale
10:

c = a + ++b;

En este primer ejemplo primero se aplica el pre-incremento, y b valdrá 11 a


continuación se evalúa la expresión "a+b", que dará como resultado 111, y por último
se asignará este valor a c, que valdrá 111.

c = a + b++;

En este segundo ejemplo primero se avalúa la expresión "a+b", que dará como
resultado 110, y se asignará este valor a c, que valdrá 110. Finalmente se aplica en
post-incremento, y b valdrá 11.
Los operadores unitarios sufijos (post-incremento y post-decremento) se
evalúan después de que se han evaluado el resto de las expresiones. En el primer
ejemplo primero se evalúa ++b, después a+b y finalmente c =<resultado>. En el
segundo ejemplo, primero se evalúa a+b, después c = <resultado> y finalmente b++.

3.3.1.3. Operadores de asignación

Los operadores de asignación atribuyen a una variable, es decir, depositan en la


zona de memoria correspondiente a dicha variable, el resultado de una expresión o
valor de otra variable.
Existen varios operadores de asignación, el más evidente y el más usado es el
"=", pero no es el único.
Entre los operadores de asignación tenemos: "=", "*=", "/=", "%=", "+=", "-=".
Y la sintaxis es:
<variable> <operador de asignación> <expresión>

En general, para todos los operadores mixtos la expresión:

Expresion1 op= Expresion2

Tiene el mismo efecto que la expresión:

Expresion1 = Expresion1 op Expresion2

El funcionamiento es siempre el mismo, primero se evalúa la expresión de la


derecha, se aplica el operador mixto, si existe y se asigna el valor obtenido a la
variable de la izquierda. En la siguiente tabla se muestra estas operaciones y sus
equivalencias. (Figura 3.4).

Operador Sentencia Abreviada Sentencia no Abreviada


+= m=+n m = m + n;
-= m-=n m = m - n;
*= m*=n m = m * n;
/= m/=n m = m / n;
%= m=%n m=m%n
Figura 3. 4. Tabla de Equivalencia de Operadores de Asignación

3.3.1.4. Operadores relacionales

Los operadores relacionales comprueban la igualdad o desigualdad entre dos


valores o expresiones.
Sintaxis:

<expresión1> > <expresión2>


<expresión1> < <expresión2>
<expresión1> <= <expresión2>
<expresión1> >= <expresión2>
<expresión1> == <expresión2>
<expresión1> != <expresión2>

El resultado de cualquier evaluación de este tipo, es un valor verdadero (true) o


falso (false). Siendo verdadero cualquier valor distinto de 0, aunque por lo general se
usa el valor 1 y falso representado por el valor 0.

El significado de cada operador es evidente:


> mayor que
< menor que
>= mayor o igual que
<= menor o igual que
== igual a
!= distinto a

Todos los operadores relacionales son operadores binarios (tienen 2 operandos),


y su forma general es:

Expresión1 op Expresión2

3.3.1.5. Operadores lógicos

Los operadores "&&", "||" y "!" relacionan expresiones lógicas, formando a su


vez nuevas expresiones lógicas.
Sintaxis:
<expresión1> && <expresión2>
<expresión1> || <expresión2>
!<expresión>

El operador "&&" equivale al "AND" o "Y"; devuelve "true" sólo si las dos
expresiones evaluadas son "true" o distintas de cero, en caso contrario devuelve
"false" o cero. Si la primera expresión evaluada es "false", la segunda no se evalúa.
Generalizando, con expresiones AND con más de dos expresiones, la primera
expresión falsa interrumpe el proceso e impide que se continúe la evaluación del resto
de las expresiones. Esto es lo que se conoce como "cortocircuito", y es muy
importante. A continuación se muestra la tabla de verdad del operador && (Figura
3.5):

Expresión1 Expresión2 Expresión1 && Expresión2


false ignorada false
true false false
true true true
Figura 3. 5. Tabla de verdad del operador "&&":

El operador "||" equivale al "OR" u "O inclusivo"; devuelve "true" si cualquiera


de las expresiones evaluadas es "true" o distinta de cero, en caso contrario devuelve
"false" o cero. Si la primera expresión evaluada es "true", la segunda no se evalúa. A
continuación se muestra la tabla de verdad del operador "||". (Figura 3.6):

Expresión1 Expresión2 Expresión1 || Expresión2


false false false
false true true
true ignorada true
Figura 3. 6. Tabla de verdad del operador "||" .
El operador "!" es equivalente al "NOT", o "NO", y devuelve "true" sólo si la
expresión evaluada es "false" o cero, en caso contrario devuelve "false".
La expresión "!E" es equivalente a (0 == E). A continuación se muestra la tabla
de verdad del operador "!" (Figura 3.7):

Expresión !Expresión
false true
true false
Figura 3. 7. Tabla de verdad del operador "!".

3.3.1.6. Operador "sizeof"

Este operador tiene dos usos diferentes.


Sintaxis:

sizeof (<expresión>)
sizeof (nombre_de_tipo)

En ambos casos el resultado es una constante entera que da el tamaño en bytes


del espacio de memoria usada por el operando, que es determinado por su tipo. El
espacio reservado por cada tipo depende de la plataforma.
En el primer caso, el tipo del operando es determinado sin evaluar la expresión,
y por lo tanto sin efectos secundarios. Si el operando es de tipo "char", el resultado es
1. A pesar de su apariencia, sizeof() NO es una función, sino un OPERADOR.

3.3.1.7. Operadores a nivel de bit enteros

Los operadores a nivel de bit operan independientemente sobre cada uno de los
bits de un valor.
NOT: El operador NOT unario, ~, invierte todos los bits de su operando. Por
ejemplo, en número 42, que tiene el siguiente patrón de bits 00101010 se convierte en
11010101 después de aplicar el operador NOT.
AND: El operador AND, &, combina los bits de manera que se obtiene un 1 si
ambos operandos son 1, obteniendo 0 en cualquier otro caso.
00101010 (representación en byte del numero 42)
&
00001111 (representación en byte del numero 15)
= 00001010 (representación en byte del numero 10)
OR: El operador OR, |, combina los bits de manera que se obtiene un 1 si
cualquiera de los operandos es un 1.
00101010 (representación en byte del numero 42)
|
00001111 (representación en byte del numero 15)
= 00101111 (representación en byte del numero 47)
XOR: El operador XOR, ^, combina los bits de manera que se obtiene un 1 si
cualquiera de los operandos es un 1, pero no ambos, y cero en caso contrario.
00101010 (representación en byte del numero 42)
^
00001111 (representación en byte del numero 15)
= 00100101 (representación en byte del numero 37)

La tabla siguiente muestra cómo actúa cada operador a nivel de bit sobre cada
combinación de bits de operando (Figura 3.8):

A B OR AND XOR NOT


0 0 0 0 0 1
1 0 1 0 1 1
0 1 1 0 1 1
1 1 1 1 0 0
Figura 3. 8. Tabla de los operadores a nivel de bit.

Desplazamiento a la izquierda: El operador desplazamiento a la izquierda,


<<, mueve hacia la izquierda todos los bits del operando de la izquierda un número de
posiciones de bit especificado en el operando de la derecha. Al realizarse el
desplazamiento se pierden por el extremo izquierdo del operando el número de bits
desplazados y se rellena el operando con ceros por la derecha el mismo número de
bits.
Desplazamiento a la derecha: El operador desplazamiento a la derecha, >>,
mueve hacia la derecha todos los bits del operando de la izquierda un número de
posiciones de bit especificado por el operando de la derecha.
Ejemplo: Si se desplaza el valor 35 a la derecha dos posiciones de bit, se
obtiene como resultado que el valor 8.

a = a >> 2;
int a = 35;

Cuando un valor tiene bits que se desplazan fuera por la parte izquierda o
derecha de una palabra, esos bits se pierden. Si se estudian en binario estas
operaciones se observa con mayor claridad.
00100011 (representación en byte del numero 35)
>> 2
= 00001000 (representación en byte del numero 8)

3.3.1.8. Operador condicional ternario ?:


El operador condicional (?:) el cual es conocido por su estructura como
ternario. Este operador permite controlar el flujo de ejecución del programa. Permite
evaluar situaciones tales como: Si se cumple tal condición entonces haz esto, de lo
contrario haz esto otro.
Sintaxis:

( (condición) ? proceso1 : proceso2 )

En donde, condición es la expresión que se evalúa, proceso1 es la tarea a


realizar en el caso de que la evaluación resulte verdadera, y proceso2 es la tarea a
realizar en el caso de que la evaluación resulte falsa.
Ejemplo:

int edad;
cout << "Cual es tu edad: ";
cin >> edad;
cout << ( (edad < 18) ? "Eres joven aun" : "Ya tienes la mayoría de edad" );

3.4. Expresiones.

Una expresión es una combinación de operadores y operandos de cuya


evaluación se obtiene un valor. Los operandos pueden ser nombres que denoten
objetos variables o constantes, funciones, literales de cualquier tipo adecuado de
acuerdo con los operadores u otras expresiones más simples. La evaluación de una
expresión da lugar a un valor de algún tipo, una expresión se dice que es del tipo de
su resultado.
Ejemplos de expresiones:

a + 5*b
(a >= 0) && ((b+5) > 10)
a
-a * 2 + b
--b + (- 4*a*c)

Las expresiones se evalúan de acuerdo con la precedencia de los operadores.


Ante una secuencia de operadores de igual precedencia, la evaluación se realiza según
el orden de escritura, de izquierda a derecha. El orden de evaluación puede
modificarse usando operadores que denoten precedencia como los paréntesis.

3.4.1. Reglas de precedencias

El resultado de una expresión depende del orden en que se ejecutan las


operaciones. Por ejemplo considere la siguiente expresión: 3 + 4 * 2. Si se resuelve
primero la suma y luego la multiplicación el resultado será 14. Pero si se realiza
primero la multiplicación y luego la suma el resultado es 11. Con el objeto de que el
resultado de una expresión sea claro e inequívoco, es necesario crear reglas que
definan el orden de ejecución.
La interpretación de cualquier expresión en C++ está determinada por la
precedencia y asociatividad de los operadores en dicha expresión. Cada operador
tiene una precedencia, y los operadores en una expresión se evalúan en orden de
mayor a menor precedencia. La evaluación de operadores con la misma precedencia
viene determinada por su asociatividad. Los paréntesis anulan las reglas de
precedencia.
En la siguiente tabla se listan los operadores en C++, su precedencia y su
asociatividad. Los operadores se listan en orden de prioridad decreciente (los situados
más arriba tienen mayor prioridad). Los operadores en la misma línea horizontal
tienen la misma precedencia.
3.4.2. Tabla de precedencia de los operadores en C++.

En la siguiente tabla se muestran las prioridades de los operadores en el


lenguaje C++. (Figura 3.9):

Operador Propósito Asociatividad


() Denota precedencia en una De izquierda a derecha
expresión
sizeof Tamaño de un objeto De derecha a izquierda
++ -- Incremento y decremento prefijo De derecha a izquierda
! ~ + - Operadores Unario De derecha a izquierda
* / % Operaciones aritméticas De izquierda a derecha
multiplicación, división y modulo
+ - Operaciones aritméticas adicción De izquierda a derecha
y sustracción
<< >> Desplazamiento binario De izquierda a derecha
< > <= >= Operadores de relación De izquierda a derecha
== != Operadores de igualdad De izquierda a derecha
& Y binario De izquierda a derecha
^ O exclusivo binario De izquierda a derecha
| O inclusivo binario De izquierda a derecha
&& Y lógico De izquierda a derecha
|| O lógico De izquierda a derecha
?: Operador condicional De izquierda a derecha
= *= /= += Operadores de asignación De derecha a izquierda
-= &= ^= |=
%= <<=
++ -- Incremento y decremento sufijo De derecha a izquierda
, Separador coma De izquierda a derecha
Figura 3. 9. Tabla de prioridades de los operadores.

3.4.3. Ejemplo de expresiones.

Ejemplo 1:
Se tiene la siguiente expresión y=2*5*5+3*5+7, se desea evaluarla y encontrar
el resultado.
Solución:
Se resuelve tomando encuentra la precedencia de los operadores y su
asociatividad.
y = 2 * 5 * 5 + 3 * 5 + 7;
2 * 5 = 10 (multiplicación más a la izquierda primero)
y = 10 * 5 + 3 * 5 + 7;
10 * 5 = 50 (Multiplicación más a la izquierda)
y = 50 + 3 * 5 + 7;
3 * 5 = 15 (Multiplicación antes de la suma)
y = 50 + 15 + 7;
50 + 15 = 65 (Suma más a la izquierda)
y = 65 + 7;
65 + 7 = 72 (Se resuelve la última operación aritmética)
y = 72 (por último se realiza la asignación a la variable y)
Ejemplo 2:
Dados a=1, b=2 y c=3 efectúe la siguiente expresión:
d = 10 * a > c * 10 + b
Solución:
Se resuelve tomando encuentra la precedencia de los operadores y su
asociatividad.
d = 10 * 1 > 3 * 10 + 2
(Se realiza primero la operación aritmética de mayor precedencia más a la
izquierda 10*1 y luego 3*10)
d = 10 > 30 + 2
(Se realiza primero la operación aritmética 30 + 2)
d = 10 > 32
(Se resuelve la operación relacional 10>32 dando como resultado un valor
falso. Todo valor falso es igual a 0)
d = 0 (por último se realiza la asignación a la variable d)

Ejemplo 3:
Efectúe la siguiente expresión: d = 10 > 5 && 3 * 10
Solución:
Se resuelve tomando encuentra la precedencia de los operadores y su
asociatividad. A pesar de que el operador lógico && tiene menor prioridad obliga a
resolver la expresión más a la izquierda 10 > 5, aunque el operador aritmético * tenga
mayor prioridad.
d = 10 > 5 && 3 * 10
(Se separa la expresión debido a la presencia del operador lógico y se
resuelve la primera expresión 10 > 5, siendo su resultado verdadero, todo valor
verdadero será representado por el valor 1)
d = 1 && 3 * 10
(Dado un resultado verdadero en la primera expresión se resuelve la
siguiente 3 * 10, de haber resultado falsa toda la expresión seria falsa y no
realizaría dicha operación. Al resolver la operación 3 * 10 el resultado es 30,
donde todo valor diferente a cero es un valor verdadero.)
d = 1 && 1
(El resultado final de la expresión será un valor verdadero)
d = 1 (por último se realiza la asignación a la variable d)

Ejemplo 4:
Dado a = 1, b = 2 efectúe la siguiente expresión:
d = a++ > 5 / 2 || 3 * --b < ( 3 + a)
Solución:
Se resuelve tomando encuentra la precedencia de los operadores y su
asociatividad. A pesar de que el operador lógico || tiene menor prioridad obliga a
resolver la expresión mas a la izquierda a++ > 5 / 2, aunque el las operación
decremento prefijo tenga mayor prioridad.
d = a++ > 5 / 2 || 3 * --b < (3 + a)
(Resolvemos la expresión a++ > 5 / 2)
a++ > 5 / 2
(La operación incremento sufijo por tener menor prioridad se resuelve
después de todas las operaciones siendo resuelta después de finalizar toda la
expresión a++ > 5 / 2, se asigna el valor de la variable a)
1>5/2
(se resulte la operación aritmética de mayor precedencia 5 / 2 y luego la
operación relacional 1 > 2, siendo falsa)
d = 0 || 3 * --b < (3 + a)
(Antes de resolver la próxima expresión primero se realiza el incremento
de sufijo, dando como resultado el valor a en 2)
(Resolvemos la expresión a++ > 5 / 2)
3 * --b < (3 + a)
(El paréntesis denota prioridad en la expresión por lo cual se deberá
resolver la operación 3 + a antes, sustituyendo la variable por su valor 2 el
resultado es 5)
3 * --b < 5
(Se resuelve la operación de mayor prioridad –b el cual decrementará en
memoria inmediatamente el valor de la variable b)
3*1<5
(Resolvemos la operación aritmética y luego la operación relacional
dando como resultado un valor verdadero)
d = 0 || 1
(El resultado final de la expresión será un valor verdadero)
d = 1 (por último se realiza la asignación a la variable d)

3.5. Sentencias

Las sentencias son unidades completas, ejecutables en si mismas. Existen


muchos tipos de sentencias que incorporan expresiones aritméticas, lógicas o
generales como componentes de dichas sentencias.
Las sentencias simples se separan por punto y coma y las compuestas se
agrupan en bloques mediante llaves.

3.5.1. Sentencias Simple

Una sentencia simple es una expresión de algún tipo terminada por un carácter
(;). Por ejemplo las declaraciones o las sentencias aritméticas.
flota real; // declaración de variable
area = base * altura; // expresión aritmética

3.5.2. Sentencia vacía o nula

En algunas ocasiones es necesario introducir en el programa una sentencia que


ocupe un lugar, pero que no realice ninguna tarea. A esta sentencia se le denomina
sentencia vacia y consta de un simple carácter (;). Por ejemplo:

3.5.3. Sentencia compuesta

Es un conjunto de declaraciones y de sentencias agrupadas dentro de llaves


{…}. También conocido como bloques. Una sentencia compuesta puede incluir otras
sentencias, simples y compuestas. Ejemplo:

{ int i = 1, j = 2;
double peso;
peso = 5.5;
j = i + j;
}

3.6. Operaciones de entrada y salida estándar.

Cuando nos referimos a entrada/salida estándar (E/S estándar) queremos decir


que los datos o bien se están leyendo del teclado, ó bien se están escribiendo en el
monitor de video. Como se utilizan muy frecuentemente se consideran como los
dispositivos de E/S por defecto y no necesitan ser nombrados en las instrucciones de
E/S.
Las operaciones de entrada y salida no forman parte del conjunto de sentencias
de C++, sino que pertenecen al conjunto de funciones y clases de la biblioteca
estándar de C++. Ellas se incluyen en los archivos de cabecera mediante la directiva
#include por lo que siempre que queramos utilizarlas deberemos introducir la línea
de código:

#include <nombrelibreria>

En el lenguaje C++ tenemos varias alternativas para ingresar y/o mostrar datos,
dependiendo de la librería que vamos a utilizar para desarrollar el programa, entre
estas están: iostream.h, stdio.h, conio.h.
<STDIO.H>
Esta librería incorpora las sentencias de entrada y salida básicas entre ellas
tenemos la sentencia scanf y la sentencia printf, denominadas sentencias de entrada y
salida con formato, ya que se les indica el tipo de conversión de tipo a realizar.
< IOSTREAM.H>
Esta biblioteca es una implementación orientada a objetos y está basada en el
concepto de flujos. A nivel abstracto un flujo es un medio de describir la secuencia de
datos de una fuente a un destino o sumidero. Así, por ejemplo, cuando se introducen
caracteres desde el teclado, se puede pensar en caracteres que fluyen o se trasladan
desde el teclado a las estructuras de datos del programa.
Entre los objetos de flujo que vienen predefinidos tenemos:
cin, que toma caracteres de la entrada estándar (teclado);
cout, pone caracteres en la salida estándar (pantalla);
cerr y clog, ponen mensajes de error en la salida estándar.
Estos objetos se utilizan mediante los operadores << y >>.
<CONIO.H>
Esta librería incorpora las sentencias de entrada y salida simples de carácter o
cadenas como lo son: getch, getche, getchar, gets, putchar, puts.

3.6.1. Sentencias de Salida Estándar.

El dispositivo de salida estándar como podemos suponer es el monitor o


pantalla de nuestro ordenador. Las sentencias de salida envían datos al puerto de
video para ser visualizados. Entre estas sentencias tenemos:

3.6.1.1. Sentencia de Salida cout

La salida estándar en C++ es la pantalla de nuestro ordenador. El objeto


asociado con dicha salida estándar es cout.
El objeto cout emplea al operador de inserción "<<" y apunta al objeto donde
tiene que enviar la información. Por lo tanto la sintaxis de cout será:

cout<<variable1<<variable2<<...<<variablen;

Las cadenas de texto son variables y se ponen entre " " (comillas dobles).

cout << "Hola";


cout << 489;
cout << 13.69;
cout << x;
cout << "El valor de pi es = " << 3.1416;

Si queremos incluir saltos de línea podemos emplear el código de escape '\n' o


bien el manipulador "endl".
cout << "Una linea.\n ";
cout << "segunda linea.\n";
cout << "tercera linea." << endl;
cout << "cuarta linea." << endl;

3.6.1.2. Sentencia de Salida printf

La rutina printf permite la aparición de valores numéricos, caracteres y cadenas


de texto por pantalla. El prototipo de la sentencia printf es el siguiente:

printf (control, arg1,arg2...);

En la cadena de control indicamos la forma en que se mostraran los argumentos


posteriores, también podemos introducir una cadena de texto (sin necesidad de
argumentos) o combinar ambas posibilidades, así como secuencia de escape. En el
caso de que utilicemos argumentos deberemos indicar en la cadena de control tanto
modificadores como argumentos se vayan indicar. El modificador esta compuesto por
el carácter % seguido por un carácter de conversión indica de que tipo de dato se
trata.
Ejemplo:

printf("Color %s, num1 %d, num2 %5d, real %5.2f.\n", "rojo", 12, 8, 3.4);

Imprimirá la siguiente línea en la pantalla (incluyendo el carácter de nueva línea


\n):
Color rojo, num1 123, num2 00089, real 3.14.

3.6.1.3. Tabla de caracteres de formato salida

Las letras de control de formato más usadas son (Figura 3.10):

Carácter Descripción
Esto imprime un número como un carácter ASCII. Por lo que, `printf
‘c’ "%c", 65' imprimiría la letra ‘A’. La salida para un valor cadena es el
primer carácter de la cadena.
‘d’ Esto imprime un entero decimal.
‘i’ Esto también imprime un entero decimal.
Esto imprime un número en notación científica (exponencial). Por
ejemplo, printf "%4.3e", 1950 imprime ‘1.950e+03’, con un total de 4
‘e’
cifras significativas de las cuales 3 siguen al punto decimal. Los
modificadores ‘4.3’ son descritos más abajo.
‘f’ Esto imprime un número en notación punto flotante.
Esto imprime en notación científica o en notación punto flotante, la que
‘g’
quiera que sea más corta.
‘o’ Esto imprime un entero octal sin signo.
‘s’ Esto imprime una cadena.
‘x’ Esto imprime un entero hexadecimal sin signo.
Esto imprime un entero hexadecimal sin signo. Sin embargo, para los
‘X’ valores entre 10 y 15, utiliza las letras desde la ‘A’ a la ‘F’ en lugar de
esas mismas letras pero en minúsculas.
Esta no es realmente una letra de control de formato. Pero tiene un
significado especial cuando se usa después de un ‘%’: la secuencia ‘%%’
‘%’
imprime el carácter ‘%’. No consume ni necesita ningún item o argumento
correspondiente.
Figura 3. 10. Tabla de caracteres de formato salida

El formato completo de los modificadores es el siguiente:

% [signo][longitud][.][precisión] carácter de conversión.

Signo: indicamos si el valor se ajustara a la izquierda, en cuyo caso


utilizaremos el signo menos, o a la derecha (por defecto).
Longitud: Especifica la longitud máxima del valor que aparece en por pantalla.
Si la longitud máxima del valor que aparece por pantalla. Si la longitud es menos que
el número de dígitos del valor, este aparecerán ajustado a la izquierda.
Precisión: Indicamos el numero máximo de decimales que tendrá el valor.

La sentencia printf puede incluir caracteres especiales o secuencias de escape


que permitan modificar la impresión de los datos.
Caracteres especiales más utilizados:
\n // salto de línea
\t // tabulador horizontal
\v // tabulador vertical
\b // retroceso
\r // retorno de carro
\f // salto de página
\a // alerta (campana)
\\ // carácter \
\? // interrogante
\' // comilla simple
\" // comilla doble
\ooo // carácter ASCII dado por tres dígitos octales (ooo serán dígitos)
\xhh // carácter ASCII dado por dos dígitos hexadecimales (hh serán dígitos)
\0 // carácter nulo
Los argumentos de la sentencia de entrada pueden estar representados por
operaciones o sentencias. La sentencia printf resulte estas operaciones antes de
ejecutar la etapa de control. Se resolverán los argumentos de derecha a izquierda unas
vez finalizados se resuelve la etapa de control de izquierda a derecha.
Ejemplo:

int edad = 5;
printf("edad2: %d, edad1: %d, edad inicial: %d", edad, ++edad, edad+
+);

Muestra por salida:

edad2: 7, edad1: 7, edad inicial: 5

3.6.1.4. Sentencia de Salida puts

Esta sentencia muestra por la salida estándar una cadena de caracteres o


literales y salta a la próxima línea, su sintaxis:

puts(direccion_cadena_caracteres);
ó
puts(“literales”);

Ejemplo:

char cadena[25]=”Casa”;
// se declara una cadena de caracteres {‘C’,’a’,’s’,’a’,’\0’}
puts(cadena);
// Muestra los caracteres de cadena hasta encontrar el fin de cadena
puts(100);
// Muestra los caracteres desde la posición de memoria 100 hasta
// encontrar el carácter de fin de cadena
puts(“cadena”);
// Muestra la palabra: cadena por pantalla

3.6.1.5. Sentencia de Salida putchar

Esta sentencia muestra por la salida estándar un carácter, su sintaxis:

putchar (variable_caracter);
ó
putchar (valor_caracter);
ó
putchar (‘simbolo_caracter’);

Ejemplo:

char car = ‘65’;


putchar (car); // Muestra por pantalla el carácter ‘A’
putchar (65); // Muestra por pantalla el carácter ‘A’
putchar (‘A’); // Muestra por pantalla el carácter ‘A’
3.6.2. Sentencias de Entradas Estandar

El dispositivo de entrada estándar como podemos suponer es el teclado de


nuestro ordenador. Las sentencias de entrada escanean este puerto para atrapar datos
y ser utilizados. Entre estas sentencias tenemos:

3.6.2.1. Sentencia de Entrada cin

Para extraer datos desde el teclado empleamos el objeto "cin", que se utiliza en
conjunción con el operador de extracción representado como ">>", lee información
del flujo cin (a la izquierda del operador) y las almacena en las variables indicadas a
la derecha). La sintaxis sería la siguiente:

cin>>variable1>>...>>variablen;

Un ejemplo de código utilizando ambos objetos podría ser el siguiente:

#include <iostream.h>
void main ()
{
int i;
cout<<"Introduce un número";
cin>>i; // atrapa un valor entero ingresado por teclado
}

Muestra por pantalla la frase "Introduce un número" y posteriormente


almacenaría el valor introducido por teclado en la variable i.

3.6.2.2. Sentencia de Entrada scanf


La rutina scanf permite entrar datos en la memoria del ordenador a través del
teclado. El prototipo de la sentencia scanf es el siguiente:

scanf(control, arg1, arg2, ...);

En la cadena de control indicaremos por regla general, los modificadores que


harán referencia al tipo de dato de los argumentos. Al igual que en la sentencia printf
los modificadores, estarán formado por el carácter % seguido de un carácter de
conversión. Los argumentos indicados serán, nuevamente las variables. La principal
característica de la sentencia scanf es que necesita saber la posición de la memoria del
ordenador en que se encuentra la variable para poder almacenar la información
obtenida, para indicarle está posición utilizamos el símbolo ampersand (&), aunque
colocaremos delante del nombre de cada variable (esto no es necesario en las cadenas
de caracteres o punteros).
Ejemplo:

include<stdio.h>
void main( ) /*solicita dos datos*/
{
char nombre[10];
int edad;
printf ("introduce tu edad:");
scanf("%d", &edad);
printf ("introduce tu nombre:");
scanf("%s", nombre); // nombre no lleva & por ser una referencia
}

3.6.2.3. Sentencia de Entrada gets


Esta sentencia lee y guarda una cadena introducida por la entrada estándar, su
sintaxis:

char cadena[25];

puts(“Ingrese un nombre:”);
gets(cadena);
// Envía los caracteres a partir de la posición de memoria de la cadena
// y transforma el carácter intro (enter) en un carácter fin de cadena
puts(cadena);
// Muestra lo que se ingreso por la entrada en pantalla

3.6.2.4. Sentencia de Entrada getchar

Esta sentencia lee y retorna cuando se presione la tecla intro, un único


carácter introducido mediante el teclado y salta a la próxima línea. Muestra el
carácter por la pantalla. Su sintaxis:

char letra;
letra=getchar( );
// atrapa un carácter y lo retorna después del carácter intro.

3.6.2.5. Sentencia de Entrada getch

Lee y retorna un único carácter al presionar una tecla. No muestra el


carácter por pantalla. Su sintaxis:
char letra;
letra=getch( );
// atrapa un carácter al momento de presionarlo y no lo muestra

3.6.2.6. Sentencia de Entrada getche

Esta sentencia lee y retorna un único carácter introducido mediante el


teclado por el usuario al igual que la sentencia getch pero a diferencia esta si
muestra el carácter por pantalla. Su sintaxis:

char letra;
letra=getche( );
// atrapa un carácter al momento de presionarlo y lo muestra

CAPITULO 4

ESTRUCTURAS DE CONTROL
4.1. Concepto.

Para existir un control de un proceso o situación, se deben dar condiciones


necesarias para ello, es decir que las condiciones son el punto de partida principal de
una sentencia de control.
Podemos definir las estructuras de control como sentencias condicionales
compuesta, que al cumplirse la condición necesaria, se realicen las sentencias simples
contenidas en esta.

4.2. Tipos de estructuras de control

El C++, como todo lenguaje de programación basado en la algorítmica, posee


una serie de estructuras de control para gobernar el flujo de los programas.
Debemos recordar que la evaluación de una condición producirá como
resultado un cero si es falsa y un número cualquiera distinto de cero si es cierta, este
hecho es importante a la hora de leer los programas, ya que una operación
matemática, por ejemplo, es una condición válida en una estructura de control.
Las estructuras de control básicas las podemos dividir en: sentencias de control
selectivas y las sentencias de control iterativo o ciclos.

4.2.1. Estructuras de control selectivas.

Las sentencias de control selectivas son estructuras de control que funciona de


esta manera: Las instrucciones comienzan a ejecutarse de forma secuencial (en orden)
y cuando se llega a una estructura condicional, la cual está asociada a una condición,
se decide que camino tomar dependiendo siempre del resultado de la condición
siendo esta falsa o verdadera. Cuando se termina de ejecutar este bloque de
instrucciones se reanuda la ejecución en la instrucción siguiente a la de la
condicional.
Dentro de las estructuras de selección encontramos en el C++, las de
condición simple (sentencia if), bicondicionales (sentencia if-else) y las de condición
múltiple (switch).

4.2.1.1. La sentencia if o condicional simple

Es una sentencia condicional simple cuya traducción es: si la expresión a


evaluar es verdadera realiza las instrucciones que controle dicha sentencia. Si el
resultado de la expresión es falso el flujo del programa continuará con las sentencias
debajo del final del if. Su sintaxis es:

if (expresión)
{
//sentencias
}

Si solo controla una sentencia no es necesario usar los operadores de bloque.

if (expresión)
sentencia_unica;

Podemos representar esta sentencia con el siguiente diagrama (Figura 4.1),


Fiugra 4. 1. Diagrama de flujo sentencia if.

donde se muestra que solo se realizara la sentencia 1 si el resultado lógico de la


expresión es verdadera de no serlo continúa el flujo del programa.
Ejemplo:

int x=6;
if(x>5)
k++;

Como se ve x tiene un valor y el if evalúa su estatus, como el resultado es


verdadero k incrementara su valor una sola vez y el flujo de programa continuará.

Ejemplo 1: Determine si un número es igual mayor o menor que cero.

#include <iostream.h>
void main()
{ int numero;
cout<<"Ingrese un numero:";
cin>>numero;
if(numero == 0) //La condición indica que tiene que ser igual a Cero
{
cout<<"El Numero Ingresado es Igual a Cero";
}
if(numero > 0) // la condición indica que tiene que ser mayor a Cero
{
cout<<"El Numero Ingresado es Mayor a Cero";
}
if(numero < 0) // la condicion indica que tiene que ser menor a Cero
{
cout<<"El Numero Ingresado es Menor a Cero";
}
}

Ejemplo 2: Determine si un número entero es par o impar.

#include <iostream.h>
void main()
{ int num;
cout<<"Ingrese un numero:";
cin>>num;
//Un numero será par sí es divisible por 2
if(num %2==0) //La condición indica que se cumpla si es divisible
cout<<"El numero ingresado es par";
//Un numero será impar sí no es divisible por 2
if(num %2!=0) //La condición indica que se cumpla si no es divisible
cout<<"El numero ingresado es impar";
}

Ejemplo 3: Convertir un carácter atrapado en un número entre [0,9].


#include <iostream.h>
#include <stdio.h>
#include <conio.h>
void main()
{ int num=-1;
char car;
cout<<"Ingrese un carácter:";
car=getche(); //atrapa un símbolo
//La condición se cumple se asignara el número correspondiente
if(car==’0’) num=0;
if(car==’1’) num=1;
if(car==’2’) num=2;
if(car==’3’) num=3;
if(car==’4’) num=4;
if(car==’5’) num=5;
if(car==’6’) num=6;
if(car==’7’) num=7;
if(car==’8’) num=8;
if(car==’9’) num=9;

if(num!=-1)
cout<<"El Numero es ="<<num;
if(num==-1)
cout<<"Carácter no es un símbolo numérico";
}

4.2.1.2. La sentencia if-else o bicondicional


Esta estructura cumple las mismas características de la sentencia if, cuando la
condición es verdadera realizara una sentencia o conjunto de sentencias verdaderas,
pero si esta es falsa realizara una sentencia o conjunto de sentencias falsas.

if (expresión)
{
//sentencias condición verdadera
}
else
{
// sentencias condición falsa
}

Si solo controla una sentencia no es necesario usar los operadores de bloque.

if(expresión)
sentencia_unica1;
else
sentencia_unica2;

Podemos representar esta sentencia con el siguiente diagrama (Figura 4.2),

Fiugra 4. 2. Diagrama de flujo sentencia if-else.


donde se muestra que solo se realizara la sentencia 1 si el resultado lógico de la
expresión es verdadera, pero si esta no se cumple realiza la sentencia 2, una vez
realice alguna de las sentencias dado el resultado de la expresión continúa el flujo del
progama.
De esta manera podemos mostrar el siguiente ejemplo:

x=0;
if( x ==5)
{ k++;
}
else
{ k--;
}

Como la evaluación de x es falsa, k decrementará en uno su valor y continuará


con el flujo del programa.

Ejemplo 1: Determine si un numero entero es positivo o negativo.

#include <iostream.h>
void main()
{ int num;
cout<<"Ingrese un numero:";
cin>>num;
if(num>= 0) //La condición indica que tiene mayor o igual a cero
{
cout<<"El numero ingresado es positivo";
}
else // de no cumplirse, entonces debe ser menor que cero
{
cout<<"El numero ingresado es negativo";
}
}

Ejemplo 2: Determine si un número entero es par o impar.

#include <iostream.h>
void main()
{ int num;
cout<<"Ingrese un numero:";
cin>>num;
if(num %2==0) //La condición indica que se cumpla si es divisible
cout<<"El numero ingresado es par";
else //si la condición no se cumple entonces no es divisible
cout<<"El numero ingresado es impar";
}

Las sentencias pueden controlar otras sentencias de control secuencial


permitiendo evaluar los procesos solo cuando sean necesarios conocidas como
sentencias secuenciales anidadas. Por ejemplo si queremos determinar si un número
es igual mayor o menor que cero, usando sentencias anidadas if-else resulta:

#include <iostream.h>
void main()
{
int numero;
cout<<"Ingrese un numero:";
cin>>numero;
if(numero == 0)
{ cout<<"El Numero Ingresado es Igual a Cero";
}
else if(numero > 0)
{ cout<<"El Numero Ingresado es Mayor a Cero";
}
else {
cout<<"El Numero Ingresado es Menor a Cero";
}
}

4.2.1.3. La sentencia switch.

Sirve para seleccionar una de entre múltiples alternativas de acuerdo al valor de


una expresión. La sentencia switch es específicamente útil cuando la selección se
basa en el valor de una variable simple o de una expresión simple denominada
expresión de control múltiple o selector.
Su sintaxis:

switch (expresión)
{
case valor_expresion_1:
//sentencias 1
break;
case valor_expresion_2:
//sentencias 2
break;
....
case valor_expresion_n:
//sentencias n
break;
default:
//sentencias x
}

Podemos representar esta sentencia mediante el diagrama siguiente (Figura


4.3),

Fiugra 4. 3. Diagrama de flujo sentencia switch.


donde dado el valor de la expresión realizara las operaciones y continuara con
el flujo del programa.
Esta sentencia la podemos representar usando sentencias anidadas if…else, si la
sentencia break no fuese utilizada entonces se representaría por un conjunto de
sentencias if una tras otra.
Como verán dependiendo de valor se selecciona el caso por ejemplo:

x=3;
switch (x)
{
case 1: k= 20*2;
break;
case 2: k=20/2;
break;
case 3: k=20+2;
//no es necesario usar break
}

Como x es igual a 3 el case efectuará la operación k=20+2, el uso de default es


opcional.

Ejemplo: Escribir un programa que lea por teclado las notas desde la A-H, y
muestre por pantalla el rendimiento académico del alumno.

#include <conio.h>
#include <iostream.h>
main()
{ char letra;
cout<<"Ingrese la Calificación y presione Enter: ";
cin>>letra;
switch (letra)
{
case 'A': cout<<"Excelente";
break;
case 'B': cout<<"Notable";
break;
case 'C': cout<<"Aprobado";
break;
case 'D':
case 'E':
case 'F': cout<<"Desaprobado";
break;
default: cout<<"No esposible esta nota";
}
}

La sentencia break es opcional. Cuando se encuentra, provoca la salida de


switch. En caso contrario continua la siguiente secuencia case o default aunque no se
cumpla la condición. Para aclarar esto, tomemos el siguiente ejemplo:

int c;
...
scanf ("%d", &c);
switch (c) { case 1:
case 2: Funcion2 ();
case 3: Funcion3 ();
break;
case 4: Funcion4_1 ();
Funcion4_2 ();
break;
case 5: Funcion_5 ();
default: FuncionX ();
}

La siguiente tabla indica qué función se ejecuta dependiendo del valor de c


(Figura 4.4).
Si se pulsa Se ejecuta las funciones
1 Funcion2() y Funcion3()
2 Funcion2() y Funcion3()
3 Funcion3()
4 Funcion4_1() y Funcion4_2()
5 Funcion5() y FuncionX()
Otra cosa FuncionX()
Fiugra 4. 4. Tabla de ejecución de funciones.

4.2.2. Estructuras de control iterativas.

Las Sentencias de Iteración o Ciclos son estructuras de control que repiten la


ejecución de un grupo de instrucciones. Básicamente, una sentencia de iteración es
una estructura de control condicional, ya que dentro de la misma se repite la
ejecución de una o más instrucciones mientras o hasta que una a condición
específica se cumpla. Muchas veces tenemos que repetir un número definido o
indefinido de veces un grupo de instrucciones por lo que en estos casos utilizamos
este tipo de sentencias. En C++ los ciclos o bucles se construyen por medio de las
sentencias for, while y do - while. La sentencia for es útil para los casos en donde se
conoce de antemano el número de veces que una o más sentencias han de repetirse.
Por otro lado, la sentencia while es útil en aquellos casos en donde no se conoce de
antemano el número de veces que una o más sentencias se tienen que repetir.

4.2.2.1. Sentencia for

Es un bucle o sentencia repetitiva que, permite realizar un proceso de manera


cíclica, desde un punto partida inicial, hasta un punto final, atreves de un incremento
o contador de actividad. La sentencia for:
1. Ejecuta la sentencia de inicializaciones.
2. Verifica la expresión booleana de condición de término:
a. si es cierta, ejecuta la sentencia entre llaves y la sentencia de iteración para
volver a verificar la expresión booleana de condición de término.
b. si es falsa, sale del bucle.
Podemos representar el flujo de la sentencia en el siguiente diagrama (Figura
4.5):

Fiugra 4. 5. Diagrama de flujo sentencia for.


Sintaxis:

for (inicio; condición; iteración)


sentencia;

o si se desean repetir varias sentencias:

for (inicio; condición; iteración)


{
sentencia_1;
sentencia_2;
sentencia_n;
}

Vemos que la sentencia for tiene tres secciones: inicio, en dónde se da un valor
inicial a una variable o variables de control del bucle; condición, que es una
expresión que devuelve un valor verdadero o falso, y hace que el bucle se repita
mientras sea cierta y salga de este solo cuando la condición sea falsa; e iteración, en
dónde se determina el cantidad del incremento o decremento de la variable o
variables de control. Las tres secciones están separadas por punto y coma. El cuerpo
del bucle puede estar formado por una o por varias sentencias. En este último caso
deben encerrarse entre llaves {}. Las llaves sólo son necesarias si se quieren repetir
varias sentencias, aunque se recomienda su uso porque facilita la lectura del código
fuente y ayuda a evitar errores al modificarlo.
Habitualmente, en la expresión lógica de condición de término se verifica que
la variable de control alcance un determinado valor. Por ejemplo:

for (i = valor_inicial; i <= valor_final; i++)


{ sentencia;
}

Algunos ejemplos de la sentencia for:


En la siguiente secuencia se muestran en pantalla los números del 1 al 10 y sus
cuadrados.

int i;
for (i = 1; i <= 10; i++)
{
printf ("\nValor de i: %d", i);
printf ("|tValor de i2: %d|n", i * i);
}

Esta secuencia, se muestran en pantalla las letras mayúsculas de la A a la Z.

char letra;
for (letra = 'A'; letra <= 'Z'; letra++)
cout<< letra<<endl;

El valor de incremento/decremento de las variables de control puede ser


diferente de 1. El siguiente ejemplo muestra en pantalla los números pares
comprendidos entre 1 y 100, descendentemente:

int i;
for (i = 100; i >= 1; i = i - 2)
cout << i <<"\t ";

Es posible tener más de una variable de control del bucle. En el bucle for las
secciones de inicialización e incremento pueden tener, a su vez, subsecciones, en
cuyo caso van separadas por el operador secuencial (,). Un ejemplo es:

int i, j;
for (i = 0, j = 1; i + j < N; i++, j++)
cout<< i + j<<endl;

que visualiza los N primeros números impares.


No debe confundirse esta sentencia con un anidamiento de bucles for. Un
anidamiento tiene el siguiente aspecto:

int i, j;
for (i = 0; i <= 100; i++)
{ //cuerpo_del_bucle_externo;
for (j = 0; j <= 100; j++)
{ //cuerpo_del_bucle_interno;
}
}
La condición de salida del bucle no tiene por qué referirse a la variable de
control. Esto queda ilustrado en el siguiente ejemplo:

char a;
int i;
for (i = 1; a != 's'; i++)
{ printf ("\n%d", i);
a = getch ();
}

En este ejemplo se van mostrando en pantalla los números 1, 2,...mientras


presionemos cualquier carácter hasta que se teclee el carácter ‘s’.
El bucle for puede no tener cuerpo. Esta característica permite crear retardos en
un programa.

int i;
for (i = 0; i < 100; i++);

El bucle provoca un retardo de 100 ciclos, saldrá del bucle cuando la variable
sea mayor o igual a100.
El bucle for puede tener vacía cualquier sección. En un bucle for puede faltar
una, dos o las tres secciones. Por ejemplo, es correcto escribir

register int i;
for (i = 0; i != 10; ) /* Falta la 3ª sección (incremento) */
{
scanf ("%d", &i);
printf ("\n%d", i);
}
que va mostrando en pantalla los valores que se ingresen, finalizando al
ingresar el número 10 (que también se visualiza).
También podemos escribir un bucle como:

for ( ; ; )
{
cuerpo_del_bucle;
}

que es un bucle sin fin, debe haber en el cuerpo del bucle una sentencia que
rompa el ciclo del programa como lo es una sentencia break. Ejemplo:

void main ()
{ int n;
for ( ; ; )
{ printf ("\nTeclee un número: ");
scanf ("%d", &n);
if (!n) break;
printf ("\nEl cuadrado es %d", n * n);
}
}

4.2.2.2. Sentencia while

La sentencia while tiene una condición del bucle (una expresión lógica) que
controla la secuencia de repetición. La sentencia evalúa la condición antes de que se
ejecute el cuerpo del bucle si se cumple la condición, realiza los procesos que
controla la sentencia, sino sale de esta y continúa con el flujo del programa. Como
podemos observar en la figura 4.6:
Fiugra 4. 6. Diagrama de flujo sentencia while.
El cuerpo del bucle no se ejecutará nunca si la primera vez no se cumple la
condición. El bucle puede ser infinito si no se modifican adecuadamente las variables
de la condición dentro del bucle.
Sintaxis:

while ( condición )
{
sentencias .......... ;
}

Veamos algunos ejemplos.


En esta sentencia se solicita un carácter del teclado mientras no se teclee el
carácter ‘n’ ni el carácter ‘s’. Cuando se teclea alguno de estos caracteres, se
almacena en c y se abandona el bucle.

char c;
while (c != 's' && c != 'n')
c = getche ();

El siguiente ejemplo es un caso de bucle while sin cuerpo. Se mantendrá en el


ciclo hasta que se teclea el carácter ‘s’.
while (getch () !=’s’);

El siguiente programa utiliza un bucle while para solicitar del usuario que
adivine un número. Realizar un programa que muestre los número del 1 al 100.

#include <conio.h>
#include <iostream.h>
main()
{ int contador=0;
while ( contador < 100 )
{ contador++;
cout<<"\t "<<contador;
}
cout<<"Presione Enter para salir";
}

4.2.2.3. Equivalencias ente las sentencias for y while.

Las sentencias for y while son equivalentes entre si ya que se pueden emular
entre sí, aunque podríamos usar ambas sin problemas, se recomienda el uso de
sentencias for para recorridos y la sentencia while cuando dependa del resultado de
un proceso:
Emulando un while mediante una sentencia for:

for ( ; condición ; ) // while carece de inicio y de iteraciones


{
sentencias;
}

Emulando un for mediante una sentencia while:

inicio; // la sentencia for posee valores de inicio


while ( condición )
{
sentencias;
iteraciones; // la sentencia for posee valores de iteraciones
}

Ejemplo 1: Determine si un numero es capicúo (se lee igual en ambos sentidos).

Usando la sentencia while Usando la sentencia for


#include <conio.h> #include <conio.h>
#include <iostream.h> #include <iostream.h>
void main() void main()
{ int a, inv, aux; { int a, inv, aux;
cout<<”Ingrese un numero:”; cout<<”Ingrese un numero:”;
cin>>a; cin>>a;
aux = a; for(aux=a, inv=0; aux>0; aux/=10)
inv = 0; { // separar dígitos e invertir
while ( aux > 0 ) inv = inv * 10 + aux % 10;
{ // separar dígitos e invertir }
inv = inv * 10 + aux % 10; if(a == inv)
aux = aux / 10; cout<<"El numero es capicúo”;
} else
if(a == inv) cout<<"El numero no es capicúo”;
cout<<"El numero es capicúo”; }
else
cout<<"El numero no es capicúo”;
}

Ejemplo 2: Muestre un cuadro usando el carácter ’x’ y una dimensión n. Sera


necesario usar sentencia anidadas para desplazar el cursor de manera horizontal y
vertical.

Usando la sentencia while Usando la sentencia for


#include <conio.h> #include <conio.h>
#include <iostream.h> #include <iostream.h>
void main() void main()
{ int i, j, n; { int i, j, n;
cout<<”Ingrese la dimensión:”; cout<<”Ingrese la dimensión:”;
cin>>num; cin>>num;
i = 0; for(i = 0; i < n ; i++)
while(i < n ) {
{ j=0; for(j = 0; j < n; j++)
while( j < n) if(i==0 || i==n-1|| j==0|| j==n-1)
{ cout<<"x”;
if(i==0||i==n-1||j==0||j==n-1) else cout<<” ”;
cout<<"x”;
else cout<<” ”; cout<<endl;
j++; }
} }
cout<<endl;
i++;
}
}

Ejemplo 2: Muestre una X usando el carácter ’x’ y una dimensión n.

Usando la sentencia while Usando la sentencia for


#include <conio.h> #include <conio.h>
#include <iostream.h> #include <iostream.h>
void main() void main()
{ int i, j, n; { int i, j, n;
cout<<”Ingrese la dimensión:”; cout<<”Ingrese la dimensión:”;
cin>>num; cin>>num;
i = 0; for(i = 0; i < n ; i++)
while(i < n ) {
{ j=0; for(j = 0; j < n; j++)
while( j < n) if(i==j || i+j==n-1)
{ cout<<"x”;
if(i==j || i+j==n-1) else cout<<” ”;
cout<<"x”;
else cout<<” ”; cout<<endl;
j++; }
} }
cout<<endl;
i++;
}
}
4.2.2.4. Sentencia do-while

La sentencia do - while se utiliza para especificar un bucle condicional que se


ejecuta al menos una vez. Esta situación se suele dar en algunas circunstancias en las
que se ha de tener la seguridad de que una determina acción se ejecutara una o varias
veces, pero al menos un vez. El flujo de ejecución podemos obsérvala en la siguiente
figura 4.7:

Fiugra 4. 7. Diagrama de flujo sentencia do-while.

En esta sentencia a diferencia de la sentencia for y while, primero realizara las


sentencias que controle una vez realizada evaluara la condición, si esta es verdadera,
volverá a realizar las sentencias hasta que la condición en algún momento sea falsa.
Las sentencias se ejecuta al menos una vez, incluso aunque la expresión se
evalúe como falsa, puesto que la evaluación se hace al final, a diferencia de la
sentencia while y for, en el que la evaluación de la condición se hace al principio.

Sintaxis:

do{
sentencias;
}while(condición);
Esta sentencia por su conformación la hacen preferibles en problemas que se
espere un valor antes de poder ejecutar procesos, como por ejemplo: validaciones,
menús de espera, ingreso de datos con clave única, etc.
Ejemplo: Realizar un programa que convalide el ingreso de dos notas con rango
[0 – 20] y halle el promedio de ambas notas.

#include <conio.h>
#include <iostream.h>
void main()
{ int nota1, nota2;
do{ //termina del ciclo solo si el valor nota1 está en el rango
cout << "Ingrese la primera nota: ";
cin>>nota1;
if(nota1<0 || nota1>20)
cout << "Nota fuera de los rangos[0-20]"<<endl;
}while(nota1<0 || nota1>20));
do{ //termina del ciclo solo si el valor nota2 está en el rango
cout << "Ingrese la segunda nota: ";
cin>>nota2;
if(nota2<0 || nota2>20)
cout << "Nota fuera de los rangos[0-20]"<<endl;
}while(nota2<0 || nota2>20));

cout<<"El promedio de las nontas es:"<<(nota1+nota2)/2;


}

En el siguiente ejemplo se solicita un carácter del teclado hasta que se pulse


cualquiera de los caracteres 'S' o 'N'.

#include <iostream.h>
void main ()
{
char tecla;
do {
cout<<”Pulse S o N: "<<endl;
tecla = getch ();
} while (tecla != 'S' && tecla != 'N');
}

4.2.2.5. La sentencia break

Es una sentencia de ruptura de secuencia, permite cortar ciclos de programas.


La sentencia break se puede colocar dentro de un bucle o bucles anidados. Cuando se
ejecuta la sentencia break se abandona el bucle más interno. A todos los efectos la
sentencia break actúa como un salto a la instrucción siguiente al bucle en el que se
ejecuta.

4.2.2.6. La sentencia continue

La sentencia continue, no abandona el bucle si no hace que se ejecute la


siguiente iteración. En el bucle while la ejecución del continue hace que el flujo del
programa salte a la condición. En el bucle for la ejecución del continue hace que la
expresión de incremento, para después continuar normalmente con la condición. Es
decir, la ejecución del continue evita que se ejecute el resto del cuerpo del bucle.
Esta sentencia se utiliza en los bucles for, while y do/while. Cuando se ejecuta
fuerza un nuevo ciclo del bucle, saltándose cualquier sentencia posterior. Por
ejemplo,

int i, n;
for (i = 1; i <= 100; i++)
{
n = i / 2;
if (i == 2 * n)
continue;
printf ("\n%d", i);
}
el bucle :muestra en pantalla sólo los números impares, puesto que para los
números pares la expresión i == 2 * n se evalúa como cierta, ejecutándose la
sentencia continue que fuerza de inmediato un nuevo ciclo del bucle.

CAPITULO 5

ARREGLOS

5.1. Definición

Los arreglos son usados extensamente por los programadores para contener
listas de datos en la memoria, por ejemplo, los datos almacenados en un disco suelen
leerse y ponerse dentro de un arreglo con el objetivo de facilitar la manipulación de
dichos datos, ya que los datos en memoria pueden ser modificados, clasificados,
marcados para su eliminación, etc. para luego ser reescritos al disco.
Un arreglo (array) es una colección de datos del mismo tipo, que se almacenan
en posiciones consecutivas de memoria y reciben un nombre común. Para diferenciar
cada elemento de un arreglo se utiliza un índice, que especifique su posición relativa
en el arreglo.
Los índices son números que se utilizan para identificar a cada uno de los
componentes de un arreglo. Por ejemplo podemos pensar en los casilleros, así que si
deseamos guardar o retirar un paquete nos dirigimos al casillero el cual sería el
arreglo; y dado el número específico el cual representa el índice para identificar el
lugar del casillero en donde quedó guardado el paquete.

Un arreglo es una colección finita, homogénea y ordenada de elementos.


 Finita: Todo arreglo tiene un límite; es decir, debe determinarse cuál será el
número máximo de elementos que podrán formar parte del arreglo.
 Homogénea: Todos los elementos del arreglo deben ser del mismo tipo.
 Ordenada: Se puede determinar cuál es el primer elemento, el segundo, el
tercero,.... y el n-ésimo elmento.

5.2. Declaración de arreglos

Para declarar un arreglo se emplea la sintaxis:

tipo identificador[ [tamaño] ] [ = { lista de inicialización } ] ;

donde,
 tipo se refiere al tipo de datos que contendrá el arreglo. El tipo puede ser
cualquiera de los tipos estándar (char, int, float, etc.) o un tipo definido por
el usuario. Es más, el tipo del arreglo puede ser de una estructura creada
con: struct, class.
 identificador se refiere al nombre que le daremos al arreglo.
 tamaño es opcional e indica el número de elementos que contendrá el
arreglo. Si un arreglo se declara sin tamaño, el mismo no podrá contener
elemento alguno a menos que en la declaración se emplee una lista de
inicialización.
 lista de inicialización es opcional y se usa para establecer valores para
cada uno de los componentes del arreglo. Si el arreglo es declarado con un
tamaño especifico el número de valores inicializados no podrá ser mayor a
dicho tamaño.

Ejemplos:

int int_A[5];
long long_A[5] = { 1, 2, 3, 4, 5 };
char char_A[] = { 'a', 'b', 'c' );
int bidim_A[4][4]={{1,1},{2,3}};

5.3. Clasificación de los arreglos

Los arreglos los podemos clasificar de acuerdo con el número de dimensiones


que tienen. Así se tienen los:
 Unidimensionales (vectores): representados por una colección de elementos
con un mismo nombre y diferenciados por un índice.
 Multidimensionales: Bidimensionales (tablas o matrices) y
Tridimensionales. Representados por colecciones de arreglos
unidimensionales (matrices) o múltiples matrices.

5.3.1. Arreglos unidimensionales.

Los arreglos unidimensionales pueden ser apreciados en la vida cotidiana como


por ejemplo un armario el cual está dividido por gavetas, una cola de personas que
realizan un mismo proceso como la compra de un mismo artículo, etc.
Podemos definir los arreglos unidimensionales como una colección de variables
del mismo tipo, con un mismo nombre y diferenciadas a través de un índice. También
podríamos representarlo como un conjunto compuesto por elementos internos con un
universo limitado. (Figura 5.1)
Figura 5. 1.Representación de un arreglo unidimensional.

El formato para declarar un arreglo unidimensional es:

tipo nomb_arr[tamaño] = { lista de inicialización } ; //La lista es opcional

donde, el tamaño representa la dimensión o cantidad de elementos o variables


del mismo tipo que componen al arreglo.
El nombre de arreglo representa la dirección de todos los elementos declarado
en el. Donde cada variable declarada en el arreglo está compuesta por una dirección y
contenido.
Por ejemplo, para declarar un arreglo de enteros llamado listanum con diez
elementos se hace de la siguiente forma:

int listanum[10];

El ejemplo declara un arreglo de enteros con diez elementos o variables enteras


desde listanum[0] hasta listanum[9].
Cada una de las variables declaradas puede ser utilizada referenciando su
índice. La forma como pueden ser accesados los elementos de un arreglo, es de la
siguiente forma:

listanum[2] = 15; /* Asigna 15 al 3er elemento del arreglo */


num = listanum[2];
// Asigna el contenido del 3er elemento a la variable num

Para evaluar, leer, modificar un arreglo de elementos es necesario realizar


recorridos a través de este, es decir si se quiere realizar alguna operación sobre el
arreglo es necesario utilizar sentencias iterativas que permitan recorrer el arreglo
desde una posición a otra.

Sea la declaración de los arreglos A y B:

int A[10], B[10];

Si se desea leer o asignar a los 10 elementos del arreglo, es necesario


desplazarnos por cada una de las variables desde la posición 0 hasta la posición 9.
Si se desea leer los 10 elementos del arreglo A.

int i;
for (i = 0; i < 10; i++)
{
cout<<”Ingrese el elemento A[”<<i<<”]:”;
cin>>A[i];
}

Para asignar a los 10 elementos del arreglo B el mismo valor del arreglo A.

int i;
for (i = 0; i < 10; i++)
{
B[i] = A[i];
}

Si se desea buscar un elemento en el arreglo A, es necesario comparar el valor


buscado con cada elemento del arreglo.
int i, encontrado;
for (encontrado=0, i = 0; encontrado==0 && i < 10; i++)
{
if ( buscado == A[i])
encontrado = 1;
}
if ( encontrado==1) cout<<”El elemento buscado fue encontrado”;
else cout<<”El elemento no fue localizado”;

Si se desea ordenar los elementos del arreglo A, es necesario comparar cada


valor del arreglo y ubicarlo en la posición correspondiente, en la siguiente sentencia
se utilizara el método de la burbuja el cual consiste en comparar un elemento con el
siguiente y dependiendo del criterio de comparación y valor de peso este será
cambiado o no.

int i, cambio, aux, n=10;


do{ // nos indica la cantidad de veces necesarias para ordenar
cambio = 0; //si su valor se mantiene en cero el arreglo estará
ordenado
for (i = 0; i < n-1; i++) // el último elemento no será comparado
if ( A[i] <criterio> A[i+1]) // criterio (>) ordena de menor a mayor
{ cambio = 1; // criterio (<) ordena de mayor a menor
aux = A[i]; // intercambia los valores A[i] con A[i+1]
A[i] = A[i+1];
A[i+1] = aux;
}
}
}while( cambio!=0);

El ejemplo anterior usando el método de ordenamiento por selección seria:

int i, j, aux, n=10;


for (i = 0; i < n-1; i++) // selecciona la posición del elemento a ordenar
for (j = i+1; i < n; j++) // busca si hay un elemento que cumpla criterio
if ( A[i] <criterio> A[j]) // criterio (>) ordena de menor a mayor
{ // intercambia los valores A[i] con A[j]
aux = A[i];
A[i] = A[j];
A[j] = aux;
}

Si desea mostrar los elementos del arreglo A:

int i;
cout<<”Los elementos del arreglo:”<<endl;
for ( i = 0; i < 10; i++)
{
cout<<A[i]<<”\t”;
}

Ejemplo: Programa para determinar el mayor y menor de n números enteros.

#include <conio.h>
#include <iostream.h>
#define max 100
void main()
{ int num[max];
int i, n, mayor, menor;
do { cout<<”Ingrese la cantidad de numeros a ingresar:”;
cin>>n;
}while( n<1 || n>100);
for(i = 0; i < n ; i++) // ingresa los n números
{ cout<<”Ingrese el numero[”<<i+1<<”]:”;
cin>>num[i];
}
for(i = 1, mayor= num[0], menor=num[0]; i < n; i++)
{ // compara los números y los guarda sea el resultado
if(mayor < num[i]) mayor = num[i];
if(menor > num[i]) menor = num[i];
}
cout<<"El mayor: ”<<mayor<<” y el menor: ”<<menor<<endl;
}
5.3.2. Arreglos multidimensionales.

Se podrán declarar arreglos de mayor dimensión, ya sean arreglos


bidimensionales y tridimensionales como sigue:

// Bidimensional
tipo_de_dato identificador[tamaño1][tamaño2];
// Tridimensional
tipo_de_dato identificador[tamaño1][tamaño2][tamaño3];

dónde:

 tipo_de_dato: Es el tipo de datos que contendrá la matriz. Hasta ahora sólo


conocemos los tipos básicos de datos; int, float, double, char. Posteriormente
veremos cómo definir nuestros propios tipos de datos.
 identificador: Es el nombre que le damos a la variable matriz y por el cual la
referenciaremos en un programa.
 [tamaño]: Indica la dimensión o el número de elementos de tipo
tipo_de_datos contendrá la matriz identificada. Si se definen 2 dimensiones
(arreglo bidimensional), la cantidad total será igual al producto de ambas
dimensiones o tamaños.

5.3.2.1. Arreglo bidimensional.

Un arreglo bidimensional es aquel en donde los componentes son accesados por


medio de una pareja de índices que apunten a la fila y columna del componente
requerido. Los arreglos de este tipo son conocidos también con el nombre de
matrices o tablas. Conceptualmente, podemos pensar en un arreglo bidimensional
como en una lista compuesta de filas y columnas, en donde para referirnos a una de
ellas emplearemos un número para indicar la posición de fila y otro número para
indicar la posición de la columna del componente deseado.(Figura 5.2)

Figura 5. 2. Representación de un arreglo bidimensional.

La primera dimensión establece la cantidad de filas. La segunda dimensión


establece la cantidad de columnas. Al igual que en los arreglos de una dimensión,
todos los datos serán del mismo tipo y nombre y diferenciada por dos índices.
También podemos decir que representa un conjunto compuesto por
subconjuntos interno, donde la primera dimensión representa la cantidad de estos
conjuntos los cuales tienen la misma cantidades de elementos todos del tipo declarado
representado por la segunda dimensión. Es decir lo representamos como un arreglo de
arreglos unidimensionales.

5.3.2.2. Arreglo Tridimensional

Los arreglos tridimensionales están compuestos por tres dimensiones donde la


primera dimensión representa la cantidad de arreglos bidimensionales, la segunda
dimensión representa la cantidad de arreglos unidimensionales y la tercera dimensión
representa la cantidad de elementos que contiene cada arreglo unidimensional.
Permite realizar representaciones en tres planos. (Figura 5.3)
Figura 5. 3. Representación de un arreglo tridimensional.

5.3.2.3. Declaración de los arreglos bidimensionales y tridimensionales

Para declarar un arreglo bidimensional, matriz o tabla:

tipo_de_dato nomb_matriz[fila][columnas];

Para declarar un arreglo tridimensional:

tipo_de_dato nomb_arreglo[fila][columnas][elementos];

Algunas declaraciones de matrices y arreglos tridimensionales.

Matriz de números reales de 10x10:

float matriz[10][10];

Matriz tridimensional de números enteros 20x20x10:


int Tridimensional[20][20][10];

Como ya se supondrá el acceso a cada elemento de la matriz se realiza


especificando su posición, pero ésta comienza a contarse desde el valor 0, es decir,
la matriz que hemos declarado (matriz) tendrá elementos desde el [0][0] al [9][9].
Esto puede causar algunos mal entendidos cuando se trabaja con matrices
estáticas. Por ejemplo:

a = matriz [2][1];

Al tomar el valor del elemento (2,1) comenzando a contar desde 0, es decir


que el elemento se ubica en la tercera fila (fila 2) de la segunda columna (columna 1).

tridimensional [5][16][1] = 67;

Introduce el valor 67 en la entrada de la matriz especificada

Las variables de tipo matriz como el resto de las declaraciones, se


pueden inicializar en el momento de su declaración, ayudándose de las llaves
({}) para la inclusión de inicializaciones múltiples.

int matriz[2][3] = {
{ 1,2,3 },
{ 4,5,6 }
};

Estas líneas nos declararían una matriz llamada "matriz" de 2x3 elementos
inicializada con los valores indicados. Las matrices son extremadamente útiles para
trabajar con multitud de problemas matemáticos que se formulan de esta forma o
para mantener tablas de datos a los que se accede con frecuencia y por tanto su
referencia tiene que ser muy rápida.
Se permite la inicialización de arreglos, debiendo seguir el siguiente formato:

tipo nombre_arr[ n] = {lista- nvalores};


tipo nombre_arr[ n ][ m ] = {{lista-0},…, {lista-n-1}};
tipo nombre_arr[ n][m][l] = {{{lista0-0},…,{lista0-m-1}},

{{listan-1-0},…,{listan-1-m-1}};
};

Por ejemplo:

int i[10] = {1,2,3,4,5,6,7,8,9,10};


int num[3][4]= { {0,1,2,3}, {4,5,6,7}, {8,9,10,11} };
int num[2][2][2]={{{0,1}, {2,3}},
{{4,5}, {6,7}}};

A igual que los arreglos unidimensionales, los arreglos multidimensionales


utilizaran sentencias interactivas anidadas para cada una de las dimensiones.

5.3.2.4. Ejemplo de arreglos multidimensionales

Ejemplo 1: Realice un programa que lea una matriz de n * n elementos enteros


y muestre cuántos de estos elementos son primos.

#include <conio.h>
#include <iostream.h>
#define max 100
void main()
{ int num[max][max];
int i, j, n, div, contprimos;
do { cout<<”Ingrese la cantidad de números a ingresar:”;
cin>>n;
}while( n<1 || n>100);
for(i = 0; i < n ; i++) // recorre las filas
for(j = 0; j < n ; j++) // recorre las columnas
{ cout<<”Ingrese el numero[”<<i+1<<”][”<<j+1<<”]:”;
cin>>num[i][j];
}
for(contprimos=0, i = 0; i < n ; i++) // recorre las filas
for(j = 0; j < n ; j++) // recorre las columnas
{
for(div=2;div<num[i][j] && num[i][j]%div!=0; div++);
if(div==num[i][j]) contprimos++;
}
cout<<"Existen : ”<<contprimos<<” numeros primos”<<endl;
}

Ejemplo 2: calcular el determinante de una matriz de n * n elementos enteros.

#include<iostream.h>
void main()
{
int i,j,k,l,m,n ;
float a[50][50];
float det;
cout << "Introducir el ORDEN DE LA MATRIZ : N = " << endl;
cin >> n;
m=n-1;
cout << "Introducir los elementos" << endl;
cout << "------------------------" << endl;;
for(i=0; i<n; i++)
for(j=0; j<=n; j++)
cin >> a[i][j];
det=a[0][0];
for(k=0;k=m;k++)
{ l=k+1;
for(i=l;i<n;i++)
{ for(j=l;j<n;j++)
a[i][j] = ( a[k][k]*a[i][j]-a[k][j]*a[i][k] )/a[k][k];
}
det=det*a[k+1][k+1];
}
cout << endl;
cout << "DETERMINANTE = " << det << endl;
cout << "------------------------" << endl;
}

Ejemplo 3: Realice un programa que lea una matriz de n * n elementos enteros


y muestre el arreglo solo ordenando los números pares.

#include <conio.h>
#include <iostream.h>
#define max 20
void main()
{ int num[max][max];
int i, j, k, n, aux, auxA[max*max];
do { cout<<”Ingrese la cantidad de números a ingresar:”;
cin>>n;
}while( n<1 || n>100);
for(i = 0; i < n ; i++)
for(j = 0; j < n ; j++)
{ cout<<”Ingrese el numero[”<<i+1<<”][”<<j+1<<”]:”;
cin>>num[i][j];
}
for(i = 0,k=0; i < n ; i++)
for(j = 0; j < n ; j++)
auxA[k++]=num[i][j];

for(i = 0; i < k-1 ; i++)


if(auxA[i]%2==0)
for(j = i+1; j < k ; j++)
if(auxA[j]%2==0 && auxA[i] >auxA[j])
{ aux=auxA[i];
auxA[i]=auxA[j];
auxA[j]=aux;
}

cout<<"La matriz ordenado solo sus valores pares es:”<<endl;


for(i = 0,k=0; i < n ; i++)
{ for(j = 0; j < n ; j++)
{ num[i][j]= auxA[k++];
cout<<num[i][j]<<” ”;
}
cout<<endl;
}
}
Ejemplo 4: Realice un programa que lea una matriz de n * n elementos enteros
y muestre si la diagonal principal es igual a la secundaria. Además muestre cuales
filas son iguales a su columna.

#include <conio.h>
#include <iostream.h>
#define max 20
void main()
{ int num[max][max];
int i, j, k, n, v;
do { cout<<”Ingrese la cantidad de números a ingresar:”;
cin>>n;
}while( n<1 || n>100);
for(i = 0; i < n ; i++)
for(j = 0; j < n ; j++)
{ cout<<”Ingrese el numero[”<<i+1<<”][”<<j+1<<”]:”;
cin>>num[i][j];
}

for(i = 0; i < n ; i++)


if(num[i]i] != num[i][n-1-i])
break;
if(i==n)
cout<<”la diagonal principal es igual a la secundaria”<<endl;
else
cout<<”las diagonal principal y secundaria son distintas”<<endl;

for(i = 0; i < n ; i++)


for(k=0; k<n; k++)
{ for(j = 0; j< n ; j++)
if(num[i]j] != num[k][j])
break;
if(j==n)
cout<<”la Fila:”<<i+1<<” es igual a la Columna:”<<k+1<<endl;
}
}

Además de clasificar los arreglos según su dimensión, también los podemos


declarar según sea su tipo de datos: En arreglos numéricos y arreglos alfanuméricos o
de caracteres. Tomando como diferencia primordial el contenido que puede
almacenar sus variables, notaremos que en los numéricos puede existir diversidad de
símbolos (como se ha reflejado en los apartados anteriores) y en los de carácter un
único símbolo, por lo que son llamadas cadenas de caracteres.

5.3.3. Cadenas de caracteres.

Una cadena es un conjunto de caracteres, o valores de tipo "char", terminados


con el carácter nulo, es decir el valor numérico 0. Internamente se almacenan en
posiciones consecutivas de memoria. Este tipo de estructuras recibe un tratamiento
especial, y es de gran utilidad y de uso continuo.
Lo que distingue a una cadena de caracteres, con respecto a un arreglo
numérico, es que la cadena de caracteres tiene como último carácter al carácter nulo
‘\0’. Esto permite conocer hasta donde evaluaremos la secuencia de caracteres.
La manera de definir una cadena es la siguiente:

char nombre_identificador [<longitud máxima>];


Por ejemplo, si se declara el arreglo:

char cadena[8];

Se podrá asignar los siguientes valores a cada uno de sus elementos:

cadena[0] = 'A' ;
cadena[1] = 'R' ;
cadena[2] = 'R' ;
cadena[3] = 'E' ;
cadena[4] = 'G' ;
cadena[5] = 'L' ;
cadena[6] = 'O' ;
cadena[7] = '\0';

Al contener el carácter nulo, el arreglo cadena será reconocido por las funciones
y objetos diseñados para manipular cadenas de caracteres. Entre estas las sentencias
de salida cout, printf, puts, etc.
Para manejar y realizar operaciones con las cadenas de caracteres es necesario
realizar sentencias de recorrido hasta encontrar el carácter fin de cadena. Lo que lo
diferencia de los arreglos numéricos que es necesario conocer la cantidad de
elementos a evaluar.
Un arreglo es necesario leer y mostrar cada elemento uno a uno en el caso de
las cadenas solo es necesario usar una función que atrape cadenas de caracteres o las
muestre, ejemplo:

char cadena[10];
get ( cadena); // Lee una cadena de caracteres
cin>>cadena; // Lee la cadena y transforma el carácter intro en’\0’
scanf(”%s”, cadena); // Lee la cadena y transforma el carácter intro
en’\0’
cout<<cadena; // muestra todos los caracteres hasta el carácter
final

Para evaluar una cadena siempre sera necesario al menos una sentencia de
recorrido.
Ejemplo: realice un programa que lea una frase y cambie un carácter de la frase
por un nuevo carácter.

#include <conio.h>
#include <iostream.h>
#define max 100
void main()
{ char frase[max];
char carV, carN;
cout<<”Ingrese una frase:”
gets(frase);
cout<<”Ingrese el carácter a modificar:”
carV = getch();
cout<<”Ingrese el nuevo nuevo carácter:”
carN = getch();

for(i = 0; frase[i] !=’\0’ ; i++)


if( frase[i] ==carV)
frase[i] = carN ;

cout<<"La nueva frase es : ”<<frase<<endl;


}
Para manejar un arreglo de cadenas de caracteres se debe declarar como un
arreglo bidimensional de elementos de tipo char, como por ejemplo: Leer 10 nombres
y una calificación.

#include <iostream.h>
#include <conio.h>
void main()
{
unsigned short int calif[10];
char nombre[10][20]; // Se declara un arreglo bidimensional
// para 10 nombres de 20 caracteres por
// nombre más un carácter para el nulo.
for( int x=0 ; x < 10 ; x++)
{
cout << "NOMBRE [" << x +1<< "] = " ;
cin >> nombre[x];
cout << "CALIFICACION [" << x+1 << "] = " ;
cin >> calif[x];
}
}

Cuando se tiene un arreglo de cadenas de caracteres, se puede utilizar para


asignar valores en las declaraciones de cadena.
Por Ejemplo:

char nombres[][5] = { "HUGO", "PACO", "LUIS" } ;

Es equivalente a:
char nombres[3][5];
nombres[0] = "HUGO" ; // no aplicable
nombres[1] = "PACO" ; // no aplicable
nombres[2] = "LUIS" ; // no aplicable

Esto también puede manejarse a nivel de caracteres, así:

char nombres[][5] = { 'H','U','G','O','\0',


'P','A','C','O','\0',
'L','U','I','S','\0' };

o así:

char nombres[3][5];

nombres[0][0] = 'H' ;
nombres[0][1] = 'U' ;
nombres[0][2] = 'G' ;
nombres[0][3] = 'O' ;
nombres[0][4] = '\0' ;
nombres[1][0] = 'P' ;
nombres[1][1] = 'A' ;
nombres[1][2] = 'C' ;
nombres[1][3] = 'O' ;
nombres[1][4] = '\0' ;
nombres[2][0] = 'L' ;
nombres[2][1] = 'U' ;
nombres[2][2] = 'I' ;
nombres[2][3] = 'S' ;
nombres[2][4] = '\0' ;

Al manejar arreglos bidimensionales de cadenas, no es necesario escribir el


valor de la primera dimensión de los arreglos cuando, en su declaración, se asignan
valores constantes a los elementos. La ventaja que tiene es que, al no especificar una
de las dimensiones, la cantidad de cadenas a manejar puede variarse con sólo
agregarlas a la lista o eliminarlas de ella.
Además de las funciones gets() y puts(), existe otro grupo de funciones para el manejo
de cadenas de caracteres, como strlen() y strupr(). Los prototipos de estas funciones se
encuentran declarados en la libreria STRING. En la tabla (Figura 5.4) siguiente se describen
brevemente algunas de las funciones para el manejo de cadenas de caracteres, cuyos
prototipos se encuentran declarados en STRING.H.

FUNCION DESCRIPCION
stpcpy Copia una cadena de caracteres en otra.
strcat Añade una cadena de caracteres a otra.
strchr Busca, en una cadena, un carácter dado.
strcmp Compara dos cadenas.
strcmpi Macro que compara dos cadenas sin distinguir entre mayúsculas y minúsculas.
strcpy Copia una cadena.
strdup Copia una cadena a una nueva localidad.
_strerror Genera un mensaje de error definido por el programador.
strerror Retorna el apuntador al mensaje asociado con el valor del error.
stricmp Compara dos cadenas sin diferenciar entre mayúsculas y minúsculas
strlen Determina la longitud de una cadena.
strlwr Convierte las mayúsculas de una cadena en minúsculas.
strncat Añade el contenido de una cadena al final de otra.
strncmp Compara parte de una cadena con parte de otra.
Compara parte de una cadena con parte de otra, sin distinguir entre mayúsculas
strncmpi
y minúsculas.
strncpy Copia un número de bytes dados, desde una cadena hacia otra.
strnset Hace que un grupo de elementos de una cadena tengan un valor dado.
Busca la primera aparición, en una cadena, de cualquier carácter de un
strpbrk
conjunto dado.
strrchr Busca la última aparición de un carácter en una cadena.
strrev Invierte el orden de los caracteres de una cadena.
strset Hace que los elementos de una cadena tengan un valor dado.
Busca en una cadena el primer segmento que es un subconjunto de un conjunto
strspn
de caracteres dado.
strstr Busca en una cadena la aparición de una subcadena dada.
_strtime Convierte la hora actual a una cadena.
strtod Convierte una cadena a un valor double ó long double.
strtol Convierte una cadena a un valor long.
strtoul Convierte una cadena a un valor unsigned long.
strupr Convierte las minúsculas de una cadena a mayúsculas.
Figura 5. 4. Tabla de funciones para el manejo de cadenas de caracteres.

5.3.3.1. Ejemplo de cadenas de caracteres

Ejemplo1: Realice un programa que lea una frase y muestre si es palindrome.

#include <conio.h>
#include <iostream.h>
#define max 100
void main()
{ char frase[max];
int i, j;
cout<<”Ingrese una frase:”
gets(frase);
for(i = 0; frase[i] !=’\0’ ; i++);
for(i=i-1, j = 0; frase[i] == frase[j] && i > j ; j++);

if( i<=j)
cout<<"La frase : ”<<frase<< “ es palindrome”<<endl;
else
cout<<"La frase : ”<<frase<< “ no es palindrome”<<endl;
}

Ejemplo2: Realice un programa que lea n nombres, convierta los caracteres a


mayúscula y los muestre ordenados alfabéticamente.

#include <conio.h>
#include <iostream.h>
#define max 25
#define max2 100
void main()
{ char nom[max2][max], aux[max];
int i, j, k, v, n, cambio;
cout<<”Ingrese cantidad de nombres:”
cin>>n;
cout<<”Ingrese los nombres:”<<endl;
for( i=0 ; i < n ; i++)
{ cout << "|tNombre: [" << i +1<< "] = " ;
cin >> nom[i];
}
// transforma los n nombres a mayúscula
for(i = 0; i<n ; i++)
for(j = 0; nom[i][j] !=’\0’ ; j++)
if(nom[i][j]>=’a’ && nom[i][j]<=’z’)
nom[i][j]=nom[i][j] – 32;

// ordena los n nombres método burbuja


do{ cambio=0;
for(i = 0; i<n-1 ; i++)
{ v=0;
for (j=0; v==0 && nom[i][j]!=’\0’&& nom[i+1][j]!=’\0’; j++)
if(nom[i][j] > nom[i+1][j]) v=1;
else if(nom[i][j] < nom[i+1][j]) v=2;
if(v==0){ if(nom[i][j]!=’\0’) v=1;
if(nom[i+1][j]!=’\0’) v=2;
}
if( v==1){ cambio=1;
for(j = 0; nom[i][j] !=’\0’ ; j++)
aux[j]=nom[i][j];
aux[j]=nom[i][j];
for(j = 0; nom[i+1][j] !=’\0’ ; j++)
nom[i][j]=nom[i+1][j];
nom[i][j]=nom[i+1][j];
for(j = 0; aux[j] !=’\0’ ; j++)
nom[i+1][j]=aux[j];
nom[i+1][j]=aux[j];
}
}
}while(cambio==1);

cout<<"Los nombres ordenados alfabéticamente:”<<endl;


for(i = 0; i<n ; i++)
cout<< nom[i]<< endl;
}

Ejemplo 3: Realice un programa que lea n nombres, convierta a mayúscula y


elimine los nombres repetidos.

#include <conio.h>
#include <iostream.h>
#define max 25
#define max2 100
void main()
{ char nom[max2][max], aux[max];
int i, j, k, l, v, n;
cout<<”Ingrese cantidad de nombres:”
cin>>n;
cout<<”Ingrese los nombres:”<<endl;
for( i=0 ; i < n ; i++)
{ cout << "|tNombre: [" << i +1<< "] = " ;
cin >> nom[i];
}
// transforma los n nombres a mayúscula
for(i = 0; i<n ; i++)
for(j = 0; nom[i][j] !=’\0’ ; j++)
if(nom[i][j]>=’a’ && nom[i][j]<=’z’)
nom[i][j]=nom[i][j] – 32;

// busca las frases repetidas y las elimina


for(k=0;k<n-1; k++)
for(i = k; i<n ; i++)
{ v=0;
for (j=0; v==0 && nom[i][j]!=’\0’&& nom[i+1][j]!=’\0’; j++)
if(nom[i][j] > nom[i+1][j]) v=1;
else if(nom[i][j] < nom[i+1][j]) v=2;
if(v==0) { if(nom[i][j]!=’\0’) v=1;
if(nom[i+1][j]!=’\0’) v=2;
}
if( v==0){
for(l = i; l<n ; l++)
{ for(j = 0; nom[l+1][j] !=’\0’ ; j++)
nom[l][j]=nom[l+1][j];
nom[l][j]=nom[l+1][j];
}
i--;
n--;
}
}

cout<<"Los nombres sin repetir son:”<<endl;


for(i = 0; i<n ; i++)
cout<< nom[i]<< endl;
}
CAPITULO 6

APUNTADORES

6.1. Referencias de memoria.

Las referencias son novedades absolutas del C++ (no se encuentran disponibles
en C). Una referencia es un nuevo tipo de datos que nos va a permitir utilizar las
características de los punteros pero tratándolos como variables ordinarias. Podéis
imaginaros una referencia como un "alias" de una variable o, mejor dicho, como la
misma variable disponible pero con un nombre distinto.
La inicialización de una referencia es simple, sólo tendremos que asociar una
referencia a una variable que ya esté creada. Una vez realizado la inicialización, la
referencia va a estar continuamente asociada con la variable correspondiente. Si
quisiéramos hacer que la referencia fuese el "alias" de otra variable o referenciar a
otra variable no podríamos, ocurre un error de compilación, solo podrá realizarse en
el momento de su declaración.
La declaración de una referencia:
Tipo_dato &nombre_referencia = variable_declarada;

Ejemplo:

// dato es una variable definida y es de tipo entero.


int dato;
// referenciaDato es la referencia creada de dato.
int &referenciaDato = dato;

Como se observa para crear una referencia no necesitamos más que la variable
a la que queremos referenciar, que en el ejemplo es dato, junto la referencia en sí, que
se va a definir con el símbolo &. De este modo, referenciaDato es la referencia o
alias de la variable dato.
Una vez realizada, cualquier cambio que hagamos sobre dato se verá reflejado
en referenciaDato y viceversa, es decir, si realizamos una modificación en
referenciaDato, esta también se va a ver reflejada en la variable dato.
Ejemplo:

#include <iostream.h>
void main()
{
int dato = 50;
int& refDato = dato;

cout << "La variable dato vale " << dato << ’\n’;
cout << "La variable refDato vale " << refDato << ’\n’;
// multiplicamos la variable dato por 2, ahora dato = 100
dato *= 2;
cout << "La variable dato vale " << dato << ’/n’;
cout << "La variable refDato vale " << refDato << ’\n’;

// incrementamos el valor de refDato = 101;


refDato ++;
cout << "La variable dato vale " << dato << ’\n’;
cout << "La variable refDato vale " << refDato;
}

Si se observa los resultados en la salida, tenemos que:

La variable dato vale 50


La variable refDato vale 50
La variable dato vale 100
La variable refDato vale 100
La variable dato vale 101
La variable refDato vale 101

Los cambios efectuados en dato y refDato se ven involucrados. Debido a que


dato y refDato comparten la misma dirección de memoria y por eso, cualquier
cambio que efectuemos sobre dato afectará a refDato y viceversa. (Figura 6.1)

Figura 6. 1. Referencia de memoria.

Para comprobar que las direcciones de dato y refDato son las misma,
realizamos:

void main()
{
int dato = 50;
int& refDato = dato;

cout << "La dirección de la variable dato es " << &dato << endl;
cout << "La dirección de la referencia refDato es " << &refDato;
}

6.2. Apuntadores.

6.2.1. Concepto.

Los apuntadores o punteros se definen como una variable que contiene la


dirección de memoria de un dato o de otra variable que contiene al dato. El puntero
apunta al espacio físico donde está el dato o la variable. Pueden apuntar a un objeto
de cualquier tipo, como una estructura o una función. Se utilizan para referenciar y
manipular estructuras de datos, bloques de memoria, paso de argumentos.
Los punteros o apuntadores podemos conceptualizarlos como referencias de
memoria variables, las cuales permiten realizar operaciones con las posiciones de
memorias, permitiendo el desplazamiento entre las variables y además con la
posibilidad de modificar el contenido de estas.
Es una herramienta cómoda, poderosa y directa para el manejo de variables
complejas, argumentos, parámetros, etc.

6.2.2. Declaración de un apuntador.

Para declarar un puntero, este deberá tener un tamaño de memoria a la cual se


pueda ubicar y un nombre el cual se identifica su forma de referenciar y el operar ‘*’
el cual nos indica la presencia del puntero:
tipo_de_variable_apuntada *nombre_del_puntero ;

Ejemplo:

int *pint ;
double *pfloat ;
char *letra , *codigo , *caracter ;

En estas declaraciones indicamos al compilador que reserve una posición de


memoria para albergar la dirección de una variable, del tipo indicado, la cual será
referenciada con el nombre que hayamos dado al puntero. El tipo de dato del puntero
indica la capacidad del puntero de ubicarse en una variable o estructura de dato, la
cual deberá ser del mismo tamaño.
Cuando declaramos un puntero como:

int *a;

Indicamos que:
 a representa la dirección de memoria que apunta.
y
 *a representa el contenido de la dirección de memoria apuntada.

Obviamente, un puntero debe ser inicializado antes de usarse, y una de las eventuales
formas de hacerlo es la siguiente:

int var1 ; // declaro ( y creo en memoria ) una variable entera )

int *pint ; // un puntero que contendrá la dirección de una variable entera

pint = &var1 ; //escribo en la dirección del puntero la de la variable

"&nombre_de_una_variable " implica la dirección de la misma.


Esquemáticamente, lo que hemos hecho se puede simbolizar como se muestra
en la Figura 6.2:

Figura 6. 2. Representación de un puntero.

La variable var1 reserva una dirección de memoria de 2 bytes solo para ella, el
puntero apunta a esta posición permitiendo utilizar el contenido de esta.
En la declaración del puntero, está implícita otra información: el tamaño (en
bytes) de la variable apuntada.
El símbolo &, ó dirección, puede aplicarse a variables, funciones, etc., pero no
a constantes ó expresiones, ya que éstas no tienen una posición de memoria asignada.
La operación inversa a la asignación de un puntero, de referenciación del
mismo, se puede utilizar para hallar el valor contenido por la variable apuntada. Así
por ejemplo serán expresiones equivalentes:

y = var1 ;
y = *pint ;
printf("%d" , var1 ) ;
printf("%d" , *pint) ;

En estos casos, la expresión " *nombre_del_puntero ", implica " contenido de la


variable apuntada por el mismo”. Ejemplo de esto:

#include <stdio.h>
void main()
{
char var1 ; //una variable del tipo carácter
char *pchar; // un puntero a una variable del tipo carácter
pchar = &var1 ; //*asignamos al puntero la dirección de la variable
for (var1 = 'a'; var1 <= 'z'; var1++)
printf("%c", *pchar) ; // imprimimos el valor de la variable apuntada
}

Vemos acá, que en el for se incrementa el valor de la variable, y luego para


imprimirla usamos la de referenciación de su puntero.
El programa imprimirá las letras del abecedario de la misma manera que lo
habría hecho si la sentencia del printf hubiera sido, printf("%c" , var1 ). Hay un
error, que se comete con bastante frecuencia, y es cargar en la dirección apuntada por
un puntero a un tipo dado de variable, el contenido de otro tipo de las mismas, por
ejemplo:

double d = 10.0 ;
int i = 7 , *pint ;
pint = &i ;
*pint = 10 ; // correcto, equivale a asignar a i el valor 10
*pint = d ; // ERROR se pretende cargar en una variable entera un
valor double
pint = &d ; // INCORRECTO se pretende apuntar a una variable
double con un puntero declarado como apuntador a int
pint = 4358 ; // ??????

El primer error, la asignación de un double, produce la pérdida de información


dada por la conversión automática de tipo de variable, el segundo produce un llamado
de atención rotulado como " asignación sospechosa de un pointer”. Resumiendo, las
variables ó constantes cargadas por de referenciación de un puntero, deben coincidir
en tipo con la declaración de aquel.
La asignación de una constante a un pointer, y no a la variable apuntada por él,
es un serio error, ya que debe ser el compilador, el encargado de poner en él el valor
de la dirección, aquel así lo declara dando un mensaje de "conversión de puntero no
transportable" . Si bien lo compila, ejecutar un programa que ha tenido esta
advertencia es similar a jugar a la ruleta rusa, puede "colgarse" la máquina ó lo que es
peor destruirse involuntariamente información contenida en un disco, etc.
Hay un sólo caso en el que esta asignación de una constante a un puntero es
permitida, muchas funciones para indicar que no pueden realizar una acción ó que se
ha producido un error de algún tipo, devuelven un puntero llamado "Null Pointer", lo
que significa que no apunta a ningún lado válido, dicho puntero ha sido cargado con
la dirección NULL (por lo general en valor 0), así la asignación: pint = NULL; es
válida y permite luego operaciones relacionales del tipo if( pint ) ..... ó if( pint !=
NULL ) para convalidar la validez del resultado devuelto por una función.
Una advertencia seria que se debe tener en cuenta que los punteros no son
enteros, como parecería a primera vista, ya que el número que representa a una
posición de memoria, sí lo es. Debido al corto alcance de este tipo de variable,
algunos compiladores pueden, para apuntar a una variable muy lejana, usar cualquier
otro tipo, con mayor alcance que el antedicho.

6.2.3. Apuntadores y arreglos

Hay una relación muy cercana entre los punteros y los arreglos. El nombre de
un arreglo es equivalente a la dirección del elemento[0] del mismo.

int a[10];
// La dirección del primer elemento: &a[0]
// La dirección del arreglo se refleja como su mismo nombre: a
// entonces a == &a[0], ambas direcciones son equivalentes

El nombre de un arreglo, para el compilador, es un apuntador inicializado con la


dirección del primer elemento del arreglo. Sin embargo hay diferencias entre ambos.
6.2.4. Asignación de los apuntadores.

Observemos las líneas siguientes:

float var1 , conjunto[] = { 9.0 , 8.0 , 7.0 , 6.0 , 5.0 };


float *punt ;

punt = conjunto ; // equivalente a hacer : punt = &conjunto[0]


var1 = *punt ;
*punt = 25.1 ;

Es válido asignar a un puntero el valor de otro, el resultado de ésta operación es


cargar en el puntero punt la dirección del elemento[0] del arreglo, y posteriormente
en la variable var1 el valor del mismo (9.0) y para luego cambiar el valor de dicho
primer elemento a 25.1.
La diferencia entre un puntero y el denominador de un arreglo, el primero es
variable, es decir que puedo asignarlo, incrementarlo, etc, en cambio el arreglo es una
constante, que apunta siempre al primer elemento del arreglo con que fue declarado,
por lo que su contenido no puede ser variado. Si lo piensa un poco, es lógico, ya que
un "conjunto" implica la dirección del elemento conjunto[0], por lo que, si yo
cambiara su valor, apuntaría a otro lado dejando de ser un "conjunto”.
El siguiente ejemplo nos muestra un tipo de error frecuente:

int conjunto[5] , lista[] = { 5 , 6 , 7 , 8 , 0 ) ;


int *apuntador ;

apuntador = lista ; // correcto


conjunto = apuntador; // ERROR (conjunto es una dirección constante)
lista = conjunto ; // ERROR ( son direcciones constantes )
apuntador = &conjunto;
//ERROR no se puede aplicar el operador & a una constante
6.2.5. Incremento y decremento de un apuntador.

Dado las siguientes instrucciones podemos analizar:

int *pint , arreglo_int[5] ;


double *pdou , arreglo_dou[6] ;

pint = arreglo_int ; // pint apunta a arreglo_int[0]


pdou = arreglo_dou ; // pdou apunta a arreglo_dou[0]
pint += 1 ; // pint apunta a arreglo_int[1]
pdou += 1 ; // pdou apunta a arreglo_dou[1]
pint++ ; // pint apunta a arreglo_int[2]
pdou++ ; // pdou apunta a arreglo_dou[2]

Hemos declarado y asignado dos punteros, uno a int y otro a doublé, con las
direcciones de dos arreglos. Ambos estarán ahora apuntando a los elementos[0] de los
arreglos. En las dos instrucciones siguientes incrementamos en uno dichos punteros.
En el compilador, estas sentencias se leen como: incremente el contenido del
puntero (dirección del primer elemento del arreglo) en un número igual a la cantidad
de bytes que tiene la variable con que fue declarado. Es decir que el contenido de pint
es incrementado en dos bytes (un int tiene 2 bytes) mientras que pdou es
incrementado 8 bytes (por ser un puntero a double), el resultado entonces es el mismo
para ambos, ya que luego de la operación quedan apuntando al elemento siguiente del
arreglo, arreglo_int[1] y arreglo_dou[1].

Vemos que de ésta manera será fácil recorrer un arreglo, independientemente del
tamaño de variables que lo compongan, permitiendo por otro lado que el programa
sea transportable a distintos hardwares sin preocuparnos de la diferente cantidad de
bytes que pueden asignar los mismos, a un dado tipo de variable.
6.2.6. Aritmética de referencia

Debido a que los operadores * y ++ ó -- tienen la misma precedencia y se


evalúan de derecha a izquierda, y los paréntesis tienen mayor precedencia que ambos,
muchas operaciones que los utilizan en conjunto a todos estos operadores, pueden
parecer poco claras y dar origen a un sin número de errores, analicémoslas
detalladamente, partiendo de:

int *p , a[] = { 0 , 10 , 20 , 30 , 40 , 50 , 60 , 70 , 80 , 90 } ;
int var ;
p=a;

El puntero está apuntando a a[0]. Si:

*p = 27 ;

Se asignamos al elemento apuntado por p (que seria a[0]) un valor constante.


De forma inversa:

var = *p ;

var sería asignada al valor contenido de a[0], y p seguiría apuntando al mismo


elemento. Si fuese de la forma:

var = *( p + 1 ) ;

Carga a var con el contenido del elemento siguiente al apuntado por p (el cual
seria a[1]). El puntero p, no varía sigue apuntando a a[0]. De la misma forma: var =
*( p + 3 ) asignará 30 a var, sin modificar el contenido de p.
En cambio la expresión:

var = *( p++ ) ;

Se asigne a var el valor de lo apuntado por p e incremente luego éste para


que apunte al próximo elemento. Así en var quedaría con el valor de a[0] y p
apuntaría a a[1]. Si en vez de esto hubiéramos preincrementado a p tendríamos:

var = *( ++p ) ;

Apunte con p al próximo elemento y asigne a var con el valor de éste. En este
caso var sería igualada al valor de a[1] y p quedaría apuntando al mismo .
En las dos operaciones anteriores los paréntesis son superfluos ya que al analizarse
los operadores de derecha a izquierda, daría lo mismo escribir:

var = *p++ ; // sintácticamente igual a var = *(p++)


var = *++p ; // " " " var = *(++p)

6.2.7. Aritmética de punteros

La aritmética más frecuentemente usada con punteros son las sencillas


operaciones de asignación, incremento ó decremento y de referenciación. Todo otro
tipo de aritmética con ellos está prohibida ó es de uso peligroso ó poco transportable.
Por ejemplo no está permitido, sumar, restar, dividir, multiplicar, etc., dos
apuntadores entre sí. Por ejemplo.

int *p, *q;


p = p + q; // no tendría caso realizar
// le asignamos a p una dirección que no conocemos

Otras operaciones están permitidas, como la comparación de dos


punteros, por ejemplo (punt1 == punt2) ó (punt1 < punt2), sin embargo
este tipo de operaciones son potencialmente peligrosas, ya que con
algunos modelos de pointers pueden funcionar correctamente y con
otros no.

6.2.8. Los apuntadores y las cadenas de caracteres

No hay gran diferencia entre el trato de punteros a arreglos, y a cadenas de


caracteres, ya que estos dos últimos son entidades de la misma clase. Sin embargo
analicemos algunas particularidades.
Los punteros prestan un mejor beneficio a la hora de
Así como inicializamos una cadena con un grupo de caracteres terminados en
'\0', podemos asignar al mismo un puntero:

p = "Esto es una cadena constante ";

Esta operación no implica haber copiado el texto, sino sólo que a p se le ha


asignado la dirección de memoria donde reside la "E" del texto. A partir de ello
podemos manejar a p como lo hemos hecho hasta ahora. Veamos un ejemplo:

#include <iostream.h>
#define TEXTO1 "¿ Hola , como "
#define TEXTO2 "le va a Ud. ? "
void main()
{ char palabra[20] , *p ;
int i ;
p = TEXTO1 ;
for( i = 0 ; ( palabra[i] = *p++ ) != '\0' ; i++ ) ;
p = TEXTO2 ;
cout<< palabra ;
cout<< p;
}

Definimos primero dos cadenas constantes TEXTO1 y TEXTO2, luego


asignamos al puntero p la dirección del primero, y seguidamente en el for copiamos el
contenido de éste en el arreglo palabra, observe que dicha operación termina cuando
el contenido de lo apuntado por p es el terminador de la cadena, luego asignamos a p
la dirección de TEXTO2 y finalmente se muestra ambas cadenas, obteniendo una
salida del tipo: " ¿ Hola , como le va a UD. ? ".
Reconozcamos que esto se podría haber escrito más compacto, ya que palabra
también es un puntero y NULL es cero, cambiamos el for:

while( *palabra++ = *p++ ) ;

Un programa ejemplo del uso de puntero en cadena podría ser el modificar una
frase dada a mayúscula como se muestra a continuación:

#include <iostream.h>
#include <stdio.h>
#include <conio.h>
#define max 100
void main()
{ char frase [max];
char *p;

p=frase;
cout<<”Ingrese una frase:”;
gets(p);
for( ; *p!=’|0’ ; *p++)
if(*p>=’a’ && *p<=’z’)
*p=*p-32; // como los caracteres difieren en 32
//de mayúscula a minúscula
cout<<”La frase ingresada en mayúscula:”<<frase<<endl;
}
6.2.9. Arreglos de apuntadores

Es una práctica muy habitual, sobre todo cuando se tiene que tratar con cadenas
de distinta longitud, generar arreglos cuyos elementos son punteros, que albergarán
las direcciones de dichas cadenas. Si imaginamos a un puntero como una flecha, un
arreglo de ellos equivaldría a un conjunto de flechas.
Así como:

char *flecha;

Se declara un puntero a un carácter. Entonces la declaración:

char *flecha[5];

implica un arreglo de 5 punteros a caracteres.


Los arreglos de punteros pueden ser inicializados de la misma forma que un
arreglo común, es decir dando los valores de sus elementos, durante su declaración,
por ejemplo si quisiéramos tener un arreglo donde el subíndice de los elementos
coincidiera con el nombre de los días de la semana, podríamos escribir:

char *dias[] = { "número de día no válido" ,


"lunes" ,
"martes" ,
"miercoles" ,
"jueves" ,
"viernes" ,
"sabado" ,
"domingo"
};
Igual que antes, no es necesario en este caso indicar la cantidad de elementos,
ya que el compilador los calcula por la cantidad de términos dados en la
inicialización. Así el elemento dias[0] será un puntero con la dirección de la primera
cadena, dias[1], la del segundo, etc.
Sea A una matriz de enteros (arreglo de 2 dimensiones) y P un arreglo de
punteros enteros:

int A[5][5]; // Matriz de dos dimensiones


int *P[5]; //arreglo de punteros

Las declaraciones anteriores nos indican la declaración de 25 elementos de tipo


entero para la matriz A y de 5 elementos declarados como punteros de tipo int para P
Como se muestra la Figura 6.3.

Figura 6. 3. Representación de una matriz entera y un arreglo de punteros

Si queremos que cada puntero en el arreglo de punteros, apunte a cada arreglo


de la matriz, entonces asignaremos a cada puntero con las filas de la matriz A:

int i;
int A[5][5]; // Matriz o arreglo de dos dimensiones
int *P[5]; // arreglo de punteros
for (i=0; i<5; i++)
P[i]=A[i];

Como ese muestra en la Figura 6.4.


Figura 6. 4. Arreglo de punteros apunta a cada fila de la matriz.

Aunque podríamos pensar que el bucle que asigna a los elementos del arreglo
de punteros P las filas de A (P[i]=A[i]), puede sustituirse por P=A, no se puede ya
que los niveles de dirección son diferentes. Expresados de otra forma, los tipos de P y
A no son iguales ni admiten una conversión entre ellos:

El tipo de P int *[5] ( arreglo de 5 elementos tipo puntero a int)


El tipo de P[i] int * (puntero a int)
El tipo de A int (*)[5] ( puntero a una arreglo de 5 elementos tipo int)
El tipo de A[i] int * (puntero a int)

6.2.10. Apuntador a apuntador.

Para especificar que una variable es un puntero a un puntero, la sintaxis


utilizada es la siguiente:

tipo_de_variable_apuntada **nomb_var_puntero_a_puntero ;

donde tipo_de_variable_apuntada especifica el tipo del apuntador la doble


dirección ** indica un puntero que apunta a un puntero y
nomb_var_puntero_a_puntero es el identificador de la variable puntero a puntero.
Por ejemplo:

int A, *P, **PP;


A = 10; // dato
P = &A; //puntero que apunta al dato
PP = &P; // puntero que apunta al puntero que apunta al dato

Gráficamente en memoria esto sería como se muestra en la figura 6.5.

Figura 6. 5. Apuntador a apuntador.

Un ejemplo utilizando un puntero a puntero seria el código siguiente:

void main()
{ int i, j;
int A[5][5]={{1,2,3,4,5},
{5,4,3,2,1},{0,1,2,3,4},{4,3,2,1,0},{0,1,0,1,0}};
int *p[5] ; // arreglo de punteros
int **q ; //puntero a puntero entero
for(i=0; i<5; i++)
p[i] =A[i];
q = p;
for(i=0; i<5; i++)
{ for(j=0; j<5; j++)
cout<<q[i][j];
cout<<endl;
}
}

Como podemos observar con q[i][j] podemos recorrer elementos del arreglo
podemos notar, q[i] es misma dirección que A[i], si q siendo la dirección de A que es
la misma dirección de A[0], entonces q+1 es la posición de A[1] y q+i es la dirección
q+i. De esta observación concluimos que las representaciones siguientes son iguales:
q[i][j], *(q[i]+j), *(*(q+i)+j)
Según lo expuesto, las direcciones q+1 y *(q+1) tienen significados diferentes,
por ejemplo:
q+1+2 es la dirección del elemento q[3] de la matriz de punteros.
*(q+1)+2 es la dirección del elemento q[1][2].
*(*(q+1)+2) es el valor del elemento q[1][2].

6.3. Asignación de memoria dinámica.

Cuando se habla de asignación dinámica de memoria se hace referencia al


hecho de crear variables que reserven espacio en memoria en tiempo de ejecución
del programa, así como liberar el espacio reservado para dichas variables, cuando ya
no son necesarias, también durante el tiempo de ejecución.
Supongamos que debemos recibir una serie de datos de entrada, digamos del
tipo double, y debemos procesar según un determinado algoritmo a aquellos que
aparecen una ó más veces con el mismo valor.
Si no estamos seguros de cuantos datos van a ingresar a nuestro programa,
pondremos alguna limitación, suficientemente grande a los efectos de la precisión
requerida por el problema, digamos 10000 valores como máximo, debemos definir
entonces un arreglo de tipo double capaz de albergar a diez mil de ellos, por lo que el
mismo ocupará del orden de los 80Kb de memoria.
Si definimos este en main(), ese espacio de memoria permanecerá ocupado
hasta el fin del programa, aunque luego de aplicarle el algoritmo de cálculo ya no lo
necesitemos más, comprometiendo seriamente nuestra disponibilidad de memoria
para albergar a otras variables. Una solución posible sería definirlo en una función
llamada en main (), que se ocupara de llenar el arreglo con los datos, procesarlos y
finalmente devolviera algún tipo de resultado, borrando con su retorno a la masiva
variable de la memoria.
Los programas ejecutables creados dividen la memoria disponible en varios
segmentos, uno para el código (en lenguaje máquina), otro para albergar las variables
globales, otro para el stack (a través del cual se pasan argumentos y donde residen las
variables locales) y finalmente un último segmento llamado memoria de apilamiento
ó amontonamiento.(Figura 6.6).

Figura 6. 6. Esquema de asignación de memoria.

La memoria de apilamiento es la zona destinada a albergar a las variables


dinámicas, es decir aquellas que crecen (en el sentido de ocupación de memoria) y
decrecen a lo largo del programa, pudiéndose crear y desaparecer (desalojando la
memoria que ocupaban) en cualquier momento de la ejecución.
Supongamos que queremos ubicar un único dato, declaramos un puntero al tipo
de la variable, ejemplo:

double *p ;

notemos que ésta declaración no crea lugar para la variable, sino que asigna un
lugar en la memoria para que posteriormente se guarde ahí la dirección de aquella.
Para reservar una cantidad dada de byte, se efectúa una llamada a alguna de las
funciones dedicadas al manejo del mismo. La más tradicional es malloc() (su nombre
deriva de memory allocation), a esta función se le da como argumento la cantidad de
bytes que se quiere reservar, y nos devuelve un pointer apuntando a la primer
posición de la "pila" reservada. En caso que la función falle en su cometido (la
memoria está llena) devolverá un puntero inicializado con NULL.

p = malloc(8) ;

hemos pedido 8 bytes (los necesarios para albergar un double) y hemos


asignado a p el retorno de la función, es decir la dirección de la memoria reservada.
Como es algo engorroso recordar el tamaño de cada tipo variable, agravado por
el hecho de que, si reservamos memoria de esta forma, el programa no se ejecutará
correctamente, si es compilado con otro compilador que asigne una cantidad distinta
de bytes a dicha variable, es más usual utilizar sizeof, para indicar la cantidad de
bytes requerida:

p = malloc( sizeof(double) ) ;

Se comprueba si se reservo con éxito mediante:

if( p == NULL )
rutina_de_error() ;

si no lo fue, estas sentencias me derivan a la ejecución de una rutina de error


que tomará cuenta de este caso. Por supuesto podría combinar ambas operaciones en
una sola, como:

if( ( p = malloc( sizeof(double) ) ) == NULL )


{ cout<<"no hay mas lugar en memoria!") ;
exit(1) ;
}
se ha reemplazado aquí la rutina de error, por un mensaje y la terminación del
programa, por medio de exit() retornando un código de error .

6.3.1. La función malloc

Es la función genérica para asignar memoria dinámica a apuntadores. Su


prototipo es:

void * malloc (size_t nbytes);

donde nbytes es el número de bytes que queremos que sean asignados al


apuntador. La función retorna un apuntador de tipo void*, por lo que tenemos que
hacer type cast al valor al tipo del apuntador destino, por ejemplo:

char * r;
r = (char *) malloc (10);

Esto asigna a r un apuntador a un bloque de 10 bytes. Cuando queremos asignar


un bloque de data a un tipo diferente a char (diferente a 1 byte) debemos multiplicar
el número de elementos deseados por el tamaño de cada elemento. Tenemos a
disposición el operador sizeof, que retorna el tamaño del tipo de un dato en concreto.

int * b;
b = (int *) malloc (5 * sizeof(int));

Este código asigna a b un apuntador a un bloque de 5 enteros de tipo int, este


tamaño puede ser igual a 2, 4 o más bytes de acuerdo al sistema donde el programa es
compilado.
6.3.2. La función calloc.

calloc es muy similar a malloc en su funcionamiento, su principal diferencia es


en su prototipo:

void * calloc (size_t nelements, size_t size);

ya que admite 2 parámetros en vez de 1. Estos dos parámetros se multiplican


para obtener el tamaño total del bloque de memoria a ser asignado. Usualmente el
primer parámetros (nelements) es el número de elementos y el segundo (size) sirve
para especificar el tamaño de cada elemento. Por ejemplo, podríamos definir a b con
calloc así:

int *b;
b = (int *) calloc (5, sizeof(int));

Otra diferencia entre malloc y calloc es que calloc inicializa todos sus
elementos a 0.

6.3.3. La función realloc

Cambia el tamaño de un bloque de memoria ya asignado a un apuntador.

void * realloc (void * pointer, size_t size);

El parámetro pointer recibe un apuntador a un bloque de memoria ya asignado


o un apuntador nulo, y size especifica el nuevo tamaño que el bloque de memoria
deberá tener. La función asigna size bytes de memoria al apuntador. La función podría
necesitar cambiar la localidad del bloque de memoria para que el nuevo tamaño
pueda encajar, en ese caso el contenido actual del bloque es copiado al nuevo para
garantizar que la data existente no se pierda. La función retorna el nuevo apuntador.
Si no ha sido posible asignar el bloque de memoria con el nuevo tamaño retorna un
apuntador nulo, pero el pointer especificado como parámetro y su contenido
permanecen sin cambios.

6.3.4. La función free

Libera un bloque de memoria dinámica previamente asignado usando malloc,


calloc o realloc.

void free (void * pointer);

Esta función debe ser usada solamente para liberar memoria asignada con las
funciones malloc, calloc y realloc.

6.3.5. Operadores new

Este operador permite solicitar memoria dinámica. new es seguido por un tipo
de dato y opcionalmente el número de elementos requeridos entre corchetes [].
Retorna un apuntador al comienzo del nuevo bloque de memoria asignada. Su forma
es:

pointer = new type


ó
pointer = new type [elementos]
La primera expresión es usada para asignar memoria para contener un solo
elemento de tipo type. La segunda se usa para asignar un bloque (un arreglo) de
elementos de tipo type. Por ejemplo:

int *b;
b = new int [5];

En este caso, se ha asignado espacio para 5 elementos de tipo int en un heap y


ha retornado un apuntador a su comienzo que ha sido asignado a b. Por lo tanto,
ahora, b apunta a un bloque de memoria válido con espacio para 5 elementos int.

Podría preguntarse cuál es la diferencia entre declarar un arreglo normal y


asignar memoria a un apuntador como hemos hecho. La más importante es que el
tamaño de un arreglo debe ser un valor constante, el cual limita su tamaño a lo que
decidamos al momento de designar el programa antes de su ejecución, mientras que
la asignación dinámica de memoria permite asignar memoria durante la ejecución del
programa usando cualquier variable, constante o combinación de ambas como
tamaño.
La memoria dinámica es generalmente administrada por el sistema operativo, y
en interfaces multitarea puede ser compartida entre varias aplicaciones, por lo que
existe la posibilidad de que la memoria se acabe. Si esto ocurre y el sistema operativo
no puede asignar la memoria que solicitamos con el operador new, se retorna un
apuntador nulo. Por esta razón se recomienda siempre chequear si el apuntador
retornado es nulo, luego de una llamada a new.

int *b;
b = new int [5];
if (bobby == NULL)
{
// error asignando memoria.
};
6.3.6. Operador delete

Ya que la necesidad de memoria dinámica está limitada a momentos concretos


dentro de un programa, una vez que no se necesita más debería ser liberada para que
se convierta en disponible para futuras solicitudes de memoria dinámica. Para este
propósito existe el operador delete, cuya forma es:

delete pointer;
ó
delete [] pointer;

La primera expresión debería ser utilizada para borrar memoria asignada para
un único elemento, y la segunda para memoria asignada para múltiples elementos
(arreglos). En la mayoría de los compiladores ambas expresiones son equivalentes y
pueden ser utilizadas sin distinción, aunque en realidad son dos operadores diferentes
y así deben ser considerados para sobrecarga de operadores.

6.3.7. Ejemplos de asignación de memoria dinámica

Ejemplo1: Reserva de memoria de varios datos simples.

#include<stdlib.h>
#include<iostream.h>
void main()
{
int *dato_simple;
dato_simple = (int *) malloc (3*sizeof(int));
}

Ejemplo2: Realice un programa que lea n enteros y los muestre.

#include<stdlib.h>
#include<iostream.h>

void main()
{ int n, i;
int *datos;
cout<<”Ingrese la cantidad de datos:”;
cin>>n;
datos = (int *) malloc (n*sizeof(int));
for(i=0; i<n; i++)
{ cout<<”Ingrese el numero#”<<i+1<<”:”;
cin>>datos[i];
}
cout<<”Los números ingresados:”;
for(i=0; i<n; i++)
cout<<datos[i]<<endl;
}
CAPITULO 7

FUNCIONES

7.1. Concepto.

Las funciones son un conjunto de instrucciones que realizan una tarea


específica. En general toman ciertos valores de entrada, llamados parámetros y
proporcionan un valor de salida o valor de retorno; aunque en C++, tanto unos como
el otro son opcionales, y pueden no estar presentes.
Desde un punto de vista práctico, podemos decir que una función es una parte
de un programa (subrutina) con un nombre, que puede ser invocada (llamada a
ejecución) desde otras partes tantas veces como se desee. Una función en un método
de organización, creando una visión clara del código de un programa, además que
permite su reutilización.
Si observamos detalladamente notaremos que el cuerpo principal de un
programa es una función, lo que cabe destacar que las funciones son la base del
lenguaje.

7.2. Prototipos de funciones

Un prototipo es una declaración de una función. Consiste en una presentación


de la función, exactamente con la misma estructura que la definición, pero sin cuerpo
y terminada con un ";". En C++ es necesario en muchos casos el uso de los
prototipos. Esto permite identificar o reconocer la existencia de la función en el
programa.
La estructura de un prototipo es:

[extern|static]<tipovalor_retorno> <identificador>(<lista_parámetros>);

En general, el prototipo de una función se compone de las siguientes secciones:


 Opcionalmente, una palabra que especifique el tipo de almacenamiento, puede
ser extern o static. Si no se especifica ninguna, por defecto será extern.
 El tipo del valor de retorno, que puede ser void, si no necesitamos valor de
retorno. En C, si no se establece, será int por defecto, aunque en general se
considera una mala técnica de programación omitir el tipo de valor de retorno
de una función. En C++ es obligatorio indicar el tipo del valor de retorno.
 El identificador de la función. Es costumbre, muy útil y muy recomendable,
poner nombres que indiquen, lo más claramente posible, qué es lo que hace la
función, y que permitan interpretar qué hace el programa con sólo leerlos.
Cuando se precisen varias palabras para conseguir este efecto se puede usar
alguna de las reglas más usuales. Una consiste en separar cada palabra con un
"_". Por ejemplo, si hacemos una función que busque el número de teléfono
de una persona en una base de datos, podríamos llamarla "busca_telefono" o
"BuscaTelefono".
 Una lista de declaraciones de parámetros entre paréntesis. Los parámetros de
una función son los valores de entrada (y en ocasiones también de salida).
Para la función se comportan exactamente igual que variables, y de hecho
cada parámetro se declara igual que una variable. Una lista de parámetros es
un conjunto de declaraciones de parámetros separados con comas. Puede
tratarse de una lista vacía. En C es preferible usar la forma "func(void)" para
listas de parámetros vacías. En C++ este procedimiento se considera obsoleto,
se usa simplemente "func()".

Por ejemplo:
int Mayor(int a, int b);

Un prototipo sirve para indicar al compilador los tipos de retorno y los de los
parámetros de una función, de modo que compruebe si son del tipo correcto cada vez
que se use esta función dentro del programa, o para hacer las conversiones de tipo
cuando sea necesario.
En el prototipo, los nombres de los parámetros son opcionales, y si se incluyen
suele ser como documentación y ayuda en la interpretación y comprensión del
programa. El ejemplo de prototipo anterior sería igualmente válido si se escribiera
como:

int Mayor(int, int);


Esto sólo indica que en algún lugar del programa se definirá una función
"Mayor" que admite dos parámetros de tipo int y que devolverá un valor de tipo int.
No es necesario escribir nombres para los parámetros, ya que el prototipo no los usa.
En otro lugar del programa habrá una definición completa de la función.
Normalmente, los prototipos de las funciones se declaran dentro del fichero del
programa, o bien se incluyen desde un fichero externo, llamado fichero de cabecera.
Las funciones son extern por defecto. Esto quiere decir que son accesibles
desde cualquier punto del programa, aunque se encuentren en otros ficheros fuente
del mismo programa.
En contraposición las funciones declaradas static sólo son accesibles dentro del
fichero fuente donde se definen.

7.3. Definición de funciones

Al igual que hemos visto con las variables, las funciones deben declararse, para
lo que usaremos los prototipos, pero también deben definirse.
Una definición contiene además las instrucciones con las que la función
realizará su trabajo, es decir, su cuerpo o código.
La sintaxis de una definición de función es:

[extern|static] <tipo_valor_retorno> <identificador>(<lista_parámetros>)


{
[sentencias] //cuerpo de la función
return valor; //debe ser del mismo valor que tipo_valor_retorno
}

Como vemos, la sintaxis es idéntica a la del prototipo, salvo que se elimina el


punto y coma final, y se añade el cuerpo de función que representa el código que será
ejecutado cuando se llame a la función. El cuerpo de la función se encierra entre
llaves "{}".
La definición de la función se hace más adelante o más abajo, según se mire, es
decir, se hace después que el prototipo y cuerpo principal main(). Lo correcto es
hacerlo después de la función main() si existe el prototipo, de lo contrario solo se
define por encima del cuerpo principal ya que la misma función sirve como prototipo
en su definición.
Una función muy especial es la función main(). Se trata de la función de
entrada, y debe existir siempre, ya será la que tome el control cuando se ejecute el
programa.
El valor de retorno es el valor de respuesta que da la función una vez culminada
mediante el uso de la palabra reservada return, este deberá ser del mismo tipo que la
función definida, cualquier otro valor de retorno será erróneo. Si la función está
definida con la palabra void, se indica que no deberá retornar valor de respuesta, es
decir que en el cuerpo de la función esta la respuesta de esta de forma implícita.

7.4. Invocación a funciones

Una invocación ó llamada a una función implica pasarle el control de la


ejecución del programa, así como los argumentos ó parámetros que requiere para
realizar su tarea.
Por ejemplo se tienen las líneas:

saludo(); //invocación a la función saludo()


precio = calcula(costo); //invocación a la función calcula()

En la primera, se invoca a la función saludo() y no se le pasa ningún argumento.


En la segunda, se invoca a la función calcula(), pasándosele como argumento una
copia del valor que tiene la variable costo. El valor retornado por calcula() se asigna a
la variable precio.
El programa seria:
#include <iostream.h>
#include <conio.h>
// Declaración de funciones
void saludo();
float calcula(float);

// Función principal
void main()
{ float costo, precio;
clrscr();
cout << "COSTO : $ ";
cin>> costo;
saludo(); // invocación a la función saludo()
precio = calcula(costo); // invocación a la función calcula()
cout << "PRECIO : $ " << precio;
getch();
}

// Definición de la función saludo()


void saludo()
{ cout << "!! BIENVENIDO A LA VENTA ESPECIAL !!";
}

// Definición de la función calcula()


float calcula(float x)
{ return( x * 1.6);
}

7.5. Parámetros y Argumentos

Son el medio a partir del cual podemos expandir el ámbito de variables locales
de funciones, hacia otras funciones y además quienes nos permiten establecer
comunicaciones entre funciones. Si nos vemos ante la necesidad de visualizar o
modificar el valor de una variable local en otra función que llamaremos, debemos
invocar a dicha función haciendo referencia de su nombre, seguido de los parámetros
o nombres de variables para las cuales, ampliaríamos su ámbito.

void funcion_llamada(int x) // función que recibe un argumento


{ …
}
void una_funcion(void) // variables de ámbito local
{ int a,b;

funcion_llamada(a); // llamada de la función con un parámetro

}

Desde luego que, sobre la base de la comunicación entre funciones y la teoría


del paradigma procedimental donde aplicamos la disgregación de procesos, nos
podemos encontrar con las siguientes variantes:
 Llamado de funciones sin pasar parámetros.
 Llamado de funciones pasando parámetros.
Está claro que dichas funciones pueden o no devolvernos valores hacia el
origen de su llamado.

7.5.1. Paso de parámetros

Cuando se invoca a una función, en ocasiones es necesario enviarle algunos


elementos indispensables para realizar su tarea. Estos elementos, enviados en la
invocación, se llaman parámetros actuales. Dadas las dos necesidades básicas en una
función, de poder obtener el contenido de una variable o el de modificar el contenido
de una variable, clasificaremos el paso de ellos en:

7.5.1.1. Paso de parámetros por valor o contenido


Cuando surge la necesidad de obtener el valor o contenido de una variable
original o local a una función, en otra función, se utiliza paso de parámetros por valor
cuando se envía una copia de los valores de los parámetros actuales. En este caso, la
función invocada no podrá modificar los valores de las variables utilizadas como
parámetros actuales, sino que trabajará con las copias que se le envían. Estas copias
son recibidas en los parámetros formales, los cuales se comportan como variables
locales pertenecientes a la función invocada.
Al igual que las variables locales, una vez que termina la ejecución de la
función invocada, las variables pertenecientes a los parámetros formales se destruyen;
y por lo tanto, se pierden los valores que envió el módulo invocador.
El siguiente programa se muestra el paso de parámetros por valor:

#include <iostream.h>
void precio(double);
void main(void)
{
double costo;
cout << "COSTO : ";
cin>> costo;
precio(costo); // Se invoco a precio() y se le envía una
// copia del valor de costo
cout << "\n"
cout << costo; // El valor de costo se conserva despues
// de la invocación de precio()
}
void precio(double recibido)
{
recibido=recibido * 1.5; // Se modifica el valor de la copia
// guardada en recibido
cout << "\n"
cout << recibido;
}

Supongamos que se desea intercambiar los valores enteros de a y b mediante


una función intercambio con paso por valor:
#include <iostream.h>
void intercambiar(int, int);
void main(void)
{
int a = 5, b = 7;
cout << "a= "<< a<< " , b= "<< b<<endl;
intercambiar (a, b); // se envía el contenido de a y b
cout << "a= "<< a<< " , b= "<< b<<endl;
}
void intercambiar(int a, int b) //asigna los contenidos a las variables a y b
{ int aux;
aux = a;
a = b;
b = aux;
}

Aunque podemos pensar que en el programa anterior intercambia los valores de


a y b, en la función nunca modifica a los valores y que recibe una copia de sus
contenidos. Las variables a y b locales de la función main() son diferentes a las
variables locales de a y b en la función intercambiar().

7.5.1.2. Paso de parámetros por referencia

La referencia indica trabajar sobre la dirección de memoria que ocupa el


parámetro o variable original.
A diferencia del paso por valor, el paso por referencia permite que la función
invocada modifique el valor original de las variables usadas como argumentos.
En C++, el paso de parámetros por referencia se realiza creando un alias del
identificador de la variable que forma el parámetro actual, como se muestra en el
siguiente programa.

#include <iostream.h>
#include <conio.h>
void oferta(float &);

void main(void)
{ float precio;
cout << "CUAL ES EL PRECIO :"<<endl ;
cin>> precio;
oferta(precio);
cout << " DIJO USTED :" << precio << endl;
cout << " ESO ES CON EL 20 % DE DESCUENTO"<<endl;
}
void oferta(float &bajo)
{
bajo *=0.8 ;
cout << "PRECIO REBAJADO: " << bajo;
}

En el programa se puede decir que bajo es otro identificador asociado a la


variable precio, por lo que la función oferta() realmente modifica el valor contenido
en dicha variable. Según lo anterior, podemos decir que es preferible utilizar una
variable existente en lugar crear una nueva, y eliminar el paso de parámetros por
valor.
Supongamos que se desea intercambiar los valores enteros de a y b mediante
una función intercambio pero esta vez usamos paso por referencia:

#include <iostream.h>
void intercambiar(int, int);

void main(void)
{ int a = 5, b = 7;
cout << "a= "<< a<< " , b= "<< b<<endl;
intercambiar (a, b); // se envía el contenido de a y b
cout << "a= "<< a<< " , b= "<< b<<endl;
}

void intercambiar(int &a, int &b)


//asigna un alias a las variables a y b de main()
{ int aux=a;
a = b;
b = aux;
}

Salida:

a=5,b=7
a=7,b=5

En este caso mostrara por salida los cambios de las variables, ya que en la
función se crea un alias de las mismas variables paramétricas.
Si al argumento por referencia lo precedemos de la palabra const, dicha variable
no podrá ser alterada en el ámbito de la función receptora, por lo tanto nunca afectará
a la variable original.

7.5.1.3. Arreglos como parámetros de función

Al pasar un arreglo como parámetro de una función en su invocación, esta no


envía el contenido de los elementos del arreglo sino que, pasara la dirección de
comienzo del arreglo. El argumento de arreglo de la función invocada tomara la
misma dirección que el del arreglo enviado como parámetro.
Lo que modifiquemos en el arreglo dentro de la función también se verá
afectado en el arreglo paramétrico envido.
En el siguiente ejemplo se leen n nombres, convirtiendo a mayúscula y se
muestran:

#include <conio.h>
#include <iostream.h>
#define max 25
void Leer( char m[][], int n)
{ int i;
for(i=0; i<n; i++)
{ cout<<”Ingrese el nombre#”<<i+1<<”:”<<endl;
cin>>m[i];
}
}
void Mostrar( char m[][], int n)
{ int i;
cout<<”Los nombres:”<< endl;
for(i=0; i<n; i++)
cout<<m[i]<<endl;
}
void Mayuscula( char m[])
{ int i;
for(i=0; m[i]!=’|0’; i++)
if (m[i] >=’a’ && m[i]<=’z’)
m[i]=m[i] - 32;
}
void main()
{ char nom[max][max];
int n;
cout<<”Ingrese cantidad de nombres:”
cin>>n;
cout<<”Ingrese los nombres:”<<endl;
Leer(nom, n);
for(i = 0; i<n ; i++)
Mayuscula( nom[i]);
Mostrar(a[i],n));
}

7.5.1.4. Puntero como parámetros de una función

Los apuntadores y arreglos nos hacen notar que la definición de los argumentos
tipodato s[]; y tipodato *s; son completamente equivalentes cuando se pasa un
nombre de un arreglo a una función. Ya que al pasar un arreglo como parámetro, lo
que en realidad se está pasando es la dirección del comienzo del mismo.
En el siguiente ejemplo se lee una frase y muestra la muestra en minúscula y
en mayúscula:
#include <conio.h>
#include <iostream.h>
#define max 100

void mayuscula( char *m)


{ for(; *m!=’|0’; m++)
if (*m >=’a’ && *m<=’z’)
*m = *m - 32;
}

void minuscula( char *m)


{ for(; *m!=’|0’; m++)
if (*m >=’A’ && *m<=’Z’)
*m = *m + 32;
}

void main()
{ char frase [max],*p;
p=frase;
cout<<”Ingrese la frase:”
gets(p);
mayuscula( p);
cout<<”La frase en mayuscula”<<frase<<endl;
minuscula( p);
cout<<”La frase en minuscula”<<frase<<endl;
}

7.6. Funciones recursivas

Se dice que una función es recursiva cuando puede invocarse a sí misma. En


cada invocación se crean las variables locales, por lo que es indispensable incluir una
condición de salida, ya que de no hacerlo se agotará la memoria disponible en la pila.
El cálculo del factorial de un número es un problema clásico en la aplicación de
las funciones recursivas. En el listado se presenta un programa en C++ que calcula el
factorial de un número dado.

#include <iostream.h>
#include <conio.h>
long int factorial(unsigned short int);
void main()
{ unsigned short int num;
long int result;
do { cout << "El FACTORIAL del número: " ;
cin>> num ;
} while(num <0 || num> 19 );
result = factorial(num);
cout << " es : " << result;
}
long int factorial(unsigned short int n)
{ if(n==0) return 1;
else return n*(factorial(n-1)) ;
}

En el código anterior, si el número dado por el usuario es 4, el proceso realizado


por el programa se podría representar de la manera siguiente.

Numero de
Valor de n Resultado
invocación
1 4 4*(factorial(3))
2 3 3*(factorial(2))
3 2 2*(factorial(1))
4 1 1*(factorial(0))
5 0 1
Figura 7. 1. Tabla de resultado de una función recursiva.
Resultados de invocar a la función factorial() pasándole como parámetro el
número 4.
En cada invocación, se crea en la pila una variable cuyo identificador es n y su
valor cambiará como se muestra en la tabla (Figura 7.1). Como en las invocaciones 1,
2, 3 y 4 el valor de n es diferente de 0, la función vuelve a invocarse a sí misma,
quedando sin resolver el resultado.
Cuando el valor de n es igual a 0 (invocación 5), la función retorna el valor 1 la
la invocación 4, por lo que el resultado en ésta se calcula así:

1 * (factorial(0)) = 1 * 1 = 1

La invocación 4 retorna a la invocación 3 el valor 1 y el resultado en la


invocación 4 es:

2 * (factorial(1)) = 2 * 1 = 2

A su vez, la invocación 3 retorna a la invocación 2 el valor 2. El resultado en la


invocación 2 es:

3 * (factorial(2)) = 3 * 2 = 6

Posteriormente, la invocación 2 retorna el valor 6 a la invocación 1. El


resultado en la invocación 1 es:

4 * (factorial(3)) = 4 * 6 = 24

Finalmente, la invocación 1 retorna el valor 24 a la función invocadora main(),


la cual asigna a la variable result el valor recibido ( 24 ).
CAPITULO 8

ESTRUCTURAS, CLASES Y OBJETOS

8.1. Estructuras.

8.1.1. Concepto.

Todas las variables que hemos utilizado hasta ahora permiten almacenar un dato
y de un único tipo, excepto los arreglos que almacenas varios datos pero de un mismo
tipo.
En ocasiones es necesario contar con tipos de datos que estén compuestos por
conjuntos de elementos de diferentes tipos entre sí. Por ejemplo si se quiere
representar datos de personas esta puede estar asociada a mucha información de la
persona, es decir cantidades matrices de variables, las cuales para poder dar relación
entre ellas tendríamos que hacerlo por su posición pero aun así siguen siendo datos
desasociados. Lo que dificulta su visión y uso. (Figura 8.1)
Nombres Apellidos Dirección … etc.
Persona0 { “José” “Vallejo” “Caracas” … xxx
Persona1 { “María” “García” “Maturín” … xxx
… … … … … … …
PersonaN { Ashlie “Sánchez” “Cumana” … xxx

Figura 8. 1.Datos de personas sin el uso de estructuras.

Las estructuras se crean con la finalidad de agrupar una o más variables,


generalmente de diferentes tipos, bajó un mismo nombre para hacer más fácil su
manejo y representación. Las estructuras son tipos derivados definidos por el usuario
que representan colecciones de elementos.
Ahora sería más fácil de expresar por ejemplo el conjunto de datos de una
persona como una ficha de datos como por ejemplo nombre, apellido, dirección, etc.
Donde algunos de estos datos podrían a su vez ser representados por estructuras como
por ejemplo fechas que estarían compuestos por los datos días, mes, año. (Figura 8.2)
PersonaN

Persona2
Persona1
Persona0
Nombre “Rosmil”
Apellido “Infante”
… …
Fecha 20 02 1985

Figura 8. 2. Datos de personas usando estructuras.

Una estructura es un nuevo tipo de dato compuesto creado por el usuario con el
fin de agrupar datos con una relación en común, estos grupos de datos serán los
conocidos como: datos miembros o campos o registros del nuevo tipo.

8.1.2. Definición de tipos de estructuras


La palabra struct se utiliza para definir un tipo compuesto o agrupación de
datos (simples y/o compuestos).
Para definir una estructura usaremos el siguiente formato:

struct nombre_nuevo_tipo { // Composición de Campos o registros


tipo_dato_1 nombre_campo_1;

tipo_dato_n nombre_campo_n;
};

Después de definir un tipo de estructura, podemos declarar variables de ese tipo


así:
nombre_nuevo_tipo nombre_variable;

Se podrán declarar variables derivadas o compuestas, arreglos, punteros,


referencias, con este nuevo tipo de dato definido.
Por ejemplo, si se requiere definir un nuevo tipo llamado persona cuyos
elementos sean:
 cedula número entero largo.
 nombre cadena de caracteres.
La estructura a definir seria:

struct persona {
long int cedula ;
char nombre[50] ;
};

En este caso, persona es un nuevo tipo que puede utilizarse para declarar una
variable de la estructura como:

persona estudiante ;
8.1.3. Acceso a los miembros de una estructura.

Para acceder a los datos miembros o campos de una estructura utilizaremos el


operador de contenido (.), de la forma:

Nombre_variable_estructura.dato_miembro;

Una vez declarada una estructura, las variables permiten las siguientes
operaciones:

 Inicializar la variable estructura, ya sea al declararla o después de ello (mediante


una asignación), como se muestra en:

// inicialización al declarar estud


persona estud={ 11232425, “Luisa Pera” };
persona estud1;
// inicialización por asignación después de la declaración
estud1.cedula = 12185312 ;
strcpy(estud1.nombre, “Alexander Blanco”) ;

 Obtener su dirección mediante el operador dirección &, como se muestra en:

persona estud, estud1;


persona *p_estud = &estud;
p_estud = &estud1;

 Acceder a sus miembros, como se muestra:

long int cedula;


persona estud={ 12345678, “Armando Paredes” };
cedula = estud1.cedula;
 Asignar una estructura a otra utilizando el operador asignación:

persona estud_a={ 23456789, “Ashlie Dorta” };


persona estud_b={ 14000001, “Valentina Vallejo” };
persona aux;
aux = estud_a;
estud_a = estud_b;
estud_b = aux;

En el siguiente ejemplo, lee datos de 2 personas y las muestra ordenadas por el


dato miembro cedula.

#include <iostream.h>
struct persona { long int cedula ;
char nombre[50] ;
};
void main()
{ persona per1, per2, aux; //declara las variables estructuras
// Lee los datos de personas
cout<<”Datos primera persona:”<<endl;
cout<<” Nombre:”<<endl;
gets(per1.nombre);
cout<<” Cedula:”<<endl;
cin>>per1.cedula;
cout<<”Datos segunda persona:”<<endl;
cout<<” Nombre:”<<endl;
gets(per2.nombre);
cout<<” Cedula:”<<endl;
cin>>per2.cedula;
cout<<endl;
// Ordena los datos de personas por cedula
if(per1.cedula > per2.cedula)
{ aux = estud_a;
estud_a = estud_b;
estud_b = aux;
}
// Muestra los datos de personas ordenados por cedula
cout<<”Datos Ordenados:”<<endl;
cout<<per1.cedula<<” ”<<per1.nombre<<endl;
cout<<per2.cedula<<” ”<<per2.nombre<<endl;
}

Se muestra en salida al ejecular:

Datos primera persona:


Nombre:Maria Lopez
Cedula:12924345
Datos primera persona:
Nombre:Juana Ruiz
Cedula:10988888

Datos Ordenados:
10988888 Juana Ruiz
12924345 Maria Lopez

8.1.4. Arreglos de Estructuras

Los arreglos se expresan como colecciones de variables de un mismo tipo el


cual se diferencian cada una de estas variables a través de un índice. Las estructuras
son variables compuestas, al igual que las variables primitivas, pueden agruparse en
arreglos y ser tratados de la misma forma que colecciones primitivas.
Después de definir un tipo de estructura, podemos declarar arreglos de la forma:

nombre_nuevo_tipo nombre_coleccion[Cantidad_elementos];

Para matrices o arreglos bidimensionales:


nombre_nuevo_tipo nombre_matriz[cant_filas][cant_columnas];

Un ejemplo de ello se muestra en el código siguiente de búsqueda binaria de


una palabra en ingles para mostrar su expresión en español:

#include <iostream.h>
#include <string.h>
// Define la estructura dicc.
struct dicc {
char *ingl ;
char *espa ;
};
// Declaramos un arreglo de estructura
dicc tabla[] = {
"absolute","absoluto",
"append","anexar",
"begin","iniciar,principiar,comenzar",
"close","cerrar",
"do","hacer",
"file","archivo",
"while","mientras"
};

#define NUMESTR (sizeof(tabla)/sizeof(dicc))


int busqbin(char * , dicc * , int) ;

void main()
{ char pal[80];
int nret ;
do{ cout << "\n\nPara finalizar, escriba FIN en : \n";
cout << "\n Palabra en inglés : ";
cin>> pal;
nret = busqbin(pal,tabla,NUMESTR);
if(nret == -1)
{ if(strcmp(strupr(pal),"FIN")==0)
cout << "\n\n!! Finalizado !!\n\n";
else
cout << "\n\nPalabra no registrada\n\n";
}
else cout << "\n" << pal << "=" << tabla[nret].espa ;
}while(strcmp(strupr(pal)," FIN")!="0);" }

// Busca una palabra en ingles


int busqbin(char *pal, dicc *t, int N)
{ int bajo=0, alto=N-1, medio, cond;
while(bajo <=alto)
{ medio=(bajo+ alto) / 2 ;
if((cond==strcmp(pal, t[medio].ingl)) < 0)
alto=medio-1 ;
else if(cond> 0)
bajo = medio + 1 ;
else
return (medio) ;
}
return(-1) ;
}

8.1.5. Apuntadores a estructuras

Al declarar una estructura se le asocia un bloque de memoria. La dirección


inicial del bloque puede asignarse a un apuntador, por lo que la estructura puede
manejarse a través de éste.
Un apuntador a una estructura se declara de la misma forma en que se declara
cualquier apuntador.
Para evaluar el contenido de los datos miembros de la dirección de memoria
asignada al puntero, es necesario un nuevo operador (->), usado de la forma:

puntero_estructura->dato_miembro;

Por ejemplo, si usamos el tipo dicc definido anteriormente, podemos escribir la


línea:
dicc *apd ;

que declara a apd como un apuntador a objetos de tipo dicc.


En este momento, se puede declarar una variable dinámica utilizando el
apuntador apd, como se muestra a continuación:

apd = new dicc;

El acceso a los elementos de la estructura apuntada por apd se logra a través:

apd->ingl;
apd->espa;

En el listado siguiente se muestra el manejo de una lista del tipo de estructura


alumnos.

#include <iostream.h>
#include <conio.h>

void inserta(), elimina(), despliega();


// Define a alumno como un tipo de estructura
struct alumno { char nombre[25];
int calif;
struct alumno *sig;
};
alumno *primero, *actual;
void main()
{ char opcion;
primero = actual = NULL ;
do { clrscr();
cout << "LISTA :"<<endl;
cout << "1.- Ingresar alumno"<<endl;
cout << "2.- Mostrar lista alumnos"<<endl;
cout << "0.- Salir"<<endl;
cout << "Escriba el número de su opción : ";
opcion=getche();
switch(opcion)
{ case '1' : inserta(); break;
case '2' : despliega(); break;
}
} while(opcion !='0');
}

void inserta()
{ alumno *nuevo;
nuevo = new alumno;
if(!nuevo)
{ cout << “No hay memoria”<<endl; getch();
}
else { cout << "Ingrese alumno: "<<endl;
cout << "Nombre: "; gets(nuevo->nombre);
cout << "Calificacion:"; cin>> nuevo->calif;
if(!primero)
{ nuevo->sig = primero;
primero->sig = nuevo;
actual = nuevo;
}
else { nuevo->sig = actual->sig;
actual->sig = nuevo ;
}
}
void despliega()
{ actual=primero;
clrscr();
cout<<”Listado de estudiantes:”<<endl;
if(!actual)
cout << “Lista vacia”<<endl;
while(actual)
{ cout << actual->nombre<< " " << actual->calif<<endl;
actual = actual->sig;
}
getch();
}

8.2. Uniones
Los tipos union comparten muchas de las características sintácticas y
funcionales de los tipos struct, sin embargo existen algunas diferencias; la principal
es que la union permite que solamente uno de sus miembros esté activo a la vez,
mientras que en la estructura todos los miembros están activos al mismo tiempo. Otra
diferencia es que el tamaño de la union es el tamaño del miembro más grande,
mientras que en la estructura su tamaño es la suma de los tamaños de sus miembros.
Las uniones son recomendables cuando se van a manejar variables que pueden
compartir espacio en la memoria, debido a que sus valores van a requerirse en
momentos diferentes.
Por ejemplo, sean tres variables: una de tipo int, otra de tipo float y otra de tipo
double.
Si no van a manejarse simultáneamente, puede definirse un nuevo tipo union
que las abarque de la forma:

union nums {
int x ; // 2 bytes
float y ; // 4 bytes
double z ; // 8 bytes
};

En este momento se cuenta con un nuevo tipo llamado nums, así que podemos
utilizarlo para efectuar la siguiente declaración:

nums varnums ; // Se declara la variable varnums


// que ocupa 8 bytes de memoria.

Se pueden accesar los miembros de varnums a través del operador punto, por
ejemplo:

varnums.x = 10 ; // Se asigna el valor 10 al miembro


// x de la union varnums.
8.3. Clases y objetos

8.3.1. Definición de una clase

Una clase es muy parecido a una estructura, ya que la misma es una estructura
de tipo especial que permite representar características reales de los objetos o cosas,
que no podemos realizar con las estructura, dando una perspectiva real en un
programa.
Una clase representa al conjunto de objetos que comparten una estructura y
comportamientos comunes.
Una clase es un tipo definido por el usuario que describe los atributos y
métodos de los objetos que se crearan a partir de la misma. Los atributos definen el
estado de un determinado objeto y los métodos son las operaciones que definen su
comportamiento.
La definición de una clase consta de dos partes: el nombre de la clase
precedido por la palabra reservada class, y el cuerpo de la clase encerrado entre
llaves y seguido de un punto y coma. Esto es:

class nombre_clase {
cuerpo_de_la_clase
};

El cuerpo de la clase en general consta de modificadores de acceso (public,


protected y private), atributos, mensajes y métodos. Un método implícitamente define
un mensaje (el nombre del método es el mensaje).
Un ejemplo de la definición de una clase, seria la clase punto que puede tomar
la siguiente forma:

class punto { prívate: //acceso privado de los datos miembros


int x,y ; // Miembros Datos o Atributos
public: //acceso publico de los metodos
void fun() { return y; } // Funciones Miembros o Metodos

};

8.3.1.1. Atributos.

Son componentes de un objeto que almacenan datos. También se les denomina


variables miembro. Estos datos pueden ser de tipo primitivo (int, double, char...) o, a
su vez, de otro tipo de objeto (lo que se denomina agregación o composición de
objetos). La idea es que un atributo representa una propiedad determinada de un
objeto.
Un atributo es lo mismo que una variable cualquiera, salvo que como los
atributos se declaran para pertenecer a una clase específica, se dice que todos los
atributos de dicha clase son miembros de la misma. La declaración de los atributos es
exactamente igual que declarar cualquier otra variable. Aunque con la diferencia de
que estos se les adiciona el tipo de acceso, lo cual pueden ser manejados como
variables comunes (public) o mediante el uso de métodos para el caso de las privadas
(prívate).

Ejemplo de la clase punto podemos representarlo como un conjunto de datos


miembros o atributos visibles y no visibles como lo son x, y, color.

class punto {
public: //acceso público de los métodos
char color[25];
prívate: //acceso privado de los datos miembros
int x,y ; // Miembros Datos o Atributos privados
public: //acceso público de los métodos
void fun() { return y; } // Funciones Miembros o Métodos

};

En el ejemplo color representa un dato miembro del objeto que es visible de


manera externa, es decir puede ser usada de la forma tradicional. A diferencia de los
datos miembro x y y los cuales no podrán ser visibles de forma externa lo cual
implicaría que no existe para las funciones y deben ser manejadas a través del uso de
funciones miembros o métodos de la clase.

8.3.1.2. Métodos.

Son un componente de un objeto que lleva a cabo una determinada acción o


tarea con los atributos.
En comparación con la programación tradicional, un método es lo mismo que
una función cualquiera, salvo que como los métodos se declaran para pertenecer a
una clase específica, se dice que todos los métodos de dicha clase son miembros de la
misma. Por lo demás, la declaración y definición de los métodos es exactamente igual
que declarar y definir cualquier otra función.
Las funciones miembro o métodos se incluyen en la definición de una clase, de
dos formas:
 Al definir el método dentro de la definición de la clase.
Por ejemplo:

class punto
{ int x,y ;
public:
int dax() { return x ; } // Define el método dentro de la clase
};

 Declarar el metodo dentro de la definición de la clase y escribir la definición de la


función fuera de la definición de la clase.
Por ejemplo:

class punto
{ int x,y ;
public:
int dax() ; // Declaración del método dentro de la clase
};
int punto::dax() // Definición del método fuera de la clase
{
return x ;
}

En la línea de cabecera de la definición de la función miembro dax() se utiliza


el llamado operador de resolución de ámbito (::). Este operador indica que la función
dax() es una función miembro de la clase punto.
Varias clases diferentes pueden usar los mismos nombres de función. El
compilador sabe cuál función pertenece a cuál clase y esto es posible por el operador
de resolución de ámbito y el nombre de la clase.

8.3.1.3. Tipos de acceso.

Una de las características fundamentales de una clase es ocultar tanta


información como sea posible. Por consiguiente, es necesario imponer ciertas
restricciones en el modo en que se puede manipular una clase y de cómo se puede
utilizar los datos y el código dentro de una clase.
Las funciones miembros o dato miembro de una clase sólo pueden ser llamadas
relativas a un objeto específico. Para llamar a una función miembro o dato miembro
visible desde alguna parte del programa que se encuentre fuera de la clase, se debe
usar el nombre del objeto y el operador de direccionamiento (.) punto.

//Para acceder a un atributo visible de la clase


Nombre_objeto.nombre_atributo;
//Para acceder a un método visible de la clase
Nombre_objeto.nombre_metodo(parámetro_del_metodo);

Ejemplo:

#include <iostream.h>
// Esto define la clase CRender
class CRender { public:
char buffer[256];
void m_Renderizar(const char *cadena);
};
// implementar m_Renderizar() para la clase
void CRender::m_Renderizar(const char *cadena)
{ strcpy(buffer, cadena); //copia la cadena
}

void main ()
{ CRender render1, render2; // crear 2 objetos clase
render1.m_Renderizar("Inicializando el objeto render1");
render2.m_Renderizar("Inicializando el objeto render2");

cout << "buffer en render1: ";


cout << render1.buffer << endl; // tenemos acceso a buffer es público
cout << "buffer en render2: ";
cout << render2.buffer << endl;
}

Por visibilidad se entiende al acto de acceder a los miembros de una clase. En


este sentido, los miembros de una clase pueden ser: públicos, privados y protegidos.
Un miembro público significa que el acceso al mismo puede darse dentro del
interior de la clase, dentro de una subclase, y desde un objeto instanciado de
cualquiera de estas. Por ejemplo, los miembros de la clase CRender son accesibles
dentro de la misma y podrán accederse desde cualquier otra clase que se derive de
CRender, así como desde cualquier objeto instanciado de estas.
Un miembro privado significa que el acceso al mismo puede darse solamente
dentro del interior de la clase que lo posee. Normalmente, el programador creador de
una clase declara a los atributos de la clase como privados y a los métodos como
públicos, esto con la idea de que el usuario de la clase no pueda tener acceso a los
atributos sino es a través de los métodos definidos para el caso.
Un miembro protegido se comporta de manera parecida a un miembro privado,
salvo que estos son accesibles dentro de la clase que lo posee y desde las clases
derivadas, pero no desde los objetos instanciados a raíz de dichas clases.
Por defecto al crear miembros de una clase (atributos o métodos) si no se les
indica el tipo de accesibilidad por defecto, serán privados. Son necesarios métodos
públicos para poder acceder a las variables o métodos que no sean públicos.

class Par { // atributos por defecto privados


double a, b;
public: // métodos de visibilidad publica
double exta() { return a; }
double extb() { return b; }
};

8.3.2. Definición de Objetos

Un objeto representa alguna entidad de la vida real, es decir, alguno de los


objetos que pertenecen a una clasificación con los que podemos interactuar. A través
del estudio de ellos se adquiere el conocimiento necesario para, mediante la
abstracción y la generalización, agruparlos según sus características en conjuntos,
estos conjuntos determinan las clases de objetos a utilizar. Primero existen los
objetos, luego aparecen las clases en función de la solución que estemos buscando.
Ésta es la forma más común de adquirir conocimiento aunque no es la única. En
ocasiones el proceso puede ser a la inversa y comenzar el análisis en una base teórica
abstracta, sustentada por el conocimiento previo que da lugar primeramente a clases
de objetos que satisfagan las necesidades de la solución.
Los objetos tienen características fundamentales que nos permiten conocerlos
mediante la observación, identificación y el estudio posterior de su comportamiento;
estas características son:
 Identidad. Es la propiedad que permite a un objeto diferenciarse de otros.
 Comportamiento. El comportamiento de un objeto está directamente relacionado
con su funcionalidad y determina las operaciones que este puede realizar o a las
que puede responder ante mensajes enviados por otros objetos.
 Estado. El estado de un objeto se refiere al conjunto de los valores de sus
atributos en un instante de tiempo dado.

Se define a un objeto como la instancia de una clase.


Una instancia es un elemento tangible (ocupa memoria durante la ejecución del
programa) generado a partir de una definición de clase. Todos los objetos empleados
en un programa han de pertenecer a una clase determinada. De la forma:

class Tipo_clase { //atributos


//métodos
};

Tipo_clase nombre_objeto_Tipo_clase;

Ejemplo:

class punto
{ int x,y ;
public:
int extx(){ return x ; }
int exty(){ return y ; }
void asigx() { x = a; }
void asigy() { y = b; }
void mostrar(){ cout<<”( ”<<x<<” , ”<<y<<” )” ; }
};
void main()
{ punto a; //Objeto a del tipo punto
a.asigx(10);
a.asigy(15);
a.mostrar();
}

Salida:

( 10 , 15 )
CAPITULO 9

PASO DE MENSAJES: MÉTODOS, PASO DE PARÁMETROS

9.1. Control de acceso a los miembros de una clase.

El control de acceso se suele llamar también ocultación de la implementación.


Incluir funciones dentro de las estructuras (a menudo llamado encapsulación) produce
tipos de dato con características y comportamiento, pero el control de acceso pone
fronteras en esos tipos.
Una clase puede contener partes públicas y partes privadas. Por defecto, todos
los miembros definidos en la clase son privados. Para hacer las partes de una clase
públicas (esto es, accesible desde cualquier parte de su programa) deben declararse
después de la palabra reservada public. Todas las variables o funciones definidas
después de public son accesibles a las restantes funciones del programa.
Especialmente, el resto de su programa accede a un objeto a través de sus funciones y
datos públicos.
Dado que una característica clave de la POO es la ocultación de datos, debe
tener presente que aunque puede tener variables públicas, desde un punto de vista
conceptual debe tratar de limitar o eliminar su uso. En su lugar, debe hacer todos los
datos privados y controlar el acceso a ellos, a través de funciones públicas. El
mecanismo para hacer privados datos o funciones es anteponerle la palabra reservada
private.
Por defecto, una clase es privada, aunque es conveniente especificar la
visibilidad expresamente, por legibilidad y por compatibilidad con versiones
posteriores que pueden recomendar su uso obligatorio. (Figura 9.1)
Existen tres clases de usuarios de una clase:
 La propia clase
 Usuarios genéricos
 Clases derivadas

Cada usuario tiene diferentes privilegios o niveles de acceso. Cada nivel de


privilegio de acceso se asocia con una palabra reservada:
 private. Por defecto todo lo declarado dentro de una clase es privado (private),
y sólo se puede acceder a ella con las funciones miembros declaradas en el
interior de la clase,
 public. Los miembros que se declaran en la región pública (public) se puede
acceder a través de cualquier objeto de la clase de igual modo que se accede a
los miembros de una estructura.
 Protected. Los miembros que se declaran en la región protegida (protected)
sólo se pueden acceder por funciones miembros declaradas dentro de la clase,
por funciones miembro de clases derivadas de esta clase.

Figura 9. 1. Visibilidad de una clase.

Ejemplo de visibilidad de miembros de una clase:


class punto
{ int x, y ;
public:
int color;
void asigx() { x = a; }
void asigy() { y = b; }
void mostrar2(){ cout<<”( ”<<x<<” , ”<<y<<” )” ; }
private:
void mostrar(){ mostrar2(); }
};
void main()
{ punto a; //Objeto a del tipo punto
a.color = 1; //asignación del atributo publico
a.x = 20; //ERROR!!! atributo x de a no es visible
a.asigx(10); //asignación del atributo x de a
// usando un método publico
a.mostrar2(); //ERROR!!! método privado de no visible
a.mostrar(); //llamada al método privado a través de uno publico
}

Es necesaria la existencia de un método público para acceder a atributos y


métodos privados.

9.2. Métodos de una clase.

Un método como ya ha visto es una función miembro la cual cumple un


propósito especifico al igual como una función, solo que esta es parte de la clase con
el propósito adicional de poder interactuar con los atributos de la clase.
Para declarar un método su estructura general consta de dos partes, la
declaración y el cuerpo del método.

Declaracion_ del_ método {


Cuerpo_del_metodo
};
La Declaracion_del_metodo proporciona información sobre su nombre, la
accesibilidad del método, el número de parámetros que recibe, etc.
El Cuerpo_del_metodo contiene el conjunto de sentencias que manipula los
datos de cada objeto.

9.2.1. Pasos de mensajes.

Los métodos de una clase constituyen la lógica de la clase, es decir, contienen


el código que manipula el estado del objeto. Constituyen el mecanismo utilizado para
implementar los mensajes entre objetos. Quiere decir, cuando un objeto se comunica
con otro por un mensaje lo hace por medio de la invocación al método
correspondiente del objeto.
Un paso de mensaje no es más que el mecanismo de invocara un método por
medio de las referencias de la siguiente forma:

referencia.metodo (parametros);

Existen diversos tipos de paso de mensaje según sea la definición de sus


prototipos y que parámetros recibe.

9.2.1.1. Paso de Parámetros.

Son los valores paramétricos que se envía a un método en el momento de su


invocación. Estos permiten diferenciar prototipos o funciones que cumplen un mismo
propósito pero con la diferencia de los datos enviados en el momento de su
invocación son de tipos diferentes y/o con uno o más parámetros.

//sin parámetros o defecto


referencia.metodo ();
//por parámetros
referencia.metodo (parámetro_tipo1);
referencia.metodo (parámetro_tipo2);
referencia.metodo (parametro1, parametro2,..,parametron);

Al declara métodos de una clase con un mismo nombre, se diferenciaran los


métodos por los parámetros que recibe.
Según sean sus parámetros los métodos pueden ser métodos por defecto
aquellos que no reciben datos y métodos paramétricos los que reciben datos
adicionales para su funcionalidad.

9.2.1.2. El puntero this.

En uno de los puntos anteriores comentábamos que un método perteneciente a


una clase tenía acceso a los miembros de su propia clase sin necesidad de pasar como
parámetro el objeto con el que se estaba trabajando. Esto no es tan sencillo, puesto
que es lógico pensar que los atributos (datos) contenidos en la clase son diferentes
para cada objeto de la clase, es decir, se reserva memoria para los miembros de datos,
pero no es lógico que cada objeto ocupe memoria con una copia de los métodos, ya
que replicaríamos mucho código.
En realidad, los objetos de una clase tienen un atributo específico asociado, su
dirección. La dirección del objeto nos permitirá saber que variables debemos
modificar cuando accedemos a un miembro de datos. Esta dirección se pasa como
parámetro (implícito) a todas las funciones miembro de la clase y se llama this.
Si en alguna función miembro queremos utilizar nuestra propia dirección
podemos utilizar el puntero como si lo hubiéramos recibido como parámetro. Por
ejemplo, para retornar el valor de un atributo escribimos:
float empleado::cuanto_cobra () {
return sueldo;
}

Pero también podríamos haber hecho lo siguiente:

float empleado::cuanto_cobra () {
return this->sueldo;
}

Utilizar el puntero dentro de una clase suele ser redundante, aunque a veces es
útil cuando trabajamos con punteros directamente.

9.2.2. Métodos sobrecargados.

Cada método tiene una forma, que son su nombre, el tipo y número de sus
parámetros. Existe una característica para tener dos métodos con el mismo nombre.
Esta característica se denomina sobrecarga de métodos.
El concepto de sobrecarga de métodos se puede aplicar siempre que los
parámetros sean diferentes, bien por su tipo, bien porque el número de parámetros de
un método u otro es diferente. De la forma:

class Nom_clase{
void nom_metodo() {… }
void nom_metodo( tipoa parametrox) { … }
void nom_metodo(tipob parametroy) { … }
void nom_metodo(tipo parámetro1, tipo parametro2) { … }
….
};

Ejempló:

class Articulo { private:


float precio;
public:
void setPrecio() { precio = 3.50;
}
void setPrecio(float newPrecio) {
precio = nuevoPrecio;
}
};

Entonces cuando hacemos un llamado a este método, el compilador hace


referencia al tipo de parámetro. La sobrecarga seria redefinir cualquiera de estos
métodos utilizando los mismos parámetros pero para un proceso distinto.

9.2.3. Constructores y destructores.

Los constructores y los destructores son un tipo de sobrecarga de funciones


miembros especiales.
Podemos clasificar los objetos en cuatro tipos diferentes según la forma en que
se crean:
 Objetos automáticos: son los que se crean al encontrar la declaración del
objeto y se destruyen al salir del ámbito en que se declaran.
 Objetos estáticos: se crean al empezar la ejecución del programa y se
destruyen al terminar la ejecución.
 Objetos dinámicos: son los que se crean empleando el operador new  y se
destruyen con el operador delete.
 Objetos miembro: se crean como miembros de otra clase o como un elemento
de un arreglo.
Los objetos que se crean con el uso explícito del constructor son objetos
automáticos.
9.2.3.1. Constructores

Un constructor especifica la manera en que será creado e inicializado un nuevo


objeto de cierta clase. Los constructores en C++ pueden ser definidos por el usuario
ó generados por el lenguaje. El compilador de C++ invoca automáticamente al
constructor apropiado cada vez que se defina un nuevo objeto.
Esto puede ocurrir en una declaración de datos, cuando se copia un objeto o a
través de la asignación dinámica de memoria a un nuevo objeto por medio del
operador new.
Los constructores se pueden considerar como funciones de inicialización, y
como tales pueden tomar cualquier tipo de parámetros, incluso por defecto. Los
constructores se pueden sobrecargar, por lo que podemos tener muchos constructores
para una misma clase (como ya sabemos, cada constructor debe tomar parámetros
distintos).
Existe un constructor especial que podemos definir o no definir que tiene una
función muy específica: copiar atributos entre objetos de una misma clase. Si no lo
definimos se usa uno por defecto que copia todos los atributos de los objetos, pero si
lo definimos se usa el nuestro.
Este constructor se usa cuando inicializamos un objeto por asignación de otro
objeto.
Podemos definir un constructor por defecto de la forma:

class NomClase { …
NombClase() // Constructor por defecto
{ // inicializaciones de atributos
}
};

Podemos definir un constructor por argumento paramétricos de la forma:


class NomClase { …
NombClase(tipo parametrico)// Constructor por parámetros
{ // inicializaciones por parametros
}
};

Para añadir un constructor a la clase punto escribimos:

class punto
{ int x,y ;
public:
int dax() { return x ; }
int day() { return y ; }
punto() // constructor por defecto
{ x=0; y=0; }
punto(int nx, int ny) // constructor por parámetro
{ x = nx ;
y = ny ;
}
};

En el momento de instanciar o crear un objeto se hace llamado al constructor


inicializando al objeto. De la forma:

void main()
{ punto a; //Inicialización punto a constructor por defecto
punto b(10,10); //Inicialización punto b con constructor por parámetro
}

9.2.3.2. Destructores

Los destructores destruyen a los objetos creados, liberando la memoria


asignada. Pueden ser invocados explícitamente por medio del operador delete.
Podemos definir un destructor usando la negación implícita del constructor
mediante el operador (~) de la forma:

class NomClase { …
~NombClase() // destructor
{ }
};

Siguiendo con el ejemplo de la clase punto:

class punto
{ int x,y ;
public:
int dax() { return x ; }
int day() { return y ; }
punto(int nx, int nx) ;
~punto() ; // Declaracion del Destructor
};

punto::punto(int nx, int nx)


{ x = nx ;
y = ny ;
}
punto::~punto() // Definicion del Destructor
{ delete x ;
delete y ;
}

9.2.4. Métodos estáticos y funciones amigas

Dentro de las peculiaridades de las clases encontramos dos tipos de funciones


especiales: los métodos estáticos y las funciones amigas. Los comentamos separados
del bloque relativo a clases y miembros por su similitud y por la importancia de las
funciones amigas en la sobrecarga de operadores.
Su característica común es que no poseen parámetro implícito this.

9.2.4.1. Métodos estáticos

Al igual que los atributos estáticos mencionados en el punto anterior, las


funciones miembro estáticas son globales para los miembros de la clase y deben ser
definidas fuera del ámbito de la declaración de la clase.
Estos métodos son siempre públicos, se declaren donde se declaren. Al no tener
parámetro this no pueden acceder a los miembros no estáticos de la clase (al menos
directamente, ya que se le podría pasar un puntero al objeto para que modificara lo
que fuera).

9.2.4.2. Funciones amigas (friend)

Son funciones que tienen acceso a los miembros privados de una clase sin ser
miembros de la misma. Se emplean para evitar la ineficiencia que supone el tener que
acceder a los miembros privados de una clase a través de métodos.
Como son funciones independientes de la clase no tienen parámetro this, por lo
que el acceso a objetos de una clase se consigue pasándoles como parámetro una
referencia al objeto (una referencia como tipo implica pasar el objeto sin copiar,
aunque se trata como si fuera el objeto y no un puntero), un puntero o el mismo
objeto. Por la misma razón, no tienen limitación de acceso, ya que se definen fuera de
la clase.
Para hacer amiga de una clase a una función debemos declararla dentro de la
declaración de la clase precedida de la palabra friend, como se muestra en el
siguiente código:

class X { private:
int i;
...

friend int f(X&, int);


// función amiga que toma como parámetros una referencia a
// un objeto del tipo X y un entero y retorna un entero

};

En la definición de la función (que se hace fuera de la clase como las funciones


normales) podremos usar y modificar los miembros privados de la clase amiga sin
ningún problema:

int f(X& objeto, int i) { int j = objeto.i;


objeto.i = i;
return j;
}

Es importante ver que aunque las funciones amigas no pertenecen a la clase se


declaran explícitamente en la misma, por lo que forman parte de la interface de la
clase.
Una función miembro de una clase puede ser amiga de otra:

class X { ...
void f();
...
};

class Y { ...
friend void X::f();
};

Si queremos que todas las funciones de una clase sean amigas de una clase
podemos poner:
class X {
friend class Y;
...
};

En el ejemplo todas las funciones de la clase Y son amigas de la clase X, es


decir, todos los métodos de Y tienen acceso a los miembros privados de X.

9.3. Asignación de objetos

La forma de inicializar un objeto es mediante el uso del operador de asignación


(=). Por ejemplo:

class punto { int x,y ;


public:
int dax() { return x ; }
int day() { return y ; }
punto() { x = 0 ; y=0; }
punto(int nx, int ny) { x = nx ; y = ny ; }
};

void main()
{ punto a, b(10,10);
a = b;
}

Cuando se realiza la operación asignación, ambos objetos existen. En C++ el


operador de asignación, por omisión de la clase seria:

punto& punto::operator=(const punto& pun)


{ x = pun.x;
y = pun.y;
return *this;
}
9.4. Ejemplo de Programa usando clases

Programa que suma números complejos:

#include <iostream.h>
class NumComp
{ private:
double real;
double comp;
public:
NumComp();
NumComp(double);
NumComp(double, double);
NumComp* suma(NumComp&);
double getReal();
double getComp();
};
NumComp::NumComp() {
real = 0;
comp = 0;
}
NumComp::NumComp(double real) {
this->real = real;
comp = 0;
}
NumComp::NumComp(double real, double comp) {
this->real = real;
this->comp = comp;
}
NumComp* NumComp::suma(NumComp& otro) {
return new NumComp(this->real + otro.real, this->comp + otro.comp);
}
double NumComp::getReal() {
return this->real;
}
double NumComp::getComp() {
return this->comp;
}
void main() {
NumComp a(23, 1);
NumComp b(-1, 5);
NumComp* c = a.suma(b);
cout << "La suma es: " << c->getReal() << "+" <<
c>getComp() << "i" << endl;
delete c;
}
CAPITULO 10

HERENCIA

10.1. Herencia.

La herencia es una propiedad esencial de la Programación Orientada a Objetos


que consiste en la creación de nuevas clases a partir de otras ya existentes. Este
término ha sido tomado prestado de la herencia biológica, donde un hijo ciertas
facetas físicas o del comportamiento de sus progenitores.
La herencia en C++ es un mecanismo de abstracción creado para poder facilitar
y mejorar el diseño de las clases de un programa. Con ella se pueden crear nuevas
clases a partir de clases ya hechas, siempre y cuando tengan un tipo de relación
especial.
Las clases que heredan de clases base se denominan derivadas, estas a su vez
pueden ser clases bases para otras clases derivadas. Se establece así una clasificación
jerárquica.
La herencia es una forma de reutilización del software, en la cual se crean
clases nuevas a partir de clases existentes, mediante la absorción de sus atributos y
comportamientos, y enriqueciendo éstos con las capacidades que las clases nuevas
requieren.

10.2. Jerarquía de clases.


Cada nueva clase obtenida mediante herencia se conoce como clase derivada, y
las clases a partir de las cuales se deriva, clases base. Además, cada clase derivada
puede usarse como clase base para obtener una nueva clase derivada. Y cada clase
derivada puede serlo de una o más clases base. En este último caso hablaremos de
derivación múltiple.
Esto nos permite crear una jerarquía de clases tan compleja como sea necesario.
Ese es el principio de la programación orientada a objetos. Esta propiedad nos
permite encapsular diferentes partes de cualquier objeto real o imaginario, y
vincularlo con objetos más elaborados del mismo tipo básico, que heredarán todas sus
características. Lo veremos mejor con un ejemplo.
Un ejemplo muy socorrido es de las personas. Supongamos que nuestra clase
base para clasificar a las personas en función de su profesión sea "Persona". Presta
especial atención a la palabra "clasificar", es el punto de partida para buscar la
solución de cualquier problema que se pretenda resolver usando POO. Lo primero
que debemos hacer es buscar categorías, propiedades comunes y distintas que nos
permitan clasificar los objetos, y crear lo que después serán las clases de nuestro
programa. Es muy importante dedicar el tiempo y atención necesarios a esta tarea, de
ello dependerá la flexibilidad, reutilización y eficacia de nuestro programa.
Ten en cuenta que las jerarquías de clases se usan especialmente en la
resolución de problemas complejos, es difícil que tengas que recurrir a ellas para
resolver problemas sencillos.
Siguiendo con el ejemplo, partiremos de la clase "Persona". (Figura 10.1)
Independientemente de la profesión, todas las personas tienen propiedades comunes,
nombre, fecha de nacimiento, género, estado civil, etc.
Figura 10. 1. Jerarquía de clases para Persona

La siguiente clasificación debe ser menos general, supongamos que dividimos a


todas las personas en dos grandes clases: empleados y estudiantes. Lo importante es
decidir qué propiedades que no hemos incluido en la clase "Persona" son exclusivas
de los empleados y de los estudiantes. Por ejemplo, los ingresos por nómina son
exclusivos de los empleados, la nota media del curso, es exclusiva de los estudiantes.
Una vez hecho eso crearemos dos clases derivadas de Persona: "Empleado" y
"Estudiante".
Haremos una nueva clasificación, ahora de los empleados. Podemos clasificar a
los empleados en ejecutivos y comerciales. De nuevo estableceremos propiedades
exclusivas de cada clase y crearemos dos nuevas clases derivadas de "Empleado":
"Ejecutivo" y "Comercial".
Ahora veremos las ventajas de disponer de una jerarquía completa de clases.
Cada vez que creemos un objeto de cualquier tipo derivado, por ejemplo de tipo
Comercial, estaremos creando en un sólo objeto un Comercial, un Empleado y una
Persona. Nuestro programa puede tratar a ese objeto como si fuera cualquiera de esos
tres tipos. Es decir, nuestro comercial tendrá, además de sus propiedades, como
comercial tambien, su nómina como empleado, y su nombre, edad y género como
persona.
Siempre podremos crear nuevas clases para resolver nuevas situaciones.
Consideremos el caso de que en nuestra clasificación queremos incluir una nueva
clase "Becario", que no es un empleado, ni tampoco un estudiante; la derivaríamos de
Persona. También podemos considerar que un becario es ambas cosas. Sería un
ejemplo de derivación múltiple, podríamos hacer que la clase derivada Becario, lo
fuera de Empleado y Estudiante.
Podemos aplicar procedimientos genéricos a una clase en concreto, por
ejemplo, podemos aplicar una subida general del salario a todos los empleados,
independientemente de su profesión, si hemos diseñado un procedimiento en la clase
Empleado para ello.

10.3. Clases bases y derivadas.

Las clases creadas pueden ser reutilizadas en nuevos programas donde sus
definiciones requieran las mismas funcionalidades. Una clase utilizada para derivar
nuevas clases se denomina clase base (padre, superclase o ascendiente).
Una clase creada de otra clase, donde permita utilizar las funciones necesarias
de la clase (clase base) se denomina clase derivada o subclase. La terminología
supone una clase base o clase padre, y una clase derivada o clase hija. Esta relación
supone un orden de jerarquía simple. A su vez, una clase derivada puede ser utilizada
como una clase base para derivar más clases. Por consiguiente se puede construir
jerarquías de clases, en las que cada clase sirve como padre o raíz de una nueva clase.
Las clases nuevas se denominan clases derivadas, en donde cada clase derivada se
convierte en candidata a clase base para alguna clase futura.
Cada clase derivada se debe referir a una clase base declarada anteriormente. La
declaración de una clase derivada tiene la siguiente sintaxis:

Class clase_derivada: <especificadores_de_acceso> clase_base


{
...
};
El operador (:) nos indica que se herede .Los especificadores de acceso pueden
ser: public, protected o private.

10.3.1. Clases de derivación

Los especificadores de acceso a las clases base definen los posibles tipos de
derivación: public, protected y private. El tipo de acceso a la clase base específica
cómo recibirá la clase derivada a los miembros de la clase base. Si no se especifica un
acceso a la clase base, C++ supone que su tipo de herencia es privado.
 Derivación pública (public). Todos los miembros public y protected de la
clase base son accesibles en la clase derivada, mientras que los miembros
private de la clase base son siempre inaccesibles en la clase derivada.

#include <iostream.h>
class base { int i, j;
public:
void set(int a, int b) { i = a; j = b; }
void mostrar() { cout << i << " " << j << "\n"; }
};

class derivada : public base { int k;


public:
derivada(int x) { k = x; }
void mostrar_k()
{ cout << k << "\n"; }
};
void main()
{
derivada obj(3);
obj.set(1, 2); // accesar a miembro de base
obj.mostrar(); // accesar a miembro de base
obj.mostrar_k(); // usa miembro de la clase derivada
};
 Derivación privada (private). Todos los miembros de la clase base se
comportan como miembros privados de la clase derivada. Esto significa que
los miembros public y protected de la clase base no son accesibles más que
por las funciones miembro de la clase derivada. Los miembros privados de la
clase siguen siendo inaccesibles desde la clase derivada.

#include <iostream.h>
class base { int i, j;
public:
void set(int a, int b) { i = a; j = b; }
void mostrar() { cout << i << " " << j << "\n"; }
};
// Miembros públicos de 'base' son privados en 'derivada'
class derivada : privatec base { int k;
public:
derivada(int x) { k = x; }
void mostrar_k()
{ cout << k << "\n"; }
};
void main()
{
derivada obj(3);
obj.set(1, 2); // Error!!!, no se puede acceder a set()
obj.mostrar(); // Error!!!, no se puede acceder a set()
obj.mostrar_k(); // usa miembro de la clase derivada
};

 Derivación protegida (protected). Todos los miembros public y protected de


la clase base se comportan como miembros protected de la clase derivada.
Estos miembros no son, pues, accesibles al programa exterior, pero las clases
que se deriven a continuación podrán acceder normalmente a estos miembros
(datos o funciones).

Usando miembros protegidos:


#include <iostream.h>
class base { protected:
int i, j; // privados base, pero accesibles a derivada
public:
void set(int a, int b) { i = a; j = b; }
void mostrar() { cout << i << " " << j << "\n"; }
};

class derivada : public base { int k;


public:
// derivada puede accesar en base a 'j' e 'i'
void set_k() { k = i * j; }
void mostrar_k()
{ cout << k << "\n"; }
};
void main()
{
derivada obj(3);
obj.set(1, 2); // Ok, conocido por derivada
obj.mostrar(); // Ok, conocido por derivada
obj.set_k();
obj.mostrar_k();
};

Usando protected para clase base:

#include <iostream.h>
class base { int i;
protected:
int j;
public:
int k;
void seti(int a) { i = a; }
int geti() { return i; }
};
// Heredar 'base' como protected.
class derivada : protected base
{ public:
void setj(int a) { j = a; }; // j es protected aqui.
void setk(int a) // k es tambien protected.
{ k = a; };
int getj() { return j; }
int getk() { return k; }
};
void main()
{
derivada obj;

/* La proxima linea es ilegal porque seti() es


un miembro protegido de derivada, lo cual lo
hace inaccesible fuera de derivada. */
// obj.seti(10);

// cout << obj.geti(); // ilegal -- geti() es protected.


// obj.k = 10; // tambien ilegal porque k es protected.
// estas declaraciones son correctas
obj.setk(10);
cout << obj.getk() << " ";
obj.setj(12);
cout << obj.getj() << " ";
}

10.4. Tipos de herencia.

En C++ existen los tipos de herencia: simple y múltiple.

10.4.1. Herencia simple

La herencia simple es aquella en la que cada clase derivada hereda de una


única clase. Cada clase tiene un solo ascendiente, y puede tener muchos
descendientes.
En la herencia, las clases derivadas "heredan" los datos y las funciones
miembro de las clases base, pudiendo las clases derivadas redefinir estos
comportamientos y añadir comportamientos nuevos propios de las clases derivadas.
Para no romper el principio de encapsulamiento (ocultar datos cuyo conocimiento no
es necesario para el uso de las clases), se proporciona un nuevo modo de visibilidad
de los datos/funciones: "protected". Cualquier cosa que tenga visibilidad protected se
comportará como pública en la clase Base y en las que componen la jerarquía de
herencia, y como privada en las clases que no sean de la jerarquía de la herencia.
Antes de utilizar la herencia, nos tenemos que hacer una pregunta, y si tiene
sentido, podemos intentar usar esta jerarquía: Si la frase <claseB> ES-UN <claseA>
tiene sentido, entonces estamos ante un posible caso de herencia donde clase A será la
clase base y clase B la derivada.
Ejemplo: clases Barco, Acorazado, Carguero, etc. un Acorazado ES-UN Barco,
un Carguero ES-UN Barco, un Trasatlántico ES-UN Barco, etc.
En este ejemplo tendríamos las cosas generales de un Barco (en C++).

class Barco {
protected:
char *nombre;
float peso;
public:
//Constructores y demás funciones básicas de barco
};

y ahora las características de las clases derivadas, podrían (a la vez que heredan
las de barco) añadir cosas propias del subtipo de barco que vamos a crear, por
ejemplo:

class Carguero: public Barco {


private:
float carga;
//El resto de cosas
};

class Acorazado: public Barco {


private:
int numeroArmas;
int Soldados;
// El resto de cosas
};

Como vimos existen 3 clases de derivación o herencia que se diferencian en el


modo de manejar la visibilidad de los componentes de la clase resultante:
Herencia publica (class Derivada: public Base ): Con este tipo de herencia se
respetan los comportamientos originales de las visibilidades de la clase Base en la
clase Derivada.
Herencia privada (clase Derivada: private Base): Con este tipo de herencia
todo componente de la clase Base, será privado en la clase Derivada (las propiedades
heredadas serán privadas aunque estas sean públicas en la clase Base)
Herencia protegida (clase Derivada: protected Base): Con este tipo de
herencia, todo componente publico y protegido de la clase Base, será protegido en la
clase Derivada, y los componentes privados, siguen siendo privados.

10.4.2. Herencia múltiple.

La herencia múltiple es aquella en la cual una clase derivada tiene más de una
clase base.
La herencia múltiple es el mecanismo que permite al programador hacer clases
derivadas a partir, no de una sola clase base, sino de varias. Para entender esto mejor,
pongamos un ejemplo: Cuando ves a quien te atiende en una tienda, como persona
que es, podrás suponer que puede hablar, comer, andar, pero, por otro lado, como
empleado que es, también podrás suponer que tiene un jefe, que puede cobrarte
dinero por la compra, que puede devolverte el cambio, etc.
Si esto lo trasladamos a la programación sería herencia múltiple (clase
empleado_tienda):
class Persona {
...
Hablar();
Caminar();
...
};

class Empleado {
Persona jefe;
int sueldo;
Cobrar();
...
};
class empleado_tienda: public Persona, Empleado {
...
AlmacenarStock();
ComprobarExistencias();
...
};

Por tanto, es posible utilizar más de una clase para que otra herede sus
características.

class clase_derivada: <especificadores_de_acceso1> clase_base1,


<especificadores_de_acceso2> clase_base2, … ,
<especificadores_de_acceson> clase_basen
{
...
};

Una declaración multiple seria la clase derivada D:

class B { ... };
class C1 : public B { ... };
class C2 : public B { ... };
class D : public C1, C2 { ... };
Ejemplo Programa banco usando herencia multiple.

#include<iostream.h>
#include<cstdlib.h>
#include<conio.h>
class valor
{ protected:
double cap,cap1,cap2,monto,capital;
public:
valor();
};
valor::valor()
{ capital=capital;
}
class ctacte
{ protected:
string num_cuenta;
public:
ctacte();
};
ctacte::ctacte()
{ num_cuenta=num_cuenta;
}
class accion
{ protected:
char operacion;
public:
accion();
};
accion::accion()
{ operacion=operacion;
}
class propiedad : public valor,ctacte,accion
{ protected:
char rpta;
double monto,t,deposito,retiro;
public:
propiedad();
void calcular();
void mostrar();
};
propiedad::propiedad()
{ valor::capital=capital;
ctacte::num_cuenta=num_cuenta;
accion::operacion=operacion;
}
void propiedad::calcular()
{ cout<<"nn";
cout<<"ttt INGRESAR NUMERO DE CUENTA: "<<endl;
cin>>num_cuenta;
system("cls");
cout<<"nn";
cout<<"ttt INGRESAR CAPITAL: "<<endl;
cin>>capital;
system("cls");
t=capital;
deposito=0;
retiro=0;

do
{
cout<<"nn"
<<"ttt QUE OPERACION DESEA REALIZARn"
<<"ttt === ========= ===== ========nnn"
<<"tþ (1) DEPOSITO "<<"nn"
<<"tþ (2) RETIRO "<<"nn";
cin>>operacion;
switch(operacion)
{
case'1': { cout<<"nn";
cout<<"tttINGRESAR DEPOSITO"<<endl;
cin>>cap1;
deposito=deposito+cap1;
monto=capital+cap1;
capital=monto;
}break;
case'2': { cout<<"nn";
cout<<"tttINGRESAR RETIRO"<<endl;
cin>>cap2;
if (cap2 >capital)
cout<<"No puede retirar mas dinero"<< endl;
else{
retiro=retiro+cap2;
monto=capital-cap2;
capital=monto;}
}break;
}
system("cls");
cout<<"DESEA CONTINUAR S/N : "<<endl;
cin>>rpta;
}while(rpta=='S'||rpta=='s');
system("cls");

}
void propiedad::mostrar()
{
calcular();
cout<<"nn"
<<"tttMOSTRANDO DATOS LA CUENTA BANCARIAn"
<<"ttt========= ===== == ====== ========nnn"
<<"tþ Su capital inicial es : $ "<<t<<"nn"
<<"tþ El deposito total es : $ "<<deposito<<"nn"
<<"tþ El retiro total es : $ "<<retiro<<"nn"
<<"tþ Su actual capital es : $ "<<capital<<"nn";
}
void main()
{
propiedad a;
a.mostrar();
getch();
}

S-ar putea să vă placă și