Programación Orientada a Objetos

Capítulo 9: Structs y Clases Básicas

Las clases son el pilar de la programación orientada a objetos en C++. En este capítulo aprenderás cómo agrupar datos y comportamiento, cómo funcionan los constructores y destructores, qué es el puntero this, y cómo se organizan los objetos en memoria. También verás una introducción a las vtables para entender cómo funciona el polimorfismo.

Objetivos

Structs

Un struct (estructura) es una forma de agrupar variables relacionadas bajo un solo nombre. En lugar de manejar datos dispersos (nombre, edad, email como variables separadas), puedes crear un tipo Usuario que contenga todos esos datos juntos.

¿Por qué usar structs? Los structs mejoran la organización del código y el modelado de conceptos del mundo real. Si estás programando un videojuego, un struct Jugador con miembros como posicion, vida, puntos es mucho más claro que tener docenas de variables sueltas. Además, puedes pasar toda la información de un jugador a funciones con un solo parámetro.

#include <iostream>

struct Point {
    int x;
    int y;
};

int main() {
    Point p1;
    p1.x = 10;
    p1.y = 20;

    Point p2 = {30, 40};  // Inicialización agregada

    std::cout << "p1: (" << p1.x << ", " << p1.y << ")
";
    std::cout << "p2: (" << p2.x << ", " << p2.y << ")
";

    return 0;
}

Memory Layout de un Struct

graph LR
    subgraph "Point p = {10, 20}"
    X["x = 10<br/>0x1000<br/>(4 bytes)"] --> Y["y = 20<br/>0x1004<br/>(4 bytes)"]
    end

    SIZE["sizeof(Point) = 8 bytes"]

    style X fill:#3498db,color:#fff
    style Y fill:#3498db,color:#fff
    style SIZE fill:#27ae60,color:#fff

Los miembros están almacenados contiguamente en memoria.

Classes

Una class (clase) es como un struct con superpoderes. Además de contener datos (miembros), puede contener funciones (métodos) que operan sobre esos datos. Este concepto - combinar datos y comportamiento - es la base de la programación orientada a objetos (OOP).

¿Por qué OOP? La OOP permite modelar conceptos complejos del mundo real de forma natural. Una clase CuentaBancaria no solo almacena el saldo, sino que también puede tener métodos como depositar() y retirar(). Esto encapsula la lógica del negocio junto con los datos que maneja.

Principios fundamentales de OOP:

#include <iostream>

class Rectangle {
   private:
    int width;
    int height;

   public:
    // Constructor
    Rectangle(int w, int h) : width(w), height(h) {
        std::cout << "Rectangle creado
";
    }

    // Destructor
    ~Rectangle() { std::cout << "Rectangle destruido
"; }

    int area() { return width * height; }
};

int main() {
    Rectangle rect(10, 20);
    std::cout << "Área: " << rect.area() << "
";  // 200

    return 0;
}  // Destructor llamado automáticamente
struct vs class
// Struct: miembros PUBLIC por defecto
struct Point {
    int x;  // public
    int y;  // public
};

// Class: miembros PRIVATE por defecto
class Rectangle {
    int width;   // private
    int height;  // private
};

// En C++, la ÚNICA diferencia entre struct y class es el acceso por defecto
// Convención:
// - struct: para POD (Plain Old Data) sin lógica
// - class: para objetos con comportamiento

Regla de estilo:

  • Usa struct para POD (Plain Old Data): solo datos, sin lógica compleja
  • Usa class para objetos con comportamiento (métodos, encapsulación)

Access Specifiers

#include <iostream>

class BankAccount {
   private:
    double balance;  // Solo accesible dentro de la clase

   protected:
    std::string accountNumber;  // Accesible en clases derivadas

   public:
    // Constructor
    BankAccount(double initial) : balance(initial) {}

    // Métodos públicos: interfaz pública
    void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }

    double getBalance() const { return balance; }
};

