23嵌入式工程师面试/笔试——概念整理自查

        23年的春招已经启动了,今年的嵌入式就业环境还是非常不错的,求职过程中看了很多的资料和视频,也陆陆续续面试了很多家公司,一直在整理和总结。对于面试或者笔试而言,通过这篇文章,我们可以大概知道嵌入式软件工程师面试或者笔试可能会问到什么,用到什么。

        当然,这一篇都是比较基础的概念,主要用于了解常见的问题,其实大厂面试更关注个人的项目经历以及学校背景,一般来说不会太考这些(部分也会用到)。但是对于中小厂来说,相关或者类似的问题在面试或者笔试中都或多或少出现过;这些概念还是有一定帮助的,可以多多思考自己哪些地方还不太会的,并且查漏补缺。

        后续还会更新Linux篇、代码纠错篇、项目整理篇、网络通信协议篇,预计未来两周内更新(可以关注一下)

        注意,答案只是引子,对于不同的概念,还需要根据公司的项目和要求进行拓展和深入思考,如果有什么需要补充,或者表达有误的地方,还请各位大佬指正。

目录

基础概念

头文件

数组、引用和指针

局部变量和全局变量

关键字

volatile

const

sizeof

static

extern

堆&栈

程序的内存分配

堆栈溢出的原因

堆和栈的区别

结构体

赋值

计算结构体大小

 结构体成员数组大小

和联合体的区别

排序算法:

冒泡排序

选择排序

快速排序:

小结:


基础概念

头文件

预编译,编译过程最先做的工作是啥?  

  • 编译#字开头的指令,如拷贝#include 包含的头文件代码
  • #define 宏定义的替换,条件编译 ifndef  

何时需要预编译 ?

  • 总是经常使用但是不经常改动的大型代码。
  • 程序由多个模块组成,所有模块都使用一组标准的包含文件和相同的编译选 项,将所有包含文件预编译为一个 “预编译头”。

 # 与 ##的区别以及作用?

  • # :把宏参数变成一个字符串;
  • ## :把两个宏参数连接到一起(只能两个)<个人认为有点类似于字符串拼接函数:strcat>

数组、引用和指针

引用与指针的区别

  • 引用必须初始化,指针不必初始化   
  • 引用初始化后不能改变,但是指针可以改变所指的对象 ③不存在空值的引用,但是存在空值的指针

拓展:如何用宏返回数组的大小

#define arraylenth(a)(sizeof(a)/sizeof(a[0]))

局部变量和全局变量

特性和关系

  • 可以重名,局部变量会屏蔽,全局变量,要使用全局变量,需使用::  
  • 可以用 extern 关键字 或者引用头文件 的方式,引用已经被定义了的而全局变量
  • 局部变量存在于栈区 全局变量存储在静态区 动态申请数据存放在堆区

关键字

volatile

作用:防止变量被编译器优化

原理:

  • 编译优化原因:内存访问速度远远比不上 cpu 处理的速度,为了提高性能, 从硬件上引入高速缓存 cache,加速对内存的访问。
  • 编译优化过程:当使用 volatile 声明函数变量的时候,系统总是重新从它所在的内存读取数据。遇到这个关键字声明的变量,编译器对访问该变量的代码不再进行优化,从而提供对特殊地址的稳定访问; 如果不使用 valatile,编译器将对所声明的语句进优化,可能出现错误
  • 编译优化的方法有:将内存变量缓存到寄存器;调整指令顺序充分利用 CPU 指 令流水线。

const

作用:对变量加以限定使其不能被修改

const 和指针一起使用  :

const int *p1;  

int const *p2;  

int *const p3;

在三种情况中,第三种指针是只读的,p3作为地址 本身的值不能被修改; 第一二种情况,指针所指向的数据是只读的,p1,p2 的值可以修改,但指向的数据不能被修改。

绝对保护 :int *const *p const

const 和函数形参一起使用   使用 const 单独定义变量可以用#define 命令替换,const 通常放在函数形参中。 如果形参是一个指针,为了防止在函数内部修改指针指向的数据就可以用 const 来限制 也许能够产生更加紧凑的代码,比如 把0x7788赋值到0x8877 三行 → 一行

sizeof

