Diferencias entre class y struct y el control de acceso

En C++, las class y los struct son herramientas para agrupar datos y comportamientos en un nuevo tipo de dato. Aunque la mayoría de los programadores las usan de forma casi intercambiable, existe una diferencia técnica fundamental: el acceso por defecto. En un struct, todos sus miembros son public (cualquiera puede verlos o modificarlos), mientras que en una class, todos son private por defecto.

Esta distinción no es solo sintáctica, sino conceptual. Usamos un struct para representar tipos de datos simples, lo que se conoce como Plain Old Data (POD), como un par de coordenadas o un color, donde la prioridad es la facilidad de acceso a sus valores. Sin embargo, cuando creamos un objeto que debe mantener una lógica interna coherente —un invariante—, usamos una class. Si tienes una clase CuentaBancaria, no quieres que cualquier parte del código pueda cambiar el saldo directamente; necesitas que el acceso sea private para que solo tus métodos public (como depositar() o retirar()) puedan modificarlo, garantizando que el saldo nunca sea negativo.

Si intentas acceder a un miembro private desde fuera de su clase, el compilador te detendrá con un error, protegiendo la integridad del objeto. Si te equivocas y no marcas una función que solo lee datos como const, el compilador te impedirá usar esa función cuando trabajes con objetos constantes, rompiendo la seguridad del código.

#include <iostream>
#include <string>
#include <utility> // Para std::move

// Un struct es ideal para datos simples. 
// Por defecto, x e y son públicos.
struct Punto {
    double x = 0.0; // [C++11] Non-Static Data Member Initialization (NSDMI)
    double y = 0.0;
};

// Una class se usa cuando hay reglas (invariantes) que proteger.
class Robot {
private:
    std::string nombre;
    int bateria = 100; // [C++11] NSDMI para evitar basura en memoria
    
    // static constexpr: Una constante que pertenece a la clase, no a cada objeto.
    // Se define en el header y es evaluada en tiempo de compilación.
    static constexpr int MAX_ENERGIA = 100;

protected:
    // protected: Accesible desde la clase y sus clases derivadas (hijos),
    // pero no desde el exterior (como el main).
    int id_fabricacion;

public:
    // Constructor que inicializa miembros mediante una lista de inicialización.
    Robot(std::string n, int id) : nombre(std::move(n)), id_fabricacion(id) {}

    // Función miembro que modifica el estado (no es const).
    void mover(Punto destino) {
        if (bateria > 0) {
            bateria--; 
            // El compilador usa el puntero implícito 'this' para saber
            // que nos referimos a la 'bateria' de ESTE objeto específico.
            std::cout << nombre << " se mueve a (" << destino.x << ", " << destino.y << ")\n";
        }
    }

    // Función miembro const: Promete que NO modificará ningún miembro de la clase.
    // Es vital para poder usar este método en objetos marcados como const.
    int get_bateria() const {
        return bateria;
    }

    void mostrar_info() const {
        std::cout << "ID: " << id_fabricacion << " | Nombre: " << nombre 
                  << " | Batería: " << bateria << "%\n";
    }

    // friend: Permite que una función externa acceda a miembros privados
    // de forma controlada (usado aquí para diagnóstico).
    friend void inspeccionar_interno(const Robot& r);
};

// Esta función no es miembro de Robot, pero puede ver su 'bateria' gracias a 'friend'.
void inspeccionar_interno(const Robot& r) {
    std::cout << "[Diagnóstico] Nivel real de batería: " << r.bateria << "%\n";
}

// Ejemplo de struct anidado para organizar jerarquías.
struct SistemaOperativo {
    struct ModuloControl { 
        int version = 1;
    };
    ModuloControl modulo;
};

int main() {
    // Instancia de la clase Robot.
    Robot bot("Explorer-1", 5001);
    
    // Uso de un struct de forma directa.
    Punto pos{10.5, 20.0};

    bot.mover(pos);
    bot.mostrar_info();
    
    // Llamada a la función friend.
    inspeccionar_interno(bot);

    // Acceso a un miembro de un struct anidado.
    SistemaOperativo sys;
    std::cout << "Versión del módulo: " << sys.modulo.version << "\n";

    return 0;
}

Desglose del código

En el ejemplo, el struct Punto permite acceder a pos.x directamente porque sus miembros son public. Hemos usado NSDMI (inicialización de miembros en la declaración) para asegurar que x e y comiencen en 0.0 sin necesidad de un constructor manual.

En la class Robot, la encapsulación es la protagonista. El miembro bateria es private, lo que significa que si intentas hacer bot.bateria = 500; en el main, el compilador lanzará un error. El método mover es la única vía para reducir la batería, manteniendo el control sobre cómo cambia ese valor. El puntero this actúa de forma invisible: cuando escribes bateria--, el compilador lo traduce internamente para referirse a la dirección de memoria del objeto actual.

La función get_bateria() const es fundamental para la const-correctness. Al añadir const, le decimos al compilador que este método es seguro de llamar incluso si tenemos una referencia constante a un robot (const Robot&), ya que garantizamos que no modificará nada. Por otro lado, id_fabricacion es protected, lo que permitiría que una clase RobotGuerrero (hija de Robot) acceda a su ID, pero seguiría siendo invisible para el main.

Para la constante MAX_ENERGIA, hemos usado static constexpr. Al ser static, no ocupa espacio extra en cada instancia de Robot; existe una única copia para todos los robots, y al ser constexpr, el compilador puede optimizar su uso al máximo durante la compilación. Finalmente, la función inspeccionar_interno actúa como un “amigo” (friend), rompiendo la barrera de privacidad solo para esa función específica, algo útil en sistemas de diagnóstico o serialización.

El error frecuente

Un error muy común en principiantes es olvidar inicializar un miembro de datos en el constructor y no usar NSDMI.

class Sensor {
public:
    Sensor() {} // ERROR: 'valor' no tiene un valor definido.
private:
    int valor;
};

Si creas un objeto Sensor, el miembro valor no será 0, sino que contendrá “basura” (lo que hubiera en esa posición de memoria). Esto no produce un error de compilación, sino un comportamiento indefinido (Undefined Behavior). Al ejecutar el programa, los resultados serán erráticos y dependientes del estado de la memoria en ese momento. Herramientas como AddressSanitizer son fundamentales para detectar estos accesos a memoria no inicializada.

Dominar el control de acceso es la base para construir sistemas que no se rompan cuando la lógica de negocio se vuelve compleja.

35

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio