Preguntas y Respuestas para Entrevistas de C++

C++Beginner
Practicar Ahora

Introducción

Bienvenido a esta guía completa diseñada para equiparte con el conocimiento y la confianza necesarios para destacar en entrevistas de C++. Navegar por las complejidades de C++ requiere una comprensión profunda de sus principios fundamentales, características avanzadas y aplicaciones prácticas. Este documento cubre meticulosamente un amplio espectro de temas, desde conceptos fundamentales y paradigmas de Programación Orientada a Objetos hasta intrincaciones del C++ moderno, estructuras de datos, algoritmos y principios de diseño de sistemas. Ya sea que te estés preparando para un puesto de nivel de entrada o un rol de ingeniería senior, este recurso proporciona respuestas detalladas, estrategias prácticas de resolución de problemas y perspectivas sobre las mejores prácticas, asegurando que estés bien preparado para abordar cualquier desafío. Embarquémonos en este viaje para dominar C++ y desbloquear tu potencial profesional.

CPP

Fundamentos y Conceptos Clave de C++

Explica la diferencia entre std::vector y std::list.

Respuesta:

std::vector es un array dinámico que proporciona asignación de memoria contigua, acceso aleatorio rápido (O(1)), pero inserciones/eliminaciones lentas en el medio (O(n)). std::list es una lista doblemente enlazada que ofrece inserciones/eliminaciones eficientes en cualquier lugar (O(1)), pero acceso aleatorio lento (O(n)) y una mayor sobrecarga de memoria por elemento.


¿Cuál es el propósito de la palabra clave virtual en C++?

Respuesta:

La palabra clave virtual habilita el polimorfismo al permitir que la implementación de una función miembro de una clase derivada sea llamada a través de un puntero o referencia de clase base. Asegura que la función anulada correcta se invoque en tiempo de ejecución según el tipo real del objeto, en lugar del tipo del puntero/referencia.


Describe el concepto de RAII (Resource Acquisition Is Initialization).

Respuesta:

RAII es un idioma de programación en C++ donde la gestión de recursos (por ejemplo, memoria, manejadores de archivos, mutexes) está ligada a la vida útil de un objeto. Los recursos se adquieren en el constructor y se liberan en el destructor. Esto garantiza que los recursos se liberen correctamente, incluso si ocurren excepciones, evitando fugas de recursos.


¿Cuál es la diferencia entre una copia superficial (shallow copy) y una copia profunda (deep copy)?

Respuesta:

Una copia superficial copia solo los valores de las variables miembro, lo que significa que si un miembro es un puntero, solo se copia el puntero en sí, no los datos a los que apunta. Ambos objetos comparten entonces el mismo recurso subyacente. Una copia profunda asigna nueva memoria para los datos apuntados y copia el contenido, asegurando que cada objeto tenga su propia copia independiente de los recursos.


¿Cuándo deberías usar const en C++?

Respuesta:

const debe usarse para declarar variables cuyos valores no deben cambiar, para especificar que un parámetro de función no será modificado y para marcar funciones miembro que no modifican el estado del objeto. Mejora la claridad del código, ayuda al compilador a optimizar y previene modificaciones accidentales.


Explica la diferencia entre nullptr, NULL y 0 en C++.

Respuesta:

nullptr es una palabra clave introducida en C++11 específicamente para representar un valor de puntero nulo, proporcionando seguridad de tipos y evitando ambigüedades con tipos enteros. NULL es típicamente una macro definida como 0 o (void*)0. 0 es un literal entero que puede convertirse implícitamente a un puntero nulo, pero también puede ser un valor entero, lo que lleva a posibles ambigüedades.


¿Qué son los punteros inteligentes (smart pointers) y por qué se utilizan?

Respuesta:

Los punteros inteligentes son objetos que actúan como punteros pero gestionan automáticamente la memoria a la que apuntan, previniendo fugas de memoria. Utilizan RAII para garantizar que la memoria asignada dinámicamente se desasigne cuando el puntero inteligente sale de su ámbito. Los tipos comunes incluyen std::unique_ptr (propiedad exclusiva) y std::shared_ptr (propiedad compartida con conteo de referencias).


¿Qué es la sobrecarga de operadores (operator overloading) en C++?

Respuesta:

La sobrecarga de operadores permite que los operadores de C++ (como +, -, ==, <<) se redefinan para tipos definidos por el usuario. Permite que los operadores se comporten de manera diferente según los tipos de los operandos, haciendo que el código sea más intuitivo y legible cuando se trabaja con clases personalizadas, como números complejos o contenedores personalizados.


Describe el concepto de semántica de movimiento (move semantics) en C++11.

Respuesta:

Las semánticas de movimiento, introducidas con las referencias rvalue, permiten que los recursos (como la memoria asignada dinámicamente) se 'muevan' de un objeto a otro en lugar de copiarse. Esto evita copias profundas costosas cuando los recursos de un objeto temporal ya no son necesarios, mejorando significativamente el rendimiento para operaciones como devolver objetos grandes de funciones o redimensionar contenedores.


¿Qué es la regla de tres/cinco/cero en C++?

Respuesta:

La Regla de Tres establece que si una clase define un destructor, un constructor de copia o un operador de asignación de copia, probablemente necesite los tres. La Regla de Cinco añade el constructor de movimiento y el operador de asignación de movimiento para C++11 y posteriores. La Regla de Cero, preferida con el C++ moderno, sugiere que si una clase no gestiona recursos brutos, no debería necesitar ninguna de estas funciones miembro especiales, confiando en su lugar en punteros inteligentes y contenedores de la biblioteca estándar.


Programación Orientada a Objetos (POO) en C++

