Fundamentos de C++: Referencias, Gestión de Memoria Dinámica y Clases

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 que sizeof(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 tipo T.

Liberación de Memoria Dinámica:

  • delete: Libera la memoria asignada por new y llama al destructor del objeto (si es un tipo de clase).
  • delete[]: Libera la memoria asignada para un array por new T[size] y llama al desttructor para cada elemento del array.

Diferencias entre malloc/free y new/delete:

  • Origen: malloc/free son funciones de la librería estándar de C, mientras que new/delete son operadores del lenguaje C++.
  • Inicialización/Destrucción: new/delete invocan automáticamente constructores y destructores para objetos de clase, asegurando una gestión adecuada del estado del objeto. malloc/free solo manejan memoria cruda.
  • Tamaño: Con new, el compilador deduce el tamaño necesario. Con malloc, el programador debe especificar explícitamente el tamaño en bytes (e.g., malloc(10 * sizeof(int))).
  • Sobrecarga: Los operadores new y delete pueden 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 public suelen agruparse al principio, seguidos por los private o protected.
  • Es una buena práctica hacer que todas las variables miembro sean private y proporcionar métodos public (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 clave this.

#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;
}

Etiquetas: C++ Referencias Punteros Gestión de memoria new

Publicado el 7-20 10:21