int main() {
    BankAccount account(1000.0);
    account.deposit(500);
    std::cout << "Balance: " << account.getBalance() << "
";  // 1500

    // account.balance = 9999;  // ❌ Error: private

    return 0;
}

Access Specifiers

classDiagram
    class Example {
        - int priv
        # int prot
        + int pub
        + publicMethod()
    }

    note for Example "🔒 private: Solo dentro de la clase<br/>🔐 protected: Clase + derivadas<br/>🔓 public: Accesible desde cualquier lugar"
¿Por qué usar private? Básico

Encapsulación: Ocultar detalles de implementación.

Ventajas:

  • Control: puedes validar valores en setters
  • Flexibilidad: puedes cambiar la implementación sin afectar código externo
  • Invariantes: garantizas que el objeto siempre está en un estado válido
class BankAccount {
   private:
    double balance;  // Protegido de modificación directa

   public:
    void deposit(double amount) {
        if (amount > 0) {  // Validación
            balance += amount;
        }
    }
};

Constructores

#include <iostream>

class Person {
   private:
    std::string name;
    int age;

   public:
    // Constructor por defecto
    Person() : name("Unknown"), age(0) {
        std::cout << "Constructor por defecto
";
    }

    // Constructor con parámetros
    Person(std::string n, int a) : name(n), age(a) {
        std::cout << "Constructor con parámetros
";
    }

    // Constructor de copia
    Person(const Person& other) : name(other.name), age(other.age) {
        std::cout << "Constructor de copia
";
    }

    void print() const { std::cout << name << ", " << age << " años
"; }
};

int main() {
    Person p1;               // Constructor por defecto
    Person p2("Alice", 30);  // Constructor con parámetros
    Person p3 = p2;          // Constructor de copia

    p1.print();
    p2.print();
    p3.print();

    return 0;
}

Initializer list

#include <iostream>

class Example {
   private:
    const int id;
    int& ref;
    int value;

   public:
    // ✅ Initializer list: inicializa antes de entrar al cuerpo
    Example(int i, int& r, int v) : id(i), ref(r), value(v) {
        // Cuerpo del constructor (después de inicialización)
    }

    // ❌ Esto NO compila: const e referencias deben inicializarse en
    // initializer list Example(int i, int& r, int v) {
    //     id = i;     // ❌ Error: const no puede asignarse
    //     ref = r;    // ❌ Error: referencia no puede asignarse
    //     value = v;  // ✅ OK pero ineficiente (asignación, no inicialización)
    // }
};
Cuándo usar Initializer List

DEBES usar initializer list para:

  • Miembros const
  • Referencias
  • Objetos sin constructor por defecto
  • Clases base

DEBERÍAS usar initializer list para:

  • Todos los miembros (más eficiente: inicialización directa vs asignación)
// ❌ Menos eficiente
MyClass(int x, int y) {
    this->x = x;  // Construcción por defecto + asignación
    this->y = y;
}

// ✅ Más eficiente
MyClass(int x, int y) : x(x), y(y) {
    // Construcción directa con valores
}

Destructores

#include <iostream>

class Resource {
   private:
    int* data;

   public:
    Resource(int size) {
        data = new int[size];
        std::cout << "Recurso adquirido
";
    }

    ~Resource() {
        delete[] data;
        std::cout << "Recurso liberado
";
    }
};

int main() {
    {
        Resource res(100);
        // Usar res...
    }  // Destructor llamado automáticamente aquí

    std::cout << "Fin del programa
";

    return 0;
}
Orden de construcción y destrucción Intermedio

Construcción: De base a derivada, de miembros a clase.

Destrucción: Orden inverso (de derivada a base, de clase a miembros).

class Base {
   public:
    Base() { std::cout << "Base ctor\n"; }
    ~Base() { std::cout << "Base dtor\n"; }
};

class Derived : public Base {
   public:
    Derived() { std::cout << "Derived ctor\n"; }
    ~Derived() { std::cout << "Derived dtor\n"; }
};

Derived d;
// Output:
// Base ctor
// Derived ctor
// Derived dtor  ← Inverso
// Base dtor

El puntero this

#include <iostream>

class Counter {
   private:
    int count;