作用:sizeof 在编译阶段处理,作用为取得一个对象(数据类型或数据对象)的 长度(即占用内存的大小,以 1 字节为单位) 

  • 指针可以看做变量的一种,32 位操作系统 sizeof 指针都为 4,例子:   int *p;   sizeof(p) =4;   sizeof(*p) = sizeof(int )=4;  
  • 对于静态数组,sizeof 可以直接计算数组大小,例:   int a[10];   char b[ ]= “hello”;   Sizeof (a) = 4 * 10 =40;   Sizeof (b) = 6; (求字符串长度时要加上字符串结束符/0)  
  • 数组作为函数形参时候,数组名当做指针 使用,例:   Void fun (char p[ ])   {   Sizeof (p) ; //结果为 4   }

sizeof 与 stelrn 的区别:  

  • *sizeof 是操作符, strlen 为函数;  
  • *sizeof 可以使用类型作为参数,如 int char;
  • strlen 只能使用 char*做参数且以\0 为结尾  ;
  • *sizeof 为数组时候,不退化, 传递给 strlen 时数组会被退化成指针;

static

作用:static 最主要功能是隐藏,其次因为 static 变量存放在静态存储区,具备持久性和默认值为 0,比如说全局变量需要被定义在多个.c文件包含的头文件里,就可以在不同.c文件里用static的形式来声明同名全局变量

静态成员和非静态成员的概念:

  • 静态成员:静态类中的成员加入static修饰符,即是静态成员.可以直接使用类名+静态成员名访问此静态成员,因为静态成员存在于内存,非静态成员需要实例化才会分配内存,所以静态成员不能访问非静态的成员..因为静态成员存在于内存,所以非静态成员可以直接访问类中静态的成员.静态成员在每个类中只有一个拷贝,是解决同一个类的不同对象之间数据和函数共享问题的。
  • 非成静态员:所有没有加Static的成员都是非静态成员,当类被实例化之后,可以通过实例化的类名进行访问..非静态成员的生存期决定于该类的生存期..而静态成员则不存在生存期的概念,因为静态成员始终驻留在内容中,一个类中也可以包含静态成员和非静态成员,类中也包括静态构造函数和非静态构造函数.

静态成员和非静态成员访问方式不一样:

  • 静态成员(通过类名.静态成员名访问).非静态成员(通过对象名.非静态成员名访问)
  • 静态成员属于类.该类的所有对象共同拥有这一个成员.非静态成员属于对象,每个对象都有一份.静态成员不论有类有多少个对象.只在内存中分配一块空间.

extern

作用:

  • 函数内的局部变量,函数外定义的变量为全局变量,为静态存储方式,生存 周期为整个程序,有效范围为定义变量的位置开始到本源文件结束。   如果在定义前想要引用该全局变量,则应该加上 extern 作为 “外部变量声明”。   多个源文件的工程想要引用一个源文件的外部变量也只许引用变量的文件中加入 extern 关键字加以声明,但是可以在引用的模块内修改其变量的值(慎用)   
  • extern “C”: C++代码调用 C 语言代码。在 C++的头文件中使用。

注意:public用来修饰类的成员变量,说明外部方法可以直接访问类的变量。extern一般用来修饰全局变量,说明当前使用的变量已经在其它地方声明了

堆&栈

程序的内存分配

  • ①栈区 (stack):由编译器进行管理,自动分配和释放,存放的是函数调用过 程中的各种参数,局部变量,返回值以及函数返回地址。  
  • ②堆区(heap) :用于程序动态申请分配和释放空间,malloc 和 free,程序员 申请的空间在使用结束后应该释放,则程序自动收回。
  • ③全局(静态)存储区: 分为 DATA(已经初始化),BSS(未初始化)段,DATA 段存放的是全局变量和 静态变量; BSS(未初始化)存放未初始化的全局变量和静态变量。 程序运行 结束后自动释放,其中 BSS(全部未初始化区)会被系统自动清零。  
  • ④文字常量区 :存放常量字符串,程序结束后由系统释放。  
  • ⑤程序代码段:存放程序的二进制代码。

堆栈溢出的原因

  • ①函数调用层次太深,函数递归调用时,系统要在栈中不断保存函数调用时的 线程和产生的变量,递归调用太深,会造成栈溢出,这是递归无法返还。  
  • ②(most)动态申请空间使用后没有释放。由于 C 语言中没有垃圾资源自动回收机制, 因此需要程序员主动释放已经不再使用的动态地址空间。  
  • ③数组访问越界,C 语言没有提供数组下标越界检查,如果在程序中出现数组 下标访问超出数组范围,运行过程中可能会存在内存访问错误。  
  • ④指针非法访问,指针保存了一个非法地址,通过这样的指针访问所指向的地 址时会产生内存访问错误。

堆和栈的区别

  • ①申请方式:   Strack(栈): 由编译器自带分配释放,存放函数的参数值,局部变量等。   Heap(堆):程序员自己申请,并指名大小-->malloc 函数。  
  • ②申请后的系统响应   Strack(栈):只要栈剩余空间>所申请空间,都会提供。   Heap(堆):操作系统有记录空闲内存的链表:收到申请->遍历链表->寻找->申请空间的 堆结点  
  • ③申请内存的大小限制   Strack(栈):向低地址扩展的数据结果,连续内存区域,栈 获得的空间较小。   Heap(堆):向高地址扩展的,不连续内存区域;链表遍历方向为(低地址->高地址)。   栈获得空间灵活,空间也大。  
  • ④申请效率   Strack(栈):系统自由分配,速度快。   Heap(堆):速度慢,容易产生内存碎片。  
  • ⑤存储内容   Strack(栈):第一进栈 :主函数中的下一条指令的地址 -->函数的各个参数,参数由右往左 进栈。-->函数的局部变量(静态变量不入栈)。调用结束后,顺序相反,局部变量先出栈。 Heap(堆): 程序员自己安排  
  • ⑥分配方式   Strack(栈):栈 有两种分配方式,静态分配和动态分配。静态分配是编译器完成的,比如 局部变量的分配,动态分配由 alloca 函数进行分配,但栈的动态分配和堆是不同的,栈的 动态内存由编译器进行释放,无需手工实现。   Heap(堆):堆都是动态分配的,没有静态分配的堆。

结构体

赋值

①初始化:如: 
 
Struct st{  
Char a;  
Int b;  
}x={ ‘A’, 1 }; 
 
②定义变量后按字段赋值,如: 
 
Struct st{  
Char a;  
Int b;  
};  
Struct st x;  
X.a = ‘A’;  
X.b = 1; 
 
③结构体变量的赋值,如: 
 
Struct st {  
Char a;  
Int b;  
} ; Struct st x,y; 
X.a= ‘A’; X.b=1; 
 
Y=x;

计算结构体大小

  • ①结构体偏移量的概念:结构体中的偏移量指的是一个成员的实际地址和结构体首地址之间的距离。
  • ②结构体大小的计算方法:结构体会涉及到字节对齐,(目的是为了让计算机快速读取,用空间交换速度),即最后一个成员的大小+最后一个成员的偏移量+末尾的填充节数。
  • ③结构体内偏移规则:第一步:每个成员的偏移量都必须是当前成员所占内存大小的整数倍,如果不是整数倍,编译器会在前填充字节。第二步:当所有成员大小计算完毕后,编译器会对当前结构体大小是否是最宽的结构体成员的大小的整数倍,如果不是整数倍,编译器会填充字节(如下代码段)
Struct s1{ 成员的大小 成员的偏移量

int a; 4 0

char b; 1 4

int c; 4 5+3(填充字节)

long d; 8 12+4(填充字节)

char e; 1 24

}

Sizeof(s1)=24+1+7(填充字节)

 结构体成员数组大小

① 
Struct st {  
Int a;  
Int b;  
Char c [ 0 ];  
}st_t;  
sizeof(st_t) = 8  
② 
 Struct book {  
Char name[5];  
Float price;  
}book[2];  
Sizeof( book ) = 12;  
Sizeof (book[2]) = 24;

第一步:结构体成员占字节数族大的元素为 sizeof ( float ) = 4;则用 4 来分配其他
成员; 
 
第二步:数组超过四个字节,换行继续,剩下是哪个字节不够 float 型数据使用,
则换行。

和联合体的区别

  • 结构体和联合体:都是由不同的数据类型成员组成,但是在同一时刻,联合体只存放了一个被选中的成员(所有成员公用一块地址);而结构体成员都存在(不同成员存放地址不同)。
  • 联合体不同成员赋值,会对其他成员重写,原来成员的值会不存在。结构体的不同成员赋值是互不影响的。
    union abc
    {
          int i;
          char m;
    };
    
    *在联合体 abc 中,整形量 i 和字符 m 共用一块内存位置。  
    *当一个联合体被说明时,编译程序自动产生一个变量,其长度为联合体中最大的变量长度

排序算法:

冒泡排序

原理:从左到右相邻元素进行比较,每比较一轮找到序列中最大或者最小的一个,这个数会从右边冒出来。

  • 第一轮比较:所有数最大的会浮现到最右边
  • 第二轮比较:所有数第二大的数会浮现到倒数第二个位置有n 个数据需要比较(n-1)轮,除第一轮外都不用比较全部
int main()  
{ 
 
int a[]={22,62,1,-22,95,47,85,63,-45,152,19,28};  
int n; //数组个数  
int i;  
int j; //需要进行比较的轮数  
int buf; //用于冒泡交换  
n=sizeof(a)/sizeof(a[0]);  
for(i=0;i<n-1;++i)//进行 n-1 轮比较  
{  
for(j=0;j<n-i-1;++j)//每轮进行 n-i-1 次  
  {  
     if(a[j]>a[j+1])
     {
     buf=a[j];  
     a[j]=a[j+1];  
     a[j+1]=buf;  
     }  
  }  
}  
for(i=0;i<n;++i)  
{  
printf("%d\x20",a[i]);  
}  
printf("\n");  
system("pause");  
return 0;  
}

选择排序

原理: 给定一个数组,设定一个临时变量用来存储最小值,将第一位数与后面的数字进行一一比较,有比第一个数小则交换位置,然后与其他数进行比较来选择是否交换,以此来找到第一位然后是第二位。

int main()  
{  
    int i=0;  
    int j=0;  
    int temp=0;  
    int a[10]={10,62,-25,147,98,14,2,63,85,34};  
    for (i=0;i<10;i++)  
    {  
        for(j=i+1;j<10;j++)  
        {  
            if(a[i]>a[j])  
            {  
            temp=a[i];  
            a[i]=a[j];  
            a[j]=temp;  
            }  
        }  
    }  
    for(i=0;i<10;i++)  
    {  
        printf("%d",a[i]);
        printf(" ");  
    }  
    system("pause");  
    return 0;  
}

快速排序(虽然C++可以直接用,但是要了解原理):

原理:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。

#include<stdio.h>
//快速排序函数,形参列表为数组,左指针位置,右指针位置,int *arr等价于int arr[]
void QkSort(int *arr, int left, int right){
    if (left > right)  //左指针位置必须大于右指针位置
    {
        return;
    }
    //变量tmp为基准数,在此规定基准数为序列的第一个数,即左指针指向的数
    int tmp = arr[left];
    int i = left; //左指针
    int j = right;   //右指针
    //外循环,直到左指针和右指针相等时退出,表示根据当前基准数以完成当前序列排序
    while (i != j)
    {   //内循环1,寻找到比基准数小的数时退出循环,此循环控制右指针    
        while (arr[j] >= tmp && j > i)
        {
            j--;
        }
        //内循环2,寻找到比基准数大的数时退出循环,此循环控制左指针
        while (arr[i] <= tmp && j > i)
        {
            i++;
        }
        //经过以上两个内循环后,此时的左指针和右指针分别指向了
        //比基准数小和比基准数大的数
        //接下来要将这两个指针的数据进行交换
        if (j > i)//交换前判断右指针是否大于左指针
        {
            int t = arr[i];
            arr[i] = arr[j];
            arr[j] = t;
        }
    }//外循环尾

    //执行完循环后,就找到了基准数的排序位置,将基准数tmp与i位置进行交换
    arr[left] = arr[i];
    arr[i] = tmp;
    //*********************************************
    //下面的程序为递归,可能存在多层递归调用
    //*********************************************
    //此时的数组分为了两部分,基准数左边都是小于基准数的,右边都是大于基准数的,
    //现在进行递归,对基准数左边的数进行排序,此时递归可能会有多层
    QkSort(arr, left, i - 1);
    //进行到这步时,基准数左边已经全部有序,而右边还未进行排序,
    //现在进行递归,对基准数右边的数据进行排序,此时递归可能有多层
    QkSort(arr, i + 1, right);
}

int main()
{
    int arr[] = { 0, 4, 3, 5, 65, 2, 64, 68, 34, 94, 53, 74, 13 };
    int len = sizeof(arr)/sizeof(int);
    printf("待排序数值:");
    for (int i = 0; i <=len-1; i++)
    {
        printf("%d ",arr[i]);
    }
    printf("\n");
    printf("排序后的数值:");
    QkSort(arr,0,len-1);//调用快速排序函数
    for (int i = 0; i <=len-1; i++)
    {
        printf("%d ", arr[i]);
    }
}

小结:

       总的来说,嵌入式软件真的需要用到非常多的知识,从以C为首的编程语言概念,到PCB画板的操作,再到Linux操作系统,再到网络通信协议的应用,甚至更近一步的嵌入式AI...嵌入式工程师真的是一个非常长期的学习之路,不过难也伴随着更高的不可替代性,所以趁着现在的东风,一起冲一冲吧o(* ̄▽ ̄*)ブ

        下一章还会更新嵌入式笔试/面试的Linux篇、代码纠错篇、项目整理篇、网络通信协议篇,预计未来两周内更新(感兴趣的话可以关注和收藏一下~),期待和你一起进步呐!

猜你喜欢

转载自blog.csdn.net/TenYao_/article/details/129301773
今日推荐