Cuestiones Técnicas para Ingenieros de Software

  1. Problema de Combinatoria: Disposición Familiar

Imaginemos tres pares de padres e hijos. Si se paran en una fila, y cada padre e hijo de la misma familia no pueden estar adyacentes (es decir, el padre A no puede estar junto al hijo A, etc.), ¿cuántas disposiciones diferentes son posibles?

  1. 120
  2. 48
  3. 240
  4. 144

Respuesta: C.

Análisis de Solución:

Método 1: Construcción Paso a Paso

Sean los tres pares (P1, H1), (P2, H2), (P3, H3). En total, hay 6 personas.

  1. El primer puesto puede ser ocupado por cualquiera de las 6 personas.
  2. Para el segundo puesto, si el primero fue P1, el segundo no puede ser H1. Esto deja 4 opciones (P2, H2, P3, H3).
  3. A partir de aquí, la situación se ramifica:
    • **Caso A:** Si la tercera persona es el hijo del primero (por ejemplo, H1 después de P1), entonces las tres personas restantes deben cumplir las restricciones. Hay solo 2 formas de organizar los dos pares restantes para que los hijos no estén junto a sus padres (P2 H3 P3 H2 o P3 H2 P2 H3, y similarmente para las otras permutaciones de los dos pares restantes).
    • **Caso B:** Si la tercera persona NO es el hijo del primero (por ejemplo, P1 _ P2), entonces tenemos 2 opciones (P2 o P3). Luego, las 3 personas restantes se pueden organizar de 4 maneras para cumplir las restricciones.

Esto lleva a un cálculo de: 6 * 4 * (2 + 2 * 4) = 240.

Método 2: Principio de Inclusión-Exclusión

El número total de permutaciones de 6 personas es P(6,6) = 6! = 720.

  • Calcular el total de permutaciones - (una pareja adyacente) + (dos parejas adyacentes) - (tres parejas adyacentes).
  • Una pareja adyacente: Elegimos 1 de 3 parejas (C(3,1)). Consideramos la pareja como una unidad (2! formas internas). Las 5 "unidades" (la pareja + 4 individuos) se permutan de 5! formas. Total: C(3,1) * 2! * 5! = 3 * 2 * 120 = 720.
  • Dos parejas adyacentes: Elegimos 2 de 3 parejas (C(3,2)). Cada pareja es una unidad (2! * 2! formas internas). Las 4 "unidades" se permutan de 4! formas. Total: C(3,2) * (2!)^2 * 4! = 3 * 4 * 24 = 288.
  • Tres parejas adyacentes: Elegimos 3 de 3 parejas (C(3,3)). Cada pareja es una unidad ((2!)^3 formas internas). Las 3 "unidades" se permutan de 3! formas. Total: C(3,3) * (2!)^3 * 3! = 1 * 8 * 6 = 48.

Aplicando el principio: 720 - 720 + 288 - 48 = 240.

Método 3: Enfoque Modular

  1. Selecciona dos pares de familias de las tres disponibles: C(3,2) = 3 formas.
  2. Organiza estas cuatro personas de modo que ningún padre e hijo estén juntos. Hay 2 * 2 = 4 formas (ejemplo: P1 P2 H1 H2, P1 H2 P2 H1, etc., considerando permutaciones de pares y dentro de pares).
  3. La pareja restante (dos personas) se puede insertar en los 5 "espacios" entre las 4 personas ya colocadas. Esto es P(5,2) = 5 * 4 = 20 formas.

Total: 3 * 4 * 20 = 240.

  1. Errores en el Uso de Punteros 'const' en C++

Examine el siguiente código en C++ y determine qué líneas contienen errores de compilación:

int main()
{
   int valA = 10;
   int valB = 1;
   const int *ptrC1;          // (1)
   int const *ptrC2 = &valA;  // (2)
   ptrC2 = &valB;             // (3)
   int *const ptrC3 = &valA;  // (4)
   *ptrC3 = 20;               // (5)
   *ptrC2 = 30;               // (6)
   ptrC3 = &valB;             // (7)
   return 0;
}

  1. 1,2,3,4,5,6,7
  2. 1,3,5,6
  3. 6,7
  4. 3,5

Respuesta: C.

