Preguntas de prueba escritas clásicas del ingeniero de software integrado

1. Use la directiva de preprocesamiento #define para declarar una constante para indicar cuántos segundos hay en un año (ignorando el problema del año bisiesto)

Respuesta: La parte principal propensa a errores de esta pregunta es: darse cuenta de que esta expresión desbordará un número entero de una máquina de 16 bits, por lo que el símbolo de número entero largo L se usa para decirle al compilador que esta constante es un número entero largo.

 #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL

2. Escriba una macro MIN "estándar" que tome dos argumentos y devuelva el más pequeño.

Respuesta: La principal parte propensa a errores de esta pregunta es: sepa cómo encerrar cuidadosamente los parámetros entre paréntesis en la macro.

#define MIN(A,B) ((A)<=(B)?(A):(B)) 

Por supuesto, el uso de macros también tiene efectos secundarios. Tome este ejemplo: el efecto de la definición de macro en MIN(*p++, b) es: ((*p++) <= (b) ? (*p++) : (b)) Esta expresión producirá efectos secundarios, el puntero p realizará dos operaciones de autoincremento ++.

3. Use la variable a para dar la siguiente definición: una matriz con 10 punteros, el puntero apunta a una función, la función tiene un parámetro entero y devuelve un número entero.

Respuesta: Los principales errores en esta pregunta son: punteros de función y matrices de punteros.

int (*a[10])(int);

4. ¿Cuál es la función de la palabra clave estática?

Respuesta: En lenguaje C, la palabra clave static tiene tres funciones obvias:

En el cuerpo de la función, a una variable declarada como estática solo se le asignará memoria una vez cuando se llame a la función, y no se reasignará durante todo el tiempo de ejecución; fuera del cuerpo de la función
o en un archivo fuente, una variable declarada como estática solo puede ser accedido por todas las funciones en el archivo fuente, pero no por funciones en otros archivos fuente. Es una variable global local;
dentro de un archivo fuente, una función declarada estática solo puede ser llamada por otras funciones en el archivo fuente. Es decir, la función está restringida al ámbito local del archivo fuente en el que se declara.
5. ¿Cuál es la función de la palabra clave const?

Respuesta: En pocas palabras, const significa constante.

La variable definida por const, su valor no se puede cambiar, y permanece fijo en todo el alcance;
al igual que la definición de macro, puede evitar la aparición de números ambiguos, y también es muy conveniente para ajustar y modificar parámetros;
puede proteger los modificados Algo para evitar modificaciones accidentales y mejorar la solidez del programa. const está garantizado por el compilador que realiza comprobaciones en el momento de la compilación.
const y punteros

¿Qué significan las siguientes afirmaciones:`

1.const int a;
2.int const a;
3.const int *a;
4.int * const a;
5.const int * const a;
6.int const * const a;`

Las funciones de los dos primeros son las mismas, a es un entero constante;

El tercero significa que a es un puntero a un entero constante (es decir, los enteros son inmutables, pero los punteros pueden serlo); el cuarto
significa que a es un puntero constante a un entero (es decir, el entero apuntado por el puntero es modificable, pero el puntero no es modificable); los
dos últimos significan que a es un puntero constante a un entero constante (es decir, el entero apuntado por el puntero no es modificable, y el puntero también es inmutable).
const y funciones

const generalmente se usa en los parámetros de la función.Si el parámetro es un puntero, para evitar que los datos apuntados por el puntero se modifiquen dentro de la función, se puede usar const para restringirlo. Por ejemplo, hay muchos parámetros modificados const en el programa String:

void StringCopy(char* strDestination, const char *strSource);

const también puede significar que la función devuelve una constante, que se coloca en el valor de retorno de la función. Por ejemplo:

const char * GetString(void);

En la declaración y definición de una función miembro de clase, const se coloca después de la tabla de parámetros de la función y antes del cuerpo de la función, lo que indica que el puntero this de la función es una constante y los miembros de datos del objeto no se pueden modificar. Por ejemplo:

void getId() const;

6. ¿Cuál es el significado de la palabra clave volátil?, y se dan tres ejemplos diferentes.

Respuesta: Una variable definida como volátil significa que la variable puede cambiarse inesperadamente, de modo que el compilador no asuma 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 usar una copia de seguridad almacenada en un registro. Aquí hay algunos ejemplos de variables volátiles:

Los registros de hardware mapeados en memoria generalmente se agregan con voliate, porque cada lectura y escritura en él puede tener diferentes significados; las
variables interactivas en la función de interrupción deben modificarse con la palabra clave volatile, para que cada lectura no se almacene automáticamente El valor del tipo (variable global, variable estática) se lee desde su dirección de memoria para garantizar que son los datos que queremos;
la bandera compartida entre tareas en un entorno multitarea debe agregarse a volatile.
¿Puede un parámetro ser constante y volátil?

Sí, por ejemplo, un registro de estado de solo lectura. Es volátil porque se puede cambiar inesperadamente. Es constante porque los programas no deberían intentar modificarlo. El hecho de que el software no se pueda cambiar no significa que mi hardware no pueda cambiar su valor Esta es la aplicación en la microcomputadora de un solo chip.

Artículo de referencia: volátil en C - déjame mantenerlo como está.

¿Puede un puntero ser volátil?

Poder. Aunque esto no es muy común. Un ejemplo es cuando una subrutina de servicio modifica un puntero a un búfer.

¿Qué tiene de malo la siguiente función:`

int square(volatile int *ptr)
{
    
    
        return *ptr * *ptr;
}

El propósito de este código es devolver el cuadrado del valor al que apunta 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 square(volatile int *ptr) 
{
    
    
  	int a,b;
   	a = *ptr;
    b = *ptr;
    return a * b;
}`

Dado que el valor de *ptr puede cambiar inesperadamente, ayb pueden ser diferentes. Como resultado, es posible que este código no devuelva el valor cuadrado que espera. El código correcto es el siguiente:

long square(volatile int *ptr) 
{
    
    
    int a;
    a = *ptr;
    return a * a;
}

7. Dada una variable entera a, escriba dos piezas de código, la primera establece bit3 de a, y la segunda borra bit3 de a.

Respuesta: Esta pregunta borra el bit 3 de a, usando el método "&=~".

#define BIT3 (0x1 << 3)
static int a;
 
void set_bit3(void) 
{
    
    
    a |= BIT3;
}
void clear_bit3(void) 
{
    
    
    a &= ~BIT3;
}

8. Los sistemas integrados a menudo tienen funciones que requieren que el programador acceda a una ubicación de memoria específica. En un determinado proyecto, se requiere establecer el valor de una variable entera cuya dirección absoluta es 0x67a9 a 0xaa66.

Solución: esta pregunta prueba si sabe que es legal convertir un número entero (dirección absoluta) a un puntero para acceder a una dirección absoluta.

int *ptr;
ptr = (int *)0x67a9;
*ptr = 0xaa55;

9. Las interrupciones son una parte importante de los sistemas integrados, lo que ha llevado a muchos desarrolladores de compiladores a proporcionar una extensión, lo que permite que el estándar C admita interrupciones. El hecho representativo es que se creó una nueva palabra clave __interrupt. El siguiente código usa la palabra clave __interrupt para definir una subrutina de servicio de interrupción (ISR), comente este código.

__interrupt double compute_area (double radius) 
{
    
    
    double area = PI * radius * radius;
    printf("/nArea = %f", area);
    return area;
}

Respuesta: Hay tantos errores en esta función que la gente no sabe por dónde empezar:

Los ISR no pueden pasar parámetros, no pueden devolver un valor;
el punto flotante generalmente no vuelve a entrar en muchos procesadores/compiladores. Algunos procesadores/compiladores necesitan empujar registros al frente de la pila, y algunos procesadores/compiladores simplemente no permiten operaciones de punto flotante en el ISR. Además, ISR debe ser corto y eficiente, no es aconsejable realizar operaciones de coma flotante en ISR;
la función printf(char * lpFormatString,…) traerá problemas de reentrada y rendimiento, y no se puede usar en ISR.
Explicación de estos requisitos:

a. ¿Por qué no puede haber un valor de retorno?

La llamada de la función de servicio de interrupción es a nivel de hardware. Cuando se genera una interrupción, el puntero de la PC se ve obligado a saltar a la entrada correspondiente de la función de servicio de interrupción. La entrada de la interrupción es aleatoria y no es llamada por una determinada pieza de código. Si hay un valor de retorno, ¿A quién se devuelve el valor de retorno? Obviamente, este valor de retorno no tiene sentido. Si hay un valor de retorno, debe ser empujado a la pila, así que cuándo y cómo sacar la pila volverse irresoluble.

