Descargar como docx, pdf o txt
Descargar como docx, pdf o txt
Está en la página 1de 616

0.1.1 ¿Dónde comenzar?

Toda actividad creativa necesita herramientas, y la programación


no es una excepción. En su forma más simple (por no decir la más
primitiva), la programación requiere una hoja de papel y un lápiz.
Por supuesto, no vamos a practicar una programación como esta:
sería una práctica bastante buena para escribir código sin formato,
pero es imposible de ejecutar y poco práctico. Como ejecutar el
código es el único método para descubrir si es correcto, la
programación requiere una computadora equipada con algunas
herramientas adicionales.
En este documento, le mostraremos algunas formas de usar su
computadora como estación de trabajo de desarrollador. Sin
embargo, no olvide que hay muchos factores que afectan las
acciones que deben (o no deben) realizarse: plataforma de
hardware, sistema operativo, versión del sistema operativo, etc.
No conocemos su sistema, por lo que nuestra guía es general. Le
mostraremos la dirección: tendrá que buscar soluciones usted
mismo.
Desafortunadamente, todas nuestras sugerencias requieren
acceso a Internet, ya sea permanente o temporal. Debe descargar
prácticamente todos los archivos de instalación desde los sitios de
los productos. Otras herramientas funcionan solo en línea. Lo
sentimos, no hay soluciones alternativas.

Por cierto, puede ejecutar con éxito todos nuestros programas y


ejemplos sin el uso de ningún entorno especializado, solo
mediante el uso de un editor de texto estándar y herramientas de
compilación de línea de comandos.
Sin embargo, no lo recomendamos, y es por eso que vamos a
utilizar algunas aplicaciones especiales que nos permitirán utilizar
una variedad de herramientas en un solo lugar.
En general, hay dos formas de asesorarle:

 usando IDE instalado localmente


 utilizando herramientas en línea

1 – By Gerson
0.1.2 ¿Qué es IDE?
IDE ( Integrated Development Environment ) es una
aplicación de software que generalmente consta de un editor de
código, un compilador, un depurador y un generador de interfaz
gráfica de usuario (GUI).
La programación con un IDE tiene muchas ventajas: obtienes un
kit de herramientas que contiene todo lo que puedas
necesitar. Los programadores reales también suelen usar un
IDE. Un IDE le brinda un cómodo escritorio equipado con todos los
medios, suministros y ayudas.
También hay algunas desventajas. Los escritorios cómodos
generalmente pesan mucho. También los IDEs. Es posible que
consuman muchos recursos y, francamente, probablemente no
necesite la mayoría de las funciones que pueden realizar.
El uso de herramientas en línea le permite escribir, almacenar y
ejecutar su código sin instalar nada. Imagínelo como un IDE
simplificado accesible de forma remota a través de Internet. Eso
significa que necesita dos cosas: un navegador de Internet y
acceso a Internet.
Si puede probar ambos enfoques, elija el que sea más conveniente
para usted. Si no puede, elija el que puede usar.

2 – By Gerson
0.1.3 Elija su IDE

Hay muchos IDE en el mercado, tanto gratuitos como no


gratuitos. Para tener una idea aproximada de cuán grande es la
lista de entornos de desarrollo integrados para el lenguaje C ++,
puede visitar Wikipedia .
Escribimos y probamos todos nuestros ejemplos con NetBeans. No
significa que pensemos que NetBeans es el mejor. Es posible que
otros productos estén más en línea con sus gustos y hábitos, por
lo que no necesita seguir nuestro camino. Siéntase libre de tomar
sus propias decisiones. Sin embargo, tenga en cuenta que muy
pocos de los ejercicios de este curso pueden estar preconfigurados
para NetBeans. Por este motivo, debe recordar que algunos
elementos prácticos del curso podrían no funcionar en otros IDE
de la forma en que pretendíamos que funcionaran.
Queremos mostrarle 5 IDEs de muestra. Nuevamente, esto no
significa que pensemos que son mejores que los demás: pueden
ser más populares que muchos otros, o en realidad nos gustan por
varias razones (no todas las razones técnicas). Aquí están:

Microsoft © Visual Studio Express ®


Un entorno de desarrollo de plataforma única (ahora tiene incluso
soporte multiplataforma) diseñado especialmente para construir
programas C ++, tanto bajo como para el sistema operativo MS
Windows.

 sitio de inicio: https://www.visualstudio.com

 descargas: https://www.visualstudio.com/products/free-
developer-offers-vs.aspx

 licencia: está disponible para descarga la versión gratuita


patentada pero limitada denominada Visual Studio
Community; Se requiere registro.
3 – By Gerson
Eclipse
Entorno de desarrollo multiplataforma diseñado especialmente
para Java. Programación en C ++ posible sin configuración
adicional (versión de C ++ dedicada disponible para descargar).

 sitio de inicio: https://eclipse.org

 descargas: https://www.eclipse.org/downloads

 licencia: Licencia pública de Eclipse (gratuita y abierta)

NetBeans
Entorno de desarrollo multiplataforma diseñado especialmente
para Java. Programación en C ++ posible sin configuración
adicional (versión de C ++ dedicada disponible para descargar).

 sitio de inicio: https://netbeans.org

 descargas: https://netbeans.org/downloads/index.html

 licencia: Licencia común de desarrollo y distribución o Licencia


pública GNU versión 2 (gratuita y abierta)

Código :: Bloques
Entorno de desarrollo multiplataforma diseñado para
programación C / C ++. El instalador predeterminado de Windows
no incluye el compilador de C ++; en su lugar, use el que contiene
"mingw-setup" dentro del nombre del archivo.

 sitio de inicio: http://www.codeblocks.org

 descargas: http://www.codeblocks.org/downloads/binaries

 licencia: Licencia pública GNU versión 3 (gratuita y abierta)

XCode

4 – By Gerson
Entorno de desarrollo de plataforma única diseñado especialmente
para crear aplicaciones para sistemas operativos diseñados por
Apple Inc. Programación en C ++ totalmente disponible.

 sitio de inicio: https://developer.apple.com/xcode

 descargas: https://developer.apple.com/xcode/download

 licencia: propietaria pero gratuita para usuarios de Max OS


X; integrado con OS X y preinstalado.

Desafortunadamente, no podemos proporcionar ningún soporte en


la instalación y / o uso de ningún IDE, ya sea para los que
mencionamos y los que no mencionamos. Si tiene problemas,
busque ayuda del proveedor de su software o (recomendado) de
otros usuarios del producto en particular. Hay muchas fuentes
disponibles: preguntas frecuentes, foros, mesas de ayuda,
comunidades, bases de conocimiento, etc. Es poco probable que
su problema no le haya sucedido a otra persona, y si lo ha hecho,
es muy probable que se haya resuelto. Busque soluciones y las
encontrará.
Si es un usuario de Linux , intente utilizar las herramientas de
su sistema principal para descargar e instalar un IDE. Si utiliza
cualquier otro sistema operativo, busque un paquete de
instalación nativo completo.

5 – By Gerson
0.1.4 Herramientas en línea: ideone

Para comenzar con la programación, no tiene que instalar


nada. Si está leyendo esto, es muy probable que tenga un
navegador de Internet que funcione y una conexión a Internet
activa. Esto significa que puede usar uno de los siguientes sitios y
comenzar a programar de inmediato, así como así.

La primera es una herramienta llamada ideone disponible


en http://ideone.com . Aunque no necesita registrarse para
comenzar su trabajo, le sugerimos que lo haga, ya que habilitará
algunas características adicionales valiosas. Registrarse es fácil,
ciertamente no es más difícil que registrarse en el sitio web del
Instituto C ++ . También puede usar su cuenta de Facebook
para iniciar sesión en ideone : hará que todo el proceso sea aún
más rápido y más conveniente.

Después de iniciar sesión, tendrá que personalizar un poco, y dos


cosas son esenciales: cambie su lenguaje de programación
predeterminado a "C ++" (no se olvide de hacer eso) y luego
habilite el resaltado de sintaxis: hará que sea más fácil Haz tu
trabajo.

Para probar el entorno ideone , vaya a la pestaña "nuevo código"


y simplemente copie y pegue el siguiente texto en el campo del
código fuente:

#include <iostream>
usando el espacio de nombres estándar;
int main (void) {cout << "Está funcionando" << endl; }

Luego haga clic en el botón "Ideone it". El texto que dice "Está
funcionando" debería aparecer casi de inmediato en el campo
estándar; esto significa que su código fuente ha sido compilado y
ejecutado felizmente.

¿Fácil? ¡Fácil! Pero ... hay un inconveniente: no puedes enviar más


de 1000 envíos al mes. Sin embargo, no creemos que eso
realmente te afecte.

6 – By Gerson
Algo importante: si su programa lee cualquier dato de un usuario,
deberá preparar los datos dentro del campo estándar antes de
ejecutar el código (normalmente el proceso de lectura de entrada
es interactivo).

Visite http://ideone.com/faq para más detalles.

0.1.5 Herramientas en línea: cpp.sh

Otra herramienta en línea con una funcionalidad muy similar es


el shell C ++ , disponible en http://cpp.sh . No necesita
registrarse y su funcionamiento principal es muy similar al
de ideone , aunque el shell C ++ no ofrece tantas funciones útiles
como ideone (por ejemplo, no puede guardar ni publicar su
código). Tampoco incluye ningún soporte (para decirle la verdad,
es tan simple de usar que en realidad no necesita ninguna ayuda).

Ahora es el momento de comenzar a aprender algo de


programación real.

7 – By Gerson
1.1.1 Lenguaje natural vs. lenguaje de programación

Estamos aquí para mostrarle qué es el lenguaje C ++ y para qué


lo podemos usar. Consideremos por un momento qué es el
lenguaje en sí, no solo el lenguaje C ++, sino también cualquier
lenguaje que la gente use. Trataremos de no usar definiciones de
sonido científicas y trataremos de hablar informalmente. Podemos
decir que un lenguaje es una herramienta para expresar y
registrar pensamientos humanos . Realmente nos gusta esta
definición. En otras palabras, es un mecanismo conocido por
nosotros y nuestros socios, que nos permite comprender y ser
entendidos. Utilizamos el lenguaje para hablar, escribir, leer,
escuchar y pensar.
Al menos un idioma nos acompaña durante toda nuestra vida: es
nuestro idioma nativo (nuestra lengua materna), que aprendemos
casi inconscientemente desde el principio. Muchos de nosotros
también aprenderemos otros idiomas, principalmente como
resultado de una decisión consciente, forzada por las condiciones
8 – By Gerson
sociales o las necesidades comerciales. Los idiomas que usamos
para comunicarnos con otras personas se llaman idiomas
naturales. Fueron creados durante muchos siglos y aún están
sujetos a cambios. Si ignoramos los idiomas que se han creado
artificialmente, como el esperanto o incluso el quenya (el lenguaje
utilizado por los elfos en el mundo de Tolkien), su desarrollo es
casi independiente de sus hablantes y evoluciona naturalmente,
de una manera que nos da poco o ningún control sobre eso.
Sin embargo, hay idiomas cuya creación y desarrollo fueron (y a
menudo continúan siendo) dictados por algunas necesidades
específicas, y cuyo desarrollo está completamente sujeto al
control de grupos muy amplios de personas, como comités
internacionales o grupos de trabajo. La forma de estos idiomas
está definida por los estándares internacionales, y aunque muchas
personas puedan entenderlos, el intercambio de pensamientos
entre los seres humanos no es su prioridad.
Tales lenguajes son, entre otros, lenguajes de
programación . Probablemente ya estés familiarizado con este
concepto. Un lenguaje de programación se define por un cierto
conjunto de reglas rígidas, mucho más inflexibles que cualquier
lenguaje natural. Por ejemplo, estas reglas determinan qué
símbolos (letras, dígitos, signos de puntuación, etc.) podrían
usarse en el idioma. Esta parte de la definición del lenguaje se
llama léxico . Otro conjunto de reglas determina las formas
apropiadas de clasificar los símbolos: esta es la sintaxis del
lenguaje. También debemos ser capaces de reconocer el
significado de cada declaración expresada en el idioma dado, y
esto es lo que llamamos semántica . Cualquier programa que
escribamos debe estar libre de errores de estas tres
maneras:léxica, sintáctica y semánticamente , de lo contrario,
el programa no se ejecutará o producirá resultados
inaceptables. Puede estar seguro de que encontrará todos estos
errores, ya que errar es humano y son estos humanos falibles
quienes escriben los programas de computadora.
El poder expresivo de los lenguajes de programación es mucho,
mucho más débil que los lenguajes naturales. No podemos
(aunque podemos tratar de) usar dicho lenguaje para expresar
emociones humanas, y es difícil imaginar una declaración de amor
codificada dentro de un lenguaje de programación. Esto se debe a
que el mensaje incrustado dentro de un programa de computadora

9 – By Gerson
no está destinado a un ser humano, sino a una máquina.
Quizás se pregunte por qué necesitamos usar un lenguaje de
programación, y esa es una buena pregunta. Tratemos de
responderlo.

1.1.2 Lenguaje natural vs. lenguaje de programación

Una computadora, incluso la más sofisticada técnicamente, carece


incluso de un rastro de inteligencia. Se podría decir que es como
un perro bien entrenado: solo responde a
un conjunto predeterminado de comandos conocidos . Y a
veces, como un perro, simplemente se apaga y se niega a hacer
lo que se le dice. Los comandos reconocidos son muy
simples. Podemos imaginar que la computadora responde a
órdenes como "tomar ese número, agregarlo a otro y guardar el
resultado".
Un conjunto completo de comandos conocidos se denomina lista
de instrucciones , a veces abreviado como IL . Los diferentes
tipos de computadoras pueden variar según el tamaño de sus IL y
las instrucciones en sí pueden ser completamente diferentes en
diferentes modelos.
El IL es de hecho un alfabeto que se conoce comúnmente
como lenguaje de máquina . Este es el lenguaje más simple y
primario que podemos usar para dar comandos a nuestra
computadora. Podemos decir que es la lengua materna de la
computadora.
La programación de computadora es un acto de componer los
comandos seleccionados (instrucciones) en un orden que causará
el efecto deseado. El efecto en sí puede ser diferente en todos los
casos: depende de la intención, la imaginación, el conocimiento y
la experiencia del programador.
Es posible (y esto se hace a menudo en la práctica) que un
programa de computadora se codifique directamente en lenguaje
de máquina usando instrucciones elementales (órdenes). Este tipo
de programación es tediosa, consume mucho tiempo y
es altamente susceptible a los errores de un
programador . Durante las primeras etapas de la tecnología

10 – By Gerson
informática, era el único método de programación disponible, y
rápidamente reveló sus serias fallas.
La programación en lenguaje de máquina requiere un
conocimiento completo del diseño de hardware de una
computadora y su estructura interna. Esto también significa que
reemplazar la computadora con una que difiera en diseño de su
predecesora puede hacer que todo el conocimiento del
programador sea inutilizable. Además, los viejos programas
podrían ser completamente inútiles si la nueva computadora usara
una IL diferente. Por lo tanto, un programa escrito para un tipo
específico de computadora podría ser completamente inútil para
otras computadoras y viceversa. En segundo lugar, los programas
escritos en lenguaje máquina son muy difíciles de entender
para los humanos , incluidos los programadores
experimentados. También es el caso de que desarrollar un
programa en lenguaje de máquina lleva mucho tiempo y es muy
costoso y engorroso.
Todas estas circunstancias conducen a la necesidad de algún tipo
de puente entre el lenguaje de las personas (lenguaje natural) y
el lenguaje de computadora (lenguaje de máquina). Ese puente
también es un lenguaje, un lenguaje común intermedio para
humanos y computadoras que trabajan juntos. Tal lenguaje a
menudo se llama lenguaje de programación de alto nivel .
Un lenguaje como este es al menos algo similar a un lenguaje
natural: utiliza símbolos, palabras y convenciones legibles para los
humanos. Este lenguaje permite a los humanos expresar
comandos complejos para computadoras.
Tal vez se pregunte cómo convencer a una computadora para que
comprenda los programas escritos de esta manera. Por más que
lo intente, alentar a la computadora no es suficiente, pero puede
traducir su programa al lenguaje de máquina. Además, la
traducción puede ser realizada por una computadora, haciendo
que todo el proceso sea rápido y eficiente.
¿Qué tan genial es eso? No necesita aprender muchos lenguajes
de máquina diferentes, solo necesita conocer un lenguaje de
programación de alto nivel. Si hay un traductor diseñado para una
computadora específica, puede ejecutar su programa sin ningún
problema. En otras palabras, los programas escritos en lenguajes
de alto nivel pueden traducirse a cualquier número de lenguajes

11 – By Gerson
de máquina diferentes y, por lo tanto, pueden usarse en muchas
computadoras diferentes. Esto se llama portabilidad .

1.1.3 Lenguaje natural vs. lenguaje de programación

La traducción a la que nos referimos está hecha por un programa


informático especializado llamado compilador . El proceso de
traducción de un lenguaje de alto nivel al lenguaje de máquina se
llama compilación .
Ahora volvamos a temas más interesantes relacionados con el
proceso de creación de un nuevo programa. Ya sabemos que
nuestra tarea principal es escribir el programa de acuerdo con las
reglas del lenguaje de programación elegido. Este programa (que
de hecho es solo un texto) se llama código fuente , o
simplemente fuente, mientras que el archivo que contiene la
fuente se llama archivo fuente .
Para escribir el código fuente, necesitará un editor de texto que le
permita manipular el texto sin ninguna información de formato
(por este motivo, Microsoft Word no es una buena opción; el Bloc
de notas es mejor). Este código se coloca en un archivo y el
nombre del archivo debe implicar su contenido. Por ejemplo, es
común que un archivo que contiene el código fuente en el lenguaje
C ++ tenga su nombre terminando con el sufijo .cpp , por lo que
si escribe un programa de computadora y decide nombrarlo como
proggie, sería una buena idea poner el código fuente en un archivo
llamado proggie.cpp. Algunas plataformas pueden preferir otros
sufijos como cc , cp , cxx , c ++ o incluso C(tenga en cuenta que
esta es una letra mayúscula). Consulte la documentación de su
compilador para más detalles.
Luego, su código fuente necesita ser compilado. Para hacer esto,
necesita ejecutar un compilador apropiado e indicarle dónde
almacenó el código fuente que desea traducir al lenguaje de
máquina. El compilador lee su código, realiza un análisis complejo
y luego determina si cometió o no algún error durante el proceso
de codificación. Si es así, te lo dirá. Esto no es para hacerte sentir
mal: estos análisis son muy perspicaces y útiles. Pero recuerde
que están hechos por una máquina, no por un humano, y no debe
esperar demasiado de ellos. De acuerdo, si tu error fue que
12 – By Gerson
intentaste sumar dos números usando # en lugar de + , el
compilador te informará amablemente de tu error. Sin embargo,
si escribiste , en lugar de +, el compilador no puede adivinar que
su intención era sumar dos números, en lugar de restarlos. El
compilador no pensará por ti . Pero eso también está bien. Si
el compilador hiciera todo, los desarrolladores no tendrían
trabajos.
Si el compilador no encuentra ningún error en su fuente, el
resultado será un archivo que contiene su programa traducido al
lenguaje de máquina. Ese archivo se llama comúnmente
un archivo ejecutable . El nombre del archivo depende del
compilador que use y del sistema operativo con el que esté
trabajando. Por ejemplo, la mayoría de los compiladores
diseñados para el sistema Unix / Linux crean un archivo de salida
llamado "a.out" de forma predeterminada. Los compiladores
diseñados para su uso en MS Windows® pueden asignar el mismo
nombre a este archivo que el archivo de origen, mientras solo
cambian el sufijo de .cpp a .exe .
Por supuesto, todo el proceso es en realidad un poco más
complicado. Su código fuente puede ser enorme y dividido entre
varias o incluso docenas de archivos fuente. También puede ser
que el programa no haya sido escrito solo por usted, sino por todo
un equipo, en cuyo caso la división de las fuentes en múltiples
archivos es simplemente imprescindible. En este caso, el proceso
de compilación se divide en dos fases: una compilación de su
fuente para traducirlo al lenguaje de máquina y unir (o pegar) su
código ejecutable con el código ejecutable derivado de los otros
desarrolladores en un único y unificado producto. La fase de
"pegar" los diferentes códigos ejecutables se conoce comúnmente
como vinculación , mientras que el programa que lleva a cabo el
proceso se denomina vinculador .
¿Cuál es el uso más común de C ++? Es el llamado lenguaje de
programación de objetos, es decir, adecuado principalmente para
aplicaciones grandes y complejas, especialmente aquellas que
trabajan en entornos gráficos. Conocer el lenguaje C ++ es muy
útil si quieres aprender C # o Java. El aparato conceptual utilizado
por el lenguaje C ++ es común para todos los lenguajes de
programación de objetos, ya que C ++ es uno de los lenguajes
más antiguos de esa clase.

13 – By Gerson
1.1.4 Lecturas adicionales

Si te tomas en serio el aprendizaje del lenguaje “C ++” (y no


esperamos menos de ti), ciertamente no terminarás tu educación
con este increíble curso.
Entre los cientos de libros escritos en el lenguaje C ++, hay uno
que recomendamos en particular. El libro se ha publicado docenas
de veces en todo el mundo y está disponible en más de 20 idiomas
(naturales) diferentes. El título del libro es simplemente " El
lenguaje de programación C ++ ". Fue el primer libro en
describir el lenguaje de programación C ++, escrito por el creador
del lenguaje, Bjarne Stroustrup. Stroustrup comenzó a desarrollar
C ++ en 1979 y actualmente, como era de esperar, se considera
la autoridad más importante en este campo. Puedes comprar el
libro aquí .
Otro libro que vale la pena leer es " Thinking in C ++ " de Bruce
Eckel. Este libro está disponible para descargar de forma
gratuita. Lo puedes encontrar aquí .
Uno de los libros publicados recientemente que disfrutamos
particularmente es uno escrito por Alex Allain, llamado " Jumping
into C ++ ". Es un gran libro para principiantes, que presenta una
guía paso a paso para convertirse en un programador de C
++. Puedes pedir el libro aquí .
Por último, pero no menos importante, para aquellos que buscan
mejorar sus programas y aprender más sobre las mejores
prácticas en C ++, hay un gran libro de Scott Meyers, " C ++
efectivo ". Puede ordenar el libro aquí .
Para obtener una lista más completa de buenos libros de C ++,
puede consultar este hilo en stackoverflow.com.
Si conoce otros buenos libros que vale la pena recomendar, no
dude en contribuir a este hilo en nuestra página de Facebook.
Una vez que sea un programador competente, es posible que
desee tener una fuente de conocimiento a través del cual pueda
encontrar rápidamente las respuestas a las preguntas
emergentes, o simplemente llenar los vacíos en su memoria. En
lugar de un manual, querrá un libro que describa brevemente los
estándares del idioma: todo lo que es realmente importante y
14 – By Gerson
nada más. Necesita lo que se llama un informe (mejorado y
actualizado permanentemente), publicado por el comité de
estandarización ISO . Puede encontrar la versión más reciente
del estándar en https://isocpp.org/std .
Pero bueno, es demasiado pronto para eso. Mira eso cuando
termines este curso. No antes.

Inicio
1.2.1 Su primer programa (1)

Ahora le mostraremos un programa muy simple (y, al mismo


tiempo, completamente inútil) escrito en lenguaje C
++. Usaremos este ejemplo para presentarle algunas reglas
básicas que rigen el idioma. El programa en sí mismo se
modificará muchas veces, a medida que se enriquezca con varios
elementos adicionales para ampliar nuestro conocimiento de
programación. Listo? Muy bien entonces, vámonos.
Primero necesitamos definir nuestras expectativas para el
programa. Serán muy modestos. Queremos que aparezca un
texto breve en la pantalla. Supongamos que el texto dirá:

Soy yo, tu primer programa.

15 – By Gerson
Eso es todo lo que queremos en este momento.
¿Qué pasos adicionales debe realizar nuestro primer
programa? Intentemos enumerarlos aquí:

1. para comenzar
2. para escribir el texto en la pantalla
3. para detener

Este tipo de descripción estructurada y semiformal de cada paso


del programa se denomina algoritmo. Las fuentes del algoritmo de
la palabra se remontan al idioma árabe y se originaron en los
primeros tiempos medievales, y para nosotros, esto representa el
comienzo de la programación de computadoras.
Ahora es tiempo de ver nuestro programa. Aquí está →
Parece un poco misterioso, ¿no? Echemos un vistazo a cada línea
del programa cuidadosamente y descubramos su significado y
propósito. La descripción no es particularmente precisa y aquellos
de ustedes que ya conocen un poco de C ++ probablemente
concluirán que es demasiado simplista y algo infantil. Hicimos esto
a propósito, no estamos construyendo Roma en un día. ¡Ni
siquiera en una semana!

Vamonos.
source_01_02_01_itsme.cpp

16 – By Gerson
1.2.2 Su primer programa (2)

Presta atención al carácter # (hash) al comienzo de la primera


línea. Significa que el contenido de esta línea es la
llamada directiva de preprocesador . Te contaremos más sobre
el preprocesador más adelante, pero por ahora solo diremos que
es una parte separada del compilador cuya tarea es leer
previamente el texto del programa y hacer algunas modificaciones
en él. El prefijo "pre" sugiere que estas operaciones se realizan
antes de que tenga lugar el procesamiento completo
(compilación).
Los cambios que introducirá el preprocesador están totalmente
controlados por sus directivas. En nuestro programa, estamos
tratando con la directiva include . Cuando el preprocesador
cumple con esa directiva, reemplaza la directiva con el contenido
del archivo cuyo nombre figura en la directiva (en nuestro caso,
este es el archivo denominado iostream ). Los cambios
realizados por el preprocesador nunca modifican el contenido de
su archivo fuente de ninguna manera. Cualquier modificación se
realiza en una copia volátil de su programa que desaparece
inmediatamente después de que el compilador termina su trabajo.
Entonces, ¿por qué necesito que el preprocesador incluya el
contenido de un archivo completamente desconocido llamado
iostream? Escribir un programa es similar a construir una
construcción con bloques prefabricados. En nuestro programa,
vamos a usar uno de esos bloques y sucederá cuando queramos
escribir algo en la pantalla. Ese bloque se llama cout (puede
encontrarlo dentro de nuestro código), pero el compilador no sabe
nada al respecto hasta ahora. En particular, el compilador no tiene
idea de que cout es un nombre válido para ese bloque, mientras
que cuot no lo es. El compilador debe ser advertido al respecto,
debe ser consciente del hecho.
Un conjunto de información preliminar que necesita el compilador
se incluye en los archivos de encabezado . Estos archivos
contienen una colección de información preliminar sobre bloques
listos para usar que puede ser utilizada por un programa para
escribir texto en la pantalla o leer letras desde el
teclado. Entonces, cuando nuestro programa va a escribir algo,
usará un bloque llamado cout, que puede hacer el truco (y mucho
17 – By Gerson
más). No queremos que el compilador se sorprenda, por lo que
debemos advertirlo al respecto. Los desarrolladores del
compilador ponen un conjunto de esta información anticipada en
el archivo iostream. Todo lo que tenemos que hacer es usar el
archivo. Esto es exactamente lo que esperamos de la directiva
include.
Pero, ¿dónde se encuentra el archivo iostream? La respuesta es
simple, pero no tan clara como podría desear: ese no es nuestro
problema. El preprocesador sabe dónde está. Buen trabajo,
preprocesador!

1.2.3 Su primer programa (3)

En el lenguaje C ++, todos los elementos de la biblioteca estándar


de C ++ se declaran dentro del espacio de
nombres denominado std . Un espacio de nombres es un
contenedor o entorno abstracto creado para contener una
agrupación lógica de entidades únicas (bloques).
Una entidad definida en un espacio de nombres está asociada solo
con ese espacio de nombres. Si desea utilizar muchas de las
entidades C ++ estándar (más adelante les contaremos todo
sobre ellas) debe insertar la instrucción de uso del espacio de
nombres en la parte superior de cada archivo, fuera de cualquier
función. La instrucción debe especificar el nombre del espacio de
nombres deseado (estándar en nuestro caso). Esto hará que las
instalaciones estándar estén disponibles durante todo el
programa.
18 – By Gerson
1.2.4 Su primer programa (4)

Ya hemos dicho algo sobre los bloques. Ahora vamos un poco más
profundo. Uno de los tipos más comunes de bloques utilizados
para construir programas en C ++ son las funciones .
Si asocia una función con las matemáticas, está en el camino
correcto. Imagine una función como una caja negra, donde puede
insertar algo en ella (aunque esto no siempre es necesario) y sacar
algo nuevo de ella, como si fuera un sombrero mágico. Las cosas
que se deben poner en el cuadro se llaman argumentos de
función (o parámetros de función). Las cosas que se deben sacar
de la caja se denominan resultados de función . Además, una
función puede hacer algo más en el lateral.
Si esto suena bastante vago, no se preocupe, hablaremos de las
funciones muchas veces y con mucho más detalle más adelante.

De vuelta a nuestro programa. El estándar del lenguaje C ++


supone que entre muchos bloques diferentes que se pueden poner
en un programa, siempre debe estar presente un bloque
específico, de lo contrario el programa no será correcto. Este
bloque es siempre una función con el mismo nombre: main .
Cada función en C ++ comienza con el siguiente conjunto de
información:

 ¿Cuál es el resultado de la función?


19 – By Gerson
 ¿Cuál es el nombre de la función?
 ¿Cuántos parámetros tiene la función y cuáles son sus
nombres?

Eche un vistazo de cerca a nuestro programa e intente leerlo


correctamente, aceptando el hecho de que aún no lo entenderá
completamente.

 el resultado de la función es un valor entero (podemos leerlo


de la palabra int , que es la abreviatura de entero)
 el nombre de la función es main (ya sabemos por qué)
 la función no requiere ningún parámetro (que podemos leer de
la palabra void ).

Este conjunto de información a veces se denomina prototipo , y


es como una etiqueta adherida a una función que anuncia cómo
puede usar esa función en su programa. El prototipo no dice nada
sobre lo que la función está destinada a hacer. Está escrito dentro
de la función y el interior de la función se llama cuerpo de la
función. El cuerpo de la función comienza con el primer
corchete de apertura { y termina con el corchete de cierre
correspondiente } . Puede sonar sorprendente, pero el cuerpo de
la función puede estar vacío, lo que significa que la función no
hace exactamente nada.
Incluso podemos crear una función que sea perezosa; puede
codificarse así:

vacío perezoso ( vacío ) {}

Este dron no proporciona ningún resultado (el primer vacío), su


nombre es "vago", no toma ningún parámetro (el segundo vacío)
y no hace absolutamente nada (el espacio en blanco entre
paréntesis).
Por cierto, los nombres de las funciones están sujetos a algunas
restricciones bastante rígidas. Más sobre esto más tarde.

20 – By Gerson
1.2.5 Su primer programa (5)

Dentro del cuerpo de la función principal, necesitamos escribir lo


que se supone que debe hacer nuestra función (y, por lo tanto, el
programa). Si miramos adentro, encontramos una referencia a un
bloque llamado cout .
En primer lugar, observe el punto y coma al final de la
línea. Cada instrucción (más precisamente, cada declaración )
en C ++ debe terminar con un punto y coma; sin ella, el programa
será incorrecto.
Esta declaración en particular dice: indique a la entidad llamada
cout que muestre el siguiente texto en la pantalla (como se indica
mediante el << digrafo, que especifica la dirección en la que se
envía el texto). Usted podría preguntar: ¿cómo sabemos que Cout
hará eso por nosotros? Bueno, lo sabemos porque así lo dice en
los estándares de lenguaje C ++.

La entidad cout (de hecho, es un objeto , pero no queremos que


aparezca esta palabra demasiado pronto, más adelante más
adelante) debe alimentarse con algo que debe mostrarse en la
pantalla. En nuestro ejemplo, el feed es solo texto
( cadena ). Para simplificar, podemos suponer que las cadenas en
el programa en C ++ siempre están entre comillas, de esa manera
el compilador reconoce el texto que se envía al usuario del
programa, y el texto que se pretende compilar se traduce al
lenguaje máquina. . Esta distinción es muy importante. Echar un
vistazo:

int main (nulo);

La línea de arriba es el prototipo de la función principal.

21 – By Gerson
"int main (nulo);"

La línea anterior no es el prototipo de la función principal, sino una


cadena que solo parece parte de un código fuente. El compilador
no está interesado en lo que hay entre las comillas y, por lo tanto,
no reconoce cadenas como el código.

¿Como funciona? Podemos imaginarlo así: la ejecución de nuestra


función principal se suspende (podemos decir que la función
principal se queda dormida) y durante ese tiempo cout , junto
con << (este tipo de símbolo se llama operador ) imprime el
texto en el pantalla. Cuando se imprime el texto, la función
principal se activa y continúa su actividad.
La forma anterior del código fuente es la más natural y quizás la
más fácil de leer por los humanos, pero aún puede escribirlo de
una manera diferente. Por ejemplo:

cout
<<
" Soy yo, tu primer programa".
;

En el lenguaje C ++ no es necesario escribir solo una declaración


por línea. Puede colocar dos (o más) declaraciones en la misma
línea, o dividir una declaración en varias líneas, pero tenga en
cuenta que la legibilidad (para los humanos) es un factor muy
importante. Sin embargo, los compiladores, a diferencia de los
humanos, nunca se quejarán de tu estilo.

22 – By Gerson
1.2.6 Su primer programa (6)

Estamos casi al final ahora. Solo queda una línea en nuestro


programa. Esto es:

devuelve 0;

Esta es otra declaración (además de la invocación de función) del


lenguaje C ++. Su nombre es solo regresar y eso es lo que
hace. Utilizado en la función, provoca el final de la ejecución de la
función. Si realiza return en algún lugar dentro de una función,
esta función interrumpe inmediatamente su ejecución.
El cero después del retorno de la palabra es el resultado de su
función main. Es importante: así es como su programa le dice al
sistema operativo lo siguiente: hice lo que tenía que hacer, nada
me impidió hacerlo y todo está bien .

Si fueras a escribir:

retorno 1;

Esto significaría que algo salió mal, no permitió que su programa


se ejecute con éxito y el sistema operativo podría utilizar esa
información para reaccionar adecuadamente. ¿Entonces eso es
todo? ¡Si eso es! Veamos nuevamente nuestro programa y
veamos qué sucede paso a paso:

 introdujimos la función llamada main en nuestro programa; se


ejecutará cuando inicie el programa;
 hicimos uso de una entidad llamada cout dentro de la función
principal: imprimirá el texto en la pantalla;

23 – By Gerson
 el programa finaliza inmediatamente después de la impresión,
lo que indica que se ha logrado todo lo que esperaba lograr.

Espero que no haya sido tan difícil como parecía a primera


vista. Ahora intentemos persuadir a la computadora para que
calcule algo para nosotros; después de todo, para eso están.

1.3.1 Números y cómo los ven las computadoras

¿Sabes cómo las computadoras realizan cálculos en números? Tal


vez haya oído hablar del sistema binario y sepa que es el
sistema que usan las computadoras para almacenar números y
que pueden realizar cualquier operación sobre ellos. Aquí no
vamos a ver las complejidades de los sistemas de numeración
posicional, pero diremos que los números manejados por las
computadoras modernas son de dos tipos:

 enteros , es decir, números enteros o aquellos que carecen de


la parte fraccional,
 números de punto flotante (o simplemente flotantes ) que
contienen (o pueden contener) la parte fraccionaria.

Esta definición es bastante simplista, pero lo suficientemente


buena para nuestros propósitos. Esta distinción es muy
importante y el límite entre estos dos tipos de números es muy
estricto. Ambos tipos de números difieren significativamente en
cómo se almacenan en la memoria de una computadora y en el
rango de valores aceptables. Además, la característica de un

24 – By Gerson
número que determina su tipo, rango y aplicación se
denomina tipo .

Entonces, ahora conocemos dos tipos del lenguaje C ++: un tipo


entero (conocido como int ) y un tipo de coma flotante (conocido
como flotante ).
Por ahora, dejemos a un lado los números de coma flotante (es
cierto: más sobre ellos más adelante) y consideremos una
pregunta que tal vez sea un poco banal a primera vista: ¿cómo
reconoce el lenguaje C ++ los enteros?

Bueno, es casi lo mismo que cuando los escribes en una hoja de


papel: son simplemente una cadena de dígitos que forman un
número. Pero hay un problema: no puede incluir ningún carácter
que no sea un dígito dentro del número.

Tomemos, por ejemplo, el número once millones ciento once mil


ciento once. Si tomara un lápiz en la mano en este momento,
probablemente escribiría el número de esta manera:

11,111,111

o (si eres polaco o alemán) así:

11.111.111

o incluso así:

11 111 111

Por supuesto, esto facilita la lectura si el número consta de muchos


dígitos. Sin embargo, a C ++ no le gusta esto en absoluto. Tienes
que escribir este número de la siguiente manera:

25 – By Gerson
11111111

Si no lo hace, el compilador le gritará. ¿Y cómo codifica números


negativos en C ++? Como de costumbre, agregando un
menos. Puedes escribir:

-11111111

Los números positivos no necesitan estar precedidos por el signo


más, pero puede hacerlo si lo desea. Las siguientes líneas
describen el mismo número:

+123
123

Por ahora trataremos solo con números enteros: introduciremos


números de punto flotante muy pronto.

Hay dos convenciones adicionales, desconocidas para el mundo de


las matemáticas. El primero de ellos nos permite usar los números
en una representación octal. Si un número entero está precedido
por el dígito 0, se tratará como un valor octal. Esto significa que
el número debe contener dígitos tomados del rango de 0 a 7
solamente.

0123

es un número octal con el valor (decimal) igual a 83 .

El segundo nos permite usar números hexadecimales. Dicho


número debe ir precedido por el prefijo escrito como 0x o 0X.

0x123
26 – By Gerson
es un número hexadecimal con el valor (decimal) igual a 291 .

1.3.2 Una variable es variable (1)

Entonces el lenguaje C ++ nos permite escribir


números. Probablemente no le sorprenda que también podamos
hacer algunas operaciones aritméticas con estos números:
sumar, restar, multiplicar y dividir. Contenga su entusiasmo, lo
haremos pronto. Pero es bastante normal preguntar cómo
almacenar los resultados de tales operaciones para usarlos en
otras operaciones. Hay "contenedores" especiales para ese
propósito y estos contenedores se llaman variables . Como
lo sugieren las variables de nombre , el contenido de un
contenedor puede variar (casi) de cualquier manera.
¿Qué tiene cada variable?

 un nombre
 un tipo
 un valor

Comencemos con los problemas relacionados con el nombre de


una variable. Las variables no aparecen mágicamente en nuestro
programa. Nosotros (como desarrolladores) decidimos cuántas y
qué variables queremos tener en nuestro programa. También
les damos sus nombres , casi convirtiéndonos en sus
padrinos. Si desea dar un nombre a la variable, debe seguir
algunas reglas estrictas:

 el nombre de la variable puede estar compuesto por letras


mayúsculas o minúsculas latinas, dígitos y el carácter _ (guión
bajo),
 el nombre de la variable debe comenzar con una letra ,
 el carácter subrayado es una letra (extraña pero verdadera),
 las letras mayúsculas y minúsculas se tratan como diferentes
(un poco diferente que en el mundo real: Alice y ALICE son los
mismos nombres, pero son dos nombres de variables diferentes
y, en consecuencia, dos variables diferentes) .
27 – By Gerson
Estas mismas restricciones se aplican a todos los nombres de
entidad utilizados en C ++.
El estándar del lenguaje C ++ no impone restricciones en la
longitud de los nombres de variables, pero un compilador
específico puede tener una opinión diferente sobre este asunto. No
te preocupes por lo general, la limitación es tan alta que es poco
probable que realmente desee utilizar nombres (o funciones) de
variables tan largos.
Aquí hay algunos nombres de variables correctos, pero no siempre
convenientes:

 yo
 t10
 Tipo de cambio
 mostrador
 DaysToTheEndOfTheWorld
 TheNameOfAVariableWhichIsSoLongThatYouWillNotBeAbleToW
riteItWithoutMistakes
 _

El apellido puede ser ridículo desde su perspectiva, pero su


compilador cree que es genial.
Y ahora echemos un vistazo a algunos ejemplos de nombres
incorrectos:

 10t (no comienza con una letra)


 Adiós_Señora (contains illegal characters)
 Tipo de cambio (contiene un espacio)

28 – By Gerson
1.3.3 Una variable es variable (2)

El tipo es un atributo que define de forma exclusiva qué valores


se pueden almacenar dentro de la variable. Ya hemos encontrado
los tipos de entero ( int ) y coma flotante ( flotante ). El valor de
una variable es lo que le hemos puesto. Por supuesto, solo puede
ingresar un valor que sea compatible con el tipo de variable. Solo
se puede asignar un valor entero a una variable entera (o, en otras
palabras, a una variable de tipo int). El compilador no nos
permitirá ingresar un número de coma flotante aquí.
Hablemos ahora de dos cosas importantes: cómo se crean las
variables y cómo ingresar un valor dentro de ellas (o más bien,
cómo darles un valor).
La variable existe como resultado de una declaración . Una
declaración es una estructura sintáctica que une un nombre
proporcionado por el programador con un tipo específico ofrecido
por el lenguaje C ++. La construcción de dicha declaración (o la
sintaxis de la declaración) es simple: simplemente use el nombre
del tipo deseado, luego el nombre de la variable (o nombres de
variables separados por comas si hay más de uno). Toda la
declaración termina con un punto y coma.

1.3.4 Una variable es variable (3)

Intentemos declarar una variable de tipo int llamada Counter . La


parte relevante del programa se verá así:
int Counter;

29 – By Gerson
¿Qué declara el siguiente fragmento de un programa?
int variable1, balance_cuenta, facturas;
Declara tres variables de tipo int nombradas
(respectivamente) variable1 , balance_cuenta y facturas .
Recuerde que puede usar tantas declaraciones de variables
como sea necesario para lograr su objetivo.

1.3.5 Una variable es variable (4)

¿Y cómo le damos un valor a la variable recientemente


declarada? Debe usar el llamado operador de
asignación . Aunque esto suena bastante misterioso, el operador
tiene una sintaxis simple y una interpretación inequívoca.
El operador de asignación parece muy familiar: aquí está:
=
Veamos algunos ejemplos:
Contador = 1;
La declaración anterior dice: asigne el valor de 1 a una variable
llamada Counter o, un poco más corto, asigne 1 a Counter .
Algunos programadores usan una convención diferente y leen
declaraciones como: El contador se convierte en 1 .

1.3.6 Una variable es variable (5)

Otro ejemplo →

30 – By Gerson
En este caso, el nuevo valor de la variable Resultado será el
resultado de sumar 100 a 200, pero probablemente ya lo haya
adivinado, ¿verdad?

1.3.7 Una variable es variable (6)

Y ahora un ejemplo un poco más difícil →


Al ver esto, un matemático probablemente protestaría: ningún
valor puede ser igual a sí mismo más uno. Esto es una
contradicción.
Pero en el lenguaje C ++, sign = no significa que es igual a , sino
que asigna un valor . Entonces, ¿cómo leemos tal registro en el
programa?

Tome el valor actual de la variable x, agregue 1 y almacene el


resultado en la variable x
En efecto, el valor de la variable x se incrementa en uno, lo que
no tiene nada que ver con comparar la variable con ningún valor.

1.3.8 Palabras clave

Eche un vistazo al lado derecho de la pantalla: verá una lista de


palabras que juegan un papel muy especial en cada programa de
lenguaje C ++. Se llaman palabras clave o (más
precisamente) palabras clave reservadas . Están reservados
porque no puede usarlos como nombres : ni para sus
variables, ni para sus funciones o cualquier otra entidad con
31 – By Gerson
nombre que desee crear. El significado de la palabra reservada
está predefinido y no se puede cambiar de ninguna manera.
Afortunadamente, debido a que el compilador de C ++ distingue
entre mayúsculas y minúsculas, puede modificar cualquiera de
estas palabras cambiando el caso de cualquier letra, creando así
una nueva palabra, que ya no está reservada.

Por ejemplo, no puedes hacer esto :


eres;
No puede tener una variable llamada int , está prohibida.
Pero puedes hacer esto en su lugar:
Eres;
El compilador será feliz, muy feliz.

32 – By Gerson
1.3.9 Comentarios sobre los comentarios (1)

Ahora vamos a hacer algunos comentarios . No nos referimos a


comentarios sobre sus logros o nuestros logros, aunque estamos
seguros de que tiene muchos logros de los que estar
orgulloso. Nos referimos a otros comentarios, a saber,
comentarios sobre el programa y dentro del programa al mismo
tiempo.
El desarrollador puede querer agregar algunas palabras dirigidas
no al compilador sino a los humanos, generalmente para explicar
a otros lectores del código cómo funcionan los trucos utilizados en
el código, o los medios de las variables y funciones, y
posiblemente, para almacenar información sobre quién es el autor
y cuándo se escribió el programa.
Los desarrolladores buenos y responsables describen cada entidad
importante recién creada; en particular, explicando el papel de los
parámetros y los valores modificados o devueltos como
resultados, así como explicando qué hace realmente el código.

¿Cómo dejamos algo como esto en el código fuente? Tenemos que


hacerlo de una manera que no obligue al compilador a
interpretarlo como parte del código. El comentario insertado en el
programa, que se omite en el momento de la compilación, se
denomina comentario.
Si queremos ser precisos, deberíamos decir que cada
comentario es léxicamente equivalente a un espacio . Cada
vez que el compilador encuentra un comentario en su programa,
el comentario es completamente transparente: desde su punto de
vista, este es solo un espacio (independientemente de la duración
del comentario real).

El lenguaje C ++ admite dos formas de insertar comentarios:

// comentarios de línea

y
33 – By Gerson
/ * bloquear comentarios * /

Un comentario de línea descarta todo desde donde se encuentra


el par de signos de barra diagonal (//) hasta el final de esa misma
línea.

En el lenguaje C ++, un comentario de bloque es un texto que


comienza con un par de los siguientes caracteres:

/ *
y termina con un par de los siguientes caracteres
*/

El comentario puede abarcar varias líneas o puede ocupar solo una


línea o parte de una línea.

1.3.10 Comentarios sobre los comentarios (2)

A la derecha, puede ver un ejemplo en el que el compilador ignora


todo, desde el par de signos de barra diagonal. El comentario de
línea puede comenzar en cualquier parte de la línea. Esto también
podría ser una línea en blanco, sin ningún contenido.

1.3.11 Comentarios sobre los comentarios (3)

A la derecha puede ver un ejemplo de un comentario similar, pero


introducido en el código utilizando el segundo método.
Cualquier desarrollador nuevo que lea el programa sabrá el
verdadero significado de la variable. El desarrollador leerá el
código más rápido y tomará menos tiempo entenderlo.

34 – By Gerson
1.3.12 Comentarios sobre los comentarios (4)

Los desarrolladores a menudo colocan una nota al comienzo de la


fuente para informar cuándo escriben el programa indicando quién
lo modificó y por qué. La nota puede aparecer así →
Tenga en cuenta que, a pesar de la estructura complicada y la
multitud de estrellas, la condición que indica cómo se debe
comenzar y terminar el comentario se cumple por completo.
Los comentarios pueden ser útiles en otro aspecto: puede
usarlos para marcar un fragmento de código que
actualmente no necesita por el motivo que sea. A menudo
hacemos esto durante las pruebas del programa para aislar el
lugar donde se puede ocultar un error.

1.3.13 Comentarios sobre los comentarios (5)

Solo tenemos una cosa más que decir sobre los comentarios. Los
compiladores difieren en la evaluación de si se puede colocar otro
comentario dentro de un solo comentario. Considere el siguiente
programa →

35 – By Gerson
La pregunta es: ¿se le permite anidar un comentario de bloque
(como / * int j; * / ) dentro de otro comentario de bloque?
La respuesta es no.
No puede usar tal construcción en su código.

1.4.1 Números de coma flotante (1)

Una palabra (o 2.0 palabras) sobre números de punto flotante en


la vida real y en el lenguaje C ++.
Anteriormente, nos familiarizamos con el concepto
de tipo de datos , y aprendimos que uno de los tipos básicos
conocidos en el lenguaje C ++ es el tipo entero llamado int . Ahora
es el momento de hablar sobre otro tipo, diseñado para
representar y almacenar los números que (como diría un
matemático) tienen una fracción decimal no vacía .
Estos son los números que tienen (o pueden tener) una parte
fraccionaria después del punto decimal, y aunque esta es una
definición muy simplista, es suficiente para nuestros
propósitos. Cada vez que usamos un término como "dos y medio"
o "cero punto cuatro", pensamos en números que la computadora
considera números flotantes .

1.4.2 Números de coma flotante (2)

Volvamos a los valores que citamos hace un momento. "Dos y


medio" parece normal cuando lo escribe en un programa, aunque
si su idioma nativo usa una coma en lugar de un punto en el
número, debe asegurarse de que su número contenga
puntos y no comas . El compilador no lo aceptará o (en
circunstancias muy raras) no comprenderá sus intenciones, ya que
la coma tiene su propio significado reservado en el lenguaje C ++.
Si desea utilizar un valor de solo "dos y medio", debe escribirlo
como se muestra en la imagen →
Observe una vez más: hay un punto entre "2" y "5", no una coma.

36 – By Gerson
1.4.3 Números de coma flotante (3)

Como probablemente pueda imaginar, puede escribir el valor de


"cero punto cuatro" en C ++ como →
No olvide esta regla simple: puede omitir cero cuando es el único
dígito delante o después del punto decimal. En esencia, puede
escribir el valor 0.4 como a la derecha.

1.4.4 Números en coma flotante (4)

Por ejemplo: el valor de 4.0 podría escribirse como 4. sin cambiar


su tipo o valor.
Nota: el punto decimal es esencial para reconocer los números de
coma flotante en C ++. Mira estos dos números:

4
4.0

37 – By Gerson
Puede pensar que son exactamente iguales, pero el compilador de
C ++ los ve de manera completamente diferente:

4 es un int .
4.0 es un flotador .

Podemos decir que el punto hace flotar . No te olvides de eso.

1.4.5 Números de coma flotante (5)

Cuando desee usar cualquier número que sea muy grande o muy
pequeño, puede usar la llamada notación científica . Tomemos,
por ejemplo, la velocidad de la luz expresada en metros por
segundo. Escrito directamente se vería así:
300000000
Para evitar el tedioso trabajo de escribir tantos ceros, los libros
de texto de física usan una forma abreviada, que probablemente
ya haya visto:
3 · 108
Significa: tres veces diez a la potencia de ocho
En el lenguaje C ++, el mismo efecto se logra en una forma
ligeramente diferente: eche un vistazo:
3E8
La letra E (también puede usar la letra minúscula e , proviene de
la palabra exponente ) es una representación concisa de la
frase multiplicada por diez por el poder de .
Nota:

 el exponente (el valor después de la "E") debe ser un número


entero.
 la base (el valor frente a la "E") puede o no ser un número
entero.

38 – By Gerson
1.4.6 Números en coma flotante (6)
Veamos cómo se usa esta convención para registrar números
que son muy pequeños (en el sentido de su valor absoluto que
es cercano a cero).
Una constante física llamada constante de Planck (denotada como
h), según los libros de texto, tiene el valor de
6.62607 · 10-34
Si desea utilizarlo en un programa, lo escribiría de esta manera:
6.62607E-34
Y eso es. No es tan difícil, ¿verdad?

1.4.7 Números de coma flotante (7)

Volvamos a los valores de coma flotante. Ya sabemos qué es una


variable, y también sabemos cómo declarar una variable entera,
por lo que ahora es el momento de declarar variables de tipo de
punto flotante.
Hacemos esto usando la palabra clave float . Sabiendo que
podemos declarar dos variables de punto flotante, llamadas PI (no
podemos nombrarlo Π porque, como ya sabes, el lenguaje C ++
se asusta cuando los nombres de variables se escriben con letras
griegas) y Campo →
Como puede ver, la diferencia al declarar las variables enteras y
flotantes es bastante pequeña desde el punto de vista de la
sintaxis.

1.4.8 Números de coma flotante (8)

Esta diferencia en términos de semántica es significativa, como


puede ver en el ejemplo →
Con un poco de reflexión, podemos descubrir que el símbolo (más
precisamente, el operador ) que realiza
39 – By Gerson
la división matemática es un solo carácter / (barra
inclinada). Echa un vistazo a este código →
Puede ser un poco sorprendente saber que el valor que se
ingresará en la variable i es 2 (sí, ¡solo 2!), Mientras que la
variable x será igual a 2.5. Mire este ejemplo cuidadosamente, ya
que ilustra una diferencia muy importante entre estos dos tipos de
datos.

1.4.9 Números en coma flotante (9)

¿Qué sucede cuando tenemos que convertir valores enteros en


valores flotantes o viceversa? Siempre podemos transformar
de int a flotante , pero puede conducir a una pérdida de
precisión .
Considera el ejemplo →
Después de cambiar de int a float , el valor de la variable f es
100.0, porque el valor de tipo int (100) se convierte
automáticamente en float (100.0).
La transformación afecta la representación interna (máquina) de
esos valores ya que las computadoras usan diferentes
métodos para almacenar flotantes e ints en su memoria .

40 – By Gerson
1.4.10 Números de coma flotante (10)

Consideremos la situación opuesta ahora.


Como probablemente haya adivinado, estas sustituciones
resultarán en una pérdida de precisión: el valor de la
variable i será 100. Veinticinco centésimas no tiene sentido en
el mundo de los ints . Además, convertir un flotante en
un int no siempre es factible .
Las variables enteras (como flotadores ) tienen una capacidad
limitada. No pueden contener números arbitrariamente grandes (o
arbitrariamente pequeños).
Por ejemplo, si un determinado tipo de computadora usa cuatro
bytes (es decir, 32 bits) para almacenar valores int , solo puede
usar los números del rango de -2147483648 a 2147483647.

1.4.11 Números de coma flotante (11)

La variable i no puede almacenar un valor tan grande, pero no


está claro qué sucederá durante la asignación. Ciertamente, se
producirá una pérdida de precisión , pero el valor asignado a la
variable i no se conoce de antemano.
En algunos sistemas, puede ser el valor int máximo permitido ,
mientras que en otros se produce un error y en otros el valor
asignado puede ser completamente aleatorio.
Esto es lo que llamamos un problema dependiente de la
implementación . Es la segunda (y más fea) cara de la
portabilidad del software.

41 – By Gerson
1.4.12 Operadores

Un operador es un símbolo del lenguaje de programación, que


puede operar sobre los valores. Por ejemplo, un operador de
asignación es el signo = . Ya sabemos que puede asignar
valores a las variables.
Ahora veamos algunos otros operadores disponibles en el lenguaje
C ++ y descubramos qué reglas rigen su uso y cómo interpretar
las operaciones que realizan. Comencemos con los operadores
asociados con operaciones aritméticas ampliamente
reconocibles. El orden de su aparición no es
accidental. Hablaremos más sobre esto después.

1.4.13 Multiplicación

Un asterisco * es un operador de multiplicación . Si observa


el código aquí, verá que la variable k se establecerá en el valor de
120, mientras que la variable z se establecerá en 0.625.

42 – By Gerson
1.4.14 División

Una barra oblicua / es un operador divisional . El valor frente


a la barra es un dividendo , mientras que el valor detrás de la
barra es un divisor .
Mira el fragmento del programa; por supuesto, k se establecerá
en 2, z en 0.5.

1.4.15 División por cero (1)

Como probablemente haya adivinado, dividir por cero está


estrictamente prohibido, pero la penalidad por violar esa regla se
aplicará en diferentes momentos.
Si se atreve a escribir algo así, el compilador se volverá loco:
obtendrá un error de compilación, un error de tiempo de ejecución
o algún mensaje en tiempo de ejecución, posiblemente también
algunas palabras de elección sobre sus capacidades de
programación. OK, el último fue una broma. Aún así, en algunos
43 – By Gerson
casos no podrá ejecutar su programa. Como regla general, no
debes dividir por cero.

1.4.16 División por cero (2)

En el siguiente ejemplo, el compilador no le dirá nada, pero cuando


intente ejecutar ese código, el resultado de la operación puede ser
sorprendente. No es un numero. Es un valor destacado especial
llamado inf (como en infinitivo ). En general, este tipo de
operación ilegal es una llamada excepción .
Cuando encuentre excepciones en su programa, debe reaccionar
en consecuencia. Discutiremos esto más tarde.

1.4.17 Adición

El operador de suma es el signo + (más), que la mayoría de


nosotros ya conocemos de la clase de matemáticas.
Nuevamente, eche un vistazo al fragmento del programa: como
puede suponer, k es igual a 102 yz a 1.02.

44 – By Gerson
1.4.18 Resta

El operador de resta es obviamente el signo - (menos), aunque


debe tener en cuenta que este operador también tiene otro
significado: puede cambiar el signo de un número.
Este es un buen momento para mostrarle una distinción muy
importante entre operadores unarios y binarios (en el
lenguaje C ++ también hay un operador ternario , más sobre
eso un poco más adelante).
Como de costumbre, conozcamos un fragmento del programa:
puede adivinar que k será igual a -100, mientras que z será igual
a 0.0.

1.4.19 Unario menos

En aplicaciones de "sustracción", el operador menos espera dos


argumentos: el izquierdo (un minuendo en términos aritméticos)
y el derecho (un sustraendo ).

45 – By Gerson
Por esta razón, el operador de resta se considera uno de los
operadores binarios, al igual que los operadores de suma,
multiplicación y división. Pero el operador menos se puede usar
de una manera diferente: eche un vistazo al fragmento →
Como probablemente haya adivinado, a la variable j se le asignará
el valor de 100. Usamos el operador menos como operador
unario , ya que solo espera un argumento: el correcto.

1.4.20 Unario plus

El operador + puede expresar la misma naturaleza dual, que


también podemos usar como operador unario: su función es
preservar el signo. Echa un vistazo al fragmento →
Aunque tal construcción es sintácticamente correcta, usarla no
tiene mucho sentido y sería difícil encontrar una buena razón para
hacerlo.

1.4.21 Resto

46 – By Gerson
El operador restante es bastante peculiar, porque no tiene
equivalente entre los operadores aritméticos tradicionales.
Su representación gráfica en el lenguaje C ++ es el
carácter % (porcentaje), que puede parecer un poco confuso. Es
un operador binario (realiza la operación de módulo ) y ambos
argumentos no pueden ser flotantes (¡no lo olvides!).
Mira el ejemplo →
La variable k es 3 (porque 2 * 5 + 3 = 13).
Ah, y una cosa más que debes recordar: no puedes calcular el
resto con el argumento correcto igual a cero . ¿Puedes
adivinar por qué?
Probablemente recuerdes lo que dijimos antes sobre dividir entre
cero. Y debido a que la división por 0 invoca un comportamiento
indefinido, la operación de módulo, que se basa en la división,
también está indefinida.
Bueno, eso es lo que dice el estándar C ++. Tenemos que aceptar
eso.

1.4.22 Prioridades

Hasta ahora, hemos tratado a cada operador como si no tuviera


conexión con los demás. Por supuesto, en la programación real,
nada es tan simple como eso. Además, muy a menudo
encontramos más de un operador en una expresión y luego las
cosas pueden complicarse muy rápidamente. Considere la
siguiente expresión.
Si su memoria no le ha fallado, debe recordar de la escuela que
las multiplicaciones preceden a las sumas. Probablemente
47 – By Gerson
recuerde que tiene que multiplicar 3 por 5, mantener el 15 en su
memoria, agregarlo a 2, obteniendo así el resultado de 17.
El fenómeno que hace que algunos operadores actúen antes que
otros se conoce como la jerarquía de prioridades. El lenguaje C
++ define con precisión las prioridades de todos los operadores y
asume que los operadores de mayor prioridad (mayor)
realizan sus operaciones antes que los operadores con
menor prioridad .
Entonces, si sabemos que * tiene una prioridad más alta que + ,
podemos descubrir cómo se calculará el resultado final.

1.4.23 Enlaces

La vinculación del operador determina el orden de los cálculos


realizados por algunos operadores con igual prioridad, colocados
uno al lado del otro en una expresión. La mayoría de los
operadores en el lenguaje C ++ tienen el enlace del lado
izquierdo , lo que significa que el cálculo de esta expresión de
muestra se realiza de izquierda a derecha, es decir, 3 se agregará
a 2 y 5 se agregará al resultado.
Ahora, en este punto, puede estar resoplando y diciendo que todos
los niños saben perfectamente que la suma es conmutativa y que
no importa en qué orden se realicen las sumas. Y sí, tienes razón,
pero no del todo. Las adiciones realizadas por la computadora no
siempre son conmutativas. De Verdad. Le mostraremos esto con
más detalle más adelante. Pero por ahora, sea paciente y confíe
en nosotros.

48 – By Gerson
1.4.24 Lista de prioridades (1)

Como es nuevo en operadores de lenguaje C ++, presentar una


lista completa de las prioridades de los operadores puede no ser
una buena idea. En su lugar, le mostraremos su forma truncada y
la ampliaremos de manera consistente durante la introducción de
nuevos operadores.
Esta tabla ahora se ve de la siguiente manera →
Nota: hemos revisado los operadores en orden de mayor a
menor prioridad .

1.4.25 Lista de prioridades (2)

¡Examen! Queremos comprobar si comprende el concepto de


encuadernación. Intenta trabajar con la siguiente expresión →
Ambos operadores (* y%) tienen la misma prioridad, por lo que
solo puede adivinar el resultado cuando conoce la dirección de
enlace. ¿Estás listo?

49 – By Gerson
1.4.26 Lista de prioridades (3)

¡Sí tienes razón! El resultado es 1. ¿Puedes oír el sonido de las


trompetas? ¡Están jugando para ti! Felicidades.

1.4.27 Paréntesis (1)

Por supuesto, siempre podemos usar paréntesis, que pueden


cambiar el orden natural del cálculo. De acuerdo con las reglas
aritméticas, las subexpresiones entre paréntesis siempre se
calculan primero .
Puede usar tantos paréntesis como necesite y, a menudo, los
usamos para mejorar la legibilidad de una expresión, incluso si no
cambian el orden de las operaciones.
Aquí hay un ejemplo de una expresión de paréntesis múltiples →
Ahora intente averiguar el valor dado a la variable l .

50 – By Gerson
1.4.28 Paréntesis (2)

Sí, son 10. ¡Bien hecho!

1.4.29 Operadores continuación (1)

Aquí hay algunos operadores en el lenguaje C ++ que no


encontrará en los libros de texto de matemáticas. Algunos de ellos
se usan con frecuencia para incrementar una variable en
uno .
Esto a menudo se hace cuando contamos algo (por ejemplo,
ovejas). Consideremos el siguiente fragmento:

int SheepCounter;
SheepCounter = 0;

Cada vez que una oveja corre por nuestros pensamientos,


queremos que la variable se incremente, así:

SheepCounter = SheepCounter + 1;

Operaciones similares aparecen con mucha frecuencia en


programas típicos, por lo que los creadores del lenguaje C ++
introdujeron un conjunto de operadores especiales para estas
acciones. Uno de ellos es el operador ++ (más más).
Puede lograr el mismo efecto de una manera más corta:

SheepCounter ++;

Se ve mucho más elegante ahora, ¿no?


51 – By Gerson
1.4.30 Operadores continuación (2)

Del mismo modo, también puede disminuir el valor de una variable


elegida.
Por ejemplo, si apenas podemos esperar nuestras vacaciones,
nuestra mente hace la siguiente operación todas las mañanas:

DaysUntilHoliday = DaysUntilHoliday - 1;

Podemos escribir esto de una manera más compacta:

DaysUntilHoliday--;

1.4.31 Operadores continuación (3)

Lo sentimos, pero ahora es el momento de presentar algunas


palabras nuevas:

 El signo ++ se llama operador de incremento .


 El signo - se llama operador de decremento .

Así que le mostramos los operadores ++ y - después de una


variable (un especialista en la sintaxis de los lenguajes de
programación diría que se usan como operadores postfix).
Sin embargo, ambos operadores se pueden poner delante de una
variable (como operadores de prefijo), de esta manera:

++ SheepCounter;
--DíasUntilHoliday;

El efecto es exactamente el mismo: SheepCounter aumentará en


1, DaysUntilHoliday disminuirá en 1.

52 – By Gerson
Hay una diferencia bastante significativa, como se describe por los
nombres de estos operadores.

1.4.32 Pre y post operadores (1)

Damas y caballeros, aquí están.


Puede parecer un poco extraño al principio, pero crecerán en
ti. Analicemos los efectos de estos operadores.

Operación: ++ Variable - Variable


Efecto: Incrementa / disminuye la variable en 1 y usa su valor ya
aumentado / reducido.

Operación: Variable ++ Variable--


Efecto: utilice el valor de la variable original (sin cambios) y luego
incremente / disminuya la variable en 1.

El comportamiento de estos operadores justifica la presencia del


prefijo pre (antes) y post- (después) en sus nombres: pre porque
la variable se modifica primero y luego se usa su
valor; y post- porque se usa el valor de la variable y luego se
modifica la variable.

53 – By Gerson
1.4.33 Operadores anteriores y posteriores (2)

Eche un vistazo a dos ejemplos simples.


Primero, la variable i se establece en 1. En la segunda declaración
vemos los siguientes pasos:

 se tomará el valor de i (ya que usamos el operador de


incremento posterior),
 la variable i se incrementará en 1.

En efecto, j recibirá el valor de 1 y yo recibiré el valor de 2.

1.4.34 Pre y post operadores (3)

Las cosas serán un poco diferentes ahora.


A la variable i se le asigna el valor de 1; a continuación,
la variable i se incrementa y se convierte en 2; a continuación, el
valor aumentado se asigna a la variable j .
En efecto, tanto i como j serán iguales a 2.

54 – By Gerson
1.4.35 Pre y post operadores (4)

Mire cuidadosamente este programa. Tracemos su ejecución paso


a paso.

1. A la variable i se le asigna el valor de 4;


2. Tomamos el valor original de i (4), lo multiplicamos por 2,
asignamos el resultado (8) a j y eventualmente (post-)
incrementamos la variable i (ahora es igual a 5);
3. Nosotros (pre) disminuimos el valor de j (ahora es igual a 7),
este valor reducido se toma y multiplica por 2, y el resultado
(14) se asigna a la variable i .

1.4.36 Pre y post operadores (prioridades)

¿Qué más necesitas saber sobre los nuevos operadores?


Bueno, en primer lugar, su prioridad es bastante alta, más alta
que los operadores *, / y%.
En segundo lugar, tienen unión del lado derecho. Nuestra tabla de
prioridades es actualmente la siguiente →

55 – By Gerson
1.4.37 Operadores de acceso directo (1)

Ahora es el momento para el siguiente conjunto de operadores,


los que facilitan la vida de un desarrollador. El que ya hemos
descrito se ocupa de sumar y restar uno.
Sin embargo, a menudo necesitamos algo más que sumar o restar,
o queremos usar un valor diferente; podemos usar este operador
cuando queremos calcular una serie de valores sucesivos de
potencias de 2.

1.4.38 Operadores de acceso directo (2)

Usamos esta expresión cuando nuestra manada es


extremadamente numerosa.

1.4.39 Operadores de acceso directo (3)

En el lenguaje C ++ hay una forma corta de escribir estas


operaciones. Puedes escribirlos de la siguiente manera →

1.4.40 Operadores de acceso directo (4)

Intentemos presentar una descripción general de tales


operaciones. Si op es un operador de dos argumentos (¡esta es
56 – By Gerson
una condición muy importante!) Y el operador se usa en el
siguiente contexto:

variable = variable op expresión;


Se puede simplificar y mostrar de la siguiente manera:
variable op = expresión;
Echa un vistazo a los ejemplos aquí →
Asegúrate de entenderlos a todos. Y relájate, porque todavía
tenemos mucho trabajo por delante.

1.5.1 Tipo de caracteres (1)

Hasta ahora hemos tratado el lenguaje C ++ (y la computadora


misma) como una herramienta para realizar cálculos en
números. Esto es consistente con la creencia común de que una
computadora es solo una calculadora, aunque sea muy
inteligente. Sabes que no es cierto, ya que la computadora
también se puede usar fácilmente para el procesamiento de
textos.
Podemos definir una "palabra" como una cadena de caracteres
(letras, números, signos de puntuación, etc.). Tratamos con tales
cadenas en la primera lección cuando usamos cout para escribir
texto en la pantalla de la computadora.
Ahora, sin embargo, ignoraremos la cadena que consta de varios
caracteres y centraremos nuestra atención en caracteres
individuales. Volveremos al problema del procesamiento de
cadenas cuando comencemos a trabajar en matrices, porque en
el lenguaje C ++ todas las cadenas se tratan como matrices .

57 – By Gerson
1.5.2 Tipo de personaje (2)

Para almacenar y manipular caracteres, el lenguaje C ++


proporciona un tipo especial de datos. Este tipo se llama char ,
que es una abreviatura de la palabra "carácter".
Intentemos declarar una variable para almacenar un solo carácter.
Parece familiar, ¿no? Ahora hablemos un poco sobre cómo las
computadoras almacenan personajes.

1.5.3 Código ASCII

Las computadoras almacenan caracteres como


números . Cada carácter utilizado por las computadoras
corresponde a un número único, y viceversa, y hay muchos más
caracteres de los que cabría esperar. Muchos de ellos son
invisibles para los humanos pero esenciales para las
computadoras.
Algunos de estos caracteres se denominan "espacios en blanco",
mientras que otros se denominan "caracteres de control" porque
su propósito es controlar los dispositivos de entrada / salida.
Un ejemplo de un espacio en blanco que es completamente
invisible a simple vista es un código especial, o un par de códigos
(diferentes sistemas operativos pueden tratar este problema de
manera diferente), que se utilizan para marcar los extremos de
las líneas dentro de los archivos de texto.

58 – By Gerson
Los humanos no ven estos signos, pero pueden ver el efecto de su
aplicación donde se rompen las líneas.
Podemos crear prácticamente cualquier número de asignaciones
de caracteres, pero un mundo en el que cada tipo de computadora
utiliza una codificación de caracteres diferente sería un mundo
extremadamente inconveniente.
Las computadoras no podrían ponerse de acuerdo en nada y no
podrían hacer ningún trabajo, un poco como los políticos
elegidos. Esto ha llevado a la necesidad de un estándar universal
y ampliamente aceptado, implementado por (casi) todas las
computadoras y sistemas operativos en todo el mundo.
ASCII (abreviatura de American Standard Code for Information
Interchange) es el dispositivo más utilizado y casi todos los
dispositivos modernos (como computadoras, impresoras,
teléfonos móviles, tabletas, etc.) lo utilizan. El código permite 256
caracteres diferentes, pero solo nos interesan los primeros 128.
Si desea verificar cómo se construye el código, eche un vistazo a
la tabla →
Míralo con cuidado, porque hay algunas cosas interesantes
sucediendo aquí. Veamos uno en particular. Vea cuál es el código
para el carácter más común, el espacio. Sí, es 32. Ahora
compruebe cuál es el código para la letra minúscula "a". Es el 97,
¿verdad? Y ahora busquemos la mayúscula "A". Es 65. Ahora reste
el código de "a" de "A", y ¿qué obtienes? 32! Sí, ese es el código
para un espacio. Vamos a utilizar esta característica interesante
del código ASCII pronto.
También tenga en cuenta que las letras están ordenadas en el
mismo orden que en el alfabeto latino.
Por cierto, el código ASCII está siendo reemplazado (o más bien
extendido) por un nuevo estándar internacional llamado
UNICODE.
Afortunadamente, el conjunto ASCII es
un subconjunto UNICODE . UNICODE es capaz de representar
prácticamente todos los personajes utilizados en todo el
mundo. Pasaremos un poco más de tiempo más tarde en esto.

59 – By Gerson
1.5.4 Valores de tipo de caracteres (1)

¿Cómo usamos valores de tipo char en el lenguaje C


++? Podemos hacerlo de dos maneras, las cuales son ligeramente
diferentes entre sí.
La primera forma nos permite especificar el carácter en sí
pero encerrado entre comillas simples
(apóstrofes) . Supongamos que queremos que se asigne el valor

60 – By Gerson
de la letra mayúscula "A" a una variable de algunas diapositivas
anteriores.
Así es como lo hacemos →

1.5.5 Valores de tipo de caracteres (2)

Si desea utilizar un solo carácter en el lado derecho, no puede


omitir apóstrofes bajo ninguna circunstancia. Si lo intentas, puede
que al compilador no le guste.
Ahora asignemos un asterisco a nuestra variable. Me gusta esto

1.5.6 Valores de tipo de caracteres (3)

El segundo método es asignar un valor entero no negativo que


es el código del carácter deseado. Eso significa que la asignación
que ve a la derecha → pondrá una "A" en la variable Carácter .
Sin embargo, este segundo método es menos recomendable, y si
puede evitarlo, debería hacerlo. ¿Porque preguntas?
Bueno, en primer lugar, porque es ilegible para los humanos. Sin
conocer el código ASCII, es imposible adivinar qué significa
realmente ese "65". Puede ser el código para un personaje, pero
igualmente puede ser que un programador sociópata haya hecho
esto para salvar el número de ovejas ya contadas.
Y en segundo lugar, extraño pero aún cierto, hay un número
significativo de computadoras en el mundo que usan códigos
distintos de ASCII. Por ejemplo, muchos de los mainframes de IBM
usan un código comúnmente llamado EBCDIC (Código de
61 – By Gerson
intercambio decimal codificado con código binario extendido), que
es muy diferente de ASCII y se basa en conceptos radicalmente
diferentes.

1.5.7 Literales

Bien, creemos que estás listo para un nuevo término: un


literal . El literal es un símbolo que identifica de forma única
su valor . Algunas personas prefieren usar una definición
diferente: el literal significa en sí mismo .
Elija la definición que considere más clara y vea los siguientes
ejemplos simples:

 Carácter: esto no es un literal; es probablemente un nombre


variable; cuando lo miras, no puedes adivinar qué valor está
asignado actualmente a esa variable
 'A': esto es un literal; cuando lo miras, inmediatamente sabes
su valor; incluso sabes que es un literal del tipo char
 100: también es literal (de tipo int )
 100.0: es otro literal, esta vez del tipo flotante
 i + 100: esta es una combinación de una variable y un literal
unidos con el operador +; Tal estructura se llama expresión.

62 – By Gerson
1.5.8 Literales de caracteres (1)

Si eres una persona inquisitiva (y creemos que lo eres),


probablemente estés haciendo esta pregunta: si se da un literal
de tipo char como el carácter encerrado en los apóstrofes, ¿cómo
codificas el apóstrofe mismo?
El lenguaje C ++ utiliza una convención especial que también se
extiende a otros caracteres, no solo a los apóstrofes. Comencemos
con un apóstrofe de todos modos. Un apóstrofe se ve así →

El carácter \ (llamado barra invertida) actúa como el


llamado carácter de escape porque al usar el \ escapamos del
significado normal del carácter que sigue a la barra diagonal.
En este ejemplo, escapamos del papel habitual del apóstrofe (es
decir, delimita los literales de tipo char ), y el apóstrofe que sigue
al \ es simplemente un carácter de apóstrofe.

1.5.9 Literales de caracteres (2)

También puede usar el carácter de escape para escapar del


personaje de escape .
Sí, lo sabemos, es raro, pero el ejemplo de la derecha debería
aclararlo.
Así es como ponemos una barra diagonal inversa en una variable
de tipo char →

1.5.10 Caracteres de escape (1)

El lenguaje C ++ también nos permite escapar en algunas otras


circunstancias. Comencemos con aquellos que denotan literales
que representan espacios en blanco.

63 – By Gerson
\ n : denota una transición a una nueva línea y, a veces, se
denomina LF (avance de línea), ya que las impresoras reaccionan
a este carácter moviendo el papel hacia adelante una línea de
texto.
1.5.11 Caracteres de escape (2)

\ r - denota un retorno al comienzo de la línea y a veces se


llama CR (Retorno de carro - "carro" era sinónimo de "cabezal de
impresión" en tiempos pre-digitales); Las impresoras responden a
este carácter como si se les hubiera pedido que reiniciaran la
impresión desde el margen izquierdo de la línea ya impresa.
Para que una impresora comience a imprimir una nueva línea,
debe enviar esos dos caracteres en un orden particular: LF para
expulsar el papel y CR para mover el encabezado al comienzo de
la nueva línea.

1.5.12 Personajes de escape (3)

\ a (como en alarma) es una reliquia del pasado cuando los


teletipos se usaban a menudo para comunicarse con las
computadoras (¿sabes qué es un teletipo ?); enviar este
personaje a un teletipo enciende su timbre , por lo tanto, el
personaje se llama oficialmente BEL (como campana); Si envía
este personaje a la pantalla, escuchará un sonido: no será un
timbre real, sino un pitido corto.
El poder de la tradición persiste incluso en el mundo de TI.

1.5.13 Caracteres de escape (4)

\ 0 (nota: el carácter después de la barra invertida es un cero,


no un O): llamado nulo (de la palabra latina nullus - none) es un
carácter que no representa ningún carácter; a pesar de las
primeras impresiones de que podría ser muy útil.

1.5.14 Caracteres de escape (5)

Ahora intentemos escapar en una dirección ligeramente


diferente. El primer ejemplo explica la variante cuando una barra

64 – By Gerson
invertida es seguida por dos o tres dígitos octales (los dígitos del
rango de 0 a 7). El número codificado de esta manera se tratará
como un valor ASCII.
Y puede verse así →
47 octal es 39 decimal. Mire la tabla de códigos ASCII y verá que
es el código ASCII para un apóstrofe, por lo que esto es
equivalente a
'\''
(pero solo para computadoras que implementan el código ASCII).

1.5.15 Caracteres de escape (6)

El segundo escape se refiere a la situación cuando \ es seguido


por la letra X (minúscula o mayúscula, no importa).
Aquí necesitamos uno o dos dígitos hexadecimales, que se
tratarán como código ASCII. Aquí hay un ejemplo →
Como probablemente haya adivinado, 27 hexadecimales son 39
decimales.

1.5.16 Los valores de Char son valores int (1)

Existe un supuesto en el lenguaje C ++ que puede parecer


sorprendente en un primer momento: el carbón tipo es tratado
como un tipo especial de int tipo . Esto significa que:

 Siempre puede asignar un valor char a una variable int ;


 Siempre puede asignar un valor int a una variable char , pero
si el valor excede 127 (el código de caracteres más alto en
ASCII puro, pero 255 podría ser válido para muchas
codificaciones modernas) obtendrá una pérdida de valor;

65 – By Gerson
 El valor del tipo char puede estar sujeto a los mismos
operadores que los datos del tipo int .

Podemos verificar esto usando un ejemplo


simple. Anteriormente dijimos que en ASCII, la
"distancia" entre letras mayúsculas y
minúsculas es 32, y que 32 también es el código
del carácter de espacio. Mira este fragmento →
Esta secuencia de sumas y restas posteriores llevará el valor
de Char a su valor original ("A"). ¿Puedes ver por qué? Si no, no
te preocupes. Todo se aclarará pronto.

1.5.17 Los valores de Char son valores int (2)

Todas las tareas a la derecha son correctas. Intenta descubrir sus


significados →

1.5.18 Los valores de Char son valores int (3)

¡Bien hecho! Aquí están las respuestas: 97, 97, 97, 65, 65, 65

66 – By Gerson
1.6.1 Quien pregunta no se equivoca

Un programador escribe un programa y el programa hace


preguntas. Una computadora ejecuta el programa y proporciona
las respuestas. El programa debe poder reaccionar de acuerdo con
las respuestas recibidas. Afortunadamente, la computadora solo
conoce dos tipos de respuesta: sí, esto es cierto y no, esto es
falso. Nunca recibirá una respuesta como "No sé" o
"Probablemente sí, pero no estoy seguro". Si recibe estas
respuestas, consulte a su terapeuta sobre ellas.
Para hacer preguntas, el lenguaje C ++ utiliza un conjunto de
operadores muy especiales. Los enumeraremos uno tras otro,
ilustrando sus efectos en algunos ejemplos simples.

1.6.2 Pregunta: ¿es x igual a y?

Pregunta: ¿son iguales dos valores?


Para hacer esta pregunta, utiliza el operador == (igual igual).

Pero no olvides esta importante distinción:

 = es un operador de asignación
 == es la pregunta "¿son iguales estos valores?"

Es un operador binario con un enlace del lado


izquierdo . Necesita dos argumentos y verifica si son
iguales . Ahora hagamos algunas preguntas. Intenta adivinar las
respuestas.

1.6.3 ¿Es x igual a y?

La pregunta es trivial. Por supuesto, 2 es igual a 2. La


computadora responderá "verdadero".

67 – By Gerson
1.6.4 Pregunta: ¿es x igual a y?

Este también es trivial. La respuesta será "falsa".

1.6.5 Pregunta: ¿es x igual a y?

Aquí no podemos encontrar la respuesta si no sabemos qué valor


se almacena actualmente en la variable i .
Si la variable se ha cambiado muchas veces durante la ejecución
de su programa, la respuesta a esta pregunta solo puede ser dada
por la computadora y solo en tiempo de ejecución.

1.6.6 Pregunta: ¿es x igual a y?

Aquí tenemos otro desarrollador que cuenta las ovejas negras y


blancas por separado y solo puede quedarse dormido cuando hay
exactamente el doble de ovejas negras que blancas.
La pregunta es la siguiente →

Debido a la baja prioridad del operador ==, esta pregunta se trata


como equivalente a esta:

BlackSheepCounter == (2 * WhiteSheepCounter)
68 – By Gerson
1.6.7 Pregunta: ¿x no es igual a y?

Para hacer esta pregunta, usamos ! = (Exclamación igual). Es un


pariente muy cercano del operador ==. También es un operador
binario y tiene la misma baja prioridad. Imaginemos que
queremos preguntarnos si el número de días que quedan hasta el
fin del mundo no es actualmente igual a cero →
Si la respuesta es "verdadera", eso nos da la oportunidad de ir al
teatro o visitar a nuestros amigos. Si la respuesta es "falsa" ...
bueno, eso es malo.

1.6.8 Pregunta: ¿es x mayor que y?

Puede hacer esta pregunta utilizando el operador > (mayor). Si


quieres saber que hay más ovejas negras que blancas, puedes
escribirlas de la siguiente manera →
La respuesta "verdadera" confirma; la respuesta "falsa" lo niega.

1.6.9 Pregunta: ¿es x mayor o igual que y?

El operador "mayor" tiene otra variante especial, no estricta, pero


se denota de manera diferente que en la notación aritmética
clásica:> = (mayor igual). Estos son dos personajes posteriores,
no uno.
Ambos operadores (estrictos y no estrictos), así como otros dos
que mencionamos en la siguiente sección, son operadores binarios
con enlace del lado izquierdo, y su prioridad es mayor que la
mostrada por == y ! = .
Si queremos saber si podemos dejar nuestro gorro o no en casa,
hacemos la siguiente pregunta →
69 – By Gerson
1.6.10 Pregunta: ¿es x menor que (o igual a) y?

Como probablemente ya haya supuesto, los operadores que


estamos utilizando en este caso son el operador <(menos) y su
hermano no estricto <= (menos igual).
Mire este sencillo ejemplo: vamos a verificar si existe el riesgo de
que la policía de carreteras nos multe (la primera pregunta es
estricta, la segunda no).

1.6.11 ¿Cómo usamos la respuesta que obtuvimos?

¿Qué podemos hacer con la respuesta que nos ha dado la


computadora? Bueno, tenemos al menos dos opciones: primero,
podemos memorizarlo (almacenarlo en una variable) y usarlo más
tarde. ¿Como hacemos eso?
Bueno, usamos una variable arbitraria de tipo int como esta →
Si la respuesta es "verdadera" porque el Valor1 es mayor o igual
que el Valor2 , la computadora asignará 1 a Respuesta
(posiblemente 1 sea diferente de cero). Si Value1 es menor
que Value2 , la variable Answer será 0.

1.6.12 La tabla de prioridad (una actualización)

70 – By Gerson
La segunda opción es más conveniente y mucho más común:
podemos usar la respuesta para tomar una decisión sobre el futuro
de nuestro programa. Utilizamos una instrucción especial para
este propósito y la abordaremos muy pronto.
Pero ahora necesitamos actualizar nuestra tabla de
prioridades. Ahora se ve así →

1.6.13 Condiciones y ejecuciones condicionales (1)

Ya sabemos cómo preguntar, pero aún no sabemos cómo hacer


un uso razonable de las respuestas. Necesitamos un mecanismo
que nos permita hacer algo si se cumple una condición y no
hacerlo de otra manera. Es como en la vida real: hacemos ciertas
cosas o no cuando una condición específica se cumple o no, por
ejemplo, salimos a caminar si hace buen tiempo o nos quedamos
en casa si no lo es.
Para tomar estas decisiones, el lenguaje C ++ nos ofrece una
instrucción especial. Debido a su naturaleza y su aplicación, se
llama instrucción condicional (o declaración condicional).
Hay varias variantes de la misma. Comenzaremos con el más
simple y avanzaremos hasta los más difíciles.

71 – By Gerson
La primera forma de una declaración condicional, que puede ver a
la derecha, está escrita de manera muy informal pero figurativa

Esta declaración condicional consta de los siguientes
elementos estrictamente necesarios en este orden (y solo este):

 si palabra clave
 paréntesis izquierdo (de apertura)
 una expresión (una pregunta o una respuesta) cuyo valor se
interpretará únicamente en términos de "verdadero" (cuando
su valor sea distinto de cero) y "falso" (cuando sea igual a cero)
 paréntesis derecho (de cierre)
 una instrucción (solo una, pero aprenderemos cómo lidiar con
esa limitación)

Entonces, ¿cómo funciona esa declaración?

 si la expresión true_or_not encerrada entre paréntesis


representa la verdad (es decir, su valor no es igual a cero), se
ejecutará la declaración detrás de esta condición
( do_this_if_true ).
 si la expresión true_or_not representa la falsedad (su valor es
igual a cero), la declaración detrás de esta condición se omite
y la siguiente instrucción ejecutada será la que sigue a la
declaración condicional.

1.6.14 Condiciones y ejecuciones condicionales (2)

En la vida real, a menudo expresamos una voluntad:

si hace buen tiempo, saldremos a caminar


luego almorzaremos

Afortunadamente, almorzar no es una actividad condicional y no


depende del clima.

72 – By Gerson
Sabiendo qué condiciones influyen en nuestro comportamiento y
asumiendo que tenemos funciones sin parámetros, GoForAWalk
() y HaveLunch () , podemos escribir el siguiente fragmento →

1.6.15 Condiciones y ejecuciones condicionales (3)

Ahora volvamos a nuestro amigo, el programador, que se duerme


cuando cuenta 120 ovejas.
La suspensión se implementa como una función especial
llamada SleepAndDream () .
Esta función no requiere ningún parámetro.

Podemos leerlo como: " si SheepCounter es mayor o igual a 120 ,


¡que se duerma y sueñe!"

1.6.16 Condiciones y ejecuciones condicionales (4)

Dijimos que solo puede haber una declaración después de


la declaración if .
Cuando tenemos que ejecutar condicionalmente más de una
instrucción, necesitamos usar las llaves { y } que crean una
estructura conocida como una declaración compuesta o
(mucho más simple) un bloque . El compilador trata el bloque
como una sola instrucción.
Así es como podemos usar para eludir la limitación de la
declaración if .
73 – By Gerson
Seamos un poco más amables con nuestro programador →

1.6.17 Condiciones y ejecuciones condicionales (5)

Ahora es el momento de algunos comentarios estilísticos. Escribir


los bloques como en el ejemplo anterior es, por supuesto,
sintácticamente correcto, pero muy poco elegante. Puede hacer
que el texto de nuestro programa se ejecute fuera del margen
derecho del editor.
Hay varias formas de codificar los bloques. No intentaremos
argumentar que algunos son mejores que otros, pero vamos a
utilizar el llamado estilo K&R .
Las letras K y R son las iniciales de los creadores del lenguaje C,
el Sr. Kernighan y el Sr. Ritchie (el lenguaje C es el ancestro aún
vivo de C ++). Usaron este estilo en sus artículos y creemos que
es prudente seguirlos.
El mismo fragmento, escrito de acuerdo con el estilo K&R, se ve
así →

Tenga en cuenta que un bloque ejecutado condicionalmente


está sangrado : mejora la legibilidad del programa y manifiesta
su naturaleza condicional.
En la siguiente sección, analizaremos otra variante de la
declaración condicional, que también le permite realizar una
acción solo cuando no se cumple la condición.

Ahora alimente a sus perros pastores, por favor. Han estado


esperando tanto tiempo que comienzan a mirar a las ovejas.

74 – By Gerson
1.7.1 Entrada y salida

Pasemos un tiempo en dos características importantes y


extremadamente útiles que podemos usar para conectar la
computadora con el mundo exterior.
Cuando los datos se mueven en la dirección del humano (usuario)
al programa de computadora, se llama entrada . Los datos
transferidos en la dirección opuesta, es decir, de la computadora
al humano, se denominan salida .
Ya hemos aprendido sobre una entidad útil que sirve para generar
datos, ¿puedes recordar su nombre? Sí, es la secuencia cout , y la
usamos junto con el operador << en el primer programa que
escribimos en el lenguaje C ++, justo al comienzo de este curso.
El operador << en sí mismo a veces se denomina operador de
inserción ya que inserta una cadena de caracteres en el
dispositivo de caracteres (por
ejemplo, una consola).

Las capacidades
de corte reales son mucho
más impresionantes: es
capaz de escribir los datos de
prácticamente cualquier tipo
en la pantalla de una
computadora.
Entonces, ¿qué hacemos si
queremos generar el valor de
tipo int o float , o char , no solo una cadena simple?

75 – By Gerson
1.7.2 Salida (1)

Para hacer esta y otras tareas más complejas, necesitamos usar


cualquiera de los flujos de salida asociados con la pantalla (más
formalmente: con la consola ) y enviar un valor de una variable
allí.
cout es uno de estos flujos y está listo para funcionar sin ninguna
preparación especial; solo necesita el nombre del archivo de
encabezado.

Si desea imprimir el valor de una variable entera en la pantalla, lo


único que tiene que hacer es enviarlo a la secuencia cout a través
del operador <<, que indica la dirección deseada de transferencia
de datos.
Tanto el operador << como la secuencia cout son responsables de
dos acciones importantes:

 Convertir la representación interna (máquina) del valor entero


en una forma aceptable para los humanos
 transferir el formulario convertido al dispositivo de salida, por
ejemplo, consola

Las transmisiones son herramientas muy potentes y convenientes


tanto para entrada como para salida. Pueden generar fácilmente
varios valores de diferentes tipos y mezclarlos con el
texto. También pueden ingresar fácilmente muchos valores a la
vez.
Veamos las transmisiones en algunas aplicaciones. El primero es
trivial: utilizamos cout para imprimir un valor de
una variable int . Lo hacemos así →
Podemos esperar que una cadena que consta de los caracteres '1',
'1' y '0' aparecerá en la pantalla de inmediato.

76 – By Gerson
1.7.3 Salida (2)

También puede conectar más de un operador << en


una declaración cout y cada uno de los elementos impresos puede
ser de un tipo diferente y una naturaleza diferente.
Mira el ejemplo de la derecha. Estamos usando un literal de
cadena (como el primer elemento) y una variable
entera (como el último elemento) en una operación cout .

En este ejemplo →
este fragmento de código da como resultado la cadena Sheep
contada hasta el momento: 123 impresos en la pantalla.

1.7.4 Salida (3)

Una expresión también es un elemento legal de corte . El ejemplo


de la derecha muestra uno de esos casos →

1.7.5 Salida (4)

Si desea que un valor de tipo int se presente como un número


hexadecimal de punto fijo, debe usar el
denominado manipulador . Un manipulador es un tipo especial
de entidad que le dice a la secuencia que el formulario de datos
debe cambiarse de inmediato. Todos los elementos emitidos
77 – By Gerson
después de la activación del manipulador se presentarán en la
forma deseada.
Un manipulador que está diseñado para cambiar la secuencia a un
modo hexadecimal se llama hexadecimal . El fragmento a la
derecha generará una cadena que consta de los caracteres 'F' y
'F'.
Técnicamente, un manipulador es una función que cambia una de
las propiedades de la secuencia de salida, llamada campo
base . La propiedad se usa para determinar qué número se debe
usar como base durante la conversión de cualquier valor int en
texto legible para humanos.
Hay dos hechos importantes que debe comprender aquí:

1. cualquier manipulador comienza su trabajo desde el punto en


el que se colocó y continúa su trabajo incluso después del final
de la declaración cout ; termina de funcionar solo cuando otro
manipulador cancela su acción;
2. el nombre del manipulador puede estar en conflicto con
cualquier otro nombre declarado por el programador; por
ejemplo, puede tener su propia variable llamada hex que
podría ocultar el nombre del manipulador; dichos conflictos se
resuelven mediante un mecanismo especializado llamado
espacio de nombres; Más sobre esto más adelante.

1.7.6 Salida (5)

El ejemplo de la derecha → muestra cómo los manipuladores


comienzan y terminan su trabajo.
Nota: el manipulador dec cambia la secuencia a una forma
decimal. No lo tenemos explícitamente en la mayoría de los casos,
ya que el decimal es el modo de trabajo predeterminado para
las secuencias de salida.
El fragmento generará las tres muestras del mismo valor:

78 – By Gerson
1. FF como una representación hexadecimal de 255 (como efecto
del manipulador hexadecimal)
2. FF nuevamente (la activación hexadecimal anterior todavía
funciona aquí)
3. 255 (como resultado de la activación del manipulador dec)

1.7.7 Salida (6)

El manipulador oct cambia el flujo al modo octal.


El fragmento a la derecha → mostrará "377" en la
pantalla. ¿Puedes adivinar por qué?
Sí, 255 en decimal es 377 en octal. Bien hecho.

1.7.8 Salida (7)

Los tres manipuladores que le mostramos anteriormente son solo


uno de los métodos (probablemente el más simple) para acceder
a la propiedad de campo base. Puede lograr el mismo efecto
utilizando el manipulador setbase , que instruye directamente
a la secuencia sobre qué valor base debe usar durante la
conversión.
Los únicos valores aceptables para el parámetro setbase son 8, 10
y 16. Esperemos que haya entendido el propósito de los tres
manipuladores. Si no, regresa y míralos un poco
más. Eventualmente vendrá a ti ... esperamos.
El programa de la derecha → muestra el uso
del manipulador setbase .
79 – By Gerson
Nota: requiere un archivo de encabezado llamado iomanip (los
tres manipuladores anteriores no lo hacen).

1.7.9 Salida (8)

En general, los flujos de salida (incluido cout ) pueden reconocer


el tipo del valor impreso y actuar en consecuencia, es decir,
utilizarán una forma adecuada de presentación de datos para los
valores char y float.
El fragmento a la derecha → hará que la transmisión imprima el
siguiente texto en la pantalla:
X-2.5

1.7.10 Salida (9)

cout puede reconocer el tipo real de su elemento incluso


cuando es un efecto de una conversión.
Discutiremos las conversiones más tarde, pero por ahora solo
queremos mencionar que una frase escrita como:

80 – By Gerson
(newtype) expr

Cambia el tipo de la expresión expr en el tipo newtype.


Lo que significa es que podemos ver el código ASCII de cualquier
carácter almacenado dentro de una variable char y viceversa, o
ver un carácter cuyo código ASCII se coloca dentro de
una variable int .
El fragmento a la derecha → muestra el siguiente texto en la
pantalla:

X 88 88 X

1.7.11 Salida (10)

A veces podemos querer (y a veces tenemos que) romper la


línea que se envía a la pantalla.
Cuando presentamos muchos resultados diferentes uno por uno
en la misma línea de texto, no se ve bien y no querrá verlo. Una
línea está bien, pero mil líneas escritas así te dejarán ciego.

Podemos romper la línea de dos maneras. Primero, podemos usar


uno de los caracteres de control llamado "nueva línea" y codificado
como \ n (nota: usamos dos caracteres para escribirlo, pero el
compilador lo ve como un solo carácter; no deje que lo engañe).
El carácter de nueva línea obliga a la consola a completar la
línea actual y comenzar una nueva.

Podemos lograr exactamente el mismo efecto utilizando un


manipulador llamado endl (como "línea final").
El fragmento a la derecha → ilustra ambos métodos y hace que la
consola muestre las siguientes tres líneas de texto:

81 – By Gerson
1
2
3

1.7.12 Salida (11)

Los flujos de salida intentan emitir valores flotantes en una forma


más compacta, y se toma una decisión para cada valor flotante
impreso.
Por ejemplo, el siguiente fragmento:

float x = 2.5, y = 0.0000000025;


cout << x << endl << y << endl;

producirá el siguiente resultado en la pantalla:

2.5
2.5e-009

El primero se conoce como punto fijo, mientras que el segundo


es científico (puede recordar ese término cuando hablamos de
literales de coma flotante).

Hay dos manipuladores diseñados implícitamente para elegir la


salida deseada según las necesidades y preferencias del usuario:
Sus nombres son:

 fijo,
 científico.

82 – By Gerson
Tenga en cuenta que inicialmente las transmisiones no están
configuradas ni en el modo fijo ni en el científico, sino en el modo
predeterminado (automático).
El uso de cualquiera de los manipuladores anteriores cambia el
flujo al modo deseado; sin embargo, no puede volver al método
predeterminado de procesamiento de flotantes utilizando ninguno
de los manipuladores.
Debe usar una función especial (pero aquí no vamos a hablar de
esto intencionalmente).
El programa de la derecha → generará el siguiente texto:

2.500000 0.000000
2.500000e + 000 2.500000e-009

1.7.13 Entrada (1)

Por supuesto, igualmente importante como salida de datos es la


entrada de datos . En realidad, es difícil imaginar un programa
no trivial que no requiera ningún dato del usuario, aunque puede
hacer lo siguiente:

 codificar todos los datos necesarios dentro del código fuente


(que a veces se denomina codificación rígida )
 cuando necesite repetir la ejecución del programa con otros
datos, simplemente modifique el programa, compílelo y
ejecútelo nuevamente.

83 – By Gerson
Esta no es una solución particularmente conveniente . Es
mucho mejor obtener la información del usuario, transferirla al
programa y luego usarla para los cálculos. Entonces, ¿cómo un
programa de lenguaje C ++ obtiene datos de un humano y los
almacena en variables?
La forma más simple es invertir mentalmente la dirección de la
transferencia y reconocer eso para la entrada de datos:

 usamos cin stream en lugar de cout


 usamos >> operador en lugar de <<.

Por cierto, el operador >> a menudo se denomina operador de


extracción .
La corriente cin , junto con el operador de extracción, es
responsable de:

 transferir la forma legible por humanos de los datos desde el


dispositivo de entrada, por ejemplo, una consola
 convirtiendo los datos en la representación interna (máquina)
del valor que se está ingresando.

1.7.14 Entrada (2)

Imagine que queremos preguntarle al usuario sobre el número


máximo de ovejas que queremos contar antes de que el
programador se duerma.
El usuario ingresa el valor desde el teclado y el programa lo
almacena en una variable específica ( MaxSheep ). Esa
afirmación se ve así →
Probablemente vea la similitud con la emisión de datos
usando cout : tenemos una secuencia, tenemos un operador y
tenemos una variable.
En este punto, las similitudes terminan y comienzan las
diferencias. Primero, el argumento para cout puede no ser una
variable. También puede ser una expresión. Echar un vistazo:

84 – By Gerson
cout << 2 * i;

Aquí queremos que se imprima el valor duplicado de i , y eso


es factible. Usando un flujo de entrada,
necesitamos especificar explícitamente la variable que
puede almacenar los datos ingresados por el usuario.

1.7.15 Entrada (3)

Ahora le mostraremos un programa simple pero completo que


hace lo siguiente:

 solicita al usuario que ingrese un único valor entero,


 lo cuadra,
 imprime el resultado con un comentario apropiado.

Analizar este programa no debería ser un problema para ti ...


¡esperamos!
source_01_07_14_squares.cpp

1.7.16 Entrada (4)

¿Entonces prefieres raíces cuadradas a cuadrados? No hay


problema, pero debemos recordar dos cosas: primero, no existe
un operador de raíz cuadrada ; y segundo, que las raíces
cuadradas de los números negativos no existen .
85 – By Gerson
Podemos resolver el primer problema al encontrar una función que
sepa cómo calcular la raíz. Este tipo de función existe y toma el
argumento del tipo flotante .
El resultado también es un flotante (por supuesto, el cuadrado de
un número entero sigue siendo un número entero, pero la raíz de
cualquier número no siempre es un número entero, como la raíz
cuadrada de 2).

La función que vamos a usar se llama sqrtf (flotante de raíz


cuadrada) y necesita exactamente un argumento. Ah, una cosa
más: para usar esta función, debe incluir un archivo de
encabezado llamado cmath .

Necesitamos lidiar con números negativos también. Si eres


descuidado e ingresas un número negativo, el programa
simplemente te ignorará a ti y a tu entrada por completo. Puede
que no sea cortés, pero al menos no intentará doblegar las reglas
de las matemáticas. El enunciado condicional decidirá si vemos el
resultado o no.

Ahora es el momento de centrarse en el uso de datos de punto


flotante y la función sqrtf .

Aquí está el programa completo →

source_01_07_15_squareroots.cpp

86 – By Gerson
2.1.1 La declaración condicional (1)
La declaración condicional: más condicional que antes
Concluimos nuestra última discusión sobre declaraciones
condicionales con la promesa de que pronto presentaremos una
forma más compleja y flexible. Comenzamos con una frase simple
que decía: "Si hace buen tiempo, saldremos a caminar". Nota: no
dice nada sobre lo que sucederá si llueve gatos y perros. Solo
sabemos que no iremos afuera, pero lo que podríamos hacer en
cambio no se sabe. Es posible que también queramos planificar
algo en caso de mal tiempo.
En este caso, podemos decir, por ejemplo: "Si hace buen tiempo,
saldremos a caminar, de lo contrario iremos a un teatro". Esta
frase hace que nuestros planes sean más resistentes a los
caprichos del destino: sabemos lo que haremos si se cumplen las
condiciones y sabemos lo que haremos si no todo sigue nuestro
camino. En otras palabras, tenemos un plan "B" .
El lenguaje "C ++" nos permite expresar estos planes
alternativos. Lo hacemos con una segunda forma, un poco más
compleja, de la declaración condicional: aquí está →

87 – By Gerson
Así que ahora tenemos una nueva palabra: de lo contrario , esta
es una palabra clave (palabra reservada). La declaración que
comienza con "else" nos dice qué hacer si no se cumple la
condición especificada para if.

Entonces la ejecución if-else va de la siguiente manera:


si la condición es "verdadera" (su valor no es igual a cero) se
ejecuta perform_if_condition_true y la instrucción condicional
llega a su fin;
si la condición es "falsa" (es igual a cero) se
ejecuta perform_if_condition_false y la instrucción condicional
llega a su fin.

2.1.2 La declaración condicional (2)

Al usar esta forma de la declaración condicional, podemos


describir nuestros planes así →

88 – By Gerson
2.1.3 La declaración condicional (3)

Al igual que las otras instrucciones más simples que ya hemos


encontrado, tanto if como else pueden contener solo una
declaración . Si desea agregar más de una instrucción, debe usar
un bloque, como en el ejemplo de la derecha →

2.1.4 La declaración condicional (4)

Ahora es el momento de discutir dos casos especiales de la


declaración condicional. Primero, piense cuándo la instrucción
colocada después de if es otra if .
Así que esto es lo que tenemos planeado para este domingo: si
hace buen tiempo, iremos a buscar un buen restaurante. Si
encontramos un buen restaurante, almorzaremos allí. De lo
contrario, comeremos un sándwich. Si hace mal tiempo, iremos al
teatro. Si no hay boletos, iremos de compras al centro comercial
más cercano.
Complicado, ¿verdad? Escribamos lo mismo en lenguaje "C
++". Ahora mira el código de la derecha →

Dos puntos importantes a tener en cuenta:

89 – By Gerson
 el uso de la declaración if como esta se conoce
como anidamiento ; recuerde que todo lo demás se refiere al
primero más cercano si eso no coincide con ningún otro ; así es
como los if sy else s se emparejan;
 mire cómo la sangría mejora la legibilidad y enfatiza el
anidamiento de las declaraciones condicionales
internas. Bonita, ¿no es así?

2.1.5 La declaración condicional (5)

Este segundo caso especial es algo similar a la anidación, pero


con una diferencia importante. Nuevamente, vamos a cambiar
nuestros planes y expresarlos de la siguiente manera: “Si hace
buen tiempo, saldremos a caminar, de lo contrario si conseguimos
entradas, iremos al teatro, de lo contrario si hay mesas libres En
el restaurante, iremos a almorzar. Si todo falla, volveremos a casa
y jugaremos al ajedrez ”. ¿Puedes ver cuántas alternativas hemos
enumerado aquí? Escribamos el mismo escenario en lenguaje "C
++" →
Cuando ensamblas declaraciones if posteriores , se
llama cascada . Observe nuevamente cómo la sangría mejora la
legibilidad del código.
Ahora ponga declaraciones condicionales en el fondo de su mente,
deje que su subconsciente haga el trabajo y piense en nuestros
90 – By Gerson
viejos amigos: tipos int , char y float . Es hora de conocer a sus
hermanos.

2.2.1 No solo el int es un int (1)

¿No sería genial si la vida del desarrollador pudiera organizarse lo


suficientemente bien con solo type int para operar con enteros,
type char para manipular caracteres y type float para trabajar con
cálculos de punto flotante? Desafortunadamente, un repertorio de
tipos tan estrecho plantea algunos problemas.
La mayoría de las computadoras en uso actual almacenan
entradas que usan 32 bits (4 bytes); Esto significa que podemos
operar las entradas desde el rango de [-2147483648 a
2147483647]. Puede suceder que:

 no necesitamos valores tan grandes ; si contamos ovejas,


es poco probable que tengamos dos mil millones de ellas,
entonces, ¿por qué desperdiciar la mayoría de estos 32 bits si
ni siquiera los necesitamos?
 que necesitamos valores mucho más grandes , por
ejemplo, si tenemos la intención de calcular el número exacto

91 – By Gerson
de los seres humanos que viven en la Tierra; en este caso
necesitamos más de 32 bits para representar ese número,
 solo necesitamos dos valores diferentes para representar los
estados de activación / desactivación o las condiciones de
verdadero / falso que en realidad no tienen una interpretación
numérica (aunque podemos hacer presunciones para ellos y
usar 1 como 'activado' y 0 como 'desactivado', por ejemplo)
 el número de habitantes en la Tierra nunca será un número
negativo ; Parece un desperdicio real que nunca se use hasta
la mitad del rango permitido.

2.2.2 No solo el int es un int (2)

Por estas razones, el lenguaje "C ++" proporciona algunos


métodos para definir exactamente cómo pretendemos almacenar
números grandes / pequeños. Esto permite que el compilador
asigne memoria, ya sea más pequeña de lo habitual (por ejemplo,
16 bits en lugar de 32) o más grande de lo habitual (por ejemplo,
64 bits en lugar de 32). También podemos declarar que
garantizamos que el valor almacenado en la variable será no
negativo. En este caso, el ancho del rango de la variable no
cambia, sino que se desplaza hacia los números positivos.
Esto significa que en lugar del rango -2147483648 a 2147483647
tenemos el rango 0 a 4294967295.

2.2.3 No solo el int es un int (3)

Para especificar nuestros requisitos de memoria, podemos usar


algunas palabras clave adicionales llamadas modificadores:

92 – By Gerson
 largo : solía declarar que necesitamos un
rango de entradas más amplio que el estándar;
 short : se utiliza para declarar que
necesitamos un rango de entradas más estrecho
que el estándar;
 unsigned : se utiliza para declarar que una
variable se usará solo para números no negativos; esto puede
sorprenderlo, pero podemos usar este modificador junto con el
tipo char ; Lo explicaremos pronto.

Veamos algunos ejemplos.

2.2.4 No solo el int es un int (4)

La variable Contador usará menos (o tantos) bits que


el int estándar (por ejemplo, podría tener 16 bits de longitud; en
este caso, el rango de la variable se suprimirá al rango de [-32768
a 32767]).

La palabra int puede omitirse ya que se considera que todas


las declaraciones que carecen de un nombre de tipo
especifican int de forma predeterminada , como esta:

Contador corto;

2.2.5 No solo el int es un int (5)

La variable Ants utilizará más (o la misma cantidad) de bits que


el int estándar (por ejemplo, 64 bits, por lo que puede usarse para
almacenar números del rango de [-9223372036854775808 a
9223372036854775807]. ¿Puede leer esos números tan grandes?
Nota: podemos omitir nuevamente la palabra int :

hormigas largas;

93 – By Gerson
2.2.6 No solo el int es un int (6)

Si decidimos que una variable nunca será un valor negativo,


podemos usar el modificador sin signo →
Como antes, podemos omitir el int :

Positivo sin signo ;

2.2.7 No solo el int es un int (7)

También podemos mezclar algunos de los modificadores juntos:


eche un vistazo →

Podemos eliminar la palabra int y la declaración conservará su


significado:

sin firmar largo HugeNumber;

2.2.8 No solo el int es un int (8)

Un ejemplo más modesto es así →

Su forma equivalente es:

corderos cortos sin signo;

2.2.9 No solo el int es un int (9)

Los modificadores largos y cortos no deben usarse junto


con el tipo char (¿por qué?) Y no deben usarse simultáneamente
en una sola declaración. Pero no hay nada que nos impida usar el
modificador sin signo con una variable de tipo char. ¿Qué
obtenemos de tal declaración?
No olvide que no podemos omitir la palabra char . La mayoría de
los compiladores en uso actual asumen que los caracteres se
94 – By Gerson
almacenan usando 8 bits (1 byte). Eso puede ser suficiente para
almacenar un valor pequeño, como la cantidad de meses o incluso
el día del mes. Si tratamos la variable char como un número
entero con signo, su rango sería [-128 a 127].
Si no necesitamos ningún valor con signo (como en el ejemplo
aquí →), su rango cambia a [0 a 255]. Esto puede ser suficiente
para muchas aplicaciones y también puede resultar en ahorros
significativos en el uso de la memoria.
Ahora presta atención a esto: hasta ahora hemos usado literales
enteros, asumiendo que todos ellos son del
tipo int . Generalmente es cierto, pero hay algunos casos en que
el compilador reconoce literales de un tipo largo . Esto sucederá
si:

 un valor literal va más allá del rango aceptable de tipo int ;


 se agrega una letra L o l al literal, como 0L o 1981l; ambos
literales son de tipo largo.

2.2.11 Flotadores y sus rasgos

Hablamos un poco acerca de cómo la suma de computadoras no


siempre es conmutativa. ¿Puedes adivinar por qué es
esto? Imagine que tiene que sumar una gran cantidad de valores
de coma flotante; algunos de ellos son muy grandes y otros muy
pequeños (cercanos a cero).
Si se agrega un valor flotante muy pequeño a uno muy grande,
podría terminar con una sorpresa. Volvamos al ejemplo anterior:
supongamos que nuestra computadora solo guarda 8 dígitos
precisos de cualquier flotante .
Si agregamos estos dos flotadores, probablemente obtendremos

11111110656.000000

como resultado. El valor menor simplemente desaparece sin dejar


rastro.

95 – By Gerson
No podemos evitar estos efectos cuando sumamos / restamos los
números de tipo flotante (y también el doble , porque también
están afectados por este problema).
Este fenómeno es una de las llamadas anomalías numéricas .

2.2.12 En memoria de George Boole

George Boole (1815-1864) fue un matemático, filósofo y lógico


inglés y estamos hablando de él por una razón muy
importante. Uno de sus logros más importantes fue la lógica
algebraica , referida por el propio Boole como "las leyes del
pensamiento". La lógica algebraica no funciona con números, sino
solo con dos valores de verdad , y no utiliza operaciones
aritméticas estándar como la suma y la multiplicación, sino
la conjunción , la disyunción y la negación .
Teniendo en cuenta el hecho de que prácticamente todas las
computadoras modernas están construidas utilizando los
teoremas de Boole, podemos decir sin exagerar que Boole fue en
realidad uno de los fundadores de TI.
Hay un tipo en el lenguaje "C ++" cuyo nombre conmemora a
George Boole, el tipo bool .
Es un tipo muy intrigante. Las variables de este tipo solo pueden
almacenar dos valores distintos: verdadero y falso. Nota: todas
estas palabras nuevas ( bool , verdadero y falso ) son palabras
clave. No te olvides de eso.

Mira el ejemplo de la derecha →


Hemos declarado una variable allí. Ni su nombre ni su valor
requieren comentarios adicionales. Hay muchos contextos donde
esta variable puede ser útil. Uno de los más espectaculares es el
siguiente:

96 – By Gerson
if (developer_is_hungry) {
HaveLunch ();
developer_is_hungry =! developer_is_hungry;
}

El signo de exclamación que hemos usado en la tarea es un


operador de negación. Es un operador de prefijo unario que
cambia el valor lógico de sus argumentos: debido al operador,
verdadero se convierte en falso y viceversa. Como puede ver,
almorzar cambia el estado lógico de uno de los factores más
importantes del bienestar de un programador.
Para ser sincero, el tipo bool es solo una variante muy especial del
tipo int. Es muy corto (las variables de este tipo ocupan solo 8
bits, lo que todavía es demasiado, porque un bit sería
suficiente). Se comporta como un int dentro de expresiones
( verdadero es equivalente a 1 mientras que falso es
equivalente a 0 ).
Volveremos a este tipo y sus valores pronto, y también al álgebra
y los operadores lógicos de George Bool.

2.3.1 Dos programas simples (1)

Ahora le mostraremos algunos programas simples pero


completos. No los explicaremos en detalle, porque creemos que
los comentarios dentro del código son guías suficientes.
Todos estos programas resuelven el mismo problema: encuentran
el mayor de varios números y lo imprimen.
Comencemos con el caso más simple: cómo identificar el mayor
de dos números.
97 – By Gerson
2.3.2 Dos programas simples (2)

Ahora intentemos encontrar el mayor de tres


números . Encontramos el mayor de los dos primeros y lo
comparamos con el tercero. Aquí vamos →

98 – By Gerson
2.3.3 Dos programas simples (3)

En este punto, debería poder escribir un programa que encuentre


el mayor de cuatro, cinco, seis o incluso diez números. Ya conoce
el esquema, por lo que la extensión del programa no necesita
ser particularmente compleja.

Pero, ¿qué sucede si le pedimos que escriba un programa que


encuentre el mayor de cien números? ¿Te imaginas el código?

99 – By Gerson
Necesitarás cientos de declaraciones de variables de tipo int . Si
cree que puede hacer eso, intente imaginar escribir un programa
que encuentre el mayor de un millón de números.

Imagine el código que contiene 99 declaraciones condicionales y


cien declaraciones cin .

2.3.4 Dos programas simples (4)

Ignoremos el lenguaje "C ++" por el momento e intentemos


analizar el problema sin pensar en la programación. En otras
palabras, intentemos escribir el algoritmo , y cuando estemos
contentos con él, intentaremos implementarlo.
Vamos a utilizar un tipo de notación que no es un lenguaje de
programación (no puede compilarse ni ejecutarse), pero es
formalizado, conciso y legible. Llamamos a este pseudocódigo .
Hay un ejemplo de pseudocódigo a la derecha → Míralo. ¿Que esta
pasando?

Primero, podemos simplificar nuestro programa si, al comienzo del


código, asignamos la variable max con un valor que será más
pequeño que cualquiera de los números ingresados. Usaremos
-999999999
para este propósito.

En segundo lugar, suponemos que nuestro algoritmo no sabe de


antemano cuántos números se entregarán al
programa. Esperamos que el usuario ingrese tantos números
como desee: el algoritmo funcionará igualmente bien con cien o
mil números. ¿Como hacemos eso? Bueno, hacemos un trato con
el usuario: cuando se ingresa el valor -1, será una señal de que
no hay más datos y que el programa debe finalizar su trabajo. De
lo contrario, si el valor ingresado no es igual a - 1, el programa
leerá otro número y así sucesivamente.

100 – By Gerson
El truco se basa en la suposición de que cualquier parte del código
se puede realizar más de una vez, de hecho, tantas veces como
sea necesario.
Realizar una cierta parte del código más de una vez se
llama bucle . Probablemente ya sabes lo que es un bucle. Vea, los
pasos 2 a 5 hacen un bucle. ¿Podemos usar una estructura similar
en el programa escrito en el lenguaje "C ++"? Si podemos. Y les
contaremos a todos pronto.

2.3.5 El ciclo llamado "while" (1)

¿Cuánto tiempo suele necesitar para lavarse las manos? ¿Diez


segundos? ¿Un minuto? ¿Más? ¿O depende de qué tan sucias
estén tus manos? Cuando tus manos están muy sucias, las lavas
durante mucho tiempo. De lo contrario, lleva menos
tiempo. ¿Estaría de acuerdo con esta afirmación →?

Tenga en cuenta que este registro también declara que si sus


manos están limpias, no las lavará en absoluto.

2.3.6 El ciclo llamado "while" (2)

101 – By Gerson
Así que ahora has aprendido uno de los bucles disponibles en el
lenguaje "C ++". En general, el bucle se ve así →

Si cree que se parece a la instrucción if , tiene toda la razón. De


hecho, solo hay una diferencia sintáctica: reemplazamos la
palabra "if" por la palabra "while".
La diferencia semántica es más importante: cuando se cumple la
condición, si realiza sus declaraciones solo una vez; while repite
la ejecución siempre que la condición se evalúe como "verdadera".

2.3.7 El ciclo llamado "while" (3)

Veamos algunos comentarios específicos:

 si quieres , mientras que para ejecutar más de una instrucción,


debe (como si ) utiliza un bloque - echar un vistazo aquí →
 una o más instrucciones ejecutadas dentro del bucle se llaman
cuerpo del bucle
 Si la condición es "falsa" (igual a cero) la primera vez, el cuerpo
no se ejecuta ni una sola vez (¿recuerda la analogía de no tener
que lavarse las manos si no están sucias?)
 el cuerpo debería poder cambiar el valor de la condición, porque
si la condición es verdadera al principio, el cuerpo podría correr
continuamente hasta el infinito (observe que el acto de lavado
cambia el estado de impureza)

102 – By Gerson
2.3.8 El ciclo llamado "while" (4)

Aquí hay un ejemplo de un ciclo que no puede finalizar su


ejecución. Este bucle imprimirá infinitamente "Estoy atrapado
dentro de un bucle" en la pantalla.

2.3.9 El ciclo llamado "while" (5)

Volvamos al algoritmo del que hablamos recientemente. Le


mostraremos cómo usar el ciclo recién aprendido. Por cierto,
queremos presentarte una novedad más. Hasta ahora
hemos declarado variables en un lugar y les hemos asignado
valores en otro.
Podemos combinar estos dos pasos declarando la variable y
asignando el valor al mismo tiempo . Esto se hace agregando
el signo = seguido de una expresión cuyo valor se asigna a la
variable en el momento de su creación. Por ejemplo, si necesita
una variable que necesita el valor de cero, puede hacer esto →

2.3.10 El ciclo llamado "while" (6)


Así que ahora tenemos una nueva palabra en nuestro
vocabulario: la parte de la declaración en el lado derecho del signo
= se llama iniciador . El iniciador que viste antes era literal, pero
puedes usar expresiones más complejas como estas →
Usaremos iniciadores a menudo. Son muy convenientes y útiles.

103 – By Gerson
2.3.11 El ciclo llamado "while" (7)

Analiza este programa cuidadosamente. Localice el cuerpo del


bucle y descubra cómo se sale del cuerpo.
Comprueba cómo el código de la derecha implementa el algoritmo
del que hablamos anteriormente.

source_02_03_11_max_c.cpp

2.3.12 El ciclo "while" en algunos ejemplos (1)

Este programa cuenta los números pares e impares que provienen


del teclado. Échale un vistazo.

104 – By Gerson
Ciertos fragmentos se pueden simplificar sin cambiar el
comportamiento del programa. Echa un vistazo a la siguiente
diapositiva.

source_02_03_12_odds.cpp

2.3.13 El ciclo "while" en algunos ejemplos (2)

105 – By Gerson
Intente recordar cómo el lenguaje "C ++" interpreta la verdad de
una condición y tenga en cuenta que estas dos formas son
equivalentes.

2.3.14 El ciclo "while" en algunos ejemplos (3)

La condición que verifica si un número es impar puede codificarse


así →

2.3.15 El ciclo "while" en algunos ejemplos (4)

Suponemos que nada te sorprende, ¿verdad? Pero hay dos cosas


que podemos escribir de manera más compacta. Primero, la
condición del bucle while.

106 – By Gerson
2.3.16 El ciclo "while" en algunos ejemplos (5)

Otro cambio requiere que tengamos algún conocimiento de cómo


funciona el post-decremento. Lo usaremos para compactar
nuestro programa una vez más.

2.3.17 El ciclo "while" en algunos ejemplos (6)

Estamos convencidos de que esta es la forma más simple de este


programa, pero puede desafiarnos si se atreve.

2.3.18 El ciclo "hacer en ingles do" o hacerlo al menos una


vez (1)

Ya sabemos que el tiempo bucle tiene dos características


importantes:

 comprueba la condición antes de ingresar al cuerpo,


 el cuerpo no se ingresará si la condición es falsa.

Estas dos propiedades a menudo pueden causar complicaciones


innecesarias. Por esta razón, hay otro bucle en el lenguaje “C ++”,
que actúa como una imagen de espejo del tiempo de bucle.
107 – By Gerson
Decimos esto porque en ese bucle:

 la condición se verifica al final de la ejecución del cuerpo,


 El cuerpo del bucle se ejecuta al menos una vez, incluso si no
se cumple la condición.

Este bucle se llama do loop. Su sintaxis simplificada se enumera


aquí →
Si desea ejecutar un cuerpo que contenga más de una declaración,
debe usar un bloque.

2.3.19 El ciclo "hacer" o hacerlo al menos una vez (2)

Regresemos al programa que busca el mayor número. En primer


lugar, usaremos el ciclo " do " en lugar de " while " para fines de
enseñanza. En segundo lugar, eliminamos la vulnerabilidad que
implica la excesiva confianza en la buena voluntad del

108 – By Gerson
usuario. Nuestro nuevo programa no se engañará al ingresar el
valor de -1 como primer número. Aquí está el programa →
Echar un vistazo. Utilizamos la variable de contador para contar
los números ingresados para poder indicarle al usuario que no
podemos buscar el mayor número si no se proporciona ningún
número.
Como tenemos que leer al menos un número , tiene sentido
usar el bucle do . Utilizamos este enfoque en el programa.

source_02_03_19_maxdo.cpp

109 – By Gerson
2.3.20 "para in ingles for " - el último bucle (1)

El último tipo de bucle disponible en el lenguaje "C ++" proviene


del hecho de que a veces es más importante contar los "giros"
del bucle que verificar las condiciones.
Imagine que el cuerpo de un bucle debe ejecutarse exactamente
cien veces. Si desea utilizar el tiempo de bucle para tal fin, puede
ser algo como esto:
Podemos distinguir tres elementos independientes allí:

 la iniciación del contador


 la comprobación de la condición
 la modificación del contador

2.3.21 "para" - el último bucle (2)

Es posible crear algo así como un esquema generalizado para este


tipo de bucles, aquí está →
110 – By Gerson
Esta forma de codificar el bucle es muy común, por lo que hay una
forma especial y breve de escribirlo en lenguaje "C ++".

2.3.22 "para" - el último bucle (3)

Aquí hemos reunido las tres partes decisivas. El ciclo es claro y


fácil de entender. Su nombre es para .

2.3.23 "para" - el último bucle (4)

El bucle for puede tomar la siguiente forma →


La variable utilizada para contar los giros del bucle a menudo se
denomina variable de control .
Tenga en cuenta que la variable de control no tiene que declararse
antes de usarse en el ciclo for . Se puede declarar dentro del

111 – By Gerson
bucle, pero en este caso estará disponible durante y solo
durante la ejecución del bucle.
Aquí hay un ejemplo:

para (int i = 0; i <100; i ++) {


/ * el cuerpo va aquí * /
}

2.3.24 "para" - el último bucle (5)

El bucle for tiene una singularidad interesante. Si omitimos


cualquiera de sus tres componentes, se presume que hay un 1 allí.

Una de las consecuencias de esto es que un bucle escrito de esta


manera es un bucle infinito (¿sabes por qué?).

Bueno, la expresión condicional no está allí, por lo que se supone


automáticamente que es verdadera. Y debido a que la condición
nunca se vuelve falsa, el ciclo se vuelve infinito.

112 – By Gerson
2.3.25 "para" - el último bucle (6)

Veamos un programa corto cuya tarea es escribir algunas de las


primeras potencias de 2.
La variable exp se usa como una variable de control para el bucle
e indica el valor actual del exponente. La exponenciación misma
se reemplaza por la multiplicación por 2. Como 2 0 es igual a 1,
entonces 2 ∙ 1 es igual a 2 1 , 2 ∙ 2 1 es igual a 2 2 y así
sucesivamente.
Responda esta pregunta: ¿cuál es el máximo exponente para el
cual nuestro programa todavía imprime el resultado?
source_02_02_26_pow2.cpp

2.3.26 romper y continuar - las especias del bucle (1)

Hasta ahora, hemos tratado el cuerpo del bucle como


una secuencia indivisible e inseparable de instrucciones que se
realizan completamente en cada vuelta del bucle. Sin embargo,
como desarrollador, podría enfrentarse a las siguientes opciones:

 parece que no es necesario continuar el ciclo como un


todo; deberíamos dejar de ejecutar el cuerpo del bucle e ir más
allá;
 parece que necesitamos comenzar la prueba de condición sin
completar la ejecución del turno actual.

El lenguaje "C ++" nos proporciona dos instrucciones especiales


para implementar ambas tareas. Digamos, por razones de
113 – By Gerson
precisión, que su existencia en el lenguaje no es necesaria: un
programador experimentado puede codificar cualquier algoritmo
sin estas instrucciones.
El famoso informático holandés Edsger Dijkstra lo demostró en
1965. Estas adiciones, que no mejoran el poder expresivo del
lenguaje sino que solo simplifican el trabajo del desarrollador, a
veces se llaman dulces sintácticos .
Estas dos instrucciones son:

 break : sale del bucle de inmediato y finaliza


incondicionalmente la operación del bucle; el programa
comienza a ejecutar la instrucción más cercana después del
cuerpo del bucle;
 continuar : se comporta cuando el programa llega
repentinamente al final del cuerpo; se alcanza el final del
cuerpo del bucle y la expresión de condición se prueba de
inmediato.

Ambas palabras son palabras clave.


Ahora veamos dos ejemplos simples. Regresaremos a nuestro
programa que reconoce el mayor número ingresado. Lo
convertiremos dos veces, usando ambas instrucciones. Analice el
código y juzgue si usaría alguno de ellos y cómo lo haría.
La variante de descanso va aquí →
Tenga en cuenta que la única forma de salir del cuerpo es realizar
el descanso, ya que el bucle en sí es infinito ( para (;;) ).

source_02_03_26_maxbreak.cpp

114 – By Gerson
2.3.27 romper y continuar: las especias del bucle (2)

Y ahora la variante continua .

source_02_03_27_maxcontinue.cpp

115 – By Gerson
2.4.1 Las computadoras y su lógica

¿Has notado que las condiciones que hemos usado hasta ahora
son muy simples, bastante primitivas, de hecho? Las condiciones
que usamos en la vida real son mucho más complejas. Veamos la
siguiente oración:
Si tenemos algo de tiempo libre y el clima es bueno, saldremos a
caminar.
Hemos utilizado la conjunción " y ", lo que significa que salir a
caminar depende del cumplimiento simultáneo de las dos
condiciones .
116 – By Gerson
En el lenguaje de la lógica, las condiciones de conexión como esta
se llaman conjunciones. Y ahora otro ejemplo:
Si usted está en el centro comercial o yo en el centro comercial,
uno de nosotros comprará un regalo para mamá.
La aparición de la palabra " o " significa que la compra depende
de al menos una de estas condiciones . En lógica, un
compuesto como este se llama disyunción .

Claramente, el lenguaje "C ++" necesita tener operadores para


construir conjunciones y disyunciones. Sin ellos, el poder
expresivo del lenguaje se vería sustancialmente debilitado. Se
llaman operadores lógicos .

2.4.2 Orgullo y prejuicio (1)

Un operador lógico de conjunción en lenguaje "C ++" es


un dígrafo && (ampersand ampersand). Es un operador binario
con una prioridad inferior a la expresada por los operadores de
comparación.
Nos permite codificar condiciones complejas sin el uso de
paréntesis como este →

2.4.3 Orgullo y prejuicio (2)

El resultado del operador && puede determinarse mediante la


llamada tabla de verdad . Si consideramos la conjunción de:
izquierda derecha
el conjunto de posibles valores de argumentos y los valores
correspondientes de la conjunción se ve así →
117 – By Gerson
2.4.4 Ser || no ser

El operador de disyunción es el dígrafo || (bar bar). Es un


operador binario con una prioridad inferior a && (al igual que "+"
en comparación con "*"). Su tabla de verdad se ve así →

118 – By Gerson
2.4.5 Ser || ! ser - estar

Además, hay otro operador que se puede utilizar para construir


condiciones. Es un operador unario que realiza
una negación lógica .
Su funcionamiento es simple: convierte la verdad en falsedad y la
falsedad en verdad.
119 – By Gerson
¡Este operador está escrito como un solo carácter ! (signo de
exclamación) y su prioridad es muy alta: lo mismo que los
operadores de incremento y decremento. Su tabla de verdad es
realmente simple →

2.4.6 Algunas expresiones lógicas (1)

Tenga en cuenta que las siguientes condiciones son equivalentes


entre sí →

120 – By Gerson
2.4.7 Algunas expresiones lógicas (2)

Quizás recuerdes las leyes de De Morgan de la escuela. Ellos


dijeron eso:

La negación de una conjunción es la disyunción de las negaciones.


La negación de una disyunción es la conjunción de las negaciones.

Tratemos de escribir lo mismo usando el lenguaje "C ++" →


Observe cómo usamos los paréntesis para codificar las
expresiones.

Por cierto, ninguno de los operadores de dos argumentos


anteriores se puede usar en la forma abreviada conocida
como op = . Vale la pena recordar esta excepción.

! ( p && q ) ==! p || ! q
! ( p || q ) ==! p &&! q

2.4.8 Cómo lidiar con bits individuales (1)

121 – By Gerson
Los operadores lógicos toman sus argumentos como un todo,
independientemente de cuántos bits
contengan. Los operadores solo conocen
los valores 0 o falso (cuando se
restablecen todos los bits), lo que
significa "falso", o no 0 o verdadero
(cuando se establece al menos un bit), lo
que significa "verdadero". El resultado de
sus operaciones es el valor 0 (falso) o 1
(verdadero) .
Esto significa que el fragmento de la derecha asignará el valor
verdadero a la variable j si i no es falso. De lo contrario, será falso
(¿por qué?).

2.4.9 Cómo lidiar con bits individuales (2)

Sin embargo, hay cuatro operadores que le permiten


manipular bits individuales de datos. Se llaman operadores
bit a bit .
Cubren todas las operaciones que mencionamos anteriormente en
el contexto lógico y un operador adicional.
Este es el llamado xor (como exclusivo o) y denotado
como ^ (caret). Aquí están todos ellos:

 & (ampersand) conjunción bit a bit


 El | (barra) disyunción bit a bit
 ~ (tilde) negación bit a bit
 ^ (caret) bitwise exclusivo o

Hagámoslo más fácil:

 & requiere exactamente dos "1" para proporcionar "1" como


resultado
 El | requiere al menos un "1" para proporcionar "1" como
resultado
 ^ requiere exactamente un "1" para proporcionar "1" como
resultado

122 – By Gerson
Mencionemos algo importante aquí: los argumentos de estos
operadores deben ser enteros ( int , así
como long , short o char ); No podemos usar carrozas aquí.
La diferencia en la operación de los operadores lógicos y de bits
es importante: los operadores lógicos no penetran en el nivel de
bits de su argumento. Solo les interesa el valor entero final.
Los operadores bit a bit son más estrictos: se ocupan de cada bit
por separado. Si suponemos que la variable int ocupa 32 bits,
puede imaginar la operación bit a bit como una evaluación de 32
veces del operador lógico para cada par de bits de los
argumentos. Esta analogía es obviamente imperfecta, ya que en
el mundo real todas estas 32 operaciones se realizan al mismo
tiempo.

123 – By Gerson
2.4.10 Cómo lidiar con bits individuales (3)

Ahora le mostraremos un ejemplo de la diferencia en la operación


entre operaciones lógicas y de bits. Supongamos que se ha
realizado la siguiente declaración:
que i = 15, J = 22;
Si suponemos que las entradas se almacenan con 32 bits, la
imagen en bits de las dos variables será esta →

2.4.11 Cómo lidiar con bits individuales (4)

Esta declaración se da:


int log = i && j;
Estamos tratando con una conjunción lógica . Tracemos el
curso de los cálculos. Ambas variables i y j no son ceros, por lo
que se considerará que representan "verdadero".
Si revisamos la tabla de verdad para el operador &&, podemos ver
que el resultado será "verdadero", que es un número entero igual
a 1. Esto significa que la imagen bit a bit de la variable
de registro es esta →

2.4.12 Cómo lidiar con bits individuales (5)

Ahora la operación bit a bit , aquí está:


int bit = i & j;
El operador & operará con cada par de bits correspondientes por
separado, produciendo los valores de los bits relevantes del
resultado. Por lo tanto, el resultado es este →

124 – By Gerson
Estos bits corresponden al valor entero de 6.

2.4.13 Cómo lidiar con bits individuales (6)

Probemos los operadores de negación ahora. Primero el


lógico:
int logneg =! i;
La variable logneg se establecerá en 0, por lo que su imagen
consistirá solo en ceros.

La negación bit a bit va aquí:


int bit = ~ i;
Puede sorprenderle saber que el valor de la variable bitneg es -
16. ¿Extraño? ¡No, en absoluto!
Si te sorprende, trata de pasar un tiempo investigando los secretos
del sistema de numeración binario y las reglas que rigen los
llamados números complementarios de dos . Es una buena
lectura antes de acostarse.

2.4.14 Cómo lidiar con bits individuales (7)

Podemos usar cada uno de los operadores de dos argumentos


anteriores en sus formas abreviadas. Estos son ejemplos de
notaciones equivalentes →

125 – By Gerson
2.4.15 Cómo lidiar con bits individuales (8)

Ahora le mostraremos para qué puede usar operadores bit a


bit. Imagine que tiene que escribir una pieza importante de un
sistema operativo. Le han dicho que debe usar una variable
declarada de la siguiente manera →

2.4.16 Cómo lidiar con bits individuales (9)

La variable almacena la información sobre varios aspectos de la


operación del sistema. Cada bit de la variable almacena un valor
sí / no .
También le han dicho que solo uno de estos bits es suyo: el bit
número tres (recuerde que los bits están numerados de 0 y el
número 0 es el más bajo, mientras que el más alto es el número
31).
Los bits restantes no pueden cambiar porque están destinados a
almacenar otros datos. Aquí está tu bit marcado con la letra "x"

2.4.17 Cómo lidiar con bits individuales (10)

126 – By Gerson
Puede enfrentar las siguientes tareas:
#1
verifique el estado de su bit; desea averiguar el valor de su
bit; comparar toda la variable con cero no hará nada porque los
bits restantes pueden tener valores completamente
impredecibles, pero podemos usar la siguiente propiedad de
conjunción:

x&1=x
x&0=0

Si aplicamos la operación & a la variable FlagRegister junto con la


siguiente imagen de bit:

00000000000000000000000000001000

(tenga en cuenta el "1" en la posición de su bit), obtenemos una


de las siguientes cadenas de bits:

00000000000000000000000000001000
si su bit se estableció en "1",

00000000000000000000000000000000
si su bit se restableció a "0"

Esta secuencia de ceros y unos, cuya tarea es tomar el valor o


cambiar los bits seleccionados, se llama máscara de
bits . Intentemos crear una máscara de bits para detectar el
estado de su bit .
Debe apuntar al tercer bit. Ese bit tiene el peso de 2 3 = 8.
Deberíamos poder crear una máscara adecuada mediante la
siguiente declaración:

127 – By Gerson
int TheMask = 8;

También podemos hacer una secuencia de instrucciones


dependiendo del estado de su bit - aquí está →

2.4.18 Cómo lidiar con bits individuales (11)

#2
Restablezca su bit: asigna un cero al bit mientras todos los demás
bits permanecen sin cambios; usaremos la misma propiedad de la
conjunción que antes, pero usaremos una máscara ligeramente
diferente, así:

1111111111111111111111111111110111

Tenga en cuenta que la máscara se creó como resultado de la


negación de todos los bits de la variable TheMask .
Restablecer el bit es simple y se ve así (elige el que más te guste)

128 – By Gerson
2.4.19 Cómo lidiar con bits individuales (12)

#3
Establezca su bit: asigna un "uno" a su bit mientras que todos los
demás bits permanecen sin cambios; utilizaremos la siguiente
propiedad de disyunción:
x|1=1
x|0=x
Ahora estamos listos para configurar su bit con una de las
siguientes instrucciones →

2.4.20 Cómo lidiar con bits individuales (13)

#4
para negar su bit, reemplace "uno" con "cero" y "cero" con
"uno". Usemos una propiedad interesante del operador xor :
x ^ 1 =! x
x^0=x
y niega tu bit con las siguientes instrucciones →

129 – By Gerson
2.4.21 Cómo lidiar con bits individuales (14)

El lenguaje "C ++" nos ofrece otra operación relacionada con bits
individuales: desplazamiento . Se aplica solo a valores enteros
y no se puede usar con flotantes como argumentos.
Usas esta operación inconscientemente todo el tiempo. ¿Cómo
multiplicas cualquier número por 10? Echar un vistazo:
12345 ∙ 10 = 123450
Como puede ver, multiplicar por diez es solo un caso de
desplazamiento de todos los dígitos a la izquierda y agregar un "0"
a la derecha. ¿Cómo se divide por 10? Miremos:
12340 ÷ 10 = 1234
Todo lo que haces es mover los dígitos a la derecha.

La computadora realiza el mismo tipo de operación con una


diferencia: como 2 es la base para los números binarios (no
10), desplazar un valor un bit a la izquierda corresponde a
multiplicarlo por 2 ; respectivamente, desplazar un bit hacia
la derecha es como dividir entre 2 (observe que se pierde el
bit más a la derecha).
El cambio de bit puede ser:

 lógico si todos los bits de la variable se desplazan; este tipo de


desplazamiento se produce cuando lo aplica a los enteros sin
signo;
 aritmética si el desplazamiento omite el llamado bit de signo :
en la notación de complemento a dos, el bit más alto de una
variable juega el papel del bit de signo; si es igual a "1", el valor
se trata como negativo; Esto significa que el desplazamiento
aritmético no puede cambiar el signo del valor desplazado.

Los operadores de cambio en el lenguaje "C ++" son un par de


dígrafos, << y >>, que indican claramente en qué dirección
actuará el cambio. El argumento izquierdo de estos operadores es
un valor entero cuyos bits se desplazan. El argumento correcto
130 – By Gerson
determina el tamaño del turno. Muestra que esta operación
ciertamente no es conmutativa .
La prioridad de estos operadores es muy alta. Los verá en la tabla
actualizada de prioridades al final de esta sección.

2.4.22 Cómo lidiar con bits individuales (15)

Suponga la existencia de las siguientes declaraciones:


int Firmado = -8, VarS;
unsigned Unsigned = 6, VarU;
y mira estos cambios →

Podemos usar ambos operadores con el siguiente formulario de


acceso directo:
Firmado >> = 1; / * división por 2 * /
Sin signo << = 1; / * multiplicación por 2 * /

131 – By Gerson
2.4.23 Cómo lidiar con bits individuales (16)

Y aquí está la tabla de prioridades actualizada que contiene todos


los operadores que hemos introducido durante la lección actual →

2.5.1 Caso y cambio vs. if (1) Case and switch vs. if (1)

Como ya sabemos, una if-cascade es un nombre para una


construcción de código donde muchas instrucciones if se colocan
consecutivamente una tras otra , como en el siguiente ejemplo

Por supuesto, no hay obstáculos para usar y mantener un código
como este, pero hay algunas desventajas que pueden ser
desalentadoras. Cuanto más larga sea la cascada, más difícil será
leer y comprender para qué está sangrado.
También es difícil modificar la cascada: es difícil agregar una
nueva rama y eliminar cualquier rama creada anteriormente.
El lenguaje "C ++" nos ofrece una manera de facilitar estas
selecciones. Por cierto, esto es solo más sintaxis dulce. Puede
132 – By Gerson
administrarlo sin él, pero no dude en usarlo cuando
sus ifs comiencen a crecer ampliamente.

2.5.2 Caso y cambio vs. if (2) Case and switch vs. if (2)

Echemos un vistazo al fragmento de la derecha →


Es un ejemplo de cómo reemplazar una cascada if con una
instrucción especializada. Tenga en cuenta que las
palabras switch y case son palabras clave.
La nueva instrucción se llama switch y de hecho es un
switch. ¿Entonces, cómo funciona?
Primero, el valor de la expresión encerrada dentro del paréntesis
después de evaluar la palabra clave de cambio (esto a veces se
denomina expresión de cambio ). Luego, se busca el bloque
para encontrar un literal precedido por la palabra clave case que
sea igual al valor de la expresión.
Cuando se encuentra un caso, se ejecutan las instrucciones detrás
de los dos puntos. Si hay una interrupción entre ellos, la ejecución
de la instrucción switch finaliza . De lo contrario, todas las
instrucciones se ejecutan hasta que se alcanza el final del bloque
o se cumple otra interrupción .
¿Qué sucede si la expresión de conmutación tiene un valor que no
ocurre en ninguno de los casos? La respuesta es simple: nada:
ninguna de las ramas de la instrucción switch se ejecuta.

133 – By Gerson
2.5.3 Caso y cambio vs. if (3) Case and switch vs. if (3)

Modifiquemos los requisitos. Podemos suponer ahora que nuestro


programa estará satisfecho (dirá "OK") si la variable i es igual a 4
o 3.
¿Significa esto que tenemos que crear dos ramas para ambas
posibilidades? Afortunadamente no. Todo lo que tenemos que
hacer es agregar más de un caso delante de cualquier rama, así

2.5.4 Caso y cambio vs. if (4) Case and switch vs. if (4)
También podemos suponer que nuestro programa no tiene una
opinión sobre dónde los valores de i son distintos a los
especificados hasta ahora, y queremos que el programa lo exprese
claramente. ¿Hemos hecho un millón de casos nuevos que cubren
todo el rango del tipo int ?
No. Podemos usar un caso generalizado que cubra todos los
eventos que no se mencionan en los casos anteriores. Así es como
se ve →
134 – By Gerson
Tenga en cuenta que el valor predeterminado también es una
palabra clave.

No te olvides de usar el descanso . Dejar de lado esta palabra


clave es uno de los errores más comunes que cometen los
desarrolladores (no solo al comienzo de sus carreras).
Simple, ¿verdad? Y que elegante.

Pensemos en los siguientes comentarios importantes:

 el valor después del caso no debe ser una expresión


que contenga variables o cualquier otra entidad cuyos valores
no se conozcan en el momento de la compilación
 las ramas de casos se escanean en el mismo orden en que
se especifican en el programa; Esto significa que las selecciones
más comunes deberían ser las primeras (podría hacer que su
programa sea un poco más rápido en algunos casos)

Ahora nos despedimos para cambiar las declaraciones. Es hora de


abordar el importante problema de las matrices .

2.6.1 Matrices: ¿por qué? (1)

Puede ser que tengamos que leer, almacenar, procesar y


finalmente imprimir docenas, tal vez cientos, tal vez miles de
números. ¿Entonces que? ¿Debemos declarar una variable
separada para cada valor? ¿Tendremos que pasar largas horas
escribiendo declaraciones como la de la derecha →?
135 – By Gerson
Si no cree que esta sea una tarea complicada, le sugerimos que
tome una hoja de papel y escriba un programa que lea cinco
números de tipo int y los imprima en orden de menor a mayor (NB
este tipo de procesamiento se llama clasificación ). No creemos
que tenga suficiente espacio en su papel para la tarea.
Hasta ahora, hemos declarado variables que pueden almacenar
exactamente un valor dado a la vez. Estas variables se
llaman escalares y son análogas a los términos
matemáticos. Todas las variables que hemos usado hasta ahora
son en realidad escalares.
Piense en lo conveniente que sería si pudiéramos declarar una
variable que pudiera almacenar más de un valor. Por ejemplo, 100
o 1000 o incluso 10,000. Seguiría siendo una y la misma variable
pero muy amplia y espaciosa.
Suena atractivo, ¿verdad? Quizás, pero ¿cómo manejaría un
contenedor tan lleno de valores diferentes? ¿Cómo elegiría el que
necesitamos?
¿Deberíamos enumerarlos? Y diremos: dame el valor número
2; asigne el valor número 15; aumentar el valor número 10000 .
Probablemente sea una buena idea, ¿no le parece?
Le mostraremos cómo declarar tales variables de valores
múltiples. Haremos esto con un ejemplo que usamos antes.

2.6.2 Matrices: ¿por qué? (2) Arrays – why?

Leemos este registro de la siguiente manera: creamos una


variable llamada números ; está destinado a
almacenar cinco (tenga en cuenta el número entre paréntesis)
valores de tipo int (que conocemos por la palabra clave int al
comienzo de la declaración). Digamos lo mismo en una
terminología más técnica: números es una matriz que consta
de 5 valores de tipo int . Dado que dicha matriz se

136 – By Gerson
llama vector en términos matemáticos, también diremos que esta
declaración declara un vector int de tamaño igual a 5.
Todos los elementos de una matriz tienen el mismo tipo.

Tiempo para una curiosidad intrigante. El lenguaje "C ++" tiene


una convención que dice que los elementos en una matriz están
numerados a partir de 0 . Esto significa que el elemento
almacenado al comienzo de la matriz tendrá el número 0. Dado
que hay 5 elementos en nuestra matriz, el último tiene el número
4. No se olvide de esto: lo necesitará cuando haga sus propias
matrices Pero no se preocupe, pronto se acostumbrará y se
volverá natural para usted.
Antes de continuar, debemos tener en cuenta lo siguiente:
nuestro vector es una colección de elementos , pero cada
elemento es un escalar .

2.6.3 Matrices: ¿cómo? (3) Arrays – how? (3)

¿Cómo asignamos un valor a un elemento elegido de una


matriz? Asignemos el valor de 111 al primer elemento de la
matriz. Lo hacemos de esta manera →

2.6.4 Matrices: ¿cómo? (4) Arrays – how? (4)

Necesitamos un valor almacenado en el tercer elemento de la


matriz y queremos asignarlo a la variable i . Así es como podemos
hacerlo →

2.6.5 Matrices: ¿cómo? (5) Arrays – how? (5)

Y ahora queremos que el valor del quinto elemento se copie en


el segundo elemento. ¿Puedes adivinar cómo hacerlo?
137 – By Gerson
El valor dentro de los corchetes, que selecciona un elemento del
vector, se denomina índice , mientras que la operación de
seleccionar un elemento de la matriz se conoce
como indexación .
Nota: todos los índices que hemos usado hasta ahora son
literales. Sus valores son fijos en tiempo de ejecución, pero
cualquier expresión también podría ser el índice. Esto abre
muchas oportunidades.

2.6.6 Matrices: ¿cómo? (6) Arrays – how? (6)

Queremos calcular la suma de todos los valores almacenados


en la matriz de números . Declaramos una variable donde se
almacenará la suma e inicialmente le asignamos un valor de 0; su
nombre es suma .
Luego agregamos todos los elementos de la matriz usando
el bucle for , que es una gran herramienta para procesar
matrices. Echa un vistazo al fragmento →
Hablemos de este ejemplo por un momento. La variable i tomará
los valores 0, 1, 2, 3 y 4 posteriormente e indexará la matriz
de números seleccionando los elementos siguientes: el primero,
segundo, tercero, cuarto y quinto. El operador + = agregará cada
uno de estos elementos a la variable suma , dando el resultado
final al final del ciclo.

2.6.7 Matrices: ¿cómo? (7) Arrays – how? (7)


138 – By Gerson
La siguiente tarea es asignar el mismo valor (por ejemplo, 2012)
a todos los elementos de la matriz.

2.6.8 Matrices: ¿cómo? (8) Arrays – how?


Ahora intentemos reorganizar los elementos de la matriz, es
decir, invertir el orden de los elementos :
se intercambiarán los elementos primero y quinto, así como el
segundo y cuarto . El tercero permanecerá intacto.
Pregunta: ¿cómo podemos intercambiar los valores de dos
variables? Veamos el fragmento: si hacemos algo como esto,
perderíamos el valor que almacenamos previamente en
la variable2 .
Cambiar el orden de las tareas tampoco nos
ayudará. Desafortunadamente, necesitamos una tercera variable
que sirva como almacenamiento auxiliar.

2.6.9 Matrices: ¿cómo? (9) Arrays – how? (9)

Así es como lo hacemos →

139 – By Gerson
2.6.10 Matrices: ¿cómo? (10) Arrays – how? (10)
No sabemos lo que piensas, pero definitivamente no nos gusta
cómo se hace. Todavía es aceptable con una matriz de 5
elementos, pero con 99 elementos ciertamente no lo sería.

2.6.11 Matrices: ¿cómo? (11) Arrays – how? (11)


Usemos los servicios de un bucle for . Mire cuidadosamente cómo
manipulamos los valores de los índices.
Durante el primer giro del ciclo, la variable i será igual a 0, por lo
que las instrucciones en el cuerpo realmente realizarán las
siguientes operaciones:
auxiliar = números [0];
números [0] = números [4];
números [4] = auxiliar;

En la segunda vuelta, i será igual a 1, por lo que:


auxiliar = números [1];
números [1] = números [3];
números [3] = auxiliar;

Como puede ver, el bucle hace el mismo trabajo acortando el


código fuente y haciéndolo más legible.

140 – By Gerson
2.7.1 Inicialización de matriz (1) Array initialization (1)

Sí, puede iniciar matrices, es decir, asignarles valores iniciales en


el momento de la declaración. Hacemos esto ligeramente
diferente al inicio de escalares porque necesitamos especificar
más de un valor .
La sintaxis de un iniciador de matriz (para ser precisos, un vector)
es clara y legible. Imagine que queremos crear una matriz donde
el valor de cualquier elemento sea igual a su índice. Un iniciador
adecuado se vería así →
Como puede ver, el iniciador vectorial es simplemente una lista de
valores encerrados entre llaves .

2.7.2 Inicialización de matriz (2) Array initialization (2)

Si proporciona menos valores que el tamaño de una matriz, como


este, no pasará nada malo. El compilador determina que aquellos
elementos para los que no especificó ningún valor deben
establecerse en 0.

2.7.3 Inicialización de matriz (3) Array initialization (3)

Si proporciona más elementos de los que se pueden almacenar


en una matriz, como este →, será un error. El compilador estará
más insatisfecho.

141 – By Gerson
2.7.4 Inicialización de matriz (4) Array initialization (4)

Un ejemplo mas. Mire esto →, no especificamos el tamaño de la


matriz, pero proporcionamos un iniciador .
Esto es legal y obligará al compilador a suponer que el tamaño de
la matriz es igual a la longitud del iniciador.
La matriz de vectores se declarará de la siguiente manera:

int vector [7] = {0,1,2,3,4,5,6};

2.8.1 No solo ints (1) Not only ints (1)


Hasta ahora, hemos discutido los vectores cuyos elementos son
de tipo int solamente. No te preocupes También puede
usar matrices de cualquier otro tipo .
Por ejemplo, esta es una matriz en la que puede almacenar 10
valores de punto flotante.

2.8.2 No solo ints (2) Not only ints (2)

Y puedes almacenar 20 caracteres aquí...

2.8.3 No solo ints (3) Not only ints (3)

... así como los valores lógicos (booleanos).

142 – By Gerson
Prácticamente cada pieza de datos se puede agregar en una
matriz. Incluso una matriz. Te lo mostraremos pronto.

2.9.1 No solo vectores (1) Not only vectors (1)

Asumimos hasta ahora que las matrices consisten en escalares,


pero de hecho las matrices pueden contener elementos de
una estructura mucho más compleja . Consideremos el caso
cuando los elementos de la matriz son solo matrices.
¿Sorprendente? ¡De ningún modo! A menudo nos encontramos
con tales matrices en nuestras vidas. Probablemente el mejor
ejemplo de esto es un tablero de ajedrez.
Lo que vamos a decir ahora probablemente enfurecerá a los
jugadores de ajedrez experimentados, por lo que nos disculpamos
de antemano por todas nuestras simplificaciones e inexactitudes.
Un tablero de ajedrez está formado por filas y columnas . Hay 8
filas y 8 columnas. Cada columna está marcada con las letras de
la A a la H. Cada línea está marcada con los números del 1 al 8.
Puede identificar la ubicación de cada campo mediante pares de
letras y dígitos.
Por lo tanto, sabemos que la esquina inferior derecha del tablero
(la que tiene la torre blanca) es A1, mientras que la esquina
opuesta es H8.
Supongamos que podemos usar los valores int seleccionados para
representar cualquier pieza de ajedrez. También podemos
suponer que cada fila en el tablero de ajedrez es un ... ¡vector!
Tratemos de declararlo, aquí está →

143 – By Gerson
2.9.2 No solo vectores (2) Not only vectors (2)

Desafortunadamente, tenemos 8 de estas filas. ¿Esto significa


que tenemos que declarar 8 matrices como esta?

int fila1 [8], fila2 [8], fila3 [8], fila4 [8], fila5 [8], fila6 [8], fila7 [8], fila8
[8];

¿Estás teniendo la sensación de deja vu? Ya nos hemos


encontrado con un dilema similar, cuando estábamos buscando la
razón para usar vectores.
El tablero de ajedrez es, de hecho, una matriz de 8 elementos de
elementos en filas individuales. Resumamos:

 los elementos de las filas son campos , 8 de ellos por fila;


 los elementos del tablero de ajedrez son filas , 8 de ellos por
tablero de ajedrez

Ahora estamos listos para crear una matriz para el tablero de


ajedrez, y aquí está la declaración →
La variable del tablero de ajedrez es una llamada matriz
bidimensional . También se llama, por analogía con el álgebra,
una matriz .

2.9.3 No solo vectores (3) Not only vectors (3)

Los dos pares de paréntesis le dicen al compilador que la matriz


declarada no es un vector, es una matriz cuyos elementos son
vectores .
El acceso a cualquier campo seleccionado en nuestro tablero
requiere dos índices : el primero selecciona la fila y el segundo,
el número de campo dentro de la fila, que es de hecho un número
de columna.

144 – By Gerson
Mira nuestro hermoso tablero de ajedrez. Cada campo contiene un
par de índices que deben darse para acceder al contenido del
campo.
Mirando la figura aquí → podemos colocar algunas piezas de
ajedrez en nuestro tablero. Primero, agreguemos todas las torres:
tablero de ajedrez [0] [0] = ROOK;
tablero de ajedrez [0] [7] = ROOK;
tablero de ajedrez [7] [0] = ROOK;
tablero de ajedrez [7] [7] = ROOK;

Si quisiéramos colocar un caballero en C4, lo haríamos de la


siguiente manera:
tablero de ajedrez [3] [2] = CABALLERO;

Y ahora un peón en E5:


tablero de ajedrez [4] [4] = peón;

Y eso es. ¡Mate!

2.9.4 No solo vectores (4) Not only vectors (4)

Vamos a profundizar ahora en la naturaleza multidimensional de


las matrices. Para encontrar cualquier elemento de la matriz
bidimensional, tenemos que usar dos " coordenadas ":
una vertical (número de fila) y una horizontal (número de
columna).
Imagine que estamos desarrollando un software para una estación
meteorológica automática. El dispositivo registra la temperatura
del aire cada hora y lo hace durante todo el mes. Esto nos da un
total de 24 * 31 = 744 valores.

145 – By Gerson
Intentemos diseñar una matriz capaz de almacenar todos estos
resultados. Primero, tenemos que decidir qué tipo de datos sería
mejor para esta aplicación. Creemos que un flotador sería lo
mejor, ya que nuestro termómetro puede medir la temperatura
con una precisión de 0.1 grados centígrados.
A continuación, decidimos arbitrariamente que las filas registrarán
las lecturas cada hora en punto (por lo que la fila tendrá 24
elementos) y cada una de las filas se asignará a un día del mes
(por lo que necesitamos 31 filas). Aquí está nuestra declaración →

Ahora intentaremos determinar la temperatura media mensual del


mediodía. Agregaremos las 31 lecturas registradas al mediodía y
dividiremos la suma entre 31. Suponemos que la temperatura de
medianoche se almacena primero. Aquí está el código relevante
junto con las declaraciones necesarias:

temperatura de flotación [31] [24];


suma flotante = 0.0, promedio;
para ( int día = 0; día <31; día ++)
sum + = temp [día] [11];
promedio = suma / 31;
cout << "Temperatura media al mediodía:" << promedio << endl;

2.9.5 No solo vectores (5) Not only vectors (6)

Ahora busquemos la temperatura más alta durante todo el mes -


vea el código →

146 – By Gerson
2.9.6 No solo vectores (6) Not only vectors (6)
Ahora queremos contar los días en que la temperatura al
mediodía era de al menos 20 grados centígrados.

2.9.7 No solo vectores (7) Not only vectors (7)

Ahora vamos a llenar toda la matriz con ceros para prepararla


para el próximo mes →

2.9.8 No solo vectores (8) Not only vectors (8)

El lenguaje "C ++" no limita el número de dimensiones de la


matriz . Aquí puede ver un ejemplo de una matriz tridimensional.
Imagina un hotel. Es un hotel enorme que consta de tres edificios,
de 15 pisos cada uno. Hay 20 habitaciones en cada
piso. Necesitamos una matriz capaz de recopilar y procesar
información sobre el número de invitados registrados en cada
habitación.

147 – By Gerson
Paso uno: el tipo de elemento de la matriz. Creemos que
el int sería una buena opción, aunque también debería estar sin
firmar, ya que no hay un número negativo de invitados.
Paso dos: analice con calma la situación. Resuma la información
disponible: 3 torres, 15 pisos, 20 habitaciones.
Ahora podemos escribir la declaración →

El primer índice (0 a 2) selecciona uno de los edificios ; el


segundo (0 a 14) selecciona el piso ; el tercero (0 a
19) selecciona el número de habitación .
Ahora podemos reservar una habitación para dos recién casados:
en el segundo edificio, en el décimo piso, habitación catorce:

invitados [1] [9] [13] = 2;

y libera la segunda habitación en el quinto piso ubicada en el


primer edificio:

invitados [0] [4] [1] = 0;

Antes de finalizar esta parte de nuestro curso, verifiquemos si hay


vacantes en el decimoquinto piso del tercer edificio:

int room; vacante


int = 0;
para (habitación = 0; habitación <20; habitación ++)
if (invitados [2] [14] [habitación] == 0)
vacante ++;

La variable de vacante contiene 0 si todas las habitaciones están


ocupadas o el número de habitaciones disponibles es 0.

148 – By Gerson
2.10.1 Estructuras: ¿por qué las necesitamos? (1)
Structures – why do we need them? (1)

Antes de comenzar con las estructuras, necesitamos informarle


sobre un tipo completamente nuevo, llamado string . Por ahora,
no le diremos más de lo que usamos y cómo declaramos variables
de este tipo. Prometemos que le daremos más detalles en una
sección separada dedicada exclusivamente a las cadenas, y le
mostraremos cómo manipularlos y cómo disfrutar de su uso.
Para ser honesto, una cadena es poco más que un tipo, y todo lo
que queremos decirle en este momento es que las variables de
tipo cadena pueden almacenar cadenas de caracteres, como
apellidos, apellidos, nombres de calles y todos los demás nombres
que puede pensar, incluido R2D2.
Las variables de tipo cadena se pueden asignar con los mismos
operadores que cualquier otra variable que hayamos encontrado
anteriormente. Por ejemplo, si queremos almacenar el nombre de
nuestro plato favorito en una variable de cadena, lo hacemos de
la siguiente manera:

string dish_to_order = "pizza";

Imagine que nosotros, como desarrolladores, tenemos el siguiente


trabajo que hacer: estamos obligados a diseñar una estructura de
datos que pueda almacenar información sobre los estudiantes que
asisten a nuestro curso. Necesitamos almacenar el nombre de
cada estudiante, su tiempo dedicado a estudiar los capítulos y el
número del último capítulo completado. Sabemos que el número
total de estudiantes no excederá de 100,000 y esto nos lleva a
escribir la siguiente declaración →
La matriz nos permite almacenar 100000 nombres.
Intentemos manipular esa matriz. Por ejemplo, suponga que el
primer estudiante registrado es el Sr. Bond (James
Bond). Guardemos la información en nuestra matriz:

student_name [0] = "Bond";

149 – By Gerson
2.10.2 Estructuras: ¿por qué las necesitamos? (2)
Structures – why do we need them? (2)

El tiempo pasado en el sitio se almacenará como flotante. Esto no


es particularmente obvio o conveniente, pero ciertamente
podemos tratarlo. El número de horas se representará como una
fracción decimal. Esto nos lleva directamente a la siguiente
declaración →
Sabemos que el Sr. Bond ha pasado tres horas y treinta minutos
estudiando nuestro curso. Denotémoslo de la siguiente manera:

student_time [0] = 3.5;

Nota: 3h30m = 3 horas y media, por lo tanto 3.5.

2.10.3 Estructuras: ¿por qué las necesitamos? (3)


Structures – why do we need them? (3)

El problema principal aquí es que los datos relacionados con el


mismo objeto (un estudiante) se dispersan entre tres
variables , aunque lógicamente deberían existir como una unidad
consolidada. El manejo de múltiples matrices es engorroso y
propenso a errores, y cuando la vida nos obliga a recopilar
información adicional (por ejemplo, dirección de correo
electrónico) tendremos que declarar otra matriz y hacer muchos
otros cambios a lo largo del programa. No nos gusta, y puedes
estar seguro de que tampoco te gustará.
Ya sabemos qué es una matriz. La matriz es un agregado de
elementos. Los elementos están numerados y son del mismo
tipo. ¿Podemos usar un agregado cuyos elementos podrían
150 – By Gerson
ser de diferentes tipos ? ¿Podrían identificarse por nombres, no
por números? ¿Y es una buena idea?
¡Sí, es una gran idea! Este agregado mágico se
llama estructura .

Una estructura puede contener cualquier número de


elementos de cualquier tipo . Cada uno de estos elementos se
llama campo. Cada campo se identifica por su nombre, no por su
número. Claramente, que los nombres de campo deben ser únicos
y no pueden duplicarse dentro de una sola estructura. Le
mostraremos cómo declarar una estructura adecuada a nuestras
necesidades y le explicaremos su significado.
Aquí está la declaración de la estructura →

 la declaración de la estructura siempre comienza con la palabra


clave struct
 hay una llamada etiqueta de estructura después de la palabra
clave ( ESTUDIANTE en este caso); es el nombre de la
estructura misma; existe una costumbre ampliamente
aceptada de componer etiquetas de estructura con letras
mayúsculas simplemente para distinguirlas de las variables
ordinarias
 aquí viene el corchete de apertura, una señal de que la
declaración de campos comienza en este punto
 nuestra estructura tiene tres campos: el primero es
una cadena y se llama nombre ; el segundo es un flotador y se
llama tiempo ; el tercero es un int y se llama Recent_chapter

La declaración finaliza con el corchete de cierre seguido del


punto y coma .

151 – By Gerson
2.10.4 Estructuras: ¿por qué las necesitamos? (4)
Structures – why do we need them? (4)

Queremos enfatizar que la declaración anterior no crea una


variable, sino que solo describe la estructura que vamos a usar en
nuestro programa. Si queremos declarar una variable como
estructura, podemos hacerlo de una de las dos formas posibles →
Esta declaración establece dos variables ( variables
estructuradas )
denominadas stdnt y stdnt2 respectivamente. Las variables son
de tipo struct STUDENT o simplemente STUDENT ( tenga
en cuenta que la declaración de estructura crea un nuevo nombre
de tipo). Sabemos que esta variable consta de tres campos con
nombre, pero aún no sabemos cómo acceder a ellos. Como el
lenguaje "C ++" ofrece un operador de indexación
especializado [] para matrices, también nos da el
llamado operador de selección diseñado para estructuras y se
denota como un solo carácter . (punto).
La prioridad del operador de selección es muy alta, igual a la
prioridad del operador [] .

2.10.5 Estructuras: ¿por qué las necesitamos? (5)


Structures – why do we need them? (5)

Es un operador binario. Su argumento izquierdo debe


identificar la estructura, mientras que el derecho debe ser
el nombre del campo conocido en esta estructura .

152 – By Gerson
El resultado de este operador es el campo seleccionado de la
estructura y, por lo tanto, la expresión que contiene este operador
a veces se denomina selector .
Esto significa que el selector aquí → da como resultado la selección
de un campo llamado tiempo . El tipo de esta expresión es el tipo
del campo seleccionado y es un valor l.
En consecuencia, podemos usar estos dos selectores:

stdnt.time = 1.5;
y

flotador t;
t = stdnt.time;

2.10.6 Estructuras: ¿por qué las necesitamos? (6)

Prácticamente cualquier dato se puede usar como campo de una


estructura: escalares, matrices y casi casi todas las
estructuras. Decimos "casi" porque la estructura no puede ser
un campo en sí misma .
Podemos agregar estructuras dentro de una matriz, por lo que si
queremos declarar una matriz de estructuras ESTUDIANTE,
podemos hacerlo de esta manera →
El acceso a los campos seleccionados requiere dos operaciones
posteriores:

 en el primer paso, el operador [] indexará la matriz para


acceder a la estructura que necesitamos
 en el segundo paso, el operador de selección selecciona el
campo deseado

Esto significa que si queremos seleccionar el campo


de tiempo del elemento de cuarto stdnts , lo escribiremos así:

153 – By Gerson
stdnts [3] .time
Ahora recopilamos todas estas tareas que se realizan para las tres
matrices separadas. Míralos cuidadosamente:
stndts [0] .name = "Bond";
stndts [0] .time = 3.5;
stdnts [0] .recent_chapter = 4;

2.10.7 Declarando las estructuras


Declaring the structures

A los fines de otras consideraciones, utilizaremos una estructura


simple diseñada para almacenar la fecha. Está equipado con tres
campos, cada uno de tipo int , llamado año , mes y día , que
denotan claramente su función y propósito.
La primera forma posible de declarar la estructura es aquí →
Por supuesto, podemos escribir esta declaración de manera mucho
más compacta:

struct DATE { int año, mes, día; };

Ambas variantes son iguales.


Esta declaración no crea ninguna variable nueva, sino que solo
anuncia al compilador nuestra intención de usar esta
etiqueta de estructura para declarar nuevas variables . La
nueva variable se declararía, por ejemplo, de esta manera:

FECHA Fecha de Nacimiento;


Podemos usarlo para almacenar la fecha de nacimiento de Harry
Potter:

154 – By Gerson
DateOfBirth.year = 1980;
DateOfBirth.month = 7;
DateOfBirth.day = 31;
También podemos usar la etiqueta de estructura para declarar
una matriz de estructuras:
FECHA Visitas [100];
Acceder a una única estructura almacenada en la matriz es
fácil. Si queremos modificar los datos de la primera visita,
hacemos esto:

Visitas [0] .año = 2012;


Visitas [0] .month = 1;
Visitas [0] .day = 1;

También podemos matar dos pájaros de un tiro definiendo la


etiqueta de estructura y declarando cualquier número de variables
simultáneamente en la misma declaración, así:
struct DATE {
int año, mes, día;
} DateOfBirth, Visitas [100];
FECHA current_date;
También podemos omitir la etiqueta y declarar solo las variables:
struct {
int año, mes, día;
} the_date_of_the_end_of_the_world;
En este caso, sin embargo, se hace más difícil determinar el tipo
de variable the_date_of_the_end_of_the_world (por ejemplo, si
queremos usarlo con el operador sizeof ). Sin una etiqueta,
tenemos que denotarlo como:
sizeof (struct {int año, mes, día;})
Creemos que esto es demasiado complejo e ilegible, en
comparación con sizeof (struct DATE) .

155 – By Gerson
2.10.8 Estructuras: ¿por qué las necesitamos? (7)
Structures – why do we need them? (7)

Una estructura puede ser un campo dentro de otra


estructura . Imagine que tenemos que extender nuestra
estructura de ESTUDIANTE y agregar un campo para guardar la
fecha en que un estudiante en particular visitó el curso la última
vez. Podemos hacerlo de esta manera →
Significa que tendremos que usar dos operaciones de selección
posteriores para profundizar en la estructura, es decir, primero
seleccionamos una estructura dentro de la estructura, y luego
seleccionamos el campo deseado de la estructura interna.
Así es como funciona:
HarryPotter.last_visit.year = 2012;
HarryPotter.last_visit.month = 12;
HarryPotter.last_visit.day = 21;

156 – By Gerson
Prueba sorpresa: ¿cuándo nos visitó Harry por última vez?

2.11.1 Estructuras: algunas reglas importantes (1)


Structures – a few important rules (1)

Los nombres de campo de una estructura pueden superponerse


con los nombres de las etiquetas y eso no es un problema, aunque
puede causarle dificultades para leer y comprender el programa.
El fragmento aquí → es completamente correcto.

2.11.2 Estructuras: algunas reglas importantes (2)


Structures – a few important rules (2)

Puede suceder que al compilador particular con el que está


trabajando no le guste cuando el nombre de la etiqueta de una
estructura se superpone con el nombre de la variable (ya sabe
cómo son esos compiladores); por lo tanto, es mejor evitar trucos
como los de aquí →
157 – By Gerson
Alternativamente, consiga un nuevo compilador, uno que no se
queje tanto. OK, solo bromeaba.

2.11.3 Estructuras: algunas reglas importantes (3)


Structures – a few important rules (3)

Dos estructuras pueden contener campos con los mismos


nombres : el siguiente fragmento es correcto →

2.11.4 Inicializando estructuras (1)


Initializing structures (1)

Puede inicializar sus estructuras tan pronto como en el momento


de la declaración . El iniciador de la estructura está encerrado
entre llaves y contiene una lista de valores asignados a los
campos posteriores , comenzando por el primero.
Los valores enumerados en el iniciador deben ajustarse a los tipos
de campos. Si el iniciador contiene menos elementos que el
158 – By Gerson
número de campos de la estructura, se presume que la lista se
amplía implícitamente con ceros.
Si el campo particular es una matriz o una estructura, debe tener
su propio iniciador, que también está sujeto a extenderse con
ceros. Si el iniciador "interno" está completo, podemos omitir los
corchetes circundantes.
Echa un vistazo al ejemplo →
El iniciador es equivalente a la siguiente secuencia de
asignaciones:

fecha.año = 2012;
date.month = 12;
date.day = 21;

2.11.5 Inicializando estructuras (2)


Initializing structures (2)

El iniciador de este formulario es funcionalmente equivalente a las


siguientes asignaciones:

he.name = "Bond";
he.time = 3.5;
he.recent_chapter = 4;
he.last_visit.year = 2012
he.last_visit.month = 12;
he.last_visit.day = 21;
Debido a la integridad del inicializador interno, podemos escribir
la siguiente forma simplificada:
ESTUDIANTE he = {"Bond", 3.5, 4, 2012, 12, 21};

159 – By Gerson
Esta simplificación (omitiendo las llaves internas) no se puede
aplicar en el siguiente caso:
ESTUDIANTE ella = {"Mata Hari", 12., 12, {2012}};
El iniciador interno, que se refiere al campo last_visit , no cubre
todos los campos. Esto significa que será equivalente a la
siguiente secuencia de tareas:
she.name = "Mata Hari";
she.time = 12 .;
she.recent_chapter = 12;
she.last_visit.year = 2012
she.last_visit.month = 0;
she.last_visit.day = 0;

2.11.6 Inicializando estructuras (3)


Initializing structures (3)

¿Qué sucede cuando aplicamos un inicializador "vacío"? →


Esto es lo que pasa:
nobody.name = "";
nobody.time = 0.0;
nobody.recent_chapter = 0;
nobody.last_visit.year = 0
nobody.last_visit.month = 0;
nobody.last_visit.day = 0;

3.1.1 Punteros: los fundamentos absolutos (1)


Pointers – the absolute basics (1)

160 – By Gerson
Los punteros también son valores , pero diferentes de los
que hemos estado usando hasta ahora. Los tipos que hemos
estado utilizando están estrechamente relacionados con el
procesamiento de datos informáticos, pero reflejan plenamente
nuestras ideas e intuición.
Todos usamos números enteros en la vida cotidiana para
contar. Usamos flotadores cuando pagamos algo e ints cuando
contamos algo. Los punteros no tienen una analogía simple y
obvia con nuestra vida cotidiana y sus valores son ilegibles para
los humanos y completamente inútiles.
Sin embargo, las computadoras pueden hacer un gran uso de los
punteros, brindando a los desarrolladores opciones poderosas al
diseñar algoritmos y estructuras de datos.
Ahora, prepárate para el hecho de que no todo será
inmediatamente obvio para ti. Esto es normal: tomará algún
tiempo antes de que pueda comprender los punteros mismos y
sus rasgos específicos.

3.1.2 Punteros: los fundamentos absolutos (2)

Los recuerdos informáticos modernos son grandes y


rápidos. Probablemente ya sepa que el tamaño de la memoria se
expresa en unidades llamadas bytes. Probablemente también
sepa que cuando declara cualquier variable, la variable ocupa una
pequeña parte de la memoria de la computadora.
Hasta ahora, ha sido importante para nosotros saber qué valor se
almacena en la variable. De ahora en adelante, también nos
interesará saber dónde se almacena este valor.
Este rasgo de los datos (para decirlo más formalmente,
este atributo ) a menudo se denomina dirección . Todos

161 – By Gerson
vivimos en ciertas direcciones al igual que cada variable "vive" en
su dirección, también.
Intenta obtener esta importante diferencia:

 el valor de la variable es lo que almacena la variable;


 la dirección de la variable es información sobre dónde se ubica
esta variable (dónde vive)

Los punteros se utilizan para almacenar información sobre


la ubicación (dirección) de cualquier otro dato . Podemos
decir que los punteros son como señales . No dicen nada sobre el
lugar en sí, pero nos muestran claramente cómo llegar.

3.1.3 El primer puntero


The first pointer

Comencemos con una declaración simple y una operación


igualmente simple. También nos encontraremos con algunos
operadores nuevos.
Ya en el momento de la declaración, podemos ver la diferencia
entre los tipos de datos regulares y los tipos de puntero. Echa un
vistazo →
Usamos el asterisco ("*") aquí de una manera completamente
nueva. No tiene nada que ver con la multiplicación. Puede que te
sientas un poco confundido por esto, pero no te preocupes.
Esta naturaleza dual de los asteriscos se aclarará pronto. La
sintaxis del lenguaje "C ++" asegura que siempre podrá descubrir
para qué se utiliza el asterisco en un contexto particular.
Esta declaración establece una variable llamada p . No es
un int : el asterisco significa que p es un puntero y se
utilizará para almacenar información sobre la ubicación de los
datos de tipo int . Los punteros siempre se utilizan para señalar
los datos específicos a los que se hace referencia en la declaración.

162 – By Gerson
El lenguaje "C ++" también puede usar los llamados punteros
amorfos , que pueden usarse para señalar cualquier tipo de
datos, pero vamos a discutirlos al final de nuestra historia.

¿Cuál es el tipo de variable p ? Hay algunas respuestas, idénticas


en significado, pero considerablemente diferentes.
Primero, podemos decir que p es una variable de tipo " puntero
a int ".
En segundo lugar, puede ser: p es una variable de
tipo int * donde "*" se lee simplemente como "asterisco".

3.1.4 Cómo asignar un valor (1)


How to assign a value (1)

¿Cómo asignamos un valor a la variable de puntero?


¿Podemos asignar un valor al puntero? Por supuesto, de la misma
manera que puede asignar cualquier valor a cualquier otra
variable: usando el operador =.
Una pregunta más importante es ¿qué valores podemos asignar a
los punteros?
Usar un literal no es una opción . El compilador no le permitirá
escribir algo como esto porque, en primer lugar, la sintaxis del
lenguaje "C ++" no lo permite y, en segundo lugar, si sabe lo que
está almacenado en la memoria de la computadora en la
dirección 148324 , entonces es clarividente y la programación de
computadoras es solo una pérdida de tiempo.

3.1.5 Cómo asignar un valor (2)


How to assign a value (2)

163 – By Gerson
Hay una excepción distintiva. Puede asignar cero a la variable
del puntero . Hacer esto no molesta al compilador.

Un puntero al que se le asigna un valor de cero se llama puntero


nulo (como en latín, nulo - ninguno).
Tal puntero no apunta a nada. Es como un cartel con toda la
escritura eliminada. Todavía existe, pero sin ningún propósito para
el vagabundo. Pero a pesar de todas las apariencias, este puntero
puede ser muy útil y, a veces, incluso necesario.

3.1.6 Cómo asignar un valor (3)


How to assign a value (3)

Debido al hecho de que asignar un valor a un puntero a veces


causa malentendidos y errores (algunos pueden confundir el
puntero con una variable de tipo int ), existe un acuerdo no escrito
de que los desarrolladores evitan asignar punteros nulos. En
cambio, siguen esta próxima convención →
El símbolo NULL es en realidad igual a cero. Parece una variable
pero no puede modificar su valor. Es una llamada macro . Le
contaremos más sobre esto en la lección 8.
La misma convención también dice que NULL debe asignarse solo
a punteros. Es una cuestión de cuidado y elegancia del estilo de
programación.
Tenemos una advertencia: si desea utilizar el símbolo NULL,
debe incluir el archivo de encabezado llamado cstring , o
cualquier otro archivo de encabezado que incluya cstring (uno de
ellos es iostream ).

El símbolo NULL se define allí.

164 – By Gerson
3.1.7 Cómo asignar un valor (4)
How to assign a value (4)

¿Cómo asignamos cualquier valor significativo que no sea NULL al


puntero? Podemos asignar el puntero con el valor que apunta a
cualquier variable ya existente .
Para hacer eso, necesitamos un operador & llamado operador de
referencia . Este es un operador de prefijo unario y no tiene nada
en común con los operadores binarios y bit a bit.
El operador simplemente encuentra la dirección de su
argumento. Ver la declaración →

3.1.8 Cómo asignar un valor (5)


How to assign a value (5)

Después de completar la asignación, la variable p apuntará al


lugar donde se almacena la variable i en la memoria. Podemos
hacerlo así →

3.1.9 Cómo asignar un valor (6)


How to assign a value (6)

Si asigna un NULL al puntero, se verá así. De ahora en adelante,


el puntero p no apunta a la variable i ni a ninguna otra variable.
Usaremos los siguientes símbolos para expresar esto. Como
puede ver, el puntero p está conectado a tierra .

165 – By Gerson
3.1.10 Cómo obtener un valor (1)
How to get a value (1)
¿Cómo obtenemos un valor señalado por el puntero?
¿Qué podemos hacer con un puntero que no es nulo y apunta a
un valor? Podemos desreferenciarlo .
La desreferenciación es una operación en la que la variable
de puntero (como veremos más adelante, no es solo una
variable, sino también una expresión que produce un puntero)
se convierte en sinónimo del valor al que apunta .
Observe cómo podemos declarar una variable de tipo int ( ivar ) y
una variable de tipo int * ( ptr ). Podemos hacerlo dentro de una
sola declaración →

3.1.11 Cómo obtener un valor (2)


How to get a value (2)

Ahora asignamos el valor de 2 a la variable ivar : ¿todo está claro


hasta ahora?

3.1.12 Cómo obtener un valor (3)


How to get a value (3)

Ahora hacemos que el puntero ptr apunte a la variable ivar

166 – By Gerson
3.1.13 Cómo obtener el valor (4)
How to get the value (4)

¿Cómo obtenemos el valor señalado por el puntero?


Tenemos que usar el operador conocido (un asterisco: "*") pero
en una situación completamente nueva, como
un desreferenciador .
Si coloca un asterisco delante de un puntero, obtiene un valor que
se almacena en la ubicación señalada por el puntero.

3.1.14 Cómo obtener un valor (5)


How to get a value (5)

La siguiente invocación muestra 2 en la pantalla, ya que el valor


de ptr desreferenciado se envía a cout (nota: ptr apunta
a ivar y ivar es igual a 2).

3.1.15 Cómo establecer un valor


How to set a value

¿Cómo establecemos un valor señalado por el puntero?


Si escribe una declaración como la de aquí →
No cambiará el valor del puntero . Simplemente cambiará el
valor señalado por el puntero en su lugar. Presta atención a esta
importante diferencia.

167 – By Gerson
3.1.16 Cómo obtener un valor (6)
How to get a value (6)

¿Cómo obtener un valor señalado por el puntero?


No olvide que si declara un puntero de la siguiente manera:
ANY_TYPE * puntero;
esto significa que:

 la variable del puntero es de tipo ANY_TYPE *


 la * pointerexpression es del tipo ANY_TYPE

Observe la ubicación del asterisco en ambos casos.


Además, no olvide que desreferenciar los punteros NULL está
estrictamente prohibido y genera problemas graves muy
rápidamente.

3.1.17 tamaño del operador


sizeof operator

El operador sizeof se distingue por su apariencia. Los operadores


que hemos encontrado hasta ahora están codificados como
caracteres individuales o dígrafos. Este nuevo operador parece
una variable.

No se deje engañar: este es un operador de prefijo unario y


con la mayor prioridad posible. También hay otra diferencia: un
operador típico requiere un valor como argumento y generalmente
cambia el valor de ciertas maneras.

168 – By Gerson
Este nuevo operador espera que su argumento sea literal o
variable o una expresión entre paréntesis o el nombre del tipo
(este es el único operador "C" que permite que su argumento sea
un tipo).

El operador proporciona información sobre cuántos bytes de


memoria ocupa (o puede ocupar) su argumento . El nombre
explica el propósito: aquí está →
Nota: no hay espacio entre "tamaño" y "de".
El sizeof no es solamente un operador - es también una
palabra clave.

3.1.18 Otro nuevo operador (1)


Another new operator (1)

Variable i se le asignará el valor de 1, porque los carbonilla valores


siempre ocupan un byte.
Tenga en cuenta que podemos lograr el mismo efecto escribiendo:
i = sizeof (char);
No puede usar paréntesis cuando el argumento es literal o un
valor, pero debe usarlos cuando el argumento es de tipo.

3.1.19 Otro nuevo operador (2)


Another new operator (2)
169 – By Gerson
La variable i se establecerá en el valor de 10 porque este es el
número de bytes ocupados por toda la matriz de pestañas .

3.1.20 Otro nuevo operador (3)


Another new operator (3)

La variable I se establecerá en el valor de 1: ¿puedes entender


por qué?

3.1.21 Otro nuevo operador (4)


Another new operator (4)

El siguiente ejemplo no es tan obvio →


Los valores del tipo int ocupan 32 bits, es decir, 4 bytes en la
mayoría de los compiladores / computadoras modernos, pero no
podemos garantizar que esto sea cierto en todos los casos.

3.1.22 Otro nuevo operador (5)


Another new operator (5)

Creemos que sería un buen ejercicio para usted compilar y


ejecutar el siguiente programa en su computadora. Al hacer esto,
170 – By Gerson
aprenderá cómo su computadora y compilador usan la
memoria .
El programa es quizás un poco simplista, pero su función es
identificar características interesantes de su entorno, y es lo
suficientemente bueno para esto.

171 – By Gerson
3.2.1 Punteros frente a matrices (1)
Pointers vs. arrays (1)

¿Qué tienen en común los punteros y las matrices?


Pues mucho. Comencemos con una definición importante: si
hay un nombre de una matriz sin índices, siempre es un
sinónimo del puntero que apunta al primer elemento de la
matriz .
¿Como funciona?
Hemos declarado un puntero a int y la matriz de tres elementos
de tipo int .

172 – By Gerson
3.2.2 Punteros frente a matrices (2)
Pointers vs. arrays (2)

Las dos asignaciones que siguen a la declaración


establecerán Ptr en el mismo valor. En otras palabras, la siguiente
comparación siempre es cierta:
Arr == y Arr [0]
El diagrama ilustra el efecto de esta asignación.

3.2.3 La aritmética de los punteros (1)


The pointers' arithmetic (1)

La aritmética de los punteros es significativamente diferente de la


aritmética de los enteros, ya que es relativamente reducida y solo
permite las siguientes operaciones:

 Agregar un valor entero a un puntero dando un puntero


( ptr + int → ptr )
 restando un valor entero de un puntero que da un puntero
( ptr - int → ptr )

173 – By Gerson
 restando un puntero de un puntero que da un número
entero ( ptr - ptr → int )
 comparar los dos punteros para igualdad o desigualdad (tal
comparación da un valor de tipo int que representa verdadero
o falso) ( ptr == ptr → int ; ptr ! = ptr → int )

Cualquier otra operación está prohibida o no tiene sentido. Estas


son las únicas operaciones que puede usar.
Analicemos brevemente lo que le sucede a un puntero que está
sujeto a una de estas operaciones. Seguiremos nuestra revisión
asumiendo las siguientes declaraciones y tareas →
En este punto, ptr1 apunta al primer elemento de la matriz .

3.2.4 La aritmética de los punteros (2)


The pointers' arithmetic (2)

Después de la siguiente asignación, ptr2 apunta también al


primer elemento de la matriz : la figura muestra el estado
actual de las variables.

3.2.5 La aritmética de los punteros (3)


The pointers' arithmetic (3)

Podemos verificar si los dos punteros son iguales; sí, lo son, ya


que apuntan al mismo elemento de la matriz .

174 – By Gerson
3.2.6 La aritmética de los punteros (4)
The pointers' arithmetic (4)

Veamos cómo funciona la suma. Estas declaraciones realizan la


misma operación: agregan 1 a ptr2 .
La interpretación de esta operación es la siguiente:

 se tiene en cuenta a qué tipo apunta el puntero; en nuestro


ejemplo, es int
 se determina cuántos bytes de memoria ocupa el
tipo (usamos el operador sizeof para este propósito), en
nuestro caso será sizeof ( int )
 el valor que queremos agregar al puntero se multiplica por el
tamaño dado
 el producto resultante aumenta la dirección almacenada en el
puntero

En efecto, el puntero se mueve al siguiente valor int en la


memoria.
El efecto del incremento se muestra en el diagrama.
¿Qué pasaría si agregamos 2 en lugar de 1?
En este caso, ptr2 aumentaría en (2 * sizeof ( int )) y ptr2 se
movería a través de dos valores int y señalaría el tercer elemento
de la matriz (es decir, matriz [2] ).

La comparación:

ptr1 == ptr2

obviamente es falso, mientras que este:


175 – By Gerson
ptr1! = ptr2

es cierto, ya que ambos punteros apuntan a datos


completamente diferentes .

3.2.7 La aritmética de los punteros (5)


The pointers' arithmetic (5)

Ahora restemos los punteros de la siguiente manera:


Dijimos anteriormente que una resta da un resultado de
tipo int . ¿Cómo se calcula?

 teniendo en cuenta: el tipo al que apuntan los punteros


( int ); significa que ambos punteros necesitan apuntar al
mismo tipo ; el compilador lo revisará
 las direcciones almacenadas en los punteros se restan
 el resultado de la resta se divide por el tamaño del tipo
señalado por los punteros

El resultado final nos dice cuántas variables de un tipo dado (es


decir, int ) se ajustan entre las direcciones almacenadas en los
punteros. En nuestro caso es 1, y este valor se asignará a
la variable i .
El resultado será mayor que 0 si ptr2 apunta a la memoria ubicada
después de ptr1 ; de lo contrario, será menor que 0.

176 – By Gerson
3.2.8 La aritmética de los punteros (6)
The pointers' arithmetic (6)

Intenta adivinar el resultado de la siguiente operación →


Encontrarás la respuesta en la siguiente diapositiva.

3.2.9 La aritmética de los punteros (7)


The pointers' arithmetic (7)

Aquí está la respuesta →

3.2.10 La aritmética de los punteros (8)


The pointers' arithmetic (8)

Supongamos que se ha realizado la siguiente operación →


¿Puedes adivinar el efecto?
Encontrarás la respuesta en la siguiente diapositiva.

177 – By Gerson
3.2.11 La aritmética de los punteros (9)
The pointers' arithmetic (9)

Aquí está la respuesta →

3.2.12 La aritmética de los punteros (10)


The pointers' arithmetic (10)

Intenta determinar el resultado de la siguiente resta →


¿Es 2?
¡Sí lo es! ¡Buen trabajo!

3.3.1 ¿Qué es una función?


What is a function?

La palabra " función " probablemente no sea tan ajena a


ti. Ciertamente, lo habrás escuchado muchas veces antes, incluso
en circunstancias que no tienen nada que ver con la programación.
Podemos apostar que has conocido esta palabra en una clase de
matemáticas. ¿Te acuerdas? El seno es solo uno de los muchos
ejemplos.
Una función es un tipo de cuadro (no siempre negro) que puede
hacer algo útil para nosotros, por ejemplo, evaluar un valor o
realizar algunas acciones. El primero significa que una función
178 – By Gerson
tiene (o puede tener) un resultado . Esto último significa que
una función tiene (o puede tener) un efecto .
En otras palabras, una función es solo una parte separada de un
código que se puede usar en (casi) cualquier momento para
evaluar algo, hacer algo o ambos. Identificamos una función
por su nombre .
El nombre de una función está sujeto a las mismas restricciones
que el nombre de una variable. Además, no podemos tener una
variable y una función del mismo nombre.

Cada función puede modificar su propio comportamiento


utilizando parámetros (¿recuerda? La función seno
también tiene un parámetro). Los parámetros pueden afectar lo
que se calcula o lo que se realiza dentro de la función. Además,
una función puede modificar los valores de sus parámetros si es
necesario.
Cuando desee que una función en particular realice sus acciones
y / o evaluaciones, debe invocarla .
La invocación de la función es una instrucción especial en el
lenguaje C ++. La invocación debe especificar el nombre de la
función que se invoca (siempre), los parámetros que la función
debe usar (si es necesario) y cómo usar el resultado evaluado por
la función (si corresponde).

Hay un inconveniente: no debe invocar una función que el


compilador desconoce. El compilador debe conocer la naturaleza
de la función (nombre, parámetros, resultado) y hay algunas
formas de informar al compilador sobre todas las funciones que
puede utilizar.

En general, podemos dividir las funciones en dos grupos:

 funciones escritas por otra persona (no usted) que están


disponibles por el entorno, a veces
llamadas funciones predefinidas o de biblioteca
 funciones escritas por usted

Puede usar ambos con la misma facilidad.


179 – By Gerson
3.3.2 ¿Por qué necesitamos funciones?
Why do we need functions?

Las funciones facilitan la creación de programas de múltiples


maneras. Podemos decir que es casi imposible escribir un
programa grande y complejo sin usar funciones. Incluso si tiene
éxito, no será un buen software por muchas razones.
Las funciones de biblioteca son las primeras funciones que utiliza
un desarrollador novato, pero muy pronto se hace evidente que
nunca son suficientes para resolver problemas complicados. Este
es el momento en que el desarrollador decide escribir sus propias
funciones.
No olvide: siempre es mejor asegurarse de que valga la pena
implementar la función que necesita desde cero. Siempre es una
buena idea verificar si la función que desea no ha sido
escrita por otra persona , para evitar reinventar la puerta
abierta.

Las funciones permiten a los desarrolladores dividir un problema


(y también un código) en partes más pequeñas. Un código más
pequeño es más fácil de escribir, probar, mantener y comprender.
Tener un conjunto de funciones bien escritas y probadas le permite
al desarrollador construir el código de una manera similar a la
construcción de viviendas: usando bloques prefabricados. Este
método de dividir el problema se conoce como descomposición .

En general, hay dos enfoques posibles que puede usar durante la


descomposición: el enfoque de arriba hacia abajo (cuando
intenta definir primero las funciones más generales y luego las
divide en funciones más simples y más especializadas) y
el enfoque de abajo hacia arriba (cuando comienzas tu trabajo
creando un conjunto de funciones altamente definidas y altamente
180 – By Gerson
especializadas, y luego ensamblándolas en estructuras más
complejas).
Los desarrolladores experimentados saben que crear funciones
siempre es una buena idea, incluso si la nueva función se invoca
solo una vez durante la ejecución del programa. Si el código que
ha escrito es demasiado largo para caber en una pantalla,
considere dividirlo en funciones.
Es más fácil controlar una manada de funciones pequeñas y bien
definidas que una gran porción de código que no se puede ver de
un vistazo.

3.3.3 Introducción a las funciones.


Introduction to functions

Cada función se caracteriza por los siguientes rasgos:

 nombre
 parámetros
 tipo de resultado

181 – By Gerson
La parte del código que especifica todos estos elementos se
conoce como la declaración de función . El compilador debe
conocer la declaración de la función para permitirle interpretar
correctamente las invocaciones de la función. La declaración de
función a veces se denomina prototipo de función .
Una declaración de función no dice nada sobre lo que la función
hace exactamente. Esa información es proporcionada por el
cuerpo de la función, que es una parte separada del código, entre
paréntesis.
Una declaración de función enriquecida con un cuerpo de función
forma la denominada definición de función .

Imagine que necesitamos una función que pueda evaluar la


segunda potencia de cualquier número flotante .
Nos damos cuenta de que:

 cuadrado sería un buen nombre para la función; por supuesto,


podemos llamarlo de muchas maneras diferentes, por
ejemplo, sqr , SecondPowerOf o incluso JohnDoe (aunque
probablemente no sea una buena idea, ¿verdad?)
 las funciones necesitan un parámetro: el valor que se elevará a
la potencia de 2; x será un buen nombre para el parámetro,
aunque no muy original;
 el tipo de resultado es flotante (como el parámetro)

Podemos escribir la declaración de función ahora. Puedes verlo a


la derecha →
Tenga en cuenta que el primer flotante especifica el tipo del
resultado, mientras que el segundo es el tipo del parámetro.

3.3.4 Primera función (1)


First function (1)

182 – By Gerson
Si queremos aprovechar esta función, necesitamos entregar su
definición . Puedes verlo a la derecha →
Nota: la transformación de una declaración en una definición
requiere que agreguemos un cuerpo, pero el cuerpo también
reemplaza el punto y coma que termina la declaración (ver
diapositiva anterior). El cuerpo de la función no termina con un
punto y coma.
El cuerpo contiene:

 una declaración de la variable de resultado ; la variable se


usará dentro de la función y solo dentro de la función ; no
es visible ni accesible en ninguna otra parte de su código;
 a la variable resultante se le asigna el valor
del parámetro x multiplicado por sí mismo; Este es el método
más simple y rápido de elevar un número a la potencia de 2;
 la última instrucción de la función cuadrada es return ; Esta
instrucción es responsable de dos acciones importantes:

1. indica qué valor se devuelve (proporcionado) como resultado


de la función
2. se termina la ejecución de la función de

3.3.5 Primera función (2)


First function (2)
Ahora haremos uso de nuestra primera función y la haremos parte
de un programa completo y ejecutable. Aquí está a la derecha →

183 – By Gerson
Los parámetros definidos dentro de la función se
denominan parámetros formales . Los valores realmente
transferidos a la función (por lo tanto, existentes fuera de la
función) se denominan parámetros reales .
La invocación de la función es solo el nombre de la función que se
invoca junto con los valores transferidos (pasados) a la función
como parámetros reales.
Como puede ver, hemos declarado una variable llamada arg y
le hemos asignado el valor de 2.0. A continuación, hemos
invocado la función cuadrada , entregando la variable arg como
argumento (el parámetro real).
El resultado de la función se envía a la pantalla y se muestra como
parte del mensaje que dice " La segunda potencia de 2 es 4 ".
source_03_03_05_square.cpp

184 – By Gerson
3.3.6 Primera función (3)
First function (3)
¿Es posible colocar la función cuadrada después
de la función principal y no antes ? Sí, lo es, pero no olvide que
el compilador debe conocer todos los rasgos de la función
invocada.
Por lo tanto, debe colocar la declaración de función antes de la
primera invocación de función.
Echa un vistazo al código modificado aquí →

Nota: puede omitir el nombre del parámetro formal en el prototipo


de la función.
Ahora está listo para aprender sobre algunas de las propiedades
más avanzadas de las funciones.
source_03_03_06_power2.cpp

185 – By Gerson
3.4.1 Declarar funciones
Declaring functions

Una declaración de función está destinada a informar a un


compilador sobre el nombre de la función, su tipo de

186 – By Gerson
retorno y el tipo de los parámetros (si los hay) . Una forma
general de declaración de función (o prototipo de función -
podemos usar estos términos indistintamente) está aquí a la
derecha →

 return_type describe el tipo de resultado devuelto


(entregado) por la función (por ejemplo, esperamos que
la función seno devuelva un valor de tipo float ya que
los datos int son completamente inutilizables en este
contexto); puede usar cualquiera de los tipos de C ++
como return_type , incluido un tipo muy especial
llamado void ; una función de tipo void no devuelve ningún
resultado; podemos decir que tal función puede tener un efecto
pero definitivamente no tiene resultado; si omite return_type ,
el compilador supone que la función devuelve un valor de
tipo int

 nombre_función es un identificador que nombra la función y


la distingue de todas las demás funciones (nota: puede
tener más de una función del mismo nombre, en contraste con
las variables; esto se llama sobrecarga y se lo informaremos
pronto) )

 parámetros_lista (¡tiene que encerrarlo entre paréntesis!) es


una lista separada por comas de pares de "nombres de tipo",
donde se puede omitir cada uno de los nombres ; la lista puede
estar vacía pero los paréntesis aún deben estar allí; Puede
enfatizar el hecho de que su función no tiene parámetros al
poner la palabra vacío dentro de los paréntesis, de esta
manera:

nula diversión ( nula );


Esta declaración describe una función llamada fun que no
devuelve un resultado y no tiene parámetros; Aquí hay un ejemplo
de dos declaraciones de funciones equivalentes:
int func ( int número);
int func ( int );

En este contexto, el nombre del parámetro ( número ) no significa


nada para el compilador y lo ignora por completo; no es un
argumento en contra de especificar el nombre del parámetro en
187 – By Gerson
las declaraciones, ya que podría ser muy útil para las personas
que intentan entender su código

 la declaración termina con un punto y coma que no debe


omitirse.

Aunque cada uno de los elementos de la lista de parámetros se


parece a la sintaxis de una declaración de variable, no puede
declarar más de un nombre de parámetro al mismo
tiempo. Puedes hacerlo:
int x, y;
pero debe usar la siguiente forma detallada si declara dos (o más)
parámetros de función:
nula diversión ( int x, int y);

3.4.2 Definiendo funciones


Defining functions

Una definición de función especifica el código que se ejecutará en


cada invocación de función. Se diferencia de la declaración en el
hecho de que un punto y coma se reemplaza por un cuerpo
que contiene una secuencia de declaraciones y / o
instrucciones .
Si return_type es nulo, es posible que el cuerpo no
contenga la instrucción de devolución , pero si se usa de
todos modos, debe tener la siguiente forma:
regreso;
Nota: no hay ningún valor después del retorno (obviamente,
una función nula no puede devolver ningún valor).
Se supone que la declaración de retorno se ejecuta
implícitamente dentro del cuerpo de la función vacía justo
antes del corchete de cierre. No tiene que escribirlo allí
explícitamente.
Si return_type no es nulo , el cuerpo debe contener al menos
una declaración de retorno que especifique el valor del resultado
188 – By Gerson
de la función. No puede dejar el cuerpo de una función sin
especificar el valor del resultado.
Cualquier valor que desee que devuelva el cuerpo de la función
debe tener un tipo compatible con el tipo especificado como tipo
de función.
Puede usar tantas declaraciones de retorno como
necesite para implementar eficazmente su algoritmo.

3.4.3 Funciones de ejemplo (1)


Example functions (1)

Supongamos que necesitamos una función altamente


especializada para saludar a un usuario que se atrevió a ejecutar
nuestro programa.
Elegimos "Saludar" como el nombre de esa función, y decidimos
que la nueva función no debería tener resultados ni parámetros.
Puede ver la definición de la función de saludo a la derecha →

3.4.4 Funciones de ejemplo (2)


Example functions (2)

Algunos usuarios (aquellos con egos muy grandes) necesitan ser


recibidos más de una vez. Para evitar escribir funciones separadas
para diferentes categorías de usuarios, escribiremos una función
universal que se pueda indicar cuántas veces se deben gritar
189 – By Gerson
los saludos . Usaremos un parámetro de tipo int para este
propósito.
La función está lista. Puedes verlo a la derecha →
Como puede ver, hemos utilizado una función previamente
definida. De esta manera, si cambiamos el texto del saludo (por
ejemplo, " Estoy listo para servirle, mi Maestro "), tendría un
efecto en ambas funciones.
Ahora preste atención a cómo se ve la invocación de la función en
el código. Hemos tenido que usar un par de paréntesis, aunque no
hayamos especificado un parámetro.
También tenga en cuenta cómo nombramos la función o, más
bien, cómo pegamos todas las palabras juntas. Esta técnica de
combinar palabras para recibir el nombre de una variable o una
función se llama CamelCase (¡realmente!), Ya que las letras
mayúsculas consecutivas parecen jorobas en comparación con el
resto del nombre.
Usamos CamelCase en muchos de nuestros ejemplos, aunque es
una cuestión de estética y no un requisito de idioma.

3.4.5 Funciones de ejemplo (3)


Example functions (3)
Ahora le mostraremos un programa completo que demuestra el
funcionamiento de nuestras funciones. El programa le pregunta al
usuario el tamaño de su ego y responde con un saludo
adecuado. El tamaño del ego se mide en metros (disculpas a todos
los usuarios de unidades imperiales).

190 – By Gerson
Mire cómo enviamos el valor del parámetro real a la función
invocada. Intente imaginar cómo el parámetro real reemplaza un
parámetro formal dentro de una función.
El usuario recibe un saludo por kilómetro y un saludo adicional
(para aquellos sin ego).
source_03_04_04_ave.cpp

#include <iostream>

usando el espacio de nombres estándar ;

void Greet ( void )


{
cout << "¡Usuario de Ave!" << endl ;
}

void GreetManyTimes ( int howmanytimes )


{
while ( howmanytimes> 0 )
{
Greet () ;
howmanytimes-- ;
}
}

int main ( void )


{
191 – By Gerson
int sizeofego ;

cout <<"¿Qué tan grande es tu ego? [Km]" << endl ;


cin >> sizeofego ;
GreetManyTimes ( 1 + sizeofego ) ;
devuelve 0 ;
}

include <iostream>

using namespace std;

void Greet(void)
{
cout << "Ave user!" << endl;
}

void GreetManyTimes(int howmanytimes)


{
while(howmanytimes > 0)
{
Greet();
howmanytimes--;
}
}

int main(void)
{
int sizeofego;

192 – By Gerson
cout << "How big is your ego? [km]" << endl;
cin >> sizeofego;
GreetManyTimes(1 + sizeofego);
return 0;
}

3.4.6 Funciones de ejemplo (4)


Example functions (4)
Veamos una función más seria ahora. Su tarea es convertir un
valor de temperatura expresado en Fahrenheit a Celsius.
Como ya sabrás, la fórmula dice que:
[° C] = ([° F] - 32) × 5 ⁄ 9
Hemos incorporado la fórmula dentro de
la función FahrenheitToCelsius .
Vamos a probar nuestra función obligándola a evaluar los
resultados de algunos valores característicos. Haremos esto
mediante el uso de la función TestTheFunction , que está diseñada
para producir una salida clara y legible que permita a los
evaluadores (nosotros) verificar la corrección de la función.
El código completo está a la derecha →
Esperamos que el programa produzca el siguiente resultado:
Fahrenheit 32 corresponde a 0 Centígrados
Fahrenheit 212 corresponde a 100 Centígrados
Fahrenheit 451 corresponde a 232.778 centígrados
source_03_04_06_temps.cpp

193 – By Gerson
3.4.7 La sintaxis de invocación - suplemento
The invocation syntax - supplement

Como ya sabes, una función puede:

 devuelve un valor cuando tiene un nombre de tipo delante de


su nombre o no tiene el nombre de tipo allí (en este caso, se
considera que la función devuelve un valor int ); dicha función
tiene un resultado y también puede tener un efecto

 no devuelve nada cuando la palabra clave nula está delante


de su nombre; tal función no tiene un resultado y podemos
esperar que tenga un efecto

Hemos mencionado que estos dos tipos de funciones difieren en


la forma en que usan la declaración de devolución , pero también
hay otra diferencia con respecto a las invocaciones.
Supongamos que tenemos dos funciones, que se muestran
esquemáticamente a la derecha →
Tenga en cuenta que:

 la única forma aceptable de la invocación VoidFunction se ve


así:

VoidFunction (2);
la función NonVoid se puede invocar de las dos formas siguientes:

194 – By Gerson
valor = NonVoidFunction (2);
Función No Vacío (2);
Significa que el resultado de cualquier función no nula puede
ser aceptado por el invocador (el primero) y asignado a una
variable, o utilizado de cualquier otra manera, o puede
ser ignorado por el invocador (el último) y simplemente olvidado
inmediatamente después de la devolución de la función

3.5.1 Efectos secundarios Side effects


Cualquier función debe tener la capacidad de comunicarse con
su entorno. Debe poder recibir datos (números, textos, etc.),
procesarlos y compartir los resultados. Ya conocemos dos tipos
de comunicación como esta:

 transferir datos a una función utilizando parámetros


reales cuyos valores se asignan a parámetros formales

 transferir datos de una función usando el resultado de la


función ; tenga en cuenta que solo se puede transferir un valor
por tales medios porque la sintaxis de la declaración
de devolución le permite especificar solo un valor

Hemos dicho antes que la variable definida dentro del cuerpo de


la función no se puede usar ni acceder desde fuera de la
función. Además, hay un tipo especial de variable
llamada variable global .
Las variables globales se declaran fuera de cualquier función y,
por lo tanto, son accesibles para todas las funciones declaradas en
el mismo archivo fuente.
Tenga en cuenta que la declaración de la variable debe preceder
a la definición de la función para que la función pueda reconocerla.

195 – By Gerson
Las variables globales permiten obtener funciones y
proporcionar datos de cualquier tipo . Si una función modifica
cualquier variable global que no utiliza ningún otro mecanismo de
transferencia de datos, decimos que esta función tiene un efecto
secundario .
Los efectos secundarios, aunque a veces son útiles, no se
recomiendan y se consideran un signo de mal estilo de
programación porque hacen que el código sea difícil de entender.
Echa un vistazo al código de la derecha →
El globvar es una variable global. Su declaración no está
contenida en ninguna función. La función func incrementa
el globvar en cada invocación. Podemos decir que globvar se usa
para contar las ejecuciones de func .
Tenga en cuenta que la función principal también utiliza esta
variable, aunque no modifica el valor de la variable. Por esta
razón, suponemos que la función principal no causa efectos
secundarios.
Evitaremos el uso de efectos secundarios en futuros
ejemplos. Trátelos solo como una posibilidad, no como una rutina.
source_03_05_01_enjoy.cpp

196 – By Gerson
3.5.2 Pasando parámetros por valor
Passing parameters by value
Hasta ahora, hemos estado asumiendo que los valores del
parámetro real se envían a la función y no hemos dicho una
palabra sobre el camino de regreso. Nuestros parámetros viajan
al cuerpo de la función y no esperamos que regresen de allí.
Hagamos un experimento simple que muestre si una función
puede cambiar el valor de su parámetro.
Ahora eche un vistazo al código de la derecha →
Como puede ver,
la función AmIAbleToChangeMyParameter incrementa el valor de
su parámetro. También lo informa al usuario. La pregunta es: ¿el
valor modificado es visible fuera de la función? En otras
palabras: ¿el cambio del parámetro formal refleja el valor
real del parámetro?
197 – By Gerson
Vamos a compilar el código y ejecutarlo. Debe producir el
siguiente resultado:
var = 1
----------
Tengo: 1
Estoy a punto de devolver: 2
----------
var = 1
Analice el ejemplo cuidadosamente y haga el experimento usted
mismo.
Como puede ver, el valor del parámetro formal no reemplaza
el valor del parámetro real al regresar de la
función . Podemos decir que el parámetro real tiene un boleto
unidireccional: transporta un valor a la función y no lo lleva al
invocador.
No te olvides de eso. Esta forma de comunicación se basa en
transferir un valor del invocador a la función. Y es por eso que este
método se llama pasar parámetros por valor .
source_03_05_02_not_able.cpp

198 – By Gerson
3.5.3 Pasando parámetros por referencia (1)
Passing parameters by reference (1)

El lenguaje C ++ no solo ofrece un método para pasar


parámetros. El segundo método se llama pasar por
referencia y permite que las funciones afecten los valores de un
parámetro real.
Si va a pasar algún parámetro por referencia, debe anunciarlo
mientras declara la función. Vea el ejemplo a la derecha →
Se ve muy similar al ejemplo anterior, ¿no? Mire cuidadosamente
y encuentre una diferencia específica. Esta es una diferencia muy
importante, que cambia radicalmente el comportamiento de la
función.

Sí tienes razón. La diferencia se debe a que el signo & se coloca


delante del nombre del parámetro. Aclaremos:

 nombre de tipo: el parámetro de nombre se pasa por valor

 tipo y nombre: el parámetro de nombre se pasa por


referencia

Tenga en cuenta que este es el tercer papel que juega el carácter


& en el lenguaje "C ++". ¿Te acuerdas de los otros dos?
Cuando se pasa un parámetro por referencia, significa que un
parámetro formal es solo un sinónimo de un parámetro
real . Cada modificación realizada en un parámetro formal afecta
inmediatamente a un parámetro real asociado. Podemos decir
informalmente que los parámetros pasados por referencia tienen
tickets de retorno y devuelven sus valores modificados al
invocador.
Como puede sospechar, el código producirá el siguiente resultado:

199 – By Gerson
var = 1
----------
Tengo: 1
Estoy a punto de devolver: 2
----------
var = 2
source_03_05_03_able.cpp

3.5.4 Pasando parámetros por referencia (2)


Passing parameters by reference (2)
Hacemos la selección del método de pasar por cada
parámetro individual . Puede mezclar parámetros de ambos
tipos si lo encuentra útil. Use " pasar por valor " si no necesita
compartir los resultados de la función usando los valores del
parámetro, y use " pasar por referencia " en todos los demás
casos.

200 – By Gerson
Vea el ejemplo a la derecha →
La función MixedStyles asigna su segundo parámetro con un valor
incrementado de su primer parámetro. Esto significa que el primer
parámetro se pasa por valor, mientras que el último se pasa por
referencia.
El programa produce el siguiente resultado:

var1 = 1, var2 = 2
source_03_05_04_mixed.cpp

3.5.5 Pasando parámetros por referencia (3)


Passing parameters by reference (3)

El método de "pasar por referencia" tiene


una limitación importante y obvia . Si un parámetro se
declara como aprobado por referencia (por lo que está
precedido por el signo &), su parámetro real
correspondiente debe ser una variable .
Un parámetro real que se refiere a un parámetro formal "pasado
por valor" puede ser una expresión en general, por lo que

201 – By Gerson
podemos usar no solo una variable sino también un resultado
literal, o incluso una invocación de función.
Decimos que esta limitación es "obvia" porque la función no puede
colocar un valor en algo que no sea una variable. No puede asignar
un nuevo valor a un literal ni forzar a una expresión a cambiar su
resultado. Ver el fragmento a la derecha →
Se permiten todas las invocaciones siguientes:

 ByVal (i);
 ByVal (i + 2);
 ByVal (intfun (0));

Si desea modificar las invocaciones para aprovechar


la función ByRef , solo puede usar la primera. Todos los demás
causarán un error de compilación .

3.5.6 Pasando parámetros por valor


Passing parameters by value
Pasando parámetros por valor: nuevamente, pero de manera
diferente
¿Es posible utilizar "pasar por valor" y poder propagar el valor
fuera de la función a pesar de la naturaleza unidireccional de este
método?
La respuesta es sí". Le mostraremos cómo hacerlo, pero queremos
enfatizar que esta no es la forma en que recomendamos
hacerlo. Este método se hereda del lenguaje de programación "C",
el ancestro "C ++", y era la única forma disponible de realizar una
202 – By Gerson
comunicación bidireccional basada en parámetros. El lenguaje C
no ofrece nada similar al mecanismo de "pasar por referencia" y,
por lo tanto, el uso de otros métodos (incluso podríamos decir un
poco arriesgado) está totalmente justificado.
La idea se basa en transferir un puntero a un valor, no el valor en
sí. Si declaras una función con un prototipo como este:

nulo ByPtr ( int * ptr);

habilita la función para tratar las direcciones que apuntan


a valores int y, por lo tanto, le da a la función la posibilidad de
modificar los valores señalados por el parámetro .
Ahora eche un vistazo al código de la derecha →
La función ByPtr toma un parámetro, que es un puntero ,
y accede al valor apuntado por el puntero utilizando
el operador * (desreferencia).
Nota: si p es un puntero a un valor, * p representa el valor en sí.
En efecto, la función ByPtr modifica la variable sin siquiera saber
acerca de su existencia.
Algunos dirían que este es el tercer método para pasar
parámetros. No estamos de acuerdo: sigue siendo un método
antiguo de "pasar por valor". Solo los valores son diferentes.
El código de ejemplo produce el siguiente resultado:
variable = 2
source_03_05_06_byptr.cpp

203 – By Gerson
3.6.1 Parámetros - cont.
Parameters – cont.

Ahora vamos a reescribir nuestra función Greet para que sea más
flexible. Queremos que:

 poder emitir cualquier saludo , no solo el predefinido en el


código fuente,

 poder emitir el saludo más de una vez , a petición del


invocador.

Esto significa que nuestro NewGreet (así es como llamamos a la


nueva función) debe tener dos parámetros destinados a:

204 – By Gerson
 guardar el saludo

 almacenar el número de repeticiones de saludo

La función completa junto con la función principal corta está aquí


a la derecha →
source_03_06_01_greet.cpp

3.6.2 Parámetros predeterminados: un ejemplo simple


Default parameters – a simple example

La nueva función nos facilita la vida, pero queremos más de ella


(¿no es siempre así?). Hemos aprendido que la mayoría de
nuestros usuarios requieren solo un saludo al mismo tiempo,
probablemente debido al tamaño promedio de sus egos. Esto
puede significar que podríamos usar la siguiente forma de saludo
con más frecuencia que otras:

NewGreet ("Buenos días", 1);

205 – By Gerson
Es posible que solo queramos omitir el "1", con la esperanza de
que la función sea lo suficientemente inteligente como para
adivinar lo que vamos a hacer.
¿Es razonable esperar que cualquier función pueda comportarse
de una manera tan conveniente?
La respuesta es sí, pero ... Pero primero debemos informarle que
algunas de las invocaciones no especificarán todos los parámetros
esperados, e indicarán qué valores deben usarse en lugar de los
ausentes. Este mecanismo se llama " parámetros
predeterminados " y presentaremos sus reglas modificando
ligeramente nuestra función anterior.
Podemos modificar la declaración del segundo parámetro formal
usando una frase:
= valor
para indicar que queremos que el compilador asuma el valor
predeterminado para el parámetro cuando lo omitimos durante la
invocación. Si declaramos la función de la siguiente manera:
NewGreet (string greet, int repeats = 1)
el compilador tratará las invocaciones de un parámetro como
esta:
NewGreet ("Hola");
como si estuvieran (explícitamente) escritos de la siguiente
manera:
NewGreet ("Hola", 1);
Ahora mire cuidadosamente el ejemplo a la derecha →
Tenga en cuenta las diferentes formas de invocación. El programa
producirá el siguiente resultado:

Hola
hola
buenos dias
hola
Como puede ver, el valor del parámetro especificado
explícitamente invalida el valor predeterminado.
source_03_06_02_newgreet.cpp

206 – By Gerson
3.6.3 Parámetros predeterminados: un ejemplo más
complejo
Default parameters – a more complex example
Es posible que desee preguntar ahora si es posible tener más de
un parámetro predeterminado en una función, es decir, si
podemos elegir el valor predeterminado no solo para el parámetro
de repeticiones sino también para el saludo .
Si es posible. Aquí hay un ejemplo de cómo hacerlo a la derecha

Este mecanismo es útil, pero para aprovecharlo al máximo no
debe olvidarse de las siguientes limitaciones:

 el orden de los parámetros es muy importante (en contraste


con los parámetros normales, no predeterminados, que pueden
estar en prácticamente cualquier orden); intuitivamente,
podemos decir que los parámetros no predeterminados deben
codificarse antes que los predeterminados; el compilador no
podrá identificar los parámetros de otra manera

 si se declara más de un parámetro con un valor predeterminado


y se especifica al menos un parámetro real en la invocación, los
parámetros reales se asignan a sus homólogos formales en el

207 – By Gerson
mismo orden en que se enumeran en la declaración de
función; Esto significa que no puede usar el valor
predeterminado para el primer parámetro y especificar un valor
explícito para el segundo

source_03_06_03_newgreet2.cpp

3.7.1 Anatomía de una invocación de función


Anatomy of a function invocation
Hasta ahora, hemos estado usando el término " invocación de
función " sin entrar en detalles. Simplemente hemos asumido
que las invocaciones hacen su trabajo de forma automática o
mágica (o como solían decir los desarrolladores,
"automágicamente"). Ahora es el momento de arreglar esto y
explicar cómo funciona este mecanismo.
Mira el ejemplo de la derecha →
Hay una función que toma su argumento, lo multiplica por 2 y
devuelve el resultado al invocador. La función se invoca tres veces
dentro de la función principal. ¿Bastante claro? Bien, sigamos
adelante.
Vamos a ver el código desde la perspectiva del compilador. Lee
el código de función , lo traduce al código de la máquina y
almacena el código traducido en un lugar separado en la memoria,
208 – By Gerson
pero el código no puede usarse "tal cual" sin pasos adicionales. El
código de cada función debe complementarse con dos elementos
importantes: un prólogo y un epílogo .
Un prólogo es la parte del código ejecutada implícitamente
antes de la función . El prólogo es responsable de transferir los
parámetros del código del invocador y de almacenarlos en un área
transitoria especial llamada "pila".
El epílogo se ejecuta implícitamente justo después del
código de la función y es responsable de transferir el resultado
de la función y de borrar la pila de los valores colocados allí por el
prólogo

3.7.2 Prologue and epilogues


Prologues and epilogues
Echa un vistazo al diagrama de la derecha →
Este diagrama ilustra el flujo de control durante la invocación de
una función del ejemplo anterior. Todas las demás invocaciones
se llevarán a cabo de la misma manera.
Este enfoque tiene algunas ventajas obvias . El código de
función, el prólogo y el epílogo ocupan el mismo espacio de

209 – By Gerson
memoria independientemente de cuántas veces se invoque la
función. Significa que invocarlo de esta manera ahorra memoria y
hace que su programa sea más compacto.
Pero...
Una de las paradojas más interesantes de la programación de
computadoras dice que cuando un código es compacto, no
puede ser rápido al mismo tiempo; y viceversa, cuando el
código es rápido, no puede ser compacto. Por supuesto, es más
una broma que una ley científica, pero en este caso la regla
funciona muy bien.
Trate de imaginar que nuestro programa invocará la función
muchas veces (por ejemplo, cientos o miles de veces). Puede
significar que tendrá que pagar un alto precio (en el sentido del
tiempo) por todas esas transferencias de control y ejecuciones de
prólogo / epílogo. El precio es más alto cuando la función es corta
(es decir, más corta que el prólogo y el epílogo).
Este ejemplo muestra que a veces, en casos bien definidos, sería
mejor evitar la cadena prólogo-función-epílogo e insertar el
código de la función directamente en el código del
invocador .

3.7.3 Función en línea


Function inlining

Eche un vistazo al diagrama modificado a la derecha →


Este diagrama ilustra otro enfoque del problema de la invocación
de funciones. Cuando la función es corta y rápida, y
cuando esperamos que la función se invoque con mucha
210 – By Gerson
frecuencia , es mejor (y más efectivo) escribir el código de la
función en cada invocación.
Por supuesto, tendremos que pagar por eso. No se sorprenda por
el hecho de que el tamaño total del código será significativamente
mayor que antes.
La táctica de compilar invocaciones de funciones se llama función
en línea . Una función compilada como esta se llama función en
línea .

3.7.4 Funciones en línea


Inline functions

Si desea que una determinada función se compile e invoque como


una función en línea, debe marcarla de manera especial.
Debe preceder la declaración de función con la palabra clave
en línea . Mira el ejemplo a la derecha →
Afortunadamente para nosotros, la sintaxis de esta construcción
tiene cierta flexibilidad:

211 – By Gerson
 no importa si la palabra clave en línea se coloca antes o
después del nombre del tipo; ambas líneas son sintácticamente
correctas:

función int en línea ( parámetro int )


función en línea int ( parámetro int )

 si necesita usar tanto la declaración como la definición para la


misma función, no importa dónde coloque la palabra clave
en línea ; es correcto usarlo en la declaración y omitirlo en la
definición; También es igualmente válido usarlo en la definición
y omitirlo en la declaración; por supuesto, usar la palabra clave
en ambos lugares también es válido.

3.8.1 Diferentes herramientas para diferentes tareas

Diferentes tareas requieren diferentes herramientas, aunque estas


herramientas pueden tener exactamente el mismo nombre. Por
ejemplo, la herramienta llamada "cuchillo" se ve muy diferente
cuando la usa un cirujano, un cocinero o un asesino en
serie. Dijimos antes que no se nos permite tener una variable y

212 – By Gerson
una función del mismo nombre. Es hora de preguntar si podemos
tener más de una función del mismo nombre.
Si podemos. Es natural que deseemos tener diferentes
herramientas del mismo nombre para diferentes
propósitos . Por ejemplo, necesitamos una función para
encontrar el mayor de dos números flotantes. No parece difícil,
¿verdad?
Bueno, hemos escrito esta función para ti. Echa un vistazo al
fragmento de la derecha →
Podría argumentar que podríamos haber escrito la función de una
manera más compacta. Estamos de acuerdo contigo Es
demasiado detallado. Siéntase libre de reescribirlo de una manera
más inteligente.

3.8.2 Max - versión extendida

Imagine que un día nuestras necesidades aumentaron y de


repente quisimos tener una función muy similar que pudiera
encontrar el mayor de los tres valores. ¿Qué podíamos hacer?
Por supuesto, podemos hacer uso de la función anterior y,
suponiendo que queremos encontrar la mayor de las variables a,
byc, hacemos algo como esto:

x = max (max (a, b), c);

213 – By Gerson
¿Qué, no te gusta? A nosotros tampoco nos gusta. No es
agradable ni efectivo. Sería mejor olvidar nuestra antigua función
y escribir una nueva y elegante que se adaptara mucho más a lo
que queremos.
También sería una buena idea nombrarlo como el
anterior: max . Este nombre ilustra perfectamente el rol y el
propósito de la función.
Echa un vistazo al fragmento de la derecha →

3.8.3 ¿Cómo funciona?

El mecanismo que nos permite tener más de una función de un


nombre determinado se llama sobrecarga (ya que el mismo
nombre está sobrecargado con diferentes significados).
Hay una limitación importante: todas las funciones
sobrecargadas deben ser claramente distinguibles para el
compilador . El compilador no puede dudar sobre cuáles de las
variantes sobrecargadas deben usarse en una parte particular del
código.

214 – By Gerson
Nuestros ejemplos no dejan dudas. La elección es simple: si la
invocación contiene tres parámetros reales, se elige la segunda
variante. Si hay dos parámetros, el compilador usa la primera
variante. Cualquier otra variante de la invocación se considera un
error.
¿Qué circunstancias tiene en cuenta el compilador al elegir la
función de destino (una de las pocas disponibles)?

 el número de parámetros : por ejemplo, si hay tres funciones


sobrecargadas con (respectivamente) dos, tres y cuatro
parámetros, y la invocación especifica dos parámetros, solo la
primera de las funciones puede usarse como un objetivo (esta
función se llama ' el mejor candidato ')

 los tipos de parámetros : si hay más de una función con el


mismo número de parámetros, la función de destino se
selecciona en función de la conformidad de tipo de
los parámetros

Nota: el tipo de retorno no se tiene en cuenta cuando el


compilador busca el mejor candidato para una determinada
invocación. Esto debería ser obvio para usted si recuerda que el
valor de retorno de cualquier función escrita puede ignorarse.
En este sentido, dos funciones que difieren solo en el tipo de
retorno son indistinguibles para el compilador. Esto significa que
el siguiente fragmento es incorrecto:
int fnc(int a) {
return a;
}

void fnc(int a) {
}

int fnc ( int a) {


devuelve a;
}
void fnc ( int a) {
}

215 – By Gerson
3.8.4 ¿Cómo encontrar al mejor candidato? (1)

El número de diferentes tipos de datos en el lenguaje C ++ es


significativo, por lo que el mecanismo responsable de encontrar el
mejor candidato tiene que usar algunas simplificaciones para no
obligar a los desarrolladores a crear una función separada para
cada tipo de datos diferente. Eche un vistazo al siguiente ejemplo

Intente responder a esta pregunta: ¿cuál de estas dos funciones
sobrecargadas es el mejor candidato para la invocación?
Sí, tiene toda la razón (esperamos): es el primero, con
la declaración del parámetro int x .

3.8.5 ¿Cómo encontrar al mejor candidato? (2)

Hemos cambiado un poco nuestro ejemplo. ¿Puedes ver la


diferencia? →
¿Cuál de las funciones es el mejor candidato ahora? Quieres decir
que es el segundo, ¿no? Lo siento, te equivocas.
No hay un buen candidato para la invocación , ¿por qué?

216 – By Gerson
El literal 1.0 no es de tipo flotante . Es de tipo doble (¡en
serio!). El compilador del lenguaje C ++ intenta promover los
tipos si no hay un ajuste exacto (como en nuestro ejemplo:
obviamente, un flotante no es un doble ).

Lamentablemente (para ser sincero, afortunadamente) la


dirección de este tipo de promoción va de menos precisa a más
precisa, y no al revés. Significa que cualquier flotador puede
promoverse a doble , pero no se puede promover ningún doble (o
más bien degradar) a flotar .
En esta situación, el compilador le informará que no puede
encontrar el mejor candidato y las compilaciones fallarán.
Tienes dos opciones:

 puede escribir la tercera instancia de PlayWithNumber con un


parámetro de tipo double

 puedes convencer al compilador de que tu literal es de


tipo float ; puede hacerlo simplemente agregando el sufijo f al
número, así:

PlayWithNumber (1.0f);

El compilador estará satisfecho.

3.8.6 Un nuevo operador: uno de tres argumentos

Ahora es un buen momento para mostrarle otro operador de


lenguaje "C ++" llamado ?: . Este operador es muy original
porque requiere tres argumentos. Así es como lo usamos →
Este operador funciona de la siguiente manera:

217 – By Gerson
 calcula el valor de la expresión1

 Si el valor calculado no es cero , el operador devuelve el valor


de expresión2 , descuidando completamente la expresión3

 Si el valor calculado en el paso 1 es cero , el operador devuelve


el valor de expresión3 , omitiendo la expresión2 .

Esto significa que el resultado de la siguiente expresión:

i = i> 0? 1: 0;

se calculará de la siguiente manera:

 a la variable i se le asignará un 1 si su valor anterior fue mayor


que cero, y 0 en caso contrario. Tenga en cuenta que podemos
lograr el mismo efecto usando una declaración condicional:

si (i> 0)
i = 1;
más
i=0

Este formulario es algo más extenso, aunque no podemos negar


que es más legible al mismo tiempo.

3.8.7 Un nuevo operador: un ejemplo (1)

Podemos usar el nuevo operador ternario (vale la pena


mencionar que este es el único operador ternario en el lenguaje
"C ++") para implementar nuestra función máxima de dos
argumentos de una manera más compacta. Aquí está →

218 – By Gerson
3.8.8 Un nuevo operador: un ejemplo (2)

La variante de tres argumentos también puede hacer uso del


operador ternario, aunque tenemos que admitir que el código de
la derecha → no es el más legible.
Utilice el operador ?: Con precaución. No ofrece mucha
expresividad, pero destruye mucha claridad. Pero depende de
usted lo que decida hacer.

3.9.1 Ordenar una matriz (1)

Piense en este capítulo como una digresión, pero una digresión


muy útil. Vamos a contarte sobre la clasificación . La
clasificación es muy grave y hasta el momento se han inventado
muchos algoritmos de clasificación que difieren mucho tanto en
velocidad como en complejidad.
Le mostraremos un algoritmo muy simple, fácil de entender, pero
no demasiado eficiente. Se usa muy raramente, y ciertamente no
para matrices grandes y extensas.

Podemos decir que la matriz se puede ordenar de dos maneras:

 aumentando (o más precisamente, no disminuyendo ) si, en


cada par de elementos adyacentes, el primer elemento no es
mayor que el segundo

 disminuyendo (o más precisamente, no aumentando ) si, en


cada par de elementos adyacentes, el primer elemento no es
menor que el último.

219 – By Gerson
En las siguientes secciones, clasificaremos la matriz en orden
creciente para que los números se ordenen de menor a
mayor. Aquí está nuestra matriz →

Intentemos usar el siguiente enfoque: tomamos el primer y el


segundo elemento y los comparamos; si determinamos que están
en el orden incorrecto (el primero es mayor que el segundo),
los intercambiamos ; Si su orden es válida, no hacemos
nada. Un vistazo a nuestra tabla confirma la segunda versión: los
elementos n. ° 1 y n. ° 2 están en el orden correcto, como 8 <10 .
Ahora mira el segundo y el tercer elemento. Están en las
posiciones equivocadas. Tenemos que intercambiarlos. Vamos a
hacerlo.

3.9.2 Ordenar una matriz (2)

Podemos ir más allá y mirar el tercer y cuarto elementos. De


nuevo, esto no es lo que se supone que debe ser. Tenemos que
intercambiarlos →

3.9.3 Ordenar una matriz (3)

Ahora podemos verificar los elementos cuarto y quinto. Bueno, sí,


ellos también están en las posiciones equivocadas. Otro cambio.

220 – By Gerson
3.9.4 Ordenar una matriz (4)

El primer paso a través de la matriz está hecho. Todavía estamos


lejos de terminar nuestro trabajo, pero algo extraño ha sucedido
mientras tanto.
El elemento más grande (10) obedeció obedientemente al final de
la matriz, que es donde queremos que esté. Todos los elementos
restantes forman un desastre pintoresco, pero este ya está en
casa.

3.9.5 Ordenar una matriz (5)

Ahora, por un momento, trate de imaginar esta matriz de una


manera ligeramente diferente, es decir, así →
Mira - 10 está en la parte superior. Podríamos decir que
flotó desde el fondo hasta la superficie, como las
burbujas en una copa de champán. De hecho, el método
de clasificación recibe su nombre de esta misma
observación: se llama clasificación de burbujas .

3.9.6 Ordenar una matriz (6)

Está bien, el champán se ha ido, así que ahora es el momento de


volver a la clasificación. Lo hacemos con placer, comenzando con

221 – By Gerson
el segundo paso a través de la matriz. Observamos el primer y el
segundo elemento: ¡oh, es necesario un intercambio!

3.9.7 Ordenar una matriz (7)

Ahora el segundo y el tercer elemento: sí, 8 es una burbuja y sube


a la superficie →

3.9.8 Ordenar una matriz (8)

Tiempo para el tercer y cuarto elemento: también tenemos que


intercambiarlos →

3.9.9 Ordenar una matriz (9)

El segundo pase está terminado ya que 8 ya está en su


lugar. Comenzamos el próximo pase de inmediato. Mire el primer
y el segundo elemento con cuidado: sí, es hora de un intercambio

3.9.10 Ordenar una matriz (10)

222 – By Gerson
Ahora 6 necesita encontrar su lugar. Lo ayudaremos e
intercambiaremos el segundo y el tercer elemento.

3.9.11 Ordenar una matriz (11)

¡Ay! ¡Mira! ¡La matriz ya está ordenada! ¡No tenemos nada más
que hacer! ¡Esto es exactamente lo que queríamos!
Como puede ver, la esencia de este algoritmo es simple:
comparamos los elementos adyacentes y al intercambiar algunos
de ellos logramos nuestro objetivo.
Intentaremos codificar en el lenguaje "C ++" todas las acciones
realizadas durante una sola pasada a través de la matriz, y luego
pensaremos en cuántas pasadas realmente necesitamos. No
hemos analizado esto hasta ahora, y lo discutiremos más
adelante.

3.9.12 Ordenar una matriz (12)

¿Cuántos pases necesitamos para ordenar toda la matriz?


Podemos resolver este problema de la siguiente manera:
presentamos otra variable; su tarea es observar si se produjo
algún intercambio durante el pase o no; si no hubo intercambio,
entonces la matriz ya está ordenada y no hay que hacer nada más.
Declaramos una variable denominada intercambiada y le
asignamos un valor de falso , para indicar que no hubo
intercambios. De lo contrario, se le asignará verdadero .

223 – By Gerson
3.9.13 Ordenar una matriz (13)

Esperamos que pueda leer y comprender este programa. En la


siguiente diapositiva puede ver un programa completo enriquecido
por una conversación con el usuario, que le permite al usuario
ingresar e imprimir elementos de la matriz.

224 – By Gerson
225 – By Gerson
3.9.14 Ordenar una matriz (14)

The bubble sort - versión final.

source_03_09_14_bsort.cpp

226 – By Gerson
3.10.1 nulo - el tipo muy excepcional (1)
void – the very exceptional type (1)

Hemos visto el tipo inusual llamado vacío en nuestros ejemplos


varias veces. Lo hemos usado para indicar que una función no
devuelve un resultado o no espera ningún parámetro.
De acuerdo con la siguiente función prototipo →
la función debe invocarse sin parámetros y no devolver ningún
resultado. Así es como debemos invocarlo:

nada en absoluto();

3.10.2 nulo - el tipo muy excepcional (2)


void – the very exceptional type (2)

A pesar de que el tipo vacío no representa ningún valor útil, es


posible declarar punteros a este tipo, como en el siguiente ejemplo

Puede preguntar cómo usar un puntero que apunta a la nada ,
y luego preguntar por qué tener ese puntero. Este tipo de puntero,
que es del tipo void *, se llama puntero amorfo para enfatizar el
hecho de que puede señalar cualquier valor de cualquier tipo. Esto
significa que el puntero de tipo void * no puede estar sujeto al
operador de desreferencia , por lo que no debe escribir algo
como esto:

* ptr = 1;

Si ptr era de tipo void *, * ptr sería de tipo void y el compilador


prohibiría la asignación de un valor de tipo int .

227 – By Gerson
Sin embargo, los punteros de tipo void * son muy útiles cuando
necesita tener un puntero, pero no sabe para qué se puede usar
en el futuro.
Tan pronto como quede claro, el puntero se puede convertir
fácilmente en otro puntero del tipo deseado (por supuesto, un tipo
de puntero) que siempre es posible y no causa ninguna pérdida
de precisión.

3.10.3 Memoria bajo demanda (1)

En los ejemplos que hemos visto hasta ahora, la gestión de la


memoria ha tenido lugar fuera de nuestra conciencia. Las partes
de la memoria que hemos usado para almacenar valores estaban
ocultas detrás de los nombres de escalares y
matrices. Aparecieron tan pronto como fueron declarados y
desaparecieron cuando nuestro programa finalizó su
operación. Todo el trabajo asociado con la asignación de memoria
fue organizado por el compilador y no nos importó cómo
funcionaba. Y así es exactamente como debería ser: los lenguajes
de alto nivel y sus compiladores están diseñados para exonerar a
los desarrolladores de tales responsabilidades.
A menudo sucede que el desarrollador quiere tener control total
sobre cuánta memoria se usa y cuándo se usa exactamente. Esto
es especialmente importante cuando no sabe de antemano cuál es
el tamaño de los datos a procesar. Para gestionar la asignación y
liberación de memoria, el lenguaje "C ++" nos proporciona dos
palabras clave especializadas. Aquí están los dos para ti →
La nueva palabra clave se utiliza para solicitar la creación de
un nuevo bloque de memoria . Cuando la memoria asignada ya
no sea necesaria y / o utilizada, sería una buena idea devolverla
al sistema operativo. Hacemos esto con la palabra clave delete .

3.10.4 Memoria bajo demanda (2)


La nueva palabra clave que realiza esta primera tarea se puede
utilizar de la siguiente manera:

228 – By Gerson
 necesita especificaciones precisas con respecto a la entidad que
se está creando; debe expresarse como una descripción de tipo
y si la entidad creada es una matriz, también se debe dar el
tamaño de la matriz (como en el primer ejemplo)

 el nuevo devuelve un puntero de tipo conforme a la entidad


recién creada

 el área de memoria recién asignada no se llena (inicia) de


ninguna manera, por lo que debe esperar que solo
contenga basura

3.10.5 Memoria bajo demanda (3)

Cuando ya no necesitamos la memoria, podemos liberarla


(liberarla) usando la palabra clave delete de la siguiente manera

 usamos el formulario delete [] si queremos liberar la memoria


asignada para una matriz, de lo contrario usamos delete ,

 solo puedes liberar todo el bloque asignado, no una parte de él,

 después de realizar la función de eliminación , todos los


punteros que apuntan a los datos dentro del área liberada se
vuelven ilegales; intentar usarlos puede provocar la finalización
anormal del programa.

3.10.6 Memoria bajo demanda (4)

229 – By Gerson
Ahora le mostraremos un programa completo, aunque no muy útil,
que demuestra el uso de ambas palabras clave.

 Declaramos una variable llamada arr que apuntará a los datos


de tipo float (el tipo del puntero es float *); inicialmente no se
asigna ningún valor a esta variable;

 Usamos la nueva palabra clave para asignar un bloque de


memoria suficiente para almacenar una matriz flotante que
consta de 5 elementos;

 Hacemos uso de la matriz recién asignada (para ser precisos,


un vector) y luego la lanzamos utilizando la palabra clave delete

Preste atención al hecho de que el puntero devuelto por new se


trata como si fuera una matriz. ¿Sorprendente?
El manejo de las matrices dinámicas (creadas durante la
ejecución del programa) no es diferente al uso de matrices
regulares declaradas de la manera habitual.
Se lo debemos al operador [] . Independientemente de la
naturaleza de la matriz, podemos acceder a sus elementos de la
misma manera.

source_03_10_06.newdel.cpp

230 – By Gerson
3.10.7 Memoria bajo demanda (5)

Ser capaz de asignar la cantidad de memoria que realmente se


necesita nos permite escribir programas que
pueden adaptarse al tamaño de los datos que se procesan
actualmente. Volvamos al algoritmo de clasificación de burbujas
que le mostramos anteriormente. Este programa asumió que
había exactamente 5 números para ordenar. Esto es obviamente
un inconveniente grave.
Puede suceder que algún día queramos ordenar 10,000 números
o posiblemente cientos de miles. Por supuesto, puede declarar una
matriz del tamaño máximo predecible, pero no sería
razonable. Una forma mucho mejor es preguntarle al usuario
cuántos números se ordenarán y luego asignar una matriz del
tamaño apropiado.
Intentemos comenzar con un ejemplo más simple. En el siguiente
programa, asignamos una matriz que contiene 5 elementos de
tipo int , establecemos sus valores, los sumamos y, finalmente,
liberamos la memoria previamente asignada.

231 – By Gerson
3.10.8 Memory on demand (6)

The improved bubble sort program goes here →


We encourage you to compile and run the program yourself.

source_03_10_08_newbsort.cpp

4.1.1 Matrices de punteros (1)

232 – By Gerson
No hay nada que impida que los elementos de la matriz sean
punteros. Imagina que necesitamos una matriz dinámica. La
matriz es bidimensional y no podemos predecir cuántas filas
tendrá o cuántas columnas.
Solo sabemos que el número de columnas se almacena en
la variable cols y el número de filas en la variable de filas .
Ni siquiera vamos a intentar asignar la matriz de la siguiente
manera:

nuevo int [filas] [cols];

Parece bastante razonable a primera vista, pero en realidad está


mal, porque el compilador no sabe nada sobre el tamaño de las
filas durante la compilación y no podrá generar el código de
indexación correcto, y el único resultado que podemos esperar de
este fragmento de código se encuentra el error de compilación y
una pequeña dosis de decepción personal.
Entonces, ¿qué debemos hacer si queremos que nuestra matriz
sea totalmente configurable y de fácil acceso con una notación
clara y simple como esta:

ptrarr [r] [c]

es posible?

233 – By Gerson
4.1.2 Arreglos de punteros (2)

Sí, es posible.
Así es como lo hacemos:

 almacenamos el puntero al comienzo de cada fila por separado


para que podamos alcanzar cada fila como cualquier otro
vector. ¿Cómo almacenamos estos punteros? En la matriz, por
supuesto! Lo llamaremos el conjunto de filas ; cada fila
tendrá tantos elementos como columnas de la matriz deseada

 cada elemento en la matriz de filas será un puntero a la fila


separada

 necesitamos un puntero más para apuntar a la matriz de filas,


lo llamamos ptrarr

Veamos la figura que ilustra la esencia de nuestra idea →

4.1.3 Arreglos de punteros (3)

¿Cuál es el tipo de la variable ptrarr ? Primero, digamos de qué


tipo no es: ciertamente no es un puntero a un int ( int * ). Esto se
debe a que el resultado de desreferenciar el ptrarr no es
un int sino un puntero a int .

234 – By Gerson
Esto significa que el tipo de ptrarr es " un puntero a un puntero
a int ", que denotamos como " int ** ". Ahora podemos escribir
una declaración completa →

4.1.4 Arreglos de punteros (4)

Una vez que hemos declarado el puntero, podemos asignar la


matriz de filas. Lo hacemos de la siguiente manera →
Bien, entonces expliquemos el significado de esta cláusula.
En primer lugar, el puntero devuelto por new es de tipo int
** (porque es un puntero a puntero a int ) y se asigna a ptrarr .
En segundo lugar, los elementos de la matriz de filas serán
punteros a las filas, por lo que su tipo es int * .

4.1.5 Arreglos de punteros (5)

Finalmente, necesitamos asignar memoria para cada fila y


almacenar el puntero resultante dentro del elemento correcto de
la matriz de filas.
Con mucho, la forma más fácil de hacer esto es usar un bucle →

4.1.6 Arreglos de punteros (6)

Es realmente fácil usar una matriz como esta. Por ejemplo, si


queremos asignar 0 al elemento que se encuentra en la fila r,
columna c, lo hacemos así →

235 – By Gerson
4.1.7 Arreglos de punteros (7)

¿Como funciona?

 la expresión ptrarr [r] se interpreta como * (ptrarr + r), lo que


significa la desreferenciación del elemento que apunta a la
fila seleccionada;

 el puntero se desreferencia una vez más para que toda la


expresión de indexación tenga el siguiente aspecto:

* (* (ptrarr + r) + c)

y este es simplemente el valor deseado de tipo int .

El proceso de desreferenciar el elemento denotado como

ptrarr [2] [1]

está aquí en el diagrama de la derecha →

236 – By Gerson
4.1.8 Triangular matrices (1)
La ventaja de este tipo de matrices es que, a diferencia de las
matrices comunes, cada fila puede tener una longitud
diferente . Esto es útil para los algoritmos que no necesitan una
matriz completa para ejecutarse, sino solo una porción de
ella. Se refiere específicamente a las llamadas matrices
triangulares .
Podemos asignar este tipo de matriz así →

4.1.9 Triangular matrices (2)

El código de la derecha ilustra el proceso de construcción,


inicialización, impresión y liberación de una matriz triangular de
este tipo →
Preste atención a cómo se obtiene la " triangularidad " (el
tamaño del bloque de memoria asignado depende del número de
fila) y cómo el valor asignado a los elementos refleja su ubicación
en la matriz.
Tenga en cuenta que el orden de liberación de la memoria es
inverso al orden de asignación.

source_04_01_09_triangle.cpp

237 – By Gerson
4.2.1 ¿Qué es una conversión?

Una conversión es el acto de cambiar la naturaleza de los


datos sin (si es posible) cambiar el valor . Uno de los
ejemplos más simples de conversión es un cambio de tipo de
datos. Echa un vistazo al fragmento de la derecha →
El literal del valor 1 y del tipo int se convierte en datos del mismo
valor pero de tipo largo . Esto significa que la representación
interna de los datos subjetivos puede cambiarse (en este caso
particular, el número de bits utilizados para almacenar el valor
podría aumentarse) pero el valor en sí permanece intacto.

238 – By Gerson
Puede evitar la conversión codificando el literal de una manera
más clara, por ejemplo, así:

datos largos = 1L;

El sufijo ' L ' (o ' l ' - son intercambiables) dice que el literal se
define explícitamente como largo .

Todas las conversiones posibles se dividen en dos clases:

 conversiones automáticas que se realizan de alguna manera


a nuestras espaldas: no las requerimos, pero el compilador
sabe dónde deben aplicarse y las aplica sin solicitar nuestro
permiso; por esta razón también se llaman conversiones
implícitas

 conversiones explícitas que se realizan a las órdenes del


desarrollador y se expresan con un lenguaje especial; el
compilador obedece la voluntad del programador y convierte
los datos de acuerdo con él; Este tipo de conversión a menudo
se denomina conversión de texto , debido a su construcción
de sintaxis

4.2.2 Conversiones implícitas

Conversiones implícitas: ¿dónde podemos encontrarlas?

239 – By Gerson
Como ya sabe, las conversiones implícitas pueden ocurrir sin
su entrada . El compilador decide dónde se deben realizar. Por
supuesto, debemos ser conscientes de esto, y debemos poder
encontrar todas las circunstancias que pueden hacer que el
compilador convierta nuestros datos.
Podemos decir que las conversiones implícitas afectan los datos
de los tipos fundamentales (básicos) y con mayor frecuencia se
asocian con un cambio de la representación interna .
Este tipo de conversión ocurre cuando asigna, por
ejemplo, int a float o float a long o double a int . Hay muchas
otras combinaciones posibles.
Hagamos una lista de algunos de los contextos donde las
conversiones implícitas juegan un papel importante (aunque casi
invisible):

 un valor se usa como parte de una expresión


compleja construida de muchos valores de diferentes tipos
(vea el ejemplo 1 a la derecha → donde la variable Short se
convierte en el tipo int para ser compatible con la variable Int )

 un valor desempeña el papel de una condición lógica dentro


de instrucciones como if , while , do , etc. (vea el ejemplo 2
donde la variable Double debe convertirse en int para poder
utilizarla para controlar la ruta de ejecución)

 un valor está sujeto a asignación y se usa para:

1. cambiar el valor de una variable (ejemplo 3: int se convierte


en flotante )

2. establecer el valor de un parámetro formal (ejemplo 4: el


parámetro real del tipo float se convierte en el parámetro
formal del tipo int )

3. especifique el valor de retorno de una función (ejemplo 5: el


valor del tipo int es devuelto por la función que tiene el tipo de
retorno especificado como flotante )

Por supuesto, no hemos enumerado todas las posibilidades. Vea


si puede identificar a otros.

240 – By Gerson
4.2.3 Conversiones explícitas
Conversiones explícitas: ¿cómo podemos iniciarlas?
En general, el lenguaje C ++ nos brinda dos formas de especificar
conversiones explícitas:

 el llamado casting de estilo C (llamado así por el predecesor


de C ++, el lenguaje de programación C, que usaba, y aún usa,

241 – By Gerson
una sintaxis de ese tipo); en este caso, especificamos la
conversión de la siguiente forma:

(new_type_name) expression_of_old_type

 la llamada notación funcional , que es una convención de


sintaxis nativa de C ++, no disponible en C (nombrada debido
a su similitud con la invocación de funciones conocida); en este
caso, el nombre de un nuevo tipo (objetivo) se trata como una
función y la conversión toma la siguiente forma:

new_type_name (expression_of_old_type)

Ambas formas son iguales cuando se aplican a los valores de los


tipos estándar (básicos), pero la notación funcional también se
puede usar para objetos y, al hacerlo, puede tener más de un
valor.
Por cierto, estas dos opciones no son todas las opciones
disponibles. Volveremos a este tema más adelante cuando
hablemos de las clases.

Eche un vistazo al código de ejemplo a la derecha →


Ilustra ambos tipos de conversiones explícitas.
¿Cuál te gusta más?

Oh, olvidamos mencionar que el programa emite '4' a la


pantalla. Pero lo sabías sin nuestra ayuda, ¿no?

source_04_02_03_conv1.cpp

242 – By Gerson
4.2.4 Conversiones: ganancias y pérdidas (1)

Cada vez que ocurre una conversión, el compilador hace todo lo


posible para preservar el valor original, pero eso no siempre es
posible. Le mostraremos algunos ejemplos que demuestran
posibles ganancias y pérdidas derivadas de las conversiones.
Analicemos algunas de las posibilidades. Comenzamos con los
tipos integrales.
Un buen escenario es cuando la longitud de la representación
de memoria permanece igual o aumenta ; Podemos estar
seguros de que se preservará el valor original. Podemos
expandirlo con cero bits para llenar el espacio de memoria objetivo
y el bit de signo puede moverse a su nueva posición, pero el valor
en sí no cambiará.
En el ejemplo de la derecha →, el valor máximo del tipo corto se
transfiere con éxito a la variable int .
El programa mostrará "igual" a la pantalla, confirmando el hecho
de que el valor no ha cambiado.
source_04_02_04_conv2.cpp

243 – By Gerson
4.2.5 Conversiones: ganancias y pérdidas (2)

Ahora vamos a probar el peor escenario. Mira el ejemplo de la


derecha →
Queremos transferir el valor máximo permitido para las variables
int a la variable corta.
El programa mostrará "no es igual" a la pantalla. Esto significa que
la variable de destino no puede recibir el valor original.
Este accidente infeliz se llama pérdida de valor . Intente hacer
la misma prueba en su computadora y encuentre el valor
realmente asignado a la variable s.
source_04_02_05_conv3.cpp

244 – By Gerson
4.2.6 Conversiones: ganancias y pérdidas (3)

Hagamos dos pruebas para los valores de coma flotante. El


programa de prueba es casi el mismo. Puedes verlo a la derecha

Convertiremos el valor flotante en un valor doble. Como saben,
los dobles no solo tienen un rango más amplio que los flotadores,
sino también una mejor precisión (precisión).
Esperamos que la conversión sea exitosa y ambas variables,
original y objetivo, almacenarán el mismo valor.
Y estamos en lo cierto. El programa generará 'igual'.
source_04_02_06_conv4.cpp

4.2.7 Conversiones: ganancias y pérdidas (4)

Como antes, hemos invertido la dirección de la conversión, pero


aquí el rango no es un problema. El valor que se convierte es lo
245 – By Gerson
suficientemente pequeño como para almacenarse en cualquier
variable flotante. El problema aquí es la precisión.
Los flotantes no pueden almacenar tantos dígitos significativos
como se especifica en el literal asignado a la variable d.
No perderemos ningún valor. ¿Esto significa que sufriremos una
pérdida de precisión?
Por desgracia sí. Sale "no igual" a la pantalla.

source_04_02_07_conv5.cpp

4.2.8 Conversiones: ganancias y pérdidas (5)

El ejemplo de la derecha → parece ser obvio. Las conversiones de


tipos de punto flotante a tipos integrales siempre causan una
pérdida de precisión. No hay escapatoria de esto. Siempre
perdemos la parte fraccionaria de un número flotante.
Pero prepárese también para el hecho de que, cuando el flotador
es extremadamente grande (o extremadamente pequeño),
también experimentará una pérdida de valor. Esto se aplica a los
valores más allá del alcance del tipo integral objetivo.

246 – By Gerson
Intente compilar y ejecutar el programa de ejemplo usted
mismo. Los resultados te sorprenderán, te lo prometemos.

source_04_02_08_conv6.cpp

4.2.9 Promociones

Para evitar ambos tipos de pérdidas, el compilador utiliza una


estrategia llamada promoción . Una promoción implica la
conversión de datos que participan en una evaluación al
tipo más seguro .
Por ejemplo, cuando se usan un int y un short en la misma
expresión, podemos esperar que el valor del rango más estrecho
( short ) se promocione al tipo con el rango más amplio ( int ) y
no habrá ninguna pérdida. de valor.
Del mismo modo, cuando un flotador se encuentra con
un doble en la misma expresión, el flotador se promocionará a
un doble .

247 – By Gerson
Formalmente, todas las promociones se llevan a cabo de acuerdo
con el siguiente conjunto de reglas (las reglas se aplican en el
siguiente orden hasta que todos los datos utilizados en una
expresión particular tengan el mismo tipo: ¡esta condición es muy
importante!):

 los datos de tipo char o short int se convertirán a tipo int (esto
se llama una promoción de enteros );

 los datos de tipo flotante experimentan una conversión a


tipo doble ( promoción de coma flotante );

 si hay algún valor de tipo double en la expresión, los otros


datos se convertirán a double ;

 si hay algún valor de tipo long int en la expresión, los otros


datos se convertirán a long int ;

Si el contexto en el que se calcula la expresión requiere otro tipo


que el resultante de las conversiones, la última conversión será al
tipo solicitado por el contexto.
Sabiendo cómo funcionan las conversiones implícitas (a veces
llamadas automáticas ), podemos analizar el proceso de cálculo
del valor de esta expresión junto con el programa completo →

Podemos predecir que se realizarán las siguientes conversiones


implícitas:

 las promociones van primero, lo que resulta en las siguientes


conversiones:

int (Corto) + int (Char)

 la suma de Short y Char , así como la variable Float se


convertirá a doble , es decir:

double (( int (Short) + int (Char)) + double (Float))


248 – By Gerson
 la suma final se calculará como un doble , pero debido al
contexto (que surge del tipo del argumento izquierdo del
operador =) que es int , tiene lugar otra conversión en
un tipo int ; por lo tanto, la forma final de la expresión se ve
así:

int ( double (( int (Short) + int (Char)) + double (Float)))

Ahora intente hacer algunos experimentos usted mismo,


cambiando el conjunto de variables involucradas en la expresión
y el tipo de la variable objetivo. Continúe los experimentos hasta
que esté seguro de comprender el funcionamiento de las
conversiones.

4.3.1 ¿Qué es una cadena? What is a string?

Una cadena (en el sentido del lenguaje C ++) no es un cordón o


una cinta, sino un conjunto de caracteres . Puede preguntar

249 – By Gerson
para qué podemos usarlo si tenemos un tipo de datos
como char . Buena pregunta.
Las variables char son útiles (y generalmente todo lo que
necesitamos) cuando queremos procesar caracteres individuales,
pero son extremadamente difíciles cuando tenemos que lidiar con
datos que contienen todo tipo de nombres (por ejemplo, apellidos,
nombres de ciudades, nombres de calles, etc.) o simplemente
texto (por ejemplo, acuerdos, declaraciones o simplemente libros
o correos electrónicos). Procesar ese tipo de datos como un
conjunto de caracteres se asemeja a la ortografía: es tedioso y
lento. Es mucho más fácil tratar a todos los personajes como un
todo, almacenados, asignados y procesados al mismo tiempo.
En nuestro nivel actual de conocimiento, podemos suponer que
string es un tipo como int o float . No es del todo cierto, pero
podemos descuidar todos los detalles aburridos y comenzar desde
este punto. A su debido tiempo, le mostraremos por qué la cadena
es algo diferente a un tipo de datos ordinario. También será una
buena oportunidad para mostrarle algunos rasgos interesantes de
la programación de objetos .

Comencemos con una declaración simple. Echa un vistazo a la


línea de la derecha →

Hemos declarado una variable llamada PetName . Está diseñado


para almacenar los nombres de los animales.
Tenga en cuenta un hecho muy importante: el tipo
de cadena no es un tipo incorporado (como int que se puede
usar en cualquier momento sin una preparación especial). Si
queremos usar cadenas en nuestro código, tenemos que poner
la directiva #include en la parte superior de nuestro
programa y solicitar que se incluya un archivo de encabezado
llamado cadena durante la compilación.
Esta falta de directiva causará un error de compilación. Recuerda,
has sido advertido.
Hay otro hecho importante que se aplica a las cadenas. La palabra
" cadena " no es una palabra clave a diferencia de otros nombres
de tipo como int o float .

250 – By Gerson
4.3.2 Inicializando una cadena (1)

Una cadena se puede inicializar de manera idéntica a la


utilizada para otros tipos regulares, es decir, mediante el uso de
un operador de asignación seguido de un literal de cadena (un
conjunto de caracteres entre comillas dobles). Pero no olvide:
los apóstrofes son solo para literales de caracteres. Las
cadenas siempre usan comillas .
En efecto, la variable recién creada llamada PetName se asignará
con una cadena que consta de los caracteres " Lassie ". Esta no es
la única información almacenada dentro de la variable PetName ,
pero no queremos adelantarnos a los hechos.
Tenga en cuenta que a veces tenemos que usar la cadena de
palabras en dos significados diferentes: como un nombre de
tipo (como en una variable de cadena de tipo ) y como una
entidad que consiste en un número finito de caracteres .

4.3.3 Inicializando una cadena (2)

También hay otra forma de inicializar variables de cadena, más


adecuado para el estilo de programación de objetos. Echa un
vistazo al fragmento de la derecha →
No usamos el operador de asignación aquí . Usamos una
sintaxis que se parece claramente a la invocación de la función,
con un par de paréntesis y un argumento dentro. Puede preguntar

251 – By Gerson
si se invoca una función real llamada PetName . La respuesta es
sí y no". Sí, porque hay una función especializada responsable de
crear cadenas y la función se invoca cada vez que desea crear una
nueva cadena. No, porque no puedes invocar directamente la
función como cualquier otra función regular.
Ambas formas de inicialización son iguales y sus resultados son
exactamente los mismos.

4.3.4 Inicializando una cadena (3)

También puede usar un valor almacenado en cualquier variable


de cadena para inicializar la variable recién declarada como en el
ejemplo de la derecha →
Ambas formas (de asignación y funcionales) son permisibles.

4.3.5 Operadores de cadena: + (1)

Al igual que cualquier otro tipo de datos, el tipo de cadena tiene


sus propios operadores . Uno de los más importantes y más
utilizados es el operador + . No se deje engañar: no agrega
cadenas de la misma manera que la adición aritmética
tradicional. En cambio, concatena cadenas (es decir, las pega
juntas, dando una nueva cadena que ha surgido de los valores de
sus argumentos).
Puede parecer obvio, pero vale la pena mencionarlo aquí: la
concatenación de + no es conmutativa (a + b no siempre es
igual a b + a) en contraste con la suma de + .
Ahora eche un vistazo al programa de la derecha →
Intente predecir su salida.

source_04_03_05_concat.cpp

252 – By Gerson
4.3.6 Operadores de cadena: + (2)

El operador + (concatenación) tiene una limitación


importante. No puede concatenar literales . Puede concatenar
cualquier variable con un literal, un literal con una variable y
obviamente una variable con otra variable, pero concatenar los
literales es algo que el operador nunca hará por nosotros.
El programa de la derecha contiene un error →

Intenta detectarlo. Te hemos ayudado un poco. ¿Puedes


verlo? (disculpas a los daltónicos).
source_04_03_06_error.cpp

253 – By Gerson
4.3.7 Operadores de cadena: + =

Esperamos que no se sorprenda con la noticia de que el operador


+ (concatenación), al igual que su primo aritmético, puede usarse
como un operador de atajo.
El ejemplo de la derecha explica todo, ¿no? →

source_04_03_07_tobe.cpp

254 – By Gerson
4.3.8 Cadenas de entrada (1)

Como probablemente haya notado, generar cadenas es fácil y


generalmente no causa ningún problema. Puede colocar una
variable de cadena entre otras expresiones destinadas a
mostrarse en la pantalla utilizando la secuencia cout , y su
contenido se enviará carácter por carácter.
La introducción de cadenas es un poco más complicada, debido al
hecho de que la secuencia cin trata los espacios (para ser
precisos, no solo espacios regulares sino también todos los
llamados caracteres blancos ) como delimitadores, delimitando
los límites entre los datos. Significa que puede tener problemas si
desea ingresar y almacenar una cadena que contenga caracteres
blancos.
Intente compilar y ejecutar el programa a la derecha →.
Ingrese algunas palabras del teclado y sepárelas con
espacios. Puedes escribir algo como esto:

Ser o no ser

y presione Entrar. No se sorprenda si solo ve la primera palabra


impresa en la pantalla (por ejemplo, Para). La secuencia cin está
convencida de que ha ingresado el espacio para marcar el final de
la cadena. No es un error Es por diseño.

source_04_03_08_cincout.cpp

255 – By Gerson
4.3.9 Cadenas de entrada (2)

Si desea ingresar una línea completa de texto y tratar los


caracteres blancos como cualquier otro carácter, debe usar
la función getline . Esta función obtiene / lee todos los caracteres
ingresados tal cual y no favorece ningún carácter, excepto el
carácter que representa la clave, que marca el final de la
línea. Como resultado, todos los caracteres ingresados antes de
presionar la tecla se ingresarán como una cadena.
Ahora eche un vistazo al programa modificado a la derecha →
Compilar y ejecutarlo. Asegúrese de que la famosa pregunta del
príncipe Hamlet ya no se interrumpa.

source_04_03_09_getline.cpp

256 – By Gerson
4.3.10 Comparación de cadenas (1)

Se pueden comparar cadenas (como cualquier otro dato) . El


caso más simple ocurre cuando desea verificar si dos variables de
tipo cadena contienen cadenas idénticas .
Algo similar sucede cuando ingresa su contraseña al iniciar sesión
en una cuenta. La contraseña que ingresó se compara con la
contraseña almacenada en el sistema y si ambas cadenas son
iguales, se otorga acceso. (El proceso real es obviamente más
complejo, pero nuestra descripción es lo suficientemente buena
como para mostrarle la idea de comparar cadenas).
Si desea verificar si dos cadenas (es decir, variables de tipo
cadena) contienen la misma cadena (es decir, una cadena de
caracteres), puede usar el antiguo operador bueno == . Puede
responder a su pregunta y satisfacer su curiosidad.

Echa un vistazo al programa de la derecha →


El programa ilustra el proceso de ingresar la contraseña y verificar
si es válida.

source_04_03_10_passwd.cpp

257 – By Gerson
4.3.11 Comparación de cadenas (2)

Por supuesto, también puede comparar dos cadenas de formas


más flexibles. Todos los operadores diseñados para comparar
datos están a su disposición:> <> = <=! =. Puede verificar si una
de las cadenas es mayor / menor que la otra, pero recuerde que
estas comparaciones se llevan a cabo en orden alfabético donde
'a' es mayor que 'A' (sic) y obviamente 'z' es mayor que 'a ', pero
menos obviamente,' a 'es mayor que' 1 '.
Puede usar este tipo de comparación de cadenas, por
ejemplo, para determinar el orden alfabético. El código de la
derecha → obtiene dos líneas de texto y las imprime en orden
alfabético.
Puede usarlo para despejar cualquier duda sobre cómo la
computadora entiende el alfabeto.

source_04_03_11_compare.cpp

258 – By Gerson
4.3.12 Comparación de cadenas: el enfoque objetivo

Las cadenas ofrecen otro método de comparación más complejo,


pero también más poderoso. Podemos usarlo si tenemos algún
conocimiento previo de cómo los objetos manipulan sus
datos. Esta será una buena introducción a la práctica del estilo de
programación orientado a objetos.
En el enfoque clásico, generalmente tenemos algunos datos y
un conjunto de funciones que operan en los datos. Estamos
acostumbrados a la siguiente forma de iniciar el procesamiento de
datos:

función (datos);

En el enfoque orientado a objetos, utilizamos una terminología


ligeramente diferente. Tanto los datos como las funciones están
incrustados en el llamado objeto. Los datos son la propiedad del
objeto o la variable miembro , y las funciones son
los métodos del objeto o las funciones miembro . Volveremos
a estos términos pronto y explicaremos su significado con más
detalle.
Si queremos que un método particular (función miembro) procese
datos incrustados dentro de un objeto, activamos la función
miembro para el objeto . Se parece a esto:

object.member_function ();

Comencemos con un ejemplo muy simple. Reescribiremos el


programa de verificación de contraseña para mostrarle una de las
numerosas funciones miembro que existen en cada objeto de
cadena (estamos tan avanzados ahora que ya no usamos el
término " variable " en este contexto).
259 – By Gerson
La función miembro se llama " comparar " y el nombre habla por
sí mismo: está diseñado para comparar una cadena con otra
cadena. La función miembro devuelve 0 (cero) si las cadenas son
idénticas.
Eche un vistazo al programa de la derecha → Hemos reemplazado
la aparición del operador == con la activación de la función
miembro de comparación equivalente . Ahora míralo con
cuidado. Tenga en cuenta la expresión:

secret.compare (contraseña)

Dice: active la función de comparación de miembros para


el objeto secreto para comparar la cadena almacenada
en secreto con la cadena almacenada en contraseña . Podemos
escribir esta expresión de la siguiente manera:

contraseña.compare (secreto)

sin cambiar el comportamiento del programa.

source_04_03_12_passwd2.cpp

4.3.13 Comparación de cadenas: el enfoque objetivo

Por supuesto, las posibilidades de la función de comparación de


miembros no se detienen al verificar la identidad de las
260 – By Gerson
cadenas. La función también puede diagnosticar todas las posibles
relaciones entre dos cadenas. Así es como funciona:

str1.compare (str2) == 0 cuando str1 == str2


str1.compare (str2)> 0 cuando str1> str2
str1.compare (str2) <0 cuando str1 <str2

Tenga en cuenta que la dirección de la desigualdad es la misma


para las notaciones funcionales y objetivas.
Nuevamente, reescribamos uno de nuestros programas anteriores
solo para demostrar el aspecto del código. La nueva versión está
a la derecha →
¿Puedes imaginar cómo funciona?

source_04_03_13_compare2.cpp

4.4.1 Subcadenas

Todas las operaciones de cadena que le hemos mostrado hasta


ahora se han referido a cadenas completas, es decir, ambas
cadenas se tomaron integralmente, desde el primero hasta el
261 – By Gerson
último carácter. Las cadenas se pueden procesar de manera más
precisa cuando solo se tienen en cuenta partes seleccionadas de
ellas. Una parte de una cadena se llama una subcadena .
Si queremos crear una nueva cadena que consta de caracteres
tomados de otra subcadena de cadena (o incluso la misma),
podemos usar una función miembro llamada substr , y su
prototipo informal simplificado se ve así:

newstr = oldstr.substr (substring_start_position, length_of_substring)

La subcadena de cualquier cadena está definida por dos


" coordenadas ":
un lugar donde comienza la subcadena (especificada por
el parámetro substring_start_position ) y su longitud
(especificada por el parámetro length_of_substring ).
Tenga en cuenta que los caracteres dentro de una cadena están
numerados, y el primer carácter es ese número 0. Por lo tanto, si
una cadena contiene n caracteres, el último es el número n - 1 .

Ambos parámetros tienen valores predeterminados. Esto nos


permite usar la función de una manera más flexible. Entonces:

 s.substr (1,2) describe una subcadena de la cadena s, que


comienza en su segundo carácter y termina en su tercer
carácter (inclusive)

 s.substr (1) describe una subcadena que comienza en el


segundo carácter de la cadena s y contiene todos los caracteres
restantes de s , incluido el
último; el parámetro omitido length_of_substring por defecto
cubre todos los caracteres restantes en la cadena s

 s.substr () es solo una copia de toda la cadena s


(los parámetros substring_start_position tienen el
valor predeterminado 0)

Mira el ejemplo de la derecha. Intenta predecir su salida.

262 – By Gerson
Sí, es BEFABCDEF .
Y ahora preste atención, porque esto es muy importante: obtener
una subcadena requiere que sea preciso . No debe definir una
subcadena que no se ajuste totalmente dentro de la cadena
original (por ejemplo, comienza al lado del final de la cadena
original). Hacer esto hará que su instrucción sea rechazada, y es
lo que llamamos una excepción .
Discutiremos las excepciones en la siguiente sección: estad
atentos.

source_04_04_01_substr.cpp

263 – By Gerson
4.4.2 La longitud de una cuerda

Cada cuerda tiene una longitud. Incluso una cadena vacía (que no
contiene caracteres) tiene una longitud de cero. Es obvio que en
algún momento podemos querer saber cuánto dura una cadena
en particular.
Esta información es proporcionada por dos funciones de miembros
gemelos. Sus nombres son diferentes, pero sus comportamientos
son idénticos. Podemos decir que estas funciones
son sinónimos .
Sus prototipos informales se ven así:

int string_size = S.size ();


int string_length = S.length ();

Por supuesto, estas dos variables int son iguales.

Ambas funciones devuelven un valor igual al número de


todos los caracteres almacenados actualmente dentro de
una cadena . Tenga en cuenta que la función substr devuelve
una cadena. Esto significa que la cadena resultante conserva
todos sus rasgos originales y, por lo tanto, tiene sus propias
funciones miembro como substr o tamaño .
El programa de la derecha, aunque complicado, es
correcto. Intenta predecir su salida.
¿Qué dijiste? 2? Esa es una buena respuesta.
Te animamos a experimentar. Compila y ejecuta el programa con
la línea

int pos = 1;

cambiado a:
264 – By Gerson
int pos = 2;

¿Puede explicar qué sucedió con su programa durante la ejecución


y por qué?

source_04_04_02_substr2.cpp

265 – By Gerson
4.4.3 Comparación de cadenas más detallada

Como ya sabemos cómo definir y usar las subcadenas, podemos


dar un paso atrás y mostrarle algunas otras variantes de las
funciones de comparación. Nos permiten no solo comparar
cadenas enteras, sino también sus subcadenas. Sus prototipos
son claros de entender y fáciles de usar (bueno, esperamos que
esté de acuerdo). Aquí hay dos de ellos (como de costumbre, se
los damos de manera informal):

S.compare (substr_start, substr_length, other_string)


S.compare (substr_start, substr_length, other_string, other_substr_start,
other_substr_length)

Comenzaremos con el primer caso, obviamente más simple. Esta


variante de la función de comparación de miembros compara la
otra cadena completa con la subcadena de la cadena fuente. Esto
significa que el siguiente fragmento generará 0 (las cadenas son
iguales):

cadena S = "ABC";
cout << S.compare (1,1, "B");

La segunda variante nos permite usar solo una parte de la


otra cadena . Del mismo modo, el siguiente fragmento también
generará 0:

cadena S = "ABC";

266 – By Gerson
cout << S.compare (1,1, "ABC", 1,1);

Ahora analice el código a la derecha. Su tarea es adivinar el


número de salida del programa. ¡Buena suerte!

PD: ¿Crees que es -1 ? Tienes razón.

source_04_04_03_compare.cpp

267 – By Gerson
4.4.4 Encontrar cadenas dentro de cadenas

A veces no solo tenemos que extraer una subcadena de una


cadena particular, sino también (que es mucho más
complejo) encontrar una subcadena dentro de otra cadena,
teniendo en cuenta la posibilidad de que pueda fallar (no todos los
pajares contienen agujas).
Las cadenas pueden hacer esto por nosotros. Pueden buscar una
subcadena o un solo carácter. Para este propósito, necesitamos
usar una de las variantes de la función find member. Dos de ellos
son particularmente útiles:

int where_it_begins = S.find (otra_cadena, start_here);


int where_it_is = S.find (any_character, start_here);

En ambas variantes, el parámetro start_here


tiene un valor predeterminado de 0, por lo que cuando lo omita,
la cadena se buscará desde el principio .
El resultado devuelto por las funciones apunta a la primera
ubicación dentro de la cadena donde comienza la cadena
buscada o donde se encuentra el carácter
buscado (dependiendo de la variante). Si la búsqueda falla,
ambas funciones devuelven un valor especial denotado
como string :: npos (discutiremos esta convención de sintaxis
pronto). Puede usarlo para verificar si su pajar contiene el hallazgo
deseado.
Por supuesto, es hora de dar un ejemplo. Compruébalo a la
derecha. En nuestra opinión, su funcionamiento es bastante
predecible. ¿Qué piensas?
Intente cambiar el código para probar algunos de los posibles
resultados.

source_04_04_04_find.cpp

268 – By Gerson
269 – By Gerson
4.4.5 ¿Qué tan grande es realmente la cuerda?

Hemos dicho anteriormente que podemos recuperar información


sobre la longitud de la cadena usando el tamaño de las funciones
miembro de longitud, pero no mencionamos que el valor obtenido
de la función es solo parcialmente cierto. Nos dice cuántos
caracteres están almacenados actualmente dentro de la
cadena , pero no dice nada acerca de la memoria ocupada por los
buffers asignados a esa cadena. Por supuesto, el tamaño del búfer
debe ser mayor que la cadena en sí si queremos que toda la
máquina funcione de manera eficiente.
Cada vez que ampliamos la cadena, por ejemplo, concatenando
otra cadena, el nuevo contenido se coloca en los búferes. Si los
búferes son lo suficientemente grandes, entonces extender la
cadena no requiere que los búferes se extiendan. Por supuesto,
cuando los búferes están llenos y la cadena se está extendiendo
nuevamente, los búferes se reasignan para adaptarse al nuevo
contenido. Este proceso es totalmente transparente y, en la
mayoría de los casos, ni siquiera necesita saberlo.

Obviamente, si lo desea, puede controlar el proceso cuando


conoce la naturaleza de la cadena en particular y su propósito. Las
cadenas ofrecen algunos medios para verificar cómo funcionan los
búferes y para reconocer la condición de la cadena.
Por ejemplo, puede solicitar a cualquier cadena el tamaño de los
búferes asignados actualmente. La respuesta proviene de la
función miembro llamada capacidad . Puede usarlo de la siguiente
manera:

int current_used = S.capacity ();

Por supuesto, el resultado de la función siempre es mayor o igual


al tamaño / longitud de la cadena (como ya sabe, estas palabras
son sinónimos en este contexto).

Cada cadena puede crecer, pero hay un límite para su extensión


y en este caso, el límite no es el cielo, sino un valor definido para
270 – By Gerson
todas las cadenas en la implementación. Puede descubrirlo
utilizando otra función llamada max_size (hace exactamente lo
que dice). Puede esperar que el valor sea realmente grande, más
grande de lo que probablemente necesite en los programas
típicos. Aquí hay un ejemplo de cómo usarlo:

int not_more_than = S.max_size ();

Nota: este valor es común para todas las cadenas que usa en su
programa. Obviamente, el valor anterior puede ser diferente para
cada instancia de la cadena.
El programa de la derecha demuestra la coexistencia de estos tres
valores. Compílelo y ejecútelo usted mismo, y también cambie los
valores de la variable y pruebe su comportamiento.
No intente aumentar el límite superior del ciclo for en una cantidad
significativa. Puede bloquear su sistema. ¿Qué, no nos
crees? Pues bien, pruébalo!
source_04_04_05_length.cpp

271 – By Gerson
4.4.6 ¿Cómo podemos controlar el tamaño de la cadena?

Podemos controlar el tamaño de la memoria que utiliza una


cadena con la función miembro de reserva . Puede funcionar en
ambas direcciones con la misma facilidad, es decir , puede
reducir los búferes y expandirlos . La función requiere un
parámetro de tipo int para especificar el tamaño deseado de las
memorias intermedias asignadas.
Una cosa importante que debe saber aquí: el contenido de la
cadena no cambia de ninguna manera; podemos decir que el
contenido es inmune al efecto de la función de reserva .
Sin embargo, la función puede no ser tan estricta como usted
cuando se trata de determinar el tamaño final de la memoria
asignada. Puede redondear el valor del parámetro para ajustarlo
a los requisitos de memoria actuales y / o las condiciones de la
plataforma de destino.
272 – By Gerson
El programa de la derecha → muestra qué puede esperar de esta
función. Tenga en cuenta que el contenido permanece intacto en
cada caso.

source_04_04_06_capacity.cpp

273 – By Gerson
4.4.7 Cómo controlar el contenido de la cadena (1)

También podemos controlar la longitud del contenido de la cadena


de muchas maneras. Las funciones que le mostraremos aquí
también pueden cambiar las asignaciones de memoria, esto es un
posible efecto secundario.
En primer lugar, podemos vaciar la cadena, eliminando por
completo todos los caracteres almacenados actualmente
dentro de ella . Esto es equivalente a asignar una cadena vacía
a la cadena, pero podría ser un poco más rápido.
El vaciado de la cadena se realiza mediante la función miembro
llamada clear . No requiere parámetros.

El cambio del tamaño de la cadena se lleva a cabo mediante la


función miembro llamada redimensionar. Su variante básica
requiere un parámetro de tipo int, que especifica el nuevo tamaño
deseado de la cadena. Si el parámetro es más pequeño que el
tamaño de cadena actual, la cadena se truncará .
Si el parámetro es mayor que el tamaño de la cadena, la cadena
se expandirá . También puede usar un formulario sobrecargado
de la función que le permite especificar un carácter que se utilizará
para llenar el espacio recién asignado (el carácter nulo '\ 0' se usa
para ese propósito de manera predeterminada).

También puede verificar si una cadena en particular


está vacía (es decir, no contiene caracteres en absoluto; no tiene
nada que ver con el tamaño real de los búferes asignados). Puede
lograr el mismo efecto al comparar la cadena con una cadena
vacía, pero la función miembro será más efectiva para realizar esta
tarea.

274 – By Gerson
El nombre de la función está vacío , no requiere parámetros y
devuelve un valor de tipo bool , determinando la verdad de la
siguiente oración: esta cadena está vacía ahora .

El programa de la derecha es un ejemplo simple que muestra


cómo puede usar estas tres funciones.

source_04_04_07_resize.cpp

4.4.8 Cómo controlar el contenido de la cadena (2)

Ya estamos bastante acostumbrados a las cadenas como un todo


indivisible, pero, por supuesto, eso no significa que no podamos
acceder a cada carácter por separado. Dicho acceso es
imprescindible en muchos algoritmos, por ejemplo, no es posible
implementar incluso el código de cifrado más simple sin poder
analizar y modificar cada carácter de cualquier cadena.
Las cadenas ofrecen algunas funciones miembro convenientes
para hacer esto. Hemos dicho anteriormente que las cadenas no
son matrices. Sí, sigue siendo cierto, pero pueden presentar su
contenido como si fuera una matriz real . Podemos suponer
que es una especie de enmascaramiento muy útil: una cadena

275 – By Gerson
puede usar una máscara y mostrarse como una matriz ordinaria
(bueno, casi ordinaria). Nos permite leer y escribir cada
carácter por separado .
El programa de la derecha hace uso de esta valiosa propiedad de
cadena. Convierte todos sus caracteres en mayúsculas. Lo
sentimos: el algoritmo es demasiado simple para funcionar en
todas partes y siempre. No lo use para aplicaciones serias. Eso
definitivamente sería una mala idea. Una buena idea es explicar
cómo funciona realmente y de dónde proviene su debilidad. Le
instamos a que intente hacerlo usted mismo.
Hasta ahora, no hemos dicho una palabra sobre cómo es posible
que una cadena haga algo como esto. Bueno, se debe a un
mecanismo llamado "sobrecarga del operador". Le contaremos
más al respecto cuando lleguemos a construir nuestros propios
objetos. Una vez más, lo sentimos: en este momento, debe
limitarse a la función de usuario de ese mecanismo. Cambiaremos
esto pronto, lo prometemos.

source_04_04_08_index.cpp

4.5.1 Agregar una (sub) cadena

Vamos a cambiar de tema por un tiempo ahora, pero planeamos


volver a esto en un futuro cercano, cuando le mostramos algunas
técnicas avanzadas relacionadas con las capacidades de objetos
avanzados de las cadenas. Y ahora vamos a terminar mostrándole
algunas funciones útiles para miembros que pueden hacer la vida
de un programador mucho más fácil.

276 – By Gerson
Al final de esta sección, le diremos con precisión qué aspectos de
las cadenas hemos omitido y por qué. Pero hasta entonces,
intenta adivinar.

Una de las funciones que vamos a presentar aquí es agregar . Este


nombre habla por sí mismo: está diseñado para agregar una
cadena a otra . Si eres un estudiante atento, probablemente
hayas notado que el operador + = puede realizar la misma
tarea y te preguntarás por qué necesitamos otra herramienta para
la misma tarea.
Tienes razón en parte. La función de agregar es mucho más
flexible y útil que + = y puede darle más opciones. Ahora vamos
a tratar de convencerte de ello.
En primer lugar, se puede utilizar el append función para añadir
una cadena - He aquí un ejemplo:

cadena str1 = "contenido"; str2 = "apéndice"; str1.append (str2);


// str1 contiene "contentappendix" ahora

A continuación, la función append puede agregar no solo una


cadena, sino también una subcadena de la cadena, como esta:

cadena str1 = "contenido"; str2 = "apéndice"; str1.append (str2,0,3);


// str1 contiene "contentapp" ahora

Y finalmente, el apéndice puede agregar un carácter


(posiblemente repetido n veces), como este:

cadena str1 = "contenido"; str1.append (3, 'x');


// str1 contiene "contentxxx" ahora

Todas estas variantes no solo afectan el contenido de la cadena,


sino que también devuelven la cadena modificada como
resultado. Puede (pero no es necesario) hacer uso de este efecto,

277 – By Gerson
por ejemplo, creando una cadena de invocaciones
de anexos posteriores .

Ahora está listo para analizar el ejemplo de la derecha → También


está listo para adivinar cuál será el resultado producido por el
programa. Es fácil, ¿no es así?

source_04_05_01_append.cpp

4.5.2 Agregar un personaje

Si desea agregar solo un carácter a una cadena, puede hacerlo


usando la función de agregar , pero hay una manera más
eficiente, usando la función de miembro push_back . El ejemplo
de la derecha es más significativo que cualquier descripción que
podamos darle, así que aquí está.
Míralo →
Cuando compila y ejecuta el código, podrá admirar la belleza del
clásico alfabeto latino. ¿Puedes creer que tiene más de 2000
años? Todavía se ve muy joven.

278 – By Gerson
source_04_05_02_pushback.cpp

4.5.3 Insertar una (sub) cadena o un carácter

Insertar una cadena en una cadena es como distender su


contenido desde dentro. Un nuevo conjunto de contenidos se
"empuja" al antiguo. Por ejemplo: el siguiente fragmento de
código imprimirá "ser o no ser" en la secuencia cout:

string quote = "ser"; quote.append (quote); cita.insert (6, "o no");


cout << cita << endl;

Como probablemente haya adivinado, el primer parámetro


especifica dónde debe estar la inserción, mientras que el segundo
dice qué debe insertarse allí. Esta es solo una de las diversas
posibilidades que ofrece la función. Otra función miembro puede
insertar una subcadena de una cadena especificada de una
manera muy similar a la función append.
También hay una función que puede insertar un solo carácter,
opcionalmente duplicado más de una vez. Ambas funciones se
ponen a trabajar en el siguiente ejemplo →

El ejemplo dará salida


279 – By Gerson
¿Por qué tan serio?

a la pantalla.

source_04_05_03_append.cpp

280 – By Gerson
4.5.4 Asignación de una (sub) cadena o un carácter

La función de asignar miembro realiza un trabajo que es muy


similar al trabajo de la inserción, pero no retiene el contenido de
la cadena anterior y, en su lugar, simplemente lo reemplaza por
uno nuevo . Puede preguntar por qué demonios queremos usar
la función de asignación cuando el operador = puede realizar
exactamente la misma tarea .
Bueno, no es tan simple. La asignación es tan universal como
las funciones de inserción o adición y, por lo tanto, es más
conveniente en algunas aplicaciones específicas.
El ejemplo de la derecha asigna una cadena con una serie de
estrellas →

source_04_05_04_assign.cpp

281 – By Gerson
4.5.5 Sustitución de una (sub) cadena

Reemplazar una (sub) cadena con otra (sub) cadena

La función de reemplazar miembro es más


sutil. Puede reemplazar una parte de la cadena con otra
cadena o la subcadena de otra cadena .
La función necesita saber qué parte de la cadena va a reemplazar
y debe especificar esto entregando dos números: el primero que
describe la posición inicial y el segundo que dice cuántos
caracteres se reemplazarán.

La primera función sobrecargada necesita una cadena (como


tercer parámetro) para reemplazar el contenido anterior (puede
ser más largo o más corto o de igual tamaño en comparación con
el original). La segunda función le permite especificar la
subcadena que se utilizará como sustitución.
La segunda variante de la función está aquí a la derecha →

Como de costumbre (casi), la función no solo modifica la cadena,


sino que también devuelve la cadena modificada como resultado
que podemos usar para construir una serie de reemplazos que
ocurren en una declaración.

El código saldrá

Pensaré en eso mañana

a la pantalla.

source_04_05_05_replace.cpp

282 – By Gerson
4.5.6 Borrar una (sub) cadena

También podemos eliminar una parte de una cadena ,


haciendo que la cadena sea más corta que antes. Podemos hacer
esto usando una función miembro llamada borrar (un nombre que
no deja lugar a dudas) y la función requiere dos números para
realizar su tarea: el lugar donde comienza la subcadena que se
eliminará (este valor predeterminado es cero) y la longitud de la
subcadena (este valor predeterminado es la longitud de la cadena
original).
Esto significa que una invocación como esta

TheString.erase ();

borra todos los caracteres de la cadena y lo deja vacío.

La convención que hace que la función realice una modificación y


devuelva la cadena modificada como resultado sigue siendo
válida, por lo que el ejemplo de la derecha → produce el siguiente
resultado en la pantalla:
283 – By Gerson
Tengo la sensación de que estamos en Kansas

Tenga en cuenta que la secuencia de invocaciones de borrado no


se puede revertir, es decir, una declaración como esta

WhereAreWe.erase (25, 4) .erase (38, 8);

generará resultados completamente diferentes. ¿Puedes explicar


porque?

source_04_05_06_erase.cpp

284 – By Gerson
4.5.7 Intercambiando el contenido de dos cadenas

Cuando se trata de ordenar, tenemos que poder intercambiar el


contenido de dos celdas seleccionadas de una matriz. Tenemos
que tener una variable auxiliar para ese propósito, ya que la
operación se asemeja a reemplazar el contenido líquido de dos
vasos: no puede hacerlo con éxito sin usar un tercer
vaso. Entonces, si queremos reemplazar una cadena (o una
bebida) almacenada dentro de las variables Glass1 y Glass2 ,
necesitamos una tercera variable (vidrio) para hacerlo. Así es
como va:

AGlass = Glass1;
Glass1 = Glass2;
Glass2 = AGlass;

Desafortunadamente, esto es muy ineficaz cuando se relaciona


con cadenas. Sería mucho más simple (y también mucho más
rápido) no transferir todo el contenido de ambas cadenas, sino
simplemente reemplazar los punteros que los identifican (es como
reemplazar a los propietarios de las gafas en lugar de intercambiar
el contenido de las gafas entre ellas). Esta acción es realizada por
una función miembro llamada swap . Esta función es muchas
veces más rápida que el intercambio normal (realizado
físicamente).
Eche un vistazo al ejemplo de la derecha → Estamos seguros de
que no tendrá problemas para identificar las cadenas que se
envían a la consola.

Terminaremos nuestro viaje al mundo de las cuerdas en este


punto, aunque somos conscientes de que hay muchos más temas
285 – By Gerson
para discutir y explicar. Volveremos a ellos cuando estemos más
familiarizados con la terminología y la tecnología de los objetos.
Hasta la vista, strings! See you later!

source_04_05_07_swap.cpp

4.6.1 Los espacios de nombres nos rodean (1)

El término " espacio de nombres " puede sonar muy misterioso


para usted, pero queremos asegurarle que conoce muy bien este
fenómeno y, de hecho, se encuentra con muchos espacios de
nombres diferentes cada día. Además, eres parte de muchos
espacios de nombres aunque no te des cuenta. Sí, ha encontrado
la palabra clave "espacio de nombres" en casi todos los programas
en este curso, pero queremos brindarle una perspectiva más
amplia de este concepto antes de entrar en detalles más técnicos
y detallados ... uh.
El espacio de nombres es un espacio en el que un nombre
particular tiene un significado claro y sin
ambigüedades . Por ejemplo, su familia más cercana es un
espacio de nombres en el que su nombre de pila lo identifica a
286 – By Gerson
usted y (muy probablemente) solo a usted. Cuando deja el espacio
de su nombre de casa, su nombre de pila deja de ser claro. En
comunidades grandes, existe la posibilidad de que conozcas a
otras personas con el mismo nombre y es probable que si alguien
grita tu nombre en público, algunas personas vuelvan la cabeza
en la dirección de la voz.

¿Hay algún método para hacer que su nombre de pila sea más
explícito? Sí, por supuesto. Su nombre tiene que
ser calificado con otro nombre con la esperanza de que este
nuevo grupo verbal no sea ambiguo en el nuevo espacio más
amplio.
Probablemente ya hayas notado que ya tienes algo como esto. Es
tu apellido, ¿no?
Por supuesto, es muy posible que este tipo de calificación no
sea suficiente . Puede haber otra persona con el mismo nombre
de pila y apellidos que el suyo, incluso cuando no tiene ninguna
relación. Esto significa que se debe usar otro nivel de calificación
y, como probablemente ya sepa, diferentes culturas han creado
sus propias formas de manejar tales problemas, por ejemplo,
usando el nombre de uno de sus padres, o su lugar de nacimiento,
o su fecha de nacimiento, y así en.

Todos estos trucos te llevan a la misma solución: crear o elegir un


espacio de nombre en el que puedas ser tú mismo y no confundirte
con nadie más.

4.6.2 Los espacios de nombres nos rodean (2)

A estas alturas ya deberías poder identificar docenas de espacios


de nombres diferentes que afecten tu vida. Algunos de ellos son
bastante reales, mientras que otros son más abstractos, pero
todos existen para organizar nuestras vidas y reducir el riesgo de
malentendidos, o (más formalmente) para evitar la posibilidad de
un conflicto de nombres .
Un buen ejemplo de espacios de nombres reales son las placas
de matrícula de automóviles (son muy reales con
287 – By Gerson
seguridad). Dependiendo de las regulaciones legales locales,
puede suceder que cada país, estado, provincia, región,
comunidad, etc., puedan crear su propio espacio de nombres para
distinguir sus placas y los propietarios de las placas. También hay
otro nivel en este espacio: códigos de registro de vehículos
internacionales que definen un espacio de nombre mundial.
Los dominios de Internet son más abstractos, pero no menos útiles
(y definitivamente no más baratos). Nos permiten a todos
distinguir diferentes sitios de Internet del mismo nombre por su
propósito, idioma, residencia o cualquier otro aspecto que pueda
codificarse de acuerdo con las convenciones comunes de
Internet. Un servidor llamado "chat" puede ser un buen sitio para
los entusiastas de los gatos franceses, mientras que otro servidor
con el mismo nombre puede agrupar a muchas personas
hambrientas de conversaciones en inglés.
Intenta enumerar los tres espacios de nombres que te acompañan
en tu vida. Debería ser lo suficientemente fácil.
Listo? ¿Deberíamos continuar?

4.6.3 Presentación del espacio de nombres (1)

No queremos que escriba programas erróneos, pero esta vez lo


alentamos a compilar el código de la derecha → ¿Puede explicar
qué tiene de malo?
Sí, le falta la línea:

usando el espacio de nombres estándar;

¿Es crucial esta línea? Sí lo es. Es fatal . El compilador estará


completamente desorientado si tratamos de forzarlo a usar
nombres sin definir el espacio de nombres al que pertenecen
(estos dos: cout y endl ).
Puede esperar muchas quejas de su compilador irate.
¿Hay alguna manera de calificar ambos nombres para vincularlos
al espacio de nombre de su casa? Sí hay. Pero necesitamos usar

288 – By Gerson
una sintaxis especial para este propósito. Se ve así (escrito de
manera muy informal):

home_name_space :: entity_name

Sugerencia: el espacio de nombres hogar para ambos nombres se


llama “ std ” (como en la norma ). ¿Puedes reescribir el programa
para que sea correcto y no usar la misteriosa frase " usar el
espacio de nombres "?

source_04_06_03_error.cpp

4.6.4 Presentación del espacio de nombres (2)

Hemos modificado un poco el código. ¿Puedes ver el


cambio? Hemos calificado todos los nombres ambiguos con un
prefijo que consiste en el espacio de nombre de inicio ( std ) y un
operador especial escrito como " :: ".
El nombre oficial del operador es " operador de resolución de
alcance ". Tiene algunas aplicaciones más, no solo nombres
calificados con sus espacios de nombre de inicio; de ahí que su
nombre se refiera a conceptos más generales.
Como puede sospechar, un acto calificativo como este es una
forma alternativa de usar la declaración " usar el espacio de
nombres ".
289 – By Gerson
source_04_06_04_qualified.cpp

4.6.5 Definir un espacio de nombres

¿Sabes qué es un troll? Pues claro que sí. Un troll es una criatura
muy desagradable conocida por todos. Los trolls llegan a nuestro
mundo desde muchos lugares diferentes, por ejemplo, desde las
mazmorras de Hogwarts o desde las profundidades oscuras de
Mordor, o incluso desde muchos foros de Internet donde realizan
sus actividades destructivas. Incluso nos han dado el verbo "troll".
¡Recuerde, no alimente a los trolls!

290 – By Gerson
Imagine que queremos usar trolls (dos, para ser precisos) en
nuestro software. Vamos a tomar uno de Hogwarts y uno de
Mordor. ¿Podemos cooperar con ellos simultáneamente sin correr
el riesgo de confundir uno con el otro?
Tenemos que crear dos espacios de nombres diferentes y vincular
a los trolls a sus orígenes. La definición de un espacio de nombres
se ve así:

espacio de nombres the_name_of_the_space {


}

Cualquier entidad declarada dentro de un espacio de nombres


(entre sus corchetes de apertura y cierre) estará vinculada a este
espacio de nombres y, por lo tanto, lógicamente separada de
cualquier otra entidad con el mismo nombre.
Eche un vistazo al ejemplo → Hemos definido dos espacios de
nombres y hemos colocado un troll dentro de cada uno de ellos. Si
queremos hacer uso de ellos, tenemos que calificar sus nombres
con los nombres del espacio de nombres de inicio.
Por supuesto, los desarrolladores reales no usan trolls en su
software. Puedes imaginar un caso más realista para ti si no te
gustan (trolls, no desarrolladores).
Supongamos que dos programadores independientes han
implementado diferentes partes de un sistema muy complejo. No
podemos garantizar que todos los nombres que hayan usado sean
diferentes en pares. Tal requisito sería poco práctico y difícil de
hacer cumplir (los desarrolladores tendrían que ponerse de
acuerdo sobre cada nuevo nombre introducido en sus códigos). Es
más fácil suponer que todas sus entidades (por ejemplo, variables
y funciones) están vinculadas (o más bien encerradas dentro) a
diferentes espacios de nombres con nombres de los nombres de
pila del desarrollador o sus apodos.

source_04_06_05_trolls.cpp

291 – By Gerson
4.6.6 Usar un espacio de nombre (1)

Si alguno de los espacios de nombres disponibles es más utilizable


o preferible (por alguna razón, depende del desarrollador), se
puede usar de una manera que sugiera que el compilador intenta
calificar cada nombre no calificado con este / estos nombres de

292 – By Gerson
espacio de nombres ( puede haber más de un espacio de nombres
de este tipo).
El acto de usar un espacio de nombres seleccionado se lleva
a cabo mediante la instrucción de uso del espacio de nombres . Si
la declaración se coloca fuera de cualquier bloque (una parte de
un código encerrado entre { y } corchetes), afecta el código
después de la declaración hasta el final del archivo fuente.

El ejemplo de la derecha → muestra un caso en el que se han


especificado dos declaraciones " usando el espacio de nombres "
en el mismo código. El primero nos permite usar
convenientemente los identificadores cout y endl . Este último
declara que cualquier troll no calificado pertenece al espacio de
nombres de Hogwarts .
Tenga en cuenta que el uso de declaraciones de espacio de
nombres no debe conducir a una situación en la que se pueda
considerar que un identificador se originó en más de un espacio
de nombres. Esto significa que no puede usar las siguientes dos
declaraciones en el mismo ámbito (bloque) de código:

usando el espacio de nombres Hogwarts;


usando el espacio de nombres Mordor;

Hará que todas las referencias a los trolls no calificados sean


ambiguas. Esto no es lo que queremos. Los trolls tampoco lo
quieren.

source_04_06_06_trolls2.cpp

293 – By Gerson
4.6.7 Usar un espacio de nombre (2)

Si la instrucción de uso del espacio de nombres se coloca dentro


de un bloque , su alcance termina en el mismo lugar donde
termina el bloque. Puede usar este efecto para usar
selectivamente (y desuso) cualquiera de los espacios de
nombres disponibles. En el ejemplo de la derecha → hemos
utilizado los tres espacios de nombres disponibles y lo hemos
hecho de la siguiente manera:

 El espacio de nombre estándar se usa globalmente (en todo


el archivo fuente)

 Los espacios de nombres de Hogwarts y Mordor se usan de


forma selectiva (en las partes seleccionadas del código)

source_04_06_07_trolls3.cpp

294 – By Gerson
4.6.8 Expandir un espacio de nombres

El ejemplo actual → muestra cómo extender cualquier espacio de


nombres previamente definido. Como puede ver, hemos
construido un código que contiene definiciones "dobles" de
nuestros espacios de nombres. De hecho, no se duplican,
se extienden .
Supongamos que todas las definiciones de espacio de nombres
que usan el mismo identificador están " pegadas " juntas y
construimos un espacio de nombres más grande juntos. Tenga en
cuenta que cada extensión se puede colocar dentro de un archivo
fuente separado. Esto significa que cualquiera de los espacios de
nombres puede estar disperso entre muchos archivos fuente
creados por diferentes desarrolladores.
Tenga en cuenta que la primera aparición de un espacio de nombre
se llama " un espacio de nombre original ". Cualquier espacio
de nombre con el mismo nombre (identificador) que aparece
después del espacio de nombre original se denomina " espacio
de nombre de extensión ".
Puede haber más de una extensión de un espacio de nombres.

source_04_06_08_trolls4.cpp

4.6.9 Usar una entidad

La declaración de uso con la que hemos estado trabajando hasta


ahora ha utilizado todo el espacio de nombres, es decir, califica
implícitamente todas las entidades vinculadas al espacio. Hay otra
forma de declaración que nos permite decidir selectivamente
qué entidades deben usarse y cuáles deben permanecer
ocultas dentro del espacio. Pero hay una condición importante:
ninguna de las declaraciones utilizadas puede causar ambigüedad
en el proceso de identificación de todas las entidades utilizadas en
su código.
El ejemplo de la derecha → muestra un código que emplea la
instrucción de uso para seleccionar dos criaturas diferentes de dos
295 – By Gerson
mundos diferentes. En efecto, su uso es más fácil sin
comprometer la singularidad y la claridad.

source_04_06_09_trolls5.cpp

4.6.10 Un espacio de nombre sin nombre

Hay una alternancia interesante en la definición del espacio de


nombres. Podemos definir un espacio de nombre sin nombre ( un
espacio de nombre anónimo ).
Este tipo de espacio de nombres se usa implícita y
automáticamente en un archivo fuente donde su definición es
visible. Es otra forma de introducir entidades (por ejemplo,
variables) accesibles a través de todo el archivo fuente.
El ejemplo de la derecha → muestra dos espacios de nombres:
uno anónimo y otro creado con un nombre. Se puede acceder a
todas las entidades definidas dentro del espacio de nombre
anónimo sin ninguna otra preparación.

source_04_06_10_trolls6.cpp

4.6.11 Renombrar un espacio de nombre


Puede suceder que uno de los espacios de nombres que tenga o
desee usar tenga un nombre inconveniente (por cualquier
motivo). Puede cambiarle el nombre localmente, es decir, se
reconocerá con su nuevo nombre solo en el archivo fuente donde
se produjo el acto de cambio de nombre.
Puede usarlo con una forma especial de la declaración del espacio
de nombres, aquí está:
espacio de nombres new_name = old_name;
El nuevo nombre del espacio de nombres se puede usar junto con
el antiguo. Esto significa que el cambio de nombre es en
realidad alias , ya que ambos nombres siguen siendo válidos.

296 – By Gerson
Mira el ejemplo →
Muestra cómo un nombre largo y retorcido podría tener un alias
para facilitar un poco la vida del desarrollador.
Dejemos solo el espacio de nombre "C ++" por ahora. El mundo
real está esperando.

source_04_06_11_trolls7.cpp

5.1.1 Clases y objetos en la vida real (1)

Ahora vamos a dar un paso más allá de la programación de


computadoras y de las computadoras en general, porque hemos
llegado a un punto en el que no podemos comenzar a discutir
problemas de programación de objetos. Ya hemos hablado sobre
por qué es tan importante y dónde está la diferencia entre la
programación de objetos y la programación.
Todos nuestros programas y todas las técnicas que hemos
utilizado hasta ahora se enmarcan en el llamado estilo de
programación procesal. Fue el enfoque dominante para el
desarrollo de software durante muchas décadas de TI y todavía se
usa en la actualidad. Además, podemos decir que no se
desvanecerá en el futuro, ya que funciona muy bien para tipos
específicos de proyectos (generalmente, no muy complejos y no
grandes, pero hay muchas excepciones a esa regla). El enfoque
de objeto es bastante joven (de todos modos, mucho más joven
que el enfoque de procedimiento) y es particularmente útil cuando
lo aplicamos a proyectos grandes y complejos llevados a cabo por
grandes equipos formados por muchos desarrolladores. Este tipo
de comprensión de la estructura de un proyecto facilita muchas
tareas importantes, por ejemplo, dividir el proyecto en pequeños,
El lenguaje "C ++" fue creado como una herramienta universal
para la programación de objetos. Esto no significa que no pueda
usarlo para la programación de procedimientos. También puede
usarlo con bastante éxito para la programación de procedimientos,
pero ese no es su propósito principal. Como probablemente sepa,
el lenguaje "C ++" creció como una extensión de objeto de su
distinguido ancestro, el lenguaje de programación "C". Si recuerda
(¡por supuesto, lo hace!) Para qué se utiliza el operador "++",
297 – By Gerson
puede comprender una broma (o más bien una alusión) oculta en
nuestro nombre de lenguaje de programación. Sí, es una versión
incrementada de "C", incrementada porque está llena de muchas
opciones nuevas que no están disponibles para su antepasado.
Esto no significa que "C" sea peor que "C ++" por ningún
motivo. Es una herramienta diferente utilizada en diferentes
entornos . Un martillo no es peor que un hacha. Todo depende
de la aplicación y de las necesidades.

5.1.2 Clases y objetos en la vida real (2)

En el enfoque procesal, podemos distinguir dos mundos diferentes


y separados: el mundo de los datos y el mundo del código. El
mundo de los datos está poblado por variables de diferentes tipos,
mientras que el mundo del código está habitado por códigos
agrupados en funciones. Las funciones pueden usar datos pero no
viceversa. Además, las funciones pueden abusar de los datos, es
decir, utilizar el valor de manera no autorizada (por ejemplo,
cuando la función seno obtiene un saldo de la cuenta bancaria
como parámetro).

298 – By Gerson
El enfoque del objeto sugiere una forma de pensar completamente
diferente. Los datos y el código están encerrados juntos en el
mismo mundo, divididos en clases. Cada clase es como una receta
que se puede usar cuando se desea crear un objeto útil (sí, de
aquí proviene el nombre del enfoque). Puede producir tantos
objetos como necesite para resolver su problema. Cada objeto
tiene un conjunto de rasgos (se llaman propiedades) y puede
realizar un conjunto de actividades (que se llaman métodos).
Las recetas pueden modificarse si son inadecuadas para fines
específicos y, en efecto, pueden crearse nuevas clases. Las nuevas
clases heredan propiedades y métodos de los originales y
generalmente agregan algunos nuevos, creando herramientas
nuevas y más específicas.
Los objetos son encarnaciones de ideas expresadas en clases, al
igual que un trozo de tarta de queso en su plato es una
encarnación de una idea expresada en la receta impresa en un
viejo libro de cocina. Los objetos interactúan entre sí,
intercambian datos o activan sus métodos. Una clase construida
correctamente (y, por lo tanto, los objetos) puede proteger los
datos sensibles y ocultarlos de modificaciones no autorizadas. No
existe un límite claro entre los datos y el código: viven como uno
solo en los objetos.
Queremos mostrarle que todos estos conceptos no son tan
abstractos como puede sospechar a primera vista. Por el
contrario, todos están tomados de experiencias de la vida real y,
por lo tanto, son realmente útiles en la programación de
computadoras: no crean una vida artificial: reflejan hechos,
relaciones y circunstancias reales.

299 – By Gerson
5.1.3 Clase: ¿qué es? (1)

La palabra "clase" tiene muchos significados, pero no todos son


compatibles con nuestras ideas. Para nuestros propósitos, una
"clase" es como una "categoría" o "género". Es un conjunto
(entendido como un conjunto en el significado matemático) de
todos los seres potenciales conectados entre sí a través de
similitudes definidas con precisión.
Intentaremos señalar algunas clases que creemos que son buenos
(esperamos) ejemplos de este concepto.
Veamos por un momento los vehículos. Todos los vehículos
existentes (y aún no existentes) en el mundo están relacionados
entre sí por una sola característica importante: la capacidad de
moverse. Podrías decir: "Está bien, mi perro también se mueve,
¿es mi perro un vehículo?" No, por supuesto que no, a menos que
montes en la parte trasera de tu perro como un caballo en
miniatura. Pero supongamos que no. Por lo tanto, debemos
mejorar nuestra definición, es decir, enriquecerla con otros
300 – By Gerson
criterios, distinguir los vehículos de otros seres y objetos y
conectarlos más entre sí. Sugerimos lo siguiente: los vehículos son
entidades creadas artificialmente utilizadas para el transporte,
movidas por fuerzas de la naturaleza y dirigidas (conducidas) por
humanos.
Entonces, su perro no es un vehículo al final, ni siquiera si lo monta
como un pequeño caballo.
La clase de "vehículos" es muy amplia. Demasiado
amplia. Necesitamos definir algunas clases más
especializadas. Las clases especializadas son (serán) llamadas
"subclases". La clase de "vehículos" será una "superclase" para
todos ellos. Tenga en cuenta que la jerarquía crece de arriba a
abajo, como las raíces de los árboles. La clase más general y más
amplia siempre está en la parte superior (una super) mientras que
sus descendientes están debajo (subs).
Estamos seguros de que puede señalar algunas subclases
potenciales para la superclase "vehículo". Hay muchas
clasificaciones posibles. Hemos elegido algunos basados en el
entorno y decimos que hay (al menos) cuatro subclases:

 vehículos terrestres
 vehículos acuáticos
 vehículos aéreos
 vehículos espaciales

Hemos ignorado los vehículos subterráneos. Lo siento por eso.


Para resumir una historia larga, discutiremos solo la primera
subclase: la subclase de tierra. Puede continuar este ejercicio
usted mismo si lo desea. De hecho, en realidad sería un buen
ejercicio.
Los vehículos terrestres pueden dividirse aún más dependiendo de
cómo impactan el suelo. Entonces, podemos decir:

 vehículos de ruedas
 vehículos rastreados
 aerodeslizador

301 – By Gerson
La jerarquía que hemos creado se ilustra en la figura de la derecha

Tenga en cuenta la dirección de las flechas: siempre apuntan a la
superclase. La clase de nivel superior es una excepción: no tiene
su propia superclase. No olvides esto.

5.1.4 Clase: ¿qué es? (2)

Otro buen ejemplo (para quienes no les gusta la mecánica y


prefieren la vida silvestre a la tecnología) es la jerarquía del reino
animal. No entraremos en muchos detalles, lo siento, y nuestra
perspectiva será un poco imprecisa. No estamos aquí para
enseñarle zoología: es solo un medio para alcanzar el objetivo
didáctico, no el objetivo en sí.

302 – By Gerson
Podemos decir que todos los animales (nuestra clase de nivel
superior) se pueden dividir (por laicos como nosotros) en cinco
subclases:

 mamíferos
 reptiles
 aves
 pescado
 anfibios

Como todos nosotros (o al menos la mayoría de nosotros) aquí


somos mamíferos, veamos la primera subclase para un análisis
más detallado. Podemos identificar las siguientes subclases (por
supuesto, es solo una de las docenas de divisiones posibles)

 mamíferos salvajes
 mamíferos domesticados

Hemos mostrado la jerarquía a la derecha →


Intenta extenderlo de la forma que quieras y encuentra el lugar
adecuado para los humanos. Debería ser la parte más difícil de
este ejercicio.

5.1.5 Objeto: ¿qué es?

Una clase es un conjunto de objetos. Un objeto es un ser que


pertenece a una clase. Un objeto es una encarnación de requisitos,
rasgos y cualidades asignados a una clase específica. Esto parece
303 – By Gerson
ser bastante fácil, pero tenga en cuenta los siguientes hechos
importantes. Las clases forman una jerarquía. Esto puede
significar que un objeto que pertenece a una clase específica
pertenece a todas las superclases al mismo tiempo. También
puede significar que cualquier objeto perteneciente a una
superclase puede no pertenecer a ninguna de sus subclases.
Por ejemplo: cualquier automóvil personal es un objeto que
pertenece a la clase de "vehículos con ruedas". También significa
que el mismo automóvil pertenece a todas las superclases de su
clase local; por lo tanto, también es miembro de la clase de
"vehículos". Su perro (o su gato) es un objeto que pertenece a la
clase de "mamíferos domesticados", lo que significa que también
está incluido en la clase de "animales".
Podemos decir que cada subclase es más especializada (o más
específica) que su superclase. También significa que cada
superclase es más general (más abstracta) que todas sus
subclases. Tenga en cuenta que hemos supuesto que una clase
solo puede tener una superclase; esto no siempre es cierto, pero
preferiríamos discutir esto un poco más adelante.

5.1.6 Herencia

304 – By Gerson
Ahora estamos listos para definir uno de los conceptos
fundamentales de la programación de objetos, llamado
"herencia". Cualquier objeto vinculado a un nivel específico de una
jerarquía de clases hereda todos los rasgos (así como los
requisitos y cualidades) definidos dentro de cualquiera de las
superclases. La clase de inicio del objeto puede definir nuevos
rasgos (así como requisitos y cualidades), que serán heredados
por cualquiera de sus subclases.
No debería tener ningún problema para hacer coincidir esta regla
con ejemplos específicos, ya sea que se aplique a animales o
vehículos.

5.1.7 ¿Qué tiene un objeto?

La convención de programación de objetos supone que cada


objeto existente puede estar equipado con tres grupos de
atributos:

1. un objeto tiene un nombre que lo identifica de forma exclusiva


dentro de su espacio de nombres de inicio (aunque también
puede haber algunos objetos anónimos)

2. un objeto tiene un conjunto de propiedades individuales que lo


hacen original, único o sobresaliente (aunque existe la
posibilidad de que algunos objetos no tengan propiedades)

3. un objeto tiene un conjunto de habilidades para realizar


actividades específicas que pueden cambiar el objeto en sí o
algunos de los otros objetos

Hay una pista (tenemos que advertirte: esto no siempre funciona)


que puede ayudarte a identificar cualquiera de estas tres
esferas. Cada vez que describe un objeto y usa:

 un sustantivo, probablemente definas el nombre del objeto

 adjetivo, probablemente defina la propiedad del objeto

 un verbo, probablemente definas la actividad del objeto

305 – By Gerson
Aquí hay dos frases de muestra que son buenos ejemplos:

"Max es un gato grande que duerme todo el día"


Nombre del objeto = Máx.
Home class = Cat
Propiedad = Tamaño (grande)
Actividad = Dormir (todo el día)

“Un Cadillac rosado se fue rápido”


Nombre del objeto = Cadillac
Home class = Vehículos con ruedas
Propiedad = Color (rosa)
Actividad = conducir (rápidamente)

5.1.8 ¿Por qué todo esto?

Volvamos a la programación ahora. Usted merece saber para qué


sirven todos estos términos extraños y cómo utilizarlos.
La programación de objetos es el arte de definir y expandir
clases. La clase es un modelo de una parte muy específica de la
realidad que refleja las propiedades y actividades que se
encuentran en el mundo real. Las clases definidas al principio son
demasiado generales e imprecisas para cubrir el mayor número
posible de casos reales. Pero no hay razón para que no pueda
definir nuevas subclases más precisas. Heredarán todo de su
superclase, por lo que el trabajo que se dedicó a crearlo no se
desperdiciará. La nueva clase puede agregar nuevas propiedades
y nuevas actividades y, por lo tanto, puede ser más útil en
aplicaciones específicas. Obviamente, puede usarlo como una
superclase para cualquier número de subclases recién creadas. El
proceso no necesita tener un final. Puedes crear tantas clases
como necesites.
La clase que defina no tiene nada que ver con la creación de
objetos: la existencia de una clase no significa que ninguno de los
objetos compatibles se creará automáticamente. La clase en sí
306 – By Gerson
misma no puede crear un objeto, debe crearlo usted mismo. Pero
el lenguaje "C ++" le permite hacer esto.
Ahora es el momento de mostrarle cómo definir la clase más
simple y cómo crear un objeto. Mira el ejemplo de la derecha →
Hemos definido una clase. La clase es bastante pobre: no tiene
propiedades ni actividades. Eso no importa en este
momento. Cuanto más simple sea la clase, mejor para nuestros
propósitos.
La definición comienza con la clase de palabra clave. A la palabra
clave le sigue un identificador que nombra la clase (nota: no la
confunda con el nombre del objeto; estas son dos cosas
diferentes). Luego, agrega un par de llaves. El contenido dentro
de estos define todas las propiedades y actividades de la clase.
Nuestros corchetes están vacíos, por lo que la clase también está
vacía.

5.1.9 El primer objeto

La clase recién definida se convierte en un equivalente de un tipo,


y podemos usarla como un nombre de tipo. Imagine que
queremos crear un objeto de la clase OurClass .
Declaramos una variable que puede almacenar objetos de esa
clase y crear un objeto al mismo tiempo.
Lo hacemos de la siguiente manera →

Dejemos las clases en paz por un momento. Te contaremos un


poco sobre las pilas. ¿Tu interesado?

307 – By Gerson
5.2.1 Pila aka LIFO (1)

Una pila es una estructura desarrollada para almacenar


datos de una manera muy específica. Imagina una pila. Una pila
de monedas sería lo mejor aquí. No puedes poner una moneda en
ningún otro lado sino en la parte superior de la pila. Del mismo
modo, no puedes sacar una moneda de la pila desde otro lugar
que no sea la parte superior de la pila. Si desea obtener la moneda
en la parte inferior, debe eliminar todas las monedas que se
encuentran encima.
El nombre alternativo para una pila (pero solo en la terminología
de TI, por supuesto) es LIFO . Esta es una abreviatura para una
descripción del comportamiento de la pila: " Último en entrar -
Primero en salir ". La moneda que va a la pila por última vez
sale primero.
Una pila es un objeto para dos operaciones elementales
denominadas convencionalmente " push " (cuando se coloca un
nuevo elemento en la parte superior) y " pop " (cuando un
elemento existente se retira de la parte superior).
Usamos pilas a menudo en muchos algoritmos clásicos. Es difícil
imaginar la implementación de muchas herramientas
ampliamente utilizadas sin el uso de pilas.
Intentemos implementar una pila en "C ++". Será una pila muy
simple. Solo podrá almacenar valores int .
Le mostraremos cómo hacerlo en dos enfoques independientes:
de procedimiento y objetivo.
Comencemos con el procedimiento.

308 – By Gerson
5.2.2 Pila aka LIFO (2)

Primero, tenemos que decidir cómo almacenar los valores int que
llegarán a nuestra pila. Sugerimos usar el método más simple y
usar un vector para este trabajo. Suponemos (de forma algo
temeraria) que no habrá más de 100 valores en la pila al mismo
tiempo. También suponemos que el elemento en el índice 0 está
en la parte inferior de la pila.
La pila en sí ya está declarada a la derecha →

5.2.3 Puntero de pila

Pero la matriz no es suficiente para implementar una


pila. Necesitamos algunos detalles adicionales. Por ejemplo,
necesitamos una variable que pueda ser responsable del
almacenamiento de varios elementos almacenados
actualmente en la pila. Esta variable generalmente se denomina
" puntero de pila " o SP para abreviar.
Inicialmente, la pila está vacía, por lo que al puntero de la pila se
le debe asignar el valor 0.
Ya lo hemos hecho por ti. Echa un vistazo a la derecha →

5.2.4 Empujar

Ahora estamos listos para definir una función que coloca un


valor en la pila. Esto es lo que supusimos:

 el nombre de la función es "push"

 la función obtiene un parámetro de tipo int (este es el valor


que se colocará en la pila)

309 – By Gerson
 la función no devuelve nada (se explica por sí mismo,
¿verdad?)

 la función coloca el valor del parámetro en el primer


elemento libre en el vector e incrementa el SP

Así es como lo hemos hecho: eche un vistazo →


La función no comprueba si hay espacio para el nuevo valor. Sin
riesgo, sin diversión (es broma).

5.2.5 Pop

Ahora es el momento de que la función quite un valor de la


pila. Así es como lo imaginamos:

 el nombre de la función es "pop" (no queremos descubrir


América nuevamente)

 la función no tiene ningún parámetro

 la función devuelve el valor tomado de la pila

 la función lee el valor desde la parte superior de la pila


y disminuye SP

La función está aquí a la derecha →


La función no verifica si hay algún elemento en la pila (preferimos
abstenernos de comentarios).

310 – By Gerson
5.2.6 La pila en acción

Armemos todas las piezas juntas para poner la pila en


movimiento. El programa de la derecha introduce tres números en
la pila, los extrae e imprime sus valores en la pantalla.
Aquí está →
El programa muestra el siguiente texto en la pantalla:
1
2
3
source_05_02_06.stack.cpp

311 – By Gerson
5.2.7 Pros y contras

Nuestra pila de procedimientos está lista. Por supuesto, tiene


algunas debilidades y la implementación podría mejorarse de
muchas maneras, pero la idea general está bien y podemos usar
nuestra pila si es necesario.
Pero…
Cuanto más lo usamos, más desventajas descubrimos. Echemos
un vistazo a algunos de ellos.

 dos variables esenciales (stack y SP) son completamente


vulnerables; cualquiera puede modificarlos de forma
incontrolable , destruyendo la pila; esto no significa que se
haga de manera maliciosa; por el contrario, puede suceder
como resultado de un descuido; imagina que accidentalmente
has escrito algo como esto:

SP = 100;
El funcionamiento de la pila estará completamente desorganizado.

 Puede suceder que un día necesite más de una pila; prepárese


para trabajar duro: tendrá que crear otro vector para el
almacenamiento de la pila, otro puntero de pila para un nuevo
vector, probablemente también más funciones push y pop ;

 También puede suceder que no solo


necesite funciones push y pop , sino también algunas otras
cosas; puede implementarlos, pero trate de imaginar lo que
sucederá cuando tenga docenas de pilas implementadas por
separado

 hemos usado la pila int hasta ahora, pero es posible que desee
usar las pilas definidas para otros tipos: flotantes, cadenas o
incluso matrices y estructuras; ¿Qué pasará entonces?

El enfoque objetivo proporciona soluciones para cada uno de estos


problemas. Vamos a nombrarlos primero.

312 – By Gerson
 la capacidad de ocultar (proteger) los valores seleccionados
contra el acceso no autorizado se
denomina encapsulación ; no se puede acceder ni modificar
los valores encapsulados si desea utilizarlos exclusivamente

 cuando tiene una clase que implementa todos los


comportamientos de pila necesarios, puede producir tantas
pilas como desee; no necesita copiar o replicar ninguna parte
del código

 la capacidad de enriquecer la pila con nuevas funciones


proviene de la herencia ; puede crear una nueva clase (o más
precisamente una subclase ) que herede todos los rasgos
existentes de la superclase y agregue algunos nuevos

 puede crear una plantilla que sea una clase generalizada y


parametrizada , lista para materializarse en muchas
encarnaciones diferentes; su código puede adaptarse a
diferentes requisitos y, por ejemplo, crear pilas listas para
trabajar con otros tipos de datos

Escribamos una nueva implementación de pila desde


cero. Utilizaremos el enfoque objetivo, que lo guiará paso a paso
en el mundo de la programación de objetos.

5.2.8 Pila desde cero (1)

Por supuesto, la idea central sigue siendo la misma. Usaremos un


vector como almacenamiento de la pila y una variable int como
puntero de la pila. Solo tenemos que colocarlos a los dos en la
clase.
Es fácil. Lo acabamos de hacer. Echa un vistazo →

313 – By Gerson
5.2.9 Pila desde cero (2)

Uh oh ... nos olvidamos de algo.


Queríamos encapsular ambas variables y hacerlas inaccesibles
desde el mundo exterior.
Este tipo de datos se llama privado en la programación de
objetos. Es privado porque solo la clase misma puede acceder y
modificarlo.
Si queremos marcar alguna parte de los datos de una clase como
privados, debemos agregar la palabra clave antes de las
declaraciones.
Echa un vistazo →

No te olvides del colon, es obligatorio.


Para ser honesto, no necesita utilizar la palabra clave privada en
este momento. Se asume como predeterminado cuando no se
especifica implícitamente ninguna otra opción. Pero preferimos ser
estrictos, así que lo hemos usado de todos modos.

5.2.10 Pila desde cero (3)

Ahora es el momento de dos funciones que implementan


operaciones push y pop. El lenguaje "C ++" supone que una
función de este tipo (que es una actividad de clase) puede
describirse de dos maneras diferentes:

 dentro de la clase , cuando el cuerpo de la clase contiene una


implementación completa del método

 fuera de la clase , cuando el cuerpo contiene solo el


encabezado de la función; el cuerpo de la función se coloca
fuera de la clase

314 – By Gerson
Depende de usted cuál de los dos elige. Queremos mostrarles a
ambos y hemos decidido implementar la función pop dentro del
cuerpo de la clase, mientras que hemos implementado
la función push fuera de la clase.
Queremos invocar estas funciones para insertar y reventar
valores. Esto significa que ambos deben ser accesibles para el
usuario de cada clase (en contraste con las variables previamente
declaradas que están ocultas para los usuarios de una clase
ordinaria). Este tipo de componente se llama " público " y
tenemos que usar una palabra clave para enfatizar este hecho.
Las declaraciones en sí no deberían sorprenderte. Echa un vistazo

La declaración de clase está completa. Parece que todos sus


componentes (públicos y privados) han sido listados. Ahora
tenemos que entregar las implementaciones para las actividades
anunciadas.

5.2.11 Pila desde cero (4)

Así es como lo hacemos.


Analiza el código de la derecha →

315 – By Gerson
Las funciones que implementan las actividades de la clase y se
ubican fuera del cuerpo de la clase deben describirse de una
manera muy específica. Sus nombres
deben calificarse utilizando el nombre de la clase de inicio y
el operador " :: ".
No debes olvidar eso. Si omite el prefijo de calificación, la función
no se considerará parte de la clase. Será una función ordinaria en
su lugar.

Tenga en cuenta un hecho importante: ambas funciones


acceden a las variables de clase sin ningún
obstáculo . Además, recuerde la siguiente regla: los
componentes de una clase son completamente visibles para otros
componentes de la misma clase. Volveremos a esto pronto.
Parece que nuestra nueva clase está lista para ser
utilizada. ¿Derecho?
En realidad no, no lo es. Hemos olvidado algo Echa un vistazo: el
valor inicial de SP es desconocido . Esto significa que la pila
no funcionará correctamente si el valor de SP no es igual a 0 al
comienzo de la operación. ¿Existe algún método para garantizar
que la primera operación realizada por la pila pondrá a cero el
valor de SP?
Sí hay.

Tenemos que agregar otra función a nuestra clase. Es una función


muy especializada que se invoca implícitamente cada vez que se
crea una nueva pila. Lo llamamos un " constructor " porque
es responsable de la construcción adecuada de cada nuevo
objeto de la clase .
Desafortunadamente, no podemos nombrar esta función de la
manera que queramos. Su nombre está determinado por los
requisitos del lenguaje "C ++". Debe llamarse igual que su clase
de origen. Lo sentimos, esto no es negociable.
Hay otro hecho importante que vale la pena mencionar: los
constructores no son funciones reales. No tienen ningún tipo de
devolución, ni siquiera nulas. Debe tener esto en cuenta al
declarar o definir un nuevo constructor.

316 – By Gerson
5.2.12 Pila desde cero (5)

La clase esta completa. Echa un vistazo a la derecha →


Ahora estamos listos para crear nuevas pilas y hacer uso de
ellas. Vamos a hacerlo.

317 – By Gerson
5.2.13 Pila desde cero (6)

Analizar el fragmento a la derecha →


Hemos creado tres objetos de la clase "Pila". A continuación, los
usamos de una manera malabarista. Intenta predecir el valor que
se muestra en la pantalla.
Observe cómo invocamos una función desde un objeto. Esta es la
misma convención que ya hemos usado para cadenas. No hay
forma de confundir la pila como un sujeto con las actividades de
una función: están permanentemente vinculadas a los objetos.

Todas estas funciones han sido declaradas como públicas, por lo


que podemos usarlas libremente y no hay confusión. Una
pregunta más interesante es si los componentes privados no están
realmente disponibles.
Intente modificar el código y agregue una línea como esta a las
funciones principales:

little_stack.SP ++;

No se sorprenda si el compilador le grita. Tiene que. Tu intento


fallará (afortunadamente).

source_05_02_13.stack2.cpp

5.2.14 Pila desde cero (7)

Ahora vamos a hacer algo más. Queremos una nueva clase para
manejar las pilas. Queremos que la nueva clase pueda evaluar una
suma de todos los elementos almacenados actualmente en la
pila. Raro, verdad?

318 – By Gerson
No queremos modificar la pila previamente definida. Es lo
suficientemente bueno como es, y no queremos cambiarlo de
ninguna manera. Queremos una nueva pila con nuevas
capacidades .
En otras palabras, queremos construir una subclase de
la clase Stack .
El primer paso no será asombroso. Simplemente definiremos una
nueva subclase señalando la clase que se utilizará como
superclase.
Así es como se ve →

La clase aún no define ningún componente nuevo, pero eso no


significa que esté vacío. Deriva todos los componentes
definidos por su superclase : el nombre de la superclase se
escribe después de los dos puntos directamente después del
nombre de la nueva clase.
Cualquier objeto de la clase AddingStack puede hacer todo lo que
hace cada objeto de la clase Stack .
Ahora vamos a enseñarle algunos trucos nuevos.

1. Queremos que la función push no solo empuje el valor a la pila,


sino que también agregue el valor a la variable de suma

2. Queremos que la función pop no solo extraiga el valor de la pila,


sino que también reste el valor de la variable suma

¿Estamos pidiendo demasiado? Esperamos que no

5.2.15 Pila desde cero (8)

319 – By Gerson
Primero, agregamos una nueva variable a la clase. Será una
variable privada, como todas las variables anteriores. No
queremos que nadie manipule el valor de la suma .
En segundo lugar, agregamos dos funciones ... Detener, ¿es
realmente "agregar"? ¡Ya tenemos funciones en la
superclase! ¿Podemos hacer algo así?
Si podemos. Significa que vamos a cambiar la funcionalidad de las
funciones, no sus nombres. Podemos decir con mayor precisión
que la interfaz de la clase sigue siendo la misma cuando
cambiamos la implementación al mismo tiempo.

Comenzaremos con la implementación de la función push. Esto es


lo que esperamos de él:

 para agregar el valor a la variable suma


 para empujar el valor a la pila

5.2.16 Pila desde cero (9)

Esta es la nueva función push →


La primera declaración es clara y no requiere una explicación,
¿verdad? Pero la segunda línea parece un poco misteriosa. Qué
significa eso?
Tenga en cuenta que no necesitamos definir la operación de
insertar el valor en la pila una vez más. La implementación de esa
actividad se implementa dentro de la clase Stack. Lo único que
debemos hacer es usarlo, pero debemos indicar claramente la
clase que contiene la función para evitar confundirla con cualquier
otra función del mismo nombre.
320 – By Gerson
Es por eso que tenemos que poner el prefijo "Stack ::" delante de
la invocación.

5.2.17 Pila desde cero (10)

Esta es la nueva función pop →


Todo está claro hasta ahora ... esperamos.

5.2.18 Pila desde cero (11)

Nos hemos olvidado de algo otra vez. Definimos la variable suma


pero no proporcionamos un método para obtener su valor. Parece
que está escondido. ¿Cómo podemos revelarlo, pero hacerlo de
una manera que aún lo proteja de modificaciones?
Tenemos que definir una nueva función. Lo llamaremos
"getSum". Su única tarea es devolver el valor de la suma.
Esta función está a la derecha →

Intente adivinar qué más debe modificarse para completar la


declaración.

321 – By Gerson
5.2.19 Pila desde cero (12)

Hay otra cosa que debe tenerse en cuenta, y nos preguntamos si


ya lo has notado, ¿verdad?
Sí, se trata del valor inicial de la variable suma . Debe
ponerse a cero cuando se crea el objeto. Podemos hacer esto en
el constructor, no es difícil. Ya sabemos cómo escribir
constructores, pero hay una cosa que debe enfatizarse.
Cuando crea el objeto de clase AddingStack , también debe
ocuparse de las inicializaciones de la superclase. Echa un vistazo
al nuevo constructor →

Tenga en cuenta la frase ": Pila ()". Es una solicitud para invocar
al constructor de la superclase antes de que el constructor
actual comience su trabajo.
Echemos un vistazo a un código completo de la nueva clase ahora.

5.2.20 Pila desde cero (13)

Aquí está →
Podemos verificar su funcionamiento ahora. Lo haremos con la
ayuda de una función principal muy simple. Lo encontrará en la
próxima diapositiva.

322 – By Gerson
5.2.21 Pila desde cero (14)

Lo puedes encontrar aquí →


Este código mostrará las siguientes dos líneas en la pantalla:

45
0

No debería tener problemas para explicar de dónde provienen


estos valores. En la siguiente sección, vamos a discutir la
anatomía de las declaraciones de clase y lo haremos de una
manera un poco más formal.

source_05_02_21.stack3.cpp

323 – By Gerson
5.3.1 Componentes de clase

Una clase es un agregado que consiste en variables (también


llamadas campos o propiedades) y funciones (a veces llamadas
métodos). Ambas variables y funciones son componentes
de clase .
La clase de la derecha → tiene tres componentes: una variable de
tipo int llamada value y dos funciones
llamadas setVal y getVal respectivamente. La clase se
llama Clase .

Dado que todos los componentes se declaran sin el uso de


un especificador de acceso (no se agregó una palabra
clave pública o privada entre las declaraciones), los tres
componentes son privados .
Esto significa que una clase se define de la siguiente manera:

clase A {
Tipo Var;
324 – By Gerson
};

debe leerse como:

clase A {
privado:
Tipo Var;
};

5.3.2 Especificadores de acceso

La clase se ha reconstruido para mostrar el uso


de especificadores de acceso . Los componentes setVal y getV
al son públicos : son accesibles para todos los usuarios de la
clase. El componente de valor es privado : solo es accesible
dentro de la clase.

325 – By Gerson
5.3.3 Crear un objeto

Cualquier objeto de la clase está equipado con todos los


componentes definidos en la clase. Esto significa que
el objeto_objeto tiene los mismos tres componentes que su clase
base.
Los componentes públicos están disponibles para su uso. Puedes
hacerlo:

the_object.setVal (0);

Los componentes privados están ocultos y no están


disponibles. No debes hacer esto:

the_object.value = 0;

5.3.4 Anulación de nombres de componentes

Puede colocar la definición de la función declarada en la clase


dentro de la clase misma o fuera de la clase. Ambos son posibles.
Una función que es un componente de clase tiene acceso completo
a otros componentes de clase, incluidos los privados.
Si alguna función introduce una entidad del nombre idéntica a
cualquiera de los componentes de la clase, el nombre del
componente de la clase se anula. Solo se puede acceder mediante
la calificación con el nombre de la clase de inicio.
La función setVal usa un parámetro llamado valor . El parámetro
anula el componente de clase llamado valor . Esto significa que la
tarea

326 – By Gerson
valor = valor;

se aplica al parámetro de valor y no tiene nada que ver con el


componente del mismo nombre. Una de las formas adecuadas de
la tarea se ve así:

Clase :: valor = valor;

La calificación revela el nombre anulado del


componente. La función setVal implementada está aquí a la
derecha →

5.3.5 puntero "este"

Suponemos que cada objeto está equipado con un componente


especial que contiene los siguientes rasgos:

 su nombre es este

 no debe declararse explícitamente (es una palabra clave),


por lo que no puede anularse

 que es un puntero al objeto actual - cada objeto tiene su


propia copia de la presente puntero

327 – By Gerson
La regla general dice que:

 si S es una estructura o clase y S tiene


un componente llamado C y

 si p es un puntero a una estructura de tipo S

 entonces se puede acceder al componente C de las dos


maneras siguientes:

(* p). C // p está explícitamente desreferenciado para acceder


al componente C
p-> C // p está implícitamente desreferenciado para acceder
al componente C

Esto significa que el nombre del componente anulado puede


revelarse usando el método de la derecha →

5.3.6 Nombres de componentes calificados (1)

Si cualquier cuerpo de función de clase se proporciona fuera del


cuerpo de la clase, su nombre debe calificarse con el nombre de
la clase de origen y el operador " :: ".
Una función definida de esta manera tiene el mismo acceso
completo a todos los componentes de la clase que cualquier
función definida dentro de la clase.
Todas las reglas para anular nombres también son válidas.
328 – By Gerson
5.3.7 Nombres de componentes calificados (2)

Los nombres de funciones de clase se pueden sobrecargar al


igual que los nombres de funciones comunes. También se pueden
usar los valores de parámetros predeterminados. El ejemplo de la
derecha → muestra una clase con dos funciones del mismo
nombre: setVal .
El primero tiene un parámetro y establece el campo de valor con
el valor del parámetro. El segundo no tiene parámetros y
establece el campo de valor con -2.
Todas las otras restricciones en la construcción de las funciones
de clase sobrecargadas también se aplican.

329 – By Gerson
5.3.8 Constructores

Una función con un nombre idéntico a su nombre de clase


de inicio se denomina constructor . El constructor está
diseñado para construir el objeto durante su creación, es
decir, para inicializar valores de campo, asignar memoria, crear
otros objetos, etc. El constructor puede acceder a todos los
componentes del objeto como cualquier otra función miembro de
la clase, pero no debe invocarse directamente.
El constructor no debe declararse utilizando especificaciones
de tipo de retorno , incluidas las especificaciones de tipo nulo.

La clase de la derecha → está equipada con un constructor


(llamado Clase , al igual que la Clase misma). El constructor
inicializa el campo de valor con el valor de -1.
Declarar el objeto de la clase, por ejemplo, haciéndolo de la
siguiente manera:

Objeto de clase ;
330 – By Gerson
invoca implícitamente al constructor . Puede asegurarse de
que el constructor haga su trabajo empleando las siguientes
instrucciones:

cout << object.getVal () << endl;

Si no se han producido otras modificaciones de objetos mientras


tanto, debe esperar que el valor de -1 salga a la pantalla.

Nota: no puedes hacer esto:

object.Class ()

o esto

Clase :: Clase ();

331 – By Gerson
5.3.9 Sobrecarga de nombres de constructor (1)

Los constructores también pueden estar sobrecargados,


dependiendo de las necesidades y requisitos específicos. La clase
de la derecha → tiene dos constructores diferentes que difieren en
su número de parámetros. El segundo constructor requiere un
parámetro (el primero no requiere ninguno) y establece
el valor del campo con el valor del parámetro recibido.
El constructor real se elige durante la creación del objeto. Mire el
siguiente fragmento, por favor:

Clase object1, object2 (100);


cout << object1.getVal () << endl;
cout << object2.getVal () << endl;

El objeto object1 se creará utilizando un constructor sin


parámetros, mientras que el object2 se creará con un constructor
de un parámetro.
El fragmento generará los siguientes valores:

-1
100

Tenga en cuenta que no debe exigir el uso de un constructor


inexistente ; esto significa que el siguiente fragmento es
incorrecto:

Clase objectx (2,100);


332 – By Gerson
5.3.10 Sobrecarga de nombres de constructor (2)

333 – By Gerson
Si una clase tiene un constructor (o más precisamente, al menos
un constructor), se debe elegir uno de ellos durante la creación
del objeto, es decir, no se le permite escribir una declaración que
no especifique un constructor de destino.
La clase de la derecha tiene un constructor de un parámetro. Esto
significa que la única forma permitida de creación de objetos de
esa clase debe ser similar a la siguiente:

Objeto de clase (2);

La siguiente forma

Objeto de clase;

No se permite. No lo hagas

5.3.11 Copiar constructores

334 – By Gerson
Hay un tipo especial de constructor destinado a copiar un objeto
en otro . Los constructores de este tipo tienen un parámetro
referenciado a un objeto de la misma clase y se usan para copiar
todos los datos importantes del objeto fuente al objeto recién
creado (o más precisamente, al objeto que se está creando
actualmente ).
Se los conoce como constructores de copia y se invocan
implícitamente cuando un iniciador sigue una declaración de un
objeto. También se puede invocar si la declaración especifica un
constructor adecuado para la declaración.

La palabra "copia" no debe tomarse literalmente. Los datos del


objeto en realidad no necesitan ser copiados. Simplemente podría
ser manipulado y procesado. Lo que es más importante es que los
datos se toman de un objeto diferente de la misma clase (o
similar). Si el constructor de copia no existe dentro de una clase
particular y el iniciador se usa realmente durante la declaración de
un objeto, su contenido se copiará realmente (en un significado
literal) "campo por campo", como si el objeto hubiera sido clonado
.
Tenga en cuenta que la palabra clave const utilizada en la
declaración de parámetros es una promesa de que la función no
intentará modificar los valores almacenados en el objeto
referenciado. El compilador hará todo lo posible para cumplir con
su palabra.

El constructor de copia también se usará cuando el contexto


requiera una copia de un objeto específico , por ejemplo,
cuando un objeto particular se transfiere a una función como un
parámetro real de valor pasado.
El ejemplo de la derecha muestra dos clases diferentes. El primero
tiene un constructor de copia, mientras que el segundo no. Se
crean dos objetos para ambas clases mientras que los dos
segundos objetos se crean copiando los primeros.
El programa emite el número 200 dos veces. Intenta explicar esto.

source_5_03_11_copying.cpp

335 – By Gerson
5.3.12 Pérdidas de memoria

Los constructores que hemos visto hasta ahora han hecho su


trabajo iniciando objetos, pero ninguna de sus acciones ha tenido
que revertirse. En otras palabras, no había nada que atar
(limpiar) después de que el objeto terminara su vida.
Esto es bastante raro en la programación real. A muchos de los
objetos se les asigna memoria que necesitan para su
operación. Esta memoria debería liberarse cuando el objeto
finalice su actividad y la mejor manera de hacerlo es realizar la
limpieza automáticamente. Si no se limpia la memoria, se
producirá un fenómeno denominado " pérdida de memoria ",
donde la memoria no utilizada (¡pero aún asignada!) Aumenta de
tamaño y afecta el rendimiento del sistema.

También podemos provocar fugas de memoria


intencionalmente. Mira el ejemplo de la derecha →
La clase Class tiene solo un constructor, que es responsable de
asignar memoria del tamaño especificado por su valor de
parámetro. El objeto de esta clase se crea como una variable local
dentro de las funciones MakeALeak () .
Podemos imaginar que la creación de objetos consta de dos fases:

1. el objeto mismo se crea y una parte de la memoria se asigna


implícitamente al objeto

2. el constructor asigna explícitamente otra parte de la


memoria

La variable objeto es un ejemplo de una " variable


automática ". Esto significa que la variable finaliza
automáticamente su vida cuando finaliza la ejecución de la función
que contiene la declaración de la variable.

336 – By Gerson
Podemos esperar que un retorno de las funciones MakeALeak
() provoque la siguiente acción:

 la memoria asignada al objeto en sí se libera (esto se hace


implícitamente).

Desafortunadamente, la memoria asignada explícitamente por el


constructor permanece asignada . Para empeorar las cosas,
hemos perdido el único puntero que contenía la dirección de esa
memoria (fue almacenada por el campo de valor , pero el objeto
que contiene este campo ya no existe).
La porción bastante grande de memoria se ha filtrado. Qué pena.

5.3.13 Destructores

337 – By Gerson
Podemos protegernos de este peligro definiendo una función
especial llamada destructor . Los destructores tienen las
siguientes restricciones:

 si una clase se llama X, su destructor se llama ~ X

 una clase no puede tener más de un destructor

 un destructor debe ser una función sin parámetros (tenga


en cuenta que las dos últimas restricciones son las mismas,
¿puede explicar por qué?)

 un destructor no debe invocarse explícitamente

El ejemplo de la derecha muestra una clase de la diapositiva


anterior, pero ligeramente modificada: hemos agregado un
destructor a la clase . El destructor libera la memoria asignada
al campo de valor, protegiéndonos de la pérdida de memoria.

El programa de la derecha →, cuando se compila y ejecuta,


muestra las dos líneas siguientes en la pantalla:

Asignación (1000) realizada.


Eliminación hecha.

source_5_03_13_memleak.cpp

5.4.1 La palabra clave "auto" (1)

La palabra clave " auto " vino del antepasado de "C ++", el
lenguaje de programación "C". La palabra ha conservado su
significado original, pero "C ++" le ha agregado un nuevo

338 – By Gerson
sabor. Comencemos con las características originales de la palabra
y el efecto que la palabra tiene en su programa.
Todas las variables en su código pertenecen a una de dos
categorías. Son:

 variables automáticas, creadas y destruidas , a veces


repetidamente y automáticamente (de ahí su nombre)
durante la ejecución del programa

 variables estáticas, que existen continuamente durante


toda la ejecución del programa

Los lenguajes de programación “C” y “C ++” suponen que todas


las variables son automáticas por defecto a menos que se
declaren explícitamente como estáticas. Es interesante que
la palabra clave " auto " todavía se use comúnmente en versiones
anteriores del lenguaje "C" para marcar explícitamente las
variables automáticas.
La palabra clave se conserva para garantizar la compatibilidad con
versiones anteriores y aún es respetada, incluso por los
compiladores "C ++", aunque rara vez se utiliza en los tiempos
modernos.
Echa un vistazo al programa de la derecha →

La variable var se declara dentro de la función divertida y


es automática . Hemos utilizado la palabra clave "auto", pero el
comportamiento del programa seguirá siendo el mismo si elimina
la palabra clave (pruébelo usted mismo). La variable se crea cada
vez que se invoca la función divertida y la variable se destruye
cada vez que la función completa su ejecución.
Esto significa que, independientemente del número de
invocaciones, la función siempre producirá la misma salida, es
decir:

var = 100
var = 100
var = 100
339 – By Gerson
var = 100
var = 100

source_05_04_01_auto.cpp

5.4.2 La palabra clave "auto" (2)

Hemos modificado el código de la diapositiva anterior. La


modificación es muy sutil: acabamos de reemplazar la palabra
clave "auto" por "static" .
El código parece casi idéntico, pero su comportamiento ha
cambiado radicalmente . La variable " var " se crea e
inicia una vez durante el llamado " prólogo del programa " y se
destruye después de la finalización del programa durante la
operación del llamado " epílogo del programa ".
Esto significa que la variable var existe incluso cuando
la función fun no funciona, por lo que el valor de la variable se
conserva entre invocaciones divertidas posteriores .
El programa de la derecha → produce el siguiente resultado:

var = 100
var = 101
var = 102
var = 103
var = 104

source_05_04_02_static.cpp

5.4.3 Instancias de la clase

340 – By Gerson
Anteriormente hemos dicho que definir la clase en sí no tiene
efectos directos. La clase es como un muñeco cobrado vida cuando
se crea su encarnación (en este caso, su objeto).
Cada objeto creado a partir de una clase particular se
denomina instancia de una clase . Desde este punto de vista,
cada instancia de la clase es un universo separado y no tiene
nada que ver con ninguna de las instancias restantes.
Todos los componentes del objeto (campos y funciones) están
encerrados dentro de la instancia. Podemos decir (aunque esto no
es estrictamente cierto a nivel de código de máquina) que cada
instancia de una clase es un espécimen separado de las especies
definidas por la clase.
También podemos decir que ninguno de estos componentes
realmente existe hasta que se crea la primera instancia. En
consecuencia, no debemos usar ninguno de los componentes de
la clase hasta que hayamos creado un objeto de esa clase.

El fragmento a la derecha → está mal y provocará un error de


compilación.

341 – By Gerson
5.4.3 Instancias de la clase

Anteriormente hemos dicho que definir la clase en sí no tiene


efectos directos. La clase es como un muñeco cobrado vida cuando
se crea su encarnación (en este caso, su objeto).
Cada objeto creado a partir de una clase particular se
denomina instancia de una clase . Desde este punto de vista,
cada instancia de la clase es un universo separado y no tiene
nada que ver con ninguna de las instancias restantes.
Todos los componentes del objeto (campos y funciones) están
encerrados dentro de la instancia. Podemos decir (aunque esto no
es estrictamente cierto a nivel de código de máquina) que cada
instancia de una clase es un espécimen separado de las especies
definidas por la clase.
También podemos decir que ninguno de estos componentes
realmente existe hasta que se crea la primera instancia. En
consecuencia, no debemos usar ninguno de los componentes de
la clase hasta que hayamos creado un objeto de esa clase.

El fragmento a la derecha → está mal y provocará un error de


compilación.

5.4.4 Componentes estáticos de la clase.

Todas las reglas de la diapositiva anterior son verdaderas si se


refieren a los componentes no estáticos de la clase (campos y
funciones). El lenguaje "C ++" también nos permite definir otros
tipos de componentes, como probablemente ya haya adivinado,
se llaman " componentes estáticos ".
Existe un componente estático durante toda la vida del
programa . Además, siempre hay un solo
componente, independientemente del número de instancias de
la clase.

342 – By Gerson
Podemos decir que todas las instancias comparten los mismos
componentes estáticos.
El programa de la derecha introduce una clase con dos campos del
mismo tipo: uno estático y otro no estático (tenga en cuenta que
no podemos usar el término "auto" en este caso, ya que "C ++"
crea un contexto completamente diferente) .

Por cierto, la variable estática dentro de la parte pública de


la Clase es solo una declaración . Esto significa que la variable
debe tener tanto una definición separada expresada
explícitamente como una posible inicialización, y ambas deben
colocarse fuera de la definición de clase.
Otro razonamiento dice que la definición tiene que estar separada
del cuerpo de la clase porque las variables estáticas en realidad
no son parte de ningún objeto. En nuestro programa, hacemos
esto con una línea del código entre el cuerpo de la Clase y
la función principal . Eliminar esta línea genera un error.
Este programa es correcto y produce el siguiente resultado en la
pantalla:

Estático = 1, no estático = 10
Estático = 2, no estático = 20

¿Puedes explicar porque? Observe cómo nos referimos a


la variable estática dentro de la clase y fuera de ella: ¿puede ver
la diferencia?

source_05_04_04_static2.cpp

343 – By Gerson
5.4.5 Variables de clase estática (1)

Los rasgos únicos de las variables de clase estática


las predestinan para ser utilizadas como contadores de
instancias de una clase en particular. El programa de la derecha
→ implementa esta idea de una manera muy simple. Primero,
equipamos la clase Class con el contador de campo
estático . La variable Counter se define fuera de la clase y se le
asigna un valor de cero para mostrar que ninguna de las instancias
de la clase existe hasta ahora.
Incrementaremos el Contador dentro del constructor
de la Clase y lo disminuiremos dentro del destructor
de la Clase .

Observe una vez más que se accede al


campo Contador directamente cuando se usa dentro de la clase y
con el operador " :: " cuando se usa fuera de la clase. También es
posible acceder a la variable estática a través de cualquiera de las
instancias de clase existentes, como esta:

cout << b.Contador;

siempre que la variable accedida esté disponible públicamente.


El programa mostrará las siguientes líneas en la pantalla:

344 – By Gerson
2 instancias hasta ahora
4 instancias
¡Adiós!

source_05_04_05_counter.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
static int Counter ;
Clase ( nula ) { ++ Contador; } ;
~ Clase ( nula ) {
--Contador ;
if ( Contador == 0 ) cout << "¡Adiós, adiós!" << endl ;
};
void HowMany ( void ) { cout << Contador << "instancias" << endl ;}
};
int Clase :: Contador = 0 ;
int main ( void ) {
Clase a ;
Clase b ;
cout << Class :: Counter << "instancias hasta ahora" << endl ;
Clase c ;
Clase d ;
re. HowMany () ;
devuelve 0 ;

345 – By Gerson
}

5.4.6 Variables de clase estática (2)

Por supuesto, no hay obstáculos para hacer que una variable


particular sea privada y estática al mismo tiempo. Obviamente,
esto evitará el acceso directo a la variable, pero puede ser algo
que queramos si queremos proteger el valor contra cualquier
modificación no autorizada.
El programa de la derecha nos muestra esta posibilidad →

El programa produce el siguiente resultado:


2 instancias
4 instancias
¡Adiós!

Tenga en cuenta que cualquier intento de acceder a


la variable Contador expresado así:

Clase :: Contador = 1;

Están estrictamente prohibidos .


source_05_04_06_counter2.cpp

346 – By Gerson
5.4.7 Variables de clase estática (3)

No solo las variables de clase se pueden declarar como estáticas,


las funciones también se pueden declarar así. El ejemplo de la
derecha → muestra un programa que contiene una clase con dos
componentes estáticos: una variable y una función.
La función estática , como una variable estática, se puede
acceder también (o más precisamente, invoca) cuando no se han
creado instancias de la clase.
347 – By Gerson
Tenga en cuenta que la función estática se puede invocar desde
dentro de la clase, de esta manera:

Cuántos();

o utilizando cualquiera de las instancias existentes, como esta:

b.HowMany ();

El programa muestra las siguientes líneas en la pantalla:

0 instancias
2 instancias
4 instancias
¡Adiós!

source_05_04_07_counter3.cpp

5.4.8 Componentes estáticos versus no estáticos

La coexistencia de componentes estáticos y no estáticos dentro de


una sola clase causa algunos problemas adicionales que debemos
tener en cuenta. Podemos definir cuatro eventos particulares
cuando ambos tipos de componentes interactúan entre sí.
Son:

1. un componente estático accede a un componente estático

2. una estática componente accede a un no


estático componente

3. un componente no estático accede a


un componente estático

348 – By Gerson
4. un no estático componente accede a un no
estático componente

Para simplificar las cosas, supongamos que vamos a probar solo


aquellos componentes que son funciones. Nuestras conclusiones
también serán ciertas para las variables.
Hemos preparado una tabla para recopilar los resultados de
nuestras pruebas. Vamos a llenar cada celda de la tabla una por
una.

5.4.9 Interacción estática → estática

349 – By Gerson
El primer programa de prueba (que se muestra aquí →) muestra
un caso en el que una función estática llamada funS2 intenta
invocar otra función estática llamada funS1 .
Un caso como este siempre es posible , ya que ambas funciones
están disponibles durante toda la vida del programa. Se puede
acceder con éxito desde dentro de un objeto y desde dentro de la
clase.
Este programa puede compilarse con éxito y produce el siguiente
resultado:

estático
estático

source_05_04_09_static_static.cpp

5.4.10 Componentes estáticos versus no estáticos

Podemos llenar la primera celda de nuestra tabla de resultados -


aquí está →

350 – By Gerson
5.4.11 Interacción estática → no estática

El segundo programa de prueba (que se muestra aquí →) muestra


un caso en el que una función estática llamada funS1 intenta
invocar una función no estática llamada funN1 .
Un caso como este no es posible , ya que la función que se
invoca existe cuando y solo cuando cualquiera de los objetos que
contienen esta función también existe. No se puede acceder con
éxito a la función sin especificar el objeto asociado.
Este programa no se puede compilar correctamente .

source_05_04_11_static_nonstatic.cpp

5.4.12 Componentes estáticos versus no estáticos

Estamos listos para llenar la segunda celda, aquí está →

351 – By Gerson
5.4.13 Interacción no estática → estática

El tercer caso de prueba (puede encontrarlo aquí →) se refiere a


la situación en la que una función no estática
llamada funN1 invoca una función estática llamada funS1 .
Un caso como este siempre es posible , ya que la función
estática está disponible antes de que se haya creado ningún
objeto.
Este programa puede compilarse con éxito y produce el siguiente
resultado:

estático

source_05_04_13_nonstatic_static.cpp

5.4.14 Componentes estáticos versus no estáticos

Es hora de ir a la tercera celda: así es como se ve →

352 – By Gerson
5.4.15 Componentes estáticos versus no estáticos

La última opción restante es simple. No necesitamos hacer ningún


experimento para encontrar la respuesta a la siguiente pregunta:
¿Es posible invocar una función no estática desde una función no
estática?
Sí, obviamente es posible. Además, lo hemos hecho muchas veces
antes.
La tabla completa ya está lista →

5.5.1 Punteros a objetos

Hasta ahora hemos tratado los objetos como variables ordinarias


y asumimos que un objeto se crea en el lugar donde se declara y
se destruye cuando se sale del alcance de la declaración. Esta es
solo una de las muchas encarnaciones de objetos posibles.

353 – By Gerson
Los objetos también pueden existir como entidades
dinámicamente creadas y destruidas. En otras palabras,
los objetos pueden aparecer a pedido , cuando se necesitan,
y desaparecer de la misma manera.
Comencemos con el siguiente programa simple →

Hemos definido una clase (o más bien un esqueleto para una clase
futura) y la llamamos Clase . La clase no tiene campos ni
funciones miembro. Solo hay un constructor y un destructor
dentro de esta clase. Ambos no hacen nada; su única actividad es
anunciar que han sido invocados.
La función principal declara la variable ptr , que es un puntero a
los objetos de la clase Class . A continuación, hemos creado un
objeto de esa clase utilizando la nueva palabra clave. Tenga en
cuenta que podemos omitir los paréntesis vacíos después
del nombre de la Clase ; en cualquier caso, se activará el
constructor sin parámetros.

Finalmente, el objeto se destruye usando la palabra


clave delete . El proceso de destruir el objeto comienza con la
invocación implícita de su destructor. En efecto, el programa
producirá el siguiente resultado:

Objeto construido!
Objeto destruido!

source_05_05_01_dynaobj.cpp
5.5.2 Punteros a campos

Todas las variables, incluidos los objetos, cobran vida de la


manera "ordinaria" (por declaración, no por el uso de
la nueva palabra clave) viven en un área separada de la
memoria llamada pila . Es una región de memoria dedicada
a almacenar todas las entidades automáticas .
Intenta imaginar la pila como una criatura viviente cuyo tamaño
varía durante la ejecución del programa. La pila crece cuando se
354 – By Gerson
crean nuevas variables automáticas y se reduce cuando las
variables ya no son necesarias. Tenga en cuenta que este proceso
está más allá de su control. No puede afectar la forma en que
cambia la pila.

Las entidades creadas "a pedido" (por la nueva palabra clave) se


crean en una región de memoria específica que
generalmente se denomina montón . A diferencia de la pila, el
montón está completamente (casi completamente, para ser
sincero) bajo su control. Usted decide cuántas variables, matrices,
objetos, etc. ocuparán el montón y depende de usted cuando estas
entidades terminen sus vidas.
Se debe acceder al objeto que se almacena en el montón de una
manera que se asemeje al acceso a las estructuras asignadas
dinámicamente. No debe usar la notación "punteada" ordinaria, ya
que no hay una estructura (objeto) que pueda desempeñar el
papel del argumento izquierdo del operador "." A menos que
desreferencia el puntero. En su lugar, debe utilizar el operador
"flecha" (->) .

Mire el programa aquí → Hemos agregado un campo a


la Clase . Se declara en la parte pública de la clase para que pueda
acceder libremente desde fuera de la clase. Hay un inconveniente:
debe utilizar el operador "->". Mire los paréntesis alrededor del
argumento del operador ++ . ¿Realmente los necesitamos? ¿Qué
pasaría cuando los quitaras?
Las entidades estáticas a las que se accede a nivel de clase
utilizando el operador "::" se comportan como de costumbre.
El programa producirá el siguiente resultado:

Objeto construido!
1
Objeto destruido!

source_05_05_02_dynaobj2.cpp

355 – By Gerson
5.5.3. Punteros a funciones

Las funciones de miembro invocadas para un objeto al que se


accede a través del puntero también deben accederse utilizando
el operador de flecha.
Hemos agregado una función miembro a nuestra clase. Mira el
ejemplo de la derecha →

El programa producirá el siguiente resultado:

Objeto construido!
valor = 2
Objeto destruido!

source_05_05_03_dynaobj3.cpp

5.5.4 Seleccionar el constructor

Si una clase tiene más de un constructor, se puede elegir uno de


ellos durante la creación del objeto. Esto se hace especificando la
forma de la lista de parámetros asociada con el nombre de la clase.
La lista debe ser inequívocamente compatible con uno de los
constructores de clases disponibles.
Hemos modificado nuestro programa nuevamente. Ya hay dos
constructores.
Hemos creado dos nuevos objetos dentro de las funciones
principales. Se diferencian en el constructor utilizado para
construir cada uno de los objetos. En efecto, sus campos de valor
tienen diferentes valores asignados a ellos.
Intenta rastrear el flujo del programa.

El programa producirá el siguiente resultado:

356 – By Gerson
Objeto construido (# 1)
Objeto construido (# 2)
valor = 2
valor = 3
Objeto destruido! val = 3
Objeto destruido! val = 2

¿Puedes explicar cada una de las líneas de la salida?

source_05_05_04_dynaobj4.cpp

5.5.5 Arreglos de punteros a objetos (1)

Eche un vistazo al código de la derecha → Hay una clase que


implementa una matriz muy simple que contiene elementos de
tipo int . El tamaño de la matriz está determinado por el valor del
parámetro pasado al constructor de la clase.
La clase nos ofrece dos métodos para acceder a la matriz. El
primero (denominado Get ) devuelve el valor del elemento
almacenado en la celda de un índice especificado por el valor del
parámetro.
El segundo (llamado Put ) puede establecer un nuevo valor de la
celda seleccionada (los parámetros de las funciones especifican el
índice y el valor respectivamente).

Sí, estamos de acuerdo, ambos métodos son extremadamente


imprudentes: no verifican el valor del índice de ninguna
manera .
La función principal realiza una prueba de operación de clase
simple y produce el siguiente resultado:

Matriz de 2 ints construidos.


#1:100
357 – By Gerson
#2:101
Matriz de 2 ints destruidos.

source_05_05_05_arr.cpp

5.5.6 Arreglos de punteros a objetos (2)

No hay obstáculos para reunir punteros a objetos dentro de una


matriz. El programa de la derecha → muestra la sintaxis que
debemos usar para acceder a dicha colección.
La clase Array es exactamente la misma que en la diapositiva
anterior, pero hemos cambiado significativamente la
función principal . Hemos utilizado dos objetos de la clase Array y
les hemos almacenado punteros en la matriz arr (no confunda
estas dos entidades: Array es una clase definida por
nosotros, arr es una matriz normal incorporada en el lenguaje).

Ahora siga cuidadosamente el flujo del programa y observe la


sintaxis que hemos utilizado para acceder a cada uno de los
objetos existentes.
El programa produce el siguiente resultado:

Matriz de 2 ints construidos.


Matriz de 2 ints construidos.
#1:10; #1:11;
#2:11; #2:12;
Matriz de 2 ints destruidos.
Matriz de 2 ints destruidos.

source_05_05_06_arr2.cpp

5.5.7 Objetos dentro de objetos (1)

358 – By Gerson
Un objeto de cualquier clase puede ser el campo de un
objeto de cualquier otra clase . Todas las reglas relativas al
acceso a los componentes de la clase también se cumplen en este
caso.
Hemos preparado un ejemplo simple para ilustrar el problema:
puede encontrarlo a la derecha →
Hay una clase muy simple llamada Element . Está destinado a
almacenar un valor int . Míralo, es muy simple, ¿no?
La segunda clase que se muestra en el ejemplo también es simple,
pero hay algo interesante al respecto: hay dos campos de
la clase Elemento .
Como puede ver, podemos manipularlos sin gran dificultad. Se
comportan como cualquier otro componente de clase.

No te sorprenderá cuando digamos que el código produce el


siguiente resultado:

Elemento # 1 = 2
Elemento # 2 = 3

Hay un problema que necesita una explicación más


detallada: ¿cómo funcionan los constructores ?
Hay tres objetos en nuestro programa de ejemplo: coll (visible
en el nivel de función principal ) y el1 junto con el2 (visible en
el nivel de objeto coll ). Queremos saber la secuencia en la que
trabajan todos los constructores. En este caso, tenemos que
modificar nuestro ejemplo.

source_05_05_07_objinside.cpp

5.5.8 Objetos dentro de objetos (2)

359 – By Gerson
Aquí va el código modificado → Hemos limpiado la función
principal de todo lo que realmente no es necesario para rastrear
la actividad de los constructores.
Compilamos y ejecutamos el programa. Esto es lo que vemos en
la pantalla:

Elemento construido!
Elemento construido!
Colección construida!

La conclusión es: los constructores de objetos internos


(objetos almacenados dentro de otros objetos) se invocan
antes de que los constructores del objeto externo
comiencen su trabajo .
Esta regla debe aplicarse repetidamente.
source_05_05_08_objinside2.cpp

5.5.9 Objetos dentro de objetos (3)

Hemos modificado el código una vez más. Intenta encontrar la


enmienda en el código fuente de la derecha →
Sí, hemos cambiado la forma del constructor de
la clase Elemento : antes era una función sin parámetros,
ahora necesita un parámetro de tipo int .

La modificación ha invalidado el programa, no se puede


compilar . ¿Por qué?
El compilador generará un mensaje de error que dice algo como
esto:

En el constructor 'Colección :: Colección ()':


error: no hay función coincidente para la llamada a 'Element :: Element ()'

360 – By Gerson
El constructor invocado implícitamente (a veces llamado
el constructor predeterminado ) es el que no tiene
parámetros. No hemos entregado tal constructor. Tampoco
hemos entregado el constructor de copia. El único constructor
disponible no es compatible ni con el predeterminado ni con el
constructor de copia, y esto hace que nuestro programa sea
incorrecto.
¿Cómo podemos lidiar con esto? ¿Hay algún truco para decirle al
compilador que queremos que use el otro constructor en lugar del
predeterminado?
Sí hay. El lenguaje "C ++" nos ofrece una sintaxis especial para
esta y otras situaciones similares.

source_05_05_09_objinside3.cpp

5.5.10 Objetos dentro de objetos (4)

Si queremos que se invoque un constructor que no sea el


predeterminado durante la creación de un objeto que es parte
de otro objeto, deberíamos usar la sintaxis de la derecha →
Eche un vistazo a esto: podemos presentarlo de la siguiente
manera esquemática:

Clase (...): inner_field_constr1 (...), inner_field_constr2 (...) {...}

Esto significa que debe enumerar todos los constructores de


objetos internos que desea utilizar en lugar de los
constructores predeterminados .
Esto se expresa en la línea que dice:

Colección (nula): el2 (2), el1 (1) {…}

361 – By Gerson
El programa se puede compilar correctamente y cuando se ejecuta
produce el siguiente resultado:

Elemento (1) construido!


Elemento (2) construido!
Colección construida!

Tenga en cuenta que los constructores internos se han invocado


en la secuencia que refleja el orden de la declaración dentro
de la clase Colecciones ( el1 primero), no en el orden en que se
enumeraron los constructores en el encabezado del constructor
de la Clase ( el2 primero).

Y, por cierto, existe la siguiente alternancia para este


caso: cuando el constructor se divide entre la declaración y
la definición , la lista de constructores alternativos
debe asociarse con la definición , no con la declaración.
Esto significa que el siguiente fragmento es correcto:

clase X {
público:
X (int x) {};
};
clase Y {
X x;
público:
Y(int x);
};
Y::Y(int x) : x(1) { };
source_05_05_10_objinside4.cpp
6.1.1 Definición de una subclase simple (1)

362 – By Gerson
Podemos usar cada clase como base (o base) para definir o
construir otra clase ( una subclase ). También es posible
usar más de una clase para definir una subclase . Puede ver
ambos casos a la derecha →
Tenga en cuenta que las flechas siempre apuntan a la (s)
superclase (s) .

El diagrama de la izquierda ilustra una " herencia única ", y la


derecha una " herencia múltiple " o " herencia múltiple ".
Le mostraremos algunos ejemplos de ambos tipos de herencia.
También podemos escribir sobre superclases como clases base y
subclases como clases derivadas .

363 – By Gerson
6.1.2 Definición de una subclase simple (2)

La clase de la derecha → servirá como una superclase . Analice


su estructura: no es difícil, lo prometemos.
El programa emite el siguiente texto:

101
source_06_01_02_inher.cpp

6.1.3 Definición de una subclase simple (3)

Si queremos definir una clase llamada Y como una subclase


de una superclase llamada X , usamos la siguiente sintaxis →
La diferencia con la notación que usamos antes está en el hecho
de que tenemos que:

 colocar dos puntos después del nombre de la clase de


subclase

 opcionalmente, coloque un llamado especificador de


visibilidad (volveremos a esto pronto)

 agregar un nombre de superclase

Si hay más de una superclase, tenemos que enlistarlos a todos


usando comas como separadores, como este:

clase A: X, Y, Z {...};

Comencemos con el caso más simple posible.

364 – By Gerson
6.1.4 Definición de una subclase simple (4)

Eche un vistazo aquí → Hemos definido una clase llamada Sub ,


que es una subclase de una clase llamada Super . También
podemos decir que la clase Sub se deriva de la clase Super .
La clase Sub no introduce nuevas variables ni nuevas
funciones. ¿Significa esto que cualquier objeto de
la clase Sub hereda todos los rasgos de la clase Super , siendo de
hecho una copia de los objetos de la clase Super ?
No. No lo hace.

Si compilamos el siguiente código, no obtendremos nada más que


errores de compilación que indiquen que
los métodos put y get son inaccesibles. ¿Por qué?
Cuando omitimos el especificador de visibilidad , el
compilador supone que vamos a aplicar una " herencia
privada ". Esto significa que todos los componentes públicos
de la superclase se convierten en acceso privado, y los
componentes privados de la superclase no serán
accesibles en absoluto. En consecuencia, significa que no puede
usar este último dentro de la subclase.

Tenemos que decirle al compilador que queremos preservar la


política de acceso utilizada anteriormente . Hacemos esto
usando un especificador de visibilidad "público":

clase Sub: public Super {


};

No se deje engañar: esto no significa que los componentes


privados de la clase Super (como la variable de almacenamiento )
se convertirán mágicamente en públicos. Los componentes
privados seguirán siendo privados, los componentes públicos
seguirán siendo públicos.
Esto es exactamente lo que queremos ahora.

365 – By Gerson
clase Sub: Super {
};
int main (nulo) {
Subobjeto;
object.put (100);
object.put (object.get () + 1);
cout << object.get () << endl;
devuelve 0;
}

6.1.5 Definición de una subclase simple (5)

Los objetos de la clase Sub pueden hacer casi las mismas cosas
que sus hermanos mayores creados a partir de
la clase Super . Usamos la palabra 'casi' porque ser una subclase
también significa que la clase ha perdido el acceso a los
componentes privados de la superclase . No podemos escribir
una función miembro de la clase Sub que pueda manipular
directamente la variable de almacenamiento .
Esta es una restricción muy seria. ¿Hay algún trabajo alrededor?
Si.

Existe el tercer nivel de acceso que aún no hemos mencionado. Se


llama " protegido ".

366 – By Gerson
La palabra clave protegida significa que cualquier componente
marcado con él se comporta como un componente público
cuando lo utiliza cualquiera de las subclases y se ve como
un componente privado para el resto del mundo .
Deberíamos agregar que esto es cierto solo para las clases
heredadas públicamente (como la clase Super en nuestro ejemplo
aquí →)
Hagamos uso de la palabra clave ahora mismo.

source_06_01_05_inher2.cpp

#include <iostream>

usando el espacio de nombres estándar ;

clase Super {
privado :
almacenamiento int ;
public :
void put ( int val ) { almacenamiento = val ; }
int get ( void ) { devolver almacenamiento ; }
};

clase Sub : public Super {


};

int main ( void ) {


Subobjeto ;

objeto. poner ( 100 ) ;


objeto. put ( objeto. get () + 1 ) ;

367 – By Gerson
cout << objeto. get () << endl;
devuelve 0 ;
}

6.1.6 Definición de una subclase simple (6)

Como puede ver en el código de ejemplo → hemos agregado una


nueva funcionalidad a la clase Sub .
Hemos agregado la función de impresión . No es especialmente
sofisticado, pero hace una cosa importante: accede a la variable
de almacenamiento desde la Superclase. Esto no sería posible si
la variable se declarara como privada .

En el ámbito de la función principal , la variable permanece oculta


de todos modos. No debes escribir nada como esto:

object.storage = 0;

El compilador será muy terco al respecto.

Casi olvidamos mencionar que nuestro nuevo programa producirá


el siguiente resultado:

almacenamiento = 101

source_06_01_06_inher3.cpp

#include <iostream>

usando el espacio de nombres estándar ;

368 – By Gerson
clase Super {
protegido :
almacenamiento int ;
public :
void put ( int val ) { almacenamiento = val ; }
int get ( void ) { devolver almacenamiento ; }
};

clase Sub : public Super {


public :
void print ( void ) { cout << "storage =" << storage << endl ; }
};

int main ( void ) {


Subobjeto ;

objeto. poner ( 100 ) ;


objeto. put ( objeto. get () + 1 ) ;
objeto. print () ;
devuelve 0 ;
}

6.1.7 Definición de una subclase simple (7)

Ahora es una buena oportunidad para hacer un pequeño resumen


aquí. Sabemos que cualquier componente de la clase puede
declararse como:

 público
 privado

369 – By Gerson
 protegido

Estas tres palabras clave también se pueden usar en un contexto


completamente diferente para especificar el modelo de herencia
de visibilidad . Hasta ahora, hemos hablado de las palabras
clave públicas y privadas utilizadas en tal caso. No debería
sorprenderle que la palabra clave protegida también pueda
emplearse en este rol.

Echa un vistazo a la tabla aquí →


Reúne todas las combinaciones posibles de la declaración de
componentes y el modelo de herencia, presentando el acceso
resultante a los componentes cuando la subclase está
completamente definida.
Se lee de la siguiente manera (eche un vistazo a la primera fila): si
un componente se declara como público y su clase se hereda como
pública, el acceso resultante es público .
Familiarícese con la tabla: es una herramienta básica para resolver
todos los problemas relacionados con la herencia de los
componentes de la clase.

370 – By Gerson
6.1.8 Definición de una subclase simple (8)

Terminaremos nuestro tema actual con un ejemplo muy simple


que demuestra la herencia múltiple. Debemos enfatizar que el uso
de esta técnica se reconoce comúnmente como una jerarquía de
clases propensa a errores y ofuscadora .
Cualquier solución que evite la herencia múltiple es generalmente
mejor y, de hecho, muchos lenguajes de programación de objetos
contemporáneos no ofrecen herencia múltiple en
absoluto. Creemos que es un buen argumento para considerar
cuando se hacen suposiciones de diseño.

Este ejemplo debería ser claro (esperamos).


El programa producirá el siguiente resultado:

almacenamiento = 3
seguro = 5

source_06_01_08_multiinher

371 – By Gerson
6.2.1 Compatibilidad de tipos: el caso más simple

Cada nueva clase constituye un nuevo tipo de datos . Cada


objeto construido sobre la base de dicha clase es como un valor
del nuevo tipo .
Esto significa que cualquiera de los dos objetos puede (o no)
ser compatible en el sentido de sus tipos.
Eche un vistazo al ejemplo muy simple a la derecha →

Los objetos de la clase Cat no son compatibles con los objetos de


la clase Dog , aunque la estructura de ambas clases es
idéntica. Ninguna de las siguientes asignaciones es válida y
ambas causarán un error de compilación :

a_dog = a_cat;
a_cat = a_dog;

Como puede ver, las clases de perros y gatos no tienen nada en


común en el sentido de la herencia: ambas son completamente
independientes.
Entonces, podemos decir que los objetos derivados de clases
que se encuentran en diferentes ramas del árbol de
herencia son siempre incompatibles .
El programa producirá el siguiente resultado:

372 – By Gerson
¡Maullar! ¡Maullar!
¡Guau! ¡Guau!

source_06_02_01_compat.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Cat {
public :
void MakeSound ( void ) { cout << "¡Miau! ¡Miau!" << endl
};
class Dog {
public :
void MakeSound ( void ) { cout << "¡Guau! ¡Guau!" << endl
};
int main ( void ) {
Cat * a_cat = ne w Cat () ;
Perro * a_dog = ne w Perro () ;
a_cat -> MakeSound () ;
a_dog -> MakeSound () ;
devuelve 0 ;
}

6.2.2 Compatibilidad de tipos: caso más complejo (1)


373 – By Gerson
Hemos reconstruido nuestro ejemplo significativamente. Todavía
tenemos las dos clases anteriores Perro y Gato , pero las hemos
colocado en el mismo árbol de herencia. Echa un vistazo →
Las clases de perros y gatos ahora son descendientes (para ser
precisos, niños ) de la clase base común de mascotas . También
hemos equipado todas las clases con constructores, lo que nos
permite dar nombres únicos a todos los objetos que crearemos en
el futuro. Nuestras mascotas también pueden correr (pero es
posible que necesite una correa).

Resumamos lo que creamos.

 los objetos derivados de la clase Pet pueden ejecutarse (no


vamos a crear una subclase de Turtle , por lo que este nombre
de actividad puede permanecer)

 los objetos derivados de las clases Perro y Gato pueden


ejecutarse (heredan esta habilidad de su
superclase); también pueden emitir sonidos (tenga en cuenta
que esta habilidad no está disponible para objetos de
la clase Mascota )

Y entonces:

 Los objetos de gatos y perros pueden hacer todo lo que


las mascotas pueden hacer

 Las mascotas no pueden hacer todo lo que el gato y


el perro pueden hacer

Lo diremos nuevamente, pero esta vez usando un poco menos de


terminología “zoológica”:

 los objetos de la subclase tienen al menos las mismas


capacidades que los objetos de la superclase

 los objetos de la superclase pueden no tener las mismas


capacidades que los objetos de la subclase
374 – By Gerson
Esto nos lleva a la siguiente conclusión:

 Los objetos de la superclase son compatibles con los


objetos de la subclase.

 los objetos de la subclase no son compatibles con los


objetos de la superclase

Esto significa que:

 usted puede hacer lo siguiente:

a_pet = perro nuevo ("Huckleberry");


a_pet -> Ejecutar ();

pero no puedes hacer nada como esto:

a_pet -> MakeSound ();

porque las mascotas no saben hacer sonidos (al menos en nuestro


mundo de clases)

 no tiene permitido hacer lo siguiente:

a_dog = nueva mascota ("mascota extraña");

El programa producirá el siguiente resultado:

mascota: estoy corriendo


Spike: estoy corriendo
Spike: ¡Guau! ¡Guau!

375 – By Gerson
Tom: estoy corriendo
Tom: Miau! ¡Maullar!

source_06_02_02_compat2.cpp

#include <iostream>
#include <cadena>
usando el espacio de nombres estándar ;
clase Pet {
protegida :
cadena Nombre ;
public :
Pet ( string n ) { Name = n ; }
void Run ( void ) { cout << Nombre << ": estoy corriendo" << endl ; }
};
clase Perro : mascota pública {
público :
perro ( cadena n ) : mascota ( n ) {};
vacío MakeSound ( vacío ) { cout << Nombre << ": ¡Guau! ¡Guau!" << endl ; }
};
clase Cat : public Pet {
public :
Cat ( string n ) : Pet ( n ) {} ;
vacío MakeSound ( vacío ) { cout << Nombre << ": ¡Miau! ¡Miau!" << endl ; }
};
int main ( nulo ) {
Pet a_pet ( "mascota" ) ;
Cat a_cat ( "Tom" ) ;

376 – By Gerson
Perro a_dog ( "Spike" ) ;
una mascota. Ejecutar () ;
un perro. Ejecutar () ; un perro. MakeSound () ;
un gato. Ejecutar () ; un gato. MakeSound () ;
devuelve 0 ;
}

6.2.3 Compatibilidad de tipos: caso más complejo (2)

El código de ejemplo aquí → ilustra las reglas que acabamos de


describir.
Analice el código y preste atención a cómo el puntero a los objetos
de la superclase (un puntero del tipo Pet * ) puede servir como
puntero a los objetos de la subclase.
Los comentarios marcan las declaraciones que serían ilegales en
este contexto.

El código produce el siguiente resultado:

Tom: estoy corriendo


Spike: estoy corriendo

source_06_02_03_compat3.cpp

#include <iostream>
#include <cadena>
usando el espacio de nombres estándar ;
clase Pet {
protegida :

377 – By Gerson
cadena Nombre ;
public :
Pet ( string n ) { Name = n ; }
void Run ( void ) { cout << Nombre << ": estoy corriendo" << endl ; }
};
class Perro : public Pet {
public :
Dog ( string n ) : Pet ( n) {} ;
vacío MakeSound ( vacío ) { cout << Nombre << ": ¡Guau! ¡Guau!" << endl ; }
};
clase Cat : public Pet {
public :
Cat ( string n ) : Pet ( n ) {} ;
vacío MakeSound ( vacío ) { cout << Nombre << ": ¡Miau! ¡Miau!" << endl ; }
};
int main ( void ) {
Pet * a_pet1 = new Cat ("Tom") ;
Pet * a_pet2 = nuevo perro ("Spike") ;

a_pet1 -> Ejecutar () ;


// 'a_pet1 -> MakeSound ();' No está permitido aquí!
a_pet2 -> Ejecutar () ;
// 'a_pet2 -> MakeSound ();' No está permitido aquí!

devuelve 0 ;
}

378 – By Gerson
6.2.4 Compatibilidad de tipos: cómo recuperar lo perdido

Por supuesto, es posible cuestionar el ejemplo de la diapositiva


anterior. ¿Por qué no se nos permite ordenar a nuestra mascota
que haga un sonido? Sabemos con certeza que tanto los perros
como los gatos emiten sonidos, a menudo ruidosos y molestos,
¿cuál es el problema?
El problema proviene de las comprobaciones estáticas
realizadas por el compilador durante el proceso de
compilación . El compilador está convencido de que las mascotas
no pueden emitir sonidos y no nos permitirán ni siquiera intentar
hacerlo.
¿Podemos instar al compilador a cambiar de opinión?

Sí, podemos usar los operadores de reparto . El primero de


estos puede usarse en este contexto, y se llama static_cast . Lo
usamos cuando queremos afectar al compilador y expresar un
mensaje como este:
Soy sensato, sé lo que estoy haciendo y quiero usar el puntero a
la superclase en relación con el objeto de la subclase; Le garantizo
que existe el objeto adecuado.
El operador static_cast se ve así →

El target_type es un nombre de tipo (o una descripción de tipo),


que queremos que el compilador para su uso en la evaluación del
valor de an_expression .
Por ejemplo: el siguiente formulario

static_cast <Perro *> (a_pet)

obliga al compilador a suponer


que a_pet se convierte (temporalmente) en un puntero de
tipo Dog * .
Esto significa que nuestro perro gana la capacidad de ladrar ...
adiós pacífico el domingo por la tarde.
379 – By Gerson
6.2.5 Compatibilidad de tipos: volver a nuestras mascotas

Hemos aplicado el operador static_cast para permitir que


nuestras mascotas vuelvan a emitir sus sonidos.
Así es como funciona →

El resultado se ve así:

Tom: estoy corriendo


Tom: Miau! ¡Maullar!
Spike: estoy corriendo
Spike: ¡Guau! ¡Guau!

source_06_02_05_compat4.cpp

380 – By Gerson
6.2.6 Compatibilidad de tipos: abuso del poder del
propietario

Desafortunadamente, es fácil maltratar y abusar del poder


del operador static_cast . Si lo usa sin pensar y sin el cuidado
adecuado, puede meterse en problemas.
Eche un vistazo al ejemplo (muy pobre) a la derecha →
Hemos tratado de tratar a un gato como a un perro
y viceversa . Los resultados son desastrosos: el programa
producirá el siguiente resultado:

Spike: estoy corriendo


Spike: ¡Miau! ¡Maullar!
Tom: estoy corriendo
Tom: Guau! ¡Guau!

Es una pesadilla, ¿no?


Este efecto es causado por el hecho de que el compilador no
puede verificar si el puntero que se está convirtiendo es
compatible con el objeto al que apunta . El compilador se ve
obligado a reconocer el puntero como válido; en realidad, no tiene
otra opción. Solo tiene que confiar en que sabemos lo que estamos
haciendo.
La verificación completa de la validez del puntero es posible
cuando y solo cuando el programa se está ejecutando (en
otras palabras, durante el tiempo de ejecución). El lenguaje "C
++" tiene un segundo operador de conversión diseñado

381 – By Gerson
especialmente para este caso. Su nombre es algo
sugerente: dynamic_cast .
El nombre dice que la conversión se lleva a cabo dinámicamente
con respecto al estado actual de todos los objetos creados. Esto
significa que la conversión puede (o no) ser exitosa, haciendo que
nuestro programa se detenga si quiere que un perro
maulle. Volveremos a este problema pronto.

source_06_02_06_compat5.cpp

6.2.7 Compatibilidad de tipos - caso final

La regla que establece que los objetos que se encuentran en


los niveles superiores son compatibles con los objetos en
los niveles inferiores de la jerarquía de clases funciona incluso
cuando la cadena de herencia es arbitrariamente larga .
El ejemplo aquí → muestra una cadena de tres clases. El puntero
a la superclase superior también funciona perfectamente para los
objetos inferiores.

382 – By Gerson
El programa emite el siguiente texto:

Sr. Bigglesworth: ¡Miau! ¡Maullar!


Sr. Bigglesworth: ¡Miau! ¡Maullar!

source_06_02_07_compat6.cpp

6.3.1 Reemplazar un método en la subclase (1)

Anular un método en la subclase: el resto

Cuando una subclase declara un método del nombre


previamente conocido en su superclase, se anula el método
383 – By Gerson
original . Esto significa que la subclase oculta el significado
anterior del identificador del método y cualquier invocación
codificada dentro de la subclase se referirá al método más nuevo.
Mira el ejemplo a la derecha →

El código demuestra dos aspectos importantes de la anulación.


En primer lugar, puede experimentar los efectos de anulación
cuando invoca MakeSound desde
los objetos a_cat y a_dog . Obviamente "escuchará" el sonido
adecuado para la clase de animales que los emite.
En segundo lugar, puede ver que los efectos de la anulación
pueden revertirse (o anularse) si usa el operador static_cast en
reversa. ¿Recuerdas que en la sección anterior usamos el
operador para tratar un puntero a una superclase como un puntero
a su subclase (llamado downcasting )? Actualmente, estamos
haciendo lo mismo a la inversa: estamos tratando de tratar un
puntero a una subclase ( Perro o Gato ) como un puntero a su
superclase ( Mascota ), llamado " uppercasting ".

En nuestro caso, la difusión superior muestra la


implementación original del método MakeSound .
Esto significa que el programa de ejemplo mostrará las siguientes
líneas en la pantalla:

Kitty the Cat dice: ¡Miau! ¡Maullar!


Kitty the Pet dice: ¡Shh! Shh!
Doggie the Dog dice: ¡Guau! ¡Guau!
Doggie the Pet dice: ¡Shh! Shh!

¿Puedes ver? Tanto el gato como el perro "recordaron" los sonidos


hechos por sus predecesores.

source_06_03_01_override.cpp

384 – By Gerson
6.3.2 Reemplazar un método en la subclase (2)

Anulación de un método en la subclase - continuación

El segundo programa de ejemplo muestra el otro lado del mismo


efecto. Puedes verlo aquí →
385 – By Gerson
Esta vez, no usamos static_cast , pero algunos de los punteros se
declaran explícitamente como apuntando a la superclase común
( Pet ). Las clases mismas permanecen intactas. Simplemente
hemos cambiado la forma en que tratamos los punteros.
Los efectos esperados (y recibidos) son casi los mismos: el código
mostrará el siguiente texto en la pantalla:

Kitty the Pet dice: ¡Shh! Shh!


Kitty the Cat dice: ¡Miau! ¡Maullar!
Doggie the Pet dice: ¡Shh! Shh!
Doggie the Dog dice: ¡Guau! ¡Guau!

source_06_03_02_override2.cpp

6.3.3 Reemplazar un método en la subclase (3)

Anulación de un método en la subclase - continuación

386 – By Gerson
Queremos pedirle ahora toda su atención, ya que vamos a
presentar una de las nociones objetivas más importantes:
el polimorfismo . Este es un método para redefinir el
comportamiento de una superclase (¡pero solo el que acepta
explícitamente ser tratado de esta manera!) Sin tocar su
implementación .

La palabra " polimorfismo " significa que la misma clase puede


mostrar muchas formas (" poli " - como en " poligamia ")
(" morfos ") no definidos por la clase en sí, sino por sus
subclases .
Otra definición dice que el polimorfismo es la capacidad de
realizar el comportamiento de clase de múltiples
maneras . Ambas definiciones son ambiguas y en realidad no
podrían usarse para explicar la verdadera naturaleza del
fenómeno. Creemos que un ejemplo funcionará mejor.

Echa un vistazo al código de la derecha. Es casi lo mismo que el


anterior. La función principal puede parecer diferente, pero las
definiciones de clase son casi idénticas. Solo hay una diferencia:
se ha agregado una palabra clave al código. ¿Puedes encontrarlo?
Sí, tiene razón: es la palabra " virtual ", colocada frente a
la función miembro MakeSound , dentro del cuerpo de la
clase Pet .
Esta palabra significa que el método no se anulará en ninguna de
las posibles subclases. También significa que el método
será redefinido (reemplazado) al nivel de la clase original.
Para abreviar una larga historia, el programa de ejemplo generará
un texto algo sorprendente. Así es como va:

Kitty the Cat dice: ¡Miau! ¡Maullar!


Kitty the Cat dice: ¡Miau! ¡Maullar!
Kitty the Cat dice: ¡Miau! ¡Maullar!
Doggie the Dog dice: ¡Guau! ¡Guau!
Doggie the Dog dice: ¡Guau! ¡Guau!
Doggie the Dog dice: ¡Guau! ¡Guau!
387 – By Gerson
Paremos por un momento dentro de la función principal . ¿Puedes
ver lo que hemos hecho? Hemos invocado la MakeSound método
de seis veces: dos veces por los objetos del animal doméstico de
clase ( a_dog y a_cat ) y cuatro veces por su superclase
( a_pet1 y a_pet2 ).
Hemos recibido exactamente el mismo resultado que si
la función MakeSound definida dentro de
la clase Pet simplemente hubiera ... desaparecido.
Parece que la clase Pet no siempre es la misma, cualquiera de
sus subclases puede cambiar su comportamiento .
En otras palabras, no puede predecir el comportamiento de la
clase que contiene una función virtual cuando no conoce todas sus
subclases. También puede significar que algunos de los aspectos
de la clase se definen fuera de la clase.
Así es como funciona el polimorfismo.

No vaya más allá hasta que esté familiarizado con los dos últimos
ejemplos del código. También lo alentamos a que haga algunos
experimentos por su cuenta, para que realmente pueda
familiarizarse con él.

source_06_03_03_poly.cpp

6.3.4 Reemplazar un método en la subclase (4)

El siguiente ejemplo muestra que el enlace entre el origen de la


función virtual (dentro de la superclase) y su reemplazo (definido
dentro de la subclase) se crea dinámicamente, durante la
ejecución del programa.
Echa un vistazo al ejemplo →

388 – By Gerson
Invocamos el método MakeSound como parte
del constructor Pet . Ya sabemos que el método
se reemplaza polimórficamente por las nuevas
implementaciones presentadas por
las subclases Cat y Dog . Todavía no sabemos cuándo ocurre el
reemplazo.
El programa generará las siguientes líneas:

Kitty the Pet dice: ¡Shh! Shh!


Doggie the Pet dice: ¡Shh! Shh!

Esto significa que el enlace entre las funciones originales y sus


implementaciones polimórficas se establece cuando se crea el
objeto de la subclase, no antes.

source_06_03_04_poly2.cpp
6.3.5 Reemplazar un método en la subclase (5)

Anulación de un método en la subclase - continuación


El experimento que vamos a realizar utilizando nuestro software
conejillo de indias se refiere al hecho de que el método virtual
puede invocarse no solo desde fuera de la clase sino también
desde dentro.
Mira este ejemplo, por favor →

Hemos agregado un nuevo método a la clase Mascota . Su


objetivo es implementar todas las acciones realizadas por una
mascota cuando se despierta. Asumimos (queridas mascotas,
perdónanos) que cada mascota haga un sonido entonces.
En efecto, el método MakeSound se invoca indirectamente, a
través del método WakeUp que se define una vez en la superclase
y no se anula en ningún otro lado.
El código produce el siguiente resultado:

389 – By Gerson
Kitty the Cat dice: ¡Miau! ¡Maullar!
Doggie the Dog dice: ¡Guau! ¡Guau!

source_06_03_05_poly3.cpp
6.4.1 Pasar un objeto como parámetro de función

Cualquier objeto puede usarse como parámetro de


función y, viceversa, cualquier función puede tener un
parámetro como objeto de cualquier clase .
Todas las reglas relativas al paso de parámetros se aplican
también a los objetos. Echa un vistazo al ejemplo →

El código muestra dos formas de pasar un objeto a una


función: por puntero y por referencia . Ya sabemos que pasar
un puntero es solo una forma específica de pasar un valor porque
un puntero es solo un tipo específico de valor que nos permite
modificar el estado de un objeto dentro de una función.
Hay dos funciones en el ejemplo. Ambos tienen dos parámetros,
pero solo el segundo es de interés para nosotros. La primera
función declara un puntero allí y la segunda una referencia . En
ambos casos, la clase objetivo es la misma: " Mascota ".
Creamos dos objetos dentro de la función principal: el primero
( p1 ) es manejado por un puntero y creado por la nueva palabra
clave. El segundo ( p2 ) es una variable automática.
Invocamos cada una de las funciones dos veces, la primera según
sus declaraciones, la segunda reemplazando la forma en que se
manejan los objetos: p2 está sujeto al operador & ; p1 es
desreferenciado por el operador * .

El programa producirá el siguiente resultado:

anónimo dice: sin comentarios


sin nombre dice: sin comentarios
sin nombre dice: sin comentarios
390 – By Gerson
anónimo dice: sin comentarios

source_06_04_01_param.cpp

6.4.2 Pasar un objeto por valor

¿Se pueden pasar los objetos por valor como valores ordinarios
de tipos integrados básicos como int o float ?
Sí, por supuesto, pero debe tener en cuenta que cualquier
modificación realizada a estos parámetros no abandonará la
función: los efectos de cualquier operación que afecte al objeto
ocurrirán en la copia del objeto, no en el objeto en sí.

El ejemplo aquí → ilustra las tres variantes posibles para hacer uso
de un objeto dentro de una función. El nombre de la función
individual muestra la forma en que la función recibió su
parámetro. Las tres funciones intentan nombrar su parámetro (es
decir, invocar su método llamado NameMe ).
Inmediatamente después de que se completa la invocación
de la función , se activa el método MakeSound para verificar si el
nombramiento ha sido exitoso y si puede observarse fuera de la
función.
El programa produce el siguiente resultado:

no_name dice: sin comentarios


Beta dice: sin comentarios
Gamma dice: sin comentarios

Esto significa que nombrar un objeto " Alfa " ha afectado una copia
del objeto mascota , no el objeto en sí.
También hay otro inconveniente grave que aparece cuando pasa
por valor un objeto de una subclase de una clase especificada
como un tipo de parámetro. Puede provocar la "desaparición" de
parte del objeto. Entonces, pasar un objeto por valor
391 – By Gerson
generalmente no es una buena idea. Volveremos a este problema
pronto, mostrándole un aspecto diferente del problema.

source_06_04_02_param2.cpp

6.4.3 Pasar un objeto de una subclase (1)

Pasar un objeto de una subclase (por referencia)

Ahora vamos a introducir una jerarquía de clases un poco más


complicada. Está aquí a la derecha →

392 – By Gerson
Podemos ilustrar su estructura usando el siguiente diagrama:

Hay una función llamada PlayWithPet , lista para obtener un


parámetro de tipo Pet & (una referencia al objeto Pet ). La función
se invoca con cuatro objetos diferentes tomados de diferentes
niveles de la jerarquía de clases.
Esto es posible porque (como hemos mencionado anteriormente)
un objeto de la superclase es compatible con los objetos de
cualquiera de las subclases. Podemos declarar un parámetro
formal de un tipo como una superclase y pasar un parámetro real
de cualquiera de las subclases del parámetro formal. Esto es
exactamente lo que vamos a hacer en nuestro ejemplo.

¿Puedes predecir la salida? Antes de responder, tenga en cuenta


si la función MakeSound es virtual o no, es decir, si estamos
lidiando con un polimorfismo o no.

393 – By Gerson
No, no lo estamos.

El resultado esperado es:

la criatura está en silencio :(


El perro está en silencio :(
El perro está en silencio :(
Perro is silent :(

source_06_04_03_param3.cpp

6.4.4 Pasar un objeto de una subclase (2)

Pasar un objeto de una subclase (por puntero nuevamente)

Hemos reescrito el programa anterior. Lo puedes encontrar aquí



La modificación no ha cambiado su comportamiento y el resultado
es exactamente el mismo. Hemos cambiado la interfaz de
la función PlayWithPet y ahora la función acepta un puntero, no
una referencia. Obviamente, también hemos tenido que cambiar
la función principal.

source_06_04_04_param4.cpp

#include <iostream>
#include <cadena>
usando el espacio de nombres estándar ;
clase Pet {

394 – By Gerson
protegido : nombre de cadena ;
public : Pet ( string name ) { this -> name = name ; }
void MakeSound ( void ) { cout << nombre << "es silencioso :(" << endl ; }
};
clase Perro : público Mascota {
público : Perro ( nombre de cadena ) : Mascota ( nombre ) {}
void MakeSound ( vacío ) { cout << nombre << "dice: ¡Guau!" << endl ; }
};
clase GermanShepherd : public Dog {
public : GermanShepherd ( string name ) : Dog ( name ) {}
void MakeSound ( nulo ) { cout << nombre << "dice: ¡Wuff!" << endl ; }
};
clase MastinEspanol : public Dog {
public : MastinEspanol ( string name ) : Dog ( name ) {}
void MakeSound ( void ) { cout << name << "dice: Guau!" << endl ; }
};
nulo PlayWithPet ( Pet * pet ) {
mascota -> MakeSound () ;
}
int main ( void ) {
Pet * pet = new Pet ( "criatura" ) ;
Perro * perro = Perro nuevo ( "Perro" ) ;
GermanShepherd * gs = new GermanShepherd ( "Hund" ) ;
MastinEspanol * mes = nuevo MastinEspanol ( "Perro" ) ;
PlayWithPet ( mascota ) ;
PlayWithPet ( perro ) ;
PlayWithPet ( gs ) ;
PlayWithPet ( mes ) ;
395 – By Gerson
devuelve 0 ;
}

<cadena>

6.4.5 El operador dynamic_cast (1)

El operador dynamic_cast aplicado a un puntero

Hemos reorganizado radicalmente nuestra perrera. Puedes verlo


aquí →
Hablemos de los cambios que hemos realizado.

En primer lugar, hemos modificado el método MakeSound dentro


de la clase de nivel superior: ahora es virtual . Probablemente
pueda predecir las consecuencias de esta acción.
En segundo lugar, hemos hecho que el árbol de clases sea más
alto (o más bien, más profundo, con respecto al hecho de que el
árbol crece de arriba a abajo). Hemos agregado dos niveles
adicionales al árbol. El nivel más bajo se compone de dos ramas
que contienen especies muy específicas: pastor alemán y Mastin
Español (Mastín español) . Estos perros ladran en sus idiomas
nativos (no se confundan: hay más cosas en el Cielo y la Tierra de
las que se sueñan en nuestra tecnología informática) y, además,
también se ejecutan en sus idiomas. Eche un vistazo a
sus métodos MakeSound y observe dos nuevos métodos
llamados Laufen y Ejecutar respectivamente (ambas palabras se
traducen como "ejecutar" en inglés).

La función PlayWithPet obtiene un puntero de tipo Pet * (es decir,


un puntero a los objetos de clase de nivel superior). La función
hace dos cosas importantes:

 invoca el método MakeSound ; el método está marcado como


virtual, por lo que esperamos que los objetos "nacionales"
puedan hacer sus sonidos nativos;
396 – By Gerson
 intenta reconocer la naturaleza del puntero recibido y forzar al
objeto puntiagudo a comportarse de acuerdo con su origen;
este es el momento en que el operador dynamic_cast
se vuelve indispensable

Ahora introduzcamos la siguiente regla:


Si el operador dynamic_cast se usa de la siguiente manera:

dynamic_cast <pointer_type> (pointer_to_object)

y la conversión de pointer_to_object al tipo de pointer_type es


posible, entonces el resultado de la conversión es un nuevo
puntero que es totalmente utilizable . De lo contrario, el
resultado de la conversión es igual a NULL.

Este mecanismo nos permite reconocer convenientemente la


naturaleza del puntero. Ver la declaración:

if (gs = dynamic_cast <GermanShepherd *> (mascota))


gs -> ejecutando ();

Si el puntero de mascota identifica un objeto de


la clase GermanShepherd o (¡no se olvide!) De cualquiera de sus
subclases, hacemos uso del puntero convertido almacenado en
la variable gs . De lo contrario no hacemos nada.

Analice el ejemplo con mucho cuidado. ¡A continuación, compílalo


y libera a los perros! Esto es lo que deberías ver en la pantalla:

la criatura está en silencio :(


El perro dice: ¡Guau!
El perro dice: ¡Wuff!
Perro corre (gs)!

397 – By Gerson
Perro says: Guau!
Perro runs (mes)!

No vaya más allá hasta que pueda explicar cada línea de la


salida. También le sugerimos que expanda este código y realice
algunos experimentos con usted mismo. Confíe en nosotros: este
será un tiempo bien empleado.

source_06_04_05_param5.cpp

#include <iostream>
#include <cadena>
usando el espacio de nombres estándar ;
clase Pet {
protegido : nombre de cadena ;
public : Pet ( string name ) : name ( name ) {}
virtual void MakeSound ( void ) { cout << name << "is silent :(" << endl ; }
};
class Dog : public Pet {
public : Dog ( nombre de cadena ) : Pet ( nombre ) {}
void MakeSound ( void ) { cout << name << "dice: ¡Guau!" << endl ; }
};
clase GermanShepherd : public Dog {
public : GermanShepherd ( string name ) : Dog ( name ) {}
void MakeSound ( void ) { cout << name << "dice: Wuff!" << endl ; }
vacío Laufen( nulo ) { cout << nombre << "ejecuta (gs)!" << endl ; }
};
clase MastinEspanol : public Dog {

398 – By Gerson
public : MastinEspanol ( string name ) : Dog ( name ) {}
void MakeSound ( void ) { cout << name << "dice: Guau!" << endl ; }
vacío Ejecutar ( vacío ) { cout << nombre << "<< endl ; }
};
nulo PlayWithPet ( Pet * pet ) {
GermanShepherd * gs ;
MastinEspanol * mes ;
mascota -> MakeSound () ;
if ( gs = dynamic_cast <GermanShepherd *> ( pet ))
gs -> Laufen () ;
if ( mes = dynamic_cast <MastinEspanol *> ( mascota ))
mes -> Ejecutar () ;
}
int main ( nulo ) {
Pet * pet =nueva mascota ( "criatura" ) ;
Perro * perro = Perro nuevo ( "Perro" ) ;
GermanShepherd * gs = new GermanShepherd ( "Hund" ) ;
MastinEspanol * mes = nuevo MastinEspanol ( "Perro" ) ;
PlayWithPet ( mascota ) ;
PlayWithPet ( perro ) ;
PlayWithPet ( gs ) ;
PlayWithPet ( mes ) ;
regreso0 ;
}

6.4.6 El operador dynamic_cast (2)

399 – By Gerson
El operador dynamic_cast aplicado a una referencia

Esta vez, la perrera ha permanecido intacta, pero hemos cambiado


la forma en que tratamos a nuestros perros. Echa un vistazo al
ejemplo →
La función PlayWithPet no tiene un puntero sino
una referencia . En consecuencia, las siguientes dos partes de
los programas también han cambiado:

 la función principal invoca el PlayWithPet de una manera


ligeramente diferente (eche un vistazo)

 la forma de utilización de dynamic_cast es bastante diferente


aquí; el operador toma la siguiente forma:

Dynamic_cast <reference_type> (reference_to_object)

y devuelve una referencia recién transformada (convertida) que,


como resultado, puede usarse como un valor l ordinario ; no
necesitamos asignarlo a una variable si queremos usarlo; Esto es
exactamente lo que hicimos dentro de la función modificada.

Hay una pregunta que debe hacer aquí y tenemos mucha


curiosidad si la va a hacer. Como no podemos adivinar sus
pensamientos, tenemos que hacerlo nosotros mismos:
¿Qué pasará cuando el operador dynamic_cast no pueda
hacer su tarea?

En el ejemplo anterior, solo verificamos el resultado del casting e


hicimos algo o no. No usamos nada similar aquí. ¿Es esto algo
razonable?
Bueno, vamos a verlo. Sin riesgo, sin educación, ¿verdad?

El programa, compilado y ejecutado, produce el siguiente


resultado decepcionante:

400 – By Gerson
la criatura está en silencio :(
terminar llamado después de lanzar una instancia de 'std :: bad_cast'
what (): std :: bad_cast

Esta aplicación ha solicitado el Tiempo de ejecución para terminarlo de una


manera inusual.
Póngase en contacto con el equipo de soporte de la aplicación para obtener
más información.

La forma del mensaje puede diferir si ha utilizado un compilador


diferente; también es posible que no vea nada más que el último
mensaje exitoso, es decir:

la criatura está en silencio :(

y una ventana de mensaje que dice que su programa ha finalizado


debido a un error de tiempo de ejecución.

Este es el resultado esperado de nuestra indiferencia. No debemos


usar una referencia (o puntero) convertida sin estar seguros de
que el resultado ya está definido. Ya sabemos cómo hacerlo
usando punteros. ¿Podemos hacer lo mismo con las referencias?
No, no podemos No tan fácilmente, de todos modos.

Pero esto no significa que nos quedemos con nuestros propios


dispositivos. Hay una manera de protegernos de los efectos de los
castings fallidos, pero para ser sincero, no queremos explicarlo en
este momento.
Estamos planeando discutir este tema en el próximo capítulo, así
que discúlpenos por ahora.

source_06_04_06_param6.cpp
401 – By Gerson
#include <iostream>
#include <cadena>
usando el espacio de nombres estándar ;
clase Pet {
protegido : nombre de cadena ;
public : Pet ( string name ) : name ( name ) {}
virtual void MakeSound ( void ) { cout << name << "is silent :(" <<
};
clase Perro: público Mascota {
público : Perro ( nombre de cadena ) : Mascota ( nombre ) {}
void MakeSound ( vacío ) { cout << nombre << "dice: ¡Guau!" << en
};
clase GermanShepherd : public Dog {
public : GermanShepherd ( string name ) : Dog ( name ) {}
void MakeSound ( void ) { cout << name << "dice: Wuff!" << endl ;
void Laufen ( void ) { cout << nombre << "ejecuta (gs)!" << endl ; }
};
clase MastinEspanol : public Dog {
public : MastinEspanol ( string name ) : Dog ( name ) {}
void MakeSound ( void ) { cout << name << "dice: Guau!" << endl ;
Void Ejecutar ( void ) { cout << << nombre de "carreras (MES)!" <<
};
nulo PlayWithPet ( Pet & pet ) {

402 – By Gerson
pet. MakeSound () ;
Dynamic_cast <GermanShepherd &> ( mascota ) . Laufen () ;
dynamic_cast <MastinEspanol &> ( mascota ) . Ejecutar () ;
}
int main ( void ) {
Pet pet ( "criatura" ) ;
Perro perro ( "Perro" ) ;
GermanShepherd gs ( "Hund" );
MastinEspanol mes ( "Perro" ) ;
PlayWithPet ( mascota ) ;
PlayWithPet ( perro ) ;
PlayWithPet ( gs ) ;
PlayWithPet ( mes ) ;
devuelve 0 ;
}

6.4.7 El operador dynamic_cast (3)

El operador dynamic_cast aplicado a una referencia

La modificación es muy sutil: eche un vistazo dentro de


la función PlayWithPet →
Aquí hay algo nuevo: la declaración try-catch . Se parece a
esto:

tratar {

403 – By Gerson
thing_we_want_to_try_although_we_are_not_quite_sure_if_it_is_reason
able;
} captura(…) {}

Con esta declaración podemos "probar" algunas actividades


riesgosas y errores de "captura" que aparecen durante la
ejecución.
En nuestros ejemplos, detectamos el error y no hacemos nada.
Gracias a eso, nuestros perros están listos para volver a su vida
normal. El error que surge durante el lanzamiento fallido
simplemente se ignora, pero en efecto (paradójicamente), el
lanzamiento ahora es seguro y no causará daños en caso de falla.

El programa produce esta salida:

la criatura está en silencio :(


El perro dice: ¡Guau!
El perro dice: ¡Wuff!
Perro corre (gs)!
Perro says: Guau!
Perro runs (mes)!

source_06_04_07_param7.cpp

#include <iostream>
#include <cadena>
usando el espacio de nombres estándar ;
clase Pet {
protegido : nombre de cadena ;
public : Pet ( string name ) : name ( name ) {}
virtual void MakeSound ( void ) { cout << name << "is silent :(" << endl ; }

404 – By Gerson
};
clase Perro : público Mascota {
público : Perro ( nombre de cadena ) : Mascota ( nombre ) {}
void MakeSound ( vacío ) { cout << nombre << "dice: ¡Guau!" << endl ; }
};
clase GermanShepherd : public Dog {
public : GermanShepherd ( string name ) : Dog ( name ) {}
void MakeSound ( nulo ) { cout << nombre << "dice: ¡Wuff!" << endl ; }
void Laufen ( void ) { cout << nombre << "ejecuta (gs)!" << endl ; }
};
clase MastinEspanol : public Dog {
public : MastinEspanol ( string name ) : Dog ( name ) {}
void MakeSound ( void ) { cout << name << "dice: Guau!" << endl ; }
Void Ejecutar ( void ) { cout << << nombre de "carreras (MES)!" << endl ; }
};
nulo PlayWithPet ( Pet & pet ) {
mascota. MakeSound () ;
pruebe {
dynamic_cast <GermanShepherd &> ( mascota ) . Laufen () ;
} catch ( ... ) {}
try {
dynamic_cast <MastinEspanol &> ( pet ) . Ejecutar () ;
} captura ( ... ) {}
}
int main ( void ) {
Pet pet ( "criatura" ) ;
Perro perro ( "Perro" ) ;
GermanShepherd gs ( "Hund" ) ;
MastinEspanol mes ( "Perro" ) ;
PlayWithPet ( mascota ) ;
PlayWithPet ( perro ) ;

405 – By Gerson
PlayWithPet ( gs ) ;
PlayWithPet ( mes ) ;
devuelve 0 ;
}

6.5.1 Más sobre copiar constructores (1)

El constructor de copia es una forma específica de constructor


diseñada para hacer una copia más o menos literal de un
objeto. Puede reconocer este constructor por su encabezado
distinguible .
Suponiendo que una clase se llama A, su constructor de copia se
declarará como:

A (A y)

Esto significa que el constructor espera que un parámetro sea


una referencia a un objeto cuyo contenido está destinado a
copiarse al objeto recién creado. No hay obligación de declarar su
propio constructor de copia dentro de ninguna de las clases.
Si no hay un constructor de copia explícito en alguna clase, se
utilizará un constructor implícito en su lugar. El constructor
implícito simplemente clona (bit a bit) el objeto fuente ,
produciendo una copia gemela del mismo. Funciona
satisfactoriamente en la mayoría de los casos, pero requiere
atención y cierta consideración cuando se usa con objetos que
contienen otros objetos o punteros.
Comencemos con un ejemplo muy simple →

La clase declarada en el ejemplo no tiene un constructor de


copia explícito . Esperamos que se use un constructor implícito
entonces. Se activará dos veces, durante las inicializaciones de
los objetos o2 y o3 . El ejemplo nos recuerda dos formas posibles
de especificar la declaración de un objeto, usando

406 – By Gerson
el operador = en la notación de asignación clásica y usando
la notación funcional .
Los objetos o2 y o3 se crearán como copias gemelas de los
objetos o1 y o2 respectivamente, pero debemos enfatizar que
estos tres objetos no tienen nada en común .
En particular, cada uno de ellos tiene su propio campo
de datos . Los tres objetos viven sus propias vidas, separados de
sus hermanos. Como puede predecir, el incremento aplicado
al objeto o1 no afecta ni al objeto o2 ni al objeto o3 . En efecto, el
código producirá el siguiente resultado:

124
123
123

source_06_05_01_cc.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
datos int ;
pública :
Clase ( int valor ) : los datos ( valor ) {}
vacío de la subasta ( void ) { datos ++ ; }
valor int ( void ) { datos de retorno ; }
};
int main ( void ) {
Clase o1 ( 123 ) ;
Clase o2 = o1 ;
Clase o3 ( o2 ) ;

407 – By Gerson
o1. incremento () ;
cout << o1. valor () << endl ;
cout << o2. valor () << endl ;
cout << o3. valor () << endl ;

devuelve 0 ;
}

6.5.2 Más sobre copiar constructores (2)

Ahora le mostraremos que confiar ciegamente en el constructor


de copia implícita puede ser peligroso y causar efectos
adversos. Echa un vistazo al ejemplo →

Hemos modificado nuestra clase ligeramente. Su interfaz sigue


siendo la misma y la funcionalidad que ofrece la clase sigue siendo
la misma. Solo hemos cambiado la implementación de la clase:
ahora los datos no se almacenan en una variable regular, sino
en una pieza de memoria asignada por el nuevo operador .
En efecto, el campo de datos se declara como un puntero a
los valores int (en otras palabras, es una variable de tipo int * ).

Anteriormente dijimos que el constructor de copia implícita hace


una copia gemela de un objeto. Tenga en cuenta que esto es de
un objeto, no de las entidades existentes fuera del
objeto . Esto significa que el campo de datos obviamente se
copiará (clonará) en el objeto recién creado, pero en efecto
apuntará al mismo fragmento de memoria.
Esto también significa que diferentes objetos pueden tener algo
en común: pueden compartir algunos datos entre ellos.
No vamos a decir que esto siempre está mal y siempre significa
problemas. Si lo está haciendo intencionalmente, puede ser útil y
408 – By Gerson
útil. Pero si lo ha hecho por casualidad o por accidente, puede ser
muy engorroso y difícil de diagnosticar.

El código que hemos creado produce el siguiente resultado:

124
124
124

source_06_05_02_ccptr.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Class {
int * data ;
public :
Class ( int value ) {
data = new int ;
* datos = valor ;
}
incremento nulo ( nulo ) {( * datos ) ++ ; }
valor int ( void ) { retorno * datos ; }
};
int main ( void ) {
Clase o1 ( 123 ) ;
Clase o2 = o1 ;

409 – By Gerson
Clase o3 ( o2 ) ;
o1. incremento () ;
cout << o1. valor () << endl ;
cout << o2. valor () << endl ;
cout << o3. valor () << endl ;
devuelve 0 ;
}

6.5.3 Más sobre copiar constructores (3)

Por supuesto, podemos protegernos de este tipo de


comportamiento simplemente no utilizando el constructor de
copia.
El ejemplo de la derecha → muestra un fragmento de código que
no utiliza ninguna sintaxis que puede resultar en la aplicación del
constructor de copia implícita. Solo usa el constructor explícito,
que obtiene un parámetro de tipo int .
¿Es elegante? No lo es.
¿Es seguro? Realmente no. Es casi seguro que usted (o cualquier
otro desarrollador) olvidará esa restricción y escribirá un código
que la omita.

Lo hemos colocado aquí solo con fines didácticos. Como


probablemente haya adivinado, el código produce el siguiente
resultado:

124
123
123
410 – By Gerson
source_06_05_03_ccptr2.cpp

incluir <iostream>
usando el espacio de nombres estándar ;
clase Class {
int * data ;
public :
Class ( int value ) {
data = new int ;
* datos = valor ;
}
incremento nulo ( nulo ) {( * datos ) ++ ; }
valor int ( void ) { retorno * datos ; }
};

int main ( void ) {


Clase o1 ( 123 ) ;
Clase o2 ( o1. Valor ()) ;
Clase o3 ( o2. Valor ()) ;
o1. incremento () ;
cout << o1. valor () << endl ;
cout << o2. valor () << endl ;
cout << o3. valor () << endl ;
devuelve 0 ;
411 – By Gerson
}

6.5.4 Más sobre copiar constructores (4)

Supongamos que no queremos que nuestros objetos compartan


ningún dato. Estamos decididos a aislar los objetos y mantenerlos
separados. Teniendo en cuenta la estructura de la clase,
deberíamos considerar agregar un constructor de copia explícito a
nuestra clase.
El constructor será responsable de hacer una copia por sí
mismo. No olvide que no se realizarán actividades automáticas si
una clase ya está equipada con un constructor de copia.
Nuestro nuevo constructor de copia (que se muestra aquí a la
derecha →) ha asignado una nueva pieza de memoria y ha copiado
el contenido de datos originales. Esto asegura que el objeto estará
separado y no tendrá partes comunes.

Ahora podemos volver a la forma anterior de inicialización de


nuevo objeto.
El código mostrará las siguientes líneas en la pantalla:

124
123
123

source_06_05_04_excc.cpp

#include <iostream>
usando el espacio de nombres estándar ;

412 – By Gerson
clase Class {
int * data ;
public :
Class ( int value ) {
data = new int ;
* datos = valor ;
}
Clase ( Clase y fuente ) {
data = new int ;
* datos = fuente. valor () ;
}
incremento nulo ( nulo ) {( * datos ) ++ ; }
valor int (nulo ) { retorno * datos ; }
};
int main ( void ) {
Clase o1 ( 123 ) ;
Clase o2 = o1 ;
Clase o3 ( o2 ) ;
o1. incremento () ;
cout << o1. valor () << endl ;
cout << o2. valor () << endl ;
cout << o3. valor () << endl ;
devuelve 0 ;
}

413 – By Gerson
6.5.5 Más sobre copiar constructores (5)

Ya hemos dicho que usar un objeto como parámetros de función


pasados por valor no es una buena idea y ahora vamos a agregar
un argumento fuerte para respaldar esto. Le mostraremos un
aspecto interesante y algo sorprendente del uso de constructores
de copia.
Como ya sabe, el mecanismo de pasar parámetros por valor
supone que una función opera en la copia de un parámetro
real. Esto está claro cuando consideramos parámetros de tipos
simples (como int o float ), pero se vuelve más complejo cuando
el parámetro es un objeto.
No hacemos simplemente una copia de un objeto. La única forma
automática de obtener la copia es invocar al constructor de la
copia y hacer frente a todas las ventajas y desventajas de ese
enfoque.

El ejemplo de la derecha → muestra que el constructor de copia


se invocará cuando un objeto se pase a una función por
valor. Hemos tenido que hacer que el constructor sea un poco
detallado: esta es la forma más sencilla de trazar sus caminos.
La salida del programa no es realmente compleja: dice:

Hola del constructor de copias!


¡Estoy aquí!

source_06_05_05_ccbyvalue.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Dummy {
public :
414 – By Gerson
Dummy ( int value ) {}
Dummy ( Dummy & source ) {
cout << "¡Hola del constructor de copias!" << endl ;
}
};
anular DoSomething ( Dummy ob ) {
cout << "Estoy aquí!" << endl ;
}
int main ( void ) {
Dummy o1 ( 123 ) ;
DoSomething ( o1 ) ;
devuelve 0 ;
}
6.5.6 Más información sobre la copia de constructores (6)

En algunos casos (especialmente cuando la estructura de la clase es co


lo que cualquier constructor de copia es capaz de proporcionar), es pos
copia que obliga al usuario de su clase para construir sus objetos de
constructores descritos explícitamente).
es posible? Si. Todo lo que tiene que hacer es especificar el constructo
dentro de la parte privada de su clase.
Cualquier intento de hacer uso del constructor de copia (ya sea implícit
El programa de la derecha → causará al menos dos errores de compilac

source_06_05_06_nocc.cpp

#include <iostream>

415 – By Gerson
usando el espacio de nombres estándar ;
clase Dummy {
private :
Dummy ( Dummy & source ) {}
public :
Dummy ( int value ) {}
};
anular DoSomething ( Dummy ob ) {
cout << "Estoy aquí!" << endl ;
}
int main ( void ) {
Dummy o1 ( 123 ) ;
Maniquí o2 = o1 ;
DoSomething ( o1 ) ;
devuelve 0 ;
}

6.5.7 Más sobre constructores predeterminados (1)

Ahora le mostraremos algunos ejemplos diseñados para


sistematizar nuestro conocimiento de constructores. El primer
ejemplo → muestra una clase que no tiene un constructor en
absoluto. Es legal? Sí, si conoce todos los peligros posibles
relacionados con el uso de objetos no inicializados.
Una clase como esta es una expresión de nuestra voluntad de no
usar ningún constructor. Nuestra voluntad será parcialmente
reconocida: la clase estará equipada implícitamente con el
llamado constructor implícito predeterminado (sin
parámetros), pero el constructor no hará nada en
absoluto. ¡Nuestra voluntad será voluntaria de hecho!

Echa un vistazo al ejemplo. La clase no tiene constructor. En


efecto, sus campos no se inicializarán de ninguna manera. Los
416 – By Gerson
valores generados por el método de visualización son
completamente aleatorios. El número que hemos visto no se
repetirá cuando ejecutes el programa en tu computadora.
Una de nuestras salidas es la siguiente:

i = 2147344384, f = 1.54143e-044
i = 5641768, f = 7.89812e-039

source_06_05_07_nocons.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase NoConstructorsAtAll {
public :
int i ;
flotador f ;
Pantalla vacía ( vacío ) { cout << "i =" << i << ", f =" << f << e
};
int main ( void ) {
NoConstructorsAtAll o1 ;
NoConstructorsAtAll * o2 ;
o2 = nuevo NoConstructorsAtAll ;
o1. Mostrar () ;
o2 -> Pantalla () ;
devuelve 0 ;

417 – By Gerson
}

6.5.8 Más sobre constructores predeterminados (2)

Eche un vistazo al ejemplo de la derecha → Se ve bastante bien,


¿no? Pero no es bueno en absoluto: provocará errores de
compilación. ¿Por qué?
Hay un constructor dentro de la clase. La existencia de cualquier
constructor es como una declaración dada por un desarrollador:
" Estoy listo para usar constructores en mi clase ".
El compilador reconoce la declaración como una disposición para
proporcionar todos los constructores necesarios para que el
compilador ya no produzca el constructor implícito
predeterminado para esa clase.

El constructor predeterminado debe invocarse implícitamente


cuando se crea un nuevo objeto (dos veces en nuestro ejemplo).
Cómo podemos resolver el problema? Por supuesto, podemos
escribir nuestro propio constructor predeterminado (sin
parámetros).
Hará que nuestro código sea totalmente compilable y puede verse
más o menos así:

WithConstructor (nulo): i (0), f (0.0) {}

y ser colocado en algún lugar de la parte pública de la


clase. Causará que ambos campos se pongan a cero al comienzo
de la vida de un objeto.

También hay al menos una solución más simple (y ligeramente


sorprendente). Todo se revelará en la próxima diapositiva.

418 – By Gerson
source_06_05_08_withcons.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase WithConstructor {
public :
int i ;
flotador f ;
WithConstructor ( int a, float b ) : i ( a ) , f ( b ) {}
void Display ( void ) { cout << "i =" << i << ", f =" << f << endl ; }
};
int main ( void ) {
WithConstructor o1 ;
Con Constructor * o2 ;
o2 = nuevo WithConstructor ;
o1. Mostrar () ;
o2 -> Pantalla () ;
devuelve 0 ;
}

6.5.9 Más sobre constructores predeterminados (3)

El ejemplo de la derecha → es correcto y se puede compilar con


éxito. ¿Cómo es esto posible? ¡No hemos agregado nuevos
constructores a la clase todavía!
No lo hemos hecho, es cierto, pero hemos cambiado el encabezado
del constructor existente agregando valores predeterminados a
ambos parámetros. Esto significa que desde la perspectiva de un
compilador, una invocación como esta

419 – By Gerson
Con Constructor ()

es solucionable incluso cuando el constructor sin parámetros no


se puede encontrar dentro de la clase.

El compilador buscará otro candidato compatible para la


invocación y lo encontrará, asumiendo que corresponde a la
siguiente invocación:

Con Constructor (0, 0.0)

Hemos guardado el programa. Ahora está sano y salvo y está listo


para producir el siguiente resultado:

i = 0, f = 0
i = 0, f = 0

source_06_05_09_withcons2.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase WithConstructor {
public :
int i ;
flotador f ;
WithConstructor ( int a = 0 , float b = 0 ) : i ( a ) , f ( b ) {}
void Display ( void ) { cout << "i =" << i << ", f =" << f << endl ; }
420 – By Gerson
};
int main ( void ) {
WithConstructor o1 ;
Con Constructor * o2 ;
o2 = nuevo WithConstructor ;
o1. Mostrar () ;
o2 -> Pantalla () ;
devuelve 0 ;
}

6.5.10 Composiciones frente a constructores (1)

La herencia no es la única forma de coexistencia de clase /


objeto. De hecho, la herencia es completamente inútil cuando
expresamos muchas de las complicadas relaciones existentes en
el mundo real.
Por ejemplo, podemos imaginar una clase diseñada para reunir
diferentes tipos de perros. Hacer una clase y ponerlo en la parte
inferior de un árbol de herencia de raza de perro no es una buena
idea y no dará frutos deseables. La solución correcta nos vendrá
a la mente cuando comprendamos el papel real de la clase:
debería reunir algunos perros, de hecho, consistirá en perros
(como una perrera).
Esto significa que la clase debe incluir perros, no heredarlos. La
perrera no tiene nada o muy poco en común con un perro (por
ejemplo, no hay perrera que pueda ladrar).

Podemos decir que cualquier estructura compleja está compuesta


por elementos más simples; por ejemplo, un automóvil está
compuesto por un motor, transmisión, suspensión, etc. Si
imaginamos todas estas partes como clases, veremos la clase de
automóvil como una composición que tiene nada que ver con la
herencia.
Como probablemente hayas adivinado, construir una clase se
llama composición .
421 – By Gerson
Mira el ejemplo de la derecha →
Este ejemplo muestra una clase ficticia simple que consta
(compuesta) de dos objetos de dos clases independientes.
No tendrá problemas para ver que el programa produce el
siguiente resultado:

A está haciendo algo


B está haciendo algo

source_06_05_10_compo.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase A {
public :
void Do ( void ) { cout << "A está haciendo algo" << endl ; }
};
clase B {
public :
void Do ( void ) { cout << "B está haciendo algo" << endl ; }
};
clase Compo {
público :
A f1 ;
B f2 ;
};
int main ( void ) {

422 – By Gerson
Compo co ;
co. f1 . Do () ;
co. f2 . Do () ;
devuelve 0 ;
}

6.5.11 Composiciones frente a constructores (2)

El ejemplo de la derecha → muestra la misma clase compuesta


aunque sus componentes han sido modificados. Los hemos
equipado con copiadores de constructores. ¿Puedes verlos? No
son tan complejos y en realidad no hacen nada útil. Solo emiten
un mensaje que nos permite notar que el constructor ha sido
invocado.
También hemos tenido que agregar constructores
predeterminados, ¿por qué? Deberías saber la respuesta ahora.

La clase Compo no tiene un constructor de copia. Esto significa


que el compilador generará un constructor de copia implícito para
la clase. Hasta donde sabemos, este constructor copia los objetos
poco a poco. Ahora estamos a punto de descubrir otra
característica interesante del constructor.
Compilamos el código y lo ejecutamos. Se produce la siguiente
salida:

copiando A ...
copiando B ...
A está haciendo algo
B está haciendo algo

Esto significa que, además de su actividad normal (la clonación),


el constructor de copia toma en consideración todos los
constructores de copia existentes (implícitos y explícitos) definidos
423 – By Gerson
dentro de los objetos utilizados para componer la clase. Esto le da
la oportunidad de copiar los componentes de la manera más
adecuada.

Ahora vamos a hacer otro experimento.

source_06_05_11_compo2.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase A {
public :
A ( A & src ) { cout << "copiando A ..." << endl ; }
A ( vacío ) {}
vacío Hacer ( vacío ) { cout << "A está haciendo algo" << endl ; }
};
clase B {
público :
B ( B & src ) { cout << "copiando B ..." << endl ; }
B ( vacío ) {}
vacío Do ( vacío ) { cout << "B está haciendo algo" << endl ; }
};
clase Compo {
public :
Compo ( void ) {} ;
A f1 ;
B f2 ;
};

424 – By Gerson
int main ( void ) {
Compo co1 ;
Compo co2 = co1 ;
co2. f1 . Do () ;
co2. f2 . Do () ;
devuelve 0 ;
}

6.5.12 Composiciones frente a constructores (3)


Hemos modificado la clase Compo ; puede encontrarla aquí →
Ahora tiene su propio constructor de copia. Se invocará
implícitamente una vez durante la vida de nuestro programa en el
momento en que se crea el objeto co2 .
El programa produce el siguiente resultado:

Copiando Compo ...


A está haciendo algo
B está haciendo algo

¿Te sorprende esto? El constructor de copia explícito (escrito por


nosotros) no ha invocado ninguno de los constructores de copia
del componente. Desafortunadamente, definir su propio
constructor de copia significa que asume la responsabilidad total
de todas las actividades necesarias para llevar a cabo una copia
responsable. Esto significa que necesitamos modificar el
constructor. Una forma de hacer esto es agregar una línea como
esta:

Compo (Compo & src): f1 (src.f1), f2 (src.f2) {cout << "Copiando Compo
..." << endl; }

en lugar de

425 – By Gerson
Compo (Compo & src) {cout << "Copiando Compo ..." << endl; }

La solución es correcta a pesar de cómo se ve. El programa


modificado se comporta de la manera que queremos, produciendo
el siguiente resultado:

copiando A ...
copiando B ...
Copiando Compo ...
A está haciendo algo
B está haciendo algo

source_06_05_12_compo3.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase A {
public :
A ( A & src ) { cout << "copiando A ..." << endl ; }
A ( vacío ) {}
vacío Hacer ( vacío ) { cout << "A está haciendo algo" << en
};
clase B {
public :
B ( B & src ) { cout << "copiando B ..." << endl ; }
B ( vacío ) {}

426 – By Gerson
vacío Do ( vacío ) { cout << "B está haciendo algo" << endl ;
};
class Compo {
public :
Compo ( Compo & src ) { cout << "Copiando Compo ..." << e
Compo ( nulo ) {} ;
A f1 ;
B f2 ;
};
int main ( void ) {
Compo co1 ;
Compo co2 = co1 ;
co2. f1 . Do () ;
co2. f2 . Do () ;
volver 0;
}

6.6.1 ¿Qué es una constante de todos modos?

Vamos a dejar atrás los problemas de herencia por ahora y


diremos algunas palabras sobre cómo el "C ++" ve
las constantes . Una constante (en el sentido matemático) es
una entidad que nunca cambia su valor. El mejor ejemplo de tal
constante es, por supuesto, π, cuyo valor (3.1415926535 ...) es
(muy probablemente) el mismo en cada parte de nuestro
universo.
427 – By Gerson
El lenguaje "C ++" utiliza el término "constante" en un
sentido más amplio . Le mostraremos algunos de los
significados ocultos detrás de la palabra (clave) 'const'.
En general, los compiladores "C ++" no piensan que " const "
significa "invariable". Es más probable que estén convencidos de
que " const " es "inalterable". Hay una diferencia sutil pero
importante entre estas dos palabras.

6.6.2 Variables constantes

La frase "variable constante" suena extraño, ¿no? ¿Es un


oxímoron? Parcialmente si.
Echa un vistazo a las siguientes declaraciones →

size1 y size2 son variables de tipo int y tienen un valor


de 100 . Ambas entidades se comportan como constantes (más
precisamente, como variables de solo lectura ). Tenga en
cuenta que la palabra clave const se encuentra en diferentes
lugares en cada línea. Ambas formas son aceptables .

El compilador protegerá ambas variables de la


modificación . Las siguientes dos líneas causarán errores de
compilación:

tamaño1 ++;
tamaño2 = tamaño1;

Puede usar ambos símbolos (nombres de const) en cualquier lugar


donde pueda usar un literal o una expresión que conste de
literales, como en este ejemplo:

const int size = 100;


int buffer [ tamaño ];

428 – By Gerson
Este fragmento será inválido si elimina la
palabra const ( algunos compiladores permiten la compilación
de este ejemplo , porque
tienen implementada una extensión especial ) .

Tenga en cuenta que no debe declarar una constante sin


inicialización (piense en esto por un momento y lo encontrará
obvio). La siguiente línea provocará un error de compilación:

tamaño int const ;

6.6.3 Agregados constantes

Los agregados (estructuras y matrices, así como las matrices de


estructuras y estructuras de matrices, etc. ) también pueden
declararse como constantes , aunque los efectos son algo
diferentes.
Echa un vistazo al siguiente fragmento →

los puntos y los datos son variables de solo lectura y no


debe modificarlos . Ambas líneas son incorrectas:

- puntos [2];
data.key = 0;

429 – By Gerson
Además, al contrario de los ejemplos anteriores, no puede tratar
estos símbolos como literales. Algunos de los compiladores "C ++"
pueden considerar la siguiente línea como incorrecta:

int array [puntos [2] + data.key];

ya que el compilador puede no ser capaz de determinar el número


de elementos de la matriz durante el tiempo de compilación.

6.6.4 Punteros constantes

Los punteros también pueden ser declarados como constantes,


aunque la sintaxis utilizada para estas declaraciones puede ser un
poco sorprendente.
Echa un vistazo al siguiente fragmento →

Tenga en cuenta que la palabra clave const se coloca después


de * y antes del nombre de la variable ; no olvide esto.
Tanto iptr como cptr no deben modificarse . Esto significa
que las siguientes líneas causarán errores de compilación:

--iptr;
++ cptr;

Las entidades señaladas por los punteros constantes pueden


modificarse sin restricciones. Las siguientes dos líneas serán
aceptadas y realizadas con éxito:

* iptr = 0;
* cptr = 'T';
430 – By Gerson
int arr [ 5 ] = { 1 , 2 , 4 , 8 , 16 } ;
int * const iptr = arr + 2 ;
char * const cptr = "¿Por qué?" ;

6.6.5 Punteros a constantes

Los punteros constantes no son equivalentes para punteros


a constantes ; no lo olvides, es importante.
Echa un vistazo al siguiente fragmento →
Las palabras clave const han cambiado su ubicación y ahora se
ubican al comienzo de las declaraciones. Tenga en cuenta que el
siguiente formulario también es correcto:

int const * iptr = arr + 2;


char const * cptr = "¿Por qué?";

Tanto iptr como cptr pueden modificarse . Las siguientes


líneas son correctas:

--iptr;
++ cptr;

Por el contrario, las entidades señaladas por estos punteros


ya no pueden modificarse . Las siguientes dos líneas no serán
aceptadas:

* iptr = 0;
* cptr = 'T';

431 – By Gerson
int arr [ 5 ] = { 1 , 2 , 4 , 8 , 16 } ;
const int * iptr = arr + 2 ;
const char * cptr = "¿Por qué?" ;

6.6.6 Punteros constantes a constantes

Ambas variantes anteriores se pueden mezclar entre sí


dando un puntero constante a un valor constante .
Echa un vistazo al siguiente fragmento →

Ninguna de las siguientes líneas son correctas en el alcance


de esta declaración :

--iptr;
++ cptr;
* iptr = 0;
* cptr = 'T';

int arr [ 5 ] = { 1 , 2 , 4 , 8 , 16 } ;
const int * const iptr = arr + 2 ;
const char * const cptr = "¿Por qué?" ;

6.6.7 Parámetros de función constante (1)

432 – By Gerson
Parámetros de función constante (pasados por valor)

Cualquiera de los parámetros de función pasados por valor


puede declararse como constante .
Tenga en cuenta que los efectos de estas declaraciones solo son
observables dentro de la función y no tienen impacto en el mundo
exterior.
Echa un vistazo al siguiente fragmento →

La función no debe modificar el parámetro n, aunque (como


probablemente sepa perfectamente) esa modificación no se
propagará fuera de la función de todos modos.
Este fragmento es correcto, ya que la n no se modifica de ninguna
manera dentro de la función.

int fun ( const int n ) {


return n * n ;
}

6.6.8 Parámetros de función constante (2)

Parámetros de función constante (aprobados por referencia)

Cualquiera de los parámetros de función pasados por


referencia puede declararse como constante .
Podemos decir que esta es una forma más fuerte de la
declaración anterior . Podemos entenderlo como
una promesa solemne hecha por la función: no voy a modificar
su parámetro real .
Echa un vistazo al siguiente fragmento →

433 – By Gerson
El fragmento es incorrecto, ya que la función intenta romper la
promesa. El compilador no se comprometerá en esto.

int fun ( const int & n) {


return n ++;
}

6.6.9 Resultados de la función constante

Cualquier función puede declarar su resultado como const .


Este formulario no tiene mucho sentido cuando se aplica a los
valores de los tipos estándar (como int o float ), pero puede ser
esencial cuando se usa con punteros, agregados y objetos.
Declaraciones como estas significan: " Te daré un valor que no
debes cambiar ".

Hay muchas razones para esta advertencia. Una de las más obvias
es que el valor devuelto se encuentra en la memoria de solo
lectura.
Los literales de cadena de estilo C se almacenan comúnmente en
la parte de la memoria en la que no se puede escribir.
Por ejemplo, el siguiente programa (muy probablemente) causará
un desastre menor:

int main ( nulo ) {


char * str = "¡Hola!";
str [5] = '?';
devuelve 0;
}

434 – By Gerson
Pregunta: ¿cuál de las siguientes funciones puede obtener
la variable str como parámetro y garantizar que el desastre no
ocurra?

nulo f1 ( char * p);


nulo f2 ( char * const p);
nulo f3 ( char const * p);

Encontrarás la respuesta en la siguiente diapositiva.

Echa un vistazo al siguiente fragmento →


La función divertida devuelve un puntero a la cadena de estilo
C. Ya sabemos que la cadena es inalterable, por lo que
enfatizamos este hecho a través de la forma de la declaración de
función. En efecto, el resultado de la función puede asignarse
solo a una variable que garantice la seguridad .

Esta línea será rechazada por el compilador:

char * p = diversión ();

Este será aceptado:

const char * str = diversión ();

const char * fun ( void ) {


return "¡Atención!" ;
}

435 – By Gerson
6.6.10 Variables de clase constante

La respuesta prometida es: solo la tercera función (llamada f3 )


garantiza que no va a modificar los datos señalados por el
parámetro.
Cualquier clase puede declarar su campo como constante .
Dicha declaración tiene un significado más débil que cuando se
aplica a una variable regular. Significa que el campo
es inalterable durante la vida del objeto , y nada más.
En particular, diferentes objetos pueden tener estos campos
asignados con diferentes valores. Podemos decir que
la constancia se limita a los límites de un solo objeto .
Un campo de clase const debe inicializarse dentro de una
lista de inicialización dentro de cualquiera de los
constructores de clase . Cualquier otra asignación será
rechazada.

Echa un vistazo a la siguiente declaración de clase →


Todos los constructores inicializan el campo const con un valor
diferente. Todas las inicializaciones son válidas.
Los siguientes fragmentos, insertados dentro de la parte pública
de la Clase , se reconocerán como no válidos:

Clase ( doble f) {campo = f; }


nula diversión ( int n) {campo + = n; }

clase Clase {
privado :
campo int const ;
public :
436 – By Gerson
Class ( int n ) : field ( n ) {} ;
Clase ( Clase & c ) : campo ( 0 ) {} ;
Clase ( nula ) : campo ( 1 ) {} ;
};

6.6.11 Objetos constantes

Estamos muy cerca del final de nuestro viaje por la Tierra de las
Constantes. Se paciente. Constantemente.
Un objeto de cualquier clase puede declararse
como constante .
Esto significa que el objeto no debe modificarse durante su
vida . El compilador protegerá el objeto de cualquier intento de
intentar cambiar su estado. Entonces:

 Está prohibido modificar directamente los campos de cualquier


objeto

 invocando las funciones miembro de cualquier objeto está


prohibido

Echa un vistazo a la siguiente declaración de clase →

Supongamos que tenemos las siguientes declaraciones (todas


válidas):

Clase o1 (1);
437 – By Gerson
const clase o2 (2);
que i;

Como puede ver, los constructores pueden afectar el contenido del


objeto, lo que parece razonable. Sin embargo, las siguientes tres
líneas serán rechazadas:

o2.field = 3;
o2.set (1);
i = o2.get ();

Se considerarán válidos si reemplaza ' o2 ' por ' o1 '.

Tenga en cuenta que no importa si la función miembro modifica el


estado del objeto; invocar la función get () del objeto o2
también está prohibido, aunque la función solo lee el campo del
objeto.

clase Clase {
public :
int field ;
Clase ( int n ) : campo ( n ) {} ;
Clase ( Clase & c ) : campo ( 0 ) {} ;
Clase ( nula ) : campo ( 1 ) {} ;
conjunto vacío ( int n ) { campo = n ; }
int get ( nulo ) { campo de retorno ; }
};

438 – By Gerson
6.6.12 Funciones miembro constantes

Cualquiera de las funciones miembro de la clase puede


declararse como constante .
Esta es una promesa de que la función no modificará el estado del
objeto. La sintaxis de la declaración puede ser sorprendente ya
que la palabra clave const se coloca después de la lista de
parámetros, así:

 nombre de tipo (parámetros) const; en declaraciones

 escriba el nombre (parámetros) const {…} en las definiciones

Echa un vistazo a la siguiente declaración de clase →


¿Puedes ver la diferencia entre esta y la clase discutida
anteriormente?
Sí, la función get se declara como una función const .
En efecto, la siguiente línea se considerará válida:

i = o2.get ();

Nota: el compilador intentará forzar al programa a cumplir la


promesa. No debe modificar ninguna de las variables de clase ni
invocar funciones no constantes dentro de la función get . Estos
serán reconocidos como errores.

clase Clase {
public :
int field ;
Clase ( int n ) : campo ( n ) {} ;
Clase ( Clase & c ) : campo ( 0 ) {} ;
439 – By Gerson
Clase ( nula ) : campo ( 1 ) {} ;
conjunto vacío ( int n ) { campo = n ; }
int get ( nulo ) const { campo de retorno ; }
};

6.7.1 ¿Amigo o enemigo?

Los amigos difieren de otras personas en el hecho de que tienen


acceso a muchos aspectos de nuestro mundo
privado . Conocen nuestros secretos, sentimientos, contraseñas
de cuentas de correo electrónico o incluso PIN de tarjetas de
crédito. Esto es posible porque confiamos en ellos.
Hasta ahora, hemos visto a las clases como criaturas muy
cautelosas e incrédulas. Tienen muchas cosas privadas que son
inaccesibles para otros. Ellos vigilan sus posesiones para
mantenerlos protegidos .

Puede parecernos que las clases son cuerpos pobres, solitarios y


ligeramente paranoicos. Afortunadamente, esto no es cierto. Las
clases también pueden tener amigos. Te lo contaremos ahora.
Un amigo de una clase puede ser:

 una clase (se llama la clase de amigo )


 una función (se llama la función de amigo )

Un amigo (clase de función) puede acceder a esos componentes


ocultos a otros. Los amigos pueden acceder o usar
componentes privados y protegidos de la clase .
Una amistad en este sentido es como un permiso y una expresión
de confianza.

6.7.2 ¿Cómo decirlo?

440 – By Gerson
Si hay dos clases llamadas, por ejemplo, A y B , y si A quiere
que B pueda acceder a sus posesiones privadas, tiene que
anunciar que B es su amigo. El anuncio funciona solo en una
dirección. La clase B no puede decir simplemente: " Soy el amigo
de A ", no funcionará.
Tenga en cuenta que es como en la palabra real. Nuestros amigos
son aquellos a quienes nos referimos como amigos. Las personas
que afirman que son nuestros amigos pueden ser mentirosas.

Echa un vistazo al ejemplo →


Una clase llamada Class ha anunciado que Friend es su amigo. En
efecto, un objeto de la clase Friend puede manipular los campos
privados de la Clase e invocar sus métodos privados.
Tenga en cuenta que no importa dónde agregue la
declaración de amistad , es decir, la línea que comienza con la
frase:

clase de amigo ...;

puede existir dentro de cualquiera de las partes de la clase


(pública, privada o protegida), pero debe colocarse fuera de
cualquier función o agregado.

Este programa genera:

Es un secreto, ese campo = 100

source_06_07_02_friendclass.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {

441 – By Gerson
amigo clase Amigo ;
privado :
campo int ;
void print ( void ) { cout << "Es un secreto, ese campo =" << campo << endl ;
};
class Friend {
public :
void DoIt ( Class & c ) { c. campo = 100 ; C. print () ; }
};
int main ( void ) {
Clase o ;
Amigo f ;

f. DoIt ( o ) ;
devuelve 0 ;
}

6.7.3 Las reglas

Hay algunas reglas adicionales que deben tenerse en cuenta:

 una clase puede ser amiga de muchas clases

 una clase puede tener muchos amigos

 el amigo de un amigo no es mi amigo

 la amistad no se hereda : la subclase debe definir sus propias


amistades

Considere el siguiente ejemplo →


442 – By Gerson
Intenta predecir su salida.
Sí, es:

Es un secreto, ese campo = 111

source_06_07_03_friendclass2.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase A {
amigo clase B ;
amigo clase C ;
privado :
campo int ;
protegido :
vacío de impresión ( vacío ) { cout << "Es un secreto, ese campo =" << campo
};
clase C {
public :
void DoIt ( A & a ) { a. print () ; }
};
clase B {
public :
void DoIt ( A y a, C y c ) { a. campo = 111 ; C. DoIt ( a ) ; }
};
int main ( nulo ) {
Aa;Bb;Cc;

443 – By Gerson
si. DoIt ( a, c ) ;
devuelve 0 ;
}

6.7.4 Funciones de amigo

Una función también puede ser amiga de una clase . Dicha


función puede acceder a todos los componentes privados y / o
protegidos de la clase.
Las reglas son un poco diferentes a las anteriores:

 una declaración de amistad debe contener un prototipo


completo de la función de amigo (incluido el tipo de
retorno); una función con el mismo nombre, pero incompatible
en el sentido de conformidad de los parámetros, no será
reconocida como amiga

 una clase puede tener muchas funciones de amigo

 una función puede ser amiga de muchas clases

 una clase puede reconocer como amigos funciones tanto


globales como miembros

Echa un vistazo al ejemplo →


La clase A tiene tres amigos. Son:

1. la clase B

2. el general DoIt () función

3. la función miembro dec (de la clase C )

Tenga en cuenta la línea:

444 – By Gerson
clase A;

en la parte superior del archivo fuente. No hace nada excepto


informar al compilador que una clase llamada A estará en uso. La
falta de línea causará errores de compilación ya que el compilador
no se dará cuenta de la existencia de la clase A cuando analice
el cuerpo de la clase C.

No resolverá el problema moviendo la clase A a la parte superior


del archivo fuente. ¿Sabes por qué?
El programa de ejemplo escribe:

Es un secreto, ese campo = 99

source_06_07_04_friendfunc.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase A ;
clase C {
public :
void dec ( A & a ) ;
};
clase A {
amigo clase B ;
amigo vacío C :: dec ( A & ) ;
amigo vacío DoIt ( A & ) ;
privado :
campo int ;

445 – By Gerson
protegida :
vacío de impresión ( void ) { cout << "Es un secreto, ese campo =" << campo
};
nulo C :: dec ( A y a ) { a. campo - ; }
clase B {
public :
void DoIt ( A & a ) { a. print () ; }
};
nulo DoIt ( A y a ) {
a. campo = 99 ;
}
int main ( void ) {
Aa;Bb;Cc;

DoIt ( a ) ;
b. DoIt ( a ) ;
devuelve 0 ;
}

7.1.1 ¿Cómo meterse en problemas? (1)

Podemos decir (aunque suene muy fatalista) que no hay código


libre de errores . Una de las leyes de Murphy incluso establece
que un código libre de errores es en realidad solo uno que no se
ha probado suficientemente.
Los errores provienen de todas partes, y algunos de ellos son
causados por humanos, los creadores del código, así como sus
usuarios. El primero escribe código erróneo, el último ingresa
datos erróneos. Los resultados pueden ser ruinosos en ambos
446 – By Gerson
casos. El factor humano es obviamente muy importante, pero hay
muchas otras razones que pueden causar errores fatales: mal
funcionamiento del hardware, fallas de transmisión, problemas de
alimentación y muchos otros.
Es casi imposible predecir todos los errores posibles (e imposibles)
y proteger su programa contra todos los males de nuestro
mundo. Obviamente, debe intentar hacerlo en los casos más
comunes , pero debe aceptar el hecho desagradable de que no
se pueden predecir todos los peligros.

Echa un vistazo al ejemplo →


El código no es realmente complejo, pero la experiencia nos
enseña que incluso un código simple puede tener consecuencias
desastrosas. La función del código es muy simple: obtiene dos
valores flotantes e imprime su cociente.
Es difícil imaginar algo más simple.

Compile y ejecute el programa e ingrese "0" (cero) como el


segundo valor. ¿Qué pasa entonces?

El código se cancela, ya que la división por cero no es


posible en un mundo de números reales .
La reacción real de su computadora puede variar: a veces será
solo un mensaje breve, pero algunos entornos pueden ser más
comunicativos y producir informes de diagnóstico estrictos y
precisos. Esperemos que su máquina no se levante contra
usted. Pase lo que pase, el código terminará su vida útil y no podrá
cumplir sus funciones hasta que lo inicie nuevamente.
¡Inténtalo tú mismo!

source_07_01_01_division.cpp

#include <iostream>

447 – By Gerson
usando el espacio de nombres estándar ;

int main ( void ) {


float a, b ;
cin >> a ;
cin >> b ;
cout << a / b << endl ;
devuelve 0 ;
}

7.1.2 ¿Cómo meterse en problemas? (2)

Algunos podrían decir: "¡Oh, no es un problema!" El programa


termina su trabajo inmediatamente después de la división, por lo
que no importa si muestra el resultado correcto o no. Tienes que
lanzarlo de nuevo a pesar de los accidentes que ocurren mientras
tanto.
Desafortunadamente, la vida no es tan fácil. Cada programa que
sea remotamente más complejo que este debe estar preparado
para procesar más de un conjunto de datos de entrada. Anular un
programa puede significar que los datos restantes no se
procesarán en absoluto.
OK, eso no es motivo de preocupación cuando el programa evalúa
los cocientes, pero se convertirá en un gran problema (¡tu
problema!) Si el programa está diseñado para calcular el interés
de los depósitos bancarios (por cierto, incluidos tus depósitos).

Hemos modificado nuestro programa para hacerlo más similar a


un programa que procesa datos masivos en serie. Ahora está listo
para aceptar pares de valores y calcular sus cocientes en un
bucle. Sí, debe estar preparado para el hecho de que los datos
448 – By Gerson
terminarán eventualmente y el procesamiento debería detenerse
en ese momento.
Eche un vistazo al ejemplo y verá cómo lo hicimos →

Queremos llamar su atención sobre la forma en que el código


reconoce el hecho de que el usuario no desea ingresar más
datos. El evento en el que no hay más datos para procesar se
llama " Fin del archivo " o simplemente EOF .
EOF puede ser causado por circunstancias físicas (el final real de
un archivo de disco que contiene datos), pero también puede ser
provocado por un usuario que ingresa datos desde un teclado.
En el mundo Unix / Linux, la combinación de teclas comúnmente
conocida como ^ D [ Control-D ] está diseñada para generar el
EOF. En el mundo de Windows®, se asigna lo mismo al acceso
directo ^ Z [ Control-Z ] (aunque ambos métodos difieren
ligeramente en su funcionamiento). Asumimos que el usuario que
desea que el programa se detenga ingresa uno de estos "atajos
sagrados" y el programa se detiene.
¿Cómo podemos saber que EOF ha
sucedido? La secuencia cin junto con el operador " >> " devuelve
un valor útil (pero comúnmente ignorado), una referencia a sí
mismo, lo que nos permite construir declaraciones más complejas
como esta:

cin >> a >> b;

Hay una excepción: si no hay datos en la secuencia de


entrada o si los datos no son válidos,
la secuencia cin devuelve una referencia nula , que se evalúa
como falsa en contextos booleanos. Es por eso que colocamos la
primera de las operaciones cin dentro de la condición del bucle
while.

source_07_01_02_divisionloop.cpp

449 – By Gerson
#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {


float a, b ;
mientras que ( cin >> a ) {
cin >> b ;
cout << a / b << endl ;
}
devuelve 0 ;
}

7.1.3 ¿Cómo meterse en problemas? (3)

Algunos (tal vez usted) podrían decir: “¡Oh, eso no es un


problema! ¿Por qué no realiza una comprobación simple antes de
comenzar la evaluación? Simplemente compare
la variable b con 0 y cuando la comparación sea verdadera omita
la división y emita un mensaje sarcástico. ¡Pedazo de pastel!
Bueno. Pongamos la idea en práctica. Es fácil en
realidad. Una declaración if hará todo el trabajo. Tal vez el
mensaje no sea lo suficientemente sarcástico, pero tal vez sea
satisfactorio para fines educativos.
El código está aquí →

450 – By Gerson
¿Salvamos al mundo? Realmente no. Trate de imaginar que
tenemos que evaluar fórmulas más complejas que una sola
división.
Por ejemplo, queremos construir un código con docenas de
expresiones que incluyan no solo divisiones, sino también
invocaciones de funciones sensibles al punto de un dominio de los
argumentos, como sqrt () , que no le gustan los negativos, o tan
() , que es impotente contra argumentos que son múltiplos de 90
grados.

¿Tenemos que agregar un si alrededor de cada operación riesgosa


como lo hicimos en este ejemplo? ¿Te imaginas lo complicado que
sería ese código?
"¿Desconcertado?", Dice alguien. "De ningún modo. Podemos
separar la parte riesgosa del problema y ponerlo dentro de la
función. El resultado de la función será un mensaje que indique si
la evaluación se realizó sin problemas ".
Eso suena bastante bien. Intentemos codificarlo.

source_07_01_03_safedivloop.cpp

#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {


float a, b ;
mientras que ( cin >> a ) {
cin >> b ;
if ( b! = 0.0 )
cout << a / b << endl ;

451 – By Gerson
más
cout << "¿Me estás tomando el pelo?" << endl ;
}
devuelve 0 ;
}

7.1.4 ¿Cómo meterse en problemas? (4)

Eche un vistazo al programa y observe la función div () →

La función simboliza cualquier evaluación compleja que puede no


ser exitosa. La idea es simple: cuando la función descubre que
hay un problema con los argumentos o con los resultados
intermedios, sale inmediatamente y devuelve falso como
resultado .
Un resultado igual a verdadero significa que la evaluación fue
exitosa. Pero tenemos que pagar por ello: no podemos usar la
función para propagar el resultado del cálculo, ya que lo usamos
para diferentes propósitos.
Complica la interfaz, pero se nos permite usar la función,
ignorando su resultado, cuando estamos absolutamente seguros
de que nuestros datos son correctos y no causarán ningún error.

Desafortunadamente, la creencia de que las cosas siempre


salen bien es muy común entre los programadores
novatos . Lleva tiempo aprender que todo puede salir
mal. Incluso una operación simple como imprimir un valor en la
pantalla puede fallar.
No queremos convertirlo en un codificador paranoico que
desperdicia la mayor parte del tiempo del proyecto buscando
riesgos e implementando protecciones. Aquí también se debe
encontrar una media dorada.

452 – By Gerson
Bien, perdón por esta breve digresión. Es hora de volver a nuestro
código. La función tiene tres parámetros: el primero se utiliza para
devolver el resultado de la función, mientras que los otros
transmiten valores de argumento a la función. Claro, simple y
elegante.
De Verdad?

source_07_01_04_safedivfun.cpp

#include <iostream>
usando el espacio de nombres estándar ;
bool div ( float & res, float arg1, float arg2 ) {
if ( arg2 == 0.0 )
return false ;
res = arg1 / arg2 ;
volver verdadero ;
}
int main ( void ) {
float r, a, b ;
mientras que ( cin >> a ) {
cin >> b ;
if ( div ( r, a, b ))
cout<< r << endl ;
más
cout << "¿Me estás tomando el pelo?" << endl ;
}
devuelve 0 ;
453 – By Gerson
}

7.1.5 ¿Cómo meterse en problemas? (5)

Trate de imaginar que nuestra función (segura) es invocada


muchas veces por otras funciones . Ninguno de ellos quiere
producir ningún mensaje, sarcástico o no. Están en silencio y
hacen su trabajo en silencio. Esto puede significar que las
funciones deberían terminar sus acciones tan pronto como esté
claro que la función div () devuelve falso. También significa que
las funciones deben pasar información sobre un error "superior" a
las funciones que las invocaron.
Observe que la cadena de funciones invocadas puede ser muy
larga. Si solo las funciones de nivel más alto son responsables de
reaccionar a los errores que ocurren en los niveles más
bajos, puede dar lugar a que el código se "hinche" . El swell
contiene el código que no hace más que descubrir errores e
intentar manejarlos.
Hemos mostrado esto en el ejemplo aquí como un pseudocódigo

Durante muchos años, esta filosofía de manejo de errores fue la


única forma de asegurar el código contra los efectos de los
errores. Por supuesto, no hay obstáculo para usar este enfoque en
los programas "C ++", y tal vez sea el método más efectivo en
programas pequeños y simples. De todos modos, "C ++" nos
proporciona un enfoque más conveniente y flexible para el
problema.
Estamos a punto de contarte todo al respecto.

bool low_level_eval ( ... ) {


:
if ( something_went_wrong ) return false ;
:
454 – By Gerson
}
bool middle_level_eval ( ... ) {
:
bool result = low_level_eval ( ... ) ;
si ( ! resultado ) devuelve falso ;
:
}
bool top_level_eval ( ... ) {
:
resultado bool = middle_level_eval ( ... ) ;
si ( ! resultado) devuelve falso ;
:
}
int main ( void ) {
:
bool result = top_level_eval ( ... ) ;
if ( ! result ) {
cout << "Sarcasm!" << endl ;
retorno 1 ;
}
}

7.1.6 ¿Cómo salir de problemas? (1)

Comenzamos con una famosa frase atribuida a Lucius Annaeus


Seneca, un antiguo filósofo romano, pero la oración no estaba
completa. Su forma completa se lee en latín: Errare humanum
est, sed in errare perseverare diabolicum , que se puede traducir
al inglés como Errar es humano, pero persistir en el error es
455 – By Gerson
diabólico . Podemos decir que esta oración, que data de 2000
años, es la esencia de la filosofía de manejo de errores "C ++".

Comencemos con algo de terminología. "C ++" no sabe nada


sobre "errores". Cuando las cosas no salen como se supone, se
llama una "excepción". Esta es una distinción importante, ya que
"excepción" significa más que solo "error". Todos los errores son
excepciones, pero no todas las excepciones son errores.
Cuando algo sale mal en un programa, surge una
excepción. Algunas excepciones surgen automáticamente,
completamente fuera de nuestro control, mientras que otras
pueden crearse a solicitud nuestra.
Una computadora puede descubrir un tipo de fundición incorrecto,
lo que puede traer efectos desfavorables, pero no puede
diagnosticar automáticamente el hecho de que 1024 puede no ser
un año de nacimiento válido para una persona que vive en el siglo
XXI. Este tipo de excepciones deben detectarse y generarse
manualmente.

Una excepción son los datos . ¿Sorprendido? Imagine una


excepción como una caja alada, capaz de volar, que aparece
cuando ocurre algo malo, en el momento en que sucede. El cuadro
contiene datos que pueden ayudar a identificar la razón de la
falla. Los datos pueden ser de cualquier tipo: puede ser un int ,
un flotante , una cadena, un objeto de cualquier clase, lo que
sea. Tendrá que elegir los datos que serán más útiles en su
situación.
La parte del código que puede causar problemas debe
marcarse (en realidad anidarse) dentro de un tipo especial de
bloque. El bloque está destinado a ser observado cuidadosamente
durante su ejecución.
Cuando surge una excepción, la ejecución del bloque
finaliza , pero el programa en sí sigue vivo. La excepción recién
surgida comienza a volar sobre el código (disculpe nuestro
estilo "poético"), llevando los datos dados y la caja vuela hasta el
final del bloque.

456 – By Gerson
Si no hay nadie que quiera atrapar la excepción , continúa
volando al nivel más alto de la cadena de funciones con la
esperanza de que haya alguien allí que quiera atraparla.
Si la excepción no se detecta en el nivel superior (en la función
principal), hará que el programa se detenga y emita un mensaje
de diagnóstico apropiado. Este es el escenario "diabólico".

But the reverse may also happen. The exception is caught by


another part of the code, aware of the current circumstances
and able to serve the exception. The exception may be served fully
and the program may be restored to its normal operation.
The exception may be served partially and the exception may
be thrown away again and fly to another level of execution.
There are many possibilities and many ways to reach a happy
ending.

Let’s add that the act of a new exception arising is described as


‘an exception has been thrown’. We don’t identify who does
the throwing – it doesn’t matter.
The main idea of this concept is that the normal, routine
processing should be separated from the code run in case of an
emergency. These two paths should not cross at all, or at least
should not cross too often.
A coder has a tool enabling her/him to think about one aspect of
the program execution at the same time. The code won’t swell any
more as the emergencies are clearly separated and not mixed with
the processing algorithms. Let’s have a closer look at this.

7.1.7 ¿Cómo salir de problemas? (2)

Hemos dicho que la excepción son los datos. El lenguaje "C ++"
proporciona algunas clases especializadas reunidas en una
estructura jerárquica que refleja las diferentes naturalezas de las
diferentes excepciones. Estas estructuras incluyen todas las
excepciones estándar generadas por funciones reunidas en
bibliotecas estándar.

457 – By Gerson
Esto no significa que tenga que usar solo objetos de estas clases:
puede usar los datos que desee, pero si va a preparar su código
para que funcione con alguna función estándar, debe conocer y
comprender La estructura de la jerarquía.
Los elementos más importantes de la jerarquía están aquí →

La clase superior, llamada excepción , es una base para todas las


excepciones, las predefinidas y las que están definidas por el
usuario. En general, los objetos de esta clase no se crean ni se
lanzan, y la clase solo se usa para definir algunos
comportamientos y propiedades comunes a todas las excepciones.
Dos clases diferentes se derivan de la clase de excepción. El
primero, denominado logic_error , está destinado a representar
excepciones relacionadas con la lógica del programa, es decir, el
algoritmo, su implementación, validez de datos y
cohesión. Podemos decir que este tipo de excepción se lanza a
niveles más altos de abstracción del programa.
La segunda clase, llamada runtime_error , se usa para identificar
excepciones lanzadas debido a accidentes "inesperados" como la
falta de memoria. Usamos la palabra inesperado porque se
convierte en un oxímoron en este contexto: no podemos decir que
un evento es inesperado cuando nos estamos preparando para
servirlo.
Todas estas entidades se definen dentro de la excepción
del archivo de encabezado . Esto significa que la línea

#include <excepción>

puede ser necesario en un código que hace uso de cualquiera de


estas clases.
Todos estos identificadores son accesibles a través del espacio
de nombres estándar .

458 – By Gerson
7.1.8 Anatomía de un objeto de excepción

La clase de excepción es muy modesta. De hecho, define solo tres


componentes:

 un constructor (no muy útil para nosotros porque, como hemos


mencionado antes, los objetos de esta clase no se crean

 un destructor virtual, originalmente vacío

 una función virtual llamada qué devuelve la cadena de estilo C


(un puntero a la matriz de caracteres terminados por el
carácter nulo ('\ 0')

La función what proporciona un texto (más o menos detallado)


que describe la naturaleza y la causa de la excepción. Puede usarlo
para encontrar fuentes más detalladas de la excepción.

7.1.9 ¿Dónde se lanzan las excepciones?

459 – By Gerson
Si desea detectar excepciones, debe marcar la parte del código en
la que pueden ocurrir las excepciones. Para ello, utilice la
instrucción "probar". De hecho, la declaración "probar" realmente
no hace nada, en particular, no cambia la ejecución de su
bloque. Solo activa un observador de excepciones de vuelo
(estamos bromeando, por supuesto).
Como puede ver, el nombre de la declaración es muy ilustrativo:
se lee: intente ejecutar esto y veremos qué sucede.
Una cosa importante: en el lenguaje "C ++" (a diferencia de otros
lenguajes de programación más nuevos), la división por cero no
arroja una excepción . Esto significa que tendremos que tirarlo
por nuestra cuenta.
No hay problema. Podemos hacerlo.

intente {
:
:
:
}

7.1.10 ¿Dónde se capturan las excepciones?

Si está decidido a atrapar alguna de las excepciones de vuelo,


debe colocar la declaración de captura directamente después de
la declaración de prueba . Por cierto, puede haber más de
un enunciado catch , pero queremos discutirlo más tarde, cuando
toda la idea se aclare.
Piense en la instrucción catch como una función sin nombre con
un parámetro que especifica el tipo de excepciones que se desea
capturar. La captura capturará solo aquellas excepciones que
sean compatibles con el tipo especificado.

Si lo escribe de esta manera:


460 – By Gerson
catch (string & anyproblem) {…}

Significará: quiero captar las excepciones que llevan cadenas .

Esta forma:

captura (excepción y otro problema) {…}

significa: voy a atrapar las excepciones que llevan objetos de la


clase de excepción o de cualquier otra clase derivada de la clase
de excepción .

atrapar ( qué ) {
:
:
:
}

7.1.11 ¿Cómo se lanzan las excepciones? (1)

Si desea lanzar una excepción, debe usar la declaración del mismo


nombre. La declaración de lanzamiento requiere datos que serán
"empaquetados" en una excepción antes de su partida.
Si va a enviar un valor int , escribirá algo como esto:

lanzar 997;

Si desea lanzar una excepción equipada con un objeto de cualquier


clase, debe especificar el constructor que se invocará para
preparar los datos, así:

461 – By Gerson
tirar una cuerda ("¡Adiós mundo!");

Parece que ya tenemos todos los bloques que necesitamos para


construir nuestro primer programa utilizando el mecanismo try-
catch . No será particularmente complejo, pero veamos cómo
funcionan las excepciones.

7.1.12 ¿Cómo se lanzan las excepciones? (2)

Comenzaremos escribiendo una función para realizar una división


de coma flotante. La función proporcionará un resultado si los
argumentos son válidos, de lo contrario arrojará una excepción
que contenga una cadena que se queja.
Esta función carece de alguna ornamentación útil; volveremos a
eso más adelante.
Así es como se ve →

float div ( float a, float b ) {


if ( b == 0.0 )
throw string ( "No puedo creer - división por cero :("
devuelve a / b ;
}

7.1.13 ¿Cómo se lanzan las excepciones? (3)

462 – By Gerson
Y aquí va el programa completo →
Ahora, preste atención al cuerpo del bucle while. Observe cómo se
intenta la división y cómo se detecta la excepción.
Compile el programa y ejecútelo. Probar su comportamiento en
diferentes casos.

source_07_01_13_except.cpp

#include <iostream>
usando el espacio de nombres estándar ;
float div ( float a, float b ) {
if ( b == 0.0 )
throw string ( "division by zero :(" ) ;
return a / b ;
}
int main ( void ) {
float a, b ;
while ( cin > > a ) {
try {
cin >> b ;
cout << div ( a, b )<< endl ;
} catch ( string & problem ) {
cout << "¡Mira lo que hiciste, mal usuario!" << endl ;
cout << problema << endl ;
}
}

463 – By Gerson
devuelve 0 ;
}

7.1.14 ¿Cómo no se capturan las excepciones?

Eche un vistazo al programa: lo hemos estropeado para mostrarle


lo que sucede cuando no se detecta la excepción.
¿Puedes ver la diferencia? Hemos cambiado la especificación de
tipo en la instrucción catch (era: string & , ahora es int ).
Ejecute este programa e ingrese cero como el segundo valor.
El programa se cancelará con un mensaje que dice que se ha
lanzado una instancia de una excepción no controlada.
¿Puedes explicar por qué vimos eso?

source_07_01_13_notcaught.cpp

#include <iostream>
usando el espacio de nombres estándar ;
float div ( float a, float b ) {
if ( b == 0.0 )
throw string ( "division by zero :(" ) ;
return a / b ;
}
int main ( void ) {
float a, b ;
while ( cin > > a ) {

464 – By Gerson
try {
cin >> b ;
cout << div ( a, b )<< endl ;
} catch ( int problem ) {
cout << "¡Mira lo que hiciste, mal usuario!" << endl ;
cout << problema << endl ;
}
}
devuelve 0 ;
}

7.2.1 Lanzar y atrapar unidos

Ahora vamos a hablar sobre algunos detalles con respecto a


la instrucción de lanzamiento . Por supuesto, no podemos hablar
de ello sin su compañero, la declaración catch . Sin embargo,
el lanzamiento es el personaje principal de nuestra historia ahora,
aunque prometemos que el próximo episodio estará dedicado
a atrapar la vida y los problemas.
Volvamos a la forma más simple de manejo de excepciones, donde
nuestros dos actores desempeñan sus roles en el mismo
escenario, es decir, dentro de la misma función. Ya has aprendido
cómo funciona. Tome nuestro ejemplo (que se muestra aquí →)
como un recordatorio rápido.

La función DoCalculations contiene cuatro fragmentos "sensibles"


en los que puede ocurrir un error: están conectados a divisiones
que, como todos sabemos perfectamente, a veces pueden salir
mal.
Todas estas divisiones están "protegidas" mediante verificaciones
apropiadas y en caso de un argumento incorrecto, se lanza una

465 – By Gerson
excepción. La excepción está "empaquetada" en un cuadro de
tipo cadena y lleva un mensaje simple que describe el problema.
También hay una rama de captura diseñada para detectar la
excepción y proporcionar un manejo muy simple (y, para ser
honesto, muy ingenuo).

Un hecho importante que queremos enfatizar aquí: la


especificación de excepción colocada en el encabezado de la rama
de captura , por ejemplo, esta:

captura (cadena y exc)

funciona como una declaración de variable local


(automática) . Esto significa que dentro del siguiente fragmento:

int main ( nulo ) {


string str;
prueba {
lanzar cadena ("1");
} catch (string & str) {
cout << str;
}
devuelve 0;
}

Hay dos variables diferentes , llamadas str (la primera está


oculta por la segunda dentro del bloque catch ).

El programa de ejemplo muestra el siguiente texto en la pantalla:

Algo malo sucedió: Arg mal d

466 – By Gerson
Deberías poder explicar esto fácilmente.

source_07_02_01_trycatchtogether.cpp

#include <iostream>
usando el espacio de nombres estándar ;
flotador DoCalculations ( flotar a, float b, flotador c, flotador d ) {
try {
float x ;
if ( a == 0.0 )
arrojar cadena ( "Arg incorrecto" ) ;
x=1/a;
if ( b == 0.0 )
arrojar cadena ( "Arg incorrecto b" ) ;
x/=b;
Si( c == 0.0 )
lanzar cadena ( "Arg incorrecta c" ) ;
x/=c;
if ( d == 0.0 )
arroja una cadena ( "Bad arg d" ) ;
retorno x / d ;
} catch ( string & exc ) {
cout << "Algo malo sucedió:" << exc << endl ;
devuelve 0 ;

467 – By Gerson
}
}
int main ( void ) {
DoCalculations ( 1 , 2 , 3 , 0 ) ;
devuelve 0 ;
}

7.2.2 Lanzar y atrapar separados

Como saben, el lanzamiento y la captura también pueden vivir por s


mecanismo seguirá funcionando de manera efectiva, pero por supuesto
Esto significa que el objeto de excepción puede volar por encima de
para encontrar su propia captura .
Ahora vamos a llevar a cabo un divorcio. Throw permanecerá en su f
la función principal . El resto del código permanecerá sin cambios: aquí

Tenga en cuenta un aspecto interesante: cuando la instrucción throw s


valor útil.
Esto no es un problema ya que la función no volverá al lugar donde e
función y la invocación se rompe irremediablemente cuando comie
Como le mostraremos pronto, esto no se aplica a las variables / objeto
El programa producirá exactamente el mismo resultado que el anterior

source_07_02_02_trycatchseparated.cpp

#include <iostream>
usando el espacio de nombres estándar ;
468 – By Gerson
flotador DoCalculations ( flotar a, float b, flotador c, flo
float x ;
if ( a == 0.0 )
arrojar cadena ( "Arg incorrecto" ) ;
x=1/a;
if ( b == 0.0 )
arrojar cadena ( "Arg incorrecto b" ) ;
x/=b;
si ( c ==0.0 )
lanzar cadena ( "Arg incorrecta c" ) ;
x/=c;
if ( d == 0.0 )
arroja una cadena ( "Bad arg d" ) ;
retorno x / d ;
}
int main ( void ) {
try {
DoCalculations ( 1 , 2 , 3 , 0 ) ;
} catch ( string & exc ) {
cout << "Algo malo sucedió:"<< exc << endl ;
469 – By Gerson
}
devuelve 0 ;
}

7.2.3 Epílogo lanzamiento vs. función (1)

Las ejecuciones de funciones consisten, en general, en tres


fases: prólogo (cuando se crean todas las variables / objetos
automáticos), ejecución (cuando se realiza el código de función)
y epílogo (cuando se destruyen las entidades creadas
previamente).
Necesitamos hacerle una pregunta muy importante: ¿qué
sucederá cuando la instrucción throw se ejecute dentro de la
función?
Sabemos que la función no volverá después de los pasos pero, en
cierta medida, irá por atajos, omitiendo la finalización
normal. ¿Esto significa que también se omite el epílogo?
Afortunadamente no . Se realizarán todas las limpiezas deseadas,
dando al programa la oportunidad de recuperar su funcionamiento
normal en el futuro más cercano.
Echa un vistazo al ejemplo →

Esta es una rutina, código claro, sin ninguna instrucción


de lanzamiento . Lo usamos para demostrar cómo el epílogo
puede manifestar sus efectos, aunque normalmente funciona en
silencio sin molestar a nadie.
Hay una clase simple en el código equipada con un constructor y
un destructor. No hacen nada excepto enviar mensajes cortos que
prueban que se invocaron las funciones.
470 – By Gerson
El programa producirá el siguiente resultado:

Objeto construido
El objeto dice: hola
Objeto destruido

source_07_02_03_prologueepilogue.cpp

#include <iostream>
usando el espacio de nombres estándar ;
class Class {
public :
Class ( void ) { cout << "Objeto construido" << endl ; }
~ Class ( void ) { cout << "Objeto destruido" << endl ; }
void Hello ( void ) { cout << "El objeto dice: hola" << endl ; }
};

flotante DoCalculations ( void ) {


objeto de clase ;
objeto. Hola () ;
retorno 0.0 ;
}

471 – By Gerson
int main ( void ) {
DoCalculations () ;
devuelve 0 ;
}

7.2.4 Epílogo lanzamiento vs. función (2)

Aquí está el programa modificado →


Hemos agregado tres instrucciones de lanzamiento dentro de
la función DoCalculations . Ahora podemos controlar con precisión
(utilizando un valor de parámetro) cuál se ejecutará. Tenga en
cuenta que el primero se coloca al comienzo de la función, antes
de crear el objeto, el segundo en el medio y el tercero al final de
la función.
La definición de clase sigue siendo la misma.
La función principal invocará DoCalculations tres veces y
podremos observar el comportamiento de la función.

El programa genera el siguiente texto:

-------
fatal 1
-------
Objeto construido
Objeto destruido
fatal 2
-------
Objeto construido
El objeto dice: hola
Objeto destruido

472 – By Gerson
fatal 3

En el primer caso, no se crearon objetos durante la ejecución de


la función (interrumpida), ya que el lanzamiento se produjo antes
de la línea donde se declara el objeto . No hay rastro de actividad
de constructor o destructor. En los dos casos restantes, tanto el
constructor como el destructor nos demostraron que funcionaban,
aunque la función no alcanzó su final normal.
son muy buenas noticias. Ahora podemos usar throw sin temor a
causar pérdidas de memoria.

source_07_02_04_prologueepiloguethrow.cpp

incluir <iostream>
usando el espacio de nombres estándar ;
class Class {
public :
Class ( void ) { cout << "Objeto construido" << endl ; }
~ Class ( void ) { cout << "Objeto destruido" << endl ; }
void Hello ( void ) { cout << "El objeto dice: hola" << endl ; }
};
){
if ( i == 0 )
arrojar cadena ( "fatal 1" ) ;
Objeto de clase ;
if ( i == 1 )
arrojar cadena ( "fatal 2" ) ;
objeto. Hola () ;
if ( i == 2 )
arrojar cadena ( "fatal 3" ) ;
}

473 – By Gerson
int main ( void ) {
for ( int i = 0 ;i < 3 ; i ++ ) {
try {
cout << "-------" << endl ;
DoCalculations ( i ) ;
} catch ( string & exc ) {
cout << exc << endl ;
}
}
devuelve 0 ;
}

7.2.5 Lanzar y los objetos que arroja

Hemos dicho anteriormente que la instrucción throw está


obligada a arrojar un valor, por ejemplo, un objeto; no puede
arrojar nada (suena razonable, ¿no?).
Hemos utilizado objetos de cadena en ejemplos anteriores, pero
ahora vamos a demostrar que throw puede lanzar cualquier
objeto de cualquier clase accesible . También sabemos que el
tipo de objeto lanzado puede usarse para elegir la captura que
será responsable de manejar la excepción, pero lo dejaremos por
ahora; describiremos este aspecto en la siguiente sección.

En este ejemplo aquí → usamos la misma clase en dos roles: una


vez como datos que están siendo "procesados" por
la función DoCalculations y una vez como un contenedor de
excepción . ¡Estas dos aplicaciones no tienen nada en común!
Esto no significa que deseamos establecer una nueva forma de
devolver los resultados de la función. Por el contrario, queremos
mostrar que estos dos caminos (rutina y excepcional) van
completamente por separado.

Tenga en cuenta que ejecutar una línea como esta:


474 – By Gerson
tirar Clase ("excepción 1");

provocará la creación de un nuevo objeto de clase Clase.


Esto significa que se invocará al constructor apropiado antes
de que la función termine su vida.
No significa que se invoque al destructor antes de que la función
termine su vida. El objeto estará vivo incluso después del epílogo
de la función. Debe vivir hasta que encuentre
su captura objetivo y morirá solo cuando la captura termine su
trabajo. No antes

Este programa produce el siguiente resultado:

-------
Objeto [excepción 1] construido
Atrapado!
excepción 1
Objeto [excepción 1] destruido
-------
Objeto [objeto] construido
Objeto [excepción 2] construido
Objeto [objeto] destruido
Atrapado!
excepción 2
Objeto [excepción 2] destruido
-------
Objeto [objeto] construido
Objeto [objeto] dice: hola
Objeto [excepción 3] construido
Objeto [objeto] destruido
Atrapado!

475 – By Gerson
excepción 3
Objeto [excepción 3] destruido

Analice con mucho cuidado las tres rutas de ejecución del


programa y rastree el destino de los objetos " excepción n ".

source_07_02_05_throwobj.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) { cout << "Objeto [" << msg << "] construido" <<
~ Class ( void ) { cout << "Objeto [" << msg << "] destruido" << endl ;
"Object [" << msg << "] dice: hola" << endl ; }
};
anular DoCalculations ( int i ) {
if ( i == 0 )
throw Class ( "excepción 1" ) ;
Objeto de clase ( "objeto" ) ;
if ( i == 1 )
arrojar clase ( "excepción 2" ) ;
objeto. Hola () ;
if ( i == 2 )
arrojar clase ( "excepción 3" ) ;
}
int main ( void ) {
476 – By Gerson
for ( int i = 0 ; i < 3 ; i ++ ) {
try {
cout << "-------" << endl ;
DoCalculations ( i ) ;
} catch ( Class & exc ) {
cout << "¡Atrapado!" << endl ;
cout << exc. msg << endl ;
}
}
devuelve 0 ;
}

7.2.6 Lanzar y cómo podemos averiguarlo

¿Cómo podemos saber si una función (como la llamada función en este


Por supuesto, podemos aprenderlo leyendo el cuerpo de la función, pe
vale la pena considerar:

1. La función puede ser muy larga y compleja: leerla puede llevar mu


lanzamiento

2. El código fuente de la función puede ser inaccesible; puede suceder


compilado archivos (binarios) que contienen solo código ejecutable y
pero no los cuerpos .

Por supuesto, cuando mira nuestro ejemplo, inmediatamente nota cuán


No será tan fácil cuando la función tenga cientos de líneas de código. D
Y así es, aunque no lo hayamos mencionado hasta ahora. Lo haremos a
Por cierto, el programa de ejemplo mostrará "¡Atrapado!" En la pantalla

477 – By Gerson
source_07_02_06_nothrowspec.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( int i ) {
throw Class ( "objeto" ) ;
}
int main ( void ) {
try {
function ( 1 ) ;
} catch ( Class & exc ) {
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

478 – By Gerson
7.2.7 Lanzamiento y su especificación (1)

Una función, que genera una excepción, puede (pero no


tiene que) especificar los tipos de las entidades que se
lanzan.
Hay una sintaxis especial para declaraciones codificadas como
parte del encabezado de una función. Esto significa que podría
usarse incluso cuando el cuerpo de la función es inaccesible
u oculto .
Permite al programador anunciar todas las excepciones que
pueden abandonar la función y, por lo tanto, preparar a otros
programadores para eventos que pueden ocurrir durante la
ejecución de la función.

Hay más de una forma de especificación: la más simple se ve así:

tirar (x)

Esto significa que la función produce un tipo de excepción ,


de tipo x , por ejemplo:

función vacía (vacío) throw (string);

La forma más compleja se ve así:

tirar (x1, x2, .., xn)

Esto significa que la función arroja n excepciones


diferentes de los tipos x1 , x2 , ..., xn respectivamente, por
ejemplo:

int doit (int i) throw (int, string, Class);


479 – By Gerson
Esta función puede generar excepciones de
tipo int , string y Class .

La última forma se ve así:

lanzar()

y significa " la función no arroja ninguna excepción ".


Ahora puede aprender todo sobre las excepciones lanzadas por
una función en particular de un vistazo. La especificación
de lanzamiento hace que la vida del programador sea mucho
más fácil. Úsalo.

Mira el ejemplo →
Hemos agregado la especificación de lanzamiento a nuestra
función.
But, apart from providing information. what does the specification
actually change? It’s a kind of promise. The program declares that
the function won’t throw objects of types other than those
specified. What’ll happen when the programmer doesn’t keep the
promise?
We’ll answer that question soon, but now we should also add that
the lack of specification means nothing. A function without the
specification may throw something or nothing at all.
Now we want to draw your attention to an additional interesting
aspect: note that the throw keyword has two different meanings.
Be aware!

source_07_02_07_throwspec.cpp

#include <iostream>

480 – By Gerson
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( vacío ) throw ( Class ) {
throw Class ( "objeto" ) ;
}
int main ( void ) {
try {
function () ;
} catch (Class & exc ) {
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

7.2.8 Lanzamiento y su especificación (2)

Ahora veremos qué les sucede a los malos programadores que no


cumplen su palabra.
Mira el ejemplo →

La función especifica que arroja excepciones de tipo Class ,


aunque en realidad arroja cadenas .
¿Qué pasará entonces?

481 – By Gerson
¿Compilacion? Va bien : sin problemas, sin errores, sin
advertencias.
¿Ejecución? Houston, tenemos un problema: el programa ha
sido interrumpido y ha aparecido un mensaje. Dice que
hubo una excepción no detectada del tipo ' std :: string ' .
Oh sí, está claro. No hay rama de captura para este tipo de
excepción. ¡Tenemos que reparar nuestro código!

source_07_02_08_badthrowspec.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( vacío ) throw ( Class ) {
throw string ( "object" ) ;
}
int main ( void ) {
try {
function () ;
} catch ( Class & exc ) {
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

7.2.9 Lanzamiento y su especificación (3)


482 – By Gerson
Aquí está el programa corregido →
Desafortunadamente, la corrección no ha corregido nuestro
problema en absoluto. Las cosas siguen yendo mal exactamente
de la misma manera, y vemos el mismo mensaje malo en la
pantalla.
Así es como funciona la especificación throw: no tiene ningún
efecto durante la compilación, pero hace que el programa finalice
si no se cumple la promesa.
Así que ahora nuestro programa necesita otra corrección.

source_07_02_09_badthrowspec2.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( vacío ) throw ( Class ) {
throw string ( "object" ) ;
}
int main ( void ) {
try {
function () ;
} catch ( string & exc ) {

483 – By Gerson
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

7.2.10 Lanzamiento y su especificación (4)

Así es como hemos salvado nuestra reputación →


La promesa se cumple y el programa finalmente hace su trabajo
con éxito.
Hay otro experimento por realizar: cómo funciona la especificación
vacía.

source_07_02_10_goodthrowspec.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( vacío ) throw ( string ) {
throw string ( "objeto" ) ;
}
int main ( void ) {

484 – By Gerson
try {
function () ;
} catch ( string & exc ) {
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

7.2.11 Lanzamiento y su especificación (5)

Puede encontrar el código relevante aquí →


Compilar y ejecutarlo. ¿Los resultados son los esperados?

source_07_02_11_emptythrowspec.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
485 – By Gerson
función vacía ( vacío ) throw () {
throw string ( "objeto" ) ;
}
int main ( void ) {
try {
function () ;
} catch ( string & exc ) {
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

7.2.12 Lanzamiento y su especificación (6)

Ahora te mostraremos algo más complejo. Echa un vistazo al


código →
La función arroja dos excepciones diferentes (esto es lo que dice
la especificación), pero las excepciones de diferentes tipos
se manejan de manera diferente .
Tenga en cuenta que hay otra función ( nivel denominado ) que
ha creado una plataforma intermedia entre la función y
la principal .
La función de nivel captura excepciones de tipo cadena (los
objetos de esta clase terminan sus vidas allí),
pero las excepciones de clase vuelan a la función principal y
quedan atrapadas allí.

Es una ilustración muy simple del concepto que dice que


el manejo de excepciones puede distribuirse entre
486 – By Gerson
diferentes partes del programa . Puede manejar sus
excepciones en los lugares más adecuados y no necesita
recolectar todas las capturas en una función o módulo.
Queremos que respondas una pregunta: mira, no
hay lanzamiento dentro de la función de nivel, ¿verdad?
Entonces, ¿por qué hay una especificación de lanzamiento en el
encabezado? ¿Es obligatorio?

source_07_02_12_doubledthrowspec.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( int i ) throw ( string, Class ) {
switch ( i ) {
case 0 : throw string ( "string" ) ;
caso 1 : clase de lanzamiento ( "objeto" ) ;
predeterminado : cout << "OK" << endl ;
}
}
nivel vacío ( int i ) throw ( Class ) {
try {
function ( i ) ;
} catch ( string & exc ) {

487 – By Gerson
cout << "String [" << exc << "] atrapado en el nivel ()" << endl ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 2 ; i ++ ) {
cout << "-------" << endl ;
pruebe {
nivel ( i ) ;
} catch ( Class & exc ) {
cout << "Object [" << exc. msg << "] capturado en main ()" << endl ;
}
}
devuelve 0 ;
}

7.2.13 Manejo de excepciones inesperadas

Sí, es obligatorio
¿Por qué? Porque las excepciones de tipo Class en realidad se
lanzan desde la función de nivel . No importa que sean arrojados
desde el cuerpo de otra función. La especificación debe contener
todas las excepciones que abandonan la función ,
independientemente de su procedencia.

Deberíamos decir algunas palabras sobre el mecanismo


responsable de manejar excepciones no especificadas. En primer
lugar, las excepciones incompatibles con la especificación se
llaman excepciones inesperadas (esto tiene sentido,
¿verdad?).
Si aparece alguna excepción inesperada, se invoca una
función de tiempo de ejecución especial . Esta es la función

488 – By Gerson
que finaliza el programa y emite el mensaje de diagnóstico
que ya hemos leído algunas veces. Su nombre es inesperado () .
No puede modificar la función (es parte del entorno estándar),
pero puede especificar su propia función si desea realizar algunas
limpiezas adicionales antes de que el programa termine su vida
útil.
Su función se invocará al comienzo de la ejecución inesperada de
la función () .
Recuerde: no puede "guardar" su programa de esta manera: su
destino está sellado y el programa finalizará de todos modos.

Si realmente necesita hacer algo durante las últimas respiraciones


del programa, debe:

 codificar una función sin parámetros de tipo void

 invoque una función llamada set_unexpected , pasándole el


nombre de su función

Echa un vistazo al ejemplo →


El encabezado de la función contiene una especificación con una
lista de excepciones vacía. La función no debería lanzar ninguna
excepción, pero lo hace. Será castigado (ya sabes cómo).
También hay una función llamada last_chance () . Lo usaremos
para decir nuestras últimas palabras antes de que finalice el
programa.

La primera línea de la función principal configura el entorno, y


ahora estamos listos para ejecutar el programa. La salida se verá
así:

¡Mira lo que has hecho! ¡Has lanzado una excepción ilegal!

y luego, algunas otras líneas de mensajes de diagnóstico estándar.

489 – By Gerson
Este mecanismo puede ser muy útil, pero no intente usarlo si no
está seguro de lo que está haciendo. Puede lograr el mismo
objetivo de una manera más simple (muy probablemente).

source_07_02_13_unexpect.cpp

#include <iostream>
#include <string>

usando el espacio de nombres std ;


clase Clase {
public :
string msg ;
Clase ( string txt ) : msg ( txt ) {}
};
función vacía ( vacío ) throw () {
throw string ( "objeto" ) ;
}
void lastchance ( void ) {
cout << "¡Mira lo que has hecho! ¡Has lanzado una excepción ilegal!" << endl ;
}
int main ( void ) {
set_unexpected ( lastchance ) ;
intente {
function () ;
} catch ( string & exc ) {
cout << "¡Atrapado!" << endl ;
}
devuelve 0 ;
}

490 – By Gerson
7.3.1 Jerarquía de clases de excepción

Hemos dicho anteriormente que cualquier objeto de cualquier


clase (o incluso el valor de cualquier tipo básico) puede usarse
como contenedor para un evento. A pesar de esto, existe una
jerarquía predefinida de clases diseñada especialmente para este
propósito.
¿Te acuerdas de esta foto? → Se lo mostramos en nuestra primera
reunión dedicada a las excepciones. Ahora vamos a profundizar en
este tema.

Puede preguntar sobre la justificación: ¿por qué necesitamos


categorizar así? ¿Es realmente útil? ¿Es efectivo?
Sí lo es. Nos permite construir un mecanismo universal y
extensible para manejar excepciones.
A veces podemos querer identificar con precisión todas las
posibles causas de nuestros problemas. Y a veces es suficiente
para nosotros saber las condiciones generales que pueden causar
la excepción.
Ambos enfoques pueden coexistir dentro del mismo programa, por
ejemplo, en diferentes niveles de la lógica del programa. Es por
eso que dicha jerarquía puede simplificar nuestro código.
Además, la jerarquía puede ampliarse de muchas maneras, lo que
nos permite adaptar este esquema a nuestras necesidades
específicas.

Pero antes de comenzar a discutir la jerarquía, necesitamos decir


algunas palabras sobre una nueva palabra clave que aún no
hemos mencionado.

491 – By Gerson
7.3.2 La palabra clave 'explícita'

La palabra clave explícita se puede colocar delante de la


declaración de constructor de una clase. Protege al constructor de
ser utilizado en cualquier contexto que requiera el uso de
conversiones implícitas. Este constructor solo puede usarse de
manera explícita ; por lo tanto, la palabra clave se usa cuando
un programador quiere evitar cualquier efecto secundario que
pueda resultar del uso imprudente de los constructores
seleccionados, o de todos los constructores.

Echa un vistazo al ejemplo →


Hay dos clases muy similares, llamadas A y B ,
respectivamente. Ambos son tontos, de hecho, no hacen nada en
absoluto, pero son adecuados para nuestros propósitos
didácticos. Como puede ver, son casi idénticos. La única diferencia
proviene de la declaración del constructor: la clase A hace uso de
la palabra clave explícita, mientras que la clase B no.
En efecto, la siguiente línea, ubicada dentro de la función
principal, causará un error de compilación :

A a = 1;

mientras que la siguiente línea, la que dice:

B b = 1;

será considerado OK

Como sabe, la sintaxis utilizada en ambas líneas se supone


implícitamente como codificada de la siguiente manera:

492 – By Gerson
A a (1);

pero cuando se usa la palabra clave explícita, esta presunción es


ilegal.

Tenga en cuenta que la siguiente función también sería incorrecta:

Una diversión ( nula ) { return 0; }

mientras que este no:

B fun ( nulo ) { return 0; }

¿Puedes explicar porque?

class A {
public :
explícito A ( int ) {}
};

clase B {
public :
B ( int ) {}
};
int main ( void ) {
A a = 1 ; // ¡error de compilación!

493 – By Gerson
Bb=1;
devuelve 0 ;
}

7.3.3 La clase 'excepción'

La clase de excepción es una base (o una raíz ) para todas las


demás excepciones predefinidas. Contiene una función
llamada what , que está diseñada para proporcionar un puntero a
la llamada cadena de estilo "C" (una secuencia de caracteres
terminada con un carácter nulo ) que describe la naturaleza de
la excepción .
Tenga en cuenta que la forma exacta de este mensaje depende de
la implementación y diferentes compiladores pueden
nombrar los mismos eventos de diferentes maneras .

El programa de ejemplo aquí → ilustra el funcionamiento de ese


mecanismo. Intentamos realizar una conversión de texto
ilegal y utilizamos el objeto de excepción capturado por
la instrucción catch para demostrar el mensaje que podemos
recibir.
Tenemos "[Bad dynamic_cast!]" ... ¿y tú?

source_07_03_03_exception.cpp

#include <iostream>
#include <excepción>
usando el espacio de nombres estándar ;

494 – By Gerson
clase A {
public :
virtual void f ( void ) {}
};

clase AA : public A {
public :
void aa ( void ) {} ;
};

int main ( nulo ) {


Aa;
pruebe {
dynamic_cast <AA &> ( a ). aa () ;
} catch ( excepción ex ) {
cout << "[" << ex. what () << "]" << endl ;
}
devuelve 0 ;
}

7.3.4 La clase 'logic_error'

excepción ← error_lógico

La clase logic_error se deriva directamente de la clase


de excepción .
Está diseñado para representar todas las excepciones causadas
por una violación de las reglas impuestas por la lógica del

495 – By Gerson
algoritmo / programa . Se puede (pero no siempre) hacen que
las excepciones de este tipo son evitables , es decir, que no
sucederá si todos los datos procesados son válidos. También
significa que este tipo de excepción puede predecirse
estáticamente analizando la estructura del código fuente.

El constructor de la clase nos permite "empaquetar" un mensaje


detallado dentro del objeto de excepción.
Tenga en cuenta que si va a utilizar alguna de las excepciones
mencionadas en esta sección, debe incluir el archivo de
encabezado llamado stdexcept .
Por lo tanto, la siguiente directiva es obligatoria en un código
que hace uso de estas clases:

#include <stdexcept>

error_lógico de clase : excepción pública {


público :
error_lógico explícito ( const string & what_arg ) ;
}

7.3.5 La clase 'domain_error'

excepción ← error_lógico ← error_dominio

La clase domain_error se deriva de la clase logic_error . Está


diseñado para representar todas las excepciones causadas por
los datos que exceden el rango permitido . El término
"dominio" proviene de la terminología matemática y describe un
conjunto de todos los argumentos para los que se define la
función. Usar un valor de 100 para el campo diseñado para
496 – By Gerson
almacenar la edad de un empleado es una buena razón para lanzar
tal excepción.

clase domain_error : pública logic_error {


público :
explícita domain_error ( const string & what_arg ) ;
};

7.3.6 La clase 'invalid_argument'

excepción ← error_lógico ← argumento_no válido

La clase invalid_argument se deriva de la clase logic_error . Está


diseñado para representar todas las excepciones causadas
por pasar argumentos inadecuados a métodos, funciones o
constructores .
Pasar una cadena que contiene solo letras a una función que
espera una fecha codificada en formato ISO8601 puede
considerarse una razón para lanzar este tipo de excepción.

clase invalid_argument : public logic_error {


public :
explicit invalid_argument ( const string & what_arg ) ;
};

7.3.7 La clase 'length_error'

excepción ← error_lógico ← longitud_error

497 – By Gerson
La clase length_error se deriva de la clase logic_error . Está
diseñado para representar todas las excepciones causadas por
el uso de valores ilegales para especificar el tamaño /
longitud de los agregados de datos .
Puede esperar este tipo de excepción cuando intenta extender una
cadena a una longitud inaceptable.

class length_error : public logic_error {


public :
explicit length_error ( const string & what_arg ) ;
};

7.3.8 La clase 'out_of_range'

excepción ← error_lógico ← fuera del rango

La clase out_of_range se deriva de la clase logic_error . Está


diseñado para representar excepciones relacionadas con el uso
de índices / claves ilegales al acceder a colecciones de
datos numerados / codificados .
Si tiene una colección de 111 sellos postales, el acceso a los datos
del sello 112 debería generar esta excepción.

clase out_of_range : public logic_error {


public :
explícito out_of_range ( const string & what_arg ) ;
};

7.3.9 La clase 'runtime_error'


498 – By Gerson
excepción ← runtime_error

La clase runtime_error se deriva directamente de la clase


de excepción . Está diseñado para representar todas las
excepciones causadas por circunstancias que pueden ocurrir
durante la ejecución del programa .
Las excepciones de este tipo generalmente no se
pueden evitar, es decir, pueden ocurrir incluso si toda la
estructura del código es correcta. También significa que este tipo
de excepción no puede predecirse estáticamente.

clase runtime_error : excepción pública {


public :
explícita runtime_error ( const string & what_arg ) ;
}

7.3.10 La clase 'range_error'

excepción ← runtime_error ← range_error

La clase range_error se deriva de la clase runtime_error . Está


diseñado para representar excepciones causadas por
la obtención de resultados de cálculo que exceden el rango
permitido .

Tenga en cuenta la diferencia: una excepción de este tipo no se


aplica a los valores de los argumentos, sino a los resultados
obtenidos de argumentos válidos.

clase range_error : public runtime_error {


public :
explicit range_error ( const string & what_arg ) ;

499 – By Gerson
};

7.3.11 La clase 'overflow_error'

excepción ← runtime_error ← overflow_error

La clase overflow_error se deriva de la clase runtime_error . Está


diseñado para representar excepciones causadas por
la obtención de resultados demasiado grandes para
representar cualquier valor útil (en el sentido del dominio).

overflow_error de clase : public runtime_error {


public :
overflow_error explícito ( const string & what_arg ) ;
};

7.3.12 La clase 'underflow_error'

excepción ← runtime_error ← underflow_error

La clase underflow_error se deriva de


la clase runtime_error . Está diseñado para representar
excepciones causadas por la obtención de resultados
demasiado pequeños para representar cualquier valor
útil (en el sentido del dominio).

clase underflow_error : public runtime_error {


public :
explícito underflow_error ( const string & what_arg ) ;
};

500 – By Gerson
7.3.13 ¿Qué sigue?

La jerarquía que acabamos de mostrar puede usarse como base


para construir su propia estructura, reflejando sus necesidades y
circunstancias específicas. Puede extender la estructura (en
realidad, el árbol) en cualquier dirección para que sea más
profunda y / o más ancha si desea definir sus propias clases de
excepciones, o para refinar las clases existentes para hacerlas más
precisas.
Por ejemplo, si desea crear una categoría especializada de
excepciones diseñada para distinguir una clase muy específica de
errores de subdesbordamiento, puede hacerlo de esta manera →

7.3.14 bad_alloc

Además de estas clases, hay una serie de excepciones derivadas


directamente de la clase de excepción . Están sangrados para
representar fallas más generales relacionadas con las actividades
del programa que se refieren a operaciones de bajo nivel.
La excepción bad_alloc puede generarse como un efecto no
deseado de invocar a los operadores nuevos o nuevos [] cuando
el tiempo de ejecución o el sistema operativo no pueden cumplir
con nuestros requisitos de memoria.

7.3.15 mala_excepción (1)


501 – By Gerson
excepción ← mala_excepción

La excepción bad_exception se produce cuando una función


intenta generar una excepción no especificada dentro de su
especificación throw.
Tenga en cuenta que esta excepción no se puede detectar
directamente. El programa aquí → no muestra "Es tan malo ..."
o "Listo", ni siquiera mensajes "Obtuve el doble" .

source_07_03_15_badexc.cpp

#include <iostream>
#include <excepción>
usando el espacio de nombres estándar ;
función vacía ( vacío ) throw ( int ) {
throw 3.14 ;
}
int main ( void ) {
try {
function () ;
} catch ( double f ) {
cout << "Got double" << endl ;
} catch ( bad_exception bad ) {
cout << "Es tan malo ..." << endl ;
}
cout << "Listo" << endl ;
devuelve 0 ;
}

7.3.16 mala_excepción (2)


502 – By Gerson
El manejo adecuado de la excepción bad_exception requiere que
la función especifique bad_exception en su lista de
lanzamiento (parece una paradoja pero es cierta), y la
función del controlador inesperado debe definirse y
establecerse . El incumplimiento de cualquiera de estas
condiciones dará como resultado un comportamiento no deseado
del programa.
El programa aquí → mostrará las siguientes líneas en la pantalla:

¡Una excepción inesperada llegó!


Es muy malo...
Hecho

Tenga en cuenta la declaración de lanzamiento vacía dentro de


la función inexp . Esta forma no significa que la función no arroje
nada; por el contrario, significa que la excepción recibida por la
función se vuelve a generar .
Volveremos a este problema pronto.

source_07_03_16_badexc2.cpp

#include <iostream>
#include <excepción>
usando el espacio de nombres estándar ;
void unexp ( void ) {
cout << "¡Llegó una excepción inesperada!" << endl ;
tirar ;
}
función void ( void ) throw ( int , bad_exception ) {

503 – By Gerson
throw 3.14 ;
}
int main ( void ) {
set_unexpected ( unexp ) ;
intente {
function () ;
} captura( doble f ) {
cout << "Tiene doble" << endl ;
} catch ( bad_exception bad ) {
cout << "Es tan malo ..." << endl ;
}
cout << "Listo" << endl ;
devuelve 0 ;
}

7.4.1 Diferentes capturas para diferentes propósitos (1)

Como ya has aprendido, se supone que la instrucción catch


"atrapa" los eventos que pasan por el alcance de la
instrucción. También sabe que la captura “captura” solo estas
excepciones que son compatibles en tipo con el encabezado
de captura . Por ejemplo, las siguientes instrucciones:

catch (string excp) {…}

captura excepciones encapsuladas dentro de objetos de


tipo cadena e ignora todas las demás.
Hay una forma especializada de captura que puede capturar
literalmente todas las excepciones pasajeras: se ve así:
504 – By Gerson
captura(…) { … }

pero a diferencia de la forma anterior, no puede identificar el


objeto de excepción ni utilizarlo . Por lo tanto, está destinado
a algunas acciones muy generales, como limpiezas finales, en
lugar de para el manejo sutil y fino de un problema bien
reconocido.
Si un bloque try contiene más de una rama catch , entonces,
como máximo, se puede ejecutar una de ellas. Una excepción
atrapada por una de las ramas deja el bloque try y cualquiera de
sus otras capturas no será molestada.
Este programa → demuestra el funcionamiento de
la rama ... catch . Por cierto, el tri-gráfico "..." se conoce como
puntos suspensivos. De Verdad.

El programa produce el siguiente resultado:

Excepción atrapada!
Excepción atrapada!
Excepción atrapada!

source_07_04_01_ellipsis.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función nula ( int i ) {
switch ( i ) {

505 – By Gerson
case 0 : throw out_of_range ( "0" ) ;
caso 1 : lanzar overflow_error ( "1" ) ;
caso 2 : arrojar domain_error ( "2" ) ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 3 ; i ++ ) {
try {
function ( i ) ;
}
catch ( ... ) {
cout << "¡Excepción capturada!" << endl ;
}
}
devuelve 0 ;
}

7.4.2 Capturas diferentes para diferentes propósitos (2)

Hemos modificado nuestro programa. ¿Puedes ver la


diferencia? Sí, hemos cambiado
el encabezado catch y hemos agregado la " excepción ex " en
lugar de los puntos suspensivos.
Esto significa que la rama puede capturar todas las excepciones
cuyos objetos son compatibles en tipo con la clase
de excepciones . Como ya sabe, todas las excepciones
(excluyendo la última, que es solo una excepción de clase )

506 – By Gerson
lanzadas por una función se derivan de la clase de excepción y,
por lo tanto, son de tipo compatible.
Tenga en cuenta que ahora podemos identificar un objeto,
nombrarlo localmente (como ex ) y hacer uso de sus propiedades
y / o funciones. Invocamos la función qué para averiguar qué
quiere decir el objeto sobre sí mismo.

El programa modificado → produce el siguiente resultado:

Excepción capturada: 0
Excepción capturada: 1
Excepción capturada: 2
Excepción detectada: excepción desconocida

Tenga en cuenta que la forma del último mensaje puede diferir de


un compilador a otro. Diferentes entornos pueden tener diferentes
convenciones de nomenclatura. No se sorprenda si su compilador
le da opiniones diferentes a las nuestras.

fuente_07_04_02_clase

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;

507 – By Gerson
función nula ( int i ) {
switch ( i ) {
case 0 : throw out_of_range ( "0" ) ;
caso 1 : lanzar overflow_error ( "1" ) ;
caso 2 : arrojar domain_error ( "2" ) ;
caso 3 : lanzar excepción () ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 4 ; i ++ ) {
try {
function ( i ) ;
}
catch (except & ex) {
cout<<"Excepción capturada:"<< ej. what()<< endl;
}
}
devuelve 0 ;
}

7.4.3 Capturas diferentes para diferentes propósitos (3)

Si vamos a proporcionar, o si tenemos que hacerlo, diferentes


formas de manejar diferentes excepciones, se nos permite
especificar tantas ramas de captura diferentes como
queramos (o necesitemos). En nuestro ejemplo,

508 – By Gerson
la función arroja cuatro excepciones diferentes y podemos hacer
cuatro ramas de captura : una rama por tipo de excepción.
El programa modificado está aquí →
Actualmente, cada una de las excepciones se maneja por
separado.
El programa genera el siguiente texto:

Fuera de rango: 0
Desbordamiento: 1
Dominio: 2
Excepción: excepción desconocida

La nota que hicimos al final de la diapositiva anterior (sobre los


diferentes gustos del compilador) sigue siendo válida.

source_07_04_03_branches.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función nula ( int i ) {
switch ( i ) {
case 0 : throw out_of_range ( "0" ) ;
caso 1 : lanzar overflow_error ( "1" ) ;
caso 2 : arrojar domain_error ( "2" ) ;
caso 3 : lanzar excepción () ;
}
}

509 – By Gerson
int main ( void ) {
f or ( int i = 0 ; i < 4 ; i ++ ) {
try {
function ( i ) ;
}
catch ( out_of_range & ofr ) {
cout << "Fuera de rango:" << ofr. what () << endl ;
}
catch ( overflow_error & ovf ) {
cout << "Desbordamiento:" << ovf. que ()<< endl ;
}
catch ( domain_error & dmn ) {
cout << "Dominio:" << dmn. what () << endl ;
}
catch ( excepción y ex ) {
cout << "Excepción:" << ex. what () << endl ;
}
}
devuelve 0 ;
}

7.4.4 Capturas diferentes para diferentes propósitos (4)

No hay necesidad de elegir entre "todo o nada". Podemos elegir


selectivamente las excepciones que queremos capturar y manejar
con cuidado, y aquellas que queremos manejar muy brevemente.
510 – By Gerson
El código aquí → mezcla los dos enfoques anteriores: algunas de
las excepciones se capturan individualmente mientras
que otras van a puntos suspensivos .
El programa genera el siguiente texto:

Fuera de rango: 0
Desbordamiento: 1
Dominio: 2
Excepción: excepción desconocida
Algo malo sucedio

Hay una condición importante: la rama de puntos suspensivos, si


existe, debe ser la última rama especificada . De lo contrario,
el compilador producirá un error. ¿Puedes adivinar por qué?

source_07_04_04_brancheselli.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función nula ( int i ) {
switch ( i ) {
case 0 : throw out_of_range ( "0" ) ;
caso 1 : lanzar overflow_error ( "1" ) ;
caso 2 : arrojar domain_error ( "2" ) ;
caso 3 : lanzar excepción () ;
caso 4: tirar "tan mal" ;
}
}
int main ( void ) {
511 – By Gerson
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch ( out_of_range & ofr ) {
cout << "Fuera de rango:" << ofr. what () << endl ;
}
catch ( overflow_error & ovf ) {
cout << "Desbordamiento:" << ovf. que () <<;
}
catch ( domain_error & dmn ) {
cout << "Dominio:" << dmn. what () << endl ;
}
catch ( excepción y ex ) {
cout << "Excepción:" << ex. what () << endl ;
}
catch ( ... ) {
cout << "Algo malo sucedió" << endl ;
}
}
devuelve 0 ;
}

7.4.5 Orden de las ramas de captura (1)

Nuestra última pregunta puede sugerir que el orden de las


ramas de captura es importante . ¿Realmente importa?
La respuesta no es realmente clara: sí y no .
512 – By Gerson
Le mostraremos dos ejemplos que justifican ambas respuestas.

Comenzaremos con la variante "no".


Eche un vistazo al ejemplo: hemos intercambiado dos de las
primeras ramas de captura. ¿Cambia el comportamiento del
programa ?
No, no lo hace . El programa produce exactamente el mismo
resultado que el anterior:

Fuera de rango: 0
Desbordamiento: 1
Dominio: 2
Excepción: excepción desconocida
Algo malo sucedio

source_07_04_05_swap1.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función nula ( int i ) {
switch ( i ) {
case 0 : throw out_of_range ( "0" ) ;
caso 1 : lanzar overflow_error ( "1" ) ;
caso 2 : arrojar domain_error ( "2" ) ;
caso 3 : lanzar excepción () ;
caso 4: tirar "tan mal" ;
}

513 – By Gerson
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch (overflow_error & ovf) {
cout<<"Desbordamiento:"<< ovf. what()<< endl;
}
catch (out_of_range & ofr) {
cout<<"Fuera de rango:"<< ofr. what()<< endl;
}
catch (domain_error & dmn) {
cout<<"Dominio:"<< dmn. what()<< endl;
}
captura (excepción y
"Excepción:" << ej. what () << endl ;
}
catch ( ... ) {
cout << "Algo malo sucedió" << endl ;
}
}
devuelve 0 ;
}

7.4.6 Orden de las ramas de captura (2)

514 – By Gerson
Es hora del próximo cambio. Ahora hemos intercambiado las
ramas de captura primera y cuarta.
¿Cambia el comportamiento del programa?
Si, lo hace!

En primer lugar, debe esperar que el compilador produzca algunos


mensajes de advertencia. No evitará que su programa se compile
con éxito, pero las advertencias sugieren que no todo ha salido
como nos hubiera gustado.
El programa produce esta salida, que justifica las advertencias:

Excepción: 0
Excepción: 1
Excepción: 2
Excepción: excepción desconocida
Algo malo sucedio

¿Que pasó aquí? ¿Por qué el programa cambió su comportamiento


tan radicalmente?
Probablemente ya hayas adivinado la respuesta, ¿no?

source_07_04_06_swap2.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función nula ( int i ) {
switch ( i ) {
case 0 : throw out_of_range ( "0" ) ;

515 – By Gerson
caso 1 : lanzar overflow_error ( "1" ) ;
caso 2 : arrojar domain_error ( "2" ) ;
caso 3 : lanzar excepción () ;
caso 4: tirar "tan mal" ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch (excepción y ex) {
cout<<"Excepción:"<< ex. what()<< endl;
}
catch (out_of_range & ofr) {
cout<<"Fuera de rango:"<< ofr. what()<< endl;
}
catch (overflow_error & ovf) {
cout<<"Desbordamiento:"<< ovf. what()<< endl;
}
catch (domain_error &
"Dominio:" << dmn. what () << endl ;
}
catch ( ... ) {
cout << "Algo malo sucedió" << endl ;
}
}
devuelve 0 ;
}

7.4.7 Orden de las ramas de captura (3)

516 – By Gerson
Cuando la excepción llega a un conjunto de ramas de captura , se elige
de destino.
Esto significa que cuando se coloca un tipo / clase más general an
rama no recibirá ninguna excepción .
Es por eso que el compilador le advierte que el orden de las ramas d
permanentemente en el proceso de selección del controlador de excepc
Esta es también la razón por la cual la rama con la clase de excepció
la clase de excepción .

Tenga en cuenta que, además, no puede especificar más de una rama


como un error.
Ahora le mostraremos lo que sucede cuando no todas las excepciones e
Echa un vistazo al ejemplo →

Como probablemente haya adivinado, la excepción que lleva la excep


una rama de captura que quiera recibirla. ¿Qué pasa entonces?
Desafortunadamente, debe estar preparado para un pequeño desastre.

Excepción: 0
Excepción: 1
Excepción: excepción desconocida
Excepción: 2

pero inmediatamente después de esto, verá algunos mensajes alarman

Debe esperar este tipo de comportamiento cada vez que una excepción

source_07_04_07_hierarchy.cpp

#include <iostream>

517 – By Gerson
#include <excepción>
# include <stdexcept>
using namespace std ;
función vacía ( int i ) {
switch ( i ) {
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;
caso 4 : tirar "tan mal" ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch ( excepción y ex ) {
cout << "Excepción:" << ex. what () << endl ;
}
}
devuelve 0 ;
}

7.4.8 Orden de las ramas de captura (4)

518 – By Gerson
Podemos reparar nuestro programa de una manera muy simple:
todo lo que tenemos que hacer aquí es agregar una rama de
puntos suspensivos, que será responsable de encargarse de todas
las excepciones huérfanas. Por supuesto, no es un remedio para
todos sus problemas con excepciones no controladas. La vida real
crea circunstancias más complicadas.
Nuestro programa reparado produce el siguiente resultado:

Excepción: 0
Excepción: 1
Excepción: excepción desconocida
Excepción: 2
Algo malo sucedio

Y el programa terminará su vida normalmente.

source_07_04_08_hierarchy2.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función vacía ( int i ) {
switch ( i ) {
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;

519 – By Gerson
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;
caso 4 : tirar "tan mal" ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch (excepción y ex) {
cout<<"Excepción:"<< ex. what()<< endl;
}
catch (...) {
cout<<"Algo malo sucedió"<< endl;
}
}
devuelve 0 ;
}

7.4.9 Orden de las ramas de captura (5)


520 – By Gerson
Ahora te vamos a poner a prueba. Las siguientes dos diapositivas
deberían demostrar que comprende el proceso de selección del
controlador de excepciones de destino.
Hemos modificado ligeramente el programa anterior; puede
encontrarlo aquí →
¿Puedes predecir su salida?
Encontrarás la respuesta en la siguiente diapositiva.

source_07_04_09_hierarchy3.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función vacía ( int i ) {
switch ( i ) {
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;
caso 4 : tirar "tan mal" ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch (logic_error & le) {

521 – By Gerson
cout<<"Error lógico:"<< le. what()<< endl;
}
catch (excepción y ex) {
cout<<"Excepción:"<< ex. what()<< endl;
}
catch (...) {
cout<<"Algo malo sucedió"<< endl;
}
}
devuelve 0 ;
}

7.4.10 Orden de las ramas de captura (6)

Su respuesta debería verse así:

Error lógico: 0
Error lógico: 1
Excepción: excepción desconocida
Excepción: 2
Algo malo sucedio

Otra prueba para ti: está aquí → ¿Puedes ver la


diferencia? ¿Puedes predecir la salida del programa?

source_07_04_10_hierarchy4.cpp

522 – By Gerson
#include <iostream>
#include <excepción>
# include <stdexcept>
utilizando espacio de nombres std ;
función vacía ( int i ) {
switch ( i ) {
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;
caso 4: tirar "tan mal" ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
function ( i ) ;
}
catch (logic_error & le) {
cout<<"Error lógico:"<< le. what()<< endl;
}
catch (runtime_error & re) {
cout<<"Error de tiempo de ejecución:"<< re. what()<< endl;
}
catch (excepción y ex) {
cout<<"Excepción:"<< ex. what()<< endl;
}
captura (...
"Algo malo sucedió" << endl ;
}
}
devuelve 0 ;
}

523 – By Gerson
7.4.11 Compartir la responsabilidad (1)

La respuesta es:

Error lógico: 0
Error lógico: 1
Excepción: excepción desconocida
Error de tiempo de ejecución: 2
Algo malo sucedio

¿Tu respuesta es la misma?

Ahora haremos que nuestro programa sea más complejo → Hemos


agregado un intermediario entre el principal y la función . El
intermediario maneja algunas de las excepciones pasajeras, es
decir, aquellas compatibles con la clase de excepción . Ahora el
proceso de manejo se dispersa en dos niveles : inferior
(dentro del corredor ) y superior (dentro del principal ).

La salida del programa es la siguiente:

Broker - excepción: 0
Broker - excepción: 1
Broker - excepción: excepción desconocida
Broker - excepción: 2
Algo malo sucedio

Como puede ver, la mayoría de las excepciones han sido


manejadas por el corredor y no han alcanzado la función
principal. Puede usar este método cuando desee dividir la
responsabilidad de manejar diferentes tipos de excepciones
entre las diferentes capas de la lógica del programa.
524 – By Gerson
source_07_04_11_broker.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función vacía ( int i ) {
switch ( i ) {
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;
caso 4 : tirar "tan mal" ;
}
}
corredor vacío ( int i ) {
try { function ( i ) ; }
catch ( excepción ex ) { cout << "Broker - excepción:" << ex. what () << endl ;
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
broker ( i ) ;
}
catch (logic_error & le) {
cout<<"Error lógico:"<< le. what()<< endl;

525 – By Gerson
}
catch (runtime_error & re) {
cout<<"Error de tiempo de ejecución:"<< re. what()<< endl;
}
catch (excepción y ex) {
cout<<"Excepción:"<< ex. what()<< endl;
}
captura (...
"Algo malo sucedió" << endl ;
}
}
devuelve 0 ;
}

7.4.12 Compartir la responsabilidad (2)

El programa de la derecha → verificará su comprensión del


problema. ¿Puedes predecir su salida?
Sí, se verá así:

Broker - error lógico: 0


Broker - error lógico: 1
Excepción: excepción desconocida
Error de tiempo de ejecución: 2
Algo malo sucedio

source_07_04_12_broker2.cpp

526 – By Gerson
#include <stdexcept>

#include <excepción>

#include <iostream>

usando el espacio de nombres estándar ;

función vacía ( int i ) {

switch ( i ) {

case 0 : throw domain_error ( "0" ) ;

caso 1 : throw logic_error ( "1" ) ;

caso 2 : lanzar excepción () ;

caso 3 : arrojar range_error ( "2" ) ;

caso 4 : tirar "tan mal" ;

corredor vacío ( int i ) {

try { function ( i ) ; }

catch ( logic_error & le ) { cout << "Broker - logic_error:" << le. what () << endl ; }

int main ( void ) {

for ( int i = 0 ; i < 5 ; i ++ ) {

try {

broker ( i ) ;

catch ( logic_error & le ) {

cout << "Error lógico:" << le. what () << endl ;

catch ( runtime_error & re ) {

cout << "Error de tiempo de ejecución:" << re. que () <<;

catch ( excepción y ex ) {

cout << "Excepción:" << ex. what () << endl ;

catch ( ... ) {

cout << "Algo malo sucedió" << endl ;

devuelve 0 ;

527 – By Gerson
}

7.4.13 Compartir la responsabilidad (3)

Un corredor mal construido puede arruinar la lógica de manejo de


excepciones en niveles superiores. Echa un vistazo al código de
ejemplo →
El corredor decidió tomar el control de todas las excepciones que
llegan. Ninguno de ellos dejará el corredor. Podemos decir que
"todo lo que llega al corredor permanece en el corredor".

El resultado del programa no es muy variado: así es como se ve:

El corredor barrió los problemas debajo de la alfombra


El corredor barrió los problemas debajo de la alfombra
El corredor barrió los problemas debajo de la alfombra
El corredor barrió los problemas debajo de la alfombra
El corredor barrió los problemas debajo de la alfombra

source_07_04_13_broker3.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función vacía ( int i ) {
switch ( i ) {
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;

528 – By Gerson
caso 4 :tirar "tan mal" ;
}
}
corredor vacío ( int i ) {
try {
function ( i ) ;
}
catch (...) {
cout<<"El corredor barrió los problemas debajo de la alfombra"<< endl;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
broker ( i ) ;
}
catch (logic_error & le) {
cout<<"Error lógico:"<< le. what()<< endl;
}
catch (runtime_error & re) {
cout<<"Error de tiempo de ejecución:"<< re. what()<< endl;
}
catch (excepción y ex) {
cout<<"Excepción:"<< ex. what()<< endl;
}
captura (...
"Algo malo sucedió" << endl ;
}
}
devuelve 0 ;
}

529 – By Gerson
7.4.14 Compartir la responsabilidad (4)

La responsabilidad de manejar las excepciones no solo


puede dividirse , sino que también puede compartirse . Esto
significa que el manejo de las mismas excepciones puede
proporcionarse en más de un nivel .
Tenga en cuenta que cualquiera de las ramas de captura
también puede generar una excepción, y la excepción no se
manejará en el lugar donde se creó, sino en un nivel superior.

Echa un vistazo al código →


El corredor intenta atrapar todas las excepciones (¡y tiene éxito!),
Pero inmediatamente las vuelve a lanzar utilizando la instrucción
de lanzamiento sin argumentos , lo que significa:

lanzar la misma excepción que acabas de recibir

Esta es la única forma posible de volver a lanzar una excepción en


la rama de puntos suspensivos donde la excepción capturada es
completamente anónima y, por lo tanto, imperceptible.
Por supuesto, este formulario puede usarse en otros tipos de
capturas, pero no existen obstáculos para usar variantes menos
anónimas, como esta:

captura (excepción ex) {


tirar ex;
}

Tenga en cuenta que puede lanzar otra (nueva) excepción en lugar


de lanzar la excepción recibida. Esta podría ser una buena idea
cuando desee cambiar la categoría de la excepción.
530 – By Gerson
Aquí hay un ejemplo:

catch (logic_error err) {


lanzar "Tenemos un problema";
}

El programa de ejemplo produce el siguiente resultado:

El corredor barrió los problemas debajo de la alfombra


Error lógico: 0
El corredor barrió los problemas debajo de la alfombra
Error lógico: 1
El corredor barrió los problemas debajo de la alfombra
Excepción: excepción desconocida
El corredor barrió los problemas debajo de la alfombra
Error de tiempo de ejecución: 2
El corredor barrió los problemas debajo de la alfombra
Algo malo sucedio

Es más variado que el anterior, ¿no? Más interesante también,


¿verdad?

source_07_04_14_broker4.cpp

#include <iostream>
#include <excepción>
# include <stdexcept>
using namespace std ;
función vacía ( int i ) {
switch ( i ) {

531 – By Gerson
case 0 : throw domain_error ( "0" ) ;
caso 1 : throw logic_error ( "1" ) ;
caso 2 : lanzar excepción () ;
caso 3 : arrojar range_error ( "2" ) ;
caso 4 : tirar "tan mal" ;
}
}
corredor vacío ( int i ) {
try {
function ( i ) ;
}
catch ( ... ) {
cout << "El corredor barrió los problemas debajo de la alfombra" << endl ;
tirar ;
}
}
int main ( void ) {
for ( int i = 0 ; i < 5 ; i ++ ) {
try {
broker ( i ) ;
}
catch ( logic_error & le ) {
cout << "Error lógico:" << le. what () << endl ;
}
catch ( runtime_error & re ) {
cout << "Error de tiempo de ejecución:" << re. what () << endl ;
}
catch ( excepción y ex ) {
cout << "Excepción:" << ex. what () << endl ;
}
captura ( ...
"Algo malo sucedió" << endl ;
}

532 – By Gerson
}
devuelve 0 ;
}

7.5.1 Apilar de nuevo

¡Nuestro buen viejo amigo, el montón, ha vuelto! Vamos a


reescribirlo para:

 hazlo más confiable y seguro

 mostrar cómo las excepciones pueden mejorar la calidad del


código

 presentar un ejemplo simple de construcción de un módulo


reutilizable independiente

La forma inicial del código está aquí →


Hemos introducido dos enmiendas importantes en comparación
con sus encarnaciones anteriores:

 los valores almacenados en la pila todavía se encuentran dentro


de una matriz, pero el constructor define dinámicamente
el tamaño de la matriz ; tenga en cuenta la especificación de
tamaño de pila predeterminada

 Como consecuencia de la modificación anterior, hemos tenido


que agregar un destructor responsable de eliminar la
matriz al final de la vida útil de la pila.

Una función principal muy simple demuestra que la pila se


comporta correctamente.
Enserio ?
No, en absoluto. Es fácil imaginar lo que sucederá cuando use
demasiados empujones y / o estallidos posteriores. La pila será
completamente impredecible entonces.

533 – By Gerson
Las mejoras son muy necesarias. Pongámonos a trabajar.

source_07_05_01_stack.cpp

class Stack {
private :
int * stackstore ;
int stacksize ;
int SP ;
public :
Stack ( int tamaño = 100 ) ;
~ Pila () ;
vacío push ( valor int ) ;
int pop ( nulo ) ;
};
Pila :: Pila ( tamaño int ) {
stackstore = new int [ tamaño] ;
stacksize = tamaño ;
SP = 0 ;
}
Pila :: ~ Pila ( nula ) {
eliminar [] stackstore ;
}
pila vacía :: push ( valor int ) {
stackstore [ SP ++ ] = value ;
}
int Stack :: pop ( void ) {
return stackstore [ --SP ] ;
}

534 – By Gerson
#include <iostream>
usando el espacio de nombres estándar ;
int main ( void ) {
Stack stk ;
stk. empujar ( 1 ) ;
cout << stk. pop () << endl ;
devuelve 0 ;

}7.5.2 Pila de nuevo - nuevas excepciones

Intentemos identificar todas las sorpresas "malas" que nuestra pila pue
enfrentar en su vida. Podemos ver cuatro de ellos aquí:

1. especificación de tamaño de pila incorrecta (menor o igual a cero)

2. falla en la asignación de memoria para la pila

3. invocar push cuando la pila está llena

4. invocando pop cuando la pila está vacía

Definiremos nuestras propias excepciones para todos estos eventos.


Pensamos eso:

 # 1 será descrito por una nueva excepción derivada de la clase leng

 # 2 será descrito por una nueva excepción derivada de la clase bad_

 # 3 y # 4 serán descritos por una nueva excepción derivada de logic

Puede que no esté de acuerdo, pero creemos que los mejores nombres
para estos son los que están aquí →
Por supuesto, la convención de nomenclatura es solo cuestión de gusto
Puedes nombrarlos de manera diferente ... si crees que tenemos mal g

1. stack_size_error

535 – By Gerson
2. stack_bad_alloc

3. stack_overflow

4. stack_empty

7.5.3 Pila de nuevo - nuevas clases de excepción (1)

Ahora le mostraremos algunas declaraciones de las clases proyectadas.


Nota: declaraciones , no definiciones. Queremos separar estas dos pa
para implementarlas en diferentes archivos de origen.
Aquí están las declaraciones junto con las inclusiones requeridas para u
ompilación exitosa →

Analice los siguientes aspectos del código:

 los nombres de las clases base usadas en cada declaración

 los constructores definidos para cada clase

 el prefijo std :: colocado dentro de las declaraciones; podemos omit

source_07_05_03_newexceptions

#include <iostream>
#include <excepción>
#include <stdexcept>

clase stack_size_error : public std :: length_error {

536 – By Gerson
public :
explícito stack_size_error ( const std :: string & msg ) ;
};
clase stack_bad_alloc : public std :: bad_alloc {
público :
explícito stack_bad_alloc ( void ) ;
};
clase stack_overflow : public std :: logic_error {
public :
explicitstack_overflow ( const std :: string & msg ) ;
};
clase stack_empty : public std :: logic_error {
public :
explicit stack_empty ( const std :: string & msg ) ;
};

7.5.4 Pila de nuevo - nuevas clases de excepción (2)

Aquí están las definiciones correspondientes a las declaraciones anterio


Comprender este código no debería causarte ningún problema. De hech
cada uno de los constructores invoca el constructor de su clase base y
agrega nuevas funcionalidades.

stack_size_error :: stack_size_error ( const std :: string & msg ) : std :: length_error ( msg ) {
};
stack_bad_alloc :: stack_bad_alloc ( vacío ) : std :: bad_alloc () {
};

537 – By Gerson
stack_overflow :: stack_overflow ( const std :: string & msg ) : std :: logic_error ( msg ) {
};
stack_empty :: stack_empty (const std :: string & msg ) : std :: logic_error ( msg ) {
};

7.5.5 Pila de nuevo - nueva declaración de pila (3)

La implementación de la pila también se dividirá. Comenzamos con la


declaración de pila modificada. Tenemos que completarlo con una
especificación de lanzamiento apropiada.
Esperamos que:

 el constructor lanza dos excepciones: stack_size_error y stack_bad_

 la función push genera la excepción s tack_overflow

 la función pop genera la excepción stack_empty

La declaración completa está aquí →

source_07_05_05_newthrows

538 – By Gerson
class Stack {
private :
int * stackstore ;
int stacksize ;
int SP ;
public :
Stack ( int size = 100 ) throw ( stack_size_error, stack_bad_alloc ) ;
~ Pila () ;
vacío push ( valor int ) throw ( stack_overflow ) ;
int pop ( void ) throw ( stack_empty );
};

7.5.6 Pila de nuevo - nuevo constructor


Ahora es el momento de construir todas las definiciones requeridas.
Comenzaremos con el constructor.
Tenemos dos cosas importantes que hacer:

 compruebe si el tamaño inicial de la pila no es demasiado bajo y

 arrojará una excepción en tal caso

 intente asignar memoria para la pila y verifique si fue exitosa;


 vamos a volver a lanzar nuestra propia excepción en tal caso

Este código de constructor mejorado está aquí →

source_07_05_06_construct

Stack :: Stack ( int size ) throw ( stack_size_error, stack_bad_alloc ) {


if ( size <= 0 )
throw stack_size_error ( "el tamaño debe ser> = 0" ) ;

539 – By Gerson
pruebe {
stackstore = new int [ tamaño ] ;
} catch ( std :: bad_alloc ba ) {
throw stack_bad_alloc () ;
}
stacksize = tamaño ;
SP = 0 ;
}
7.5.7. Apilar de nuevo - nuevo empuje

Modificar la función push debería ser fácil. Necesitamos verificar


si el SP no ha excedido su valor máximo permitido ( stacksize - 1)
y lanzaremos un evento en tal caso.
El código push mejorado está aquí →

source_07_05_07_push

pila vacía :: push ( valor int ) throw ( stack_overflow ) {


if ( SP == stacksize )
throw stack_overflow ( "tamaño de pila excedido" ) ;
stackstore [ SP ++ ] = valor ;
}

7.5.8 Pila de nuevo - nuevo pop


El nuevo código pop no requiere nuevos comentarios o explicaciones, ¿

540 – By Gerson
source_07_05_08_pop

int Stack :: pop ( void ) throw ( stack_empty ) {


if ( SP == 0 )
throw stack_empty ( "la pila está vacía" ) ;
devolver el almacén de pilas [ --SP ] ;
}
7.5.9 Pila de nuevo: un archivo de encabezado para un nuevo m

Hemos dicho anteriormente que vamos a construir un nuevo código de


como un " agregado " de dos archivos fuente. Ahora es el momento
cumplir esa promesa.
Primero, escribiremos el archivo de encabezado , un archivo
contiene todas las declaraciones necesarias . El archivo puede
incluido por cualquier otro archivo fuente haciendo uso de nuestra
recién creada. Este es el mismo mecanismo que utilizamos cua
incluimos, por ejemplo, el archivo de encabezado iostreams .
El encabezado no contiene ninguna definición, por lo general, ya que no
necesitan durante la fase de compilación.

Aquí está el archivo de encabezado para nuestra pila →


Lo hemos llamado mystack.h . Asumimos que el archivo con todas
definiciones requeridas se llamará mystack.cpp .
Hay algunas cosas misteriosas dentro del archivo, y vamos a discuti
ahora.

La directiva #ifdef es utilizada por un preprocesador para verificar s


símbolo de tiempo de compilación está definido o no. En nue
ejemplo, el símbolo marcado se llama __MYSTACK__ . Es esencial elegi

541 – By Gerson
nombre único para el proyecto específico. Por lo general, el nombre de
del nombre del encabezado en sí, y hemos seguido esa convención aqu
Si el símbolo no está definido ( ndef ), el preprocesador analizará el re
del archivo o lo omitirá. Tenga en cuenta que no omite todo el conten
del archivo, sino solo la parte anidada en
las directivas #ifndef y #endif .

La siguiente directiva, #define , de


el símbolo __MYSTACK__ . ¿Puedes adivinar para qué sirve? Es sim
cuando el archivo de encabezado se incluye por primera vez, se ana
como un todo y cada línea se tiene en cuenta.
Durante la segunda y posteriores inclusiones, su contenido se ign
porque el preprocesador sabe que __MYSTACK__ ya se ha definido. E
simple truco acelera el proceso de compilación y ayuda al preprocesado
evitar inclusiones múltiples innecesarias del mismo archivo
encabezado .
El encabezado está listo. Es hora de las definiciones e implementacione

mystack.h

#ifndef __MYSTACK__
#define __MYSTACK__

#include <iostream>
#include <excepción>
#include <stdexcept>

clase stack_size_error : public std :: length_error {


public :
explícito stack_size_error ( const std :: string & msg ) ;

542 – By Gerson
};

clase stack_bad_alloc : public std :: bad_alloc {


público :
explícito stack_bad_alloc ( void ) ;
};

clase stack_overflow : public std :: logic_error {


public :
explicit stack_overflow ( const std :: string & msg ) ;
};

clase stack_empty : public std :: logic_error {


public :
explicit stack_empty ( const std :: string & msg ) ;
};

class Stack {
private :
int * stackstore ;
int stacksize ;
int SP ;
public :
Stack ( int size = 100 ) throw ( stack_size_error, stack_bad_alloc ) ;
~ Pila () ;
vacío push ( valor int ) throw ( stack_overflow ) ;
int pop ( void ) throw ( stack_empty );
};
543 – By Gerson
#endif

7.5.10 Pila de nuevo - implementación

Aquí está el contenido de mystack.cpp →


Ya conoce todas sus partes, excepto una línea importante: la primera l
Tenga en cuenta que hemos incluido nuestro encabezado. Es obligator
no lo olvides.

mystack.cpp

#incluye "mystack.h"

stack_size_error :: stack_size_error ( const std :: string & msg ) : std :: length_error ( ms


};

stack_bad_alloc :: stack_bad_alloc ( vacío ) : std :: bad_alloc () {


};

stack_overflow :: stack_overflow ( const std :: string & msg ) : std :: logic_error ( msg ) {
};

stack_empty :: stack_empty ( const std :: string & msg ) : std :: logic_error ( msg ) {
};

Stack :: Stack ( int size ) throw ( stack_size_error, stack_bad_alloc ) {

544 – By Gerson
if ( size <= 0 )
throw stack_size_error ( "el tamaño debe ser> = 0" ) ;
pruebe {
stackstore = new int [ tamaño ] ;
} catch ( std :: bad_alloc ba ) {
throw stack_bad_alloc () ;
}
stacksize = tamaño ;
SP = 0 ;
}

Stack :: ~ Stack ( void ) {


eliminar stackstore ;
}

pila vacía :: push ( valor int ) throw ( stack_overflow ) {


if ( SP == stacksize )
throw stack_overflow ( "tamaño de pila excedido" ) ;
stackstore [ SP ++ ] = valor ;
}

int Stack :: pop ( void ) throw ( stack_empty ) {


if ( SP == 0 )
throw stack_empty ( "la pila está vacía" ) ;
devolver el almacén de pilas [ --SP ] ;
}

545 – By Gerson
7.5.11 Pila de nuevo - función principal

Necesitamos la función principal para ejecutar la pila. Será fácil en com


con el trabajo duro con la nueva implementación de la pila.
La nueva función principal está aquí →

Tenga en cuenta que hemos incluido la directiva #include que se


refiere al archivo de encabezado " mystack.h ".
Así es como el compilador aprende sobre la pila y todos sus componen
así como las excepciones que hemos definido conjuntamente.
El proceso de compilación debería tener el siguiente aspecto.

 el compilador compila el archivo " mystack.cpp " y produce un archiv

objeto (su nombre puede ser diferente en diferentes plataformas;

 algunos compiladores pueden usar " mystack.o ", otros " mystack.ob

no se sorprenda).

 El compilador compila el archivo " main.cpp " y produce un archivo

objeto de un nombre, por ejemplo, " main.obj "

 El vinculador vincula ambos archivos, agregando un código tomado

bibliotecas estándar, y produce un archivo ejecutable al final.

¿Terminamos?
No, no lo estamos. Definitivamente necesitamos hacer algunas pruebas
para demostrar (incluso de forma limitada) que nuestra pila funciona d
manera confiable.

main.cpp

546 – By Gerson
#incluye "mystack.h"
#include <iostream>
usando el espacio de nombres estándar ;
int main ( void ) {
Stack stk ;
stk. empujar ( 1 ) ;
cout << stk. pop () << endl ;
devuelve 0 ;
}
7.5.12 Volver a apilar: nueva y mejor función principal

Aquí están las prendas nuevas del emperador, lo siento, las principales
nuevas →
Ahora estamos preparados para todas las circunstancias
(al menos eso creemos). Todas las operaciones de pila están cubiertas
el bloque try , y tenemos un conjunto completo de capturas listo para
diagnosticar todos los problemas. Hagamos algunas pruebas de choqu

source_07_05_12_main2.cpp

#incluye "mystack.h"
#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {


try {
Stack stk ;

547 – By Gerson
stk. empujar ( 1 ) ;
cout << stk. pop () << endl ;
}
catch ( stack_bad_alloc sba ) {
cout << "No hay espacio para la pila, ¡lo siento!" << endl ;
}
catch ( stack_size_error sse ) {
cout << "Las pilas de ese tamaño no existen, ¡lo siento!" << endl ;
}
catch ( stack_overflow se) {
cout << "La pila es demasiado pequeña para tantos empujes, ¡lo sien
}
catch ( stack_empty su ) {
cout << "La pila está vacía, ¡lo siento!" << endl ;
}
devuelve 0 ;
}

7.5.13 Pila de nuevo - pruebas de choque (1)

Primero, verificaremos si el constructor detecta correctamente los valo


de tamaño de pila que son demasiado bajos →
Ok, funciona bien, tenemos:

No existen pilas de ese tamaño, ¡lo siento!

source_07_05_13_main3.cpp

548 – By Gerson
#incluye "mystack.h"
#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {


try {
Stack stk ( 0 ) ;
stk. empujar ( 1 ) ;
cout << stk. pop () << endl ;
}
catch ( stack_bad_alloc sba ) {
cout << "No hay espacio para la pila, ¡lo siento!" << endl ;
}
catch ( stack_size_error sse ) {
cout << "Las pilas de ese tamaño no existen, ¡lo siento!" << endl ;
}
captura( stack_overflow se ) {
cout << "La pila es demasiado pequeña para tantos empujes, ¡lo sien
}
catch ( stack_empty su ) {
cout << "La pila está vacía, ¡lo siento!" << endl ;
}
devuelve 0 ;
}

549 – By Gerson
7.5.14 Pila de nuevo - pruebas de choque (2)

A continuación, verificaremos si el constructor puede manejar


nuestras
emandas exorbitantes sobre el tamaño de la pila →
El nuestro puede - vemos:

No hay espacio para la pila, ¡lo siento!

source_07_05_14_main4.cpp

#incluye "mystack.h"
#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {


try {
Stack stk ( 2000000000 ) ;
stk. empujar ( 1 ) ;
cout << stk. pop () << endl ;
}
catch ( stack_bad_alloc sba ) {
cout << "No hay espacio para la pila, ¡lo siento!" << endl ;
}
catch ( stack_size_error sse ) {
cout << "Las pilas de ese tamaño no existen, ¡lo siento!" << endl ;
}
catch (stack_overflow se) {

550 – By Gerson
cout<<"La pila es demasiado pequeña para tantos empujes, ¡lo siento!" << endl;
}
catch (stack_empty su) {
cout<<"La pila está vacía, ¡lo siento!" << endl;
}
devuelve 0 ;
}

7.5.15 Pila de nuevo - pruebas de choque (3)

¿Nuestra pila es demasiado a prueba de empuje? →


Sí, lo es, dice:

La pila es demasiado pequeña para tantos empujones, ¡lo siento!

source_07_05_15_main5.cpp

#incluye "mystack.h"
#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {

551 – By Gerson
try {
Stack stk ( 1 ) ;
stk. empujar ( 1 ) ;
stk. empujar ( 2 ) ;
cout << stk. pop () << endl ;
}
catch ( stack_bad_alloc sba ) {
cout << "No hay espacio para la pila, ¡lo siento!" << endl ;
}
catch ( stack_size_error sse ) {
cout <<"No existen pilas de ese tamaño, ¡lo siento!" << endl ;
}
catch ( stack_overflow se ) {
cout << "La pila es demasiado pequeña para tantos empujes, ¡lo siento!" << endl ;
}
catch ( stack_empty su ) {
cout << "La pila está vacía, ¡lo siento!" << endl ;
}
devuelve 0 ;
}

7.5.16 Pila de nuevo - pruebas de choque (4)

¿Y qué hay de demasiados pops? →


Eso también está bien:

La pila está vacía, ¡lo siento!


552 – By Gerson
Nuestra misión está completa. Es hora de descansar.

source_07_05_16_main6.cpp

#incluye "mystack.h"
#include <iostream>

usando el espacio de nombres estándar ;

int main ( void ) {


try {
Stack stk ( 1 ) ;
stk. empujar ( 1 ) ;
cout << stk. pop () << endl ;
cout << stk. pop () << endl ;
}
catch ( stack_bad_alloc sba ) {
cout << "No hay espacio para la pila, ¡lo siento!" << endl ;
}
catch ( stack_size_error sse ) {
cout <<"No existen pilas de ese tamaño, ¡lo siento!" << endl ;
}
catch ( stack_overflow se ) {
cout << "La pila es demasiado pequeña para tantos empujes, ¡lo siento!" << endl ;
}
catch ( stack_empty su ) {
cout << "La pila está vacía, ¡lo siento!" << endl ;
}
devuelve 0 ;
}

553 – By Gerson
8.1.1 Operadores: una mirada al pasado (1)

Un operador es un símbolo diseñado para operar con datos, es


así de simple. El lenguaje "C ++" tiene una amplia gama de
operadores diferentes que operan en muchos tipos diferentes de
datos. Algunos de los operadores son más universales, algunos
son más específicos, algunos de ellos están escritos
como símbolos únicos , algunos son gráficos o
incluso trigrafos , otros son palabras clave .

Todos los operadores de lenguaje "C ++" se dividen en muchos


grupos y hay más de una clasificación para ayudarnos a
clasificarlos. Una de las clasificaciones posibles se basa en una
serie de argumentos. Sabemos que hay:

 operadores unarios

 operadores binarios

 operadores ternarios (para ser sincero, solo hay un operador


ternario en el lenguaje "C ++")

Algunos de los operadores pueden pertenecer a más de uno de los


grupos anteriores según el contexto en el que se utilizan, por
ejemplo, "+" y "&" (y más) pueden funcionar como operadores
unarios y binarios.

Otra clasificación se basa en la ubicación del operador. Nos


distinguimos:

 operadores de prefijo (colocados delante de su argumento)

 operadores de postfix (colocados después de su argumento)

 operadores infijos (colocados entre sus argumentos)

554 – By Gerson
8.1.2 Operadores: una mirada al pasado (2)

Algunos de los operadores de lenguaje "C ++" son, podemos


decir, bastante obvios. Un ejemplo de un operador "obvio" es
"+". Todos entienden el papel de este operador (al menos todos
los que han estudiado matemáticas por un tiempo), cómo funciona
y qué resultados podemos esperar de él.
Hay (al menos) un "pero". ¿Recuerdas cuando hablamos de
cuerdas? ¿Recuerdas cómo funciona el operador "+" cuando se
relaciona con una cadena? Su funcionamiento no es tan obvio allí
y no tiene muchas de sus propiedades de adición tradicionales (es
decir, no es conmutativo).

¿Cómo es posible que el mismo operador pueda tener rasgos tan


diferentes y operar con argumentos tan diferentes? Sabemos
perfectamente que sumar números requiere algoritmos
completamente diferentes de sumar (concatenar) cadenas, y la
única similitud radica en la forma del operador.
Este no es el único caso. Tenga en cuenta que ">>" y "<<" se
encuentran en la misma canasta. Su esfera principal de aplicación
está relacionada con los números integrales (ese fue el primer
papel de estos operadores en el lenguaje "C").
Como sabe, en "C ++" han adquirido un significado adicional y se
usan comúnmente como operadores conectados con operaciones
de entrada / salida de flujo. Nos atrevemos a decir que aparecen
con mucha más frecuencia en el segundo papel que en el primero.
Esto es posible mediante un mecanismo similar a la sobrecarga de
funciones. "C ++" permite al programador no solo sobrecargar
funciones (para asignar una nueva implementación al
nombre de una función ya existente) sino también
sobrecargar operadores .

Afortunadamente, el programador no puede cambiar el significado


del operador existente (por ejemplo, no puede forzar a "+" a
restar ints o flotantes ) pero puede definir nuevos dominios para
ello (por ejemplo, las cadenas son un nuevo dominio para "+") .
En este capítulo, aprenderemos cómo definir y usar algunos de los
nuevos significados de operadores conocidos.
555 – By Gerson
Tenga en cuenta que "C ++" no le permite definir operadores
completamente nuevos (por ejemplo, no puede definir un
operador como este: "$ # $"). Solo puede redefinir cualquiera de
los operadores existentes.

8.1.3 ¿Qué queremos lograr?

Le mostraremos cómo agregar algunos operadores nuevos a una


clase existente, ya que esta es la aplicación principal del
mecanismo de sobrecarga del operador. Intentaremos darle a
nuestra pila nuevas formas de acceder a sus funcionalidades. No
queremos agregarle ninguna operación nueva, solo queremos
crear un nuevo método para invocar las que ya están
implementadas.
Mencionamos los operadores "<<" y ">>" hace una diapositiva y
ahora queremos sobrecargarlos una vez más.

Esta imagen → ilustra nuestros planes. Queremos que "<<" sea


sinónimo de la invocación del método push y queremos que ">>"
desempeñe el papel de una función de miembro pop . ¿Como
hacemos eso?
El lenguaje "C ++" trata a los operadores sobrecargados
como funciones muy específicas . El número de parámetros de
estas funciones debe corresponder al número de argumentos del
operador, pero no es tan simple como podría esperarse (por
ejemplo, una función que implementa un nuevo rol de operador
binario no debe tener dos argumentos).
El nombre de dicha función específica también es específico:
consiste en una palabra clave "operador" pegada a un símbolo de
operador, por ejemplo, una función que implementa el operador
">>" se llamará:

operador >>

No puedes nombrarlo de otra manera.

556 – By Gerson
Una función de operador puede implementarse de dos maneras:

 como función miembro de una clase : se supone


implícitamente que un objeto de esa clase es uno de los
argumentos del operador requerido

 como una función "independiente" - la función debe


especificar explícitamente los tipos de todos sus argumentos

Comencemos con el primer método.

Pila de pila ( 100 ) ;


int var ;

pila << 200 ; // push


stack >> var ; // pop

8.1.4 Implementación del operador << (1)

Primero, definiremos una función de operador para el operador


"<<" (la nueva cara de la función de miembro de inserción ).
Agregaremos un nuevo método de declaración en el archivo de
encabezado: así es como se ve →

Nota:

 el operador debe aceptar diferentes formas de sus


argumentos, como:

 variable, por ejemplo, pila << VAR;

557 – By Gerson
 expresión, p. ej. stack << 2 * VAR;

 literal, por ejemplo, pila << 2;

 etc

esto significa que el parámetro correspondiente de la


función del operador debe pasarse por valor

 el operador debería poder lanzar las mismas excepciones


que la función miembro que usa; por lo tanto, declaramos
la función de la misma manera que la función push

 declaramos que la función del operador es nula , ya que no


queremos que el operador evalúe ningún valor
nuevo; podemos cambiar de opinión en el futuro ... ¿quién
sabe?

Como puede ver, hasta ahora parece bastante simple.

mystack_01.h

558 – By Gerson
.1.5 Implementación del operador << (2)
Implementing the << operator (2)
La definición correspondiente de la función del operador está aquí

Como hemos dicho anteriormente, el objeto de la clase es el
primero de los argumentos del operador (el izquierdo, para
ser precisos), por lo que no tenemos nada más que hacer, excepto
invocar el método push con un valor del segundo operador
(derecho) argumento.

A pesar de estas reservas, la función se comporta con bastante


normalidad y se obedecen todas las reglas generales que se
refieren a las funciones.
mystack_01.cpp

8.1.6 Implementando el operador << (3)


Implementing the << operator (3)

La nueva función principal que hace uso del nuevo operador está
aquí →
El nuevo operador funciona igualmente bien con literales,
expresiones y variables. Compruébalo tú mismo. No confíes en
nuestra palabra .
main_01.cpp
559 – By Gerson
560 – By Gerson
8.1.7 Implementación del operador >> (1)
Implementing the >> operator (1)
En segundo lugar, definiremos el operador para la función de
miembro pop . Tenga en cuenta que hay una diferencia
importante entre el operador anterior y el actual.
No se nos permite (por razones obvias) almacenar un valor
extraído de la pila dentro de un literal o una
expresión . Tenemos que ponerlo en una variable (o para ser
más precisos, en un valor l ).
Por otro lado, un parámetro pasado por valor no puede transferir
datos en ninguna otra dirección que no sea desde el exterior a la
función. Esto no es lo que necesitamos.
Pero es por eso que declaramos el único argumento de la función
como aprobado por referencia.
La declaración completa está aquí →
mystack_02.h

8.1.8 Implementación del operador >> (2)


Implementing the >> operator (2)
Así que ahora debería poder escribir las implementaciones de la
función sin nuestra ayuda, pero para el registro, lo damos aquí

mystack_02.cpp

561 – By Gerson
8.1.9 Implementando el >> operador (3)
Implementing the >> operator (3)

Aquí está la función principal modificada →


Intente utilizar el ">>" ilegalmente, es decir, en conjunción con
una expresión o un literal. No debería funcionar (en cualquier
caso, esperamos que no funcione).
main_02.cpp

8.1.10 Mejora del operador << (1)


Improving the << operator (1)
Echa un vistazo a la diapositiva anterior una vez más.
Hay dos formas diferentes de utilizar el operador " << ": una
implementada por nosotros y otra que obtuvimos de la biblioteca
de secuencias . ¿Puedes ver una sutil diferencia entre ellos?
Los operadores de transmisión pueden estar encadenados de esta
manera:
562 – By Gerson
cout << i << endl;
Nuestros operadores no son tan inteligentes (todavía). No
intentes usarlos así. Provocará una gran cantidad de errores de
compilación.
¿Queremos mejorar a nuestros operadores para que puedan
hacer el mismo truco? Por supuesto lo hacemos. ¿Cómo lo
hacemos?
Tenga en cuenta que la línea que citamos anteriormente es
interpretada por el compilador de la siguiente manera:
(cout << i) << endl;
Esto significa que la expresión dentro de los paréntesis
devuelve una referencia a una secuencia (a saber:
la secuencia cout ) para que pueda usarse (reutilizarse) como
argumento izquierdo para el siguiente operador " << " en una
cadena.
Podemos hacer lo mismo con nuestra pila, pero significa que las
funciones del operador ya no pueden quedar anuladas . Tienen
que devolver un valor como referencia al objeto de pila, el mismo
objeto para el cual los operadores se activaron por primera
vez. Así es como debería verse la declaración →
main_03.cpp

8.1.11 Mejora del operador << (2)


Improving the << operator (2)
Y aquí va la definición mejorada del operador →
Como puede ver, la función devuelve una referencia a su
objeto materno (o tal vez paterno, desafortunadamente, no
sabemos el sexo del objeto).
Usamos el puntero this para ese propósito. ¿Puedes explicar
porque?
main_03.cpp

563 – By Gerson
8.1.12 Mejora del operador << (3)
Improving the << operator (3)
Es hora de algunas pruebas: la nueva función principal está aquí

¡Pruébalo!
main_03.cpp

8.1.13 Mejora del operador << (4)


Improving the << operator (4)
Ahora vamos a mejorar el operador " >> " de la misma
manera. Debería ser facil.
La nueva declaración va primero →
mystack_04.h

564 – By Gerson
8.1.14 Mejora del operador << (5)
Improving the << operator (5)
Y la nueva definición →
mystack_04.cpp

8.1.15 Mejora del operador << (6)


Improving the << operator (6)
La función principal también necesita algunas enmiendas →
¿Funciona? Si lo hace. ¿El pop funciona en el orden
correcto? ¡Inténtalo tú mismo!
mystack_04.cpp

565 – By Gerson
8.1.16 Los mismos efectos de una manera diferente
The same effects in a different way
Implementar operadores sobrecargados como funciones miembro
es fácil cuando eres el "propietario" de la clase y puedes modificar
su código fuente de cualquier manera. Puede ser imposible cuando
el código fuente no está disponible. Por supuesto, en muchos
casos puede ser posible escribir su propia subclase que contenga
todos los operadores deseados, pero esto puede ser inconveniente
en algunos casos.
Afortunadamente, se le permite escribir funciones de
operador fuera de cualquier clase : usar ambos tipos de
operadores es lo mismo, y cuando usa un operador sobrecargado
sin conocer su implementación, no puede adivinar cuál de los
métodos se ha utilizado.
En nuestro caso, una función de operador independiente debe
tener dos argumentos y el orden de los argumentos refleja el
orden de los argumentos del operador.
Tenga cuidado: no todos los operadores pueden estar
sobrecargados de esta manera.
Discutiremos ambos aspectos pronto.

Supongamos que nuestra pila ha vuelto a su estado original y no


contiene ninguna función de operador. No lo
tocaremos. Agregaremos todas las definiciones requeridas al
módulo principal.
La nueva función principal actualizada está aquí →
mystack.cpp , mystack.h , main_05.cpp

566 – By Gerson
8.1.17 Un operador de indexación para la pila (1)
An indexing operator for the stack (1)
Para mostrarle el poder de la sobrecarga del operador, le
mostraremos un ejemplo de implementación de un operador algo
sorprendente. Redefiniremos el significado del operador de
indexación .
Sí, sabemos que está diseñado para matrices o colecciones tipo
matriz y parece que la pila no tiene nada que ver con eso. Pero,
desde cierto punto de vista, una pila es una matriz, una muy
específica, pero sigue siendo una matriz.
Será una indexación muy poco ortodoxa. Estamos de acuerdo: su
usabilidad es muy discutible, pero en nuestra opinión funcionará
perfectamente como conejillo de indias en el laboratorio de
nuestro operador.
Queremos que la indexación funcione de esta extraña manera:
Pila [0] devuelve una referencia al elemento que se encuentra en
la parte superior de la pila

567 – By Gerson
Pila [-1] devuelve una referencia al elemento que se encuentra
debajo de la parte superior de la pila
, etc., etc.
Tenga en cuenta que esta es una referencia , no un valor , ya
que queremos usar el operador dentro de las expresiones de valor
l , por ejemplo, como un argumento izquierdo del operador de
asignación.
Un intento de alcanzar un elemento de pila inexistente provocará
una excepción. Parece que std :: range_error es el mejor
candidato para este propósito.
OKAY. Escribamos una declaración →
Nota:

 la función devuelve un valor de tipo int & como el tipo de


elemento de la pila es int

 la función tiene un argumento: el índice ; lo pasamos por valor


ya que el índice de la matriz no necesita ser una variable;
también puede ser una expresión

mystack_06.h

8.1.18 Un operador de indexación para la pila (2)


An indexing operator for the stack (2)
Aquí está la definición →
Observe cómo encontramos el elemento objetivo y cómo
calculamos su posición dentro de la pila.
mystack_06.cpp

568 – By Gerson
8.1.19 Un operador de indexación para la pila (3)
An indexing operator for the stack (3)
La versión más nueva de la función principal está aquí →
El programa produce el siguiente resultado:
4
1
0
0
Consulte el código fuente y responda la pregunta: ¿es esto lo que
esperábamos? ¿Es la prueba que estamos buscando de que
nuestro operador funciona correctamente?
Tenemos curiosidad por saber lo que piensas. Si tan solo
pudiéramos.
main_06.cpp

569 – By Gerson
8.2.1 Tipos enumerados: ¿por qué los necesitamos? (1)
Enumerated types – why do we need them? (1)

Supongamos que alguien ha configurado una tarea para que la


implementemos: un calendario en línea. Tendremos que crear un
programa capaz de almacenar datos sobre muchos eventos de
diferente naturaleza y coordinarlos de muchas maneras.
Nos hemos dado cuenta de que tendremos que procesar muchas
fechas y tenemos que tener en cuenta no solo las fechas en sí
mismas, sino también los días de la semana correspondientes (ya
que no queremos programar tareas fatigantes para los domingos).
Tendremos que elegir un método para representar los días de la
semana y manipularlos. Por supuesto, podemos usar
el tipo int para ese propósito y asumir que 0 significa
domingo, 1 significa lunes, etc. ¿Es lo suficientemente inteligente?
No lo es.
Permítanos asegurarle que hacer esto le causará algunos
problemas. Usted o cualquiera de sus compañeros de trabajo
olvidará el sistema algún día y no estará seguro de si 5 es viernes
o no. Los dígitos no son buenas asociaciones cuando opera
entre semana y cada tarea no es lo suficientemente obvia ni
clara.
¿Qué pasa con los datos de tipo char ? ¿Podemos usar un solo
carácter para marcar cada día de la semana?
No, tampoco es una buena idea, ya que hay dos días de la semana
con nombres que comienzan con 'S' y dos con nombres que
comienzan con 'T'.
Entonces, ¿qué pasa con el uso de dos personajes? Esto debería
ser inequívoco, ¿verdad?
Sí, pero necesita un tipo de cadena en lugar de un carácter .
Pero las cuerdas son demasiado "pesadas" para un propósito tan
simple: sería como usar artillería contra los mosquitos. Debe
haber algo más fácil y más claro.

570 – By Gerson
8.2.2 Tipos enumerados: ¿por qué los necesitamos? (2)
Enumerated types – why do we need them? (2)

El preprocesador de lenguaje "C ++" ofrece un método para


crear símbolos que serán reemplazados por sus valores
durante el tiempo de compilación.
Estos símbolos se comportan como constantes y no puede cambiar
su valor durante el tiempo de ejecución, por lo que esta podría ser
la forma ideal de representar los días de la semana de una manera
muy clara para los humanos y muy útil para las computadoras.

Echa un vistazo al ejemplo →


La directiva #define tiene la siguiente sintaxis:

#definir cadena de símbolo

dónde:

 el símbolo es un nombre elegido arbitrariamente construido


como el nombre de cualquier variable o función; la convención
no oficial pero respetada dice que el símbolo debe contener
letras mayúsculas solamente, para distinguirse fácilmente de
las variables regulares (nuestros símbolos obedecen a esta
convención)

571 – By Gerson
 la cadena es solo una serie de caracteres

 el preprocesador reemplazará automáticamente cada aparición


del símbolo con la cadena , pero no olvide que este proceso
ocurre solo durante el tiempo de compilación y sus efectos son
temporales: su archivo fuente permanece intacto.

Tenga en cuenta que no debe tratar estos símbolos como una


constante real. Ninguno de los símbolos tiene ningún valor :
siempre se tratan como cadenas. ¡Tenga en cuenta que el
siguiente fragmento hará que a la variable x se le asigne el valor
0, no 2!

#define ALPHA 2-1


#define BETA ALPHA * 2
int x = BETA;

¿Puedes explicar porque?


Intente analizar el fragmento con el primer #define modificado
de la siguiente manera:
#definir ALFA (2-1)
¿Puedes ver la diferencia fundamental? Volvamos a nuestros días
laborables y a los 7 símbolos que elegimos para representarlos.

572 – By Gerson
8.2.3 Tipos enumerados: ¿por qué los necesitamos? (3)
Enumerated types – why do we need them? (3)

Trate de imaginar algunas operaciones de rutina haciendo uso de


nuestros nuevos símbolos. Los hemos recogido aquí →
La primera línea (una declaración junto con una inicialización) se
ve bien. Usamos int como portador para los valores del día de la
semana y parece que esta es la forma más natural de
representarlos. Puede causar un pequeño desperdicio de memoria
porque usar 32 bits para un valor que varía de 0 a 6 no es muy
eficiente (4 bits serían suficientes).
La segunda línea no es tan buena como la primera. Su significado
es claro: queremos pasar al siguiente día de la semana (por
ejemplo, cuando queremos posponer una fiesta o una entrevista),
pero trate de imaginar lo que sucederá cuando big_day sea igual
a 6 ( SÁBADO ). El resultado no será igual a DOMINGO como nos
gustaría. Malas noticias.
La tercera línea puede causar resultados inesperados entonces.
La cuarta línea de ejemplo podría traer un verdadero desastre. El
compilador ni siquiera nos advertirá que el valor de la variable no
pertenece a su dominio natural.
Debe haber algo mejor.

573 – By Gerson
8.2.4 Tipos enumerados: ¿cómo los definimos?
Enumerated types– how do we define them?

La respuesta es un tipo enumerado . Así es como se ve →


Vamos a explicar lo que hemos hecho:

 la palabra clave enum comienza la declaración del tipo

 'día de la semana' es el nombre del nuevo tipo que se está


creando; el nombre del tipo debe obedecer las reglas con
respecto a los nombres en general

 A continuación se incluye una lista de todos los valores que


crean el nuevo tipo , separados por comas y encerrados entre
llaves.

 el compilador asignará implícitamente el valor de 0 al primer


elemento de la lista

 A cualquier símbolo, excepto al primero, se le asignará un


valor mayor en uno que el elemento anterior de la lista.

Esto significa que al símbolo DOMINGO se le asigna el valor


de 0 , LUNES - 1 , MARTES - 2 , etc.

8.2.5 Tipos enumerados: ¿cómo los usamos? (1)


Enumerated types – how do we use them? (1)

El tipo enumerado se trata de una manera muy


específica. Cuando se asigna un valor del tipo a
cualquier valor int , todo es O K y el compilador lo acepta sin
reservas.
La asignación aquí → es completamente válida.
En general, cualquier valor de tipo enum se promueve
implícitamente al tipo int cuando se usa en un contexto que
requiere valores integrales, por ejemplo, cuando se usa junto con
operadores como +, -, etc.
574 – By Gerson
8.2.6 Tipos enumerados: ¿cómo los usamos? (2)
Enumerated types – how do we use them? (2)
Cuando el tipo enumerado desempeña el papel de un valor l , la
situación cambia. Asignarle un valor int (p. Ej., Así) provocará una
advertencia de compilación ya que el compilador reconoce estas
asignaciones como un riesgo potencial para la integridad de los
datos.
Puede que tenga que modificar la asignación de la siguiente
manera:
día de la semana = static_cast <weekday> (0);

o use una forma alternativa de conversión de tipo como esta:

día de la semana f = (día de la semana) 0;


Ambas formas son aceptables en este contexto. ¿Puedes señalar
alguna otra forma de codificar una conversión como esta?
No olvide que en este caso usted asume la plena
responsabilidad de la validez de los datos . El compilador se
lavará las manos de estas tareas (¿los compiladores tienen
manos?).
En general, los valores de tipo enum son ints y pueden usarse
como argumentos en cualquier operación que
acepte ints . Internamente se almacenan igual que
las entradas también. Por ejemplo, la siguiente línea
cout << DOMINGO << endl;
emitirá una línea que contiene '0' (el dígito cero), no la palabra
"DOMINGO". ¿Inconveniente? Si, estamos de
acuerdo. Afortunadamente, podemos cambiarlo con la ayuda de
una pequeña sobrecarga del operador. Te lo mostraremos
pronto.

575 – By Gerson
8.2.7 Tipos enumerados: ¿cómo los usamos? (3)
Enumerated types – how do we use them? (3)

Cualquiera de los elementos de la enum lista tipo puede ser


seguido por el signo '=' y una expresión que resulta en
un int valor. En este caso, al símbolo se le asignará el valor
especificado por la expresión (las reglas predeterminadas se
omiten aquí).
En el ejemplo aquí → el tipo enumerado Símbolos representa los
siguientes valores:
ALFA: -1
BETA: 1
GAMMA: 2

8.2.8 Tipos enumerados: ¿cómo los usamos? (4)


Enumerated types – how do we use them? (4)

Es posible que se haya asignado más de un símbolo


del tipo enum con el mismo valor; en otras palabras, algunos (o
incluso todos) símbolos pueden representar valores idénticos.
En el siguiente ejemplo → los símbolos A y C representan el mismo
valor: 1.
El compilador no tendrá comentarios.

8.2.9 Tipos enumerados: ¿cómo los usamos? (5)


Enumerated types – how do we use them? (5)

Por otro lado, todos los símbolos en la lista deben ser


únicos , incluso si se les asigna el mismo valor.
El ejemplo aquí → no es válido y provocará un error de
compilación.

576 – By Gerson
8.2.10 Tipos enumerados: ¿cómo los usamos? (6)
Enumerated types – how do we use them? (6)

En general, los símbolos de tipo enum deben ser únicos en


un espacio de nombres , es decir, dos tipos
de enumeración diferentes no pueden usar símbolos idénticos.
El ejemplo aquí → también es inválido.

8.2.11 Tipos enumerados: ¿cómo los usamos? (7)


Enumerated types – how do we use them? (7)

Puede evitar este conflicto colocando uno o ambos tipos


de enumeración en conflicto dentro de una clase / clases
separadas, como en el ejemplo →
Podemos decir que ambos CAT ahora viven en mundos diferentes
y no se molestan entre sí.

577 – By Gerson
8.2.12 Tipos enumerados: ¿cómo los usamos? (8)
Enumerated types – how do we use them? (8)

El uso de tipos de enumeración puede protegernos de muchas


amenazas, pero algunas de ellas siguen siendo graves.
Echa un vistazo aquí →
El operador + desconoce los días de la semana y puede pasar
de SÁBADO a literalmente en ninguna parte, dejando el dominio
de tipo permitido.
Pregunta: ¿por qué no podemos usar el siguiente formulario?
d = d + 1;

8.2.13 Tipos enumerados: ¿cómo los usamos? (9)


Enumerated types – how do we use them? (9)

Podemos evitar estos problemas sobrecargando el operador + .


Lo hemos hecho de una manera tan astuta →
Ahora, agregar un int a un día de la semana tendrá en cuenta la
naturaleza cíclica de los días y las semanas, por
ejemplo, DOMINGO + 15 dará como resultado el LUNES .
Tenga en cuenta la siguiente línea:
volver día de la semana ((int (día) + días)% 7);
¿Podemos escribirlo de una forma más simple, como esta?
regresar el día de la semana ((día + días)% 7);
No, no podemos La expresión ' día + días ' invocará a
nuestro operador sobrecargado + recursivamente y el programa
se bloqueará. Tenemos que expresar explícitamente nuestra
voluntad de agregar dos entradas , no un día de la semana y
un int .
578 – By Gerson
8.2.14 Tipos enumerados: ¿cómo los usamos? (10)

Vamos a dar un paso más adelante y equipar nuestro código con


la capacidad de generar los valores de tipo de día de la semana
en una forma legible para humanos.
Haremos esto sobrecargando el operador << stream. No será tan
complejo, solo bastante laborioso.
Aquí está el código completo →
Siéntase libre de experimentar con él. También podría agregar
una implementación sobrecargada
del operador >> stream. Adelante, noqueate.
Intente implementar el operador << de una manera más
inteligente evitando esa instrucción de cambio terriblemente
detallada . Debería ser posible hacerlo más compacto.
¿Te atreverás?

579 – By Gerson
8.3.1 ¡Advertencia!
Warning!

Antes de comenzar una discusión detallada sobre la sobrecarga de


operadores en "C ++", debemos advertirle.
No olvide que los operadores sobrecargados no le brindan
nada más que una forma más elegante de invocar
funciones . No aumentan la eficiencia del código ni su fiabilidad.
Sí, estamos de acuerdo en que
pueden mejorar significativamente la legibilidad del
código . También pueden acortar el código fuente . Esto es
indiscutible, pero no olvide que abusar de esa herramienta puede
ser contraproducente y podría generar mucho tiempo perdido
580 – By Gerson
investigando el significado real de operaciones aparentemente
inequívocas.
No juegues con operadores solo por diversión. No son
juguetes Úselos cuando esté absolutamente seguro de que los
beneficios superarán los riesgos.
Nunca hagas bromas de todos modos. No implemente '+' como
una operación para realizar sustracciones. No es gracioso.

8.3.2 Número de argumentos


Number of arguments

El número de argumentos de la función de operador


sobrecargado está estrictamente restringido y se define con
precisión por el contexto en el que existe la función.
Dos aspectos son decisivos:

 la ubicación en la que se define la función del operador

 el operador sobrecarga

Todos los criterios se han reunido en una tabla aquí →


Tenga en cuenta que en un caso en el que la función de operador
es una función de miembro de clase, el objeto de la clase se
reconoce como uno de los argumentos
requeridos (generalmente el izquierdo).
Hay una excepción a las reglas anteriores. Uno de los
operadores requiere un parámetro que, de hecho, es un
ficticio. Pronto te familiarizarás con eso.

581 – By Gerson
8.3.3 Lo que no debes hacer
What you mustn’t do

No olvides que no tienes permitido:

 definir nuevos operadores (aquellos que no se conocen en


el lenguaje "C ++")

 cambiar la prioridad de los operadores redefinidos

 operadores de sobrecarga que trabajan con tipos de


datos estándar

Ahora le mostraremos todos


los operadores sobrecargables (perdón por el
neologismo). También le mostraremos algunos requisitos y
limitaciones importantes para cada uno de ellos.

582 – By Gerson
8.3.4 Operadores aritméticos
Arithmetic operators

Operadores +-*/%

¿Se puede implementar como


SI
una función global?

¿Se puede implementar como


SI
una función miembro?

Dependiendo del
Tipo de valor de retorno
contexto

Todos los operadores anteriores son binarios .


El ejemplo aquí → ilustra una implementación simple de una clase
que almacena vectores de 2 elementos y puede agregarlos y
encontrar su producto de puntos.
Tenga en cuenta que la primera función devuelve un vector,
mientras que la segunda devuelve un escalar. La primera de las
funciones es una función miembro, y la segunda es global.
El programa produce el siguiente resultado en la pantalla:
(1, 1)
0
Tenga en cuenta que el primero de los operadores recién
definidos puede estar encadenado , por ejemplo, de la
siguiente manera:
v3 = v1 + v2 + v3;
mientras que el segundo no puede ser tratado de la misma
manera, ¿por qué? ¿Puedes explicarlo?
source_08_03_04_arith.cpp

583 – By Gerson
8.3.5 Operadores bit a bit
Bitwise operators

Operadores ^ | & ~ << >>

¿Se puede implementar como una función global? SI

¿Se puede implementar como una función


SI
miembro?

Dependiendo del
Tipo de valor de retorno
contexto

584 – By Gerson
Todos los operadores anteriores, excepto el ~ , son binarios .
Resista la tentación de sobrecargar el operador ^ para una
exponenciación, como en los lenguajes de programación FORTRAN
o PERL y en algunas hojas de cálculo.
La exponenciación real tiene una prioridad muy alta (más alta que
el operador * ), mientras que ^ tiene una prioridad más baja que
otros operadores aritméticos. Puede traer algunos efectos
secundarios inesperados y no deseados.
Hemos reconstruido nuestro ejemplo anterior, puedes encontrarlo
aquí →
Ahora la clase V es capaz de desplazar a la derecha bit a bit cada
uno de sus elementos y evaluar su producto (es un uso bastante
inusual del operador ~ , sí, lo sabemos).
El programa produce el siguiente resultado:
(7, 3)
21
source_08_03_05_bitwise.cpp

585 – By Gerson
8.3.6 Operador de asignación
Assignment operator

Operadores =

¿Se puede implementar como una


NO
función global?

¿Se puede implementar como una


SI
función miembro?

Una referencia a un objeto o un


Tipo de valor de retorno
valor l en general

El comportamiento predeterminado de los operadores = es


bastante simple cuando se aplica a los objetos: simplemente copia
el contenido del objeto campo por campo, haciendo una copia al
carbón del objeto fuente.
Tenga en cuenta que puede ser peligroso si el objeto copiado
contiene punteros a datos asignados dinámicamente, porque
dos o más objetos diferentes pueden hacer uso de datos
compartidos implícitamente. Puede ser útil cuando se hace
intencionalmente, pero también puede ser crítico cuando se hace
por error.
La sobrecarga del operador = puede proteger el objeto de
problemas de esta naturaleza y también puede agregar algunas
funcionalidades, quizás algo complicadas, a tareas simples.
El ejemplo aquí → muestra una asignación que intercambia el
vector que se está asignando: no es muy útil ni divertido, pero es
muy ilustrativo. Eso esperamos, de todos modos.
El programa produce el siguiente resultado:
(8, 4)
Intente adivinar la salida del programa cuando la función principal
toma la siguiente forma:

586 – By Gerson
int main ( nulo ) {
V v1 (4, 8), v2, v3;
v2 = v3 = v1;
cout << "(" << v2.vec [0] << "," << v2.vec [1] << ")" << endl;
devuelve 0;
}
source_08_03_06_assign.cpp

8.3.7 Operadores relacionales


Relational operators

Operadores ==! =>> = <<=

¿Se puede implementar como una función global? SI

¿Se puede implementar como una función miembro? SI

587 – By Gerson
Tipo de valor de retorno booleano

Todos los operadores anteriores son binarios y todos


devuelven un valor booleano que representa la veracidad de la
condición que se evalúa (verifica).
Nota: la sobrecarga de uno de los operadores no tiene
absolutamente ningún impacto en los demás , por ejemplo,
redefinir el significado del operador == no redefine
implícitamente el operador ! = , Y viceversa. La sobrecarga
del operador < sobrecargará automáticamente ni
los operadores > ni el > = .
El programa de ejemplo (puede encontrarlo aquí →) muestra el
comportamiento de dos operadores nuevos: el primero es
bastante obvio (devuelve verdadero cuando ambos vectores
contienen los mismos valores en el mismo orden) mientras que el
segundo es un poco complicado: comprueba la relación "mayor
que" utilizando la suma total de los elementos del vector.
El programa emite el siguiente texto:
falso
cierto
source_08_03_07_relation.cpp

588 – By Gerson
8.3.8 Operadores lógicos
Logical operators

Operadores ! && ||

¿Se puede implementar como una función global? SI

¿Se puede implementar como una función miembro? SI

Tipo de valor de retorno Booleano

Todos los operadores anteriores (excepto ! ) Son binarios y


devuelven un valor booleano evaluado en el sentido del álgebra
de Bool.
Nota: los operadores binarios originales (no
sobrecargados) usan una táctica de evaluación de
cortocircuito, lo que significa que intentan omitir una evaluación
del argumento derecho si el argumento izquierdo es suficiente
para determinar el resultado final.

Cuando el argumento izquierdo de || es verdadero , toda la


expresión también es verdadera , independientemente del valor
de argumento correcto.
Cuando el argumento izquierdo de && es falso , toda la expresión
también es falsa , independientemente del valor del argumento
correcto.
No importa si los argumentos son variables o relaciones simples,
pero sí importa cuando son invocaciones de funciones.
Supongamos que tenemos dos funciones, f1 () y f2 () , ambas
devuelven bool . En la siguiente expresión:
f1 () && f2 ()
la función f2 () se invocará o no dependiendo del resultado f1 () .

589 – By Gerson
Como la función lógica sobrecargada no puede hacer tal distinción,
las dos funciones anteriores se invocarán cuando se usen juntas
en la misma expresión.
El programa de ejemplo aquí → define el operador && para
verificar la siguiente condición, verbalmente definida:
Los vectores izquierdo y derecho tienen al menos un elemento
distinto de cero.
El segundo de los operadores devuelve un valor que refleja la
condición:
Todos los elementos del vector son distintos de cero.
El programa emitirá las siguientes dos líneas:
cierto
cierto
source_08_03_08_logical.cpp

590 – By Gerson
8.3.9 Operadores de asignación compuesta
Compound assignment operators

+ = - = * =% = / = & = | = ^ =
Operadores
>> = << =

¿Se puede implementar como una


NO
función global?

¿Se puede implementar como una


SI
función miembro?

Una referencia a un objeto o un


Tipo de valor de retorno
valor l en general

Tenga en cuenta que ninguno de los operadores anteriores se


deriva implícitamente de su contraparte regular y
viceversa . Sobrecargar el operador + no produce el operador +
=y así sucesivamente. La implementación de ambos
componentes de un par de este tipo debería ser compatible (es
decir, X + = Y debería evaluarse de la misma manera y dar como
resultado el mismo valor que X = X + Y ), pero no hay
automatización. Todo depende de ti.
Todas las funciones de operador de esta categoría tienen un
argumento y modifican directamente su objeto de inicio. La
función debe devolver una referencia al objeto modificado
para permitir el encadenamiento .
En el siguiente programa → hemos definido los operadores + y +
=.
El programa genera:
(8, 12)
source_08_03_09_compound.cpp

591 – By Gerson
8.3.10 Operadores de incremento y decremento de prefij

Operadores ++--

¿Se puede implementar como una


NO
función global?

¿Se puede implementar como una


SI
función miembro?

Una referencia a un objeto o un


Tipo de valor de retorno
valor l en general

592 – By Gerson
La forma de prefijo de ++ / - debe implementarse como una
función de operador sin parámetros y (ya que modifica su
argumento antes de su uso) debe devolver una referencia al
objeto modificado.
Usted es responsable de garantizar que los operadores de prefijo
y sufijo dejen sus argumentos en el mismo estado y que ambos
sean coherentes con + = 1 y - = 1 . El compilador no es lo
suficientemente astuto como para hacerlo por ti.
El programa de ejemplo (puede encontrarlo aquí →) le muestra un
prefijo ++ sobrecargado que afecta a todos los elementos
vectoriales. El programa genera:
(2, 3)
source_08_03_10_prefix.cpp

593 – By Gerson
8.3.11 Operadores de incremento y decremento de postfix

Operadores ++--

¿Se puede implementar como una función


NO
global?

¿Se puede implementar como una función


SI
miembro?

Una referencia a un objeto o un valor l en


Tipo de valor de retorno
general

La forma de postfix de ++ / - debe implementarse como una


función de operador de un parámetro (sic! Tenga en cuenta
que el parámetro de tipo int es un completo ficticio y no debe
usarlo dentro de la función) y ya que sirve el objeto antes de que
se vea afectado por la modificación, debe devolver una copia del
objeto no modificado .
La presencia del parámetro dummy int es el único rasgo que
permite al compilador distinguir entre operadores
sobrecargados con prefijo y postfix .

El programa de ejemplo aquí → genera el siguiente texto:

(3, 4)

source_08_03_11_postfix.cpp

594 – By Gerson
#include <iostream>
usando el espacio de nombres estándar ;
clase V {
public :
int vec [ 2 ] ;
V ( int a0, int a1 ) { vec [ 0 ] = a0 ; vec [ 1 ] = a1 ; }
V operador ++ ( int ninguno ) {
V v ( vec [ 0 ] , vec [ 1 ]) ; para
( int i = 0 ; i < 2 ; i ++ )
++ vec [ i ]
return v ;
}
};
int main ( nulo ) {
V v1 ( 2 , 3 ) ;
v1 ++ ;
cout << "(" << v1. vec [ 0 ] << "," << v1. vec [ 1 ] << ")" << endl ;
devuelve 0 ;
}

595 – By Gerson
8.4.1 Operador de subíndice

Operador []

¿Se puede implementar como una función


NO
global?

¿Se puede implementar como una función


SI
miembro?

Una referencia a un objeto o un valor l en


Tipo de valor de retorno
general

La forma original del operador [] es muy compleja, tanto en


sintaxis como en aspectos semánticos.
Básicamente, requiere dos argumentos , un puntero (porque
el nombre de una matriz se interpreta como un puntero a su
primer elemento) y un valor int como índice. Además, la forma
original del operador es conmutativa , es decir, en la siguiente
declaración

int arr [10];

ambas asignaciones son válidas :

arr [0] = 0;
1 [arr] = 1;

El operador de índice sobrecargado difiere un poco de su


antepasado habitual. Puede aceptar índices cuando no son
enteros: prácticamente todos los tipos de datos (incluidas las
clases) se pueden usar como índice, lo que permite al
programador implementar algoritmos de búsqueda complejos.

596 – By Gerson
Por ejemplo, puede ocultar búsquedas y actualizaciones de bases
de datos bajo una máscara de indexación. Además,
los índices int pueden ser negativos. La semántica de dicha
indexación está completamente definida por el
programador .
El operador de índice sobrecargado ya no es conmutativo . Lo
sentimos, por favor dirija sus quejas a otra parte.

El operador necesita encontrar una referencia a los datos


señalados directa o indirectamente por el valor del índice y, por lo
tanto, el operador funcionará correctamente en ambos lados del
operador de asignación.
El operador debe declararse como un parámetro : solo el valor
del índice proviene del mundo exterior, porque el objeto en sí
mismo se trata como un segundo argumento .
Echa un vistazo al ejemplo →

Es un método bastante extraño de traducir un número al nombre


de una variable. Podemos decir que la clase Arr es una matriz sin
una matriz . Esa pseudo-matriz acepta índices que comienzan
desde 1 , no desde 0 (esta era una convención estándar en los
primeros lenguajes de programación ... en momentos en que los
dinosaurios construían computadoras).

El programa genera las siguientes líneas:

16
9
4
1

source_08_04_01_index.cpp

597 – By Gerson
#include <iostream>
# include <stdexcept>
using namespace std ;
clase Arr {
privado :
int a, b, c, d ;
public :
Arr () { a = b = c = d = 0 ; }
int & operator [] ( int index ) {
switch ( index ) {
case 1 : return a ;
caso 2 : retorno b ;
caso 3 : retorno c ;
caso4 : retorno d ;
valor predeterminado : throw range_error ( "índice incorrecto" ) ;
}
}
};
int main ( nulo ) {
Arr arr ;

para ( int i = 1 ; i <= 4 ; i ++ )


arr [ i ] = i * i ;
para ( int i = 4 ; i> 0 ; i-- )
cout << arr [ i ] << endl ;
devuelve 0 ;
}

598 – By Gerson
8.4.2 Operador de invocación de funciones

Operador ()

¿Se puede implementar como una función global? NO

¿Se puede implementar como una función miembro? SI

Tipo de valor de retorno ninguna

El operador de invocación de funciones es muy diferente. Estas


son algunas de sus características:

 el número de sus parámetros no está


predefinido ; puedes usar tantos parámetros como quieras

 el tipo de retorno no está predefinido ni sugerido ; usa el


que necesitas

 una clase puede contener más de una función


de operador () sobrecargada ; especifique todos los que
necesite, pero recuerde mantenerlos distinguibles por el
número y los tipos de parámetros (relacione los requisitos con
las funciones sobrecargadas)

El operador se puede usar para crear objetos que pretenden ser


funciones (se pueden usar para hacer cosas más avanzadas, pero
eso está más allá del alcance de este curso).
El programa de ejemplo → muestra este tipo de clase de imitación.
Sobrecarga el operador () dos veces para crear una apariencia
para dos funciones sobrecargadas, encontrando el máximo de sus
argumentos.

El programa genera:

599 – By Gerson
2
3

source_08_04_02_invoke.cpp

#include <iostream>
usando el espacio de nombres estándar ;
clase Fun {
public :
int operator () ( int a1, int a2 ) {
return a1> a2 ? a1 : a2 ;
}
int operator () ( int a1, int a2, int a3 ) {
return a1> a2 ? ( A1> a3 ? A1 : a3 ) : ( a2> a3 ? A2: a3 ) ;
}
};
int main ( void ) {
Fun f ;

cout << f ( 1 , 2 ) << endl ;


cout << f ( 1 , 2 , 3 ) << endl ;
devuelve 0 ;
}

600 – By Gerson
8.4.3 Operadores de puntero

Operador &*

¿Se puede implementar como una función global? SI

¿Se puede implementar como una función miembro? SI

Tipo de valor de retorno ninguna

A pesar de sus muchas diferencias, estos dos operadores se


agrupan debido a su contexto de uso similar.
Se utilizan para crear pseudo-punteros o los
llamados punteros inteligentes, es decir, valores que pueden
no tener un significado real de "apuntar" (no tiene sentido crear
punteros reales producidos sintéticamente cuando el lenguaje ya
los ofrece).
Estos valores de pseudo puntero pueden ser de cualquier tipo y
tipo (por ejemplo, pueden ser cadenas utilizadas como claves en
bases de datos), pero no olvide que su fantasía debe ajustarse a
limitaciones razonables. Sería bueno para asegurarse de que P es
siempre igual a * & P .

El programa aquí → muestra un ejemplo muy limitado del uso de


estos operadores.
Analice cómo hemos usado cadenas como pseudo-
punteros. El operador & sobrecargado dentro de la clase P crea
un nuevo "puntero", que representa un objeto con
el valor sin campo seleccionado , mientras que
el operador sobrecargado * crea un nuevo objeto basado en el
valor de "puntero" desreferenciado ( cadena en realidad).
El programa genera la siguiente línea:

'Charlie' -> 2

601 – By Gerson
source_08_04_03_pointers

#include <iostream>

usando el espacio de nombres estándar ;

clase P {
public :
int no ;
P ( int n ) : no ( n ) {}
P () : no ( 0 ) {}
operador de cadena & () {
switch ( no ) {
case 0 : return "alpha" ;
caso 1 : devolver "bravo" ;
caso 2 : devolver "charlie" ;
}
}
};

P & operador * ( cadena s ) {


P*p;
if ( ! s. compare ( "alpha" ))
p = nuevo P ( 0 ) ;
si no ( ! s. compare ( "bravo" ))
p = nuevo P ( 1 ) ;
si no ( ! s. compare ( "charlie" ))
p = nuevo P ( 2 );
de lo contrario
602 – By Gerson
p = nuevo P ( - 1 ) ;
volver * p ;
}

int main ( void ) {


P p1 ( 2 ) ;
cadena s = & p1 ;
P p2 = * s ;
cout << "'" << s << "' ->" << p2. no << endl ;
devuelve 0 ;
}

8.4.4 Otros operadores sobrecargables

Todos los operadores enumerados aquí → se pueden cargar en el


lenguaje "C ++".
No los describiremos en detalle ni le daremos programas de
ejemplo, porque estamos convencidos de que el uso de estos
operadores cae en cuestiones reservadas estrictamente para el
nivel profesional de programación.
Pero siéntase libre de experimentar con ellos por su cuenta.

603 – By Gerson
8.4.5 Operadores no sobrecargables

Los operadores enumerados aquí → no se pueden


sobrecargar en el lenguaje "C ++".

8.4.6 Fracción: una clase con operadores sobrecargados

Ahora queremos mostrarle una clase real y (tal vez) útil


de fracciones vulgares .
Implementaremos la fracción como un par de valores int que
representan un numerador y un denominador .
Vamos a equipar a la clase con un conjunto básico de operadores
que nos permita manipular las fracciones en términos de las cuatro
operaciones aritméticas básicas.

Queremos que nuestra nueva clase:

 tener dos campos int , uno para el numerador y el


denominador (¡ambos privados!)

 tener un conjunto de constructores convenientes:

 un constructor predeterminado , que establece la


fracción al valor de cero (en realidad 0/1 )

604 – By Gerson
 un constructor de un parámetro , que establece la
fracción al valor igual al valor del parámetro (en
realidad parámetro / 1 )

 un constructor de dos parámetros , que establece la


fracción con el numerador igual al primer parámetro y la
fracción con el denominador igual al segundo
parámetro; el constructor debe proteger el objeto de
establecer el denominador en 0

 tener un conjunto de operadores convenientes:

 un operador diseñado para reducir la fracción, ¡hemos


decidido usar el ! para este propósito (sí, sabemos que
se ve raro, pero no tenemos mejores ideas)

 cuatro operadores que realizan suma, resta,


multiplicación y división ; el último no debería
permitirnos realizar la división por cero

 dos funciones auxiliares, que evalúan el divisor común


más grande y el multiplicador común más
bajo (¡ambos privados!)

 tener un conjunto de funciones útiles:

 una función que convierte una fracción en una forma


legible para humanos , por ejemplo, "[3/5]" para "tres
quintos"

 una función que convierte una fracción en un doble ,


por ejemplo, 3/5 en 0.6

También queremos tener un operador de flujo sobrecargado que


nos permita generar las fracciones directamente en los flujos de
salida.
¿Estás listo?

605 – By Gerson
8.4.7 Fracción: un archivo de encabezado

Hemos organizado nuestra lista de deseos en forma de un archivo


de encabezado. Lo puedes encontrar aquí →
Nota:

 LCM es la abreviatura de multiplicador común más bajo

 GCD es corto del divisor común más grande

 una función que convierte una fracción en una cadena se


llama GetString

 una función que convierte una fracción en un valor doble se


llama GetValue

fracción.h

#ifndef __FRACTION_H__
#define __FRACTION_H__

#include <stdexcept>
#include <cadena>

Fracción de clase {
privado :
int numerador, denominador ;
int LCM ( int x, int y ) ;
int GCD ( int x, int y ) ;
public :
Fraction () ;
Fracción ( int n ) ;
Fracción ( int n, int d ) throw ( std :: domain_error ) ;
606 – By Gerson
std:: string GetString ( vacío ) ;
doble GetValue ( nulo ) ;
Operador de fracciones! ( nulo ) ;
Operador de fracciones + ( Fracción arg ) ;
Operador de fracción * ( Fracción arg ) ;
Operador de fracción / ( Fracción arg ) throw ( std :: domain_error ) ;
Fracción y operador + = ( Fracción arg ) ;
};

std :: ostream & operator << ( std :: ostream & ostr, Fraction & f ) ;

#endif

8.4.8 LCM y GDM

Las implementaciones de los algoritmos LCM y GDM son clásicas



Ahora, consulte cualquier manual aritmético para encontrar los
antecedentes teóricos de los algoritmos.
Tenga en cuenta que estas son solo dos de las muchas
implementaciones posibles: las nuestras no son las mejores ni las
más efectivas, pero son cortas y simples.

int Fraction::LCM(int x, int y) {


int i = y;
while(y % x)
y += i;
return y;
}

Fracción int :: MCD ( int x, int y ) {


607 – By Gerson
para ( ;; ) {
x% = y ;
si ( ! x )
devuelve y ;
y% = x ;
si ( ! y )
devuelve x ;
}
}

8.4.9 Constructores

Los constructores de clases no son realmente tan complicados →


Tenga en cuenta que somos un poco descuidados cuando se trata
de reconocer y almacenar el signo de la fracción. No verificamos
si el numerador y el denominador tienen signos idénticos o
diferentes.
Quizás valga la pena organizar esto de una manera más reflexiva.
Puedes arreglarlo si quieres. No vamos a ... ¡así es como rodamos!

Fracción :: Fracción () : numerador ( 0 ) , denominador ( 1 ) {


};

Fracción :: Fracción ( int n ) : numerador ( n ) , denominador ( 1 ) {


};

Fracción :: Fracción ( int n, int d ) throw ( domain_error ) : numerador ( n ) , denominado


if ( denominator == 0 )
throw domain_error ( "fracción mala" ) ;
};

608 – By Gerson
8.4.10 GetString

Hemos implementado la función GetString () utilizando


la clase ostringstream . El objeto de esta clase se comporta como
una secuencia de salida, pero envía el texto de salida no a un
archivo o dispositivo, sino a un búfer interno al que se puede
acceder utilizando el método str () .
Requiere la inclusión del archivo de encabezado sstream.
La función GetValue () es simple.

Fracción de cadena :: GetString ( void ) {


ostringstream os ;
os << "[" << numerador << "/" << denominador << "]" ;
volver os. str () ;
}

fracción doble :: GetValue ( void ) {


return double ( numerador ) / double ( denominador ) ;
}

8.4.11 Reducción de la fracción


El ! El operador realiza la reducción (en nuestra clase, de todos
modos) →
Lo implementamos usando la función GCD y creamos un nuevo
objeto de fracción reducida.
Fracción Fracción :: operador ! ( nulo ) {
int gcd = GCD ( numerador, denominador ) ;
Fracción de retorno ( numerador / mcd, denominador / mcd ) ;
}

609 – By Gerson
8.4.12 Agregar fracciones

El operador + realiza la suma (no solo en nuestra clase,


afortunadamente) →
Primero, encontramos un nuevo denominador común para ambos
argumentos (usando la función LCM ), y segundo, evaluamos el
numerador resultante.
Un objeto recién creado que contiene el resultado se devuelve
como el resultado del operador.
Tenga en cuenta que no reducimos el resultado . ¿Es eso una
buena idea?
Fracción Fracción :: operador + ( arg Fracción ) {
int common_denom = LCM ( denominador, arg. Denominador ) ;
int numera = numerador * denominador común / denominador +
arg. numerador * common_denom / arg. denominador;
Fracción f ( numera, common_denom ) ;
volver f ;
}

8.4.13 Multiplicar fracciones


El operador * realiza la multiplicación →
Un objeto recién creado que contiene el resultado se devuelve
como el resultado del operador.
Tenga en cuenta que reducimos el resultado . ¿Es eso una
buena idea?

Fracción Fracción :: operador * ( Fracción arg ) {


int numera = numerador * arg. numerador;
int denomi = denominador * arg. denominador;
Fracción f ( numera, denomi ) ;
volver ! f ;
}

610 – By Gerson
8.4.14 División de fracciones

El operador / realiza la división →


Tenemos que protegernos de crear una fracción con el nominador
igual a cero. Lanzaremos una excepción en este caso.
Un objeto recién creado que contiene el resultado se devuelve
como el resultado del operador.
Tenga en cuenta que reducimos el resultado . Bien, no haremos
más preguntas en este momento.

Fraction Fraction :: operator / ( Fraction arg ) throw ( domain_error ) {


if ( arg. Numerator == 0 )
throw domain_error ( "división por cero" ) ;
int numera = numerador * arg. denominador;
int denomi = denominador * arg. numerador;
Fracción f ( numera, denomi ) ;
volver ! f ;
}

8.4.15 Adición de compuesto

Hemos decidido agregar un operador adicional a nuestra clase: +


= →
Podemos usarlo como base para implementar todos (o algunos)
de los operadores compuestos restantes.

Fracción y Fracción :: operator + = ( Fracción arg ) {


int common_denom = LCM ( denominador, arg. Denominador ) ;
int numera = numerador * denominador común / denominador +

611 – By Gerson
arg. numerador * common_denom / arg. denominador;
numerador = numera ;
denominador = common_denom ;
devuelve * esto ;

8.4.16 Fracción de envío a flujo de salida

La última parte de nuestro código es bastante simple: es la


implementación del sobrecargado << operador de transmisión →
Si desea ver o usar el código completo, puede encontrarlo
aquí: fracción.cpp

ostream & operator << ( ostream & ostr, Fraction & f ) {


return ostr << f. GetString () ;
}

8.4.17 Función principal

Aquí hay un código de prueba simple que muestra


nuestra clase Fraction en acción →
El código produce el siguiente resultado en la pantalla:

[1/2] -> 0.5


[2/3] -> 0.666667
612 – By Gerson
[1/2]+[2/3]=[7/6]
[2/3]+[2/3]+[2/3]=[6/3]
[2/3]+[2/3]+[2/3]=[2/1]
[1/2]*[2/3]=[1/3]
[1/2]:[2/3]=[3/4]
[11/8]

tester.cpp

# incluye "fracción.h"
#include <iostream>
usando el espacio de nombres estándar ;
int main ( void ) {
Fracción f1 ( 1 , 2 ) , f2 ( 2 , 3 ) , f ;
cout << f1 << "->" << f1. GetValue () << endl ;
cout << f2 << "->" << f2. GetValue () << endl ;
f = f1 + f2 ;
cout << f1 << "+" <<;
f = f2 + f2 + f2 ;
cout << f2 << "+" << f2 << "+" << f2 << "=" << f << endl ;
f =! f ;
cout << f2 << "+" << f2 << "+" << f2 << "=" << f << endl ;
f = f1 * f2 ;
cout << f1 << "*" << f2 << "=" << f << endl ;

613 – By Gerson
f = f1 / f2 ;
cout << f1 << ":" << f2 << "=" << f << endl ;
Fracción f3 ( 7, 8 ) ;
f3 + = f1 ;
cout << f3 << endl ;
devuelve 0 ;
}

Buen trabajo, ¡has completado el curso!

¡Felicidades! Has llegado al final de este curso. Esperamos que


hayas disfrutado de tu aventura de aprendizaje y hayas aprendido
muchas cosas nuevas e interesantes y útiles.

¿Qué sigue? ¡Obtenga la certificación C ++ !

Bueno, cada proceso de aprendizaje necesita un trabajo extenso


y mucha energía. Es verdad. Sin embargo, también necesita una
base sólida que pueda darle una sensación de integridad y logro.
Los alumnos disciplinados ven la certificación como la parte final
de su proceso de aprendizaje, como su elemento crucial e
indispensable.
Y creemos que todos sus esfuerzos deben ser coronados
obteniendo una certificación profesional y reconocida por la
industria. ¿Por qué? Debido a que le dará una ventaja competitiva
sobre los demás, le permitirá mantenerse motivado y le dará más
satisfacción profesional. ¡Compruébalo por ti mismo !

614 – By Gerson
615 – By Gerson
616 – By Gerson

También podría gustarte