Análisis de Errores:

  • (1) const int *ptrC1;: Declara un puntero a un entero constante. El valor al que apunta ptrC1 no puede modificarse a través de ptrC1, pero ptrC1 puede apuntar a diferentes ubicaciones. Esta línea es correcta; el puntero no se inicializa, lo cual es una advertencia pero no un error de compilación.
  • (2) int const *ptrC2 = &valA;: Es equivalente a const int *ptrC2 = &valA;. Declara un puntero a un entero constante, inicializado con la dirección de valA. El valor al que apunta ptrC2 no puede modificarse a través de ptrC2, pero ptrC2 puede cambiar para apuntar a otra variable. Correcta.
  • (3) ptrC2 = &valB;: Modifica ptrC2 para que apunte a valB. Como ptrC2 es un puntero a un valor constante, pero no un puntero constante a sí mismo, su valor puede cambiarse. Correcta.
  • (4) int *const ptrC3 = &valA;: Declara un puntero constante a un entero no constante. Esto significa que ptrC3 siempre apuntará a la dirección de memoria de valA y no puede reasignarse a otra dirección. Sin embargo, el valor en la dirección a la que apunta (valA) sí puede modificarse a través de ptrC3. Correcta.
  • (5) *ptrC3 = 20;: Modifica el valor en la dirección a la que apunta ptrC3 (que es valA). Como valA no es constante, esta operación es válida. Correcta.
  • (6) *ptrC2 = 30;: Intenta modificar el valor en la dirección a la que apunta ptrC2 (que ahora es valB). Sin embargo, ptrC2 fue declarado como un puntero a un entero constante (const int *ptrC2), lo que impide modificar el valor apuntado a través de este puntero. ERROR.
  • (7) ptrC3 = &valB;: Intenta reasignar ptrC3 para que apunte a valB. Pero ptrC3 fue declarado como un puntero constante (int *const ptrC3), lo que significa que no puede ser reasignado después de su inicialización. ERROR.
  1. Aritmética de Punteros en C

¿Qué imprimirá el siguiente fragmento de código?

#include <stdio.h>

int main()
{
   int datos[5] = {1, 2, 3, 4, 5};
   int *p_final = (int *)(&datos + 1);
   printf("%d\n", *(p_final - 1));
   return 0;
}

  1. 1
  2. 2
  3. 5
  4. Se produce un error

Respuesta: C.

Análisis:

  • datos es un array de 5 enteros.
  • &amp;datos es un puntero al array completo de 5 enteros. Su tipo es int (\*)\[5\].
  • &amp;datos + 1 avanza el puntero &amp;datos en el tamaño de un array de 5 enteros. Esto lo posiciona *después* del último elemento del array datos. Es decir, &amp;datos + 1 apunta al byte inmediatamente después de datos\[4\].
  • (int \*)&amp;datos + 1) realiza un cast explícito a un puntero a entero (int \*). Ahora, p\_final es un int\* que apunta al inicio de la memoria *después* del array datos.
  • p\_final - 1 retrocede p\_final un sizeof(int). Dado que p\_final apuntaba al byte justo después de datos\[4\], p\_final - 1 ahora apunta al inicio de datos\[4\].
  • \*(p\_final - 1) desreferencia este puntero, obteniendo el valor de datos\[4\], que es 5.

Por lo tanto, la salida será 5.

  1. Polimorfismo y Funciones Virtuales en C++

Considere el siguiente código C++:

#include <cstdio>

struct Base {
  void metodoFoo() { printf("Base::foo\n"); }
  virtual void metodoBar() { printf("Base::bar\n"); }
  Base() { metodoBar(); }
};

struct Derivada : Base {
  void metodoFoo() { printf("Derivada::foo\n"); }
  void metodoBar() { printf("Derivada::bar\n"); }
};

int main() {
  Base *ptr = new Derivada();
  ptr->metodoFoo();
  ptr->metodoBar();
  delete ptr; // Liberar memoria
  return 0;
}

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

  1. Base::bar
    Base::foo
    Derivada::bar
  2. Base::foo
    Base::bar
    Derivada::bar
  3. Base::bar
    Derivada::foo
    Derivada::foo
  4. Base::foo
    Base::bar
    Base::foo