   public:
    Counter() : count(0) {}

    // this es un puntero al objeto actual
    Counter& increment() {
        this->count++;  // Equivalente a: count++;
        return *this;   // Retorna referencia al objeto actual
    }

    int getCount() const { return count; }
};

int main() {
    Counter c;
    c.increment().increment().increment();           // Method chaining
    std::cout << "Count: " << c.getCount() << "
";  // 3

    return 0;
}

El Puntero this

flowchart TB
    subgraph "Código visible"
    OBJ["obj<br/>(Example)"]
    CALL["obj.foo()"]
    OBJ --> CALL
    end

    subgraph "Internamente (compilador)"
    THIS["this = &obj<br/>(puntero implícito)"]
    FOO["void foo(Example* this) {<br/>  this->value = 42;<br/>}"]
    THIS --> FOO
    end

    CALL -.->|"traduce a"| THIS

    style OBJ fill:#3498db,color:#fff
    style CALL fill:#9b59b6,color:#fff
    style THIS fill:#e67e22,color:#fff
    style FOO fill:#27ae60,color:#fff
Usos comunes de this
  • Desambiguar parámetros vs miembros: this->name = name;
  • Retornar el objeto actual: return *this; (method chaining)
  • Pasar el objeto a otra función: other.process(this);
  • Comparar con otro objeto: if (this == &other)

Const member functions

#include <iostream>

class Point {
   private:
    int x, y;

   public:
    Point(int x, int y) : x(x), y(y) {}

    // Función const: no modifica el objeto
    int getX() const {
        // x = 10;  // ❌ Error: no puedes modificar en función const
        return x;
    }

    // Función no-const: puede modificar
    void setX(int newX) { x = newX; }
};

int main() {
    const Point p1(10, 20);
    std::cout << p1.getX() << "
";  // ✅ OK: getX es const
    // p1.setX(30);  // ❌ Error: setX no es const

    Point p2(30, 40);
    p2.setX(50);  // ✅ OK: p2 no es const

    return 0;
}
const correctness Intermedio

Marcar funciones como const es parte de const correctness:

  • Indica que la función no modifica el objeto
  • Permite llamar la función en objetos const
  • El compilador verifica que no modifiques miembros
class Point {
    int x, y;

   public:
    int getX() const { return x; }  // ✅ Puede leer
    void setX(int v) { x = v; }     // No-const: puede modificar
};

const Point p(10, 20);
p.getX();    // ✅ OK: getX es const
p.setX(30);  // ❌ Error: setX no es const

Regla: Marca todas las funciones que no modifiquen el objeto como const.

Static members

#include <iostream>

class Counter {
   private:
    static int totalCount;  // Compartido por TODAS las instancias
    int instanceCount;

   public:
    Counter() : instanceCount(0) { totalCount++; }

    void increment() {
        instanceCount++;
        totalCount++;
    }

    static int getTotalCount() {  // Función static: sin acceso a this
        // return instanceCount;  // ❌ Error: no hay 'this'
        return totalCount;  // ✅ OK: static
    }

    int getInstanceCount() const { return instanceCount; }
};

// Definición de miembro static (fuera de la clase)
int Counter::totalCount = 0;

int main() {
    Counter c1, c2;
    c1.increment();
    c2.increment();
    c2.increment();

    std::cout << "c1: " << c1.getInstanceCount() << "
";        // 1
    std::cout << "c2: " << c2.getInstanceCount() << "
";        // 2
    std::cout << "Total: " << Counter::getTotalCount() << "
";  // 5

    return 0;
}

Static vs Instance Members

flowchart TB
    subgraph "Memoria global"
    STATIC["static totalCount = 5<br/>🌐 UNA copia compartida"]
    end

    subgraph "Instancia c1"
    C1["instanceCount = 1<br/>📦 Copia individual"]
    end

