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
- Dominar la diferencia entre struct y class
- Entender constructores, destructores y el ciclo de vida de objetos
- Usar member functions y el puntero
this - Aplicar access specifiers (public, private, protected)
- Conocer const member functions y static members
- Comprender el memory layout de objetos
- Ver una introducción a vtables y polimorfismo
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:
- Encapsulación: Ocultar detalles de implementación usando
private - Abstracción: Exponer interfaces simples para funcionalidad compleja
- Herencia: Crear clases derivadas que extienden otras clases (próximos capítulos)
- Polimorfismo: Objetos de diferentes tipos pueden responder al mismo mensaje (funciones virtuales)
#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: 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
structpara POD (Plain Old Data): solo datos, sin lógica compleja - Usa
classpara 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)
// }
}; 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
- 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.
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:
- El compilador busca el vptr del objeto
- Accede a la vtable
- 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 (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
- Sé la diferencia entre
struct(public por defecto) yclass(private por defecto) - Entiendo el propósito de constructores y destructores
- Uso initializer lists para inicializar miembros
- Aplico encapsulación con
privateypublic - Marco funciones que no modifican el objeto como
const - Conozco el propósito de
thisy cuándo usarlo - Entiendo qué son static members y cómo se almacenan
- Sé que las funciones virtuales usan vtables para polimorfismo
Preguntas para reflexionar
¿Cuál es la diferencia entre struct y class en C++?
Solo el acceso por defecto:
struct: miembros public por defectoclass: 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?
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?
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?
| Aspecto | Instance Member | Static Member |
|---|---|---|
| Copias | Una por objeto | Una compartida por todos |
| Acceso | Requiere objeto | Clase::miembro |
| this | Sí | No |
Static members viven en la sección de datos globales, no en cada instancia.
¿Qué es una vtable y para qué sirve?
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:
- Accede al vptr del objeto
- Busca en la vtable la función correcta
- Llama a la implementación real
Costo: Overhead de memoria (vptr) y performance (indirección).
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.