Respuesta: A.

Análisis:

  1. Base *ptr = new Derivada();:
    • Se crea un objeto de tipo Derivada.
    • Durante la construcción de Derivada, primero se llama al constructor de la clase base Base.
    • Dentro del constructor Base(), se llama a metodoBar(). En este punto, el objeto aún se está construyendo como Base (la parte Derivada aún no se ha inicializado). Las llamadas a funciones virtuales desde un constructor o destructor siempre resuelven a la implementación de la clase actual (Base en este caso), no a la de la clase derivada.
    • Por lo tanto, Base::metodoBar() es llamada, imprimiendo "Base::bar".
  2. ptr->metodoFoo();:
    • metodoFoo() NO es una función virtual.
    • Las llamadas a funciones no virtuales se resuelven estáticamente en tiempo de compilación basándose en el tipo del puntero (Base\*), no en el tipo real del objeto al que apunta (Derivada).
    • Por lo tanto, se llama a Base::metodoFoo(), imprimiendo "Base::foo".
  3. ptr->metodoBar();:
    • metodoBar() SÍ es una función virtual.
    • Las llamadas a funciones virtuales se resuelven dinámicamente en tiempo de ejecución basándose en el tipo real del objeto al que apunta (Derivada).
    • Por lo tanto, se llama a Derivada::metodoBar(), imprimiendo "Derivada::bar".

La salida combinada es: "Base::bar", "Base::foo", "Derivada::bar".

  1. Comandos Básicos de Linux

En un sistema Linux, para otorgar permisos de lectura, escritura y ejecución a todos los usuarios sobre un archivo llamado mi\_archivo, se usaría el comando: ____1____. Para cambiar el propietario de mi\_archivo a usuario\_dev y el grupo propietario a grupo\_ops, se usaría el comando: ____2____.

  1. chmod 776 mi_archivo, chown usuario_dev mi_archivo
  2. chmod 777 mi_archivo, chown grupo_ops mi_archivo
  3. chmod 777 mi_archivo, chown usuario_dev mi_archivo
  4. chmod 778 mi_archivo, chown grupo_ops mi_archivo

Respuesta: C.

Análisis:

  • Permisos (chmod):
    • El sistema de permisos numérico (octal) usa 3 dígitos: uno para el propietario, uno para el grupo y uno para "otros".
    • Cada dígito es la suma de: 4 (lectura), 2 (escritura), 1 (ejecución).
    • Para lectura, escritura y ejecución (rwx) para todos los usuarios, necesitamos 4+2+1=7 para cada categoría.
    • Por lo tanto, el comando es chmod 777 mi\_archivo.
  • Propietario (chown):
    • El comando chown se utiliza para cambiar el propietario de un archivo.
    • Su sintaxis básica es chown nuevo\_propietario archivo.
    • Para cambiar el grupo propietario, se usa chown propietario:grupo archivo o chgrp grupo archivo.
    • El problema pide cambiar el propietario a usuario\_dev, por lo que el comando es chown usuario\_dev mi\_archivo.
  1. Patrones de Diseño para Reducir el Uso de Recursos (Selección Múltiple)

¿Cuáles de los siguientes patrones de diseño contribuyen a reducir el consumo de recursos?

  1. Prototype
  2. Singleton
  3. Flyweight
  4. Abstract Factory

Respuesta: BC.

Análisis:

  • A. Prototype (Prototipo): Este patrón se enfoca en crear nuevos objetos clonando instancias existentes. Aunque evita la inicialización costosa de un objeto, sigue creando *nuevos* objetos. No reduce inherentemente el *número* total de objetos o el uso de recursos compartidos, sino el coste de *crearlos*.
  • B. Singleton (Instancia Única): Este patrón asegura que una clase tenga solo una instancia y proporciona un punto de acceso global a ella. Al garantizar una única instancia, se reduce significativamente el uso de memoria y otros recursos que múltiples instancias idénticas consumirían.
  • C. Flyweight (Peso Ligero): Este patrón se utiliza para minimizar el uso de memoria o el costo computacional compartiendo la mayor cantidad de datos posible entre múltiples objetos similares. Es ideal para situaciones donde hay un gran número de objetos que tienen mucha información en común, permitiendo que la "parte intrínseca" (compartida) de su estado sea compartida, mientras que la "parte extrínseca" (única) se mantiene por separado. Esto reduce drásticamente el número de objetos almacenados.
  • D. Abstract Factory (Fábrica Abstracta): Este patrón proporciona una interfaz para crear familias de objetos relacionados o dependientes sin especificar sus clases concretas. Su objetivo principal es desacoplar el código cliente de las implementaciones concretas de las familias de productos, no directamente reducir el consumo de recursos.
  1. Teoría de Grafos: Construcción de un Árbol