¿Cuáles son los cuatro pilares principales de la Programación Orientada a Objetos (POO)? Explica brevemente cada uno.

Respuesta:

Los cuatro pilares principales son Encapsulación (agrupación de datos y métodos), Herencia (creación de nuevas clases a partir de las existentes), Polimorfismo (objetos que adoptan muchas formas) y Abstracción (ocultación de detalles complejos de implementación).


Explica el concepto de Encapsulación en C++ y por qué es importante.

Respuesta:

La encapsulación es la agrupación de datos y métodos que operan sobre los datos dentro de una única unidad (clase). Es importante para la ocultación de datos, protegiendo los datos del acceso externo y promoviendo la modularidad y la mantenibilidad al controlar el acceso a través de interfaces públicas.


¿Cuál es la diferencia entre polimorfismo en tiempo de compilación (estático) y en tiempo de ejecución (dinámico) en C++?

Respuesta:

El polimorfismo en tiempo de compilación se logra a través de la sobrecarga de funciones y la sobrecarga de operadores, resueltos en tiempo de compilación. El polimorfismo en tiempo de ejecución se logra a través de funciones virtuales y punteros/referencias a clases base, resueltos en tiempo de ejecución, lo que permite la invocación dinámica de métodos.


¿Cuándo deberías usar una clase abstracta frente a una interfaz (clase puramente virtual) en C++?

Respuesta:

Una clase abstracta se utiliza cuando deseas proporcionar una clase base con alguna implementación común y algunas funciones puramente virtuales. Una interfaz (una clase con solo funciones puramente virtuales) se utiliza cuando solo deseas definir un contrato que las clases derivadas deben implementar, sin ningún detalle de implementación.


Explica el propósito de la palabra clave 'virtual' en C++.

Respuesta:

La palabra clave 'virtual' se utiliza para lograr el polimorfismo en tiempo de ejecución. Cuando una función se declara virtual en una clase base, permite que las versiones de esa función de las clases derivadas sean llamadas a través de un puntero o referencia de clase base, permitiendo la invocación dinámica de métodos basada en el tipo real del objeto.


¿Qué es un constructor y un destructor en C++? ¿Cuándo se llaman?

Respuesta:

Un constructor es una función miembro especial que se llama automáticamente cuando se crea un objeto, y se utiliza para inicializar el estado del objeto. Un destructor es una función miembro especial que se llama automáticamente cuando se destruye un objeto, y se utiliza para liberar los recursos adquiridos por el objeto.


¿Qué es el puntero 'this' en C++?

Respuesta:

El puntero 'this' es un puntero implícito y constante disponible dentro de cualquier función miembro no estática de una clase. Apunta al objeto para el cual se llama la función miembro, permitiendo el acceso a los propios miembros del objeto y distinguiendo entre variables miembro y variables locales con el mismo nombre.


Diferencia entre los especificadores de acceso public, private y protected en C++.

Respuesta:

Los miembros públicos son accesibles desde cualquier lugar. Los miembros privados solo son accesibles desde dentro de la misma clase. Los miembros protegidos son accesibles desde dentro de la misma clase y desde las clases derivadas, pero no desde fuera de la jerarquía de clases.


¿Qué es la anulación de métodos (method overriding) y la sobrecarga de métodos (method overloading)?

Respuesta:

La anulación de métodos ocurre cuando una clase derivada proporciona una implementación específica para una función virtual ya definida en su clase base. La sobrecarga de métodos ocurre cuando múltiples funciones en el mismo ámbito tienen el mismo nombre pero diferentes parámetros (número, tipo u orden).


Explica el concepto de 'interfaz' en C++.

Respuesta:

En C++, una interfaz se implementa típicamente como una clase abstracta que contiene solo funciones puramente virtuales. Define un contrato que las clases concretas deben cumplir implementando todas las funciones puramente virtuales, asegurando un conjunto específico de comportamientos sin proporcionar ningún detalle de implementación.


¿Qué es la Regla de Tres/Cinco/Cero en C++?

Respuesta:

La Regla de Tres establece que si defines cualquiera de los siguientes: destructor, constructor de copia o operador de asignación de copia, deberías definir los tres. La Regla de Cinco extiende esto para incluir el constructor de movimiento y el operador de asignación de movimiento. La Regla de Cero sugiere que si no gestionas recursos brutos, no necesitas definir ninguno de ellos, confiando en las versiones generadas por el compilador.


Características Avanzadas de C++ y C++ Moderno

Explica el propósito de std::move y std::forward en C++11 y posteriores.

Respuesta:

std::move convierte incondicionalmente su argumento a una referencia rvalue, habilitando la semántica de movimiento (transferencia de propiedad de recursos). std::forward convierte condicionalmente su argumento a una referencia rvalue según si el argumento original era un rvalue, preservando las categorías de valor en escenarios de reenvío perfecto (perfect forwarding).


¿Cuál es la Regla de Cero, Tres y Cinco en C++?

Respuesta:

La Regla de Tres establece que si defines cualquiera de los siguientes: destructor, constructor de copia u operador de asignación de copia, deberías definir los tres. La Regla de Cinco extiende esto para incluir el constructor de movimiento y el operador de asignación de movimiento. La Regla de Cero sugiere que si tu clase no gestiona recursos directamente, no deberías definir ninguno de ellos y confiar en los valores predeterminados generados por el compilador o en punteros inteligentes.


Describe el concepto de 'reenvío perfecto' (perfect forwarding) y cómo std::forward lo facilita.

Respuesta:

El reenvío perfecto permite que una plantilla de función acepte argumentos arbitrarios y los reenvíe a otra función preservando sus categorías de valor originales (lvalue o rvalue) y calificadores const/volatile. std::forward es crucial para esto, ya que convierte condicionalmente su argumento a una referencia rvalue solo si el argumento original era un rvalue, asegurando la resolución de sobrecarga correcta para la llamada reenviada.


¿Qué son los punteros inteligentes (std::unique_ptr, std::shared_ptr, std::weak_ptr) y por qué se prefieren sobre los punteros brutos?

Respuesta:

Los punteros inteligentes son envoltorios RAII (Resource Acquisition Is Initialization) alrededor de punteros brutos que gestionan automáticamente la memoria, previniendo fugas de memoria y punteros colgantes (dangling pointers). unique_ptr proporciona propiedad exclusiva, shared_ptr permite la propiedad compartida a través del conteo de referencias, y weak_ptr rompe referencias circulares en ciclos de shared_ptr. Simplifican la gestión de recursos y mejoran la seguridad del código.


Explica la diferencia entre noexcept y throw() en C++.

Respuesta:

throw() (obsoleto en C++11) era una especificación de excepción dinámica que comprobaba en tiempo de ejecución si se lanzaba una excepción no listada, lo que llevaba a std::unexpected. noexcept (a partir de C++11) es una especificación en tiempo de compilación que indica que una función no lanzará excepciones. Si una función noexcept lanza una excepción, se llama a std::terminate, proporcionando garantías más sólidas para la optimización.


¿Qué es una expresión lambda en C++11 y cuáles son sus componentes principales?

Respuesta:

Una expresión lambda es un objeto de función anónimo que se puede definir en línea. Sus componentes principales son la cláusula de captura ([]), la lista de parámetros (()), la especificación mutable (opcional), la especificación de excepción (opcional), el tipo de retorno (opcional, deducido) y el cuerpo de la función ({}). Las lambdas son útiles para callbacks y algoritmos concisos.


¿Cómo difieren const y constexpr en C++?

Respuesta:

const indica que el valor de una variable no puede cambiarse después de la inicialización, o que una función miembro no modifica el estado del objeto. constexpr (a partir de C++11) indica que un valor o función puede evaluarse en tiempo de compilación. constexpr implica const para las variables, pero const no implica constexpr.


¿Qué es SFINAE (Substitution Failure Is Not An Error) y cómo se utiliza?

Respuesta:

SFINAE es un principio en metaprogramación de plantillas de C++ donde si una instanciación de plantilla falla durante la sustitución de parámetros de plantilla, no es un error, sino que esa sobrecarga o especialización particular se elimina del conjunto de candidatos. Se utiliza comúnmente con std::enable_if para habilitar o deshabilitar condicionalmente instanciaciones de plantillas basadas en rasgos de tipo (type traits).


Explica el concepto de 'plantillas variádicas' (variadic templates) en C++11.

Respuesta:

Las plantillas variádicas son plantillas que pueden tomar un número variable de argumentos. Utilizan paquetes de parámetros (typename... Args o Args...) para representar una secuencia de cero o más parámetros de plantilla o argumentos de función. Típicamente se procesan recursivamente o utilizando expresiones de plegado (fold expressions, C++17) para operar sobre cada elemento del paquete.


¿Qué son las 'referencias rvalue' y cómo habilitan la 'semántica de movimiento' (move semantics)?

Respuesta:

Las referencias rvalue (&&) se enlazan solo a rvalues (objetos temporales u objetos a punto de ser destruidos), distinguiéndolos de las referencias lvalue (&). Esta distinción permite al compilador elegir sobrecargas (constructores/operadores de asignación de movimiento) que 'roban' recursos de objetos temporales en lugar de realizar costosas copias profundas, habilitando así la semántica de movimiento y mejorando el rendimiento.


Describe el propósito de std::optional, std::variant y std::any en C++17.

Respuesta:

std::optional representa un valor opcional, que contiene un valor o está vacío, útil para funciones que podrían no devolver un resultado. std::variant es una unión segura de tipos, que contiene uno de un conjunto especificado de tipos en un momento dado. std::any puede contener un valor de cualquier tipo único, proporcionando almacenamiento heterogéneo seguro de tipos, similar a un puntero void pero con información de tipo.


Estructuras de Datos y Algoritmos en C++

Explica la diferencia entre std::vector y std::list en C++. ¿Cuándo elegirías uno sobre el otro?

Respuesta:

std::vector es un array dinámico que proporciona memoria contigua, acceso aleatorio rápido (O(1)), pero inserciones/eliminaciones lentas en el medio (O(N)). std::list es una lista doblemente enlazada, que ofrece inserciones/eliminaciones en O(1) en cualquier lugar pero acceso aleatorio en O(N). Elige vector para acceso aleatorio frecuente y list para inserciones/eliminaciones frecuentes en el medio.


¿Qué es una tabla hash (o mapa hash) y cómo funciona std::unordered_map en C++?

Respuesta:

Una tabla hash almacena pares clave-valor utilizando una función hash para calcular un índice en un array de cubetas (buckets). std::unordered_map es la implementación de tabla hash de C++. Utiliza hashing para mapear claves a índices de cubetas, y típicamente maneja colisiones utilizando encadenamiento separado (listas enlazadas en las cubetas) o direccionamiento abierto, proporcionando una complejidad de tiempo promedio de O(1) para inserciones, eliminaciones y búsquedas.


Describe el concepto de notación Big O. Proporciona ejemplos para O(1), O(N) y O(N^2).

Respuesta:

La notación Big O describe el límite superior de la complejidad temporal o espacial de un algoritmo a medida que crece el tamaño de la entrada. O(1) es tiempo constante (por ejemplo, acceso a elementos de array). O(N) es tiempo lineal (por ejemplo, iterar a través de una lista). O(N^2) es tiempo cuadrático (por ejemplo, bucles anidados para bubble sort).


Explica la diferencia entre una pila (stack) y una cola (queue). ¿Cuáles son sus operaciones principales?

Respuesta:

Una pila es una estructura de datos LIFO (Last-In, First-Out - Último en entrar, primero en salir), mientras que una cola es una estructura de datos FIFO (First-In, First-Out - Primero en entrar, primero en salir). Las operaciones principales de una pila son push (añadir a la cima) y pop (eliminar de la cima). Las operaciones principales de una cola son enqueue (añadir al final) y dequeue (eliminar del frente).


¿Qué es un árbol de búsqueda binaria (BST)? ¿Cuáles son sus ventajas y desventajas?

Respuesta:

Un BST es una estructura de datos basada en árboles donde el valor del hijo izquierdo es menor que el del padre, y el valor del hijo derecho es mayor. Las ventajas incluyen búsqueda, inserción y eliminación eficientes (promedio O(log N)). Las desventajas incluyen la posibilidad de árboles sesgados (peor caso O(N)) y una mayor sobrecarga de memoria en comparación con los arrays.


¿Cómo funciona quicksort? ¿Cuál es su complejidad temporal promedio y en el peor caso?

Respuesta:

Quicksort es un algoritmo de ordenación de divide y vencerás. Selecciona un elemento como pivote y particiona el array alrededor del pivote, colocando los elementos más pequeños a su izquierda y los más grandes a su derecha. Luego ordena recursivamente los sub-arrays. Su complejidad temporal promedio es O(N log N), pero su peor caso es O(N^2) si la selección del pivote conduce consistentemente a particiones muy desequilibradas.


¿Cuál es el propósito de std::map en C++? ¿Cómo difiere de std::unordered_map?

Respuesta:

std::map es un contenedor asociativo que almacena pares clave-valor en orden ordenado según las claves, típicamente implementado como un árbol binario de búsqueda auto-balanceado (por ejemplo, árbol rojo-negro). Proporciona una complejidad de tiempo de O(log N) para las operaciones. std::unordered_map utiliza hashing y ofrece una complejidad promedio de O(1) pero no mantiene el orden ordenado.


Explica el concepto de recursión. Proporciona un ejemplo simple.

Respuesta:

La recursión es una técnica de programación donde una función se llama a sí misma para resolver un problema. Implica un caso base para detener la recursión y un paso recursivo que descompone el problema en subproblemas más pequeños y similares. Ejemplo: Calcular el factorial (n!) donde factorial(n) = n * factorial(n-1) con factorial(0) = 1.


¿Qué es una estructura de datos de grafo? Nombra dos formas comunes de representar un grafo.

Respuesta:

Un grafo es una estructura de datos no lineal que consta de nodos (vértices) y aristas que los conectan. Puede representar relaciones entre entidades. Dos representaciones comunes son la Matriz de Adyacencia (una matriz 2D donde matrix[i][j] indica una arista entre i y j) y la Lista de Adyacencia (un array o mapa donde cada índice/clave representa un vértice y su valor es una lista de sus vecinos).


¿Cuándo usarías un std::set en lugar de un std::vector o std::list?

Respuesta:

std::set es un contenedor asociativo que almacena elementos únicos en orden ordenado, típicamente implementado como un BST auto-balanceado. Usa std::set cuando necesites almacenar elementos únicos, mantenerlos en orden ordenado y realizar búsquedas, inserciones y eliminaciones eficientes (O(log N)). vector y list permiten duplicados y no mantienen inherentemente el orden ordenado.


Diseño de Sistemas y Concurrencia en C++

Explica la diferencia entre un proceso y un hilo (thread). ¿Cuándo elegirías uno sobre el otro?

Respuesta:

Un proceso es una unidad de ejecución independiente con su propio espacio de memoria, mientras que un hilo es una unidad de ejecución ligera dentro de un proceso, que comparte su memoria. Elige procesos para aislamiento y robustez (por ejemplo, aplicaciones separadas), y hilos para concurrencia dentro de una sola aplicación para compartir datos y reducir la sobrecarga.


¿Qué es un mutex en C++ y cómo se utiliza para prevenir condiciones de carrera (race conditions)?

Respuesta:

Un mutex (exclusión mutua) es un primitivo de sincronización que protege los recursos compartidos del acceso simultáneo por múltiples hilos. Un hilo adquiere el mutex antes de acceder al recurso compartido y lo libera después, asegurando que solo un hilo pueda acceder a la sección crítica a la vez, previniendo así las condiciones de carrera.


Describe un escenario común donde puede ocurrir un interbloqueo (deadlock). ¿Cómo puedes prevenirlo?

Respuesta:

Un interbloqueo ocurre cuando dos o más hilos se bloquean indefinidamente, cada uno esperando que el otro libere un recurso. Un escenario común es cuando dos hilos poseen cada uno un mutex e intentan adquirir el otro. Las estrategias de prevención incluyen el ordenamiento consistente de bloqueos (locks), el uso de std::lock, o el empleo de std::unique_lock con std::defer_lock.


¿Qué es una variable de condición (condition variable) en C++ y cuándo es útil?

Respuesta:

Una variable de condición permite a los hilos esperar a que una cierta condición se vuelva verdadera. Es útil para patrones productor-consumidor o cuando un hilo necesita señalar a otro que ha ocurrido algún evento. Los hilos esperan en la variable de condición, y otro hilo los notifica cuando se cumple la condición, típicamente en conjunto con un mutex.


