Este documento presenta la resolución de ejercicios seleccionados de los primeros capítulos de un manual de C++, abarcando conceptos fundamentales como la entrada/salida básica, estructuras de control, tipos de datos, punteros, referencias, la biblioteca estándar (string y vector) y arreglos. Cada solución incluye el código C++ correspondiente y, cuando es necesario, explicaciones detalladas en español.
Capítulo 1
Ejercicio 1.2
Un programa sencillo para solicitar al usuario dos números enteros y mostrar su suma.
#include <iostream>
int main() {
std::cout << "Por favor, introduce dos números enteros: " << std::endl;
int numero1 = 0, numero2 = 0;
std::cin >> numero1 >> numero2;
std::cout << "La suma de " << numero1 << " y " << numero2 << " es " << numero1 + numero2 << std::endl;
return 0;
}
Ejercicio 1.3
El clásico programa "Hola, Mundo".
#include <iostream>
int main() {
std::cout << "Hola, Mundo" << std::endl;
return 0;
}
Ejercicio 1.4
Programa para calcular el producto de dos números enteros ingresados por el usuario.
#include <iostream>
int main() {
std::cout << "Introduce dos números enteros para multiplicarlos: " << std::endl;
int factor1 = 0, factor2 = 0;
std::cin >> factor1 >> factor2;
std::cout << "El producto de " << factor1 << " y " << factor2 << " es " << factor1 * factor2 << std::endl;
return 0;
}
Ejercicio 1.5
Reescritura del ejercicio 1.4, mostrando cada parte del mensaje de salida de forma individual.
#include <iostream>
int main() {
int valA = 0, valB = 0;
std::cout << "Ingresa dos números para obtener su producto:" << std::endl;
std::cin >> valA >> valB;
std::cout << "El producto de ";
std::cout << valA;
std::cout << " y ";
std::cout << valB;
std::cout << " es ";
std::cout << valA * valB;
std::cout << std::endl;
return 0;
}
Ejercicio 1.6
Análisis de la validez de las siguientes expresiones std::cout:
// Asumamos que tenemos: int numA = 1, numB = 2;
std::cout << "La suma de " << numA;
// Válido. Imprime "La suma de 1".
<< " y " << numB;
// Inválido. El operador << requiere un objeto de flujo de salida a su izquierda.
// Corrección: std::cout << " y " << numB;
<< " es " << numA + numB << std::endl;
// Inválido. Mismo motivo que el anterior.
// Corrección: std::cout << " es " << numA + numB << std::endl;
// La sentencia completa correcta sería:
// std::cout << "La suma de " << numA << " y " << numB << " es " << numA + numB << std::endl;
Ejercicio 1.9
Calcula la suma de los números enteros del 50 al 100 inclusive utilizando un bucle while.
#include <iostream>
int main() {
int sumaTotal = 0;
int contador = 50;
while (contador <= 100) {
sumaTotal += contador;
contador++;
}
std::cout << "La suma de los números del 50 al 100 es: " << sumaTotal << std::endl;
return 0;
}
Ejercicio 1.10
Imprime los números del 10 al 0 en orden descendente, cada uno en una nueva línea.
#include <iostream>
int main() {
int cuentaRegresiva = 10;
while (cuentaRegresiva >= 0) {
std::cout << cuentaRegresiva-- << std::endl;
}
return 0;
}
Ejercicio 1.11
Solicita al usuario dos números y muestra todos los números en el rango definido por ellos.
#include <iostream>
#include <algorithm> // Para std::min y std::max
int main() {
int limiteInferior = 0, limiteSuperior = 0;
std::cout << "Introduce dos números enteros: " << std::endl;
std::cin >> limiteInferior >> limiteSuperior;
// Aseguramos que limiteInferior sea el menor y limiteSuperior el mayor
int inicio = std::min(limiteInferior, limiteSuperior);
int fin = std::max(limiteInferior, limiteSuperior);
std::cout << "Los números entre " << inicio << " y " << fin << " son:" << std::endl;
while (inicio <= fin) {
std::cout << inicio++ << std::endl;
}
return 0;
}
Ejercicio 1.13
Reescribe los ejercicios 1.9, 1.10 y 1.11 utilizando bucles for en lugar de while.
Suma de 50 a 100:
#include <iostream>
int main() {
int totalAcumulado = 0;
for (int numActual = 50; numActual <= 100; ++numActual) {
totalAcumulado += numActual;
}
std::cout << "La suma de 50 a 100 es: " << totalAcumulado << std::endl;
return 0;
}
Cuenta regresiva de 10 a 0:
#include <iostream>
int main() {
for (int i = 10; i >= 0; --i) {
std::cout << i << std::endl;
}
return 0;
}
Números en un rango definido:
#include <iostream>
#include <algorithm> // Para std::min y std::max
int main() {
std::cout << "Introduce dos valores enteros: " << std::endl;
int val1 = 0, val2 = 0;
std::cin >> val1 >> val2;
int comienzo = std::min(val1, val2);
int final = std::max(val1, val2);
std::cout << "Números del " << comienzo << " al " << final << ":" << std::endl;
for (int k = comienzo; k <= final; ++k) {
std::cout << k << std::endl;
}
return 0;
}
Sección 1.4.3: Lectura de un número indefinido de datos de entrada
Este programa lee una secuencia de números enteros de la entrada estándar, sumándolos hasta que se encuentre el fin de archivo (EOF). En sistemas Windows, esto se logra típicamente con Ctrl+Z seguido de Enter; en UNIX/Linux, con Ctrl+D.
#include <iostream>
int main() {
int sumaAcumulada = 0;
int valorLeido = 0;
// Lee hasta que se encuentra el fin de archivo, sumando todos los valores.
std::cout << "Introduce una serie de números (Ctrl+Z para finalizar):" << std::endl;
while (std::cin >> valorLeido) {
sumaAcumulada += valorLeido;
}
std::cout << "La suma total es: " << sumaAcumulada << std::endl;
return 0;
}
Sección 1.4.4: Contar las ocurrencias consecutivas de cada valor en la entrada
Este programa procesa una secuencia de valores enteros, identificando y contando cuántas veces aparece cada valor de forma consecutiva.
#include <iostream>
int main() {
int valorActual = 0, valorNuevo = 0;
std::cout << "Introduce una serie de números (Ctrl+Z para finalizar):" << std::endl;
// Aseguramos que haya al menos una entrada para procesar
if (std::cin >> valorActual) {
int conteo = 1; // Contador para el valor actual
// Lee el resto de los valores
while (std::cin >> valorNuevo) {
if (valorNuevo == valorActual) {
++conteo; // El valor actual se repite
} else {
// El valor ha cambiado, imprimimos el resumen del anterior
std::cout << valorActual << " aparece " << conteo << " veces." << std::endl;
valorActual = valorNuevo; // Actualizamos al nuevo valor
conteo = 1; // Reiniciamos el contador
}
}
// Imprimimos el resumen para el último valor procesado
std::cout << valorActual << " aparece " << conteo << " veces." << std::endl;
} else {
std::cout << "No se introdujeron datos." << std::endl;
}
return 0;
}
Ejercicio 1.20
Lee y muestra un objeto de tipo Sales_item. Se asume la existencia de un archivo de cabecera Sales_item.h que define la clase Sales_item y sobrecarga los operadores << y >>.
#include <iostream> // Incluye la librería estándar de E/S
#include "Sales_item.h" // Incluye el encabezado de la clase Sales_item
int main() {
Sales_item articuloVenta; // Crea un objeto Sales_item para almacenar información
std::cout << "Introduce datos de venta (ISBN, unidades vendidas, precio):" << std::endl;
while (std::cin >> articuloVenta) {
// Lee los datos de un artículo de venta desde la entrada estándar (cin)
// hasta que la entrada finalice o haya un error.
// Se espera que los datos incluyan ISBN, cantidad vendida e ingresos totales.
// Esto se realiza mediante el operador de flujo de entrada >> sobrecargado.
std::cout << articuloVenta << std::endl;
// Muestra la información del artículo de venta leído en la salida estándar (cout).
// Esto se realiza mediante el operador de flujo de salida << sobrecargado.
}
return 0;
}
Ejercicio 1.21
Suma dos objetos Sales_item (ejercicio con dos entradas).
#include <iostream>
#include "Sales_item.h"
int main() {
Sales_item venta1, venta2;
std::cout << "Introduce los datos de dos transacciones de venta:" << std::endl;
std::cin >> venta1 >> venta2; // Lee un par de registros de transacción
std::cout << "La suma de las transacciones es: " << venta1 + venta2 << std::endl; // Imprime su suma
return 0;
}
Ejercicio 1.22
Suma múltiples objetos Sales_item (ejercicio con entradas repetidas). Se asume que todas las ventas tienen el mismo ISBN.
#include <iostream>
#include "Sales_item.h"
int main() {
Sales_item registroActual, totalGeneral;
std::cout << "Introduce registros de venta para sumar (mismo ISBN, Ctrl+Z para finalizar):" << std::endl;
// Lee el primer registro para inicializar el total
if (std::cin >> totalGeneral) {
// Luego, lee los registros restantes y súmalos
while (std::cin >> registroActual) {
totalGeneral += registroActual;
}
std::cout << "El total acumulado de las ventas es: " << totalGeneral << std::endl;
} else {
std::cerr << "Error: No se introdujeron datos de venta." << std::endl;
return -1; // Indica fallo
}
return 0;
}
Ejercicio 1.23
Cuenta cuántas veces aparece un ISBN consecutivo en la entrada.
#include <iostream>
#include "Sales_item.h"
int main() {
Sales_item itemActual, itemNuevo;
int contadorRegistros = 1; // Inicializa el contador para el primer elemento
std::cout << "Introduce registros de venta para contar (Ctrl+Z para finalizar):" << std::endl;
if (std::cin >> itemActual) { // Asegura que haya al menos una entrada
while (std::cin >> itemNuevo) {
if (itemActual.isbn() == itemNuevo.isbn()) { // Compara los ISBN
contadorRegistros++;
} else {
// El ISBN ha cambiado, imprime el resumen del anterior
std::cout << "ISBN " << itemActual.isbn() << " tiene " << contadorRegistros << " registros." << std::endl;
contadorRegistros = 1; // Reinicia el contador para el nuevo ISBN
itemActual = itemNuevo; // Actualiza el item actual
}
}
// Imprime el resumen para el último grupo de registros
std::cout << "ISBN " << itemActual.isbn() << " tiene " << contadorRegistros << " registros." << std::endl;
} else {
std::cerr << "Error: No se introdujeron datos." << std::endl;
return -1;
}
return 0;
}
Ejercicio 1.25
Suma múltiples objetos Sales_item, agrupándolos por ISBN. Si los ISBN son diferentes, imprime el total del ISBN anterior y comienza uno nuevo.
#include <iostream>
#include "Sales_item.h" // Se asume la existencia de esta cabecera
int main() {
Sales_item totalAcumulado, transaccionActual; // Define dos objetos para el total y la transacción actual
std::cout << "Introduce transacciones de venta (Ctrl+Z para finalizar):" << std::endl;
// Asegura que la primera entrada sea válida para iniciar el procesamiento
if (std::cin >> totalAcumulado) {
// Procesa el resto de las entradas
while (std::cin >> transaccionActual) {
if (totalAcumulado.isbn() == transaccionActual.isbn()) {
// Si los ISBN son iguales, suma la transacción al total
totalAcumulado += transaccionActual;
} else {
// Si los ISBN son diferentes, imprime el total del ISBN anterior
std::cout << totalAcumulado << std::endl;
// Y comienza un nuevo total con la transacción actual
totalAcumulado = transaccionActual;
}
}
// Imprime el total para el último ISBN después de que el bucle finalice
std::cout << totalAcumulado << std::endl;
} else {
// No se introdujeron datos, emite una advertencia
std::cerr << "Advertencia: No se introdujeron datos de venta." << std::endl;
return -1; // Indica un error
}
return 0;
}
Capítulo 2
Ejercicio 2.1
Tipos fundamentales de C++: Descripción, tamaño mínimo y precisión.
Tipos enteros (sin booleanos ni caracteres extendidos):
| Tipo | Significado | Tamaño Mínimo (bits) |
|---|---|---|
short |
Entero corto | 16 |
int |
Entero | 16 |
long |
Entero largo | 32 |
long long |
Entero muy largo (C++11) | 64 |
Los tipos enteros (excepto bool y los tipos de caracteres extendidos) pueden ser con signo (signed) o sin signo (unsigned). Los tipos con signo pueden representar números positivos, negativos y cero, utilizando el bit más significativo como bit de signo. Los tipos sin signo solo pueden representar números mayores o iguales a cero, utilizando todos sus bits para almacenar el valor numérico.
Tipos de punto flotante:
| Tipo | Significado | Tamaño Mínimo (bits) | Dígitos Significativos (aproximados) |
|---|---|---|---|
float |
Punto flotante de precisión simple | 32 | 7 |
double |
Punto flotante de precisión doble | 64 | 16 |
long double |
Punto flotante de precisión extendida | Más que double (normalmente 80 o 128) |
18-19 |
Ejercicio 2.2
Selección del tipo adecuado para variables como tasaInteres, capitalInicial y montoPago.
Para variables como la tasa de interés, el capital inicial y el monto de pago, el tipo double es generalmente la mejor elección. Estas cantidades pueden ser tanto números enteros como números reales con decimales, y double ofrece una precisión adecuada (típicamente 15-17 dígitos decimales) para la mayoría de las aplicaciones financieras y científicas. El tipo float a menudo no proporciona suficiente precisión, mientras que long double, aunque ofrece mayor precisión, puede tener un costo de rendimiento adicional que no suele ser necesario en la mayoría de los casos, y la diferencia en costo computacional entre float y double es a menudo mínima o inexistente en hardware moderno.
Ejercicio 2.6
Análisis de la inicialización de variables enteras con literales octales.
#include <iostream>
int main() {
int mes = 9, dia = 7;
// 'mes' y 'dia' son definidos como enteros e inicializados con los valores 9 y 7 respectivamente.
// Esto es válido.
// int mesOctal = 09, diaOctal = 07;
// Esto es inválido para 'mesOctal'.
// Un literal que comienza con '0' indica una base octal.
// En el sistema octal, los dígitos válidos van del 0 al 7.
// Por lo tanto, '09' no es un número octal válido, lo que resultaría en un error de compilación.
// '07' es un número octal válido (su valor decimal es 7).
return 0; // Agregado para un main válido
}
Ejercicio 2.8
Uso de secuencias de escape en cadenas de caracteres.
#include <iostream>
int main() {
std::cout << "2\115\n"; // Imprime "2M" (donde '\115' es el código ASCII para 'M' en octal)
std::cout << "2\t\115\n"; // Imprime "2\tM" (donde '\t' es un tabulador, y '\115' es 'M')
return 0;
}
Ejercicio 2.9
Identificación y corrección de errores de inicialización y declaración.
// Errores:
// std::cin >> int input_value;
// El operador >> espera un objeto ya definido a su derecha.
// Además, 'int' es un nombre de tipo incorporado y no puede usarse como identificador aquí.
// Corrección:
int valorDeEntrada = 0; // Declarar e inicializar la variable primero
std::cin >> valorDeEntrada;
// int indice = { 3.14 };
// Esto es una inicialización de lista (curly-braced initialization).
// Intenta convertir un double (3.14) a un int, lo que implica una posible pérdida de información.
// La inicialización de lista prohíbe las conversiones estrechas que puedan perder información.
// Corrección:
double indiceDecimal = { 3.14 }; // O int indice = 3; si la parte decimal no es relevante.
// double salario = sueldo = 9999.99;
// 'sueldo' no está definido antes de su uso.
// Corrección:
double sueldo = 0.0;
double salario = sueldo = 9999.99; // Ahora 'sueldo' está definido.
// int i = 3.14;
// Esto es una conversión implícita de double a int. La parte decimal se truncará (3.14 se convierte en 3).
// Aunque es legal, puede no ser lo deseado y es una posible pérdida de información.
// Corrección (si se desea la precisión del double):
double j = 3.14;
// O (si se desea un int y se entiende el truncamiento):
int k = static_cast<int>(3.14); // Hacer explícita la conversión.
Ejercicio 2.10
Comportamiento de la inicialización por defecto de variables globales y locales.
#include <iostream>
#include <string> // Para std::string
std::string cadenaGlobal; // 'cadenaGlobal' se inicializa por defecto a una cadena vacía.
int enteroGlobal; // 'enteroGlobal' se inicializa por defecto a 0.
// Las variables definidas fuera de cualquier función se inicializan a cero.
int main() {
int enteroLocal; // 'enteroLocal' es un entero definido dentro de una función.
// Las variables de tipos incorporados definidas dentro de una función
// no se inicializan por defecto y tienen un valor indeterminado (basura).
std::string cadenaLocal; // 'cadenaLocal' es un objeto de clase std::string definido localmente.
// Las clases definen cómo se inicializan sus objetos.
// std::string se inicializa por defecto a una cadena vacía.
// Para demostrar sus valores (ejecutar en un compilador real):
std::cout << "cadenaGlobal: '" << cadenaGlobal << "'\n";
std::cout << "enteroGlobal: " << enteroGlobal << "\n";
// std::cout << "enteroLocal: " << enteroLocal << "\n"; // ¡Cuidado! Valor indeterminado.
std::cout << "cadenaLocal: '" << cadenaLocal << "'\n";
return 0;
}
Ejercicio 2.11
Distinción entre declaración y definición con extern.
Una declaración hace que un nombre sea conocido en un ámbito determinado; una definición crea la entidad asociada con el nombre. Una variable solo puede definirse una vez, pero puede declararse muchas veces.
// (a) extern int valorInicial = 1024;
// Esto es una DEFINICIÓN. Cualquier declaración que incluya una inicialización se convierte en una definición.
// El uso de 'extern' aquí indica que la variable *podría* estar definida en otro lugar,
// pero la inicialización fuerza una definición en este punto.
// (b) int contador;
// Esto es una DECLARACIÓN y una DEFINICIÓN. Si es una variable global o estática,
// se inicializa por defecto a 0. Si es local y de tipo incorporado, su valor es indeterminado.
// En cualquier caso, se asigna memoria para ella.
// (c) extern int valorExterno;
// Esto es una DECLARACIÓN. Indica al compilador que la variable 'valorExterno' está definida
// en otra parte (usualmente en otro archivo fuente). No se asigna memoria para ella en este punto,
// simplemente se hace saber que existe.
Ejercicio 2.12
Identificadores de variables válidos e inválidos en C++.
int main() {
// int double = 3.14; // Inválido: 'double' es una palabra clave reservada.
int _ = 0; // Válido: '_' es un identificador legal (aunque no recomendado para evitar conflictos).
// int atrapar-22; // Inválido: '-' no es un carácter permitido en identificadores (excepto en literales de cadena).
// int 1_o_2 = 1; // Inválido: Los identificadores no pueden comenzar con un dígito.
double Doble = 3.14; // Válido: 'Doble' es diferente de 'double' debido a la capitalización.
return 0;
}
Ejercicio 2.13
Entendiendo el ámbito de las variables.
int global_i = 42; // Variable global 'global_i'
int main() {
int local_i = 100; // Variable local 'local_i' (oculta la global_i en este ámbito)
int j = local_i; // 'j' se inicializa con el valor de 'local_i', que es 100.
std::cout << "El valor de j es: " << j << std::endl; // Salida: 100
std::cout << "El valor de global_i (usando ::) es: " << ::global_i << std::endl; // Salida: 42
return 0;
}
Ejercicio 2.14
Análisis del ámbito de una variable en un bucle for.
#include <iostream>
int main() {
int i_externa = 100, suma = 0; // 'i_externa' se define y se inicializa aquí.
// El 'for' define una nueva variable 'i_interna'.
// Esta 'i_interna' es una variable local al bucle y oculta a 'i_externa' dentro del bucle.
for (int i_interna = 0; i_interna != 10; ++i_interna) {
suma += i_interna; // 'suma' acumula los valores de 'i_interna' (0 a 9).
}
// Después del bucle, 'i_interna' deja de existir.
// La 'i_externa' original sigue siendo accesible.
// Salida: 'i_externa' (100) y 'suma' (0+1+...+9 = 45).
std::cout << i_externa << " " << suma << std::endl; // Salida: 100 45
return 0;
}
Sección 2.3 Tipos Compuestos
2.3.1 Referencias
Una referencia es un alias para un objeto existente. Una vez inicializada, una referencia siempre permanece ligada al objeto al que se inicializó. Es fundamental que las referencias se inicialicen en el momento de su definición, ya que no son objetos en sí mismas y no pueden reasignarse para referenciar otro objeto.
// Ejemplo de uso de referencias
int valorEntero = 42;
int &refValor = valorEntero; // 'refValor' se enlaza a 'valorEntero'.
// 'refValor' es ahora otro nombre para 'valorEntero'.
// Las referencias deben inicializarse. Esto sería un error:
// int &refSinInicializar;
// Operaciones a través de la referencia afectan al objeto original
refValor = 2; // Asigna 2 a 'valorEntero' (ahora valorEntero es 2)
int copiaValor = refValor; // 'copiaValor' toma el valor de 'valorEntero' (que es 2)
int &otraRef = refValor; // 'otraRef' se enlaza al mismo objeto que 'refValor' (es decir, a 'valorEntero')
// Las referencias no pueden enlazarse a literales o resultados de expresiones:
// int &refInvalida = 10; // Error: un literal no es un objeto.
// El tipo de la referencia y el tipo del objeto al que se enlaza deben coincidir:
// double valorDoble = 3.14;
// int &refTipoIncorrecto = valorDoble; // Error: 'refTipoIncorrecto' es de tipo int&,
// pero 'valorDoble' es de tipo double.
Ejercicio 2.15
Validez de las declaraciones de referencias.
int main() {
int valNumerico = 1.01; // Válido: 'valNumerico' se inicializa con 1 (truncamiento de 1.01).
// int &refAConstante = 1.01; // Inválido: Una referencia rvalue (como un literal o temporal) no puede
// enlazarse directamente a una referencia lvalue no-constante.
// Un literal no es un objeto que pueda ser referenciado por una lvalue reference.
int inicial = 0;
int &refValida = inicial; // Válido: 'refValida' se enlaza a un objeto int existente 'inicial'.
// int &refSinInicializar; // Inválido: Las referencias deben inicializarse al declararse.
return 0;
}
Ejercicio 2.16
Comportamiento de las asignaciones con referencias.
int main() {
int numeroEntero = 0, &refEntero = numeroEntero;
double valorDecimal = 0.0, &refDecimal = valorDecimal;
refDecimal = 3.14159; // Válido: Se asigna 3.14159 al objeto al que 'refDecimal' está ligada (valorDecimal).
refDecimal = refEntero; // Válido: Se asigna el valor del objeto al que 'refEntero' está ligada (numeroEntero, que es 0)
// al objeto al que 'refDecimal' está ligada (valorDecimal).
// valorDecimal ahora es 0.0.
numeroEntero = refDecimal; // Válido: Se asigna el valor de 'valorDecimal' (0.0) a 'numeroEntero'.
// Se produce una conversión implícita de double a int (truncando a 0).
refEntero = valorDecimal; // Válido: Se asigna el valor de 'valorDecimal' (0.0) al objeto ligado a 'refEntero' (numeroEntero).
// Se produce una conversión implícita de double a int (truncando a 0).
return 0;
}
Ejercicio 2.17
Efecto de las referencias en las variables originales.
#include <iostream>
int main() {
int variableOriginal, &referenciaVariable = variableOriginal;
variableOriginal = 5; // 'variableOriginal' se asigna a 5.
// Dado que 'referenciaVariable' es un alias para 'variableOriginal',
// 'referenciaVariable' también "vale" 5.
referenciaVariable = 10; // Se asigna 10 al objeto al que 'referenciaVariable' está ligada,
// que es 'variableOriginal'.
// Ahora 'variableOriginal' es 10.
// La salida mostrará el valor actual de 'variableOriginal' y 'referenciaVariable',
// que es el mismo, ya que 'referenciaVariable' es solo otro nombre para 'variableOriginal'.
std::cout << variableOriginal << " " << referenciaVariable << std::endl; // Salida: 10 10
return 0;
}
2.3.2 Punteros
Un puntero es un objeto que almacena la dirección de memoria de otro objeto. A diferencia de las referencias, los punteros son objetos en sí mismos, lo que significa que pueden ser asignados y copiados. También pueden apuntar a diferentes objetos a lo largo de su vida útil. Un puntero no necesita ser inicializado en el momento de su definición, aunque es una buena práctica inicializarlos con nullptr si no se conoce la dirección a la que deben apuntar.
Obtener la dirección de un objeto: Para obtener la dirección de un objeto, se utiliza el operador de dirección (&).
int valorObjetivo = 42;
int *ptr = &valorObjetivo; // 'ptr' almacena la dirección de 'valorObjetivo'.
Acceder a un objeto a través de un puntero: Si un puntero apunta a un objeto, se puede acceder a ese objeto utilizando el operador de desreferencia (*).
int otroValor = 42;
int *puntero = &otroValor;
std::cout << *puntero; // Imprime el valor del objeto al que apunta 'puntero' (42).
Ejercicio 2.18
Modificación de un puntero y el objeto al que apunta.
#include <iostream>
int main() {
int valorInicial = 1;
int segundoValor = 0;
int *punteroEntero = &valorInicial; // 'punteroEntero' apunta a 'valorInicial' (1).
// Asigna la dirección de 'segundoValor' al puntero 'punteroEntero'.
// Ahora 'punteroEntero' apunta a 'segundoValor'.
punteroEntero = &segundoValor;
// Desreferencia 'punteroEntero' (accede a 'segundoValor') y asigna 5.
// 'segundoValor' ahora es 5.
*punteroEntero = 5;
// 'valorInicial' no ha sido afectado por el puntero desde el primer cambio, sigue siendo 1.
// 'segundoValor' ha sido modificado a 5.
std::cout << valorInicial << " " << segundoValor << std::endl; // Salida: 1 5
return 0;
}
Ejercicio 2.19
Comparación entre referencias y punteros.
| Referencias | Punteros |
|---|---|
| No son objetos en sí mismas. | Son objetos en sí mismos. |
| Una vez inicializadas, no pueden desligarse de su objeto. | Pueden reasignarse para apuntar a diferentes objetos durante su vida útil. |
| Deben inicializarse al definirse. | No necesitan inicializarse al definirse, pero los punteros no inicializados tienen un valor indeterminado. |
| No tienen dirección propia (usan la del objeto). | Tienen su propia dirección de memoria. |
| No pueden ser nulas. | Pueden ser nulos (apuntar a nullptr). |
| No se puede realizar aritmética de punteros con referencias. | Se puede realizar aritmética de punteros. |
Ejercicio 2.20
Uso de punteros para modificar el valor de un objeto.
#include <iostream>
int main() {
int valorBase = 42;
// 'ptrAValor' es un puntero que almacena la dirección de 'valorBase'.
int* ptrAValor = &valorBase;
// Desreferencia 'ptrAValor' para acceder a 'valorBase'.
// Multiplica el valor de 'valorBase' por sí mismo y lo asigna de nuevo a 'valorBase'.
*ptrAValor = *ptrAValor * *ptrAValor;
// Muestra el nuevo valor de 'valorBase'.
std::cout << valorBase << std::endl; // Salida: 1764 (42 * 42)
return 0;
}
Ejercicio 2.21
Análisis de las inicializaciones de punteros.
int main() {
int numero = 0;
// double* ptrDoble = № // Inválido: Intentando asignar la dirección de un 'int'
// a un puntero a 'double'. Los tipos deben coincidir.
// int* ptrEntero = numero; // Inválido: No se puede asignar directamente un valor entero
// a un puntero. Un puntero espera una dirección de memoria.
// Solo se puede asignar un literal de entero 0 (o nullptr) para
// inicializar un puntero nulo.
int* p = № // Válido: 'p' es un puntero a 'int' y se inicializa
// con la dirección de un objeto 'int' ('numero').
return 0;
}
Ejercicio 2.22
Interpretación de un puntero en una condición if.
// if (p)
// Esta condición evalúa si el puntero 'p' es nulo o no.
// Si 'p' contiene una dirección válida (no es nullptr), la condición es verdadera.
// Si 'p' es nullptr, la condición es falsa.
// Es una forma de comprobar si el puntero apunta a algo.
// if (*p)
// Esta condición desreferencia el puntero 'p' y evalúa el valor del objeto al que apunta.
// Si el puntero 'p' es válido (no nulo) y el valor del objeto al que apunta es distinto de cero,
// la condición es verdadera.
// Si el puntero 'p' es nullptr, desreferenciarlo es un comportamiento indefinido y probablemente causará un fallo.
// Si el puntero 'p' es válido pero el objeto al que apunta tiene un valor de cero, la condición es falsa.
// Es una forma de comprobar el valor del objeto apuntado.
Ejercicio 2.23
¿Se puede saber si un puntero apunta a un objeto válido?
En general, no. Si un puntero de tipo incorporado (como int* o char*) no se inicializa al definirse, contendrá un valor "basura" (indeterminado), que podría ser cualquier cosa. No hay forma fiable de saber si ese valor corresponde a una dirección de memoria válida o si esa dirección de memoria, aunque válida, pertenece a un objeto al que se tiene permiso de acceso y que es de un tipo compatible. Desreferenciar un puntero no inicializado es un error grave que conduce a un comportamiento indefinido.
La única excepción es si el puntero se inicializa explícitamente a nullptr (o 0), en cuyo caso se puede comprobar si es nulo. Siempre es buena práctica inicializar los punteros a nullptr si no se tiene una dirección de objeto concreta para asignarles en el momento de la definición.
Ejercicio 2.24
Uso de punteros void* y compatibilidad de tipos.
int main() {
int entero = 42;
void* ptrGenerico = &entero; // Válido: Un puntero 'void*' puede almacenar la dirección de cualquier tipo de objeto.
// No se sabe el tipo del objeto al que apunta, por lo que no se puede desreferenciar directamente.
// long* ptrLargo = &entero; // Inválido: Un puntero a 'long' no puede almacenar la dirección de un objeto 'int'.
// Los tipos del puntero y del objeto apuntado deben ser compatibles.
return 0;
}
Ejercicio 2.25
Identificación de tipos en declaraciones múltiples.
int main() {
int* ptrEntero, entero, &referencia = entero;
// 'ptrEntero' es un puntero a int.
// 'entero' es una variable de tipo int.
// 'referencia' es una referencia a int, ligada a 'entero'.
int valInt, *ptrNulo = 0;
// 'valInt' es una variable de tipo int.
// 'ptrNulo' es un puntero a int, inicializado a nullptr (equivalente a 0).
int* punteroUno, punteroDos;
// 'punteroUno' es un puntero a int.
// 'punteroDos' es una variable de tipo int (el asterisco solo se aplica a 'punteroUno').
return 0;
}
Ejercicio 2.26
Reglas de inicialización y modificación de objetos const.
int main() {
// const int buffer; // Inválido: Un objeto 'const' debe ser inicializado al definirse.
int contador = 0; // Válido: 'contador' es un entero normal, inicializado a 0.
const int tamano = contador; // Válido: 'tamano' es un 'const int' inicializado con el valor actual de 'contador' (0).
// El valor de 'contador' se copia, no se enlaza.
++contador; // Válido: 'contador' no es 'const', por lo que puede modificarse (ahora es 1).
// ++tamano; // Inválido: 'tamano' es un objeto 'const', no puede ser modificado después de su inicialización.
return 0;
}
Ejercicio 2.27
Validez de las declaraciones con const, referencias y punteros.
int i = -1; // Un entero no-constante
const int i_const = -1; // Un entero constante
// (a) int i_var = -1, &ref_invalida = 0;
// Inválido: Una referencia no-constante (int&) no puede enlazarse a un literal (0),
// que es un rvalue. Una referencia a const (const int&) sí podría.
// (b) int *const ptr_constante = &i2;
// Inválido si 'i2' no está definida o es 'const int'.
// Asumiendo que 'i2' es `int i2;`, entonces sería válido:
// int i2;
// int *const ptr_constante = &i2; // Válido: ptr_constante es un puntero constante a un int no-constante.
// (c) const int c_entero = -1, &ref_c = 0;
// Válido: 'c_entero' es un const int inicializado. 'ref_c' es una referencia a const int
// enlazada a un literal (0), lo cual es válido.
// (d) const int *const ptr_c_c = &i2;
// Inválido si 'i2' no está definida. Asumiendo `int i2;`:
// const int *const ptr_c_c = &i2; // Válido: ptr_c_c es un puntero constante a un int constante.
// Puede apuntar a un int no-constante, pero no puede modificarlo a través del puntero.
// (e) const int *puntero_c = &i2;
// Inválido si 'i2' no está definida. Asumiendo `int i2;`:
// const int *puntero_c = &i2; // Válido: puntero_c es un puntero a un int constante.
// (f) const int &const ref_constante_c;
// Inválido: Una referencia no puede ser constante a sí misma, solo su enlace es constante.
// Además, las referencias deben inicializarse.
// (g) const int i_con_i = i_const, &r_i = i_con_i;
// Válido: 'i_con_i' es un const int inicializado con 'i_const'.
// 'r_i' es una referencia a const int ligada a 'i_con_i'.
Ejercicio 2.28
Validez de las declaraciones de const punteros y referencias (sin inicialización explícita).
int main() {
// int i; // Necesario para algunas declaraciones a continuación
// const int ic = 0; // Necesario para algunas declaraciones a continuación
// (a) int *const cp;
// Inválido: 'cp' es un puntero constante a int. Un puntero constante debe ser inicializado
// con la dirección de un objeto al momento de su definición.
// (b) int *p1, *const p2;
// Inválido: 'p2' es un puntero constante a int y no está inicializado.
// (c) const int ic_sin_init, &r = ic_sin_init;
// Inválido: 'ic_sin_init' es un 'const int' y no está inicializado.
// Consecuentemente, 'r' no puede ligarse a un objeto no inicializado.
// (d) const int *const p3;
// Inválido: 'p3' es un puntero constante a un int constante y no está inicializado.
const int *p;
// (e) const int *p;
// Válido: 'p' es un puntero a un int constante. Los punteros a constantes (donde
// el puntero en sí no es 'const') no necesitan ser inicializados, aunque es una
// buena práctica inicializarlos a `nullptr` si no apuntan a nada específico.
return 0;
}
Ejercicio 2.29
Validez de las asignaciones con const y punteros.
int main() {
int i = 0;
const int ic = 0;
int *p1 = &i;
const int *const p3 = ⁣ // p3 es un puntero constante a un entero constante.
i = ic; // (a) Válido: Asigna el valor de 'ic' (const) a 'i' (no-const).
// Se copia el valor, la constancia de 'ic' no se propaga.
// p1 = p3; // (b) Inválido: 'p1' es un puntero a un int no-constante.
// 'p3' es un puntero a un int constante.
// No se puede asignar directamente un puntero a const a un puntero no-const,
// ya que permitiría modificar un objeto const a través de 'p1'.
// p1 = ⁣ // (c) Inválido: Igual que (b). La dirección de un objeto 'const int'
// no puede asignarse a un 'int*'.
// p3 = ⁣ // (d) Inválido: 'p3' es un puntero constante ('*const p3').
// Su valor (la dirección a la que apunta) no puede cambiarse una vez inicializado.
// Aunque aquí se inicializa a &ic, reasignarlo es un error.
// int *const p2 = &i; // Asumamos esta declaración para la siguiente línea
// p2 = p1; // (e) Inválido: 'p2' es un puntero constante ('*const p2').
// Su valor no puede cambiarse.
// ic = *p3; // (f) Inválido: 'ic' es un 'const int'. No puede modificarse después de su inicialización.
return 0;
}
Ejercicio 2.30
Identificación de const de alto nivel y bajo nivel.
const int valor_const = 0; // 'valor_const' es un 'const int'.
// Es un const de alto nivel: el objeto en sí mismo es constante.
int valor_no_const = valor_const; // 'valor_no_const' es un 'int' normal.
int *ptr_no_const = &valor_no_const; // 'ptr_no_const' es un puntero a 'int'.
// No tiene const de alto nivel ni de bajo nivel.
int &ref_no_const = valor_no_const; // 'ref_no_const' es una referencia a 'int'.
// No tiene const de alto nivel ni de bajo nivel.
const int *ptr_a_const = &valor_const; // 'ptr_a_const' es un puntero a 'const int'.
// Es un const de bajo nivel: apunta a un objeto constante.
// El puntero en sí no es constante, puede apuntar a otra cosa.
const int *const ptr_const_a_const = &valor_const; // 'ptr_const_a_const' es un puntero constante a un 'const int'.
// Tiene const de alto nivel (el puntero es constante)
// y const de bajo nivel (apunta a un objeto constante).
const int &ref_a_const = valor_const; // 'ref_a_const' es una referencia a 'const int'.
// Es un const de bajo nivel: se refiere a un objeto constante.
// (Las referencias siempre son constantes en su enlace,
// pero la 'const' de bajo nivel se refiere al tipo del objeto).
Ejercicio 2.31
Combinación de reglas de const.
int a = 0;
const int ca = 0;
int *p1 = &a; // p1 es un int*, no const.
const int *p2 = &ca; // p2 es un const int*, const de bajo nivel.
const int *const p3 = &ca; // p3 es un const int* const, const de alto y bajo nivel.
int &r1 = a; // r1 es un int&, no const.
const int &r2 = ca; // r2 es un const int&, const de bajo nivel.
// r1 = ca; // Válido: Asigna el valor de 'ca' (const int) a 'a' (int) a través de 'r1'.
// 'ca' es const de alto nivel, pero su valor se puede copiar a un no-const.
// p1 = p2; // Inválido: 'p1' es un 'int*' (no-const de bajo nivel).
// 'p2' es un 'const int*' (const de bajo nivel).
// No se puede asignar un puntero a const a un puntero no-const
// (se perdería la constancia).
// p2 = p1; // Válido: 'p2' es un 'const int*' (const de bajo nivel).
// 'p1' es un 'int*' (no-const de bajo nivel).
// Se puede asignar un puntero no-const a un puntero a const
// (se añade constancia de bajo nivel).
// p1 = p3; // Inválido: 'p1' es 'int*' (no-const de bajo nivel).
// 'p3' es 'const int* const' (const de alto y bajo nivel).
// Error de const de bajo nivel.
// p2 = p3; // Válido: 'p2' es 'const int*' (const de bajo nivel).
// 'p3' es 'const int* const' (const de alto y bajo nivel).
// Se puede asignar un puntero constante a un puntero no constante
// (se pierde la constancia de alto nivel, lo cual es permitido).
Ejercicio 2.32
Inicialización de punteros nulos.
int main() {
// int nulo = 0, *p = nulo;
// Inválido: No se puede asignar una variable entera ('nulo') a un puntero ('p')
// directamente. Solo el literal entero 0 se puede usar para inicializar un puntero nulo
// (o el más moderno y seguro `nullptr`).
// Corrección:
int nulo = 0; // Esta variable 'nulo' ya no es relevante para el puntero nulo.
int *punteroNulo = nullptr; // Forma moderna y preferida de inicializar un puntero nulo.
// O:
// int *punteroNulo2 = 0; // Forma antigua válida.
return 0;
}
Ejercicio 2.33
Análisis de las deducciones de tipo con auto y las asignaciones resultantes.
Dadas las siguientes declaraciones iniciales:
int i_var = 0, &r_i_var = i_var;
const int ci_var = i_var, &cr_ci_var = ci_var;
Y las deducciones con auto (como en el Ejercicio 2.34):
auto a_auto = r_i_var; // a_auto es un int (la const de alto nivel de r_i_var se ignora)
auto b_auto = ci_var; // b_auto es un int (la const de alto nivel de ci_var se ignora)
auto c_auto = cr_ci_var; // c_auto es un int (cr_ci_var es un alias para ci_var, su const es de alto nivel)
auto d_auto = &i_var; // d_auto es un int* (la dirección de un int es int*)
auto e_auto = &ci_var; // e_auto es const int* (la dirección de un objeto const es un const de bajo nivel)
const auto f_auto = ci_var; // f_auto es const int (el tipo deducido es int, luego se aplica const)
auto &g_auto = ci_var; // g_auto es const int& (g_auto se enlaza a un objeto const)
Las siguientes asignaciones y su validez:
a_auto = 42; // Válido: 'a_auto' es un 'int', se puede modificar.
b_auto = 42; // Válido: 'b_auto' es un 'int', se puede modificar.
c_auto = 42; // Válido: 'c_auto' es un 'int', se puede modificar.
*d_auto = 42; // Válido: 'd_auto' es 'int*', *d_auto es 'int', se puede modificar el objeto apuntado.
// e_auto = 42; // Inválido: 'e_auto' es 'const int*', se le está intentando asignar un 'int' (valor),
// no una dirección de puntero.
// Además, si intentáramos `*e_auto = 42;`, sería inválido porque *e_auto es 'const int'.
e_auto = &c_auto; // Válido: 'e_auto' es 'const int*', se le asigna la dirección de un 'int'.
// Esto es válido porque un puntero a 'const' puede apuntar a un objeto no-'const'.
// f_auto = 42; // Inválido: 'f_auto' es 'const int', no se puede modificar.
// g_auto = 42; // Inválido: 'g_auto' es 'const int&', se refiere a 'ci_var' que es 'const', no se puede modificar a través de ella.
Ejercicio 2.34
Demostración del comportamiento de auto.
#include <iostream>
int main() {
int i_original = 0, &r_original = i_original;
auto a_deducido = r_original; // a_deducido es un int (la referencia de r_original se ignora, la const de alto nivel también)
const int ci_original = i_original, &cr_ci_original = ci_original;
auto b_deducido = ci_original; // b_deducido es un int (la const de alto nivel de ci_original se ignora)
auto c_deducido = cr_ci_original; // c_deducido es un int (cr_ci_original es un alias para ci_original cuya const es de alto nivel)
auto d_deducido = &i_original; // d_deducido es un int* (la dirección de un int es int*)
auto e_deducido = &ci_original; // e_deducido es const int* (la dirección de un objeto const es un const de bajo nivel)
const auto f_deducido = ci_original; // f_deducido es const int (el tipo deducido de ci_original es int; luego se aplica const)
auto &g_deducido = ci_original; // g_deducido es una const int& que está ligada a ci_original
std::cout << "Valores iniciales: i_original=" << i_original << ", ci_original=" << ci_original << std::endl;
std::cout << "Tipos deducidos y valores iniciales:" << std::endl;
std::cout << "a_deducido (int): " << a_deducido << std::endl;
std::cout << "b_deducido (int): " << b_deducido << std::endl;
std::cout << "c_deducido (int): " << c_deducido << std::endl;
std::cout << "d_deducido (int*): " << d_deducido << ", valor apuntado: " << *d_deducido << std::endl;
std::cout << "e_deducido (const int*): " << e_deducido << ", valor apuntado: " << *e_deducido << std::endl;
std::cout << "f_deducido (const int): " << f_deducido << std::endl;
std::cout << "g_deducido (const int&): " << g_deducido << std::endl;
a_deducido = 42;
b_deducido = 42;
c_deducido = 42;
*d_deducido = 42; // Modifica i_original
e_deducido = &a_deducido; // Reasigna el puntero (legal, ya que e_deducido no es const)
// f_deducido = 42; // Error: f_deducido es const int
// g_deducido = 42; // Error: g_deducido es const int& y se refiere a un const
std::cout << "\nValores después de algunas asignaciones:" << std::endl;
std::cout << "i_original (modificado por *d_deducido): " << i_original << std::endl; // Ahora 42
std::cout << "a_deducido: " << a_deducido << std::endl; // Ahora 42
std::cout << "b_deducido: " << b_deducido << std::endl; // Ahora 42
std::cout << "c_deducido: " << c_deducido << std::endl; // Ahora 42
std::cout << "d_deducido (apunta a i_original): " << *d_deducido << std::endl; // Ahora 42
std::cout << "e_deducido (ahora apunta a a_deducido): " << *e_deducido << std::endl; // Ahora 42
return 0;
}
Ejercicio 2.35
Deducción de tipos para const con auto y decltype.
const int val_c = 42;
auto j_auto = val_c; // j_auto: int (const de alto nivel de val_c se ignora)
const auto &k_auto_ref = val_c; // k_auto_ref: const int& (auto deduce int, luego se aplica const&)
auto *p_auto_ptr = &val_c; // p_auto_ptr: const int* (la dirección de un const int es un puntero a const int)
const auto j2_auto_c = val_c; // j2_auto_c: const int (auto deduce int, luego se aplica const)
const auto &k2_auto_c_ref = val_c; // k2_auto_c_ref: const int& (auto deduce int, luego se aplica const&)
Ejercicio 2.36
Comportamiento de decltype con paréntesis en una variable.
int main() {
int valA = 3, valB = 4;
decltype(valA) refC = valA; // refC es de tipo int (el tipo de la variable valA).
decltype((valB)) refD = valA; // refD es de tipo int& (los paréntesis en un nombre de variable
// hacen que decltype devuelva una referencia si la variable es un lvalue).
// Por lo tanto, refD se convierte en una referencia a valA.
++refC; // Incrementa refC. valA permanece 3, refC se convierte en 4.
++refD; // Incrementa el objeto al que refD está ligada, que es valA. valA se convierte en 4.
std::cout << "valA: " << valA << std::endl; // Salida: 4
std::cout << "valB: " << valB << std::endl; // Salida: 4
std::cout << "refC: " << refC << std::endl; // Salida: 4
std::cout << "refD (referencia a valA): " << refD << std::endl; // Salida: 4
return 0;
}
Ejercicio 2.37
Deducción de tipo con decltype y una expresión de asignación.
int main() {
int x = 3, y = 4;
decltype(x) z = x; // z: int (el tipo de x). z se inicializa con el valor de x (3).
decltype(x = y) w = x; // w: int& (el tipo de una expresión de asignación es una referencia
// al tipo del operando izquierdo).
// La expresión `x = y` produce un lvalue de tipo `int&`.
// La asignación `w = x` significa que w se liga a x.
// La expresión `x = y` no se evalúa aquí, solo se usa su tipo.
std::cout << "x: " << x << std::endl; // Salida: 3
std::cout << "y: " << y << std::endl; // Salida: 4
std::cout << "z: " << z << std::endl; // Salida: 3
std::cout << "w (referencia a x): " << w << std::endl; // Salida: 3
return 0;
}
Ejercicio 2.38
Diferencias clave entre decltype y auto.
Tanto decltype como auto se utilizan para la deducción automática de tipos de variables, pero tienen diferencias fundamentales en cómo operan y qué propiedades del tipo preservan.
Deducción de Tipo de auto:
autodeduce el tipo de una variable a partir de su inicializador.- Generalmente,
autoignora los calificadores de tipo de alto nivel (comoconstde alto nivel y referencias) del inicializador. Se deduce el tipo "base". - Si se desea conservar las propiedades de
consto de referencia, se deben especificar explícitamente (const autooauto&).
Deducción de Tipo de decltype:
decltypededuce el tipo de una variable directamente de la expresión que se le pasa, preservando con precisión todos los calificadores de tipo, incluyendoconstde alto y bajo nivel, y referencias.- Si la expresión es un nombre de variable,
decltypedevuelve el tipo de esa variable, incluyendoconstde alto nivel y referencias. - Si la expresión es un lvalue (algo que tiene una dirección de memoria, como una variable entre paréntesis),
decltypedevuelve un tipo de referencia.
Ejemplos de auto y decltype con tipos similares:
int entero = 5;
auto deducido_auto = entero; // deducido_auto es int (auto deduce el tipo base)
decltype(entero) deducido_decltype = entero; // deducido_decltype es int (decltype toma el tipo exacto)
En este caso, ambos deducen el tipo int porque la expresión entero es simplemente un int y no una referencia ni const de alto nivel en la forma en que auto lo ignore.
Ejemplos de auto y decltype con tipos diferentes:
int val_original = 5;
const int& ref_constante = val_original;
auto variable_auto = ref_constante; // variable_auto es int (auto ignora la referencia y el const de alto nivel)
decltype(ref_constante) variable_decltype = val_original; // variable_decltype es const int& (decltype preserva la referencia y el const)
En este segundo ejemplo, auto deduce int, mientras que decltype deduce const int&, manteniendo la naturaleza referencial y la constancia de la expresión original.
En resumen, decltype es más preciso y "literal" en su deducción de tipos, incluyendo propiedades como referencias y constancia, mientras que auto a menudo simplifica el tipo, eliminando la constancia de alto nivel y las referencias a menos que se especifique explícitamente.
Ejercicio 2.39
Error de compilación por falta de punto y coma después de la definición de una estructura.
struct Foo { /* Este es un struct vacío */ }; // ¡IMPORTANTE: El punto y coma es obligatorio aquí!
int main() {
return 0;
}
Si se omite el punto y coma después de la declaración de una estructura (struct Foo { /* ... */ }), el compilador generará un error similar a:
Error: se esperaba ';' después de la definición de la estructura
Ejercicio 2.40
Definición de la estructura Sale\_data (o DatosVenta).
#include <string> // Necesario para std::string
struct Sales_data {
std::string numeroISBN; // Identificador del libro (ej: ISBN)
std::string tituloLibro; // Título del libro
unsigned unidadesVendidas = 0; // Número de unidades vendidas
double ingresosTotal = 0.0; // Ingresos generados por las ventas
double precioUnitario = 0.0; // Precio de venta por unidad
};
Ejercicio 2.41
Ejercicios con la estructura Sales\_data.
1. Lectura y suma de dos transacciones:
#include <iostream>
#include <string> // Necesario para std::string
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sales_data transaccion1, transaccion2;
double precio = 0;
std::cout << "Introduce los datos de la primera transacción (ISBN, unidades, precio):" << std::endl;
std::cin >> transaccion1.bookNo >> transaccion1.units_sold >> precio;
transaccion1.revenue = transaccion1.units_sold * precio;
std::cout << "Introduce los datos de la segunda transacción (ISBN, unidades, precio):" << std::endl;
std::cin >> transaccion2.bookNo >> transaccion2.units_sold >> precio;
transaccion2.revenue = transaccion2.units_sold * precio;
if (transaccion1.bookNo == transaccion2.bookNo) {
unsigned totalUnidades = transaccion1.units_sold + transaccion2.units_sold;
double totalIngresos = transaccion1.revenue + transaccion2.revenue;
std::cout << transaccion1.bookNo << " " << totalUnidades << " " << totalIngresos << " ";
if (totalUnidades != 0) {
std::cout << totalIngresos / totalUnidades << std::endl;
} else {
std::cout << "(sin ventas)" << std::endl;
}
return 0;
} else {
std::cerr << "Error: Los datos deben referirse al mismo ISBN." << std::endl;
return -1;
}
}
2. Lectura y suma de múltiples transacciones con el mismo ISBN:
#include <iostream>
#include <string>
struct Sales_data {
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
int main() {
Sales_data totalAcumulado;
double precioTransaccion;
std::cout << "Introduce la primera transacción (ISBN, unidades, precio):" << std::endl;
if (std::cin >> totalAcumulado.bookNo >> totalAcumulado.units_sold >> precioTransaccion) {
totalAcumulado.revenue = totalAcumulado.units_sold * precioTransaccion;
Sales_data transaccionNueva;
while (std::cin >> transaccionNueva.bookNo >> transaccionNueva.units_sold >> precioTransaccion) {
transaccionNueva.revenue = transaccionNueva.units_sold * precioTransaccion;
if (totalAcumulado.bookNo == transaccionNueva.bookNo) {
totalAcumulado.units_sold += transaccionNueva.units_sold;
totalAcumulado.revenue += transaccionNueva.revenue;
} else {
// Si el ISBN cambia, imprime el total del ISBN anterior
std::cout << totalAcumulado.bookNo << " " << totalAcumulado.units_sold << " " << totalAcumulado.revenue << " ";
if (totalAcumulado.units_sold != 0)
std::cout << totalAcumulado.revenue / totalAcumulado.units_sold << std::endl;
else
std::cout << "(sin ventas)" << std::endl;
// Reinicia totalAcumulado con la nueva transacción
totalAcumulado = transaccionNueva;
}
}
// Imprime el total para el último ISBN procesado
std::cout << totalAcumulado.bookNo << " " << totalAcumulado.units_sold << " " << totalAcumulado.revenue << " ";
if (totalAcumulado.units_sold != 0)
std::cout << totalAcumulado.revenue / totalAcumulado.units_sold << std::endl;
else
std::cout << "(sin ventas)" << std::endl;
return 0;
} else {
std::cerr << "Error: No se introdujeron datos iniciales." << std::endl;
return -1;
}
}
Ejercicio 2.42
Definición de Sales\_data en un archivo de cabecera y uso en los programas anteriores.
Archivo Sales\_data.h:
#ifndef SALES_DATA_H_
#define SALES_DATA_H_
#include <string> // Necesario para std::string
struct Sales_data {
std::string bookNo; // Número ISBN
unsigned units_sold = 0; // Cantidad de unidades vendidas
double revenue = 0.0; // Ingresos totales por ventas
};
#endif // SALES_DATA_H_
Uso en el programa para sumar dos transacciones:
#include <iostream>
#include "Sales_data.h" // Ahora incluimos nuestro propio archivo de cabecera
int main() {
Sales_data primeraVenta, segundaVenta;
double precioUnitario = 0;
std::cout << "Introduce los detalles de la primera venta (ISBN, unidades, precio):" << std::endl;
std::cin >> primeraVenta.bookNo >> primeraVenta.units_sold >> precioUnitario;
primeraVenta.revenue = primeraVenta.units_sold * precioUnitario;
std::cout << "Introduce los detalles de la segunda venta (ISBN, unidades, precio):" << std::endl;
std::cin >> segundaVenta.bookNo >> segundaVenta.units_sold >> precioUnitario;
segundaVenta.revenue = segundaVenta.units_sold * precioUnitario;
if (primeraVenta.bookNo == segundaVenta.bookNo) {
unsigned totalUnidades = primeraVenta.units_sold + segundaVenta.units_sold;
double totalIngresos = primeraVenta.revenue + segundaVenta.revenue;
std::cout << primeraVenta.bookNo << " " << totalUnidades << " " << totalIngresos << " ";
if (totalUnidades != 0)
std::cout << totalIngresos / totalUnidades << std::endl;
else
std::cout << "(sin ventas)" << std::endl;
return 0;
} else {
std::cerr << "Error: Los ISBN de las ventas no coinciden." << std::endl;
return -1;
}
}
Uso en el programa para sumar múltiples transacciones por ISBN:
#include <iostream>
#include "Sales_data.h" // Ahora incluimos nuestro propio archivo de cabecera
int main() {
Sales_data totalVentas;
double precioActual;
std::cout << "Introduce los datos de la primera transacción (ISBN, unidades, precio):" << std::endl;
if (std::cin >> totalVentas.bookNo >> totalVentas.units_sold >> precioActual) {
totalVentas.revenue = totalVentas.units_sold * precioActual;
Sales_data nuevaTransaccion;
while (std::cin >> nuevaTransaccion.bookNo >> nuevaTransaccion.units_sold >> precioActual) {
nuevaTransaccion.revenue = nuevaTransaccion.units_sold * precioActual;
if (totalVentas.bookNo == nuevaTransaccion.bookNo) {
totalVentas.units_sold += nuevaTransaccion.units_sold;
totalVentas.revenue += nuevaTransaccion.revenue;
} else {
std::cout << totalVentas.bookNo << " " << totalVentas.units_sold << " " << totalVentas.revenue << " ";
if (totalVentas.units_sold != 0)
std::cout << totalVentas.revenue / totalVentas.units_sold << std::endl;
else
std::cout << "(sin ventas)" << std::endl;
totalVentas = nuevaTransaccion; // Inicia el nuevo total con la transacción actual
}
}
// Imprime el último total después de procesar todas las entradas
std::cout << totalVentas.bookNo << " " << totalVentas.units_sold << " " << totalVentas.revenue << " ";
if (totalVentas.units_sold != 0)
std::cout << totalVentas.revenue / totalVentas.units_sold << std::endl;
else
std::cout << "(sin ventas)" << std::endl;
return 0;
} else {
std::cerr << "Error: No se introdujeron datos iniciales." << std::endl;
return -1;
}
}
Capítulo 3: Cadenas de Caracteres, Vectores y Arreglos
Uso de la declaración using para espacios de nombres
La declaración using permite hacer visibles nombres específicos de un espacio de nombres (como std) sin necesidad de prefijarlos con el nombre del espacio de nombres completo. Esto puede simplificar el código, pero debe usarse con cuidado para evitar colisiones de nombres.
Ejercicio 3.1
Reescribe los ejercicios 1.9, 1.10 y 1.11 utilizando declaraciones using para std::cout, std::cin y std::endl.
Suma de 50 a 100:
#include <iostream>
using std::cout;
using std::endl;
int main() {
int sumaValores = 0, valorInicial = 50;
while (valorInicial <= 100) {
sumaValores += valorInicial;
valorInicial += 1;
}
cout << "La suma de 50 a 100 inclusive es: " << sumaValores << endl;
return 0;
}
Cuenta regresiva de 10 a 0:
#include <iostream>
using std::cout;
using std::endl;
int main() {
int cuenta = 10;
while (cuenta >= 0) {
cout << cuenta-- << endl;
}
return 0;
}
Números en un rango definido:
#include <iostream>
#include <algorithm> // Para std::min y std::max
using std::cin;
using std::cout;
using std::endl;
int main() {
int primero = 0, segundo = 0;
cout << "Introduce dos números enteros: " << endl;
cin >> primero >> segundo;
int valorMenor = std::min(primero, segundo);
int valorMayor = std::max(primero, segundo);
cout << "Los números en el rango de " << valorMenor << " a " << valorMayor << " son:" << endl;
while (valorMenor <= valorMayor) {
cout << valorMenor++ << endl;
}
return 0;
}
Alternativa a las declaraciones using individuales, se puede usar una directiva using namespace std; para importar todo el espacio de nombres std al ámbito actual. Esto es común en ejemplos cortos, pero se desaconseja en archivos de cabecera o proyectos grandes debido a posibles colisiones de nombres.
// Ejemplo de directiva using namespace
using namespace std;
Ejercicio 3.2
Escribe un programa que lea la entrada estándar línea por línea, y luego modifícalo para que lea palabra por palabra.
Lectura línea por línea: Utiliza std::getline para leer una línea completa, incluyendo espacios, hasta que se encuentre un carácter de nueva línea o el fin de archivo (Ctrl+Z).
#include <iostream>
#include <string>
using namespace std;
int main() {
string lineaEntrada;
cout << "Introduce texto (línea por línea, Ctrl+Z para finalizar):" << endl;
while (getline(cin, lineaEntrada)) { // Lee hasta el final de la línea
cout << "Línea leída: " << lineaEntrada << endl;
}
return 0;
}
Lectura palabra por palabra: Utiliza el operador >> de std::cin para leer palabras individuales, delimitadas por espacios en blanco, hasta que se encuentre el fin de archivo.
#include <iostream>
#include <string>
using namespace std;
int main() {
string palabraEntrada;
cout << "Introduce texto (palabra por palabra, Ctrl+Z para finalizar):" << endl;
while (cin >> palabraEntrada) { // Lee hasta el siguiente espacio en blanco
cout << "Palabra leída: " << palabraEntrada << endl;
}
return 0;
}
Ejercicio 3.3
Explica las diferencias entre el operador de entrada de cadenas (>>) y la función getline al manejar caracteres en blanco.
El operador de entrada de cadenas (>>) y la función getline en C++ tienen comportamientos distintos al procesar caracteres en blanco (espacios, tabulaciones, saltos de línea) de la entrada estándar:
1. Operador de entrada (>>) para std::string:
- Por defecto, el operador
>>omite cualquier carácter en blanco inicial de la entrada. - Comienza a leer caracteres a partir del primer carácter no blanco y continúa hasta que encuentra el siguiente carácter en blanco (espacio, tabulación, nueva línea) o el fin de la entrada.
- Solo extrae "palabras" o "tokens" individuales. Una vez que se detiene, el carácter en blanco que causó la detención (y cualquier otro carácter posterior) permanece en el búfer de entrada.
Ejemplo: Si el usuario introduce "Hola Mundo" y se usa std::cin >> miCadena;, miCadena contendrá "Hola", y " Mundo" permanecerá en el búfer de entrada.
2. Función getline(std::cin, std::string):
getlinelee toda una línea de texto desde el flujo de entrada.- No omite los caracteres en blanco iniciales (los incluye en la cadena resultante).
- Continúa leyendo caracteres hasta que encuentra el carácter de nueva línea (
'\n') o el fin de archivo. El carácter de nueva línea se consume del búfer pero no se almacena en la cadena. - La cadena resultante contendrá todos los caracteres leídos, incluidos los espacios internos.
Ejemplo: Si el usuario introduce "Hola Mundo" y se usa std::getline(std::cin, miCadena);, miCadena contendrá "Hola Mundo".
En resumen, si necesita leer entradas palabra por palabra, el operador >> es apropiado. Si necesita leer líneas completas de texto, incluyendo los espacios, getline es la elección correcta.
Ejercicio 3.4
Escribe un programa que lea dos cadenas y determine si son iguales. Si no lo son, indica cuál es mayor lexicográficamente. Luego, modifica el programa para comparar sus longitudes.
Comparación lexicográfica:
#include <iostream>
#include <string>
using namespace std;
int main() {
string cadenaUno, cadenaDos;
cout << "Introduce la primera cadena: " << endl;
cin >> cadenaUno;
cout << "Introduce la segunda cadena: " << endl;
cin >> cadenaDos;
if (cadenaUno == cadenaDos) {
cout << "Las dos cadenas son iguales." << endl;
} else if (cadenaUno > cadenaDos) {
cout << "La primera cadena es mayor lexicográficamente." << endl;
} else {
cout << "La segunda cadena es mayor lexicográficamente." << endl;
}
return 0;
}
Comparación por longitud:
#include <iostream>
#include <string>
using namespace std;
int main() {
string textoA, textoB;
cout << "Introduce la primera cadena: " << endl;
cin >> textoA;
cout << "Introduce la segunda cadena: " << endl;
cin >> textoB;
auto longitudA = textoA.size();
auto longitudB = textoB.size();
if (longitudA == longitudB) {
cout << "Ambas cadenas tienen la misma longitud: " << longitudA << endl;
} else if (longitudA > longitudB) {
cout << "La primera cadena es más larga." << endl;
} else {
cout << "La segunda cadena es más larga." << endl;
}
return 0;
}
Ejercicio 3.5
Escribe un programa que lea múltiples cadenas y las concatene. Luego, modifica el programa para que las cadenas concatenadas estén separadas por espacios.
Concatenación sin espacios:
#include <iostream>
#include <string>
using namespace std;
int main() {
char continuar = 's';
string fragmento, resultadoFinal;
cout << "Introduce cadenas de texto para concatenar (Ctrl+Z o 'n' para finalizar):" << endl;
while (cin >> fragmento) {
resultadoFinal += fragmento; // Concatena directamente
cout << "¿Deseas introducir otra cadena? (s/n): ";
cin >> continuar;
if (continuar == 'n' || continuar == 'N')
break;
cout << "Introduce la siguiente cadena: " << endl;
}
cout << "La cadena concatenada es: " << resultadoFinal << endl;
return 0;
}
Concatenación con espacios:
#include <iostream>
#include <string>
using namespace std;
int main() {
char continuar = 's';
string parteCadena, cadenaUnida;
cout << "Introduce cadenas de texto para unir (Ctrl+Z o 'n' para finalizar):" << endl;
while (cin >> parteCadena) {
if (cadenaUnida.empty()) { // Si es la primera parte, no añade un espacio inicial
cadenaUnida += parteCadena;
} else { // Para las partes siguientes, añade un espacio antes
cadenaUnida += " " + parteCadena;
}
cout << "¿Deseas continuar? (s/n): ";
cin >> continuar;
if (continuar == 'n' || continuar == 'N')
break;
cout << "Introduce la siguiente cadena: " << endl;
}
cout << "La cadena resultante es: " << cadenaUnida << endl;
return 0;
}
Ejercicio 3.6
Escribe un programa que utilice un bucle for basado en rangos para reemplazar todos los caracteres de una cadena por 'X'.
#include <iostream>
#include <string>
using namespace std;
int main() {
string textoUsuario;
cout << "Introduce una cadena de texto (puede contener espacios): " << endl;
getline(cin, textoUsuario); // Lee la línea completa
for (char &caracter : textoUsuario) { // Bucle basado en rangos, con referencia para modificar
caracter = 'X';
}
cout << "Cadena modificada: " << textoUsuario << endl;
return 0;
}
Ejercicio 3.7
¿Qué pasaría si en el ejercicio 3.6 se declara la variable de control del bucle for como char en lugar de auto&?
Si la variable de control del bucle se declara como char caracter : textoUsuario en lugar de char &caracter : textoUsuario (o auto &caracter : textoUsuario), el programa compilará y se ejecutará. Sin embargo, no producirá el resultado deseado.
Al usar char caracter, se está creando una copia de cada carácter de la cadena en cada iteración. La modificación caracter = 'X'; cambiará la copia temporal, pero no afectará al carácter original dentro de la cadena textoUsuario. Por lo tanto, la cadena original permanecerá sin cambios.
Para modificar los caracteres de la cadena directamente, es fundamental usar una referencia, como char &caracter o auto &caracter.
Ejercicio 3.8
Reescribe el programa del ejercicio 3.6 para reemplazar todos los caracteres de una cadena por 'X' utilizando un bucle while y subíndices.
#include <iostream>
#include <string>
using namespace std;
int main() {
string textoParaModificar;
cout << "Introduce una cadena de texto (puede contener espacios): " << endl;
getline(cin, textoParaModificar);
decltype(textoParaModificar.size()) indice = 0; // Utiliza decltype para el tipo del índice
while (indice < textoParaModificar.size()) { // Recorre la cadena mientras el índice sea válido
textoParaModificar[indice] = 'X'; // Accede y modifica el carácter por su subíndice
++indice;
}
cout << "Cadena modificada: " << textoParaModificar << endl;
return 0;
}
Ejercicio 3.9
Analiza el siguiente código: string s; cout << s\[0\] << endl;
Este código es incorrecto y tiene un comportamiento indefinido. Al declarar string s; sin inicializarla, la cadena s está vacía. Acceder a s[0] en una cadena vacía es un intento de acceder a un elemento fuera de los límites válidos del arreglo subyacente de la cadena. El estándar de C++ no garantiza ningún comportamiento específico en esta situación; podría bloquear el programa, acceder a memoria inválida o incluso parecer funcionar en algunos compiladores sin mostrar el error.
Para acceder a un carácter de una cadena, primero debe asegurarse de que la cadena no esté vacía y que el índice esté dentro del rango \[0, s.size() - 1\].
Ejercicio 3.10
Escribe un programa que lea una cadena y elimine todos los caracteres de puntuación.
#include <iostream>
#include <string>
#include <cctype> // Para ispunct
using namespace std;
int main() {
string cadenaOriginal;
cout << "Introduce una cadena con signos de puntuación: " << endl;
getline(cin, cadenaOriginal);
string cadenaSinPuntuacion;
for (char caracter : cadenaOriginal) {
if (!ispunct(caracter)) { // Si el carácter no es un signo de puntuación
cadenaSinPuntuacion += caracter; // Añádelo a la nueva cadena
}
}
cout << "Cadena sin puntuación: " << cadenaSinPuntuacion << endl;
return 0;
}
Ejercicio 3.11
¿Es legal la siguiente sentencia for basada en rangos? Si es así, ¿cuál es el tipo de c?
const string textoConst = "¡Manténgase fuera!";
for (auto &caracter : textoConst) { /* ... */ }
Esta sentencia for basada en rangos es legal. El tipo de la variable caracter se deducirá como const char&. Dado que textoConst es una cadena constante (const string), sus elementos individuales (caracteres) también se consideran constantes. Al usar auto &, se crea una referencia a cada carácter, y como el carácter es constante, la referencia también debe serlo. Por lo tanto, caracter es una referencia constante a un carácter (const char&), lo que significa que no se puede modificar el carácter a través de caracter.
3.3 Tipo vector de la Biblioteca Estándar
Ejercicio 3.12
Identifica las definiciones de objetos vector incorrectas y describe las correctas.
// (a) vector<vector<int>> matrizInt;
// Correcta: Define un vector llamado 'matrizInt' donde cada elemento es a su vez un vector de enteros.
// Es un vector de vectores de enteros (es decir, una matriz bidimensional).
// (b) vector<string> vectorCadenas = matrizInt;
// Incorrecta: 'vectorCadenas' está diseñado para almacenar elementos de tipo 'string',
// mientras que 'matrizInt' es un vector de 'vector<int>'. No se pueden inicializar
// tipos de vector incompatibles directamente de esta manera.
// (c) vector<string> nombres(10, "indefinido");
// Correcta: Define un vector llamado 'nombres' que contiene 10 elementos.
// Cada uno de estos 10 elementos es una cadena inicializada con el valor "indefinido".
Ejercicio 3.13
Para cada definición de objeto vector, indica cuántos elementos contiene y cuál es su valor.
// vector<int> vec1;
// Contiene: 0 elementos.
// vector<int> vec2(10);
// Contiene: 10 elementos. Todos inicializados a 0 (valor por defecto para int).
// vector<int> vec3(10, 42);
// Contiene: 10 elementos. Todos inicializados a 42.
// vector<int> vec4{10};
// Contiene: 1 elemento. El valor del elemento es 10 (inicialización de lista).
// vector<int> vec5{10, 42};
// Contiene: 2 elementos. Los valores son 10 y 42.
// vector<string> vec6{10};
// Contiene: 10 elementos. Todos inicializados a una cadena vacía (valor por defecto para string).
// vector<string> vec7{10, "hola"};
// Contiene: 10 elementos. Todos inicializados a la cadena "hola".
Ejercicio 3.14
Escribe un programa que lea una secuencia de números enteros de la entrada estándar y los almacene en un vector de enteros.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> numerosIngresados;
int valorEntero;
char respuesta;
cout << "Introduce números enteros. (Introduce 'n' después de un número para finalizar)" << endl;
while (cin >> valorEntero) {
numerosIngresados.push_back(valorEntero); // Añade el número al vector
cout << "¿Quieres continuar introduciendo números? (s/n): ";
cin >> respuesta;
if (respuesta != 's' && respuesta != 'S')
break;
}
cout << "Los números introducidos son: ";
for (int num : numerosIngresados) { // Recorre e imprime los elementos del vector
cout << num << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.15
Escribe un programa que lea una secuencia de cadenas de la entrada estándar y las almacene en un vector de cadenas.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<string> palabrasIngresadas;
string entradaCadena;
char respuesta;
cout << "Introduce palabras. (Introduce 'n' después de una palabra para finalizar)" << endl;
while (cin >> entradaCadena) {
palabrasIngresadas.push_back(entradaCadena); // Añade la palabra al vector
cout << "¿Quieres continuar introduciendo palabras? (s/n): ";
cin >> respuesta;
if (respuesta != 's' && respuesta != 'S')
break;
}
cout << "Las palabras introducidas son: ";
for (const string& palabra : palabrasIngresadas) { // Recorre e imprime los elementos del vector
cout << palabra << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.16
Imprime el número de elementos y los valores de cada uno de los vector definidos en el ejercicio 3.13.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Función auxiliar para imprimir un vector (plantilla para reuso)
template <typename T>
void imprimirVector(const std::string& nombre, const std::vector<T>& vec) {
cout << "Vector " << nombre << " tiene " << vec.size() << " elementos." << endl;
if (vec.empty()) {
cout << " Está vacío." << endl;
} else {
cout << " Elementos: ";
for (const T& elemento : vec) {
cout << elemento << " ";
}
cout << endl;
}
}
int main() {
vector<int> vec1;
vector<int> vec2(10);
vector<int> vec3(10, 42);
vector<int> vec4{10};
vector<int> vec5{10, 42};
vector<string> vec6{10};
vector<string> vec7{10, "hola"};
imprimirVector("vec1", vec1);
imprimirVector("vec2", vec2);
imprimirVector("vec3", vec3);
imprimirVector("vec4", vec4);
imprimirVector("vec5", vec5);
imprimirVector("vec6", vec6);
imprimirVector("vec7", vec7);
return 0;
}
Ejercicio 3.17
Lee una lista de palabras, conviértelas a mayúsculas y luego imprímelas, ocho palabras por línea.
#include <iostream>
#include <string>
#include <vector>
#include <cctype> // Para toupper
using namespace std;
int main() {
vector<string> listaPalabras;
string palabraIndividual;
char deseaContinuar = 's';
cout << "Introduce palabras (Ctrl+Z o 'n' para finalizar):" << endl;
while (cin >> palabraIndividual) {
listaPalabras.push_back(palabraIndividual);
cout << "¿Deseas introducir otra palabra? (s/n): ";
cin >> deseaContinuar;
if (deseaContinuar != 's' && deseaContinuar != 'S')
break;
}
// Convertir a mayúsculas
for (string &p : listaPalabras) {
for (char &c : p) {
c = toupper(c);
}
}
// Imprimir, 8 palabras por línea
cout << "\nPalabras en mayúsculas:" << endl;
int contadorPalabras = 0;
for (const string &p : listaPalabras) {
cout << p << " ";
contadorPalabras++;
if (contadorPalabras % 8 == 0) { // Si ya se imprimieron 8 palabras, nueva línea
cout << endl;
}
}
if (contadorPalabras % 8 != 0) { // Asegura una nueva línea si la última no se completó
cout << endl;
}
return 0;
}
Ejercicio 3.18
Analiza el siguiente código: vector<int> miVector; miVector\[0\] = 42;
Este código es inválido. La línea vector<int> miVector; declara un vector vacío, lo que significa que no contiene ningún elemento. La operación miVector\[0\] = 42; intenta acceder al elemento en el índice 0 y asignarle un valor. Sin embargo, dado que el vector está vacío, no existe un elemento en el índice 0. Intentar acceder a un índice fuera de los límites de un vector es un comportamiento indefinido.
Para añadir elementos a un vector, se debe usar un método como push\_back o redimensionar el vector antes de acceder a sus elementos por índice.
Corrección:
#include <vector>
int main() {
std::vector<int> miVector;
miVector.push_back(42); // Añade un elemento al final del vector. Ahora miVector[0] es válido.
// O bien, si se sabe el tamaño de antemano:
// std::vector<int> miVector(1); // Crea un vector con 1 elemento (inicializado a 0).
// miVector[0] = 42; // Ahora es válido.
return 0;
}
Ejercicio 3.19
¿Cuáles son las tres formas de definir un vector<int> con diez elementos, cada uno con el valor 42?
Aquí hay varias formas de lograrlo:
1. Inicialización de lista (C++11 o posterior):
std::vector<int> numerosEjemplo = {42, 42, 42, 42, 42, 42, 42, 42, 42, 42};
2. Constructor con tamaño y valor inicial:
std::vector<int> numerosEjemplo(10, 42);
3. Bucle para añadir elementos:
std::vector<int> numerosEjemplo;
for (int i = 0; i < 10; ++i) {
numerosEjemplo.push_back(42);
}
4. Redimensionar y asignar con bucle (o std::fill):
std::vector<int> numerosEjemplo(10); // Crea un vector de 10 elementos, inicializados a 0
for (int &num : numerosEjemplo) { // Recorre y asigna el valor
num = 42;
}
// Alternativamente con std::fill:
// std::fill(numerosEjemplo.begin(), numerosEjemplo.end(), 42);
Ejercicio 3.20
Escribe un programa que lea un conjunto de números enteros en un vector. Calcula e imprime la suma de cada par de elementos adyacentes. Luego, modifica el programa para que calcule e imprima la suma del primer y el último elemento, del segundo y el penúltimo, etc.
Suma de pares adyacentes:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> coleccionNumeros;
int valorEntrada;
cout << "Introduce una serie de números enteros (Ctrl+Z para finalizar):" << endl;
while (cin >> valorEntrada) {
coleccionNumeros.push_back(valorEntrada);
}
if (coleccionNumeros.empty()) {
cout << "No se introdujeron elementos." << endl;
return 0;
}
if (coleccionNumeros.size() == 1) {
cout << "Solo se introdujo un elemento: " << coleccionNumeros[0] << endl;
return 0;
}
cout << "Suma de pares adyacentes:" << endl;
for (decltype(coleccionNumeros.size()) i = 0; i < coleccionNumeros.size() - 1; i += 2) {
cout << coleccionNumeros[i] + coleccionNumeros[i+1] << " ";
}
// Si el número de elementos es impar, imprime el último elemento sin par
if (coleccionNumeros.size() % 2 != 0) {
cout << "\nEl último elemento sin par es: " << coleccionNumeros[coleccionNumeros.size() - 1];
}
cout << endl;
return 0;
}
Suma de elementos del extremo opuesto:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> conjuntoNumeros;
int valorInput;
cout << "Introduce una serie de números enteros (Ctrl+Z para finalizar):" << endl;
while (cin >> valorInput) {
conjuntoNumeros.push_back(valorInput);
}
if (conjuntoNumeros.empty()) {
cout << "No se introdujeron elementos." << endl;
return 0;
}
cout << "Suma de elementos opuestos (primero + último, segundo + penúltimo, ...):" << endl;
// Iterar hasta la mitad del vector
for (decltype(conjuntoNumeros.size()) i = 0; i < conjuntoNumeros.size() / 2; ++i) {
cout << conjuntoNumeros[i] + conjuntoNumeros[conjuntoNumeros.size() - 1 - i] << " ";
}
// Si el número de elementos es impar, imprime el elemento central
if (conjuntoNumeros.size() % 2 != 0) {
cout << "\nEl elemento central es: " << conjuntoNumeros[conjuntoNumeros.size() / 2];
}
cout << endl;
return 0;
}
3.4 Iteradores
Los iteradores son objetos que actúan como "punteros inteligentes" a elementos dentro de contenedores (como string y vector). Proporcionan una forma genérica de acceder a los elementos de un contenedor sin exponer su implementación subyacente. Los iteradores permiten recorrer los elementos de un contenedor de principio a fin y, en algunos casos, modificarlos.
Los contenedores de la biblioteca estándar proporcionan métodos como begin() y end() para obtener iteradores al primer elemento y a la posición "uno después del último" respectivamente. El operador de desreferencia (*) se usa para acceder al valor del elemento al que apunta un iterador.
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
vector<string> textoEjemplo(3, "¡Hola, mundo C++!");
// Recorre el vector usando iteradores. cbegin() y cend() devuelven iteradores constantes.
for (auto it = textoEjemplo.cbegin(); it != textoEjemplo.cend(); ++it) {
// Desreferenciar el iterador (*it) nos da el objeto al que apunta.
// Si el objeto es una clase (como string), podemos usar el operador flecha (->)
// para acceder a sus miembros.
if (!it->empty()) { // Comprueba si la cadena no está vacía
cout << *it << endl;
}
}
return 0;
}
Diferencia entre . y ->:
- El operador
.se usa para acceder a miembros de un objeto directamente cuando se tiene una instancia del objeto. - El operador
->se usa para acceder a miembros de un objeto a través de un puntero o un iterador (que se comporta como un puntero). Es equivalente a desreferenciar el puntero/iterador y luego usar el operador.:(\*ptr).miembro.
Ejercicio 3.21
Reescribe el programa del ejercicio 3.16 para imprimir el número de elementos y los valores de cada uno de los vectors utilizando iteradores.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Función auxiliar (plantilla) para imprimir un vector usando iteradores
template <typename T>
void imprimirVectorConIteradores(const std::string& nombre, const std::vector<T>& vec) {
cout << "Vector " << nombre << " tiene " << vec.size() << " elementos." << endl;
if (vec.cbegin() == vec.cend()) { // Comprueba si está vacío usando iteradores
cout << " Está vacío." << endl;
} else {
cout << " Elementos: ";
for (auto it = vec.cbegin(); it != vec.cend(); ++it) { // Itera con iteradores constantes
cout << *it << " ";
}
cout << endl;
}
}
int main() {
vector<int> vec1;
vector<int> vec2(10);
vector<int> vec3(10, 42);
vector<int> vec4{10};
vector<int> vec5{10, 42};
vector<string> vec6{10};
vector<string> vec7{10, "hola"};
imprimirVectorConIteradores("vec1", vec1);
imprimirVectorConIteradores("vec2", vec2);
imprimirVectorConIteradores("vec3", vec3);
imprimirVectorConIteradores("vec4", vec4);
imprimirVectorConIteradores("vec5", vec5);
imprimirVectorConIteradores("vec6", vec6);
imprimirVectorConIteradores("vec7", vec7);
return 0;
}
Ejercicio 3.22
Modifica el programa del ejercicio 3.20 para que lea la entrada línea por línea, almacene cada línea en un vector de cadenas, y luego imprima el primer párrafo en mayúsculas.
#include <iostream>
#include <string>
#include <vector>
#include <cctype> // Para toupper
using namespace std;
int main() {
vector<string> lineasDeTexto;
string lineaLeida;
cout << "Introduce líneas de texto. Una línea vacía marcará el fin del párrafo a procesar." << endl;
cout << "(Ctrl+Z para finalizar la entrada completamente):" << endl;
// Lee líneas hasta que se introduce una línea vacía o se termina la entrada
while (getline(cin, lineaLeida) && !lineaLeida.empty()) {
lineasDeTexto.push_back(lineaLeida);
}
// Si hay más entrada después de la línea vacía, se ignorará en este bucle.
// Si la entrada termina con Ctrl+Z sin línea vacía, procesa todo.
if (lineasDeTexto.empty()) {
cout << "No se introdujo ningún párrafo." << endl;
return 0;
}
cout << "\nPrimer párrafo en mayúsculas:" << endl;
// Itera sobre las líneas del vector usando iteradores
for (auto itLinea = lineasDeTexto.begin(); itLinea != lineasDeTexto.end(); ++itLinea) {
// Itera sobre los caracteres de cada línea usando iteradores
for (auto itChar = itLinea->begin(); itChar != itLinea->end(); ++itChar) {
*itChar = toupper(*itChar); // Convierte el carácter a mayúscula
}
cout << *itLinea << endl; // Imprime la línea modificada
}
return 0;
}
Ejercicio 3.23
Crea un vector con diez enteros. Utiliza iteradores para duplicar el valor de cada elemento.
#include <iostream>
#include <vector>
#include <ctime> // Para time
#include <cstdlib> // Para srand y rand
using namespace std;
int main() {
vector<int> numerosAleatorios;
srand(static_cast<unsigned>(time(nullptr))); // Inicializa el generador de números aleatorios
// Rellena el vector con 10 números aleatorios (0-99)
for (int i = 0; i < 10; ++i) {
numerosAleatorios.push_back(rand() % 100);
}
cout << "Números originales en el vector: ";
// Usa un iterador constante para mostrar los valores originales
for (auto it = numerosAleatorios.cbegin(); it != numerosAleatorios.cend(); ++it) {
cout << *it << " ";
}
cout << endl;
cout << "Números duplicados en el vector: ";
// Usa un iterador no constante para modificar y mostrar los valores
for (auto it = numerosAleatorios.begin(); it != numerosAleatorios.end(); ++it) {
*it *= 2; // Duplica el valor del elemento al que apunta el iterador
cout << *it << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.24
Reescribe el programa del ejercicio 3.20 (suma de elementos adyacentes y opuestos) utilizando iteradores en lugar de subíndices.
Suma de pares adyacentes con iteradores:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> elementosNumericos;
int valor;
cout << "Introduce una secuencia de números enteros (Ctrl+Z para finalizar):" << endl;
while (cin >> valor) {
elementosNumericos.push_back(valor);
}
if (elementosNumericos.empty()) {
cout << "No se introdujeron elementos." << endl;
return 0;
}
if (elementosNumericos.size() == 1) {
cout << "Solo un elemento: " << *elementosNumericos.cbegin() << endl;
return 0;
}
cout << "Suma de elementos adyacentes (usando iteradores):" << endl;
for (auto it = elementosNumericos.cbegin(); it + 1 != elementosNumericos.cend(); it += 2) {
cout << (*it + *(it + 1)) << " ";
}
if (elementosNumericos.size() % 2 != 0) {
cout << "\nÚltimo elemento sin par: " << *(elementosNumericos.cend() - 1);
}
cout << endl;
return 0;
}
Suma de elementos del extremo opuesto con iteradores:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> secuenciaNumeros;
int numeroLeido;
cout << "Introduce una secuencia de números enteros (Ctrl+Z para finalizar):" << endl;
while (cin >> numeroLeido) {
secuenciaNumeros.push_back(numeroLeido);
}
if (secuenciaNumeros.empty()) {
cout << "No se introdujeron elementos." << endl;
return 0;
}
cout << "Suma de elementos opuestos (usando iteradores):" << endl;
auto iterInicio = secuenciaNumeros.cbegin();
auto iterFin = secuenciaNumeros.cend() - 1; // Puntero al último elemento
while (iterInicio < iterFin) {
cout << (*iterInicio + *iterFin) << " ";
++iterInicio;
--iterFin;
}
// Si hay un elemento central (tamaño impar)
if (iterInicio == iterFin) {
cout << "\nElemento central: " << *iterInicio;
}
cout << endl;
return 0;
}
Ejercicio 3.25
Crea un programa que utilice iteradores para implementar un histograma de calificaciones (0-100), agrupando las calificaciones en rangos de diez (0-9, 10-19, etc.).
#include <iostream>
#include <vector>
using namespace std;
int main() {
// Vector para almacenar el conteo de calificaciones en cada rango (0-9, 10-19, ..., 90-99, 100).
// Hay 11 rangos: 0-9, 10-19, ..., 90-99, y el último para 100.
vector<unsigned> histogramaNotas(11, 0); // Inicializa todos los contadores a cero.
int calificacion;
cout << "Introduce calificaciones entre 0 y 100 (Ctrl+Z para finalizar):" << endl;
// Itera usando un bucle while para leer calificaciones
while (cin >> calificacion) {
if (calificacion >= 0 && calificacion <= 100) {
// Calcula el índice del rango (por ejemplo, 75/10 = 7, para el rango 70-79).
// Utiliza un iterador para acceder y modificar el elemento correspondiente.
// *(histogramaNotas.begin() + calificacion / 10) es lo mismo que histogramaNotas[calificacion / 10].
++*(histogramaNotas.begin() + calificacion / 10);
} else {
cout << "Calificación " << calificacion << " fuera del rango 0-100. Ignorada." << endl;
}
}
cout << "\nHistograma de calificaciones (rangos de 10):" << endl;
// Imprime el histograma recorriendo el vector con iteradores
int rango = 0;
for (auto it = histogramaNotas.cbegin(); it != histogramaNotas.cend(); ++it, ++rango) {
cout << "Rango " << rango * 10 << "-" << (rango == 10 ? 100 : (rango * 10 + 9)) << ": " << *it << " estudiantes" << endl;
}
return 0;
}
Ejercicio 3.26
En el programa de búsqueda binaria (ejemplo en el manual), ¿por qué se utiliza mid = beg + (end - beg) / 2; en lugar de mid = (beg + end) / 2;?
La razón principal es que C++ no define la operación de suma entre dos iteradores directamente. Es decir, beg + end no es una operación válida para iteradores arbitrarios. Sumar dos direcciones de memoria de esta manera no tiene un significado bien definido y no produciría una dirección útil para el punto medio.
En contraste, C++ sí define las operaciones de resta entre dos iteradores (si ambos apuntan a elementos del mismo contenedor o uno es el "iterador pasado el final"). La expresión (end - beg) calcula la distancia (el número de elementos) entre los dos iteradores. Esta distancia es un tipo integral (generalmente std::iterator\_traits<iterator>::difference\_type).
Por lo tanto, mid = beg + (end - beg) / 2; es correcto porque:
end - begcalcula el número de elementos en el rango\[beg, end).- Dividir esto por
2nos da la mitad de la distancia. - Sumar esta distancia (
beg + distancia\_media) a un iterador (beg) es una operación válida, que muevebeghacia adelante por esa cantidad de elementos, llevando al iteradormida la posición central del rango.
Esta es una forma robusta y correcta de calcular el iterador que apunta al elemento medio de un rango.
3.5 Arreglos
Ejercicio 3.27
Identifica las declaraciones de arreglos incorrectas.
// (a) unsigned tam_buffer = 1024; int arregloInt[tam_buffer];
// Inválido: 'tam_buffer' es una variable de tiempo de ejecución (no 'const' ni 'constexpr').
// Las dimensiones de los arreglos de tamaño fijo (C-style arrays) deben ser expresiones constantes
// conocidas en tiempo de compilación. (Esto sería un Variable Length Array (VLA) que es una
// extensión de C y no estándar en C++).
// (b) int arregloInt2[4 * 7 - 14];
// Válido: '4 * 7 - 14' es una expresión constante (28 - 14 = 14), cuyo valor se conoce en tiempo de compilación.
// (c) int arregloInt3[obtenerTamano()];
// Inválido: 'obtenerTamano()' es una llamada a función que no se garantiza que sea una expresión
// constante en tiempo de compilación (a menos que 'obtenerTamano' sea 'constexpr').
// (d) char cadenaChar[11] = "fundamental";
// Inválido: La cadena literal "fundamental" tiene 11 caracteres ('f', 'u', 'n', 'd', 'a', 'm', 'e', 'n', 't', 'a', 'l')
// MÁS un carácter nulo terminador ('\0') que se añade automáticamente.
// Esto significa que necesita un espacio para 12 caracteres.
// El arreglo 'cadenaChar' solo tiene espacio para 11, resultando en un desbordamiento de búfer.
// La inicialización es demasiado grande para el arreglo.
Ejercicio 3.28
Explica la inicialización por defecto de los siguientes arreglos.
std::string arrCadenasGlobal[10]; // Definido fuera de cualquier función
int arrEnterosGlobal[10]; // Definido fuera de cualquier función
int main() {
std::string arrCadenasLocal[10]; // Definido dentro de main
int arrEnterosLocal[10]; // Definido dentro de main
return 0;
}
arrCadenasGlobalyarrCadenasLocal: Ambos son arreglos de objetosstd::string. La clasestd::stringtiene un constructor por defecto que inicializa una cadena vacía. Por lo tanto, todos los elementos dearrCadenasGlobalyarrCadenasLocalse inicializarán por defecto a cadenas vacías, independientemente de si se definen globalmente o localmente.arrEnterosGlobal: Es un arreglo de tipos incorporados (int) definido a nivel global (fuera de cualquier función). Las variables de tipos incorporados con ámbito estático (globales o estáticas) se inicializan por defecto a cero. Por lo tanto, todos los elementos dearrEnterosGlobalse inicializarán a 0.arrEnterosLocal: Es un arreglo de tipos incorporados (int) definido a nivel local (dentro demain). Las variables de tipos incorporados con ámbito automático (locales) no se inicializan por defecto y contienen un valor indeterminado (basura). Intentar leer o usar los valores de los elementos dearrEnterosLocalsin inicializarlos explícitamente es un comportamiento indefinido.
Ejercicio 3.29
Compara las similitudes y diferencias entre vector y arreglos (C-style arrays).
Similitudes:
- Ambos son colecciones de elementos del mismo tipo, almacenados en memoria de forma contigua.
- Se pueden acceder a los elementos por índice (subíndice) utilizando el operador
\[\]. - Ambos pueden usarse con bucles basados en rangos (
for (auto& elem : coleccion)) en C++11 y posteriores.
Diferencias:
- Tamaño: Los arreglos tienen un tamaño fijo que debe conocerse en tiempo de compilación. No se pueden redimensionar. Los
vectorson contenedores dinámicos cuyo tamaño puede cambiar en tiempo de ejecución. - Gestión de memoria: Los arreglos (especialmente los locales) suelen gestionar su memoria en la pila o la sección de datos. Los
vectorgestionan su memoria dinámicamente en el heap, lo que les permite crecer y encogerse. - Funcionalidad: Los
vectorson objetos que encapsulan su memoria y proporcionan una rica interfaz (métodos comopush\_back,pop\_back,size,empty, etc.). Los arreglos son tipos de datos de bajo nivel sin métodos asociados. - Copia y asignación: Los
vectorse pueden copiar y asignar directamente con el operador=, realizando una copia profunda de sus elementos. Para arreglos, una asignación directa (arr1 = arr2;) no es posible; se necesita un bucle o funciones comostd::copyomemcpy. - Comprobación de límites:
vectorofreceat()para acceso con comprobación de límites en tiempo de ejecución (lanza una excepción si el índice es inválido), mientras que el acceso con\[\]para arreglos yvectors no realiza comprobaciones de límites, lo que puede llevar a errores de comportamiento indefinido.
En general, vector es preferible en C++ moderno por su seguridad, flexibilidad y funcionalidades adicionales, a menos que haya requisitos específicos de rendimiento o interacción con APIs de C que justifiquen el uso de arreglos C-style.
Ejercicio 3.30
Identifica el error en el siguiente código:
constexpr size_t tam_arreglo = 10;
int vectorNumeros[tam_arreglo];
for (size_t indice = 0; indice <= tam_arreglo; ++indice)
vectorNumeros[indice] = indice;
El error se encuentra en la condición del bucle for: indice <= tam\_arreglo. Los arreglos en C++ tienen índices válidos que van desde 0 hasta tamaño - 1. En este caso, vectorNumeros tiene tam\_arreglo (10) elementos, por lo que sus índices válidos son de 0 a 9. Cuando indice alcanza el valor de tam\_arreglo (10), se intenta acceder a vectorNumeros\[10\], lo cual está fuera de los límites del arreglo. Esto resulta en un comportamiento indefinido.
La condición correcta para el bucle debería ser indice < tam\_arreglo;.
Ejercicio 3.31
Escribe un programa para definir un arreglo de 10 ints e inicializar cada elemento con su índice.
#include <iostream>
#include <cstddef> // Para size_t
using namespace std;
int main() {
const size_t dimension = 10; // Dimension del arreglo
int miArreglo[dimension];
// Inicializar cada elemento con su índice
for (size_t i = 0; i < dimension; ++i) {
miArreglo[i] = i;
}
// Imprimir los elementos del arreglo
cout << "Elementos del arreglo: ";
for (int elemento : miArreglo) { // Bucle basado en rangos
cout << elemento << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.32
Copia el arreglo del ejercicio 3.31 en otro arreglo. Luego, copia los elementos del vector del ejercicio 3.31 en otro vector.
Copiar un arreglo a otro:
#include <iostream>
#include <cstddef> // Para size_t
using namespace std;
int main() {
const size_t tamanoArr = 10;
int arrOriginal[tamanoArr];
int arrCopia[tamanoArr]; // Segundo arreglo para la copia
// Inicializar arrOriginal con sus índices
for (size_t i = 0; i < tamanoArr; ++i) {
arrOriginal[i] = i;
}
// Copiar elementos de arrOriginal a arrCopia
for (size_t i = 0; i < tamanoArr; ++i) {
arrCopia[i] = arrOriginal[i];
}
// Imprimir arrCopia para verificar
cout << "Elementos del arreglo copiado: ";
for (int valor : arrCopia) {
cout << valor << " ";
}
cout << endl;
return 0;
}
Copiar un vector a otro:
#include <iostream>
#include <vector>
#include <cstddef> // Para size_t
using namespace std;
int main() {
const size_t tamanoVec = 10;
vector<int> vecOriginal;
// Inicializar vecOriginal con sus índices
for (size_t i = 0; i < tamanoVec; ++i) {
vecOriginal.push_back(i);
}
// Copiar vecOriginal a vecCopia. Los vectores tienen un constructor de copia
// y operador de asignación que realizan una copia profunda.
vector<int> vecCopia = vecOriginal;
// O: vector<int> vecCopia; vecCopia = vecOriginal;
// Imprimir vecCopia para verificar
cout << "Elementos del vector copiado: ";
for (int valor : vecCopia) {
cout << valor << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.33
Considera el programa del ejercicio 3.25. ¿Qué pasaría si el arreglo histogramaNotas no se inicializara?
En el ejercicio 3.25, el arreglo histogramaNotas (que es un std::vector<unsigned>) se inicializa con std::vector<unsigned> histogramaNotas(11, 0);. Esto asegura que todos los contadores de rangos de calificaciones comiencen en cero.
Si el arreglo histogramaNotas no se inicializara (por ejemplo, std::vector<unsigned> histogramaNotas(11); sin el 0), los elementos del vector se inicializarían por defecto a cero, ya que unsigned es un tipo numérico básico y std::vector garantiza la inicialización a cero para sus elementos de tipos aritméticos si solo se especifica el tamaño. Por lo tanto, en este caso particular, la omisión del 0 en el constructor del vector no cambiaría el resultado, ya que unsigned se inicializa a 0 por defecto.
Sin embargo, si histogramaNotas fuera un arreglo C-style (ej. unsigned histogramaNotas\[11\];) y se definiera dentro de una función (ámbito local/automático), sus elementos no se inicializarían y contendrían valores indeterminados (basura). En ese escenario, el programa produciría resultados incorrectos y un comportamiento indefinido, ya que estaría sumando a valores aleatorios.
Ejercicio 3.34
¿Qué ocurre con la siguiente sentencia si p1 y p2 son punteros?
p1 = p2;
Si p1 y p2 son punteros del mismo tipo (o tipos compatibles, por ejemplo, int\* y const int\* si se asigna el no-const al const), esta sentencia asigna el valor de p2 a p1. Es decir, p1 ahora apunta a la misma dirección de memoria que p2. El puntero p1 "deja de apuntar" a lo que apuntaba anteriormente (si apuntaba a algo) y comienza a apuntar al mismo objeto que p2.
- Si
p1yp2apuntan a elementos del mismo arreglo,p1ahora apunta al mismo elemento quep2. - Si
p1op2sonnullptr,p1toma ese valor nulo. - Si los tipos de
p1yp2son incompatibles (ej.int\*ydouble\*), la asignación generaría un error de compilación.
Ejercicio 3.35
Escribe un programa para establecer todos los elementos de un arreglo a cero utilizando punteros.
#include <iostream>
#include <cstddef> // Para size_t
#include <numeric> // Para std::iota, opcional para inicializar
using namespace std;
int main() {
const size_t tamanoArreglo = 10;
int miArreglo[tamanoArreglo];
// Inicializar el arreglo con valores de ejemplo (opcional, para demostrar el cambio)
std::iota(std::begin(miArreglo), std::end(miArreglo), 1); // Rellena con 1, 2, ..., 10
cout << "Arreglo antes de modificar: ";
for (int valor : miArreglo) {
cout << valor << " ";
}
cout << endl;
// Usar punteros para establecer todos los elementos a cero
int *ptrActual = begin(miArreglo); // Obtiene un puntero al primer elemento
int *ptrFin = end(miArreglo); // Obtiene un puntero "uno más allá del final"
while (ptrActual != ptrFin) {
*ptrActual = 0; // Desreferencia el puntero y asigna 0 al elemento
++ptrActual; // Mueve el puntero al siguiente elemento
}
cout << "Arreglo después de modificar (todos ceros): ";
for (int valor : miArreglo) {
cout << valor << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.36
Escribe un programa para comparar dos arreglos para ver si son iguales. Luego, modifica el programa para comparar dos vectors.
Comparar dos arreglos:
#include <iostream>
#include <cstddef> // Para size_t
#include <iterator> // Para std::begin, std::end
#include <numeric> // Para std::iota
using namespace std;
int main() {
const size_t dimension = 5;
int arr1[dimension];
int arr2[dimension];
// Inicializar los arreglos de ejemplo
std::iota(std::begin(arr1), std::end(arr1), 1); // arr1 = {1, 2, 3, 4, 5}
std::iota(std::begin(arr2), std::end(arr2), 1); // arr2 = {1, 2, 3, 4, 5}
// arr2[2] = 99; // Descomenta para probar la desigualdad
cout << "Arreglo 1: ";
for (int val : arr1) cout << val << " "; cout << endl;
cout << "Arreglo 2: ";
for (int val : arr2) cout << val << " "; cout << endl;
bool sonIguales = true;
// Usar punteros para recorrer y comparar los arreglos
int *p1 = std::begin(arr1);
int *p2 = std::begin(arr2);
int *end1 = std::end(arr1);
while (p1 != end1) {
if (*p1 != *p2) {
sonIguales = false;
break;
}
++p1;
++p2;
}
if (sonIguales) {
cout << "Los arreglos son iguales." << endl;
} else {
cout << "Los arreglos NO son iguales." << endl;
}
return 0;
}
Comparar dos vectors:
#include <iostream>
#include <vector>
#include <numeric> // Para std::iota
using namespace std;
int main() {
vector<int> vecA(5);
vector<int> vecB(5);
// Inicializar los vectores de ejemplo
std::iota(vecA.begin(), vecA.end(), 10); // vecA = {10, 11, 12, 13, 14}
std::iota(vecB.begin(), vecB.end(), 10); // vecB = {10, 11, 12, 13, 14}
// vecB[3] = 20; // Descomenta para probar la desigualdad
cout << "Vector A: ";
for (int val : vecA) cout << val << " "; cout << endl;
cout << "Vector B: ";
for (int val : vecB) cout << val << " "; cout << endl;
// La clase vector tiene un operador == sobrecargado para la comparación de contenido
if (vecA == vecB) {
cout << "Los vectores son iguales." << endl;
} else {
cout << "Los vectores NO son iguales." << endl;
}
return 0;
}
Ejercicio 3.37
Analiza el siguiente código y explica su comportamiento.
const char arrCaracteres[] = { 'h', 'o', 'l', 'a' };
const char *ptrCaracter = arrCaracteres;
while (*ptrCaracter) {
cout << *ptrCaracter << endl;
++ptrCaracter;
}
Este código declara un arreglo de caracteres arrCaracteres con los elementos 'h', 'o', 'l', 'a'. A diferencia de un literal de cadena (ej. "hola"), la inicialización con una lista de caracteres no añade automáticamente un carácter nulo terminador ('\\0') al final. Por lo tanto, arrCaracteres solo contiene cuatro caracteres.
El puntero ptrCaracter se inicializa para que apunte al principio de arrCaracteres. El bucle while (\*ptrCaracter) continúa mientras el carácter al que apunta ptrCaracter no sea '\\0' (el valor numérico de '\\0' es 0). Sin embargo, dado que arrCaracteres no tiene un terminador nulo, el bucle continuará leyendo caracteres más allá del final del arreglo, accediendo a memoria que no pertenece al arreglo. Esto es un comportamiento indefinido.
El programa intentará imprimir 'h', 'o', 'l', 'a', y luego continuará imprimiendo datos arbitrarios de la memoria (posiblemente caracteres ilegibles, números o incluso causando un fallo de segmentación) hasta que, por casualidad, encuentre un byte cuyo valor sea 0, o hasta que el sistema operativo detenga el programa por acceder a memoria inválida.
Para que este código sea correcto, arrCaracteres debería haberse declarado con espacio para el carácter nulo y haberse inicializado en consecuencia (ej. const char arrCaracteres\[\] = "hola"; o const char arrCaracteres\[\] = { 'h', 'o', 'l', 'a', '\\0' };).
Ejercicio 3.38
¿Qué ocurre si se suman dos punteros?
La operación de sumar dos punteros, como ptr1 + ptr2, no está definida en C++ y resultará en un error de compilación. Los punteros representan direcciones de memoria, y sumar dos direcciones no tiene un significado lógico en el contexto de la manipulación de memoria o de los elementos de un arreglo.
Sin embargo, C++ sí define la aritmética de punteros en relación con un entero. Por ejemplo:
ptr + n: Mueve el punteronposiciones de memoria hacia adelante (dondenes un entero).ptr - n: Mueve el punteronposiciones de memoria hacia atrás.ptr1 - ptr2: Calcula la distancia entre dos punteros (el número de elementos entre ellos) si ambos apuntan al mismo arreglo (o uno es el "pasado el final").
La suma de punteros solo está permitida si uno de los operandos es un entero y el otro es un puntero. Esto permite navegar por los elementos de un arreglo.
Ejercicio 3.39
Escribe un programa para comparar dos objetos std::string. Luego, escribe un programa para comparar dos cadenas C-style.
Comparar dos objetos std::string:
#include <iostream>
#include <string>
using namespace std;
int main() {
string textoA, textoB;
cout << "Introduce la primera cadena: " << endl;
cin >> textoA;
cout << "Introduce la segunda cadena: " << endl;
cin >> textoB;
// Los objetos std::string tienen operadores de comparación sobrecargados.
if (textoA == textoB) {
cout << "Las cadenas son iguales." << endl;
} else if (textoA > textoB) {
cout << "La primera cadena es mayor lexicográficamente." << endl;
} else { // textoA < textoB
cout << "La segunda cadena es mayor lexicográficamente." << endl;
}
return 0;
}
Comparar dos cadenas C-style:
#include <iostream>
#include <cstring> // Para la función strcmp
using namespace std;
int main() {
char c_cadena1[100]; // Suficiente espacio para las entradas
char c_cadena2[100];
cout << "Introduce la primera cadena (C-style): " << endl;
cin >> c_cadena1;
cout << "Introduce la segunda cadena (C-style): " << endl;
cin >> c_cadena2;
// strcmp compara dos cadenas C-style.
// Devuelve 0 si son iguales, un valor < 0 si la primera es menor,
// y un valor > 0 si la primera es mayor.
int resultadoComparacion = strcmp(c_cadena1, c_cadena2);
if (resultadoComparacion == 0) {
cout << "Las cadenas C-style son iguales." << endl;
} else if (resultadoComparacion < 0) {
cout << "La primera cadena C-style es menor lexicográficamente." << endl;
} else { // resultadoComparacion > 0
cout << "La primera cadena C-style es mayor lexicográficamente." << endl;
}
return 0;
}
Ejercicio 3.40
Define dos arreglos de caracteres e inicialízalos con literales de cadena. Luego, define un tercer arreglo para almacenar el resultado de concatenar los dos primeros. Utiliza strcpy y strcat.
#include <iostream>
#include <cstring> // Para strcpy, strcat, strlen
using namespace std;
int main() {
const char* textoParte1 = "Bienvenido a ";
const char* textoParte2 = "la familia de C++";
// Calcular el tamaño necesario para el arreglo resultante
// strlen no cuenta el '\0', así que necesitamos espacio extra para él.
size_t longitudTotal = strlen(textoParte1) + strlen(textoParte2) + 1;
char* cadenaConcatenada = new char[longitudTotal]; // Asignación dinámica
// Copiar la primera parte
strcpy(cadenaConcatenada, textoParte1);
// Concatenar la segunda parte
strcat(cadenaConcatenada, textoParte2);
cout << "Primera parte: " << textoParte1 << endl;
cout << "Segunda parte: " << textoParte2 << endl;
cout << "Cadena concatenada: " << cadenaConcatenada << endl;
delete[] cadenaConcatenada; // Liberar la memoria asignada dinámicamente
return 0;
}
Ejercicio 3.41
Escribe un programa para inicializar un vector de enteros a partir de un arreglo de enteros.
#include <iostream>
#include <vector>
#include <cstddef> // Para size_t
#include <iterator> // Para std::begin, std::end
#include <numeric> // Para std::iota
using namespace std;
int main() {
const size_t capacidad = 7;
int arregloOrigen[capacidad];
// Rellenar el arreglo con valores de ejemplo (10, 11, ...)
std::iota(std::begin(arregloOrigen), std::end(arregloOrigen), 10);
cout << "Elementos del arreglo original: ";
for (int elemento : arregloOrigen) {
cout << elemento << " ";
}
cout << endl;
// Inicializar el vector usando el rango de iteradores del arreglo
vector<int> vectorDestino(std::begin(arregloOrigen), std::end(arregloOrigen));
cout << "Elementos del vector inicializado desde el arreglo: ";
for (int elemento : vectorDestino) {
cout << elemento << " ";
}
cout << endl;
return 0;
}
Ejercicio 3.42
Escribe un programa para copiar los elementos de un vector de enteros en un arreglo de enteros.
#include <iostream>
#include <vector>
#include <cstddef> // Para size_t
#include <numeric> // Para std::iota
#include <algorithm> // Para std::copy
using namespace std;
int main() {
vector<int> vectorOrigen(8); // Vector con 8 elementos
// Rellenar el vector con valores de ejemplo (100, 101, ...)
std::iota(vectorOrigen.begin(), vectorOrigen.end(), 100);
cout << "Elementos del vector original: ";
for (int elemento : vectorOrigen) {
cout << elemento << " ";
}
cout << endl;
// Crear un arreglo de tamaño suficiente para contener los elementos del vector
// Es CRÍTICO que el arreglo tenga al menos el tamaño del vector.
int* arregloDestino = new int[vectorOrigen.size()];
// Copiar elementos del vector al arreglo
// Opción 1: Bucle manual
// for (size_t i = 0; i < vectorOrigen.size(); ++i) {
// arregloDestino[i] = vectorOrigen[i];
// }
// Opción 2: Usando std::copy (preferido)
std::copy(vectorOrigen.begin(), vectorOrigen.end(), arregloDestino);
cout << "Elementos del arreglo inicializado desde el vector: ";
for (size_t i = 0; i < vectorOrigen.size(); ++i) {
cout << arregloDestino[i] << " ";
}
cout << endl;
delete[] arregloDestino; // Liberar la memoria asignada dinámicamente
return 0;
}
3.6 Arreglos Multidimensionales
Ejercicio 3.43
Escribe tres versiones de un programa que imprima los elementos de un arreglo bidimensional ia (int ia[3][4]). La primera versión debe usar un bucle for basado en rangos, la segunda un bucle for normal con subíndices, y la tercera un bucle for normal con punteros. En todas las versiones, utiliza tipos de datos directos, sin alias de tipo, auto ni decltype.
#include <iostream>
#include <cstddef> // Para size_t
using namespace std;
int main() {
int matrizNumeros[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
cout << "--- Usando bucle for basado en rangos (tipos directos): ---" << endl;
// Para las filas, 'fila' es una referencia a un arreglo de 4 enteros (int (&fila)[4])
for (const int (&fila)[4] : matrizNumeros) {
// Para los elementos de la fila, 'columna' es un entero
for (int columna : fila) {
cout << columna << " ";
}
cout << endl;
}
cout << "\n--- Usando bucle for normal con subíndices (tipos directos): ---" << endl;
for (size_t i = 0; i != 3; ++i) { // Recorre las filas
for (size_t j = 0; j != 4; ++j) { // Recorre las columnas
cout << matrizNumeros[i][j] << " ";
}
cout << endl;
}
cout << "\n--- Usando bucle for normal con punteros (tipos directos): ---" << endl;
// 'ptrFila' es un puntero a un arreglo de 4 enteros (int (*ptrFila)[4])
for (int (*ptrFila)[4] = matrizNumeros; ptrFila != matrizNumeros + 3; ++ptrFila) {
// '*ptrFila' desreferencia el puntero a la fila, obteniendo el arreglo de 4 enteros.
// 'ptrElemento' es un puntero a un entero (int*) que recorre los elementos de la fila.
for (int *ptrElemento = *ptrFila; ptrElemento != *ptrFila + 4; ++ptrElemento) {
cout << *ptrElemento << " ";
}
cout << endl;
}
return 0;
}
Ejercicio 3.44
Reescribe el programa del ejercicio 3.43 utilizando un alias de tipo para simplificar la lectura del código.
#include <iostream>
#include <cstddef> // Para size_t
using namespace std;
// Alias de tipo para un arreglo de 4 enteros
using ArrayDeInt4 = int[4];
int main() {
int matrizDatos[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
cout << "--- Usando bucle for basado en rangos (con alias de tipo): ---" << endl;
// 'fila' es una referencia a 'ArrayDeInt4' (que es un int[4])
for (const ArrayDeInt4 &fila : matrizDatos) {
for (int elemento : fila) {
cout << elemento << " ";
}
cout << endl;
}
cout << "\n--- Usando bucle for normal con subíndices (con alias de tipo): ---" << endl;
for (size_t i = 0; i != 3; ++i) {
for (size_t j = 0; j != 4; ++j) {
cout << matrizDatos[i][j] << " ";
}
cout << endl;
}
cout << "\n--- Usando bucle for normal con punteros (con alias de tipo): ---" << endl;
// 'ptrFila' es un puntero a 'ArrayDeInt4' (es decir, int (*)[4])
for (ArrayDeInt4 *ptrFila = matrizDatos; ptrFila != matrizDatos + 3; ++ptrFila) {
for (int *ptrElemento = *ptrFila; ptrElemento != *ptrFila + 4; ++ptrElemento) {
cout << *ptrElemento << " ";
}
cout << endl;
}
return 0;
}
Ejercicio 3.45
Reescribe el programa del ejercicio 3.43 utilizando auto para simplificar la lectura del código.
#include <iostream>
using namespace std;
int main() {
int arregloMultidimensional[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
cout << "--- Usando bucle for basado en rangos (con auto): ---" << endl;
// 'filaRef' se deduce como 'const int (&)[4]' (referencia a arreglo de 4 const int)
for (const auto &filaRef : arregloMultidimensional) {
// 'elemento' se deduce como 'const int'
for (const auto elemento : filaRef) {
cout << elemento << " ";
}
cout << endl;
}
cout << "\n--- Usando bucle for normal con subíndices (con auto): ---" << endl;
// Los tipos 'i' y 'j' se deducen como 'int' (o 'size_t' si se usara el operador de resolución de ámbito '::')
for (auto i = 0; i != 3; ++i) {
for (auto j = 0; j != 4; ++j) {
cout << arregloMultidimensional[i][j] << " ";
}
cout << endl;
}
cout << "\n--- Usando bucle for normal con punteros (con auto): ---" << endl;
// 'ptrAFilas' se deduce como 'int (*)[4]' (puntero a arreglo de 4 int)
for (auto ptrAFilas = arregloMultidimensional; ptrAFilas != arregloMultidimensional + 3; ++ptrAFilas) {
// 'ptrAElemento' se deduce como 'int*' (puntero a int)
for (auto ptrAElemento = *ptrAFilas; ptrAElemento != *ptrAFilas + 4; ++ptrAElemento) {
cout << *ptrAElemento << " ";
}
cout << endl;
}
return 0;
}