Dado un grafo completamente conectado con n vértices y m aristas, ¿cuántas aristas deben eliminarse al menos para formar un árbol?

  1. n-1
  2. m-1
  3. m-n+1
  4. m-n-1

Respuesta: C.

Análisis:

Un árbol con n vértices siempre tiene exactamente n-1 aristas. Si tenemos un grafo con m aristas, y queremos transformarlo en un árbol (que tendrá n-1 aristas), la cantidad de aristas que debemos eliminar es la diferencia entre las aristas existentes y las que debe tener un árbol.

Número de aristas a eliminar = m - (n-1) = m - n + 1.

  1. Algoritmo de Búsqueda Binaria

En la secuencia ordenada (22, 34, 55, 77, 89, 93, 99, 102, 120, 140), ¿cuántas comparaciones son necesarias para encontrar los elementos 77, 34 y 99 respectivamente, utilizando una búsqueda binaria?

  1. 3, 3, 3
  2. 3, 3, 4
  3. 3, 4, 3
  4. 4, 2, 4

Respuesta: D.

Aálisis:

La secuencia es arr = [22, 34, 55, 77, 89, 93, 99, 102, 120, 140]. Los índices son de 0 a 9.

Búsqueda de 77:

  • Paso 1: low = 0, high = 9. mid = (0+9)/2 = 4. arr[4] = 89. 89 > 77, entonces high = mid - 1 = 3. (1 comparación)
  • Paso 2: low = 0, high = 3. mid = (0+3)/2 = 1. arr[1] = 34. 34 < 77, entonces low = mid + 1 = 2. (2 comparaciones)
  • Paso 3: low = 2, high = 3. mid = (2+3)/2 = 2. arr[2] = 55. 55 < 77, entonces low = mid + 1 = 3. (3 comparaciones)
  • Paso 4: low = 3, high = 3. mid = (3+3)/2 = 3. arr[3] = 77. ¡Encontrado! (4 comparaciones)

77 encontrado en 4 comparaciones.

Búsqueda de 34:

  • Paso 1: low = 0, high = 9. mid = 4. arr[4] = 89. 89 > 34, entonces high = 3. (1 comparación)
  • Paso 2: low = 0, high = 3. mid = 1. arr[1] = 34. ¡Encontrado! (2 comparaciones)

34 encontrado en 2 comparaciones.

Búsqueda de 99:

  • Paso 1: low = 0, high = 9. mid = 4. arr[4] = 89. 89 < 99, entonces low = mid + 1 = 5. (1 comparación)
  • Paso 2: low = 5, high = 9. mid = (5+9)/2 = 7. arr[7] = 102. 102 > 99, entonces high = mid - 1 = 6. (2 comparaciones)
  • Paso 3: low = 5, high = 6. mid = (5+6)/2 = 5. arr[5] = 93. 93 < 99, entonces low = mid + 1 = 6. (3 comparaciones)
  • Paso 4: low = 6, high = 6. mid = (6+6)/2 = 6. arr[6] = 99. ¡Encontrado! (4 comparaciones)

99 encontrado en 4 comparaciones.

Las búsquedas requieren 4, 2 y 4 comparaciones respectivamente.

  1. Agregación de Segmentos de Red IP

Dados los segmentos de red IP 10.1.8.0/24 y 10.1.9.0/24, ¿cuál de los siguientes es el segmento de red agregado correcto?

  1. 10.0.0.0/8
  2. 10.1.0.0/16
  3. 10.1.8.0/23
  4. 10.1.10.0/24

Respuesta: C.

Análisis:

Para agregar segmentos de red, necesitamos encontrar el prefijo de red más largo común a ambos. Convertimos el tercer octeto a binario:

  • 10.1.8.0/24:
    • 8 en binario es 00001000.
    • Dirección completa: 10.1.00001000.0/24
  • 10.1.9.0/24:
    • 9 en binario es 00001001.
    • Dirección completa: 10.1.00001001.0/24

Comparamos los bits de las direcciones, comenzando desde la izquierda:


10.1.00001000.0
10.1.00001001.0

Los primeros 16 bits (10.1.) son idénticos. En el tercer octeto, los primeros 7 bits (0000100) son idénticos. El octavo bit es diferente (0 vs 1).

El prefijo común más largo es de 16 (para 10.1.) + 7 (para 0000100) = 23 bits.

La dirección de red agregada se forma tomando el prefijo común y poniendo a cero el resto de los bits del host. 10.1.00001000.0/23, donde 00001000 es 8 en decimal.

Por lo tanto, la red agregada es 10.1.8.0/23.

  1. Constructor de Copia y Gestión de Memoria en C++

¿Es el siguiente código completamente correcto? ¿Qué resultado probable podría tener?

#include <cstdio> // Para printf, aunque no se usa directamente

class Recurso {
public:
   int valor;
   Recurso() : valor(0) {}
};

class Gestor {
private:
   Recurso *p_rec;
public:
   Gestor() { p_rec = new Recurso(); }
   ~Gestor() { delete p_rec; }
};

void procesarGestor(Gestor g_param) {
   // g_param es una copia de 'g_original'
   // Cuando g_param sale de ámbito, su destructor es llamado
}

int main() {
   Gestor g_original; // Se crea una instancia de Gestor
   procesarGestor(g_original); // Se pasa g_original por valor, lo que implica una copia
   // Cuando g_original sale de ámbito, su destructor es llamado
   return 0;
}

  1. El programa se ejecuta normalmente.
  2. Error de compilación.
  3. El programa colapsa (crash).
  4. El programa entra en un bucle infinito.

Respuesta: C.

Análisis:

Este código tiene un problema grave de gestión de memoria que lleva a un "doble free" (doble liberación de memoria), lo que causa un colapso del programa.

Cuando se pasa g\_original a procesarGestor por valor (procesarGestor(g\_original)), el compilador genera automáticamente un constructor de copia por defecto. Este constructor de copia por defecto realiza una "copia superficial" (bitwise copy).

Esto significa:

  1. g\_original se crea, y su miembro p\_rec apunta a una instancia de Recurso recién asignada en el heap.
  2. Al llamar a procesarGestor(g\_original), el constructor de copia implícito de Gestor se invoca para crear g\_param. Este constructor copia el valor del puntero p\_rec de g\_original a g\_param. Ahora, g\_original.p\_rec y g\_param.p\_rec apuntan A LA MISMA INSTANCIA DE Recurso en el heap.
  3. Cuando procesarGestor termina, g\_param sale de ámbito y su destructor (~Gestor()) es llamado. Este destructor ejecuta delete p\_rec;, liberando la memoria a la que apunta p\_rec (la instancia de Recurso).
  4. Cuando main termina, g\_original sale de ámbito y su destructor (~Gestor()) es llamado. Este destructor también ejecuta delete p\_rec;, intentando liberar la misma memoria que ya fue liberada por el destructor de g\_param. Esto es una "doble liberación" y generalmente resulta en un colapso del programa o un comportamiento indefinido.

Solución: Para corregir este problema, se debe implementar un "constructor de copia profundo" para la clase Gestor, siguiendo la Regla de los Tres (o Cinco). Un constructor de copia profundo asignaría una nueva instancia de Recurso y copiaría el contenido, en lugar de solo el puntero.

Código corregido con un constructor de copia:

#include <cstdio>

class Recurso {
public:
   int valor;
   Recurso() : valor(0) {}
   // Constructor de copia para Recurso
   Recurso(const Recurso& otro) : valor(otro.valor) {}
};

class Gestor {
private:
   Recurso *p_rec;
public:
   Gestor() { p_rec = new Recurso(); }
   ~Gestor() { delete p_rec; }

   // Constructor de Copia Profundo
   Gestor(const Gestor& otro) {
       p_rec = new Recurso(*(otro.p_rec)); // Crea un nuevo Recurso y copia su contenido
   }