Explica el concepto de atomicidad. ¿Cómo se pueden lograr operaciones atómicas en C++?

Respuesta:

La atomicidad significa que una operación es indivisible y parece ocurrir instantáneamente, completándose enteramente o no ocurriendo en absoluto. En C++, las operaciones atómicas se pueden lograr utilizando tipos std::atomic para tipos de datos fundamentales, o protegiendo las secciones críticas con mutexes para operaciones más complejas.


¿Para qué se utilizan std::future y std::promise en la concurrencia de C++?

Respuesta:

std::promise se utiliza para establecer un valor o una excepción que será recuperado por un objeto std::future. std::future proporciona una forma de acceder al resultado de una operación asíncrona. Juntos, permiten la comunicación asíncrona y la recuperación de resultados de tareas que se ejecutan en hilos separados.


¿Cómo std::async simplifica la ejecución de tareas asíncronas en comparación con la creación manual de std::thread?

Respuesta:

std::async simplifica la ejecución asíncrona gestionando automáticamente la creación (o reutilización) de hilos, la ejecución y la recuperación de resultados. Devuelve directamente un std::future, manejando posibles excepciones y la lógica de join/detach, mientras que std::thread requiere la gestión manual de estos aspectos.


Discute las compensaciones (trade-offs) entre usar std::shared_ptr y punteros brutos en un entorno multihilo.

Respuesta:

std::shared_ptr proporciona gestión automática de memoria y conteo de referencias seguro para hilos, reduciendo fugas de memoria y punteros colgantes. Sin embargo, las actualizaciones de su contador de referencias son atómicas, lo que incurre en una sobrecarga de rendimiento. Los punteros brutos son más rápidos pero requieren una gestión manual cuidadosa de la memoria y son propensos a condiciones de carrera si no están protegidos por mutexes en el acceso concurrente.


¿Qué es un pool de hilos (thread pool) y por qué es beneficioso en el diseño de sistemas?

Respuesta:

Un pool de hilos es una colección de hilos pre-inicializados que pueden ser reutilizados para ejecutar tareas. Es beneficioso porque reduce la sobrecarga de crear y destruir hilos para cada tarea, limita el número de hilos concurrentes para prevenir el agotamiento de recursos y mejora la respuesta general del sistema y el rendimiento.


Al diseñar un sistema concurrente de alto rendimiento, ¿cuáles son algunas consideraciones clave con respecto a la coherencia de caché (cache coherence) y el falso compartido (false sharing)?

Respuesta:

La coherencia de caché asegura que todos los procesadores vean una vista consistente de la memoria. El falso compartido ocurre cuando elementos de datos no relacionados, accedidos por diferentes hilos, residen en la misma línea de caché, causando invalidaciones innecesarias de líneas de caché y degradación del rendimiento. Las consideraciones de diseño incluyen una disposición cuidadosa de los datos (padding) y evitar el estado mutable compartido siempre que sea posible.


Resolución Práctica de Problemas y Desafíos de Codificación

Dado un array ordenado y un valor objetivo, devuelve el índice si se encuentra el objetivo. Si no, devuelve el índice donde estaría si se insertara en orden. Asume que no hay duplicados.

Respuesta:

Este es un problema clásico de búsqueda binaria. Inicializa low = 0, high = n-1. Mientras low <= high, calcula mid. Si nums[mid] == target, devuelve mid. Si nums[mid] < target, low = mid + 1. De lo contrario, high = mid - 1. Finalmente, devuelve low.


Explica cómo detectar un ciclo en una lista enlazada y proporciona un algoritmo de alto nivel.

Respuesta:

Utiliza el Algoritmo de Detección de Ciclos de Floyd (Tortuga y Liebre). Inicializa dos punteros, slow y fast, ambos comenzando en la cabeza. slow se mueve un paso a la vez, fast se mueve dos pasos. Si alguna vez se encuentran, existe un ciclo. Si fast llega a nullptr o fast->next es nullptr, no hay ciclo.


¿Cómo invertirías una cadena en su lugar (in-place) en C++?

Respuesta:

Usa dos punteros, left comenzando al principio y right al final de la cadena. Intercambia los caracteres en left y right, luego incrementa left y decrementa right. Continúa hasta que left cruce right. Esto modifica la cadena directamente sin espacio adicional.


Describe la diferencia entre std::vector y std::list en términos de disposición de memoria y características de rendimiento.

Respuesta:

std::vector almacena elementos de forma contigua en memoria, permitiendo acceso aleatorio O(1) y eficiencia de caché. Las inserciones/eliminaciones en el medio son O(N). std::list es una lista doblemente enlazada, que almacena elementos de forma no contigua. Las inserciones/eliminaciones son O(1) una vez que se encuentra el iterador, pero el acceso aleatorio es O(N) debido al recorrido.


Implementa una función para verificar si una cadena dada es un palíndromo, ignorando caracteres no alfanuméricos y la capitalización.

Respuesta:

Usa dos punteros, left y right. Mueve left hacia adelante y right hacia atrás, saltando caracteres no alfanuméricos. Convierte los caracteres válidos a minúsculas. Compara los caracteres en left y right. Si no coinciden, no es un palíndromo. Continúa hasta que left >= right.


Dado un array de enteros, encuentra la suma máxima de un subarray contiguo.

Respuesta:

Este es el Algoritmo de Kadane. Mantén current_max y global_max. Itera a través del array: current_max = max(num, current_max + num). Actualiza global_max = max(global_max, current_max) en cada iteración. Inicializa ambos con el primer elemento o infinito negativo.


Explica cómo encontrar el 'k'ésimo elemento más pequeño en un array no ordenado de manera eficiente.

