En C++, las referencias y la gestión de memoria dinámica son conceptos cruciales para escribir código eficiente y seguro. Además, la introducción de clases y objetos revoluciona la forma en que estructuramos y organizamos nuestro código, permitiendo la programación orientada a objetos.
Referencias
Una referencia en C++ actúa como un alias o un nombre alternativo para una varible existente. Una vez que una referencia se inicializa con una variable, cualquier operación realizada a través de la referencia afectará directamente a la variable original.
Características Clave de las Referencias:
- Las referencias deben inicializarse en el momento de su declaración. No se permiten referencias nulas.
- Una vez inicializada, una referencia no puede ser reasignada para referenciar a otra variable.
- Internamente, una referencia es simplemente otro nombre para la misma ubicación de memoria.
Ejemplo de Uso de Referencias:
Consideremos diferentes formas de pasar argumentos a funciones:
- Paso por valor: Una copia del argumento se pasa a la función. Las modificaciones dentro de la función no afectan a la variable original.
- Paso por puntero: La dirección de memoria de la variable se pasa a la función. Las modificaciones a través del puntero sí afectan a la variable original.
- Paso por referencia: Se pasa un alias de la variable. Las modificaciones a través de la referencia afectan directamente a la variable original.
#include <iostream>
// Paso por valor
void modificarValor(int num) {
num = 1000; // Esta modificación solo afecta a la copia local
}
// Paso por puntero
void modificarConPuntero(int* ptr) {
*ptr = 1111; // Modifica el valor original a través del puntero
}
// Paso por referencia
void modificarConReferencia(int& refNum) {
refNum = 1111; // Modifica el valor original a través de la referencia
}
int main() {
int miVariable = 50;
std::cout << "Valor original: " << miVariable << std::endl;
modificarValor(miVariable);
std::cout << "Después de modificarValor: " << miVariable << std::endl; // Sigue siendo 50
modificarConPuntero(&miVariable);
std::cout << "Después de modificarConPuntero: " << miVariable << std::endl; // Ahora es 1111
miVariable = 50; // Reiniciar para el ejemplo de referencia
modificarConReferencia(miVariable);
std::cout << "Después de modificarConReferencia: " << miVariable << std::endl; // Ahora es 1111
// Ejemplo de referencia a otra referencia
int original = 200;
int& ref1 = original;
int& ref2 = ref1; // ref2 es también un alias de original
ref2 = 300;
std::cout << "Original: " << original << ", Ref1: " << ref1 << ", Ref2: " << ref2 << std::endl; // Todos son 300
// Referencias constantes
const int constante = 10;
const int& refConstante = constante;
// refConstante = 20; // Error: no se puede modificar a través de una referencia constante
return 0;
}
Diferencias Clave entre Punteros y Referencias:
- Un puntero almacena una dirección de memoria, mientras que una referencia es un alias.
- Los punteros pueden ser nulos, pero las referencias deben referenciar a un objeto válido.
- Los punteros no constantes pueden cambiar el objeto al que apuntan, mientras que las referencias, una vez inicializadas, siempre se refieren al mismo objeto.
sizeof(referencia)devuelve el tamaño del objeto referenciado, mientras quesizeof(puntero)devuelve el tamaño de la dirección de memoria (generalmente 4 u 8 bytes).
Gestión de Memoria Dinámica (Heap)
La memoria dinámica se gestiona en el "heap" (montón), permitiendo asignar y liberar memoria en tiempo de ejecución. C++ ofrece dos mecanismos principales para esto: las funciones de C (malloc, free) y los operadores de C++ (new, delete).
Asignación de Memoria Dinámica:
new: Asigna memoria y, para tipos de clase, llama al constructor del objeto.new T[size]: Asigna memoria para un array de tipoT.
Liberación de Memoria Dinámica:
delete: Libera la memoria asignada pornewy llama al destructor del objeto (si es un tipo de clase).delete[]: Libera la memoria asignada para un array pornew T[size]y llama al desttructor para cada elemento del array.
Diferencias entre malloc/free y new/delete:
- Origen:
malloc/freeson funciones de la librería estándar de C, mientras quenew/deleteson operadores del lenguaje C++. - Inicialización/Destrucción:
new/deleteinvocan automáticamente constructores y destructores para objetos de clase, asegurando una gestión adecuada del estado del objeto.malloc/freesolo manejan memoria cruda. - Tamaño: Con
new, el compilador deduce el tamaño necesario. Conmalloc, el programador debe especificar explícitamente el tamaño en bytes (e.g.,malloc(10 * sizeof(int))). - Sobrecarga: Los operadores
newydeletepueden ser sobrecargados para personalizar la gestión de memoria.
#include <iostream>
int main() {
// Asignación de un solo entero
int* ptrInt = new int;
*ptrInt = 100;
std::cout << "Valor del entero dinámico: " << *ptrInt << std::endl;
delete ptrInt; // Liberar la memoria del entero
// Asignación de un array de enteros
int size = 5;
int* ptrArray = new int[size];
for (int i = 0; i < size; ++i) {
ptrArray[i] = (i + 1) * 10;
}
std::cout << "Array dinámico: ";
for (int i = 0; i < size; ++i) {
std::cout << ptrArray[i] << " ";
}
std::cout << std::endl;
delete[] ptrArray; // Liberar la memoria del array
return 0;
}
Clases y Objetos
La programación orientada a objetos (POO) se basa en los conceptos de clases y objetos.
Objeto:
Un objeto es una instancia de una clase, representando una entidad del mundo real con propiedades (atributos) y comportamientos (métodos).
Clase:
Una clase es una plantilla o un plano que define la estructura y el comportamiento de los objetos. Agrupa datos (atributos) y funciones (métodos) que operan sobre esos datos.
Relación Clase-Objeto:
La relación es de abstracción a concreción. La clase es la abstracción general, y los objetos son instancias concretas de esa clase. Un objeto es una "instancia" de una clase.
Miembros de una Clase:
Los miembros de una clase pueden ser:
- Atributos (Variables miembro): Representan las propiedades del objeto.
- Métodos (Funciones miembro): Definen los comportamientos y operaciones que el objeto puede realizar.
Modificadores de Acceso:
Controlan la visibilidad y el acceso a los miembros de la clase:
public: Accesibles desde cualquier lugar (dentro o fuera de la clase).private: Accesibles solo desde dentro de la propia clase. Es la opción por defecto si no se especifica.protected: Accesibles desde dentro de la clase y desde clases derivadas (herencia).
Reglas de Escritura de Clases:
- Generalmente, las declaraciones de métodos van antes que las declaraciones de variables miembro.
- Los miembros
publicsuelen agruparse al principio, seguidos por losprivateoprotected. - Es una buena práctica hacer que todas las variables miembro sean
privatey proporcionar métodospublic(getters y setters) para acceder y modificar sus valores de forma controlada (encapsulación). - Las definiciones de los métodos de una clase suelen realizarse fuera de la declaración de la clace (en archivos
.cpp), utilizando el operador de resolución de ámbito (Clase::metodo()).
const en Clases:
Cuando un método miembro se declara con const al final (void miMetodo() const;), indica que ese método no modificará ningún miembro variable de la clase. Esto es útil para garantizar la integridad de los objetos constantes.
#include <iostream>
#include <string>
class Producto {
public:
// Constructor
Producto(const std::string& n, double p) : nombre(n), precio(p) {
std::cout << "Creando producto: " << nombre << std::endl;
}
// Método público
void mostrarDetalles() const {
std::cout << "Nombre: " << nombre << ", Precio: $" << precio << std::endl;
// std::cout << stock; // Error: No se puede acceder a miembro privado desde método const
}
// Método público que puede modificar el estado
void actualizarPrecio(double nuevoPrecio) {
if (nuevoPrecio >= 0) {
precio = nuevoPrecio;
std::cout << "Precio de " << nombre << " actualizado a $" << precio << std::endl;
} else {
std::cout << "Error: El precio no puede ser negativo." << std::endl;
}
}
private:
// Miembros privados
std::string nombre;
double precio;
int stock = 0; // Inicialización en C++11+
};
int main() {
// Instanciación de un objeto de la clase Producto
Producto miProducto("Laptop", 1200.50);
// Acceso a miembros públicos
miProducto.mostrarDetalles();
miProducto.actualizarPrecio(1150.00);
miProducto.mostrarDetalles();
// Producto otroProducto; // Error: El constructor por defecto no existe y se requiere nombre y precio
return 0;
}
sizeof en Clases:
El operador sizeof aplicado a una clase devuelve el tamaño en memoria que ocupará un objeto de esa clase. Este tamaño incluye todos los miembros no estáticos y considera el padding (relleno) para la alineación de memoria. Los métodos y los miembros estáticos no contribuyen al tamaño de cada objeto individual.
Una clase vacía (sin miembros no estáticos) ocupa 1 byte para asegurar que cada objeto tenga una dirección única.
static en Clases:
Los miembros static (variables y métodos) pertenecen a la clase en sí, no a instancias individuales de la clase. Solo existe una copia de un miembro estático, compartida por todos los objetos de la clase.
- Variables miembro estáticas: Se declaran con la palabra clave
static. Deben ser inicializadas fuera de la declaración de la clase. Son útiles para contadores, constantes compartidas, etc. - Métodos miembro estáticos: También se declaran con
static. Pueden ser llamados usando el nombre de la clase (Clase::metodoEstatico()) sin necesidad de crear un objeto. Dentro de un método estático, solo se puede acceder a otros miembros estáticos de la clase; no se puede acceder a miembros no estáticos ni usar la palabra clavethis.
#include <iostream>
class Contador {
public:
Contador() {
contadorInstancias++; // Incrementa el contador cada vez que se crea un objeto
idInstancia = contadorInstancias; // Asigna un ID único basado en el contador
}
void mostrarId() const {
std::cout << "Instancia ID: " << idInstancia << std::endl;
}
// Método estático para acceder al contador compartido
static int obtenerContadorInstancias() {
return contadorInstancias;
}
private:
int idInstancia;
// Variable miembro estática: compartida por todas las instancias
static int contadorInstancias;
};
// Inicialización de la variable miembro estática fuera de la clase
int Contador::contadorInstancias = 0;
int main() {
std::cout << "Número inicial de instancias: " << Contador::obtenerContadorInstancias() << std::endl;
Contador c1;
Contador c2;
Contador c3;
c1.mostrarId();
c2.mostrarId();
c3.mostrarId();
std::cout << "Número total de instancias creadas: " << Contador::obtenerContadorInstancias() << std::endl;
return 0;
}