    subgraph "Instancia c2"
    C2["instanceCount = 2<br/>📦 Copia individual"]
    end

    C1 -.->|"comparte"| STATIC
    C2 -.->|"comparte"| STATIC

    style STATIC fill:#e74c3c,color:#fff
    style C1 fill:#3498db,color:#fff
    style C2 fill:#3498db,color:#fff

static members viven en la sección de datos globales, no en cada instancia del objeto.

Definición de Static Members

Los miembros static deben definirse FUERA de la clase:

// header.h
class MyClass {
    static int count;
};

// source.cpp
int MyClass::count = 0;  // Definición

// ❌ Sin esto, tendrás linker error: "undefined reference"

Memory layout de objetos

#include <iostream>

class Example {
   private:
    int a;   // 4 bytes
    char b;  // 1 byte
    int c;   // 4 bytes

   public:
    void foo() {}
};

int main() {
    std::cout << "sizeof(Example): " << sizeof(Example) << "
";
    // Probablemente 12 bytes (no 9) por padding

    return 0;
}
Data Alignment y Padding Avanzado

El compilador añade padding para alinear datos en direcciones eficientes:

class Example {
    int a;   // 4 bytes
    char b;  // 1 byte
    // [padding: 3 bytes]
    int c;  // 4 bytes
};

sizeof(Example) = 12 bytes, no 9

    Sin padding :
┌────┬─┬────┐
│ a  │b│ c  │  ← c no está alineado a 4 bytes
└────┴─┴────┘

    Con padding :
┌────┬─┬───┬────┐
│ a  │b│## #│ c  │  ← c alineado a 4 bytes
└────┴─┴───┴────┘
     └─────┘ padding

    Por qué
    ? Acceder a memoria no alineada es MÁS LENTO(o causa crash en algunas CPUs).

Tip: Ordena miembros de mayor a menor tamaño para minimizar padding:

// ✅ Mejor: 12 bytes
class Optimized {
    int a;   // 4
    int c;   // 4
    char b;  // 1
    // [padding: 3]
};

Introducción a vtables (polimorfismo)

#include <iostream>

class Base {
   public:
    virtual void foo() {  // virtual = polimórfica
        std::cout << "Base::foo()
";
    }
};

class Derived : public Base {
   public:
    void foo() override { std::cout << "Derived::foo()
"; }
};

int main() {
    Base* ptr = new Derived();
    ptr->foo();  // Llama a Derived::foo() (polimorfismo)

    delete ptr;
    return 0;
}

VTable (Virtual Table)

flowchart LR
    subgraph "Base object"
    BOBJ["vptr<br/>(8 bytes)"] --> |apunta a| BVTABLE["VTable Base<br/>Base::foo()"]
    end

    subgraph "Derived object"
    DOBJ["vptr<br/>(8 bytes)"] --> |apunta a| DVTABLE["VTable Derived<br/>Derived::foo()"]
    end

    POLY["Polimorfismo dinámico:<br/>Runtime lookup"]

    BVTABLE -.->|"ejemplo"| POLY
    DVTABLE -.->|"ejemplo"| POLY

    style BOBJ fill:#3498db,color:#fff
    style DOBJ fill:#9b59b6,color:#fff
    style BVTABLE fill:#e67e22,color:#fff
    style DVTABLE fill:#e74c3c,color:#fff
    style POLY fill:#27ae60,color:#fff

Cada objeto con funciones virtual tiene un puntero (vptr) a una tabla de funciones virtuales (vtable). sizeof(Base) = 8 bytes (vptr en 64-bit)

¿Cómo funciona el polimorfismo? Avanzado

Cuando llamas una función virtual a través de un puntero/referencia:

  1. El compilador busca el vptr del objeto
  2. Accede a la vtable
  3. Llama a la función correcta según el tipo REAL del objeto (runtime)
Base* ptr = new Derived();
ptr->foo();

// Assembly (simplificado):
// mov rax, [ptr]        ; Cargar vptr
// mov rax, [rax]        ; Cargar dirección de foo() de vtable
// call rax              ; Llamar a Derived::foo()

Costo: Indirección extra (2 dereferences). Más lento que llamada directa, pero permite polimorfismo.

Rule of Three/Five

#include <iostream>

class MyString {
   private:
    char* data;

   public:
    // Constructor
    MyString(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // Destructor
    ~MyString() { delete[] data; }

    // Constructor de copia
    MyString(const MyString& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }

    // Operador de asignación
    MyString& operator=(const MyString& other) {
        if (this != &other) {  // Evitar auto-asignación
            delete[] data;
            data = new char[strlen(other.data) + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    void print() const { std::cout << data << "
"; }
};
Rule of Three/Five/Zero

Rule of Three (C++98): Si defines uno, probablemente necesitas los tres:

  • Destructor
  • Constructor de copia
  • Operador de asignación

Rule of Five (C++11): Añade move semantics:

  • + Constructor de movimiento
  • + Operador de asignación de movimiento

Rule of Zero (C++11): Si puedes, NO definas ninguno. Usa RAII con smart pointers:

class Modern {
    std::unique_ptr<int[]> data;  // Se gestiona solo
    // No necesitas destructor, copy ctor, etc.
};

Checklist

Preguntas para reflexionar

¿Cuál es la diferencia entre struct y class en C++?
Respuesta:

Solo el acceso por defecto:

  • struct: miembros public por defecto
  • class: miembros private por defecto

Todo lo demás es idéntico (herencia, funciones virtuales, etc.).

Convención: Usa struct para POD (Plain Old Data), class para objetos con comportamiento.

¿Por qué debo usar initializer lists en constructores?
Respuesta:

Eficiencia: Inicialización directa vs construcción + asignación.

// ❌ Menos eficiente
MyClass(int x) {
  this->x = x; // Construcción por defecto + asignación
}

// ✅ Más eficiente
MyClass(int x) : x(x) { // Construcción directa con valor
}

Obligatorio para:

  • Miembros const
  • Referencias
  • Objetos sin constructor por defecto
¿Qué es el puntero this y cuándo se usa?
Respuesta:

this es un puntero al objeto actual. Se pasa implícitamente a todas las member functions (excepto static).

Usos comunes:

  • Desambiguar: this->name = name;
  • Method chaining: return *this;
  • Pasar el objeto: other.process(this);
class Example {
  int value;
  void foo() {
    this->value = 42; // this = puntero al objeto
  }
};
¿Qué diferencia hay entre miembros static e instance?
Respuesta:
AspectoInstance MemberStatic Member
CopiasUna por objetoUna compartida por todos
AccesoRequiere objetoClase::miembro
thisNo

Static members viven en la sección de datos globales, no en cada instancia.

¿Qué es una vtable y para qué sirve?
Respuesta:

Una vtable (virtual table) es una tabla de punteros a funciones virtuales.

Cada clase con funciones virtual tiene una vtable. Cada objeto tiene un vptr (puntero a su vtable).

Propósito: Implementar polimorfismo dinámico (runtime).

Base* ptr = new Derived();
ptr->foo(); // Llama a Derived::foo() (no Base::foo())

En runtime, el compilador:

  1. Accede al vptr del objeto
  2. Busca en la vtable la función correcta
  3. Llama a la implementación real

Costo: Overhead de memoria (vptr) y performance (indirección).

¡Felicidades!

Has completado los 9 capítulos fundamentales de C++. Ahora tienes una base sólida en: sintaxis básica, tipos de datos, control de flujo, arrays, punteros, memoria dinámica, funciones, y programación orientada a objetos con structs y clases.

Próximos pasos recomendados: Practica construyendo proyectos pequeños, estudia templates y la STL, explora C++ moderno (C++11/14/17/20), y profundiza en patrones de diseño.

Siguiente capítulo: Operadores Bitwise →