Capítulo 12: Técnicas Avanzadas y Buenas Prácticas
12.1 Nivel Introductorio
Documentación y comentarios efectivos
La documentación es esencial para crear código legible y mantenible. Los comentarios efectivos ayudan a otros desarrolladores (y a ti mismo en el futuro) a entender el propósito y funcionamiento del código.
Importancia de la documentación
- Facilita el mantenimiento: Ayuda a comprender rápidamente cómo funciona el código y facilita futuras modificaciones.
- Mejora la colaboración: Permite que otros desarrolladores entiendan y trabajen con tu código más fácilmente.
- Ayuda en la depuración: Proporciona contexto sobre las decisiones de diseño y lógica.
Tipos de comentarios
Comentarios de una línea:
cpp// Esto es un comentario de una líneaComentarios de múltiples líneas:
cpp/* Esto es un comentario de múltiples líneas */
Buenas prácticas en comentarios
- Se claro y conciso: Evita comentarios redundantes o innecesarios.
- Actualiza los comentarios: Asegúrate de que los comentarios reflejen el código actual.
- Explica el "por qué": Los comentarios deben explicar la intención detrás del código, no solo describir lo que hace.
Documentación automatizada
Doxygen: Herramienta que genera documentación a partir de comentarios en el código.
Ejemplo de comentario para Doxygen:
cpp/** * @brief Calcula el área de un círculo. * @param radio El radio del círculo. * @return El área calculada. */ double calcularAreaCirculo(double radio) { return 3.1416 * radio * radio; }
Estándares de codificación
Los estándares de codificación son un conjunto de reglas y recomendaciones para escribir código de manera consistente.
Beneficios
- Consistencia: Hace que el código sea más fácil de leer y entender.
- Legibilidad: Mejora la comprensión al seguir convenciones conocidas.
- Mantenibilidad: Facilita el trabajo en equipo y las futuras modificaciones.
Aspectos comunes en los estándares
Nomenclatura:
- Variables y funciones:
camelCaseosnake_case. - Clases y estructuras:
PascalCase. - Constantes: Mayúsculas y guiones bajos, e.g.,
MAX_TAMANO.
- Variables y funciones:
Indentación y espaciado:
- Uso consistente de espacios o tabulaciones.
- Indentar bloques de código dentro de estructuras como
if,for, etc.
Longitud de líneas:
- Limitar las líneas a 80 o 100 caracteres para mejorar la legibilidad.
Organización del código:
- Agrupar funciones relacionadas.
- Separar código en archivos
.hy.cpp.
Uso de llaves:
Colocar las llaves de apertura en la misma línea o en la siguiente, pero ser consistente.
cpp// Estilo 1 if (condicion) { // Código } // Estilo 2 if (condicion) { // Código }
12.2 Nivel Intermedio
Depuración y pruebas unitarias
La depuración y las pruebas son fundamentales para garantizar la calidad y confiabilidad del software.
Depuración con gdb
gdb es un depurador que permite ejecutar programas paso a paso, inspeccionar variables y entender el flujo del programa.
Compilación para depuración
Compila tu programa con la opción -g para incluir información de depuración:
g++ -g programa.cpp -o programaComandos básicos de gdb
Iniciar
gdb:bashgdb programaEstablecer un punto de interrupción:
gdb(gdb) break mainEjecutar el programa:
gdb(gdb) runEjecutar la siguiente línea (sin entrar en funciones):
gdb(gdb) nextEntrar en una función:
gdb(gdb) stepContinuar hasta el siguiente punto de interrupción:
gdb(gdb) continueImprimir el valor de una variable:
gdb(gdb) print variableListar código fuente:
gdb(gdb) listSalir de
gdb:gdb(gdb) quit
Uso de valgrind
valgrind es una herramienta para detectar errores de memoria, como fugas o accesos inválidos.
Comprobación de fugas de memoria
Ejecutar con
valgrind:bashvalgrind --leak-check=full ./programaInterpretar el informe:
- "Definitely lost": Memoria perdida sin referencias.
- "Indirectly lost": Memoria accesible solo a través de bloques perdidos.
- "Still reachable": Memoria no liberada pero aún referenciada.
Ejemplo de fuga de memoria
void crearArreglo() {
int* arreglo = new int[10];
// Olvidamos liberar la memoria
}
int main() {
crearArreglo();
return 0;
}Al ejecutar con valgrind, detectará que se asignó memoria que nunca fue liberada.
Pruebas unitarias
Las pruebas unitarias verifican que las unidades individuales de código funcionen correctamente.
Frameworks de pruebas
Google Test (gtest):
Ejemplo:
cpp// mi_codigo.h int sumar(int a, int b); // mi_codigo.cpp int sumar(int a, int b) { return a + b; } // test_mi_codigo.cpp #include <gtest/gtest.h> #include "mi_codigo.h" TEST(TestSuma, Positivos) { EXPECT_EQ(5, sumar(2, 3)); } int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }Catch2:
Ejemplo:
cpp#define CATCH_CONFIG_MAIN #include <catch2/catch.hpp> #include "mi_codigo.h" TEST_CASE("Sumar números positivos", "[sumar]") { REQUIRE(sumar(2, 3) == 5); }
Beneficios de las pruebas unitarias
- Detección temprana de errores.
- Facilita refactorizaciones.
- Documenta el comportamiento esperado.
12.3 Nivel Avanzado
Metaprogramación en tiempo de compilación
La metaprogramación permite realizar cálculos y operaciones en tiempo de compilación, lo que puede mejorar el rendimiento y detectar errores antes de ejecutar el programa.
Plantillas como metaprogramación
Las plantillas pueden utilizarse para implementar lógica compleja durante la compilación.
Ejemplo: Cálculo del factorial en tiempo de compilación
template <unsigned int N>
struct Factorial {
static const unsigned int value = N * Factorial<N - 1>::value;
};
template <>
struct Factorial<0> {
static const unsigned int value = 1;
};
// Uso
int main() {
std::cout << "Factorial de 5: " << Factorial<5>::value << std::endl; // Imprime 120
return 0;
}Uso de constexpr
Las funciones constexpr se evalúan en tiempo de compilación si se les proporcionan argumentos constantes.
Ejemplo:
constexpr int potencia(int base, int exponente) {
return (exponente == 0) ? 1 : base * potencia(base, exponente - 1);
}
int main() {
constexpr int resultado = potencia(2, 8); // resultado calculado en compilación
std::cout << "2^8 = " << resultado << std::endl; // Imprime 256
return 0;
}Optimización de código y profiling
La optimización mejora el rendimiento, y el profiling ayuda a identificar las partes del código que requieren optimización.
Optimización durante la compilación
Niveles de optimización:
-O0: Sin optimizaciones (por defecto).-O1: Optimización básica.-O2: Optimización moderada.-O3: Máxima optimización, puede aumentar el tiempo de compilación.
Ejemplo:
g++ -O2 programa.cpp -o programaUso de gprof para profiling
Pasos para usar gprof:
Compilar con información de profiling:
bashg++ -pg programa.cpp -o programaEjecutar el programa:
bash./programaGenerar el informe:
bashgprof programa gmon.out > informe.txt
Interpretación del informe
El informe muestra:
- Tiempo invertido en cada función.
- Número de llamadas a cada función.
- Relación entre funciones (quién llama a quién).
Patrones de diseño avanzados
Los patrones de diseño son soluciones reutilizables a problemas comunes en el diseño de software.
Patrón Decorador
Permite agregar responsabilidades adicionales a un objeto dinámicamente.
Ejemplo:
class Bebida {
public:
virtual std::string getDescripcion() = 0;
virtual double costo() = 0;
};
class Cafe : public Bebida {
public:
std::string getDescripcion() override {
return "Café";
}
double costo() override {
return 1.0;
}
};
class DecoradorBebida : public Bebida {
protected:
Bebida* bebida;
public:
DecoradorBebida(Bebida* b) : bebida(b) {}
};
class ConLeche : public DecoradorBebida {
public:
ConLeche(Bebida* b) : DecoradorBebida(b) {}
std::string getDescripcion() override {
return bebida->getDescripcion() + ", con leche";
}
double costo() override {
return bebida->costo() + 0.5;
}
};
// Uso
Bebida* miCafe = new Cafe();
miCafe = new ConLeche(miCafe);
std::cout << miCafe->getDescripcion() << ": $" << miCafe->costo() << std::endl;Salida:
Café, con leche: $1.5Patrón Strategy
Define una familia de algoritmos intercambiables.
Ejemplo:
class Ordenamiento {
public:
virtual void ordenar(std::vector<int>& datos) = 0;
};
class OrdenamientoBurbuja : public Ordenamiento {
public:
void ordenar(std::vector<int>& datos) override {
// Implementación del algoritmo de burbuja
}
};
class OrdenamientoQuickSort : public Ordenamiento {
public:
void ordenar(std::vector<int>& datos) override {
// Implementación de QuickSort
}
};
class Contexto {
private:
Ordenamiento* estrategia;
public:
void setEstrategia(Ordenamiento* est) {
estrategia = est;
}
void ejecutarEstrategia(std::vector<int>& datos) {
estrategia->ordenar(datos);
}
};
// Uso
Contexto contexto;
std::vector<int> datos = {5, 2, 9, 1};
Ordenamiento* burbuja = new OrdenamientoBurbuja();
Ordenamiento* quicksort = new OrdenamientoQuickSort();
contexto.setEstrategia(burbuja);
contexto.ejecutarEstrategia(datos); // Ordena usando burbuja
contexto.setEstrategia(quicksort);
contexto.ejecutarEstrategia(datos); // Ordena usando QuickSortPatrón Observer
Define una dependencia uno a muchos entre objetos, de manera que cuando uno cambia de estado, notifica a sus dependientes.
Ejemplo:
class Observador {
public:
virtual void actualizar(int valor) = 0;
};
class Sujeto {
private:
std::vector<Observador*> observadores;
int estado;
public:
void agregar(Observador* obs) {
observadores.push_back(obs);
}
void setEstado(int valor) {
estado = valor;
notificar();
}
void notificar() {
for (auto obs : observadores) {
obs->actualizar(estado);
}
}
};
class ObservadorConcreto : public Observador {
private:
int estadoObservado;
public:
void actualizar(int valor) override {
estadoObservado = valor;
std::cout << "Observador actualizado: " << estadoObservado << std::endl;
}
};
// Uso
Sujeto sujeto;
Observador* obs1 = new ObservadorConcreto();
Observador* obs2 = new ObservadorConcreto();
sujeto.agregar(obs1);
sujeto.agregar(obs2);
sujeto.setEstado(10); // Ambos observadores serán notificadosSalida:
Observador actualizado: 10
Observador actualizado: 10