Respuesta:

El enfoque más eficiente es Quickselect, que es una variación de Quicksort. Tiene una complejidad de tiempo promedio de O(N). Alternativamente, usar un min-heap (cola de prioridad) y extraer k elementos sería O(N log K), o ordenar el array primero sería O(N log N).


¿Cómo implementarías una caché LRU (Least Recently Used) básica?

Respuesta:

Usa una std::list (o std::deque) para mantener el orden de uso y un std::unordered_map para almacenar pares clave-valor junto con iteradores a sus nodos de lista correspondientes. Al acceder, mueve el elemento al principio de la lista. Al insertar cuando está llena, elimina el elemento al final de la lista.


Dados dos arrays ordenados, mézclalos en un único array ordenado.

Respuesta:

Usa dos punteros, uno para cada array, comenzando desde sus inicios. Compara los elementos apuntados y agrega el menor al array de resultados, avanzando su puntero. Si un array se agota, adjunta los elementos restantes del otro. Esto es O(M+N) en tiempo y O(M+N) en espacio.


Describe un método para encontrar todas las permutaciones de una cadena dada.

Respuesta:

Esto se puede resolver usando recursión y backtracking. Para cada carácter, intercámbialo con cada carácter a su derecha (incluido él mismo) y encuentra recursivamente las permutaciones para la subcadena restante. Usa un std::set o verifica duplicados si la cadena de entrada tiene caracteres repetidos.


Depuración, Pruebas y Optimización de Rendimiento

Describe técnicas comunes de depuración en C++. ¿Cómo abordas un error difícil de reproducir?

Respuesta:

Las técnicas comunes incluyen el uso de un depurador (puntos de interrupción, ejecución paso a paso), registro (logging) y comprobaciones de aserción (assert). Para errores difíciles de reproducir, intentaría reducir el alcance, agregar un registro exhaustivo, usar puntos de interrupción condicionales y considerar técnicas como la bisección o los sanitizadores de memoria (ASan, MSan).


¿Cuál es el propósito de assert() en C++? ¿Cuándo deberías usarlo en lugar de lanzar una excepción?

Respuesta:

assert() se utiliza para depuración para verificar condiciones que siempre deberían ser verdaderas. Si la condición es falsa, termina el programa. Usa assert() para errores de lógica interna que indican un bug, y excepciones para errores de tiempo de ejecución recuperables que el código externo podría manejar.


Explica el concepto de pruebas unitarias (unit testing). ¿Cuáles son algunos frameworks populares de pruebas unitarias en C++?

Respuesta:

Las pruebas unitarias implican probar componentes o funciones individuales de un programa de forma aislada para asegurar que funcionen como se espera. Ayuda a detectar errores temprano y facilita la refactorización. Los frameworks populares de C++ incluyen Google Test (GTest), Catch2 y Boost.Test.


¿Cómo identificas los cuellos de botella de rendimiento en una aplicación C++?

Respuesta:

Utilizaría un perfilador (profiler) (por ejemplo, Callgrind de Valgrind, perf, Google Perftools) para identificar los "puntos calientes" (hot spots) en el código, como funciones que consumen la mayor parte del tiempo de CPU o memoria. Analizar grafos de llamadas y fallos de caché (cache misses) también ayuda a localizar cuellos de botella.


¿Cuál es la diferencia entre una compilación de lanzamiento (release build) y una compilación de depuración (debug build) en C++? ¿Por qué es importante esta distinción para el rendimiento?

Respuesta:

Una compilación de depuración incluye símbolos de depuración y deshabilita las optimizaciones, lo que facilita la depuración pero es más lento. Una compilación de lanzamiento habilita las optimizaciones del compilador y omite los símbolos de depuración, lo que resulta en ejecutables más rápidos y pequeños. Esta distinción es crucial porque las mediciones de rendimiento siempre deben realizarse en compilaciones de lanzamiento.


Nombra algunas técnicas comunes de optimización de rendimiento en C++ a nivel de código.

Respuesta:

Las técnicas incluyen minimizar las asignaciones de memoria, usar std::move para una transferencia eficiente de recursos, optimizar estructuras de datos para la localidad de caché, evitar copias innecesarias, usar la corrección const y aprovechar las optimizaciones del compilador (por ejemplo, desenrollado de bucles, inlining).


¿Qué es la 'Regla de Cero/Tres/Cinco' en C++? ¿Cómo se relaciona con la gestión de recursos y las posibles implicaciones de rendimiento?

Respuesta:

Dicta cómo gestionar los recursos. Regla de Cero: si no hay punteros/recursos brutos, los miembros especiales predeterminados están bien. Regla de Tres/Cinco: si defines un destructor, constructor de copia u operador de asignación de copia, probablemente necesites definir los tres (o cinco, incluyendo constructor/asignación de movimiento). Esto previene fugas de recursos y asegura copias profundas correctas, lo que puede afectar el rendimiento si no se maneja eficientemente (por ejemplo, copias excesivas).


¿Cómo puede la corrección const contribuir a una mejor calidad de código y potencialmente al rendimiento?

Respuesta:

La corrección const ayuda a imponer la inmutabilidad, haciendo que el código sea más seguro y fácil de razonar al prevenir modificaciones accidentales. También permite al compilador realizar optimizaciones más agresivas, ya que sabe que ciertos datos no cambiarán, lo que potencialmente conduce a un mejor rendimiento.


Explica el concepto de 'localidad de caché' (cache locality) y por qué es importante para el rendimiento en C++.

Respuesta:

La localidad de caché se refiere a la organización de los patrones de acceso a datos para maximizar los aciertos de caché (cache hits). Las CPU modernas son mucho más rápidas que la memoria principal, por lo que acceder a datos que ya están en la caché de la CPU es significativamente más rápido. Una buena localidad de caché (temporal y espacial) reduce la latencia de acceso a la memoria, lo que genera mejoras sustanciales de rendimiento.


¿Cuándo usarías un analizador estático (static analyzer) en el desarrollo de C++, y qué beneficios proporciona?

Respuesta:

Usaría un analizador estático (por ejemplo, Clang-Tidy, Cppcheck) de forma temprana y regular en el ciclo de desarrollo. Ayuda a identificar posibles errores, violaciones de estándares de codificación y problemas de diseño sin ejecutar el código, mejorando la calidad del código, la mantenibilidad y previniendo errores en tiempo de ejecución.


Preguntas Basadas en Escenarios y Patrones de Diseño

Estás diseñando un sistema de registro (logging). ¿Cómo te asegurarías de que solo exista una instancia del logger en toda la aplicación y que sea fácilmente accesible?

Respuesta:

Utilizaría el patrón de diseño Singleton. Esto asegura una única instancia y proporciona un punto de acceso global. Un constructor privado y un método estático para obtener la instancia son componentes clave.


Describe un escenario donde el patrón de diseño Observer sería beneficioso. ¿Cómo lo implementarías en C++?

Respuesta:

Útil cuando el cambio de estado de un objeto necesita notificar a múltiples objetos dependientes sin acoplarlos. Por ejemplo, elementos de la interfaz de usuario que se actualizan basándose en cambios en el modelo de datos. Implementar con una interfaz abstracta Subject (publicador) y Observer (suscriptor), donde Subject mantiene una lista de Observers a notificar.


Necesitas crear varios tipos de documentos (por ejemplo, PDF, HTML, TXT) a partir de una fuente de datos común, pero la lógica de creación para cada tipo de documento es compleja y varía. ¿Qué patrón de diseño usarías?

Respuesta:

El patrón Factory Method. Define una interfaz para crear un objeto, pero permite que las subclases decidan qué clase instanciar. Esto desacopla el código cliente de las clases concretas que instancia, permitiendo agregar fácilmente nuevos tipos de documentos.


¿Cómo diseñarías un sistema para procesar diferentes tipos de paquetes de red (por ejemplo, TCP, UDP, ICMP) donde cada tipo de paquete requiere una lógica de manejo específica?

Respuesta:

El patrón Strategy. Define una interfaz común para el manejo de paquetes y luego implementa estrategias concretas para cada tipo de paquete. La lógica de procesamiento principal puede entonces cambiar dinámicamente entre estas estrategias según el tipo de paquete, promoviendo la flexibilidad y la extensibilidad.


Tienes una biblioteca existente que proporciona una clase con una interfaz que no coincide con las necesidades de tu aplicación actual. ¿Cómo puedes usar esta clase sin modificar su código fuente?

Respuesta:

Utiliza el patrón Adapter. Crea una clase adaptadora que implemente la interfaz que tu aplicación espera y que internamente utilice una instancia de la clase de la biblioteca existente, traduciendo las llamadas entre las dos interfaces.


Considera un escenario donde necesitas agregar nuevas funcionalidades (por ejemplo, registro, comprobaciones de seguridad, caché) a objetos existentes sin alterar su estructura. ¿Qué patrón es adecuado?

Respuesta:

El patrón Decorator. Permite agregar comportamiento a un objeto individual, dinámicamente, sin afectar el comportamiento de otros objetos de la misma clase. Envuelve el objeto original con un objeto decorador que agrega la nueva funcionalidad.


Estás construyendo una aplicación GUI compleja. ¿Cómo separarías los datos de la aplicación (modelo) de su presentación (vista) y la lógica de interacción del usuario (controlador)?

Respuesta:

Utiliza el patrón Model-View-Controller (MVC). El Modelo gestiona los datos y la lógica de negocio, la Vista muestra los datos y el Controlador maneja la entrada del usuario y actualiza tanto el Modelo como la Vista. Esta separación mejora la mantenibilidad y la capacidad de prueba.


¿Cuándo preferirías usar una función virtual en lugar de un puntero a función para implementar un comportamiento polimórfico?

Respuesta:

Las funciones virtuales se prefieren para el polimorfismo en tiempo de compilación dentro de una jerarquía de clases, permitiendo la distribución dinámica (dynamic dispatch) basada en el tipo real del objeto. Los punteros a función ofrecen flexibilidad en tiempo de ejecución para llamar a diferentes funciones, pero no soportan inherentemente el polimorfismo orientado a objetos ni las búsquedas en tablas virtuales.


Necesitas crear una familia de objetos relacionados (por ejemplo, diferentes tipos de widgets de UI para Windows, Mac y Linux) sin especificar sus clases concretas. ¿Qué patrón usarías?

Respuesta:

El patrón Abstract Factory. Proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas. Esto te permite cambiar entre diferentes 'fábricas' (por ejemplo, WindowsWidgetFactory, MacWidgetFactory) para producir widgets específicos de la plataforma.


¿Cómo manejarías una situación en la que el estado de un objeto cambia y se requieren diferentes comportamientos basados en ese estado, sin usar grandes sentencias condicionales?

Respuesta:

El patrón State. Permite que un objeto altere su comportamiento cuando cambia su estado interno. El objeto parece cambiar de clase. Cada estado está encapsulado en una clase separada, y el objeto de contexto delega el comportamiento a su objeto de estado actual.


Mejores Prácticas, Idiomas y Calidad del Código

¿Cuál es la Regla de Cero, Tres, Cinco o Seis en C++?

