Logo de Adafaceadaface

90 Preguntas de Entrevista en C++ para Contratar a los Mejores Ingenieros

Al evaluar a los candidatos de C++, los reclutadores y gerentes de contratación necesitan una forma confiable de evaluar sus habilidades. Puede ser desalentador navegar por los matices de C++ e identificar la verdadera experiencia, como se analiza en nuestra publicación de blog sobre las habilidades requeridas para un desarrollador de C++.

Esta publicación de blog proporciona una lista seleccionada de preguntas de entrevista de C++ categorizadas por nivel de experiencia, desde recién graduados hasta profesionales experimentados, incluida una sección de preguntas de opción múltiple (MCQ). Está diseñado para proporcionarle las preguntas correctas para evaluar la comprensión y las habilidades prácticas de un candidato.

Al usar estas preguntas, puede identificar el mejor talento de C++; para una evaluación más optimizada y objetiva, considere usar nuestra prueba en línea de C++ antes de la entrevista.

Tabla de contenidos

Preguntas de entrevista de C++ para recién graduados

Preguntas de entrevista de C++ para juniors

Preguntas de entrevista intermedias de C++

Preguntas de entrevista de C++ para experimentados

MCQ de C++

¿Qué habilidades de C++ debe evaluar durante la fase de entrevista?

3 consejos para usar preguntas de entrevista de C++

Contrate al mejor talento de C++ con evaluaciones de habilidades

Descargue la plantilla de preguntas de entrevista de C++ en múltiples formatos

Preguntas de entrevista de C++ para recién graduados

1. ¿Cuál es la diferencia entre `struct` y `class` en C++? Es como preguntar: '¿Son gemelos o solo buenos amigos?'

En C++, la principal diferencia entre struct y class reside en su especificador de acceso predeterminado. Los miembros de una struct son públicos por defecto, mientras que los miembros de una class son privados por defecto. Esto significa que si no especificas explícitamente un especificador de acceso (como public:, private:, o protected:) para un miembro en una struct, será accesible desde cualquier lugar. Por el contrario, en una class, necesitarías declarar explícitamente los miembros como public para hacerlos accesibles desde fuera de la clase.

Más allá del acceso predeterminado, son esencialmente lo mismo. Tanto struct como class pueden tener funciones miembro, herencia, y pueden ser usadas para crear objetos. En esencia, struct se usa a menudo para estructuras de datos simples, mientras que class se prefiere para objetos más complejos con encapsulación.

2. ¿Puedes explicar qué es un puntero en C++? Imagínalo como un mapa del tesoro que te dice dónde está escondido algo valioso.

En C++, un puntero es una variable que almacena la dirección de memoria de otra variable. Piense en ello como un mapa del tesoro; el mapa en sí no es el tesoro, pero le dice dónde encontrar el tesoro. De manera similar, el puntero no es el dato real, sino que contiene la dirección de dónde se almacenan los datos en la memoria.

Aquí hay un ejemplo simple:

int number = 42; int *pointerToNumber = &number; // pointerToNumber ahora contiene la dirección de memoria de number //Para acceder al valor en la dirección, utilice el operador de desreferencia * int value = *pointerToNumber; //value will be 42

  • & es el operador "dirección de", que le da la dirección de memoria de una variable.
  • * al declarar una variable puntero (por ejemplo, int *pointer) significa "esta variable almacenará una dirección de memoria de un entero". Cuando se usa en un puntero existente (por ejemplo, *pointerToNumber), es el operador de desreferencia que obtiene el valor almacenado en esa dirección de memoria.

3. ¿Cuál es el propósito de las palabras clave `new` y `delete` en C++? Piense en ellas como herramientas para pedir prestados y devolver juguetes de una gran biblioteca de juguetes.

En C++, new y delete son operadores utilizados para la gestión dinámica de la memoria. new se utiliza para asignar memoria en el heap (la "biblioteca de juguetes"), que es una región de memoria disponible para el programa durante la ejecución. Cuando usas new, básicamente estás pidiendo prestado un juguete de la biblioteca. Devuelve un puntero a la memoria recién asignada. Por ejemplo: int* ptr = new int;.

delete se utiliza para liberar la memoria que se asignó previamente con new. Esto devuelve el "juguete" a la biblioteca. Si no usas delete, la memoria permanece asignada, lo que lleva a una fuga de memoria. Por ejemplo: delete ptr; ptr = nullptr; (establecer el puntero en nullptr después de eliminarlo es una buena práctica para evitar punteros colgantes).

4. ¿Qué es un constructor en C++? ¿Por qué es útil? Imagina que es como configurar un juguete nuevo cuando lo recibes, para que esté listo para jugar.

En C++, un constructor es una función miembro especial de una clase que se llama automáticamente cuando se crea un objeto de esa clase. Su propósito principal es inicializar los miembros de datos del objeto y realizar cualquier otra configuración necesaria para asegurar que el objeto esté en un estado válido y utilizable.

Los constructores son útiles porque garantizan que los objetos se inicialicen correctamente antes de ser utilizados. Esto evita un comportamiento impredecible debido a datos no inicializados. Al igual que configurar un juguete nuevo, coloca todas las piezas en su lugar para que el programa se ejecute correctamente desde el principio. Por ejemplo: class MyClass { public: MyClass() { myVariable = 0; } int myVariable; }; asegura que myVariable siempre sea 0 cuando se crea un objeto MyClass.

5. ¿Cuál es la diferencia entre "pasar por valor" y "pasar por referencia"? Considera enviar una copia de tu dibujo versus dejar que tu amigo dibuje directamente sobre tu original.

"Pasar por valor" significa que se pasa una copia del valor de la variable a la función. Cualquier modificación realizada al parámetro dentro de la función no afecta a la variable original fuera de la función. Es como enviar a tu amigo una copia de tu dibujo. Pueden garabatear en la copia, pero tu original permanece intacto.

"Pasar por referencia", por otro lado, pasa la dirección de memoria (una referencia) de la variable a la función. Por lo tanto, cualquier cambio realizado al parámetro dentro de la función afecta directamente a la variable original fuera de la función. Esto es como dejar que tu amigo dibuje directamente sobre tu dibujo original; sus cambios son permanentes. En código (por ejemplo, C++), esto a menudo involucra punteros o variables de referencia. Por ejemplo, void modify(int &x) usa "pasar por referencia".

6. Explique qué se entiende por sobrecarga de funciones en C++. Es como tener tu nombre, pero la gente te llama por diferentes apodos.

La sobrecarga de funciones en C++ permite definir múltiples funciones con el mismo nombre pero diferentes listas de parámetros (diferente número, tipos u orden de argumentos) dentro del mismo alcance. El compilador luego elige la función apropiada para llamar en función de los argumentos pasados durante la llamada a la función. Es como tener un nombre de función que puede realizar acciones ligeramente diferentes dependiendo de la entrada.

Por ejemplo:

int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; } int add(int a, int b, int c) {return a + b + c;}

Aquí, la función add está sobrecargada. Una toma dos argumentos int, otra toma dos argumentos double y una tercera toma tres argumentos int. El compilador seleccionará la función add apropiada en función de los tipos y el número de argumentos que proporcione cuando la llame.

7. ¿Cuál es el propósito de la palabra clave const? Piense en ello como una regla que dice: '¡Puedes mirar, pero no tocar!' sobre algo.

La palabra clave const en programación se utiliza para declarar una variable u objeto como constante, lo que significa que su valor no se puede cambiar después de que se inicializa. Es como decir: "Esto está fijado; ¡no lo modifiques!". Esto ayuda a prevenir modificaciones accidentales que podrían conducir a errores.

Piensa en const como una promesa al compilador (y a otros desarrolladores) de que un valor particular no será alterado. Permite optimizaciones durante la compilación y mejora la legibilidad del código al indicar claramente qué valores están destinados a permanecer constantes. Por ejemplo:

const int max_size = 100;

Esto declara max_size como un entero constante con un valor de 100. Cualquier intento de modificar max_size más adelante en el código resultará en un error de compilación.

8. ¿Qué son los archivos de encabezado y los archivos fuente en C++? Imagina organizar juguetes donde la etiqueta de la caja es el encabezado y los juguetes reales son la fuente.

En C++, los archivos de encabezado (por ejemplo, .h o .hpp) son como las etiquetas en las cajas de juguetes. Declaran lo que hay dentro: funciones, clases, variables, etc. Contienen declaraciones pero no la implementación real. Por ejemplo, un encabezado podría declarar una función int add(int a, int b);. Piensa en ello como un contrato, que le dice a otras partes de tu código qué funcionalidad está disponible. Los encabezados se incluyen usando #include.

Los archivos fuente (por ejemplo, .cpp) son como la propia caja de juguetes, que contiene los juguetes reales o, en términos de programación, la implementación de las funciones y clases declaradas en el encabezado. La función int add(int a, int b) declarada en el encabezado se definiría en el archivo fuente de la siguiente manera: int add(int a, int b) { return a + b; }. Los archivos fuente se compilan para crear archivos objeto, que luego se enlazan para formar el ejecutable.

9. Describe en qué consiste la herencia en C++. Es como cuando un robot de juguete obtiene nuevas características de un coche de juguete. ¿Cuáles son los beneficios?

La herencia en C++ es un mecanismo mediante el cual una nueva clase (clase derivada) adquiere propiedades y comportamientos de una clase existente (clase base). El robot de juguete que hereda características de un coche de juguete ilustra esto: el robot, la clase derivada, gana funcionalidades (como moverse sobre ruedas) del coche, la clase base. Es una forma de crear una relación jerárquica entre clases.

Los beneficios incluyen la reutilización del código: no es necesario reescribir el código para funcionalidades similares. También permite el polimorfismo, donde los objetos de diferentes clases pueden ser tratados como objetos de un tipo común (la clase base). Esto conduce a un código más organizado y mantenible porque los cambios en la clase base se propagan automáticamente a las clases derivadas (a menos que se anulen). Además, la herencia ayuda a crear abstracciones al definir interfaces comunes en la clase base.

10. ¿Qué es el polimorfismo en C++? Considere una forma que puede dibujarse como un cuadrado o un círculo. ¿Cuáles son las diferentes formas en que se puede lograr el polimorfismo en C++?

Polimorfismo en C++ significa "muchas formas". Permite que objetos de diferentes clases se traten como objetos de un tipo común. Específicamente, le permite llamar a métodos en un objeto sin saber su tipo exacto en tiempo de compilación. Esto permite escribir código genérico que puede funcionar con objetos de diferentes tipos.

Hay varias formas de lograr el polimorfismo en C++:

  • Funciones virtuales (Polimorfismo en tiempo de ejecución): El uso de funciones virtuales en una clase base permite que las clases derivadas anulen el comportamiento de la función. Al llamar a la función a través de un puntero o referencia de clase base, la función real llamada depende del tipo de tiempo de ejecución del objeto. El ejemplo de la forma usaría esto:

class Shape { public: virtual void draw() { // Implementación predeterminada o no hacer nada } }; class Circle : public Shape { public: void draw() override { // Dibujar un círculo } }; class Square : public Shape { public: void draw() override { // Dibujar un cuadrado } };

  • Sobrecarga de funciones (polimorfismo en tiempo de compilación): Definir múltiples funciones con el mismo nombre pero diferentes tipos de parámetros dentro del mismo ámbito.

  • Sobrecarga de operadores (polimorfismo en tiempo de compilación): Redefinir el comportamiento de los operadores (por ejemplo, +, -, *) para tipos definidos por el usuario.