b. ¿No se pueden pasar parámetros a ISR?

De la misma manera, también es la razón por la que la pila se destruirá de esta manera, porque la función que pasa los parámetros definitivamente requerirá una operación push and pop.Ya que ingresa a la línea aleatoria de la función de servicio de interrupción, es un problema para cualquiera que le pase parámetros.

c. ¿La ISR debe ser lo más breve y concisa posible?

Si una interrupción se genera con frecuencia y su ISR correspondiente consume bastante tiempo, la respuesta a la interrupción se retrasará infinitamente y se perderán muchas solicitudes de interrupción.

La función d.printf(char * lpFormatString,...) traerá problemas de reentrada y rendimiento, y no se puede usar en ISR.

Esto implica un problema de anidamiento de interrupciones. Dado que las funciones de glibc como printf usan un mecanismo de búfer, este búfer se comparte, lo que equivale a una variable global. Cuando llega la interrupción de la primera capa, escribe una parte del contenido, justo en este momento. , llegó una interrupción de mayor prioridad, también llamó a printf y también escribió algo de contenido en el búfer, por lo que el contenido del búfer estaba desordenado.

Las funciones reentrantes se utilizan principalmente en entornos multitarea. Una función reentrante es simplemente una función que puede interrumpirse, es decir, puede interrumpirse en cualquier momento durante la ejecución de esta función y transferirse a la programación del sistema operativo para su ejecución. Una pieza de código , y no habrá ningún error al devolver el control; las funciones no reentrantes usan algunos recursos del sistema, como el área de variables globales, la tabla de vectores de interrupción, etc., por lo que si se interrumpe, pueden ocurrir problemas. Las funciones no se pueden ejecutar en una multitarea ambiente.

La conexión y la diferencia entre la función de servicio de interrupción y la llamada de función:

Conexión: ambos deben proteger el punto de interrupción (es decir, la dirección de la siguiente instrucción), saltar a la subrutina o interrumpir la rutina de servicio, proteger el contexto, subrutina o manejo de interrupciones, restaurar el contexto, restaurar el punto de interrupción (es decir, volver al programa principal). Ambos pueden anidarse, es decir, la subrutina que se está ejecutando se transfiere a otra subrutina o el programa de interrupción que se está procesando se interrumpe por otra nueva solicitud de interrupción, y el anidamiento puede ser de varios niveles.

Diferencia: La diferencia fundamental entre los dos se refleja principalmente en la diferencia en el tiempo de servicio y los objetos de servicio. Primero, el tiempo de llamada del proceso de subrutina es conocido y fijo, es decir, cuando se ejecuta el comando de llamada (CALL) en el programa principal, el programa principal llama a la subrutina y la ubicación del comando de llamada es conocida y fija. El momento en que ocurre el proceso de interrupción es generalmente aleatorio.Cuando la CPU recibe una aplicación de interrupción de la fuente de interrupción al ejecutar un determinado programa principal, ocurre el proceso de interrupción, y el circuito de hardware generalmente genera la aplicación de interrupción, y el tiempo de aplicación es aleatorio (el tiempo de ocurrencia de la interrupción suave es fijo), también se puede decir que el diseñador del programa organiza la llamada a la subrutina de antemano, y el entorno de trabajo del sistema determina aleatoriamente la ejecución del programa de servicio de interrupción;

En segundo lugar, la subrutina sirve completamente al programa principal, y los dos pertenecen a la relación maestro-esclavo.Cuando el programa principal necesita la subrutina, llama a la subrutina y devuelve el resultado de la llamada al programa principal para continuar con la ejecución. El programa de servicio de interrupción y el programa principal generalmente son irrelevantes, y no hay duda de quién sirve a quién, y los dos son paralelos;

En tercer lugar, el proceso de llamada de subrutinas por parte del programa principal es completamente un proceso de procesamiento de software y no requiere circuitos de hardware especiales, mientras que el sistema de procesamiento de interrupciones es una combinación de software y hardware, que requiere circuitos de hardware especiales para completar el proceso de procesamiento de interrupciones;

En cuarto lugar, se pueden realizar varios niveles de anidamiento de subrutinas, y el número máximo de niveles de anidamiento está limitado por el tamaño de la pila abierta por la memoria de la computadora, mientras que el número de niveles de anidamiento de interrupciones está determinado principalmente por el número de prioridades de interrupción y, en general, el número de niveles de prioridad no será muy grande.