Respuesta:

La Regla de Cero establece que si no gestionas recursos tú mismo, no necesitas definir destructores, constructores de copia/movimiento, u operadores de asignación de copia/movimiento personalizados. La Regla de Tres/Cinco/Seis se aplica cuando sí gestionas recursos, requiriendo que definas estas funciones miembro especiales (destructor, constructor de copia, asignación de copia, constructor de movimiento, asignación de movimiento y, opcionalmente, constructor predeterminado) para manejar correctamente la propiedad de los recursos y evitar problemas como la doble liberación o fugas de memoria.


Explica el idioma RAII (Resource Acquisition Is Initialization) y proporciona un ejemplo.

Respuesta:

RAII es un idioma de programación en C++ donde la adquisición de recursos (como la asignación de memoria o la apertura de archivos) está ligada a la inicialización del objeto, y la desasignación de recursos está ligada a la destrucción del objeto. Esto asegura que los recursos se liberen correctamente cuando el objeto sale de ámbito, incluso si ocurren excepciones. std::unique_ptr y std::lock_guard son ejemplos comunes.


¿Por qué es importante la corrección const en C++?

Respuesta:

La corrección const asegura que los objetos o datos marcados como constantes no puedan ser modificados, mejorando la seguridad, legibilidad y mantenibilidad del código. Permite al compilador imponer la inmutabilidad, ayuda a prevenir efectos secundarios accidentales y permite una mejor optimización. También permite que los objetos const se pasen a funciones que esperan referencias const.


¿Cuál es el propósito de usar std::move y std::forward?

Respuesta:

std::move convierte su argumento a una referencia rvalue, indicando que los recursos del objeto pueden ser 'movidos' desde él, habilitando la semántica de movimiento. std::forward convierte condicionalmente su argumento a una referencia rvalue basándose en si el argumento original era un rvalue, preservando la categoría de valor (lvalue o rvalue) de un argumento reenviado en escenarios de reenvío perfecto, típicamente dentro de funciones de plantilla.


¿Cuándo deberías preferir std::unique_ptr sobre std::shared_ptr?

Respuesta:

Prefiere std::unique_ptr cuando necesites propiedad exclusiva de un objeto asignado dinámicamente. Tiene una sobrecarga mínima e indica claramente la propiedad única. Usa std::shared_ptr solo cuando múltiples propietarios necesiten compartir el mismo recurso, ya que implica una sobrecarga de conteo de referencias.


¿Cuáles son algunos beneficios de usar nullptr en lugar de NULL o 0 para punteros nulos?

Respuesta:

nullptr es un tipo distinto (std::nullptr_t) que puede convertirse implícitamente a cualquier tipo de puntero, pero no a tipos integrales. Esto previene errores comunes como llamar accidentalmente a una función sobrecargada que espera un entero cuando se pretendía un puntero, mejorando la seguridad de tipos y la claridad del código en comparación con NULL (que a menudo es 0 o (void*)0) o 0.


Explica el concepto del idioma 'PIMPL' (Pointer to IMPLementation).

Respuesta:

El idioma PIMPL oculta los detalles de implementación de una clase moviéndolos a un objeto separado, asignado dinámicamente, al que apunta un puntero privado. Esto reduce las dependencias de compilación, mejora los tiempos de compilación y permite realizar cambios en la implementación privada sin recompilar el código cliente. También ayuda a mantener la compatibilidad binaria.


¿Por qué generalmente es una mala práctica usar using namespace std; en archivos de encabezado (header files)?

Respuesta:

Usar using namespace std; en archivos de encabezado contamina el espacio de nombres global para cualquier archivo que incluya ese encabezado. Esto puede llevar a colisiones de nombres y errores de ambigüedad, especialmente en proyectos grandes o al combinar bibliotecas. Es mejor calificar explícitamente los nombres (por ejemplo, std::vector) o usar declaraciones using dentro de ámbitos específicos (por ejemplo, dentro de un archivo .cpp o una función).


¿Cuál es el propósito de la palabra clave explicit para los constructores?

Respuesta:

La palabra clave explicit previene las conversiones implícitas del tipo de un constructor de un solo argumento al tipo de la clase. Esto evita creaciones de objetos o conversiones de tipo no deseadas, haciendo el código más seguro y predecible. Por ejemplo, explicit MyClass(int) previene MyClass obj = 5; pero permite MyClass obj(5);.


¿Cómo evitas que una clase sea copiada o movida?

Respuesta:

Para prevenir que una clase sea copiada, declara su constructor de copia y su operador de asignación de copia como delete. Para prevenir el movimiento, declara su constructor de movimiento y su operador de asignación de movimiento como delete. Por ejemplo: MyClass(const MyClass&) = delete; MyClass& operator=(const MyClass&) = delete;.


Resumen

Dominar C++ para entrevistas es un viaje que recompensa la preparación diligente. Este documento ha proporcionado una base de preguntas comunes y respuestas perspicaces, equipándote con el conocimiento para discutir con confianza conceptos centrales, características avanzadas y enfoques para la resolución de problemas. Recuerda, el éxito en una entrevista no se trata solo de saber las respuestas correctas, sino también de demostrar tu comprensión, pasión y capacidad para pensar críticamente.

El panorama de C++ está en constante evolución, y el aprendizaje continuo es clave para mantenerse a la vanguardia. Utiliza esta guía como un trampolín para una exploración más profunda, práctica y codificación práctica. Abraza nuevos desafíos, contribuye a proyectos y nunca dejes de perfeccionar tus habilidades. Tu dedicación al aprendizaje sin duda allanará el camino para una carrera exitosa y gratificante en el desarrollo de software.