   // Operador de Asignación (para la Regla de los Tres/Cinco, no es relevante para este ejemplo de crash)
   // Gestor& operator=(const Gestor& otro) {
   //     if (this != &otro) {
   //         delete p_rec;
   //         p_rec = new Recurso(*(otro.p_rec));
   //     }
   //     return *this;
   // }
};

void procesarGestor(Gestor g_param) {
   // g_param ahora es una copia independiente de g_original
}

int main() {
   Gestor g_original;
   procesarGestor(g_original); // Llama al constructor de copia
   return 0;
}

  1. Características de las Interfaces en C++ (Selección Múltiple)

En el contexto de la programación orientada a objetos en C++, ¿cuáles de las siguientes afirmaciones son incorrectas respecto a las interfaces (clases puramente abstractas)?

  1. Una interfaz puede contener métodos virtuales no puros.
  2. Una clase puede implementar múltiples interfaces.
  3. Una interfaz no puede ser instanciada directamente.
  4. Una interfaz puede contener métodos que ya tienen una implementación.

Respuesta: AD.

Análisis:

  • A. Una interfaz puede contener métodos virtuales no puros. (INCORRECTA): En C++, una "interfaz" se conceptualiza como una clase abstracta que contiene **exclusivamente funciones virtuales puras** (declaradas con = 0). Si tuviera métodos virtuales no puros, ya no sería una interfaz en el sentido estricto, sino una clase abstracta ordinaria.
  • B. Una clase puede implementar múltiples interfaces. (CORRECTA): Una clase en C++ puede heredar de múltiples clases puramente abstractas (interfaces), implementando todos sus métodos virtuales puros. Esta es la forma en que C++ logra la "herencia múltiple de interfaz".
  • C. Una interfaz no puede ser instanciada directamente. (CORRECTA): Dado que una interfaz (una clase puramente abstracta) contiene al menos una función virtual pura, no se pueden crear objetos directamente de ella. Solo se pueden crear instancias de clases concretas que hereden de la interfaz e implementen todos sus métodos virtuales puros.
  • D. Una interfaz puede contener métodos que ya tienen una implementación. (INCORRECTA): Una interfaz en C++ se define por sus funciones virtuales puras, que carecen de implementación en la clase base. Si una interfaz contuviera métodos con implementación, dejaría de ser una interfaz "pura" y se convertiría en una clase abstracta con funciones normales o virtuales con implementación predeterminada.
  1. Afirmaciones sobre el Protocolo HTTP (Selección Múltiple)

¿Cuáles de las siguientes afirmaciones sobre el protocolo HTTP son correctas?

  1. HTTP es un protocolo de capa de aplicación basado en TCP.
  2. HTTP es un protocolo de flujo binario que se utiliza comúnmente entre navegadores y servidores web.
  3. El encabezado de respuesta ETag de HTTP se utiliza principalmente para la validación de la expiración de la información.
  4. El encabezado de respuesta Cache-Control en HTTP 1.0 se utiliza principalmente para controlar el almacenamiento en caché en el navegador.

Respuesta: AC.

Análisis:

  • A. HTTP es un protocolo de capa de aplicación basado en TCP. (CORRECTA): HTTP (Hypertext Transfer Protocol) opera en la capa de aplicación del modelo OSI/TCP/IP y utiliza TCP para establecer conexiones confiables y ordenadas entre el cliente y el servidor.
  • B. HTTP es un protocolo de flujo binario que se utiliza comúnmente entre navegadores y servidores web. (INCORRECTA): HTTP es un protocolo basado en texto (un protocolo de "flujo de mensajes"), no binario. Sus mensajes son legibles por humanos, consisten en encabezados y, opcionalmente, un cuerpo.
  • C. El encabezado de respuesta ETag de HTTP se utiliza principalmente para la validación de la expiración de la información. (CORRECTA): ETag (Entity Tag) es un encabezado de respuesta HTTP utilizado para la validación de caché. Permite que el cliente envíe el ETag previamente recibido al servidor para verificar si el recurso ha cambiado. Si no ha cambiado, el servidor puede responder con un 304 Not Modified, ahorrando ancho de banda. No es directamente para la "expiración", sino para la "validación de frescura".
  • D. El encabezado de respuesta Cache-Control en HTTP 1.0 se utiliza principalmente para controlar el almacenamiento en caché en el navegador. (INCORRECTA): El encabezado Cache-Control fue introducido en HTTP 1.1 para ofrecer un control más granular sobre el comportamiento del caché. En HTTP 1.0, se utilizaba principalmente el encabezado Expires.
  1. Programación Multihilo vs. Multiproceso (Selección Múltiple)