10. ¿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");
}

Solución: esta pregunta prueba si comprende los principios de la conversión automática de enteros en lenguaje C. Descubrí que algunos desarrolladores entienden muy poco de estas cosas. De todos modos, la respuesta al problema de los enteros sin signo es que la salida es ">6". El motivo es que todos los operandos se convierten automáticamente en 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.

Hay otro punto de conocimiento importante:

En circunstancias normales, cuando se realizan operaciones en valores de tipo int, la velocidad de operación de la CPU es la más rápida. En x86, la aritmética de 32 bits es dos veces más rápida que la aritmética de 16 bits. El lenguaje C es un lenguaje que se enfoca en la eficiencia, por lo que realizará una promoción de enteros para que el programa se ejecute lo más rápido posible. Por lo tanto, debe recordar las reglas de promoción de enteros para evitar algunos problemas de desbordamiento de enteros.

La promoción de enteros es una regulación en el lenguaje de programación C. Al calcular expresiones (incluidas operaciones de comparación, operaciones aritméticas, operaciones de asignación, etc.), tipos más pequeños que el tipo int (char, signed char, unsigned char, short, unsigned short, etc. .) primero debe promoverse al tipo int y luego realizar la operación de la expresión.

En cuanto al método de promoción, consiste en realizar una extensión de bits de acuerdo con el tipo original (si el tipo original es un carácter sin signo, realice una extensión cero, si el tipo original es un carácter firmado, realice una extensión de bits con signo) a 32 bits. Es decir:

Para caracteres sin signo, realice una extensión de cero, es decir, rellene los bits altos de la izquierda con 0 a 32 bits;
para caracteres con signo, realice una extensión de bits de signo. Si su bit de signo es 0, entonces el bit alto izquierdo se llena con 0 a 32 bits; si su bit de signo es 1, el bit alto izquierdo se llena con 1 a 32 bits.
11. Evalúe el siguiente fragmento de código:

unsigned int compzero = 0xFFFF;

Solución: para un procesador cuyo tipo int no sea de 16 bits, el código anterior es incorrecto. debe escribirse de la siguiente manera:

unsigned int compzero = ~0;

12. Aunque no son tan comunes como las computadoras no integradas, los sistemas integrados aún tienen el proceso de asignación dinámica de memoria desde el montón. Entonces, ¿cuáles son los posibles problemas de la asignación dinámica de memoria en un sistema integrado?

Respuesta: La asignación dinámica inevitablemente causará problemas:

Fugas de memoria: Las fugas de memoria generalmente son causadas por defectos de codificación del programa en sí. No hay operaciones libres y otras similares después de la memoria malloc común. El sistema repetidamente mallocs durante el proceso en ejecución, consumiendo la memoria del sistema, causando el kernel OOM , y un determinado proceso debe solicitar la memoria de la eliminación y la salida.
Fragmentación de la memoria: la fragmentación de la memoria es un problema del sistema, malloc repetido y libre, y el sistema no puede reciclar la memoria después de liberarla inmediatamente. Esto se debe a que el algoritmo de asignación encargado de asignar memoria dinámicamente inutiliza estas memorias libres.Este problema ocurre porque estas memorias libres aparecen en diferentes lugares de manera pequeña y discontinua.
¿Cuál es el resultado del fragmento de código a continuación y por qué?

char *ptr;
if ((ptr = (char *)malloc(0)) == NULL) 
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

El parámetro de la función malloc() puede ser 0.

13. Typedef se usa con frecuencia en lenguaje C para declarar un sinónimo de un tipo de datos existente. También puedes hacer algo similar con un preprocesador. Por ejemplo, considere el siguiente ejemplo:

#define dPS struct s *
typedef struct s * tPS;

La intención de los dos casos anteriores es definir dPS y tPS como un puntero a la estructura s. ¿Qué método es mejor?

Respuesta: typedef es mejor. Considere el siguiente ejemplo:

dPS p1,p2;
tPS p3,p4;

Si es la extensión de la primera define:

struct s * p1, p2;

p1 es un puntero y p2 es una estructura. Obviamente, no es la respuesta que queremos.

Supongo que te gusta

Origin blog.csdn.net/qizhi321123/article/details/131474717
Recomendado
Clasificación