拿下嵌入式软件工程师面试题(day1)

前言

(1)如果你在读大学,不管你本科毕业是读研还是就业,你都可以早点准备嵌入式面试题,本系列教程的面试题均基于C语言。

(2)像嵌入式学得好,且学历不错的本科生和研究生,都有机会进入华为、小米、大疆、智元等公司,薪资待遇也是不错的。

(3)早点接触面试题,心里有底,大家一起学起来吧。

(4)希望各位大佬多多指点经验,大家一起交流,共同进步!本人邮箱:[email protected]

第一题(考查对volatile关键字的认识——华为)

#include
static jmp_buf  buf;

main()    
{
  volatile  int b;
  b =3;

  if(setjmp(buf)!=0)  
  {
    printf("%d ", b);  
    exit(0);
  }
  b=5;
  longjmp(buf , 1);
}

请问,这段程序的输出是

(a) 3
(b) 5
© 0
(d) 以上均不是

第一题答案(b)5

详细解析

(1)volatile是C语言中的一个关键字,用于声明一个变量为"易变"(volatile)。当一个变量被声明为volatile时,编译器将对该变量的访问和操作做出特殊处理,以确保对该变量的读写操作不会被优化、重排序或省略,从而提供对变量的准确和可预测的访问。
(2)这是因为这个变量可能在一个寄存器,直接与外部设备相连,你写入之后,该寄存器也有可能被外部设备的写操作所改变;或者,该变量被一个中断程序,或被另一个进程改变了。
(3)volatile 不会被编译器优化影响,在longjump 后,它的值是后面假定的变量值,b最后的值是5,所以5被打印出来.

(4)setjmp : 设置非局部跳转 /* setjmp.h*/

Lonjjmp: 执行一个非局部跳转 /* setjmp.h*/

反思

(1)请先允许见识狭窄的我先说一句,我用了两年C语言都没见过这个头文件,以至于我开始都以为这题考的不是C语言哈哈,直到我把题目代码在VS里编译后,才相信,还是学的东西太少了,一下子暴露了自己的知识上限(doge),其次题目代码少包含了一个头文件#include 。这个程序看似很复杂,实际超级简单。

拿下嵌入式软件工程师面试题(day1)_第1张图片

我提出问题,头文件有什么作用?函数setjmp()、longjmp()是什么作用?该程序的逻辑功能是什么?

(2)头文件有什么作用?

头文件是C标准库中的一个头文件,它提供了用于非局部跳转的函数setjmp()和longjmp()的声明和定义。

setjmp()和longjmp()函数允许在程序的不同位置进行跳转,而不是按照正常的顺序执行代码。这种非局部跳转的能力对于处理异常、错误处理、状态回滚等情况非常有用。

具体来说,头文件中提供的函数有以下作用:

setjmp()函数:用于设置一个跳转点,并将当前程序状态保存到jmp_buf类型的变量中。它在设置跳转点时返回0,但当通过longjmp()函数跳转回来时,setjmp()函数会返回非零值,用于判断是否发生了跳转。

longjmp()函数:用于进行非局部跳转,返回到之前通过setjmp()函数设置的跳转点。它的第一个参数是之前保存的jmp_buf变量,第二个参数是用于标识跳转原因的值。

通过结合使用setjmp()和longjmp()函数,可以实现一种跳转回之前的程序状态的机制,从而实现异常处理、错误处理、状态回滚等操作。

需要注意的是,由于非局部跳转可能会导致代码流程的混乱和难以维护,应该谨慎使用setjmp()和longjmp()函数,并确保在使用时遵循相关的规范和最佳实践。

(3)函数setjmp()、longjmp()是什么作用?

函数setjmp()longjmp()是C标准库中提供的用于非局部跳转的函数。

  1. setjmp()函数:setjmp()函数用于设置一个跳转点,并将当前程序状态保存到一个jmp_buf类型的变量中。它的原型如下:

    int setjmp(jmp_buf env);
    ```
    
    ``setjmp()`函数在设置跳转点时返回0,并将程序状态保存在`env`变量中。这样,通过调用`longjmp()`函数可以跳回到`setjmp()`函数所在的位置,并恢复保存的程序状态。
    
  2. longjmp()函数:longjmp()函数用于进行非局部跳转,返回到之前通过setjmp()函数设置的跳转点。它的原型如下:

    void longjmp(jmp_buf env, int value);
    ```
    
    ``longjmp()`函数接受一个`jmp_buf`类型的变量`env`作为参数,以及一个用于标识跳转原因的整数值`value`。调用`longjmp()`函数会使程序跳转回`setjmp()`所在的位置,并且`setjmp()`函数的返回值被设置为`value`。
    

这种非局部跳转的机制可以用于异常处理、错误处理以及特定的控制流程需求。例如,当发生某个异常情况时,可以使用setjmp()函数设置一个跳转点,然后在需要的地方调用longjmp()函数跳转回来,从而实现异常处理。这种机制在一些C语言框架和库中被广泛使用,例如错误处理和资源回收的实现。

需要注意的是,非局部跳转是一种强大但也容易导致代码复杂性和难以维护性的特性,因此在使用时应慎重考虑,并确保遵循相关的规范和最佳实践。

(4)程序逻辑分析

这段代码展示了使用C标准库中的setjmp()和longjmp()函数来实现非局部跳转的示例。

首先,jmp_buf是一个用于存储跳转位置信息的数据类型。在代码中,我们声明了一个名为buf的jmp_buf类型变量。

接下来,main()函数中声明了一个volatile修饰符的整型变量b,并将其初始化为3。

然后,通过使用setjmp(buf)函数,在此处设置了一个跳转点。setjmp()函数会将当前程序状态保存到buf中,并返回0作为返回值。这是第一次调用setjmp()函数,因此返回值为0。

之后,代码中的条件判断语句if(setjmp(buf)!=0)检测到setjmp()函数的返回值不为0,表示发生了跳转。在这种情况下,程序会执行跳转后的代码块。

在跳转后的代码块中,通过调用printf()函数输出变量b的值,并调用exit(0)函数来终止程序运行。

然而,在跳转之前,我们将变量b的值修改为5。

最后,通过调用longjmp(buf, 1)函数,程序将跳转回之前设置的跳转点,并且setjmp()函数的返回值被设置为1。这会导致条件判断语句为真,从而执行跳转后的代码块。

综上所述,这段代码的输出结果将是"5"。

Tips

(1)需要注意的是,volatile关键字只会影响对变量的访问和修改行为,而不会影响变量本身的类型或存储方式。volatile并不提供线程安全性或互斥访问的保证,因此在多线程环境中使用volatile变量时需要额外的同步机制。

(2)以下是一个示例,展示了volatile变量的使用:

#include 

int main() {
    volatile int counter = 0;

    while (counter < 10) {
        printf("Counter: %d\n", counter);
        counter++;
    }

    return 0;
}

在这个程序中,我们声明了一个 volatile 整数变量 counter。volatile 关键字的作用是告诉编译器,该变量的值可能会被意外地改变,因此在编译器优化过程中不要对该变量进行优化。

在这个示例中,counter 变量在每次循环迭代时增加 1,然后将其值打印出来。由于 counter 是 volatile 类型的,编译器会确保每次循环都会读取变量的最新值,而不是依赖于缓存或寄存器中的旧值。这样可以确保输出的结果是连续递增的数字,而不是出现不连续或重复的数字。

如果我们将 counter 声明为非 volatile 类型,编译器可能会进行优化,例如将 counter 的值保存在寄存器中,以避免每次迭代都从内存中读取。这样可能导致打印出的结果不符合预期,例如可能会输出重复的数字或者不按顺序递增。

因此,使用 volatile 关键字可以确保程序正确地处理那些可能被意外改变的变量,如硬件寄存器、多线程环境下的共享变量等。

Volatile在嵌入式中的应用:

可以去看看别人做项目的程序中什么时候用volatile,加以理解;光看白话文,而不看代码,很难理解透彻。

当我们使用 volatile 关键字修饰变量时,我们告诉编译器该变量的值可能在程序的其他地方被意外地修改,因此编译器不应该对该变量进行某些优化,以确保程序的行为符合我们的预期。

volatile 关键字的作用主要有以下几个方面:

(1)防止编译器优化:编译器在进行代码优化时,为了提高程序的性能和效率,可能会对变量进行优化,例如将变量存储在寄存器中,而不是每次都从内存中读取。然而,这种优化可能导致问题,特别是当变量的值可能会在程序的其他地方被修改时。通过使用 volatile 关键字,我们告诉编译器不要对这些变量进行优化,每次都从内存中读取最新的值。

(2)处理硬件寄存器:在嵌入式系统或与硬件交互的程序中,我们经常需要使用 volatile 关键字来处理硬件寄存器。硬件寄存器是与外部设备直接通信的接口,其值可以在任何时候被修改。通过使用 volatile 关键字修饰硬件寄存器,我们确保编译器不会对寄存器的读取和写入进行优化,并且始终与硬件保持同步。

(3)处理多线程共享变量:在多线程编程中,多个线程可能同时访问和修改共享变量。这种情况下,使用 volatile 关键字可以确保每个线程都能正确地读取和写入共享变量的最新值,而不会依赖于线程本地的缓存。volatile 关键字提供了一种简单的同步机制,用于确保可见性和一致性。

(4)需要注意的是,volatile 关键字并不能解决所有的并发问题,它只能保证可见性和一致性,但不能提供原子操作或线程安全。对于更复杂的多线程场景,需要使用其他同步机制,如互斥锁(mutex)或原子操作来保证线程安全性。

综上所述,使用 volatile 关键字可以确保变量在特定的场景下的可见性和一致性,避免编译器的优化干扰。它在处理硬件寄存器、多线程共享变量或其他需要确保变量值正确性的情况下非常有用。

第二题(考查类型转换)

main()
{
   struct node 
   {
     int a;
     int b;
     int c;     
   };
   struct node  s= { 3, 5,6 };
   struct node *pt = &s;
   printf("%d" ,  *(int*)pt);

}

请问这段程序的输出是:

(a) 3
(b) 5
© 6
(d) 7

第二题答案(a) 3

详细解析

(1)这段代码定义了一个结构体node,其中包含三个整型成员变量a、b和c。然后创建了一个名为s的node类型的结构体变量,并初始化其成员变量为3、5和6。

(2)接下来,声明了一个指向node类型结构体的指针pt,并将其指向结构体变量s的地址。

(3)最后,通过使用类型转换

(int*)

将指针pt转换为指向整型的指针,然后使用间接访问运算符

*

对其进行解引用,即

*(int*)pt

这样可以将指针pt解释为指向整型变量的指针,并取得该整型变量的值

(4)最终,通过调用printf()函数,将解引用后的整型值打印出来。

(5)根据代码中的初始化,输出结果将是3,因为pt指向s结构体的起始地址,而在s结构体中,第一个成员变量a的值为3。

第三题(考查递归调用)

int  foo ( int x , int  n) 
{
  int val;
  val =1;
  
  if (n>0) 
  {
    if (n%2 == 1)  val = val *x;
    
    val = val * foo(x*x , n/2);
  }
  return val;
}

这段代码对x和n完成什么样的功能(操作)?

(a) x^n (x的n次幂)
(b) x*n(x与n的乘积)
© n^x(n的x次幂)
(d) 以上均不是

第三题答案(a)

详细解析

这段代码定义了一个名为foo的递归函数,用于计算一个整数x的n次幂。

函数内部有一个局部变量val,并将其初始化为1。

接下来,通过嵌套的条件语句进行递归计算。首先,判断n是否大于0。如果是,进入第一个内部条件语句。

在第一个内部条件语句中,判断n是否为奇数(即n % 2 == 1)。如果是奇数,将val乘以x,即val = val * x。

然后,调用foo()函数本身,传入x的平方和n除以2的结果。这样就实现了递归调用,将问题规模减半。

递归调用在函数内部进行,通过每次将n减半的方式,最终达到递归的结束条件(即n等于0,其实n/2小于1就结束了,因为是形参是int类型)。当n等于0时,递归函数返回1。

最后,在函数的最外层,返回变量val的值。

通过这种递归的方式,函数foo()可以计算x的n次幂,并将结果作为返回值返回。

需要注意的是,对于较大的n值,由于递归的深度增加,可能导致栈溢出或性能下降。在实际使用中,需要注意递归调用的层数和性能影响。

你可能感兴趣的:(嵌入式软件工程师面试题,学习,嵌入式软件工程师)