  • Plantillas (polimorfismo en tiempo de compilación): Usar plantillas para escribir código genérico que puede funcionar con diferentes tipos de datos. El ejemplo de la forma es menos relevante aquí, pero las plantillas pueden ser útiles si tiene una función de dibujo genérica que necesita manejar diferentes tipos de datos de formas.

11. ¿Qué son los espacios de nombres en C++? ¿Cómo ayudan? Piense en ellos como diferentes habitaciones en su casa para evitar colisiones de juguetes.

Los espacios de nombres en C++ son una forma de agrupar entidades relacionadas como clases, funciones, variables, etc., bajo un solo nombre. Ayudan a organizar el código y a prevenir colisiones de nombres, especialmente cuando se usan múltiples bibliotecas o proyectos grandes. Piense en ellos como la creación de diferentes habitaciones (espacios de nombres) en su casa (código) para almacenar cosas similares.

Por ejemplo, podría tener dos funciones llamadas print, pero una pertenece al espacio de nombres Math y la otra a Graphics. Sin espacios de nombres, esto causaría un conflicto de nombres. Con los espacios de nombres, puede diferenciarlos usando Math::print() y Graphics::print(). Esto mejora la legibilidad y el mantenimiento del código.

namespace Math { int add(int a, int b) { return a + b; } } namespace Graphics { void drawCircle() { /* ... */ } }

12. Explique la diferencia entre `==` y `=` en C++. Uno comprueba si las cosas son iguales, y el otro asigna un valor. ¿Puede dar un ejemplo de cuándo importa?

En C++, = es el operador de asignación, y == es el operador de igualdad. El operador de asignación (=) asigna el valor del lado derecho a la variable del lado izquierdo. El operador de igualdad (==) compara los valores de ambos lados y devuelve un valor booleano (verdadero o falso) que indica si son iguales.

Importa significativamente en las sentencias condicionales. Por ejemplo:

int x = 5; if (x = 10) { // Asignación: a x se le asigna 10, y la condición se evalúa como verdadera (10 es distinto de cero) // Este bloque siempre se ejecutará, y x será 10. } if (x == 10) { // Igualdad: Comprueba si x es igual a 10 // Este bloque se ejecutará solo si x es igual a 10. }

En la primera sentencia if, a x se le asigna el valor 10, y el resultado de la asignación (10) se trata como un booleano verdadero porque es distinto de cero. El código dentro del bloque if se ejecutará. En la segunda sentencia if, se evaluará como verdadero y ejecutará las sentencias dentro.

13. ¿Qué es la Biblioteca de Plantillas Estándar (STL) en C++? Considérela como una caja de piezas de juguete prefabricadas que se pueden usar de muchas maneras.

La Biblioteca de Plantillas Estándar (STL) en C++ es una colección de clases y funciones genéricas preconstruidas que proporcionan estructuras de datos y algoritmos de programación comunes. Piense en ella como una caja de herramientas llena de componentes listos para usar, como contenedores (por ejemplo, vector, list, map), algoritmos (por ejemplo, sort, find) e iteradores (para recorrer contenedores). Estos componentes están diseñados para funcionar con varios tipos de datos, promoviendo la reutilización y eficiencia del código.

Esencialmente, en lugar de escribir sus propias implementaciones para tareas comunes como ordenar o gestionar listas, puede utilizar directamente los componentes de la STL. Esto ahorra tiempo de desarrollo y garantiza que se está utilizando código bien probado y optimizado. Por ejemplo, std::vector proporciona funcionalidad de matriz dinámica, std::sort ordena eficientemente los elementos y std::map implementa una estructura similar a un diccionario.

14. ¿Cómo funciona el manejo de excepciones en C++? Imagine que se rompe un juguete, ¿cómo se maneja el problema con elegancia sin romper todos los demás juguetes?

El manejo de excepciones en C++ permite tratar errores o circunstancias excepcionales que surgen durante la ejecución del programa. Emplea bloques try, catch y throw. Un bloque try encierra el código que podría lanzar una excepción. Si se produce una excepción dentro del bloque try, el programa busca un bloque catch adecuado para manejarla.

Analogía: Imagina que un juguete se rompe. Intentamos jugar con él. Si se rompe (lanza una excepción), en lugar de que toda la sala de juegos se derrumbe, atrapamos el juguete roto, quizás lo apartamos para repararlo (manejamos la excepción), y continuamos jugando con los otros juguetes sin interrupción. Los otros juguetes representan el resto del programa, que continúa ejecutándose normalmente porque el error se manejó correctamente. Se muestra una estructura de código básica a continuación:

try { // Código que podría lanzar una excepción if (toy_breaks) { throw ToyBrokenException("¡Oh no, el juguete está roto!"); } } catch (const ToyBrokenException& e) { // Manejar la excepción std::cerr << "Excepción capturada: " << e.what() << std::endl; // Realizar acciones de limpieza o recuperación }

15. ¿Qué son las funciones virtuales en C++? ¿Puedes dar un ejemplo de cómo las funciones virtuales son útiles con la herencia?

Las funciones virtuales en C++ son funciones miembro declaradas con la palabra clave virtual. Su propósito principal es lograr el polimorfismo en tiempo de ejecución, también conocido como despacho dinámico. Cuando se llama a una función virtual a través de un puntero o referencia del tipo de clase base, la función real que se ejecuta está determinada por el tipo del objeto al que se apunta o al que se hace referencia, no por el tipo del puntero o la referencia en sí.

Aquí hay un ejemplo que ilustra su utilidad:

class Animal { public: virtual void makeSound() { std::cout << "Sonido genérico de animal" << std::endl; } }; class Dog : public Animal { public: void makeSound() override { std::cout << "¡Guau!" << std::endl; } }; class Cat : public Animal { public: void makeSound() override { std::cout << "¡Miau!" << std::endl; } }; int main() { Animal* animal1 = new Dog(); Animal* animal2 = new Cat(); animal1->makeSound(); // Output: Woof! animal2->makeSound(); // Output: Meow! delete animal1; delete animal2; return 0; }

En este ejemplo, aunque animal1 y animal2 son punteros de tipo Animal*, la función makeSound() que se llama en realidad depende de si el puntero apunta a un objeto Dog o a un objeto Cat. Esto demuestra cómo las funciones virtuales te permiten tratar objetos de diferentes clases de manera uniforme, al tiempo que les permite exhibir sus comportamientos específicos.

16. Explica el concepto de fugas de memoria en C++. ¿Qué podría causarlas y cómo puedes prevenirlas?

Las fugas de memoria en C++ ocurren cuando la memoria asignada dinámicamente (usando new) ya no es accesible para el programa, pero no ha sido desasignada (usando delete o delete[]). Esto conduce a recursos de memoria desperdiciados, lo que podría causar una degradación del rendimiento o incluso fallos del programa. Las causas incluyen: olvidar delete la memoria asignada con new, excepciones lanzadas antes de que se desasigne la memoria, y perder el rastro de los punteros a la memoria asignada.

Las estrategias de prevención implican: emplear punteros inteligentes (por ejemplo, unique_ptr, shared_ptr) para gestionar automáticamente la desasignación de memoria, usar RAII (Adquisición de Recursos es Inicialización) para vincular la gestión de recursos a la vida útil del objeto, y ser meticuloso al emparejar cada new con un delete correspondiente. También el uso de herramientas de detección de fugas de memoria puede identificar las fugas durante el desarrollo.

17. ¿Qué es una función amiga en C++? Imagina una función a la que se le permite jugar con las partes privadas de un juguete, con el permiso del juguete.

En C++, una función amiga es una función a la que se le concede acceso especial a los miembros privados y protegidos de una clase. Normalmente, solo las funciones miembro de una clase pueden acceder a estos miembros. Una función amiga, sin embargo, no es una función miembro de la clase, pero se declara como amiga dentro de la definición de la clase. Esta declaración le da a la función amiga el derecho de acceder a las partes privadas y protegidas de la clase, eludiendo efectivamente las restricciones de acceso habituales.

Piensa en esto: la clase es como un jardín amurallado, y normalmente, solo los jardineros (funciones miembro) pueden cuidar las plantas dentro (miembros privados). Una función amiga es como un visitante al que se le ha dado una llave especial para entrar en el jardín y ayudar, aunque no sea jardinero. La clase (el jardín amurallado) concede explícitamente este acceso a través de la palabra clave friend. Ejemplo: friend void someFunction(MyClass obj);

18. Describe el propósito de la palabra clave static en C++. ¿Cómo cambia el significado de static cuando se aplica a una variable versus una función?

En C++, la palabra clave static tiene diferentes significados según el contexto. Cuando se aplica a una variable dentro de una función, significa que la variable se inicializa solo una vez, y su valor persiste en múltiples llamadas a la función. Tiene ámbito local pero tiempo de vida global. Cuando static se aplica a una variable fuera de una función (ámbito global), significa que la variable tiene enlace interno; solo es visible dentro del mismo archivo fuente (unidad de traducción) donde está definida.

Cuando se aplica a una función, static también significa que la función tiene enlace interno, y solo es visible dentro del mismo archivo fuente donde está definida. Esto previene conflictos de nombres y te permite crear funciones con el mismo nombre en diferentes archivos fuente. Por ejemplo:

static int my_static_function() { return 0; }

19. ¿Qué son los punteros inteligentes en C++? ¿Por qué y cuándo deberías usarlos? Considéralos como manejadores de juguetes que limpian automáticamente los juguetes cuando terminas.

Los punteros inteligentes en C++ son clases que se comportan como punteros normales pero proporcionan gestión automática de memoria. Actúan como manejadores de juguetes, asegurando que los juguetes (memoria asignada dinámicamente) se limpien cuando ya no se necesitan, previniendo fugas de memoria. Logran esto a través de la Adquisición de Recursos es Inicialización (RAII), donde el puntero inteligente adquiere el recurso (memoria) y lo libera automáticamente en su destructor.

Debes usar punteros inteligentes para gestionar la memoria asignada dinámicamente en lugar de punteros sin procesar para evitar fugas de memoria y punteros colgantes. Específicamente, usa std::unique_ptr para la propiedad exclusiva, std::shared_ptr para la propiedad compartida y std::weak_ptr para observar std::shared_ptr sin tomar la propiedad. Por ejemplo:

#include <memory> void foo() { std::unique_ptr<int> ptr(new int(10)); // Eliminación automática cuando ptr sale del ámbito // ... usa ptr }

20. Explica qué significa RAII (Adquisición de recursos es inicialización). ¿Por qué es RAII una buena práctica a seguir en C++?

RAII (Adquisición de recursos es inicialización) es una técnica de programación donde la adquisición de un recurso (por ejemplo, memoria, manejadores de archivos, sockets de red) está ligada a la vida útil de un objeto. Cuando se crea el objeto, el recurso se adquiere en el constructor. Cuando el objeto sale del ámbito y se destruye, el recurso se libera automáticamente en el destructor. Esto asegura que los recursos siempre se liberan, independientemente de cómo el código salga del ámbito (por ejemplo, salida normal, excepciones).

RAII es una buena práctica en C++ porque ayuda a prevenir fugas de recursos y simplifica la gestión de recursos. Al automatizar la liberación de recursos, reduce las posibilidades de olvidar liberar recursos manualmente, lo que puede llevar a fugas de memoria u otros problemas relacionados con los recursos. RAII también se integra bien con el manejo de excepciones. Cuando se lanza una excepción, la pila se desenrolla y se llaman a los destructores de los objetos que han salido del ámbito, asegurando que los recursos se liberen correctamente incluso en circunstancias excepcionales. Por ejemplo:

class FileHandler { FILE* file; public: FileHandler(const char* filename, const char* mode) { file = fopen(filename, mode); if (!file) { throw std::runtime_error("Could not open file"); } } ~FileHandler() { if (file) { fclose(file); } } // otros métodos para interactuar con el archivo };

Preguntas de entrevista de C++ para principiantes

1. ¿Cuál es la diferencia entre `class` y `struct` en C++?

La principal diferencia entre class y struct en C++ radica en su especificador de acceso predeterminado. Para class, los miembros son privados por defecto, lo que significa que solo son accesibles desde dentro de la propia clase o por amigos. Por el contrario, para struct, los miembros son públicos por defecto, lo que significa que son accesibles desde cualquier lugar.

Esencialmente, puedes usar class y struct indistintamente, pero el control de acceso predeterminado cambia el comportamiento. Si no especificas un especificador de acceso, class por defecto es private y struct por defecto es public.

2. Explica qué es un puntero y por qué es útil?

Un puntero es una variable que almacena la dirección de memoria de otra variable. En lugar de contener directamente un valor, "apunta a" la ubicación en la memoria donde se almacena ese valor. Piénsalo como una dirección de una casa, en lugar de la casa en sí. Por ejemplo, en C++, int *ptr; declara un puntero ptr que puede almacenar la dirección de una variable entera. Puedes asignarlo usando el operador de dirección: ptr = &my_int;.

Los punteros son útiles por varias razones. Permiten:

  • Manipular directamente la memoria, lo cual es esencial para tareas como la asignación dinámica de memoria (malloc en C, new en C++).
  • Pasar estructuras de datos grandes eficientemente a funciones pasando sus direcciones en lugar de copiar toda la estructura. Esto ahorra memoria y mejora el rendimiento.
  • Implementar estructuras de datos como listas enlazadas y árboles, donde los elementos están conectados a través de punteros. Si está trabajando con aplicaciones o bibliotecas que requieren mucha memoria o son críticas para el rendimiento, los punteros serán esenciales.

3. ¿Qué significa la palabra clave const en C++?

La palabra clave const en C++ es un calificador de tipo que especifica que el valor de una variable no se puede cambiar después de la inicialización. Esencialmente, convierte una variable en de solo lectura. Esto ayuda a garantizar la integridad de los datos y a evitar la modificación accidental de valores importantes.

Usar const ofrece varios beneficios: Mejora la legibilidad del código al indicar claramente qué variables están destinadas a permanecer constantes. Permite al compilador realizar optimizaciones, ya que sabe que el valor no cambiará. También ayuda a detectar errores en tiempo de compilación si se intenta modificar una variable const. const se puede aplicar a variables, punteros, parámetros de función y funciones miembro de clases.

4. ¿Cuáles son las diferencias entre paso por valor, paso por referencia y paso por puntero?

Pasar por valor crea una copia de la variable y la pasa a la función. Las modificaciones a la variable dentro de la función no afectan a la variable original fuera de la función. Pasar por referencia pasa la dirección de memoria de la variable original a la función. Por lo tanto, cualquier cambio realizado en la variable dentro de la función afecta directamente a la variable original. Finalmente, pasar por puntero implica pasar un puntero (que es una variable que almacena la dirección de memoria) a la función. Al igual que pasar por referencia, los cambios realizados a través del puntero dentro de la función afectarán a la variable original. Sin embargo, los punteros ofrecen más flexibilidad, permitiendo operaciones como la aritmética de punteros y el manejo de punteros nulos.

5. ¿Cuál es la diferencia entre `malloc` y `new`?

malloc y new se utilizan para la asignación dinámica de memoria, pero difieren significativamente.

malloc es una función de C que asigna un bloque de memoria bruta de un tamaño especificado en bytes. Devuelve un void*, que debe convertirse explícitamente al tipo deseado. malloc no realiza ninguna inicialización ni llama a constructores. free se utiliza para liberar la memoria asignada por malloc.

new es un operador de C++ que asigna memoria y construye un objeto de un tipo especificado. Calcula automáticamente el tamaño de memoria requerido basándose en el tipo del objeto. new también llama al constructor del objeto para inicializarlo. El operador delete se utiliza para liberar la memoria asignada por new y llama al destructor del objeto. new es seguro para el tipo, lo que significa que no necesita convertir el puntero devuelto.

6. ¿Qué significa el término 'fuga de memoria' en C++?

En C++, una fuga de memoria ocurre cuando la memoria asignada dinámicamente ya no es accesible para el programa pero no ha sido liberada usando delete o delete[]. Esto significa que la memoria está reservada pero no es utilizable, desperdiciando efectivamente recursos. Con el tiempo, las fugas de memoria repetidas pueden agotar la memoria disponible, lo que lleva a la ralentización del programa o incluso a bloqueos.

Las causas comunes incluyen olvidar delete la memoria asignada con new, perder el rastro de los punteros a la memoria asignada o excepciones que impiden la ejecución de la declaración delete. Se utilizan herramientas como los analizadores de memoria y los depuradores para detectar y resolver fugas de memoria identificando asignaciones sin las correspondientes desasignaciones.

7. Explique el concepto de herencia.

La herencia es un concepto fundamental en la programación orientada a objetos donde una nueva clase (subclase o clase derivada) se crea a partir de una clase existente (clase base o clase padre). La subclase hereda las propiedades y el comportamiento (atributos y métodos) de la clase base. Esto promueve la reutilización del código, reduce la redundancia y establece una relación "es-un" entre las clases.

Por ejemplo, considere una clase Vehicle con atributos como velocidad y color. Una clase Car puede heredar de Vehicle, ganando automáticamente esos atributos. Car puede entonces agregar sus propios atributos específicos, como number_of_doors (número_de_puertas). La herencia te permite modelar relaciones del mundo real y crear una jerarquía de clases.

8. ¿Qué es la sobrecarga de funciones?

La sobrecarga de funciones es una característica de la programación orientada a objetos donde múltiples funciones pueden tener el mismo nombre pero diferentes parámetros. El compilador las diferencia en función del número, tipo u orden de los argumentos pasados durante la llamada a la función. Esto te permite crear funciones que realizan operaciones similares pero aceptan diferentes entradas.

Por ejemplo, podrías tener una función add() que puede sumar dos enteros, dos números de punto flotante o un entero y un número de punto flotante. La función add() específica que se llama está determinada por los argumentos que proporcionas. En lenguajes como C++ y Java, la sobrecarga de funciones es una forma común de proporcionar flexibilidad y expresividad en tu código. Lenguajes como Python no admiten la sobrecarga de funciones de la misma manera, típicamente confiando en valores de argumentos predeterminados y argumentos variables (*args, **kwargs) para lograr resultados similares.

9. ¿Cuál es el propósito de un constructor?

El propósito principal de un constructor es inicializar el estado de un objeto cuando se crea. Es un método especial dentro de una clase que se llama automáticamente cuando se crea una nueva instancia (objeto) de esa clase.

Los constructores aseguran que el objeto comience con datos válidos y significativos. Pueden establecer valores predeterminados para los atributos, realizar cualquier configuración necesaria o hacer cumplir restricciones en el estado inicial del objeto. Si una clase no tiene un constructor definido explícitamente, el compilador proporciona automáticamente un constructor predeterminado (generalmente vacío).

10. ¿Qué es un destructor y cuándo se llama?

Un destructor es una función miembro especial de una clase que se llama automáticamente cuando un objeto de esa clase sale del ámbito o se elimina explícitamente. Su propósito principal es liberar cualquier recurso (memoria, identificadores de archivos, conexiones de red, etc.) que el objeto haya mantenido durante su vida útil. Se nombra con una tilde (~) seguida del nombre de la clase (por ejemplo, `MyClass()`).

Los destructores se llaman en las siguientes situaciones:

  • Cuando una variable local sale de su ámbito.
  • Cuando un objeto se elimina explícitamente utilizando delete.
  • Durante el desenrollado de la pila en el manejo de excepciones (si se lanza una excepción mientras el objeto está en el ámbito).
  • Cuando la vida útil de un objeto temporal finaliza.
  • Cuando un programa sale y los objetos globales/estáticos se están destruyendo. Los destructores se llaman en el orden inverso de los constructores.

11. ¿Qué son las funciones virtuales?

Las funciones virtuales son una característica fundamental del polimorfismo en la programación orientada a objetos, principalmente en lenguajes como C++. Permiten que una clase derivada anule el comportamiento de una función de la clase base. Cuando se llama a una función virtual a través de un puntero o referencia de la clase base, la función real que se ejecuta se determina en tiempo de ejecución en función del tipo real del objeto, no del tipo de puntero/referencia.

Aquí hay un breve resumen:

  • Propósito: Habilitar el polimorfismo en tiempo de ejecución.
  • Declaración: Se declara usando la palabra clave virtual en la clase base.
  • Anulación: Las clases derivadas pueden proporcionar su propia implementación de la función virtual.
  • Despacho dinámico: La versión correcta de la función a llamar se resuelve en tiempo de ejecución (despacho dinámico).

12. ¿Qué es el polimorfismo y cómo se logra en C++?

Polimorfismo significa "muchas formas". En C++, es la capacidad de una sola función u operador para comportarse de manera diferente en diferentes contextos. Permite que objetos de diferentes clases sean tratados como objetos de un tipo común.

C++ logra el polimorfismo a través de dos mecanismos principales:

  • Polimorfismo en tiempo de compilación (estático): Se logra mediante la sobrecarga de funciones y la sobrecarga de operadores. La función correcta a llamar se determina en tiempo de compilación basándose en los argumentos proporcionados. También usa plantillas, lo que permite que funciones o clases operen con tipos genéricos.

  • Polimorfismo en tiempo de ejecución (dinámico): Se logra mediante funciones virtuales y herencia. Esto permite que una clase derivada anule la función de una clase base, y la función correcta a llamar se determina en tiempo de ejecución basándose en el tipo real del objeto que utiliza punteros o referencias.

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

13. ¿Cuáles son los modificadores de acceso en C++ (público, privado, protegido) y cómo funcionan?

Los modificadores de acceso de C++ controlan la visibilidad y accesibilidad de los miembros de una clase (variables y métodos). Hay tres modificadores de acceso principales:

  • public: Los miembros declarados como public son accesibles desde cualquier lugar, tanto dentro como fuera de la clase. No hay restricciones para acceder a los miembros públicos.
  • private: Los miembros declarados como private solo son accesibles desde dentro de la misma clase. No se puede acceder a ellos directamente desde fuera de la clase, ni siquiera desde clases derivadas. Implementan la ocultación de datos.
  • protected: Los miembros declarados como protected son accesibles desde dentro de la misma clase y desde clases derivadas (subclases). No son accesibles desde fuera de la jerarquía de clases. El acceso protected proporciona un nivel de acceso entre public y private, lo que permite a las clases derivadas heredar y usar ciertos miembros, al tiempo que impide el acceso externo. Por ejemplo:

class Base { public: int publicVar; // Accesible desde cualquier lugar private: int privateVar; // Solo accesible desde dentro de Base protected: int protectedVar; // Accesible desde Base y clases derivadas };

14. Explica la Biblioteca de Plantillas Estándar (STL). ¿Cuáles son algunos contenedores comunes?

La Biblioteca de Plantillas Estándar (STL) es un conjunto de clases de plantillas de C++ que proporciona estructuras de datos y funciones de programación comunes, como listas, pilas, arrays, etc. Es una biblioteca de clases de contenedores, algoritmos e iteradores; es una biblioteca genérica, lo que significa que sus componentes están parametrizados. Esto les permite funcionar con una variedad de tipos de datos.

Algunos contenedores STL comunes incluyen:

  • vector: Un array dinámico.
  • list: Una lista doblemente enlazada.
  • deque: Una cola de doble terminación.
  • set: Una colección de elementos únicos.
  • map: Una colección de pares clave-valor donde cada clave es única. A menudo se implementa como un árbol rojo-negro para búsquedas eficientes.
  • unordered_map: Similar a map, pero implementado como una tabla hash para búsquedas potencialmente más rápidas (pero no garantizadas).

15. ¿Qué es un iterador en C++?

En C++, un iterador es un objeto que actúa como un puntero generalizado, que te permite recorrer elementos en un contenedor (como arrays, vectores, listas) sin necesidad de conocer la estructura subyacente de ese contenedor. Piénsalo como un puntero que sabe cómo moverse al siguiente elemento de una colección.

Los iteradores proporcionan una forma consistente de acceder a elementos secuencialmente. Se caracterizan por operaciones como el incremento (++ para avanzar al siguiente elemento), la desreferencia (* para acceder al elemento actual) y la comparación (==, != para comprobar si dos iteradores apuntan al mismo elemento o si un iterador ha llegado al final del contenedor). Los algoritmos de la Biblioteca de Plantillas Estándar (STL) utilizan iteradores extensivamente para operar sobre diferentes tipos de contenedores.

16. ¿Qué son las plantillas en C++?

Las plantillas en C++ son una característica poderosa que permite la programación genérica. Permiten escribir código que puede funcionar con diferentes tipos de datos sin tener que escribir versiones separadas para cada tipo.

Esencialmente, las plantillas son planos para crear funciones o clases. El tipo real utilizado se determina en tiempo de compilación. Esto evita la duplicación de código y aumenta la reutilización del código. Puedes tener plantillas de funciones y plantillas de clases. Por ejemplo:

template <typename T> T max(T a, T b) { return (a > b) ? a : b; }

17. ¿Qué es el manejo de excepciones y por qué es importante?

El manejo de excepciones es un mecanismo para lidiar con errores en tiempo de ejecución (excepciones) en un programa de manera elegante, evitando que el programa se bloquee. Implica identificar secciones de código propensas a errores, encerrarlas en bloques try y proporcionar bloques catch para manejar tipos de excepciones específicos. Opcionalmente, un bloque finally asegura la ejecución del código independientemente de si ocurrió una excepción.

Es importante porque mejora la robustez y la fiabilidad del programa. Sin él, una excepción no manejada puede llevar a una terminación abrupta. El manejo de excepciones permite que un programa se recupere de errores, los registre para depuración o realice acciones de limpieza antes de apagarse o continuar la operación sin problemas. Ayuda a crear aplicaciones más fáciles de usar al prevenir bloqueos inesperados y proporcionar mensajes de error informativos.

18. ¿Cuál es la diferencia entre los operadores de pre-incremento y post-incremento?

La diferencia clave radica en cuándo ocurre la operación de incremento en relación con la evaluación de la expresión. El pre-incremento (++x) incrementa la variable antes de que su valor se use en la expresión. El post-incremento (x++) incrementa la variable después de que su valor original se haya usado en la expresión.

Por ejemplo:

int x = 5; int y = ++x; // x ahora es 6, y también es 6 (pre-incremento) int a = 5; int b = a++; // a ahora es 6, b es 5 (post-incremento)

19. Describe el concepto de espacios de nombres en C++.

Los espacios de nombres en C++ se utilizan para organizar el código en grupos lógicos y evitar colisiones de nombres, especialmente en proyectos grandes o al usar bibliotecas externas. Esencialmente, crean un alcance para los identificadores (variables, funciones, clases, etc.), lo que le permite usar el mismo nombre en diferentes espacios de nombres sin conflicto.

Por ejemplo, puedes definir una función print() dentro del espacio de nombres A y otra print() dentro del espacio de nombres B. Para acceder a ellas, usarías el operador de resolución de ámbito (::) como A::print() y B::print(). También puedes usar la palabra clave using para traer nombres o espacios de nombres completos al ámbito actual, simplificando el acceso (por ejemplo, using namespace A;). Ejemplo de código:

namespace A { void print() { /* ... / } } namespace B { void print() { / ... */ } }

20. Explica qué es RAII (Adquisición de Recursos es Inicialización).

RAII (Adquisición de Recursos es Inicialización) es una técnica de programación C++ donde la gestión de recursos (como la asignación de memoria, los manejadores de archivos, los sockets de red) está ligada al ciclo de vida de un objeto. La idea clave es que un recurso se adquiere durante la construcción del objeto (inicialización) y se libera automáticamente durante la destrucción del objeto (destrucción).

Esto garantiza la liberación de recursos, incluso si se lanzan excepciones, porque los destructores siempre se llaman cuando un objeto sale del ámbito. Esto evita fugas de recursos. Por ejemplo:

class FileHandler { std::ofstream file; public: FileHandler(const std::string& filename) : file(filename) { if (!file.is_open()) { throw std::runtime_error("Could not open file"); } } ~FileHandler() { file.close(); } void writeData(const std::string& data) { file << data << std::endl; } };

21. ¿Qué son los punteros inteligentes y por qué se usan?

Los punteros inteligentes son clases que actúan como punteros, pero también gestionan la memoria a la que apuntan. Desasignan automáticamente la memoria cuando el puntero inteligente ya no es necesario, lo que evita fugas de memoria.

Se utilizan porque la gestión manual de memoria con punteros sin formato en lenguajes como C++ es propensa a errores. Los punteros inteligentes aseguran una gestión adecuada de los recursos a través de RAII (Adquisición de Recursos es Inicialización). Los tipos comunes incluyen:

  • unique_ptr: Propiedad exclusiva.
  • shared_ptr: Propiedad compartida (conteo de referencias).
  • weak_ptr: Observador sin propiedad de un shared_ptr.

22. ¿Qué son las expresiones lambda?

Las expresiones lambda son funciones anónimas, lo que significa que son funciones sin nombre. Proporcionan una forma concisa de crear funciones pequeñas de una sola expresión, a menudo utilizadas para operaciones de corta duración. Las expresiones lambda son particularmente útiles al pasar funciones como argumentos a funciones de orden superior (funciones que toman otras funciones como argumentos).

En muchos lenguajes, se definen utilizando una sintaxis específica (por ejemplo, lambda argumentos: expresión en Python o (argumentos) -> expresión en Java). Pueden tomar múltiples argumentos pero usualmente consisten en una sola expresión. Ejemplo: x -> x * 2 (Java), que representa una función que toma un argumento x y devuelve x multiplicado por 2.

23. ¿Cuál es el propósito de la palabra clave `static` en C++?

La palabra clave static en C++ tiene diferentes significados dependiendo del contexto en el que se utiliza.

  • Dentro de una función: Crea una variable local estática. Esta variable se inicializa solo una vez y retiene su valor entre las llamadas a la función. Esencialmente, tiene una vida útil que se extiende más allá de la ejecución de la función, a diferencia de las variables locales regulares.
  • Dentro de una clase: Crea variables miembro estáticas y funciones miembro estáticas. Las variables miembro estáticas son compartidas por todas las instancias de la clase (solo existe una copia), y no están asociadas a ningún objeto específico. Las funciones miembro estáticas solo pueden acceder a las variables miembro estáticas y se pueden llamar usando el nombre de la clase en lugar de un objeto (por ejemplo, NombreClase::funcionEstatica();). No tienen un puntero this.
  • Fuera de una clase (ámbito global): Le da a la variable o función enlace interno. Esto significa que la variable o función solo es visible dentro de la misma unidad de traducción (archivo fuente) en la que se declara. Evita colisiones de nombres entre diferentes archivos en un proyecto.

En esencia, static controla el alcance y la duración de las variables y funciones.

24. ¿Qué es la sobrecarga de operadores?

La sobrecarga de operadores permite que operadores como +, -, *, /, ==, etc. tengan diferentes significados dependiendo de los tipos de datos de sus operandos. Esencialmente, proporciona una forma de redefinir el comportamiento de los operadores para tipos definidos por el usuario (clases o structs). Esto puede hacer que el código sea más legible e intuitivo, ya que se pueden usar operadores familiares para realizar operaciones específicas de sus estructuras de datos personalizadas. Por ejemplo, podría sobrecargar el operador + para sumar dos objetos Vector.

Lenguajes como C++, Python y C# admiten la sobrecarga de operadores, mientras que lenguajes como Java no. Cuando se implementa cuidadosamente, mejora la claridad del código. Sin embargo, el uso excesivo puede generar confusión si el comportamiento sobrecargado no es intuitivo o consistente con el significado convencional del operador.

25. ¿Qué son las funciones amigas y las clases amigas?

Las funciones amigas y las clases amigas son mecanismos en C++ que permiten que una función o clase acceda a los miembros privados y protegidos de otra clase. Esta es una excepción a las reglas de acceso típicas, que otorga privilegios especiales a entidades externas específicas.

Específicamente:

  • Una función amiga es una función que no es miembro de una clase, pero que se declara como amiga dentro de la clase. Luego puede acceder a los miembros privados y protegidos de esa clase.
  • Una clase amiga es una clase que se declara como amiga de otra clase. Todas las funciones miembro de la clase amiga pueden acceder a los miembros privados y protegidos de la clase que la declaró como amiga.

26. Explique el concepto de asignación dinámica de memoria.

La asignación dinámica de memoria es un proceso donde la memoria se asigna durante el tiempo de ejecución de un programa, a diferencia de durante la compilación (asignación estática). Esto permite que los programas soliciten memoria según sea necesario, haciendo un uso eficiente de los recursos del sistema. Las funciones comunes incluyen malloc (C), new (C++) y sus contrapartes de desasignación correspondientes como free y delete.

Los beneficios clave incluyen la flexibilidad en el manejo de tamaños de datos variables y la capacidad de crear estructuras de datos que crecen o se reducen durante la ejecución del programa. Sin la asignación dinámica, los programas necesitarían preasignar una cantidad fija de memoria, lo que podría desperdiciar espacio o enfrentar limitaciones si la memoria requerida excede la cantidad preasignada. La falta de desasignación adecuada de memoria puede conducir a fugas de memoria.

27. ¿Cuáles son algunas técnicas comunes de depuración en C++?

Las técnicas comunes de depuración en C++ incluyen el uso de un depurador (como GDB o el Depurador de Visual Studio) para recorrer el código paso a paso, inspeccionar variables y establecer puntos de interrupción. Las sentencias de impresión (usando std::cout) pueden ayudar a rastrear la ejecución del programa y los valores de las variables. Las revisiones de código y las herramientas de análisis estático también pueden identificar posibles errores desde el principio. Entender y manejar las excepciones correctamente es crucial.

Otras técnicas útiles son el uso de aserciones para verificar condiciones inesperadas, herramientas de depuración de memoria (como Valgrind) para detectar fugas y errores de memoria, y el registro para registrar eventos y errores del programa. La simplificación y la división y conquista aislando fragmentos de código para reproducir errores es útil. Por ejemplo, el uso de técnicas como la búsqueda binaria para acotar la sección de código problemática. Reproducir el error con la mínima cantidad de código también es útil.

28. ¿Cuál es la diferencia entre copia superficial y copia profunda?

La copia superficial crea un nuevo objeto, pero el nuevo objeto contiene referencias a los elementos originales. Modificar un elemento mutable dentro del objeto copiado afectará también al objeto original.

La copia profunda crea un nuevo objeto y copia recursivamente todos los objetos encontrados en el objeto original. Los cambios realizados en una copia no se reflejarán en el original.

29. ¿Qué es un archivo de encabezado y por qué los usamos?

Un archivo de encabezado es un archivo que contiene declaraciones de funciones, clases, variables y otros elementos del programa. En C y C++, normalmente tiene una extensión .h. Usamos archivos de encabezado para separar la interfaz (declaraciones) de la implementación (definiciones) de nuestro código, lo que promueve la modularidad y la reutilización.

Específicamente, los archivos de encabezado:

  • Nos permiten declarar funciones y variables en un archivo y definirlas en otro.
  • Permiten la reutilización del código incluyendo las mismas declaraciones en múltiples archivos fuente.
  • Mejoran el tiempo de compilación permitiendo al compilador analizar solo las declaraciones cuando sea necesario, en lugar de toda la implementación. Esto se logra a través de la directiva del preprocesador #include. Por ejemplo:

#include "myheader.h"

30. ¿Cuáles son algunas de las nuevas características introducidas en los estándares C++11 o posteriores que encuentras más útiles?

Varias características de C++11 y posteriores mejoran significativamente la calidad del código y la eficiencia del desarrollo. Una de las más útiles es la deducción de tipo auto, que simplifica las declaraciones de variables y evita especificaciones de tipo redundantes, especialmente con tipos de plantilla complejos. Por ejemplo:

auto x = compute_something(); // El tipo de x se deduce del tipo de retorno de compute_something()

Otra característica crucial son los bucles for basados en rango, que proporcionan una forma más concisa y legible de iterar a través de contenedores y arrays en comparación con los bucles tradicionales basados en iteradores. Además, los punteros inteligentes como std::unique_ptr y std::shared_ptr son invaluables para administrar la memoria y prevenir fugas de memoria. Automatizan la gestión de recursos y hacen que la seguridad de las excepciones sea significativamente más fácil de lograr.

Preguntas de entrevista intermedias de C++

1. ¿Qué son las funciones virtuales y por qué son importantes en C++?

Las funciones virtuales son funciones miembro declaradas con la palabra clave virtual en una clase base. Su importancia principal radica en permitir el polimorfismo en tiempo de ejecución, también conocido como despacho dinámico. Esto significa que cuando se accede a un objeto de clase derivada a través de un puntero o referencia de clase base, se llama a la versión correcta de la función virtual (la definida en la clase derivada), no a la versión de la clase base. Si la función no es virtual, siempre se llama a la versión de la clase base (despacho estático).

Las razones clave para usar funciones virtuales son:

  • Lograr polimorfismo: Permitir que objetos de diferentes clases sean tratados como objetos de un tipo común, mientras siguen ejecutando el comportamiento correcto para su tipo específico.
  • Extensibilidad: Hacer que el código sea más flexible y fácil de extender. Se pueden agregar nuevas clases derivadas sin modificar el código existente que utiliza punteros o referencias de clase base.
  • Clases abstractas: Las funciones virtuales se pueden declarar como funciones virtuales puras (virtual void foo() = 0;), haciendo que la clase base sea abstracta y obligando a las clases derivadas a implementar la función. Esto define una interfaz a la que todas las clases derivadas deben adherirse.

2. Explica la diferencia entre copia superficial y copia profunda.

Una copia superficial crea un nuevo objeto, pero copia referencias a los objetos contenidos dentro del objeto original. Esto significa que si modificas un objeto anidado en la copia, el objeto correspondiente en el original también cambia.

En contraste, una copia profunda crea un nuevo objeto y copia recursivamente todos los objetos encontrados dentro del original. Los cambios en los objetos anidados de la copia no afectarán al original. En Python, puedes crear una copia profunda usando copy.deepcopy(). Las copias superficiales son típicamente más rápidas, pero las copias profundas proporcionan más aislamiento.

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

La palabra clave friend en C++ otorga a una función o clase acceso a los miembros privados y protegidos de otra clase. Esto rompe la encapsulación hasta cierto punto, pero permite el acceso controlado en casos específicos.

Es útil cuando una función o clase necesita acceso privilegiado a los elementos internos de otra clase sin ser miembro de esa clase. Por ejemplo, una clase matriz podría declarar una clase vector como amiga para que la clase vector pudiera manipular directamente los datos internos de la matriz. El uso de friend debe hacerse con cautela, ya que el uso excesivo puede debilitar la encapsulación y dificultar el mantenimiento del código. Úsalo solo cuando haya una razón clara y justificable para omitir las restricciones de acceso normales.

4. ¿Cómo funciona el manejo de excepciones en C++? ¿Qué son try, catch y throw?

El manejo de excepciones en C++ te permite lidiar con errores en tiempo de ejecución de manera elegante. Utiliza las palabras clave try, catch y throw. Un bloque try encierra el código que podría lanzar una excepción. Si ocurre una excepción dentro del bloque try, el control se transfiere a un bloque catch. El bloque catch está diseñado para manejar un tipo específico de excepción.

La palabra clave throw se utiliza para señalar una excepción. Cuando se lanza una excepción, el entorno de ejecución de C++ busca un bloque catch coincidente para manejarla. Si no se encuentra ningún bloque catch coincidente, el programa termina. Aquí hay un ejemplo simple:

try { // Código que podría lanzar una excepción if (error_condition) { throw std::runtime_error("Ocurrió un error"); } } catch (const std::runtime_error& e) { // Manejar la excepción std::cerr << "Excepción capturada: " << e.what() << std::endl; }

5. Describe la diferencia entre new y malloc al asignar memoria en C++.

new y malloc se utilizan ambos para la asignación dinámica de memoria, pero difieren significativamente en C++. new es un operador de C++ que asigna memoria y también llama al constructor del objeto que se está creando, asegurando una inicialización adecuada. Devuelve un puntero del tipo correcto. Cuando el objeto ya no es necesario, se debe usar delete para liberar la memoria, lo que también llama al destructor del objeto.

malloc, por otro lado, es una función de C que simplemente asigna un bloque de memoria sin procesar y sin inicializar de un tamaño especificado en bytes. Devuelve un void*, que debe convertirse explícitamente al tipo deseado. malloc no llama a los constructores. Para liberar la memoria asignada con malloc, se debe usar free. No emparejar new con delete o malloc con free puede conducir a pérdidas de memoria u otro comportamiento indefinido.

6. ¿Qué son los espacios de nombres en C++, y cómo ayudan a prevenir colisiones de nombres?

Los espacios de nombres en C++ son regiones declarativas que proporcionan un ámbito a los nombres (identificadores) que se encuentran dentro de ellos. Ayudan a organizar el código y a evitar colisiones de nombres, especialmente en proyectos grandes o cuando se utilizan múltiples bibliotecas. Al encerrar el código dentro de un espacio de nombres, se crea esencialmente un área distinta donde se pueden definir nombres sin entrar en conflicto con los nombres de otros espacios de nombres o del ámbito global.

Por ejemplo, supongamos que dos bibliotecas definen una función llamada print. Sin espacios de nombres, esto conduciría a una colisión. Sin embargo, si una biblioteca define print dentro del espacio de nombres LibraryA y la otra dentro de LibraryB, se puede usar LibraryA::print() y LibraryB::print() para llamar específicamente a la función deseada. Esto evita la ambigüedad y permite el uso de múltiples bibliotecas con nombres potencialmente conflictivos. Las declaraciones using namespace pueden simplificar el acceso a los nombres dentro de un espacio de nombres.

7. Explica el concepto de sobrecarga de operadores en C++ con un ejemplo.

La sobrecarga de operadores en C++ te permite redefinir el significado de los operadores (como +, -, *, /, ==, etc.) para tipos de datos definidos por el usuario (clases). Esto te permite usar operadores con objetos de tus clases de una manera natural e intuitiva.

Por ejemplo, considera una clase Point que representa un punto en un espacio 2D. Puedes sobrecargar el operador + para sumar dos objetos Point, resultando en un nuevo objeto Point cuyas coordenadas son la suma de las coordenadas correspondientes de los puntos originales:

class Point { public: int x, y; Point(int x = 0, int y = 0) : x(x), y(y) {} Point operator+(const Point& other) const { return Point(x + other.x, y + other.y); } }; int main() { Point p1(1, 2); Point p2(3, 4); Point p3 = p1 + p2; // Usa el operador + sobrecargado // p3.x será 4, p3.y será 6 }

8. ¿Qué son los punteros inteligentes en C++, y por qué se prefieren a los punteros crudos?

Los punteros inteligentes en C++ son clases que actúan como punteros pero gestionan automáticamente la memoria a la que apuntan. Esto ayuda a prevenir fugas de memoria y punteros colgantes. Se prefieren a los punteros crudos porque los punteros crudos requieren la gestión manual de la memoria usando new y delete, lo cual es propenso a errores.

Los punteros inteligentes ofrecen gestión automática de memoria a través de RAII (Adquisición de Recursos es Inicialización). Cuando un puntero inteligente sale del ámbito, libera automáticamente la memoria que gestiona. Los tipos principales son:

  • unique_ptr: Propiedad exclusiva.
  • shared_ptr: Propiedad compartida a través del conteo de referencias.
  • weak_ptr: Observador no propietario de un shared_ptr.

El uso de punteros inteligentes conduce a un código más seguro y robusto al eliminar la necesidad de gestionar la memoria manualmente, reduciendo así el riesgo de fugas de memoria y mejorando la seguridad de excepciones. Ejemplo:

#include <memory> int main() { std::unique_ptr<int> ptr(new int(10)); // La memoria se libera automáticamente cuando ptr sale del ámbito return 0; }

9. Describe la diferencia entre polimorfismo en tiempo de compilación y polimorfismo en tiempo de ejecución.

El polimorfismo en tiempo de compilación, también conocido como polimorfismo estático o enlace temprano, se logra a través de la sobrecarga de funciones y la sobrecarga de operadores. El compilador sabe en tiempo de compilación qué función llamar basándose en el número y tipo de argumentos. Mejora el rendimiento porque la llamada a la función se resuelve durante la compilación.

Polimorfismo en tiempo de ejecución, también conocido como polimorfismo dinámico o enlace tardío, se logra principalmente a través de funciones virtuales y herencia. La decisión de qué función llamar se toma en tiempo de ejecución en función del tipo de objeto real. Esto permite una mayor flexibilidad, pero podría introducir una ligera sobrecarga debido a la resolución en tiempo de ejecución (por ejemplo, búsqueda en la vtable).

10. ¿Qué es la Biblioteca de Plantillas Estándar (STL) en C++? Dé algunos ejemplos de contenedores que proporciona.

La Biblioteca de Plantillas Estándar (STL) es un conjunto de clases y funciones de plantilla en C++ que proporcionan estructuras de datos y algoritmos de programación comunes. Ofrece componentes reutilizables, lo que hace que el código C++ sea más eficiente y fácil de mantener. La STL es una biblioteca genérica, lo que significa que sus componentes funcionan con varios tipos de datos.

Algunos ejemplos de contenedores proporcionados por la STL incluyen:

  • vector: Un array dinámico.
  • list: Una lista doblemente enlazada.
  • deque: Una cola de doble extremo.
  • set: Una colección de elementos únicos.
  • map: Una colección de pares clave-valor (array asociativo).
  • unordered_map: Similar a map, pero sin un orden garantizado y con tiempos de acceso típicamente más rápidos (implementación de tabla hash).
  • stack: Una estructura de datos LIFO (Last-In, First-Out).
  • queue: Una estructura de datos FIFO (First-In, First-Out).

11. Explique el concepto de semántica de movimiento en C++11 y cómo mejora el rendimiento.

La semántica de movimiento en C++11 permite transferir la propiedad de los recursos de un objeto a otro sin copiar. Esto es particularmente útil cuando se trata de objetos temporales u objetos que están a punto de ser destruidos. En lugar de realizar una copia profunda, los recursos (por ejemplo, memoria asignada dinámicamente) simplemente se 'mueven' al nuevo objeto, y el objeto original se deja en un estado válido pero indeterminado. El constructor de movimiento y el operador de asignación de movimiento facilitan este proceso.

El rendimiento mejora significativamente, especialmente con objetos grandes, porque se evita la copia innecesaria. La copia implica asignar nueva memoria y duplicar los datos, mientras que mover simplemente implica actualizar punteros. Por ejemplo, considere un std::vector. Copiar un vector grande puede llevar mucho tiempo. La semántica de movimiento permite que el puntero interno del vector a los datos subyacentes se transfiera a un nuevo vector, evitando la costosa operación de copia. Esto se usa comúnmente al devolver objetos por valor desde funciones. Por ejemplo, std::move se puede usar para invocar explícitamente la semántica de movimiento.

12. ¿Qué es RTTI (Información de tipo en tiempo de ejecución) en C++ y cuándo podría usarlo?

RTTI (Información de tipo en tiempo de ejecución) es un mecanismo de C++ que le permite determinar el tipo de un objeto durante el tiempo de ejecución. Se habilita principalmente a través del operador dynamic_cast y el operador typeid. dynamic_cast convierte de forma segura punteros o referencias a tipos de clase base en punteros o referencias a tipos de clase derivada, y typeid devuelve un objeto std::type_info que representa el tipo de una expresión.

Podrías usar RTTI (Información de Tipo en Tiempo de Ejecución) cuando necesites realizar operaciones que dependan del tipo real de un objeto, especialmente cuando se trata de polimorfismo. Por ejemplo, si tienes un puntero de clase base y necesitas llamar a una función específica que solo existe en una clase derivada particular, podrías usar dynamic_cast para verificar si el objeto es de ese tipo derivado antes de llamar a la función. Sin embargo, ten cuidado, la excesiva dependencia del RTTI a veces puede indicar un defecto de diseño donde el polimorfismo no se está utilizando por completo.

13. Describe el uso de las expresiones lambda en C++11 y posteriores.

Las expresiones lambda en C++11 y posteriores proporcionan una forma concisa de crear objetos de función anónimos (clausuras). Permiten definir funciones en línea, a menudo donde se usan, mejorando la legibilidad y el mantenimiento del código. Las lambdas son particularmente útiles cuando se trabaja con algoritmos que requieren objetos de función como argumentos, como std::sort, std::for_each, o std::transform.

Las características clave incluyen: listas de captura (que especifican las variables del ámbito circundante para que estén disponibles dentro de la lambda), listas de parámetros (como las funciones regulares), deducción del tipo de retorno (o especificación explícita), y el cuerpo de la función. Se pueden usar para crear funciones simples y de un solo uso sin la necesidad de definiciones formales de función.

Por ejemplo, una lambda simple para sumar dos números:

auto add = [](int x, int y) { return x + y; }; int result = add(5, 3); // result es 8

14. Explica el concepto de RAII (Adquisición de Recursos es Inicialización) y cómo se usa en C++.

RAII (Adquisición de recursos es inicialización) es una técnica de programación en C++ donde la gestión de recursos (como memoria, manejadores de archivos, sockets, etc.) está ligada al tiempo de vida de un objeto. La idea central es que un recurso se adquiere cuando se construye un objeto (inicializa), y se libera automáticamente cuando el objeto se destruye (sale del ámbito).

En C++, RAII se implementa típicamente usando clases cuyos constructores adquieren recursos y cuyos destructores los liberan. Esto asegura que los recursos siempre se limpian correctamente, incluso en presencia de excepciones, previniendo fugas de recursos. Los punteros inteligentes (como std::unique_ptr y std::shared_ptr) son ejemplos principales de RAII en acción, gestionando la memoria asignada dinámicamente.

15. ¿Qué son las plantillas en C++ y cómo permiten la programación genérica?

Las plantillas en C++ son una característica poderosa que permite la programación genérica. Permiten escribir código que puede funcionar con diferentes tipos de datos sin tener que reescribir el código para cada tipo. Esencialmente, las plantillas son planos para crear funciones o clases que operan en tipos genéricos.

Las plantillas facilitan la programación genérica mediante el uso de parámetros de tipo. Cuando define una plantilla, especifica tipos de marcador de posición. El compilador luego genera versiones específicas de la función o clase en tiempo de compilación en función de los tipos reales utilizados cuando se instancia la plantilla. Esto evita la duplicación de código y promueve la reutilización del código, mejorando la eficiencia y el mantenimiento. Por ejemplo, una función de plantilla template <typename T> T max(T a, T b) puede encontrar el máximo de cualquier tipo para el cual el operador > está definido. Las clases de plantilla operan de manera similar; un std::vector<T> usa una plantilla para crear un vector de enteros, cadenas o cualquier otro tipo definido.

16. ¿Cómo se puede lograr la seguridad de subprocesos en una aplicación C++ multihilo?

La seguridad de subprocesos en aplicaciones multihilo de C++ se puede lograr utilizando varios mecanismos. Los enfoques comunes incluyen:

  • Mutexes (Exclusión Mutua): Protegen los recursos compartidos al permitir que solo un hilo acceda a ellos a la vez. Use std::mutex para bloquear y desbloquear, y std::lock_guard o std::unique_lock para el bloqueo al estilo RAII.
  • Bloqueos de Lectura-Escritura: Permiten que múltiples hilos lean un recurso compartido concurrentemente, pero solo un hilo escriba a la vez. std::shared_mutex se puede usar para este propósito (C++17 y posteriores).
  • Operaciones Atómicas: Use variables atómicas (std::atomic<T>) para operaciones simples que necesitan ser seguras para subprocesos sin la sobrecarga de los mutexes. Útil para contadores o indicadores.
  • Variables de Condición: Permiten que los hilos esperen a que una condición específica se vuelva verdadera. Se usa junto con los mutexes. std::condition_variable proporciona métodos como wait(), notify_one() y notify_all().
  • Almacenamiento Local de Hilos: Cada hilo tiene su propia copia de una variable. Se logra usando la palabra clave thread_local, eliminando la necesidad de sincronización.
  • Evitar el Estado Mutable Compartido: Siempre que sea posible, diseñe su aplicación para minimizar o eliminar los datos mutables compartidos. Las estructuras de datos inmutables o el paso de mensajes pueden simplificar la seguridad de los hilos.

17. ¿Cuáles son los diferentes tipos de operadores de conversión en C++ (static_cast, dynamic_cast, const_cast, reinterpret_cast) y cuándo se debe usar cada uno?

C++ proporciona cuatro operadores de conversión principales: static_cast, dynamic_cast, const_cast y reinterpret_cast. static_cast realiza conversiones no polimórficas, como la conversión entre tipos numéricos o tipos de clase relacionados (verificación en tiempo de compilación). Úselo cuando sepa que la conversión es segura y está bien definida. dynamic_cast se utiliza para conversiones polimórficas (verificación en tiempo de ejecución), principalmente al convertir hacia abajo punteros o referencias dentro de una jerarquía de herencia. Devuelve nullptr si la conversión falla (para punteros) o lanza una excepción para las referencias. const_cast está diseñado específicamente para agregar o eliminar los calificadores const o volatile de un tipo. Úselo solo cuando absolutamente necesite modificar un objeto const y esté seguro de que es seguro hacerlo (el objeto no fue originalmente declarado const). reinterpret_cast es la conversión más peligrosa, ya que simplemente reinterpreta los bits de un tipo como otro tipo sin ninguna comprobación de tipos. Úselo solo para operaciones de bajo nivel, como la conversión entre punteros e enteros o cuando se trata con hardware.

18. Explique qué es un destructor virtual y por qué es importante en las jerarquías de herencia.

Un destructor virtual asegura que cuando un objeto de clase derivada se elimina a través de un puntero de clase base, también se llama al destructor de la clase derivada. Sin un destructor virtual, solo se llamaría al destructor de la clase base, lo que provocaría posibles fugas de recursos si la clase derivada asignó memoria o tenía otros recursos.

Considere un escenario donde un puntero de clase Base apunta a un objeto de clase Derived. Si se llama a delete base_pointer y el destructor de Base no es virtual, solo se ejecuta el destructor de Base. Si Derived asignó memoria, esa memoria no se liberará, lo que resultará en una fuga de memoria. Al declarar virtual ~Base() {}, garantiza que también se llamará a ~Derived() antes de que se llame a ~Base(), evitando fugas de recursos y garantizando una limpieza adecuada.

19. Describe cómo implementaría un patrón singleton simple en C++.

Para implementar un singleton en C++, normalmente se hace que el constructor sea privado para evitar la instanciación directa. Una variable miembro estática del tipo de clase contiene la única instancia, y un método estático proporciona acceso a ella. La primera vez que se llama al método estático, crea la instancia; las llamadas subsiguientes devuelven la existente. Aquí hay un ejemplo básico:

class Singleton { private: Singleton() {} static Singleton* instance; public: static Singleton* getInstance() { if (!instance) { instance = new Singleton(); } return instance; } }; Singleton* Singleton::instance = nullptr;

Este enfoque no es seguro para subprocesos. Para la seguridad de subprocesos, necesitaría agregar mecanismos de bloqueo (por ejemplo, usando mutexes) al método getInstance() para evitar condiciones de carrera durante la creación de instancias en un entorno multi-hilo. Meyers Singleton es otra forma de implementar el singleton en C++ que se encarga de la seguridad de los subprocesos.

20. ¿Qué son las plantillas variádicas en C++ y cómo son útiles?

Las plantillas variádicas son una característica de las plantillas de C++ que permite que una plantilla acepte un número variable de argumentos. Esto se logra usando un paquete de parámetros, que es un parámetro de plantilla que representa cero o más argumentos de plantilla. Se declaran usando la sintaxis de puntos suspensivos ... (por ejemplo, template<typename... Args>).

Son útiles para:

  • Crear funciones que pueden aceptar un número variable de argumentos, como printf o una función de registro personalizada.
  • Implementar estructuras de datos o algoritmos genéricos que pueden funcionar con diferentes tipos y números de elementos sin necesidad de escribir código separado para cada caso específico. Por ejemplo, std::tuple se implementa usando plantillas variádicas.
  • Realizar cálculos en tiempo de compilación que involucran un número variable de argumentos a través de técnicas de recursión o plegado.

Ejemplo:

template<typename... Args> void print_all(Args... args) { (std::cout << ... << args) << std::endl; }

Preguntas de entrevista de C++ para experimentados

1. Explique el concepto de RAII y cómo previene las fugas de memoria en C++.

RAII (Adquisición de recursos es inicialización) es una técnica de programación de C++ donde la gestión de recursos (como la asignación de memoria, el manejo de archivos, etc.) está ligada a la vida útil de un objeto. El constructor del objeto adquiere el recurso y el destructor libera el recurso. Esto asegura que los recursos se liberen automáticamente cuando el objeto sale del ámbito, independientemente de cómo se salga del ámbito (por ejemplo, finalización normal, excepción lanzada).

RAII previene fugas de memoria porque el destructor, que libera la memoria asignada, tiene la garantía de ser llamado cuando el objeto es destruido. Considere usar punteros inteligentes como std::unique_ptr o std::shared_ptr, que son envoltorios RAII alrededor de punteros sin formato. Cuando el puntero inteligente sale del alcance, automáticamente borra la memoria subyacente, previniendo fugas. Por ejemplo: std::unique_ptr<int> ptr(new int(5)); // Memoria liberada automáticamente cuando ptr sale del alcance.

2. ¿Cómo el modelo de memoria de C++ soporta la multihread, y cuáles son los desafíos clave?

El modelo de memoria de C++ define cómo los hilos interactúan con la memoria, asegurando la consistencia de los datos en programas multihilo. Proporciona garantías sobre cuándo las escrituras hechas por un hilo se vuelven visibles para otros hilos. Los elementos clave incluyen:

  • Atómicos: Las variables atómicas proporcionan primitivas de sincronización que garantizan operaciones atómicas de lectura-modificación-escritura, previniendo carreras de datos.
  • Ordenamiento de Memoria: Especifica las restricciones sobre cómo las operaciones de memoria pueden ser reordenadas por el compilador y el procesador. Los ordenamientos comunes incluyen std::memory_order_relaxed, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel, y std::memory_order_seq_cst.
  • Relación Happens-Before: Define un ordenamiento parcial de las operaciones a través de los hilos. Si la operación A happens-before la operación B, entonces los efectos de A son visibles para B.

Los desafíos en la programación multihilo surgen de:

  • Condiciones de Carrera (Data Races): Ocurren cuando múltiples hilos acceden a la misma ubicación de memoria concurrentemente, y al menos un hilo está escribiendo. El modelo de memoria proporciona herramientas (atómicos, mutexes) para prevenir esto. Ejemplo:

#include <atomic> std::atomic<int> contador = 0; //en lugar de un int contador regular contador++;

  • Interbloqueos (Deadlocks): Situaciones donde dos o más hilos están bloqueados indefinidamente, esperando que el otro libere recursos.

  • Bloqueos Activos (Livelocks): Los hilos intentan repetidamente adquirir bloqueos pero fallan, impidiendo el progreso.

  • Falso Compartimiento (False Sharing): Hilos que operan en diferentes elementos de datos que residen en la misma línea de caché, lo que lleva a la invalidación innecesaria de la caché y la degradación del rendimiento.

3. Describe la diferencia entre `std::move` y `std::forward`, ¿y cuándo debería usarse cada uno?

`std::move` convierte incondicionalmente su argumento en una referencia rvalue. Se utiliza para permitir el movimiento desde un objeto, señalando que los recursos del objeto pueden ser transferidos. Después de `std::move`, el objeto del que se movió debe quedar en un estado válido pero no especificado.

std::forward, por otro lado, es un moldeado condicional que solo convierte su argumento en una referencia rvalue si el argumento fue inicializado con un rvalue. Se utiliza principalmente en funciones de plantilla para reenviar perfectamente argumentos a otras funciones, preservando su categoría de valor original (lvalue o rvalue). Use std::move cuando quiera transferir explícitamente la propiedad. Use std::forward cuando quiera reenviar argumentos sin cambiar su categoría de valor, típicamente en código genérico.

4. ¿Cuáles son las ventajas y desventajas de usar excepciones para el manejo de errores en C++?

Las excepciones en C++ proporcionan una forma estructurada de manejar errores, ofreciendo ventajas como una mejor legibilidad del código al separar la lógica de manejo de errores del flujo principal del código. También aseguran que los errores no se ignoren, ya que las excepciones no manejadas terminan el programa, obligando a los desarrolladores a abordarlos. Las excepciones también pueden desenrollar automáticamente la pila, llamando a los destructores para los objetos que salen del alcance, evitando fugas de recursos (RAII).

Sin embargo, las excepciones tienen desventajas. Pueden introducir una sobrecarga de rendimiento, especialmente si las excepciones se lanzan y se capturan con frecuencia. Esto se debe a que el compilador necesita generar código adicional para el desenrollado de la pila y el manejo de excepciones. Las especificaciones de excepción, obsoletas en C++11 y eliminadas en C++17, también agregaron complejidad sin un beneficio significativo. En algunos casos, el uso de códigos de retorno para el manejo de errores puede ser más eficiente y predecible. Además, el manejo de excepciones puede conducir a un flujo de control complejo, lo que hace que la depuración y el razonamiento sobre el comportamiento del programa sean más desafiantes.

5. Explique el propósito del especificador noexcept y su impacto en la generación de código y la seguridad de las excepciones.

El especificador noexcept en C++ se utiliza para indicar que una función no lanzará excepciones. Su propósito principal es ayudar al compilador a optimizar la generación de código al permitirle evitar la generación de código de manejo de excepciones para esa función. Esto puede conducir a un código más pequeño y rápido, especialmente en secciones críticas para el rendimiento.

Cuando una función se declara noexcept, el compilador puede hacer ciertas suposiciones sobre su comportamiento. Si una función noexcept lanza una excepción, se llama a std::terminate, deteniendo inmediatamente la ejecución del programa. Esto ofrece una garantía sólida al llamante de que la función completará con éxito o terminará el programa, lo que puede simplificar las garantías de seguridad de las excepciones. Además, los movimientos se prefieren a las copias, lo que proporciona una mayor seguridad de las excepciones.

6. ¿Cómo resuelve el compilador las llamadas a funciones virtuales en tiempo de ejecución y cuáles son las implicaciones de rendimiento?

El compilador resuelve las llamadas a funciones virtuales en tiempo de ejecución utilizando un mecanismo llamado Tabla Virtual (vtable). Cada clase con funciones virtuales tiene una vtable, que es una matriz de punteros de función, uno para cada función virtual en la clase y sus clases base. Cada objeto de esa clase tiene un puntero (vptr) a la vtable de su clase. Cuando se llama a una función virtual a través de un puntero o referencia de clase base, el compilador utiliza el vptr para encontrar la vtable apropiada y luego usa la vtable para llamar a la función correcta. Esto se conoce como despacho dinámico.

Las implicaciones de rendimiento implican una ligera sobrecarga. Está la sobrecarga de memoria de almacenar el vptr en cada objeto y la vtable para cada clase. También hay una sobrecarga en tiempo de ejecución porque, en lugar de una llamada de función directa, hay una llamada indirecta que involucra la desreferenciación del vptr y la búsqueda en la vtable. Sin embargo, esta sobrecarga es generalmente pequeña en comparación con los beneficios del polimorfismo y el comportamiento dinámico, especialmente en sistemas complejos. Los procesadores modernos también emplean técnicas como la predicción de bifurcaciones que mitigan parte del impacto en el rendimiento. El costo de la llamada a la función virtual es generalmente una o dos desreferenciaciones de memoria adicionales.

7. Describa el principio SFINAE (La falla de sustitución no es un error) y proporcione un ejemplo de su uso.

SFINAE (Substitution Failure Is Not An Error) es un principio en la met programación de plantillas de C++. Afirma que cuando se sustituyen argumentos de plantilla en una plantilla de función o una plantilla de clase, si la sustitución resulta en un tipo o expresión inválidos, no es un error. En cambio,, el compilador simplemente elimina esa función o clase del conjunto de sobrecarga o la lista de argumentos de plantilla.

Ejemplo:

template <typename T> auto check_has_member(int) -> decltype(std::declval<T>().member, std::true_type{}); // Válido si T tiene miembro template <typename T> std::false_type check_has_member(...); // Reserva struct HasMember { int member; }; struct NoMember {}; static_assert(decltype(check_has_member<HasMember>(0))::value == true, "HasMember tiene miembro"); static_assert(decltype(check_has_member<NoMember>(0))::value == false, "NoMember no tiene miembro");

En este ejemplo, check_has_member usa SFINAE para verificar si un tipo T tiene un miembro llamado member. Si T no tiene member, la primera sobrecarga no es válida y se elimina de la consideración, y el compilador elige la segunda sobrecarga que devuelve std::false_type.

8. ¿Cuáles son las diferencias entre punteros compartidos, punteros únicos y punteros débiles? ¿Cuándo se usa cada uno?

Los punteros inteligentes se utilizan para gestionar la memoria asignada dinámicamente de forma segura. Los punteros únicos (std::unique_ptr) proporcionan propiedad exclusiva; solo un puntero único puede apuntar a un objeto a la vez. Cuando el puntero único sale del ámbito, el objeto se elimina automáticamente. Úselos cuando quiera asegurarse de que solo existe un propietario para un recurso. Los punteros compartidos (std::shared_ptr) permiten que varios punteros apunten al mismo objeto, utilizando un recuento de referencias para rastrear el número de propietarios. El objeto se elimina solo cuando el último puntero compartido sale del ámbito. Son útiles cuando varias partes de su programa necesitan compartir la propiedad de un recurso. Los punteros débiles (std::weak_ptr) proporcionan una referencia no propietaria a un objeto gestionado por un puntero compartido. No incrementan el recuento de referencias y se pueden utilizar para detectar si el objeto ha sido eliminado. Úselos para observar un objeto gestionado por un puntero compartido sin tomar posesión, evitando dependencias circulares.

9. Discuta los casos de uso de las funciones y variables constexpr en C++ moderno.

Las funciones y variables constexpr se evalúan en tiempo de compilación, lo que ofrece importantes beneficios de rendimiento y permite cálculos en tiempo de compilación. Los casos de uso clave incluyen:

  • Constantes en tiempo de compilación: Definir constantes cuyos valores se conocen en tiempo de compilación, lo que mejora la optimización del código y permite su uso en contextos que requieren evaluación en tiempo de compilación (por ejemplo, tamaños de matrices, argumentos de plantilla).
  • Metaprogramación: Realizar cálculos y lógica complejos en tiempo de compilación, reduciendo la sobrecarga en tiempo de ejecución. Por ejemplo, calcular factoriales o números de Fibonacci en tiempo de compilación.
  • Sistemas embebidos: Asegurar la previsibilidad y el determinismo del código, fundamental en entornos con recursos limitados donde el rendimiento en tiempo de ejecución es primordial.
  • Programación Genérica: Las funciones y variables constexpr se pueden usar en plantillas para realizar cálculos basados en argumentos de plantilla, lo que permite una generación de código más flexible y eficiente. Por ejemplo, considere una función que calcula el tamaño de una matriz estática basada en un parámetro de plantilla. template <size_t N> constexpr size_t get_array_size() { return N * 2; }
  • Rendimiento mejorado: Al desplazar los cálculos al tiempo de compilación, los programas pueden evitar la sobrecarga en tiempo de ejecución, lo que lleva a una ejecución más rápida. Esto es especialmente beneficioso para los cálculos de uso frecuente o aquellos que dependen de valores constantes. Por ejemplo:

constexpr int cuadrado(int n) { return n * n; } int main() { int arr[cuadrado(5)]; // Tamaño del array conocido en tiempo de compilación }

10. Explique el papel de los asignadores en C++ y cómo se pueden personalizar para requisitos específicos de gestión de memoria.

Los asignadores en C++ son responsables de encapsular las estrategias de asignación y desasignación de memoria. Desacoplan la gestión de la memoria de los objetos que la utilizan. Los contenedores de la biblioteca estándar (como std::vector, std::list, etc.) utilizan asignadores para manejar las necesidades de memoria, lo que le permite especificar comportamientos de asignación personalizados sin cambiar la lógica del contenedor.

Se pueden crear asignadores personalizados para optimizar el uso de la memoria para escenarios específicos. Por ejemplo, podría implementar un asignador que utilice un grupo de memoria para reducir la fragmentación o uno que asigne memoria de una región específica. Para crear un asignador personalizado, debe definir una clase que cumpla con los requisitos del asignador, lo que implica proporcionar tipos como value_type, pointer, reference e implementar los métodos allocate() y deallocate(). La biblioteca estándar proporciona std::allocator_traits para ayudar a garantizar que su asignador personalizado cumpla con los requisitos del estándar y para facilitar la interoperabilidad.

11. ¿Cómo funciona el concepto de reenvío perfecto y por qué es importante para la programación genérica?

El reenvío perfecto le permite pasar argumentos a otra función preservando su tipo original y su categoría de valor (lvalue o rvalue). Se logra usando std::forward. Esencialmente, evita copias y conversiones innecesarias cuando está escribiendo funciones genéricas o plantillas que necesitan trabajar con una variedad de tipos de argumentos.

En la programación genérica, el reenvío perfecto es crucial porque permite escribir funciones que pueden aceptar cualquier tipo de argumento y reenviarlos a otra función como si se pasaran directamente. Esto es particularmente importante cuando se trata de la semántica de movimiento y las referencias rvalue, donde preservar el tipo original y la categoría de valor es esencial para el rendimiento y la corrección. Sin él, podría perder la capacidad de mover objetos, lo que lleva a copias innecesarias y un comportamiento potencialmente incorrecto.

12. Describa las diversas formas de lograr el polimorfismo en tiempo de compilación en C++, y compare sus compensaciones.

El polimorfismo en tiempo de compilación en C++ se logra principalmente a través de plantillas y sobrecarga de funciones.

  • Plantillas: Permiten escribir código genérico que funciona con diferentes tipos de datos sin la necesidad de una especificación explícita del tipo. El compilador genera instancias de código específicas para cada tipo de datos utilizado. Esto ofrece un alto rendimiento, pero puede provocar una hinchazón de código si se crean muchas especializaciones. Las plantillas son muy potentes para la programación genérica.
  • Sobrecarga de funciones: Permite definir múltiples funciones con el mismo nombre pero diferentes listas de parámetros. El compilador selecciona la función adecuada en función de los tipos de argumentos en tiempo de compilación. Esto aumenta la legibilidad y la flexibilidad del código, pero se limita a las variaciones en los tipos de argumentos. Las compensaciones incluyen una posible ambigüedad si las funciones sobrecargadas tienen tipos de parámetros similares, lo que requiere un diseño cuidadoso.

13. ¿Cuál es el propósito del tipo `std::optional`, y cómo mejora la seguridad y la legibilidad del código?

`std::optional` es un tipo introducido en C++17 para representar un valor que puede o no estar presente. Es un contenedor que puede contener un valor de un tipo especificado T, o nada en absoluto. Su propósito es proporcionar una forma clara y segura para el tipo de representar valores opcionales, evitando la ambigüedad y los posibles errores asociados con el uso de valores centinela (como nullptr o números mágicos) para indicar la ausencia.

`std::optional` mejora la seguridad y la legibilidad del código al:

  • Expresar explícitamente la opcionalidad: Deja claro en la firma de la función o en la declaración de la variable que un valor podría faltar.
  • Evitar desreferencias de punteros nulos: Elimina el riesgo de desreferenciar un puntero nulo porque el opcional debe ser verificado antes de acceder al valor.
  • Mejorar la claridad del código: Al usar `std::optional`, se evita el uso de valores centinela potencialmente ambiguos. `std::optional` tiene métodos como `has_value()` y `value()` para verificar y recuperar explícitamente el valor, lo que hace que el código sea más fácil de entender.
  • Proporcionar seguridad en tiempo de compilación: `std::optional` impone la seguridad de tipos, lo que impide el uso accidental de valores ausentes, lo que conduce a un código más robusto.

14. Explica el concepto de semántica de movimiento y sus beneficios para el rendimiento y la gestión de recursos.

La semántica de movimiento permite transferir la propiedad de los recursos de un objeto a otro sin copiarlos, lo que impulsa significativamente el rendimiento, especialmente para objetos grandes. En lugar de crear una copia completa (lo cual puede ser costoso), los recursos se 'mueven' al nuevo propietario, y el objeto original se deja en un estado válido pero típicamente vacío. Esto evita la asignación y liberación de memoria innecesarias.

El beneficio clave es evitar copias profundas. Considere un std::vector. Copiarlo crea un nuevo vector y copia todos sus elementos. Mover, sin embargo, simplemente transfiere el puntero al búfer de datos subyacente al nuevo vector y establece el puntero del vector original en nullptr. Esto es mucho más rápido. La semántica de movimiento también permite una gestión eficiente de los recursos, previniendo fugas de recursos y mejorando la eficiencia del código al minimizar las operaciones redundantes. Los constructores de movimiento y los operadores de asignación de movimiento (por ejemplo, MyClass(MyClass&& other) y MyClass& operator=(MyClass&& other)) se utilizan para implementar la semántica de movimiento.

15. ¿Cómo se pueden evitar las colisiones de nombres en proyectos grandes de C++?

Las colisiones de nombres en proyectos grandes de C++ se pueden evitar principalmente mediante el uso de espacios de nombres (namespaces). Encapsular el código dentro de espacios de nombres crea ámbitos distintos, evitando eficazmente los conflictos entre identificadores (variables, funciones, clases) que de otro modo podrían compartir el mismo nombre. Por ejemplo, podrías tener namespace mi_biblioteca que contenga todo el código específico de tu biblioteca. También considera usar espacios de nombres para las clases, por ejemplo MiBiblioteca::MiClase. Además, una buena estructura de archivos ayuda a organizar el código y proporciona contexto visual a los desarrolladores.

Otras técnicas incluyen el uso de convenciones de nombres descriptivas y consistentes, y seguir el principio del mínimo privilegio; limitando el alcance de las variables y funciones tanto como sea posible. Evita usar using namespace en los archivos de encabezado, ya que puede introducir una contaminación de espacio de nombres no deseada en los archivos que incluyen el encabezado. En su lugar, califica completamente los nombres (por ejemplo, std::cout) o usa declaraciones using (por ejemplo, using std::cout;) dentro de ámbitos específicos.

16. Explica la diferencia entre un objeto de función (functor) y una expresión lambda en C++.

Un objeto de función (o functor) es un objeto de clase que sobrecarga el operator(). Esto te permite llamar a un objeto de esa clase como si fuera una función. Por ejemplo:

struct MiFunctor { int operator()(int x) { return x * 2; } }; MiFunctor mi_functor; int resultado = mi_functor(5); // Llama a la sobrecarga operator()

Una expresión lambda, por otro lado, es una forma concisa de definir un objeto de función anónima en línea. Las lambdas se utilizan típicamente para operaciones cortas y simples y pueden capturar variables de su ámbito circundante. Una expresión lambda como [](int x){ return x * 2; } es equivalente a definir un objeto de función, pero es más compacta y se puede definir directamente donde se utiliza. El compilador genera una clase functor sin nombre tras bambalinas para las expresiones lambda.

17. Describe cómo implementaría un grupo de subprocesos en C++ utilizando características de la biblioteca estándar.

Un grupo de subprocesos se puede implementar en C++ utilizando std::thread, std::mutex, std::condition_variable y una cola. Una cola (por ejemplo, std::queue o std::deque) almacena tareas (típicamente objetos de función). Un conjunto de subprocesos de trabajo monitorea continuamente esta cola. Un mutex protege el acceso a la cola y una variable de condición señala a los subprocesos cuando se agregan nuevas tareas.

Cada hilo de trabajo realiza los siguientes pasos en un bucle: adquirir el mutex, verificar si hay tareas en la cola. Si está vacía, el hilo espera en la variable de condición, liberando el mutex. Si hay tareas, elimina una, libera el mutex, ejecuta la tarea y repite. Cuando el grupo de hilos se cierra, se envía una señal a todos los hilos en espera a través de la variable de condición, lo que les permite salir con gracia. El std::future se puede usar para recuperar resultados de las tareas.

18. Discuta las implicaciones de rendimiento del uso de la herencia virtual en comparación con la herencia no virtual.

La herencia virtual introduce una sobrecarga en comparación con la herencia no virtual. Esta sobrecarga proviene principalmente de la necesidad de resolver el subobjeto de la clase base correcta en tiempo de ejecución. En la herencia no virtual, cada clase derivada contiene directamente sus subobjetos de clase base, lo que lleva a un diseño de memoria directo y eficiente. Sin embargo, con la herencia virtual, una clase base virtual se comparte entre múltiples clases derivadas, típicamente implementada utilizando tablas de funciones virtuales (vtables) y punteros virtuales (vpointers) o mecanismos similares.

Esta resolución en tiempo de ejecución incurre en costos tanto en términos de memoria como de velocidad. La memoria se utiliza para almacenar la vtable y el vpointer. La velocidad se ve afectada porque el acceso a los miembros de la clase base virtual requiere un nivel adicional de indirección a través del vpointer a la vtable, y luego al desplazamiento apropiado dentro del subobjeto de la clase base compartida. Esto contrasta con la herencia no virtual donde la dirección del miembro de la clase base se puede determinar en tiempo de compilación con un simple desplazamiento. Aunque el impacto en el rendimiento podría ser mínimo en muchos casos, puede llegar a ser notable en secciones de código críticas para el rendimiento o cuando se trata de jerarquías de herencia profundamente anidadas que involucran múltiples clases base virtuales. Por ejemplo, el tamaño de los objetos será mayor y el acceso a los miembros requerirá indirecciones adicionales.

19. Explique el propósito del polimorfismo estático y compárelo con el polimorfismo dinámico. Dé un ejemplo de cada uno

El polimorfismo estático (también conocido como polimorfismo en tiempo de compilación) se logra a través de la sobrecarga de funciones y la sobrecarga de operadores. La función específica a ejecutar se determina en tiempo de compilación en función de la firma de la función (el número y el tipo de argumentos). El polimorfismo dinámico (también conocido como polimorfismo en tiempo de ejecución) se logra a través de funciones virtuales y herencia. La función específica a ejecutar se determina en tiempo de ejecución en función del tipo real del objeto.

Ejemplo de polimorfismo estático (sobrecarga de funciones en C++):

int add(int a, int b) { return a + b; } double add(double a, double b) { return a + b; }

Ejemplo de polimorfismo dinámico (funciones virtuales en C++):

class Animal { public: virtual void makeSound() { std::cout << "Sonido genérico de animal" << std::endl; } }; class Dog : public Animal { public: void makeSound() override { std::cout << "¡Guau!" << std::endl; } }; int main() { Animal* animal = new Dog(); animal->makeSound(); // Llama a Dog::makeSound() en tiempo de ejecución }

20. ¿Cuál es la diferencia entre una copia superficial y una copia profunda? ¿Cómo se implementa una copia profunda?

Una copia superficial crea un nuevo objeto, pero hace referencia a las mismas ubicaciones de memoria que el objeto original para los datos que contiene. Modificar el objeto copiado podría afectar al original. Una copia profunda, por otro lado, crea un objeto completamente independiente y copia recursivamente todos los datos, incluyendo objetos anidados, a nuevas ubicaciones de memoria. Los cambios en la copia no afectarán al original.

Las copias profundas pueden implementarse de varias maneras, como usar JSON.parse(JSON.stringify(object)) para objetos que contienen datos serializables. Otra forma es recorriendo recursivamente el objeto y creando nuevas instancias de cada objeto anidado. Para objetos personalizados con estructuras complejas, podría ser necesario implementar un método de copia profunda personalizado adaptado a las propiedades específicas del objeto y a cómo están estructuradas en la memoria.

C++ MCQ

Pregunta 1.

Considere el siguiente código C++:

class Base { public: virtual void print() { std::cout << "Base"; } }; class Derived : public Base { public: void print() override { std::cout << "Derived"; } }; int main() { Base* ptr = new Derived(); ptr->print(); delete ptr; return 0; }

¿Cuál es la salida de este código?

Opciones:

Base

Derived

Error del compilador debido a la definición de función virtual

Error en tiempo de ejecución

Pregunta 2.

Considere el siguiente código C++:

#include <iostream> class Base { public: Base() { std::cout << "Base Constructor" << std::endl; } ~Base() { std::cout << "Base Destructor" << std::endl; } }; class Derived : public Base { public: Derived() { std::cout << "Derived Constructor" << std::endl; } ~Derived() { std::cout << "Derived Destructor" << std::endl; } }; int main() { Derived d; return 0; }

¿Cuál es el orden de las llamadas al constructor y al destructor cuando el objeto Derived d se crea y sale del alcance?

Opciones:

Constructor Base, Constructor Derivado, Destructor Base, Destructor Derivado

Constructor Derivado, Constructor Base, Destructor Derivado, Destructor Base

Constructor Base, Constructor Derivado, Destructor Derivado, Destructor Base

Constructor Derivado, Constructor Base, Destructor Base, Destructor Derivado

Pregunta 3.

¿Qué sucede si asigna memoria usando new en C++ pero se olvida de liberarla usando delete?

Opciones:

El programa se bloqueará inmediatamente.

La memoria se liberará automáticamente cuando el programa termine.

Se producirá una fuga de memoria, lo que podría provocar inestabilidad o fallas del programa con el tiempo.

El compilador emitirá un error durante la compilación.

Pregunta 4.

Considere el siguiente código C++:

class MyClass { public: static int count; MyClass() { count++; } }; int MyClass::count = 0; int main() { MyClass obj1; MyClass obj2; std::cout << MyClass::count << std::endl; return 0; }

¿Cuál será la salida de este programa?

Opciones:

0

1

2

Un error porque `count` no es accesible desde `main`.

Pregunta 5.

¿Cuál de las siguientes afirmaciones es la más precisa con respecto a las funciones friend en C++?

Opciones:

Opciones:

Una función `friend` debe ser una función miembro de la clase a la que se le da amistad.

Una función `friend` puede acceder directamente a los miembros `private` y `protected` de la clase a la que se le da amistad, pero solo para objetos pasados como argumentos.

Una función `friend` solo puede acceder a los miembros `public` de la clase a la que se le da amistad.

Una función `friend` hereda automáticamente todos los miembros de la clase a la que se le da amistad.

Pregunta 6.

Considere el siguiente código C++:

class Base { public: virtual void print() { std::cout << "Base"; } }; class Derived : public Base { public: void print() override { std::cout << "Derived"; } }; int main() { Base* ptr = new Derived(); ptr->print(); return 0; }

¿Cuál será la salida de este programa?

Opciones:

Base

Derived

Error del compilador

Error de ejecución

Pregunta 7.

¿Qué sucede si se lanza una excepción dentro de un bloque try y no hay un bloque catch coincidente para manejarla en el ámbito inmediato?

Opciones:

El programa termina inmediatamente.

El programa busca un bloque `catch` coincidente en el ámbito de la función que realiza la llamada.

Se omite el bloque `try` y el programa continúa ejecutándose desde la siguiente línea después del bloque `try`.

Se crea automáticamente un bloque `catch` predeterminado para manejar la excepción.

Pregunta 8.

Considere el siguiente código C++:

class Shape { public: virtual double area() = 0; }; class Circle : public Shape { double radius; public: Circle(double r) : radius(r) {} double area() { return 3.14 * radius * radius; } }; class Square : public Shape { double side; public: Square(double s) : side(s) {} double area() { return side * side; } }; int main() { Shape* shapePtr; // some code here return 0; }

¿Cuál de las siguientes afirmaciones, cuando se inserta en main(), causará un error de compilación?

Opciones:

Shape* shapePtr = new Circle(5.0);

Shape* shapePtr = new Square(4.0);

Shape shapeObj;

Shape* shapePtr;

Pregunta 9.

Considere la siguiente definición de clase C++:

class MyClass { private: int value; public: MyClass(int val) : value(val) {} int getValue() const { return value; } void setValue(int val) { value = val; } }; const MyClass obj(10);

¿Cuál de las siguientes afirmaciones sobre obj es verdadera?

Opciones:

`obj.setValue(20)` compilará y modificará el valor de `obj`.

`obj.getValue()` se puede llamar y devolverá 10.

Ni `obj.setValue(20)` ni `obj.getValue()` se pueden llamar.

`obj.value = 20;` es una forma válida de cambiar el estado interno del objeto.

Pregunta 10.

¿Cuál es el propósito principal de usar plantillas de clase en C++?

Opciones:

Opciones:

Para crear clases que solo pueden contener tipos de datos primitivos.

Para reducir el tamaño del ejecutable compilado.

Para crear clases genéricas que pueden funcionar con diferentes tipos de datos sin ser reescritas.

Para aplicar una estricta comprobación de tipos en tiempo de ejecución.

Pregunta 11.

¿Cuál es el propósito principal de std::move en C++11 y posteriores?

Opciones:

Opciones:

Para crear una copia profunda de un objeto, asegurando la asignación de memoria independiente.

Para convertir incondicionalmente un lvalue a una referencia rvalue, habilitando la semántica de movimiento.

Para evitar que un objeto se copie deshabilitando el constructor de copia.

Para desasignar automáticamente la memoria cuando un objeto sale del ámbito, evitando fugas de memoria.

Pregunta 12.

Considere el siguiente fragmento de código C++:

class Animal { public: Animal() { std::cout << "Constructor de Animal" << std::endl; } virtual void speak() { std::cout << "Sonido genérico de animal" << std::endl; } }; class Mamífero : virtual public Animal { public: Mamífero() { std::cout << "Constructor de Mamífero" << std::endl; } }; class Ave : virtual public Animal { public: Ave() { std::cout << "Constructor de Ave" << std::endl; } }; class Murciélago : public Mamífero, public Ave { public: Murciélago() { std::cout << "Constructor de Murciélago" << std::endl; } };

¿Cuál es el propósito principal de usar la herencia virtual en las clases Mamífero y Ave al heredar de Animal? Seleccione la opción correcta.

Opciones:

Para asegurar que el constructor de la clase Animal se llame solo una vez en la clase Murciélago, previniendo el 'problema del diamante'.

Para permitir que la clase Murciélago anule el método speak de la clase Animal.

Para habilitar el polimorfismo en tiempo de ejecución entre las clases Mamífero y Ave.

Para hacer que la clase Animal sea abstracta, forzando a las clases derivadas a implementar el método speak.

Pregunta 13.

Considere el siguiente código C++:

#include <iostream> class Complejo { private: double real, imag; public: Complejo(double r = 0.0, double i = 0.0) : real(r), imag(i) {} Complejo operator+(const Complejo& other) const { return Complejo(real + other.real, imag + other.imag); } Complejo operator*(const Complejo& other) const { return Complejo(real * other.real - imag * other.imag, real * other.imag + imag * other.real); } friend std::ostream& operator<<(std::ostream& os, const Complejo& c) { os << c.real << " + " << c.imag << "i"; return os; } }; int main() { Complejo c1(1.0, 2.0); Complejo c2(2.0, 3.0); Complejo c3 = c1 * c2 + c1; std::cout << c3 << std::endl; return 0; }

¿Cuál será la salida de este programa?

Opciones:

-4 + 7i

2 + 5i

-2 + 7i

7 + 4i

Pregunta 14.

Considere el siguiente fragmento de código C++ que utiliza std::unique_ptr con un eliminador personalizado:

#include <iostream> #include <memory> struct Resource { int data; Resource(int d) : data(d) { std::cout << "Recurso adquirido: " << data << std::endl; } ~Resource() { std::cout << "Recurso liberado: " << data << std::endl; } }; struct MyDeleter { void operator()(Resource* res) { std::cout << "Eliminador personalizado llamado para: " << res->data << std::endl; delete res; } }; int main() { std::unique_ptr<Resource, MyDeleter> ptr(new Resource(42)); return 0; }

¿Cuál será la salida de este programa?

Opciones:

Recurso adquirido: 42 Recurso liberado: 42

Recurso adquirido: 42 Llamador de eliminación personalizado llamado para: 42 Recurso liberado: 42

Recurso adquirido: 42 Llamador de eliminación personalizado llamado para: 42

Llamador de eliminación personalizado llamado para: 42

Pregunta 15.

¿Qué es el object slicing en C++ y cuáles son sus consecuencias?

Opciones:

Opciones:

El object slicing es un proceso donde un objeto de clase derivada se convierte implícitamente en un objeto de clase base, perdiendo cualquier miembro de datos específico de la clase derivada.

El object slicing es una técnica para acceder a los miembros privados de una clase base desde una clase derivada.

El object slicing se refiere a la eliminación automática de objetos no utilizados por el recolector de basura.

El object slicing es una forma de crear múltiples copias de un objeto para el procesamiento en paralelo.

Pregunta 16.

¿Cuál es el efecto principal de devolver un objeto local por valor desde una función en C++?

Opciones:

El objeto se pasa directamente al llamador sin ninguna copia.

Se crea una copia del objeto utilizando el constructor de copia (o el constructor de movimiento si está disponible) y se devuelve.

El destructor del objeto se llama antes de regresar, lo que lleva a un comportamiento indefinido.

Se devuelve un puntero al objeto, y la memoria del objeto se gestiona automáticamente por el llamador.

Pregunta 17.

¿Cuál es el tamaño del objeto c en bytes, dado el siguiente código C++?

#include <iostream> class A {}; class B : public A {}; class C : public B {}; int main() { C c; std::cout << sizeof(c) << std::endl; return 0; }

opciones:

Opciones:

0

1

2

4

Pregunta 18.

¿Cuál es el estado de la memoria después de ejecutar el siguiente fragmento de código C++? Suponga que no hay otras variables en el ámbito.

#include <iostream> #include <memory> struct Node { std::shared_ptr<Node> next; ~Node() { std::cout << "Nodo destruido" << std::endl; } }; int main() { std::shared_ptr<Node> node1 = std::make_shared<Node>(); std::shared_ptr<Node> node2 = std::make_shared<Node>(); node1->next = node2; node2->next = node1; return 0; }

Opciones:

Tanto `node1` como `node2` son destruidos, y el mensaje 'Nodo destruido' se imprime dos veces.

Ni `node1` ni `node2` son destruidos, resultando en una fuga de memoria, y el mensaje 'Nodo destruido' no se imprime.

`node1` es destruido, pero `node2` no lo es, resultando en una fuga de memoria, y el mensaje 'Nodo destruido' se imprime una vez.

`node2` es destruido, pero `node1` no lo es, resultando en una fuga de memoria, y el mensaje 'Nodo destruido' se imprime una vez.

Pregunta 19.

Considere el siguiente código C++:

class MyClass { public: int *data; MyClass(int val) : data(new int(val)) {} ~MyClass() { delete data; } //¿Cuál es la implementación correcta para el constructor de copia? };

¿Cuál de los siguientes constructores de copia garantizará una copia profunda y evitará punteros colgantes después de copiar objetos de `MyClass`?

Opciones:

```cpp MyClass(const MyClass& other) : data(other.data) {} ```

```cpp MyClass(const MyClass& other) { data = other.data; } ```

MyClass(const MyClass& other) : data(new int(*(other.data))) {} cpp MyClass(const MyClass& other) : data(other.data ? new int(*other.data) : nullptr) {} cpp MyClass(const MyClass& other) : data(new int(0)) {} ```

Pregunta 20.

¿Cuál es la salida del siguiente código C++?

#include <iostream> #include <functional> int main() { int x = 5; std::function<int(int)> adder = [x](int y) { return x + y; }; x = 10; std::cout << adder(3) << std::endl; return 0; }

Opciones:

Opciones:

8

13

3

10

Pregunta 21.

Considere el siguiente código C++:

#include <iostream> class Engine { public: Engine() { std::cout << "Engine Constructed\n"; } ~Engine() { std::cout << "Engine Destroyed\n"; } }; class Car { public: Car() : engine() { std::cout << "Car Constructed\n"; } ~Car() { std::cout << "Car Destroyed\n"; } private: Engine engine; }; int main() { Car myCar; return 0; }

Engine Constructed
Car Constructed
Engine Destroyed
Car Destroyed

¿Cuál es la salida de este programa?

Opciones:

Coche construido Motor construido Motor destruido Coche destruido

Motor construido Coche construido Motor destruido

Coche construido Coche destruido Motor construido Motor destruido

Pregunta 22.

Considere el siguiente código C++:

class Base {
    public:
        Base() { std::cout << "Constructor Base\n"; }
        ~Base() { std::cout << "Destructor Base\n"; }
    };
    class Derived : public Base {
    public:
        Derived() { std::cout << "Constructor Derivado\n"; }
        ~Derived() { std::cout << "Destructor Derivado\n"; }
    };
    int main() {
        Base* ptr = new Derived();
        delete ptr;
        return 0;
    }

¿Cuál es la salida de este programa si el destructor de `Base` NO es virtual?

Opciones:

Constructor Base Constructor Derivado Destructor Base

Constructor Base Constructor Derivado Destructor Derivado Destructor Base

Constructor Derivado Constructor Base Destructor Base

Constructor Derivado Constructor Base Destructor Derivado Destructor Base

Pregunta 23.

¿Cuál es la principal ventaja de usar objetos de función (functors) sobre las funciones regulares en C++ al realizar operaciones como ordenar o buscar con la Biblioteca de Plantillas Estándar (STL)?

Opciones:

Los functors son generalmente más rápidos que las funciones regulares debido a las optimizaciones del compilador.

Los functors pueden mantener el estado, lo que les permite ser más flexibles y adaptables que las funciones regulares.

Los functors se integran automáticamente en línea por el compilador, lo que reduce la sobrecarga de la llamada a la función.

Los funtores siempre requieren menos memoria que las funciones regulares.

Pregunta 24.

Considere el siguiente código C++:

#include <iostream>
    void foo(double d) {
        std::cout << "foo(double)" << std::endl;
    }
    void foo(int i) {
        std::cout << "foo(int)" << std::endl;
    }
    int main() {
        float f = 3.14f;
        foo(f);
        return 0;
    }

¿Cuál será la salida de este programa?

Opciones:

foo(double)

foo(int)

Error del compilador: llamada de función ambigua

No se mostrará ninguna salida.

Pregunta 25.

Considere el siguiente fragmento de código C++:

#include <iostream>
    #include <type_traits>
    template <typename T, typename = std::enable_if_t<std::is_integral<T>::value>>
    void process(T value) {
        std::cout << "Procesando un entero: " << value << std::endl;
    }
    template <typename T, typename = std::enable_if_t<std::is_floating_point<T>::value>>
    void process(T value) {
        std::cout << "Procesando un float: " << value << std::endl;
    }
    int main() {
        process(5);  
        process(5.5);
    }

¿Cuál es la salida de este programa?

Opciones:

Opciones:

Procesando un entero: 5 Procesando un flotante: 5.5

Procesando un flotante: 5 Procesando un entero: 5.5

El código no compilará debido a llamadas de función ambiguas.

El código compilará pero no producirá ninguna salida.

¿Qué habilidades de C++ deberías evaluar durante la fase de entrevista?
----------------------------------------------------------------

Evaluar el conjunto completo de habilidades de un candidato en una sola entrevista es un desafío. Sin embargo, para C++, ciertas habilidades centrales son más importantes de evaluar. Centrarse en estas áreas clave te ayudará a identificar a los candidatos con la competencia adecuada en C++.

![¿Qué habilidades de C++ deberías evaluar durante la fase de entrevista?](https://blocks-images-prod.btw.so/skills-to-evaluate-in-c-interview-17502923094383qt.webp)

### Programación Orientada a Objetos (POO)

Mide la comprensión de POO de un candidato con preguntas de opción múltiple (MCQ) dirigidas. Una evaluación como la [prueba de C++ de Adaface](https://www.adaface.com/assessment-test/cpp-online-test) puede filtrar a los candidatos en función de sus conocimientos de POO.

Para evaluar aún más las habilidades de POO, haz preguntas de entrevista dirigidas. Esto permite a los candidatos explicar su comprensión y aplicar los conceptos.

Explica la diferencia entre herencia y polimorfismo con un ejemplo práctico.

Busque una explicación clara de ambos conceptos. El candidato también debe proporcionar un ejemplo que demuestre cómo se utilizan en escenarios del mundo real, mostrando su comprensión práctica de la POO.

### Administración de memoria

Utilice preguntas de opción múltiple (MCQ) para evaluar la comprensión de la asignación, desasignación de memoria y punteros inteligentes. También puede utilizar [prueba en línea de C++](https://www.adaface.com/assessment-test/cpp-online-test) para probar estas habilidades.

Haga preguntas de entrevista para evaluar el conocimiento práctico de un candidato sobre la administración de memoria. Esto ayuda a comprender su enfoque de los posibles problemas.

Describa los problemas comunes de administración de memoria en C++ y cómo evitarlos.

Busque la conciencia de las fugas de memoria, los punteros colgantes y los desbordamientos de búfer. El candidato también debe describir cómo técnicas como RAII y los punteros inteligentes pueden mitigar estos riesgos.

### Estructuras de datos y algoritmos

Utilice MCQ que evalúen el conocimiento de las estructuras de datos comunes (arreglos, listas enlazadas, árboles, etc.) y la complejidad algorítmica. Esto ayudará a filtrar a los candidatos que carecen de una base sólida.

Utilice preguntas específicas de la entrevista para evaluar la aplicación práctica de estos conceptos. Comprenda cómo se relacionan con los problemas del mundo real.

Describa una situación en la que elegiría una tabla hash en lugar de un árbol de búsqueda binaria.

La respuesta ideal explicaría las compensaciones entre las dos estructuras de datos. También debe considerar factores como la velocidad de búsqueda, los costos de inserción/eliminación y el uso de memoria.

3 Consejos para usar preguntas de entrevista de C++
----------------------------------------

Antes de empezar a poner en práctica lo que has aprendido, aquí tienes algunos consejos para ayudarte a utilizar las preguntas de entrevista de C++ de forma más eficaz. Estos consejos te ayudarán a optimizar tu proceso de entrevista e identificar a los mejores candidatos.

### 1. Aprovecha las evaluaciones de habilidades de C++

Para agilizar tu proceso de contratación, considera la posibilidad de utilizar evaluaciones de habilidades antes de las entrevistas. Esto te ayudará a filtrar a los candidatos en función de sus capacidades técnicas.

Adaface ofrece una gama de pruebas de C++ y relacionadas para evaluar a los candidatos. Estas incluyen una [prueba online general de C++](https://www.adaface.com/assessment-test/cpp-online-test), así como evaluaciones para C y C embebido. Incluso tenemos evaluaciones para otros lenguajes como C# si eso también es relevante. Estas pruebas ofrecen información objetiva sobre la competencia de un candidato en codificación.

El uso de estas pruebas te permite centrar el tiempo de entrevista en los candidatos que han demostrado una sólida comprensión de los conceptos de C++. Este enfoque ahorra tiempo y recursos, asegurando que solo entrevistas a las personas más cualificadas.

### 2. Selecciona un conjunto de preguntas de entrevista centradas

El tiempo es limitado durante las entrevistas, por lo que es importante hacer las preguntas más relevantes. Selecciona cuidadosamente un conjunto de preguntas que se centren en los aspectos más importantes del desarrollo de C++ para tu función específica.

Considera incluir preguntas relacionadas con estructuras de datos o diseño de sistemas, dependiendo de los requisitos del puesto. Consulta más preguntas de entrevista relacionadas con [estructura de datos](https://www.adaface.com/es/blog/preguntas-entrevista-estructura-de-datos/).

Al centrarse en áreas clave, puedes maximizar tu evaluación de los candidatos en las habilidades más críticas.

### 3. Haz Preguntas de Seguimiento Efectivas

No te limites únicamente a las respuestas iniciales; hacer preguntas de seguimiento es clave para medir la verdadera comprensión del candidato. Esto te ayuda a determinar la profundidad de su conocimiento y su capacidad para aplicar los conceptos en situaciones prácticas.

Por ejemplo, si un candidato explica un concepto como el polimorfismo, una pregunta de seguimiento podría ser: "¿Puedes describir un escenario del mundo real donde el polimorfismo sería particularmente útil en C++?" Esto puede revelar si realmente entienden las implicaciones prácticas del polimorfismo.

Contrata a los mejores talentos de C++ con evaluaciones de habilidades
-------------------------------------------

¿Busca contratar desarrolladores de C++? Evaluar con precisión sus habilidades en C++ es clave para tomar las decisiones de contratación correctas. Usar pruebas de habilidades como la [Prueba en línea de C++](https://www.adaface.com/assessment-test/cpp-online-test) o la [Prueba en línea de C](https://www.adaface.com/assessment-test/c-online-test) puede proporcionar una medida objetiva de las habilidades de un candidato.

Una vez que haya utilizado pruebas de habilidades para identificar a los mejores candidatos, puede proceder con confianza a las entrevistas. Optimice su proceso de contratación con nuestra [plataforma de evaluación en línea](https://www.adaface.com/es/recorrido-del-producto/) e [inscríbase](https://app.adaface.com/app/dashboard/signup) para comenzar.

#### Prueba en línea de C++

40 minutos | 10 preguntas de opción múltiple y 1 pregunta de codificación

La prueba en línea de C++ utiliza preguntas de opción múltiple basadas en escenarios y seguimiento de código para evaluar la capacidad de un candidato para escribir programas en C++ (tipos de datos, funciones, estructuras de datos, STL), estructurar el código utilizando principios de programación orientada a objetos (clases, herencia, polimorfismo, sobrecarga), manejar excepciones y administrar la memoria. La prueba utiliza preguntas de codificación para evaluar las habilidades prácticas de codificación en C++.

[

Prueba de C++ en línea

](https://www.adaface.com/assessment-test/cpp-online-test)

Descarga la plantilla de preguntas de entrevista de C++ en múltiples formatos
-------------------------------------------------------------

![Descarga la plantilla de preguntas de entrevista de C++ en formato PNG, PDF y TXT](https://blocks-images-prod.btw.so/90-c-interview-questions-to-hire-top-engineers-1750292315104h46.webp)

Buenas preguntas para los recién graduados incluyen aquellas que evalúan su comprensión de los conceptos básicos de C++ como tipos de datos, operadores, estructuras de control y principios simples de programación orientada a objetos.

Los desarrolladores junior deben tener un conocimiento sólido de punteros, gestión de memoria, clases, herencia, polimorfismo y estructuras de datos y algoritmos básicos en C++.

Para los desarrolladores intermedios, concéntrese en preguntas relacionadas con patrones de diseño, plantillas, manejo de excepciones, la Biblioteca de Plantillas Estándar (STL) y subprocesos múltiples.

A los desarrolladores de C++ experimentados se les puede preguntar sobre diseño de sistemas, técnicas de optimización, gestión avanzada de memoria, estándares de C++ y su experiencia con proyectos a gran escala.

Las preguntas de la entrevista de C++ ayudan a evaluar la comprensión del candidato del lenguaje, sus habilidades para resolver problemas y su capacidad para aplicar los conceptos de C++ en escenarios del mundo real.

Las evaluaciones de habilidades, combinadas con preguntas de entrevista específicas, brindan una evaluación completa de la experiencia de un candidato en C++. Las evaluaciones de habilidades pueden ayudar a filtrar candidatos rápidamente, y la entrevista puede ayudar aún más a encontrar la opción adecuada.