Respecto a la programación multiproceso y multihilo, ¿cuáles de las siguientes afirmaciones son correctas?

  1. En un entorno multiproceso, los procesos hijos obtienen una copia de todos los datos del heap y la pila del proceso padre; mientras que los hilos comparten datos con otros hilos del mismo proceso, pero tienen su propio espacio de pila.
  2. Debido a que los hilos tienen su propio espacio de pila independiente y comparten datos, su sobrecarga de ejecución es relativamente grande, y son menos adecuados para la gestión y protección de recursos.
  3. La comunicación entre hilos es más rápida y el cambio de contexto entre ellos es más rápido, ya que residen en el mismo espacio de direcciones.
  4. Al utilizar variables/memoria públicas, los hilos requieren mecanismos de sincronización, ya que operan en el mismo espacio de direcciones.
  5. En un entorno multihilo, cada subproceso tiene su propio espacio de direcciones, por lo tanto, en la comunicación mutua, los hilos son menos flexibles y convenientes que los procesos.

Respuesta: ACD.

Análisis:

  • A. En un entorno multiproceso, los procesos hijos obtienen una copia de todos los datos del heap y la pila del proceso padre; mientras que los hilos comparten datos con otros hilos del mismo proceso, pero tienen su propio espacio de pila. (CORRECTA): Cuando se crea un proceso hijo, se realiza una copia (o se usa copy-on-write para optimizar) del espacio de direcciones del padre (incluyendo heap y pila). Los hilos, por otro lado, comparten el mismo espacio de direcciones (heap, código y datos globales) de su proceso padre, pero cada hilo tiene su propia pila, su propio contador de programa y sus propios registros.
  • B. Debido a que los hilos tienen su propio espacio de pila independiente y comparten datos, su sobrecarga de ejecución es relativamente grande, y son menos adecuados para la gestión y protección de recursos. (INCORRECTA): La sobrecarga de ejecución (creación, cambio de contexto) de los hilos es significativamente *menor* que la de los procesos, precisamente porque comparten el mismo espacio de direcciones. Sin embargo, precisamente por compartir datos, la gestión y protección de recursos (sincronización) es más compleja y requiere más cuidado para evitar condiciones de carrera.
  • C. La comunicación entre hilos es más rápida y el cambio de contexto entre ellos es más rápido, ya que residen en el mismo espacio de direcciones. (CORRECTA): La comunicación entre hilos es más sencilla y eficiente (por ejemplo, a través de memoria compartida directamente) y el cambio de contexto es más rápido que entre procesos, ya que el sistema operativo no necesita cambiar el espacio de direcciones de memoria virtual.
  • D. Al utilizar variables/memoria públicas, los hilos requieren mecanismos de sincronización, ya que operan en el mismo espacio de direcciones. (CORRRECTA): Dado que los hilos comparten la memoria, el acceso concurrente a datos compartidos (variables globales, heap) sin mecanismos de sincronización adecuados (como mutexes, semáforos, bloqueos) puede llevar a condiciones de carrera y resultados incorrectos.
  • E. En un entorno multihilo, cada subproceso tiene su propio espacio de direcciones, por lo tanto, en la comunicación mutua, los hilos son menos flexibles y convenientes que los procesos. (INCORRECTA): Esta afirmación es incorrecta en su premisa. Los hilos *no* tienen su propio espacio de direcciones; comparten el espacio de direcciones del proceso padre. Debido a que comparten el espacio de direcciones, la comunicación entre hilos es generalmente *más* flexible y conveniente (a través de memoria compartida) que entre procesos (que requieren mecanismos IPC como pipes, sockets, shared memory explícita).

Etiquetas: C++ Pointers combinatorics linux HTTP

Publicado el 6-12 03:28