Preguntas comunes de entrevistas en lenguaje C
Preguntas frecuentes en entrevistas en lenguaje C
Preprocesador
1. Utilice la directiva de preprocesamiento #define para declarar una constante que indique la época del año. ¿Cuántos segundos? (ignorando los problemas de los años bisiestos)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
Algunas cosas que quiero ver aquí:
1 ) Conocimiento básico de la sintaxis #define (por ejemplo, no terminar con punto y coma, uso de paréntesis, etc.)
2) Comprenda que el preprocesador evaluará la expresión constante por usted, por lo que es más limpio y costoso. -Eficaz escribir directamente cómo se calcula el número de segundos en un año en lugar de calcular el valor real.
3) Tenga en cuenta que esta expresión desbordará un número entero en una máquina de 16 bits, por lo que el símbolo de entero largo L se utiliza para indicarle al compilador que la constante es un número entero largo.
4) Si usa UL (por largo sin firmar) en sus expresiones, entonces tiene un buen punto de partida. Recuerde, las primeras impresiones cuentan.
2. Escribe una macro MIN "estándar", que toma dos parámetros y devuelve el más pequeño.
#define MIN(A,B) ((A) <= (B)? (A): (B))
Esta prueba está diseñada para los siguientes propósitos:
1) Identificar los conocimientos básicos del uso de #define en macros. Esto es muy importante. Porque antes de que el operador en línea se convirtiera en parte del estándar C, las macros eran la única forma de generar código en línea de manera conveniente. Para los sistemas integrados, el código en línea es a menudo un método necesario para lograr el rendimiento requerido.
2) Conocimiento de operadores condicionales triples. La razón por la que este operador existe en el lenguaje C es que permite al compilador producir código más optimizado que if-then-else. Es importante comprender su uso.
3) Sepa cómo rodear cuidadosamente los parámetros en macros entre paréntesis.
4) También uso esta pregunta para comenzar a discutir los efectos secundarios de las macros, por ejemplo: cuando escribe lo siguiente código ¿qué pasa?
less = MIN(*p++, b);
3. ¿Cuál es el propósito del indicador #error del preprocesador?
Si no sabes la respuesta, consulta la referencia 1. Esta pregunta es útil para distinguir a un chico normal de un nerd. Sólo un nerd leería el apéndice de un libro de texto en lenguaje C para encontrar la respuesta a una pregunta como ésta. Por supuesto, si no estás buscando un nerd, es mejor que el candidato no sepa la respuesta.
Bucles infinitos
4. Los bucles infinitos se utilizan a menudo en sistemas integrados. ¿Cómo se escriben bucles infinitos en C?
Este problema tiene varias soluciones. Mi solución preferida es:
while(1)
{
}
Algunos programadores prefieren la siguiente solución:
p>
for(;;)
{
}
Esta implementación me da vergüenza, porque esta sintaxis no expresa exactamente lo que está pasando. Si un candidato ofrece esto como solución, lo aprovecharía como una oportunidad para explorar las razones por las que lo hace. Si su respuesta básica es: "Me enseñaron a hacer esto pero nunca pensé por qué", eso me deja una mala impresión.
La tercera opción es usar goto
Loop:
...
goto Loop;
Si el candidato da la solución anterior, significa que es un programador en lenguaje ensamblador (lo cual puede ser algo bueno) o es un programador BASIC/FORTRAN que quiere ingresar a un nuevo campo.
Declaraciones de datos
5. Utilice la variable a para dar la siguiente definición
a) Un número entero
b) Un puntero a un entero (Un puntero a un número entero)
c) Un puntero a un puntero, el puntero al que apunta es un puntero a un número entero (Un puntero a un puntero a un entero)r
d) Una matriz de 10 números enteros
e) Una matriz de 10 punteros, el puntero A apunta a un número entero. (Una matriz de 10 punteros a números enteros)
f) Un puntero a una matriz de 10 enteros (Un puntero a una matriz de 10 enteros)
g) Un puntero Un puntero a una función que toma un número entero como argumento y devuelve un número entero (Un puntero a una función que toma un número entero como argumento y devuelve un número entero)
h) Un puntero con 10 punteros Una matriz de diez punteros a funciones que toman un argumento entero y devuelven un número entero (Una matriz de diez punteros a funciones que toman un argumento entero y devuelven un número entero)
La respuesta es:
a) int a; // Un número entero
b) int *a; // Un puntero a un número entero
c) int **a / / Un puntero a un número entero
d) int a[10]; // Una matriz de 10 números enteros
e) int *a[10] // Una matriz de 10 punteros a números enteros
f) int (*a)[10]; // Un puntero a una matriz de 10 enteros
g) int (*a)(int ); a que toma un argumento entero y devuelve un número entero
h) int (*a[10])(int // Una matriz de 10 punteros a funciones que toman un argumento entero y devuelven un número entero <); /p>
La gente suele afirmar que varias de las preguntas aquí son del tipo que requieren hojear un libro para responderse, y estoy de acuerdo. Cuando escribí este artículo, revisé el libro para asegurarme de que la gramática fuera correcta. Pero cuando me entrevistan, espero que me hagan esta pregunta (o algo parecido). Porque durante el tiempo que me entrevistaron, me aseguré de saber la respuesta a esta pregunta. Si el candidato no sabe todas las respuestas (o al menos la mayoría de las respuestas), entonces no está preparado para la entrevista. Si el candidato no está preparado para la entrevista, ¿por qué debería estarlo?
Estático
6. ¿Cuál es la función de la palabra clave estática?
Pocas personas pueden responder completamente a esta sencilla pregunta.
En lenguaje C, la palabra clave static tiene tres funciones obvias:
1) En el cuerpo de la función, una variable declarada como estática mantiene su valor mientras se llama a la función.
2) Dentro del módulo (pero fuera del cuerpo de la función), las funciones utilizadas dentro del módulo pueden acceder a una variable declarada como estática, pero otras funciones fuera del módulo no pueden acceder a ella. Es una variable global local.
3) Dentro de un módulo, una función declarada como estática solo puede ser llamada por otras funciones en este módulo. Es decir, la función está restringida al ámbito local del módulo en el que se declara.
La mayoría de los examinados pueden responder correctamente la primera parte, algunos pueden responder correctamente la segunda parte y muy pocas personas pueden entender la tercera parte. Esta es una grave deficiencia de un candidato que claramente no comprende los beneficios y la importancia de los datos localizados y los alcances del código.
Const
7. ¿Qué significa la palabra clave constante?
Tan pronto como escucho a un entrevistado decir "const significa constante", sé que estoy tratando con un aficionado. Dan Saks resumió completamente todos los usos de const en su artículo del año pasado, por lo que todos los lectores de ESP (Traductor: Programación de sistemas integrados) deberían estar muy familiarizados con lo que const puede y no puede hacer. Si nunca ha leído este artículo, siempre. como se puede decir que const significa "solo lectura", estará bien. Aunque esta respuesta no es la respuesta completa, la acepto como una respuesta correcta. (Si desea conocer una respuesta más detallada, lea atentamente el artículo de Saks).
Si el candidato puede responder esta pregunta correctamente, le haré una pregunta adicional:
¿Qué ¿Qué significan las siguientes afirmaciones?
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
/******/
Los dos primeros tienen el mismo efecto, a es un entero constante. El tercero significa que a es un puntero a un número entero constante (es decir, los números enteros no se pueden modificar, pero los punteros sí). El cuarto significado a es un puntero constante que apunta a un número entero (es decir, el número entero al que apunta el puntero se puede modificar, pero el puntero no se puede modificar). El último significa que a es un puntero constante a un número entero constante (es decir, el número entero al que apunta el puntero no se puede modificar y el puntero tampoco se puede modificar). Si el candidato puede responder correctamente a estas preguntas, entonces me ha dejado una buena impresión. Por cierto, puede preguntar, incluso si no usa la palabra clave const, aún puede escribir fácilmente un programa con las funciones correctas, entonces, ¿por qué sigo valorando tanto la palabra clave const? Tengo las siguientes razones:
1) La función de la palabra clave const es transmitir información muy útil a las personas que leen su código. De hecho, declarar un parámetro como constante es decirle al usuario esto. Propósito de la aplicación del parámetro. Si alguna vez ha pasado mucho tiempo limpiando la basura de otras personas, rápidamente aprenderá a apreciar esta pequeña información adicional. (Por supuesto, los programadores que saben cómo usar const rara vez dejan basura para que otros la limpien).
2) Al brindarle al optimizador información adicional, el uso de la palabra clave const puede producir un código más compacto.
3) El uso razonable de la palabra clave const permite al compilador proteger de forma natural los parámetros que no se espera que cambien y evitar que sean modificados por código no intencionado. En resumen, esto reduce la aparición de errores.
Volatile
8. ¿Qué significa la palabra clave volatile? Y da tres ejemplos diferentes.
Una variable definida como volátil significa que la variable puede cambiarse inesperadamente, de modo que el compilador no hará suposiciones sobre el valor de la variable. Para ser precisos, el optimizador debe volver a leer cuidadosamente el valor de esta variable cada vez que la usa, en lugar de utilizar la copia de seguridad almacenada en el registro.
Los siguientes son varios ejemplos de variables volátiles:
1) Registros de hardware de dispositivos paralelos (como registros de estado)
2) Variables no automáticas a las que se accede en una rutina de servicio de interrupción Variables ( Variables no automáticas)
3) Variables compartidas por varias tareas en aplicaciones multiproceso
No se contratarán personas que no puedan responder a esta pregunta. Creo que esta es la pregunta más fundamental que diferencia a los programadores de C de los programadores de sistemas integrados. Los chicos integrados están constantemente lidiando con hardware, interrupciones, RTOS, etc., todo lo cual requiere el uso de variables volátiles. No comprender el contenido volátil conducirá al desastre.
Suponiendo que el entrevistado respondió correctamente a esta pregunta (bueno, dudo que ese sea el caso), profundizaría un poco más para ver si este tipo realmente entiende la importancia de lo volátil.
1) ¿Puede un parámetro ser a la vez constante y volátil? Explique por qué.
2); ¿Puede un puntero ser volátil? Explique por qué.
3); ¿Qué hay de malo en la siguiente función:
int square(volatile int *ptr)
{
return * ptr * *ptr;
}
Aquí están las respuestas:
1) Sí. Un ejemplo es el registro de estado de sólo lectura. Es volátil porque puede cambiarse inesperadamente. Es constante porque los programas no deberían intentar modificarlo.
2); Sí. Aunque esto no es muy común. Un ejemplo es cuando una rutina de servicio modifica un puntero a un búfer.
3) Este código es un poco anormal. El propósito de este código es devolver el cuadrado del valor señalado por el puntero *ptr. Sin embargo, dado que *ptr apunta a un parámetro volátil, el compilador generará un código similar al siguiente:
int. cuadrado(volatile int * ptr)
{
int a,b;
a = *ptr;
b = *ptr ;
devuelve a * b;
}
Debido a que el valor de *ptr puede cambiar inesperadamente, a y b pueden ser diferentes. Como resultado, es posible que este código no devuelva el valor cuadrado que esperaba. El código correcto es el siguiente:
long square(volatile int *ptr)
{
int a;
a = * ptr;
return a * a;
}
Manipulación de bits
9. Los sistemas integrados siempre requieren que los usuarios realicen operaciones de bits en variables o registros. Dada una variable entera a, escriba dos fragmentos de código, el primero establece el bit 3 de a y el segundo borra el bit 3 de a. En las dos operaciones anteriores, mantenga los demás bits sin cambios.
Hay tres reacciones básicas ante este problema
1) No sé cómo empezar. La persona entrevistada nunca ha realizado ningún trabajo en sistemas integrados.
2) Utilizar campos de bits. Los campos de bits son algo que se ha colocado en el centro del lenguaje C. Garantiza que su código no sea portátil entre diferentes compiladores y también garantiza que su código no sea reutilizable. Recientemente tuve la desgracia de encontrarme con un controlador escrito por Infineon para uno de sus chips de comunicaciones más complejos, que hacía uso de campos de bits y, por lo tanto, era completamente inútil para mí porque mi compilador implementaba los campos de bits de otras maneras. Éticamente: nunca permita que un tipo no integrado se quede al lado del hardware real.
3) Operar con #defines y máscaras de bits. Este es un método extremadamente portátil y es el método que debe usarse.
La mejor solución es la siguiente:
#define BIT3 (0x1 << 3)
static int a;
void set_bit3(void)
{
a |= BIT3;
}
void clear_bit3(void)
{
a &= ~BIT3;
}
A algunas personas les gusta definir una máscara y algunas constantes de descripción para configurar y borrar valores, y esto también es aceptable. Me gustaría ver algunos puntos: Explicación de constantes, operaciones |= y &=~.
Acceder a ubicaciones de memoria fijas (Acceder a ubicaciones de memoria fijas)
10. Los sistemas integrados suelen tener características que requieren que los programadores accedan a una ubicación de memoria específica. En un proyecto determinado, es necesario establecer el valor de una variable entera con una dirección absoluta de 0x67a9 a 0xaa66. El compilador es un compilador ANSI puro. Escriba código para realizar esta tarea.
Esta pregunta prueba si sabes que es legal encasillar un número entero en un puntero para acceder a una dirección absoluta. La forma en que se logra esto varía según el estilo personal. Un código similar típico es el siguiente:
int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;
Un enfoque más oscuro es:
Un enfoque más oscuro es:
*(int * const)(0x67a9) = 0xaa55;
Incluso si su El gusto se acerca más a la segunda opción, pero te recomiendo utilizar la primera opción durante las entrevistas.
Interrupciones
11. Las interrupciones son una parte importante de los sistemas integrados, lo que ha llevado a muchos desarrolladores de compiladores a proporcionar una extensión para permitir que el estándar C admita interrupciones. El hecho más representativo es que se genera una nueva palabra clave __interrupt. El siguiente código utiliza la palabra clave __interrupt para definir una rutina de servicio de interrupción (ISR). Comente este código.
__interrupt doble área_cómputo (doble radio)
{
área doble = PI * radio * radio;
printf("\nÁrea = %f", area);
return area;
}
Hay tantos errores en esta función que la gente no sabe por dónde empezar . Fuera de:
1)ISR no puede devolver un valor. Si no entiende esto, no será contratado.
2) ISR no puede pasar parámetros. Si no ve esto, sus posibilidades de ser contratado son iguales a las del primero.
3) En muchos procesadores/compiladores, el punto flotante generalmente no es reentrante. Algunos procesadores/compiladores necesitan empujar el registro al frente de la pila, y algunos procesadores/compiladores simplemente no permiten operaciones de punto flotante en el ISR. Además, el ISR debe ser breve y eficiente; no es aconsejable realizar operaciones de punto flotante en el ISR.
4) De acuerdo con el tercer punto, printf() a menudo tiene problemas de reentrada y rendimiento. Si pierdes el tercer y cuarto punto, no te lo pondré demasiado difícil. No hace falta decir que si puede obtener los dos últimos puntos, sus perspectivas de empleo serán mejores.
Ejemplos de código
12. ¿Cuál es el resultado del siguiente código y por qué?
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a+b > 6) ? puts("> 6") : puts("<= 6");
}
Esta pregunta prueba si entiendes C Con respecto a los principios de conversión automática de enteros en el lenguaje, descubrí que algunos desarrolladores saben muy poco sobre estas cosas. De todos modos, la respuesta al problema de los enteros sin signo es que la salida es ">6". La razón es que todos los operandos se convierten automáticamente a tipos sin signo cuando hay tipos con y sin signo en la expresión. Entonces -20 se convierte en un entero positivo muy grande, por lo que la expresión se evalúa como mayor que 6. Esto es muy importante para los sistemas integrados que deberían utilizar con frecuencia tipos de datos sin firmar. Si responde mal a esta pregunta, está a punto de no conseguir el trabajo.
13. Evalúe el siguiente fragmento de código:
unsigned int zero = 0
unsigned int compzero = 0xFFFF; Complemento a 1 de cero */
Para un procesador cuyo tipo int no es de 16 bits, el código anterior es incorrecto. Debe escribirse de la siguiente manera:
unsigned int compzero = ~0;
Esta pregunta realmente puede revelar si el candidato comprende la importancia del tamaño de palabra del procesador. En mi experiencia, los buenos programadores integrados entienden los detalles del hardware y sus limitaciones con mucha precisión, mientras que los programadores de PC a menudo tratan el hardware como una molestia inevitable.
En esta etapa, el candidato está completamente abatido o lleno de confianza y está decidido a ganar. Si es obvio que el examinado no fue muy bueno, aquí es donde termina la prueba. Pero si está claro que al candidato le fue bien, entonces agrego las siguientes preguntas adicionales. Estas son preguntas más difíciles y creo que sólo a los muy buenos candidatos les irá bien. Al hacer estas preguntas, espero ver más el enfoque del candidato hacia el problema que la respuesta. Pase lo que pase, considérelo como entretenimiento...
Asignación de memoria dinámica (Asignación de memoria dinámica)
14 Aunque no es tan común como las computadoras no integradas, los sistemas integrados existen. Sigue siendo un proceso de asignación dinámica de memoria desde el montón. Entonces, ¿cuáles son los posibles problemas que pueden ocurrir al asignar memoria dinámicamente en sistemas integrados?
Aquí espero que los candidatos mencionen la fragmentación de la memoria, problemas de recolección de desechos, duración variable, etc. Este tema ha sido cubierto extensamente en revistas ESP (principalmente por P.J. Plauger, quien lo explica mucho mejor que cualquier cosa que pueda mencionar aquí), ¡así que regresa y mira esas revistas! Después de dejar que los candidatos tuvieran una falsa sensación de seguridad, se me ocurrió este pequeño programa:
¿Cuál es el resultado del siguiente fragmento de código y por qué?
char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
puts("Tengo un puntero nulo ");
else
puts("Tengo un puntero válido");
Esta es una pregunta interesante. Solo pensé en este problema recientemente después de que uno de mis colegas sin darse cuenta pasó un valor 0 a la función malloc y obtuvo un puntero legal. Este es el código anterior, el resultado de este código es "Tengo un puntero válido". Utilizo esto para iniciar una discusión como esta para ver si el entrevistado piensa que la rutina de la biblioteca lo hace correctamente. Obtener la respuesta correcta es importante, pero el enfoque para resolver el problema y el fundamento de su decisión son aún más importantes.
Typedef
15 Typedef se utiliza con frecuencia en lenguaje C para declarar un sinónimo de un tipo de datos existente. También puedes usar un preprocesador para hacer algo similar. Por ejemplo, considere el siguiente ejemplo:
#define dPS struct s *
typedef struct s * tPS;
La intención en ambos casos es Definir dPS y tPS como punteros a la estructura s. ¿Qué método es mejor? (Si es así) ¿Por qué?
Esta es una pregunta muy delicada, y cualquiera que la responda correctamente (por las razones correctas) debería ser felicitado. La respuesta es: typedef es mejor. Considere el siguiente ejemplo:
dPS p1,p2;
tPS p3,p4;
El primero se expande a
struct s * p1, p2;
.
El código anterior define p1 como un puntero a una estructura y p2 como una estructura real. Puede que esto no sea lo que desea. El segundo ejemplo define correctamente los dos punteros p3 y p4.
Sintaxis oscura
16. El lenguaje C permite algunas estructuras impactantes ¿Es legal la siguiente estructura y, de ser así, qué hace?
int a = 5, b = 7, c;
c = a+++b;
Esta pregunta servirá como una parte divertida de este cuestionario al final de. Lo creas o no, el ejemplo anterior es perfectamente gramatical. La pregunta es ¿cómo lo maneja el compilador? Los autores de compiladores menos avanzados en realidad discutirán este tema y, de acuerdo con el principio de procesamiento mínimo, el compilador debería poder manejar todos los usos legales tanto como sea posible. Por lo tanto, el código anterior se procesa como:
c = a++ + b;
Por lo tanto, después de ejecutar este código, a = 6, b = 7, c = 12.
Si sabes la respuesta o adivinaste la respuesta correcta, bien hecho. Si no sabes la respuesta, no lo tomo como una pregunta. Lo mejor que encontré sobre esta pregunta es que es un buen tema sobre estilo de codificación, legibilidad y modificabilidad del código.